OpenGL帧缓冲与离屏渲染

本篇文章主要是介绍离屏渲染与OpenGL的帧缓冲, 当有些图像不能直接绘制于屏幕上,需要在别的地方做额外的处理预合成,就使用到离屏渲染。所以其实离屏渲染的概念很简单,就是在某块内存中处理图像,经过层层处理,最终效果显示到屏幕上而已。而OpenGL提供了帧缓冲对象 (Frame Buffer Object, FBO),借助FBO很容易实现离屏渲染。

离屏渲染的背景

为什么要使用OpenGL来渲染绘制摄像头捕获图像?

Android能够让我们通过非常简单的API就可以完成摄像头的预览。但是对于预览的结果我们并不能控制,即如果需要完成类似美颜相机的功能,我们就需要将处理后的摄像头数据绘制在手机界面中。因此我们需要先启动摄像头捕获,再处理数据,最后显示。当前我们先来完成捕获与显示,最后再加入效果处理。

默认情况下,我们在 GLSurfaceView 中绘制的结果是显示到屏幕上,然而实际中有很多情况并不需要渲染到屏幕上。如果要拿到摄像头的原始数据并展示在屏幕上,那么必须使用SamplerExternalOES这个采样器。如果要经过美颜,比如大眼、瘦脸等操作,必须使用Sampler2D这个采样器。

如果不开启美颜,那么需要使用SamplerExternalOES这个采样器。如果开启了美颜,那么需要sampler2D这个采样器。也就意味着一会儿需要SamplerExternalOES,一会儿需要sampler2D。因为对于OpenGL来说摄像头数据是一个外部的纹理。

帧缓冲 —— FBO

FBO (Frame Buffer Object) 帧缓冲对象,用于离屏渲染。

通过使用FBO,OpenGL 可以重定向渲染输出,让它输出到FBO而不是GLSurfaceView窗口的Framebuffer。

1、CameraFilter使用samplerExernalOES输出FBO;

2、后续处理使用sampler2D输出FBO;

3、ScreenFilter使用sampler2D输出显示窗口;

代码示例程序

下面是一个创建FBO的示例:

 1// 1、创建FBO + FBO中的纹理
 2frameBuffer = new int[1];
 3frameTextures = new int[1];
 4GLES20.glGenFramebuffers(1, frameBuffer, 0);
 5OpenGLUtils.glGenTextures(frameTextures);
 6
 7// 2、FBO与纹理关联
 8GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, frameTextures[0]);
 9GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height,
10                    0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
11
12// 纹理关联FBO
13GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffer[0]);  // 绑定FBO
14GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, frameTextures[0], 0);
15
16// 3、解除绑定,记住:OpenGL是个状态机,面向过程编程
17GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
18GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
19
20
21
22// 生成支持缩放的纹理
23public static void glGenTextures(int[] textures) {
24    GLES20.glGenTextures(textures.length, textures, 0);
25    for (int i = 0; i < textures.length; i++) {
26        //与摄像头不同,摄像头是外部纹理 external oes
27        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[i]);
28
29        // 必须:设置纹理过滤参数设置,设置纹理缩放过滤
30        // GL_NEAREST: 使用纹理中坐标最接近的一个像素的颜色作为需要绘制的像素颜色
31        // GL_LINEAR:  使用纹理中坐标最接近的若干个颜色,通过加权平均算法得到需要绘制的像素颜色
32        // 后者速度较慢,但视觉效果好
33        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
34                               GLES20.GL_LINEAR);//放大过滤
35        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
36                               GLES20.GL_LINEAR);//缩小过滤
37
38        // 可选:设置纹理环绕方向
39        //纹理坐标的范围是0-1。超出这一范围的坐标将被OpenGL根据GL_TEXTURE_WRAP参数的值进行处理
40        //GL_TEXTURE_WRAP_S, GL_TEXTURE_WRAP_T 分别为x,y方向。
41        //GL_REPEAT:平铺
42        //GL_MIRRORED_REPEAT: 纹理坐标是奇数时使用镜像平铺
43        //GL_CLAMP_TO_EDGE: 坐标超出部分被截取成0、1,边缘拉伸
44        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,
45                               GLES20.GL_CLAMP_TO_EDGE);
46        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,
47                               GLES20.GL_CLAMP_TO_EDGE);
48        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0);
49    }
50}

根据上面的过程图可以分成以下几个部分:

AbstractFilter:初始化坐标,加载着色器程序,onDraw绘制

AbstractFboFilter:创建FBO,将FBO与纹理关联,onDraw时绑定FBO,再解除绑定

ScreenFilter extends AbstractFilter:负责从FBO中取数据,并且绘制到屏幕上。着色器程序如下:

base_vert.vart

 1attribute vec4 vPosition; // 变量float[4]一个顶点 Java传过来的
 2
 3attribute vec2 vCoord;  // 纹理坐标
 4
 5varying vec2 aCoord;
 6
 7void main(){
 8    // 内置变量:把坐标点赋值给gl_position
 9    gl_Position = vPosition;
10    // 在CameraFilter中已经处理过了,所以现在是直接从FBO中拿,无需旋转校准矩阵了
11    aCoord = vCoord;
12}

base_frag.frag

1precision mediump float; // 数据精度
2varying vec2 aCoord;
3// 绘制到屏幕,使用simpler2D
4uniform sampler2D vTexture;
5
6void main(){
7    vec4 rgba = texture2D(vTexture, aCoord);  //RGBA
8    gl_FragColor = vec4(rgba.r, rgba.g, rgba.b, rgba.a);
9}

CameraFilter extends AbstractFboFilter,负责把摄像头纹理绘制到FBO,并且时绘制矩阵校正后的图像,其对应的着色器程序是:

camera_vert.vert (对于OpenGL来说,摄像头数据是外部纹理,所以用ExternalOES采样器)

 1#extension GL_OES_EGL_image_external : require
 2// 摄像头数据比较特殊的一个地方
 3precision mediump float; // 数据精度
 4varying vec2 aCoord;
 5
 6uniform samplerExternalOES  vTexture;  // samplerExternalOES: 图片,采样器
 7
 8void main(){
 9    //  texture2D: vTexture采样器,采样aCoord 这个像素点的RGBA值
10    vec4 rgba = texture2D(vTexture,aCoord);
11    gl_FragColor = vec4(rgba.r, rgba.g, rgba.b, rgba.a);
12}

camera_frag.frag

 1attribute vec4 vPosition;
 2
 3attribute vec2 vCoord;  // 纹理坐标
 4
 5varying vec2 aCoord;
 6
 7uniform mat4 vMatrix;
 8
 9void main(){
10    // 内置变量: 把坐标点赋值给gl_position
11    gl_Position = vPosition;
12    aCoord = (vMatrix * vec4(vCoord, 1.0, 1.0)).xy;
13}

整个工程可以参考示例代码:https://github.com/zouchanglin/render-camera

后续计划

只要掌握了如何通过OpenGL提供的帧缓冲对象,那么其实对于图像的加工将变得非常容易,无论是瘦脸、美白、大眼等美颜效果,还是AI贴纸、趣味贴图等等特效… 无非就是通过层层FBO离屏渲染,渲染结束后再渲染到屏幕展示出来即可,操作之便捷可以想象。

对于抖音这样的程序来说,里面就用到了各种各样的特效处理,而且还顺带实现了视频录制的功能,后面会逐步实现一个类似于抖音这样带美颜或者人脸贴纸的视频录制程序。