大家好~我們已經實現了百萬級2D物體的流暢渲染,不過是基於計算管線實現的。本文在它的基礎上,改為基於光追管線實現,主要進行了CPU和GPU端記憶體的優化,成功地將渲染的2D物體數量由4百萬提高到了2千萬
相關文章如下:
如何用WebGPU流暢渲染百萬級2D物體?
本文不需要實現構建和遍歷BVH,而是直接使用光追管線提供的加速結構
本文的重點工作在於對CPU記憶體和GPU記憶體的優化,突破記憶體限制(如突破加速結構最大大小限制),使其支援千萬級物體的資料
本文使用WebGPU Node專案,作者的介紹在這裡。它在底層封裝了Vulkan SDK,在上層提供了WebGPU API,實現了在Nodejs環境中使用WebGPU API和光追管線來實現硬體加速的光線追蹤(需要使用nvdia的RTX顯示卡)!
我在2020年就已經基於該專案實現了3D場景渲染,相關介紹如下:
WebGPU+光線追蹤Ray Tracing 開發三個月總結
跟百萬級的Demo的需求是一樣的,除了提高渲染的2D物體數量到千萬級,目的是為了探索基於硬體的光追管線的實現能帶來的優化極限
我們最終能夠流暢渲染2千萬個圓環
效能指標:
硬體:
跟百萬級Demo的效能比較
提高的地方:
降低的地方:
下面讓我們從0開始,介紹實現和優化的步驟:
跟百萬級的Demo一樣,選擇光線追蹤演演算法
不過這裡需要傳送Primary Ray去計算射線與物體的相交,這樣才能觸發光追管線中關於相交的著色器
跟百萬級的Demo一樣,場景依然使用ECS架構
光追管線支援兩種geometry的型別:
三角形和AABB包圍盒
因為圓環是引數化的(引數化為圓點座標、半徑、圓環寬度),所以geometry型別使用AABB包圍盒
這種型別geometry被稱為「Procedural Geometry」
光追管線的加速結構跟百萬級Demo的TopLevel、BottomLevel類似,分為TLAS(top level acceleration structure)和BLAS(bottom level acceleration structure),如下圖所示:
因為場景只有1個Geometry和1個Instance,所以在BLAS中加入1個aabb(根據Geometry的引數計算aabb,其中aabb的min.z和max.z設為0),在TLAS中加入1個Instance的資料(主要包括該圓環的transform matrix、instanceId)
現在介紹下光追管線通常包含哪些著色器:
渲染的步驟為:
1、在.rgen著色器中逐個畫素產生Primary Ray
2、如果Primary Ray與AABB相交,則執行.rint著色器;否則執行.rmiss著色器,將畫素設定為黑色
在執行.rint著色器時:
在執行.rchit著色器時:
關於Procedural Geometry渲染的參考資料如下:
Vulkan->Intersection Shader - Tutorial
D3D12 Raytracing Procedural Geometry sample
場景中建立10個圓環,它們的ECS資料為:
10個gameObject
10個transform
1個geometry
1個material
geometry仍然只有1個,但instance變為10個,所以在TLAS中改為加入10個Instance的資料,BLAS不變
渲染結果如下圖所示:
我們使用「平均取樣法」來計算FPS。這個方法跟我之前實現的深度學習->優化演演算法->動量法->指數加權移動平均類似,並且那裡還做了數學證明。
這個方法的基本思想就是計算多個相鄰幀的平均值,但又不需要額外的空間來儲存多幀
參考資料如下:
影格率(FPS)計算的六種方法總結
實現思路如下:
將TLAS中每個instance的層設為transform matrix中的position.z;
將Primary Ray的flag設為gl_RayFlagsOpaqueEXT,這樣的話Primary Ray與最大層的圓環相交時會觸發.rchit著色器,然後就停止傳播,從而實現只渲染最大層的圓環
注:這裡的層不再是從1開始的正整數,而是為0.00001, 0.00002這樣的小數。這是因為為了效能考慮,將Primary Ray的最大距離設為一個很小的值(如1.0),所以層的值不能超過1.0
當嘗試跟百萬級Demo一樣渲染4百萬個圓環時,報了下面的錯誤:
Error: Out of memory Error: vkAllocateMemory failed with VK_ERROR_OUT_OF_DEVICE_MEMORY
這是因為WebGPU Node專案底層使用Vulkan來渲染,它申請的單個TLAS的記憶體容納不了4百萬個instance資料
所以我們減少圓環數量,發現當其為3百50萬個時,能夠成功渲染。其中構造加速結構的時間為25秒,FPS為1000(因為這裡使用setInterval而不是requestAnimationFrame啟動主迴圈,所以FPS可以大於60)
如果要渲染4百萬個甚至更多的圓環,就需要將1個TLAS拆成多個TLAS(BLAS還是不變,只有一個)
這樣的話,如果每個TLAS都有3百50萬個Instance資料,那麼3個TLAS就可以有超過1千萬個Instance資料了
如何拆分TLAS呢?
首先,我們可以根據Instance的層,按照下面的方法將所有Instance分為3組:
然後,將每組的Instance資料分別傳入1個TLAS中;
然後,建立3個bindGroup,每個bindGroup分別使用1個TLAS;
最後,在begin ray tracing pass時,繫結對應的bindGroup,traceRays3次,程式碼如下:
...
let commandEncoder = device.createCommandEncoder({});
let passEncoder = commandEncoder.beginRayTracingPass({});
passEncoder.setPipeline(pipeline);
bindGroups.forEach(bindGroup => {
passEncoder.setBindGroup(0, bindGroup);
passEncoder.traceRays(
0, // sbt ray-generation offset
1, // sbt ray-hit offset
2, // sbt ray-miss offset
window.width, // query width dimension
window.height, // query height dimension
1 // query depth dimension
);
})
passEncoder.endPass();
queue.submit([commandEncoder.finish()]);
這裡我們先對層最大的一組Instance做遍歷,然後再對層小一點的一組Instance做遍歷,然後再對層更小一點的一組Instance做遍歷。。。。。。
這是一個優化的方法,因為每個畫素只渲染最大層的圓環的顏色,所以如果在遍歷1個TLAS時Primary Ray與圓環相交而得到了畫素的顏色,那麼就不再對後面的TLAS進行遍歷了!
Instance Buffer儲存了所有Instance的元件資料,是一個storage buffer。當Instance的數量大於1千萬後,它的大小會超過buffer的最大大小:128MB,會報錯
所以,跟拆分加速結構TLAS一樣,我們把Instance Buffer也對應拆分,使得3個bindGroup分別使用1個TLAS和1個Instance Buffer
之前是在優化GPU端的記憶體佔用,現在要優化CPU端的記憶體佔用
當Instance數量超過1千6百萬時,在CPU端設定TLAS的instances資料時會超出記憶體大小,報下面的錯誤:
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
現在的實現思路為:
我們有5組Instance,對應5個TLAS,每個TLAS最多有3百50萬個Instance
首先現在建立了5個陣列,每個陣列最多儲存3百50萬個Instance資料;
然後將對應的陣列設定為對應的TLAS的instances資料
因為這5個陣列共包含了1千6百萬個Instance資料,超出了CPU端v8的記憶體限制,所以會報錯
可以做下面兩個優化來解決問題:
現在我們來渲染2千萬個圓環,測試下FPS
渲染結果如下圖所示:
效能指標如下:
為什麼FPS跟渲染3百50萬個圓環時一樣?
因為第一個TLAS包含的3百50萬個圓環為最高層,它們已經填滿了螢幕,所以遍歷第一個TLAS後就停止遍歷了!
我們希望測試下最壞的情況,所以強制遍歷所有的TLAS,此時FPS為45左右
當我們嘗試渲染大於2千萬個圓環時,報了下面的錯誤:
Error: Out of memory Error: vkAllocateMemory failed with VK_ERROR_OUT_OF_DEVICE_MEMORY
這說明雖然每個TLAS沒有超過最大限制(3百50萬個Instance資料),但是應該是超過了總的大小限制,也就是所有TLAS包含的總的Instance資料(大於2千萬個)超過了限制!
除非能夠修改WebGPU Node專案中的Vulkan關於TLAS記憶體分配的程式碼,否則我們通過WebGPU API無法修改該限制(因為WebGPU Node沒有提供相關的WebGPU API)
在建立BLAS和TLAS時,可以指定為下面幾個flag:
NONE
ALLOW_UPDATE
PREFER_FAST_TRACE
PREFER_FAST_BUILD
LOW_MEMORY
當修改為PREFER_FAST_BUILD而不是PREFER_FAST_TRACE,並沒有效果!不知道是因為WebGPU Node在Vulkan這層沒有處理這個flag還是顯示卡RTX2060s不支援這個flag?
其它的優化思路包括:
目前雖然構造加速結構比較慢,但隨著RTX顯示卡的升級,相信會有改善!畢竟這個是由顯示卡完成的,我們這邊能優化的餘地較小
我們來分析下面幾個情況:
為什麼「增大圓環geometry的半徑,FPS會明顯下降」?
這是因為:
這會增大圓環的AABB->增加重疊的AABB數量->增大遍歷的開銷->最終降低FPS
如果BLAS加入兩個圓環的AABB,是否就能渲染4千萬個圓環了?
這樣是可以的。之前實現的渲染是渲染2千萬個Instance,而每個Instance對應的BLAS只有一個圓環,所以就只渲染了2千萬個圓環。
實際上可以像這樣增加渲染的圓環數量,但是因為總的AABB的數量增加了一倍,導致遍歷開銷增大一倍,所以FPS也會下降一倍
能不能在相鄰的兩幀分別繪製不同的2千萬個Instance,這樣就可以渲染4千萬個Instance了?
這樣子的話不僅FPS會下降一倍,而且也是不可行的,因為不管分成幾次draw call,都需要把所有Instance資料載入到TLAS中(因為所有的Instance資料只建立一次,除非每幀都建立對應的Instance資料並且把之前的Instance資料銷燬!但這樣的話CG開銷應該會比較大),這樣所有的TLAS包含的Instance資料大小一樣不能超過2千萬個
可以在相鄰的兩幀分別繪製不同的1千萬個Instance,但這樣還不如在一幀中繪製2千萬個Instance省事
感謝大家的支援和學習~
本文主要使用光追管線,優化了記憶體佔用,將2D物體數量提升到了千萬級
使用光追管線相比使用計算管線的優點是:
缺點是:
所以,我們可以考慮在Web版產品中使用瀏覽器的WebGPU標準(基於計算管線)來實現,而在桌面版產品中基於光追管線來實現
目前已達到加速結構的記憶體限制(2千萬個Instance),不容易繼續增加Instance數量
後面可以考慮優化基於計算管線的Demo,從百萬級提升到千萬級;
另外可以考慮GPU LOD,在計算著色器中替換geometry,從而減少BLAS的佔用
如何用WebGPU流暢渲染百萬級2D物體?
WebGPU+光線追蹤Ray Tracing 開發三個月總結
Vulkan->Intersection Shader - Tutorial
影格率(FPS)計算的六種方法總結
WebGPU Ray-Tracing Spec
GLSL_NV_ray_tracing
Node.js heap out of memory
Node.js Default Memory Settings