Shader 運動模糊(Motion Blur)
瞭解完高斯模糊之後,接下來看看運動模糊。

什麼是運動模糊?根據百科的定義:動態模糊或運動模糊是靜態場景或一系列的圖片像電影或是動畫中快速移動的物體造成明顯的模糊拖動痕跡。
為什麼會出現運動模糊?攝影機的工作原理是在很短的時間裡把場景在膠片上曝光。場景中的光線投射在膠片上,引起化學反應,最終產生圖片。這就是曝光。如果在曝光的過程中,場景發生變化,則就會產生模糊的畫面。
問題一:運動模糊是否就是單一方向的高斯模糊?
我們根據運動模糊的物理成像原理可以知道,離快門關閉越近的影象越清晰,殘影是存在透明度變化的,它也受速度影響:

我們嘗試一下通過單一方向的高斯模糊來模擬運動模糊,看看效果如何:
// 只展示核心程式碼 vec4 blur13(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) { vec4 color = vec4(0.0); vec2 off1 = vec2(1.411764705882353) * direction; vec2 off2 = vec2(3.2941176470588234) * direction; vec2 off3 = vec2(5.176470588235294) * direction; color += texture2D(image, uv) * 0.1964825501511404; color += texture2D(image, uv + (off1 / resolution)) * 0.2969069646728344; color += texture2D(image, uv - (off1 / resolution)) * 0.2969069646728344; color += texture2D(image, uv + (off2 / resolution)) * 0.09447039785044732; color += texture2D(image, uv - (off2 / resolution)) * 0.09447039785044732; color += texture2D(image, uv + (off3 / resolution)) * 0.010381362401148057; color += texture2D(image, uv - (off3 / resolution)) * 0.010381362401148057; return color; } void main() { gl_FragColor = blur13(texture, st, iResolution, vec2(0., 5.)); } 複製程式碼

雖然單一方向的高斯模糊並不完全符合運動模糊的定義。但單純看效果其實分辨不太出來,我們再把效果強化:

問題二:如何讓畫面自然地動起來?
運動模糊,自然需要運動才能體現出來。首先我們實現一個簡單的位移:
// 只展示核心程式碼 vec4 blur13(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) { vec4 color = vec4(0.0); vec2 off1 = vec2(1.411764705882353) * direction; vec2 off2 = vec2(3.2941176470588234) * direction; vec2 off3 = vec2(5.176470588235294) * direction; color += texture2D(image, uv) * 0.1964825501511404; color += texture2D(image, uv + (off1 / resolution)) * 0.2969069646728344; color += texture2D(image, uv - (off1 / resolution)) * 0.2969069646728344; color += texture2D(image, uv + (off2 / resolution)) * 0.09447039785044732; color += texture2D(image, uv - (off2 / resolution)) * 0.09447039785044732; color += texture2D(image, uv + (off3 / resolution)) * 0.010381362401148057; color += texture2D(image, uv - (off3 / resolution)) * 0.010381362401148057; return color; } void main() { st += time*3.;// time: 0~1 st = fract(st); gl_FragColor = blur13(texture, st, iResolution, vec2(0., 20.)); } 複製程式碼

OK,有點辣眼睛。首先需要解決的問題是影象邊界連線到地方並沒有運動模糊:

這意味著我們要實時的取當前的座標來對影象進行取樣,所以傳入一個新的引數,表示當前運動的距離:
vec4 blur13(sampler2D image, vec2 uv, vec2 resolution, vec2 direction, vec2 speed) { vec4 color = vec4(0.0); vec2 off1 = vec2(1.411764705882353) * direction; vec2 off2 = vec2(3.2941176470588234) * direction; vec2 off3 = vec2(5.176470588235294) * direction; color += texture2D(image, fract(uv + speed)) * 0.1964825501511404; color += texture2D(image, fract(uv + (off1 / resolution) + speed)) * 0.2969069646728344; color += texture2D(image, fract(uv - (off1 / resolution) + speed)) * 0.2969069646728344; color += texture2D(image, fract(uv + (off2 / resolution) + speed)) * 0.09447039785044732; color += texture2D(image, fract(uv - (off2 / resolution) + speed)) * 0.09447039785044732; color += texture2D(image, fract(uv + (off3 / resolution) + speed)) * 0.010381362401148057; color += texture2D(image, fract(uv - (off3 / resolution) + speed)) * 0.010381362401148057; return color; } void main() { vec2 speed = vec2(0, time*3.); gl_FragColor = blur13(inputImageTexture, myst, iResolution, vec2(0., 20.), speed); } 複製程式碼

那解決完邊界問題,再分析下運動模糊出現的時機,運動開始和結束肯定不會產生模糊,只有中間過程才會有模糊,所以我們根據時間來調整模糊:
我們先構造一個從 0~1 的單位時間內,它的值從 0~1~0 的變化曲線,作為我們模糊的乘數,通過這個工具,對之前的正態分佈概率密度函式進行一點改造:

vec4 blur13(sampler2D image, vec2 uv, vec2 resolution, vec2 direction, vec2 speed) { vec4 color = vec4(0.0); vec2 off1 = vec2(1.411764705882353) * direction; vec2 off2 = vec2(3.2941176470588234) * direction; vec2 off3 = vec2(5.176470588235294) * direction; color += texture2D(image, fract(uv + speed)) * 0.1964825501511404; color += texture2D(image, fract(uv + (off1 / resolution) + speed)) * 0.2969069646728344; color += texture2D(image, fract(uv - (off1 / resolution) + speed)) * 0.2969069646728344; color += texture2D(image, fract(uv + (off2 / resolution) + speed)) * 0.09447039785044732; color += texture2D(image, fract(uv - (off2 / resolution) + speed)) * 0.09447039785044732; color += texture2D(image, fract(uv + (off3 / resolution) + speed)) * 0.010381362401148057; color += texture2D(image, fract(uv - (off3 / resolution) + speed)) * 0.010381362401148057; return color; } float normpdf(float x) { return exp(-20.*pow(x-.5,2.)); } void main() { vec2 speed = vec2(0, time); float blur = normpdf(time); gl_FragColor = blur13(inputImageTexture, myst, iResolution, vec2(0., 20.*blur), speed); } 複製程式碼

有點感覺了,接著增加時間的緩動效果,很明顯我們想要的是 easeInOut 的曲線:

float A(float aA1, float aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; } float B(float aA1, float aA2) { return 3.0 * aA2 - 6.0 * aA1; } float C(float aA1) { return 3.0 * aA1; } float GetSlope(float aT, float aA1, float aA2) { return 3.0 * A(aA1, aA2)*aT*aT + 2.0 * B(aA1, aA2) * aT + C(aA1); } float CalcBezier(float aT, float aA1, float aA2) { return ((A(aA1, aA2)*aT + B(aA1, aA2))*aT + C(aA1))*aT; } float GetTForX(float aX, float mX1, float mX2) { float aGuessT = aX; for (int i = 0; i < 4; ++i) { float currentSlope = GetSlope(aGuessT, mX1, mX2); if (currentSlope == 0.0) return aGuessT; float currentX = CalcBezier(aGuessT, mX1, mX2) - aX; aGuessT -= currentX / currentSlope; } return aGuessT; } /* * @param aX: 傳入時間變數 * @param mX1/mY1/mX2/mY2: 貝塞爾曲線四個值 * 說明: 這個函式以上的其他函式都是本函式使用的輔助函式 */ float KeySpline(float aX, float mX1, float mY1, float mX2, float mY2) { if (mX1 == mY1 && mX2 == mY2) return aX; // linear return CalcBezier(GetTForX(aX, mX1, mX2), mY1, mY2); } vec4 blur13(sampler2D image, vec2 uv, vec2 resolution, vec2 direction, vec2 speed) { vec4 color = vec4(0.0); vec2 off1 = vec2(1.411764705882353) * direction; vec2 off2 = vec2(3.2941176470588234) * direction; vec2 off3 = vec2(5.176470588235294) * direction; color += texture2D(image, fract(uv + speed)) * 0.1964825501511404; color += texture2D(image, fract(uv + (off1 / resolution) + speed)) * 0.2969069646728344; color += texture2D(image, fract(uv - (off1 / resolution) + speed)) * 0.2969069646728344; color += texture2D(image, fract(uv + (off2 / resolution) + speed)) * 0.09447039785044732; color += texture2D(image, fract(uv - (off2 / resolution) + speed)) * 0.09447039785044732; color += texture2D(image, fract(uv + (off3 / resolution) + speed)) * 0.010381362401148057; color += texture2D(image, fract(uv - (off3 / resolution) + speed)) * 0.010381362401148057; return color; } float normpdf(float x) { return exp(-20.*pow(x-.5,2.)); } void main() { float easingTime = KeySpline(time, .65,.01,.26,.99); vec2 speed = vec2(0, easingTime); float blur = normpdf(easingTime); gl_FragColor = blur13(inputImageTexture, myst, iResolution, vec2(0., 20.*blur), speed); } 複製程式碼

最後給它增加一點點的垂直形變,讓它有拉伸感:
float A(float aA1, float aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; } float B(float aA1, float aA2) { return 3.0 * aA2 - 6.0 * aA1; } float C(float aA1) { return 3.0 * aA1; } float GetSlope(float aT, float aA1, float aA2) { return 3.0 * A(aA1, aA2)*aT*aT + 2.0 * B(aA1, aA2) * aT + C(aA1); } float CalcBezier(float aT, float aA1, float aA2) { return ((A(aA1, aA2)*aT + B(aA1, aA2))*aT + C(aA1))*aT; } float GetTForX(float aX, float mX1, float mX2) { float aGuessT = aX; for (int i = 0; i < 4; ++i) { float currentSlope = GetSlope(aGuessT, mX1, mX2); if (currentSlope == 0.0) return aGuessT; float currentX = CalcBezier(aGuessT, mX1, mX2) - aX; aGuessT -= currentX / currentSlope; } return aGuessT; } /* * @param aX: 傳入時間變數 * @param mX1/mY1/mX2/mY2: 貝塞爾曲線四個值 * 說明: 這個函式以上的其他函式都是本函式使用的輔助函式 */ float KeySpline(float aX, float mX1, float mY1, float mX2, float mY2) { if (mX1 == mY1 && mX2 == mY2) return aX; // linear return CalcBezier(GetTForX(aX, mX1, mX2), mY1, mY2); } vec4 blur13(sampler2D image, vec2 uv, vec2 resolution, vec2 direction, vec2 speed) { vec4 color = vec4(0.0); vec2 off1 = vec2(1.411764705882353) * direction; vec2 off2 = vec2(3.2941176470588234) * direction; vec2 off3 = vec2(5.176470588235294) * direction; color += texture2D(image, fract(uv + speed)) * 0.1964825501511404; color += texture2D(image, fract(uv + (off1 / resolution) + speed)) * 0.2969069646728344; color += texture2D(image, fract(uv - (off1 / resolution) + speed)) * 0.2969069646728344; color += texture2D(image, fract(uv + (off2 / resolution) + speed)) * 0.09447039785044732; color += texture2D(image, fract(uv - (off2 / resolution) + speed)) * 0.09447039785044732; color += texture2D(image, fract(uv + (off3 / resolution) + speed)) * 0.010381362401148057; color += texture2D(image, fract(uv - (off3 / resolution) + speed)) * 0.010381362401148057; return color; } vec2 stretchUv(vec2 _st, float t, int direction) { vec2 stUse = _st; float stretchRatio; float currentMaxStretchRatio = 1.0; if (t < 0.5) currentMaxStretchRatio = .4*pow(t, StretchSpeedPowValue) * pow(2.0, StretchSpeedPowValue) * (MaxStretchRatio - 1.0) + 1.0; else currentMaxStretchRatio = .4*pow((1.0 - t), StretchSpeedPowValue) * pow(2.0, StretchSpeedPowValue) * (MaxStretchRatio - 1.0) + 1.0; // 居左 if (direction == 1) { stretchRatio = (currentMaxStretchRatio - 1.0) * (1.-_st.x) + 1.0; stUse.y = (_st.y - 0.5) / stretchRatio + 0.5; } // 居右 else if (direction == 2) { stretchRatio = (currentMaxStretchRatio - 1.0) * _st.x+ 1.0; stUse.y = (_st.y - 0.5) / stretchRatio + 0.5; } // 居上 else if (direction == 3) { stretchRatio = (currentMaxStretchRatio - 1.0) * _st.y + 1.0; stUse.x = (_st.x - 0.5) / stretchRatio + 0.5; } // 居下 else if (direction == 4) { stretchRatio = (currentMaxStretchRatio - 1.0) * (1.-_st.y) + 1.0; stUse.x = (_st.x - 0.5) / stretchRatio + 0.5; } // 垂直 else if (direction == 5) { stretchRatio = (currentMaxStretchRatio - 1.0) * .3 + 1.0; stUse.y = (_st.y - 0.5) / stretchRatio + 0.5; } // 水平 else if (direction == 6) { stretchRatio = (currentMaxStretchRatio - 1.0) * .5 + 1.0; stUse.x = (_st.x - 0.5) / stretchRatio + 0.5; } return stUse; } float normpdf(float x) { return exp(-20.*pow(x-.5,2.)); } void main() { float easingTime = KeySpline(time, .65,.01,.26,.99); vec2 speed = vec2(0, easingTime); float blur = normpdf(easingTime); // 形變還是用勻速的 time 時間變數 myst = stretchUv(myst, time, 5); gl_FragColor = blur13(inputImageTexture, myst, iResolution, vec2(0., 20.*blur), speed); } 複製程式碼

問題三:如何解決任意方向的運動模糊?
假設我想實現旋轉轉場:
void main() { vec2 myst = uv;// 用於座標計算 float ratio = iResolution.x / iResolution.y;// 螢幕比例 float animationTime = getAnimationTime(); float easingTime = KeySpline(animationTime, .68,.01,.17,.98); float r = 0.; float rotation = 180./180.*3.14159; if (easingTime <= .5) { r = rotation * easingTime; } else { r = -rotation + rotation * easingTime; } myst.y *= 1./ratio; myst = rotateUv(myst, r, vec2(1., 0.), -1.); myst.y *= ratio; myst = fract(myst); if (easingTime <= .5) { gl_FragColor = texture2D(inputImageTexture, myst); } else { gl_FragColor = texture2D(inputImageTexture2, myst); } } 複製程式碼

旋轉運動的運動模糊方向就不算單純水平或垂直或傾斜了,假設我們粗暴的設定一個方向,看看會是怎樣:
// 改造了一下 blur13 函式,去掉了 speed 引數,因為旋轉已經在外部完成了 vec4 blur13(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) { vec4 color = vec4(0.0); vec2 off1 = vec2(1.411764705882353) * direction; vec2 off2 = vec2(3.2941176470588234) * direction; vec2 off3 = vec2(5.176470588235294) * direction; color += texture2D(image, fract(uv)) * 0.1964825501511404; color += texture2D(image, fract(uv + (off1 / resolution))) * 0.2969069646728344; color += texture2D(image, fract(uv - (off1 / resolution))) * 0.2969069646728344; color += texture2D(image, fract(uv + (off2 / resolution))) * 0.09447039785044732; color += texture2D(image, fract(uv - (off2 / resolution))) * 0.09447039785044732; color += texture2D(image, fract(uv + (off3 / resolution))) * 0.010381362401148057; color += texture2D(image, fract(uv - (off3 / resolution))) * 0.010381362401148057; return color; } void main() { vec2 myst = uv;// 用於座標計算 float ratio = iResolution.x / iResolution.y;// 螢幕比例 float animationTime = getAnimationTime(); float easingTime = KeySpline(animationTime, .68,.01,.17,.98); float blur = normpdf(easingTime); float r = 0.; float rotation = 180./180.*3.14159; if (easingTime <= .5) { r = rotation * easingTime; } else { r = -rotation + rotation * easingTime; } myst.y *= 1./ratio; myst = rotateUv(myst, r, vec2(1., 0.), -1.); myst.y *= ratio; if (easingTime <= .5) { gl_FragColor = blur13(inputImageTexture, myst, iResolution, vec2(0., 50.*blur)); } else { gl_FragColor = blur13(inputImageTexture2, myst, iResolution, vec2(0., 50.*blur)); } } 複製程式碼

這明顯不是我們像要的方向。這裡可以換一個思路,我們可以拿下一幀的座標減去上一幀的座標,得出來的值就是我們的運動方向。

所以可以這麼做:
void main() { vec2 myst = uv;// 用於座標計算 float ratio = iResolution.x / iResolution.y;// 螢幕比例 float animationTime = getAnimationTime(); float easingTime = KeySpline(animationTime, .68,.01,.17,.98); float blur = normpdf(easingTime); float r = 0.; float rotation = 180./180.*3.14159; if (easingTime <= .5) { r = rotation * easingTime; } else { r = -rotation + rotation * easingTime; } // 當前幀進行旋轉 vec2 mystCurrent = myst; mystCurrent.y *= 1./ratio; mystCurrent = rotateUv(mystCurrent, r, vec2(1., 0.), -1.); mystCurrent.y *= ratio; // 以 fps=60 作為間隔 float timeInterval = 0.00016; if (easingTime <= .5) { r = rotation * (easingTime+timeInterval); } else { r = -rotation + rotation * (easingTime+timeInterval); } // 下一幀幀進行旋轉 vec2 mystNext = myst; mystNext.y *= 1./ratio; mystNext = rotateUv(mystNext, r, vec2(1., 0.), -1.); mystNext.y *= ratio; // 得到單位座標方向 vec2 speed= (mystNext - mystCurrent / timeInterval); if (easingTime <= .5) { gl_FragColor = blur13(inputImageTexture, mystCurrent, iResolution, speed*blur*0.01); } else { gl_FragColor = blur13(inputImageTexture2, mystCurrent, iResolution, speed*blur*0.01); } } 複製程式碼

看來傳統的單方向高斯模糊並不能滿足我們想要的效果,這裡引用另外一個函式(含隨機模糊效果):
// 運動模糊 vec4 motionBlur(sampler2D texture, vec2 _st, vec2 speed) { vec2 texCoord = _st.xy / vec2(1.0).xy; vec3 color = vec3(0.0); float total = 0.0; float offset = rand(_st); for (float t = 0.0; t <= 20.0; t++) { float percent = (t + offset) / 20.0; float weight = 4.0 * (percent - percent * percent); color += getColor(texture, texCoord + speed * percent).rgb * weight; total += weight; } return vec4(color / total, 1.0); } void main() { vec2 myst = uv;// 用於座標計算 float ratio = iResolution.x / iResolution.y;// 螢幕比例 float animationTime = getAnimationTime(); float easingTime = KeySpline(animationTime, .68,.01,.17,.98); float blur = normpdf(easingTime); float r = 0.; float rotation = 180./180.*3.14159; if (easingTime <= .5) { r = rotation * easingTime; } else { r = -rotation + rotation * easingTime; } // 當前幀進行旋轉 vec2 mystCurrent = myst; mystCurrent.y *= 1./ratio; mystCurrent = rotateUv(mystCurrent, r, vec2(1., 0.), -1.); mystCurrent.y *= ratio; // 以 fps=60 作為間隔 float timeInterval = 0.0167; if (easingTime <= .5) { r = rotation * (easingTime+timeInterval); } else { r = -rotation + rotation * (easingTime+timeInterval); } // 下一幀幀進行旋轉 vec2 mystNext = myst; mystNext.y *= 1./ratio; mystNext = rotateUv(mystNext, r, vec2(1., 0.), -1.); mystNext.y *= ratio; // 得到單位座標方向 vec2 speed= (mystNext - mystCurrent) / timeInterval * blur * 0.5; // if (easingTime <= .5) { //gl_FragColor = blur13(inputImageTexture, mystCurrent, iResolution, speed*blur*0.01); // } else { //gl_FragColor = blur13(inputImageTexture2, mystCurrent, iResolution, speed*blur*0.01); // } if (easingTime <= .5) { gl_FragColor = motionBlur(inputImageTexture, mystCurrent, speed); } else { gl_FragColor = motionBlur(inputImageTexture2, mystCurrent, speed); } } 複製程式碼
顯然,對於複雜一些的運動(非位移),單純的高斯模糊並不能帶來很逼真的效果,還需要搭配隨機模糊、變形、扭曲等因素:

接下來我們再新增一些畫布的縮放,回彈等效果進來:
void main() { vec2 myst = uv;// 用於座標計算 float ratio = iResolution.x / iResolution.y;// 螢幕比例 float animationTime = getAnimationTime(); float animationTime2 = smoothstep(.2, 1., animationTime); float easingTime = KeySpline(animationTime2, .4,.71,.26,1.07); float easingTime2 = KeySpline(animationTime2, 0.,.47,.99,.57); float blur = normpdf(easingTime); float r = 0.; float rotation = 180./180.*3.14159; if (easingTime <= .5) { r = rotation * easingTime; } else { r = -rotation + rotation * easingTime; } if (animationTime < .2) { myst -= .5; myst *= scaleUv(vec2(0.92-animationTime*.3)); myst += .5; gl_FragColor = texture2D(inputImageTexture, myst); } else { myst = stretchUv(myst, easingTime2, 1);// 左側拉伸 myst = stretchUv(myst, easingTime2, 3);// 頂部拉伸 myst = stretchUv(myst, easingTime, 5);// 垂直拉伸 // 當前幀進行旋轉 vec2 mystCurrent = myst; mystCurrent.y *= 1./ratio; mystCurrent = rotateUv(mystCurrent, r, vec2(1., 0.), -1.); mystCurrent.y *= ratio; // 以 fps=60 作為間隔,計算出實際幀速率 float timeInterval = 0.016; if (animationTime < 0.5 && animationTime + timeInterval > 0.5) timeInterval = 0.5 - animationTime; if (easingTime <= .5) { r = rotation * (easingTime+timeInterval); } else { r = -rotation + rotation * (easingTime+timeInterval); } // 下一幀幀進行旋轉 vec2 mystNext = myst; mystNext.y *= 1./ratio; mystNext = rotateUv(mystNext, r, vec2(1., 0.), -1.); mystNext.y *= ratio; // 得到單位座標方向 vec2 speed= (mystNext - mystCurrent) / timeInterval * blur * 0.5; if (easingTime <= .5) { mystCurrent -= .5; mystCurrent *= scaleUv(vec2(0.92-animationTime*.3)); mystCurrent += .5; gl_FragColor = motionBlur(inputImageTexture, mystCurrent, speed); } else { mystCurrent -= .5; mystCurrent *= scaleUv(vec2(0.92)); mystCurrent += .5; gl_FragColor = motionBlur(inputImageTexture2, mystCurrent, speed); } } } 複製程式碼
