一. GPUImage
框架的介绍及基本使用
1.GPUImage
的介绍
GPUImage是基于OpenGL ES
的一套图像、视频处理开源框架,它里面提供了大量的滤镜,使用者可以通过这些滤镜的组合实现很好的效果,同时也很方便在原有基础上实现自定义的滤镜。对于大规模并行操作(如处理图像或实时视频帧),GPU具有比CPU更显着的性能优势。而 GPUImage 所有滤镜是基于OpenGL Shader
实现的,所以滤镜效果、图像处理是在GPU上执行的,处理效率比较高,在iPhone4及其以上手机,可以做到实时流畅的效果。而且它隐藏了Objective-C
与OpenGL ES
API交互的复杂性。目前市面上的图像视频处理App,95%以上在使用GPUImage,所以学习它的使用及原理还是很有必要的。GPUImage 同时支持iOS跟Andorid平台,地址:iOS版本 Android版本 也支持 Swift版本,本文主要介绍它的 OC 版本,核心类的功能以及原理跟 Andorid 版本是相通的。
iOS开发者使用方式:直接 CocaPods 集成:
1 | pod 'GPUImage' |
首先来看下它的基本结构图:
从这张图中我们可以看到GPUImage的几个核心类:GPUImageOutput
GPUImageFilter
GPUImageInput 协议
GPUImageFrameBuffer
,接下来我们重点讲解这几个类。
2.核心功能类说明
GPUImageOutput
GPUImageOutput
是所有滤镜输入源的基类,也就是滤镜链的起点,先看下他的继承关系:
分别解释一下这几种类型:
GPUImagePicture
通过图片来初始化,本质上是先将图片转化为 CGImageRef,然后将 CGImageRef 转化为纹理。GPUImageVideoCamera
:通过相机来初始化,本质是封装了AVCaptureVideoDataOutput来获取持续的视频流数据输出,在代理方法captureOutput:didOutputSampleBuffer:fromConnection:
拿到 CMSampleBufferRef,将其转化为纹理的过程。GPUImageStillCamera
是 GPUImageVideoCamera 的子类,可以用它来实现拍照功能。GPUImageUIElement
:可以通过 UIView 或者 CALayer 来初始化。这个类可以用来实现在视频上添加文字水印的功能。GPUImageTextureInput
:通过已经存在的纹理来初始化.GPUImageRawDataInput
:通过二进制数据初始化,然后将二进制数据转化为纹理.GPUImageMovie
:通过本地的视频来初始化。首先通过 AVAssetReader 来逐帧读取视频,然后将帧数据转化为纹理。GPUImageFilter
:比较特殊,它既继承自 GPUImageOutput,又遵守协议 GPUImageInput 协议,所以它既可以作为滤镜链的源头,又可以把渲染的纹理输出给遵守 GPUImageInput 协议的类。是滤镜的核心,后面会单独介绍。
核心功能与方法:
想象一下,一个滤镜链的源头能做什么呢:
- 需要产出一个渲染对象,这个需要渲染的对象就是
GPUImageFrameBuffer
.几个关于frameBuffer的方法:
1 | - (GPUImageFramebuffer *)framebufferForOutput; |
这个方法可以获得当前正在渲染的frameBuffer
1 | - (void)removeOutputFramebuffer; |
这个方法用来移除当前渲染的frameBuffer
1 | - (void)setInputFramebufferForTarget:(id<GPUImageInput>)target atIndex:(NSInteger)inputTextureIndex; |
这个方法的调用发生在当前output渲染完毕后,需要通知下一个receiver可以开始渲染的时候,把当前Output的FrameBuffer传递给下一个Input,让它可以使用这个FrameBuffer的结果进行渲染。
- Target的添加以及管理,用来生成整个FilterChain.
GPUImageOutput 既然作为一个滤镜的源头,相对应的就得有接受者接受它输出的 FrameBuffer ,这些接受者就是Target,而且有可能有多个接受者。管理这些target的主要方法:这两个addTarget方法的作用都是将下一个实现了GPUImageInput协议的对象添加到FilterChain当中来.一旦添加到滤镜链后,在当前Output渲染完成后就会收到通知,从而进行下一步的处理。1
2- (void)addTarget:(id<GPUImageInput>)newTarget;
- (void)addTarget:(id<GPUImageInput>)newTarget atTextureLocation:(NSInteger)textureLocation;
1 | - (NSArray*)targets; |
每个Output都可以添加多个target,这个方法可以获取到当前Output所有的target.
1 | - (void)removeTarget:(id<GPUImageInput>)targetToRemove; |
这两个方法的作用是将某一个或者所有的target都移出FilterChain。当一个target被移出FilterChain之后,它将不会再收到任何当前Output渲染完成的通知。
- 获取当前的GPUImageOutput对FrameBuffer的处理结果其中最核心的方法是
1
2
3
4
5
6- (CGImageRef)newCGImageFromCurrentlyProcessedOutput;
- (CGImageRef)newCGImageByFilteringCGImage:(CGImageRef)imageToFilter;
- (UIImage *)imageFromCurrentFramebuffer;
- (UIImage *)imageFromCurrentFramebufferWithOrientation:(UIImageOrientation)imageOrientation;
- (UIImage *)imageByFilteringImage:(UIImage *)imageToFilter;
- (CGImageRef)newCGImageByFilteringImage:(UIImage *)imageToFilter;newCGImageFromCurrentlyProcessedOutput
,基本上所有的方法最终都调用了这个方法。但是GPUImageOutput并没有为这个方法提供默认的实现,而是提供了一个方法定义。具体的实现在它的两个重要的子类 GPUImageFilter 和 GPUImageFilterGroup 中。而实际上最终调用的方法都在 GPUImageFilter 中实现了.
GPUImageInput
协议
GPUImageInput
是一个协议,它定义了一个能够接收 FrameBuffer 的 receiver 所必须实现的基本功能。实现这个协议的类可以作为渲染的终点使用。
实现了 GPUImageInput 接口的类:
对这几个类进行解释:
GPUImageMovieWriter
:封装了 AVAssetWriter,可以逐帧从帧缓存的渲染结果中读取数据,最后通过 AVAssetWriter 将视频文件保存到指定的路径。GPUImageView
:继承自 UIView,通过输入的纹理,执行一遍渲染流程。我们一般使用它来呈现渲染结果。GPUImageTextureOutput
:它可以获取到输入的Framebuffer中的纹理对象.GPUImageRawDataOutput
:通过 rawBytesForImage 属性,可以获取到当前输入纹理的二进制数据。
核心功能与方法:
可以作为滤镜链的终点。基本功能主要包括:
- 接收 GPUmageOutput 的输出信息;
- 接收上一个GPUImageOutput渲染完成的通知,并且完成自己的处理;
- 接收GPUmageOutput的输出信息对应方法:根据这些方法可以看到,GPUImageInput 可以接收的信息包括上一个Output输出的FrameBuffer,FrameBuffer的size以及rotation。这些 textureIndex 都是为了提供个需要多个input的Filter准备的。
1
2
3
4- (void)setInputFramebuffer:(GPUImageFramebuffer *)newInputFramebuffer atIndex:(NSInteger)textureIndex;
- (NSInteger)nextAvailableTextureIndex;
- (void)setInputSize:(CGSize)newSize atIndex:(NSInteger)textureIndex;
- (void)setInputRotation:(GPUImageRotationMode)newInputRotation atIndex:(NSInteger)textureIndex; - 接收GPUImageOutput渲染完成的通知对应方法:上一个 GPUImageOutput 渲染完成后会通知它所有的 Target,可以参考下它在
1
- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex;
GPUImageFilter
里面的实现。
GPUImageFrameBuffer
GPUImageFrameBuffer 提供了在 GPUImageOutput 和 GPUImageInput 进行数据传递的媒介。在整个渲染流程中,GPUImageFrameBuffer作为一个纽带,将各个不同的元素串联起来;每个GPUImageFrameBuffer 都有一个自己的OpenGL Texture,每个 GPUImageOutput 都会输出一个 GPUImageFrameBuffer 对象,而每个 GPUImageInput都实现了一个setInputFramebuffer:atIndex:
方法,来接收上一个Output处理完的纹理.
- GPUImageFrameBuffer 的获取逻辑,是由
GPUImageFrameBufferCache
进行管理的,需要时从BufferCache中获取,使用完成后,被BufferCache回收。FrameBuffer 的创建跟存储是需要消耗资源的,所以 GPUImage 为了尽量减少资源的消耗,会将使用完成的 FrameBuffer 存储在缓存中,每次通过 输入的纹理size 跟 TextureOptions 作为 key 从hash map 中获取。
GPUImageFilter
GPUImageFilter 是整个GPUImage框架的核心,GPUImage所内置的100多种滤镜效果都继承于此类。例如我们经常用到的一些滤镜:
GPUImageBrightnessFilter
:亮度调整滤镜GPUImageExposureFilter
:曝光调整滤镜GPUImageContrastFilter
:对比度调整滤镜GPUImageSaturationFilter
:饱和度调整滤镜GPUImageWhiteBalanceFilter
:白平衡调整滤镜GPUImageColorInvertFilter
:反转图像的颜色GPUImageCropFilter
:将图像裁剪到特定区域GPUImageGaussianBlurFilter
:可变半径高斯模糊GPUImageSketchFilter
:素描滤镜GPUImageToonFilter
:卡通效果GPUImageDissolveBlendFilter
:两个图像的混合GPUImageFilterPipeline
: 链式组合滤镜
…
核心功能与方法:
GPUImageFilter是GPUImageOutput的子类,但是同时它也实现了GPUImageInput协议。因此,它包含了一个Input和Output的所有功能。既它可以接受一个待渲染对象,渲染完成后继续传递给下一个实现GPUImageInput协议的接受者。具体的方法调用我们在下一小节的 滤镜底层源码分析中讲解。
提供根据不同的顶点着色器(VertexShader)与片元着色器(FragmentShader)来初始化渲染程序(GLProgram)的方法,但是整个渲染过程是一样的,因此这个过程都被封装到了基类中;
1
2
3- (id)initWithVertexShaderFromString:(NSString *)vertexShaderString fragmentShaderFromString:(NSString *)fragmentShaderString;
- (id)initWithFragmentShaderFromString:(NSString *)fragmentShaderString;
- (id)initWithFragmentShaderFromFile:(NSString *)fragmentShaderFilename;这里简单介绍一下这几个
OPenGL
的术语
VertexShader
:顶点着色器,OPenGL
接收用户传递的几何数据(顶点信息和几何图元),这些数据经过顶点着色器后可以确定图形的形状以及位置。顶点着色器是 OPenGL 渲染过程的第一个着色器。- 光栅化:是将图形的立体位置转换成在屏幕上显示的像素片元的过程;
FragmentShader
:对光栅化的像素点进行着色就要使用片元着色器。它是OPenGL
渲染过程的最后一个着色器。GLProgram
:OpenGL ES
的program的面向对象封装,包括了VertexShader,FragmentShader的加载,program的link以及对attribute和uniform的获取和管理.
这里主要是一些根据不同的着色器进行创建Program的方法。
- 作为基类提供给子类可以进行覆盖的方法。
用一句话来总结GPUImageFilter的作用:就是用来接收源图像(FrameBuffer),通过自定义的顶点、片元着色器来渲染新的图像,并在绘制完成后通知响应链的下一个对象。
3.GPUImage滤镜的使用
我们先来看它的应用效果
(1) 为图片添加滤镜
直接上代码:
1 | /**初始化滤镜源头*/ |
流程说明:
- 使用图片初始化滤镜源头
GPUImagePicture
- 初始化滤镜效果
GPUImageGaussianBlurFilter
- 为当前滤镜源添加接收者Target
addTarget
useNextFrameForImageCapture
:方法是防止帧缓存被移除,如果不调用这个方法会导致Framebuffer被移除,从而导致Crash- 根据滤镜的渲染结果FrameBuffer导出图片
[gaussianBlur imageFromCurrentFramebuffer]
(2) 摄像头捕获视频流添加滤镜
核心代码:
1 | - (void)setupCamera |
(3) 混合滤镜的使用
核心代码:
1 | GPUImageView *filterView = [[GPUImageView alloc] initWithFrame:self.view.frame]; |
流程说明:
- 混合滤的核心是
GPUImageDissolveBlendFilter
的使用,它继承自GPUImageTwoInputFilter
,它需要有两个输入源 - 初始化两个输入源
GPUImageVideoCamera
跟GPUImageMovie
- 添加输入源到DissolveBlendFilter
- 添加filter到输出数据源
GPUImageMovieWriter
(4) 为视频添加水印
核心代码:
1 | GPUImageView *filterView = [[GPUImageView alloc] initWithFrame:self.view.frame]; |
流程说明:
- 混合滤镜的核心是
GPUImageDissolveBlendFilter
的使用,它继承自GPUImageTwoInputFilter
,它需要有两个输入源 - 初始化两个输入源
GPUImageVideoCamera
跟GPUImageUIElement
- 其他同上
(5) 滤镜组的使用
核心代码
1 | //创建摄像头视图 |
流程说明:
- 混合滤的核心是
GPUImageFilterGroup
的使用 - 初始化多个滤镜并且添加到滤镜组
- 设置Group的第一个以及最后一个滤镜
- 输出
二. GPUImage
底层源码分析
1.滤镜链加载流程分析
通过上面的Demo例子我们能够分析滤镜链的使用流程:
接下来我们以图片添加滤镜的例子分析GPUImage的滤镜方法调用流程:
- 使用图片初始化滤镜源头
GPUImagePicture
,调用方法:这个方法里面又会调用1
- (id)initWithImage:(UIImage *)newImageSource;
这个方法最主要的作用是根据图片的大小去1
outputFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:pixelSizeToUseForTexture onlyTexture:YES];
GPUImageFramebufferCache
中去获取一块 FrameBuffer,也就是outputFramebuffer
- 滤镜的初始化,根据当前自己的顶点着色器以及片元着色器初始化滤镜,以及创建OPenGL ES的渲染程序
GLProgram
- 为滤镜源添加Target:
- (void)addTarget:(id<GPUImageInput>)newTarget;
. 在这个方法里面会调用[self setInputFramebufferForTarget:newTarget atIndex:textureLocation];
最终会调用[target setInputFramebuffer:[self framebufferForOutput] atIndex:inputTextureIndex];
方法.这个方法最主要的作用是把当前Output的输出 Framebuffer 传递给接受者. - (void)useNextFrameForImageCapture;
设置成员变量usingNextFrameForImageCapture = YES
代表着输出的结果会被用于获取图像,所以在渲染的核心方法对1
- (void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates;
outputFramebuffer
加锁,因为默认情况下,当下一个input渲染完成之后,就会释放这个 FrameBuffer。如果你需要对当前的Filter的输出进行截图的话,则需要保留住这个 FrameBuffer。- 接下来调用方法
[imagePic processImage];
: 开始进入滤镜处理流程,接着调用方法-(BOOL)processImageWithCompletionHandler:(void (^)(void))completion;
在这个方法内部调用了Target的两个方法,进行OutputFrameBuffer的渲染与向下传递.
1 | [currentTarget setInputFramebuffer:outputFramebuffer atIndex:textureIndexOfTarget]; |
第一个方法的作用是获取从上个Output传递过来的 Framebuffer,并进行加锁操作。
第二个方法的作用是利用自身GLProgram
进行渲染,并且调用- (void)informTargetsAboutNewFrameAtTime:(CMTime)frameTime;
把渲染结果向下一个实现GPUImageInput
协议的滤镜传递。
[gaussianBlur imageFromCurrentFramebuffer];
方法:根据 Framebuffer 获取图片,里面调用- (CGImageRef)newCGImageFromCurrentlyProcessedOutput
方法,完成图片获取以及释放GCD信号量。这里信号量的作用是等待渲染完成。完成后走下面的获取图片流程。整个的方法调用流程可以参考下面的图片:1
2
3
4if (dispatch_semaphore_wait(imageCaptureSemaphore, convertedTimeout) != 0)
{
return NULL;
}
2.滤镜渲染流程分析
渲染是整个GPUImageFilter
的核心,在初始化方法中完成了OpenGL ES Program
的创建好并且link成功了之后,我们就可以使用这个Program进行渲染了。整个渲染的过程发生在- (void)renderToTextureWithVertices:textureCoordinates:
中。我们也借着解析这个方法来了解一下OpenGL ES
的渲染过程:
[GPUImageContext setActiveShaderProgram:filterProgram];
: 将初始化后得到Progrm 上下文设置为默认的context,并且激活。调用的GPUImageContext
方法1
2
3
4
5+ (void)setActiveShaderProgram:(GLProgram *)shaderProgram;
{
GPUImageContext *sharedContext = [GPUImageContext sharedImageProcessingContext];
[sharedContext setContextShaderProgram:shaderProgram];
}- 获取一个待渲染的
GPUImageFrameBuffer
,这个FrameBuffer 会根据输入纹理的尺寸(inputTextureSize)以及纹理信息(outputTextureOptions) 去GPUImageFrameBufferCahe
中获取。大致流程为:存在符合要求的Framebuffer就返回一个,没有就去创建。 - 根据
usingNextFrameForImageCapture
判断当前Framebuffer是否用于获取图片,如果是则进行加锁。1
2
3
4if (usingNextFrameForImageCapture)
{ //将这个outputFrameBuffer进行lock。
[outputFramebuffer lock];
} - 将整个FrameBuffer的数据使用backgroundColor进行清空:
1
2glClearColor(backgroundColorRed, backgroundColorGreen, backgroundColorBlue, backgroundColorAlpha);
glClear(GL_COLOR_BUFFER_BIT); - 将上一个Output传递过来的FrameBuffer作为texture用来渲染:
1
2
3glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]);
glUniform1i(filterInputTextureUniform, 2); - 将顶点的位置信息以及顶点的纹理坐标信息作为attribute传递给GPU:
1
2glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, vertices);
glVertexAttribPointer(filterTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates); - 进行渲染:
1
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
- 最后将上一个
GPUImageOutput
传递过来的FrameBuffer使命已经完成,对其进行解锁释放:整个渲染过程完成。1
[firstInputFramebuffer unlock];
三. 自定义滤镜
1.如何加载一个自定义滤镜
通过上面的学习我们知道,滤镜的效果实际是根据不同的顶点着色器以及片元着色器来实现的。是定义滤镜实际就是自定义这两种着色器。有两种方式来加载我们的自定义滤镜
- 自定义滤镜类,继承自
GPUImageFilter
,然后用字符串常量形式加载我们的Shader代码例如:然后根据1
2
3
4
5
6
7
8
9
10
11
12NSString *const kGPUImageBrightnessFragmentShaderString = SHADER_STRING
(
varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
uniform lowp float brightness;
void main()
{
lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
gl_FragColor = vec4((textureColor.rgb + vec3(brightness)), textureColor.w);
}
);GPUImageFilter
提供的初始化方法进行加载。1
2
3- (id)initWithVertexShaderFromString:(NSString *)vertexShaderString fragmentShaderFromString:(NSString *)fragmentShaderString;
- (id)initWithFragmentShaderFromString:(NSString *)fragmentShaderString;
- (id)initWithFragmentShaderFromFile:(NSString *)fragmentShaderFilename; - 另一种方式:如果只是自定义
FragmentShader
,可以是将Shader语句封装为fsh结尾的文件,然后调用下面方法进行加载1
- (id)initWithFragmentShaderFromFile:(NSString *)fragmentShaderFilename;
2. 一些特殊的自定义滤镜效果
一些特殊的滤镜效果,比如抖音的滤镜效果(闪白、灵魂出窍、抖动、缩放、毛刺、眩晕等)可以查看我的GitHub.
关于自定义滤镜部分需要你对OPenGL ES
、线性代数以及算法有基础的了解,并且熟悉GLSL着色语言
,如果想进一步学习可以参考GLSL的官方快速入门指导OpenGL ES,我们这篇文章不在涉及。
四. 总结
这篇文章主要是介绍了GPUImage
的使用、滤镜链加载流程、渲染逻辑,还有一些模块未涉及到,比如GLProgram
的创建、link过程,GPUImageMovieComposition
视频编辑模块,滤镜的自定义流程等,需要感兴趣的同学自己探究。
1.进一步学习需要掌握的内容
The OpenGL Shading Language
GLSL内建的函数介绍
2.一些参考引用
https://github.com/BradLarson/GPUImage
https://www.khronos.org/opengles/sdk/docs/reference_cards/OpenGL-ES-2_0-Reference-card.pdf
https://www.jianshu.com/u/8367278ff6cf