渲染三角形
在上个教材中,我们构建了一个最小的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可以理解的方式描述顶点的结构。
创建输入布局步骤:
需要先定义
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 } };
具体定义问题请参考百度。
使用
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());
现在,我们有了顶点缓冲区,有了输入布局,有了着色器。那么接下来,我们就需要把他们全部绑定到渲染管线上就好了,这些渲染管线在每次画图的时候就可以使用到他们了。
ID3D11DeviceContext::IASetInputLayout——绑定输入布局
void ID3D11DeviceContext::IASetInputLayout( ID3D11InputLayout *pInputLayout); // [In]输入布局
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); }
```