OpenGL ES 3.0 实现多通滤波(Multi Pass)

本文档描述了OpenGL ES 3.0 Multiple Passes(Multi-Pass)的使用原因、编程思路及具体实现的代码分析。

1、概述

多通滤波(Multiple Passes或Multi Pass),印象中模电课有此内容,可惜忘光了。最近一次遇到是用Video Toolbox作硬编码,在非实时场合,可考虑用多通滤波(Multi Pass)实现体积与质量的均衡,具体是,一通实现视频的分析,得到全局视频信息,并记录到日志中。在二通处理过程中,可根据全局信息进行码率的调整,令动态场景有更高的数据量,静态场景使用更少数据,从而在计算复杂度、体积与质量之间得到一个最优解。有关Multi Pass,我之前整理了一个文档Video Toolbox Multi-pass Encoding。

对于OpenGL ES着色器中使用多通滤波,是因为一些图像处理算法需要访问全局像素信息,而默认情况下,程序直接输出颜色信息到窗口系统提供的帧缓冲区,在此过程,片段着色器无法获取其邻近片段或像素信息、只能访问指定纹理坐标(即是,从顶点着色器中传递过来且经过管线固定功能处理的坐标值,固定功能包括图元组装、光栅化)的纹理数据,从而无法满足算法要求。因此,为实现多通滤波,不得不渲染到离线帧缓冲区,在OpenGL ES 2.0时用pbuffer实现,OpenGL ES 3.0提供了帧缓冲区对象FBO(Framebuffer Object),据OpenGL ES 3.0 Programming Guide描述,FBO比pbuffer性能更高。通过FBO + texture或FBO + Renderbuffer Object可实现渲染到离线帧缓冲区的需求。

2、实现思路

多通滤波的一般实现步骤如下所述:

  1. 创建帧缓冲区对象offline_framebuffer
  2. 创建纹理offline_texture。通常情况下,一个纹理保存一次滤波的结果。
  3. 链接纹理offline_texture到帧缓冲区对象offline_framebuffe
  4. 绑定帧缓冲区offline_framebuffe
  5. 绘制场景到帧缓冲区offline_framebuffe
  6. 执行图像处理算法,对于多通滤波,则需重复步骤2、3、4、5、6。
  7. 绑定默认帧缓冲区,即使用窗口系统提供的帧缓冲区对象
  8. 绘制一个矩形,将已处理的纹理渲染到屏幕。

下面展示两个示例,(1)基于CAEAGLLayer通过Sobel Operator实现边缘检测(Edge Detection),(2)基于GLKViewController实现快速近似抗锯齿FAXX(Fast Approximate Anti-Aliasing)。

3、实现代码分析

继承UIView的实现中,GL_FRAMEBUFFER_BINDING、GL_RENDERBUFFER_BINDING调用glGetIntegerv()得不到默认帧缓冲区和渲染缓冲区。使用GLKViewController则可以。那么,派生UIView的方式,需再创建framebuffer、renderbuffer存储多通滤波的中间结果。同时,也因为不像GLKView提供了默认帧缓冲区,派生UIView的方式在处理二通滤波时,不需要创建额外的帧缓冲区来保存中间结果,具体原因可参考我另一个文档iOS OpenGL ES 3.0 数据可视化 4:纹理映射实现2维图像与视频渲染简介。

3.1、保存默认帧缓冲区

GLint defaultFBO;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &defaultFBO);
GLint defaultRBO;
glGetIntegerv(GL_RENDERBUFFER_BINDING, &defaultRBO);

3.2、创建帧缓冲区

GLuint renderbuffer;
glGenRenderbuffers(1, &renderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
[context renderbufferStorage:GL_RENDERBUFFER fromDrawable:glLayer];

GLuint framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);

这里创建的帧缓冲区用于最终的渲染,即步骤8. 绘制一个矩形,将已处理的纹理渲染到屏幕。

3.3、获取窗口系统分析的渲染缓冲区大小

获取从CAEAGLLayer中分配的渲染缓冲区大小。这里并不使用MIPMAP技术,只需获取大小信息,为后续创建离线纹理作准备。

GLint defaultRenderbufferWidth, defaultRenderbufferHeight;
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &defaultRenderbufferWidth);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &defaultRenderbufferHeight);
printf("defalut renderbuffer size = (%d, %d)\n", defaultRenderbufferWidth, defaultRenderbufferHeight);

3.4、创建离线纹理

生成离线纹理并在后面绑定到framebuffer,一通滤波(Pass 1)将纹理渲染到这个离线纹理,之后的图像处理算法操作此纹理的数据。

GLuint offline_texture;
glGenTextures(1, &offline_texture);
glBindTexture(GL_TEXTURE_2D, offline_texture);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D,
             0/* level */,
             GL_RGBA/* internalformat */,
             defaultRenderbufferWidth, defaultRenderbufferHeight,
             0/* border */,
             GL_RGBA,
             GL_UNSIGNED_BYTE,
             0);
glBindTexture(GL_TEXTURE_2D, 0);

值得注意的,glTexImage2D的最后一个参数是0,不像正常情况下传递的图像数据,这是因为后续操作会将数据写入此纹理,此时根据图像数据创建的纹理内容会被修改,所以没必要提供数据源。

3.5、创建离线帧缓冲区

GLuint framebuffer_multipass;
glGenFramebuffers(1, &framebuffer_multipass);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_multipass);
// attach the texture to FBO color attachment point
glFramebufferTexture2D(GL_FRAMEBUFFER,        // 1. fbo target: GL_FRAMEBUFFER
                       GL_COLOR_ATTACHMENT0,  // 2. Color attachment point
                       GL_TEXTURE_2D,         // 3. tex target: GL_TEXTURE_2D
                       offline_texture,       // 4. color texture ID
                       0);                    // 5. mipmap level: 0(base)

glFramebufferTexture2D用于将一个2D纹理的某个mip级别或者立方图面连接到帧缓冲区附着点,它可用来将纹理作为颜色、缓冲区或者模版附着点连接。
参数说明:

  • target:必须设置为GL_READ_FRAMEBUFFER、GL_DRAW_FRAMEBUFFER或GL_FRAMEBUFFER
  • attachment:必须为如下枚举值之一:
    • GL_COLOR_ATTACHMENTx
    • GL_DEPTH_ATTACHMENT
    • GL_STENCIL_ATTACHMENT
    • GL_DEPTH_STENCIL_ATTACHMENT
  • textarget:指定纹理目标
  • texture:指定纹理对象
  • level:指定纹理图像的mip级别

3.6、检查帧缓冲区状态

// check FBO status
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if(status != GL_FRAMEBUFFER_COMPLETE){
    printf("Framebuffer creation fails: %d", status);
}

3.7、一通滤波

一通只是简单地把原始图像渲染到离线帧缓冲区中。

NSString *offline_vertexShaderString = @"#version 300 es  \n  "
"layout(location = 0) in vec4 a_Position;\n"
"layout(location = 1) in vec2 a_TexCoord;\n"
"uniform mat4 projection_matrix;\n"
"out vec2 v_TexCoord;\n"
"void main() {\n"
"mat4 scale = mat4(0.5, 0.0, 0.0, 0.0,"
"                  0.0, 0.5, 0.0, 0.0,"
"                  0.0, 0.0, 1.0, 0.0,"
"                  0.0, 0.0, 0.0, 1.0);"
"gl_Position = a_Position * projection_matrix;\n"
"v_TexCoord = a_TexCoord;\n}";

NSString *offline_fragmentShaderString = @"#version 300 es\n"
"precision mediump float;\n"
"uniform sampler2D u_sampler;\n"
"in vec2 v_TexCoord;\n"
"out vec4 o_Color;\n"
"void main() {\n"
"o_Color = texture(u_sampler, v_TexCoord);\n}";

GLuint offline_program = glCreateProgram();
GLint offline_vertexShader = [self compileShaderWithString:offline_vertexShaderString withType:GL_VERTEX_SHADER];
GLint offline_fragmentShader = [self compileShaderWithString:offline_fragmentShaderString withType:GL_FRAGMENT_SHADER];

glAttachShader(offline_program, offline_vertexShader);
glAttachShader(offline_program, offline_fragmentShader);
glLinkProgram(offline_program);
GLint linkStatus;
glGetProgramiv(offline_program, GL_LINK_STATUS, &linkStatus);
if (linkStatus == GL_FALSE) {
    GLint length;
    glGetProgramiv(offline_program, GL_INFO_LOG_LENGTH, &length);
    if (length > 0) {
        GLchar *infolog = malloc(sizeof(GLchar) * length);
        glGetProgramInfoLog(offline_program, length, NULL, infolog);
        fprintf(stderr, "link error = %s", infolog);
        if (infolog) {
            free(infolog);
        }
    }
}
glUseProgram(offline_program);
glValidateProgram(offline_program);

glClearColor(1.0, 1.0, 1.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
glViewport(0, 0, defaultRenderbufferWidth, defaultRenderbufferHeight);

glActiveTexture(GL_TEXTURE0);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

NSString *path = [[NSBundle mainBundle] pathForResource:@"826HS604CL29_1600x900.jpg" ofType:nil];
UIImage *image = [UIImage imageWithContentsOfFile:path];
CGImageRef imageRef = [image CGImage];
float width = CGImageGetWidth(imageRef);
float height = CGImageGetHeight(imageRef);
CGDataProviderRef  provider = CGImageGetDataProvider(imageRef);
CFDataRef textureDataRef = CGDataProviderCopyData(provider);
const unsigned char *pixels = CFDataGetBytePtr(textureDataRef);

GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

int sampler = glGetUniformLocation(offline_program, "u_sampler");
glUniform1i(sampler, 0);

int offline_projection_matrix_index = glGetUniformLocation(offline_program, "projection_matrix");
CGFloat ratio = defaultRenderbufferWidth * 1.0 / defaultRenderbufferHeight;
GLKMatrix4 orth = GLKMatrix4MakeOrtho(-ratio, ratio, - 1, 1, -1, 1);
glUniformMatrix4fv(offline_projection_matrix_index, 1, GL_FALSE, orth.m);


/*
 顶点
 0 --- 3
 |
 1 --- 2
 纹理
 0 --- 3
 |
 1 --- 2
 */
GLfloat vertexs[] = {
    -1.0f,  0.5f, 0.0f,  // Position 0
    0.0f,  0.0f,        // TexCoord 0
    -1.0f, -0.5f, 0.0f,  // Position 1
    0.0f,  1.0f,        // TexCoord 1
    1.0f, -0.5f, 0.0f,  // Position 2
    1.0f,  1.0f,        // TexCoord 2
    1.0f,  0.5f, 0.0f,  // Position 3
    1.0f,  0.0f         // TexCoord 3
};

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), vertexs);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), &vertexs[3]);

glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);

glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

3.8、二通滤波(图像处理)

二通滤波则在渲染图像到窗口系统时进行图像处理。

进行二通滤波前需要绑定默认帧缓冲区,因为新内容将渲染到窗口系统上。

// bind back to default framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, offline_texture);

绘制代码。

NSString *fragmentShaderPath = [[NSBundle mainBundle] pathForResource:@"shader.frag" ofType:nil];
NSString *fragmentShaderString = [NSString stringWithContentsOfFile:fragmentShaderPath encoding:NSUTF8StringEncoding error:nil];

GLint vertexShader = [self compileShaderWithString:vertexShaderString withType:GL_VERTEX_SHADER];
GLint fragmentShader = [self compileShaderWithString:fragmentShaderString withType:GL_FRAGMENT_SHADER];

GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
//    GLint linkStatus;
glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
if (linkStatus == GL_FALSE) {
    GLint length;
    glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
    if (length > 0) {
        GLchar *infolog = malloc(sizeof(GLchar) * length);
        glGetProgramInfoLog(program, length, NULL, infolog);
        fprintf(stderr, "link error = %s", infolog);
        if (infolog) {
            free(infolog);
        }
    }
}
glValidateProgram(program);
glUseProgram(program);

glClearColor(1.0, 1.0, 1.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
CGRect frame = [UIScreen mainScreen].bounds;
glViewport(0, 0, frame.size.width * glLayer.contentsScale, frame.size.height * glLayer.contentsScale);

glActiveTexture(GL_TEXTURE0);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

int projection_matrix_index = glGetUniformLocation(program, "projection_matrix");
glUniformMatrix4fv(projection_matrix_index, 1, GL_FALSE, orth.m);

int u_screen_width_index = glGetUniformLocation(program, "u_screen_width");
glUniform1i(u_screen_width_index, frame.size.width * 2);
int u_screen_height_index = glGetUniformLocation(program, "u_screen_height");
glUniform1i(u_screen_height_index, frame.size.height * 2);

glClear(GL_COLOR_BUFFER_BIT);

GLfloat vertexs[] = {
    -1.0f,  0.5f, 0.0f,  // Position 0
    0.0f,  0.0f,        // TexCoord 0
    -1.0f, -0.5f, 0.0f,  // Position 1
    0.0f,  1.0f,        // TexCoord 1
    1.0f, -0.5f, 0.0f,  // Position 2
    1.0f,  1.0f,        // TexCoord 2
    1.0f,  0.5f, 0.0f,  // Position 3
    1.0f,  0.0f         // TexCoord 3
};

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), vertexs);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), &vertexs[3]);

glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);

glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

[context presentRenderbuffer:GL_RENDERBUFFER];

在Android 7.0上,发现在glUseProgram之后执行glBindFrameBuffer会无效,不知是不是三星的ROM有问题。先绑定帧缓冲区再使用Program则正常工作。

你可能感兴趣的:(OpenGL ES 3.0 实现多通滤波(Multi Pass))