如何優(yōu)雅地實現(xiàn)一個分屏濾鏡

本文通過編寫一個通用的片段著色器,實現(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),乘上 horizontalCountverticalCount 的較小者监透,然后對新的尺寸進行求模運算桶错。這樣,當(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 提供了 lockunlock 的操作奥溺。 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 的捕獲和釋放都很簡單,通過 lockunlock 來實現(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)一個分屏濾鏡

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末个从,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子歪沃,更是在濱河造成了極大的恐慌嗦锐,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沪曙,死亡現(xiàn)場離奇詭異奕污,居然都是意外死亡,警方通過查閱死者的電腦和手機液走,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門碳默,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人缘眶,你說我怎么就攤上這事嘱根。” “怎么了巷懈?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵该抒,是天一觀的道長。 經(jīng)常有香客問我顶燕,道長凑保,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任涌攻,我火速辦了婚禮欧引,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘恳谎。我一直安慰自己芝此,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著癌蓖,像睡著了一般瞬哼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上租副,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天坐慰,我揣著相機與錄音,去河邊找鬼用僧。 笑死结胀,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的责循。 我是一名探鬼主播糟港,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼院仿!你這毒婦竟也來了秸抚?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤歹垫,失蹤者是張志新(化名)和其女友劉穎剥汤,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體排惨,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡吭敢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了暮芭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鹿驼。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖辕宏,靈堂內(nèi)的尸體忽然破棺而出畜晰,到底是詐尸還是另有隱情,我是刑警寧澤匾效,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布舷蟀,位于F島的核電站,受9級特大地震影響面哼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜扫步,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一魔策、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧河胎,春花似錦闯袒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽其徙。三九已至,卻和暖如春喷户,著一層夾襖步出監(jiān)牢的瞬間唾那,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工褪尝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留闹获,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓河哑,卻偏偏與公主長得像避诽,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子璃谨,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353