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

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

1、靜態分屏

靜態分屏指的是,每個屏的圖像都徹底同樣。git

分屏濾鏡實現起來比較容易,無非是在片斷着色器中,修改紋理座標和紋理的對應關係。分屏以後,每一個屏內紋理的對應關係都不太同樣。所以在實現的時候,容易寫的很複雜,會有大量的區域判斷邏輯。github

這樣實現出來的着色器拓展性比較差。假若有多種分屏濾鏡,就要實現多個着色器,並且屏數越多,區域判斷邏輯就越複雜。緩存

因此,咱們會採起一種更優雅的方式,爲全部的分屏濾鏡實現一個通用的着色器,而後將屏數看成參數,由着色器外部控制。框架

預備知識

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

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

下面以乘法爲例,其餘相似。spa

vec2 a, b, c;
c = a * b;
複製代碼

等價於code

c.x = a.x * b.x;
c.y = a.y * b.y;
複製代碼

二、向量與標量的加減乘除orm

下面以加法爲例,其餘相似。

vec2 a, b;
float c;
b = a + c;
複製代碼

等價於

b.x = a.x + c;
b.y = a.y + c;
複製代碼

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

vec2 a, b, c;
c = mod(a, b);
複製代碼

等價於

c.x = mod(a.x, b.x);
c.y = mod(a.y, b.y);
複製代碼

四、向量與標量的 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 這種非整數的分屏操做。

2、動態分屏

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

因爲每一個屏的圖像都不同,所以在渲染過程當中,須要捕獲多個不一樣的紋理。好比咱們想要實現一個四屏的濾鏡,就須要捕獲 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 內部的一個屬性,屬於業務邏輯層的東西。

代碼實現

一、捕獲和釋放

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

[firstInputFramebuffer lock];
self.firstFramebuffer = firstInputFramebuffer;
複製代碼
[self.firstFramebuffer unlock];
self.firstFramebuffer = nil;
複製代碼

二、多紋理的渲染

在捕獲了額外的紋理後,須要重寫 -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】如何優雅地實現一個分屏濾鏡

相關文章
相關標籤/搜索