本文通過(guò)模仿抖音中幾種特效的實(shí)現(xiàn)妓盲,來(lái)講解 GLSL 的實(shí)際應(yīng)用杂拨。
前言
本文的靈感來(lái)自于 《當(dāng)一個(gè) Android 開(kāi)發(fā)玩抖音玩瘋了之后(二)》 這篇文章。
這位博主在 Android 平臺(tái)上悯衬,通過(guò)自己的分析弹沽,嘗試還原了抖音上的幾種視頻特效。他是通過(guò)「部分 GLSL 代碼 + 部分 Java 代碼」的方式來(lái)實(shí)現(xiàn)的筋粗。
讀完之后策橘,在膜拜之余,我產(chǎn)生了一個(gè)大膽的想法:我可不可以在 iOS 上娜亿,只通過(guò)純 GLSL 的編寫丽已,來(lái)實(shí)現(xiàn)類似的效果呢?
很好的想法暇唾,不過(guò)促脉,由于抖音的特效是基于視頻的濾鏡,我們?cè)谶@之前只講到了關(guān)于圖片的渲染策州,如果馬上跳躍到視頻的部分瘸味,好像有點(diǎn)超綱了。
于是够挂,我又有了一個(gè)更大膽的想法:我可不可以在 iOS 上旁仿,只通過(guò)純 GLSL 的編寫,在靜態(tài)的圖片上孽糖,實(shí)現(xiàn)類似的效果呢枯冈?
這樣的話,我們就可以把更多的注意力放在 GLSL 本身办悟,而不是視頻的采集和輸出上面尘奏。
于是,就有了這篇文章病蛉。為了無(wú)縫地過(guò)渡炫加,我會(huì)沿用之前 GLSL 渲染的例子 ,只改變 Shader 部分的代碼铺然,來(lái)嘗試還原那篇文章中實(shí)現(xiàn)的六種特效俗孝。
〇、動(dòng)畫
你可能會(huì)問(wèn):抖音上的特效都是動(dòng)態(tài)的魄健,要怎么把動(dòng)態(tài)的效果赋铝,加到一個(gè)靜態(tài)的圖片上呢?
問(wèn)的好沽瘦,所以第一步革骨,我們就要讓靜態(tài)的圖片動(dòng)起來(lái)农尖。
回想一下,我們?cè)?UIKit
中實(shí)現(xiàn)的動(dòng)畫苛蒲,無(wú)非就是把指令發(fā)送給 CoreAnimation
卤橄,然后在屏幕刷新的時(shí)候,CoreAnimation
會(huì)去逐幀計(jì)算當(dāng)前應(yīng)該顯示的圖像臂外。
這里的重點(diǎn)是「逐幀計(jì)算」窟扑。在 OpenGL ES 中也是類似,我們實(shí)現(xiàn)動(dòng)畫的方式漏健,就是自己去計(jì)算每一幀應(yīng)該顯示的圖像嚎货,然后在屏幕刷新的時(shí)候,重新渲染蔫浆。
這個(gè)「逐幀計(jì)算」的過(guò)程殖属,我們是放到 Shader 中進(jìn)行的。然后我們可以通過(guò)一個(gè)表示時(shí)間的參數(shù)瓦盛,在重新渲染的時(shí)候洗显,傳入當(dāng)前的時(shí)間,讓 Shader 計(jì)算出當(dāng)前動(dòng)畫的進(jìn)度原环。至于重新渲染的時(shí)機(jī)挠唆,則是依靠 CADisplayLink
來(lái)實(shí)現(xiàn)的。
具體代碼大概像這樣:
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(timeAction)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
- (void)timeAction {
glUseProgram(self.program);
glBindBuffer(GL_ARRAY_BUFFER, self.vertexBuffer);
// 傳入時(shí)間
CGFloat currentTime = self.displayLink.timestamp - self.startTimeInterval;
GLuint time = glGetUniformLocation(self.program, "Time");
glUniform1f(time, currentTime);
// 清除畫布
glClear(GL_COLOR_BUFFER_BIT);
glClearColor(1, 1, 1, 1);
// 重繪
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
[self.context presentRenderbuffer:GL_RENDERBUFFER];
}
相應(yīng)地嘱吗,在 Shader 中有一個(gè) uniform
修飾的 Time
參數(shù):
uniform float Time;
這樣 Shader 就可以通過(guò) Time
來(lái)計(jì)算出當(dāng)前應(yīng)該顯示的圖像了玄组。
一、縮放
1谒麦、最終效果
我們要實(shí)現(xiàn)的第一種效果是「縮放」俄讹,看起來(lái)很簡(jiǎn)單,可以通過(guò)修改頂點(diǎn)坐標(biāo)和紋理坐標(biāo)的對(duì)應(yīng)關(guān)系來(lái)實(shí)現(xiàn)绕德。
這是一個(gè)很基礎(chǔ)的效果患膛,在下面的其它特效中還會(huì)用到。修改坐標(biāo)的對(duì)應(yīng)關(guān)系可以通過(guò)修改頂點(diǎn)著色器耻蛇,或者修改片段著色器來(lái)實(shí)現(xiàn)剩瓶。 這里先講修改頂點(diǎn)著色器的方式,在后面的特效中會(huì)再提一下修改片段著色器的方式城丧。
2、代碼實(shí)現(xiàn)
頂點(diǎn)著色器代碼:
attribute vec4 Position;
attribute vec2 TextureCoords;
varying vec2 TextureCoordsVarying;
uniform float Time;
const float PI = 3.1415926;
void main (void) {
float duration = 0.6;
float maxAmplitude = 0.3;
float time = mod(Time, duration);
float amplitude = 1.0 + maxAmplitude * abs(sin(time * (PI / duration)));
gl_Position = vec4(Position.x * amplitude, Position.y * amplitude, Position.zw);
TextureCoordsVarying = TextureCoords;
}
這里的 duration
表示一次縮放周期的時(shí)長(zhǎng)豌鹤,mod(Time, duration)
表示將傳入的時(shí)間轉(zhuǎn)換到一個(gè)周期內(nèi)亡哄,即 time
的范圍是 0 ~ 0.6
,amplitude
表示振幅布疙,引入 PI
的目的是為了使用 sin
函數(shù)蚊惯,將 amplitude
的范圍控制在 1.0 ~ 1.3
之間愿卸,并隨著時(shí)間變化。
這里放大的關(guān)鍵在于 vec4(Position.x * amplitude, Position.y * amplitude, Position.zw)
截型,我們將頂點(diǎn)坐標(biāo)的 x
和 y
分別乘上一個(gè)放大系數(shù)趴荸,在紋理坐標(biāo)不變的情況下,就達(dá)到了拉伸的效果宦焦。
二发钝、靈魂出竅
1、最終效果
「靈魂出竅」看上去是兩個(gè)層的疊加波闹,并且上面的那層隨著時(shí)間的推移酝豪,會(huì)逐漸放大且不透明度逐漸降低。這里也用到了放大的效果精堕,我們這次用片段著色器來(lái)實(shí)現(xiàn)孵淘。
2、代碼實(shí)現(xiàn)
片段著色器代碼:
precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
uniform float Time;
void main (void) {
float duration = 0.7;
float maxAlpha = 0.4;
float maxScale = 1.8;
float progress = mod(Time, duration) / duration; // 0~1
float alpha = maxAlpha * (1.0 - progress);
float scale = 1.0 + (maxScale - 1.0) * progress;
float weakX = 0.5 + (TextureCoordsVarying.x - 0.5) / scale;
float weakY = 0.5 + (TextureCoordsVarying.y - 0.5) / scale;
vec2 weakTextureCoords = vec2(weakX, weakY);
vec4 weakMask = texture2D(Texture, weakTextureCoords);
vec4 mask = texture2D(Texture, TextureCoordsVarying);
gl_FragColor = mask * (1.0 - alpha) + weakMask * alpha;
}
首先是放大的效果歹篓。關(guān)鍵點(diǎn)在于 weakX
和 weakY
的計(jì)算瘫证,比如 0.5 + (TextureCoordsVarying.x - 0.5) / scale
這一句的意思是,將頂點(diǎn)坐標(biāo)對(duì)應(yīng)的紋理坐標(biāo)的 x
值到紋理中點(diǎn)的距離庄撮,縮小一定的比例背捌。這次我們是改變了紋理坐標(biāo),而保持頂點(diǎn)坐標(biāo)不變重窟,同樣達(dá)到了拉伸的效果载萌。
然后是兩層疊加的效果蛙讥。通過(guò)上面的計(jì)算狼忱,我們得到了兩個(gè)紋理顏色值 weakMask
和 mask
, weakMask
是在 mask
的基礎(chǔ)上做了放大處理展蒂。
我們將兩個(gè)顏色值進(jìn)行疊加需要用到一個(gè)公式:最終色 = 基色 * a% + 混合色 * (1 - a%) 厅翔,這個(gè)公式來(lái)自 混合模式中的正常模式 乖坠。
這個(gè)公式表明了一個(gè)不透明的層和一個(gè)半透明的層進(jìn)行疊加,重疊部分的最終顏色值刀闷。因此熊泵,上面疊加的最終結(jié)果是 mask * (1.0 - alpha) + weakMask * alpha
。
三甸昏、抖動(dòng)
1顽分、最終效果
「抖動(dòng)」是很經(jīng)典的抖音的顏色偏移效果,其實(shí)這個(gè)效果實(shí)現(xiàn)起來(lái)還挺簡(jiǎn)單的施蜜。另外卒蘸,除了顏色偏移,可以看到還有微弱的放大效果。
2缸沃、代碼實(shí)現(xiàn)
片段著色器代碼:
precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
uniform float Time;
void main (void) {
float duration = 0.7;
float maxScale = 1.1;
float offset = 0.02;
float progress = mod(Time, duration) / duration; // 0~1
vec2 offsetCoords = vec2(offset, offset) * progress;
float scale = 1.0 + (maxScale - 1.0) * progress;
vec2 ScaleTextureCoords = vec2(0.5, 0.5) + (TextureCoordsVarying - vec2(0.5, 0.5)) / scale;
vec4 maskR = texture2D(Texture, ScaleTextureCoords + offsetCoords);
vec4 maskB = texture2D(Texture, ScaleTextureCoords - offsetCoords);
vec4 mask = texture2D(Texture, ScaleTextureCoords);
gl_FragColor = vec4(maskR.r, mask.g, maskB.b, mask.a);
}
這里的放大和上面類似恰起,我們主要看一下顏色偏移。顏色偏移是對(duì)三個(gè)顏色通道進(jìn)行分離趾牧,并且給紅色通道和藍(lán)色通道添加了不同的位置偏移检盼,代碼很容易看懂。
四翘单、閃白
1吨枉、最終效果
「閃白」其實(shí)看起來(lái)一點(diǎn)兒也不酷炫,而且看久了還容易被閃瞎县恕。這個(gè)效果實(shí)現(xiàn)起來(lái)也十分簡(jiǎn)單东羹,無(wú)非就是疊加一個(gè)白色層,然后白色層的透明度隨著時(shí)間不斷地變化忠烛。
2属提、代碼實(shí)現(xiàn)
片段著色器代碼:
precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
uniform float Time;
const float PI = 3.1415926;
void main (void) {
float duration = 0.6;
float time = mod(Time, duration);
vec4 whiteMask = vec4(1.0, 1.0, 1.0, 1.0);
float amplitude = abs(sin(time * (PI / duration)));
vec4 mask = texture2D(Texture, TextureCoordsVarying);
gl_FragColor = mask * (1.0 - amplitude) + whiteMask * amplitude;
}
在上面「靈魂出竅」的例子中,我們已經(jīng)知道了如何實(shí)現(xiàn)兩個(gè)層的疊加美尸。這里我們只需要?jiǎng)?chuàng)建一個(gè)白色的層 whiteMask
冤议,然后根據(jù)當(dāng)前的透明度來(lái)計(jì)算最終的顏色值即可。
五师坎、毛刺
1恕酸、最終效果
終于有了一個(gè)稍微復(fù)雜一點(diǎn)的效果,「毛刺」看上去是「撕裂 + 微弱的顏色偏移」胯陋。顏色偏移我們?cè)谏厦嬉呀?jīng)實(shí)現(xiàn)蕊温,這里主要是講解撕裂的效果。
具體的思路是遏乔,我們讓每一行像素隨機(jī)偏移 -1 ~ 1
的距離(這里的 -1 ~ 1
是對(duì)于紋理坐標(biāo)來(lái)說(shuō)的)义矛,但是如果整個(gè)畫面都偏移比較大的值,那我們可能都看不出原來(lái)圖像的樣子盟萨。所以我們的邏輯是凉翻,設(shè)定一個(gè)閾值,小于這個(gè)閾值才進(jìn)行偏移捻激,超過(guò)這個(gè)閾值則乘上一個(gè)縮小系數(shù)制轰。
則最終呈現(xiàn)的效果是:絕大部分的行都會(huì)進(jìn)行微小的偏移,只有少量的行會(huì)進(jìn)行較大偏移胞谭。
2垃杖、代碼實(shí)現(xiàn)
片段著色器代碼:
precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
uniform float Time;
const float PI = 3.1415926;
float rand(float n) {
return fract(sin(n) * 43758.5453123);
}
void main (void) {
float maxJitter = 0.06;
float duration = 0.3;
float colorROffset = 0.01;
float colorBOffset = -0.025;
float time = mod(Time, duration * 2.0);
float amplitude = max(sin(time * (PI / duration)), 0.0);
float jitter = rand(TextureCoordsVarying.y) * 2.0 - 1.0; // -1~1
bool needOffset = abs(jitter) < maxJitter * amplitude;
float textureX = TextureCoordsVarying.x + (needOffset ? jitter : (jitter * amplitude * 0.006));
vec2 textureCoords = vec2(textureX, TextureCoordsVarying.y);
vec4 mask = texture2D(Texture, textureCoords);
vec4 maskR = texture2D(Texture, textureCoords + vec2(colorROffset * amplitude, 0.0));
vec4 maskB = texture2D(Texture, textureCoords + vec2(colorBOffset * amplitude, 0.0));
gl_FragColor = vec4(maskR.r, mask.g, maskB.b, mask.a);
}
上面提到的像素隨機(jī)偏移需要用到隨機(jī)數(shù),可惜 GLSL 里并沒(méi)有內(nèi)置的隨機(jī)函數(shù)丈屹,所以我們需要自己實(shí)現(xiàn)一個(gè)缩滨。
這個(gè) float rand(float n)
的實(shí)現(xiàn)看上去很神奇,它其實(shí)是來(lái)自 這里 ,江湖人稱「噪聲函數(shù)」脉漏。
它其實(shí)是一個(gè)偽隨機(jī)函數(shù),本質(zhì)上是一個(gè) Hash 函數(shù)袖牙。但在這里我們可以把它當(dāng)成隨機(jī)函數(shù)來(lái)使用侧巨,它的返回值范圍是 0 ~ 1
。如果你對(duì)這個(gè)函數(shù)想了解更多的話可以看 這里 鞭达。
六司忱、幻覺(jué)
1、最終效果
「幻覺(jué)」這個(gè)效果有點(diǎn)一言難盡畴蹭,因?yàn)槠鋵?shí)看上去并不是很像坦仍。原來(lái)的效果是基于視頻上一幀的結(jié)果去合成,靜態(tài)的圖片很難模擬出這種情況叨襟。不管怎么說(shuō)繁扎,既然已經(jīng)盡力,不像就不像吧糊闽,下面講一下我的實(shí)現(xiàn)思路梳玫。
可以看出這個(gè)效果是殘影和顏色偏移的疊加。
殘影的效果還好右犹,在移動(dòng)的過(guò)程中提澎,每經(jīng)過(guò)一段時(shí)間間隔,根據(jù)當(dāng)前的位置去創(chuàng)建一個(gè)新層念链,并且新層的不透明度隨著時(shí)間逐漸減弱盼忌。于是在一個(gè)移動(dòng)周期內(nèi),可以看到很多透明度不同的層疊加在一起掂墓,從而形成殘影的效果谦纱。
然后是這個(gè)顏色偏移。我們可以看到梆暮,物體移動(dòng)的過(guò)程是藍(lán)色在前面服协,紅色在后面。所以整個(gè)過(guò)程可以理解成:在移動(dòng)的過(guò)程中啦粹,每間隔一段時(shí)間偿荷,遺失了一部分紅色通道的值在原來(lái)的位置,并且這部分紅色通道的值唠椭,隨著時(shí)間偏移跳纳,會(huì)逐漸恢復(fù)。
2贪嫂、代碼實(shí)現(xiàn)
片段著色器代碼:
precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
uniform float Time;
const float PI = 3.1415926;
const float duration = 2.0;
vec4 getMask(float time, vec2 textureCoords, float padding) {
vec2 translation = vec2(sin(time * (PI * 2.0 / duration)),
cos(time * (PI * 2.0 / duration)));
vec2 translationTextureCoords = textureCoords + padding * translation;
vec4 mask = texture2D(Texture, translationTextureCoords);
return mask;
}
float maskAlphaProgress(float currentTime, float hideTime, float startTime) {
float time = mod(duration + currentTime - startTime, duration);
return min(time, hideTime);
}
void main (void) {
float time = mod(Time, duration);
float scale = 1.2;
float padding = 0.5 * (1.0 - 1.0 / scale);
vec2 textureCoords = vec2(0.5, 0.5) + (TextureCoordsVarying - vec2(0.5, 0.5)) / scale;
float hideTime = 0.9;
float timeGap = 0.2;
float maxAlphaR = 0.5; // max R
float maxAlphaG = 0.05; // max G
float maxAlphaB = 0.05; // max B
vec4 mask = getMask(time, textureCoords, padding);
float alphaR = 1.0; // R
float alphaG = 1.0; // G
float alphaB = 1.0; // B
vec4 resultMask = vec4(0, 0, 0, 0);
for (float f = 0.0; f < duration; f += timeGap) {
float tmpTime = f;
vec4 tmpMask = getMask(tmpTime, textureCoords, padding);
float tmpAlphaR = maxAlphaR - maxAlphaR * maskAlphaProgress(time, hideTime, tmpTime) / hideTime;
float tmpAlphaG = maxAlphaG - maxAlphaG * maskAlphaProgress(time, hideTime, tmpTime) / hideTime;
float tmpAlphaB = maxAlphaB - maxAlphaB * maskAlphaProgress(time, hideTime, tmpTime) / hideTime;
resultMask += vec4(tmpMask.r * tmpAlphaR,
tmpMask.g * tmpAlphaG,
tmpMask.b * tmpAlphaB,
1.0);
alphaR -= tmpAlphaR;
alphaG -= tmpAlphaG;
alphaB -= tmpAlphaB;
}
resultMask += vec4(mask.r * alphaR, mask.g * alphaG, mask.b * alphaB, 1.0);
gl_FragColor = resultMask;
}
從代碼的行數(shù)可以看出寺庄,這個(gè)效果應(yīng)該是里面最復(fù)雜的。為了實(shí)現(xiàn)殘影,我們先讓圖片隨時(shí)間做圓周運(yùn)動(dòng)斗塘。
vec4 getMask(float time, vec2 textureCoords, float padding)
這個(gè)函數(shù)可以計(jì)算出赢织,在某個(gè)時(shí)刻圖片的具體位置。通過(guò)它我們可以每經(jīng)過(guò)一段時(shí)間馍盟,去生成一個(gè)新的層于置。
float maskAlphaProgress(float currentTime, float hideTime, float startTime)
這個(gè)函數(shù)可以計(jì)算出,某個(gè)時(shí)刻創(chuàng)建的層贞岭,在當(dāng)前時(shí)刻的透明度八毯。
maxAlphaR
、 maxAlphaG
瞄桨、 maxAlphaB
分別指定了新層初始的三個(gè)顏色通道的透明度话速。因?yàn)樽罱K的效果是殘留紅色,所以主要保留了紅色通道的值芯侥。
然后是疊加泊交,和兩層疊加的情況類似,這里通過(guò) for
循環(huán)來(lái)累加每一層的每個(gè)通道乘上自身的透明度的值筹麸,算出最終的顏色值 resultMask
活合。
注: 在 iOS 的模擬器上,只能用 CPU 來(lái)模擬 GPU 的功能物赶。所以在模擬器上運(yùn)行上面的代碼時(shí)白指,可能會(huì)十分卡頓。尤其是最后這個(gè)效果酵紫,由于計(jì)算量太大告嘲,親測(cè)模擬器顯示不出來(lái)。因此如果要跑代碼奖地,最好使用真機(jī)運(yùn)行橄唬。
源碼
請(qǐng)到 GitHub 上查看完整代碼。
參考
獲取更佳的閱讀體驗(yàn)参歹,請(qǐng)?jiān)L問(wèn)原文地址【Lyman's Blog】在 iOS 中使用 GLSL 實(shí)現(xiàn)抖音特效