DirectX12 之HelloWorld

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();

我们分别对上面6个函数做说明:
1 描述符堆可以看成是存放描述符的数组,而描述符则是一个资源的“身份”。
2 常量缓冲区描述符是依托描述符堆而创建的。
3 根签名则是将描述符绑定到流水线哪个阶段的一种“说明方式”。
4 编译着色器代码是因为要把HLSL代码编译成硬件可读的“中间代码”:字节码。顶点输入布局描述是描述一个几何体的顶点构成方式。
5 BoxGeometry是描述如何将几何体顶点索引数据从CPU系统内存拷贝到GPU缓冲区中的。
6 PSO是准备好欲绑定至流水线上的各种资源对象。
上述的这些操作其实是将众多包裹了命令的命令分配器传入了命令列表,以待后续再传入命令队列。

(3)执行初始化命令

真正的执行初始化其实是ExcuteCommandList函数,它将命令列表中的命令传入了命令队列,GPU才可以对他们进行计算处理。

① 关闭命令列表(mCommandList->Close)

当命令全都传入命令列表后,结束命令的记录,以便之后再将其添加到命令队列。

ThrowIfFailed(mCommandList->close());   //结束命令的记录(在将命令列表传入命令队列前必须关闭命令列表)
② 将命令列表里的命令添加至命令队列(mCommandQueue -> ExcuteCommandList)

GPU维护着至少一组命令队列,CPU会通过命令列表将命令传送至命令队列来让GPU处理。

ID3D12CommandList* cmdLists[] = { mCommandList.get() };//定义命令列表数组
    //将命令列表里的命令添加至命令队列中
mCommandQueue->ExcuteCommandLists(_countof(cmdLists), //命令列表数组元素个数
	cmdLists);  //命令列表数组首地址

(4)等待初始化完成(FlushCommandQueue)

//用围栏刷新命令队列,实现CPU与GPU的同步(此函数在D3DApp类中实现)
    FlushCommandQueue();

此方法为:强制CPU等待,直到GPU完成所有命令的处理,达到某个指定的围栏点(fence point)为止。详细请参看D3DApp类中实现。

2.OnResize函数(WndProc调用)

OnResize函数可以调整后台缓冲区和深度模板缓冲区大小。它被WndProc函数调用,而WndProc函数被WinMain函数调用。
函数执行逻辑大致分为以下几步:

(1)执行父类的OnResize函数

D3DApp::OnResize(); //执行父类OnResize函数

(2)更新纵横比并重新计算投影矩阵

//XMMatrixPerspectiveFovH函数为构建透视投影矩阵函数
 XMMATRIX P = XMMatrixPerspectiveFovH(0.25f * MathHelper::Pi, //参数1:用弧度表示垂直视场角,这里为PI/4,即45°
        AspectRatio(), //参数2:纵横比
        1.0f,   //参数3:近平面距离
        1000.0f);   //参数4:远平面距离

使用XMMatrixPerspectiveFovLH函数构建投影矩阵。这里的纵横比参数由OnMouseMove函数计算而来。

(3)矩阵数据转换

//将XMMATRIX类型数据转换成XMFLOAT4X4类型,即将XMMATRIX P转换成先前定义的XMFLOAT4X4 mProj = MathHelper::Identity4x4();
    XMStoreFloat4x4(&mProj, P);

3.Update函数(Run函数调用)

每帧更新常量缓冲区数据(将每帧改变的矩阵数据从CPU系统内存复制到GPU常量缓冲区中)。
函数执行逻辑大致分为以下几步:

(1)构建新的观察矩阵

void BoxApp::Update(const GameTimer& gt)    //每帧更新常量缓冲区数据,参数为GameTimer类实例指针(GameTimer类在D3DApp中引入)
{
    //将球坐标转换为笛卡尔坐标
    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();   //观察点的世界坐标,此处置0
    XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);  //摄像机的向上向量,此处为垂直向上的向量
    //构建观察矩阵函数XMMatrixLookAtLH,传入上述元素作为参数
    XMMATRIX view = XMMatrixLookAtLH(pos, target, up);
    XMStoreFloat4x4(&mView, view);  //转换至XMFLOAT4X4数据类型的观察矩阵

(2)更新最终的WorldViewProj矩阵

//更新worldViewProj矩阵
    XMMATRIX world = XMLoadFloat4x4(&mWorld);   //得到XMMATRIX数据类型的模型转世界矩阵
    XMMATRIX proj = XMLoadFloat4x4(&mProj);     //得到XMMATRIX数据类型的观察转裁剪矩阵
    XMMATRIX worldViewProj = world * view * proj;   //得到XMMATRIX数据类型的模型转裁剪矩阵

注意:这里得到的是XMMATRIX类型的矩阵。

(3)用最新的WorldViewProj矩阵更新常量缓冲区中数据

