DirectX12 之HelloWorld
如果用DX12书写一个最基础的图形程序,一个仅包含几何体顶点位置和顶点色的Box的程序会是怎样的呢?这个程序是DX12龙书第六章的案例。同时它也常常被图形界称为DX12的HelloWorld。接下来我们仔细分析下这段程序。
一、头文件和命名空间
一篇代码的头文件和命名空间重要性不言而喻,这里定义了3个头文件和3个命名空间。
1.定义头文件
#include "../../Common/d3dApp.h"
#include "../../Common/MathHelper.h"
//上传缓冲区类(将缓冲区放入生成的上传堆中,此案例用以生成常量缓冲区)
#include "../../Common/UploadBuffer.h"
这三个头文件都非常重要,D3DApp类是初始化D3D和APP主窗口的父类,MathHelper是数学工具类,UploadBufer类则是上传缓冲区工具类,辅助创建各类缓冲区。
2.使用的命名空间
using Microsoft::WRL::ComPtr; //COM对象的智能指针类
using namespace DirectX; //DirectXMath.h文件代码存于DirectX命名空间中
using namespace DirectX::PackedVector;
其中的COM对象可视为一种接口,而ComPtr是一种智能指针。
用法如下:
ComPtr < A > a; 定义一个ComPtr智能指针
&a是A** 类型,并增加引用,写入用
a.Get()是得到A*
a.GetAddressOf()是得到A**,只读,不改引用
a.GetAddressOfAndRelease()是得到A**并减少引用
二、定义结构体、常量数据、工具类
1. 定义顶点属性结构体Struct Vertex,包含了顶点模型坐标(Pos)和顶点色(Color)
struct Vertex //顶点属性结构体(单个顶点)
struct Vertex //顶点属性结构体(单个顶点)
{
XMFLOAT3 Pos; //顶点的模型坐标
XMFLOAT4 Color; //顶点色
};
2. 单个几何体绘制所使用的常量数据Struct ObjectConstants,包含模型空间转换至裁剪空间的变换矩阵。
struct ObjectConstants //常量结构体(单个几何体绘制所用的常量数据)
{
XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4(); //MVP矩阵(初始化为一个单位矩阵)
};
3. D3D应用程序类(BoxApp类)
这个类主要做4件事,分别是:
创建APP主窗口(InitWindowsApp函数)
运行程序消息循环(Run函数)
处理窗口消息(WndProc函数)
Direct3D初始化
接下来逐一分析:
(1) Public函数
① 构造函数
BoxApp(HINSTANCE hInstance);//构造函数,参数为当前应用程序实例句柄(这里是为了只引用当前应用程序,防止多个应用程序并行出错)
BoxApp(const BoxApp& rhs) = delete;
② 析构函数
~BoxApp(); //析构函数用于释放D3DAPP中所用的COM接口对象并刷新命令队列
下列代码是析构函数的实现:
If(md3dDevice != nullptr)
FlushCommandQueue();
由此我们可知,析构函数其实是刷新了命令队列,原因是:在销毁GPU引用的资源以前,必须等待GPU处理完队列中的所有命令,否则可能造成应用程序在退出时崩溃(FlushCommandQueue函数在d3dUtil.h中定义,作用是保持CPU和GPU的同步运行)。
③ Initialize函数
virtual bool Initialize() override; //初始化APP主窗口和D3D
(2) Private函数
① 虚函数(依据子类情况重写)
1)OnResize函数
2)Update函数
3)Draw函数
4)OnMouseDown函数
5)OnMouseUp函数
6)OnMouseMove函数
virtual void OnResize() override; //调整后台缓冲区和深度模板缓冲区大小
virtual void Update(const GameTimer& gt) override; //每帧更新常量缓冲区的数据
virtual void Draw(const GameTimer& gt) override; //将几何体的当前帧图像绘制到后台缓冲区中,并显示在屏幕上
virtual void OnMouseDown(WPARAM btnState, int x, int y) override; //鼠标按下后所引起的空间变化
virtual void OnMouseUp(WPARAM btnState, int x, int y) override; //鼠标抬起后所引起的空间变化
virtual void OnMouseMove(WPARAM btnState, int x, int y) override; //鼠标移动所引起的空间变化
① 初始化D3D时,执行的函数
这些函数分别对描述符堆、常量缓冲区、根签名、着色器编译、顶点输入布局描述、几何体属性数据流、流水线状态对象进行构建。这些函数在此申明,之后他们将会在Initialize函数里执行。
/*以下6个函数为初始化D3D时执行的函数*/
void BuildDescriptorHeaps();//构建描述符堆
void BuildConstantBuffers();//构建常量缓冲区(实际为构建常量缓冲区描述符)
void BuildRootSignature(); //构建根签名
void BuildShadersAndInputLayout(); //着色器编译和顶点输入布局描述
void BuildBoxGeometry();//构建几何体(描述如何将顶点和索引数据传至GPU缓冲区)
void BuildPSO(); //构建流水线状态对象
(3) Private接口和变量
这些COM接口指针和变量在后面均会用到,变量和接口的意义请参看代码注释。
ComPtr<ID3D12RootSignature> mRootSignature = nullptr;//初始化根签名的COM接口指针
ComPtr<ID3D12DescriptorHeap> mCbvHeap = nullptr;//初始化常量缓冲区描述符堆的COM接口指针
std::unique_ptr<UploadBuffer<ObjectConstants>> mObjectCB = nullptr; //初始化指向UploadBuffer类的智能指针
std::unique_ptr<MeshGeometry> mBoxGeo = nullptr;//初始化指向MeshGeometry结构体的智能指针
ComPtr<ID3DBlob> mvsByteCode = nullptr; //初始化指向ID3DBlob类型的内存地址的指针(内存用于存放顶点着色器编译后的字节码)
ComPtr<ID3DBlob> mpsByteCode = nullptr; //初始化指向ID3DBlob类型的内存地址的指针(内存用于存放像素着色器编译后的字节码)
std::vector<D3D12_INPUT_ELEMENT_DESC> mInputLayout = nullptr; //初始化结构体容器(容器内为顶点输入布局数据的数组)
ComPtr<ID3D12PipelineState> mPSO = nullptr; //初始化PSO的COM接口指针
XMFLOAT4X4 mWorld = MathHelper::Identity4x4(); //定义模型转世界的变换矩阵
XMFLOAT4X4 mView = MathHelper::Identity4x4(); //定义世界转观察的变换矩阵
XMFLOAT4X4 mProj = MathHelper::Identity4x4(); //定义观察转裁剪的变换矩阵
float mTheta = 1.5f * XM_PI;//定义摄像机在X轴上的旋转弧度(球坐标中为X轴观察角度)
float mPhi = XM_PIDIV4; //定义摄像机在Y轴上的旋转弧度(XM_PIDIV4为 Pi/4)(球坐标中为Y轴观察角度)float mRadius = 5.0f; //定义摄像机可见范围半径(球坐标中即为球面半径)
POINT mLastMousePos; //定义鼠标的屏幕坐标点(POINT数据类型是含有X和Y两个浮点值的结构体)
三、Windows主函数
1.WinMain函数
主函数,类似C++中的Main函数。它配置了调试功能,并判断了D3D和App窗口是否初始化成功而决定是否运行消息循环函数(Run函数)和窗口过程函数(WndProc函数)
int WINAPI WinMain(HINSTANCE hInstance, //所引用的应用程序实例句柄
HINSTANCE prevInstance, //WIN32程序用不到,这里设置为0
PSTR cmdLine, //命令行参数字符串
int showCmd) //应用程序窗口如何显示(枚举值)
{
/*针对调试版本开启运行时内存检测*/
#if defined(DEBUG) | defined(_DEBUG)
_crtsetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
#endif
try //执行
{
BoxApp theApp(hInstance);
if(!theApp.Initialize()) //初始化不成功,则返回0
return 0;
return theApp.Run(); //初始化成功,则运行Run函数,即消息循环(Run函数在D3DApp类中定义)
}
catch(DxException& e) //捕获异常(DxException& e为异常数据类型)
{
//如果返回的HRESULT是个错误值,则抛出异常,通过MessageBox函数输出相关信息,并退出程序,返回0
//消息框函数,参数1:消息框所属窗口句柄,可为nullptr。参数2:消息框的显示文本信息。参数3:标题文本。参数4:消息框样式。
MessageBox(nullptr, e.ToString().c_str(), L"HR Failed", MB_OK);
return 0;
}
}
(1)Initialize函数
此函数作用是初始化D3D,初始化应用程序主窗口。初始化的大部分工作在D3DApp类中已经完成,在此不展开详细说明,可以查阅D3DApp类中的实现。子类中主要是加入了“分别对描述符堆、常量缓冲区、根签名、着色器编译、顶点输入布局描述、几何体构建、流水线状态对象进行构建函数的执行”。
(2)Run函数(消息循环)
从消息队列中(Windows发送给App窗口的消息存于一个队列中)检索消息,再将消息分派给相应的窗口过程,在获取WM_QUIT消息之前,该函数会一直保持循环。而当没有消息分派给窗口过程的时候,Run函数会执行游戏逻辑,在此案例为绘制几何体,Update函数和Dwaw函数均在此函数中调用。
(3)WndProc函数(窗口过程)
我们在窗口中编写的代码是针对窗口接收到的消息而进行相应的处理。此函数是个回调函数,Windows系统会在需要处理消息的时候自动调用此窗口过程,所以在代码中,我们没有显式地调用过这个窗口过程函数。
四、继承工具类的构造和析构
构造函数和析构函数完全继承父类。
1.构造函数BoxApp类的继承
BoxApp::BoxApp(HINSTANCE hInstance) : D3DApp(hInstance)
{
//继承父类构造函数
}
2.BoxApp类的析构函数
BoxApp::~BoxApp()
{
//使用父类析构函数
}
五、BoxApp类中虚函数的实现
1. Initialize函数
此函数作用是初始化D3D,初始化应用程序主窗口。初始化的大部分工作在D3DApp类中已经完成,在此不展开详细说明,可以查阅D3DApp类中的实现。子类中主要是加入了“分别对描述符堆、常量缓冲区、根签名、着色器编译、顶点输入布局描述、几何体构建、流水线状态对象进行构建函数的执行”。
函数执行逻辑大致分为以下几步:
(1)重置命令列表(mCommandList->Reset())
//重置命令列表,为执行初始化命令做好准备工作
ThrowIfFailed(mCommandList->Rest(mDirectCmdListAlloc.Get(), //命令列表里的命令实际存放在命令分配器上(命令分配器在D3DApp中定义)
nullptr)); //流水线初始状态(因为是初始化,所以不用把mPSO.Get()传入,对于打包技术来说,可以设置为nullptr)
每次加入命令前必须重置命令列表,以清空内存,但又可以复用其内存空间。注意:在执行命令列表初始化之前还执行了父类的Initialize函数,确保初始化通过。(父类的Initialize函数实现可查阅D3DApp类实现)
if(!D3DApp.Initialize()) //首先执行父类的初始化函数
return false;
(2)执行初始化函数(为执行初始化命令做好准备)
//下列这些函数均需要在初始化时执行,以将各种命令设置到命令列表上
BuildDescriptorHeaps();
BuildConstantBuffers();
BuildRootSignature();
BuildShadersAndInputLayout();
BuildBoxGeometry();
BuildPSO();