iOS-滤镜那些事儿

一. GPUImage 框架的介绍及基本使用

1.GPUImage 的介绍

GPUImage是基于OpenGL ES的一套图像、视频处理开源框架,它里面提供了大量的滤镜,使用者可以通过这些滤镜的组合实现很好的效果,同时也很方便在原有基础上实现自定义的滤镜。对于大规模并行操作(如处理图像或实时视频帧),GPU具有比CPU更显着的性能优势。而 GPUImage 所有滤镜是基于OpenGL Shader实现的,所以滤镜效果、图像处理是在GPU上执行的,处理效率比较高,在iPhone4及其以上手机,可以做到实时流畅的效果。而且它隐藏了Objective-COpenGL 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 是所有滤镜输入源的基类,也就是滤镜链的起点,先看下他的继承关系:

GPUImageOutput

分别解释一下这几种类型:

  • GPUImagePicture
    通过图片来初始化,本质上是先将图片转化为 CGImageRef,然后将 CGImageRef 转化为纹理。
  • GPUImageVideoCamera:通过相机来初始化,本质是封装了AVCaptureVideoDataOutput来获取持续的视频流数据输出,在代理方法captureOutput:didOutputSampleBuffer:fromConnection:拿到 CMSampleBufferRef,将其转化为纹理的过程。GPUImageStillCamera是 GPUImageVideoCamera 的子类,可以用它来实现拍照功能。
  • GPUImageUIElement:可以通过 UIView 或者 CALayer 来初始化。这个类可以用来实现在视频上添加文字水印的功能。
  • GPUImageTextureInput:通过已经存在的纹理来初始化.
  • GPUImageRawDataInput:通过二进制数据初始化,然后将二进制数据转化为纹理.
  • GPUImageMovie:通过本地的视频来初始化。首先通过 AVAssetReader 来逐帧读取视频,然后将帧数据转化为纹理。
  • GPUImageFilter:比较特殊,它既继承自 GPUImageOutput,又遵守协议 GPUImageInput 协议,所以它既可以作为滤镜链的源头,又可以把渲染的纹理输出给遵守 GPUImageInput 协议的类。是滤镜的核心,后面会单独介绍。
核心功能与方法:

想象一下,一个滤镜链的源头能做什么呢:

  1. 需要产出一个渲染对象,这个需要渲染的对象就是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的结果进行渲染。

  1. Target的添加以及管理,用来生成整个FilterChain.

    GPUImageOutput 既然作为一个滤镜的源头,相对应的就得有接受者接受它输出的 FrameBuffer ,这些接受者就是Target,而且有可能有多个接受者。管理这些target的主要方法:
    1
    2
    - (void)addTarget:(id<GPUImageInput>)newTarget;
    - (void)addTarget:(id<GPUImageInput>)newTarget atTextureLocation:(NSInteger)textureLocation;
    这两个addTarget方法的作用都是将下一个实现了GPUImageInput协议的对象添加到FilterChain当中来.一旦添加到滤镜链后,在当前Output渲染完成后就会收到通知,从而进行下一步的处理。
1
- (NSArray*)targets;

每个Output都可以添加多个target,这个方法可以获取到当前Output所有的target.

1
2
- (void)removeTarget:(id<GPUImageInput>)targetToRemove;
- (void)removeAllTargets;

这两个方法的作用是将某一个或者所有的target都移出FilterChain。当一个target被移出FilterChain之后,它将不会再收到任何当前Output渲染完成的通知。

  1. 获取当前的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 接口的类:

GPUImageInput协议
对这几个类进行解释:

  • GPUImageMovieWriter:封装了 AVAssetWriter,可以逐帧从帧缓存的渲染结果中读取数据,最后通过 AVAssetWriter 将视频文件保存到指定的路径。
  • GPUImageView:继承自 UIView,通过输入的纹理,执行一遍渲染流程。我们一般使用它来呈现渲染结果。
  • GPUImageTextureOutput:它可以获取到输入的Framebuffer中的纹理对象.
  • GPUImageRawDataOutput:通过 rawBytesForImage 属性,可以获取到当前输入纹理的二进制数据。
核心功能与方法:

可以作为滤镜链的终点。基本功能主要包括:

  • 接收 GPUmageOutput 的输出信息;
  • 接收上一个GPUImageOutput渲染完成的通知,并且完成自己的处理;
  1. 接收GPUmageOutput的输出信息对应方法:
    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;
    根据这些方法可以看到,GPUImageInput 可以接收的信息包括上一个Output输出的FrameBuffer,FrameBuffer的size以及rotation。这些 textureIndex 都是为了提供个需要多个input的Filter准备的。
  2. 接收GPUImageOutput渲染完成的通知对应方法:
    1
    - (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex;
    上一个 GPUImageOutput 渲染完成后会通知它所有的 Target,可以参考下它在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 : 链式组合滤镜
核心功能与方法:
  1. GPUImageFilter是GPUImageOutput的子类,但是同时它也实现了GPUImageInput协议。因此,它包含了一个Input和Output的所有功能。既它可以接受一个待渲染对象,渲染完成后继续传递给下一个实现GPUImageInput协议的接受者。具体的方法调用我们在下一小节的 滤镜底层源码分析中讲解。

  2. 提供根据不同的顶点着色器(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的方法。
  1. 作为基类提供给子类可以进行覆盖的方法。

用一句话来总结GPUImageFilter的作用:就是用来接收源图像(FrameBuffer),通过自定义的顶点、片元着色器来渲染新的图像,并在绘制完成后通知响应链的下一个对象。

3.GPUImage滤镜的使用

我们先来看它的应用效果

效果效果2

(1) 为图片添加滤镜

直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**初始化滤镜源头*/
GPUImagePicture *imagePic = [[GPUImagePicture alloc] initWithImage:[UIImage imageNamed:@"picOne.jpg"]];
/**创建滤镜*/
GPUImageGaussianBlurFilter *gaussianBlur = [[GPUImageGaussianBlurFilter alloc] init];
gaussianBlur.blurRadiusInPixels = 10;
/**添加接受者,即target*/
[imagePic addTarget:gaussianBlur];
/**增加frameBUffer 计数防止被移除*/
[gaussianBlur useNextFrameForImageCapture];
/**开始处理图片*/
[imagePic processImage];
/**根据frameBuffer 获取图片*/
self.showImageView.image = [gaussianBlur imageFromCurrentFramebuffer];
流程说明:
  • 使用图片初始化滤镜源头GPUImagePicture
  • 初始化滤镜效果GPUImageGaussianBlurFilter
  • 为当前滤镜源添加接收者Target addTarget
  • useNextFrameForImageCapture:方法是防止帧缓存被移除,如果不调用这个方法会导致Framebuffer被移除,从而导致Crash
  • 根据滤镜的渲染结果FrameBuffer导出图片[gaussianBlur imageFromCurrentFramebuffer]
(2) 摄像头捕获视频流添加滤镜

核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)setupCamera
{
//videoCamera
self.gpuVideoCamera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPreset640x480 cameraPosition:AVCaptureDevicePositionBack];
self.gpuVideoCamera.outputImageOrientation = [[UIApplication sharedApplication] statusBarOrientation];
//GPUImageView填充模式
self.gpuImageView.fillMode = kGPUImageFillModePreserveAspectRatioAndFill;
//空白效果
GPUImageFilter *clearFilter = [[GPUImageFilter alloc] init];
[self.gpuVideoCamera addTarget:clearFilter];
[clearFilter addTarget:self.gpuImageView];
//Start camera capturing, 里面封装的是AVFoundation的session的startRunning
[self.gpuVideoCamera startCameraCapture];
}
#pragma mark - Action && Notification
- (IBAction)originalBtnDown:(id)sender {
/**先移除target*/
[self.gpuVideoCamera removeAllTargets];
//空白效果
GPUImageFilter *clearFilter = [[GPUImageFilter alloc] init];
[self.gpuVideoCamera addTarget:clearFilter];
[clearFilter addTarget:self.gpuImageView];
}
(3) 混合滤镜的使用

核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
GPUImageView *filterView = [[GPUImageView alloc] initWithFrame:self.view.frame];
filterView.center = self.view.center;
filterView.fillMode = kGPUImageFillModePreserveAspectRatioAndFill;
[self.view addSubview:filterView];
/*初始化混合滤镜*/
filter = [[GPUImageDissolveBlendFilter alloc] init];
/*设置滤镜混合度*/
[(GPUImageDissolveBlendFilter *)filter setMix:0.5];
/*初始化视频输出源*/
NSURL *sampleURL = [[NSBundle mainBundle] URLForResource:@"IMG_4278" withExtension:@"MOV"];
movieFile = [[GPUImageMovie alloc] initWithURL:sampleURL];
movieFile.runBenchmark = YES;
movieFile.playAtActualSpeed = YES;
/*初始化摄像头输出源*/
videoCamera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPreset640x480 cameraPosition:AVCaptureDevicePositionBack];
videoCamera.outputImageOrientation = UIInterfaceOrientationPortrait;
NSString *pathToMovie = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/Movie.m4v"];
unlink([pathToMovie UTF8String]);
NSURL *movieURL = [NSURL fileURLWithPath:pathToMovie];
//初始化接受者
movieWriter = [[GPUImageMovieWriter alloc] initWithMovieURL:movieURL size:CGSizeMake(480.0, 640.0)];
GPUImageFilter* progressFilter = [[GPUImageFilter alloc] init];
[movieFile addTarget:progressFilter];
//设置输出方向
[progressFilter setInputRotation:kGPUImageRotateRight atIndex:0];
// 响应链
[progressFilter addTarget:filter];
[videoCamera addTarget:filter];
//设置音源
movieWriter.shouldPassthroughAudio = YES;
movieFile.audioEncodingTarget = movieWriter;
[movieFile enableSynchronizedEncodingUsingMovieWriter:movieWriter];
// 显示到界面
[filter addTarget:filterView];
//添加到接收者
[filter addTarget:movieWriter];
[videoCamera startCameraCapture];
[movieWriter startRecording];
[movieFile startProcessing];
/*写入结束后保存视频*/
__weak typeof(self) weakSelf = self;
[movieWriter setCompletionBlock:^{
__strong typeof(self) strongSelf = weakSelf;
[strongSelf->filter removeTarget:strongSelf->movieWriter];
[strongSelf->movieWriter finishRecording];
/*根据movieURL保存视频到本地*/
// ...
}];

流程说明:
  • 混合滤的核心是GPUImageDissolveBlendFilter的使用,它继承自GPUImageTwoInputFilter,它需要有两个输入源
  • 初始化两个输入源GPUImageVideoCameraGPUImageMovie
  • 添加输入源到DissolveBlendFilter
  • 添加filter到输出数据源GPUImageMovieWriter
(4) 为视频添加水印

核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
GPUImageView *filterView = [[GPUImageView alloc] initWithFrame:self.view.frame];
self.view = filterView;
// 混合滤镜初始化
filter = [[GPUImageDissolveBlendFilter alloc] init];
//混合度
[(GPUImageDissolveBlendFilter *)filter setMix:0.5];
// 本地视频播放源
NSURL *sampleURL = [[NSBundle mainBundle] URLForResource:@"IMG_4278" withExtension:@"MOV"];
AVAsset *asset = [AVAsset assetWithURL:sampleURL];
CGSize size = self.view.bounds.size;
//设置moive源头
movieFile = [[GPUImageMovie alloc] initWithAsset:asset];
movieFile.runBenchmark = YES;
movieFile.playAtActualSpeed = YES;
// 水印
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
label.text = @"我是水印";
label.font = [UIFont systemFontOfSize:30];
label.textColor = [UIColor redColor];
[label sizeToFit];
UIImage *image = [UIImage imageNamed:@"watermark.png"];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
UIView *subView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height)];
subView.backgroundColor = [UIColor clearColor];
imageView.center = CGPointMake(subView.bounds.size.width / 2, subView.bounds.size.height / 2);
[subView addSubview:imageView];
[subView addSubview:label];
//设置UI源头
GPUImageUIElement *uielement = [[GPUImageUIElement alloc] initWithView:subView];
//GPUImageTransformFilter 动画的filter
NSString *pathToMovie = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/Movie.m4v"];
unlink([pathToMovie UTF8String]);
NSURL *movieURL = [NSURL fileURLWithPath:pathToMovie];
//初始化接受者
movieWriter = [[GPUImageMovieWriter alloc] initWithMovieURL:movieURL size:CGSizeMake(480.0, 640.0)];
//为调整视频方向添加一个空白滤镜
GPUImageFilter* progressFilter = [[GPUImageFilter alloc] init];
[movieFile addTarget:progressFilter];
//设置方向
[progressFilter setInputRotation:kGPUImageRotateRight atIndex:0];

[progressFilter addTarget:filter];
[uielement addTarget:filter];
movieWriter.shouldPassthroughAudio = YES;
movieFile.audioEncodingTarget = movieWriter;
[movieFile enableSynchronizedEncodingUsingMovieWriter:movieWriter];
// 显示到界面
[filter addTarget:filterView];
[filter addTarget:movieWriter];
//开始记录
[movieWriter startRecording];
[movieFile startProcessing];
__weak typeof(self) weakSelf = self;
//每一帧处理完成 大约30帧/秒
[progressFilter setFrameProcessingCompletionBlock:^(GPUImageOutput *output, CMTime time){
CGRect frame = imageView.frame;
frame.origin.x += 1;
frame.origin.y += 1;
imageView.frame = frame;
//更新UIElement
[uielement updateWithTimestamp:time];
}];
[movieWriter setCompletionBlock:^{
__strong typeof(self) strongSelf = weakSelf;
[strongSelf->filter removeTarget:strongSelf->movieWriter];
[strongSelf->movieWriter finishRecording];
/*根据movieURL保存视频到本地*/
// ...
}];
流程说明:
  • 混合滤镜的核心是GPUImageDissolveBlendFilter的使用,它继承自GPUImageTwoInputFilter,它需要有两个输入源
  • 初始化两个输入源GPUImageVideoCameraGPUImageUIElement
  • 其他同上