//用最新的worldViewProj矩阵来更新常量缓冲区
    /*struct ObjectConstants  //常量结构体(单个几何体绘制所用的常量数据)
    {
        XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4();   //MVP矩阵(初始化为一个单位矩阵)
    };*/
    ObjectConstants objConstants;   //常量缓冲区结构体实例,如上
    //将worldViewProj矩阵数据类型转换成XMFLOAT4X4
    XMStoreFloat4x4(&objConstants.WorldViewProj, //参数1:拿到结构体上的XMFLOAT4X4数据
        XMMatrixTranspose(worldViewProj));  //参数2:XMMATRIX数据(此处做了转置,以符合DX的矩阵右乘规则)
    //将数据从系统内存(CPU内存)复制到常量缓冲区(GPU显存)
    mObjectCB->CopyData(0, //参数1:常量缓冲区数组索引
        objConstants);  //参数2:数据在内存上的地址(即为结构体名)

CopyData函数将每帧改变的矩阵数据从CPU系统内存复制到了GPU常量缓冲区中。而CopyData函数是在UploadBuffer类中定义,也就是我们引用的头文件。mObjectCB是指向UploadBuffer类的指针,后面还将多次引用到。

4.Draw函数(Run函数调用)

将当前帧画面绘制到后台缓冲区,并最终显示在屏幕上。
函数执行逻辑大致分为以下几步:

(1)重置命令分配器和命令列表

命令列表获取命令前必须重置,而命令分配器作为命令的实际载体也需要重置。
重置命令分配器函数:mDirectCmdListAlloc->Reset();
重置命令列表函数:mCommandList->Reset();

//绘制前先要重置命令分配器和命令列表,但前提是要保证GPU已经执行完命令分配器中的所有命令,此案例是刷新命令列表来保证同步的
ThrowIfFailed(mDirectCmdListAlloc->Reset());//重置命令分配器(复用其内存)
ThrowIfFailed(mCommandList->Reset(mDirectCmdListAlloc.Get(), mPSO.Get()));  //重置命令列表(复用其内存)

(2)设置视口和裁剪矩形(此命令加入命令列表)

当我们需要把3D场景绘制到后台缓冲区的某个矩形子区域(视口)当中,就需要设置视口。
当我们在后台缓冲区中自定义一块矩形遮罩影响像素的剔除,就需要设置裁剪矩形。

//设置视口和裁剪矩形
    mCommandList->RSSetViewports(1, &mScreenViewport);  //设置视口。参数1:绑定的视口数量   参数2:D3D12_VIEWPORT结构体指针,结构体在D3DApp中申明,d3dUtil中实现
    mCommandList->RSSetScissorRects(1, &mScissorRect);  //设置裁剪矩形。参数1:绑定的裁剪矩形数量   参数2:D3D12_RECT结构体指针,结构体在D3DApp中申明,d3dUtil中实现

注意:此时第二个参数是结构体指针,并非COM接口指针,此指针在D3DApp中申明,d3dUtil中实现。

(3)第一次资源状态转换(此命令加入命令列表)

为了防止资源冒险,需将资源打上“标签”,于是就有了资源状态转换。将后台缓冲区资源从呈现状态转换至渲染目标状态(准备好接收图像渲染信息的缓冲区)
核心函数:
ResourceBarrier(); 即资源屏障函数

//按照资源的用途指示其状态的转变,此处将资源从呈现状态转换为渲染目标状态
mCommandList->ResourceBarrier(1,    //资源屏障函数
    &CD3DX12_RESOURCE_BARRIER::Transition(  //结构体下的Transition函数,资源转换的核心函数
        CurrentBackBuffer(),    //返回ID3D12Resource*类型,这里即指向后台缓冲区的指针(函数于D3DApp里实现)
        D3D12_RESOURCE_STATE_PRESENT,   //初始状态:呈现
        D3D12_RESOURCE_STATE_RENDER_TARGET));   //转换后状态:渲染目标,即RenderTaget

(4)清除后台缓冲区和深度模板缓冲区(此命令加入命令列表)

将后台缓冲区和深度模板缓冲区清空,为接收新的图像渲染信息做准备。
核心函数:
ClearRenderTargetView();
ClearDepthStencilView();

//清除后台缓冲区和深度缓冲区
mCommandList->ClearRenderTargetView(CurrentBackBufferView(), //参数1:RTV描述符句柄,即指向RTV数组的指针(D3DApp中实现)
    Colors::LightSteelBlue, //参数2:为渲染目标填充的颜色
    0, 
    nullptr);   //参数4:清除整个渲染目标
mCommandList->ClearDepthStencilView(DepthStencilView(), //参数1:深度模板描述符句柄(D3DApp中实现)
    D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL,  //同时清除深度和模板
    1.0f,   //清除深度缓冲区的值
    0,  //清除模板缓冲区的值
    0,  //pRects数组元素数量,可设为0
    nullptr);   //清除整个渲染目标

