Android OpenGL 开发3 - 坐标系以及矩阵变换

上一篇教程中我们学习了顶点的定义,Shader以及画一个简单的三角形。

在这一篇中,我们将介绍一下OpenGL的另外一个重要的概念:坐标系统。

概述

OpenGL希望在所有顶点着色器运行后,所有我们可见的顶点都变为标准化设备坐标(Normalized Device Coordinate, NDC)。也就是说,每个顶点的x,y,z坐标都应该在-1.0到1.0之间,超出这个坐标范围的顶点都将不可见。我们通常会自己设定一个坐标的范围,之后再在顶点着色器中将这些坐标转换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器(Rasterizer),再将他们转换为屏幕上的二维坐标或像素。

将坐标转换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步,也就是类似于流水线那样子。将对象转换到屏幕空间之前会先将其依次转换到多个坐标系统(Coordinate System)。

对我们来说比较重要的总共有5个不同的坐标系统:

  • 局部空间(Local Space,或者称为物体空间(Object Space))。 是指对象所在的坐标空间,例如,对象最开始所在的地方。例如我们定义一个物体给的初始坐标,该坐标位置无关,只是表示自己的大小形状等。它最后被放置的位置有可能位于(0,0,0),也有可能位于完全不同的另外一个位置。
  • 世界空间(World Space) : 我们想为每一个对象定义一个位置,从而使对象位于更大的世界当中。世界空间(World Space)中的坐标就如它们听起来那样:是指顶点相对于(游戏)世界的坐标。对象的坐标会从局部坐标转换到世界坐标的转换是由模型矩阵(Model Matrix)实现的。
  • 观察空间(View Space,或者称为视觉空间(Eye Space)): 观察空间(View Space)经常被人们称之OpenGL的摄像机(Camera)(所以有时也称为摄像机空间(Camera Space)或视觉空间(Eye Space))。观察空间就是将对象的世界空间的坐标转换为观察者视野前面的坐标。因此观察空间就是从摄像机的角度观察到的空间。通常需要一个观察矩阵(View Matrix),用来将世界坐标转换到观察空间
  • 裁剪空间(Clip Space): 在一个顶点着色器运行的最后,OpenGL期望所有的坐标都能落在一个给定的范围内,且任何在这个范围之外的点都应该被裁剪掉(Clipped)。被裁剪掉的坐标就被忽略了,所以剩下的坐标就将变为屏幕上可见的片段。这也就是裁剪空间(Clip Space)名字的由来。为了将顶点坐标从观察空间转换到裁剪空间,我们需要定义一个投影矩阵(Projection Matrix)。
  • 屏幕空间(Screen Space):

why

将对象的坐标转换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中进行一些操作或运算更加方便和容易。

我们之所以将顶点转换到各个不同的空间的原因是有些操作在特定的坐标系统中才有意义且更方便。例如,当修改对象时,如果在局部空间中则是有意义的;当对对象做相对于其它对象的位置的操作时,在世界坐标系中则是有意义的;等等这些。如果我们愿意,本可以定义一个直接从局部空间到裁剪空间的转换矩阵,但那样会失去灵活性。

空间转换

为了将坐标从一个坐标系转换到另一个坐标系,我们需要用到几个转换矩阵,最重要的几个分别是模型(Model)、视图(View)、投影(Projection)三个矩阵。首先,顶点坐标开始于局部空间(Local Space),称为局部坐标(Local Coordinate),然后经过世界坐标(World Coordinate),观察坐标(View Coordinate),裁剪坐标(Clip Coordinate),并最后以屏幕坐标(Screen Coordinate)结束。下面的图示显示了整个流程及各个转换过程做了什么:

我们为上述的每一个步骤都创建了一个转换矩阵:模型矩阵、观察矩阵和投影矩阵。一个顶点的坐标将会根据以下过程被转换到裁剪坐标:

1
Vclip=Mprojection⋅Mview⋅Mmodel⋅Vlocal

Android中对应的函数

Android中为上述的矩阵的构造提供了相应的utility方法,简化了我们的开发。

  • 模型矩阵

模型矩阵不必说了,直接定义顶点构造即可,就像上一篇中三角形的顶点坐标的构造。

  • 观察矩阵

Android提供了Matrix.setLookAtM()方法来构造观察矩阵

1
2
3
4
5
6
7
8
9
10
11
void setLookAtM (float[] rm, //returns the result
int rmOffset, // index into rm where the result matrix starts
float eyeX, //eye point X
float eyeY, //eye point Y
float eyeZ, //eye point Z
float centerX, //center of view X
float centerY, //center of view Y
float centerZ, //center of view Z
float upX, //up vector X
float upY, //up vector Y
float upZ) //up vector Z
  • 投影矩阵

Android提供了Matrix.frustumM() & Matrix.perspectiveM()方法来构造投影矩阵。
这里只列一下常用perspective投影:

1
2
3
4
5
6
void perspectiveM (float[] m, //the float array that holds the perspective matrix
int offset, //the offset into float array m where the perspective matrix data is written
float fovy, //field of view in y direction, in degrees
float aspect, //width to height aspect ratio of the viewport
float zNear,
float zFar)

实际例子

接下来为我们上一篇中画的三角形加上矩阵变换:

  • Projection

Projectin矩阵的计算需要放到你的Render的 onSurfaceChanged()函数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// mMVPMatrix is an abbreviation for "Model View Projection Matrix" 
private final float[] mMVPMatrix = new float[16];
private final float[] mProjectionMatrix = new float[16];
private final float[] mViewMatrix = new float[16];

@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);

float ratio = (float) width / height;

// this projection matrix is applied to object coordinates
// in the onDrawFrame() method
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
  • Camera view

通常在每次render之前,就要计算实时的camera view, 所以我们把camera view的计算放到onDrawFrame中:

1
2
3
4
5
6
7
8
9
10
11
12
@Override 
public void onDrawFrame(GL10 unused) {

...
// Set the camera position (View matrix)
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);

// Calculate the projection and view transformation
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);

// Draw shape
mTriangle.draw(mMVPMatrix);
}
  • 应用Projection and Camera Transformations

首先在vertex shader中加上一个matrix,并应用:

1
2
3
4
5
6
7
8
9
10
//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;" +
"}";

最后在GLTriangle中计算mvp matrix,然后传递给shader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class GLTriangle {
private int mMVPMatrixHandle;

public void draw(float[] mvpMatrix) { // pass in the calculated transformation matrix
...

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

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

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

// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);

}

Reference