如何优雅地实现一个分屏滤镜

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

相关文章
相关标签/搜索