音视频学习笔记十——渲染与滤镜之iOS端图像渲染

333 阅读7分钟

题记:本篇开启OpenGL渲染相关的篇章。前文介绍了视频播放的内容,但图像渲染并未详细阐述,本章会对iOS端OpenGL渲染做出补充说明,借以开启OpenGL渲染和滤镜的征程。

demo.gif

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开始

渲染流程

  1. 自定义View
    重写layerClass方法,返回CAEAGLLayer或子类: + (Class)layerClass { return [CAEAGLLayer class]; } @end

  2. 配置CAEAGLLayer
    设置图层的属性(如不透明、像素格式):

    - (void)setupLayer {
        CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer;
        eaglLayer.opaque = YES;
        eaglLayer.drawableProperties = @{
        // 帧缓冲区内容是否在呈现后不再保留,下一帧必须完全重新绘制
        kEAGLDrawablePropertyRetainedBacking : @NO,
        // 样式RGBA
        kEAGLDrawablePropertyColorFormat : kEAGLColorFormatRGBA8
        };
    }
    
  3. 初始化OpenGL ES上下文
    创建OpenGL ES上下文:

    - (void)setupContext {
    EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
    [EAGLContext setCurrentContext:context];
    self.context = context;
    }
    
  4. 创建帧缓冲区和渲染缓冲区
    配置缓冲区:

    - (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(@"配置失败!");
        }
    }
    
  5. 渲染
    实现渲染逻辑并呈现到屏幕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];
    }
    
  6. 清空缓存区

 使用完毕后,需要清空缓冲区:
 - (void)deletBuffer {
   if (_colorRenderBuffer != 0) {
     glDeleteRenderbuffers(1, &_colorRenderBuffer);
     _colorRenderBuffer = 0;
   }
   if (_colorFrameBuffer != 0) {
     glDeleteFramebuffers(1, &_colorFrameBuffer);
     _colorFrameBuffer = 0;
   }
 }

FBO(Framebuffer Object)简介

FrameBuffer帧(屏幕)缓冲,管理渲染输出的存储与处理,允许将图形渲染到离屏的缓冲区中。FrameBuffer本身并不存储数据,而是保存附件的指针。

FBO.png
  • 附件有3种类型(深度-模板组合可以组合)
    • 颜色附件
      • 存储渲染结果
      • N个颜色附件,一般不少于4个,GL_COLOR_ATTACHMENT0...
    • 深度附件
      • 深度测试,确定像素遮挡关系,GL_DEPTH_ATTAHMENT
    • 模板附件
      • 模板测试(如轮廓绘制、区域遮罩),GL_STENCIL_ATTACHMENT
  • 附件的两种形式
    • 纹理(Texture)
      • 可通过采样器在着色器中读取,如串联滤镜
      • 绑定命令glFramebufferTexture2D
    • 渲染缓冲(Renderbuffer)
      • 用于渲染输出
      • 绑定命令glFramebufferRenderbuffer

常见场景示例:

  • 渲染展示,上述例子

    // 将渲染缓冲区绑定到图层
    [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支持固定着色器和可编程着色器,这里会简介一下可编程着色器的使用流程。

  1. 编写GLSL代码,一般需要顶点和片元着色器

    // 顶点着色器
    out vec3 vColor;
    void main() {
        vColor = vec3(1.0, 0.0, 0.0); // 红色
    }
    
    // 片段着色器
    in vec3 vColor;
    void main() {
        FragColor = vec4(vColor, 1.0);
    }
    
  2. 编译与链接着色器

    // 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);
    
  3. 使用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平台上的处理。

shader平台.jpg
  1. 平台层窗口

    平台层展示显示窗口及工具栏,上一节中介绍的CAEAGLLayer即为展示图像的窗口,需要根据视频图像的width和height调整宽高比。

    播放窗口.jpg

  2. 回调设置

解码封装层通过回调的方式,通知平台层更新

  • 基本信息

    解封装的基本信息、时长、视频宽高等。

  • 更新进度&状态

    播放的进度更新,暂停播放状态,播完等,更新工具栏状态。

  • 播放数据

    视频需要展示的数据,AVFrame结构,包括数据格式,宽高,data指针等。平台层根据具体格式,创建不同处理程序,编写不同的shader。如下文的YUV420PDemo通过在setRefreshFunc设置了refreshVideo可追踪流程参考。注意此处的在播放线程,不用在主线程更新,另外回调函数是全局函数,释放时需要先处理核心层保证回调不被调用,再释放Layer。

YUV420P

对于AV_PIX_FMT_YUV420P格式的数据,前文中介绍过了,数据主要存放在AVFrame->data中。

  • data[0] 存储 Y 分量(亮度)
  • data[1] 存储 U 分量(色度)
  • data[2] 存储 V 分量(色度)

Shader数据播放已经给出,对于其中的samplerYsamplerUsamplerV设置如下,(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)