DirectX11 With Windows SDK完整目录:https://blog.csdn.net/x_jun96/article/details/80293670
目前暂时没有写HLSL具体教程的打算,而是着重于如何做到不用DirectX SDK来进行渲染。除此之外,这里也没有使用Effects框架的想法,而是直接通过调用一系列替代方法来实现。因为Effects11框架是一个微软自己编写的第三方库,用于管理着色器。在DirectX SDK中有Effects11框架的代码,但是也已经经过了8年时间了,当然我们还是可以在GitHub上看到微软其实一直都在给Effects11框架做更新,只不过未来可能会逐渐取消对fx5.0的支持。
项目源码点此:https://github.com/MKXJun/DirectX11-With-Windows-SDK
目录
这里将直接从一个已经编写好的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; // 这里alpha通道的值默认为1.0
return pOut;
}
// 像素着色器
float4 PS(VertexOut pIn) : SV_Target
{
return pIn.color;
}
HLSL代码的语法和C/C++的语法非常相似,也许后面会开坑描述一下HLSL语言,不过现在先把注意力放在这份代码中比较特别的地方。
float3
和float4
都是内置的变量类型,可以看作是C++的struct
类型,支持多种构造方式和成员访问。除此之外,还有float
和float2
两种类型。对于float4
,它的四个成员分别为x
,y
,z
和w
然后具体讲述一下变量名后面的语义:
语义名 | 具体含义 |
---|---|
POSITION | 描述该变量是一个坐标点 |
SV_POSITION | 说明该顶点的位置在从顶点着色器输出后,后续的着色器都不能改变它的值,作为光栅化的最终位置 |
COLOR | 描述该变量是一个颜色 |
SV_Target | 说明输出的颜色值将会直接保存到渲染目标视图的后备缓冲区对应位置 |
输入布局
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]输入布局
运行期间编译着色器代码
对着色器代码或文件的相关操作位于头文件件d3dcompiler.h
GameApp::CompileShaderFromFile方法
该方法主要用于简化运行期编译函数的一些参数,在了解具体代码之前还需要知道下面的函数
D3DCompileFromFile函数–运行期编译.fx/.hlsl文件
HRESULT D3DCompileFromFile(
LPCWSTR pFileName, // [In]要编译的.fx/.hlsl文件
CONST D3D_SHADER_MACRO* pDefines, // [In_Opt]忽略
ID3DInclude* pInclude, // [In_Opt]忽略
LPCSTR pEntrypoint, // [In]入口函数名
LPCSTR pTarget, // [In]使用的着色器模型
UINT Flags1, // [In]D3DCOMPILE系列宏
UINT Flags2, // [In]D3DCOMPILE_FLAGS2系列宏
ID3DBlob** ppCode, // [Out]获得着色器的二进制块
ID3DBlob** ppErrorMsgs); // [Out]可能会获得错误信息的二进制块
然后下面就是GameApp::CompileShaderFromFile
方法的具体实现了:
HRESULT GameApp::CompileShaderFromFile(const WCHAR * szFileName, LPCSTR szEntryPoint, LPCSTR szShaderModel, ID3DBlob ** ppBlobOut)
{
HRESULT hr = S_OK;
DWORD dwShaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;
#ifdef _DEBUG
// 设置 D3DCOMPILE_DEBUG 标志用于获取着色器调试信息。该标志可以提升调试体验,
// 但仍然允许着色器进行优化操作
dwShaderFlags |= D3DCOMPILE_DEBUG;
// 在Debug环境下禁用优化以避免出现一些不合理的情况
dwShaderFlags |= D3DCOMPILE_SKIP_OPTIMIZATION;
#endif
ComPtr<ID3DBlob> errorBlob = nullptr;
hr = D3DCompileFromFile(szFileName, nullptr, nullptr, szEntryPoint, szShaderModel,
dwShaderFlags, 0, ppBlobOut, errorBlob.GetAddressOf());
if (FAILED(hr))
{
if (errorBlob != nullptr)
{
OutputDebugStringA(reinterpret_cast<const char*>(errorBlob->GetBufferPointer()));
}
return hr;
}
return S_OK;
}
ID3D11Device::CreateVertexShader方法–创建顶点着色器
若读取的是顶点着色器信息,则需要使用下述方法来创建一个顶点着色器:
HRESULT ID3D11Device::CreateVertexShader(
const void *pShaderBytecode, // [In]着色器字节码
SIZE_T BytecodeLength, // [In]字节码长度
ID3D11ClassLinkage *pClassLinkage, // [In_Opt]忽略
ID3D11VertexShader **ppVertexShader); // [Out]获取顶点着色器
ID3D11DeviceContext::VSSetShader方法–给渲染管线顶点着色阶段设置顶点着色器
创建好顶点着色器后,就可以绑定到渲染管线了:
void ID3D11DeviceContext::VSSetShader(
ID3D11VertexShader *pVertexShader, // [In]顶点着色器
ID3D11ClassInstance *const *ppClassInstances, // [In_Opt]忽略
UINT NumClassInstances); // [In]忽略
ID3D11Device::CreatePixelShader方法–创建像素着色器
而若读取的是像素着色器信息,则需要使用下述方法来创建一个顶点着色器:
HRESULT ID3D11Device::CreatePixelShader(
const void *pShaderBytecode, // [In]着色器字节码
SIZE_T BytecodeLength, // [In]字节码长度
ID3D11ClassLinkage *pClassLinkage, // [In_Opt]忽略
ID3D11PixelShader **ppPixelShader); // [Out]获取像素着色器
ID3D11DeviceContext::PSSetShader方法–给渲染管线像素着色阶段设置像素着色器
创建好像素着色器后,就可以绑定到渲染管线了:
void ID3D11DeviceContext::PSSetShader(
ID3D11PixelShader *pPixelShader, // [In]像素着色器
ID3D11ClassInstance *const *ppClassInstances, // [In]忽略
UINT NumClassInstances); // [In]忽略
编译期间编译着色器代码,运行期间读取编译好的二进制信息
当然,如果能将编译着色器代码的操作从运行期转移到编译期的话会更好一些,这样我们就只需要读取编译好的着色器二进制文件了。
现在我们在项目中创建HLSL文件夹,将所有的着色器代码放到这里。
然后在里面创建两个文件:Triangle_VS.hlsl
和 Triangle_PS.hlsl
,这两个文件都加上这样一句话:
#include "Triangle.fx"
这时可以把Triangle.fx
、Triangle_VS.hlsl
和Triangle_PS.hlsl
拉入VS项目中,但是Triangle.fx
不参与项目的编译过程。可以右键该文件-属性,按下图进行操作。
而对于Triangle_VS.hlsl
和Triangle_PS.hlsl
,则在项目属性要这样设置:
这样就可以在编译项目的同时编译HLSL代码了,编译通过后,在项目的HLSL文件夹会产生Triangle_VS.cso
和Triangle_PS.cso
GameApp::InitEffect方法–着色器或特效相关的初始化
该方法同时支持运行期编译和编译期编译,如果检测到没有.cso文件被编译出来的话,就会进行运行期编译的相关操作。如果有的话,我们就需要使用下面的函数来进行读取。
D3DReadFileToBlob函数–读取编译好的着色器二进制信息
接下来,我们使用下面的函数来读取编译好的着色器二进制信息:
HRESULT WINAPI
D3DReadFileToBlob(LPCWSTR pFileName, // [In].cso文件名
ID3DBlob** ppContents); // [Out]获取二进制大数据块
下面展示了GameApp::InitEffect
方法的实现,其中输入布局的创建和输入装配阶段对输入布局的设置也放到了这里:
// 这里使用了filesystem头文件,除此之外还需要添加
// using namespace std::experimental;
bool GameApp::InitEffect()
{
ComPtr<ID3DBlob> blob;
// 已经编译好的着色器文件名
filesystem::path psoPath = "HLSL\\Triangle_PS.cso", vsoPath = "HLSL\\Triangle_VS.cso";
std::wstring wstr;
// 寻找是否有已经编译好的顶点着色器,否则在运行期编译
if (filesystem::exists(vsoPath))
{
wstr = vsoPath.generic_wstring();
HR(D3DReadFileToBlob(wstr.c_str(), blob.GetAddressOf()));
}
else
{
HR(CompileShaderFromFile(L"HLSL\\Triangle.fx", "VS", "vs_5_0", blob.GetAddressOf()));
}
// 创建顶点着色器
HR(md3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mVertexShader.GetAddressOf()));
// 创建并绑定顶点布局
HR(md3dDevice->CreateInputLayout(VertexPosColor::inputLayout, ARRAYSIZE(VertexPosColor::inputLayout),
blob->GetBufferPointer(), blob->GetBufferSize(), mVertexLayout.GetAddressOf()));
md3dImmediateContext->IASetInputLayout(mVertexLayout.Get());
blob.Reset();
// 寻找是否有已经编译好的像素着色器,否则在运行期编译
if (filesystem::exists(psoPath))
{
wstr = psoPath.generic_wstring();
HR(D3DReadFileToBlob(wstr.c_str(), blob.GetAddressOf()));
}
else
{
HR(CompileShaderFromFile(L"HLSL\\Triangle.fx", "PS", "ps_5_0", blob.GetAddressOf()));
}
// 创建像素着色器
HR(md3dDevice->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mPixelShader.GetAddressOf()));
blob.Reset();
// 将着色器绑定到渲染管线
md3dImmediateContext->VSSetShader(mVertexShader.Get(), nullptr, 0);
md3dImmediateContext->PSSetShader(mPixelShader.Get(), nullptr, 0);
return true;
}
GameApp::InitResource方法
顶点缓冲区
顶点缓冲区的作用是,将顶点数组以缓冲区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
方法来更新缓冲区资源,在绘制完成/开始前调用可以比较快地更新GPU内存的数据
而对于D3D11_USAGE_DYNAMIC
类型的缓冲区,则应当使用
ID3D11DeviceContext::Map
和ID3D11DeviceContext::Unmap
方法来实现从CPU到GPU的写入
这里将创建包含三个顶点数据的缓冲区:
// 设置三角形顶点
// 注意三个顶点的给出顺序应当按顺时针排布
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_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()));
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; // 起始偏移量
md3dImmediateContext->IASetVertexBuffers(0, 1, mVertexBuffer.GetAddressOf(), &stride, &offset);
只要绘制的内容不变,该部分的设置则只需要进行一次即可。
原始拓补类型
D3D_PRIMITIVE_TOPOLOGY
枚举定义了许多种原始拓补类型,通常会根据顶点缓冲区的顶点索引(如果有索引缓冲区则是根据这些索引的值)和装配方式进行解释,其中:
原始拓补类型 | 含义 | 对应 |
---|---|---|
D3D_PRIMITIVE_TOPOLOGY_POINTLIST | 按一系列点进行装配 | 上图(a) |
D3D_PRIMITIVE_TOPOLOGY_LINESTRIP | 按一系列线段进行装配,每相邻两个顶点(或索引数组相邻的两个索引对应的顶点)构成一条线段 | 上图(b) |
D3D_PRIMITIVE_TOPOLOGY_LINELIST | 按一系列线段进行装配,每两个顶点(或索引数组每两个索引对应的顶点)构成一条线段 | 上图(c) |
D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP | 按一系列三角形进行装配,每相邻三个顶点(或索引数组相邻的三个索引对应的顶点)构成一个三角形 | 上图(d) |
D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST | 按一系列三角形进行装配,每三个顶点(或索引数组每三个索引对应的顶点)构成一个三角形 | 下图(a) |
通常绝大多数情况下,我们都会使用D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST
ID3D11DeviceContext::IASetPrimitiveTopology方法–渲染管线输入装配阶段设置原始拓补类型
void ID3D11DeviceContext::IASetPrimitiveTopology(
D3D11_PRIMITIVE_TOPOLOGY Topology); // [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_DEFAULT;
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(md3dDevice->CreateBuffer(&vbd, &InitData, mVertexBuffer.GetAddressOf()));
// 输入装配阶段的顶点缓冲区设置
UINT stride = sizeof(VertexPosColor); // 跨越字节数
UINT offset = 0; // 起始偏移量
md3dImmediateContext->IASetVertexBuffers(0, 1, mVertexBuffer.GetAddressOf(), &stride, &offset);
// 设置图元类型
md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
return true;
}
GameApp::DrawScene方法
ID3D11DeviceContext::Draw方法–根据已经绑定的顶点缓冲区进行绘制
该方法不需要提供索引缓冲区:
void ID3D11DeviceContext::Draw(
UINT VertexCount, // [In]需要绘制的顶点数目
UINT StartVertexLocation); // [In]起始顶点索引
调用该方法后,从输入装配阶段开始,该绘制的进行将会经历一次完整的渲染管线阶段,直到输出合并阶段为止。
GameApp::DrawScene
方法的实现如下:
void GameApp::DrawScene()
{
assert(md3dImmediateContext);
assert(mSwapChain);
static float blue[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; // RGBA = (0,0,0,255)
md3dImmediateContext->ClearRenderTargetView(mRenderTargetView.Get(), reinterpret_cast<const float*>(&blue));
md3dImmediateContext->ClearDepthStencilView(mDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
// 绘制三角形
md3dImmediateContext->Draw(3, 0);
HR(mSwapChain->Present(0, 0));
}
最终的效果如下:
下一篇:
DirectX11 With Windows SDK–03 渲染一个立方体:https://blog.csdn.net/x_jun96/article/details/80301143
该文章后续视情况可能还会有所修改。