作者:位元組流動
來源:
https://blog.csdn.net/Kennethdroid/article/details/106556584 漣漪效果展示水波紋效果原理
最近一個做視訊濾鏡的朋友,讓我給他做一個動态水波紋效果,具體就是:點選螢幕上的某一位置,然後波紋以該位置為中心向周圍擴散。接到這個需求,一開始就嘗試着在 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) 采樣坐标移動距離
我們替換平滑函數後,繪制結果如下圖所示,結果符合預期,沒有了形變在邊界處的跳變。
效果展示1另外,我們在網上找到一個古怪的函數,y= (1-Math.pow(Math.abs(20*x), 4.8))*x ,繪制出來的效果如下圖所示,看起來比較有意思。
效果展示2當然,我們也可以在形變區域内建構具有多個零點的平滑函數,來制造多個波動效果,更多有意思的效果留給你去探索吧。
實作代碼路徑:
Android_OpenGLES_3_0「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。