(5)将一系列渲染配置命令加入命令列表

① 指定希望绑定到渲染流水线上的RTV和DSV

核心函数:
mCommandList->OMSetRenderTargets();

//指定希望绑定到渲染流水线上的RTV和DSV
mCommandList->OMSetRenderTargets(1,     //参数1:欲绑定的RTV的数量
    &CurrentBackBufferView(),   //参数2:RTV描述符句柄的地址,即指向RTV数组的指针(D3DApp中实现)
    true,   //参数3:RTV对象在描述符中是连续存放的
    &DepthStencilView());   //参数4:深度模板描述符句柄的地址,即指向DSV的指针(D3DApp中实现)

② 设置描述符堆和根签名

核心函数:
mCommandList->SetDescriptorHeaps(); mCommandList->SetGraphicsRootSignature();

ID3D12DescriptorHeap* descriptorHeaps[] = { mCbvHeap.Get() };   //得到CBV描述符堆数组(mCbvHeap.Get()只读不增加引用)
    //将设置CBV描述符堆和设置根签名加入命令列表
    mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), //参数1:CBV描述符堆数组元素个数
        descriptorHeaps);   //参数2:CBV描述符堆数组首地址(即CBV描述符堆数组指针)
    mCommandList->SetGraphicsRootSignature(mRootSignature.Get());   //参数:根签名COM接口指针,即ID3D12ROOTSIGNATURE*类型数据
③ 将顶点缓冲区描述符,索引缓冲区描述符,图元拓扑类型,绑定到输入装配阶段,即IA阶段

核心函数:
IASetVertexBuffers();
IASetIndexBuffer();
IASetPrimitiveTopology();

//分别将顶点缓冲区描述符,索引缓冲区描述符,图元拓扑类型,绑定到输入装配阶段(IA阶段)
mCommandList->IASetVertexBuffers(0, //参数1:输入槽索引
    1, //参数2:描述符数量
    &mBoxGeo->VertexBufferView());  //参数3:返回顶点缓冲区描述符(VertexBufferView函数在MeshGeometry结构体中实现)
mCommandList->IASetIndexBuffer(&mBoxGeo->IndexBufferView());    //参数:返回顶点索引缓冲区描述符(IndexBufferView函数在MeshGeometry结构体中实现)
mCommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRANGLELIST);   //参数:图元拓扑类型指定为三角形列表
④ 绑定Cbv表至渲染流水线

Cbv表是根参数其中一种类型,其实质是多个几何体绘制所用到的Cbv数组。
核心函数:
SetGraphicsRootDescriptorTable();

//设置CBV表到命令列表中
mCommandList->SetGraphicsRootDescriptorTable(0, //参数1:欲绑定的寄存器槽号
    mCbvHeap->GetGPUDescriptorHandleForHeapStart());    //获得堆中的首个CBV描述符句柄

(6)绘制几何体

使用索引缓冲区里的索引排序来绘制几何体
核心函数:
DrawIndexedInstanceed();
如果使用顶点排序来绘制几何体,则使用DrawVerteiceInstanced函数

//绘制单个“Box”几何体(DrawArgs容器定义的是多个几何体中的子网格几何体)
//DrawIndexedInstanced函数是按顶点索引绘制几何体
mCommandList->DrawIndexedInstanced(mBoxGeo->DrawArgs["box"].IndexCount, //参数1:单个几何体的索引数量
        1,  //几何体实例化数
        0,  //第一个索引
        0,  //第一个索引在全局索引中的位置
        0);

(7)第二次资源状态转换

将渲染好的后台缓冲区从渲染目标状态转换到呈现状态,即为交换前后台缓冲区做好准备。代码中的注释参见第一次资源状态转换。
核心函数:
mCommandList->ResourceBarrier();

//资源转换,从渲染目标状态转换到呈现状态(参数详见之前的转换)
    mCommandList->ResourceBarrier(1, 
        &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));

(8)关闭命令列表

完成命令的记录后并且在添加命令至命令队列前,需关闭命令列表。
核心函数:
mCommandList->Close();

//完成命令的记录,关闭命令列表
    ThrowIfFailed(mCommandList->Close());

(9)命令列表添加至命令队列

将命令列表中的命令添加至命令队列,让GPU计算。之前添加到命令列表的命令都会加入命令列表数组(cmdsLists[])中,再通过ExcuteCommandLists函数将其添加至命令队列。
核心函数:
mCommandQueue->ExcuteCommandLists();

//向命令队列添加欲执行的命令列表
ID3D12CommandList* cmdsLists[] = { mCommandList.Get() }; //命令列表数组
mCommandQueue->ExcuteCommandLists(_countof(cmdsLists),  //参数1:命令列表数组元素个数
 	cmdsLists);     //参数2:命令列表数组指针

(10)交换前后台缓冲区

