回到 DirectX11--使用Windows SDK来进行开发
一个立方体有8个顶点,然而绘制一个立方体需要画12个三角形,如果按照前面的方法绘制的话,则需要提供36个顶点,而且这里面的顶点数据会重复4次甚至5次。这样的绘制方法会占用大量的内存空间。
接下来会讲另外一种绘制方法,可以只提供立方体的8个顶点数据,然后用一个索引数组来指代使用哪些顶点,按怎样的顺序绘制。
立方体顶点数据
顶点数组的初始化如下:
// ******************
// 设置立方体顶点
// 5________ 6
// /| /|
// /_|_____/ |
// 1|4|_ _ 2|_|7
// | / | /
// |/______|/
// 0 3
VertexPosColor vertices[] =
{
{ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f) },
{ XMFLOAT3(-1.0f, 1.0f, -1.0f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) },
{ XMFLOAT3(1.0f, 1.0f, -1.0f), XMFLOAT4(1.0f, 1.0f, 0.0f, 1.0f) },
{ XMFLOAT3(1.0f, -1.0f, -1.0f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },
{ XMFLOAT3(-1.0f, -1.0f, 1.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) },
{ XMFLOAT3(-1.0f, 1.0f, 1.0f), XMFLOAT4(1.0f, 0.0f, 1.0f, 1.0f) },
{ XMFLOAT3(1.0f, 1.0f, 1.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
{ XMFLOAT3(1.0f, -1.0f, 1.0f), XMFLOAT4(0.0f, 1.0f, 1.0f, 1.0f) }
};
索引缓冲区
使用索引缓冲区进行替代指定顺序绘制,可以有效减少顶点缓冲区的占用空间,避免提供大量重复的顶点数据。
现在索引数组的初始化如下:
// 索引数组
WORD indices[] = {
// 正面
0, 1, 2,
2, 3, 0,
// 左面
4, 5, 1,
1, 0, 4,
// 顶面
1, 5, 6,
6, 2, 1,
// 背面
7, 6, 5,
5, 4, 7,
// 右面
3, 2, 6,
6, 7, 3,
// 底面
4, 0, 3,
3, 7, 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;
// 新建索引缓冲区
D3D11_SUBRESOURCE_DATA InitData;
ZeroMemory(&InitData, sizeof(InitData));
InitData.pSysMem = indices;
md3dDevice->CreateBuffer(&ibd, &InitData, mIndexBuffer.GetAddressOf());
在输入装配阶段,我们使用ID3D11DeviceContext::IASetIndexBuffer设置索引缓冲区:
void ID3D11DeviceContext::IASetIndexBuffer(
ID3D11Buffer *pIndexBuffer, // 索引缓冲区
DXGI_FORMAT Format, // 数据格式
UINT Offset); // 字节偏移量
于是我们可以这样:
// 输入装配阶段的索引缓冲区设置
md3dImmediateContext->IASetIndexBuffer(mIndexBuffer, DXGI_FORMAT_R16_UINT, 0);
使用16位的索引有利于节省空间,现阶段绝大多数模型的顶点数目都少于65535个。
固定缓冲区
在HLSL中,固定缓冲区的变量类似于C++这边的全局变量,供着色器代码使用。下面是一个HLSL固定缓冲区示例:
cbuffer ConstantBuffer : register(b0)
{
row_major matrix World;
row_major matrix View;
row_major matrix Proj;
}
cbuffer 用于声明一个固定缓冲区
row_major 指定矩阵内数据按行主形式排布,否则默认为列主形式。D3D的矩阵默认为行主形式,如果这里不指定,会导致从D3D传矩阵给HLSL时会发生一次转置。
matrix 等价于 float4x4
register(b0) 指的是该固定缓冲区位于寄存器索引为0的缓冲区
而在C++应用层,固定缓冲区的对应结构体可以为:
struct ConstantBuffer
{
XMMATRIX world;
XMMATRIX view;
XMMATRIX proj;
};
下面演示了如何创建一个固定缓冲区:
ComPtr<ID3D11Buffer> mConstantBuffer = nullptr;
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;
// 新建固定缓冲区,不使用初始数据
md3dDevice->CreateBuffer(&cbd, nullptr, mConstantBuffer.GetAddressOf());
更新D3D11_USAGE_DEFAULT类型的资源可以使用ID3D11DeviceContext::UpdateSubresource方法进行,在每次绘制之前调用:
void ID3D11DeviceContext::UpdateSubresource(
ID3D11Resource *pDstResource, // [In]需要更新的缓冲区
UINT DstSubresource, // [In]忽略
const D3D11_BOX *pDstBox, // [In]忽略
const void *pSrcData, // [In]用于更新的数据源
UINT SrcRowPitch, // [In]忽略
UINT SrcDepthPitch); // [In]忽略
现在需要创建一个ConstantBuffer结构体变量用于更新固定缓冲区:
ConstantBuffer mCBuffer;
mCBuffer.world = XMMatrixIdentity();
mCBuffer.view = XMMatrixLookAtLH(
XMVectorSet(0.0f, 0.0f, -5.0f, 0.0f),
XMVectorSet(0.0f, 0.0f, 0.0f, 0.0f),
XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f)
);
mCBuffer.proj = XMMatrixPerspectiveFovLH(XM_PIDIV2, AspectRatio(), 1.0f, 1000.0f);
md3dImmediateContext->UpdateSubresource(mConstantBuffer.Get(), 0, nullptr, &mCBuffer, 0, 0);
更新了数据后,还需要给顶点着色阶段设置固定缓冲区供使用。使用ID3D11DeviceContext::VSSetConstantBuffers方法:
void ID3D11DeviceContext::VSSetConstantBuffers(
UINT StartSlot, // [In]放入缓冲区的起始索引,例如上面指定了b0,则这里应为0
UINT NumBuffers, // [In]设置的缓冲区数目
ID3D11Buffer *const *ppConstantBuffers); // [In]用于设置的缓冲区数组
在每一帧绘制的时候就可以这样调用:
md3dImmediateContext->VSSetConstantBuffers(0, 1, mConstantBuffer.GetAddressOf());
HLSL代码
该例程所用的HLSL代码如下:
//cube.fx
cbuffer ConstantBuffer : register(b0)
{
row_major matrix World;
row_major matrix View;
row_major matrix Proj;
}
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 = mul(float4(pIn.pos, 1.0f), World);
pOut.posH = mul(pOut.posH, View);
pOut.posH = mul(pOut.posH, Proj);
pOut.color = pIn.color;
return pOut;
}
float4 PS(VertexOut pIn) : SV_Target
{
return pIn.color;
}
注意:在HLSL中,矩阵乘法不能用运算符,该运算符要求两个矩阵行列数相同,运算的结果也是一个同行列数的矩阵,运算过程为:Cij = Aij Bij。应该使用mul函数进行替代。
然后新建Cube_VS.hlsl和Cube_PS.hlsl,包含上面的Cube.fx,再按之前提到的内容来设置编译信息。
根据索引数组进行绘制
绘制之前我们得让立方体转起来,不然就只能看到立方体的正面。在这里我们让魔方同时绕X轴和Y轴旋转,修改世界矩阵即可:
static float phi = 0.0f, theta = 0.0f;
phi += 0.00003f, theta += 0.00005f;
mCBuffer.world = XMMatrixRotationX(phi) * XMMatrixRotationY(theta);
md3dImmediateContext->UpdateSubresource(mConstantBuffer.Get(), 0, nullptr, &mCBuffer, 0, 0);
在输入装配阶段指定好了顶点缓冲区、索引缓冲区和原始拓补类型后,再绑定固定缓冲区到顶点着色阶段,最后就可以使用ID3D11DeviceContext::DrawIndexed方法来绘制:
void ID3D11DeviceContext::DrawIndexed(
UINT IndexCount, // 索引数目
UINT StartIndexLocation, // 起始索引位置
INT BaseVertexLocation); // 起始顶点位置
现在我们要绘制12个三角形,构成立方体:
md3dImmediateContext->VSSetConstantBuffers(0, 1, mConstantBuffer.GetAddressOf());
// 绘制立方体
md3dImmediateContext->DrawIndexed(36, 0, 0);
最终效果如下: