0%

Android-OpenGL-ES笔记

概述

  • OpenGL(Open Graphics Library)是一个用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API)。
  • OpenGl一般用于在图形工作站,PC端使用,由于性能各方面原因,在移动端基本带不动OpenGl。为此,KhronosGroup 为 OpenGl 提供了一个子集,OpenGl ES(OpenGl for Embedded System)。
  • OpenGl ES是免费的跨平台的功能完善的2D/3D图形库接口的API,是OpenGL的一个子集。
  • 移动端使用到的基本上都是OpenGl ES,当然Android开发下还专门为OpenGl提供了android.opengl包,并且提供了GlSurfaceView, GLU, GlUtils等工具类。

Android 支持多版 OpenGL ES 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)及更高版本的支持。

Android 通过其 Framework API 和 Native开发套件(NDK) 来支持 OpenGL,本文主要讲解 Framework API。Android Framework 中有两个基本类,用于通过 OpenGL ES API 来创建和操控图形:GLSurfaceView 和 GLSurfaceView.Renderer。

参考:Android-OpenGl-ES官方文档

EGL 是 OpenGL ES 渲染 API 和本地窗口系统(native platform window system)之间的一个中间接口层,它主要由系统制造商实现。为了让 OpenGL ES能够绘制在当前设备上,需要 EGL 作为 OpenGL ES 与设备的桥梁。

GlSurfaceView

GlSurfaceView是一个 View,它继承自SurfaceView,并增加了Renderer,它的作用就是专门为 OpenGl 显示渲染使用的。使用方法:

1.创建一个GlSurfaceView
2.为这个GlSurfaceView设置Renderer
3.在GlSurfaceView.renderer中绘制处理显示数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GLSurfaceView glSurfaceView = new GLSurfaceView(this);
glSurfaceView.setRenderer(new GLSurfaceView.Renderer() {
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {

}

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {

}

@Override
public void onDrawFrame(GL10 gl) {

}
});
setContentView(glSurfaceView);
}

GlSurfaceView.Renderer

  • onSurfaceCreated(): 系统会在创建 GLSurfaceView 时调用一次此方法。使用此方法可执行仅需发生一次的操作,例如设置 OpenGL 环境参数或初始化 OpenGL 图形对象。
  • onDrawFrame(): 系统会在每次重新绘制 GLSurfaceView 时调用此方法。请将此方法作为绘制(和重新绘制)图形对象的主要执行点。
  • onSurfaceChanged(): 系统会在 GLSurfaceView 几何图形发生变化(包括 GLSurfaceView 大小发生变化或设备屏幕方向发生变化)时调用此方法。使用此方法可响应 GLSurfaceView 容器中的更改。

声明OpenGL

如果应用使用的 OpenGL 功能不一定在所有设备上可用,则必须在 AndroidManifest.xml 文件中包含这些要求。以下是最常见的 OpenGL 清单声明:

  • OpenGL ES 版本要求 - 如果应用需要特定版本的 OpenGL ES,则必须通过将以下设置添加到 Manifest 中来声明该要求,如下所示。

    1
    2
    3
    4
    5
    6
    <!-- Tell the system this app requires OpenGL ES 2.0. -->
    <uses-feature android:glEsVersion="0x00020000" android:required="true" />
    <!-- Tell the system this app requires OpenGL ES 3.0. -->
    <uses-feature android:glEsVersion="0x00030000" android:required="true" />
    <!-- Tell the system this app requires OpenGL ES 3.1. -->
    <uses-feature android:glEsVersion="0x00030001" android:required="true" />
  • 纹理压缩要求 - 如果应用使用了纹理压缩格式,则必须使用 <supports-gl-texture> 在 Manifest 文件中声明应用支持的格式,如需详细了解可用的纹理
    压缩格式,可参阅纹理压缩支持

简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 使用 GLSurfaceView 作为主要视图的 Activity 的最少实现
class OpenGLES20Activity : Activity() {
private lateinit var gLView: GLSurfaceView

public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
gLView = MyGLSurfaceView(this)
setContentView(gLView)
}

class MyGLSurfaceView(context: Context) : GLSurfaceView(context) {
private val renderer: MyGLRenderer

init {
// Create an OpenGL ES 2.0 context
setEGLContextClientVersion(2)
renderer = MyGLRenderer()
setRenderer(renderer)
// 该设置可防止系统在调用 requestRender() 之前重新绘制 GLSurfaceView 帧,更为高效。
// Render the view only when there is a change in the drawing data
renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY
}
}

class MyGLRenderer : GLSurfaceView.Renderer {

override fun onSurfaceCreated(unused: GL10, config: EGLConfig) {
// Set the background frame color
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
}

override fun onDrawFrame(unused: GL10) {
// Redraw background color
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
}

override fun onSurfaceChanged(unused: GL10, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
}
}
}

定义形状

默认情况下,OpenGL ES 会假定一个坐标系,其中 [0,0,0] (X,Y,Z) 指定 GLSurfaceView 帧的中心,[1,1,0] 指定帧的右上角,而 [-1,-1,0] 指定帧的左下角,此形状的坐标是按照逆时针顺序定义的。

定义三角形

通过 OpenGL ES 可以使用三维空间中的坐标定义绘制的对象,在 OpenGL 中,执行此操作的典型方式是为坐标定义浮点数的顶点数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// number of coordinates per vertex in this array
const val COORDS_PER_VERTEX = 3
var triangleCoords = floatArrayOf( // in counterclockwise order:
0.0f, 0.622008459f, 0.0f, // top
-0.5f, -0.311004243f, 0.0f, // bottom left
0.5f, -0.311004243f, 0.0f // bottom right
)

class Triangle {

// Set color with red, green, blue and alpha (opacity) values
val color = floatArrayOf(0.63671875f, 0.76953125f, 0.22265625f, 1.0f)

// (number of coordinate values * 4 bytes per float)
private var vertexBuffer: FloatBuffer = ByteBuffer.allocateDirect(triangleCoords.size * 4).run {
// use the device hardware's native byte order
order(ByteOrder.nativeOrder())

// create a floating point buffer from the ByteBuffer
asFloatBuffer().apply {
// add the coordinates to the FloatBuffer
put(triangleCoords)
// set the buffer to read the first coordinate
position(0)
}
}
}

定义方形

有多种方式可以定义方形,但在 OpenGL ES 中绘制此类形状的典型方式是使用两个绘制在一起的三角形:

定义方形

对于表示该形状的两个三角形,应按逆时针顺序定义顶点,并将这些值放入 ByteBuffer 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// number of coordinates per vertex in this array
const val COORDS_PER_VERTEX = 3
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
)

class Square2 {

private val drawOrder = shortArrayOf(0, 1, 2, 0, 2, 3) // order to draw vertices

// initialize vertex byte buffer for shape coordinates
// (# of coordinate values * 4 bytes per float)
private val vertexBuffer: FloatBuffer = ByteBuffer.allocateDirect(squareCoords.size * 4).run {
order(ByteOrder.nativeOrder())
asFloatBuffer().apply {
put(squareCoords)
position(0)
}
}

// initialize byte buffer for the draw list
// (# of coordinate values * 2 bytes per short)
private val drawListBuffer: ShortBuffer = ByteBuffer.allocateDirect(drawOrder.size * 2).run {
order(ByteOrder.nativeOrder())
asShortBuffer().apply {
put(drawOrder)
position(0)
}
}
}

绘制形状

初始化形状

在进行任何绘制之前必须初始化并加载打算绘制的形状,除非在程序中使用的形状结构(原始坐标)在执行过程中发生变化,否则应该在渲染程序的 onSurfaceCreated() 方法中对它们进行初始化,以提高内存和处理效率。

1
2
3
4
5
6
7
8
9
10
11
class MyGLRenderer : GLSurfaceView.Renderer {
private lateinit var mTriangle: Triangle
private lateinit var mSquare: Square

override fun onSurfaceCreated(unused: GL10, config: EGLConfig) {
// initialize a triangle
mTriangle = Triangle()
// initialize a square
mSquare = Square()
}
}

绘制形状

使用 OpenGL ES 2.0 绘制定义的形状需要大量代码,因为必须向图形渲染管道提供大量信息,具体来说需要定义以下内容:

  • 顶点(Vertex)着色程序 - 用于渲染形状的顶点的 OpenGL ES 图形代码。
  • 片段(Fragment)着色程序 - 用于使用颜色或纹理渲染形状面的 OpenGL ES 代码。
  • 程序 - 包含希望用于绘制一个或多个形状的着色程序的 OpenGL ES 对象。

至少需要一个顶点着色程序绘制形状,以及一个 Fragment 着色程序为该形状着色,还必须对这些着色程序进行编译,然后将其添加到之后用于绘制形状的 OpenGL ES 程序中。以下示例展示了如何定义可用于绘制 Triangle 类中的形状的基本着色程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Triangle {
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;" +
"}"
}

着色程序包含 OpenGL 着色语言 (GLSL) 代码,必须先对其进行编译,然后才能在 OpenGL ES 环境中使用。要编译此代码,可以在渲染程序类中创建一个实用程序方法:

1
2
3
4
5
6
7
8
9
10
11
fun loadShader(type: Int, shaderCode: String): Int {

// create a vertex shader type (GLES20.GL_VERTEX_SHADER)
// or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
return GLES20.glCreateShader(type).also { shader ->

// add the source code to the shader and compile it
GLES20.glShaderSource(shader, shaderCode)
GLES20.glCompileShader(shader)
}
}

要绘制形状,必须编译着色程序代码,将它们添加到 OpenGL ES 程序对象中,然后关联该程序。该操作需要在绘制对象的构造函数中完成,因此只需执行一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Triangle {
private var mProgram: Int

init {
val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)

// create empty OpenGL ES Program
mProgram = GLES20.glCreateProgram().also {

// add the vertex shader to program
GLES20.glAttachShader(it, vertexShader)

// add the fragment shader to program
GLES20.glAttachShader(it, fragmentShader)

// creates OpenGL ES program executables
GLES20.glLinkProgram(it)
}
}
}

此时可以添加绘制形状的实际调用,使用 OpenGL ES 绘制形状时需要指定多个参数,以告知渲染管道要绘制的形状以及如何进行绘制。由于绘制选项因形状而异,因此最好使形状类包含自身的绘制逻辑。

创建用于绘制形状的 draw() 方法,此代码将位置和颜色值设置为形状的顶点着色程序和 Fragment 着色程序,然后执行绘制功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private var positionHandle: Int = 0
private var mColorHandle: Int = 0

private val vertexCount: Int = triangleCoords.size / COORDS_PER_VERTEX
private val vertexStride: Int = COORDS_PER_VERTEX * 4 // 4 bytes per vertex

fun draw() {
// Add program to OpenGL ES environment
GLES20.glUseProgram(mProgram)

// get handle to vertex shader's vPosition member
positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition").also {

// Enable a handle to the triangle vertices
GLES20.glEnableVertexAttribArray(it)

// Prepare the triangle coordinate data
GLES20.glVertexAttribPointer(
it,
COORDS_PER_VERTEX,
GLES20.GL_FLOAT,
false,
vertexStride,
vertexBuffer
)

// get handle to fragment shader's vColor member
mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor").also { colorHandle ->

// Set color for drawing the triangle
GLES20.glUniform4fv(colorHandle, 1, color, 0)
}

// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount)

// Disable vertex array
GLES20.glDisableVertexAttribArray(it)
}
}

将所有的代码编译完成之后,只需从渲染程序的 onDrawFrame() 方法中调用 draw() 方法即可绘制此对象:

1
2
3
override fun onDrawFrame(unused: GL10) {
mTriangle.draw()
}

在横屏手机上绘制结果如下:

三角形-无转换

形状偏斜的原因在于,对象的顶点尚未针对显示 GLSurfaceView 的屏幕区域的比例进行校正。

投影和相机视图

在 Android 设备上显示图形时,一个基本问题在于屏幕的尺寸和形状各不相同,OpenGL 假设屏幕采用均匀的方形坐标系。

OpenGL-Android坐标系

上图左侧显示了针对 OpenGL 假定的均匀坐标系,右侧显示了坐标实际上如何映射到右侧屏幕方向为横向的设备屏幕上。要解决此问题,可以通过应用 OpenGL 投影模式和相机视图来转换坐标,这样,图形对象在任何屏幕上都具有正确的比例。

  • 投影:根据显示绘制对象的 GLSurfaceView 的宽度和高度调整绘制对象的坐标。通常只有在渲染程序的 onSurfaceChanged() 方法中确定或更改 OpenGL 视图的比例时,才需要计算投影转换。
  • 相机视图:根据虚拟相机的位置调整绘制对象的坐标。相机视图转换可能在确定 GLSurfaceView 时计算一次,也可能会根据用户操作或应用的功能动态变化。

定义投影

用于投影转换的数据使用 GLSurfaceView.Renderer 的 onSurfaceChanged() 方法计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// vPMatrix is an abbreviation for "Model View Projection Matrix"--模型视图投影矩阵
private val vPMatrix = FloatArray(16)
private val projectionMatrix = FloatArray(16)
private val viewMatrix = FloatArray(16)

override fun onSurfaceChanged(unused: GL10, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)

val ratio: Float = width.toFloat() / height.toFloat()

// this projection matrix is applied to object coordinates
// in the onDrawFrame() method
Matrix.frustumM(projectionMatrix, 0, -ratio, ratio, -1f, 1f, 3f, 7f)
}

定义相机视图

通过在渲染程序中添加相机视图转换作为绘制流程的一部分,完成绘制对象的转换流程。在以下示例代码中,相机视图转换使用 Matrix.setLookAtM() 方法进行计算,然后与之前计算的投影矩阵合并。之后,系统会将合并后的转换矩阵传递到绘制的形状。

1
2
3
4
5
6
7
8
9
10
override fun onDrawFrame(unused: GL10) {
// Set the camera position (View matrix)
Matrix.setLookAtM(viewMatrix, 0, 0f, 0f, -3f, 0f, 0f, 0f, 0f, 1.0f, 0.0f)

// Calculate the projection and view transformation
Matrix.multiplyMM(vPMatrix, 0, projectionMatrix, 0, viewMatrix, 0)

// Draw shape
mTriangle.draw(vPMatrix)
}

应用投影和相机转换

为了使用预览部分中显示的合并后的投影和相机视图转换矩阵,请先将矩阵变体添加到之前在 Triangle 类中定义的顶点着色程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Triangle {
private val vertexShaderCode =
// This matrix member variable provides a hook to manipulate
// the coordinates of the objects that use this vertex shader
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"void main() {" +
// the matrix must be included as a modifier of gl_Position
// Note that the uMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
" gl_Position = uMVPMatrix * vPosition;" +
"}"

// Use to access and set the view transformation
private var vPMatrixHandle: Int = 0
}

接下来,修改图形对象的 draw() 方法以接受合并后的转换矩阵,并将其应用于形状:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
fun draw(mvpMatrix: FloatArray) {
// Add program to OpenGL ES environment
GLES20.glUseProgram(mProgram)

// get handle to vertex shader's vPosition member
positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition").also {

// Enable a handle to the triangle vertices
GLES20.glEnableVertexAttribArray(it)

// Prepare the triangle coordinate data
GLES20.glVertexAttribPointer(
it,
COORDS_PER_VERTEX,
GLES20.GL_FLOAT,
false,
vertexStride,
vertexBuffer
)

// get handle to fragment shader's vColor member
mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor").also { colorHandle ->

// Set color for drawing the triangle
GLES20.glUniform4fv(colorHandle, 1, color, 0)
}

// get handle to shape's transformation matrix
vPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix")

// Pass the projection and view transformation to the shader
GLES20.glUniformMatrix4fv(vPMatrixHandle, 1, false, mvpMatrix, 0)

// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount)

// Disable vertex array
GLES20.glDisableVertexAttribArray(positionHandle)
}
}

这样就能按照正确的比例显示形状了。

添加动画

旋转形状

使用 OpenGL ES 2.0 旋转绘制的对象相对比较简单。在渲染程序中,再创建一个转换矩阵(旋转矩阵),然后将其与投影和相机视图转换矩阵相结合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private val rotationMatrix = FloatArray(16)

override fun onDrawFrame(gl: GL10) {
val scratch = FloatArray(16)

// ...

// Create a rotation transformation for the triangle
val time = SystemClock.uptimeMillis() % 4000L
val angle = 0.090f * time.toInt()
Matrix.setRotateM(rotationMatrix, 0, angle, 0f, 0f, -1.0f)

// Combine the rotation matrix with the projection and camera view
// Note that the vPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
Matrix.multiplyMM(scratch, 0, vPMatrix, 0, rotationMatrix, 0)

// Draw triangle
mTriangle.draw(scratch)
}

如果三角形在进行这些更改后没有旋转,请确保已对 GLSurfaceView.RENDERMODE_WHEN_DIRTY 设置取消备注。

启用连续渲染

请确保对将渲染模式设置为仅在脏时才进行绘制的行取消备注,否则 OpenGL 仅按一个增量旋转形状,然后就等待从 GLSurfaceView 容器调用 requestRender():

1
2
3
4
5
6
7
class MyGLSurfaceView(context: Context) : GLSurfaceView(context) {
init {
// Render the view only when there is a change in the drawing data.
// To allow the triangle to rotate automatically, this line is commented out:
//renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY
}
}

除非在没有任何用户互动的情况下更改对象,否则通常最好启用此标记。

响应触摸事件

设置触摸监听器

为了使 OpenGL ES 应用响应触摸事件,需要在 GLSurfaceView 类中实现 onTouchEvent() 方法。以下示例实现展示了如何监听 MotionEvent.ACTION_MOVE 事件,并将其平移到某个形状的旋转角度。这里需要取消上面renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY的注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private const val TOUCH_SCALE_FACTOR: Float = 180.0f / 320f
...
private var previousX: Float = 0f
private var previousY: Float = 0f

override fun onTouchEvent(e: MotionEvent): Boolean {
// MotionEvent reports input details from the touch screen
// and other input controls. In this case, you are only
// interested in events where the touch position changed.

val x: Float = e.x
val y: Float = e.y

when (e.action) {
MotionEvent.ACTION_MOVE -> {

var dx: Float = x - previousX
var dy: Float = y - previousY

// reverse direction of rotation above the mid-line
if (y > height / 2) {
dx *= -1
}

// reverse direction of rotation to left of the mid-line
if (x < width / 2) {
dy *= -1
}

renderer.angle += (dx + dy) * TOUCH_SCALE_FACTOR
requestRender()
}
}

previousX = x
previousY = y
return true
}

线程安全

上面的代码需要公开 angle 成员,由于渲染程序代码在独立于应用的主界面线程的线程上运行,因此必须将此公开变量声明为 volatile。

1
2
3
4
class MyGLRenderer : GLSurfaceView.Renderer {
@Volatile
var angle: Float = 0f
}

应用旋转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
override fun onDrawFrame(gl: GL10) {
// ...
val scratch = FloatArray(16)

// Create a rotation for the triangle
// long time = SystemClock.uptimeMillis() % 4000L;
// float angle = 0.090f * ((int) time);
Matrix.setRotateM(rotationMatrix, 0, angle, 0f, 0f, -1.0f)

// Combine the rotation matrix with the projection and camera view
// Note that the mvpMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
Matrix.multiplyMM(scratch, 0, mvpMatrix, 0, rotationMatrix, 0)

// Draw triangle
triangle.draw(scratch)
}

这样就可以实现拖动旋转三角形了。