根据第二次资源状态转换的状态信息,将后台缓冲区图像转到前台缓冲区呈现,实质是交换了两个缓冲区的指针。
核心函数:
mSwapChain->Present();
mSwapChain是交换链接口指针,D3DApp类中定义。

//交换前后台缓冲区
ThrowIfFailed(mSwapChain->Present(0, 0));   //交换前后台缓冲区指针(即将后台缓冲区图像转到前台缓冲区显示)
mCurrBackBuffer = (mCurrBackBuffer + 1) % SwapChainBufferCount;     //改变后台缓冲区索引,0变1,1变0(mCurrBackBuffer在D3DApp类中定义)

(11)刷新命令队列(强制CPU GPU同步)

此方法为:强制CPU等待,直到GPU完成所有命令的处理,达到某个指定的围栏点(fence point)为止。详细请参看D3DApp类中实现。

//刷新命令队列(使CPU和GPU同步)
    FlushCommandQueue();    //利用围栏值刷新命令队列

5.OnMouseDown函数(Update函数调用,更新CB)

当鼠标按下时,记录鼠标的坐标。这个函数是配合OnMouseDown函数一起运算的。鼠标经历:按下—滑动—松开。这两个函数能记录这段鼠标轨迹的向量,并换算成摄像机旋转角度和可视半径。

//当鼠标按下时记录鼠标坐标
void OnMouseDown(WPARAM btnState, int x, int y) //参数1:虚拟键代码(左右键按下)     参数2:鼠标的屏幕X坐标      参数3:鼠标的屏幕Y坐标
{
    //鼠标按下的同时记录XY坐标
    mLastMousePos.x = x;
    mLastMousePos.y = y;
    setCapture(mhMainWnd);  //在属于当前线程的指定窗口里(mhMainWnd)设置鼠标捕获
}

6.OnMouseUp函数(Update函数调用,更新CB)

松开鼠标时释放鼠标捕获。注意:这时松开鼠标并没有记录坐标,只是释放捕获,但是在OnMouseMove函数中,有段松开鼠标的代码记录了坐标,在此不要搞混。

//当鼠标抬起时释放鼠标捕获
void OnMouseUp(WPARAM btnState, int x, int y)   
{
    ReleaseCapture();   //此时不需要继续获得鼠标消息,所以释放鼠标捕获
}

7.OnMouseMove函数(Update函数调用,更新CB)

鼠标左键按下并移动,实现摄像机旋转。鼠标右键按下并移动,实现摄像机远近推拉(可视半径的变化)。

(1)根据鼠标按下左键并移动的移动距离计算成旋转角度

核心函数:
XMConvertToRadians();
将长度转换为弧度

//按下鼠标左键不放并移动,可旋转摄像机
    if((btnState & MK_LBUTTON) != 0)    //如果按下鼠标左键为真(MK_LBUTTON为鼠标左键按下的虚拟键代码)
    {
        //此将鼠标移动的向量值转化为带方向的弧度值
        float dx = XMConvertToRadians(0.25f * static_cast<float>(x - mLastMousePos.x));
        float dy = XMConvertToRadians(0.25f * static_cast<float>(y - mLastMousePos.y));
        //更新旋转角度
        mTheta += dx;   //将X轴旋转变化弧度累加到当前帧
        mPhi += dy;     //将Y轴旋转变化弧度累加到当前帧
        //限制在Y轴上的旋转角度
        mPhi = MathHelper::Clamp(mPhi, 0.1f, MathHelper::pi - 0.1f);    //将Y轴旋转弧度约束在0°-180°(0.1为精度偏差阈值)
    }

(2)根据鼠标按下右键并移动的移动距离计算成可视半径(缩放)

//按下鼠标右键不放并移动,可推拉摄像机(改变摄像机的可视半径)
    else if((btnState & MK_RBUTTON) != 0)   //如果按下鼠标右键为真(MK_RBUTTON为鼠标右键按下的虚拟键代码)
    {
        //将鼠标移动的向量转化为摄像机推拉位移的向量
        float dx = 0.005f * static_cast<float>(x - mLastMousePos.x);
        float dy = 0.005f * static_cast<float>(y - mLastMousePos.y);
        //更新摄像机的可视半径
        mRadius += dx - dy; //移动轨迹越接近X轴或Y轴,可视半径变化越快,如果X轴和Y轴变化一样,则可视半径无变化
        //限制可视半径范围
        mRadius = MathHelper::Clamp(mRadius, 3.0f, 15.0f);  //将可视范围半径限制在[3, 15]
    }

(3)松开鼠标按键后,记录当前坐标

如果是什么都没按的情况下移动鼠标,则实时每帧更新鼠标坐标,这样能使鼠标空移一段距离后按下鼠标的那一刻,dx和dy都为0,即不产生旋转和缩放,而只有按着鼠标键移动,dx和dy才不会为零。

