最终能够自己通过OpenGL或者借助一些三方库GPUImage写一些简单的滤镜、特效shader,明白原理和整个流程
OpenGL
是各个平台的统称,移动端的是OpenGL ES
,web端的是WebGL
(备注:下文将OpenGL ES将简称OpenGL
)
为什么用OpenGL
CPU
和GPU
的数据交换定义了缓存(buffer),因为从一个内存区域复制到另一个内存区域的速度是相对较慢的,并且在内存复制的过程中,CPU 和 GPU 都不能处理这区域内存下图是一个移动设备图像渲染框架草图:
在进入主题之前,我们再来了解下图片渲染到屏幕的过程,这将有助于了解OpenGL在滤镜特效中的作用
V-Sync
,GPU拿到位图会做一些图层的渲染、纹理合成等工作。再把结果放到帧缓冲区
中(Frame Buffer)正是因为有了OpenGL的存在,我们才可以对图像、视频做很多有意思的处理,而这一部分离不开OpenGL中的着色器——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 ES顶点坐标 纹理坐标
有了上面shader的铺垫,那可以思考下滤镜和特效怎么实现的了?
颜色滤镜一般不需要用到顶点着色器
在 app 内利用各种图形算法可以对图片进行一些变换,这样的效果也称为“滤镜”,滤镜效果大致可以分为以下几类:
最简单的滤镜就是第1点:独立像素点变换,也可以叫做颜色滤镜,最主要的技术就是ColorLUT
下图是一个标准的颜色查找表
在一张表中为每种颜色(总共255 * 255 * 255)记录一个对应的映射目标颜色。当用【颜色查找表】对一张照片做颜色映射时,只需要遍历照片的每个像素点,然后在表中找到该像素颜色对应的目标颜色,最后将该像素设置为目标颜色即可
比如原始颜色是红色(r:255,g:0,b:0),进行转换后变为绿色(r:0,g:255,b:0),以后所有是红色的地方都会被自动转换为绿色。而颜色查找表就是将所有的颜色进行一次(矩阵)转换,而很多的滤镜功能就是提供了这么一个转换的矩阵,在原始色彩的基础上进行颜色的转换
左上角第一个像素代表位于(0,0,0)的点,第二个像素代表位于(85,0,0)的点
来看一个简单的列子,灰度滤镜的实现(不需要使用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); } 复制代码
如果是设计师给的颜色查找表.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)); } 复制代码
这里主要结合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]; } } 复制代码
学习资料: