DirectX12(D3D12)基础教程(十)——DXR(DirectX Raytracing)基础教程(上)
?
目錄
1、前言
2、準備工作
3、Raytracing Shader
3.1、Raytracing Shader整體框架介紹
3.2、全局變量
3.2.1、渲染目標Unordered Access 2D紋理變量
3.2.2、三角形網格變量
3.2.3、常量緩沖
3.2.4、加速結構體變量
3.3、基本光線追蹤渲染過程框架
3.4、光線發射函數(Ray Generation)
3.4.1、MyRaygenShader函數
3.4.2、GenerateCameraRay函數
3.4.3、TraceRay函數
3.5、系統變量內嵌函數(Raytracing HLSL System Value Intrinsics)
3.6、最近命中(碰撞)函數(Closest Hit)
3.7、未命中函數(Miss)
4、Raytracing Shader編譯和Shader中包含頭文件的技巧
4.1、Raytracing Shader的編譯
4.2、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
如您有興趣報名,或希望了解更多詳情,請郵件聯系ChinaCM@nvidia.com獲得支持。
將從比賽中選出優勝者,每位優勝者都將獲得:
?? ?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_Htypedef 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; #endifstruct 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; //PositionXMFLOAT2 m_vTex; //TexcoordXMFLOAT3 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庫中的變量完全對應,方便以后使用。
(未完待續,預計偉大祖國生日過后繼續發布,敬請期待,謝謝!)
總結
以上是生活随笔為你收集整理的DirectX12(D3D12)基础教程(十)——DXR(DirectX Raytracing)基础教程(上)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 合并二叉树进行期权定价
- 下一篇: Linux下ppp拨号+电信3G模块