DirectX11 With Windows SDK--02 頂點/像素着色器的建立、頂點緩衝區

前言

因爲在Direct3D 11中取消了固定管線,要想繪製圖形必需要了解可編程渲染管線的流程,一個能繪製出圖形的渲染管線最少須要有這兩個可編程着色器:頂點着色器像素着色器html

本章假定讀者已經瞭解了渲染管線的工做原理,將直接來到編程實戰。git

接下來的目標以下:github

  1. 編寫第一個頂點着色器和像素着色器
  2. 經過Visual Studio自帶的HLSL編譯器生成頂點着色器和像素着色器
  3. (可選)經過D3DComplier在運行期編譯/生成着色器二進制文件
  4. 將着色器綁定到渲染管線上
  5. 瞭解頂點緩衝區,並將它綁定到輸入裝配階段
  6. 渲染出第一個三角形

這裏將直接從一個已經編寫好的HLSL代碼入手。編程

在此以前你還須要知道如何編譯着色器:數組

章節
HLSL編譯着色器的三種方法

DirectX11 With Windows SDK完整目錄數據結構

Github項目源碼app

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。ide

第一份HLSL代碼

如今咱們在項目中建立HLSL文件夾,將全部的着色器代碼放到這裏。函數

在裏面建立一個Triangle.hlsli的文件,內容以下:佈局

// Triangle.hlsli

struct VertexIn
{
    float3 pos : POSITION;
    float4 color : COLOR;
};

struct VertexOut
{
    float4 posH : SV_POSITION;
    float4 color : COLOR;
};

接下來建立Triangle_VS.hlsl文件,用於存放頂點着色器代碼:

#include "Triangle.hlsli"

// 頂點着色器
VertexOut VS(VertexIn vIn)
{
    VertexOut vOut;
    vOut.posH = float4(vIn.pos, 1.0f);
    vOut.color = vIn.color; // 這裏alpha通道的值默認爲1.0
    return vOut;
}

最後建立Triangle_PS.hlsl文件,用於存放像素着色器代碼:

#include "Triangle.hlsli"

// 像素着色器
float4 PS(VertexOut pIn) : SV_Target
{
    return pIn.color;   
}

HLSL代碼的語法和C/C++的語法很是類似,也許後面會開坑描述一下HLSL語言,不過如今先把注意力放在這份代碼中比較特別的地方。

float3float4都是內置的變量類型,能夠看做是C++的struct類型,支持多種構造方式和成員訪問。除此以外,還有floatfloat2兩種類型。對於float4,它的四個成員分別爲x,y,zw

而後具體講述一下變量名後面的語義:

語義名 具體含義
POSITION 描述該變量是一個座標點
SV_POSITION 說明該頂點的位置在從頂點着色器輸出後,後續的着色器都不能改變它的值,做爲光柵化時最終肯定的像素位置
COLOR 描述該變量是一個顏色
SV_Target 說明輸出的顏色值將會直接保存到渲染目標視圖的後備緩衝區對應位置

輸入佈局(Input Layout)

ID3D11Device::CreateInputLayout方法--建立輸入佈局

在HLSL中,用於輸入的結構體爲:

struct VertexIn
{
    float3 pos : POSITION;
    float4 color : COLOR;
};

該項目與之對應的C++結構體爲:

struct VertexPosColor
{
        DirectX::XMFLOAT3 pos;
        DirectX::XMFLOAT4 color;
        static const D3D11_INPUT_ELEMENT_DESC inputLayout[2];
};

注意:DX SDK中的xnamath.h在Windows SDK中已經被拋棄,取而代之的則是要包含頭文件directxmath.h,XNA相關的數學庫基本上都移植到這裏了,除此以外,他們都已經被放入到名稱空間DirectX中。

爲了可以創建C++結構體與HLSL結構體的對應關係,須要使用ID3D11InputLayout輸入佈局來描述每個成員的用途、語義、大小等信息。

還要留意的是,其中inputLayout並非結構體VertexPosColor的內部成員,而是靜態成員,不佔用該結構體的空間。咱們使用D3D11_INPUT_ELEMENT_DESC結構體來描述待傳入結構體中每一個成員的具體信息,定義以下:

typedef struct D3D11_INPUT_ELEMENT_DESC
{
    LPCSTR SemanticName;        // 語義名
    UINT SemanticIndex;         // 語義索引
    DXGI_FORMAT Format;         // 數據格式
    UINT InputSlot;             // 輸入槽索引(0-15)
    UINT AlignedByteOffset;     // 初始位置(字節偏移量)
    D3D11_INPUT_CLASSIFICATION InputSlotClass; // 輸入類型
    UINT InstanceDataStepRate;  // 忽略
}   D3D11_INPUT_ELEMENT_DESC;

inputLayout的初始化信息以下,描述了C++對應到HLSL的兩個成員的信息:

const D3D11_INPUT_ELEMENT_DESC VertexPosColor::inputLayout[2] = {
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
    { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0}
};

其中,語義名要與HLSL結構體中的語義名相同,如有多個相同的語義名,則語義索引就是另一種區分。相同的語義按從上到下因此分別爲0,1,2...

而後,DXGI_FORMAT在這裏一般描述數據的存儲方式、大小。用DXGI_FORMAT_R32G32B32_FLOAT僅僅是解釋爲3個float類型的值;而用DXGI_FORMAT_R32G32B32A32_FLOAT在這裏是說明顏色按RGBA存儲,而且爲4個float類型的值

輸入槽這裏只使用1個,即索引爲0的輸入槽。

初始位置則指的是該成員的位置與起始成員所在的字節偏移量。

輸入類型有兩種:D3D11_INPUT_PER_VERTEX_DATA爲按每一個頂點數據輸入,D3D11_INPUT_PER_INSTANCE_DATA則是按每一個實例數據輸入。

接下來就可使用ID3D11Device::CreateInputLayout方法建立一個輸入佈局:

HRESULT ID3D11Device::CreateInputLayout( 
    const D3D11_INPUT_ELEMENT_DESC *pInputElementDescs, // [In]輸入佈局描述
    UINT NumElements,                                   // [In]上述數組元素個數
    const void *pShaderBytecodeWithInputSignature,      // [In]頂點着色器字節碼
    SIZE_T BytecodeLength,                              // [In]頂點着色器字節碼長度
    ID3D11InputLayout **ppInputLayout);                 // [Out]獲取的輸入佈局

ID3D11DeviceContext::IASetInputLayout方法--輸入裝配階段設置輸入佈局

下面的方法可讓咱們使用剛建立好的輸入佈局:

void ID3D11DeviceContext::IASetInputLayout( 
    ID3D11InputLayout *pInputLayout);   // [In]輸入佈局

頂點/像素着色器的建立

ID3D11Device::CraeteXXXXShader方法--建立着色器

從D3D設備能夠建立出6種着色器:

方法 着色器類型 描述
ID3D11Device::CreateVertexShader ID3D11VertexShader 頂點着色器
ID3D11Device::CreateHullShader ID3D11HullShader 外殼着色器
ID3D11Device::CreateDomainShader ID3D11DomainShader 域着色器
ID3D11Device::CreateComputeShader ID3D11ComputeShader 計算着色器
ID3D11Device::CreateGeometryShader ID3D11GeometryShader 幾何着色器
ID3D11Device::CreatePixelShader ID3D11PixelShader 像素着色器

這些方法的輸入形參都是一致的,只是輸出的是不一樣的着色器,以建立頂點着色器的方法爲例:

HRESULT ID3D11Device::CreateVertexShader( 
    const void *pShaderBytecode,            // [In]着色器字節碼
    SIZE_T BytecodeLength,                  // [In]字節碼長度
    ID3D11ClassLinkage *pClassLinkage,      // [In_Opt]忽略
    ID3D11VertexShader **ppVertexShader);   // [Out]獲取頂點着色器

GameApp::InitEffect方法--着色器或特效相關的初始化

下面展現了GameApp::InitEffect方法的實現,其中輸入佈局的建立也須要放到這裏。

CreateShaderFromFile函數請到文章開頭的 HLSL編譯着色器的三種方法 查看。

// 這裏使用了filesystem頭文件,除此以外還須要添加
// using namespace std::experimental;
bool GameApp::InitEffect()
{
    ComPtr<ID3DBlob> blob;

    // 建立頂點着色器
    HR(CreateShaderFromFile(L"HLSL\\Triangle_VS.cso", L"HLSL\\Triangle_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(m_pd3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pVertexShader.GetAddressOf()));
    // 建立並綁定頂點佈局
    HR(m_pd3dDevice->CreateInputLayout(VertexPosColor::inputLayout, ARRAYSIZE(VertexPosColor::inputLayout),
        blob->GetBufferPointer(), blob->GetBufferSize(), m_pVertexLayout.GetAddressOf()));

    // 建立像素着色器
    HR(CreateShaderFromFile(L"HLSL\\Triangle_PS.cso", L"HLSL\\Triangle_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf()));
    HR(m_pd3dDevice->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pPixelShader.GetAddressOf()));

    return true;
}

GameApp::InitResource方法

頂點緩衝區(Vertex Buffer)

頂點緩衝區的做用是,將頂點數組以緩衝區ID3D11Buffer的形式提供給輸入裝配階段。

ID3D11Device::CreateBuffer方法--建立一個緩衝區

要建立頂點緩衝區,首先須要填充好緩衝區描述D3D11_BUFFER_DESC

typedef struct D3D11_BUFFER_DESC
{
    UINT ByteWidth;             // 數據字節數
    D3D11_USAGE Usage;          // CPU和GPU的讀寫權限相關
    UINT BindFlags;             // 緩衝區類型的標誌
    UINT CPUAccessFlags;        // CPU讀寫權限的指定
    UINT MiscFlags;             // 忽略
    UINT StructureByteStride;   // 忽略
}   D3D11_BUFFER_DESC;

在這裏須要詳細講述一下D3D11_USAGE枚舉類型對應的讀寫關係:

CPU讀 CPU寫 GPU讀 GPU寫
D3D11_USAGE_DEFAULT
D3D11_USAGE_IMMUTABLE
D3D11_USAGE_DYNAMIC
D3D11_USAGE_STAGING

對於D3D11_USAGE_DEFAULT類型的緩衝區,應當使用 ID3D11DeviceContext::UpdateSubresource方法來更新緩衝區資源,它的原理是將內存中的某段數據傳遞到顯存中,而後再將該顯存中的數據複製到在顯存中的緩衝區。這種更新方式咱們是沒法直接訪問緩衝區的內容的。在繪製完成/開始前調用能夠比較快地更新顯存中的數據。

而對於D3D11_USAGE_DYNAMIC類型的緩衝區,則應當使用ID3D11DeviceContext::MapID3D11DeviceContext::Unmap方法,將顯存中的數據映射到內存中,而後修改該片內存的數據,最後將修改好的數據映射回顯存中。這種更新方式咱們是能夠直接獲取來自顯存的數據的,但代價就是更新的效率會比上面的方式更低一些。

因爲目前的教程所涉及到的頂點緩衝區在建立後一般是不會修改的,所以將其設爲D3D11_USAGE_IMMUTABLE

這裏將建立包含三個頂點數據的緩衝區:

// 設置三角形頂點
// 注意三個頂點的給出順序應當按順時針排布
VertexPosColor vertices[] =
{
    { XMFLOAT3(0.0f, 0.5f, 0.5f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },
    { XMFLOAT3(0.5f, -0.5f, 0.5f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) },
    { XMFLOAT3(-0.5f, -0.5f, 0.5f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) }
};
// 設置頂點緩衝區描述
D3D11_BUFFER_DESC vbd;
ZeroMemory(&vbd, sizeof(vbd));
vbd.Usage = D3D11_USAGE_IMMUTABLE;
vbd.ByteWidth = sizeof vertices;
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;

有了緩衝區描述,還須要使用D3D11_SUBRESOURCE_DATA結構體來指定要用來初始化的數據:

typedef struct D3D11_SUBRESOURCE_DATA
{
    const void *pSysMem;        // 用於初始化的數據
    UINT SysMemPitch;           // 忽略
    UINT SysMemSlicePitch;      // 忽略
}   D3D11_SUBRESOURCE_DATA;

子資源數據結構體的填充也很簡單:

// 新建頂點緩衝區
D3D11_SUBRESOURCE_DATA InitData;
ZeroMemory(&InitData, sizeof(InitData));
InitData.pSysMem = vertices;

最後經過ID3D11Device::CreateBuffer來建立一個頂點緩衝區:

HRESULT ID3D11Device::CreateBuffer( 
    const D3D11_BUFFER_DESC *pDesc,     // [In]頂點緩衝區描述
    const D3D11_SUBRESOURCE_DATA *pInitialData, // [In]子資源數據
    ID3D11Buffer **ppBuffer);           // [Out] 獲取緩衝區

演示以下:

ComPtr<ID3D11Buffer> m_pVertexBuffer = nullptr;
HR(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffer.GetAddressOf()));

ID3D11DeviceContext::IASetVertexBuffers方法--渲染管線輸入裝配階段設置頂點緩衝區

建立好頂點緩衝區後,就能夠在渲染管線輸入裝配階段設置該頂點緩衝區了:

void ID3D11DeviceContext::IASetVertexBuffers( 
    UINT StartSlot,     // [In]輸入槽索引
    UINT NumBuffers,    // [In]緩衝區數目
    ID3D11Buffer *const *ppVertexBuffers,   // [In]指向緩衝區數組的指針
    const UINT *pStrides,   // [In]一個數組,規定了對全部緩衝區每次讀取的字節數分別是多少
    const UINT *pOffsets);  // [In]一個數組,規定了對全部緩衝區的初始字節偏移量
// 輸入裝配階段的頂點緩衝區設置
UINT stride = sizeof(VertexPosColor);   // 跨越字節數
UINT offset = 0;                        // 起始偏移量
    
m_pd3dImmediateContext->IASetVertexBuffers(0, 1, m_pVertexBuffer.GetAddressOf(), &stride, &offset);

只要繪製的內容不變,該部分的設置則只須要進行一次便可,由於渲染管線中各個部分的設置方法一經調用就會當即生效。然而若是須要繪製不一樣的內容或者效果,則須要在繪製前給渲染管線綁定好各類所需的資源。

圖元類型

D3D_PRIMITIVE_TOPOLOGY枚舉定義了許多種圖元類型,一般會根據頂點緩衝區的頂點索引(若是有索引緩衝區則是根據這些索引的值)和裝配方式進行解釋,其中:

圖元類型 含義
D3D11_PRIMITIVE_TOPOLOGY_POINTLIST 按一系列點進行裝配
D3D11_PRIMITIVE_TOPOLOGY_LINESTRIP 按一系列線段進行裝配,每相鄰兩個頂點(或索引數組相鄰的兩個索引對應的頂點)構成一條線段
D3D11_PRIMITIVE_TOPOLOGY_LINELIST 按一系列線段進行裝配,每兩個頂點(或索引數組每兩個索引對應的頂點)構成一條線段
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP 按一系列三角形進行裝配,每相鄰三個頂點(或索引數組相鄰的三個索引對應的頂點)構成一個三角形
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST 按一系列三角形進行裝配,每三個頂點(或索引數組每三個索引對應的頂點)構成一個三角形
D3D11_PRIMITIVE_TOPOLOGY_LINELIST_ADJ 每4個頂點爲一組,只繪製第2個頂點與第3個頂點的連線(或索引數組每4個索引爲一組,只繪製索引模4餘數爲2和3的連線)
D3D11_PRIMITIVE_TOPOLOGY_LINESTRIP_ADJ 繪製除了最開始和結尾的全部線段(或者索引數組不繪製索引0和1的連線,以及n-2和n-1的連線)
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST_ADJ 每6個頂點爲一組,只繪製第一、三、5個頂點構成的三角形(或索引數組每6個索引爲一組,只繪製索引模6餘數爲0, 2, 4的三角形)
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP_ADJ 拋棄全部索引模2爲奇數的頂點或索引,剩餘的進行Triangle Strip的繪製

Point List

Line list(左) or line strip(右)

Triangle list(左) or triangle strip(右)

Line list with adjacency(左) or line strip with adjacency(右)

Triangle list with adjacency(v6, v8, v10也構成一個三角形)

Triangle strip with adjacency缺圖

一般絕大多數狀況下,咱們都會使用D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST

ID3D11DeviceContext::IASetPrimitiveTopology方法--渲染管線輸入裝配階段設置圖元類型

void ID3D11DeviceContext::IASetPrimitiveTopology( 
    D3D11_PRIMITIVE_TOPOLOGY Topology);     // [In]圖元類型

該操做只須要設置一次便可。

ID3D11DeviceContext::*SSetShader方法--給渲染管線某一着色階段設置對應的着色器

這裏的*能夠是V, H, D, C, G, P,對應六個可編程渲染管線階段,除了第一個參數提供的着色器類型不一樣外,其他參數一致。

ID3D11DeviceContext::VSSetShader爲例:

void ID3D11DeviceContext::VSSetShader( 
    ID3D11VertexShader *pVertexShader,              // [In]頂點着色器
    ID3D11ClassInstance *const *ppClassInstances,   // [In_Opt]忽略
    UINT NumClassInstances);                        // [In]忽略

注意: 相似給渲染管線綁定資源的一切方法,在綁定以後就會一直生效,而不是說僅可以使用一次。因此,之後若是你須要用別的特效去繪製當前物體,就要從新綁定好渲染管線所須要的一切資源。

最後給出GameApp::InitResource方法的實現

bool GameApp::InitResource()
{
    // 設置三角形頂點
    VertexPosColor vertices[] =
    {
        { XMFLOAT3(0.0f, 0.5f, 0.5f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },
        { XMFLOAT3(0.5f, -0.5f, 0.5f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) },
        { XMFLOAT3(-0.5f, -0.5f, 0.5f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) }
    };
    // 設置頂點緩衝區描述
    D3D11_BUFFER_DESC vbd;
    ZeroMemory(&vbd, sizeof(vbd));
    vbd.Usage = D3D11_USAGE_IMMUTABLE;
    vbd.ByteWidth = sizeof vertices;
    vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
    vbd.CPUAccessFlags = 0;
    // 新建頂點緩衝區
    D3D11_SUBRESOURCE_DATA InitData;
    ZeroMemory(&InitData, sizeof(InitData));
    InitData.pSysMem = vertices;
    HR(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffer.GetAddressOf()));


    // ******************
    // 給渲染管線各個階段綁定好所需資源

    // 輸入裝配階段的頂點緩衝區設置
    UINT stride = sizeof(VertexPosColor);   // 跨越字節數
    UINT offset = 0;                        // 起始偏移量

    m_pd3dImmediateContext->IASetVertexBuffers(0, 1, m_pVertexBuffer.GetAddressOf(), &stride, &offset);
    // 設置圖元類型,設定輸入佈局
    m_pd3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    m_pd3dImmediateContext->IASetInputLayout(m_pVertexLayout.Get());
    // 將着色器綁定到渲染管線
    m_pd3dImmediateContext->VSSetShader(m_pVertexShader.Get(), nullptr, 0);
    m_pd3dImmediateContext->PSSetShader(m_pPixelShader.Get(), nullptr, 0);

    return true;
}

GameApp::DrawScene方法

ID3D11DeviceContext::Draw方法--根據已經綁定的頂點緩衝區進行繪製

該方法不須要提供索引緩衝區:

void ID3D11DeviceContext::Draw( 
    UINT VertexCount,           // [In]須要繪製的頂點數目
    UINT StartVertexLocation);  // [In]起始頂點索引

調用該方法後,從輸入裝配階段開始,該繪製的進行將會經歷一次完整的渲染管線階段,直到輸出合併階段爲止。

經過指定VertexCountStartVertexLocation的值咱們能夠按順序繪製從索引VertexCountVertexCount + StartVertexLocation - 1的頂點

GameApp::DrawScene方法的實現以下:

void GameApp::DrawScene()
{
    assert(m_pd3dImmediateContext);
    assert(m_pSwapChain);

    static float black[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; // RGBA = (0,0,0,255)
    m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), black);
    m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

    // 繪製三角形
    m_pd3dImmediateContext->Draw(3, 0);
    HR(m_pSwapChain->Present(0, 0));
}

最終的效果以下:
image

練習題

粗體字爲自定義題目

  1. 嘗試交換三角形第一個和第三個頂點的數據,屏幕將顯示什麼?爲何?
  2. 嘗試用6個頂點繪製矩形表面
  3. 嘗試用4個頂點繪製矩形表面(提示:D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。

相關文章
相關標籤/搜索