本文经过编写一个通用的片断着色器,实现了抖音中的各类分屏滤镜。另外,还讲解了延时动态分屏滤镜的实现。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】如何优雅地实现一个分屏滤镜