· 9 MIN READ
运动模糊:速度緩衝區與遮罩的取捨
鏡頭跟拍高速主體時,最廉價的運動模糊(accumulation buffer、AfterimagePass)會把整個畫面糊成一片——因為它不區分「谁在動」。電影感恰恰來自這個區分:背景的運動模糊是資訊,主體的運動模糊是瑕疵。
沒有 MRT 也能算速度#
標準做法是 G-buffer 輸出速度,但 WebGL2 單 attachment 的場合可以用深度重建,兩個 pass 解決。
第一步,本幀深度重建世界座標。 從深度紋理取 d,反投影:
vec4 clip = vec4(uv * 2.0 - 1.0, d * 2.0 - 1.0, 1.0);
vec4 world = invViewProj * clip;
world.xyz /= world.w;第二步,用上一幀的 viewProjection 重新投影,得到該像素上一幀的螢幕位置:
vec4 prev = prevViewProj * vec4(world.xyz, 1.0);
vec2 prevUV = prev.xy / prev.w * 0.5 + 0.5;
vec2 velocity = (uv - prevUV) * resolution;這個 velocity 場對靜止幾何自動為零,對鏡頭運動自動全域存在——這是它比物件維度速度更穩的地方:鏡頭甩動時背景本來就該糊。
採樣策略決定條紋與否#
12 個 tap 沿速度方向對稱展開,高斯權重:
vec3 blur(sampler2D tex, vec2 uv, vec2 vel) {
vec3 sum = texture(tex, uv).rgb;
float wsum = 1.0;
float jitter = interleavedGradientNoise(gl_FragCoord.xy);
for (int i = 1; i <= 6; i++) {
float t = (float(i) + jitter) / 6.0;
vec2 off = vel * t * 0.5; // 半快門
float w = 1.0 - 0.6 * t * t;
sum += texture(tex, uv + off).rgb * w;
sum += texture(tex, uv - off).rgb * w;
wsum += 2.0 * w;
}
return sum / wsum;
}jitter 那行是關鍵:不抖動 tap 位置,粗速度區會出現一圈圈同心條紋。藍噪聲或 IG noise 把條紋打散成顆粒,顆粒會被後面的膠片顆粒掩蓋,條紋不會。
速度要 clamp(我設 40px),快門角 180° 換算成每幀位移的一半。強度公式 velocity * 0.5 裡這個 0.5 是主體保護係數的另一半含義——
主體遮罩#
渲染主體時寫 stencil(或 depth-only prepass),模糊 pass 讀 mask,把主體像素的速度乘 0.25。效果是:掠過機翼的城市拉成橫向速度線,紙飛機邊緣保持刀銳。人眼讀取這個對比的方式是:「鏡頭在追一個清楚的東西」——这就是追随拍摄(panning)攝影的質感。
不做遮罩,主體邊緣會出現雙影;做了遮罩但係數壓到 0,主體會從速度線裡「剪貼」出來,像貼紙。0.25 是一個保留少量模糊的折衷:邊緣仍有一兩像素的暈,但主體結構完整。
備援與降級#
整合顯卡或手機上,速度重建 + 12 tap 的填充率扛不住。備援是 AfterimagePass(damp 0.72)加輕度徑向模糊:它當然是全域的,但配 0.72 的衰減,主體殘影剛好落在「速度感」而非「壞掉了」的區間。偵測條件用 hardwareConcurrency 和畫素預算就夠了,不需要真的 benchmark。
一句話總結:運動模糊的難點從來不是算法,是谁該糊。