本文經過編寫一個通用的片斷着色器,實現了抖音中的各類分屏濾鏡。另外,還講解了延時動態分屏濾鏡的實現。ios
靜態分屏指的是,每個屏的圖像都徹底同樣。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) 這一行是這個着色器的精華所在,可能不太好理解。咱們將原始的紋理座標,乘上 horizontalCount
和 verticalCount
的較小者,而後對新的尺寸進行求模運算。這樣,當原始紋理座標在 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
提供了 lock
和 unlock
的操做。 lock
會使引用計數加 1,unlock
會使引用計數減 1,當引用計數爲 0 的時候,GPUImageFramebuffer
會被加入到 cache 中,等待被重用。
因此,咱們要捕獲紋理,作法就是:在拍攝過程當中,不讓 GPUImageFramebuffer
進入 cache。
注: 這裏的引用計數不是 OC 層面的引用計數,而是
GPUImageFramebuffer
內部的一個屬性,屬於業務邏輯層的東西。
一、捕獲和釋放
GPUImageFramebuffer
的捕獲和釋放都很簡單,經過 lock
和 unlock
來實現,
[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】如何優雅地實現一個分屏濾鏡