今天我们来完成第七章的课后习题。虽然只有3道题,但是涉及的知识还是挺多的,并且第三题的加载骷髅网格,是后期都会用到的基础场景,但后面章节并没有说如何加载,所以那题非常重要。
第1题:修改"Shapes"例程,以ProceduralGeometry::CreateGeoSphere方法替换程序中的ProceduralGeometry::CreateSphere方法,并分别尝试使用0、1、2、3这4种细分等级。
这题主要考察GeoSphere的创建函数中numSubdivisions参数的使用。细分越高,模型越平滑,细分的具体算法我们先不管,函数定义如下,可以看到第一个参数是球的半径,第二个参数是细分级别,我们只需在BuildGeometry函数中创建Geosphere的时候给具体数值即可。
MeshData ProceduralGeometry::CreateGeosphere(float radius, uint32 numSubdivisions)
{
......
}
void ShapesApp::BuildGeometry()
{
......
ProceduralGeometry::MeshData sphere = proceGeo.CreateGeosphere(0.5f, 3);
......
}
编译运行,效果如下。
![80e2df931f9ccbf9b0ea636c4a6290d3.png](https://i-blog.csdnimg.cn/blog_migrate/8ca267dd4ca4bb15b6e52ebb72828cfd.jpeg)
第2题:修改"Shapes"程序,使用16个根常量取代描述表,以此设置每个物体的世界矩阵。
首先我们修改构建根签名代码,使用根常量绑定0号寄存器(即world矩阵数据),并设置16个32位数据,而1号寄存器还是不变(即viewProj矩阵),使用描述符表绑定。
void ShapesApp::BuildRootSignature()
{
CD3DX12_DESCRIPTOR_RANGE cbvTable1;
cbvTable1.Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, //描述符类型
1, //描述符表数量
1);//描述符所绑定的寄存器槽号
//根参数可以是描述符表、根描述符、根常量
CD3DX12_ROOT_PARAMETER slotRootParameter[2];
slotRootParameter[0].InitAsConstants(16, 0);
slotRootParameter[1].InitAsDescriptorTable(1, &cbvTable1);
//根签名由一组根参数构成
CD3DX12_ROOT_SIGNATURE_DESC rootSig(2, //根参数的数量
slotRootParameter, //根参数指针
0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
//用单个寄存器槽来创建一个根签名,该槽位指向一个仅含有单个常量缓冲区的描述符区域
ComPtr<ID3DBlob> serializedRootSig = nullptr;
ComPtr<ID3DBlob> errorBlob = nullptr;
HRESULT hr = D3D12SerializeRootSignature(&rootSig, D3D_ROOT_SIGNATURE_VERSION_1, &serializedRootSig, &errorBlob);
if (errorBlob != nullptr)
{
OutputDebugStringA((char*)errorBlob->GetBufferPointer());
}
ThrowIfFailed(hr);
ThrowIfFailed(d3dDevice->CreateRootSignature(0,
serializedRootSig->GetBufferPointer(),
serializedRootSig->GetBufferSize(),
IID_PPV_ARGS(&rootSignature)));
}
然后将Update函数里关于world矩阵更新的代码移到DrawRenderItems绘制函数中(方便观察)。可以看到我们并不需要用CopyData函数将数据拷贝到GPU缓存,直接设置根常量即可将数据传至GPU,即SetGraphicsRoot32BitConstants函数,由于根常量数组就这一个,所以偏移为0。
void ShapesApp::DrawRenderItems()
{
//将智能指针数组转换成普通指针数组
std::vector<RenderItem*> ritems;
for (auto& e : allRitems)
ritems.push_back(e.get());
//遍历渲染项数组
for (size_t i = 0; i < ritems.size(); i++)
{
auto ritem = ritems[i];
//设置定点索引缓冲区
......
//通过根常量更新数据
world = ritem->world;
XMMATRIX w = XMLoadFloat4x4(&world);
//XMMATRIX赋值给XMFLOAT4X4
XMStoreFloat4x4(&objConstants.world, XMMatrixTranspose(w));
//将数据拷贝至GPU缓存
//currFrameResources->objCB->CopyData(ritem->objCBIndex, objConstants);
cmdList->SetGraphicsRoot32BitConstants(0, //根参数的索引
16, &objConstants, 0);
//绘制顶点(通过索引缓冲区绘制)
......
}
}
由于passCB还存在CBV堆中,所以最后要修改堆中地址。这时地址只受帧资源影响。
//创建passCBV
for (int frameIndex = 0; frameIndex < frameResourcesCount; frameIndex++)
{
......
//int heapIndex = objectCount * frameResourcesCount + frameIndex;
int heapIndex = frameIndex;
......
}
同理设置Draw函数中的passCB堆中地址。
//设置根描述符表2
//int passCbvIndex = (int)allRitems.size() * frameResourcesCount + currFrameResourcesIndex;
int passCbvIndex = currFrameResourcesIndex;
auto passCbvHandle = CD3DX12_GPU_DESCRIPTOR_HANDLE(cbvHeap->GetGPUDescriptorHandleForHeapStart());
passCbvHandle.Offset(passCbvIndex, cbv_srv_uavDescriptorSize);
cmdList->SetGraphicsRootDescriptorTable(1, //根参数的起始索引
passCbvHandle);
编译运行,效果一样,但是现在的world矩阵数据是由16个根常量绑定的。
![0377cf82f2c1be1d6e7a9f3e77302cb5.png](https://i-blog.csdnimg.cn/blog_migrate/557ede9aeb9cb5eece30af7406ee4cdf.jpeg)
总结:根常量绑定数据是最便捷的,不需要创建CBV堆,不需要创建CBV,甚至不需要使用CopyData函数,缺点是太占内存,一个32位根常量占1DWORD,16个就是16DWORD。
3.在本书配套资源中,有一个名为Models/Skull.txt的文件。此文件含有骷髅头网格所需的顶点列表和索引列表。通过使用编辑器来查阅此文件,并修改“shapes”演示程序来加载和渲染此骷髅头网格。
首先我们打开Models/Skull.txt文件,记事本太卡,我直接用vscode打开。结构如下,前两行标注了顶点数和三角形数,后面两个大括号里包含了顶点列表和索引列表。
![46e26cdeb1bc4e78293b966abdab74db.png](https://i-blog.csdnimg.cn/blog_migrate/0cf33e41d5fbbdc007bff957f0b6cbaa.png)
打开VertexList,我们看到一行有6个浮点数,其实左边三个对应pos(顶点坐标),右边三个对应normal(顶点法线)。
![f9962082c68aedb50f4ca73c5fa4ad3d.png](https://i-blog.csdnimg.cn/blog_migrate/54c1fdec929fb05f707127b2ea75933b.png)
打开TriangleList,我们看到一行有3个UINT类型数据,其实就是索引了,每个三角形包含3个索引。
![3065e3e035f1883b1164d46dbffe6081.png](https://i-blog.csdnimg.cn/blog_migrate/24b2ab2dd21965f4505148a8e5d35647.png)
所以我们需要将这些数据读取到程序中,并用我们的数据结构去封装它,最后绘制出来。我们新建一个BuildSkullGeometry函数来构建网格。可以看到我们使用ifstream来读取txt文件中的数据,并将其最终存入顶点缓冲区和索引缓冲区中。因为现阶段我们的Vertex结构中只有pos和color两个属性,因此将normal数据都忽略掉(不读取)。因为skull网格和shapes网格是分别存入不同的顶点和索引缓存的,所以绘制三参数的两个地址都为0。注意:因为这个骷髅网格索引数超过了65536,所以我们要用DXGI_FORMAT_R32_UINT格式的索引数据类型。
void ShapesApp::BuildSkullGeometry()
{
std::ifstream fin("Models/skull.txt");//读取骷髅网格文件
if (!fin)//如果读取失败则弹框警告
{
MessageBox(0, L"Models/skull.txt not found", 0, 0);
return;
}
UINT vertexCount = 0;
UINT triangleCount = 0;
std::string ignore;
fin >> ignore >> vertexCount;//读取vertexCount并赋值
fin >> ignore >> triangleCount;//读取triangleCount并赋值
fin >> ignore >> ignore >> ignore >> ignore;//整行不读
std::vector<Vertex> vertices(vertexCount);//初始化顶点列表
//顶点列表赋值
for (UINT i = 0; i < vertexCount; i++)
{
fin >> vertices[i].Pos.x >> vertices[i].Pos.y >> vertices[i].Pos.z;//读取顶点坐标
fin >> ignore >> ignore >> ignore;//normal不读取
vertices[i].Color = XMCOLOR(DirectX::Colors::CadetBlue);//设置顶点色
}
fin >> ignore;
fin >> ignore;
fin >> ignore;
std::vector<std::int32_t> indices(triangleCount * 3);//初始化索引列表
//索引列表赋值
for (UINT i = 0; i < triangleCount; i++)
{
fin >> indices[i * 3 + 0] >> indices[i * 3 + 1] >> indices[i * 3 + 2];
}
fin.close();//关闭输入流
const UINT vbByteSize = vertices.size() * sizeof(Vertex);//顶点缓存大小
const UINT ibByteSize = indices.size() * sizeof(std::int32_t);//索引缓存大小
auto geo = std::make_unique<MeshGeometry>();
geo->name = "skullGeo";
//将顶点和索引数据复制到CPU系统内存上
ThrowIfFailed(D3DCreateBlob(vbByteSize, &geo->vertexBufferCpu));
CopyMemory(geo->vertexBufferCpu->GetBufferPointer(), vertices.data(), vbByteSize);
ThrowIfFailed(D3DCreateBlob(ibByteSize, &geo->indexBufferCpu));
CopyMemory(geo->indexBufferCpu->GetBufferPointer(), indices.data(), ibByteSize);
//将顶点和索引数据从CPU内存复制到GPU缓存上
geo->vertexBufferGpu = ToolFunc::CreateDefaultBuffer(d3dDevice.Get(),
cmdList.Get(), vbByteSize, vertices.data(), geo->vertexBufferUploader);
geo->indexBufferGpu = ToolFunc::CreateDefaultBuffer(d3dDevice.Get(),
cmdList.Get(), ibByteSize, indices.data(), geo->indexBufferUploader);
geo->vertexByteStride = sizeof(Vertex);
geo->vertexBufferByteSize = vbByteSize;
geo->indexBufferByteSize = ibByteSize;
geo->indexFormat = DXGI_FORMAT_R32_UINT;
//绘制三参数
SubmeshGeometry skullSubmesh;
skullSubmesh.baseVertexLocation = 0;
skullSubmesh.startIndexLocation = 0;
skullSubmesh.indexCount = (UINT)indices.size();
geo->DrawArgs["skull"] = skullSubmesh;
geometries["skullGeo"] = std::move(geo);
}
然后我们构建渲染项,将各属性赋值,并最终存入总渲染项中。
void ShapesApp::BuildRenderItem()
{
......
auto skullRitem = std::make_unique<RenderItem>();
XMStoreFloat4x4(&skullRitem->world, XMMatrixScaling(0.5f, 0.5f, 0.5f) * XMMatrixTranslation(0.0f, 1.0f, 0.0f));
skullRitem->objCBIndex = 2;//skull常量数据(world矩阵)在objConstantBuffer索引1上
skullRitem->primitiveType = D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST;
skullRitem->geo = geometries["skullGeo"].get();
skullRitem->indexCount = skullRitem->geo->DrawArgs["skull"].indexCount;
skullRitem->baseVertexLocation = skullRitem->geo->DrawArgs["skull"].baseVertexLocation;
skullRitem->startIndexLocation = skullRitem->geo->DrawArgs["skull"].startIndexLocation;
allRitems.push_back(std::move(skullRitem));
......
}
编译运行。可以看到骷髅网格已经绘制在窗口中了。
![d51d843eb2f227490f01867b1261a75a.png](https://i-blog.csdnimg.cn/blog_migrate/4e64f48a81ae09583c6e4e6eed566cc1.jpeg)
线框模式。
![5fae6baf40cb3fc4c04c75c9cf13d17c.png](https://i-blog.csdnimg.cn/blog_migrate/268b8cb62232ab8487e8c95f5be8bd7f.jpeg)
至此,绘制几何体章节全部结束。接下来我们将进入《光照篇》,为我们的场景加入外部灯光。
今天先到这里吧,各位下篇见。