题记:本篇开启OpenGL渲染相关的篇章。前文介绍了视频播放的内容,但图像渲染并未详细阐述,本章会对iOS端OpenGL渲染做出补充说明,借以开启OpenGL渲染和滤镜的征程。
iOS端渲染
无论是相机还是视频,图像的呈现都需要依赖图形API。iOS平台上主要用到Metal和OpenGL ES,尽管OpenGL ES在iOS13+会提示不再支持(仍可以使用),但是因其具有很好的跨平台性,还是成为多数音视频相关SDK的首选。本章会简要介绍OpenGL ES在iOS的使用流程,后续该专栏会逐步展开OpenGL和滤镜的更多细节。
在iOS使用OpenGL ES进行渲染时,两种常见的视图容器:
CAEAGLLayer
是Core Animation提供的图层- 需要手动管理帧缓冲区(Framebuffer)、渲染缓冲区(Renderbuffer)和上下文(Context)
GLKView/GLKViewController
是GLKit框架提供的高级封装- 封装了
CAEAGLLayer
的底层细节 - 自动管理缓冲区和渲染循环
- 封装了
基于此要理解OpenGL ES的流程还是建议从CAEAGLLayer
开始
渲染流程
-
自定义View
重写layerClass
方法,返回CAEAGLLayer
或子类: + (Class)layerClass { return [CAEAGLLayer class]; } @end -
配置CAEAGLLayer
设置图层的属性(如不透明、像素格式):- (void)setupLayer { CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer; eaglLayer.opaque = YES; eaglLayer.drawableProperties = @{ // 帧缓冲区内容是否在呈现后不再保留,下一帧必须完全重新绘制 kEAGLDrawablePropertyRetainedBacking : @NO, // 样式RGBA kEAGLDrawablePropertyColorFormat : kEAGLColorFormatRGBA8 }; }
-
初始化OpenGL ES上下文
创建OpenGL ES上下文:- (void)setupContext { EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; [EAGLContext setCurrentContext:context]; self.context = context; }
-
创建帧缓冲区和渲染缓冲区
配置缓冲区:- (void)setupBuffers { // 创建帧缓冲区 glGenFramebuffers(1, &_framebuffer); glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer); // 创建颜色渲染缓冲区 glGenRenderbuffers(1, &_colorRenderbuffer); glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer); // 将渲染缓冲区绑定到图层 [self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer]; // 附加颜色缓冲区到帧缓冲区 glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorRenderbuffer); // 检查帧缓冲区状态 if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { NSLog(@"配置失败!"); } }
-
渲染
实现渲染逻辑并呈现到屏幕presentRenderbuffer
:- (void)render { glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer); glViewport(0, 0, self.bounds.size.width * self.contentScaleFactor, self.bounds.size.height * self.contentScaleFactor); // 自定义渲染代码(例如清屏) glClearColor(1.0, 0.0, 0.0, 1.0); glClear(GL_COLOR_BUFFER_BIT); // 使用编译的shader渲染 xxx // 提交渲染结果 glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer); [self.context presentRenderbuffer:GL_RENDERBUFFER]; }
-
清空缓存区
使用完毕后,需要清空缓冲区:
- (void)deletBuffer {
if (_colorRenderBuffer != 0) {
glDeleteRenderbuffers(1, &_colorRenderBuffer);
_colorRenderBuffer = 0;
}
if (_colorFrameBuffer != 0) {
glDeleteFramebuffers(1, &_colorFrameBuffer);
_colorFrameBuffer = 0;
}
}
FBO(Framebuffer Object)简介
FrameBuffer帧(屏幕)缓冲,管理渲染输出的存储与处理,允许将图形渲染到离屏的缓冲区中。FrameBuffer本身并不存储数据,而是保存附件的指针。
- 附件有3种类型(深度-模板组合可以组合)
- 颜色附件
- 存储渲染结果
- N个颜色附件,一般不少于4个,GL_COLOR_ATTACHMENT0...
- 深度附件
- 深度测试,确定像素遮挡关系,GL_DEPTH_ATTAHMENT
- 模板附件
- 模板测试(如轮廓绘制、区域遮罩),GL_STENCIL_ATTACHMENT
- 颜色附件
- 附件的两种形式
- 纹理(Texture)
- 可通过采样器在着色器中读取,如串联滤镜
- 绑定命令
glFramebufferTexture2D
- 渲染缓冲(Renderbuffer)
- 用于渲染输出
- 绑定命令
glFramebufferRenderbuffer
- 纹理(Texture)
常见场景示例:
-
渲染展示,上述例子
// 将渲染缓冲区绑定到图层 [self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer]; // 附加颜色缓冲区到帧缓冲区 glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorRenderbuffer);
-
渲染到纹理(滤镜链场景)
glBindTexture(GL_TEXTURE_2D, texture); glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, 800, 600, 0, format, type, 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
-
多渲染目标,同时输出颜色、法线、位置等信息到多个颜色附件
// 绑定多个颜色附件 GLenum attachments[] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1}; glDrawBuffers(2, attachments); // 片段着色器中声明多个输出 layout(location = 0) out vec4 outColor; layout(location = 1) out vec3 outNormal;
-
多渲染目标,深度附件生成深度贴图
// 绑定深度纹理作为附件 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTexture, 0); // 禁用颜色写入(仅需深度) glDrawBuffer(GL_NONE); // 设置读取源,影响glReadPixels glReadBuffer(GL_NONE);
Shader使用简介
FBO需要使用着色器填充,OpenGL支持固定着色器和可编程着色器,这里会简介一下可编程着色器的使用流程。
-
编写GLSL代码,一般需要顶点和片元着色器
// 顶点着色器 out vec3 vColor; void main() { vColor = vec3(1.0, 0.0, 0.0); // 红色 } // 片段着色器 in vec3 vColor; void main() { FragColor = vec4(vColor, 1.0); }
-
编译与链接着色器
// 1. 创建Shader对象 GLuint vertexShader = glCreateShader(GL\_VERTEX\_SHADER); GLuint fragmentShader = glCreateShader(GL\_FRAGMENT\_SHADER); // 2. 加载GLSL代码并编译 const char* vertCode = "..."; // 读取顶点着色器代码 const char* fragCode = "..."; // 读取片段着色器代码 glShaderSource(vertexShader, 1, &vertCode, NULL); glShaderSource(fragmentShader, 1, &fragCode, NULL); glCompileShader(vertexShader); glCompileShader(fragmentShader); // 3. 检查编译错误 GLint success; glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); if (!success) { char infoLog[512]; glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); std::cerr << "顶点着色器编译失败: " << infoLog << std::endl; } // 4. 创建Shader程序并链接 GLuint shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); // 5. 检查链接错误 glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success); if (!success) { char infoLog[512]; glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog); std::cerr << "Shader程序链接失败: " << infoLog << std::endl; } // 6. 删除临时Shader对象 glDeleteShader(vertexShader); glDeleteShader(fragmentShader);
-
使用Shader程序
// 激活Shader glUseProgram(shaderProgram); // 传递Uniform变量(如变换矩阵) GLint modelLocation = glGetUniformLocation(shaderProgram, "model"); glUniformMatrix4fv(modelLocation, 1, GL_FALSE, ptr); // 绑定纹理 glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, textureID); glUniform1i(glGetUniformLocation(shaderProgram, "uTexture"), 0); // 绘制物体 xxx顶点数据处理 glDrawArrays(GL_TRIANGLES, 0, 6);
视频图像展示
之前的数据播放简要介绍了视频图像的处理过程,这里介绍一下iOS平台上的处理。
-
平台层窗口
平台层展示显示窗口及工具栏,上一节中介绍的CAEAGLLayer即为展示图像的窗口,需要根据视频图像的width和height调整宽高比。
-
回调设置
解码封装层通过回调的方式,通知平台层更新
-
基本信息
解封装的基本信息、时长、视频宽高等。
-
更新进度&状态
播放的进度更新,暂停播放状态,播完等,更新工具栏状态。
-
播放数据
视频需要展示的数据,AVFrame结构,包括数据格式,宽高,data指针等。平台层根据具体格式,创建不同处理程序,编写不同的shader。如下文的
YUV420P
。Demo通过在setRefreshFunc
设置了refreshVideo
可追踪流程参考。注意此处的在播放线程,不用在主线程更新,另外回调函数是全局函数,释放时需要先处理核心层保证回调不被调用,再释放Layer。
YUV420P
对于AV_PIX_FMT_YUV420P
格式的数据,前文中介绍过了,数据主要存放在AVFrame->data
中。
data[0]
存储 Y 分量(亮度)data[1]
存储 U 分量(色度)data[2]
存储 V 分量(色度)
Shader
在数据播放已经给出,对于其中的samplerY
、samplerU
、samplerV
设置如下,(UV分量宽高是Y分量宽高的1/2)参考编码原理:
// 获取fragment shader中uniform变量的引用,
us2_sampler[0] = glGetUniformLocation(**self**.program, "samplerY");
us2_sampler[1] = glGetUniformLocation(**self**.program, "samplerU");
us2_sampler[2] = glGetUniformLocation(**self**.program, "samplerV");
// 创建纹理,分别为samplerY的纹理单元设置为0,samplerU为1, samplerV为2.
glGenTextures(3, plane_textures);//创建texture对象
[self createTex:yData width:width height:height index:0 texture:plane_textures[0]];
glUniform1i(us2_sampler[0], 0);
[self createTex:uData width:width / 2 height:height / 2 index:1 texture:plane_textures[1]];
glUniform1i(us2_sampler[1], 1);
[self createTex:vData width:width / 2 height:height / 2 index:2 texture:plane_textures[2]];
glUniform1i(us2_sampler[2], 2);
// 根据数据创建纹理,并且绑定index的纹理单元。,
- (void)createTex:(char *)data width:(int)width height:(int)height index:(int)index texture:(GLuint)texture {
glActiveTexture(GL_TEXTURE0 + index);//设置当前活动的纹理单元
glBindTexture(GL_TEXTURE_2D, texture);//texture绑定
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, data);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}
YUV转RGB有两种类型,具体根据 AVFrame->colorspace
- ITU-R BT.601 标准(标清电视)
R = Y + 1.402 * (V - 128)
G = Y - 0.34414 * (U - 128) - 0.71414 * (V - 128)
B = Y + 1.772 * (U - 128)
- ITU-R BT.709 标准(高清电视)
R = Y + 1.5748 * (V - 128)
G = Y - 0.1873 * (U - 128) - 0.4681 * (V - 128)
B = Y + 1.8556 * (U - 128)