目錄
3.2.1、渲染目標Unordered Access 2D紋理變量
3.5、系統變量內嵌函數(Raytracing HLSL System Value Intrinsics)
4、Raytracing Shader編譯和Shader中包含頭文件的技巧
1、前言
經過了近一個多月的折騰之後,最終在我去蓬萊島附近出差的過程中,終於搞定了DXR的第一個演示的例子。當然不排除可能是因爲在蓬萊仙島粘了仙氣,才取得了突破性進展。整個過程還算正常,但是使用fallback庫簡直是讓人痛不欲生,不過很幸運,這個該死的怪物還是被降服了。也怪自己囊中羞澀,有不起RTX20系顯卡加持的電腦。
本次教程示例程序運行效果如下:
大家可以使用上下左右方向鍵來控制光源位置,看看初級光線追蹤光線反射的效果。注意本質上講,本示例中的光照模型依然使用的是光柵化渲染中的環境光+漫反射光模型,主要是爲了先讓大家理解整個框架,這裏先不引入複雜的光照模型。
首先建議大家先閱讀我的博客文章《光線追蹤渲染(RayTracing Render)核心原理詳解》之後,再來閱讀學習本篇教程。篇幅的原因很多太理論化的東西我就不過多囉嗦了,本篇教程我們將集中精力在具體實在的編程方面,也就是大家常說的“乾貨”上。同時也建議大家也一定閱讀了本系列教程中之前的系列文章,因爲D3D12編程的基本框架依然適用於DXR編程,D3D12的基本編程技巧我也就不在囉嗦贅述了。
當然作爲基礎教程,我們的例子還是以簡單爲原則,當然依舊是幾乎沒有什麼Class的C-Style的線性示例程序。同時比起復雜的光柵化渲染來說,光追渲染的過程理解起來也比較線性化。甚至可以這樣認爲,只要你線性代數基礎好,光追渲染的過程中基本不會遇到太大的障礙。但是要警惕的是DXR和現在的fallback庫可能反倒把事情搞複雜了。當然如果你有至少GTX10系以上的顯卡的話,就不用理會什麼fallback庫了,複雜度會下降一大截,真正是人民幣玩家的體驗。
另外在這裏告訴大家一個好消息:
參加中國DXR光線追蹤開發者大賽,贏取NVIDIA RTX™顯卡!
英偉達聯合微軟,Epic遊戲,NExT Studios一起,爲大家帶來中國DXR光線追蹤開發者大賽。遊戲開發者與內容創作者可以利用Microsoft® DirectX® 12光線追蹤的新特性,提交光追作品,贏取NVIDIA RTX™顯卡大獎!
詳情請見:https://developer.nvidia.com/DXR-spotlight
如您有興趣報名,或希望瞭解更多詳情,請郵件聯繫[email protected]獲得支持。
將從比賽中選出優勝者,每位優勝者都將獲得:
•NVIDIA RTX™顯卡
•來自NVIDIA,微軟,Epic Games,NExT Studios的開發者技術支持
•在NVIDIA,微軟,Epic Games,NExT Studios的社交媒體渠道上展示成果。
如何參賽
●使用Microsoft® DirectX® 12和DXR創建實時光線追蹤技術Demo。內容創作者/遊戲開發者必須使用到實時光線追蹤反射,實時陰影,實時GI的特性。
●將你的參賽作品遞交到:https://developer.nvidia.com/dxr-contest-submission
o所有參賽作品的遞交截止日期爲:2019年10月31日晚11:59
o遞交必須包含一個至少30秒的視頻片斷,以及技術demo下載地址
o遊戲開發者需要提供一段簡述,形容自己如何在技術demo中使用了微軟DX12和DXR,以及實時光線追蹤反射,GI,陰影與/或AO的特性。
OK,如果各位有興趣參加,有什麼技術問題都可以隨時留言或微信QQ找我:41750362,本人將提供免費技術支持。當然不要問我參不參賽,水平實力有限就不去獻醜了,請諒解!
言歸正傳,下面就讓我們正式開始DirectX Raytracing(簡寫爲DXR)之旅吧!
2、準備工作
目前因爲我的硬件條件限制,所以在準備最簡例子的過程中,不得不使用Fallback庫來運行演示。其實本質上說,Fallback庫就是用DirectComputer能力來模擬帶硬件加速的DXR。因爲我的顯卡只是可憐的GTX965m,無法直接創建DXR設備及相關接口,所以只能使用Fallback庫來模擬。
當然Fallback的使用對於初學者來說簡直就是噩夢,幸運的是,我居然成功的馴服了這個大概是印度程序員寫的怪獸。現在將過程分享給大家,方便我們在沒有直接硬件DXR支持的情況下,能夠成功的編寫一些實時光追的示例程序,以便儘快的掌握DXR的編程技巧。
作爲準備工作,第一步首先你要搞明白的就是將我們需要的庫和其它相關資源統統複製到你的項目文件夾中,找到fallback庫的目錄,也就是DirectX-Graphics-Samples下的Libraries和Packages目錄,如下圖所示:
以及Tools目錄:
然後把這三個目錄都複製到你的項目中,如下圖所示:
這裏要注意的就是Libraries我們只複製了D3D12RaytracingFallback一個文件夾,其它的暫時不需要。
文件夾放好之後,需要在項目中加入Fallback的工程,如下圖所示:
並且在主項目中首先引用fallbakclayer項目,如下圖:
這樣最終在編譯中就會自動複製和鏈接fallback的lib。
這些基本工作做好之後,就需要對項目的各種屬性和目錄進行更改設置。首先需要修改項目引用的Windows SDK包,如下圖所示:
接着需要對fallbacklayer項目的生成事件路徑做適當修改:
經過這些路徑修改設置後,生成文件的路徑和位置就一致了,當然這些路徑主要使用VS IDE的預定義宏來設置,這樣整個項目複製粘貼到別的地方就依然能夠正常使用。
最後一個需要注意的問題,就是因爲fallbacklayer項目使用了PIX支持,所以我們還需要把剛纔複製的packages進行一下導入。這個導入使用NuGet,操作如下圖所示:
就是在整個解決方案目錄節點的右鍵菜單中點擊圖中箭頭所示的NuGet包管理菜單,然後在彈出的界面中做如下設置:
在這個NuGet包管理對話框中,點擊加號按鈕,將我們開始複製到項目目錄裏的packages中的WinPixEventRuntime.1.0.180612001.nupkg包文件加入項目引用包中,這樣在解決方案的根目錄下就會生成一個Packages的包文件夾,裏面就是我們導入的WinPix支持包了,這樣編譯fallbacklayer項目就沒有什麼問題了。
至於NuGet的進一步說明和使用方法介紹我就不多囉嗦了,大家可以去百度一下就明白了了。
以上這些準備工作,我主要是靠圖和簡明扼要的說明介紹一下,大家遇到什麼問題可以留言垂詢。因爲對於有條件的網友來說,fallback基本上可以不用理會了,所以我們這裏就簡單的介紹一下,以便有跟我一樣的網友使用老設備想嚐鮮,可以方便的引用fallback庫了。
在最新的DXR示例中,實質上已經刪除了fallback的引用,直接使用純DXR演示了,所以對於最新的DXR Samples,如果不是GTX10xx系或RTX20xx系以上的顯卡,就沒法直接運行了,這個大家要注意。
對於找不到這些包的網友不用着急,本章教程所有的示例我已經放在了GitHub上免費開放了(GRSDXRSamples),大家可以隨時Clone,下載自己調試運行學習。
3、Raytracing Shader
在我的博客文章《光線追蹤渲染(RayTracing Render)核心原理詳解》中我已經簡單介紹了Raytracing Shader的基本框架。
本章教程中,我們直接使用微軟官方例子D3D12RaytracingSimpleLighting項目中的Raytracing.hlsl。當然爲了教程統一風格需要,其中做了一些變量名替換。
這裏我詳細介紹下該Shader中的變量和函數,也算是讓大家初步掌握Raytracing Shader的基本編寫方法。完整代碼請大家到GitHub下載後自行查看,我就不貼完整的源碼了。
因爲現在使用實時光追渲染之後,本質上整個渲染管線都緊密圍繞Raytracing Shader展開了(其實光柵化也是,只是光柵化固定的階段較多,代碼中要做的工作也比較多,所以光柵化部分就主要圍繞C /C++代碼部分進行了詳細講解),所以DXR編程的框架實際也是圍繞讓管線運行起來而展開的。
基於此,在這裏就先來學習下Raytracing Shader的基本框架和光追計算的核心思想,這種安排與之前的教程有所區別。當然前提就是至少你已經閱讀了我之前的系列教程,對D3D12接口編程已經有了比較全面整體的認知,尤其要掌握基本的設備創建、命令隊列、命令列表、根簽名、管線狀態對象、網格加載、紋理加載、採樣器、資源屏障、同步圍欄等對象的概念和基本編程方法,最好進一步對D3D12內存管理有較深刻的認識。不明白的話,建議你先暫停,折回頭去看下之前的教程,再來這裏繼續學習。
注意:前方高能警告!
3.1、Raytracing Shader整體框架介紹
首先,從整體上看,光追渲染的Shader程序框架與DirectComputer Shader比較接近。
其實從本質上說,光追渲染計算更加的偏“自由計算”化,原理上是不斷的計算生成光線(射線),然後檢測光線與物體(AABBs)及其表面三角形碰撞的情況,然後根據碰撞點的三角形重心座標,調用對應的各種“光照”算法(BRDFs),最終生成像素點(也就是光線起點)顏色的過程(一般是取n個計算顏色值結果的算數平均值)。
在實時光追渲染中已經沒有光柵化渲染過程中的那些比較固定的計算階段了。比如非常關鍵的光柵化(Rasterizer)過程,在傳統的光柵化渲染框架或管線中就純粹固化到硬件上了(當然DX支持你自己使用軟件實現一個光柵化模塊,對於一些更高級更靈活的光柵化渲染來說這種方式的誘人之處就是“可編程”)。由於這些相對固化的階段,將整個光柵化渲染管線分成了若干個階段(Stages),也就形成了光柵化渲染管線的基本框架。當然在現代的光柵化渲染管線中很多階段已經可以編程了,所以光柵化渲染管線也被稱爲“可編程管線”(注意不是“全編程管線”,目前無論光柵化渲染管線還是實時光追渲染管線都沒有做到“全編程管線”。非實時光追渲染管線則是另一回事了。)。
甚至在更早期,3D顯卡(那時候甚至還沒有GPU的概念)上將整個光柵化渲染過程全部固化在了芯片中,相對形成了比較專用的加速卡的形式。這樣做的目的其實無外乎幾個目的,首先就是爲了性能,那時甚至每秒能渲染多少三角形成了衡量3D加速卡的關鍵性能指標之一。其次就是整個光柵化的渲染思路就是不斷的剔除多餘的三角形,最終目標就是隻渲染能“看”到的少量三角形,並且使用簡化的光照模型,通過修改一些參數(比如:高光係數、漫反射光顏色,環境光顏色值等)的方式來最終決定屏幕像素的顏色,從而形成所謂的“3D渲染的畫面”。
後來隨着芯片運算能力的提高,逐步出現了可以編程的一些管線階段,比如著名的Vertex Shader 和Pixel Shader,在其中3D程序員可以通過純粹編程的方式來局部控制整個光柵化渲染的過程。但是整體來看那時3D渲染管線和過程是相對固定的。甚至彼時這些可編程的渲染階段:VS、HS、DS、GS、PS等都有一些根本上的限制。比如VS中你就只能計算當前傳入的那個頂點,只有GS有生成新頂點和新幾何體的能力,而更高級的陰影實現甚至需要所謂的多趟渲染+蠟板來實現。複雜度成幾何級數級增加。
而在GPU計算能力爆炸式增長的今天,實時光追渲染也成爲了可能。因爲在實時光追渲染的過程中,只有極少數的相對固定的計算過程,也就是說很少能通過調整幾個參數來控制或調節整個渲染過程了。更直接的說,光追渲染過程本質是一個必須進行“通用計算”的過程,而不能像傳統的光柵化渲染那樣簡單的實現“參數化”設計,它更偏向於需要“自由編程”能力了,比如爲了極致的渲染效果傳統的固化的BRDFs效果,就可能需要實時的蒙特卡洛積分計算來模擬了。
因此最終Raytracing Shader框架與用於通用計算的DirectComputer Shader框架就很類似,這也很容易理解了。二者都是需要“通用計算”的“自由編程”的能力。
這樣與DirectComputer Sheder相類似,Raytracing Shader一級結構我們可以理解爲像下圖所示:
Raytracing Shader的較詳細的基本架構如下:
全局變量定義 |
包含文件(#include) |
渲染目標(RWTexture2D) |
|
網格數據(Vertex、Index) |
|
常量緩衝(ConstantBuffer) |
|
加速結構(RaytracingAccelerationStructure) |
|
基本碰撞(命中檢測)函數 |
其它輔助工具函數 |
光線發射函數([shader("raygeneration")]) |
|
最近碰撞函數([shader("closesthit")]) |
|
未碰撞函數([shader("miss")]) |
當然作爲最一般的光追渲染來說,這個框架基本已經滿足需要了。其它更復雜的元素,後續的教程中遇到時我們再逐個介紹。目前的應用來說這已經足夠了。
3.2、全局變量
3.2.1、渲染目標Unordered Access 2D紋理變量
在Raytracing Shader中,我們首先要定義的變量就是:
RWTexture2D<float4> g_RenderTarget : register(u0);
這個變量代表整個實時光追渲染的輸出畫面,與傳統的光柵化渲染不同,這裏實質上是一個純粹的渲染到紋理的方式。只是這個紋理我們使用的是一個可“Unordered Accesses(無序訪問 或 隨機訪問)” 讀寫的2D紋理。
如何理解這個設計要求呢?那麼首先渲染到2D紋理很容易理解,因爲從本質來講,最一般的光柵化渲染到交換鏈的後緩衝區,其實也就是渲染到一個2D紋理。而比較難理解的就是爲什麼非得是Unordered Access的紋理呢?這其實也是兩種渲染方式巨大的差異導致的結果。傳統的光柵化渲染在光柵化階段,以及後續的Pixel Shader像素着色階段,其實每個像素的顏色基本都是在同一個Shader計算過程(或理解爲幾乎相同的同一個Shader函數調用路徑)中決定的,因此可以簡單形象的理解爲一個線程(GPU線程)操作一個內存單元格,沒有什麼特殊的地方。在最終寫入像素顏色值時基本也是“同時”寫入每個像素的。這種情景,你可以形象的想象一排排列非常整齊的射手,以相同的姿勢,同時舉槍射擊各自面前的靶子,在發令官一聲令下後,大家幾乎同時開槍,然後子彈幾乎同時射中靶子的情形。
而在實時光追渲染中,那麼決定每個像素的最終顏色的Shader可能就不是一個了,或者說Shader函數及調用路徑基本都不一樣了,主要是因爲我們現在執行的是光線的動態追蹤,看過我的《光線追蹤渲染(RayTracing Render)核心原理詳解》之後,我們知道光追渲染其實就是每個像素都朝一個視錐體內的特定方向“發射”一條光線(射線),在光線不斷碰撞-反射-折射的過程中到達光源後再調用不同的Shader計算決定最終的顏色值,因此一個像素最終的顏色可能會跨越不同的函數及計算路徑得到,所以可能每個像素最終被着色的時間點也會出入很大,導致最終寫入每個像素的顏色值的時機基本都是“隨機的”,因爲每條光線的路徑都可能是不同的。同時按照現代GPU對於顯存的近乎嚴苛的管理要求,我們必須明確的告訴GPU渲染目標2D紋理是需要“隨機訪問”的,這就是Unordered Access形式2D紋理作爲實時光追渲染目標的全部意義。這種情形可以與之前的例子對應想象爲在一個真實的戰場上,士兵都分散在近乎隨機分佈的散兵坑裏射擊,開槍的時機,子彈的路徑,射擊目標的類型、射擊的時機、射中沒射中等等都幾乎是隨機的一樣。
當然這也是典型的DirectComputer計算結果緩衝需要的類型形式。
總之,本質上g_RenderTarget這個變量代表一塊紋理(也就是一塊顯存,放在“默認堆”上),但與普通的只讀紋理不同,它是需要可讀寫的,同時要求是可以隨機訪問的,這裏的隨機訪問是針對GPU線程而言的。其基本訪問單位是float4,即渲染結果圖片上的每個像素點的最終顏色值。它的實質大小就在C++代碼中設定,邏輯大小(像素數)一般是窗口的大小iWidth * iHeight,字節大小就是iWidth *iHeight*4*sizeof(float)。因爲它放在默認堆也就是顯存中,所以GPU訪問速度很高,因爲需要隨機讀寫,比GPU訪問一般的只讀紋理速度要慢一些,但快過訪問共享內存中的上傳堆中緩衝的速度。
3.2.2、三角形網格變量
接下來的兩個變量:
ByteAddressBuffer g_Indices : register(t1, space0);
StructuredBuffer<ST_GRS_VERTEX> g_Vertices : register(t2, space0);
就是我們需要渲染的物體的網格數據,一般就是三角形網格數據,即三角形頂點數組及其對應的索引數組。
在傳統的光柵化渲染中,因爲渲染管線設計的相對固化,三角形網格數據傳入渲染管線都是通過專門的函數: IASetVertexBuffers、IASetIndexBuffer等來設置並傳入的,而且在代碼層面我們還要設置網格數據格式,通過管線對象結構體成員D3D12_GRAPHICS_PIPELINE_STATE_DESC::InputLayout 以及函數IASetPrimitiveTopology等來設定,同時這也是底層驅動和硬件直接支持的,性能上就有一些優勢。當然換個角度來看這其實也是一種束縛和限制,也體現出傳統光柵化渲染框架的要求下,其實從硬件開始就對數據類型做了非常細緻的劃分,而劃分的目的無非就是爲了限制不必要的數據計算擴展,比如早期你不能使用紋理來上傳頂點數據,你更不能說在頂點數據中傳入紋理數據,或者說作用於紋理上的指令是不能操作頂點數據的反之亦然。這些其實都是爲了簡化指令設計,從而最終提高性能的設計。因爲歸根結底,GPU是一個大的SIMD架構的處理芯片,高並行,超大數據量吞吐纔是其終極目的。
現在隨着GPU指令集加強,數據類型處理限制的逐步解除,以及處理能力的不斷提升,尤其是GPU“通用計算”能力的不斷提升,使得實時光追也成爲可能,我們就可以在Raytracing Shader中以緩衝區的方式直接簡單的傳入網格頂點的數據,甚至於網格頂點數據的整體數據結構都由代碼和Shader自行負責,我們不需要過多的額外的編程限制了。比如我們不需要反覆的通過InputLayout結構體數組與GPU溝通網格頂點的數據結構了。
總之,ByteAddressBuffer類型是個Shader的內置的數據類型,其含義就是BYTE*,甚至我們可以將其理解爲VOID*,也就是說這種緩衝裏的數據我們可以按照以字節爲單位大小隨意訪問,這樣我們傳入的網格索引數組,就可以按我們需要來訪問了。後續我們在介紹函數時還會詳細介紹這個緩衝區。其大小則是由代碼中指定的。這也提現了Raytracing Shader在編碼方面的的巨大靈活性。當然我們還是需要指定是從哪個寄存器組傳入的,後面的register(t1, space0)語義說明我們依然是從紋理的寄存器通道1(實質是第二個寄存器,因爲有0序號寄存器是第一個,與C/C++中數組下標類似,從0開始)上傳頂點索引數據,此時我們也發現現在所謂“紋理”數據類型其實質可代表的類型已經大大的豐富了,當然對應的操作指令也豐富了,使得我們現在都可以直接傳入BYTE*這種“極端自由”的數據。
緊接着頂點索引的是StructuredBuffer<ST_GRS_VERTEX>類型定義的頂點數據數組g_Vertices,那麼這個類型定義有些像C++中的模板實例化的語法,即我們可以將StructuredBuffer認爲是一個純數組容器模板類,而在Shader中它被我們用自定義的頂點數據類型ST_GRS_VERTEX結構體實例化了,這樣它其實要表達的意思就是ST_GRS_VERTEX g_Vertices[],也就是網格頂點數據的數組。而register(t2, space0)語義文法則跟剛纔一樣說明頂點數組是從“第三個紋理寄存器”傳入的。
3.2.3、常量緩衝
接下來的兩個常量結構體的定義:
ConstantBuffer<ST_SCENE_CONSANTBUFFER> g_stSceneCB : register(b0);
ConstantBuffer<ST_MODULE_CONSANTBUFFER> g_stModuleCB : register(b1);
與我們在一般的VS或PS中定義常量緩衝的方式有些不同,一般在我們之前的例子中都像下面這樣來定義常量緩衝區:
cbuffer MVPBuffer : register(b0)
{
float4x4 m_MVP;
};
其實兩種定義方法的含義是一樣的,只是在Raytracing Shader中我們使用的是類似DirectComputer中的常量緩衝的定義方法。從文法上我們可以將ConstantBuffer<ST_SCENE_CONSANTBUFFER>理解爲一個模板實例化類型定義。只是這裏ConstantBuffer是個單例實例化,即它只實例化一個結構體爲常量緩衝區,並不像其他的Buffer類型那樣實例化成數組,這裏可以直接的理解爲類似C/C++代碼中的定義:ST_SCENE_CONSANTBUFFER g_stSceneCB;。當然常量緩衝區使用的寄存器就是b族寄存器了。
在這裏常量緩衝區稍微做了一些區分,即第一個常量緩衝區是全局可見的,即所有的光追階段Shader函數都可以訪問,我們後續將在代碼中在全局根簽名中聲明並傳入。而第二個則是局部可見的,即在我們目前的Shader裏只是在檢測到碰撞之後的Shader函數中才能夠訪問。
3.2.4、加速結構體變量
在接下來的全局變量定義:
RaytracingAccelerationStructure g_asScene : register(t0, space0);
這個就是Raytracing Shader中特有的一個結構化緩衝區了,即我們在《光線追蹤渲染(RayTracing Render)核心原理詳解》一文中給大家介紹過得加速體結構的緩衝區。這個變量的定義其實在我們的Raytracing Shader中可以看做是一個“啞元”,即我們只是聲明它,幾乎不直接在我們的Shader中“顯式”的操作它,而最終操作它的就是驅動和GPU(或 fallback庫)。關於它的進一步的知識我們後續在創建和上傳該緩衝時,在詳細介紹。在Raytracing Shader中這幾乎是唯一一個“黑盒式”的緩衝區,即我們不知道其具體結構,更無法操作其內部元素,當然我們也無需知道這些。
3.3、基本光線追蹤渲染過程框架
與傳統的光柵化渲染管線不同,實時光追渲染過程(或者稱之爲光追渲染管線)在過程上要簡單的多。整體上如下圖所示:
1、圖中每個深灰色背景塊都代表一個完整獨立的Shader函數過程(包括子函數),綠色部分表示有硬件加速的過程。
2、圖中:
這一部分實際是光追渲染過程中相對固定的部分,可以理解爲光柵化渲染中的完全硬件固化的光柵化過程。而其中的Any Hit以及Intersection兩個過程是可編程的部分,如果不指定專門的Shader函數的話,它們就執行默認的碰撞檢測過程,可以理解爲是先檢測是否與物體的AABBs相交,接着檢測與物體上的某個三角形相交,可以理解爲就是一個“Pick(拾取)”的過程。
3、RayGeneration函數中實際要調用的最重要的Raytracing Shader內置方法就是:TraceRay(實際發射光線的函數,後面詳細介紹),所以爲了跟其他的自定義名稱的方法區別這個方法的名字在圖中使用了斜體標識。
4、圖中實線箭頭是一次光線的路徑,也就是主要光追渲染過程,而虛線箭頭表示的則是二次以上的光線(當然也要走一遍實線的過程),也就是說在Miss或Closest Hit的過程中還可以繼續重複調用TraceRay()方法再次發射出光線,這通常用於高級渲染效果的情況,如:反射、陰影、折射、透射等。
通常在這兩個方法(Miss或Closest Hit)中發射的光線(射線)就被稱爲“二次光線”或“高次光線”。一般情況下,在實時光追渲染中,使用到二次光線時已經可以有較高渲染質量了,爲性能考慮不建議再生成更高次的光線(這是與一般光追渲染的區別,一般渲染光追中都會有大量的高次光線)。當然最終這又是一個需要在渲染質量和性能之間折中考慮的編程問題。
下面就讓我們來認識每一個具體的Shader函數都是幹嘛的。
3.4、光線發射函數(Ray Generation)
在本章教程的Shader中,我們定義的光線發射函數如下:
3.4.1、MyRaygenShader函數
[shader("raygeneration")]
void MyRaygenShader()
{
float3 rayDir;
float3 origin;
// Generate a ray for a camera pixel corresponding to an index from the dispatched 2D grid.
GenerateCameraRay(DispatchRaysIndex().xy, origin, rayDir);
// Trace the ray.
// Set the ray's extents.
RayDesc ray;
ray.Origin = origin;
ray.Direction = rayDir;
// Set TMin to a non-zero small value to avoid aliasing issues due to floating - point errors.
// TMin should be kept small to prevent missing geometry at close contact areas.
ray.TMin = 0.001;
ray.TMax = 10000.0;
RayPayload payload = { float4(0, 0, 0, 0) };
TraceRay(g_asScene, RAY_FLAG_CULL_BACK_FACING_TRIANGLES, ~0, 0, 1, 0, ray, payload);
// Write the raytraced color to the output texture.
g_RenderTarget[DispatchRaysIndex().xy] = payload.color;
}
1、函數“MyRaygenShader”,是必須自定義的“光線產生函數”,其名稱可以是任意合法的標識符名稱,當然你需要在代碼中也知道他們的名字。這個函數對應實時光追渲染管線圖中開始的Ray Generation函數。
2、這個函數頂部的[shader("raygeneration")]是個函數語義文法,用於標識其後的函數定義就是光追渲染的Ray Generation方法。當然這也是爲了告訴Raytracing Shader編譯器、DXR接口以及顯卡驅動和對應硬件的一個標識,即被該語義修飾的函數就是“光線發生函數”。每個GPU線程(或者理解爲單個流處理器)在執行實時光追渲染的過程時,就知道需要從這個方法開始執行(類似C/C++函數中的main函數)。
3、光線發生函數(Ray Generation)的一個核心工作就是計算光線方向,從而生成光線(射線)的向量方程,而後續的計算就利用這個射線向量方程來計算碰撞、碰撞點,從而檢索一個Shader Table列表(實質是Shader函數列表)中對應的Shder(函數)進一步計算顏色值。
4、該函數首先取得像素點座標,其次計算出對應像素點在攝像機座標中的位置向量,然後在根據攝像機位置向量,計算出光線的方向。
根據光追基本原理,光線方向向量(Rey Dir)=像素點位置向量(Pixel Pos)- 攝像機位置向量(Camera/Eye Pos)。根據向量減法的規則,最終光線方向就從攝像機位置指向屏幕像素方向。原理示意圖如下:
而光線(射線 Ray)的起點就是屏幕像素位置向量(大家一定要注意我這裏就沒有再區分表示點的座標和向量之間的區別了,具有請參考光追原理一文)。這個計算過程更形象的如下圖所示:
5、有了光線方向,那麼接下來函數中拼裝了一個光線方程(射線方程),其實就是我們說過的方程:Ray = Origin +t*Dir(TMin < t < TMax)。當然在代碼中它是通過填充一個RayDesc結構體構建的。然後再聲明一個PayLoad(光追負載,主要就是最終像素的顏色)的自定義結構體,最終調用TraceRay函數發射光線,(該函數是個“同步函數”,後面會詳細說明),調用返回後,負載中就是像素點的顏色值,我們賦給對應像素的紋理單元即可。
3.4.2、GenerateCameraRay函數
在MyRaygenShader函數中,通過調用子函數GenerateCameraRay計算得到光線(射線的方程)的起點和方向,該函數定義如下:
inline void GenerateCameraRay(uint2 index, out float3 origin, out float3 direction)
{
float2 xy = index + 0.5f; // center in the middle of the pixel.
float2 screenPos = xy / DispatchRaysDimensions().xy * 2.0 - 1.0;
// Invert Y for DirectX-style coordinates.
screenPos.y = -screenPos.y;
// Unproject the pixel coordinate into a ray.
float4 world = mul(float4(screenPos, 0, 1), g_stSceneCB.m_mxP2W);
world.xyz /= world.w;
origin = g_stSceneCB.m_vCameraPos.xyz;
direction = normalize(world.xyz - origin);
}
1、首先GenerateCameraRay函數的輸入參數index其實就是屏幕像素點的座標,當然座標值是以屏幕座標系爲參考系的,即原點在屏幕(窗口左上角),X軸正方向朝右,Y軸正向朝下方。對應最大值分別是屏幕(窗口)的Width和Height。
2、index參數的值是通過調用名爲DispatchRaysIndex()的 “系統變量內嵌函數”得到的(系統變量內嵌函數稍後會詳細介紹)。該函數返回當前GPU計算線程的單元(可以理解爲GPU上幾千個流處理器中的一個)被分配計算的某個屏幕像素座標x,y值。
3、緊接着通過將index與另一個“系統變量內嵌函數” DispatchRaysDimensions()的返回值做除法,其實也就是計算“歸一化(normalization)”座標值,將原來的像素座標變換爲(0-1.0f)之間的座標,然後再*2.0f-1.0f,就進一步將像素座標變換到了以屏幕中心爲原點的歸一化座標系中,而值就變化到(-1.0f-1.0f)之間。這個計算過程對應原理圖如下:
圖中大寫X,Y表示像素單位的座標大小,小寫的x,y表示標準化之後的座標。同時我們注意到屏幕像素座標系的Y軸正方向向下,而標準化座標系爲了與D3D的座標系保持一致其Y軸是朝上的,所以在座標換算的時候我們取歸一化之後的負值即可。代碼中也是將這些計算拆開步驟來寫,大家應該立刻就能明白函數中的計算。
其實我們在《DirectX12(D3D12)基礎教程(七)——渲染到紋理、正交投影、UI渲染基礎》中介紹的基於窗口座標系的正交變換差不多就是這裏變換的逆變換。
4、函數中的第四行代碼float4 world = mul(float4(screenPos, 0, 1), g_stSceneCB.m_mxP2W);就是將計算得到的屏幕像素標準化座標擴展的4維齊次座標空間,其Z座標爲0,即我們假設的屏幕平面就在z=0的平面上。當然齊次w座標是1.0f,表示我們將這個擴展的座標理解爲是一個表示點的向量,也就是屏幕像素點在攝像機空間中的座標。接着我們用屏幕像素點的標準化座標乘以攝像機投影矩陣(Projection Matrix)的逆矩陣,意思就是說我們將這個點從攝像機座標系變換到了世界座標系中。接着world.xyz /= world.w;就保證了座標單位大小的一致性(仿射變換)。
5、最後我們取攝像機的位置向量,作爲起點座標(origin = g_stSceneCB.m_vCameraPos.xyz;),然後利用變換到世界座標系中的屏幕像素點位置座標減去起點座標就得到了光線的方向向量(direction = normalize(world.xyz - origin);),注意這裏丟棄了兩個向量座標的w座標,根據我們之前文章中介紹過的,4維齊次座標系中,w=0的4維向量表示3D中的純方向量(無位置),而當w=1時就表示3D中的點(有位置)。所以這裏也可以寫成如下形式:
world.xyzw /= world.w;
float4 origin =float4( g_stSceneCB.m_vCameraPos.xyz,1.0f);
float4 direction = normalize(world - origin);//4D向量表示法
6、最後我們要注意的就是起點座標origin我們直接用了攝像機的位置座標,並沒有做到世界空間變換的操作(也即沒有乘以變換矩陣),這是因爲實質上我們的攝像機位置座標已經是世界座標系中的座標值了,不需要變換了。而屏幕位置座標是相對於攝像機座標系空間設置的座標值,它必定在攝像機的座標系中,並且我們總假設屏幕就是在攝像機座標系的原點位置處,並且其方程永遠是z=0。除非你想“斜視”,那麼可以設置一個不在z平面上的屏幕平面方程試試,估計你會有驚喜。
3.4.3、TraceRay函數
最後有了光線(射線)的方程之後,我們就可以開始正式的光追計算過程了,而核心就是調用TraceRay函數,其原型如下:
Template<payload_t>
void TraceRay(RaytracingAccelerationStructure AccelerationStructure,
uint RayFlags,
uint InstanceInclusionMask,
uint RayContributionToHitGroupIndex,
uint MultiplierForGeometryContributionToHitGroupIndex,
uint MissShaderIndex,
RayDesc Ray,
inout payload_t Payload);
1、這個函數的聲明使用了模板化聲明,主要的模板參數就是payload_t,即光追渲染的負載,通常我們設定爲最終屏幕像素點顏色變量的引用。這個參數會被原封不動的以純引用的方式傳遞給所有後續的光追渲染函數,後續的這些函數就可以將計算的顏色值寫入該模板變量,最終TraceRay返回後,PayLoad中就是計算得到的像素點顏色值;
2、第一個參數AccelerationStructure就是剛纔介紹的加速體結構變量;
3、第二個參數RayFlags是指定光追碰撞檢測(命中檢測)時最終對三角形執行的操作類型,這個類似於光柵化渲染中,光柵化狀態結構體中的CullMode(剔除模式)變量,通常我們指定RAY_FLAG_CULL_BACK_FACING_TRIANGLES剔除背面三角形即可;
4、第三個參數InstanceInclusionMask是一個位掩碼,用於在複雜場景光追渲染時屏蔽一些網格實例,目前我們簡單地的設置爲~0即可,表示我們渲染所有的實例;
5、第四個參數RayContributionToHitGroupIndex表示當光線命中網格三角形時,調用的命中(hit)Shader Table(Hit函數的列表)中的Shader函數的索引;
6、第五個參數是用於多個幾何體光追渲染時,不同幾何體對象的命中Shader Table中的索引;
7、第七個參數就是指沒有命中時候的Shader Table中的索引;
8、第八和九個參數我們已經介紹過了。
最終在一般的示例中,我們先掌握第1、2、4、7、8、9參數的用法即可,其它參數除了第3個參數要傳入特殊的~0值之外,其它的都傳入0值即可。
TraceRay函數在理解上建議大家可以想象它是GPU光追的線程函數,在功能上有點類似CreateThread函數,那麼對應的線程入口函數可能就是我們後面要介紹的命中函數或者未命中函數,而線程入口參數就是自定義的變量Payload的引用。
同時TraceRay函數是個“同步函數”,即它返回之後其實表示當前的這條光線(第8個參數傳入的)的完整追蹤過程已經結束了,Payload中的值也計算完畢可以訪問使用了。
進一步考慮到命中(Hit)函數或未命中(Miss)函數都有可能再次調用TraceRay函數,那麼這個函數就會形成一個複雜多層次遞歸調用的形式,而這個遞歸的過程就是光線不斷髮射、反射、折射、透射等的過程。理論上來講其遞歸深度可以是無限的,但實際上一般遞歸次數也就不到3次左右。或者當光線最終指向光源時遞歸也就應該終止了。
另一方面從TraceRay函數的功能原理也可以看出,Raytracing Shader要求的強悍計算能力了。
3.5、系統變量內嵌函數(Raytracing HLSL System Value Intrinsics)
“系統變量內嵌函數”,並不是真正意義上的函數,它其實和我們在光柵化Shader,如VS中,定義變量時指定的語義是一個意思(指相同的語義)。比如,定義頂點位置時:float4 m_vPOS:SV_POSITION;其中的SV_POSITION語義就是說m_vPOS是位置變量。而這裏則是使用函數的形式替代這個變量定義形式的語義文法,這樣一來我們訪問系統變量時,就不一定非要定義成變量形式,直接調用函數即可。這樣我們不必在Shader函數的輸入輸出參數中才能關聯訪問系統變量。最終這使得Shader的編寫更加靈活,而可讀性也更高。函數化之後我們就可以在Shader函數的任何地方輕鬆的訪問系統變量。而五花八門的自定義系統變量名從此就被統一成了“系統變量內嵌函數”調用。
這也可以形象的理解爲將SV_POSITION改成函數SV_POSITION()直接返回位置變量,這樣我們就不用自己定義位置變量m_vPOS了。同樣我們可以等價的理解爲函數DispatchRaysIndex的意思就是定義形如 uint2 index: DispatchRaysIndex這樣的一個變量。這是Raytracing Shader中的新的語法變化,請大家深刻理解。
其它的常用的系統變量內嵌函數如下表所示(注意系統變量內嵌函數沒有參數,直接名稱加括號調用即可):
Ray dispatch system values(光線發射系統變量) |
|
名稱 |
含義描述 |
DispatchRaysIndex |
得到當前像素點的X、Y座標值,取值範圍在DispatchRaysDimensions系統變量之內 |
DispatchRaysDimensions |
在初始DispatchRays調用中指定的D3D12_DISPATCH_RAYS_DESC結構的寬度、高度和深度值。 |
Ray system values(光線(射線)方程系統變量) |
|
名稱 |
含義描述 |
WorldRayOrigin |
當前光線在世界座標系中的起點位置向量。 |
WorldRayDirection |
當前光線在世界座標系中的方向向量。 |
RayTMin |
當前光線(射線)方程中指定的t值的最小下界。 |
RayTCurrent |
當前光線(射線)與物體碰撞點的t值,範圍在TMin與TMax之間,TMin<=t<=TMax。當t==TMax時,觸發的是未命中函數。 |
RayFlags |
當前光線的RayFlags標誌值,即調用TraceRay時指定的RayFlags值。 |
Primitive/object space system values(物體空間系統變量) |
|
名稱 |
含義描述 |
InstanceIndex |
頂級光線跟蹤加速結構中當前實例的自動生成索引。 |
InstanceID |
頂層結構中的底層加速結構實例上的用戶提供的實例標識符。 |
PrimitiveIndex |
在底層加速結構實例的幾何結構內部自動生成原語的索引。 |
ObjectRayOrigin |
當前光線在物體座標系中的起點 |
ObjectRayDirection |
當前光線在物體座標系中的方向 |
ObjectToWorld3x4 |
物體空間到世界空間變換的矩陣(3行4列) |
ObjectToWorld4x3 |
物體空間到世界空間變換的矩陣(4行3列) |
WorldToObject3x4 |
世界空間到物體空間變換的矩陣(3行4列) |
WorldToObject4x3 |
世界空間到物體空間變換的矩陣(4行3列) |
Hit-specific system values(特定碰撞系統變量,主要用於Any Hit或Closest Hit等過程) |
|
名稱 |
含義描述 |
HitKind |
作爲傳遞給ReportHit的HitKind參數的返回值。 |
3.6、最近命中(碰撞)函數(Closest Hit)
光線(射線)在通過TraceRay函數發射出去之後,一旦第一次碰撞到物體網格的某個三角形後,光追渲染過程就會調用被稱之爲最近命中函數的自定義函數。在示例中,它被定義成如下的樣子:
[shader("closesthit")]
void MyClosestHitShader(inout RayPayload payload, in MyAttributes attr)
{
float3 hitPosition = HitWorldPosition();
// Get the base index of the triangle's first 16 bit index.
uint indexSizeInBytes = 2;
uint indicesPerTriangle = 3;
uint triangleIndexStride = indicesPerTriangle * indexSizeInBytes;
uint baseIndex = PrimitiveIndex() * triangleIndexStride;
// Load up 3 16 bit indices for the triangle.
const uint3 indices = Load3x16BitIndices(baseIndex);
// Retrieve corresponding vertex normals for the triangle vertices.
float3 vertexNormals[3] = {
g_Vertices[indices[0]].m_vNor,
g_Vertices[indices[1]].m_vNor,
g_Vertices[indices[2]].m_vNor
};
// Compute the triangle's m_vNor.
// This is redundant and done for illustration purposes
// as all the per-vertex normals are the same and match triangle's m_vNor in this sample.
float3 triangleNormal = HitAttribute(vertexNormals, attr);
float4 diffuseColor = CalculateDiffuseLighting(hitPosition, triangleNormal);
float4 color = g_stSceneCB.m_vLightAmbientColor + diffuseColor;
payload.color = color;
}
1、與光線發射函數類似,其語義文法標識是:[shader("closesthit")];即告訴Raytracing Shader編譯器、DXR、顯卡驅動及GPU後面這個函數就是最近碰撞函數。
2、它的第一個入口參數就是我們剛纔講的Payload自定義變量,那麼在我們的例子裏就是發出這條光線的像素點的顏色值。而其第二個參數in MyAttributes attr,其原類型是BuiltInTriangleIntersectionAttributes,即碰撞點所屬三角形(也可能是別的幾何體,但通常是三角形)的重心座標,它的原始定義如下:
struct BuiltInTriangleIntersectionAttributes
{
float2 barycentrics;
};
一般情況下它滿足下列方程(假設三角形的三個頂點座標向量分別是v0、v1、v2):
碰撞點V的位置向量(重心座標)= v0 + barycentrics.x * (v1-v0) + barycentrics.y* (v2 – v0)。
這個計算代表的具體幾何意義如下圖所示:
當然通常在實際的Raytracing Shader中我們並不這樣計算碰撞點的座標,而是通過光線(射線)的方程直接計算。而重心座標主要用來計算碰撞點的法向量,從而方便我們進一步計算光照情況。
3、函數一開始就調用了一個輔助函數HitWorldPosition來計算碰撞點的座標,該函數定義如下:
float3 HitWorldPosition()
{
return WorldRayOrigin() + RayTCurrent() * WorldRayDirection();
}
其實它裏面的計算過程就是我們剛纔說的使用射線方程計算碰撞點的座標向量。它裏面就是調用了三個“系統變量內嵌函數”。實質上它就是方程:碰撞點=光線起點向量 + 碰撞點t值*光線方向向量,因爲碰撞點必定在光線(射線)上。當然此處的碰撞點t值必定在我們發射光線時指定的TMin和TMax之間。這裏需要注意的一個細節就是當實際碰撞點的t值超過了TMax時,實質上光追渲染過程是不會調用命中函數的,而是去調用未命中函數(Miss),含義就是說碰撞點實質上超出了我們規定的射線的最大射程,因此爲了正確的光追效果建議設置較大的TMax值。
4、有了碰撞點位置的座標向量之後,命中函數中接着使用“系統變量內嵌函數” PrimitiveIndex獲得當前碰撞網格索引數組中,當前被碰撞三角形的序號,接着根據我們傳遞的索引數組的格式大小(3*sizeof(UINT16)),計算得到實際對應的索引數組中的偏移位置,再通過工具方法Load3x16BitIndices從網格索引數組中讀取出三角形三個頂點的索引,然後根據索引讀取頂點數組得到三角形三個頂點數據(g_Vertices[indices[0]],g_Vertices[indices[1]],g_Vertices[indices[2]])。接着根據我們剛纔介紹的重心座標調用工具函數HitAttribute計算出碰撞點的法向量,這個法向量就是三角形各頂點法向量的以重心座標爲權重的算數平均值,是一個均勻插值結果。實質上在更加真實的光追渲染過程中,這裏其實需要計算出法線貼圖的紋理座標,然後從法線貼圖中讀取碰撞點處的法線,因爲真實物體表面並不是均勻光滑的。這與在傳統的光柵化渲染中使用法線貼圖的方法是一致的。同樣的我也假設你對這一個方法已經瞭如指掌。
5、有了碰撞點的位置座標向量和法向量,接着調用輔助函數CalculateDiffuseLighting計算出光源位置到碰撞點的向量與碰撞點法向量的點積,並取大於0的值,因爲負值表示二者夾角大於90度了。然後再用這個點積*物體表面的反光率參數m_vAlbedo*光源的漫反射光顏色參數m_vLightDiffuseColor,得到碰撞點的漫反射顏色值。這個計算其實也是與光柵化渲染中漫反射顏色計算過程一致。
6、最後碰撞點對應像素顏色值,就設定爲我們計算的漫反射顏色值+環境光顏色(float4 color = g_stSceneCB.m_vLightAmbientColor + diffuseColor;)。作爲進一步的練習,大家可以在此基礎上擴展計算下高光反射(鏡面反射)的顏色值,徹底模擬出光柵化渲染中像素顏色值=鏡面高光+漫反射光+環境光的經典光照模型。
3.7、未命中函數(Miss)
當光線(射線)不與任何場景中的物體碰撞或者碰撞點的t值大於射線方程的TMax值時,光追渲染過程就會調用稱之爲未命中函數的Shader方法。在我們的例子中該方法定義如下:
[shader("miss")]
void MyMissShader(inout RayPayload payload)
{
float4 background = float4(0.2f, 0.5f, 1.0f, 1.0f);
payload.color = background;
}
它的含義很簡單就是爲任何沒有碰撞到物體光線的像素點設置一個天藍色的默認顏色。
通常這個函數中我們就實現一個經典的“天空盒”3D紋理採樣或像這裏一樣簡單的返回一個默認背景色即可。
4、Raytracing Shader編譯和Shader中包含頭文件的技巧
在之前的教程示例中,我都建議大家使用代碼中調用編譯函數的方法來編譯Shader,這樣主要是爲了大家將來編寫內置工具的方便。但是目前我還沒有調通Raytracing Shader的純代碼函數編譯方法,我也正在想辦法加緊研究微軟的GitHub項目DirectXShaderCompiler,後續的教程中我爭取將代碼中編譯的方法試驗通後分享給大家。現在我們暫時使用fxc.exe編譯工具編譯的方法。
4.1、Raytracing Shader的編譯
在VS2019中已經嵌入了Shader的編譯工具的命令行編譯方式,只要我們定義一個Shader文件(擴展名最好是HLSL),之後我們就可以在項目中指定使用HLSL編譯器編譯該文件。方法是在VS2019中,解決方案面板中找到Shader文件,然後點擊右鍵,彈出菜單如下:
然後點擊屬性菜單項,彈出文件的屬性對話框如下:
設定項類型爲HLSL編譯器,緊接着選定左邊配置屬性中的HLSL編譯器項,將其展開如下:
然後將右邊的屬性項改成如圖所示:
其中:着色器類型設定爲/Lib,含義是將整個Shader編譯成類似一個靜態庫的類型,它裏面包含若干個Shander函數的編譯後的機器碼,這與編譯傳統的光柵化Shader不同,傳統的光柵化Shader必須明確Shader對應的階段,同時每個階段都必須明確指定一個入口點名稱(就是Shader的主函數)。lib形式是Raytracing特有的形式,因爲裏面包含多個函數入口及函數體,所以不用再指定入口點名稱。同時這裏我們指定的着色器模型是6.3,至少指定必須是6.1,因爲從6.1開始引入了Raytracing Shader,指定6.3對應的Windows SDK版本必須是17763,因爲fxc編譯器現在包含在Windows SDK中。
接下來我們需要指定的Shader編譯選項是生成Shader代碼的C/C++包含文件,我們點擊屬性頁面座標的“輸出文件”選項,然後設置如下圖所示:
這樣Raytracing Shader編譯後就會生成一個C/C++頭文件,這個頭文件中就會以十六進制字符數組變量的形式,將Shader編譯後的二進制碼定義爲一個超大的數組,這樣我們在C/C++代碼中包含這個頭文件之後,Shader的二進制代碼就可以直接利用g_p%(Filename)的形式來直接訪問了,我們示例代碼中這個變量經過宏替換後名爲:g_pRaytracing。最終這個頭文件看起來像下面這個樣子:
4.2、Shader中包含頭文件的技巧
在我們之前系列教程的示例代碼中,我們會發現頂點數據結構的定義往往需要在Shader 和C/C++代碼中分別定義,而且還必須保持一致,這對於稍微複雜點的項目來說都是不可容忍的。
本教程中因爲我們直接引用了微軟DXR示例中的Shader,所以也保留了它解決這一問題的方法,那就是定義兩個輔助頭文件,分別是:HlslCompat.h和RayTracingHlslCompat.h。其中HlslCompat.h很簡單,它的目的就是通過typedef的方法將Shader和C/C++中的向量數據類型進行一個兼容定義如下:
#ifndef HLSLCOMPAT_H
#define HLSLCOMPAT_H
typedef float2 XMFLOAT2;
typedef float3 XMFLOAT3;
typedef float4 XMFLOAT4;
typedef float4 XMVECTOR;
typedef float4x4 XMMATRIX;
typedef uint UINT;
#endif // HLSLCOMPAT_H
接着在RayTracingHlslCompat.h中像下面這樣條件包含HlslCompat.h頭文件,並定義我們需要的頂點結構、常量緩衝結構等:
#ifndef RAYTRACINGHLSLCOMPAT_H
#define RAYTRACINGHLSLCOMPAT_H
#ifdef HLSL
#include "HlslCompat.h"
#else
using namespace DirectX;
// Shader will use byte encoding to access indices.
typedef UINT16 GRS_TYPE_INDEX;
#endif
struct ST_SCENE_CONSANTBUFFER
{
XMMATRIX m_mxP2W;
XMVECTOR m_vCameraPos;
XMVECTOR m_vLightPos;
XMVECTOR m_vLightAmbientColor;
XMVECTOR m_vLightDiffuseColor;
};
struct ST_MODULE_CONSANTBUFFER
{
XMFLOAT4 m_vAlbedo;
};
// 頂點結構
struct ST_GRS_VERTEX
{
XMFLOAT4 m_vPos; //Position
XMFLOAT2 m_vTex; //Texcoord
XMFLOAT3 m_vNor; //Normal
};
#endif // RAYTRACINGHLSLCOMPAT_H
因爲HLSL宏是在fxc編譯時預定義的一個宏,所以當RayTracingHlslCompat.h頭文件被包含在Shader文件中時,HlslCompat.h就被包含進來了,這樣Shader編譯器就理解XMFLOAT2、XMFLOAT3、XMFLOAT4等變量爲Shader數據類型float2、float3、float4等。
而在C/C++文件中包含RayTracingHlslCompat.h頭文件,在編譯時因爲沒有HLSL預定義宏,所以XMFLOAT2、XMFLOAT3、XMFLOAT4等變量含義就是原始的DirectXMath.h中的對應向量類類型。
最終通過這樣的技巧方法,我們就只需要在RayTracingHlslCompat.h這一個頭文件中維護頂點數據類型結構體、常量數據類型結構體等即可,就不需要分開定義在Shader和C/C++兩處。
當然這個方法的使用最終得益於獨立的fxc Shader編譯器,而我們之前在代碼中調用D3DCompileFromFile函數編譯的方法不能簡單的使用這個技巧,但是這也是可以用的,之後的教程中我找機會爲大家補上怎麼用D3DCompileFromFile函數來使用這一方法。
最後建議大家擴展HlslCompat.h中的兼容類型定義,使Shader的數據類型與DirectXMath庫中的變量完全對應,方便以後使用。
(未完待續,預計偉大祖國生日過後繼續發佈,敬請期待,謝謝!)