GPUImage是使用OpenGL进行滤镜处理的框架,作者后续开发了使用swift+OpenGL的GPUImage2,后续又有基于Metal的GPUImage3,之前学习了下Metal的一些基本使用,这次来学习下GPUImage3的源码。
采集视频经过滤镜渲染至界面
1 |
|
这段代码创建了一个Camera对象,一个滤镜对象,以及在Storyboard上添加的一个RenderView,然后使用–>这个运算符将三者作为一个响应链。
1 |
|
–>实际上是一个自定义运算符,它将右边的对象添加到左边对象的targets中,然后返回右边的对象,这样可以连续使用。 swift自定义运算符
分析代码
Camera对象
1 |
|
这里首先创建了一些视频采集所需要的对象,如AVCaptureSession等。
1 |
|
这里根据设置的是否采集yuv标记,来设置采集的视频数据格式,如果是采集yuv,还需要判断是否是全范围的yuv(fullRange),设置完毕后,如果是yuv格式,还会创建一个用于转换yuv到rgb的MTLRenderPipelineState,sharedMetalRenderingDevice是一个全局的设备,是对MTLDevice的一个封装,里面包含了MTLCommandQueue、MTLLibrary。如果是采集rgba,则直接设置视频格式。
1 |
|
最后创建了一个Metal的纹理缓存。
滤镜对象
这里使用的SketchFilter滤镜,它是继承自TextureSamplingOperation的子类,TextureSamplingOperation继承自BasicOperation,BasicOperation遵循了ImageProcessingOperation协议。ImageProcessingOperation协议又遵循了ImageConsumer,ImageSource,表明BasicOperation既可以是图像数据的来源,也可以是消费者,这样就可以构成响应链的一环。
1 |
|
这里是BasicOperation的初始化函数,首先根据外部传入一个输入(纹理)数量,默认是1,使用operationName保存执行对象的文件路径,用于记录错误信息。然后判断是否传入顶点着色器函数名,没有的话根据输入来确定默认的顶点着色器函数名。之后根据传入的片段着色器函数名和顶点着色器函数名创建一个用于滤镜处理的MTLRenderPipelineState。
RenderView
RenderView是继承自MTKView,并且遵守了了ImageConsumer协议,使它可以作为数据的消费者,响应链的终点。
1 |
|
RenderView初始化时也创建了一个用于渲染数据的MTLRenderPipelineState。
创建MTLRenderPipelineState
1 |
|
在创建MTLRenderPipelineState的函数中,首先创建顶点着色器和片段着色器两个MTLFunction,然后创建一个管道状态描述器(MTLRenderPipelineDescriptor),给这个描述器设置函数。然后使用全局的MTLDevice,创建管道状态(MTLRenderPipelineState),最后使用MTLRenderPipelineReflection获取片段着色器参数,将类型为buffer的参数存到一个字典中,再将管道状态和参数字典返回。
Camera采集到视频数据后的处理
1 |
|
在视频数据的回调中,首先取得CMSampleBuffer的pixelBuffer,然后取得宽高。
1 |
|
接下来判断是否是yuv数据,如果是,则创建两个纹理,存放y通道和uv通道,并且确定一个转换矩阵,之后调用convertYUVToRGB方法,将数据转换为RGB然后写入outputTexture。如果是RGBA数据,则直接写入纹理。之后将纹理传给响应链的下一环进行处理。
1 |
|
在convertYUVToRGB中,首先创建了一个commandBuffer,然后将纹理存入一个字典中,再调用commandBuffer的一个扩展方法,renderQuad,最后提交commandBuffer,开始真正的渲染过程。
在renderQuad方法中,首先根据顶点坐标创建了一个MTLBuffer ,然后创建一个MTLRenderPassDescriptor,将输出的纹理作为colorAttachments[0]的纹理,表示将渲染的结果写入这个纹理。之后创建一个renderEncoder,设置顶点buffer。然后遍历输入纹理字典,将y通道和uv通道的纹理按索引传入renderEncoder。之后再将uniform参数字典遍历,将相应的参数写入renderEncoder。最后调用绘制函数。
1 |
|
在采集的纹理转换好之后,Camera会调用updateTargetsWithTexture方法,遍历所有的targets,将纹理传给target。target会调用newTextureAvailable方法。
filter的newTextureAvailable
1 |
|
这里收先将传入的texture写入输入纹理数组。然后判断输入纹理数量是否超出最大限制,超出就什么都不做。然后根据纹理的宽高、缩放方式等,算出一个输出的宽高。
1 |
|
然后创建一个commandBuffer和一个用于接受输出的纹理对象。之后判断是否有一个替代的渲染函数,默认没有,然后执行默认的渲染函数,实际就是执行commandBuffer的renderQuad函数。然后提交commandBuffer,执行渲染。最后再寻找响应链的下一个target,将输出的纹理传给它。
RenderView的newTextureAvailable
1 |
|
这里调用draw方法,然后在draw方法中也是创建commandBuffer,然后调用commandBuffer的renderQuad方法,不过输出的纹理使用的是currentDrawable的纹理,它是MTKView的属性用于渲染显示。
总结
GPUImage3也是和GPUImage类似的滤镜链处理,每个滤镜链上的对象都有一个MTLRenderPipelineState,用于做各自的滤镜处理。输入和输出被抽象成两个协议ImageSource和ImageConsumer,每次渲染时会生成一个commandBuffer然后提交渲染命令。感觉这种响应链的模式很值得学习。同时也觉得Metal的代码比OpenGL要简练一些,更符合iOS开发者的习惯。