該原創文章首發於微信公衆號:字節流動
水波紋效果原理
最近一個做視頻濾鏡的朋友,讓我給他做一個動態水波紋效果,具體就是:點擊屏幕上的某一位置,然後波紋以該位置爲中心向周圍擴散。接到這個需求,一開始就嘗試着在 3D 座標系(x,y,z)中利用正弦或餘弦函數去修改 z 分量的值,但是這樣出來的效果太假了,壓根就沒有水波紋的真實感。
然後,我就乖乖地去研究下物理世界中的水波紋是怎樣形成的。你別說,我還真接了一盆水,坐在旁邊觀察了半天。
最後觀察出,物理世界中水波紋的特點如上圖所示,從水面的正上方往下看,在凹面上方觀察到的是縮小效果,而在凸面上方觀察到的是放大效果,然後整個水波紋效果就是放大和縮小效果的交叉排列。
因此,我們得出結論,水波紋(漣漪)效果實際上就是一組組相互交替、幅度向外部逐漸減小的縮小放大效果組合。本文將水波紋模型簡化成一組放大和縮小效果隨時間逐步向外部偏移。
如上圖所示,我們以點擊位置爲中心,發生形變的區域是內圓和外圓之間的區域,以歸一化時間變量 u_Time 大小爲半徑構建的圓(藍色虛線)爲邊界,設定內側是實現縮小效果的區域,外側爲實現放大效果的區域,也可以反之設定。
發生形變區域的寬度爲固定值 2*u_Boundary ,然後這個形變區域隨着 u_Time 的變大逐步向外側移動,最後就形成了動態的水波紋效果。
我們設採樣點到中心點的距離爲 Distance ,然後計算 Distance-u_Time=diff 的值來判定,採樣點是位於縮小區域(diff < 0)還是放大區域(diff > 0),最後我們只需要構建一個平滑的函數,以 diff 作爲輸入,diff < 0 的時候函數輸出正值,diff > 0 的時候函數輸出負值。
爲什麼要這樣做?因爲我們的根本目標就是爲了實現一定區域內的縮小和放大效果,我們以平滑函數的輸出值作爲紋理採樣座標的偏移程度,當平滑函數輸出正值時,採樣座標向圓外側偏移,呈現縮小效果,而平滑函數輸出負值時,採樣座標向圓內側偏移,呈現放大效果。
另外,爲了防止形變效果的跳變,我們還需要平滑函數滿足在邊界處輸出值爲 0 (或者接近於 0 ),表示此邊界爲是否發生形變的臨界線。
水波紋效果實現
基於上節的原理分析,我們接下來需要找一個合適的平滑函數,根據以上特徵首先我想到的函數是 -x^3 ,它滿足了平滑和輸出值(左正右負)的條件。
在構建我們想要的平滑函數時,http://fooplot.com/網站提供了在線函數繪圖功能,可以很方便看出一個函數的生成曲線。
我們根據類似 -x^3 函數,構建如下的片段着色器,試驗下效果是否符合預期。
#version 300 es
precision highp float;
in vec2 v_texCoord;
layout(location = 0) out vec4 outColor;
uniform sampler2D s_TextureMap;//採樣器
uniform vec2 u_TouchXY;//點擊的位置(歸一化)
uniform vec2 u_TexSize;//紋理尺寸
uniform float u_Time;//歸一化的時間
uniform float u_Boundary;//邊界 0.1
void main()
{
float ratio = u_TexSize.y / u_TexSize.x;
vec2 texCoord = v_texCoord * vec2(1.0, ratio);//根據紋理尺寸,對採用座標進行轉換
vec2 touchXY = u_TouchXY * vec2(1.0, ratio);//根據紋理尺寸,對中心點座標進行轉換
float distance = distance(texCoord, touchXY);//採樣點座標與中心點的距離
if ((u_Time - u_Boundary) > 0.0
&& (distance <= (u_Time + u_Boundary))
&& (distance >= (u_Time - u_Boundary))) {
float diff = (distance - u_Time); //輸入 diff
float moveDis = - pow(8 * diff, 3.0);//平滑函數 -(8x)^3 採樣座標移動距離
vec2 unitDirectionVec = normalize(texCoord - touchXY);//單位方向向量
texCoord = texCoord + (unitDirectionVec * moveDis);//採樣座標偏移(實現放大和縮小效果)
}
texCoord = texCoord / vec2(1.0, ratio);//轉換回來
outColor = texture(s_TextureMap, texCoord);
}
繪製的邏輯:
void ShockWaveSample::Draw(int screenW, int screenH)
{
LOGCATE("ShockWaveSample::Draw()");
m_SurfaceWidth = screenW;
m_SurfaceHeight = screenH;
if(m_ProgramObj == GL_NONE || m_TextureId == GL_NONE) return;
m_FrameIndex ++;
UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, (float)screenW / screenH);
glUseProgram (m_ProgramObj);
glBindVertexArray(m_VaoId);
GLUtils::setMat4(m_ProgramObj, "u_MVPMatrix", m_MVPMatrix);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureId);
GLUtils::setFloat(m_ProgramObj, "s_TextureMap", 0);
//float time = static_cast<float>(fmod(GetSysCurrentTime(), 2000) / 2000);
float time = static_cast<float>(fmod(m_FrameIndex, 150) / 120);
GLUtils::setFloat(m_ProgramObj, "u_Time", time);
//設置點擊位置
GLUtils::setVec2(m_ProgramObj, "u_TouchXY", m_touchXY);
//設置紋理尺寸
GLUtils::setVec2(m_ProgramObj, "u_TexSize", vec2(m_RenderImage.width, m_RenderImage.height));
//設置邊界值
GLUtils::setFloat(m_ProgramObj, "u_Boundary", 0.1f);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);
}
我們使用 y=-(8*x)^3 作爲平滑函數得出來的效果圖如下所示,雖然有水波紋效果,但是形變邊界跳變嚴重,原來是該平滑函數沒有滿足,在邊界處輸出值爲 0 的條件。
爲了滿足平滑函數的輸出值在邊界處爲 0 的條件,我們利用 fooplot 構建的一個函數 y=20x*(x-0.1)*(x+0.1) ,函數曲線如下圖所示,由於邊界值 u_Boundary 爲 0.1 ,該函數滿足我們的需求。
在上述片段着色器中,我們替換下平滑函數:
float x = (distance - u_Time); //輸入 diff = x
float moveDis = 20.0 * x * (x - 0.1)*(x + 0.1);//平滑函數 y=20.0 * x * (x - 0.1)*(x + 0.1) 採樣座標移動距離
我們替換平滑函數後,繪製結果如下圖所示,結果符合預期,沒有了形變在邊界處的跳變。
另外,我們在網上找到一個古怪的函數,y= (1-Math.pow(Math.abs(20*x), 4.8))*x ,繪製出來的效果如下圖所示,看起來比較有意思。
當然,我們也可以在形變區域內構建具有多個零點的平滑函數,來製造多個波動效果,更多有意思的效果留給你去探索吧。
實現代碼路徑:
Android_OpenGLES_3_0