基于openGL中shader聊滤镜特效的原理和实际应用

4,822 阅读9分钟

目标

最终能够自己通过OpenGL或者借助一些三方库GPUImage写一些简单的滤镜、特效shader,明白原理和整个流程

OpenGL简介

OpenGL是各个平台的统称,移动端的是OpenGL ES,web端的是WebGL

(备注:下文将OpenGL ES将简称OpenGL)

为什么用OpenGL

  • 定义了一套平台无关的图形操作API,提供了访问GPU的能力。
  • CPUGPU的数据交换定义了缓存(buffer),因为从一个内存区域复制到另一个内存区域的速度是相对较慢的,并且在内存复制的过程中,CPU 和 GPU 都不能处理这区域内存

下图是一个移动设备图像渲染框架草图:

在进入主题之前,我们再来了解下图片渲染到屏幕的过程,这将有助于了解OpenGL在滤镜特效中的作用

  • CPU:绘制纹理图片然后交给GPU渲染,也就是位图
  • GPU: 等待垂直同步信号V-Sync,GPU拿到位图会做一些图层的渲染、纹理合成等工作。再把结果放到帧缓冲区中(Frame Buffer)
  • 视频控制器: 根据V-Sync信号,在指定时间之前,提取帧缓冲区的屏幕显示内容,最终显示到显示器。

正是因为有了OpenGL的存在,我们才可以对图像、视频做很多有意思的处理,而这一部分离不开OpenGL中的着色器——Shader,下面就来看看

什么是Shader

OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。

3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线管理的,图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入

OpenGL图形渲染管线的每一个阶段运行着各自的小程序,这些小程序叫做着色器(Shader)。一般以字符串的方式在代码中使用,目前OpenGL中只有vertex shader(顶点着色器)fragment shader(片段着色器)是可编程的。GLSL是是OpenGL用来编写着色器(shader)的高级语言,它不是运行在CPU而是GPU

下面来看看OpenGL世界中的"Hello world"(三角形)怎么实现.

这里主要说下顶点着色器和片段着色器

  • 顶点着色器

如果需要对图像进行缩放变化,比如放大,缩小,移动效果,则需要对顶点着色器重新编程,默认顶点着色器代码如下


attribute vec4 Position; // 顶点坐标
attribute vec2 TextureCoords; // 纹理坐标
varying vec2 TextureCoordsVarying;//片段着色器的输入(纹理坐标)

void main (void) {
    gl_Position = Position;
    TextureCoordsVarying = TextureCoords;
}

  • 片段着色器

如果需要对原始图像最终输出的颜色进行调整,则需要对片段着色器重新编程

precision mediump float;

uniform sampler2D Texture;//纹理采样器
varying vec2 TextureCoordsVarying; //顶点着色器传过来的纹理坐标

void main (void) {
    //图元的每个顶点各自对应纹理坐标,用来标明该从纹理图像的哪个部分采样
    vec4 mask = texture2D(Texture, TextureCoordsVarying);//获得纹理坐标相应位置的颜色
    gl_FragColor = vec4(mask.rgb, 1.0);
}


用顶点着色器和片段着色器可以写出各种各种样的opengl程序

  • OpenGL中坐标系(可选)

了解OpenGL中的坐标系,有助于更好的了解openGL渲染管线和作业的流程:

友情链接:OpenGL ES顶点坐标 纹理坐标

有了上面shader的铺垫,那可以思考下滤镜和特效怎么实现的了?

滤镜的原理和实现

颜色滤镜一般不需要用到顶点着色器

什么是滤镜

在 app 内利用各种图形算法可以对图片进行一些变换,这样的效果也称为“滤镜”,滤镜效果大致可以分为以下几类:

  1. 独立像素点变换:包括亮度、对比、饱和度、色调、灰色化、分离RGB通道等
  2. 像素卷积变换:包括边缘检测、浮雕化、模糊、锐化(比如美颜)
  3. 仿射矩阵变换: 包括缩放、旋转、倾斜、扭曲、液化等()
  4. 多图像合成 其中最简单的就是进行独立像素点变换,

