iOS 使用 OpenGLES 渲染相机预览画面
前言
上一篇有提到 使用 AVFoundation 采集相机画面,并渲染到苹果内置的 AVCaptureVideoPreviewLayer 图层上,代码很简单,但使用上有很大局限性。AVCaptureSession 采集到的原始视频帧直接给到了 AVCaptureVideoPreviewLayer 用于渲染,我们没办法在中间环节处理视频帧数据,美颜滤镜也就没办法实现,所以我们需要借助 OpenGLES 自己实现画面渲染。
苹果的 GLKit 框架对 OpenGLES 的部分接口调用做了封装,使用起来非常方便。我们的需求可以直接使用 GLKView 实现,开发的代码量是比较少的,但为了熟悉 OpenGLES 的接口调用和管线渲染流程,我们还是使用最原始的方法,基于 CAEAGLLayer,自己来实现着色器代码。
代码实现
准备工作
使用 CAEAGLLayer
首先在项目中创建一个继承 UIView 的 XPGLKView,如果需要在图层上自定义 OpenGL 渲染,需要将 UIView 的 layerClass 设置为 CAEAGLLayer:
1 | @implementation XPGLKView |
顶点数据计算
我们需要计算两类坐标,一类是 OpenGL 顶点坐标,一类是纹理贴图的坐标,先定义一个结构体类型,包含两类坐标:
1 | typedef struct { |
我们的场景只需要 4 个顶点,使用 glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); 即可绘制两个三角形,按照顶点坐标和纹理坐标一一对应的关系,可以得到一个初步的顶点数据:
1 | VerticesCoordinates vertices[] = { |
上面看着没什么问题,但实际渲染发现图像上下颠倒了,原因是使用 CoreVideo 框架读取 Pixel Buffer 数据时是按照屏幕坐标原点(左上角)开始的,而纹理坐标正好相反,所以我们需要翻转一下纹理坐标的 Y 轴,得到最终的顶点数据:
1 | VerticesCoordinates vertices[] = { |
着色器程序
为了代码整洁,我们封装一个着色器程序,包含着色器的初始化、编译和链接功能:
1 | // 初始化方法接受顶点着色器和片段着色器的代码字符串,创建着色器程序后,调用 compileShader 编译着色器,最后将着色器绑定在当前着色器程序上 |
有了着色器程序,再来看下顶点着色器和片段着色器的代码:
1 | // 顶点着色器 |
有了顶点数据和着色器程序,接下来看一下 XPGLKView 的初始化代码。
初始化
初始化方法中设置一些变量的默认值,主要还是配置 OpenGL 环境:
1 | - (void)commonInit { |
首先通过 layer 配置帧缓冲区的一些属性,随后初始化并绑定上下文,创建我们封装过的着色器程序,创建帧缓存、顶点缓存,最后将顶点着色器属性打开。
渲染
初始化之后,就可以准备渲染画面了,渲染方法触发的时机是 AVCaptureSession 的相机数据帧回调,我们在回调里切换渲染线程,调用 displayPixelBuffer 方法:
1 |
|
渲染的方法实现:
1 | - (void)displayPixelBuffer:(CVPixelBufferRef)pixelBuffer { |
适配填充模式
按目前的顶点和纹理坐标,渲染内容默认会铺满整个图层,如果相机回调的 Pixel Buffer 宽高与图层宽高不一致,会产生拉伸的现象,因此需要适配不同的填充模式,我们定义了三种填充模式:
1 | typedef NS_ENUM(NSUInteger, XPVideoFillMode) { |
每种填充模式的顶点坐标计算方式:
1 | - (void)recalculateVerticesCoordinates { |
在渲染过程中,将顶点数据更新同步到 GPU:
1 | glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(_vertices), _vertices); |
后记
相机画面的自定义渲染没有用到 OpenGL 的高级功能,主要是对 OpenGL 对象、纹理、管线概念的理解,和 Core Animation、Core Video 的交互。下面贴一些文档可以用来参考:
1.OpenGL 中文手册 https://learnopengl-cn.github.io/
2.iOS 核心动画高级技巧 https://zsisme.gitbooks.io/ios-/content/index.html
4.《OpenGL ES应用开发实践 指南 iOS卷》