//如果是什么都没按的情况下移动鼠标,则实时每帧更新鼠标坐标(这样能使鼠标空移一段距离后按下鼠标的那刻,dx和dy都为0,即不产生旋转和缩放)
    mLastMousePos.x = x;    
    mLastMousePos.y = y;

六、类中用于初始化的函数实现

这些函数包括: BuildDescriptorHeaps();
BuildConstantBuffers();
BuildRootSignature();
BuildShadersAndInputLayOut();
BuildBoxGeometry();
BuildPSO();

1.BuildDescriptorHeaps函数

构建描述符堆,此案例中构建的是常量缓冲区描述符堆,即Cbv堆,可以看做是Cbv的数组。
此函数执行逻辑大致分为以下几步:

(1)定义D3D12_DESCRIPTOR_HEAP_DESC描述符堆属性结构体,并填充

CBV堆的类型是D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV,而CBV堆的Flag则是D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE
核心结构体:
D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;

void BoxApp::BuildDescriptorHeaps() //构建描述符堆(这里构建的是常量描述符堆)
{
    D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;     //描述符堆属性结构体
    cbvHeapDesc.NumDescriptors = 1;     //堆中描述符的数量
    cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;  //常量缓冲区描述符类型
    cbvHeapDesc.Flag = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; //常量缓冲区描述符特有的Flag
    CbvHeapDesc.NodeMask = 0;   //掩码值

(2)创建描述符堆

将结构体指针作为参数传入,创建描述符堆。IID_PPV_ARGS宏见代码注释。
核心函数:
md3dDevice->CreateDescriptorHeap();

	//创建描述符堆
    ThrowIfFaided(md3dDevice->CreateDescriptorHeap(&cbvHeapDesc,    //描述符堆属性结构体地址
        IID_PPV_ARGS(&mCbvHeap)));  //返回所构建的常量描述符堆,也可写作 IID_PPV_ARGS( mCbvHeap.GetAddressOf() ),同表示接口**类型

2.BuildConstantBuffers函数

构建常量缓冲区,这里实为构建常量缓冲区描述符,即CBV堆中元素:CBV。而缓冲区是在堆中的,所以我在这里暂时把缓冲区和堆看成是一个概念,从而把计算CBV在缓冲区中的地址转换成计算CBV在CBV堆中的地址,即数组元素的地址偏移。
此函数执行逻辑大致分为以下几步:

(1)计算CBV在CBV堆(常量缓冲区)中的地址

首先得到多个几何体所占用的常量缓冲区的首地址A,然后再通过单个几何体常量数据的字节大小和索引号,来偏移首地址A,最终得到单个几何体常量数据的地址。

	//make_unique只是把参数完美转发给要创建对象的构造函数,再从new出来的原生指针构造std::unique_ptr(可理解为定义了一个UploadBuffer类的智能指针)
    mObjectCB = std::make_unique<UploadBuffer<ObjectConstants>>(md3dDevice.Get(), 1, true);
    //计算单个物体所占的常量缓冲区大小
    UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(Sizeof(ObjectConstants));
    //得到常量缓冲区中的首地址
    D3D12_VIRTUAL_ADDRESS cbAddress = mObjectCB->Resource()->GetGPUVirtualAddress();//Resource函数返回mUploadBuffer接口指针
    //计算当前几何体在常量缓冲区中的地址(通过单个几何体所占的字节大小偏移地址)
    int boxCBufferIndex = 0;    //当前几何体索引
    cbAddress += boxCBufferIndex * objCBByteSize;//常量缓冲区地址 += 单个几何体索引 * 单个几何体字节大小

(2)定义并填充CBV属性结构体

将上面计算得到的地址赋值给结构体中的元素,为创建CBV做好准备。
核心结构体:
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;

	//常量缓冲区描述符属性结构体定义和填充
    D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;    //定义常量缓冲区描述符属性结构体
    cbvDesc.BufferLocation = cbAddress;     //填充CB地址
    cbvDesc.SizeInBytes = d3dUtil::CaclConstantBufferByteSize(Sizeof(ObjectConstants));//单个几何体所占常量缓冲区大小,其实就是objCBByteSize

(3)创建CBV(常量缓冲区描述符)

将结构体指针传入,创建CBV。
核心函数:
md3dDevice->CreateConstantBufferView();

	//创建常量缓冲区描述符
    md3dDevice->CreateConstantBufferView(&cbvDesc,  //常量缓冲区描述符属性结构体的实例地址
        mCbvHeap->GetCPUDescriptorHandleForHeapStart());    //CBV堆中的首个CBV句柄

3.BuildRootSignature

构建根签名。根签名由一组根参数组成,而根参数又由根常量、根描述符、描述符表组成。此案例中我们创建的是一个根参数仅为描述符表的根签名(其中的描述符表仅存有一个Cbv)。
此函数名应与Draw函数中的设置根签名(SetGraphicsRootSignature();)区分开,是不加“Graphics”的,那函数的其中一个参数就是这里创建的根签名。
此函数执行逻辑大致分为以下几步:

(1)根参数的定义和初始化

根签名由根参数组成,所以首先要定义根参数,而此案例的根参数是由描述符表构成,所以初始化顺序是:初始化描述符表–>初始化根参数。
核心函数: cbvTable.Init();
slotRootParameter[0].InitAsDescriptorTable();

	//根参数申明
    CD3DX12_ROOT_PARAMETER slotRootParameter[1];    //申明根参数数组(根参数可以是根常量,根描述符,或描述符表)
    //根参数初始化
    CD3DX12_DESCRIPTOR_RANGE cbvTable;  //申明描述符表
    cbvTable.Init(  //初始化描述符表
        D3D12_DESCRIPTOR_RANGE_TYPE_CBV,  //参数1:描述符表类型
        1,  //参数2:表中描述符数量
        0); //参数3:将这段描述符表绑定至此基准着色器寄存器(base shader register)
    slotRootParameter[0].InitAsDescriptorTable(1, //参数1:描述符表的数量
        &cbvTable); //参数2:指向描述符表的指针

(2)根签名属性结构体的定义和填充

将根参数传入填充结构体。
核心结构体:
CD3D12_ROOT_SIGNATURE_DESC rootSigDesc

	//根签名属性结构体定义和填充
    CD3D12_ROOT_SIGNATURE_DESC rootSigDesc(1, slotRootParameter, //传入根参数
        0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

(3)序列化根签名描述布局

传入根签名属性结构体指针,并将序列化后的根签名和错误信息存入Blob型内存(Blob内存可以存泛型数据,所以读取的时候需要转换数据类型)。
核心函数:D3D12SerializeRootSignature();

	//用单个寄存器槽来创建根签名,该槽位指向一个仅含有单个常量缓冲区的描述符表
    ComPtr<ID3DBlob> serializedRootSig = nullptr;
    ComPtr<ID3DBlob> errorBlob = nullptr;
    //将根签名的描述布局进行序列化处理
    HRESULT hr = D3D12SerializeRootSignature(&rootSigDesc,  //传入根签名结构体
        D3D_ROOT_SIGNATURE_VERSION_1, serializedRootSig.GetAddressOf(), errorBlob.GetAddressOf());

(4)创建根签名

创建根签名之前首先判断是否有错误,然后再创建。
核心函数:md3dDevice->CreateRootSignature();

	//输出错误信息
    if(errorBlob != nullptr)
    {
        ::OutputDebugStringA((char*)errorBlob->GetBufferPointer());
    }
    ThrowIfFailed(hr);
    //创建根签名
    ThrowIfFailed(md3dDevice->CreateRootSignature(0, 
        serializedRootSig->GetBufferPointer(),  //序列化后的根签名Blob内存指针
        serializeRootSig->GetBufferSize(),      //序列化后的根签名Blob内存大小
        IID_PPV_ARGS(&mRootSignature)));    //返回创建好的根签名

4.BuildShadersAndInputLayout函数

此函数主要两大作用,第一是着色器代码编译(运行中编译),第二是顶点输入布局描述。编译是为了生成硬件可读的中间代码:字节码,顶点输入布局描述是为了描述顶点结构体中元素的属性。这两块都会被作为流水线状态对象绑定到流水线上,所以在BuildPSO中会被调用。
此函数执行逻辑大致分为以下几步:

(1)编译顶点和像素着色器代码

核心函数:d3dUtil::ComileShader();

HRESULT hr = S_OK;  //操作成功
    //运行编译顶点着色器和像素着色器的HLSL代码
    mvsByteCode = d3dUtil::CompileShader(L"Shaders\\color.hlsl", nullptr, "VS", "vs_5_0");  //编译顶点着色器
    mpsByteCode = d3dUtil::CompileShader(L"Shaders\\color.hlsl", nullptr, "PS", "ps_5_0");  //编译像素着色器

(2)顶点输入布局描述

在顶点结构体中,我们当前定义了两个元素,Pos和Color,这个描述就是对他们的属性描述,具体属性参看下列代码注释。

//顶点输入布局描述
    mInputLayout = 
    {
        //元素意义分别是:语义、语义索引、数据格式、输入槽索引、元素地址偏移量(字节)、输入槽类、非实例化设为0
        { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
        { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
    };

5.BuildBoxGeometry函数

构建几何体的顶点和索引信息,继而将其拷贝至CPU系统内存,再将内存数据拷贝至GPU的顶点和索引缓冲区,最终构建出单个几何体的顶点和索引。此函数中我们将会用到MeshGeometry结构体中的诸多函数(MeshGeometry定义于d3dUtile.h头文件中)。
此函数执行逻辑大致分为以下几步:

(1)构建顶点数组

描述顶点结构体中的元素的数组(每个顶点的属性:顶点模型坐标和顶点色)。

//顶点数组(储存了顶点的模型坐标和顶点色)
std::array<Vertex, 8> vertices =
{
    vertex({ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::White) }),
    vertex({ XMFLOAT3(-1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Black) }),
    vertex({ XMFLOAT3(+1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Red) }),
    vertex({ XMFLOAT3(+1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::Green) }),
    vertex({ XMFLOAT3(-1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Blue) }),
   vertex({ XMFLOAT3(-1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Yellow) }),
   vertex({ XMFLOAT3(+1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Cyan) }),
   vertex({ XMFLOAT3(+1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Magenta) })
};

(2)构建索引列表数组

给几何体上的顶点定义索引,每个三角形的顶点索引按顺时针绕序(正面)排列所组成的索引数组。

    //索引数组(储存了每个三角形的顶点索引绕序)
    std::array<std::uint16_t, 36> indices =
    {
        //立方体前表面
        0, 1, 2,
        0, 2, 3,
        //立方体后表面
        4, 6, 5,
        4, 7, 6,
        //立方体左表面
        4, 5, 1,
        4, 1, 0,
        //立方体右表面
        3, 2, 6,
        3, 6, 7,
        //立方体上表面
        1, 5, 6,
        1, 6, 2,
        //立方体下表面
        4, 0, 3,
        4, 3, 7,
    };

(3)顶点和索引数据的传输

顶点和索引数据–>CPU系统内存–>GPU顶点和索引缓冲区。

① 将顶点数组和索引数组数据复制到CPU系统内存上

首先定义了若干变量,然后创建了系统内存,最后把数据复制到系统内存上。
核心函数:CopyMemory();

const UINT vbByteSize = (UINT)vertices.size() * sizeof(Vertex); //顶点数组大小 = 顶点数组元素个数 * 单个顶点大小
    const UINT ibByteSize = (UINT)indices.size() * sizeof(std::uint16_t);   //索引数组大小 = 索引数组元素个数 * 单个索引大小
    //make_unique只是把参数完美转发给要创建对象的构造函数,再从new出来的原生指针构造std::unique_ptr(可理解为定义了一个指向MeshGeometry结构体的智能指针)
    mBoxGeo = std::make_unique<MeshGeometry>();
    mBoxGeo->Name = "boxGeo";   //赋值Name变量
    //创建了系统内存副本(空内存)
    ThrowIfFailed(D3DCreateBlob(vbByteSize, &mBoxGeo->VertexBufferCPU));    //创建了系统内存副本(顶点缓存)
    //将顶点数据复制到内存中
    CopyMemory(mBoxGeo->VertexBufferCPU->GetBufferPointer(), //参数1:内存地址
        vertices.data(), //顶点数据
        vbByteSize);    //内存大小
    //创建了系统内存副本(空内存)
    ThrowIfFailed(D3DCreateBlob(ibByteSize, &mBoxGeo->IndexBufferCPU));
     //将索引数据复制到内存中
    CopyMemory(mBoxGeo->IndexBufferCPU->GetBufferPointer(), indices.data(), ibByteSize);
② 将顶点和索引数据从CPU系统内存复制到GPU顶点和索引缓冲区上

CreateDefaultBuffer函数可以将CPU上的资源复制到GPU的上传堆,再复制到默认堆中,这样即能保证每帧更新数据,又能保证最终顶点和索引缓存在默认堆中,从而优化了性能。详细可参看d3dUtil.cpp中实现。
mBoxGeo->VertexBufferUploader是上传堆资源,它在MeshGeometry结构体中定义,包含了传入上传堆的顶点和索引缓冲区信息。
核心函数:CreateDefaultBuffer();

//创建顶点缓冲区
    mBoxGeo->VertexBufferGPU = d3dUtil::CreateDefaultBuffer(    //CreateDefaultBuffer函数可以将CPU资源复制到GPU的上传堆再复制到默认堆
        md3dDevice.Get(), mCommandList.Get(), vertices.data(), vbByteSize, mBoxGeo->VertexBufferUploader);  //VertexBufferUploader是上传堆资源
    //创建索引缓冲区(方式同上)
    mBoxGeo->IndexBufferGPU = d3dUtil::CreateDefaultBuffer(
        md3dDevice.Get(), mCommandList.Get(), indices.data(), ibByteSize, mBoxGeo->IndexBufferUploader);
③ 构建单个几何体的顶点和索引

首先赋值了若干计算顶点和索引缓冲大小的变量,然后根据单个几何体的顶点索引数据在全局缓冲区中的位置,来构建单个几何体。
核心结构体:SubmeshGeometry submesh;
核心函数:mBoxGeo->DrawArgs[“box”]

	//顶点和索引若干变量赋值
    mBoxGeo->VertexByteStride = sizeof(Vertex);     //单个顶点大小
    mBoxGeo->VertexBufferByteSize = vbByteSize;     //顶点数组大小
    mBoxGeo->IndexFormat = DXGI_FORMAT_R16_UINT;    //索引数据格式
    mBoxGeo->IndexBufferByteSize = ibByteSize;      //索引数组大小
    //定义了单个几何体属性结构体(包括索引数量,顶点索引的起始索引,起始索引在全局索引的位置)
    SubmeshGeometry submesh;
    submesh.IndexCount = (UINT)indices.size();  //索引数量
    submesh.StartIndexLocation = 0;     //顶点索引的起始索引
    submesh.BaseVertexLocation = 0;     //起始索引在全局索引的位置
    //创建单个几何体的容器(将上述结构体放入DrawArgs容器,在Draw函数中,可以直接引用结构体中元素:mBoxGeo->DrawArgs["box"].IndexCount)
    mBoxGeo->DrawArgs["box"] = submesh; 
}

6.BuildPSO函数(Draw重置命令列表时调用)

构建PSO。PSO(流水线状态对象)包含了大量流水线状态信息,此函数主要是构建了这些对象,为了将这些对象送入渲染流水线而做好准备。
此函数执行逻辑大致分为以下几步:

(1)定义并填充PSO属性结构体

其中的光栅器状态、混合状态、深度模板状态、RTV格式、DSV格式、多重采样数量、多重采样质量,均在D3DApp.h中申明,d3dUtil.cpp中实现。
核心结构体:D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc;

D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc; //申明PSO属性结构体
ZeroMemory(&psoDesc, sizeof(D3D12_GRAPHICS_PIPELINE_STATE_DESC));   //将PSO结构体内存清零
//填充结构体
psoDesc.InputLayout = { mInputLayout.Data(), (UINT)mInputLayout.Size() };   //顶点输入布局描述
psoDesc.RootSignature = mRootSignature.Get();   //根签名
psoDesc.VS = { reinterpret_cast<BYTE*>(mvsByteCode->GetBufferPointer()), 	mvsByteCode->GetBufferSize() };    //顶点着色器字节码
psoDesc.PS = { reinterpret_cast<BYTE*>(mpsByteCode->GetBufferPointer()), 	mpsByteCode->GetBufferSize() };    //像素着色器字节码
psoDesc.RasterizerState = CD3D12_RASTERIZER_DESC(D3D12_DEFAULT);//光栅器状态(使用默认值)
psoDesc.BlendState = CD3D12_BLEND_DESC(D3D12_DEFAULT);  //混合状态(使用默认值)
psoDesc.DepthStencilState = CD3D12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);  //深度模板状态(使用默认值)
psoDesc.SampleMask = UINT_MAX;  //对所有采样点进行采样
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;//图元拓扑类型为三角形列表
psoDesc.NumRenderTagets = 1;    //渲染目标数量
psoDesc.RTVFormats[0] = mBackBufferFormat;  //RTV格式:后台缓冲区
psoDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;    //多重采样数量(m4xMsaaState在D3DApp类中定义,是否MSAA多重采样,如是,则采样数为4,否则1)
psoDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;//多重采样质量
psoDesc.DSVFormat = mDepthStencilFormat;    //DSV格式:深度模板缓冲区

(2)创建PSO

传入PSO属性结构体指针,创建PSO。
核心函数:md3dDevice->CreateGraphicsPipelineState();

	//创建PSO
    ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&mPSO)));

七、着色器代码

此案例我们仅仅是输入了顶点位置和顶点色,所以代码非常简单。DX的着色器代码为HLSL语言,类似C语言,和CG语言也非常像,所以有UnityShaderLab语言基础的朋友很容易就能看懂(注意:此处的语义和着色器函数名需要与顶点输入布局描述中一致)。
直接上代码:

cbuffer cbPerObject : register(b0)  //将常量数据绑定到寄存器为b0的常量缓冲区上
{
    float4x4 gWorldViewProj;    //模型到裁剪空间矩阵(注意:前缀多个g,应该是为了表明在GPU上的数据)
};
//顶点属性
struct VertexIn
{
    float3 Pos : POSITION;
    float4 Color : COLOR;
};
//像素属性
struct VertexOut
{
    float4 PosH : SV_POSITION;
    float4 Color : COLOR;
};
//顶点着色器
VertexOut VS(VertexIN vin)
{
    VertexOut vout;
    vout.PosH = mul(float4(vin.Pos, 1), gWorldViewProj);    //将顶点从模型空间转换到裁剪空间
    vout.Color = vin.Color;     //传入顶点色

    return vout;
}
//像素着色器
float4 PS(VertexOut pin)
{
    return pin.Color;   //计算片元颜色
}

注意:传入PSO的字节码就是着色器代码编译后生成的。

发布了3 篇原创文章 · 获赞 12 · 访问量 1483

猜你喜欢

转载自blog.csdn.net/qq_42650859/article/details/104347166