· 7 MIN READ

用 GPU 重寫霧的渲染

圖形WebGL

用 GPU 重寫霧的渲染 封面插圖
圖 — 用 GPU 重寫霧的渲染

大部分 WebGL 教學裡的霧只有一行:

float fogFactor = 1.0 - exp(-density * density * dist * dist);
color = mix(color, fogColor, fogFactor);

這行程式碼在正午的場景勉強能看,但在黃昏會立刻穿幫:遠處的樓和腳邊的積水會糊成一層同樣的灰,霧像一塊掛在鏡頭前的濾鏡,而不是存在於世界裡的空氣。

霧是體積,不是濾鏡#

真正的霧有兩個被忽略的性質:高度依賴與光深度依賴。

高度依賴很直觀——貼地的霧濃,往上遞減。把世界座標的 y 帶進指數項就行:

float heightFog(vec3 worldPos, float camY) {
  float h = max(worldPos.y - groundLevel, 0.0);
  float falloff = exp(-h * 0.06);      // 越高越淡
  float dist = length(worldPos - cameraPos);
  float slant = dist * mix(1.0, 0.35, clamp(camY / 12.0, 0.0, 1.0));
  return 1.0 - exp(-density * density * slant * slant);
}

slant 那一行是第二個性質:鏡頭俯視地面時,視線在濃霧層裡穿行的路徑更長。不做這個修正,低頭看地面會「看穿」霧層。

黃昏的霧為什麼要偏藍#

太陽角度低時,光線穿過的大氣路徑長,藍光被散射殆盡,直射光偏暖(#ffd9a0 這個量級)。但天空本身仍然是藍的,而霧的本質是懸浮微粒對四周光线的再散射——遠處霧氣接受的是天光,不是直射陽光。所以寫實的黃昏場景裡,霧的遠端顏色應該往 #7fb6ff 方向偏,近端受陽光影響偏暖。一条線性插值就能做出這個梯度:

vec3 fogCol = mix(warmFog, coolFog, smoothstep(30.0, 400.0, dist));

很多人把霧設成單一灰色,畫面立刻失去空氣感——空氣感就是這個冷暖梯度。

光柱#

黃昏最有電影感的瞬間是霧裡的丁達爾光柱。代價很低的做法:從太陽方向做一次單 raymarch,取 8 個離散步點估算光深度,再拿 noise 打散邊界。不需要真的體積渲染,只要保證光柱只在霧裡可見(乘上 fogFactor),它就不會像貼在鏡頭上的貼圖。

踩過的坑#

  • 霧的 density 用線性空間值、顏色卻在 sRGB 空間插值,會得到骯髒的紫色。一律在線性空間混合,輸出前才轉 sRGB。
  • FogExp2 的 density 超過 0.02 時,遠景剪影會整個消失,三層視差白做。我把上限定在 0.014,讓最遠一層剪影剛好「剩一個輪廓」。
  • 水面反射裡的霧要用反射後的距離重算,不能複用直接可見的 fogFactor,否則倒影裡的遠樓比實物清楚,非常出戲。

最終這套霧跑在 1440p 60fps 下,單價是大約 12 條指令每像素。用 mix() 是 3 條,但畫面差了一個時代。