本文接上篇:DirectX12(D3D12)基础教程(十)——DXR(DirectX Raytracing)基础教程(上)
目录
9.1、创建“全局根签名(Global Root Signature)”
9.2、创建“本地(局部)根签名(Local Root Signature)”
10.3、Raytracing渲染管线状态对象及子对象内存拓扑
5、C/C++代码中的其它准备工作
因为本次示例中使用了Fallback库,所以代码中开始需要包含相关的头文件。同时因为光追渲染管线状态的特殊性,所以我们需要借助DXR的辅助工具函数来构建渲染管线状态对象。另外我们将一些辅助的宏定义搬出了CPP文件,形成了单独头文件。还有我们使用fxc编译出来的Shader代码头文件。那么这些头文件都需要包含进来,因此开头处需要加入下面这些头文件:
#include "..\Commons\GRSMem.h"
#include "..\Commons\GRSCOMException.h"
#include "Shader\RayTracingHlslCompat.h" //shader 和 C++代码中使用相同的头文件定义常量结构体 以及顶点结构体等
#include "../RayTracingFallback/Libraries/D3D12RaytracingFallback/Include/d3dx12.h"
#include "../RayTracingFallback/Libraries/D3D12RaytracingFallback/Include/d3d12_1.h"
#include "../RayTracingFallback/Libraries/D3D12RaytracingFallback/Include/D3D12RaytracingFallback.h"
#include "../RayTracingFallback/Libraries/D3D12RaytracingFallback/Include/D3D12RaytracingHelpers.hpp"
#if defined(_DEBUG)
#include "Debug/x64/CompiledShaders/Raytracing.hlsl.h"
#else
#include "Release/x64/CompiledShaders/Raytracing.hlsl.h"
#endif
现在D3Dx12.h、D3D12RaytracingFallback.h、D3D12RaytracingHelpers.h等都在fallback库中有了,我们就直接从其目录包含进来。fallback库的主要头文件是D3D12RaytracingFallback.h。而D3D12RaytracingHelpers.h中就是一些重要的DXR辅助工具类的封装头文件,其作用类似于d3dx12.h中的辅助类,但功能更强一些。
6、DXR编程的基本框架
首先从总体上看,DXR编程的过程与传统的D3D12光栅化渲染编程步骤大同小异。更进一步可以将DXR(实时光追渲染)的过程理解为跟我们之前教程中的渲染到纹理的例子过程更类似。
因为DXR渲染的特殊性,它的渲染结果就是一张2D纹理(UA),最终再由D3D复制命令队列将结果复制到交换链的后缓冲上去。
与D3D12光栅化渲染相同的步骤,我们就不赘述了,请大家复习之前的教程。我们重点说一下DXR中比较特殊的几个不同的步骤:
1、要DXR渲染就需要创建DXR的设备接口,这个设备接口其实本质上是D3D12接口的扩展。即通过继承接口的方式派生而来;
2、在DXR渲染中,一般根签名对象会有多个,一般至少是两个,即一个全局根签名和一个局部根签名;
3、DXR渲染管线状态对象与D3D12传统光栅化渲染的PSO对象有较大的不同,这主要是因为二者些渲染过程已经完全不同导致的,但核心仍然是基于合理安排Shader调用顺序展开的。
4、DXR渲染过程中,不但需要各个被渲染物体的网格对象,还需要为这些网格生成对应的DXR渲染特有的“加速结构”数据,以便DXR渲染高速进行光线碰撞检测。
5、DXR渲染中,还需要我们准备好每个过程(发射光线、最近碰撞、未命中)的专门Shader Table缓冲,方便DXR再碰撞检测后按照光线发射中指定的索引序号调用对应的Shader函数进行光照计算。
6、最后我们将渲染好的2D UA纹理复制到交换链后缓冲,调用Present方法呈现画面就完成了一个完整的渲染周期。
下面我们就逐一详细介绍这些DXR渲染编程的特殊过程。
7、枚举高性能适配器(显卡)和创建DXR设备
为了尽量压缩内容,关于DXR代码部分,我仅介绍与之前使用D3D12进行传统光栅渲染教程示例中不同的部分来介绍,那些相同的内容我就不再重复赘述了,必要的话请大家复习之前的内容。
7.1、枚举高性能适配器(显卡)
从本系列教程开始,其实好几篇文章中我们都探讨了如何枚举多个适配器,并且从这些适配器中“找出”一个相对高性能的适配器(显卡)来运行我们的示例。尤其是在“多显卡”渲染的示例中,为了设置哪个显卡为“主显卡”,哪个显卡为“辅助显卡”,是颇废了些周章的。其实仔细想想,我们现在使用的方法都是挺不靠谱的,无论是判断显存、判断NUMA、等等方式,都有可能会“失效”。因为现在其实很多笔记本都是至少一个核显+一个独显这样的配置了,所以甄选合适的显卡来运行渲染是个编程中的现实问题。建议大家从编程的角度深入深入思考下这个问题。
幸运的是貌似微软也意识到了这个问题,自从DXGI1.6版发布以后,其接口IDXGIFactory6提供了专门的方法EnumAdapterByGpuPreference,用来按照性能来排序枚举系统中的适配器。该方法原型如下:
HRESULT EnumAdapterByGpuPreference(
UINT Adapter,
DXGI_GPU_PREFERENCE GpuPreference,
REFIID riid,
void **ppvAdapter
);
除了特殊的DXGI_GPU_PREFERENCE GpuPreference参数外,该方法与常用的EnumAdapter方法参数相同。而这个函数的重要之处就在于这个参数。
该参数是个枚举值,定义如下:
typedef
enum DXGI_GPU_PREFERENCE
{
DXGI_GPU_PREFERENCE_UNSPECIFIED = 0,
DXGI_GPU_PREFERENCE_MINIMUM_POWER = ( DXGI_GPU_PREFERENCE_UNSPECIFIED + 1 ) ,
DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE = ( DXGI_GPU_PREFERENCE_MINIMUM_POWER + 1 )
}DXGI_GPU_PREFERENCE;
从枚举值名称大家应该就看明白其含义了,我就不赘述了。有了这个强悍的函数,枚举适配器的过程就简单多了。在本章示例中就简化成一句代码了:
GRS_THROW_IF_FAILED(pIDXGIFactory6->EnumAdapterByGpuPreference(0
, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE
, IID_PPV_ARGS(&pIDXGIAdapter1)));
这样就保证了我们每次第一个枚举出来的适配器一定是现在系统中最强悍的一个,并且性能从高到低依次排序(DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE参数的功劳)。这就为我们将来封装多显卡引擎扫清了基本的障碍。当然性能顺序的正确性最终由Windows系统来决定,我们不需要再做额外的工作了。
需要注意的一点就是目前IDXGIFactory6接口还需要从基本的IDXGIFactoryn(n=0-5,0忽略)等基础接口通过调用QueryInterface方法得到,而目前CreateDXGIFactory2方法最高只能创建IDXGIFactory5接口。
7.2、判断系统DXR能力支持程度
在获得了性能最高适配器的接口之后,重要的工作就是判断它对DXR支持的程度了。(AMD显卡的情况不清楚,有条件的网友实验后请跟帖说下情况,谢谢了!)
首先就是判断该适配器是否直接支持硬件DXR。判定的方法如下:
D3D12_FEATURE_DATA_D3D12_OPTIONS5 stFeatureSupportData = {};
HRESULT hr = pID3DDeviceTmp->CheckFeatureSupport(
D3D12_FEATURE_D3D12_OPTIONS5
, &stFeatureSupportData
, sizeof(stFeatureSupportData));
//检测硬件是否是直接支持DXR
bISDXRSupport = SUCCEEDED(hr)
&& (stFeatureSupportData.RaytracingTier
!= D3D12_RAYTRACING_TIER_NOT_SUPPORTED);
代码中主要是通过调用CheckFeatureSupport方法检测扩展支持能力。这也是我们之前教程中说过的方法。检测的标志代码中已经很清楚了。就不赘述了。这是目前最主要的判定硬件DXR支持的方法。
当硬件DXR不被支持时,不用灰心,接着我们像下面这样调用来继续检测fallback方式是否被支持:
UUID UUIDExperimentalFeatures[] = { D3D12ExperimentalShaderModels };
// 打开扩展属性支持
HRESULT hr1 = D3D12EnableExperimentalFeatures(1
, UUIDExperimentalFeatures
, nullptr
, nullptr);
//打开DXR Fallback层支持,也就是用通用计算能力虚拟DXR
if ( ! SUCCEEDED(hr1) )
这里主要的判定方法是D3D12EnableExperimentalFeatures,原理就是判断能否打开扩展Shader Models的支持,不能打开就说明系统不能支持DXR fallback。官方例子中说只有打开了Windows系统的开发者模式,才能使用fallback方式。
最后需要注意的就是,使用DXR Fallback的时候,必须保证在创建D3D设备之前,优先调用D3D12EnableExperimentalFeatures打开了Fallback支持,否则后续的fallback功能的调用都会失败。所以在示例代码中,我们是在所有判定都完毕后,又正式创建了一个D3D12的设备接口变量。
如果上述的判断都不成立,即硬件DXR不被支持,同时Fallback方式也没法运行时,程序会给出一个提示然后彻底退出,不再执行。
后续的代码中就会一直使用bISDXRSupport这个变量来区分是直接硬件级别DXR调用还是fallback方式。当其值为TRUE时即硬件DXR支持,否则就调用Fallback的兼容DXR相关设备接口方法。阅读代码时请注意,后面就不再特别强调了。
7.3、创建DXR设备和命令列表
要使用DXR,跟使用非DXR编程步骤类似,就是首先要获得D3D12设备接口,然后在此基础上进一步获得DXR设备。紧接着就是创建命令队列、命令列表等。目前D3D12与DXR使用相同的命令队列对象接口,这也很好理解,因为命令队列就是代表适配器本身,所以无论什么命令最终都是适配器执行,因此没有必要区分命令队列,同时还要清楚的认识到,DXR中实质上大多数命令都是与D3D12中完全相同的。
而二者之间的命令列表则有差异,因为毕竟DXR是与D3D12在命令上是有扩展的,所以DXR命令列表接口就需要在原来的命令列表接口的基础上进行扩展。而在DXR Fallback库中,是简单的重新定义了一个兼容的DXR命令列表接口,这与硬件支持的纯DXR命令列表接口使用接口继承的方式不同。
7.3.1、使用DXR Fallback兼容设备
在DXR Fallback中,创建设备和特有的DXR命令列表需要像下面这样进行:
GRS_THROW_IF_FAILED(D3D12CreateRaytracingFallbackDevice(
pID3D12Device4.Get()
, CreateRaytracingFallbackDeviceFlags::ForceComputeFallback
, 0
, IID_PPV_ARGS(&pID3D12DXRFallbackDevice)));
pID3D12DXRFallbackDevice->QueryRaytracingCommandList(pICMDList.Get()
, IID_PPV_ARGS(&pIDXRFallbackCMDList));
实质上就是调用Fallback库中的专用创建接口创建DXR的fallback设备接口,然后再利用该设备接口的创建DXR Fallback命令列表接口的方法创建DXR专用的命令列表接口对象。
7.3.2、使用纯DXR设备
而相对于Fallback兼容方式的“奇特”方法,纯DXR设备的创建方法就很简单直接明了了:
GRS_THROW_IF_FAILED(pID3D12Device4->QueryInterface(IID_PPV_ARGS(&pID3D12DXRDevice)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pID3D12DXRDevice);
GRS_THROW_IF_FAILED(pICMDList->QueryInterface(IID_PPV_ARGS(&pIDXRCmdList)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIDXRCmdList);
就是两个QueryInterface搞定。这也充分的说明纯DXR设备及命令接口就是在原来对应的D3D12接口上进行了扩展。具体的方法就是在原接口的基础上继承派生出新版本的接口即可。这里实质上派生的DXR设备接口类型就是ID3D12Device5,而对应的命令列表接口类型就是ID3D12GraphicsCommandList4。根据我们之前的教程中介绍的规则,这些接口末尾的数字就是对应的版本号,这样的接口派生方式及创建方法都很COM化,很简单明了,很方便理解。
从这种创建方式中,其实也是潜藏的告诉我们,传统的D3D12光栅化渲染方式其实和现在的DXR渲染方式在整个D3D12中是兼容并存的,目前可以简单的理解为DXR就是D3D12的一个升级版本而已。
8、创建UAV和SRV堆
在DXR中,因为我们的渲染目标是一个UAV,之前的教程中没有提到过UAV的创建,所以这里单独讲解一下。另外在DXR渲染中,因为包括Vertex Buffer等所有资源都需要以缓冲形式传入管线,所以需要一个元素比较多的SRV堆来装载这些资源描述符,同时为了区别在官方例子中被重用的几个SRV,并且为了便于大家理解,那么在本章例子中,我们特别定义了一个有7个元素的SRV堆(本质上是数组,希望你已经明白这个),并定义了对应的索引常量来区分每个特别的SRV描述符。
8.2、创建UAV
在本章例子中,我们只是简单的使用“隐式堆”的形式创建了一个Unorder Access 的2D纹理,代码如下:
GRS_THROW_IF_FAILED(pID3D12Device4->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT)
, D3D12_HEAP_FLAG_NONE
, &CD3DX12_RESOURCE_DESC::Tex2D(fmtBackBuffer
, iWidth, iHeight, 1, 1, 1, 0
, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS)
, D3D12_RESOURCE_STATE_UNORDERED_ACCESS
, nullptr
, IID_PPV_ARGS(&pIDXRUAVBufs)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIDXRUAVBufs);
//UAV Heap的第一个描述符 就放UAV 也就是 DXR的Output
CD3DX12_CPU_DESCRIPTOR_HANDLE stUAVDescriptorHandle(
pIDXRUAVHeap->GetCPUDescriptorHandleForHeapStart()
, c_nDSHIndxUAVOutput
, nSRVDescriptorSize);
D3D12_UNORDERED_ACCESS_VIEW_DESC stUAVDesc = {};
stUAVDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D;
pID3D12Device4->CreateUnorderedAccessView(pIDXRUAVBufs.Get()
, nullptr
, &stUAVDesc
, stUAVDescriptorHandle);
从代码中可以看出UA纹理的创建与创建别的2D纹理类似,关键的标志参数就是D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS和D3D12_RESOURCE_STATE_UNORDERED_ACCESS。
后面就是创建UAV描述符,这个就不多啰嗦了,代码中没有什么特别的地方。
8.2、创建SRV堆
为了区分每个SRV描述符,所以本章的教程中,我们首先定义了一组索引常量如下:
const UINT c_nDSHIndxUAVOutput = 0;
const UINT c_nDSHIndxIBView = 1;
const UINT c_nDSHIndxVBView = 2;
const UINT c_nDSHIndxCBScene = 3;
const UINT c_nDSHIndxCBModule = 4;
const UINT c_nDSHIndxASBottom1 = 5;
const UINT c_nDSHIndxASBottom2 = 6;
UINT nMaxDSCnt = 7;
从常量的名称大家应该就已经猜出每个索引都是指向那个描述符了。接着我们像下面这样创建这个“较大”的描述符堆:
D3D12_DESCRIPTOR_HEAP_DESC stDXRDescriptorHeapDesc = {};
// 存放7个描述符:
// 2 - 顶点与索引缓冲 SRVs
// 1 - 光追渲染输出纹理 SRV
// 2 - 加速结构的缓冲 UAVs
// 2 - 加速结构体数据缓冲 UAVs 主要用于fallback层
stDXRDescriptorHeapDesc.NumDescriptors = nMaxDSCnt;
stDXRDescriptorHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
stDXRDescriptorHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
GRS_THROW_IF_FAILED(pID3D12Device4->CreateDescriptorHeap(
&stDXRDescriptorHeapDesc
, IID_PPV_ARGS(&pIDXRUAVHeap)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIDXRUAVHeap);
nSRVDescriptorSize = pID3D12Device4->GetDescriptorHandleIncrementSize(
D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
这段创建SRV的代码也与之前的教程中的例子没有什么区别,所以就不再赘述了。
9、创建DXR根签名
与D3D12光栅化渲染管线编程类似,DXR中也需要创建根签名。根据我们之前教程中讲解的,我们将根签名对象理解为一个“函数声明”。那么在DXR中根签名也是起这个作用,主要就是为了方便我们将参数格式、位置等信息告诉渲染管线。另外根签名还有一个重要的作用就是控制渲染管线对“参数”的可见性,具体的说在光栅化渲染管线中,就是控制什么资源在什么阶段(VS、PS等)可见。
与光栅化渲染管线不同的是,在DXR中,我们已经说过不再有“相对固定的渲染阶段”这种说法了,整个管线实质上是比较“自由”的计算过程,因此这就需要另辟蹊跷来控制传入管线的各种资源数据的“可见性”。那么在DXR中就是使用“全局根签名(Global Root Signature)”和“本地(局部)根签名(Local Root Signature)”这样的概念来加以区分。
顾名思义“全局根签名”就是所有的Raytracing阶段都能“看到并访问”的“参数列表”,而“本地(局部)根签名”就是某个特殊的Raytrading阶段(Ray Generation、Closest Hit、Miss之一)能够访问的“参数列表”。
在本章例子中,UA 2D纹理、顶点及索引数据、加速结构体数据、全局常量等数据是全局可见的,而我们为Closest Hit阶段指定了一个简单的局部根签名,用来指定被渲染球体的反光系数。其实作为一般情况的全局根签名来说,这里指定的参数有普遍的意义,也即大多数情况下,Raytracing 的全局根签名都是设定这些“参数”。而本例中的局部根签名只是一个启示作用,即告诉我们可以在局部根签名中指定一些与某个具体物体表面材质相关的参数,如:反光率、高光系数、漫反射系数、法线贴图等等,这里我们只是简单的指定了一个常数参数——反光系数。
下面就让我们看看具体如何创建这些根签名。
9.1、创建“全局根签名(Global Root Signature)”
与之前的创建根签名的方式类似,首先需要填充一组结构体,用以描述根签名中的各个参数,代码如下:
CD3DX12_DESCRIPTOR_RANGE stRanges[2] = {}; // Perfomance TIP: Order from most frequent to least frequent.
stRanges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0); // 1 output texture
stRanges[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 2, 1); // 2 static index and vertex buffers.
CD3DX12_ROOT_PARAMETER stGlobalRootParams[4];
stGlobalRootParams[0].InitAsDescriptorTable(1, &stRanges[0]);
stGlobalRootParams[1].InitAsShaderResourceView(0);
stGlobalRootParams[2].InitAsConstantBufferView(0);
stGlobalRootParams[3].InitAsDescriptorTable(1, &stRanges[1]);
CD3DX12_ROOT_SIGNATURE_DESC stGlobalRootSignatureDesc(
ARRAYSIZE(stGlobalRootParams)
, stGlobalRootParams);
这里我们实质上指定了4个参数,但是需要传入五个变量,其中Vertex Buffer和Index Buffer必须是连续的两个变量,也就是在描述符堆中必须是连续的两个描述符,并且我们指定的是从t1和t2传入,同时Index Buffer View在前,Vertex Buffer View紧跟其后。
结构填充完毕之后,我们就通过下面的代码来创建根签名:
if (!bISDXRSupport)
{//Fallback 方式
hrRet = pID3D12DXRFallbackDevice->D3D12SerializeRootSignature(
&stGlobalRootSignatureDesc
, D3D_ROOT_SIGNATURE_VERSION_1
, &pIRSBlob
, &pIRSErrMsg);
if (FAILED(hrRet))
{
if (pIRSErrMsg)
{
GRS_PRINTF(_T("编译根签名出错:%s\n")
, static_cast<wchar_t*>(pIRSErrMsg->GetBufferPointer()));
}
GRS_THROW_IF_FAILED(hrRet);
}
GRS_THROW_IF_FAILED(pID3D12DXRFallbackDevice->CreateRootSignature(
1
, pIRSBlob->GetBufferPointer()
, pIRSBlob->GetBufferSize()
, IID_PPV_ARGS(&pIRSGlobal)));
}
else
{// DirectX Raytracing
hrRet = D3D12SerializeRootSignature(
&stGlobalRootSignatureDesc
, D3D_ROOT_SIGNATURE_VERSION_1
, &pIRSBlob
, &pIRSErrMsg);
if (FAILED(hrRet))
{
if (pIRSErrMsg)
{
GRS_PRINTF(_T("编译根签名出错:%s\n")
, static_cast<wchar_t*>(pIRSErrMsg->GetBufferPointer()));
}
GRS_THROW_IF_FAILED(hrRet);
}
GRS_THROW_IF_FAILED(pID3D12DXRDevice->CreateRootSignature(1
, pIRSBlob->GetBufferPointer()
, pIRSBlob->GetBufferSize()
, IID_PPV_ARGS(&pIRSGlobal)));
}
上面具体的创建根签名的代码与之前教程中的创建的过程类似,唯一区别的地方就是当使用的是fallback方式时,我们必须使用fallback的设备接口来“编译(序列化)”根签名。
另外为了图省事,我们只是使用1.0版的Root Signature来编译根签名,因为再使用类似之前的判定是否支持1.1版的Root Signature来编写这段代码的话会过于复杂,所以我们就简化处理了。
9.2、创建“本地(局部)根签名(Local Root Signature)”
创建局部根签名就需要使用一个特殊的DXR扩展标志:D3D12_ROOT_SIGNATURE_FLAG_LOCAL_ROOT_SIGNATURE,使用该标志来填充根签名结构体:
CD3DX12_ROOT_PARAMETER stLocalRootParams[1] = {};
stLocalRootParams[0].InitAsConstants(
GRS_UPPER_SIZEOFUINT32(ST_MODULE_CONSANTBUFFER)
, 1);
CD3DX12_ROOT_SIGNATURE_DESC stLocalRootSignatureDesc(
ARRAYSIZE(stLocalRootParams)
, stLocalRootParams);
stLocalRootSignatureDesc.Flags
= D3D12_ROOT_SIGNATURE_FLAG_LOCAL_ROOT_SIGNATURE;
上面代码中除了使用该标志外,就没有太特别的地方。就不再赘述了。
接着与创建全局根签名类似,使用下面的代码来创建这个局部根签名:
if (!bISDXRSupport)
{//Fallback
hrRet = pID3D12DXRFallbackDevice->D3D12SerializeRootSignature(
&stLocalRootSignatureDesc
, D3D_ROOT_SIGNATURE_VERSION_1
, &pIRSBlob
, &pIRSErrMsg);
if (FAILED(hrRet))
{
if (pIRSErrMsg)
{
GRS_PRINTF(_T("编译根签名出错:%s\n")
, static_cast<wchar_t*>(pIRSErrMsg->GetBufferPointer()));
}
GRS_THROW_IF_FAILED(hrRet);
}
GRS_THROW_IF_FAILED(pID3D12DXRFallbackDevice->CreateRootSignature(
1
, pIRSBlob->GetBufferPointer()
, pIRSBlob->GetBufferSize()
, IID_PPV_ARGS(&pIRSLocal)));
}
else // DirectX Raytracing
{
hrRet = D3D12SerializeRootSignature(
&stLocalRootSignatureDesc
, D3D_ROOT_SIGNATURE_VERSION_1
, &pIRSBlob
, &pIRSErrMsg);
if (FAILED(hrRet))
{
if (pIRSErrMsg)
{
GRS_PRINTF(_T("编译根签名出错:%s\n")
, static_cast<wchar_t*>(pIRSErrMsg->GetBufferPointer()));
}
GRS_THROW_IF_FAILED(hrRet);
}
GRS_THROW_IF_FAILED(pID3D12DXRDevice->CreateRootSignature(1
, pIRSBlob->GetBufferPointer()
, pIRSBlob->GetBufferSize()
, IID_PPV_ARGS(&pIRSLocal)));
}
上面的代码也与一般的创建根签名的代码大同小异,不同也只是在使用fallback时,需要使用fallback的设备接口来编译(串行化)根签名。其实在fallback:: D3D12SerializeRootSignature是去除D3D12_ROOT_SIGNATURE_FLAG_LOCAL_ROOT_SIGNATURE标志后直接调用D3D12:: D3D12SerializeRootSignature方法的。
这样也说明其实所谓“全局根签名”,“局部根签名”其实内部都是一样的,也跟光栅化渲染管线的根签名对象没有本质上的区别,实质上都是“渲染管线的参数列表”,也即类似“函数签名”。而他们的区别主要在定义渲染管线时指定的方式不同。下面我们就来看看Raytracing渲染管线中是怎样使用它们的。
10、创建Raytracing渲染管线状态对象
在DXR中,因为渲染管线的相对“自由”化,所以其管线状态对象比之光栅化的渲染管线状态对象也要自由化一些,这也提现在定义它的描述结构体上。这个结构体本身定义如下:
typedef struct D3D12_STATE_OBJECT_DESC
{
D3D12_STATE_OBJECT_TYPE Type;
UINT NumSubobjects;
_In_reads_(NumSubobjects) const D3D12_STATE_SUBOBJECT *pSubobjects;
}D3D12_STATE_OBJECT_DESC;
该结构体看似简单,其实它是一个由类型化的复杂子结构体拼起来的数组,即它的pSubobjects指针指向一片连续的内存,其中每一个成员又有各自不同的类型和结构体来描述,按要求这些子对象又必须整齐的连续排列在一个内存区域中,所以DXR提供了一组辅助的简单类(在D3D12RaytracingHelpers.hpp文件中)来帮助我们构建这个结构体。
10.1、Raytracing渲染管线状态子对象
在构建Raytracing渲染管线对象的时候,我们至少要指定下列类型的子对象:
类型枚举值(D3D12_STATE_SUBOBJECT_TYPE) | 含义 | 是否必须 | 对应结构体 | D3D12RaytracingHelpers.hpp中的封装类名 |
D3D12_STATE_SUBOBJECT_TYPE_STATE_OBJECT_CONFIG | 定义状态对象的一般属性。 | 是 | typedef struct D3D12_STATE_OBJECT_CONFIG { | CD3D12_STATE_OBJECT_CONFIG_SUBOBJECT |
D3D12_STATE_SUBOBJECT_TYPE_GLOBAL_ROOT_SIGNATURE | 定义将与相关着色器一起使用的全局根签名状态子对象。 | 是 | typedef struct D3D12_GLOBAL_ROOT_SIGNATURE { | CD3D12_GLOBAL_ROOT_SIGNATURE_SUBOBJECT |
D3D12_STATE_SUBOBJECT_TYPE_LOCAL_ROOT_SIGNATURE | 定义将与相关着色器一起使用的本地根签名状态子对象。 | 是 | typedef struct D3D12_LOCAL_ROOT_SIGNATURE { | CD3D12_LOCAL_ROOT_SIGNATURE_SUBOBJECT |
D3D12_STATE_SUBOBJECT_TYPE_DXIL_LIBRARY | 描述可以包含在状态对象中的DXIL库状态子对象。(实质就是Raytracing Shader编译后的DXIL代码) | 是 | typedef struct D3D12_DXIL_LIBRARY_DESC { | CD3D12_DXIL_LIBRARY_SUBOBJECT |
D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_SHADER_CONFIG | 表示着色器配置的状态子对象。 | 是 | typedef struct D3D12_RAYTRACING_SHADER_CONFIG { | CD3D12_RAYTRACING_SHADER_CONFIG_SUBOBJECT |
D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_PIPELINE_CONFIG | 表示光线跟踪管道配置的状态子对象。 | 是 | typedef struct D3D12_RAYTRACING_PIPELINE_CONFIG { | CD3D12_RAYTRACING_PIPELINE_CONFIG_SUBOBJECT |
D3D12_STATE_SUBOBJECT_TYPE_HIT_GROUP | 描述可以包含在状态对象中的光线跟踪击中组状态子对象。 | 是 | typedef struct D3D12_HIT_GROUP_DESC { | CD3D12_HIT_GROUP_SUBOBJECT |
D3D12_STATE_SUBOBJECT_TYPE_NODE_MASK | 标识应用状态对象的GPU节点的状态子对象。 | 否 | typedef struct D3D12_NODE_MASK { | CD3D12_NODE_MASK_SUBOBJECT |
D3D12_STATE_SUBOBJECT_TYPE_EXISTING_COLLECTION | 描述可以包含在状态对象中的现有集合的状态子对象。 | 否 | typedef struct D3D12_EXISTING_COLLECTION_DESC { | CD3D12_EXISTING_COLLECTION_SUBOBJECT |
D3D12_STATE_SUBOBJECT_TYPE_SUBOBJECT_TO_EXPORTS_ASSOCIATION | 将直接在状态对象中定义的子对象与着色器导出相关联。 | 否 | typedef struct D3D12_SUBOBJECT_TO_EXPORTS_ASSOCIATION { | CD3D12_SUBOBJECT_TO_EXPORTS_ASSOCIATION_SUBOBJECT |
D3D12_STATE_SUBOBJECT_TYPE_DXIL_SUBOBJECT_TO_EXPORTS_ASSOCIATION | 此子对象在当前版本中不受支持。 | 否 | typedef struct D3D12_DXIL_SUBOBJECT_TO_EXPORTS_ASSOCIATION { | CD3D12_DXIL_SUBOBJECT_TO_EXPORTS_ASSOCIATION |
根据上表中的描述,可以看出有些子对象是我们必须要提供的,而有些则是可选的。
根据上表中的描述,可以看出有些子对象是我们必须要提供的,而有些则是可选的。当然这个不是一种必然的情况,之所以区分下必须设置和不是必须设置,只是告诉大家一般我们需要设置哪几个子对象而已。
10.2、填充Raytracing渲染管线状态结构体
在本章示例中我们基本照搬了官方示例中的Raytracing渲染管线状态对象的创建过程,整个代码如下:
CD3D12_STATE_OBJECT_DESC objRaytracingPipeline{ D3D12_STATE_OBJECT_TYPE_RAYTRACING_PIPELINE };
auto objDXILLib = objRaytracingPipeline.CreateSubobject<CD3D12_DXIL_LIBRARY_SUBOBJECT>();
D3D12_SHADER_BYTECODE stLibDXIL = CD3DX12_SHADER_BYTECODE(
(void*)g_pRaytracing, ARRAYSIZE(g_pRaytracing));
objDXILLib->SetDXILLibrary(&stLibDXIL);
{
objDXILLib->DefineExport(c_pszRaygenShaderName);
objDXILLib->DefineExport(c_pszClosestHitShaderName);
objDXILLib->DefineExport(c_pszMissShaderName);
}
auto objHitGroup = objRaytracingPipeline.CreateSubobject<CD3D12_HIT_GROUP_SUBOBJECT>();
objHitGroup->SetClosestHitShaderImport(c_pszClosestHitShaderName);
objHitGroup->SetHitGroupExport(c_pszHitGroupName);
objHitGroup->SetHitGroupType(D3D12_HIT_GROUP_TYPE_TRIANGLES);
auto objShaderConfig = objRaytracingPipeline.CreateSubobject<CD3D12_RAYTRACING_SHADER_CONFIG_SUBOBJECT>();
UINT nPayloadSize = sizeof(XMFLOAT4); // float4 pixelColor
UINT nAttributeSize = sizeof(XMFLOAT2); // float2 barycentrics
objShaderConfig->Config(nPayloadSize, nAttributeSize);
auto objLocalRootSignature = objRaytracingPipeline.CreateSubobject<CD3D12_LOCAL_ROOT_SIGNATURE_SUBOBJECT>();
objLocalRootSignature->SetRootSignature(pIRSLocal.Get());
// Define explicit shader association for the local root signature.
{
auto objRootSignatureAssociation = objRaytracingPipeline.CreateSubobject<CD3D12_SUBOBJECT_TO_EXPORTS_ASSOCIATION_SUBOBJECT>();
objRootSignatureAssociation->SetSubobjectToAssociate(*objLocalRootSignature);
objRootSignatureAssociation->AddExport(c_pszHitGroupName);
}
auto objGlobalRootSignature = objRaytracingPipeline.CreateSubobject<CD3D12_GLOBAL_ROOT_SIGNATURE_SUBOBJECT>();
objGlobalRootSignature->SetRootSignature(pIRSGlobal.Get());
auto objPipelineConfig = objRaytracingPipeline.CreateSubobject<CD3D12_RAYTRACING_PIPELINE_CONFIG_SUBOBJECT>();
UINT nMaxRecursionDepth = 1; // ~ primary rays only.
objPipelineConfig->Config(nMaxRecursionDepth);
抛去原来代码中的注释,其实整个过程中的代码也不是很多,过程也很清晰。需要注意的细节就是我们在设置局部根签名对象时,需要明确的指定一个关联关系,即指定是在哪个“阶段”Shader函数中可以访问由局部根签名指定的变量。同时这里也是个重要的提示,即可以通过这种方式设置局部根签名可以被那些“阶段”Shader函数访问。方法就是不断的AddExport,添加“阶段”Shader函数名。
10.3、Raytracing渲染管线状态对象及子对象内存拓扑
因为Raytracing渲染管线状态对象及子对象结构体的内存要求特殊性,所以它在内存中整体看起来像下面这样(以本章示例代码中填充的结构体为例展示):
从示意图中我们就可以看出最终代码实质上是拼成了一个复杂的多类型元素数组的样子。其实根本上来说这可能也是DXR驱动程序开发人员在偷懒,从设计的角度来说没有必要让调用方的程序员来拼装这个连续内存中的多类型元素数组,驱动程序内部完全可以将之封装,以减轻调用方程序员的“劳动强度”。从C++设计模式来说这其实是个典型的“聚集”模式的设计,只是现在DXR把它封装在了非正式的工具类“CD3D12_STATE_OBJECT_DESC”中。
不管怎样目前DXR的这个管线状态结构体已经被设计成了这样,并且也有个蹩脚的类来辅助我们进行填充,重要的是我们必须要能够理解它到底是个什么样子,这样方便深刻理解Raytracing渲染管线状态对象的创建方法以及其中的关键元素的构建的方法及相关子元素之间的关系。
10.4、创建Raytracing渲染管线状态对象
Raytracing渲染管线状态结构体填充完毕后,就可以调用DXR设备的方法来创建简称为RTPSO的渲染管线状态对象了,具体代码如下:
if (!bISDXRSupport)
{ // Fallback
GRS_THROW_IF_FAILED(pID3D12DXRFallbackDevice->CreateStateObject(
objRaytracingPipeline
, IID_PPV_ARGS(&pIDXRFallbackPSO)));
}
else // DirectX Raytracing
{
GRS_THROW_IF_FAILED(pID3D12DXRDevice->CreateStateObject(
objRaytracingPipeline
, IID_PPV_ARGS(&pIDXRPSO)));
}
这个创建的方法没有什么特别的地方,只是使用fallback库的方法和接口是单独特别定义的而已。
11、创建加速结构体
在实时光线追踪渲染中,最重要的能够硬件加速的阶段就是光线(射线)碰撞检测过程。这个过程其实与之前的鼠标拾取操作(pick)相类似,在实时光追渲染中射线基本上是一个像素点发射一条光线(射线),而Pick中通常只是鼠标光标位置像素处发射唯一一条射线,所以从计算量上来说,在光追渲染中,射线数量基数是屏幕像素宽度*屏幕像素高度,如:1024*768=786432=0.75M,4k:4096×2160 =8.44M。
目前的实时光追渲染中基本是一个像素发射一条光线,而在非实时光追渲染(电影级画质渲染中)通常则是一个像素点就要发射出成百上千甚至上万条光线(射线)来做渲染。然后光追渲染的本质就是不断的检测这些射线与“物体”表面三角形碰撞的情况。这个计算量就是非常惊人的,尤其对于实时光追渲染来说,在每秒30帧的基本要求下,即大约要在30毫秒(30ms)的时间段内完成至少1M的射线碰撞检测,假设场景中大概有1000万(10M)个三角形的话(实际比这个要多很多),那么碰撞检测(悲观情况下)的计算量约是1M*10M数量级,这还不算碰撞检测成功后的各种着色计算Shader的运算数量。因此必须要采取措施来加速这个阶段。
而目前主要采取的措施就是为物体创建轴对齐的AABBs包围盒,先判断射线是否与AABBs相交,再进一步判定射线与物体的某个三角形碰撞的情况(计算碰撞点重心/质心坐标)。一般情况下一个物体的包围盒中会涵盖很多三角形,如果射线不与包围盒相交,那么就不会与其中的三角形碰撞,这样可以节约很大的计算量。假设一个包围盒中平均包含1000个三角形来估计的话,那么理想情况下计算量就会下降大概1000倍左右。
在DXR中,这个生成物体碰撞检测包围盒(AABBs)的过程,以及具体的射线与物体碰撞检测的过程都可以在支持DXR的硬件上得到加速。当然碰撞的过程我们基本不需要过多的干预,即不需要编程,完全由硬件根据预制的算法高速运算即可。而生成包围盒的过程就需要编程来控制了,因为具体场景中的物体构成都是程序负责的事情,主要就是我们要告诉计算包围盒硬件过程的输入参数,即传入需要计算其包围盒的三角形网格数据。
在DXR中具体实现时将加速体结构分成了两个层级,分别称之为Top-Level(顶层)加速结构体和Bottom-Level(底层)加速结构体,二者的分工不同,顶层加速体结构主要记录实例信息及每个实例的变换矩阵(主要指模型空间自身->世界空间变换矩阵)等信息,而底层加速体结构就是每个模型实体的具体AABBs了。在概念上它们形成下面这样的结构:
其中紫色框中就是Top-Level加速结构体,而绿色框中就是Bottom-level加速结构体。
在DXR编程中我们通常需要首先与DXR设备沟通需要计算的加速结构体的缓冲大小信息,再根据这一缓冲大小信息分配Unorder Access的缓冲(注意是Buffer不是Texture),然后将缓冲交给DXR设备为我们计算加速结构体。
11.1、获取加速结构体预处理信息
在我们加载完模型的网格信息之后,在用于DXR渲染时就需要构建其对应的加速结构体信息。首先要做的就是使用基本的模型网格信息填充结构体,然后调用DXR设备接口函数获取加速结构体需要的缓冲大小等信息,代码如下:
D3D12_RAYTRACING_GEOMETRY_DESC stModuleGeometryDesc = {};
stModuleGeometryDesc.Type = D3D12_RAYTRACING_GEOMETRY_TYPE_TRIANGLES;
stModuleGeometryDesc.Triangles.IndexBuffer = pIIBBufs->GetGPUVirtualAddress();
stModuleGeometryDesc.Triangles.IndexCount = static_cast<UINT>(pIIBBufs->GetDesc().Width) / sizeof(GRS_TYPE_INDEX);
stModuleGeometryDesc.Triangles.IndexFormat = DXGI_FORMAT_R16_UINT;
stModuleGeometryDesc.Triangles.Transform3x4 = 0;
stModuleGeometryDesc.Triangles.VertexFormat = DXGI_FORMAT_R32G32B32_FLOAT;
stModuleGeometryDesc.Triangles.VertexCount = static_cast<UINT>(pIVBBufs->GetDesc().Width) / sizeof(ST_GRS_VERTEX);
stModuleGeometryDesc.Triangles.VertexBuffer.StartAddress = pIVBBufs->GetGPUVirtualAddress();
stModuleGeometryDesc.Triangles.VertexBuffer.StrideInBytes = sizeof(ST_GRS_VERTEX);
// Mark the geometry as opaque.
// PERFORMANCE TIP: mark geometry as opaque whenever applicable as it can enable important ray processing optimizations.
// Note: When rays encounter opaque geometry an any hit shader will not be executed whether it is present or not.
stModuleGeometryDesc.Flags = D3D12_RAYTRACING_GEOMETRY_FLAG_OPAQUE;
// Get required sizes for an acceleration structure.
D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAGS emBuildFlags = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PREFER_FAST_TRACE;
D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC stBottomLevelBuildDesc = {};
D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS& stBottomLevelInputs = stBottomLevelBuildDesc.Inputs;
stBottomLevelInputs.DescsLayout = D3D12_ELEMENTS_LAYOUT_ARRAY;
stBottomLevelInputs.Flags = emBuildFlags;
stBottomLevelInputs.NumDescs = 1;
stBottomLevelInputs.Type = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL;
stBottomLevelInputs.pGeometryDescs = &stModuleGeometryDesc;
D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC stTopLevelBuildDesc = {};
D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS& stTopLevelInputs = stTopLevelBuildDesc.Inputs;
stTopLevelInputs.DescsLayout = D3D12_ELEMENTS_LAYOUT_ARRAY;
stTopLevelInputs.Flags = emBuildFlags;
stTopLevelInputs.NumDescs = 1;
stTopLevelInputs.pGeometryDescs = nullptr;
stTopLevelInputs.Type = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL;
D3D12_RAYTRACING_ACCELERATION_STRUCTURE_PREBUILD_INFO stTopLevelPrebuildInfo = {};
if ( !bISDXRSupport )
{
pID3D12DXRFallbackDevice->GetRaytracingAccelerationStructurePrebuildInfo(&stTopLevelInputs, &stTopLevelPrebuildInfo);
}
else // DirectX Raytracing
{
pID3D12DXRDevice->GetRaytracingAccelerationStructurePrebuildInfo(&stTopLevelInputs, &stTopLevelPrebuildInfo);
}
GRS_THROW_IF_FALSE(stTopLevelPrebuildInfo.ResultDataMaxSizeInBytes > 0);
D3D12_RAYTRACING_ACCELERATION_STRUCTURE_PREBUILD_INFO stBottomLevelPrebuildInfo = {};
if (!bISDXRSupport)
{
pID3D12DXRFallbackDevice->GetRaytracingAccelerationStructurePrebuildInfo(&stBottomLevelInputs, &stBottomLevelPrebuildInfo);
}
else // DirectX Raytracing
{
pID3D12DXRDevice->GetRaytracingAccelerationStructurePrebuildInfo(&stBottomLevelInputs, &stBottomLevelPrebuildInfo);
}
GRS_THROW_IF_FALSE(stBottomLevelPrebuildInfo.ResultDataMaxSizeInBytes > 0);
这段代码的重点就是填充两个主要类型的结构体D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC和D3D12_RAYTRACING_GEOMETRY_DESC,然后调用DXR设备的GetRaytracingAccelerationStructurePrebuildInfo方法,得到D3D12_RAYTRACING_ACCELERATION_STRUCTURE_PREBUILD_INFO结构体的信息,在返回的这个结构体信息中,就有将要生成的加速结构体需要的缓冲大小的重要信息,这就是我们之前说的先与DXR设备沟通缓冲大小的过程。
11.2、创建放置加速结构体的缓冲
成功获取到加速体结构的预创建信息(Prebuild Info)后,我们需要创建三个缓冲,类型为Unorder Access Buffer,注意与我们之前创建的光追渲染目标Unorder Access 2D Texture类型不同,但是要求的访问方式是相同的,原因也类似,因为这个计算加速体结构的过程也是一个使用GPU的“通用计算”过程,结果缓冲也必须要能够随机无序访问。
这三个缓冲中,有一个其实是中间辅助缓冲,类似一张计算的草稿纸,通常我们称之为Scratch Buffer,另两个就是:一个Top-Level Buffer 和一个Bottom-Level Buffer,创建这三个缓冲的代码很简单如下:
GRS_THROW_IF_FAILED(pID3D12Device4->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT)
,D3D12_HEAP_FLAG_NONE
,&CD3DX12_RESOURCE_DESC::Buffer(
max(stTopLevelPrebuildInfo.ScratchDataSizeInBytes
, stBottomLevelPrebuildInfo.ScratchDataSizeInBytes)
, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS)
,D3D12_RESOURCE_STATE_UNORDERED_ACCESS
,nullptr
,IID_PPV_ARGS(&pIUAVScratchResource)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIUAVScratchResource);
{
D3D12_RESOURCE_STATES emInitialResourceState;
if (!bISDXRSupport)
{
emInitialResourceState
= pID3D12DXRFallbackDevice->GetAccelerationStructureResourceState();
}
else // DirectX Raytracing
{
emInitialResourceState
= D3D12_RESOURCE_STATE_RAYTRACING_ACCELERATION_STRUCTURE;
}
GRS_THROW_IF_FAILED(pID3D12Device4->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(
stBottomLevelPrebuildInfo.ResultDataMaxSizeInBytes
, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS),
emInitialResourceState,
nullptr,
IID_PPV_ARGS(&pIUAVBottomLevelAccelerationStructure)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIUAVBottomLevelAccelerationStructure);
GRS_THROW_IF_FAILED(pID3D12Device4->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(
stTopLevelPrebuildInfo.ResultDataMaxSizeInBytes
, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS),
emInitialResourceState,
nullptr,
IID_PPV_ARGS(&pIUAVTopLevelAccelerationStructure)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIUAVTopLevelAccelerationStructure);
}
这段代码应该很好理解了,没什么特别的地方,我们只需要注意其大小和设定的状态即可。还要注意的就是他们是放在默认类型的隐式堆上,因为这个加速体结构里面的数据只是GPU创建,GPU自己使用,CPU不需要访问,所以放在显存中是非常合适的(希望你明白我在说什么)。
11.3、创建UAV和实例缓冲区
创建好了三个缓冲,按照之前教程中所学,我们应该立刻想到接着我们应该创建每个缓冲对应的UAV了,以便指定给GPU访问使用。另外对于一个网格有多个实例的情形,那么我们还需要指定特定的实例信息缓冲(就是之前讨论过的各种变换的复合矩阵信息,以便每个实例在场景中摆放到正确的位置)。这一步编码对于fallback来说有点复杂,而对于DXR本身来说相对简单,具体代码如下:
if (!bISDXRSupport)
{
D3D12_RAYTRACING_FALLBACK_INSTANCE_DESC stInstanceDesc = {};
stInstanceDesc.Transform[0][0]
= stInstanceDesc.Transform[1][1]
= stInstanceDesc.Transform[2][2]
= 1;
stInstanceDesc.InstanceMask = 1;
UINT nNumBufferElements
= static_cast<UINT>(stBottomLevelPrebuildInfo.ResultDataMaxSizeInBytes)
/ sizeof(UINT32);
D3D12_UNORDERED_ACCESS_VIEW_DESC stRawBufferUavDesc = {};
stRawBufferUavDesc.ViewDimension = D3D12_UAV_DIMENSION_BUFFER;
stRawBufferUavDesc.Buffer.Flags = D3D12_BUFFER_UAV_FLAG_RAW;
stRawBufferUavDesc.Format = DXGI_FORMAT_R32_TYPELESS;
stRawBufferUavDesc.Buffer.NumElements = nNumBufferElements;
CD3DX12_CPU_DESCRIPTOR_HANDLE stBottomLevelDescriptor(
pIDXRUAVHeap->GetCPUDescriptorHandleForHeapStart()
, c_nDSHIndxASBottom1
, nSRVDescriptorSize);
// Only compute fallback requires a valid descriptor index when creating a wrapped pointer.
if ( !pID3D12DXRFallbackDevice->UsingRaytracingDriver() )
{
pID3D12Device4->CreateUnorderedAccessView(
pIUAVBottomLevelAccelerationStructure.Get()
, nullptr
, &stRawBufferUavDesc
, stBottomLevelDescriptor);
}
stInstanceDesc.AccelerationStructure
= pID3D12DXRFallbackDevice->GetWrappedPointerSimple(
c_nDSHIndxASBottom1
, pIUAVBottomLevelAccelerationStructure->GetGPUVirtualAddress());
GRS_THROW_IF_FAILED(pID3D12Device4->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD)
,D3D12_HEAP_FLAG_NONE
,&CD3DX12_RESOURCE_DESC::Buffer(sizeof(stInstanceDesc))
,D3D12_RESOURCE_STATE_GENERIC_READ
,nullptr
,IID_PPV_ARGS(&pIUploadBufInstanceDescs)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIUploadBufInstanceDescs);
void* pMappedData;
pIUploadBufInstanceDescs->Map(0, nullptr, &pMappedData);
memcpy(pMappedData, &stInstanceDesc, sizeof(stInstanceDesc));
pIUploadBufInstanceDescs->Unmap(0, nullptr);
}
else // DirectX Raytracing
{
D3D12_RAYTRACING_INSTANCE_DESC stInstanceDesc = {};
stInstanceDesc.Transform[0][0]
= stInstanceDesc.Transform[1][1]
= stInstanceDesc.Transform[2][2]
= 1;
stInstanceDesc.InstanceMask = 1;
stInstanceDesc.AccelerationStructure
= pIUAVBottomLevelAccelerationStructure->GetGPUVirtualAddress();
GRS_THROW_IF_FAILED(pID3D12Device4->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD)
,D3D12_HEAP_FLAG_NONE
,&CD3DX12_RESOURCE_DESC::Buffer(sizeof(stInstanceDesc))
,D3D12_RESOURCE_STATE_GENERIC_READ
,nullptr
,IID_PPV_ARGS(&pIUploadBufInstanceDescs)));
GRS_SET_D3D12_DEBUGNAME_COMPTR(pIUploadBufInstanceDescs);
void* pMappedData;
pIUploadBufInstanceDescs->Map(0, nullptr, &pMappedData);
memcpy(pMappedData, &stInstanceDesc, sizeof(stInstanceDesc));
pIUploadBufInstanceDescs->Unmap(0, nullptr);
}
// Create a wrapped pointer to the acceleration structure.
if (!bISDXRSupport)
{
UINT nNumBufferElements
= static_cast<UINT>(stTopLevelPrebuildInfo.ResultDataMaxSizeInBytes)
/ sizeof(UINT32);
D3D12_UNORDERED_ACCESS_VIEW_DESC rawBufferUavDesc = {};
rawBufferUavDesc.ViewDimension = D3D12_UAV_DIMENSION_BUFFER;
rawBufferUavDesc.Buffer.Flags = D3D12_BUFFER_UAV_FLAG_RAW;
rawBufferUavDesc.Format = DXGI_FORMAT_R32_TYPELESS;
rawBufferUavDesc.Buffer.NumElements = nNumBufferElements;
// Only compute fallback requires a valid descriptor index when creating a wrapped pointer.
CD3DX12_CPU_DESCRIPTOR_HANDLE stTopLevelDescriptor(
pIDXRUAVHeap->GetCPUDescriptorHandleForHeapStart()
, c_nDSHIndxASBottom2
, nSRVDescriptorSize);
if (!pID3D12DXRFallbackDevice->UsingRaytracingDriver())
{
//descriptorHeapIndex = AllocateDescriptor(&bottomLevelDescriptor);
pID3D12Device4->CreateUnorderedAccessView(
pIUAVTopLevelAccelerationStructure.Get()
, nullptr
, &rawBufferUavDesc
, stTopLevelDescriptor);
}
pFallbackTopLevelAccelerationStructurePointer
= pID3D12DXRFallbackDevice->GetWrappedPointerSimple(
c_nDSHIndxASBottom2
, pIUAVTopLevelAccelerationStructure->GetGPUVirtualAddress());
}
上面代码中,首先就是要填充一个称之为INSTANCE DESC实例描述结构体,再根据这个实例结构体大小创建一个Upload类型的缓冲,将实例结构体写入共享内存即可。接着就是在Fallback方式时,要创建两个Buffer的描述符(View),唯一特殊的就是fallback模式下这里非常啰嗦,需要创建称之为Wrap(包裹)类型的描述符,而硬件DXR方式则无此必要,因为硬件自己知道如何去访问那三块缓冲。
这段代码之所以看起来复杂,主要是因为Fallback方式的特殊要求闹得,假如我更换笔记本后,我就不再讲解Fallback了,然后把所有的Fallback的代码删除,那样大家也就比较好理解了。
11.4、创建加速结构体
终于,万事齐备只欠东风了,下面我们就可以正式的创建加速结构体了,代码比较清晰如下:
// Bottom Level Acceleration Structure desc
{
stBottomLevelBuildDesc.ScratchAccelerationStructureData
= pIUAVScratchResource->GetGPUVirtualAddress();
stBottomLevelBuildDesc.DestAccelerationStructureData
= pIUAVBottomLevelAccelerationStructure->GetGPUVirtualAddress();
}
// Top Level Acceleration Structure desc
{
stTopLevelBuildDesc.DestAccelerationStructureData
= pIUAVTopLevelAccelerationStructure->GetGPUVirtualAddress();
stTopLevelBuildDesc.ScratchAccelerationStructureData
= pIUAVScratchResource->GetGPUVirtualAddress();
stTopLevelBuildDesc.Inputs.InstanceDescs
= pIUploadBufInstanceDescs->GetGPUVirtualAddress();
}
// Build acceleration structure.
if (!bISDXRSupport)
{
// Set the descriptor heaps to be used during acceleration structure build for the Fallback Layer.
ID3D12DescriptorHeap* pDescriptorHeaps[] = { pIDXRUAVHeap.Get() };
pIDXRFallbackCMDList->SetDescriptorHeaps(
ARRAYSIZE(pDescriptorHeaps)
, pDescriptorHeaps);
pIDXRFallbackCMDList->BuildRaytracingAccelerationStructure(
&stBottomLevelBuildDesc
, 0
, nullptr);
pICMDList->ResourceBarrier(
1
, &CD3DX12_RESOURCE_BARRIER::UAV(
pIUAVBottomLevelAccelerationStructure.Get()));
pIDXRFallbackCMDList->BuildRaytracingAccelerationStructure(
&stTopLevelBuildDesc, 0, nullptr);
}
else // DirectX Raytracing
{
pIDXRCmdList->BuildRaytracingAccelerationStructure(
&stBottomLevelBuildDesc, 0, nullptr);
pICMDList->ResourceBarrier(
1
, &CD3DX12_RESOURCE_BARRIER::UAV(
pIUAVBottomLevelAccelerationStructure.Get()));
pIDXRCmdList->BuildRaytracingAccelerationStructure(
&stTopLevelBuildDesc
, 0
, nullptr);
}
上面的代码就是分别向结构体中指定我们创建的几个缓冲区的GPU指针,然后调用命令列表中的新的命令创建两层加速结构体,唯一要注意的就是一定是先创建Bottom-Level结构体,再创建Top-Level结构体。当然Fallback方式下,我们还需要指定描述符堆给命令列表。
最后记录了命令列表,我相信你应该想到该干什么了吧?对头那就是运行命令列表,让GPU为计算出加速结构体,后面的代码我就不在贴了,实在是看的很多,已经没什么营养了。
12、创建Raytracing Shader Table
接着一步重要的工作就是创建Raytracing Shader Table,也即要准备一份类似C++中的“函数指针数组”,只不过这里的函数指针指向的是Shader函数,即设置在Raytracing渲染管线状态对象中的Shader编译后的字节码中的函数指针,唯一的要求就是它们必须是连续排列的数组形式,其索引就是我们调用Raytracing Shader的函数“TraceRay”时指定的那个索引,这样TraceRay就知道当发生碰撞或未命中时该在那个数组中索引那个函数了。基于内存连续的要求,我就没有再使用DXR提供的辅助工具类来创建这个函数指针数组,而是直接分配一个共享内存堆(Upload Heap),然后使用定位(Placed)的方式创建每一个函数指针数组的具体Buffer。
在DXR中,这个Shader Table是三个数组,分别对应Ray Generation、Closest Hit、Miss的Shader函数组,其实其中的Ray Generation一般只有一个,因为根据我们之前讲过的光追渲染管线过程原理来看,光线最初发射这个过程只可能有一个,但是具体的TraceRay方法却可能有多处调用,所以只有“最近命中(Closest Hit)”函数和“未命中(Miss)”函数可能会有多个,尤其是对于复杂场景的复杂光照效果来说,最近命中函数可能还需要对应不同物体的不同光照效果,所以在TraceRay的参数中就有两个参数来索引到Closest Hit函数组,一个索引到不同的物体的加速体结构,另一个就是索引到最近命中组中的函数。最终实质上我们需要提供多个Closest Hit函数,并且在DXR中要求将这些函数组织成一个命名的组,即我们在之前创建Raytracing 渲染管线状态对象时,指定的Hit Group Name。这样实质上就是为Closest Hit 函数组设置了两道索引:第一道索引就是以Hit Group Name来区分不同的命中函数组,接着就是用TraceRay中的索引序号(第四个参数)索引到我们现在创建的函数指针数组中的具体位置处的函数。
当前我们的示例中,主要是为了简单起见所以每个过程都对应一个Shader函数,因此创建也就很简单了。示例代码如下:
void* pRayGenShaderIdentifier;
void* pMissShaderIdentifier;
void* pHitGroupShaderIdentifier;
// Get shader identifiers.
UINT nShaderIdentifierSize = 0;
if ( !bISDXRSupport )
{
pRayGenShaderIdentifier = pIDXRFallbackPSO->GetShaderIdentifier(c_pszRaygenShaderName);
pMissShaderIdentifier = pIDXRFallbackPSO->GetShaderIdentifier(c_pszMissShaderName);
pHitGroupShaderIdentifier = pIDXRFallbackPSO->GetShaderIdentifier(c_pszHitGroupName);
nShaderIdentifierSize = pID3D12DXRFallbackDevice->GetShaderIdentifierSize();
}
else // DirectX Raytracing
{
ComPtr<ID3D12StateObjectPropertiesPrototype> pIDXRStateObjectProperties;
GRS_THROW_IF_FAILED(pIDXRPSO.As(&pIDXRStateObjectProperties));
pRayGenShaderIdentifier = pIDXRStateObjectProperties->GetShaderIdentifier(c_pszRaygenShaderName);
pMissShaderIdentifier = pIDXRStateObjectProperties->GetShaderIdentifier(c_pszMissShaderName);
pHitGroupShaderIdentifier = pIDXRStateObjectProperties->GetShaderIdentifier(c_pszHitGroupName);
nShaderIdentifierSize = D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES;
}
D3D12_HEAP_DESC stUploadHeapDesc = { };
UINT64 n64HeapSize = 1 * 1024 * 1024; //分配1M的堆 这里足够放三个Shader Table即可
UINT64 n64HeapOffset = 0; //堆上的偏移
UINT64 n64AllocSize = 0;
UINT8* pBufs = nullptr;
D3D12_RANGE stReadRange = { 0, 0 };
stUploadHeapDesc.SizeInBytes = GRS_UPPER( n64HeapSize, D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT);//64K边界对齐大小
//注意上传堆肯定是Buffer类型,可以不指定对齐方式,其默认是64k边界对齐
stUploadHeapDesc.Alignment = 0;
stUploadHeapDesc.Properties.Type = D3D12_HEAP_TYPE_UPLOAD; //上传堆类型
stUploadHeapDesc.Properties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
stUploadHeapDesc.Properties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
//上传堆就是缓冲,可以摆放任意数据
stUploadHeapDesc.Flags = D3D12_HEAP_FLAG_ALLOW_ONLY_BUFFERS;
//创建用于缓冲Shader Table的Heap,这里使用的是自定义上传堆
GRS_THROW_IF_FAILED(pID3D12Device4->CreateHeap(&stUploadHeapDesc, IID_PPV_ARGS(&pIHeapShaderTable)));
// Ray gen shader table
{
UINT nNumShaderRecords = 1;
UINT nShaderRecordSize = nShaderIdentifierSize;
n64AllocSize = nNumShaderRecords * nShaderRecordSize;
GRS_THROW_IF_FAILED(pID3D12Device4->CreatePlacedResource(
pIHeapShaderTable.Get()
, n64HeapOffset
, &CD3DX12_RESOURCE_DESC::Buffer( n64AllocSize )
, D3D12_RESOURCE_STATE_GENERIC_READ
, nullptr
, IID_PPV_ARGS(&pIRESRayGenShaderTable)
));
pIRESRayGenShaderTable->SetName(L"RayGenShaderTable");
GRS_THROW_IF_FAILED(pIRESRayGenShaderTable->Map(
0
, &stReadRange
, reinterpret_cast<void**>(&pBufs)));
memcpy(pBufs
, pRayGenShaderIdentifier
, nShaderIdentifierSize);
pIRESRayGenShaderTable->Unmap(0, nullptr);
}
n64HeapOffset +=
GRS_UPPER(n64AllocSize, D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT); //向上64k边界对齐准备下一个分配
GRS_THROW_IF_FALSE( n64HeapOffset < n64HeapSize );
// Miss shader table
{
UINT nNumShaderRecords = 1;
UINT nShaderRecordSize = nShaderIdentifierSize;
n64AllocSize = nNumShaderRecords * nShaderRecordSize;
GRS_THROW_IF_FAILED(pID3D12Device4->CreatePlacedResource(
pIHeapShaderTable.Get()
, n64HeapOffset
, &CD3DX12_RESOURCE_DESC::Buffer(n64AllocSize)
, D3D12_RESOURCE_STATE_GENERIC_READ
, nullptr
, IID_PPV_ARGS(&pIRESMissShaderTable)
));
pIRESMissShaderTable->SetName(L"MissShaderTable");
pBufs = nullptr;
GRS_THROW_IF_FAILED(pIRESMissShaderTable->Map(
0
, &stReadRange
, reinterpret_cast<void**>(&pBufs)));
memcpy(pBufs
, pMissShaderIdentifier
, nShaderIdentifierSize);
pIRESMissShaderTable->Unmap(0, nullptr);
}
n64HeapOffset +=
GRS_UPPER(n64AllocSize, D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT); //向上64k边界对齐准备下一个分配
GRS_THROW_IF_FALSE(n64HeapOffset < n64HeapSize);
// Hit group shader table
{
UINT nNumShaderRecords = 1;
UINT nShaderRecordSize = nShaderIdentifierSize + sizeof(stCBModule);
n64AllocSize = nNumShaderRecords * nShaderRecordSize;
GRS_THROW_IF_FAILED(pID3D12Device4->CreatePlacedResource(
pIHeapShaderTable.Get()
, n64HeapOffset
, &CD3DX12_RESOURCE_DESC::Buffer(n64AllocSize)
, D3D12_RESOURCE_STATE_GENERIC_READ
, nullptr
, IID_PPV_ARGS(&pIRESHitGroupShaderTable)
));
pIRESHitGroupShaderTable->SetName(L"HitGroupShaderTable");
pBufs = nullptr;
GRS_THROW_IF_FAILED(pIRESHitGroupShaderTable->Map(
0
, &stReadRange
, reinterpret_cast<void**>(&pBufs)));
//复制Shader Identifier
memcpy(pBufs
, pHitGroupShaderIdentifier
, nShaderIdentifierSize);
pBufs = static_cast<BYTE*>(pBufs) + nShaderIdentifierSize;
//复制局部的参数,也就是Local Root Signature标识的局部参数
memcpy(pBufs, &stCBModule, sizeof(stCBModule));
pIRESHitGroupShaderTable->Unmap(0, nullptr);
}
这段代码最直观的理解就是我们先得到函数指针(GetShaderIdentifier),再得到函数指针大小(Fallback调用GetShaderIdentifierSize,DXR中是固定值D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES),然后根据这些信息以及函数个数分别创建每个Shader Table(理解做函数指针数组)的Buffer,最后将函数指针整齐的写入缓冲中即可。当然如果你再啰嗦一些,嫌弃Upload堆影响你的性能,那你就再创建一个显存堆(DEFAULT Type),进行“二次拷贝”放到显存中去。本例的代码中我就简单粗暴的创建了一个1M大小的上传堆(共享内存堆),然后以定位方式(Placed)创建了三个Shader Table的Buffer。
13、渲染!
终于所有啰嗦的准备过程都结束了,接着就是开始渲染循环了。与光栅化渲染类似,在渲染循环中,我们就是设置管线状态对象、设置根签名、设置各种资源的View(描述符堆)等,然后调用DXR的渲染命令:DispatchRays函数来渲染,这个函数就类似于光栅化渲染的DrawInstance函数(及其他几个Draw Call方法),其实它更类似DirectComputer中的Dispatch方法。当然调用该方法特殊的地方,就是需要填充一个结构体D3D12_DISPATCH_RAYS_DESC。具体渲染过程代码如下:
D3D12_DISPATCH_RAYS_DESC stDispatchRayDesc = {};
stDispatchRayDesc.HitGroupTable.StartAddress
= pIRESHitGroupShaderTable->GetGPUVirtualAddress();
stDispatchRayDesc.HitGroupTable.SizeInBytes
= pIRESHitGroupShaderTable->GetDesc().Width;
stDispatchRayDesc.HitGroupTable.StrideInBytes
= stDispatchRayDesc.HitGroupTable.SizeInBytes;
stDispatchRayDesc.MissShaderTable.StartAddress
= pIRESMissShaderTable->GetGPUVirtualAddress();
stDispatchRayDesc.MissShaderTable.SizeInBytes
= pIRESMissShaderTable->GetDesc().Width;
stDispatchRayDesc.MissShaderTable.StrideInBytes
= stDispatchRayDesc.MissShaderTable.SizeInBytes;
stDispatchRayDesc.RayGenerationShaderRecord.StartAddress
= pIRESRayGenShaderTable->GetGPUVirtualAddress();
stDispatchRayDesc.RayGenerationShaderRecord.SizeInBytes
= pIRESRayGenShaderTable->GetDesc().Width;
stDispatchRayDesc.Width = iWidth;
stDispatchRayDesc.Height = iHeight;
stDispatchRayDesc.Depth = 1;
pICMDList->SetComputeRootSignature(pIRSGlobal.Get());
pICMDList->SetComputeRootConstantBufferView(
2
, pICBFrameConstant->GetGPUVirtualAddress());
// Bind the heaps, acceleration structure and dispatch rays.
if (!bISDXRSupport)
{
pIDXRFallbackCMDList->SetDescriptorHeaps(1, pIDXRUAVHeap.GetAddressOf());
// Set index and successive vertex buffer decriptor tables
CD3DX12_GPU_DESCRIPTOR_HANDLE objIBHandle(
pIDXRUAVHeap->GetGPUDescriptorHandleForHeapStart()
, c_nDSHIndxIBView
, nSRVDescriptorSize);
pICMDList->SetComputeRootDescriptorTable(3, objIBHandle);
CD3DX12_GPU_DESCRIPTOR_HANDLE objUAVHandle(
pIDXRUAVHeap->GetGPUDescriptorHandleForHeapStart()
, c_nDSHIndxUAVOutput
, nSRVDescriptorSize);
pICMDList->SetComputeRootDescriptorTable(0, objUAVHandle);
pIDXRFallbackCMDList->SetTopLevelAccelerationStructure(
1
, pFallbackTopLevelAccelerationStructurePointer);
pIDXRFallbackCMDList->SetPipelineState1(pIDXRFallbackPSO.Get());
pIDXRFallbackCMDList->DispatchRays(&stDispatchRayDesc);
}
else // DirectX Raytracing
{
pIDXRCmdList->SetDescriptorHeaps(1, pIDXRUAVHeap.GetAddressOf());
// Set index and successive vertex buffer decriptor tables
CD3DX12_GPU_DESCRIPTOR_HANDLE objIBHandle(
pIDXRUAVHeap->GetGPUDescriptorHandleForHeapStart()
, c_nDSHIndxIBView
, nSRVDescriptorSize);
pICMDList->SetComputeRootDescriptorTable(3, objIBHandle);
CD3DX12_GPU_DESCRIPTOR_HANDLE objUAVHandle(
pIDXRUAVHeap->GetGPUDescriptorHandleForHeapStart()
, c_nDSHIndxUAVOutput
, nSRVDescriptorSize);
pICMDList->SetComputeRootDescriptorTable(0, objUAVHandle);
pICMDList->SetComputeRootShaderResourceView(
1
, pIUAVTopLevelAccelerationStructure->GetGPUVirtualAddress());
pIDXRCmdList->SetPipelineState1(pIDXRPSO.Get());
pIDXRCmdList->DispatchRays(&stDispatchRayDesc);
}
上面代码中有几个地方要注意,
首先没有明确的设置Bottom-Level加速结构体的指针,也没有明显的设置局部根签名对象,其实这些对于Raytracing渲染来说是“哑元”,分别隐藏在Top-Level加速体结构及RTPSO对象中,这样就省去了啰嗦的设置这些参数还要考虑各自对应关系的过程,其实创建它们的时候对应关系已经很明确了,也没必要再去设置一遍。
其次要注意的就是我们填充的D3D12_DISPATCH_RAYS_DESC结构体中,实质上主要是Shader Table和窗口大小信息,这其实是一个暗示,即在复杂场景的渲染中,我们可以分别指定不同的Shader Table来渲染不同的物体(当然还要指定几个Instance掩码),同时通过指定不同的窗口信息(Width、Height、Depth)进行多显卡的渲染。
最后一个要注意的就是在D3D12_DISPATCH_RAYS_DESC结构体中Ray Generation Shader Table没有Stride参数,即它是没有宽度(函数指针数组中的元素大小值)的,其实这就是明确的告诉我们Ray Generation过程的Shader函数只有一个。
Raytracing渲染结束之后,我们就是进行了一个简单的复制引擎上的纹理拷贝方法,与我们之前的教程渲染到纹理中的过程就很类似了,我就不再多啰嗦了。需要注意的是,与官方Raytracing例子不同,本示例中设置交换链后缓冲的资源屏障是直接从可提交状态变更到复制目的状态的,反之亦然。当然我们还需要与传统光栅化渲染进行复杂的混合渲染时,那么就需要像官方示例中那样乖乖的先从复制目标状态转换到可渲染状态,再转换到可提交状态。看不懂我在说什么的,请这回去复习之前讲解的资源屏障的那几章教程复习一下,还不明白的请评论发布问题,看到我会回复。
14、后记
最后DXR渲染编程的基础教程算总算是结束了,当然因为我目前的设备限制,以及水平能力限制等问题,错漏在所难免,也希望各位能够积极批评斧正,并不吝赐教。
目前本章全部示例我放在了一个单独的Github项目中:GRSDXRSamples,大家可以自由下载学习,有什么问题都可以直接在Github或本章之后留言垂询。
下一阶段看我能否有机会更换新的笔记本之后,再继续该系列教程,争取把DXR光追渲染讲透,并看看有没有可能给大家带来独显+核显的多显卡的光追渲染示例,因为fallback实在是太恶心了,无论性能、编程难度、以及效果真的让人很崩溃。