Android OpenGLES3绘图 - 帧缓冲
普通的OpenGL绘图时是绘制到当前帧上面,由于GL环境跟当前屏幕进行了关联,也就直接绘制到屏幕了。这样有两个问题:1. 如果有的帧计算得快,有的计算得慢,而屏幕刷新率是固定的,就会拖慢整体帧率;2. 在着色器里面只能处理当前位置的点,没办法处理当前点跟其他点的关系。
如果将OpenGL计算后的帧缓存起来,不直接绘制。那么就可以利用双缓冲或多缓冲技术稳定帧率;在着色器里面可以从缓存帧读取所有点,就可以进行一些相对位置处理,比如将当前点跟周围点颜色计算平均值,进行图像模糊。这就是帧缓冲。
给一个普通的绘制添加帧缓冲也很方便,基本不影响原来的绘制。假设原来绘制的是三角形,那么先配置一下帧缓冲,可以配置成2D纹理,然后进行原来的绘制,三角形就会绘制到缓冲帧的纹理上面,最后将这个2D纹理绘制到屏幕上就可以了。
实现方法:
1 原图形Shader
原图形是很多个旋转的3D箱子
image.png
static class Simple3DShader {
int program;
FloatBuffer vertexBuffer;
int[] vao;
int[] tex;
public void init() {
program = ShaderUtils.loadProgram3D();
//分配内存空间,每个浮点型占4字节空间
vertexBuffer = ByteBuffer.allocateDirect(vertices.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
//传入指定的坐标数据
vertexBuffer.put(vertices);
vertexBuffer.position(0);
vao = new int[1];
glGenVertexArrays(1, vao, 0);
glBindVertexArray(vao[0]);
int[] vbo = new int[1];
glGenBuffers(1, vbo, 0);
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glBufferData(GL_ARRAY_BUFFER, vertices.length * 4, vertexBuffer, GL_STATIC_DRAW);
tex = new int[2];
glGenTextures(2, tex, 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, tex[0]);
// 为当前绑定的纹理对象设置环绕、过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
Bitmap bitmap = ShaderUtils.loadImageAssets("wall.jpg");
GLUtils.texImage2D(GL_TEXTURE_2D, 0, bitmap, 0);
glGenerateMipmap(GL_TEXTURE_2D);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, tex[1]);
// 为当前绑定的纹理对象设置环绕、过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
Bitmap bitmap1 = ShaderUtils.loadImageAssets("face.png");
GLUtils.texImage2D(GL_TEXTURE_2D, 0, bitmap1, 0);
glGenerateMipmap(GL_TEXTURE_2D);
glUseProgram(program);
int loc0 = glGetUniformLocation(program, "texture1");
glUniform1i(loc0, 0);
int loc1 = glGetUniformLocation(program, "texture2");
glUniform1i(loc1, 1);
// Load the vertex data
glVertexAttribPointer(0, 3, GL_FLOAT, false, 5 * 4, 0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, false, 5 * 4, 3 * 4);
glEnableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
public void draw(float[] modelMat, float[] viewMat, float[] projectionMat, float rot) {
// Use the program object
glUseProgram(program);
glBindTexture(GL_TEXTURE_2D, tex[0]);
glBindTexture(GL_TEXTURE_2D, tex[1]);
int loc1 = glGetUniformLocation(program, "view");
glUniformMatrix4fv(loc1, 1, false, viewMat, 0);
int loc2 = glGetUniformLocation(program, "projection");
glUniformMatrix4fv(loc2, 1, false, projectionMat, 0);
glBindVertexArray(vao[0]);
//Matrix.setLookAtM();
for (int i = 0; i < cubePositions.length; i++) {
Matrix.setIdentityM(modelMat, 0);
Matrix.translateM(modelMat, 0, cubePositions[i][0], cubePositions[i][1], cubePositions[i][2]);
if (i % 3 == 0) {
Matrix.rotateM(modelMat, 0, rot, 0.5f, 1f, 0.2f);
}
int loc = glGetUniformLocation(program, "model");
glUniformMatrix4fv(loc, 1, false, modelMat, 0);
glDrawArrays ( GL_TRIANGLES, 0, vertices.length );
}
}
2 配置帧缓冲
创建帧缓冲对象fbo,创建纹理对象tcbo和渲染缓冲对象rbo,将它们俩跟fbo绑定,检查一下是否成功。
配置时需要纹理的宽高,要传入GLSurfaceView的宽高。这里做了一个延时初始化。
int[] fbo;
int[] tcbo;
int[] rbo;
private void initFramebuffer(int w, int h) {
if (fbo != null) {
return;
}
fbo = new int[1];
glGenFramebuffers(1, fbo, 0);
glBindFramebuffer(GL_FRAMEBUFFER, fbo[0]);
// 生成纹理
tcbo = new int[1];
glGenTextures(1, tcbo, 0);
glBindTexture(GL_TEXTURE_2D, tcbo[0]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, null);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
// 将它附加到当前绑定的帧缓冲对象
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tcbo[0], 0);
rbo = new int[1];
glGenRenderbuffers(1, rbo, 0);
glBindRenderbuffer(GL_RENDERBUFFER, rbo[0]);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, w, h);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo[0]);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
Log.e("chao", "创建Framebuffer没有完成!");
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
3 帧缓冲Shader
创建顶点数组缓冲对象vbo,绘制一个带纹理的正方形,注意纹理需要用上一步创建的纹理对象tcbo
static class ScreenShader {
float vertices[] = { // vertex attributes for a quad that fills the entire screen in Normalized Device Coordinates.
// positions // texCoords
-1.0f, 1.0f, 0.0f, 1.0f,
-1.0f, -1.0f, 0.0f, 0.0f,
1.0f, -1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 1.0f,
1.0f, -1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 1.0f, 1.0f
};
int program;
int[] vao;
public void init() {
program = ShaderUtils.loadProgramFramebuffer();
//分配内存空间,每个浮点型占4字节空间
FloatBuffer vertexBuffer = ByteBuffer.allocateDirect(vertices.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
//传入指定的坐标数据
vertexBuffer.put(vertices);
vertexBuffer.position(0);
vao = new int[1];
glGenVertexArrays(1, vao, 0);
glBindVertexArray(vao[0]);
int[] vbo = new int[1];
glGenBuffers(1, vbo, 0);
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glBufferData(GL_ARRAY_BUFFER, vertices.length * 4, vertexBuffer, GL_STATIC_DRAW);
glVertexAttribPointer(0, 2, GL_FLOAT, false, 4 * 4, 0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, false, 4 * 4, 2 * 4);
glEnableVertexAttribArray(1);
glUseProgram(program);
int loc = glGetUniformLocation(program, "screenTexture");
glUniform1i(loc, 1);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
}
4 最终绘制
先绑定帧缓冲对象,开启深度测试,用原图形Shader绘制3D箱子图案;然后解绑帧缓冲对象fbo,关闭深度测试(绑定当前屏幕),用帧缓冲Shader绘制fbo中的纹理。绘制结果跟原图形完全一致。
@Override
public void onDrawFrame(GL10 gl) {
...
// // 第一阶段处理(Pass),绘制3D图形到Framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, fbo[0]);
glClearColor(0.5f, 0.5f, 0.5f, 0.5f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
simple3DShader.draw(modelMat, viewMat, projectionMat, rot);
// 第二阶段处理,把Framebuffer绘制为2D纹理
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
screenShader.draw(tcbo);
}
// screenShader.draw
public void draw(int[] tcbo) {
glUseProgram(program);
glBindVertexArray(vao[0]);
glDisable(GL_DEPTH_TEST);
glBindTexture(GL_TEXTURE_2D, tcbo[0]);
glDrawArrays(GL_TRIANGLES, 0, 6);
}
做帧缓冲的过程可能出错,但是没关系,绘制原图形与绘制帧缓冲矩形纹理是非常独立的。可以将这两个绘制分开调试,都没问题了再结合起来。
5 后期处理
有了帧缓冲后,可以在帧缓冲的片段着色器上做一些简单的图像处理:反相、灰度;核效果:模糊、锐化、边缘检测等。
模糊.png 锐化.png 边缘检测.png核效果就是将当前点周围包括自己的9个点数值进行简单的卷积运算,结果赋给当前点,使图像呈现某种视觉效果。
#version 300 es
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D screenTexture;
//void main()
//{
// FragColor = texture(screenTexture, TexCoords);
//
// // 反相效果
// FragColor = vec4(1.0 - texture(screenTexture, TexCoords).rgb, 1.0);
//
// // 灰度效果
// float ave = (FragColor.r + FragColor.g + FragColor.b) / 3.0;
// FragColor = vec4(ave, ave, ave, 1.0);
//}
const float offset = 1.0 / 300.0;
void main()
{
vec2 offsets[9] = vec2[](
vec2(-offset, offset), // 左上
vec2( 0.0f, offset), // 正上
vec2( offset, offset), // 右上
vec2(-offset, 0.0f), // 左
vec2( 0.0f, 0.0f), // 中
vec2( offset, 0.0f), // 右
vec2(-offset, -offset), // 左下
vec2( 0.0f, -offset), // 正下
vec2( offset, -offset) // 右下
);
// 锐化核
float kernel[9] = float[](
-1.0, -1.0, -1.0,
-1.0, 9.0, -1.0,
-1.0, -1.0, -1.0
);
// // 边缘检测核
// float kernel[9] = float[](
// 1.0, 1.0, 1.0,
// 1.0,-8.0, 1.0,
// 1.0, 1.0, 1.0
// );
// // 模糊核
// float kernel[9] = float[](
// 1.0 / 16.0, 2.0 / 16.0, 1.0 / 16.0,
// 2.0 / 16.0, 4.0 / 16.0, 2.0 / 16.0,
// 1.0 / 16.0, 2.0 / 16.0, 1.0 / 16.0
// );
vec3 sampleTex[9];
for(int i = 0; i < 9; i++)
{
sampleTex[i] = vec3(texture(screenTexture, TexCoords.st + offsets[i]));
}
vec3 col = vec3(0.0);
for(int i = 0; i < 9; i++)
col += sampleTex[i] * kernel[i];
FragColor = vec4(col, 1.0);
}