OpenGLES2.0-使用FBO进行离屏渲染-FBO绘制纹理到页面实现的最简方式

188 阅读3分钟

前言

FBO(Frame Buffer Object)在 OpenGL ES 中是一个用于自定义渲染目标的对象。简单来说,它允许开发者创建一个不在屏幕上显示的缓冲区,来存储渲染的结果。这意味着可以灵活地控制渲染的输出位置和方式,而不仅仅局限于将图形直接渲染到屏幕的默认帧缓冲区。

FBO 的主要特点和作用包括:

  • 离屏渲染:能够在内存中进行渲染,而不立即显示在屏幕上。这对于一些需要预计算或中间处理的图形操作非常有用。
  • 多渲染目标:可以同时将渲染结果输出到多个缓冲区,例如同时生成颜色、深度和模板信息。
  • 提高效率:避免了不必要的屏幕重绘,节省了系统资源和提高了渲染性能。

今天我们尝试使用fbo渲染一张纹理图片,然后将此图片再渲染到屏幕上。

1.创建render

代码实现基于GlSurfaceView,为它创建一个Render对象,核心逻辑都在render中。

public class MyFboRender implements GLSurfaceView.Renderer {
    public static int sScreenWidth;
    public static int sScreenHeight;
    private final Context mContext;
    private MyFboShape myFboShape;

    public MyFboRender(@NonNull Context context) {
        this.mContext = context;
    }
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1);
        myFboShape = new MyFboShape(mContext);
    }
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        sScreenWidth = width;
        sScreenHeight = height;
        GLES20.glViewport(0, 0, width, height);
    }
    @Override
    public void onDrawFrame(GL10 gl) {
        myFboShape.draw();
    }
}

2.创建shape对象,用于封装所有opengl操作

在shape的构造方法中创建直接内存用于存储顶点数据和纹理坐标。

public MyFboShape(Context mContext) {
    this.mContext = mContext;
    mSqureBuffer = ByteBuffer.allocateDirect(bgVertex.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
    mSqureBuffer.put(bgVertex);
    mSqureBuffer.position(0);
    mSqureBufferfbo = ByteBuffer.allocateDirect(fboVertex.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
    mSqureBufferfbo.put(fboVertex);
    mSqureBufferfbo.position(0);
    mLoadedTextureId = initTexture(R.mipmap.bg);
}

同时会加载一张纹理图片,并返回其句柄mLoadedTextureId备用。

private int initTexture(int res) {
    Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), res);
    int[] textures = new int[1];
    GLES20.glGenTextures(1, textures, 0);
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_MIRRORED_REPEAT);
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_MIRRORED_REPEAT);
    GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
    bitmap.recycle();
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
    return textures[0];
}

3.draw开始绘制

创建帧缓冲区

int[] frameBuffers = new int[1];
GLES20.glGenFramebuffers(1, frameBuffers, 0);

创建纹理对象并设置格式

int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0);
int colorTextureId = textures[0];
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, colorTextureId);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_MIRRORED_REPEAT);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_MIRRORED_REPEAT);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, MyFboRender.sScreenWidth, MyFboRender.sScreenHeight, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);

为帧缓冲区设置附加纹理

int frameBufferId = frameBuffers[0];
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBufferId);
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, colorTextureId, 0);
if (GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER) != GLES20.GL_FRAMEBUFFER_COMPLETE) {
    Log.e("rzm", "glFramebufferTexture2D error");
}

创建顶点着色器和片段着色器

int frameBufferVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
int frameBufferFagmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);

加载shader的工具方法

private int loadShader(int type, String shaderCode) {
    int shader = GLES20.glCreateShader(type);
    GLES20.glShaderSource(shader, shaderCode);
    GLES20.glCompileShader(shader);
    int[] params = new int[1];
    GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, params, 0);
    if (params[0] == 0) {
        Log.e("rzm", GLES20.glGetShaderInfoLog(shader));
        GLES20.glDeleteShader(shader);
        shader = -1;
    }
    return shader;
}

创建program并将shader附加到program

mFrameBufferProgram = GLES20.glCreateProgram();
GLES20.glAttachShader(mFrameBufferProgram, frameBufferVertexShader);
GLES20.glAttachShader(mFrameBufferProgram, frameBufferFagmentShader);
GLES20.glLinkProgram(mFrameBufferProgram);
int[] params = new int[1];
GLES20.glGetProgramiv(mFrameBufferProgram, GLES20.GL_LINK_STATUS, params, 0);
if (params[0] != 0) {
    Log.e("rzm", GLES20.glGetProgramInfoLog(mFrameBufferProgram));
}
GLES20.glUseProgram(mFrameBufferProgram);

着色器参数配置

int aPositionLocation = GLES20.glGetAttribLocation(mFrameBufferProgram, "aPosition");
int aTextureCoordPosition = GLES20.glGetAttribLocation(mFrameBufferProgram, "aTextureCoord");
int uTextureLocation = GLES20.glGetUniformLocation(mFrameBufferProgram, "uTexture");
mSqureBufferfbo.position(0);
GLES20.glVertexAttribPointer(aPositionLocation, 2, GLES20.GL_FLOAT, false, 4 * (2 + 2), mSqureBufferfbo);
mSqureBufferfbo.position(2);
GLES20.glVertexAttribPointer(aTextureCoordPosition, 2, GLES20.GL_FLOAT, false, 4 * (2 + 2), mSqureBufferfbo);

GLES20.glEnableVertexAttribArray(aPositionLocation);
GLES20.glEnableVertexAttribArray(aTextureCoordPosition);

激活纹理并将加载了一张图片的纹理对象传递到shader

GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mLoadedTextureId);
GLES20.glUniform1i(uTextureLocation, 0);

绘制到fbo

GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);

注意,从fbo绑定,到fbo解绑中间的一些列操作都是针对帧缓冲区的。

接下来开始将fbo中的图片会知道屏幕

创建另一个program用于绘制

int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, windowVertexShaderCode);
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, windowFragmentShaderCode);
mWindowProgram = GLES20.glCreateProgram();
GLES20.glAttachShader(mWindowProgram, vertexShader);
GLES20.glAttachShader(mWindowProgram, fragmentShader);
GLES20.glLinkProgram(mWindowProgram);
int[] params2 = new int[1];
GLES20.glGetProgramiv(mFrameBufferProgram, GLES20.GL_LINK_STATUS, params2, 0);
if (params2[0] != 0) {
    Log.e("rzm", GLES20.glGetProgramInfoLog(mWindowProgram));
}
GLES20.glUseProgram(mWindowProgram);

为着色器配置参数

int positionHandle = GLES20.glGetAttribLocation(mWindowProgram, "aPosition");
int textureCoordHandle = GLES20.glGetAttribLocation(mWindowProgram, "aTextureCoord");
int textureHandle = GLES20.glGetUniformLocation(mWindowProgram, "uTexture");

mSqureBuffer.position(0);
GLES20.glVertexAttribPointer(positionHandle, 2, GLES20.GL_FLOAT, false, 4 * (2 + 2), mSqureBuffer);
mSqureBuffer.position(2);
GLES20.glVertexAttribPointer(textureCoordHandle, 2, GLES20.GL_FLOAT, false, 4 * (2 + 2), mSqureBuffer);

GLES20.glEnableVertexAttribArray(positionHandle);
GLES20.glEnableVertexAttribArray(textureCoordHandle);

激活纹理并绘制到屏幕

GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, colorTextureId);
GLES20.glUniform1i(textureHandle, 0);

GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

总结

  1. 整个过程共创建了两个纹理对象,第一个是加载了一张图片的mLoadedTextureId,第二个是创建后设置给fbo的colorTextureId。
  2. 这两个纹理对象是从fbo绘制到屏幕的关键。
  3. 第一步我们先绑定fbo,然后将加载了图片的mLoadedTextureId绘制到fbo中。
  4. 第二部通过fbo附加的colorTextureId将图片绘制到屏幕上。
  5. 所以图像其实就是在两张纹理之间的传递。