本文通過編寫一個通用的片段著色器,實現(xiàn)了抖音中的各種分屏濾鏡不见。另外,還講解了延時動態(tài)分屏濾鏡的實現(xiàn)订歪。
一脖祈、靜態(tài)分屏
靜態(tài)分屏指的是,每一個屏的圖像都完全一樣刷晋。
分屏濾鏡實現(xiàn)起來比較容易盖高,無非是在片段著色器中,修改紋理坐標(biāo)和紋理的對應(yīng)關(guān)系眼虱。分屏之后喻奥,每個屏內(nèi)紋理的對應(yīng)關(guān)系都不太一樣。因此在實現(xiàn)的時候捏悬,容易寫的很復(fù)雜撞蚕,會有大量的區(qū)域判斷邏輯。
這樣實現(xiàn)出來的著色器拓展性比較差过牙。假如有多種分屏濾鏡甥厦,就要實現(xiàn)多個著色器纺铭,而且屏數(shù)越多,區(qū)域判斷邏輯就越復(fù)雜刀疙。
所以舶赔,我們會采取一種更優(yōu)雅的方式,為所有的分屏濾鏡實現(xiàn)一個通用的著色器谦秧,然后將屏數(shù)當(dāng)作參數(shù)竟纳,由著色器外部控制。
預(yù)備知識
首先疚鲤,我們來了解等一下會使用到的 GLSL 運算和函數(shù)锥累。vec2
是二維向量類型,它支持下面的各種運算集歇。
1桶略、向量與向量的加減乘除(兩個向量需要保證維數(shù)相同)
下面以乘法為例,其他類似鬼悠。
vec2 a, b, c;
c = a * b;
等價于
c.x = a.x * b.x;
c.y = a.y * b.y;
2删性、向量與標(biāo)量的加減乘除
下面以加法為例亏娜,其他類似焕窝。
vec2 a, b;
float c;
b = a + c;
等價于
b.x = a.x + c;
b.y = a.y + c;
3、向量與向量的 mod 運算(兩個向量需要保證維數(shù)相同)
vec2 a, b, c;
c = mod(a, b);
等價于
c.x = mod(a.x, b.x);
c.y = mod(a.y, b.y);
4维贺、向量與標(biāo)量的 mod 運算
vec2 a, b;
float c;
b = mod(a, c);
等價于
b.x = mod(a.x, c);
b.y = mod(a.y, c);
著色器實現(xiàn)
有了上面的 GLSL 運算知識它掂,來看下我們最終實現(xiàn)的片段著色器。
precision highp float;
uniform sampler2D inputImageTexture;
varying vec2 textureCoordinate;
uniform float horizontal; // (1)
uniform float vertical;
void main (void) {
float horizontalCount = max(horizontal, 1.0); // (2)
float verticalCount = max(vertical, 1.0);
float ratio = verticalCount / horizontalCount; // (3)
vec2 originSize = vec2(1.0, 1.0);
vec2 newSize = originSize;
if (ratio > 1.0) {
newSize.y = 1.0 / ratio;
} else {
newSize.x = ratio;
}
vec2 offset = (originSize - newSize) / 2.0; // (4)
vec2 position = offset + mod(textureCoordinate * min(horizontalCount, verticalCount), newSize); // (5)
gl_FragColor = texture2D(inputImageTexture, position); // (6)
}
(1) 我們最終暴露的接口溯泣,通過 uniform
變量的形式虐秋,從著色器外部傳入橫向分屏數(shù) horizontal
和縱向分屏數(shù) vertical
。
(2) 開始運算前垃沦,做了最小分屏數(shù)的限制客给,避免小于 1.0
的分屏數(shù)出現(xiàn)。
(3) 從這一行開始肢簿,是為了計算分屏之后靶剑,每一屏的新尺寸。比如分成 2 : 2池充,則 newSize
仍然是 (1.0, 1.0)
桩引,因為每一屏都能顯示完整的圖像;而分成 3 : 2(橫向 3 屏收夸,縱向 2 屏)坑匠,則 newSize
將會是 (2.0 / 3.0, 1.0)
,因為每一屏的縱向能顯示完整的圖像卧惜,而橫向只能顯示 2 / 3 的圖像厘灼。
(4) 計算新的圖像在原始圖像中的偏移量夹纫。因為我們的圖像要居中裁剪,所以要計算出裁剪后的偏移设凹。比如 (2.0 / 3.0, 1.0)
的圖像捷凄,對應(yīng)的 offset
是 (1.0 / 6.0, 0.0)
。
(5) 這一行是這個著色器的精華所在围来,可能不太好理解跺涤。我們將原始的紋理坐標(biāo),乘上 horizontalCount
和 verticalCount
的較小者监透,然后對新的尺寸進行求模運算桶错。這樣,當(dāng)原始紋理坐標(biāo)在 0 ~ 1 的范圍內(nèi)增長時胀蛮,可以讓新的紋理坐標(biāo)在 newSize
的范圍內(nèi)循環(huán)多次院刁。另外,計算的結(jié)果加上 offset
粪狼,可以讓新的紋理坐標(biāo)偏移到居中的位置退腥。
下面簡單演示一下每一步計算的效果,幫助理解:
(6) 通過新的計算出來的紋理坐標(biāo)再榄,從紋理中讀出相應(yīng)的顏色值輸出狡刘。
效果展示
現(xiàn)在,我們得到了一個通用的分屏著色器困鸥,像三屏嗅蔬、六屏、九屏這些效果疾就,只需要修改兩個參數(shù)就可以實現(xiàn)澜术。另外,上面的實現(xiàn)邏輯猬腰,甚至可以支持 1.5 : 2.5 這種非整數(shù)的分屏操作鸟废。
二、動態(tài)分屏
動態(tài)分屏指的是姑荷,每個屏的圖像都不一樣盒延,每間隔一段時間,會主動捕獲一個新的圖像厢拭。
由于每個屏的圖像都不一樣兰英,因此在渲染過程中,需要捕獲多個不同的紋理供鸠。比如我們想要實現(xiàn)一個四屏的濾鏡畦贸,就需要捕獲 4 個不同的紋理。
預(yù)備知識
我們知道,在 GPUImage 框架中薄坏,濾鏡效果的渲染發(fā)生在 GPUImageFilter
中趋厉。
從渲染層面來說,GPUImageFilter
接收一個紋理的輸入胶坠,然后經(jīng)過自身效果的渲染君账,輸出一個新的紋理 。
但實際上沈善,由于渲染過程需要先綁定幀緩存乡数,所以紋理被包裝在 GPUImageFramebuffer
中。
因此闻牡,在不同的 GPUImageFilter
之間傳遞的對象其實是 GPUImageFramebuffer
净赴。一般的流程是,從 firstInputFramebuffer
中讀取紋理罩润,將結(jié)果渲染到 outputFramebuffer
的紋理中玖翅,然后將 outputFramebuffer
傳遞給下一個節(jié)點。
而 outputFramebuffer
是需要重新創(chuàng)建的割以,如果不做額外的緩存處理金度,在整個濾鏡鏈的渲染中,將需要創(chuàng)建大量的 GPUImageFramebuffer
對象严沥。
因此猜极, GPUImage 框架提供了 GPUImageFramebufferCache
來管理 GPUImageFramebuffer
的重用。當(dāng)需要創(chuàng)建 outputFramebuffer
的時候祝峻,會先從 GPUImageFramebufferCache
中去獲取緩存的對象魔吐,獲取不到才會重新創(chuàng)建扎筒。
由于紋理被包裝在 GPUImageFramebuffer
中莱找,所以當(dāng) GPUImageFramebuffer
被重用時,原先保存的紋理就會被覆蓋嗜桌。
GPUImageFramebuffer
提供了 lock
和 unlock
的操作奥溺。 lock
會使引用計數(shù)加 1,unlock
會使引用計數(shù)減 1骨宠,當(dāng)引用計數(shù)為 0 的時候浮定,GPUImageFramebuffer
會被加入到 cache 中,等待被重用层亿。
所以桦卒,我們要捕獲紋理,做法就是:在拍攝過程中匿又,不讓 GPUImageFramebuffer
進入 cache方灾。
注: 這里的引用計數(shù)不是 OC 層面的引用計數(shù),而是
GPUImageFramebuffer
內(nèi)部的一個屬性,屬于業(yè)務(wù)邏輯層的東西裕偿。
代碼實現(xiàn)
1洞慎、捕獲和釋放
GPUImageFramebuffer
的捕獲和釋放都很簡單,通過 lock
和 unlock
來實現(xiàn)嘿棘,
[firstInputFramebuffer lock];
self.firstFramebuffer = firstInputFramebuffer;
[self.firstFramebuffer unlock];
self.firstFramebuffer = nil;
2劲腿、多紋理的渲染
在捕獲了額外的紋理后,需要重寫 -renderToTextureWithVertices:textureCoordinates:
方法鸟妙,在里面?zhèn)鬟f多個紋理到著色器中焦人。
// 第一個紋理
if (self.firstFramebuffer) {
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, [self.firstFramebuffer texture]);
glUniform1i(firstTextureUniform, 3);
}
// 第二個紋理
if (self.secondFramebuffer) {
glActiveTexture(GL_TEXTURE4);
glBindTexture(GL_TEXTURE_2D, [self.secondFramebuffer texture]);
glUniform1i(secondTextureUniform, 4);
}
// 第三個紋理
if (self.thirdFramebuffer) {
glActiveTexture(GL_TEXTURE5);
glBindTexture(GL_TEXTURE_2D, [self.thirdFramebuffer texture]);
glUniform1i(thirdTextureUniform, 5);
}
// 第四個紋理
if (self.fourthFramebuffer) {
glActiveTexture(GL_TEXTURE6);
glBindTexture(GL_TEXTURE_2D, [self.fourthFramebuffer texture]);
glUniform1i(fourthTextureUniform, 6);
}
// 傳遞紋理的數(shù)量
glUniform1i(textureCountUniform, (int)self.capturedCount);
同時在著色器中接收并處理:
precision highp float;
uniform sampler2D inputImageTexture;
uniform sampler2D inputImageTexture1;
uniform sampler2D inputImageTexture2;
uniform sampler2D inputImageTexture3;
uniform sampler2D inputImageTexture4;
uniform int textureCount;
varying vec2 textureCoordinate;
void main (void) {
vec2 position = mod(textureCoordinate * 2.0, 1.0);
if (textureCoordinate.x <= 0.5 && textureCoordinate.y <= 0.5) { // 左上
gl_FragColor = texture2D(textureCount >= 1 ? inputImageTexture1 : inputImageTexture,
position);
} else if (textureCoordinate.x > 0.5 && textureCoordinate.y <= 0.5) { // 右上
gl_FragColor = texture2D(textureCount >= 2 ? inputImageTexture2 : inputImageTexture,
position);
} else if (textureCoordinate.x <= 0.5 && textureCoordinate.y > 0.5) { // 左下
gl_FragColor = texture2D(textureCount >= 3 ? inputImageTexture3 : inputImageTexture,
position);
} else { // 右下
gl_FragColor = texture2D(textureCount >= 4 ? inputImageTexture4 : inputImageTexture,
position);
}
}
由于這里每個屏接收的紋理都不一樣,就不可避免地要添加區(qū)域判斷邏輯了重父。
效果展示
最后垃瞧,看一下延時動態(tài)分屏的效果:
源碼
請到 GitHub 上查看完整代碼。
獲取更佳的閱讀體驗坪郭,請訪問原文地址 【Lyman's Blog】如何優(yōu)雅地實現(xiàn)一個分屏濾鏡