iOS滤镜那些事儿 | 创作者训练营第二期

移动端团队 @ 奇舞团(360集团大前端团队)

一. 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 集成:

pod 'GPUImage'
复制代码

首先来看下它的基本结构图:截屏2021-04-11 下午3.56.11.png

从这张图中我们可以看到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的方法:
- (GPUImageFramebuffer *)framebufferForOutput;
复制代码

这个方法可以获得当前正在渲染的frameBuffer

- (void)removeOutputFramebuffer;
复制代码

这个方法用来移除当前渲染的frameBuffer

- (void)setInputFramebufferForTarget:(id<GPUImageInput>)target atIndex:(NSInteger)inputTextureIndex;
复制代码

这个方法的调用发生在当前output渲染完毕后,需要通知下一个receiver可以开始渲染的时候,把当前Output的FrameBuffer传递给下一个Input,让它可以使用这个FrameBuffer的结果进行渲染。

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

GPUImageOutput 既然作为一个滤镜的源头,相对应的就得有接受者接受它输出的 FrameBuffer ,这些接受者就是Target,而且有可能有多个接受者。管理这些target的主要方法:

- (void)addTarget:(id<GPUImageInput>)newTarget;
- (void)addTarget:(id<GPUImageInput>)newTarget atTextureLocation:(NSInteger)textureLocation;
复制代码

这两个addTarget方法的作用都是将下一个实现了GPUImageInput协议的对象添加到FilterChain当中来.一旦添加到滤镜链后,在当前Output渲染完成后就会收到通知,从而进行下一步的处理。

- (NSArray*)targets;
复制代码

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

- (void)removeTarget:(id<GPUImageInput>)targetToRemove;
- (void)removeAllTargets;
复制代码

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

  1. 获取当前的GPUImageOutput对FrameBuffer的处理结果
- (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的输出信息对应方法:
- (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渲染完成的通知对应方法:

- (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)的方法,但是整个渲染过程是一样的,因此这个过程都被封装到了基类中;

- (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) 为图片添加滤镜

直接上代码:

   /**初始化滤镜源头*/
    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) 摄像头捕获视频流添加滤镜

核心代码:

- (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) 混合滤镜的使用

核心代码:

    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) 为视频添加水印

核心代码:

    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) 滤镜组的使用

核心代码

    //创建摄像头视图
    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,调用方法:
- (id)initWithImage:(UIImage *)newImageSource;
复制代码

这个方法里面又会调用

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代表着输出的结果会被用于获取图像,所以在渲染的核心方法
- (void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates;
复制代码

outputFramebuffer加锁,因为默认情况下,当下一个input渲染完成之后,就会释放这个 FrameBuffer。如果你需要对当前的Filter的输出进行截图的话,则需要保留住这个 FrameBuffer。

  • 接下来调用方法[imagePic processImage];: 开始进入滤镜处理流程,接着调用方法-(BOOL)processImageWithCompletionHandler:(void (^)(void))completion;在这个方法内部调用了Target的两个方法,进行OutputFrameBuffer的渲染与向下传递.
 [currentTarget setInputFramebuffer:outputFramebuffer atIndex:textureIndexOfTarget];
 [currentTarget newFrameReadyAtTime:kCMTimeIndefinite atIndex:textureIndexOfTarget];
复制代码

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

  • [gaussianBlur imageFromCurrentFramebuffer]; 方法:根据 Framebuffer 获取图片,里面调用- (CGImageRef)newCGImageFromCurrentlyProcessedOutput 方法,完成图片获取以及释放GCD信号量。
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方法
+ (void)setActiveShaderProgram:(GLProgram *)shaderProgram;
{
    GPUImageContext *sharedContext = [GPUImageContext sharedImageProcessingContext];
    [sharedContext setContextShaderProgram:shaderProgram];
}
复制代码
  • 获取一个待渲染的GPUImageFrameBuffer,这个FrameBuffer 会根据输入纹理的尺寸(inputTextureSize)以及纹理信息(outputTextureOptions) 去GPUImageFrameBufferCahe中获取。大致流程为:存在符合要求的Framebuffer就返回一个,没有就去创建。
  • 根据usingNextFrameForImageCapture判断当前Framebuffer是否用于获取图片,如果是则进行加锁。
 if (usingNextFrameForImageCapture)
    { //将这个outputFrameBuffer进行lock。
        [outputFramebuffer lock];
    }
复制代码
  • 将整个FrameBuffer的数据使用backgroundColor进行清空:
glClearColor(backgroundColorRed, backgroundColorGreen, backgroundColorBlue, backgroundColorAlpha);
glClear(GL_COLOR_BUFFER_BIT);
复制代码
  • 将上一个Output传递过来的FrameBuffer作为texture用来渲染:
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]);
glUniform1i(filterInputTextureUniform, 2);
复制代码
  • 将顶点的位置信息以及顶点的纹理坐标信息作为attribute传递给GPU:
glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, vertices);
glVertexAttribPointer(filterTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates);
复制代码
  • 进行渲染:
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
复制代码
  • 最后将上一个GPUImageOutput传递过来的FrameBuffer使命已经完成,对其进行解锁释放:
[firstInputFramebuffer unlock];
复制代码

整个渲染过程完成。

三. 自定义滤镜

1.如何加载一个自定义滤镜

通过上面的学习我们知道,滤镜的效果实际是根据不同的顶点着色器以及片元着色器来实现的。自定义滤镜实际就是自定义这两种着色器。有两种方式来加载我们的自定义滤镜

  • 自定义滤镜类,继承自GPUImageFilter,然后用字符串常量形式加载我们的Shader代码例如:
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提供的初始化方法进行加载。

- (id)initWithVertexShaderFromString:(NSString *)vertexShaderString fragmentShaderFromString:(NSString *)fragmentShaderString;
- (id)initWithFragmentShaderFromString:(NSString *)fragmentShaderString;
- (id)initWithFragmentShaderFromFile:(NSString *)fragmentShaderFilename;
复制代码
  • 另一种方式:如果只是自定义FragmentShader,可以是将Shader语句封装为fsh结尾的文件,然后调用下面方法进行加载
- (id)initWithFragmentShaderFromFile:(NSString *)fragmentShaderFilename;
复制代码

2. 一些特殊的自定义滤镜效果

自定义滤镜
一些特殊的滤镜效果,比如抖音的滤镜效果(闪白、灵魂出窍、抖动、缩放、毛刺、眩晕等)可以查看我的GitHub. 关于自定义滤镜部分需要你对OPenGL ES、线性代数以及算法有基础的了解,并且熟悉GLSL着色语言,如果想进一步学习可以参考GLSL的官方快速入门指导OpenGL ES,我们这篇文章不在涉及。

四. 总结

这篇文章主要是介绍了GPUImage的使用、滤镜链加载流程、渲染逻辑,还有一些模块未涉及到,比如GLProgram的创建、link过程,GPUImageMovieComposition视频编辑模块,滤镜的自定义流程等,需要感兴趣的同学自己探究。

1.进一步学习需要掌握的内容

The OpenGL Shading Language
GLSL内建的函数介绍

2.一些参考引用

github.com/BradLarson/…
www.khronos.org/opengles/sd…
www.jianshu.com/u/8367278ff…

文章分类
iOS