如何用WebGPU流暢渲染千萬級2D物體:基于光追管線
大家好~我們已經實現了百萬級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的FPS一樣,為45左右,也就是每幀花費21毫秒
硬件:
- Win10操作系統
- Nodejs環境+Vulkan驅動
- RTX2060s顯卡
跟百萬級Demo的性能比較
提高的地方:
- 渲染的物體數量多了4倍
降低的地方:
- 構造加速結構的時間多了1.5倍
不過相信隨著RTX顯卡的升級,會越來越快
下面讓我們從0開始,介紹實現和優化的步驟:
1、選擇渲染的算法
跟百萬級的Demo一樣,選擇光線追蹤算法
不過這里需要發送Primary Ray去計算射線與物體的相交,這樣才能觸發光追管線中關于相交的著色器
2、實現內存需求
跟百萬級的Demo一樣,場景依然使用ECS架構
3、渲染1個圓環
光追管線支持兩種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)
現在介紹下光追管線通常包含哪些著色器:
- .rgen
每個像素會執行一次著色器,分別產生一條Primary Ray - .rint
該著色器用于AABB包圍盒的geometry類型,當Primary Ray與AABB相交時被觸發 - .rchit
該著色器用來處理Primary Ray與第一個圓環相交的情況 - .rmiss
該著色器用來處理Primary Ray與所有圓環都沒有相交的情況 - rahit
Primary Ray與第一個圓環相交后繼續傳播,當與第二個及以后的圓環相交時觸發該著色器(本Demo沒用到該著色器)
渲染的步驟為:
1、在.rgen著色器中逐個像素產生Primary Ray
2、如果Primary Ray與AABB相交,則執行.rint著色器;否則執行.rmiss著色器,將像素設置為黑色
在執行.rint著色器時:
- 根據gl_LaunchIDEXT、gl_LaunchSizeEXT得到像素坐標,即為Primary Ray與AABB的相交點
- 如果該像素坐標在圓環內,則通過reportIntersectionEXT來觸發.rchit著色器
在執行.rchit著色器時:
- 根據material設置像素顏色(為紅色)
關于Procedural Geometry渲染的參考資料如下:
Vulkan->Intersection Shader - Tutorial
D3D12 Raytracing Procedural Geometry sample
4、渲染10個圓環
場景中創建10個圓環,它們的ECS數據為:
10個gameObject
10個transform
1個geometry
1個material
geometry仍然只有1個,但instance變為10個,所以在TLAS中改為加入10個Instance的數據,BLAS不變
渲染結果如下圖所示:

5、顯示FPS
我們使用“平均采樣法”來計算FPS。這個方法跟我之前實現的深度學習->優化算法->動量法->指數加權移動平均類似,并且那里還做了數學證明。
這個方法的基本思想就是計算多個相鄰幀的平均值,但又不需要額外的空間來保存多幀
參考資料如下:
幀率(FPS)計算的六種方法總結
6、實現剔除
實現思路如下:
將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
7、測試渲染極限
當嘗試跟百萬級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)
8、拆分加速結構
如果要渲染4百萬個甚至更多的圓環,就需要將1個TLAS拆成多個TLAS(BLAS還是不變,只有一個)
這樣的話,如果每個TLAS都有3百50萬個Instance數據,那么3個TLAS就可以有超過1千萬個Instance數據了
如何拆分TLAS呢?
首先,我們可以根據Instance的層,按照下面的方法將所有Instance分為3組:
- 每組有最小層和最大層這兩個數據,將層在其中的Instance放在該組中
- 每組按照層的大小從大到小排列,使得下一組的最大層小于等于上一組的最小層
然后,將每組的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進行遍歷了!
9、拆分Instance Buffer
Instance Buffer存儲了所有Instance的組件數據,是一個storage buffer。當Instance的數量大于1千萬后,它的大小會超過buffer的最大大小:128MB,會報錯
所以,跟拆分加速結構TLAS一樣,我們把Instance Buffer也對應拆分,使得3個bindGroup分別使用1個TLAS和1個Instance Buffer
10、優化CPU端的內存占用
之前是在優化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的內存限制,所以會報錯
可以做下面兩個優化來解決問題:
- 因為每個數組的最大容量都是一樣的(3百50萬),所以只創建1個容量為3百50萬的數組,通過"arr[index]=instance數據"來保存一個組的Instance數據,然后將其設置給對應的TLAS;然后重復使用該數組,保存下一組的Instance數據,將其設置給對應的TLAS,依次類推。。。。。。
經過這樣的優化, CPU端就只需要分配3百50萬個Instance數據的內存了,大大減少了內存占用 - 使用ArrayBuffer而不是數組來存儲每組的Instance數據,這樣可以提高速度并且減少內存占用
11、測試渲染極限
現在我們來渲染2千萬個圓環,測試下FPS
渲染結果如下圖所示:

性能指標如下:
- FPS為1000以上
- 構造加速結構的時間為150秒
為什么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)
12、嘗試優化構造加速結構
在創建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?
其它的優化思路包括:
- 不需要一次性構造所有TLAS,而是按需構造對應的TLAS
- 在worker中構造TLAS和BLAS,使其不阻塞主線程
目前雖然構造加速結構比較慢,但隨著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物體數量提升到了千萬級
使用光追管線相比使用計算管線的優點是:
- 不需要實現BVH,實現和維護更加簡單
- 因為是硬件加速射線相交計算,遍歷的性能更好
缺點是:
- 兼容性更差
需要Nodejs環境和RTX顯卡 - 不能使用最新的WebGPU標準
WebGPU Node項目使用的是2020年版本的WebGPU標準,不是最新的標準
所以,我們可以考慮在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
大家好~本文基于光追管線實現,主要進行了CPU和GPU端內存的優化,成功地將渲染的2D物體數量由4百萬提高到了2千萬
浙公網安備 33010602011771號