如何優雅地實現一個分屏濾鏡

本文通過編寫一個通用的片段著色器,實現了抖音中的各種分屏濾鏡。另外,還講解了延時動態分屏濾鏡的實現。

一、靜態分屏

靜態分屏指的是,每一個屏的圖像都完全一樣。

分屏濾鏡實現起來比較容易,無非是在片段著色器中,修改紋理坐標和紋理的對應關系。分屏之后,每個屏內紋理的對應關系都不太一樣。因此在實現的時候,容易寫的很復雜,會有大量的區域判斷邏輯。

這樣實現出來的著色器拓展性比較差。假如有多種分屏濾鏡,就要實現多個著色器,而且屏數越多,區域判斷邏輯就越復雜。

所以,我們會采取一種更優雅的方式,為所有的分屏濾鏡實現一個通用的著色器,然后將屏數當作參數,由著色器外部控制。

預備知識

首先,我們來了解等一下會使用到的 GLSL 運算和函數。vec2 是二維向量類型,它支持下面的各種運算。

1、向量與向量的加減乘除(兩個向量需要保證維數相同)

下面以乘法為例,其他類似。

vec2 a, b, c;
c = a * b;

等價于

c.x = a.x * b.x;
c.y = a.y * b.y;

2、向量與標量的加減乘除

下面以加法為例,其他類似。

vec2 a, b;
float c;
b = a + c;

等價于

b.x = a.x + c;
b.y = a.y + c;

3、向量與向量的 mod 運算(兩個向量需要保證維數相同)

vec2 a, b, c;
c = mod(a, b);

等價于

c.x = mod(a.x, b.x);
c.y = mod(a.y, b.y);

4、向量與標量的 mod 運算

vec2 a, b;
float c;
b = mod(a, c);

等價于

b.x = mod(a.x, c);
b.y = mod(a.y, c);

著色器實現

有了上面的 GLSL 運算知識,來看下我們最終實現的片段著色器。

 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 變量的形式,從著色器外部傳入橫向分屏數 horizontal縱向分屏數 vertical

(2) 開始運算前,做了最小分屏數的限制,避免小于 1.0 的分屏數出現。

(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) 的圖像,對應的 offset(1.0 / 6.0, 0.0)

(5) 這一行是這個著色器的精華所在,可能不太好理解。我們將原始的紋理坐標,乘上 horizontalCountverticalCount 的較小者,然后對新的尺寸進行求模運算。這樣,當原始紋理坐標在 0 ~ 1 的范圍內增長時,可以讓新的紋理坐標newSize 的范圍內循環多次。另外,計算的結果加上 offset,可以讓新的紋理坐標偏移到居中的位置。

下面簡單演示一下每一步計算的效果,幫助理解:

(6) 通過新的計算出來的紋理坐標,從紋理中讀出相應的顏色值輸出。

效果展示

現在,我們得到了一個通用的分屏著色器,像三屏、六屏、九屏這些效果,只需要修改兩個參數就可以實現。另外,上面的實現邏輯,甚至可以支持 1.5 : 2.5 這種非整數的分屏操作。

二、動態分屏

動態分屏指的是,每個屏的圖像都不一樣,每間隔一段時間,會主動捕獲一個新的圖像。

由于每個屏的圖像都不一樣,因此在渲染過程中,需要捕獲多個不同的紋理。比如我們想要實現一個四屏的濾鏡,就需要捕獲 4 個不同的紋理。

預備知識

我們知道,在 GPUImage 框架中,濾鏡效果的渲染發生在 GPUImageFilter 中。

從渲染層面來說,GPUImageFilter 接收一個紋理的輸入,然后經過自身效果的渲染,輸出一個新的紋理 。

但實際上,由于渲染過程需要先綁定幀緩存,所以紋理被包裝在 GPUImageFramebuffer 中。

因此,在不同的 GPUImageFilter 之間傳遞的對象其實是 GPUImageFramebuffer。一般的流程是,從 firstInputFramebuffer 中讀取紋理,將結果渲染到 outputFramebuffer 的紋理中,然后將 outputFramebuffer 傳遞給下一個節點。

outputFramebuffer 是需要重新創建的,如果不做額外的緩存處理,在整個濾鏡鏈的渲染中,將需要創建大量的 GPUImageFramebuffer 對象。

因此, GPUImage 框架提供了 GPUImageFramebufferCache 來管理 GPUImageFramebuffer 的重用。當需要創建 outputFramebuffer 的時候,會先從 GPUImageFramebufferCache 中去獲取緩存的對象,獲取不到才會重新創建。

由于紋理被包裝在 GPUImageFramebuffer 中,所以當 GPUImageFramebuffer 被重用時,原先保存的紋理就會被覆蓋。

GPUImageFramebuffer 提供了 lockunlock 的操作。 lock 會使引用計數加 1,unlock 會使引用計數減 1,當引用計數為 0 的時候,GPUImageFramebuffer 會被加入到 cache 中,等待被重用。

所以,我們要捕獲紋理,做法就是:在拍攝過程中,不讓 GPUImageFramebuffer 進入 cache

注: 這里的引用計數不是 OC 層面的引用計數,而是 GPUImageFramebuffer 內部的一個屬性,屬于業務邏輯層的東西。

代碼實現

1、捕獲和釋放

GPUImageFramebuffer 的捕獲和釋放都很簡單,通過 lockunlock 來實現,

[firstInputFramebuffer lock];
self.firstFramebuffer = firstInputFramebuffer;
[self.firstFramebuffer unlock];
self.firstFramebuffer = nil;

2、多紋理的渲染

在捕獲了額外的紋理后,需要重寫 -renderToTextureWithVertices:textureCoordinates: 方法,在里面傳遞多個紋理到著色器中。

// 第一個紋理
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);
}

// 傳遞紋理的數量
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);
    }
}

由于這里每個屏接收的紋理都不一樣,就不可避免地要添加區域判斷邏輯了。

效果展示

最后,看一下延時動態分屏的效果:

源碼

請到 GitHub 上查看完整代碼。

獲取更佳的閱讀體驗,請訪問原文地址 【Lyman's Blog】如何優雅地實現一個分屏濾鏡

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370