环境:VS2017 语言:C++
总起:
红龙书所使用的Effect库已经不建议使用了,所以这边在编译Shader所使用的是X_Jun96大佬手动的方式。
附上工程链接:https://github.com/anguangzhihen/Dx11。
红龙书本身是比较难以研读的,特别是对于初学者而言,它的一般做法是先将理论全部说一遍,然后讲一整个例子,没有由浅入深进行说明,总体而言看起来比较痛苦。不过我会把它读完。
红龙书第五章主要是渲染管线的理论知识,关于这个就不做详细叙述了,主要是两个阶段:
- 几何阶段,将模型内部坐标转换到齐次剪裁空间,主要通过wvp矩阵进行转换(Unity中为mvp);
- 光栅化阶段,通过遍历所有可见模型上的三角形,对其每个像素点进行操作(类似设置法线贴图就在该流程中)。
几何阶段对应的程序控制代码的Shader就是顶点着色器,即Vertex Shader(暂不讨论曲面细分着色器和几何着色器);而光栅化阶段是片元着色器,即Pixel Shader。
第六章是介绍Dx渲染时的各个函数与数据结构,然后是实践,看起来很多很复杂,实际上比起第四章使用的函数量少的多,所以我们直接从实践出发,看看渲染一个方块究竟要做一些什么操作。
渲染一个方块:
首先我们来看一下效果:
嗯,好的,非常完美(有同学可能要问:这不是棱锥吗?嗯……可能是方块上面的四个点坍缩成了一个点吧)。
首先我们让新建的BoxDemo类继承我们第一篇博文中编写的D3DApp,然后主要重写3个方法:Init、UpdateScene、DrawScene。
首先是Init,大部分的操作都集中在此处:
bool BoxApp::Init()
{
if (!D3DApp::Init())
return false;
BuildGeometryBuffers();
BuildFX();
return true;
}
♦ 创建顶点和索引信息、设置GPU缓存(BuildGeometryBuffers)
在渲染一个模型的时候,我们考虑的第一步就是把这个模型通过顶点建立出来。
这边我们除了顶点以外,还需要设置索引信息,什么是索引信息呢。以2D空间举例:
一个方块顶点为:(0, 0) (0, 1) (1, 1) (1, 0)。
则以如果以三角形为图元,需要两个三角形才能渲染该方块,即:0 1 2和1 2 3。(Dx为左手坐标系,顶点顺序顺时针为可见)
上面的0 1 2 1 2 3便是索引信息,因为如果要使用顶点来表示索引,便会多出很多多余的内存。
首先声明3个缓存成员变量,还有常量缓存使用的数据结构:
ID3D11Buffer* mBoxVB; // 顶点缓存
ID3D11Buffer* mBoxIB; // 索引缓存
ID3D11Buffer* mBoxCB; // 常量缓存
ConstantBuffer mCBuffer; // 常量数据结构
初始化的内容:
void BoxApp::BuildGeometryBuffers()
{
// 顶点
Vertex vertices[] =
{
{ XMFLOAT3(-1.0f, 0.0f, -1.0f), XMFLOAT4((const float*)&Colors::Red) },
{ XMFLOAT3(-1.0f, 0.0f, +1.0f), XMFLOAT4((const float*)&Colors::Green) },
{ XMFLOAT3(+1.0f, 0.0f, +1.0f), XMFLOAT4((const float*)&Colors::Blue) },
{ XMFLOAT3(+1.0f, 0.0f, -1.0f), XMFLOAT4((const float*)&Colors::Yellow) },
{ XMFLOAT3(0.0f, 1.41f, 0.0f), XMFLOAT4((const float*)&Colors::Black) },
};
// 顶点缓存的描述
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, &mBoxVB));
// 绑定到渲染管线上
UINT stride = sizeof(Vertex);
UINT offset = 0;
md3dImmediateContext->IASetVertexBuffers(0, 1, &mBoxVB, &stride, &offset);
// 创建索引
UINT indices[] = {
// 底面
0, 2, 1,
0, 3, 2,
// 4个三角面
0, 1, 4,
1, 2, 4,
2, 3, 4,
3, 0, 4,
};
// 索引缓存的描述
D3D11_BUFFER_DESC ibd;
ZeroMemory(&ibd, sizeof(ibd));
ibd.Usage = D3D11_USAGE_DEFAULT;
ibd.ByteWidth = sizeof indices;
ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
ibd.CPUAccessFlags = 0;
// 将索引设置到描述中
InitData.pSysMem = indices;
HR(md3dDevice->CreateBuffer(&ibd, &InitData, &mBoxIB));
// 绑定到渲染管线上
md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); // 设定图元类型
md3dImmediateContext->IASetIndexBuffer(mBoxIB, DXGI_FORMAT_R32_UINT, 0);
// 设置常量缓存区
D3D11_BUFFER_DESC cbd;
ZeroMemory(&cbd, sizeof(cbd));
cbd.Usage = D3D11_USAGE_DEFAULT;
cbd.ByteWidth = sizeof(ConstantBuffer);
cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
cbd.CPUAccessFlags = 0;
HR(md3dDevice->CreateBuffer(&cbd, nullptr, &mBoxCB));
md3dImmediateContext->VSSetConstantBuffers(0, 1, &mBoxCB);
}
可以看到顶点缓存、索引缓存和常量缓存都用到了D3D11_BUFFER_DESC进行描述。其中顶点缓存、索引缓存使用了 D3D11_SUBRESOURCE_DATA在描述中直接指定数据,而常量缓存到时候会在UpdateScene时动态指定。
以下是D3D11_BUFFER_DESC重要的几个参数介绍:
Usage,指定该缓存会被怎样使用:
- D3D11_USAGE_DEAFULT,GPU可读可写,当使用mapping API(例如ID3D11DeviceContext::Map)时,CPU不可读不可写。否则CPU可使用ID3D11DeviceContext::UpdateSubresource进行更新缓存信息;
- D3D11_USAGE_IMMUTABLE,指定该内容在创建后便不会被更改,GPU只读,CPU不可读不可写;
- D3D11_USAGE_DYNAMIC,指定内容CPU可写,而GPU可读,应该尽量避免该用法,因为数据会从内存传送到显存中,效率比较低;
- D3D11_USAGE_STAGING,指定内容需要CPU可读,CPU可以使用ID3D11DeviceContext::CopyResource和ID3D11DeviceContext::CopySubresourceRegion从显存传输到内存中进行读取。
BindFlags,指定当前缓存的类型:
- D3D11_BIND_VERTEX_BUFFER,顶点缓存;
- D3D11_BIND_INDEX_BUFFER,索引缓存;
- D3D11_BIND_CONSTANT_BUFFER,常量缓存。
CPUAccessFlags,CPU访问标记:
- 指定为0,CPU不可读不可写;
- D3D11_CPU_ACCESS_WRITE,CPU可写;
- D3D11_CPU_ACCESS_READ,CPU可读。
以下介绍D3D11_SUBRESOURCE_DATA的重要参数:
pSysMem,内容指针,如果缓存指定能存n个顶点,则这边给的数据必须大于或等于n。
描述创建完了之后使用ID3D11Device::CreateBuffer创建缓存。
之后使用以下三个方法分别将缓存绑定到渲染管线上:
1. ID3D11DeviceContext:: IASetVertexBuffers 绑定顶点缓存;
2. ID3D11DeviceContext:: IASetIndexBuffer 绑定索引缓存;
3. ID3D11DeviceContext:: VSSetConstantBuffers 绑定常量缓存。
在索引缓存绑定到渲染管线上之前,需要设定图元类型:
ID3D11DeviceContext:: IASetPrimitiveTopology:
- D3D11_PRIMITIVE_TOPOLOGY_POINTLIST,点;
- D3D11_PRIMITIVE_TOPOLOGY_LINESTRIP,线条,当前点与上一个点相连;
- D3D11_PRIMITIVE_TOPOLOGY_LINELIST,线,每两个点为一条线;
- D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP,三角,当前点与上两个点组成一个三角;
- D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST,三角,每三个点组成一个三角,常用。
♦ 初始化着色器(BuildFX)
接下来初始化Shader,创建Shader的CreateShaderFromFile函数直接使用大佬的了,也不做过多的介绍了。
首先声明成员变量:
ID3D11InputLayout* mVertexLayout; // 顶点布局
ID3D11VertexShader* mVertexShader; // 顶点Shader
ID3D11PixelShader* mPixelShader; // 片元Shader
接下来初始化:
void BoxApp::BuildFX()
{
ID3DBlob* blob;
// 创建顶点着色器
HR(D3DUtil::CreateShaderFromFile(L"HLSL\\Box_VS.vso", L"HLSL\\Box_VS.hlsl", "VS", "vs_5_0", &blob));
HR(md3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, &mVertexShader));
// 创建顶点布局
D3D11_INPUT_ELEMENT_DESC vertexLayout[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 }
};
HR(md3dDevice->CreateInputLayout(vertexLayout, ARRAYSIZE(vertexLayout), blob->GetBufferPointer(), blob->GetBufferSize(), &mVertexLayout));
md3dImmediateContext->IASetInputLayout(mVertexLayout);// 设定输入布局
ReleaseCOM(blob);
// 创建像素着色器
HR(D3DUtil::CreateShaderFromFile(L"HLSL\\Box_PS.pso", L"HLSL\\Box_PS.hlsl", "PS", "ps_5_0", &blob));
HR(md3dDevice->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, &mPixelShader));
ReleaseCOM(blob);
md3dImmediateContext->VSSetShader(mVertexShader, nullptr, 0);
md3dImmediateContext->PSSetShader(mPixelShader, nullptr, 0);
}
使用CreateShaderFromFile后获得ID3DBlob,相当于Shader的二进制数据,然后使用ID3D11Device:: CreateVertexShader创建Shader,最后将Shader通过ID3D11DeviceContext:: VSSetShader/ PSSetShader设置到渲染管线中。
重点来说一下顶点布局ID3D11InputLayout:
首先有两个数据结构:
// 顶点Shader的传入参数
struct Vertex
{
DirectX::XMFLOAT3 Pos;
DirectX::XMFLOAT4 Color;
};
// 常量缓存
struct ConstantBuffer
{
DirectX::XMMATRIX wvp;
};
接着看一下Shader的三个文件:
// Box.fx
cbuffer cbPerObject : register(b0)
{
row_major matrix wvp; // 默认列主矩阵
};
struct VertexIn
{
float3 PosL : POSITION;
float4 Color : COLOR;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float4 Color : COLOR;
};
// Box_VS.hlsl顶点着色器
#include "Box.fx"
VertexOut VS(VertexIn pIn)
{
VertexOut pOut;
pOut.PosH = mul(float4(pIn.PosL, 1.0f), wvp);
pOut.Color = pIn.Color;
return pOut;
}
// Box_PS.hlsl片元着色器
#include "Box.fx"
float4 PS(VertexOut pIn) : SV_Target
{
return pIn.Color;
}
从数据结构出发,C++中的常量缓存ConstantBuffer对应的是cbPerObject,而Vertex对应的是VertexIn,VertexOut是顶点着色器传给片元着色器,所以在C++中并不需要(注意这边C++的数据结构一定要和Shader的数据结构完全对应,顺序都要一致,并且传的时候不能有null。不要问我为什么知道,我的眼中常含泪水)。
常量缓存我们在上一节中已经指定好了。
而顶点数据,我们在上一节中使用Vertex创建了每一个顶点,并设置到了缓存中,但并没有告诉Shader怎么用,这边设定顶点布局便是告诉Shader怎么使用输入的顶点数据。
D3D11_INPUT_ELEMENT_DESC,顶点每一个数据的描述(这边POSITION和COLOR便分别对应Vertex中的Pos和Color):
- SemanticName,名称,如果指定COLOR,便对应Shader数据成员变量后缀的COLOR;
- SemanticIndex,在相同的名称有多个时指定第几个,其中POSITION等同于POSITION0;
- Format,指定数据的类型;
- InputSlot,指定槽位,Dx支持16个槽位;
- AlignedByteOffset,针对槽位的数据偏移;
- InputSlotClass,暂时只指定D3D11_INPUT_PER_VERTEX_DATA;
- InstanceDataStepRate,暂时只指定为0。
描述创建完之后,使用ID3D11Device::CreateInputLayout创建顶点布局,接着使用ID3D11DeviceContext::IASetInputLayout设置顶点布局。
至此终于将准备工作完成了,接下来是每帧的更新和渲染。
♦ 更新场景
void BoxApp::UpdateScene(float dt)
{
float x = mRadius * sinf(mPhi) * cosf(mTheta);
float z = mRadius * sinf(mPhi) * sinf(mTheta);
float y = mRadius * cosf(mPhi);
XMVECTOR pos = XMVectorSet(x, y, z, 1.0f);
XMVECTOR target = XMVectorZero();
XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
XMMATRIX V = XMMatrixLookAtLH(pos, target, up);
DirectX::XMStoreFloat4x4(&mView, V);
// Unity Matrix4x4 列优先填充 Unity Shader float4x4 行优先填充
DirectX::XMMATRIX world = DirectX::XMLoadFloat4x4(&mWorld); // 主行矩阵
DirectX::XMMATRIX view = DirectX::XMLoadFloat4x4(&mView);
DirectX::XMMATRIX proj = DirectX::XMLoadFloat4x4(&mProj);
mCBuffer.wvp = world * view * proj;
// 更新WVP矩阵数据
md3dImmediateContext->UpdateSubresource(mBoxCB, 0, nullptr, &mCBuffer, 0, 0);
}
常量中的数据是wvp矩阵,所以在渲染之前需要先将该矩阵计算好传给Shader,这边是固定了物体,旋转视角来观察不同角度的物体(因为没有参照物,所以看起来像是在旋转物体),因此需要每帧更新View矩阵来重置相机的位置,如果是旋转物体则需要更新World矩阵(坐标、旋转、缩放信息都包含在其中),而投影矩阵一般不用更新,除非窗口大小发生了变化。
通过UpdateSubresource更新完wvp常量后,开始渲染。
♦ 更新场景
void BoxApp::DrawScene()
{
md3dImmediateContext->ClearRenderTargetView(mRenderTargetView, reinterpret_cast<const float*>(&Colors::LightSteelBlue));
md3dImmediateContext->ClearDepthStencilView(mDepthStencilView, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
// 绘制Box的36的顶点(12个三角)
//md3dImmediateContext->DrawIndexed(36, 0, 0);
md3dImmediateContext->DrawIndexed(18, 0, 0);
HR(mSwapChain->Present(0, 0));
}
重置RT后,直接调用DrawIndexed进行渲染,好了,没了。