GPUImage是基于GUP图像和视频处理的iOS开源框架,它采用链式传递每一层渲染的帧缓存对象,通过addTarget:方法为链条添加每层的filter,直到最后通过GPUImageView来显示。
这里通过研究GPUImage是如何将摄像头抓取的图像经过一层一层滤镜渲染最后展示给用户,来学习GPUImage的整体框架设计原理。
首先介绍GPUImage的几个主要的类
GLProgram //加载顶点着色器和片元着色器程序并进行编译链接最终使用,着色器中attribute添加等
GPUImageOutput //抽象类,实现addTarget:以及从当前帧缓存获取图像等接口
GPUImageFilter //继承自GPUImageOutput,所有滤镜的父类(除去Group滤镜),主要提供给着色器传递参数的接口,以及渲染当前的帧缓存并传递给下一层target,链式结构的实现主要就是在newFrameReadyAtTime: atIndex:方法中,下面会详细讲述
GPUImageFramebuffer //OpenGL的FBO就是通过它实现的
GPUImageFramebufferCache //实现帧缓存的重用机制
GPUImageTwoInputFilter //所有多层纹理特效都是通过它来实现的,比如抖音的“幻觉”特效
GPUImageFilterGroup //组合滤镜
GPUImageVideoCamera //实现摄像头的实时视频和音频的采集
GPUImagePicture //可以作为混合滤镜的第N个纹理,比如抖音中那些萌萌的表情就可以通过它来实现
GPUImageView //对渲染好的图像进行显示
GPUImageMovieWriter //保存视频
下面开始跑一遍流程
1. 创建GPUImageVideoCamera对象并设置代理,然后通过startCameraCapture开启抓屏
2. 我们抓取到的每一帧视频和音频数据都会通过以下回调返回给我们
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
这里我们不讨论对音频的处理,这个方法对抓取到的视频帧,主要实现了两步处理
if (self.delegate)
{
[self.delegate willOutputSampleBuffer:sampleBuffer];
}
[self processVideoSampleBuffer:sampleBuffer];
首先告诉我们的代理,我将要输出一帧图像啦,这里的帧数据sampleBuffer是没有经过任何滤镜特效处理的,因此,如果你在这个代理方法中可以对后续的滤镜进行一些用户切换或者调整什么的操作。
然后我们具体来看这个processVideoSampleBuffer:的实现
3. processVideoSampleBuffer:的实现
这里有一个判断
if ([GPUImageContext supportsFastTextureUpload] && captureAsYUV) //如果支持纹理缓存并且采用YUV视频编码
由于前面我们已经配置了kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,因此这里只讨论这种情况,继续看if语句中的实现。由于YUV视频帧分为亮度和色度两个纹理,分别用GL_LUMINANCE格式和GL_LUMINANCE_ALPHA格式读取。
通过CVOpenGLESTextureCacheCreateTextureFromImage方法将帧信息加载到[[GPUImageContext sharedImageProcessingContext] coreVideoTextureCache]缓存中,然后通过convertYUVToRGBOutput来进行编码转换,在这个方法里,我们将图像渲染到了outputFramebuffer帧缓存中,最后我们调用
[self updateTargetsForVideoCameraUsingCacheTextureAtWidth:rotatedImageBufferWidth height:rotatedImageBufferHeight time:currentTime];
4. - (void)updateTargetsForVideoCameraUsingCacheTextureAtWidth:(int)bufferWidth height:(int)bufferHeight time:(CMTime)currentTime 的实现
这个方法里面对当前对象的每个target会首先调用
[currentTarget setInputFramebuffer:outputFramebuffer atIndex:textureIndexOfTarget];
即
- (void)setInputFramebuffer:(GPUImageFramebuffer *)newInputFramebuffer atIndex:(NSInteger)textureIndex;
{
firstInputFramebuffer = newInputFramebuffer;
[firstInputFramebuffer lock];
}
firstInputFramebuffer对应的是着色器中的第一个采样器即inputImageTexture,如果你用到混合纹理的话,secondInputFramebuffer对应的就是inputImageTexture2
然后调用
[currentTarget newFrameReadyAtTime:currentTime atIndex:textureIndexOfTarget];
5. newFrameReadyAtTime:atIndex:的实现
首先调用
[self renderToTextureWithVertices:imageVertices textureCoordinates:[[self class] textureCoordinatesForRotation:inputRotation]];
这里主要是将当前滤镜的特效渲染到outputFramebuffer缓存帧
然后调用
[self informTargetsAboutNewFrameAtTime:frameTime];
6. informTargetsAboutNewFrameAtTime:的实现
首先通过调用
[self setInputFramebufferForTarget:currentTarget atIndex:textureIndex];
即
- (void)setInputFramebufferForTarget:(id)target atIndex:(NSInteger)inputTextureIndex;
{
[target setInputFramebuffer:[self framebufferForOutput] atIndex:inputTextureIndex];
}
来将outputFramebuffer帧缓存的内容赋值给firstInputFramebuffer,注意这里是针对只有一个采样器的时候
然后对当前滤镜的所有target调用
[currentTarget newFrameReadyAtTime:frameTime atIndex:textureIndex];
这个方法上面提到过,这样就形成了一个链式响应结构,就这样一层一层执行到最后那个target也就是GPUImageView把图像显示出来。
参考资料
帧缓存FBO ==> https://www.jianshu.com/p/d7066d6a02cc
纹理 ==> https://learnopengl-cn.readthedocs.io/zh/latest/01%20Getting%20started/06%20Textures/