一、OpenGL简介
1.1 OpenGL规范
OpenGL 是一种跨平台的图形 API,用于为 3D 图形处理硬件指定标准的软件接口。OpenGL ES 是 OpenGL 规范的一种形式,适用于嵌入式设备。Android 支持多版 OpenGL ES API(推荐在最新 Android 设备上使用OpenGL ES 2.0 API版本):
- OpenGL ES 1.0 和 1.1 - 此 API 规范受 Android 1.0 及更高版本的支持。
- OpenGL ES 2.0 - 此 API 规范受 Android 2.2(API 级别 8)及更高版本的支持。
- OpenGL ES 3.0 - 此 API 规范受 Android 4.3(API 级别 18)及更高版本的支持。
- OpenGL ES 3.1 - 此 API 规范受 Android 5.0(API 级别 21)及更高版本的支持。
1.2 OpenGL框架基本类
Android 框架中,GLSurfaceView
是使用 OpenGL 绘制的图形的视图容器,而 GLSurfaceView.Renderer
可控制该视图中绘制的图形。
1.2.1 GLSurfaceView
此类是一个 View
,对于全屏或接近全屏的图形视图,选择GLSurfaceView
合理一些。此外,如果希望将 OpenGL ES 图形整合到其布局中的一小部分,也可以考虑使用 TextureView
。SurfaceView也可以用于OpenGL的视图容器,但是需要编写的代码比较多,暂不推荐。
1.2.2 GLSurfaceView.Renderer
此接口定义了在GLSurfaceView中绘制图形所需的方法。将此接口的实现类通过 GLSurfaceView.setRenderer()
与 GLSurfaceView
实例关联起来。GLSurfaceView.Renderer
接口要求实现以下方法:
-
onSurfaceCreated()
:系统会在创建GLSurfaceView
时调用一次此方法。通常用来设置仅需发生一次的操作,例如设置 OpenGL 环境参数或初始化 OpenGL 图形对象。 -
onDrawFrame()
:系统会在每次重新绘制GLSurfaceView
时调用此方法。是将图像绘制到GLSurfaceView的主要方法。 -
onSurfaceChanged()
:系统会在GLSurfaceView
几何图形发生变化(包括GLSurfaceView
大小发生变化或设备屏幕方向发生变化)时调用此方法。例如,系统会在设备屏幕方向由纵向变为横向时调用此方法。
1.3 OpenGL标准化设备坐标
OpenGL是一个右手坐标系。简单来说,就是正x轴在你的右手边,正y轴朝上,而正z轴是朝向后方的。想象你的屏幕处于三个轴的中心,则正z轴穿过你的屏幕朝向你。
与通常的屏幕坐标不同,OpenGL假设屏幕采用均匀的方形坐标系,OpenGL采用的是标准化设备坐标(Normalized Device Coordinates, NDC),标准化设备坐标的y轴正方向为向上,(0, 0)坐标是这个图像的中心,而不是左上角。标准化设备坐标是一个x、y和z值都在-1.0到1.0的一个坐标系,任何落在范围外的坐标都会被丢弃/裁剪,不会绘制到手机屏幕上。
例如:一个三角形的三个点在OpenGL标准坐标系中表示如下(z轴都是0,暂时先忽略z轴):
二、基础用法
下面通过一个简单的绘制三角形的例子入门OpenGL。
2.1 构建 OpenGL ES 环境
2.1.1 声明清单
manifext.xml中声明了OpenGL的版本glEsVersion是OpenGL ES 2.0。
2.1.2 创建GLSurfaceView及Render
GLSurfaceView是使用 OpenGL 绘制的图形的视图容器,而GLSurfaceView.Renderer可控制该视图中绘制的图形。
在自定义的MyGLSurfaceView中把GLSurfaceView与Render关联起来:
class MyGLSurfaceView(context: Context?, attrs: AttributeSet?) : GLSurfaceView(context, attrs) {
private val renderer: MyGLRenderer
init {
// 创建一个OpenGL ES 2.0上下文
setEGLContextClientVersion(2)
renderer = MyGLRenderer()
// GLSurfaceView关联Render
setRenderer(renderer)
}
}
GLSurfaceView.Renderer负责实际的绘制工作,这里先只把背景设置为黑色:
class MyGLRenderer : GLSurfaceView.Renderer {
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
// 设置背景色为黑色
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
// 上面提到OpenGL使用的是标准化设备坐标;
GLES20.glViewport(0, 0, width, height)
}
override fun onDrawFrame(gl: GL10?) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
}
}
至此,打开APP后可以看到一个黑色背景的GLSurfaceView。
2.2 扩展绘制方法
如果需要在OpenGL中绘制其他内容,则在onDrawFrame()方法内扩充即可,在大型的OpenGL项目中,一般采用类似Android系统View体系的模板设计模式(即ViewGroup调用子View的draw()方法,层层调用)。
下面继续介绍绘制三角形的步骤,完成绘制三角形的主要工作在自定义的Triangle类中,只需要在onDrawFrame()中调用Triangle完成三角形的绘制:
class MyGLRenderer : GLSurfaceView.Renderer {
private lateinit var triangle: Triangle
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
// 我们的例子中在Triangle构造函数中就操作了GLES20,所以一定要在onSurfaceCreated中再去创建Triangle对象
triangle = Triangle()
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
}
override fun onDrawFrame(gl: GL10?) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
// 绘制三角形
triangle.draw()
}
}
2.3 定义形状
下面继续介绍Triangle绘制三角形的主要步骤,OpenGL使用FloatBuffer来管理顶点数据提高效率。一个三角形需要由三个顶点表示,这三个顶点在交给OpenGL时需要使用FloatBuffer格式,下面是三个顶点的定义方式:
class Triangle {
// 三角形三个点的坐标值(逆时针方向,在3D坐标系中,方向决定了哪面是正面)
private var triangleCoords = floatArrayOf(
0.0f, 0.622008459f, 0.0f, // top
-0.5f, -0.311004243f, 0.0f, // bottom left
0.5f, -0.311004243f, 0.0f // bottom right
)
// 设置颜色(分别代表red, green, blue and alpha)
private val color = floatArrayOf(0.63671875f, 0.76953125f, 0.22265625f, 1.0f)
private var vertexBuffer: FloatBuffer =
// 坐标点的数目 * float所占字节
ByteBuffer.allocateDirect(triangleCoords.size * 4)
.order(ByteOrder.nativeOrder()).asFloatBuffer().apply {
// 把坐标添加到FloatBuffer
put(triangleCoords)
// 设置buffer的位置为起始点0
position(0)
}
}
2.3.1 顶点缓冲对象
这里引入三个名词:
- 顶点数组对象:Vertex Array Object,VAO,表示存放顶点的数组,即例子中的triangleCoords;
- 顶点缓冲对象:Vertex Buffer Object,VBO,表示存放顶点缓冲的数据,即例子中的FloatBuffer对象vertexBuffer;
- 索引缓冲对象:Element Buffer Object,EBO或Index Buffer Object,IBO,表示存放顶点索引的数组,3.2小节会涉及到,用于描述顶点之间的顺序来重复使用顶点。
OpenGL会在GPU内存中存储大量顶点,使用顶点缓冲对象(Vertex Buffer Objects, VBO)管理这个内存。使用缓冲对象VBO的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至GPU内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。
顶点数组对象与顶点缓冲对象关系如下,暂时简单了解即可:
2.4 绘制三角形
2.4.1 GLSL 着色器
如果要渲染图形,OpenGL需要我们至少设置一个顶点着色器和一个片段着色器。我们需要做的第一件事是用着色器语言GLSL(OpenGL Shading Language)编写顶点着色器,然后编译这个着色器,这样我们就可以在着色器程序中使用它了。
着色器(Shader)是运行在GPU上的小程序。这些小程序为图形渲染管线的某个特定部分而运行。从基本意义上来说,着色器只是一种把输入转化为输出的程序。着色器也是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出。
2.4.2 顶点着色器
顶点着色器是用于渲染形状的顶点的 OpenGL ES 图形代码。一个GLSL顶点着色器的源代码如下所示:
/**
* 顶点着色器代码
* 我们暂时将顶点着色器的源代码硬编码在代码文件顶部的C风格字符串中
**/
private val vertexShaderCode =
"attribute vec4 vPosition;" +
"void main() {" +
" gl_Position = vPosition;" +
"}"
- attribute是GLSL的关键字表示声明一个属性;
- vec4是GLSL的数据类型关键字,包含4个float分量的默认向量
- vPosition是开发者自定义的变量名;
- gl_Position是GLSL的内建变量:顶点着色器输出向量,这里把我们自定义的vPosition赋值过去,后面我们会在着色器程序中取出来操作顶点着色器中的数据;
2.4.3 片段着色器
片段着色器是用于使用颜色或纹理渲染形状面的 OpenGL ES 代码,主要工作是计算像素最后的颜色输出。一个片段着色器的源码如下:
/**
* 片段着色器代码
*/
private val fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}"
- Uniform是GLSL的关键字,是一种从CPU中的应用向GPU中的着色器发送数据的方式;与普通attribute不同的是,uniform是全局的(Global),即uniform变量在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。
- vColor是开发者自定义的变量名;
- gl_FragColor是GLSL的内建变量:片段着色器对象,这里把我们自定义的vColor赋值过去,后面我们会在着色器程序中取出来并进行操作;
2.4.4 编译着色器代码
为了能够让OpenGL使用上述着色器代码,首先需要在运行时动态编译它的源代码。编译操作只需执行一次,一般放在绘制对象的构造函数中完成。
/**
* 编译着色器
* @param type 表示着色器的类型:GLES20.GL_VERTEX_SHADER GLES20.GL_FRAGMENT_SHADER
* @param shaderCode 着色器源码;即上述硬编码的GLSL代码
**/
private fun loadShader(type: Int, shaderCode: String): Int {
// glCreateShader函数创建一个顶点着色器或者片段着色器,并返回新创建着色器的ID引用
val shader = GLES20.glCreateShader(type)
// 把着色器和代码关联,然后编译着色器
GLES20.glShaderSource(shader, shaderCode)
GLES20.glCompileShader(shader)
return shader
}
2.4.5 着色器程序
如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
class Triangle {
init {
// 编译顶点着色器和片段着色器
val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
// glCreateProgram函数创建一个着色器程序,并返回新创建程序对象的ID引用
mProgram = GLES20.glCreateProgram().also {
// 把顶点着色器添加到程序对象
GLES20.glAttachShader(it, vertexShader)
// 把片段着色器添加到程序对象
GLES20.glAttachShader(it, fragmentShader)
// 连接并创建一个可执行的OpenGL ES程序对象
GLES20.glLinkProgram(it)
}
}
}
2.4.6 总结
下面将上面几个流程串联起来,在实际绘制时执行的方法draw()中激活着色器程序,然后操作顶点着色器和片段着色器。下面是绘制三角形的完整流程代码:
class Triangle {
// 三角形三个点的坐标值(逆时针方向,在3D坐标系中,方向决定了哪面是正面)
private var triangleCoords = floatArrayOf(
0.0f, 0.5f, 0.0f, // top
-0.5f, -0.5f, 0.0f, // bottom left
0.5f, -0.5f, 0.0f // bottom right
)
// 每个顶点的坐标数
const val COORDS_PER_VERTEX = 3
// 设置颜色(分别代表red, green, blue and alpha)
private val color = floatArrayOf(0.63671875f, 0.76953125f, 0.22265625f, 1.0f)
private var vertexBuffer: FloatBuffer =
// 坐标点的数目 * float所占字节
ByteBuffer.allocateDirect(triangleCoords.size * 4)
.order(ByteOrder.nativeOrder()).asFloatBuffer().apply {
// 把坐标添加到FloatBuffer
put(triangleCoords)
// 设置buffer的位置为起始点0
position(0)
}
/**
* 顶点着色器代码;
*/
private val vertexShaderCode =
"attribute vec4 vPosition;" +
"void main() {" +
" gl_Position = vPosition;" +
"}"
/**
* 片段着色器代码
*/
private val fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}"
/**
* 着色器程序ID引用
*/
private var mProgram: Int
init {
// 编译顶点着色器和片段着色器
val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
// glCreateProgram函数创建一个着色器程序,并返回新创建程序对象的ID引用
mProgram = GLES20.glCreateProgram().also {
// 把顶点着色器添加到程序对象
GLES20.glAttachShader(it, vertexShader)
// 把片段着色器添加到程序对象
GLES20.glAttachShader(it, fragmentShader)
// 连接并创建一个可执行的OpenGL ES程序对象
GLES20.glLinkProgram(it)
}
}
private fun loadShader(type: Int, shaderCode: String): Int {
// glCreateShader函数创建一个顶点着色器或者片段着色器,并返回新创建着色器的ID引用
val shader = GLES20.glCreateShader(type)
// 把着色器和代码关联,然后编译着色器
GLES20.glShaderSource(shader, shaderCode)
GLES20.glCompileShader(shader)
return shader
}
private val vertexCount: Int = triangleCoords.size / COORDS_PER_VERTEX
private val vertexStride: Int = COORDS_PER_VERTEX * 4 // 4 bytes per vertex
/**
* 实际绘制时执行的方法
**/
fun draw() {
// 激活着色器程序,把程序添加到OpenGL ES环境
GLES20.glUseProgram(mProgram)
// 获取顶点着色器中的vPosition变量(因为之前已经编译过着色器代码,所以可以从着色器程序中获取);用唯一ID表示
val position = GLES20.glGetAttribLocation(mProgram, "vPosition")
// 允许操作顶点对象position
GLES20.glEnableVertexAttribArray(position)
// 将顶点数据传递给position指向的vPosition变量;将顶点属性与顶点缓冲对象关联
GLES20.glVertexAttribPointer(
position, COORDS_PER_VERTEX, GLES20.GL_FLOAT,
false, vertexStride, vertexBuffer)
// 获取片段着色器中的vColor变量
val colorHandle = GLES20.glGetUniformLocation(mProgram, "vColor")
// 通过colorHandle设置绘制的颜色值
GLES20.glUniform4fv(colorHandle, 1, color, 0)
// 绘制顶点数组;
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount)
// 操作完后,取消允许操作顶点对象position
GLES20.glDisableVertexAttribArray(position)
}
}
绘制结果如下:
三、绘制四边形
OpenGL只支持绘制点、线、三角形。对于绘制四边形,OpenGL ES中的典型方式是使用两个绘制在一起的三角形:
3.1 绘制几何图形的方法
OpenGL ES 提供了两类方法来绘制一个空间几何图形:
- public abstract void glDrawArrays(int mode, int first, int count) 使用VetexBuffer 来绘制,顶点的顺序由vertexBuffer中的顺序指定。
- public abstract void glDrawElements(int mode, int count, int type, Buffer indices) ,可以重新定义顶点的顺序,顶点的顺序由indices Buffer 指定。
3.2 索引缓冲对象
以上两种方式都可以用来绘制四边形,区别在于glDrawElements方式通过另外一个索引数组表示顶点间的绘制顺序,更加灵活。通过索引数组告诉 OpenGL ES 图形管道按什么顺序绘制这些顶点。
同样地,索引数组也需要通过FloatBuffer的形式传递给OpenGL:
// 四个顶点的绘制顺序数组
private val drawOrder = shortArrayOf(0, 1, 2, 0, 2, 3)
// 四个顶点绘制顺序数组的缓冲数组
private val drawListBuffer: ShortBuffer =
ByteBuffer.allocateDirect(drawOrder.size * 2).order(ByteOrder.nativeOrder())
.asShortBuffer().apply {
put(drawOrder)
position(0)
}
3.3 绘制四边形
绘制四边形的代码如下,整体逻辑和绘制三角形类似,不同的是采用了glDrawElements方法进行绘制:
class Square {
// 每个顶点的坐标数
private val COORDS_PER_VERTEX = 3
private var squareCoords = floatArrayOf(
-0.5f, 0.5f, 0.0f, // top left
-0.5f, -0.5f, 0.0f, // bottom left
0.5f, -0.5f, 0.0f, // bottom right
0.5f, 0.5f, 0.0f // top right
)
// 四个顶点的缓冲数组
private val vertexBuffer: FloatBuffer =
ByteBuffer.allocateDirect(squareCoords.size * 4).order(ByteOrder.nativeOrder())
.asFloatBuffer().apply {
put(squareCoords)
position(0)
}
// 四个顶点的绘制顺序数组
private val drawOrder = shortArrayOf(0, 1, 2, 0, 2, 3)
// 四个顶点绘制顺序数组的缓冲数组
private val drawListBuffer: ShortBuffer =
ByteBuffer.allocateDirect(drawOrder.size * 2).order(ByteOrder.nativeOrder())
.asShortBuffer().apply {
put(drawOrder)
position(0)
}
/**
* 顶点着色器代码;
* 暂时将顶点着色器的源代码硬编码在C风格字符串中
*/
private val vertexShaderCode =
"attribute vec4 vPosition;" +
"void main() {" +
" gl_Position = vPosition;" +
"}"
/**
* 片段着色器代码
*/
private val fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}"
// 设置颜色(分别代表red, green, blue and alpha)
private val color = floatArrayOf(0.63671875f, 0.76953125f, 0.22265625f, 1.0f)
/**
* 着色器程序ID引用
*/
private var mProgram: Int
init {
// 编译顶点着色器和片段着色器
val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
// glCreateProgram函数创建一个着色器程序,并返回新创建程序对象的ID引用
mProgram = GLES20.glCreateProgram().also {
// 把顶点着色器添加到程序对象
GLES20.glAttachShader(it, vertexShader)
// 把片段着色器添加到程序对象
GLES20.glAttachShader(it, fragmentShader)
// 连接并创建一个可执行的OpenGL ES程序对象
GLES20.glLinkProgram(it)
}
}
private fun loadShader(type: Int, shaderCode: String): Int {
// glCreateShader函数创建一个顶点着色器或者片段着色器,并返回新创建着色器的ID引用
val shader = GLES20.glCreateShader(type)
// 把着色器和代码关联,然后编译着色器
GLES20.glShaderSource(shader, shaderCode)
GLES20.glCompileShader(shader)
return shader
}
private val vertexStride: Int = COORDS_PER_VERTEX * 4
fun draw() {
// 激活着色器程序 Add program to OpenGL ES environment
GLES20.glUseProgram(mProgram)
// 获取顶点着色器中的vPosition变量(因为之前已经编译过着色器代码,所以可以从着色器程序中获取);用唯一ID表示
val position = GLES20.glGetAttribLocation(mProgram, "vPosition")
// 允许操作顶点对象position
GLES20.glEnableVertexAttribArray(position)
// 将顶点数据传递给position指向的vPosition变量
GLES20.glVertexAttribPointer(
position, COORDS_PER_VERTEX, GLES20.GL_FLOAT,
false, vertexStride, vertexBuffer
)
// 获取片段着色器中的vColor变量
val colorHandle = GLES20.glGetUniformLocation(mProgram, "vColor")
// 通过colorHandle设置绘制的颜色值
GLES20.glUniform4fv(colorHandle, 1, color, 0)
// 按drawListBuffer中指定的顺序绘制四边形
GLES20.glDrawElements(
GLES20.GL_TRIANGLES, drawOrder.size,
GLES20.GL_UNSIGNED_SHORT, drawListBuffer
)
// 操作完后,取消允许操作顶点对象position
GLES20.glDisableVertexAttribArray(position)
}
}
绘制结果如下:
3.4 其他方式
再介绍下不使用索引数组,而是通过glDrawArrays()方式绘制四边形的方式,这种方式需要指定四个顶点相互连接的方式是GLES20.GL_TRIANGLE_STRIP:
class Square {
// 顶点数
private val vertexCount: Int = squareCoords.size / COORDS_PER_VERTEX
fun draw() {
// 省略...
// 按GL_TRIANGLE_FAN方式连接绘制四个顶点
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, vertexCount)
// 省略...
}
}
最后
Android OpenGL开发者文档:developer.android.com/guide/topic…
opengl学习资料:learnopengl-cn.github.io/