一、图形渲染管线流程
经过前面几张的学习后,我们对OpenGL基础用法已经有了初步理解,现在来介绍下图形渲染管线流程,为OpenGL进阶知识做好准备。
OpenGL的图形渲染管线(Graphics Pipeline)是指:将一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程。主要流程如下:
- 图形渲染管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标,同时顶点着色器允许我们对顶点属性进行一些基本处理。
- 图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入(如果是
GL_POINTS
,那么就是一个顶点),并所有的点装配成指定图元的形状。图元(Primitive),任何一个绘制指令的调用都将把图元传递给OpenGL。这是其中的几个:GL_POINTS
、GL_TRIANGLES
、GL_LINE_STRIP
。 - 图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。在《Android OpenGL基础(一、绘制三角形四边形)》的例子中生成了另一个三角形。
- 几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
- 片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
- 在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段,我们叫做Alpha测试和混合(Blending)阶段。这个阶段检测片段的对应的深度(和模板(Stencil))值,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。
二、帧缓冲
下面介绍OpenGL进阶知识:帧缓冲。了解帧缓冲后,可以实现多级滤镜、镜面、离屏渲染、相机帧缓存等高级效果。
在《Android OpenGL基础(四、图片后处理)》一文中,我们介绍了图片滤镜的实现方法,如果要实现多级滤镜(比如先灰度滤镜再边缘滤镜)该如何处理。一种简单的方法是把多个滤镜的片段着色器代码都放在一起,但是这种实现方法不够灵活,代码耦合严重且不能实现自由组合滤镜。下面介绍帧缓冲的实现方式。
2.1 帧缓冲对象
在前面章节的OpenGL例子中,在设置了顶点缓冲后,我们又设置了颜色缓冲或者纹理等,而颜色缓冲或者纹理等之所以可以展示出来,是因为它们都是在默认帧缓冲的渲染缓冲上进行的。可以理解为帧缓冲(Framebuffer Object, FBO)是一种可以缓冲颜色、纹理等渲染指令结果的高级缓冲。帧缓冲可以将纹理、渲染缓冲对象(Renderbuffer Object)作为其附件,在帧缓冲激活后,渲染指令将会写入到帧缓冲的附件中。
一个完整的帧缓冲需要满足以下的条件:
- 附加至少一个缓冲(颜色、深度或模板缓冲)。
- 至少有一个颜色附件(Attachment)。
- 所有的附件都必须是完整的(保留了内存)。
- 每个缓冲都应该有相同的样本数。
OpenGL提供了自定义帧缓冲的方法,因此实现多级滤镜可以通过自定义多级帧缓冲,每个帧缓冲附加一个纹理附件,这样每个帧缓冲对应一个滤镜实现,并将结果传递给下一个帧缓冲作为下一个缓冲的纹理,实现多级滤镜自由组合。
2.2 基本用法
创建帧缓冲比较简单,此外在创建完后需要给帧缓冲附加一个附件。附件是一个内存位置,它能够作为帧缓冲的一个缓冲,可以将它想象为一个图像。
2.2.1 帧缓冲附件
帧缓冲(Framebuffer Object, FBO)的附件有两种类型:
- 纹理;
- 渲染缓冲对象(Renderbuffer Object):渲染缓冲对象(Renderbuffer Object)是在纹理之后引入到OpenGL中。渲染缓冲对象附加的好处是,它会将数据储存为OpenGL原生的渲染格式,它是为离屏渲染到帧缓冲优化过的。由于渲染缓冲对象通常都是只写的,常用于深度和模板附件(适合于大部分时间只写,而不需要从缓冲中读取值)。
选择哪种附件通常的规则是,如果你不需要从一个缓冲中采样数据,那么对这个缓冲使用渲染缓冲对象会是明智的选择。如果你需要从缓冲中采样颜色或深度值等数据,那么应该选择纹理附件。
2.2.2 创建帧缓冲
本文主要介绍以纹理作为附件的帧缓冲对象。创建方式如下:
// 创建纹理,后面会把这个纹理附加到帧缓冲上
int[] texFBO = {0};
GLES20.glGenTextures(1, texFBO, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texFBO[0]);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
// 创建帧缓冲
int[] FBO = {0};
GLES20.glGenFramebuffers(1, FBO, 0);
// 绑定帧缓冲
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, FBO[0]);
// 把纹理作为帧缓冲的附件
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, texFBO[0], 0);
Log.d(LOGTAG, "initFBO error status: " + GLES20.glGetError());
// 检查帧缓冲是否完整
int FBOstatus = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER);
if (FBOstatus != GLES20.GL_FRAMEBUFFER_COMPLETE)
Log.e(LOGTAG, "initFBO failed, status: " + FBOstatus);
2.2.3 渲染到纹理
在创建了帧缓冲及附加到其中的纹理后,接下来看怎么渲染到帧缓冲内的纹理,以及如何把帧缓冲的纹理渲染到屏幕上。要渲染到自定义FBO,只需要绑定当前FBO即可,在绑定到GL_FRAMEBUFFE目标之后,所有的读取和写入帧缓冲的操作将会影响当前绑定的帧缓冲。如果要渲染到屏幕上,则绑定到默认缓冲帧即可。
// 绑定自定义缓冲帧
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffers.get(i))
// 绑定默认缓冲帧
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0)
2.3.4 绘制流程
在加入自定义帧缓冲后的绘制流程与只有默认帧缓冲的流程对比如下:
代码流程如下所示,在onDrawFilterList中遍历自定义FBO列表,在每个列表中分别完成各自的滤镜操作,然后在渲染完成后,将纹理传给下一个帧缓冲。
class OpenGLImageFilter {
/**
* 顶点着色器代码;
*/
private val vertexShaderCode =
"attribute vec4 inputTextureCoordinate;" +
" varying vec2 textureCoordinate;" +
"attribute vec4 vPosition;" +
"void main() {" +
// 把vPosition顶点经过矩阵变换后传给gl_Position
" gl_Position = vPosition;" +
"textureCoordinate = inputTextureCoordinate.xy;" +
"}"
/**
* 片段着色器代码
*/
private val fragmentShaderCode ="""
varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
void main() {
gl_FragColor = texture2D(inputImageTexture, textureCoordinate);
}
"""
/**
* 遍历绘制滤镜list
* 开始的纹理都绘制到FBO上,直到最后一个绘制到默认缓冲帧上进行上屏
*/
fun onDrawFilterList(textureId: Int, cubeVertexBuffer: FloatBuffer?,
textureVertexBuffer: FloatBuffer?) {
if (filterLists != null) {
val size: Int = filterLists.size
var previousTexture = textureId
// 遍历滤镜list
for (i in 0 until size) {
val filter: OpenGLImageFilter = filterLists.get(i)
val isNotLast = i < size - 1
if (isNotLast) {
// 不是最后,则绑定到FBO
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffers.get(i))
} else {
// 最后一个时,绑定到默认的帧缓冲
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0)
}
GLES20.glClearColor(0f, 0f, 0f, 0f)
// 绘制到帧缓冲上的纹理
filter.onDrawItem(previousTexture, cubeVertexBuffer, textureVertexBuffer)
if (isNotLast) {
// 更新FBO纹理索引到下一个
previousTexture = frameBufferTextures.get(i)
}
}
}
}
/**
* 纹理绘制方法
* @param textureId 纹理ID
* @param vertexBuffer 顶点数组缓冲
* @param textureVertexBuffer 纹理顶点数组缓冲
*/
fun onDrawItem(textureId: Int, vertexBuffer: FloatBuffer,
textureVertexBuffer: FloatBuffer) {
GLES20.glUseProgram(glProgId)
// 顶点缓冲数组重置
vertexBuffer.position(0)
GLES20.glVertexAttribPointer(glAttribPosition, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer)
GLES20.glEnableVertexAttribArray(glAttribPosition)
// 纹理顶点数组缓冲重置
textureVertexBuffer.position(0)
GLES20.glVertexAttribPointer(
glAttribTextureCoordinate, 2, GLES20.GL_FLOAT, false, 0,
textureVertexBuffer
)
GLES20.glEnableVertexAttribArray(glAttribTextureCoordinate)
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
// 绑定纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
GLES20.glUniform1i(glUniformTexture, 0)
// 绘制纹理
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
GLES20.glDisableVertexAttribArray(glAttribPosition)
GLES20.glDisableVertexAttribArray(glAttribTextureCoordinate)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
}
}
2.3.5 删除帧缓冲
在不需要帧缓冲时,调用如下方法删除帧缓冲列表:
GLES20.glDeleteFramebuffers(frameBuffers.length, frameBuffers, 0);
三、相机帧缓冲
同样地,对于相机也可以采用帧缓冲方案,来实现更复杂的效果。可以将OpenGL相机基础改造为自定义FBO的方式来实现相机预览。在一些复杂的项目中,需要用FBO实现相机预览,并在此基础上加入多级FBO来实现自由扩展相机滤镜或其他绘制需求。实现原理比较简单:在获得相机数据后,首先将相机纹理绘制到FBO,然后依次绘制多级FBO,最终绘制到默认帧缓冲进行上屏展示。
3.1 相机与FBO
需要注意的是,在获取到相机帧后,相机帧的顶点坐标与2D图片不同(具体参考"相机预览及滤镜")。首先,分别用不同程序表示相机纹理绘制和2D纹理绘制程序,相机的OES着色器和用于FBO的2D纹理片段着色器GLSL代码如下:
class CameraGLRenderer(private val frameAvailableListener: SurfaceTexture.OnFrameAvailableListener) :
GLSurfaceView.Renderer {
// 顶点着色器
private val vertexShaderCode = """attribute vec2 vPosition;
attribute vec2 vTexCoord;
varying vec2 texCoord;
void main() {
texCoord = vTexCoord;
gl_Position = vec4 ( vPosition.x, vPosition.y, 0.0, 1.0 );
}"""
/**
* 片段着色器;用于相机纹理;
*/
private val fssOES = """#extension GL_OES_EGL_image_external : require
precision mediump float;
uniform samplerExternalOES sTexture;
varying vec2 texCoord;
void main() {
gl_FragColor = texture2D(sTexture,texCoord);
}"""
/**
* 片段着色器;用于2D的FBO
*/
private val fss2D = """precision mediump float;
uniform sampler2D sTexture;
varying vec2 texCoord;
void main() {
gl_FragColor = texture2D(sTexture,texCoord);
}"""
// 顶点着色器;四边形顶点坐标
private val squareCoords = floatArrayOf(-1f, -1f, -1f, 1f, 1f, -1f, 1f, 1f)
// 相机纹理的四个点坐标
private val texCoordOES = floatArrayOf(0f, 1f, 0f, 0f, 1f, 1f, 1f, 0f)
// 2D纹理的四个点坐标
private val texCoord2D = floatArrayOf(0f, 0f, 0f, 1f, 1f, 0f, 1f, 1f)
// 四个顶点的缓冲数组
private val vertexBuffer: FloatBuffer =
ByteBuffer.allocateDirect(squareCoords.size * 4).order(ByteOrder.nativeOrder())
.asFloatBuffer().apply {
put(squareCoords)
position(0)
}
// 相机纹理顶点的缓冲数组
private val oESvertexBuffer: FloatBuffer =
ByteBuffer.allocateDirect(texCoordOES.size * 4).order(ByteOrder.nativeOrder())
.asFloatBuffer().apply {
put(texCoordOES)
position(0)
}
// 2D纹理顶点的缓冲数组
private val texture2DvertexBuffer: FloatBuffer =
ByteBuffer.allocateDirect(texCoord2D.size * 4).order(ByteOrder.nativeOrder())
.asFloatBuffer().apply {
put(texCoord2D)
position(0)
}
/**
* OES着色器程序ID引用
*/
private var mOESProgram = 0
/**
* FBO用到的2D着色器程序
*/
private var m2DProgram = 0
init {
// 编译顶点着色器和片段着色器
val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
val oESfragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fssOES)
val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fss2D)
// glCreateProgram函数创建一个着色器程序,并返回新创建程序对象的ID引用
mOESProgram = GLES20.glCreateProgram().also {
// 把顶点着色器添加到程序对象
GLES20.glAttachShader(it, vertexShader)
// 把片段着色器添加到程序对象
GLES20.glAttachShader(it, oESfragmentShader)
// 连接并创建一个可执行的OpenGL ES程序对象
GLES20.glLinkProgram(it)
}
m2DProgram = GLES20.glCreateProgram().also {
GLES20.glAttachShader(it, vertexShader)
GLES20.glAttachShader(it, fragmentShader)
GLES20.glLinkProgram(it)
}
}
}
3.2 初始化FBO
在onSurfaceChanged中创建FrameBuffer,设置FrameBuffer的宽高:
class CameraGLRenderer(private val frameAvailableListener: SurfaceTexture.OnFrameAvailableListener) :GLSurfaceView.Renderer {
// FBO引用
private val FBO = intArrayOf(0)
// 相机纹理
private val texCamera = intArrayOf(0)
// FBO的2D纹理
private var texFBO:IntArray = intArrayOf(0)
//
private var texDraw:IntArray = intArrayOf(0)
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
initFBO(width, height)
}
/**
* 初始化FBO
**/
private fun initFBO(width: Int, height: Int) {
// 创建相机纹理
GLES20.glGenTextures(1, texCamera, 0)
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texCamera[0])
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST)
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST)
mSTexture = SurfaceTexture(texCamera[0])
mSTexture?.setOnFrameAvailableListener(frameAvailableListener)
// 创建
GLES20.glGenTextures(1, texDraw, 0)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texDraw[0])
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0,
GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST)
// 创建FBO的纹理
GLES20.glGenTextures(1, texFBO, 0)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texFBO[0])
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0,
GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST)
// 创建FBO
GLES20.glGenFramebuffers(1, FBO, 0)
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, FBO[0])
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
GLES20.GL_TEXTURE_2D, texFBO.get(0), 0)
val FBOstatus = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER)
mFBOWidth = width
mFBOHeight = height
}
}
3.3 绘制流程
自定义CameraTextureListener接口来实现多级滤镜:
public interface CameraTextureListener {
/**
*
**/
public boolean onCameraTexture(int texIn, int texOut, int width, int height);
}
绘制流程如下:
class CameraGLRenderer(private val frameAvailableListener: SurfaceTexture.OnFrameAvailableListener) :
GLSurfaceView.Renderer {
override fun onDrawFrame(gl: GL10?) {
if (mUpdateST) {
mSTexture?.updateTexImage()
mUpdateST = false
}
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
// 滤镜集合list
val texListener: CameraTextureListener = getCameraTextureListener()
if (texListener != null) {
// texCamera(OES) -> texFBO
drawTex(texCamera[0], true, FBO[0])
// call user code (texFBO -> texDraw)
val modified =
texListener.onCameraTexture(texFBO[0], texDraw[0], mCameraWidth, mCameraHeight)
if (modified) {
// texDraw -> screen
drawTex(texDraw[0], false, 0)
} else {
// texFBO -> screen
drawTex(texFBO[0], false, 0)
}
} else {
// texCamera(OES) -> screen
drawTex(texCamera[0], true, 0)
}
}
private fun drawTex(tex: Int, isOES: Boolean, fbo: Int) {
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fbo)
if (fbo == 0) {
GLES20.glViewport(0, 0, mView.getWidth(), mView.getHeight())
} else {
GLES20.glViewport(0, 0, mFBOWidth, mFBOHeight)
}
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
if (isOES) {
GLES20.glUseProgram(progOES)
GLES20.glVertexAttribPointer(vPosOES, 2, GLES20.GL_FLOAT, false, 4 * 2, vert)
GLES20.glVertexAttribPointer(vTCOES, 2, GLES20.GL_FLOAT, false, 4 * 2, texOES)
} else {
GLES20.glUseProgram(prog2D)
GLES20.glVertexAttribPointer(vPos2D, 2, GLES20.GL_FLOAT, false, 4 * 2, vert)
GLES20.glVertexAttribPointer(vTC2D, 2, GLES20.GL_FLOAT, false, 4 * 2, tex2D)
}
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
if (isOES) {
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, tex)
GLES20.glUniform1i(GLES20.glGetUniformLocation(progOES, "sTexture"), 0)
} else {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, tex)
GLES20.glUniform1i(GLES20.glGetUniformLocation(prog2D, "sTexture"), 0)
}
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
GLES20.glFlush()
}
}
The End