引言
偶尔翻到了很早前用DirectX12搭建的渲染器,很多细节都已经遗忘了,现在发上来作为一个参考,期末考完后应该会根据龙书继续学习DX12的细节。
本篇使用DX12,构建了一个加载了光照模型的基础渲染器
一 调用D3D12的基本步骤和准备工作
头文件和引用库文件
组件对象模型 COM
令DirectX不受编程语言束缚,使之向后兼容
通常视为接口,但考虑编程目的,目前当做C++类使用
隐藏大量细节
使用
- 获取指向某COM接口的指针,需要借助特定函数或另一接口的方法,而不是C++的new
- COM对象会统计其引用次数(引用计数为0,自行释放内存)
- 使用完后,应该调用Release方法
为了辅助管理,Window运行时库(Windows Runtime Library,WRL)提供了
ComPtr
类,可以当做是COM对象的智能指针Get:返回一个指针
GetAddressOf:返回COM接口指针地址
Reset:设置为nullptr并释放与之相关的所有引用,与赋值nullptr作用相同
COM接口都以大写字母I开头
1 |
|
定义待渲染数据结构(三角形)
1 | struct GRS_VERTEX |
定义变量
1 | const UINT nFrameBackBufCount = 3u;//无符号整型 |
二 创建窗口
WNDCLASSEX
设置窗口属性
1 | WNDCLASSEX windowClass = { 0 }; |
注册WNDCLASSEX结构体
1 | RegisterClassEx(&windowClass); |
Create Window
1 | HWND hwnd = CreateWindow( |
Show Window
1 | ShowWindow(hwnd, SW_SHOW); |
添加While循环
- 持续运行
1 | while (true) |
?教程一二完整代码
- 因为未知的错误,所以运行不了
1 | const UINT nFrameBackBufCount = 3u;//无符号整型 |
添加事件处理
- 把while改成这个,这下就可以正常关闭窗口了
1 | MSG msg = {}; |
- 但VS还没有终止程序,需要添加一个回调函数,之前默认的是DefWindowProc,现在改成自己的WindowProc函数
1 | WNDCLASSEX windowClass = { 0 }; |
WindowProc 回调函数
作用
- 咱们有很多消息类型,通过
Switch
来选择处理不同类型的信息。 - 比如
WM_DESTROTY
,就是在咱们点击关闭窗口按钮时会触发的,在case WM_DESTROY
里面咱们调用了PostQuitMessage(0)
方法,也就意味着退出了。
1 | LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) |
三 初始化Direct3D
基本知识
纹理格式
- 尽管名称在字面上是颜色和Alpha值,但不一定存储的是颜色信息
- 如 1 还可以储存任意3D向量
- 也有无类型的纹理,仅用它来预留内存
深度缓冲
资源与描述符
资源
- GPU会对资源进行读和写操作
- 读:例如从纹理或者存有3d场景中几何体位置信息的缓冲区读取数据
- 写:例如向后台缓冲区或深度/模版缓冲区写入数据
- 在发出绘制命令前,需要将本次draw call相关的Resource bind (or link)到render pipeline。
- 部分资源可能在每次绘制时发生变化,每次需要重新绑定
描述符
- 然而,GPU资源并非直接与render pipeline 绑定,需要**描述符(descriptor)**对象来对它进行间接引用,descriptor实际上即为一个中间层。
- 为什么要额外使用这个中间层?
- GPU Resource实际上都是一些普通的内存块
- 由于Resource通用性,它们能被设置到render pipeline的不同阶段使用
- 常见例子:先把纹理用作渲染目标(即D3d绘制到纹理技术),然后再将该纹理作为一个shader resource(经过采样作为shader的input data)
- 有时,我们只想bind a part of resource data 到 render pipeline中
- 创建一个resource 可能用的 无类型格式,这样GPU甚至不会知晓
- 作用:
- 指定资源数据
- 为GPU解释资源(无类型,则创建descriptor时指定其type类型)
- 告知D3D某个资源如何使用
- 指定resouce的局部数据
descriptor==view
Descriptor具体类型
描述符堆 Descriptor Heap
存有一系列描述符,可看做描述符数组
本质上是存放用户程序中某种特定类型描述符的一块内存
我们需要为每一种描述符都创建出单独的描述符堆。另外,也可以为同一种描述符类型多个描述符堆
我们可以用多个描述符引用同一个资源
先把纹理用作渲染目标(即D3d绘制到纹理技术),然后再将该纹理作为一个shader resource(经过采样作为shader的input data),就要分别创建两个描述符:RTV描述符和SRV描述符
如果以无类型格式创建一个资源,我们也可以创建两个Descriptor分别根据需求当做浮点值和整数值使用
创建Descriptor最佳时机在初始化期间,此过程中需要执行一些类型的检测和验证工作
全局变量
1 | const UINT FrameCount = 2; |
管线对象
1 | //管线对象 |
同步对象
1 | UINT frameIndex; |
加载管线对象
- 新建一个函数叫
LoadPipeline()
,下面在这个函数里面加载咱们需要的管线对象了
辅助方法(官方抄来,可以自己写)
1 | std::string HrToString(HRESULT hr) |
ID3D12Debug 调试层
1 |
|
IDXGIFactory4 枚举显示适配器,创建交换链
1 | ComPtr<IDXGIFactory4> mDxgiFactory; ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(mDxgiFactory.GetAddressOf()))); |
IDXGIAdapter1
- 不只1,还有2、3、4
- 现在可以通过上面创建的工厂来枚举咱们的适配器了
1 | IDXGIAdapter1* GetSupportedAdapter(ComPtr<IDXGIFactory4>& dxgiFactory,const D3D_FEATURE_LEVEL featureLevel) |
- 找到咱们支持的适配器
1 | D3D_FEATURE_LEVEL featureLevels[] = |
ID3D12Device 包含对象创建方法
ID3D12Device
接口作为最基础的接口之一,在整个d3d12编程中有很重要的作用。包含了很多重要对象的创建方法。
1 | if (adapter != nullptr) |
ID3D12CommandQueue 命令队列
- 在Direct3D12中,命令队列由接口
ID3D12CommandQueue
来表示。它是通过填充D3D12_COMMAND_QUEUE_DESC
结构来描述队列,然后调用ID3D12Device::CreateCommandQueue
来创建的。
1 | D3D12_COMMAND_QUEUE_DESC queueDesc = {}; |
IDXGISwapChain3 交换链
- 正如你看到的,它带了个数字3,也就意味着还有。。。
交换链和页面翻转
为避免动画中画面闪烁,最好将动画帧绘制在一中称为后台缓冲区的离屏(off-screen)纹理内。
- 后台缓冲区绘制完后,和前台缓冲区互换的操作,称为呈现,只需交换两个指针即可
- 后台缓冲区和前台缓冲区构成了交换链(swap chain)
- 使用两个缓冲区称为双缓冲(double buffering)
1 | DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {}; |
ID3D12DescriptorHeap 描述符堆
- 描述符堆是什么?看名字就知道是存放东西的地方,每错,它就是用来存放描述符(内存那么多数据,我怎么知道这块内存是什么?就靠描述符来对它描述)或者说是资源视图的。
1 | D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {}; |
ID3D12Resource 资源
- 首先通过
GetBuffer
方法获取交换链缓存区资源,然后为此资源创建视图。
1 | for (UINT n = 0; n < FrameCount; n++) |
ID3D12CommandAllocator 命令分配器
- 创建命令分配器来存放命令。
1 | ThrowIfFailed(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator))); |
LoadPipeline方法完整代码
1 | void LoadPipeline() |
加载资源对象
新建LoadAsset(),下面在这个函数中加载咱们需要的资源
因为咱们现在只是初始化DX而已,并不需要什么资源,所以此方法内容比较简单。
首先前面创建的命令列表在记录命令前得先关闭,然后创建了CPU和GPU同步的Fence。
```cpp
void LoadAsset()
{
ThrowIfFailed(device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, commandAllocator.Get(), nullptr, IID_PPV_ARGS(&commandList)));
ThrowIfFailed(commandList->Close());//关闭命令队列
{
ThrowIfFailed(device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence)));
fenceValue = 1;
fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (fenceEvent == nullptr)
{
ThrowIfFailed(HRESULT_FROM_WIN32(GetLastError()));
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
## 添加命令
* 创建一个新的方法`PopulateCommandList()`来记录命令,这里执行了一个最简单操作`ClearRenderTargetView`。
```cpp
void PopulateCommandList()
{
ThrowIfFailed(commandAllocator->Reset());
ThrowIfFailed(commandList->Reset(commandAllocator.Get(), pipelineState.Get()));
D3D12_RESOURCE_BARRIER resBarrier = CD3DX12_RESOURCE_BARRIER::Transition(renderTargets[frameIndex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET);
commandList->ResourceBarrier(1, &resBarrier);
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvHeap->GetCPUDescriptorHandleForHeapStart(), frameIndex, rtvDescriptorSize);
const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
resBarrier = CD3DX12_RESOURCE_BARRIER::Transition(renderTargets[frameIndex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT);
commandList->ResourceBarrier(1, &resBarrier);
ThrowIfFailed(commandList->Close());
}
同步
- 创建一个新的方法WaitForPreviousFrame()来进行同步。
1 | void WaitForPreviousFrame() |
渲染
创建一个新的方法
OnRender()
来渲染。```cpp
void OnRender()
{
PopulateCommandList();
ID3D12CommandList* ppCommandLists[] = { commandList.Get() };
commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
ThrowIfFailed(swapChain->Present(1, 0));
WaitForPreviousFrame();
}1
2
3
4
5
6
7
8
9
10
11
12
## 清理
* 创建一个新的方法OnDestroy()来清理。
```cpp
void OnDestroy()
{
WaitForPreviousFrame();
CloseHandle(fenceEvent);
}
渲染
1 | void OnRender() |
WindowProc
1 | LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) |
Main函数也要修改
1 | int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nShowCmd) |
四 背景颜色更新
全局变量:RGB颜色
1 | //RGB颜色 |
清屏颜色设置
我们需要将添加命令函数
PopulateCommandList()
里的clearColor
修改为全局变量1
const float clearColor[] = { color[0], color[1], color[2], color[3] };
添加Update函数
- 该功能实现窗口颜色的变化
1 | void OnUpdate() |
窗口
- 我们需要执行我们刚才新增的
OnUpdate()
函数
最后,我们就可以得到一个窗口不断变化背景颜色的程序了
五 绘制三角形
修改LoadAsset
方法
- 只需要修改一下
LoadAsset
方法
根签名 ID3D12RootSignature
什么是根签名?
根签名由应用配置,并将命令列表链接到着色器所需的资源。 图形命令列表同时具有图形和计算根签名。 计算命令列表只具有一个计算根签名。 这些根签名彼此独立。
详细请看根签名 - Win32 apps | Microsoft Learn
1 | { |
第一个着色器
引入新的头文件(d3dcompiler.h)和库(d3dcompiler.lib)。
第一个shader
- 新建一个Assets文件夹存放着色器文件,然后新建一个**
shaders.hlsl
**文件。
1 | ComPtr<ID3DBlob> vertexShader; |
shaders.hlsl
1 | struct PSInput |
创建管线状态对象 ID3D12PipelineState
是什么
- 表示当前设置的所有着色器和某些固定函数状态对象的状态
1 | D3D12_INPUT_ELEMENT_DESC inputElementDescs[] = |
顶点缓存和顶点缓存视图
咱们需要定义一个顶点结构体,这个结构体包含每个顶点的各种属性,暂时就包含位置和颜色2个属性。这里需要把DX的数学库(
DirectXMath.h
)包含一下1
2
3
4
5struct Vertex
{
XMFLOAT3 position;
XMFLOAT4 color;
};然后创建顶点缓存和顶点缓存视图
1 | Vertex triangleVertices[] = |
LoadAsset方法完整代码
1 | void LoadAsset() |
修改PopulateCommandList
方法
- 使用
commandList
记录新的命令去绘制三角形。 - 补充两个东西,一个视口viewport,还有一个裁剪矩形scissorRect
1 | CD3DX12_VIEWPORT viewport(0.0f,0.0f, width,height); |
- 然后在
PopulateCommandList
方法里面添加几个新的命令。
1 | void PopulateCommandList() |
效果图
六 绘制四边形
索引缓冲区
- 索引缓冲区是索引列表。每个索引代表一个顶点。每 3 个索引代表一个三角形。
顶点数组
咱们只需要4个顶点就可以绘制一个四边形,更新以后的顶点结构体数组。
1 | Vertex triangleVertices[] = |
索引数组
1 | DWORD triangleIndexs[] |
顶点缓存和索引缓存
见代码
1 | { |
七 深度测试
上次的基础上绘制两个四边形那就很简单了,直接在顶点数组里面加点数据就可以了。
- z轴越小,离摄像机越近
1 | Vertex triangleVertices[] = |
绘制第二个四边形命令
1 | commandList->DrawIndexedInstanced(6, 1, 0, 0,0);//第一个绿色四边形 |
结果:
蓝色却依旧覆盖了绿色,这是因为先绘制了绿色的四边形,然后绘制的蓝色四边形,把绿色覆盖了
深度测试
深度模板堆
1 | D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc = {}; |
修改管线状态对象,启用深度模板
1 | D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {}; |
深度缓存
创建深度模板缓存,这里得采用默认堆类型。
1 | D3D12_DEPTH_STENCIL_VIEW_DESC depthStencilDesc = {}; |
添加命令
然后在准备渲染之前得清除深度模板视图和清除渲染目标视图一样。
1 | CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvHeap->GetCPUDescriptorHandleForHeapStart(), frameIndex, rtvDescriptorSize); |
结果:
八 常量缓存
常量缓存区结构体
定义咱们需要传递给着色器的结构体,这个结构体大小必须是256字节对齐
1 | struct SceneConstantBuffer |
- 添加一个256字节对齐的方法
1 | template <typename T> |
常量缓存堆
1 | D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc = {}; |
常量缓存
1 | const UINT constantBufferSize = CalcConstantBufferByteSize<SceneConstantBuffer>(); |
根签名
根签名由根参数组成,根参数可以是根常量、根描述符和描述符表,在这里创建一个描述符表,存放了咱们的常量缓存视图。
1 | D3D12_FEATURE_DATA_ROOT_SIGNATURE featureData = {}; |
命令列表
添加一些代码去设置咱们的常量缓存堆。
1 | ID3D12DescriptorHeap* ppHeaps[] = { cbvHeap.Get() }; |
OnUpdate方法
在OnUpdate方法里面对常量缓存里面的数据进行更新
1 | const float translationSpeed = 0.005f; |
shaders.hlsl
在着色器代码里面添加相应的结构体,拿来接收咱们传递过来的数据,如何加到咱们的顶点位置上面去,这样咱们的四边形就能动起来了。
1 | cbuffer SceneConstantBuffer : register(b0) |
解决一个bug
- 运行时一直在输出信息
创建管线状态对象描述时加上下面最后一句就可以了。
1 | D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {}; |
九 绘制彩色立方体
顶点数组
1 | Vertex triangleVertices[] = |
索引数组
一个立方体就六个面,也就是12个三角形,也就是36个索引了。
1 | DWORD triangleIndexs[] |
常量缓存
咱们需要修改一下常量缓存,把上次的offset换成MVP矩阵
1 | struct SceneConstantBuffer |
OnUpdate
咱们就在更新函数里面构造MVP矩阵,你可以在这里面不断更新数据,让摄像机动起来。
1 | XMVECTOR pos = XMVectorSet(0.0f, 5.0f, -5.0f, 1.0f); |
命令列表
修改一下咱们的绘制调用
1 | commandList->DrawIndexedInstanced(36, 1, 0, 0, 0); |
着色器
修改一下着色器,在顶点着色器里面,咱们给每一个顶点的位置都乘以MVP矩阵。
1 | cbuffer SceneConstantBuffer : register(b0) |
十 纹理
加载纹理的方式有很多种,咱们采用Windows Imaging Component (WIC)。
加载纹理方法
1 | DXGI_FORMAT GetDXGIFormatFromWICFormat(WICPixelFormatGUID& wicFormatGUID) |
创建描述符堆
首先创建堆来存放咱们的常量缓冲视图和着色器资源视图。
1 | D3D12_DESCRIPTOR_HEAP_DESC cbvsrvHeapDesc = {}; |
根签名
根参数
创建一个描述符表存放常量缓冲视图和着色器资源视图,然后放入根参数索引为0的位置。
1 | D3D12_FEATURE_DATA_ROOT_SIGNATURE featureData = {}; |
静态采样
1 | D3D12_STATIC_SAMPLER_DESC sampler = {}; |
创建根签名
1 | CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC rootSignatureDesc; |
完整代码
1 | // 定义一个用于根签名功能数据的结构 |
顶点结构体
去掉颜色,加上纹理C
1 | struct Vertex |
输入布局
1 | D3D12_INPUT_ELEMENT_DESC inputElementDescs[] = |
顶点数组
- 去掉颜色,加上纹理(瞎写的纹理坐标哦)
1 | Vertex triangleVertices[] = |
加载图像数据
1 | D3D12_RESOURCE_DESC textureDesc; |
创建纹理资源
1 | CD3DX12_HEAP_PROPERTIES heapProperties = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT); |
把资源从上传堆(CPU和GPU都可以访问)拷贝到默认堆(只能GPU访问),然后对资源设置屏障(也就是这个资源干什么用的)。
1 | // 定义纹理数据结构,并初始化为零 |
创建着色器资源视图
1 | // 定义着色器资源视图描述结构体,并初始化为零 |
设置根签名和描述符表命令
1 | // 将命令列表与根签名关联,以便在绘制时使用正确的根签名 |
修改着色器代码
1 | Texture2D t1 : register(t0); |
成果
我是非常满意的
十一 Phong和Blinn-Phong光照模型
Phone
顶点结构体
首先修改顶点结构体添加法线。
1 | struct Vertex |
顶点结构体数组
因为每一个顶点都有自己的法线,所以咱们得使用36个顶点
1 | Vertex triangleVertices[] = |
常量缓存
1 | struct SceneConstantBuffer |
顶点索引数组
1 | DWORD triangleIndexs[] |
OnUpdate
修改OnUpdate方法,去掉背景颜色更新的数据,加上光位置运动的数据,并且把Phone光照模型需要的数据通过常量缓冲传递过去。
1 | void OnUpdate() |
添加命令
颜色
//const float clearColor[] = { color[0], color[1], color[2], 1.0f };
const float clearColor[] = { 0.0f, 0.0f, 0.0f, 1.0f };
管线对象
1 | D3D12_INPUT_ELEMENT_DESC inputElementDescs[] = |
着色器
修改着色器,完成咱们的Phone着色模型,这里需要注意一个点,那就是法线。
1 | Texture2D t1 : register(t0); |
Blinn-Phong
其实Blinn-Phong和Phong效果差不多,就是提升了点性能。
其他东西都不变,着色器修改一下反射光部分就可以了。
着色器代码
1 | Texture2D t1 : register(t0); |