【OpenGL ES】FBO离屏渲染
1 前言
OpenGL 默认把 framebuffer 当作渲染目的地,它由窗口系统创建并管理。应用程序也可以创建额外非可显示的 framebuffer object(FBO),以区别窗口系统提供的 framebuffer。OpenGL 应用程序可以重定向渲染目的地,让它输出到 FBO 而不是窗口系统提供的 framebuffer。
与窗口系统提供的 framebuffer 类似,FBO 包含一系列渲染目的地:颜色缓冲区(color buffer)、深度缓冲区(depth buffer)、模板缓冲区(stencil buffer),FBO 中的这些逻辑缓冲区称为附着点,颜色附着点可以有多个,深度附着点和模板附着点只有一个,FBO 具有多个颜色附加点的原因是允许在同一时间将颜色缓冲区渲染到多个目的地。FBO本身不存放数据,它只有多个附着点。
OpenGL 中有两种可附着的 framebuffer:纹理(texture)、renderbuffer。如果纹理被附着到 FBO,OpenGL 将执行“渲染到纹理”。如果 renderbuffer 被附着到 FBO,则 OpenGL 将执行“离屏渲染”。渲染到纹理的一种传统方法是先绘制到缓冲区,再使用 glCopyTexSubImage2D() 将 framebuffer 图像复制到纹理,FBO 将场景直接渲染到纹理,消除了额外的数据拷贝(从帧缓存到纹理)。
离屏渲染是指:在后台渲染数据,处理完成后再送显到前台。原理是:在一块内存中渲染数据,处理完成后再取出来送显到前台。好处有:避免花屏,提升渲染效率。
本文将使用 ImageView 显示离屏渲染后的图片(将原图片变灰)。由于 Renderer 需要一个 GLSurfaceView 调用 requestRender() 方法驱动渲染,但是本文又不将离屏渲染后的图片送显给 GLSurfaceView,因此将 GLSurfaceView 的宽高都设置为1,并且设置为透明的。如果不使用 GLSurfaceView,可以参考: EGL+FBO离屏渲染 。
读者如果对 OpenGL ES 不太熟悉,请回顾以下内容:
本文完整代码资源见→ FBO离屏渲染
项目目录如下:
2 案例
MainActivity.java
package com.zhyan8.offscreen.activity;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.widget.FrameLayout;
import android.widget.ImageView;
import androidx.appcompat.app.AppCompatActivity;
import com.zhyan8.offscreen.R;
import com.zhyan8.offscreen.model.Model;
import com.zhyan8.offscreen.opengl.MyGLSurfaceView;
import com.zhyan8.offscreen.opengl.MyRender;
public class MainActivity extends AppCompatActivity implements Model.Callback {
private FrameLayout mRootView;
private ImageView mImageView;
private MyGLSurfaceView mGlSurfaceView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRootView = findViewById(R.id.rootView);
mImageView = findViewById(R.id.imageView);
initGLView();
private void initGLView() {
mGlSurfaceView = new MyGLSurfaceView(this);
MyRender render = new MyRender(getResources());
render.setCallback(this);
mGlSurfaceView.init(render);
mGlSurfaceView.layout(mRootView);
@Override
public void onCall(final Bitmap bitmap) {
runOnUiThread(() -> {
mImageView.setImageBitmap(bitmap);
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/rootView">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"/>
</FrameLayout>
MyGLSurfaceView.java
package com.zhyan8.offscreen.opengl;
import android.content.Context;
import android.graphics.PixelFormat;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.view.WindowManager;
public class MyGLSurfaceView extends GLSurfaceView {
public MyGLSurfaceView(Context context) {
super(context);
public MyGLSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
public void init(Renderer renderer) {
setEGLContextClientVersion(3);
setEGLConfigChooser(8, 8, 8, 8, 16, 0);
getHolder().setFormat(PixelFormat.TRANSLUCENT);
setZOrderOnTop(true);
setRenderer(renderer);
public void layout(ViewGroup parent) {
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.width = 1;
params.height = 1;
parent.addView(this, params);
}
MyRender.java
package com.zhyan8.offscreen.opengl;
import android.content.res.Resources;
import android.opengl.GLES30;
import android.opengl.GLSurfaceView;
import com.zhyan8.offscreen.model.Model;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class MyRender implements GLSurfaceView.Renderer {
private Model mModel;
public MyRender(Resources resources) {
mModel = new Model(resources);
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig eglConfig) {
//设置背景颜色
GLES30.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
//启动深度测试
gl.glEnable(GLES30.GL_DEPTH_TEST);
//创建程序id
mModel.onModelCreate();
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
mModel.onModelChange(width, height);
@Override
public void onDrawFrame(GL10 gl) {
// 将颜色缓存区设置为预设的颜色
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT | GLES30.GL_DEPTH_BUFFER_BIT);
// 启用顶点的数组句柄
GLES30.glEnableVertexAttribArray(0);
GLES30.glEnableVertexAttribArray(1);
// 绘制模型
mModel.onModelDraw();
// 禁止顶点数组句柄
GLES30.glDisableVertexAttribArray(0);
GLES30.glDisableVertexAttribArray(1);
public void setCallback(Model.Callback callback) {
mModel.setCallback(callback);
}
Model.java
package com.zhyan8.offscreen.model;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.opengl.GLES30;
import com.zhyan8.offscreen.R;
import com.zhyan8.offscreen.utils.ArraysUtils;
import com.zhyan8.offscreen.utils.ShaderUtils;
import com.zhyan8.offscreen.utils.TextureUtils;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
public class Model {
private static final int TEXTURE_DIMENSION = 2; // 纹理坐标维度
private static final int VERTEX_DIMENSION = 3; // 顶点坐标维度
private Callback mCallback;
private Resources mResources;
private float mVertex[] = {-1.0f, 1.0f, 0.0f, -1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, -1.0f, 0.0f};
private float[] mFboTexture = {0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f};
protected FloatBuffer mVertexBuffer;
protected FloatBuffer mFboTextureBuffer;
// 帧缓冲对象 - 颜色、深度、模板附着点,纹理对象可以连接到帧缓冲区对象的颜色附着点
private int[] mFrameBufferId = new int[1];
private int[] mTextureId = new int[2];
private int mProgramId;
private Point mBitmapSize = new Point();
public Model(Resources resources) {
mResources = resources;
mVertexBuffer = ArraysUtils.getFloatBuffer(mVertex);
mFboTextureBuffer = ArraysUtils.getFloatBuffer(mFboTexture);
// 模型创建
public void onModelCreate() {
mProgramId = ShaderUtils.createProgram(mResources, R.raw.vertex_shader, R.raw.fragment_shader);
TextureUtils.loadTexture(mResources, R.raw.xxx, mBitmapSize, mTextureId, mFrameBufferId);
// 模型参数变化
public void onModelChange(int width, int height) {
GLES30.glViewport(0, 0, mBitmapSize.x, mBitmapSize.y);
// 模型绘制
public void onModelDraw() {
GLES30.glUseProgram(mProgramId);
//准备顶点坐标和纹理坐标
GLES30.glVertexAttribPointer(0, VERTEX_DIMENSION, GLES30.GL_FLOAT, false, 0, mVertexBuffer);
GLES30.glVertexAttribPointer(1, TEXTURE_DIMENSION, GLES30.GL_FLOAT, false, 0, mFboTextureBuffer);
//激活纹理
GLES30.glActiveTexture(GLES30.GL_TEXTURE);
//绑定纹理
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTextureId[0]);
// 绑定缓存
GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, mFrameBufferId[0]);
// 绘制贴图
GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4);
showBitmap();
private void showBitmap() {
// 分配字节缓区大小, 一个像素4个字节
ByteBuffer byteBuffer = ByteBuffer.allocate(mBitmapSize.x * mBitmapSize.y * 4);
GLES30.glReadPixels(0, 0, mBitmapSize.x, mBitmapSize.y, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, byteBuffer);
Bitmap bitmap = Bitmap.createBitmap(mBitmapSize.x, mBitmapSize.y, Bitmap.Config.ARGB_8888);
// 从缓存区读二进制缓冲数据
bitmap.copyPixelsFromBuffer(byteBuffer);
// 回调
mCallback.onCall(bitmap);
public void setCallback(Callback callback) {
mCallback = callback;
public interface Callback{
void onCall(Bitmap bitmap);
}
注意: 纹理贴图 中为防止显示的图片变形,对顶点坐标进行了正交投影变换;本文案例中却没有这样做,但显示的图片也未变形,主要因为缓存中的数据还未送显到屏幕,不知道屏幕尺寸,不必进行缩放调整。
ShaderUtils.java
package com.zhyan8.offscreen.utils;
import android.content.res.Resources;
import android.opengl.GLES30;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
public class ShaderUtils {
//创建程序id
public static int createProgram(Resources resources, int vertexShaderResId, int fragmentShaderResId) {
final int vertexShaderId = compileShader(resources, GLES30.GL_VERTEX_SHADER, vertexShaderResId);
final int fragmentShaderId = compileShader(resources, GLES30.GL_FRAGMENT_SHADER, fragmentShaderResId);
return linkProgram(vertexShaderId, fragmentShaderId);
//通过外部资源编译着色器
private static int compileShader(Resources resources, int type, int shaderId){
String shaderCode = readShaderFromResource(resources, shaderId);
return compileShader(type, shaderCode);
//通过代码片段编译着色器
private static int compileShader(int type, String shaderCode){
int shader = GLES30.glCreateShader(type);
GLES30.glShaderSource(shader, shaderCode);
GLES30.glCompileShader(shader);
return shader;
//链接到着色器
private static int linkProgram(int vertexShaderId, int fragmentShaderId) {
final int programId = GLES30.glCreateProgram();
//将顶点着色器加入到程序
GLES30.glAttachShader(programId, vertexShaderId);
//将片元着色器加入到程序
GLES30.glAttachShader(programId, fragmentShaderId);
//链接着色器程序
GLES30.glLinkProgram(programId);
return programId;
//从shader文件读出字符串
private static String readShaderFromResource(Resources resources, int shaderId) {
InputStream is = resources.openRawResource(shaderId);
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line;
StringBuilder sb = new StringBuilder();
try {
while ((line = br.readLine()) != null) {
sb.append(line);
sb.append("\n");
br.close();
} catch (Exception e) {
e.printStackTrace();
return sb.toString();
}
TextureUtils.java
package com.zhyan8.offscreen.utils;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.opengl.GLES30;
import android.opengl.GLUtils;
public class TextureUtils {
// 加载纹理贴图
public static void loadTexture(Resources resources, int resourceId, Point bitmapSize, int[] textureId, int[] frameBufferId) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = false;
Bitmap bitmap = BitmapFactory.decodeResource(resources, resourceId, options);
bitmapSize.set(bitmap.getWidth(), bitmap.getHeight());
// 生成纹理id
GLES30.glGenTextures(2, textureId, 0);
for (int i = 0; i < 2; i++) {
// 绑定纹理到OpenGL
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId[i]);
GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_NEAREST);
GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE);
GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE);
if (i == 0) {
// 第一个纹理对象给渲染管线(加载bitmap到纹理中)
GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, bitmap, 0);
} else {
// 第二个纹理对象给帧缓冲区
GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, bitmap.getWidth(), bitmap.getHeight(),
0, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, null);
// 取消绑定纹理
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, GLES30.GL_NONE);
// 创建帧缓存id
GLES30.glGenFramebuffers(1, frameBufferId, 0);
// 绑定帧缓存
GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, frameBufferId[0]);
// 将第二个纹理附着在帧缓存的颜色附着点上
GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, textureId[1], 0);
// 取消绑定帧缓存
GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, GLES30.GL_NONE);
}
ArraysUtils.java
package com.zhyan8.offscreen.utils;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
public class ArraysUtils {
public static FloatBuffer getFloatBuffer(float[] floatArr) {
FloatBuffer fb = ByteBuffer.allocateDirect(floatArr.length * Float.BYTES)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
fb.put(floatArr);
fb.position(0);
return fb;
}
vertex_shader.glsl
#version 300 es
layout (location = 0) in vec4 vPosition;
layout (location = 1) in vec2 aTextureCoord;
out vec2 vTexCoord;
void main() {
gl_Position = vPosition;
vTexCoord = aTextureCoord;
fragment_shader.glsl
#version 300 es
precision mediump float;
uniform sampler2D uTextureUnit;
in vec2 vTexCoord;
out vec4 fragColor;
void main() {