WebGL 2.0 光線步進渲染原理詳解
深入解析 WebGL 2.0 光線步進(Raymarching)技術,了解如何使用 SDF 符號距離場實現即時體積渲染,以及這種技術如何壓榨 GPU 的極限性能。

深入探索 WebGL 2.0 光線步進、SDF 和即時體積渲染
WebGL 2.0 解鎖了強大的 GPU 功能集,使傳統上需要原生 API 的高級渲染技術成為可能。其中,光線步進(Raymarching) 作為最富有表現力和數學優雅的方法之一脫穎而出,可以用最少的幾何體渲染複雜場景。
本文將通過 WebGL 2.0 光線步進的核心概念,解釋**符號距離場(SDF)**如何實現高效渲染,並展示這種技術如何從 GPU 中榨取驚人的性能。
1. 什麼是光線步進?
光線步進是一種渲染方法,它不是將幾何體投影到螢幕上,而是從相機逐像素發射一條光線,逐步穿過虛擬場景,評估距離,直到光線擊中表面或達到最大範圍。
1.1 光線步進的工作原理(視覺化圖表)
相機
|
| 光線
v
+--------------------------------- 螢幕像素
|
| 步進 1 (距離 d1)
|
|---------> 步進 2 (距離 d2)
|
|-----------------------> 步進 3 ...
|
當距離 < ε 時到達表面
光線步進本質上是一個循環:
for (int i = 0; i < MAX_STEPS; i++) {
float dist = sdf(currentPos);
if (dist < EPSILON) hitSurface();
currentPos += rayDir * dist;
}
關鍵要素是我們傳入的 SDF 函數。
2. 什麼是符號距離場(SDF)?
符號距離場 是一個返回以下值的函數:
> 0 物體外部距離
= 0 正好在表面上
< 0 物體內部距離
2.1 SDF 圖表
外部
+------------+
| d = 0.5 |
| |
內部 | SDF | 表面 d = 0
d < 0 | |
| d = -0.3 |
+------------+
2.2 範例:球體 SDF
float sdSphere(vec3 p, float r) {
return length(p) - r;
}
通過將這些基本形狀與布林運算(聯集、差集、交集)結合,你可以在不需要任何傳統幾何體的情況下構建複雜場景。
3. 為什麼在 WebGL 2.0 中使用 SDF 光線步進?
3.1 優勢
不需要頂點緩衝區或網格 所有內容都在片段著色器中通過數學計算。
無限的幾何細節 SDF 定義平滑連續的表面。
動態、可變形對象 用數學輕鬆動畫化形狀。
緊湊的場景 完整的 3D 場景可能只需要幾行 GLSL 程式碼。
3.2 使這成為可能的 WebGL 2.0 特性
- 高精度浮點數
- 完整的 GLSL ES 3.0 著色器支援
- UBO(統一緩衝區)
- 浮點紋理
- 更靈活的循環和分支
這使得在 WebGL 1.0 中難以實現的穩定、高效能光線步進成為可能。
4. 逐步構建 WebGL 2.0 光線步進器
4.1 場景光線設置
為每個像素計算一條光線:
vec3 rayOrigin = cameraPos;
vec3 rayDir = normalize(uv.x * camRight + uv.y * camUp + camForward);
4.2 光線步進循環
float marchRay(vec3 ro, vec3 rd) {
float totalDist = 0.0;
for (int i = 0; i < MAX_STEPS; i++) {
vec3 p = ro + rd * totalDist;
float d = map(p); // 你的 SDF 函數
if (d < EPSILON) break; // 擊中
if (totalDist > MAX_DISTANCE) break; // 未擊中
totalDist += d;
}
return totalDist;
}
4.3 定義 SDF 場景
float map(vec3 p) {
float s = sdSphere(p, 1.0);
float box = sdBox(p - vec3(2.0, 0.0, 0.0), vec3(0.7));
return min(s, box); // 聯集
}
4.4 法線計算
使用梯度近似:
vec3 calcNormal(vec3 p) {
const vec2 e = vec2(0.001, 0.0);
return normalize(vec3(
map(p + e.xyy) - map(p - e.xyy),
map(p + e.yxy) - map(p - e.yxy),
map(p + e.yyx) - map(p - e.yyx)
));
}
4.5 光照模型
簡單的漫反射著色:
float diffuse = max(dot(normal, lightDir), 0.0);
5. 渲染體積(霧、雲、煙霧)
SDF 光線步進可以擴展到體積渲染,其中每一步都貢獻顏色和密度,而不是在表面停止。
5.1 體積圖表
光線 →
[低密度] [中密度] [高密度] [不透明]
+--------+---------+---------+-------+
採樣1 採樣2 採樣3 採樣4
5.2 體積累積模型
vec3 color = vec3(0);
float absorption = 1.0;
for (int i = 0; i < STEPS; i++) {
float density = computeDensity(p);
vec3 sampleColor = density * vec3(0.6, 0.7, 1.0);
color += sampleColor * absorption;
absorption *= (1.0 - density * 0.1);
p += rd * STEP_SIZE;
}
光線步進體積渲染更加昂貴,但 WebGL 2.0 的性能改進(特別是在行動 GPU 上)使即時雲、霧和星雲成為可能。
6. 性能優化技術
光線步進可能很重,因此優化至關重要。
6.1 關鍵技術
距離剔除 通過依賴準確的 SDF 距離跳過大的空白區域。
包圍體積 測試光線是否進入存在 SDF 的區域。
自適應步長 在表面附近使用較小的步長,在開放空間中使用較大的步長。
提前退出 一旦擊中表面就停止步進。
減少 MAX_STEPS 即時渲染的典型值範圍是 64–128。
高效使用 WebGL 2.0 統一變數和緩衝區 將不變的值移出循環。
7. WebGL 2.0 專案的程式碼結構
目錄佈局範例:
shader/
raymarch.vert
raymarch.frag
src/
webgl2-context.js
camera.js
renderer.js
index.html
典型的片段著色器部分:
// 1. 相機/光線設置
// 2. SDF 定義
// 3. 材質和光照函數
// 4. 光線步進循環
// 5. 最終顏色輸出
8. 何時應該使用光線步進?
光線步進在以下情況下表現出色:
- 無限平滑形狀
- 分形
- 科幻場景
- 符號距離軟陰影
- 程序化幾何
- 體積效果(雲、霧、上帝之光)
在以下情況下應避免使用光線步進:
- 許多詳細的複雜網格
- 逼真的蒙皮角色
- 包含數千個物件的大型動態場景
光線步進最適合程序化世界和風格化視覺效果。
9. 結論
WebGL 2.0 光線步進僅使用數學和 GLSL 就將高級即時渲染帶入瀏覽器。使用 SDF,你可以在不需要單個頂點緩衝區的情況下創建複雜的 3D 場景。儘管該技術在計算上很密集,但仔細的優化允許創建可與原生 GPU 應用程式相媲美的富有表現力的視覺效果。
光線步進代表了數學、藝術和 GPU 程式設計的獨特交叉點,使其成為現代 WebGL 開發中最令人興奮的技術之一。
這一切跟 Volume Shader BM Test 有什麼關係
讀完原理,回到工具本身。下面是把上面的理論對回到 Volume Shader BM Test 上的幾個關鍵點。
為什麼這個測試比普通 3D Demo 更狠
普通 WebGL Demo 畫幾千個多邊形就夠了。這個測試要求 GPU 對每一個像素跑一遍光線步進迴圈,通常每像素 1000+ 次迭代,且沒有幾何資訊可以提前剔除。遊戲裡能跑滿 60 FPS 的中階顯卡,在 Heavy 模式下掉到 30 FPS 以下是常態。
為什麼手機上 FPS 會掉
手機 SoC 一升溫就會主動降頻。前 10 秒看著很漂亮,之後 GPU 頻率會被壓下來。建議接上充電,先跑 Light 或 Medium,比較手機的時候看穩定性而不是峰值 FPS。詳見手機 GPU 測試指南。
為什麼第一次跑會慢一點(Shader 編譯)
頁面第一次載入時瀏覽器要把 GLSL 編譯成驅動可執行的二進位,這一步在某些驅動上要花幾百毫秒,表現為開頭的一次卡頓。丟掉第一次結果,看第二次。
怎樣公平地比較結果
同一模式、瀏覽器分頁在前景、同一電源狀態。不同模式(Light / Medium / Heavy)的結果不能直接比,shader 負載不同。參考 FPS 結果解讀。
如果瀏覽器在用內顯怎麼辦
帶獨顯的筆電上,Chrome / Edge 經常預設走內顯。開啟 chrome://gpu,看 GL_RENDERER 那一行。如果顯示的是內顯,去作業系統的圖形設定中強制瀏覽器走獨顯。卡頓排查指南 有詳細步驟。
跑一次試試
理論看完了,直接跑一次基準測試,不到一分鐘。