(5) 滤镜组的使用

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//创建摄像头视图
GPUImageView *filterView = [[GPUImageView alloc]initWithFrame:self.view.bounds];
//显示模式充满整个边框
filterView.fillMode = kGPUImageFillModePreserveAspectRatioAndFill;
[self.view addSubview:filterView];
//初始化滤镜源
self.stillCamera = [[GPUImageStillCamera alloc]initWithSessionPreset:AVCaptureSessionPresetPhoto cameraPosition:AVCaptureDevicePositionBack];
//输出图像旋转方式
self.stillCamera.outputImageOrientation = UIInterfaceOrientationPortrait;
//反色滤镜
GPUImageColorInvertFilter *filter1 = [[GPUImageColorInvertFilter alloc]init];
//浮雕滤镜
GPUImageEmbossFilter *filter2 = [[GPUImageEmbossFilter alloc]init];
//GPUImageToonFilter *filter3 = [[GPUImageToonFilter alloc] init];
GPUImageFilterGroup *groupFilter = [[GPUImageFilterGroup alloc]init];
[groupFilter addFilter:filter1];
[groupFilter addFilter:filter2];
//[groupFilter addFilter:filter3];
[filter1 addTarget:filter2];
//[filter2 addTarget:filter3];
//定义了一个变量来保存filter-chain上的最后一个filter,后面保存图片时调用的方法里要用到。
self.lastFilter = filter2;
//设置第一个滤镜
groupFilter.initialFilters = @[filter1];
//设置最后一个滤镜
groupFilter.terminalFilter = filter2;
[self.stillCamera addTarget:groupFilter];
[groupFilter addTarget:filterView];
//解决第一帧黑屏,音频缓冲区是在视频缓冲区之前写入的。
[self.stillCamera addAudioInputsAndOutputs];
[self.view bringSubviewToFront:self.catchBtn];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//开始捕捉
[self.stillCamera startCameraCapture];
});

流程说明:
  • 混合滤的核心是GPUImageFilterGroup的使用
  • 初始化多个滤镜并且添加到滤镜组
  • 设置Group的第一个以及最后一个滤镜
  • 输出

二. GPUImage 底层源码分析

1.滤镜链加载流程分析

通过上面的Demo例子我们能够分析滤镜链的使用流程:

GPUImageFilter流

接下来我们以图片添加滤镜的例子分析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
2
[currentTarget setInputFramebuffer:outputFramebuffer atIndex:textureIndexOfTarget];
[currentTarget newFrameReadyAtTime:kCMTimeIndefinite atIndex:textureIndexOfTarget];

第一个方法的作用是获取从上个Output传递过来的 Framebuffer,并进行加锁操作。

第二个方法的作用是利用自身GLProgram进行渲染,并且调用- (void)informTargetsAboutNewFrameAtTime:(CMTime)frameTime;把渲染结果向下一个实现GPUImageInput协议的滤镜传递。

  • [gaussianBlur imageFromCurrentFramebuffer]; 方法:根据 Framebuffer 获取图片,里面调用- (CGImageRef)newCGImageFromCurrentlyProcessedOutput 方法,完成图片获取以及释放GCD信号量。
    1
    2
    3
    4
    if (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
    4
    if (usingNextFrameForImageCapture)
    { //将这个outputFrameBuffer进行lock。
    [outputFramebuffer lock];
    }
  • 将整个FrameBuffer的数据使用backgroundColor进行清空:
    1
    2
    glClearColor(backgroundColorRed, backgroundColorGreen, backgroundColorBlue, backgroundColorAlpha);
    glClear(GL_COLOR_BUFFER_BIT);
  • 将上一个Output传递过来的FrameBuffer作为texture用来渲染:
    1
    2
    3
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]);
    glUniform1i(filterInputTextureUniform, 2);
  • 将顶点的位置信息以及顶点的纹理坐标信息作为attribute传递给GPU:
    1
    2
    glVertexAttribPointer(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
    12
    NSString *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