渲染三角形

在上个教材中,我们构建了一个最小的Direct3D11应用程序,它向窗口输出单一的颜色。在本教材中,我们将在屏幕上输出一个三角形。

前言

由于固定管线的取消,现在我们要想绘制图形必须要了解可编程渲染管线的流程(这方面知识,请自行了解)。一个能绘制图形的渲染管线最少需要两个可编程着色器:顶点着色器像素着色器。关于该部分知识,请自行了解。

第一份HLSL代码

在我们项目中创建HLSL文件夹,将所有着色器代码放到这。
在里面创建一个Triangle.hlsli的文件,内容如下

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

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

接下来创建Triangle_VS.hlsl文件用于存放顶点着色器代码:

#include"Triangle.hlsli"

// 顶点着色器
VertexOut VS(VertexIn vIn) 
{
    VertexOut vOut;
    vOut.col = vIn.col;
    vOut.posH = float4(vIn.pos, 1.0f);
    return vOut;
}

最后创建Triangle_PS.hlsl文件用于存放像素着色器代码:

#include"Triangle.hlsli"


float4 PS(VertexOut vOut) : SV_TARGET
{
    return vOut.col;
}

建议在vs中点击扩展-联机-搜索HLSL Tools for Visual Studio插件
有了这两份着色器源码,我们就可以创建出相对于的着色器,为后面做准备。

XX着色器的创建

ID3D11Device::CreateXXXXShader方法--创建着色器

从D3D设备可以创建出6中着色器:
图片说明
这些方法的输入形参都是一致的,知识输入的是不同的着色器,以创建顶点着色器代码为例。

HRESULT ID3D11Device::CreateVertexShader( 
    const void *pShaderBytecode,            // [In]着色器字节码
    SIZE_T BytecodeLength,                  // [In]字节码长度
    ID3D11ClassLinkage *pClassLinkage,      // [In_Opt]忽略
    ID3D11VertexShader **ppVertexShader   // [Out]获取顶点着色器
);

于是,首先,我们需要获得着色器字节码。但是我们自由着色器代码呀,怎么变成字节码呢?

D3DCompileFromFile函数--构建着色器字节码

HRESULT D3DCompileFromFile(
    LPCWSTR pFileName,                  // [In]要编译的.hlsl文件
    CONST D3D_SHADER_MACRO* pDefines,   // [In_Opt]忽略
    ID3DInclude* pInclude,              // [In_Opt]如何应对#include宏
    LPCSTR pEntrypoint,                 // [In]入口函数名
    LPCSTR pTarget,                     // [In]使用的着色器模型
    UINT Flags1,                        // [In]D3DCOMPILE系列宏
    UINT Flags2,                        // [In]D3DCOMPILE_FLAGS2系列宏
    ID3DBlob** ppCode,                  // [Out]获得着色器的二进制块
    ID3DBlob** ppErrorMsgs           // [Out]可能会获得错误信息的二进制块
); 

其中PInclude用于决定如何处理包含文件。如果设置为nullptr,则编译的着色器代码中包含#include时会引发编译器错误。如果你需要使用#include,可以传递D3D_COMPILE_STANDARD_FILE_INCLUDE宏,他会正常处理包含文件。
所以,本例简单实现如下:

ComPtr<ID3D11VertexShader>    g_pVertexShader;
ComPtr<ID3D11PixelShader>    g_pPixelShader;
ComPtr<ID3DBlob> blob;

    // 创建顶点着色器
    D3DCompileFromFile(L"HLSL\\Triangle_VS.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, "VS", "vs_4_0", 0, 0, blob.GetAddressOf(), nullptr);
    g_pD3D11Device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, g_pVertexShader.GetAddressOf());

    // 创建像素着色器
    D3DCompileFromFile(L"HLSL\\Triangle_PS.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, "PS", "ps_4_0", 0, 0, blob.GetAddressOf(), nullptr);
    g_pD3D11Device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, g_pPixelShader.GetAddressOf());

这样,我们就获取到了编译好的着色器对象。好的,让我们开始正文。

三角形元素

三角形是图形领域最常见的图形,它由三个顶点构成。为了让GPU渲染三角形,我们必须告诉GPU三角形三个顶点的位置。所以我们要如何传递信息到GPU呢?
对于GPU而言,他需要得到三个顶点的信息。所以我们在HLSL中构建一个简单的结构体作为GPU的顶点输入。

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

但是,我们程序阶段输入的数据一般是这样

// 定义一个结构体用于接收用户的数据
struct VertexPosCol
{
    DirectX::XMFLOAT4 col;
    DirectX::XMFLOAT3 pos;
};
...
...
VertexPosCol vertexData[] = 
{
    { XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f),XMFLOAT3(0.0f, 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),XMFLOAT3(-0.5f, -0.5f, 0.5f)}
}

现在,我们在系统内存中,存放了顶点的数据,但是,我们现在这数据,因为是放在系统内存中,只有CPU可以读写,我们要让他可以被GPU使用,所以我们需要将这些数据从系统内存复制到顶点缓冲区(一个设置GPU可以读写的内存)。

ID3D11Device::CreateBuffer方法--创建顶点缓冲区

创建顶点缓冲区需要先填好缓冲区描述结构体ID3D11_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_SUBRESOURCE_DATA结构体,他描述了实际的数据

typedef struct D3D11_SUBRESOURCE_DATA
{
    const void *pSysMem;        // 用于初始化的数据
    UINT SysMemPitch;           // 忽略
    UINT SysMemSlicePitch;      // 忽略
}   D3D11_SUBRESOURCE_DATA;

最终,我们就可以通过ID3D11Device::CreateBuffer创建顶点缓冲区

HRESULT ID3D11Device::CreateBuffer( 
    const D3D11_BUFFER_DESC *pDesc,     // [In]顶点缓冲区描述
    const D3D11_SUBRESOURCE_DATA *pInitialData, // [In]子资源数据
    ID3D11Buffer **ppBuffer           // [Out] 获取缓冲区
);

所以,完整示例如下

// 定义一个结构体用于接收用户的数据
struct VertexPosCol
{
    DirectX::XMFLOAT4 col;
    DirectX::XMFLOAT3 pos;
};
// 顶点数据
VertexPosCol vertexData[] = 
{
    { XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f),XMFLOAT3(0.0f, 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),XMFLOAT3(-0.5f, -0.5f, 0.5f)}
};
D3D11_BUFFER_DESC vbd = {};
vbd.Usage = D3D11_USAGE_DEFAULT;
vbd.ByteWidth = sizeof(vertexData);
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;

D3D11_SUBRESOURCE_DATA initData = {};
initData.pSysMem = vertexData;

if (FAILED(g_pD3D11Device->CreateBuffer(&vbd, &initData, g_pVertexBuffer.GetAddressOf())))
    return;

这样,我们就成功得到了一个GPU可以访问的缓冲区。可是实际上,这块缓冲区,有三个顶点信息。我们GPU顶点着色器,每次是读取一个顶点的信息。那,我们是不是还要需要一个东西,来每次正确读取一个顶点的所有信息。这个东西就是输入布局

ID3D11Device::CreateInputLayout方法--创建输入布局

在D3D11中,输入布局是D3D对象,它以GPU可以理解的方式描述顶点的结构。
创建输入布局步骤:

  1. 需要先定义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;

    示例代码如下:

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

    具体定义问题请参考百度。

  2. 使用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]获取的输入布局

    这里我们看到,创建输入布局需要顶点着色器签名。因为我们输入布局,就是用来给顶底着色器处理接受顶点缓冲区中的数据。
    示例代码如下:

    ComPtr<ID3DBlob> blob;
    
     // 创建顶点着色器
     D3DCompileFromFile(L"HLSL\\Triangle_VS.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, "VS", "vs_4_0", 0, 0, blob.GetAddressOf(), nullptr);
     ...
    
     // 创建输入布局
     g_pD3D11Device->CreateInputLayout(inputLayout, ARRAYSIZE(inputLayout), blob->GetBufferPointer(), blob->GetBufferSize(), g_pInputLayout.GetAddressOf());

    现在,我们有了顶点缓冲区,有了输入布局,有了着色器。那么接下来,我们就需要把他们全部绑定到渲染管线上就好了,这些渲染管线在每次画图的时候就可以使用到他们了。

  3. ID3D11DeviceContext::IASetInputLayout——绑定输入布局

    void ID3D11DeviceContext::IASetInputLayout( 
     ID3D11InputLayout *pInputLayout);   // [In]输入布局
  4. ID3D11DeviceContext::IASetVertexBuffers--绑定顶点缓冲区

    void ID3D11DeviceContext::IASetVertexBuffers( 
     UINT StartSlot,     // [In]输入槽索引
     UINT NumBuffers,    // [In]缓冲区数目
     ID3D11Buffer *const *ppVertexBuffers,   // [In]指向缓冲区数组的指针
     const UINT *pStrides,   // [In]一个数组,规定了对所有缓冲区每次读取的字节数分别是多少
     const UINT *pOffsets);  // [In]一个数组,规定了对所有缓冲区的初始字节偏移量

    只要绘制的内容不变,该部分设置只需要进行一次就可以了。因为管线是一个状态机。
    下面给出简单示例

    INT stride = sizeof(VertexPosCol);
    UINT offset = 0;
    g_pImmediateContext->IASetVertexBuffers(0, 1, g_pVertexBuffer.GetAddressOf(), &stride, &offset);
    g_pImmediateContext->IASetInputLayout(g_pInputLayout.Get());

    绘制三角形

    现在,我们把顶点缓冲区和输入布局都绑定好了,也拿到了着色器对象,但是,我们还需指定渲染管线的对顶点的绘制方式。

    图元类型

    图片说明
    我们通过ID3D11DeviceContext::IASetPrimitiveTopology方法设置渲染管线绘图方式

    void ID3D11DeviceContext::IASetPrimitiveTopology( 
     D3D11_PRIMITIVE_TOPOLOGY Topology);     // [In]图元类型

    绑定着色器,绘制图形

    一切的一切都以准备就绪了,就差着色器还没绑定上,绑定着色器也很简单,因为我们已获取到我们写的着色器对象,只需要调用ID3D11DeviceContext::*SSetShader方法--给渲染管道某以着色阶段设置对应的着色器。他们形参都基本一致,以顶点着色器为例。

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

    简单示例

     g_pImmediateContext->VSSetShader(g_pVertexShader.Get(), nullptr, 0);
     g_pImmediateContext->PSSetShader(g_pPixelShader.Get(), nullptr, 0);

    最后,我们只要每次调用一下下列渲染代码,

    if (g_pImmediateContext&&g_pSwapChain)
     {
         static float blue[4] = { 0.0f, 0.0f, 1.0f, 1.0f };  // RGBA = (0,0,255,255)
         g_pImmediateContext->ClearRenderTargetView(g_pRenderTargetView.Get(), blue);
    
         g_pImmediateContext->Draw(3, 0);
         g_pSwapChain->Present(0, 0);
     }
    

```
图片说明