上一节中,借助 OpenGL ES 对二维图形的绘制,我们了解了相关概念。本节,我们来谈一个有趣的问题,坐标变换。
坐标变换对于渲染管线来说,是一个非常重要的概念。我们通过它,将一个三维对象从原始的模型坐标系中,一步一步投射到屏幕坐标系中。而可编程的 vertex shader(顶点渲染器) 给我们自定义变换过程提供了基础。
概述
接下来,我们来看看坐标变换的流程。
如上图所示,modeling transformation、view transformation、projection transformation 发生在顶点着色器中(vertex shader)。而 perspective division 和 view transformation 随后发生,且不可自定义过程。不同的转换,实际上是将要绘制的对象带入到了不同的坐标系中。我们注意到有这样几个坐标系:
- 模型坐标系:主要用以定义描述绘制对象;
- 世界坐标系:将要绘制的对象放置到世界坐标系中。所有的绘制对象需要一个共同的坐标系来决定对象之间的相对位置。正如一个杯子的坐标,只是用来描述杯子本身的形体,我们还需要把它们放到世界空间中,在它的旁边,可能还有茶壶、桌子等等。
- 视坐标:也叫照相机坐标系,你可以想象我们在那个方向、那个位置放置了一个照相机,也可以说,那是我们眼睛的位置。
- 裁剪坐标系:将区域进行裁剪,有些东西可能在视线之外,就需要裁掉;
- 归一化坐标系:OpenGL 认为它所绘制的区域是一个正方形,每个方向上范围在[-1,1]之间。
- 屏幕坐标系:或者说是窗口坐标系,就是将归一化坐标系投射到实际屏幕上。
更加形象的过程,如下图所示:
接下来,我们再来详细看看每种变换过程。
模型变换
模型变换的主要作用,是将对象从模型坐标系中转移到公共的世界坐标系中,模型坐标通常用来描述对象本身。这种变换包括了:平移、旋转、放缩,而变换本身实际上是通过与一个 4*4 的变换矩阵相乘实现的。
平移矩阵:
缩放矩阵:
沿 X 轴旋转矩阵:
沿 Y 轴旋转矩阵:
沿 Z 轴旋转矩阵:
你可能会疑惑,三维世界里的对象当然会使用三维坐标,为什么会使用 4*4 的变换矩阵。
从数学上讲,对于一个三维空间中的对象,旋转、缩放这些变换是矩阵乘法问题,而平移是矩阵加法问题。使用4个分量来描述三维坐标就是为了能够使得平移过程能够以乘法表示,从而使得 p’ = m1*p + m2
(m1 旋转缩放矩阵, m2 为平移矩阵, p 为原向量 ,p’ 为变换后的向量)。转换到 p’ = M*p
的形式。此处还可以参考链接。
同时,我们使用四个分量来描述三维世界的坐标系统实际上是个齐次坐标系统,它可以描述无穷远点。
下文引用维基百科:
一条通过原点 (0, 0) 的线之方程可写作 nx + my = 0,其中 n 及 m 不能同时为 0。以参数表示,则能写成 x = mt, y = − nt。令 Z=1/t,则线上的点之笛卡儿坐标可写作 (m/Z, − n/Z)。在齐次坐标下,则写成 (m, − n, Z)。当 t 趋向无限大,亦即点远离原点时,Z 会趋近于 0,而该点的齐次坐标则会变成 (m, −n, 0)。因此,可定义 (m, −n, 0) 为对应 nx + my = 0 这条线之方向的无穷远点之齐次坐标。因为欧氏平面上的每条线都会与透过原点的某一条线平行,且因为平行线会有相同的无穷远点,欧氏平面每条线上的无穷远点都有其齐次坐标。
在实际操作过程中,我们并不会真的先去计算好这些矩阵,然后再进行变换,android.opengl.Matrix 类能够帮助我们计算好它们,具体可以去查询相关 API 。
//矩阵旋转
Matrix.rotateM(rotationM,0,90,0,1,0);
//矩阵相乘
Matrix.multiplyMM(transformationM,0,eyesMatrix,0,rotationM,0);
视图变换
你可以假设将一个摄像机朝着某个方向在某个位置观察着三维对象,虽然在 OpenGL ES 中并不存在这样的设备。我们都知道运动是相对的,移动摄像机对物体进行拍摄,相对而言,也可以是移动物体,以达到通过移动摄像机而从不同角度观察物体的目的。
这种变换,从本质上而言和矩阵的模型变换并没有差别,实际上所执行的都是矩阵乘法。即,以视图变换矩阵和目标对象相乘。具体的推到过程这里将省略,我们可以使用 Matrix 类提供的方法获取:
API 原型为:
/**
* Defines a viewing transformation in terms of an eye point, a center of
* view, and an up vector.
*
* @param rm returns the result
* @param rmOffset index into rm where the result matrix starts
* @param eyeX eye point X
* @param eyeY eye point Y
* @param eyeZ eye point Z
* @param centerX center of view X
* @param centerY center of view Y
* @param centerZ center of view Z
* @param upX up vector X
* @param upY up vector Y
* @param upZ up vector Z
*/
public static void setLookAtM(float[] rm, int rmOffset,
float eyeX, float eyeY, float eyeZ,
float centerX, float centerY, float centerZ, float upX, float upY,
float upZ) {
......
}
投影变换
投影变换,从技术上讲是将对象从视坐标系下转义到裁剪坐标系下。它是在顶点着色器返回 gl_Position 之前进行的最后一次变换,接着,是通过透视除法(w分量),将裁剪坐标系转到归一化坐标系下。
我们最常用的投影是正交投影和透视投影。
对于正交投影,使用平行光线对三维对象进行投影,所以所投影出来的影像没有现实世界中远近的概念。在 Android 应用开发中,可以使用以下函数来获得变换矩阵:
public static void orthoM(float[] m, int mOffset,
float left, float right, float bottom, float top,
float near, float far)
由上可知,至少我们可以通过正交投影来设置要显示的区域。比如,为了让显示图形在显示的时候不发生压缩等形变:
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
// 根据屏幕方向设置投影矩阵
float ratio= width > height ? (float)width / height : (float)height / width;
if (width > height) {
// 横屏
Matrix.orthoM(projectionMatrix, 0, -ratio, ratio, -1, 1, 0, 5);
} else {
Matrix.orthoM(projectionMatrix, 0, -1, 1, -ratio, ratio, 0, 5);
}
}
下面我们来看下透视投影。
对于透视投影,观察空间是一个视椎体,远端大近端小,投射光线显然不是平行线,这样物体投射出来就有了远近的概念。离眼睛越远的地方,物体越小,越近,同一个物体显示越大。
public static void frustumM(float[] m, int offset,
float left, float right, float bottom, float top,
float near, float far)
视口变换
该过程是自动执行且固定不变的,用以进行从归一化坐标系到实际屏幕坐标系的转换过程。
小结
整个过程,我觉得有点像拍电影。将若干人物(模型坐标)集中在一个拍摄场景(世界坐标)中,在拍摄过程中,会进行镜头移动到一个固定角度(视图坐标),调好焦距(投影坐标),进行拍摄。
参考链接:
GLSL Programming/Vertex Transformations
OpenGL ES 投影变换 Projection
Article - World, View and Projection Transformation Matrices
这次,彻底搞懂 OpenGL 矩阵转换