一、引言
在Android OpenGL基础(一、绘制三角形四边形)中,我们简单实现了绘制三角形的功能。大家可能会发现,我们声明的是一个标准设备坐标系下的等边三角形:
class Triangle {
// 三角形三个点的坐标值
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
)
}
复制代码
但是实际绘制出来的结果却是:
这是因为OpenGL假设屏幕采用均匀的方形坐标系,所以在把标准坐标系下的坐标绘制到非方形的屏幕上时,就会出现拉伸:
那怎么保证顶点的实际绘制效果与预期一致,绘制出我们想要的等边三角形呢。要解决这个问题,可以通过应用 OpenGL 相机视图和投影模式来转换坐标,这样,我们的图形对象在任何屏幕上都会按正确的比例绘制。
二、坐标系统
在程序中设置了物体的顶点坐标后,OpenGL还需要经过一系列的坐标变换,最终变换到屏幕坐标,才决定了顶点的实际绘制位置。这个过程需要用到几个变换矩阵,最重要的几个分别是模型(Model)、观察(View)、投影(Projection) 三个矩阵。我们的顶点坐标起始于局部坐标(Local Coordinate),它在之后会变为世界坐标(World Coordinate),观察坐标(View Coordinate),裁剪坐标(Clip Coordinate),并最后以屏幕坐标(Screen Coordinate)的形式结束。
下面通过一个例子展示下各个变换过程的主要工作,假设我们要绘制一个正方体和一个三棱锥,在定义了正方体和三棱锥后,到他们实际绘制到屏幕上,经历了以下几个坐标变换:
2.1 局部坐标(Local Coordinate)
首先,我们需要先设置好正方体和三棱锥的顶点位置,这个时候分别在二者自身的局部坐标中设置自身顶点的坐标即可。二者的局部坐标都是一个(-1,1)的标准坐标系,此时二者之间还没有关系。
2.2 世界坐标(World Coordinate)
在各物体的局部坐标设置后,实际世界中的物体是分散放在不同地方的,如下图所示,正方体和三棱锥分别放到了不同不同的位置,这个时候二者之间才有了相互间的位置关系。
正方体和三棱锥的坐标将会从局部变换到世界空间。该变换是由模型矩阵(Model Matrix)实现的。模型矩阵是一种变换矩阵,它能通过对物体进行位移、缩放、旋转来将它置于它本应该在的位置或朝向。
目前Android OpenGL基础(一、绘制三角形四边形)中绘制三角形的例子比较简单,几何体都是放在世界坐标的正中心,所以暂时不需要特殊指定模型矩阵。暂时先只做简单了解。
2.3 观察坐标(View Coordinate)
在世界坐标中摆放完正方体和三棱锥的位置后,接下来需要设置我们想观察的位置,从不同的位置观察世界坐标中的正方体和三棱锥,看到的是不同的样子。例如从2.2小节中camera的位置去观察,看到的结果如下:
观察空间经常被人们称之OpenGL的摄像机(Camera,是个抽象的概念,不是Android手机的相机,不要混淆)。从世界空间转换到观察空间通常是由一系列的位移和旋转的组合来完成,这些组合在一起的变换通常存储在一个观察矩阵(View Matrix)里。
2.4 裁剪坐标(Clip Coordinate)
在设置了观察矩阵后,OpenGL世界的所有物体已经呈现在我们视野前方,但是实际展示的时候并不需要全部展示,因此需要把不展示的部分裁剪掉,绘制的时候忽略裁剪掉的部分,减少绘制时的运算量。决定哪部分可以展示是由投影矩阵决定的(因为使用投影矩阵能将3D坐标投影(Project)到很容易映射到2D的标准化设备坐标系中)。
为了将顶点坐标从观察变换到裁剪空间,我们需要定义一个投影矩阵(Projection Matrix),它指定了一个范围的坐标,比如在每个维度上的-1000到1000。投影矩阵接着会将在这个指定的范围内的坐标变换为标准化设备坐标的范围(-1.0, 1.0),而在矩阵投影范围之外的顶点坐标就会被裁剪掉。
将观察坐标变换为裁剪坐标的投影矩阵有两种形式,每种形式都定义了不同的平截头体。我们可以选择创建一个正射投影矩阵(Orthographic Projection Matrix)或一个透视投影矩阵(Perspective Projection Matrix)。
投影矩阵创建的观察箱(Viewing Box)被称为平截头体(Frustum),每个出现在平截头体范围内的坐标都会最终出现在用户的屏幕上。
2.4.1 正射投影
正射投影矩阵定义了一个类似立方体的平截头箱,如下图所示,它定义了一个裁剪空间,在这空间之外的顶点都会被裁剪掉。创建一个正射投影矩阵需要指定可见平截头体的宽、高和长度。在使用正射投影矩阵变换至裁剪空间之后处于这个平截头体内的所有坐标将不会被裁剪掉。下图的绿色正方体就不会被裁剪掉:
2.4.2 透视投影
透视是指实际生活中,具体我们越远的物体看起来越小。
想要达到透视的效果,需要使用透视投影矩阵。这个矩阵除了给出平截头体范围外,还修改了每个顶点坐标的w值,从而使得离观察者越远的顶点坐标w分量越大。
一个透视平截头体可以被看作一个不均匀形状的箱子,在这个箱子内部的每个坐标都会被映射到裁剪空间上的一个点:
2.4.3 区别
透视投影如左图所示,正射投影如右图所示。透视投影用于实际生活场景。正射投影主要用于二维渲染以及一些建筑或工程的程序,在这些场景中更希望顶点不会被透视所干扰。
2.5 屏幕坐标(Screen Coordinate)
最后的顶点应该被赋值到顶点着色器中的gl_Position,然后OpenGL会使用glViewPort内部的参数来将标准化设备坐标映射到屏幕坐标,每个坐标都关联了手机屏幕上的一个点。这个过程称为视口变换。
在Android OpenGL基础(一、绘制三角形四边形)的例子中,在2.1.2小节中onSurfaceChanged中的设置就是告诉OpenGL映射到屏幕坐标的视口变换。
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)
}
}
复制代码
2.6 总结
OpenGL为上述的每一个步骤都创建了一个变换矩阵:模型矩阵、观察矩阵和投影矩阵
。一个顶点坐标将会根据以下过程被变换到裁剪坐标: