DirectX12(D3D12)基礎教程(十)——DXR(DirectX Raytracing)基礎教程(下)

本文接上篇:DirectX12(D3D12)基礎教程(十)——DXR(DirectX Raytracing)基礎教程(上)


目錄

5、C/C++代碼中的其它準備工作

6、DXR編程的基本框架

7、枚舉高性能適配器(顯卡)和創建DXR設備

7.1、枚舉高性能適配器(顯卡)

7.2、判斷系統DXR能力支持程度

7.3、創建DXR設備和命令列表

7.3.1、使用DXR Fallback兼容設備

7.3.2、使用純DXR設備

8、創建UAV和SRV堆

8.2、創建UAV

8.2、創建SRV堆

9、創建DXR根簽名

9.1、創建“全局根簽名(Global Root Signature)”

9.2、創建“本地(局部)根簽名(Local Root Signature)”

10、創建Raytracing渲染管線狀態對象

10.1、Raytracing渲染管線狀態子對象

10.2、填充Raytracing渲染管線狀態結構體

10.3、Raytracing渲染管線狀態對象及子對象內存拓撲

10.4、創建Raytracing渲染管線狀態對象

11、創建加速結構體

11.1、獲取加速結構體預處理信息

11.2、創建放置加速結構體的緩衝

11.3、創建UAV和實例緩衝區

11.4、創建加速結構體

12、創建Raytracing Shader Table

13、渲染!

14、後記


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 {
  D3D12_STATE_OBJECT_FLAGS Flags;
} D3D12_STATE_OBJECT_CONFIG;

CD3D12_STATE_OBJECT_CONFIG_SUBOBJECT

D3D12_STATE_SUBOBJECT_TYPE_GLOBAL_ROOT_SIGNATURE

定義將與相關着色器一起使用的全局根簽名狀態子對象。

typedef struct D3D12_GLOBAL_ROOT_SIGNATURE {
  ID3D12RootSignature *pGlobalRootSignature;
} D3D12_GLOBAL_ROOT_SIGNATURE;

CD3D12_GLOBAL_ROOT_SIGNATURE_SUBOBJECT

D3D12_STATE_SUBOBJECT_TYPE_LOCAL_ROOT_SIGNATURE

定義將與相關着色器一起使用的本地根簽名狀態子對象。

typedef struct D3D12_LOCAL_ROOT_SIGNATURE {
  ID3D12RootSignature *pLocalRootSignature;
} 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 {
  D3D12_SHADER_BYTECODE DXILLibrary;
  UINT                  NumExports;
  D3D12_EXPORT_DESC     *pExports;
} D3D12_DXIL_LIBRARY_DESC;

CD3D12_DXIL_LIBRARY_SUBOBJECT

D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_SHADER_CONFIG

表示着色器配置的狀態子對象。

typedef struct D3D12_RAYTRACING_SHADER_CONFIG {
  UINT MaxPayloadSizeInBytes;
  UINT MaxAttributeSizeInBytes;
} D3D12_RAYTRACING_SHADER_CONFIG;

CD3D12_RAYTRACING_SHADER_CONFIG_SUBOBJECT

D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_PIPELINE_CONFIG

表示光線跟蹤管道配置的狀態子對象。

typedef struct D3D12_RAYTRACING_PIPELINE_CONFIG {
  UINT MaxTraceRecursionDepth;
} D3D12_RAYTRACING_PIPELINE_CONFIG;

CD3D12_RAYTRACING_PIPELINE_CONFIG_SUBOBJECT

D3D12_STATE_SUBOBJECT_TYPE_HIT_GROUP

描述可以包含在狀態對象中的光線跟蹤擊中組狀態子對象。

typedef struct D3D12_HIT_GROUP_DESC {
  LPCWSTR              HitGroupExport;
  D3D12_HIT_GROUP_TYPE Type;
  LPCWSTR              AnyHitShaderImport;
  LPCWSTR              ClosestHitShaderImport;
  LPCWSTR              IntersectionShaderImport;
} D3D12_HIT_GROUP_DESC;

CD3D12_HIT_GROUP_SUBOBJECT

D3D12_STATE_SUBOBJECT_TYPE_NODE_MASK

標識應用狀態對象的GPU節點的狀態子對象。

typedef struct D3D12_NODE_MASK {
  UINT NodeMask;
} D3D12_NODE_MASK;

CD3D12_NODE_MASK_SUBOBJECT

D3D12_STATE_SUBOBJECT_TYPE_EXISTING_COLLECTION

描述可以包含在狀態對象中的現有集合的狀態子對象。

typedef struct D3D12_EXISTING_COLLECTION_DESC {
  ID3D12StateObject *pExistingCollection;
  UINT              NumExports;
  D3D12_EXPORT_DESC *pExports;
} D3D12_EXISTING_COLLECTION_DESC;

CD3D12_EXISTING_COLLECTION_SUBOBJECT

D3D12_STATE_SUBOBJECT_TYPE_SUBOBJECT_TO_EXPORTS_ASSOCIATION

將直接在狀態對象中定義的子對象與着色器導出相關聯。

typedef struct D3D12_SUBOBJECT_TO_EXPORTS_ASSOCIATION {
  const D3D12_STATE_SUBOBJECT *pSubobjectToAssociate;
  UINT                        NumExports;
  LPCWSTR                     *pExports;
} 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 {
  LPCWSTR SubobjectToAssociate;
  UINT    NumExports;
  LPCWSTR *pExports;
} 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實在是太噁心了,無論性能、編程難度、以及效果真的讓人很崩潰。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章