最简单的滤镜就是第1点:独立像素点变换,也可以叫做颜色滤镜,最主要的技术就是ColorLUT

ColorLUT

将左边狗子🐶图变为右边效果图,大概的做法就是把左图的每个像素点根据相应的计算公式计算得到一个新的像素点即可,那么如何定义这个计算公式呢?颜色查找表(简称ColorLUT)就是干这事儿的,它是实现滤镜最主要的技术

下图是一个标准的颜色查找表

实现滤镜的原理:

在一张表中为每种颜色(总共255 * 255 * 255)记录一个对应的映射目标颜色。当用【颜色查找表】对一张照片做颜色映射时,只需要遍历照片的每个像素点,然后在表中找到该像素颜色对应的目标颜色,最后将该像素设置为目标颜色即可

比如原始颜色是红色(r:255,g:0,b:0),进行转换后变为绿色(r:0,g:255,b:0),以后所有是红色的地方都会被自动转换为绿色。而颜色查找表就是将所有的颜色进行一次(矩阵)转换,而很多的滤镜功能就是提供了这么一个转换的矩阵,在原始色彩的基础上进行颜色的转换

如何生成颜色查找表

  1. 用PS或其它滤镜软件打开原图。
  2. 对原图做曲线,色彩平衡、色调等调整。
  3. 调整适合后,对Lookup Table做相同的操作。
  4. 调整好Lookup Table后,导出新的Lookup Table,然后在程序中使用。

如何查找颜色表

每个像素的色彩都是由RGB三种颜色组成,如果以三维阵列来存储,我们很不好处理,所以要把Z轴单独拎出来,拼接成为一个8x8的二纬图片,相近的颜色采用一条记录存储

左上角第一个像素代表位于(0,0,0)的点,第二个像素代表位于(85,0,0)的点

具体的实现

  • demo1:

来看一个简单的列子,灰度滤镜的实现(不需要使用ColorLUT):

图片的显示由三个颜色通道(rgb)决定,而灰度滤镜所有通道的值相同

precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
//灰度计算比率 (借用GPUImage的值)
const highp vec3 ratio = vec3(0.2125, 0.7154, 0.0721);
void main (void) {
    vec4 mask = texture2D(Texture, TextureCoordsVarying);
    // Gray值
    float luminance = dot(mask.rgb, ratio);
    gl_FragColor = vec4(vec3(luminance), 1.0);
}

  • demo2:

如果是设计师给的颜色查找表.png怎么用, 这里通过GPUImage中的颜色查找滤镜GPUImageLookupFilter(就是用来处理颜色查找表和原图的)来看下这部分着色器处理的代码

NSString *const kGPUImageLookupFragmentShaderString = SHADER_STRING
(
 varying highp vec2 textureCoordinate;

 uniform sampler2D inputImageTexture;
 uniform sampler2D inputImageTexture2; // lookup texture
 
 uniform lowp float intensity;

 void main()
 {
     highp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
     //蓝色通道,textureColor.b的范围为(0,1),blueColor范围为(0,63)
     highp float blueColor = textureColor.b * 63.0;

     //根据B通道获取小正方形格子(64x64格子)
     highp vec2 quad1;  
     quad1.y = floor(floor(blueColor) / 8.0);
     quad1.x = floor(blueColor) - (quad1.y * 8.0);
     
     //quad2为大于且最靠近要查找颜色所在位置的小正方形
     highp vec2 quad2;
     quad2.y = floor(ceil(blueColor) / 8.0);
     quad2.x = ceil(blueColor) - (quad2.y * 8.0);
     
     highp vec2 texPos1;

     //因为一行有8个小正方形,所以小正方形的边长,转换为纹理坐标时,就是0.125。quad1的位置就是quad1.x * 0.125和quad1.y * 0.125。

     //根据小正方形格子和RG通道,获取纹理坐标,每个大格子的大小:1/8=0.125,每个小格子的大小:1/512
     texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);
     texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g);
     
     highp vec2 texPos2;
     //quad2和quad1差不多
     texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);
     texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g);
     
     lowp vec4 newColor1 = texture2D(inputImageTexture2, texPos1);
     lowp vec4 newColor2 = texture2D(inputImageTexture2, texPos2); 
     
      //真正的颜色在newColor1和newColor2之间。fract是取分数部分。
     lowp vec4 newColor = mix(newColor1, newColor2, fract(blueColor));
     gl_FragColor = mix(textureColor, vec4(newColor.rgb, textureColor.w), intensity);
 }
);

伪代码

precision mediump float;
 varying highp vec2 textureCoordinate;
 uniform sampler2D inputImageTexture; //原图纹理
 uniform sampler2D inputImageTexture2; // lookup texture
 
void main (void) {
    //图元的每个顶点各自对应纹理坐标,用来标明该从纹理图像的哪个部分采样
    lowp vec4 newColor1 = texture2D(inputImageTexture2, texPos1);
     lowp vec4 newColor2 = texture2D(inputImageTexture2, texPos2); 
      //真正的颜色在两个颜色之间(按照一定权重进行混合)
     lowp vec4 newColor = mix(orginColor(原图纹理颜色), LUTColor(颜色查找表), 权重);
     gl_FragColor = mix(textureColor, vec4(newColor.rgb, textureColor.w), intensity);
}

特效的原理和实现

特效的实现其实也是用顶点着色器和片段着色器,只不过从复杂度上来说比滤镜相对复杂些,可以用到更多惊艳的效果,颜色滤镜之外的都可以算作特效

比如来看一个比较简单的【分屏

原理其实就是去改变纹理坐标在y轴的偏移位置

// 精度
precision highp float;
// 通过uniform传递过来的纹理
uniform sampler2D Texture;
// 纹理坐标
varying highp vec2 varyTextureCoord;

void main() {
    
    vec2 uv = varyTextureCoord.xy;
    float y;
    // 0.0~0.5 范围内显示0.25~0.75范围的像素
    if (uv.y >= 0.0 && uv.y <= 0.5) {
        y = uv.y + 0.25;
    }else {
        // 0.5~1.0范围内显示 0.25~0.75范围的像素
        y = uv.y - 0.25;
    }
    
    // 获取纹理像素,用于显示
    gl_FragColor = texture2D(Texture, vec2(uv.x, y));
}

在iOS中的应用

这里主要结合GPUImage来简单讲滤镜、特效在音视频应用中的实际使用,由于GPUImage不是重点,所以有个简单了解就可以了

整个库的整体目录分层结构如下:

以抖音为列,我们来看看滤镜、特效在音视频应用中的使用方案

  • 相机拍摄界面

如果添加了滤镜,本质其实就是对采集到的每一帧CMSampleBufferRef运用ColorLUT

  • 特效处理界面

这里主要讲讲特效部分的预览和合成

视屏最终合成

关于特效处理部分伪代码如下

- (void)setUpExportEnvironment {
    self.exportRenderer = [[GPUImageMovie alloc] initWithURL:_exportModel.localVideoURL];
    //链式叠加
    self.moveWriter = [[GPUImageMovieWriter alloc] initWithMovieURL: '原视频文件路径'] 
    
    self.passFilter = [[GPUImageFilter alloc] init];
    [self.exportRenderer enableSynchronizedEncodingUsingMovieWriter:self.moveWriter];
    [self.exportRenderer addTarget:self.passFilter];
   //当前处理时间
    [self.passFilter setFrameProcessingCompletionBlock:^(GPUImageOutput *output, CMTime time) {
        [weakSelf mixEffectAt:time];
    }];
    
- (void)mixEffectAt:(CMTime)time {
       [self.moveWriter setPaused:YES];
        curActiveEffectModel.isUsing = YES;
        //选择对应的特效
        [_effect switch_effect:curActiveEffectModel.pictureEffectType];
        [self.moveWriter setPaused:NO];
    }
}

其他

  • 贴纸
  • 美颜

学习资料: