NDK系列之OpenGL离屏渲染与工程代码整合,本节主要是对上一节OpenGL渲染画面效果代码进行封装设计,将各种特效代码进行分离解耦,便于后期增加其他特效。
实现效果:
实现逻辑:
1.封装BaseFilter过滤器基类,实现通用的顶点着色器、片元着色器和着色器程序的初始化,赋值和绘制操作;
2.编写CamreaFilter相机过滤器,实现FBO离屏渲染和变换矩阵等相机相关操作,绘制操作完成后,将相机过滤器实现的效果的纹理ID传递给下一个过滤器,在相机过滤器效果上进行叠加其他效果。
本节主要内容:
1.封装BaseFilter过滤器基类;
2.实现CamreaFilter相机过滤器;
3.实现ScreenFilter屏幕过滤器;
4.自定义渲染器MyGlRenderer;
源码:
NdkOpenGLPlay: NDK OpenGL渲染画面效果
一、封装BaseFilter过滤器基类
1)BaseFilter构造方法,保存顶点着色器和片元着色器代码资源文件id,保存坐标系到数据缓冲区;
public BaseFilter(Context context, int vertexSourceId, int fragmentSourceId) {
this.mVertexSourceId = vertexSourceId; // 子类传递过来的顶点着色器代码ID
this.mFragmentSourceId = fragmentSourceId; // 子类传递过来的片元着色器代码ID
// 顶点相关 坐标系
float[] VERTEX = {
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f,};
mVertexBuffer = BufferHelper.getFloatBuffer(VERTEX); // 保存到 顶点坐标数据缓冲区
// 纹理相关 坐标系
float[] TEXTURE = {
0.0f, 0.0f,
1.0f, 0.0f,
0.0f, 1.0f,
1.0f, 1.0f,};
mTextureBuffer = BufferHelper.getFloatBuffer(TEXTURE); // 保存到 纹理坐标数据缓冲区
init(context);
}
2)加载并编译着色器代码,链接色器ID,输出着色器程序,并获取着色器的索引值;
private void init(Context context) {
String vertexSource = TextResourceReader.readTextFileFromResource(context, mVertexSourceId); // 顶点着色器代码字符串
String fragmentSource = TextResourceReader.readTextFileFromResource(context, mFragmentSourceId); // 片元着色器代码字符串
int vertexShaderId = ShaderHelper.compileVertexShader(vertexSource); // 编译顶点着色器代码字符串
int fragmentShaderId = ShaderHelper.compileFragmentShader(fragmentSource); // 编译片元着色器代码字符串
mProgramId = ShaderHelper.linkProgram(vertexShaderId, fragmentShaderId); // 链接 顶点着色器ID,片元着色器ID 最终输出着色器程序
// 删除 顶点 片元 着色器ID
glDeleteShader(vertexShaderId);
glDeleteShader(fragmentShaderId);
// 顶点着色器里面的如下:
vPosition = glGetAttribLocation(mProgramId, "vPosition"); // 顶点着色器:的索引值
vCoord = glGetAttribLocation(mProgramId, "vCoord"); // 顶点着色器:纹理坐标,采样器采样图片的坐标 的索引值
vMatrix = glGetUniformLocation(mProgramId, "vMatrix"); // 顶点着色器:变换矩阵 的索引值
// 片元着色器里面的如下:
vTexture = glGetUniformLocation(mProgramId, "vTexture"); // 片元着色器:采样器
}
3)完成绘制操作,顶点坐标赋值,纹理坐标赋值,采样器赋值,通知opengl绘制,将过滤器实现的效果的纹理ID返回给下一个过滤器;
public int onDrawFrame(int textureId) {
// 设置视窗大小
glViewport(0, 0, mWidth, mHeight);
glUseProgram(mProgramId); // 必须要使用着色器程序一次
// TODO 画画,绘制 等工作
// TODO 1.顶点坐标赋值
mVertexBuffer.position(0);
// 传值
glVertexAttribPointer(vPosition, 2, GL_FLOAT, false, 0, mVertexBuffer);
// 激活
glEnableVertexAttribArray(vPosition);
// TODO 2.纹理坐标赋值
mTextureBuffer.position(0);
// 传值
glVertexAttribPointer(vCoord, 2, GL_FLOAT, false, 0, mTextureBuffer);
// 激活
glEnableVertexAttribArray(vCoord);
// 只需要把OpenGL的纹理ID,渲染到屏幕上就可以了,不需要矩阵数据传递给顶点着色器了
// 变换矩阵 把mtx矩阵数据 传递到 vMatrix
// glUniformMatrix4fv(vMatrix, 1, false, mtx, 0);
// TODO 3.片元 vTexture
glActiveTexture(GL_TEXTURE0); // 激活图层
// 不需要关心摄像头 和 矩阵
// 绑定图层,为什么不需要GL_TEXTURE_EXTERNAL_OES?答:目前拿到的textureId已经是纹理ID了,不是摄像头直接采集到的纹理ID
glBindTexture(GL_TEXTURE_2D ,textureId);
// 因为CameraFilter已经做过了,我就直接显示,我用OepnGL 2D GL_TEXTURE_2D 显示就行了
// glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId); // 由于这种方式并不是通用的,所以先去除
glUniform1i(vTexture, 0); // 传递采样器
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // 通知 opengl 绘制
return textureId; // 返回纹理ID,可以告诉下一个过滤器
}
二、实现CamreaFilter相机过滤器
1)CameraFilter构造方法,将顶点着色器和片元着色器代码资源文件id传给父类,完成顶点着色器、片元着色器和着色器程序的初始化工作;
public CameraFilter(Context context) {
super(context, R.raw.camera_vertex, R.raw.camera_fragment);
}
2)准备工作,创建FBO帧缓冲区和FBO的纹理ID,并绑定起来;
public void onReady(int width, int height) {
super.onReady(width,height);
// TODO 准备工作
// TODO 第一步:创建 FBO (看不见的离屏的屏幕)
mFrameBuffers = new int[1];
// 参数1:int n, fbo 个数
// 参数2:int[] framebuffers, 用来保存 fbo id 的数组
// 参数3:int offset 从数组中第几个id来保存,从零下标开始
glGenFramebuffers(mFrameBuffers.length, mFrameBuffers, 0); // 实例化创建帧缓冲区,FBO缓冲区
// TODO 第二步:创建属于 fbo 纹理(第一节课是没有配置的,但是这个是FOB纹理,所以需要配置纹理)
// 既然上面的 FBO(看不见的离屏的屏幕),下面的目的就是要把画面显示到FBO中
mFrameBufferTextures = new int[1]; // 记录FBO纹理的ID
TextureHelper.genTextures(mFrameBufferTextures); // 生成并配置纹理
// TODO 第三步:上面的 FBO缓冲区 与 FBO纹理 还没有任何关系,现在要让他们绑定起来
glBindTexture(GL_TEXTURE_2D, mFrameBufferTextures[0]);
// 生产2D纹理图像
/*
int target, 要绑定的纹理目标
int level, level一般都是0
int internalformat, 纹理图像内部处理的格式是什么,rgba
int width, 宽
int height, 高
int border, 边界
int format, 纹理图像格式是什么,rgba
int type, 无符号字节的类型
java.nio.Buffer pixels
*/
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, null);
glBindFramebuffer(GL_FRAMEBUFFER, mFrameBuffers[0]); // 发生关系
/*
int target, fbo的纹理目标
int attachment, 附属到哪里
int textarget, 要绑定的纹理目标
int texture, 纹理
int level level一般都是0
*/
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mFrameBufferTextures[0], 0);
// TODO 第四步:解绑操作
glBindTexture(GL_TEXTURE_2D, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
3)完成绘制操作,渲染到FBO离线缓存中,顶点坐标赋值,纹理坐标赋值,变换矩阵把mtx矩阵数据传递到vMatrix,相机过滤器处理过后,其他过滤器不需要再变换矩阵,采样器赋值,通知opengl绘制,将FBO的纹理ID返回给下一个过滤器;
public int onDrawFrame(int textureId) {
// 不能调用super,因为父类做的事情,和子类是很大区别的
// super.onDrawFrame(textureId);
glViewport(0, 0, mWidth, mHeight); // 设置视窗大小
// TODO 渲染到 FBO离线缓存中
// 绑定FBO缓存(否则会绘制到屏幕上) 我们最终的效果是 离屏渲染
glBindFramebuffer(GL_FRAMEBUFFER, mFrameBuffers[0]);
glUseProgram(mProgramId); // 必须要使用着色器程序一次
// 画画 绘制操作
mVertexBuffer.position(0); // 顶点坐标赋值
glVertexAttribPointer(vPosition, 2, GL_FLOAT, false, 0, mVertexBuffer); // 传值
glEnableVertexAttribArray(vPosition); // 激活
mTextureBuffer.position(0); // 纹理坐标赋值
glVertexAttribPointer(vCoord, 2, GL_FLOAT, false, 0, mTextureBuffer); // 传值
glEnableVertexAttribArray(vCoord); // 激活
// TODO 变换矩阵,在我们CameraFilter这里就需要处理了,后面的BaseFilter就不需要了
glUniformMatrix4fv(vMatrix, 1, false, matrix, 0);
// 片元 vTexture
glActiveTexture(GL_TEXTURE0); // 激活图层
// glBindTexture(GL_TEXTURE_2D ,textureId); // 公用的那个 1
glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId); // 摄像头打交道采样器:使用额外拓展的,不能使用公用的那个 2
glUniform1i(vTexture, 0);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // 通知 opengl 绘制
// 解绑 fbo
glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// FBO的纹理ID,返回了
return mFrameBufferTextures[0]; // 你没有学过C,你就不明白,如果你学过,无需多言
}
4)相机过滤器顶点着色器代码:
attribute vec4 vPosition; // 顶点坐标,相当于:相机的四个点位置排版
attribute vec4 vCoord; // 纹理坐标,用来图形上色的
uniform mat4 vMatrix; // 变换矩阵,4*4的格式的
varying vec2 aCoord; // 把这个最终的计算成果,给片元着色器 【不需要Java传递,他是计算出来的】
void main() {
gl_Position = vPosition; // 确定好位置排版
aCoord = (vMatrix * vCoord).xy; // 兼容所有设备
}
5)相机过滤器片元着色器代码:
// 导入 samplerExternalOES
#extension GL_OES_EGL_image_external : require
// float 数据的精度 (precision lowp = 低精度) (precision mediump = 中精度) (precision highp = 高精度)
precision mediump float;
// 根据上面的数据的精度,写下面的 采样器 相机的数据
// uniform sampler2D vTexture; 由于我们用的是 安卓的相机,就不能用他
uniform samplerExternalOES vTexture; // samplerExternalOES才能采样相机的数据
varying vec2 aCoord; // 把这个最终的计算成果,给片元着色器,拿到最终的成果,我才能上色
void main() {
// 底片效果
vec4 rgba = texture2D(vTexture,aCoord); // rgba
gl_FragColor = vec4(1.-rgba.r, 1.-rgba.g, 1.-rgba.b, rgba.a);
}
三、实现ScreenFilter屏幕过滤器
1)ScreenFilter构造方法,将顶点着色器和片元着色器代码资源文件id传给父类,完成顶点着色器、片元着色器和着色器程序的初始化工作;
public ScreenFilter(Context context) {
super(context, R.raw.base_vertex, R.raw.base_fragment); // base_vertex(没有矩阵) base_fragment(没有OES 是sampler2D)
}
2)准备工作和绘制操作复用父类完成;
3)屏幕过滤器顶点着色器代码:
attribute vec4 vPosition; // 顶点坐标,相当于:相机的四个点位置排版
attribute vec2 vCoord; // 纹理坐标,用来图形上色的
varying vec2 aCoord; // 把这个最终的计算成果,给片元着色器 【不需要Java传递,他是计算出来的】
void main() {
gl_Position = vPosition; // 确定好位置排版 gl_Position OpenGL着色器语言内置的变量
// 着色器语言基础语法
// aCoord = vCoord.xy;
// aCoord是2个分量的 vCoord是四个分量的.xy取出两个分量
aCoord = vCoord;
}
4)屏幕过滤器片元着色器代码
// float 数据的精度 (precision lowp = 低精度) (precision mediump = 中精度) (precision highp = 高精度)
precision mediump float;
// 根据上面的数据的精度,写下面的 采样器 相机的数据
uniform sampler2D vTexture;// 由于我们用的是 安卓的相机,就不能用他(用OpenGL 2D显示就行了,不需要摄像头了,sampler2D)
varying vec2 aCoord;// 把这个最终的计算成果,给片元着色器,拿到最终的成果,我才能上色
void main() {
// texture2D (采样器, 坐标) gl_FragColor OpenGL着色器语言内置的变量
gl_FragColor = texture2D(vTexture, aCoord);// 直接上色,你直接上色,是上camera_fragment.glsl的底片效果
}
四、自定义渲染器MyGlRenderer
从上一节中我们知道,MyGlRenderer实现GLSurfaceView.Renderer接口,实现接口的onSurfaceCreated()、onSurfaceChanged()和onDrawFrame()方法;
1)当Surface创建时,回调onSurfaceCreated()函数,创建ScreenFilter屏幕过滤器,同样在创建ScreenFilter屏幕过滤器之前,我们要先创建CamreaFilter相机过滤器;
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
mCameraFilter = new CameraFilter(myGLSurfaceView.getContext()); // 先 FBO
mScreenFilter = new ScreenFilter(myGLSurfaceView.getContext()); // 后 渲染屏幕
}
2)当Surface改变时,回调onSurfaceChanged()函数,完成过滤器准备工作;
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
mCameraFilter.onReady(width, height);
mScreenFilter.onReady(width, height);
}
3)绘制一帧图像时,回调onDrawFrame()函数,获取纹理对象的图像数据,先通过CamreaFilter相机过滤器,实现相关效果,再将其FBO的纹理ID传递给下一个过滤器,这里由于暂无实现其他特效,就传递给了ScreenFilter屏幕过滤器,将最终成果的纹理ID通过OpenGL渲染到屏幕;
@Override
public void onDrawFrame(GL10 gl) {
// 相机过滤器,绘制一帧图像,不可见
mCameraFilter.setMatrix(mtx);
int textureId = mCameraFilter.onDrawFrame(mTextureID[0]); // 摄像头,矩阵,都已经做了
// 增加其他特效
/*textureId = 美白.onDrawFrame(textureId);
textureId = 大眼.onDrawFrame(textureId);
textureId = xxx.onDrawFrame(textureId);*/
// 屏幕过滤器,绘制一帧图像,屏幕显示
mScreenFilter.onDrawFrame(textureId); // textureId == 最终成果的纹理ID
}
注:后续将会在CamreaFilter相机过滤器和ScreenFilter屏幕过滤器中间增加各种效果过滤器,实现其他特效,如:美白,大眼,兔耳朵等。
至此,OpenGL离屏渲染与工程代码整合已完成。
源码:
NdkOpenGLPlay: NDK OpenGL渲染画面效果