回到 DirectX11--使用Windows SDK来进行开发
目前暂时没有写HLSL具体教程的打算,而是着重于如何做到不用DirectX SDK来进行渲染。除此之外,这里也没有使用Effects框架的想法,而是直接通过调用一系列替代方法来实现。因为Effects11框架是一个微软自己编写的第三方库,用于管理着色器。在DirectX SDK中有Effects11框架的代码,但是也已经经过了8年时间了,当然我们还是可以在GitHub上看到微软其实一直都在给Effects11框架做更新,只不过未来可能会逐渐取消对fx5.0的支持。
这里将直接从一个已经编写好的HLSL代码入手。
编译HLSL代码
这里的.fx文件只写了顶点着色器和像素着色器的HLSL代码:
// Triangle.fx
struct VertexIn
{
float3 pos : POSITION;
float4 color : COLOR;
};
struct VertexOut
{
float4 posH : SV_POSITION;
float4 color : COLOR;
};
// 顶点着色器
VertexOut VS(VertexIn pIn)
{
VertexOut pOut;
pOut.posH = float4(pIn.pos, 1.0f);
pOut.color = pIn.color;
return pOut;
}
// 像素着色器
float4 PS(VertexOut pIn) : SV_Target
{
return pIn.color;
}
这里具体讲述一下变量名后面的语义:
POSITION 描述该变量是一个坐标点
SV_POSITION 说明该顶点的位置在从顶点着色器输出后,后续的着色器都不能改变它的值,作为光栅化的最终位置
COLOR 描述该变量是一个颜色
SV_Target 说明输出的颜色值将会直接保存到渲染目标视图的后备缓冲区对应位置
编译期间编译着色器代码,运行期间读取编译好的二进制信息
以下操作需要确保添加头文件d3dcompiler.h
创建两个文件:Triangle_VS.hlsl 和 Triangle_PS.hlsl,这两个文件都加上这样一句话:
#include "Triangle.fx"
这时可以把Triangle.fx、Triangle_VS.hlsl和Triangle_PS.hlsl拉入VS项目中,其中Triangle.fx需要在项目中排除生成。右键Triangle.fx--属性--常规--从项目中排除--是
而对于Triangle_VS.hlsl和Triangle_PS.hlsl,则在项目属性要这样设置:
这样就可以在编译项目的同时编译HLSL代码了,编译通过后,在项目文件夹会产生Triangle_VS.cso和Triangle_PS.cso
接下来,我们使用D3DReadFileToBlob函数来读取编译好的着色器二进制信息:
HRESULT WINAPI
D3DReadFileToBlob(LPCWSTR pFileName, // [In].cso文件名
ID3DBlob** ppContents); // [Out]获取二进制大数据块
若读取的是顶点着色器信息,则需要使用ID3D11Device::CreateVertexShader方法来创建一个顶点着色器:
HRESULT ID3D11Device::CreateVertexShader(
const void *pShaderBytecode, // [In]着色器字节码
SIZE_T BytecodeLength, // [In]字节码长度
ID3D11ClassLinkage *pClassLinkage, // [In]忽略
ID3D11VertexShader **ppVertexShader); // [Out]获取顶点着色器
再用ID3D11DeviceContext::VSSetShader方法来绑定着色器到渲染管线:
void STDMETHODCALLTYPE VSSetShader(
ID3D11VertexShader *pVertexShader, // [In]顶点着色器
ID3D11ClassInstance *const *ppClassInstances, // [In]忽略
UINT NumClassInstances); // [In]忽略
例如:
ComPtr<ID3DBlob> blob = nullptr;
ComPtr<ID3D11VertexShader> vertexShader = nullptr;
HR(D3DReadFileToBlob(L"Triangle_VS.cso", blob.GetAddressOf()));
HR(md3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), 0, vertexShader.GetAddressOf()));
md3dImmediateContext->VSSetShader(vertexShader.Get(), nullptr, 0);
而若读取的是像素着色器信息,则需要使用ID3D11Device::CreatePixelShader方法来创建一个像素着色器:
HRESULT ID3D11Device::CreatePixelShader(
const void *pShaderBytecode, // [In]着色器字节码
SIZE_T BytecodeLength, // [In]字节码长度
ID3D11ClassLinkage *pClassLinkage, // [In]忽略
ID3D11PixelShader **ppPixelShader); // [Out]获取像素着色器
再用ID3D11DeviceContext::PSSetShader方法来绑定着色器到渲染管线:
void STDMETHODCALLTYPE PSSetShader(
ID3D11PixelShader *pPixelShader, // [In]像素着色器
ID3D11ClassInstance *const *ppClassInstances, // [In]忽略
UINT NumClassInstances); // [In]忽略
例如:
ComPtr<ID3DBlob> blob = nullptr;
ComPtr<ID3D11PixelShader> pixelShader = nullptr;
HR(D3DReadFileToBlob(L"Triangle_PS.cso", blob.GetAddressOf()));
HR(md3dDevice->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), 0, pixelShader.GetAddressOf()));
md3dImmediateContext->PSSetShader(pixelShader.Get(), nullptr, 0);
运行期间编译着色器代码
使用D3DCompileFromFile函数就可以在运行期编译.fx/.hlsl文件
HRESULT D3DCompileFromFile(
LPCWSTR pFileName, // [In]要编译的.fx/.hlsl文件
CONST D3D_SHADER_MACRO* pDefines, // [In]忽略
ID3DInclude* pInclude, // [In]忽略
LPCSTR pEntrypoint, // [In]入口函数名
LPCSTR pTarget, // [In]使用的着色器模型
UINT Flags1, // [In]D3DCOMPILE系列宏
UINT Flags2, // [In]D3DCOMPILE_FLAGS2系列宏
ID3DBlob** ppCode, // [Out]获得着色器的二进制块
ID3DBlob** ppErrorMsgs); // [Out]可能会获得错误信息的二进制块
编译顶点着色器,并绑定到渲染管线的代码示例如下:
CComPtr<ID3DBlob> blob = nullptr, errorBlob = nullptr;
CComPtr<ID3D11VertexShader> vertexShader = nullptr;
DWORD dwShaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;
#ifdef _DEBUG
// 设置 D3DCOMPILE_DEBUG 标志用于获取着色器调试信息。该标志可以提升调试体验,
// 但仍然允许着色器进行优化操作
dwShaderFlags |= D3DCOMPILE_DEBUG;
// 在Debug环境下禁用优化以避免出现一些不合理的情况
dwShaderFlags |= D3DCOMPILE_SKIP_OPTIMIZATION;
#endif
HR(D3DCompileFromFile("Triangle.fx", nullptr, nullptr, "VS", "vs_4_0",
dwShaderFlags, 0, blob.GetAddressOf(), errorBlob.GetAddressOf()));
if (errorBlob != nullptr)
{
OutputDebugStringA(reinterpret_cast<const char*>(errorBlob->GetBufferPointer()));
// 这里可以中断程序
}
HR(md3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), 0, vertexShader.GetAddressOf()));
md3dImmediateContext->VSSetShader(vertexShader, nullptr, 0);
编译像素着色器的操作跟上面的也相差不大,在这不重复列举。
输入布局
输入布局ID3D11InputLayout的作用是,使得C++应用层的数据类型通过输入布局的描述来准确传输给HLSL顶点中的每一项子数据。
比如该项目用的结构体为:
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中。
其中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的初始化信息如下:
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类型的值。
输入槽这里只使用1个,即索引为0的输入槽。
初始位置则指的是该成员的位置与起始成员所在的字节偏移量。
输入类型有两种:按每个顶点数据输入,按每个实例数据输入。
接下来使用ID3D11Device::CreateInputLayout方法创建一个输入布局:
HRESULT STDMETHODCALLTYPE CreateInputLayout(
const D3D11_INPUT_ELEMENT_DESC *pInputElementDescs, // [In]输入布局描述
UINT NumElements, // [In]上述数组元素个数
const void *pShaderBytecodeWithInputSignature, // [In]顶点着色器字节码
SIZE_T BytecodeLength, // [In]顶点着色器字节码长度
ID3D11InputLayout **ppInputLayout); // [Out]获取的输入布局
然后我们就可以使用 给输入装配阶段设置输入布局:
void ID3D11DeviceContext::IASetInputLayout(
ID3D11InputLayout *pInputLayout); // [In]输入布局
若输入数据的布局不再改变,则该调用只需进行一次。操作如下:
CComPtr<ID3D11InputLayout> mVertexLayout;
HR(md3dDevice->CreateInputLayout(
VertexPosColor::inputLayout,
ARRAYSIZE(VertexPosColor::inputLayout),
blob->GetBufferPointer(), blob->GetBufferSize(), mVertexLayout.GetAddressOf()));
md3dImmediateContext->IASetInputLayout(mVertexLayout.Get());
顶点缓冲区
顶点缓冲区的作用是,将顶点数组以缓冲区的形式提供给输入装配阶段。
要创建顶点缓冲区,首先需要填充好缓冲区描述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方法来更新缓冲区资源,在绘制完成/开始前调用可以比较快地更新GPU内存的数据
而对于D3D11_USAGE_DYNAMIC类型的缓冲区,则应当使用
ID3D11DeviceContext::Map和ID3D11DeviceContext::Unmap方法来实现从CPU到GPU的写入
这里将创建包含三个顶点数据的缓冲区:
// 设置三角形顶点
// 注意三个顶点的给出顺序应当按顺时针排布
VertexPosColor vertices[] =
{
{ XMFLOAT3(0.0f, 0.5f, 0.5f), XMFLOAT3(0.0f, 1.0f, 0.0f, 1.0f) },
{ XMFLOAT3(0.5f, -0.5f, 0.5f), XMFLOAT3(0.0f, 0.0f, 1.0f, 1.0f) },
{ XMFLOAT3(-0.5f, -0.5f, 0.5f), XMFLOAT3(1.0f, 0.0f, 0.0f, 1.0f) }
};
D3D11_BUFFER_DESC vbd;
ZeroMemory(&vbd, sizeof(vbd));
vbd.Usage = D3D11_USAGE_DEFAULT; // 仅GPU可读写
vbd.ByteWidth = sizeof vertices;
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER; // 用作顶点缓冲区
vbd.CPUAccessFlags = 0; // CPU无读写权限
有了缓冲区描述,还需要使用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> mVertexBuffer = nullptr;
HR(md3dDevice->CreateBuffer(&vbd, &InitData, mVertexBuffer.GetAddressOf()));
然后在输入装配阶段,我们就可以将顶点缓冲区作为输入:
// 输入装配阶段的顶点缓冲区设置
UINT stride = sizeof(VertexPosColor); // 跨越字节数
UINT offset = 0; // 起始偏移量
md3dImmediateContext->IASetVertexBuffers(0, 1, mVertexBuffer.GetAddressOf(), &stride, &offset);
只要绘制的内容不变,该部分的设置则只需要进行一次即可。
根据原始拓补类型和顶点数组进行绘制
原始拓补的设置通常是固定的,使用方法ID3D11DeviceContext::IASetPrimitiveTopology来进行设置:
void ID3D11DeviceContext::IASetPrimitiveTopology(
D3D11_PRIMITIVE_TOPOLOGY Topology); // [In]原始拓补类型
尽管原始拓补类型有很多种,但我们最常用的是D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST。该操作通常只设置一次就行:
md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
最后我们可以使用ID3D11DeviceContext::Draw方法来进行绘制,该方法不需要索引缓冲区:
void ID3D11DeviceContext::Draw(
UINT VertexCount, // [In]需要绘制的顶点数目
UINT StartVertexLocation); // [In]起始顶点索引
调用该方法后,从输入装配阶段开始,该绘制的进行将会经历一次完整的渲染管线阶段,直到输出合并阶段为止。
在每一帧的更新调用即可:
md3dImmediateContext->Draw(3, 0); // 绘制一个三角形
最终的效果如下: