之前一篇文章讲了DirectX12的初始化流程,现在来看看在此基础上如何绘制一个Cube。

首先,我们要为这个Cube准备一个shader,来告诉GPU绘制的具体流程,DirectX中的shader使用的是hlsl:

cbuffer cbPerObject : register(b0)
{
float4x4 gWorldViewProj;
}; struct VertexIn
{
float4 Color : COLOR;
float3 PosL : POSITION;
}; struct VertexOut
{
float4 PosH : SV_POSITION;
float4 Color : COLOR;
}; VertexOut VS(VertexIn vin)
{
VertexOut vout; vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj); vout.Color = vin.Color; return vout;
} float4 PS(VertexOut pin) : SV_Target
{
return pin.Color;
}

这个shader做的事情很简单,就是单纯输出顶点设置的颜色。注意到里面有一个cbuffer,这是用来接收来自外部的参数的,例如这里需要把world-view-projection矩阵传进来计算顶点经过投影变换后的位置。为了实现这一步,我们需要创建一个const buffer用来保存传递数据:

mDevice->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(cbSize),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr, IID_PPV_ARGS(&mConstBuffer));

这里指定了buffer的用途,是上传数据给shader读取用的。另外,根据DirectX的约定,const buffer的大小必须为256的整数倍,也即这里的cbSize是256的整数倍。可以通过下面这个函数来计算一个合法的const buffer大小值:

static UINT CalcConstantBufferByteSize(UINT byteSize)
{
return (byteSize + 255) & ~255;
}

由于buffer中的数据可能会频繁更新,这里使用MapUnMap组合来上传数据

	mConstBuffer->Map(0, nullptr, &mConstBufferData);
XMMATRIX world = XMLoadFloat4x4(&object->mWorldMatrix);
XMMATRIX view = XMLoadFloat4x4(&camera->mViewMatrix);
XMMATRIX proj = XMLoadFloat4x4(&camera->mProjMatrix);
XMMATRIX worldViewProj = world * view * proj;
ObjectConstants objConstants;
XMStoreFloat4x4(&objConstants.WorldViewProj, XMMatrixTranspose(worldViewProj));
memcpy(mConstBufferData, &objConstants, sizeof(ObjectConstants));
mConstBuffer->Unmap(0, nullptr);

这里只需要一个矩阵给shader,因此ObjectConstants就是只包含一个矩阵的简单结构体。值得一提的是,shader读取const buffer是按列主序来读取的,所以这里要对矩阵先进行转置,再传递给shader。

有了const buffer之后,我们需要为之创建一个view和存放view的heap:

D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;
cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
cbvHeapDesc.NumDescriptors = 1;
cbvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
cbvHeapDesc.NodeMask = 0;
mDevice->CreateDescriptorHeap(&cbvHeapDesc, IID_PPV_ARGS(&mCbvHeap)); D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
cbvDesc.BufferLocation = mConstBuffer->GetGPUVirtualAddress();
cbvDesc.SizeInBytes = cbSize;
CD3DX12_CPU_DESCRIPTOR_HANDLE handle = CD3DX12_CPU_DESCRIPTOR_HANDLE(
mCbvHeap->GetCPUDescriptorHandleForHeapStart());
mDevice->CreateConstantBufferView(&cbvDesc, handle);

接下来,为了告诉CPUshader读取的位置是在register(b0),我们需要设置root signature。这里使用DescriptorTable的形式进行初始化:

CD3DX12_DESCRIPTOR_RANGE cbvTable;
cbvTable.Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0);
CD3DX12_ROOT_PARAMETER rootParams[1];
rootParams[0].InitAsDescriptorTable(1, &cbvTable);
CD3DX12_ROOT_SIGNATURE_DESC sigDesc(1, rootParams, 0, nullptr,
D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
ComPtr<ID3DBlob> signature = nullptr;
ComPtr<ID3DBlob> error = nullptr;
HRESULT hr = D3D12SerializeRootSignature(&sigDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error);
if (error == nullptr)
{
mDevice->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(),
IID_PPV_ARGS(&mRootSignature));
}

跟const buffer相关的设置就到此为止了。现在,让我们考虑要传给shader的顶点数据,根据传入vertex shader的参数类型,我们需要创建与之相匹配的顶点数据结构和input layout。这里我们对顶点数据结构进行了拆分,即一个只包含顶点的位置数据,一个则包含除此之外的其他数据,这样做一定程度可以减少带宽,可以做到只传输必要的数据给GPU。相应地,input layout里就包含了两个slot:

struct VertexPos
{
XMFLOAT3 position;
}; struct VertexProp
{
XMFLOAT4 color;
...
}; mInputLayout =
{
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0}
};

有了数据结构之后,我们就可以根据数据结构去创建顶点缓存和索引缓存了。我们预先创建一个全局的顶点缓存和索引缓存,以及对应的view,之后就可以将能合并的object数据拷贝过去:

mDevice->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(
mVertexBufferSize * sizeof(VertexPos)),
D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&mPosVertexBufferGPU))); mVertexBufferViews[0].BufferLocation = mPosVertexBufferGPU->GetGPUVirtualAddress();
mVertexBufferViews[0].SizeInBytes = mVertexBufferSize * sizeof(VertexPos);
mVertexBufferViews[0].StrideInBytes = sizeof(VertexPos); mDevice->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(
mVertexBufferSize * sizeof(VertexProp)),
D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&mPropVertexBufferGPU))); mVertexBufferViews[1].BufferLocation = mPropVertexBufferGPU->GetGPUVirtualAddress();
mVertexBufferViews[1].SizeInBytes = mVertexBufferSize * sizeof(VertexProp);
mVertexBufferViews[1].StrideInBytes = sizeof(VertexProp); mDevice->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(mIndexBufferSize * sizeof(UINT)),
D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&mIndexBufferGPU))); mIndexBufferView.BufferLocation = mIndexBufferGPU->GetGPUVirtualAddress();
mIndexBufferView.Format = DXGI_FORMAT_R32_UINT;
mIndexBufferView.SizeInBytes = mIndexBufferSize * sizeof(UINT);

拷贝object数据时,需要记住全局buffer当前可写入位置的指针偏移,以及对应的偏移index,例如:

ThrowIfFailed(mPosVertexBufferGPU->Map(0, nullptr, &mPosVertexBufferData));
memcpy((BYTE *)mPosVertexBufferData + mPosVertexBufferOffset, data, byteWidth);
mPosVertexBufferGPU->Unmap(0, nullptr);
mBaseVertexLocation = mPosVertexBufferOffset / sizeof(VertexPos);
mPosVertexBufferOffset += byteWidth;

这些偏移量是用来后面绘制不同object时找到正确的读取数据位置用的。具体描述一个cube的vertex和index数据这里就不贴了。

如果为每个object单独创建一份顶点缓存和索引缓存的话,可以在创建的时候就把数据塞进去。这样只需要初始化一次,后面就只有读取的操作了,满足这样条件的buffer需要设置为D3D12_HEAP_TYPE_DEFAULT。为default buffer拷贝数据需要使用一个额外的upload buffer来完成:

ID3DBlob *bufferCPU = nullptr;
D3DCreateBlob(byteWidth, &bufferCPU);
CopyMemory(bufferCPU->GetBufferPointer(), data, byteWidth); mDevice->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(byteWidth),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr, IID_PPV_ARGS(uploadBuffer)); mDevice->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(byteWidth), D3D12_RESOURCE_STATE_COMMON,
nullptr, IID_PPV_ARGS(defaultBuffer)); mCommandAlloc->Reset();
mCommandList->Reset(mCommandAlloc.Get(), nullptr); mCommandList->ResourceBarrier(1,
&CD3DX12_RESOURCE_BARRIER::Transition(*defaultBuffer,
D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COPY_DEST));
D3D12_SUBRESOURCE_DATA subResourceData;
subResourceData.pData = data;
subResourceData.RowPitch = byteWidth;
subResourceData.SlicePitch = byteWidth;
UpdateSubresources<1>(mCommandList.Get(), *defaultBuffer, *uploadBuffer, 0, 0, 1, &subResourceData);
mCommandList->ResourceBarrier(1,
&CD3DX12_RESOURCE_BARRIER::Transition(*defaultBuffer,
D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_GENERIC_READ));
mCommandList->Close(); ID3D12CommandList *cmdList[] = { mCommandList.Get() };
mCommandQueue->ExecuteCommandLists(_countof(cmdList), cmdList);

至此,和顶点缓存索引缓存相关的操作就完成了。下面让我们回到最开始提到的shader,编写完shader之后还需要去编译它,这里我们选择在线编译的模式:

	UINT compileFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif
ID3DBlob *error = nullptr;
HRESULT hr;
hr = D3DCompileFromFile(srcFile.c_str(), nullptr,
D3D_COMPILE_STANDARD_FILE_INCLUDE, "VS", "vs_5_0",
compileFlags, 0, vs, &error);
error = nullptr;
hr = D3DCompileFromFile(srcFile.c_str(), nullptr,
D3D_COMPILE_STANDARD_FILE_INCLUDE, "PS", "ps_5_0",
compileFlags, 0, ps, &error);

得到编译后的shader之后,我们需要创建pipeline state object进行具体的绘制设置,这个object相当于一个大的集合体,包含了各种参数的设置:

	D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc;
ZeroMemory(&psoDesc, sizeof(D3D12_GRAPHICS_PIPELINE_STATE_DESC));
psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
psoDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
psoDesc.DSVFormat = mDepthStencilBufferFormat;
psoDesc.Flags = D3D12_PIPELINE_STATE_FLAG_NONE;
psoDesc.IBStripCutValue = D3D12_INDEX_BUFFER_STRIP_CUT_VALUE_DISABLED;
psoDesc.InputLayout = { mInputLayout.data(), mInputLayout.size() };
psoDesc.NodeMask = 0;
psoDesc.NumRenderTargets = 1;
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
psoDesc.pRootSignature = mRootSignature.Get();
CD3DX12_RASTERIZER_DESC rastDesc = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
rastDesc.FillMode = mFillMode;
rastDesc.CullMode = mCullMode;
psoDesc.RasterizerState = rastDesc;
psoDesc.RTVFormats[0] = mBackBufferFormat;
psoDesc.SampleDesc.Count = mEnableMsaa ? mMsaaCount : 1;
psoDesc.SampleDesc.Quality = mEnableMsaa ? mMsaaQuality - 1 : 0;
psoDesc.SampleMask = UINT_MAX;
psoDesc.PS = { ps->GetBufferPointer(), ps->GetBufferSize() };
psoDesc.VS = { vs->GetBufferPointer(), vs->GetBufferSize() }; mDevice->CreateGraphicsPipelineState(&psoDesc,
IID_PPV_ARGS(pipelineStateObject)));

最后终于进入到正式的绘制阶段,所谓绘制,简单来说就是设置好各种参数,准备好buffer,将顶点数据使用对应的shader最终输出到视口上,相比之前初始化的流程,这次需要多设置一些参数:

	ID3D12DescriptorHeap* descriptorHeaps[] = { mCbvHeap.Get() };
mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps);
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
mCommandList->SetGraphicsRootDescriptorTable(0,
mCbvHeap->GetGPUDescriptorHandleForHeapStart());
mCommandList->SetPipelineState(object->mPipelineState.Get());
mCommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

这里,通过调用SetGraphicsRootDescriptorTable,我们将const buffer和shader中的register(b0)关联起来。具体的绘制代码比较简单:

	mCommandList->IASetVertexBuffers(0, 2, mVertexBufferViews.data());
mCommandList->IASetIndexBuffer(&mIndexBufferView);
mCommandList->DrawIndexedInstanced(object->mIndexCount, 1,
object->mStartIndexLocation, object->mBaseVertexLocation, 0);

这里的StartIndexLocationBaseVertexLocation是用来告诉GPU从哪里读取索引缓存,如何读取顶点缓存(假如一个缓存中包含了多个object的数据)。最后的绘制效果如图:

如果你觉得我的文章有帮助,欢迎关注我的微信公众号(大龄社畜的游戏开发之路-

用DirectX12绘制一个Cube的更多相关文章

  1. 用DirectX 11绘制一个Cube

    之前一篇文章讲了如何初始化DirectX 11,现在在此基础上绘制一个Cube,总体可概括为以下几个步骤: 定义Cube顶点数据结构 创建Vertex Buffer和Index Buffer 编写应用 ...

  2. Unity3d修炼之路:用Mesh绘制一个Cube

    #pragma strict function Awake(){ var pMeshFilter : MeshFilter = gameObject.AddComponent(typeof(MeshF ...

  3. WebGL简易教程(七):绘制一个矩形体

    目录 1. 概述 2. 示例 2.1. 顶点索引绘制 2.2. MVP矩阵设置 2.2.1. 模型矩阵 2.2.2. 投影矩阵 2.2.3. 视图矩阵 2.2.4. MVP矩阵 3. 结果 4. 参考 ...

  4. [Modern OpenGL系列(三)]用OpenGL绘制一个三角形

    本文已同步发表在CSDN:http://blog.csdn.net/wenxin2011/article/details/51347008 在上一篇文章中已经介绍了OpenGL窗口的创建.本文接着说如 ...

  5. 使用canvas绘制一个时钟

    周末学习canvas的一些基础功能,顺带写了一个基础的时钟.现在加工一下,做的更好看一点,先放上效果图: 谈一些自己的理解: (1).要绘制一个新的样式(不想被其他样式影响,或者影响到其他样式),那么 ...

  6. iOS----自定义UIView,绘制一个UIView

    绘制一个UIVIew最灵活的方式就是由它自己完成绘制.实际上你不是绘制一个UIView,你只是子类化了UIView并赋予子类绘制自己的能力.当一个UIVIew需要执行绘图操作的时,drawRect:方 ...

  7. IOS 中openGL使用教程2(openGL ES 入门篇 | 绘制一个多边形)

    在上一篇我们学习了如何搭建IOS下openGL的开发环境,接下来我们来学习如何绘制一个多边形. 在2.0之前,es的渲染采用的是固定管线,何为固定管线,就是一套固定的模板流程,局部坐标变换 -> ...

  8. HTML5 在canvas绘制一个矩形

    笔者:本笃庆军 原文地址:http://blog.csdn.net/qingdujun/article/details/32930501 一.绘制矩形 canvas使用原点(0,0)在左上角的坐标系统 ...

  9. 绘制一个绿色矩形平面((50, 50)->(350, 350))

    //VS2008+opencv2.4 //绘制一个绿色矩形平面 #include "stdafx.h" #include "highgui.h" #includ ...

随机推荐

  1. pdfFactory如何设置限制打印和浏览文档权限

    当我们进行私密文件的分享时,除了要设置密码保护文件内容外,还要注意设置打印限制,防止他人利用打印的方式,进行纸质文件的传播. 在使用pdfFactory安全策略时,我们可以通过设定禁止打印的方式,完全 ...

  2. Sound Forge常规功能详解

    Sound Forge是一款有口皆碑的音频编辑软件,专为录音.母带处理和音频编辑开发.但是该如何使用Sound Forge呢,Sound Forge经常用到的功能有哪些呢?今天小编通过该文章给大家进行 ...

  3. CorelDRAW多个文件如何批量导出JPG

    好多同学对于CorelDRAW 2018批量导出图片格式的操作不太了解.这种情况比较常见,比如设计了一本画册,在同一个文档中页面比较多,如果一页一页导出那将是一项巨大的工程,这时候我们就会想到CDR的 ...

  4. Guitar Pro吉他指弹入门——美式指弹

    说起指弹吉他,很多身边的琴友首先反应到的是押尾桑,岸部真明,伍伍慧等等指弹艺术家的日式指弹.笔者在初涉指弹的时候,也是如此,但是随着学习的加深,首先认识到了汤米大神(Tommy Emmanuel),然 ...

  5. 关于GoldWave为Vegas制作音频交叉淡化特效的教程分享

    在Vegas里对音频交叉淡化的处理,是通过将两段音频交叠.调整交叠部分的音量.选取交叉淡化类型这三步来实现的,许多步骤是在音频轨道拖动音量线来实现的,操作上不够灵敏精细.其实,单就音频的交叉淡化处理, ...

  6. css3系列之linear-gradient() repeating-linear-gradient() 和 radial-gradient() repeating-radial-gradient()

    linear-gradient()  (线性渐变) repeating-linear-gradient()   (重复的线性渐变) radial-gradient()  (镜像渐变) repeatin ...

  7. github搭建html网站到外网

    最近想自己弄个网站,但又没有服务器可以用,只好借用强大得github来帮忙了,不过GitHub确实有这个功能. 感谢以下大佬得教程,非常得详细,但我觉得还是有必要记录下来. 大佬链接: https:/ ...

  8. Java —— for while do...while循环(1)

    //for循环 for(初始化语句 ;循环条件; 迭代语句){ 循环体; } //while循环 初始化语句; while(循环条件){ 循环体; 迭代语句; } //do...while循环 初始化 ...

  9. C语言讲义——dll调用

    DLL:Dynamic Link Library,动态链接库.一个应用程序可使用多个DLL文件,一个DLL文件也可以被不同的应用程序使用. 先新建一个dll项目 再创建C项目进行调用 #include ...

  10. Java基础教程——多线程:创建线程

    多线程 进程 每一个应用程序在运行时,都会产生至少一个进程(process). 进程是操作系统进行"资源分配和调度"的独立单位. Windows系统的"任务管理器&quo ...