在使用WebGL的过程中,我们会涉及到大量的坐标系,各个坐标系有各自的使用场景,我们需要通过坐标变换来将各个坐标系联系起来。今天我们就来了解一下WebGL中涉及到的各个坐标系,以及各个坐标系之间是如何进行坐标变换的。
WebGL 坐标系
模型坐标系
模型坐标系是一种基于模型本身的坐标系,坐标系的原点一般在模型的正中心(并非绝对,可使用模型中的任意一点作为原点)。通常用于描述模型中的各个顶点的在模型中的位置,遵循右手坐标系,即 X 轴向右,Y 轴向上,Z 轴朝向屏幕外。
世界坐标系
我们的一个画面当中不可能只有一个物体模型,所以单单只有一个模型坐标系肯定是不够的。我们需要把物体模型存放到世界坐标系中。世界坐标系能够承载多个物体模型,当物体模型需要放置到世界坐标系中时,需要对物体模型的模型坐标相对于世界坐标系的原点进行转换。
世界坐标系同样遵循右手坐标系。
观察坐标系
人眼或者摄像机位于世界的某一点观察这个世界,以这个观察点为原点的坐标系就叫做观察坐标系。
作为一名普通的人类,我们无法时时刻刻都在世界的中心(世界坐标系的原点)去感受世界,我们只能基于我们当前所处的位置去观察这个世界,观察坐标系就是因此而产生的。
裁剪坐标系
世界很大,但屏幕很小。虽然我们可以位于世界的任意一点去观察世界,但小小的屏幕缺容纳不下整个世界。所以我们需要一种坐标系去限制世界的可视范围,裁剪坐标系华丽登场!
要将顶点坐标从观察坐标系变换到裁剪坐标系,我们需要定义一个投影矩阵(Projection Matrix)。投影矩阵用于指定一个范围的坐标,每个坐标轴的取值范围都在[-1000,1000]内。投影矩阵会将在这个区间范围内的所有坐标变换为标准化设备坐标范围[-1.0, 1.0]。
我们称这个范围所形成的空间为平截头体(Frustum)。所有位于平截头体外的顶点都会被裁剪掉。
裁剪坐标系中的坐标转化到标准化设备坐标系的过程被称之为投影(Projection),使用投影矩阵(Projection Matrix)能将 3D 坐标投影映射到 2D 的标准设备坐标系中。
将观察坐标变换为裁剪坐标的投影矩阵一般分为两种,一种为正交投影矩阵,另一种为透视投影矩阵。
正交投影变换
正交投影矩阵会创建的是一个立方体的平截头体,平截头体之外的坐标都会被裁剪掉。 正交投影矩阵需要指定平截头体的长度、宽度和高度。
经过投影变换的坐标都会新增一个w分量,即(0,0,0)会变成(0,0,0,w)。w分量用于描述一个顶点离观察点的距离所带来的影响。
正交投影变换中,w 分量不会随着与观察点的距离的变化而发生改变,始终为 1。这意味着经过正交投影变换后,物体模型不会发生改变,不符合近大远小的规律,产生的画面不够真实。
透视投影变换
近大远小是我们在真实世界中观察物体的规律,这种效果我们称之为透视。
经过透视投影变换的坐标新增的w分量会随着与观察点的距离的变化而发生改变,距离观察点越远,w分量越大,给坐标带来影响越大。
经过透视投影变换后,超出 [ -w,w ] 的范围会被认为是不可视的,会被裁剪掉(太远了,看不到了)。
裁剪坐标系遵循左手坐标系, 裁剪坐标系的坐标也就是 gl_Position 接收的坐标。
NDC 坐标系
裁剪坐标系到NDC 坐标系的变化很简单,GPU 会对裁剪坐标执行透视除法,即将顶点坐标的 x、y、x 分量分别除以齐次 w 分量。由于距离观察点越远,w越大,经过变换后 x、y、x 分量,符合我们人眼近大远小的规律。 NDC 坐标系中,所有坐标分量的范围都会在 [-1,1] 之间。
屏幕坐标系
屏幕坐标系就十分容易理解了,就是我们日常开发所接触的坐标系。
WebGL 坐标变换流程
坐标变换的整体流程
当我们想通过WebGL在canvas标签上画出一个点是,我们需要将这个点的坐标经过一些了的变换之后,然后再把这个坐标赋值给 gl_Position。
在这个过程中,我们需要对坐标点进行如下的变换:
- 模型变换:模型坐标系 > 世界坐标系
- 视图变换:世界坐标系 > 观察坐标系
- 投影变换:观察坐标系 > 裁剪坐标系
在坐标赋值给 gl_Position 后,此时坐标数据已经从CPU传输到了GPU,顶点着色器会替我们完成以下的变换:
- 透视除法:裁剪坐标 > NDC 坐标
- 视口变换:NDC坐标 > 屏幕坐标
坐标系之间的变换我们可以分为这么几步:
- 缩放变换:计算出新坐标系坐标分量的单位向量在原坐标系下的长度, 再使用顶点坐标 * 此长度。
- 旋转变换:计算出原坐标系的坐标分量(基向量)的在新坐标系的方向, 再使用顶点坐标 * 此方向变量。
- 平移变换:计算出原坐标系的原点O在新坐标系下的坐标,将顶点 + 变换后的原点坐标 = 顶点在新坐标系下的坐标。
我们以一个点A为例,该点为一个边长为4的正方体上的一点,在以该正方体的中心为原点的模型坐标系中,该点的坐标为 (4, 4, 0)。
模型变换
接下来我们把点A所处的正方体模型放置到一个世界坐标系当中,并把正方体的中点放置到世界坐标系的点 (1, 1, 0) 中。 那顶点 A 在世界坐标系的坐标也就变成了:
// 平移变换
A = (4, 4, 0) + (1, 1, 0) = (5, 5, 0);
视图变换
此时,我们再站在世界坐标系的点B (2, 2, 0) 处观察物体。 点 B 所看到的世界处于观察坐标系中,由于观察坐标系遵循左手坐标系,X 轴、Y 轴和世界坐标系一致,但 Z 轴指向屏幕里面,与世界坐标系相反,所以,我们可以得到顶点 A 在观察坐标系的坐标为:
//选择变换
A = (5, 5, 0) * (1, 1, -1) = (5, 5, 0)
// 平移变换
//计算原坐标系的原点O在新坐标系下的坐标
O = - B = (-2, -2, 0)
//将顶点 + 变换后的原点坐标
A = (5, 5, 0) + (-2, -2, 0) = (3, 3, 0)
投影变换
此时我们为裁剪坐标系指定一个正交投影的平截头体,观察箱在所有坐标轴上的数据范围都为 [-6, 6] 。所以顶点A转变到裁剪坐标系下的坐标为:
(3 / (6 - (-6)) / 2, 3 / (6 - (-6)) / 2, 0 / (6 - (-6)) / 2) = (0.5, 0.5, 0)
正交投影下, w 分量为 1,所以顶点A最终为 (0.5, 0.5, 0, 1)。
透视除法
透视除法使顶点A在裁剪坐标系中的坐标 (0.5, 0.5, 0, 1) 除以 w 分量, 得到NDC 坐标
(0.5 / 1, 0.5 / 1, 0 / 1, 1 / 1) = (0.5, 0.5, 0, 1)
视口变换
视口变换用于将 NDC 坐标映射到屏幕坐标系(将 3D 坐标转变成 2D 坐标,在 GPU 中执行)。
视口是 gl.viewport 进行设置的:
gl.viewport(0, 0, 500, 600);
这样我们就把我们的视口设置为宽 500 ,高 600的长方形。我们基于这个长方形的左上角作为原点,创建屏幕坐标系。然后,找到 NDC 坐标系原点在该屏幕坐标系中的坐标。
//NDC 坐标系原点位于屏幕坐标系的中央
(500 / 2, 600 / 2) = (250, 300)
由于 NDC 坐标系 Y 轴方向和 屏幕坐标系 Y 轴方向相反,所以 NDC 坐标系下的 Y 轴坐标转化到屏幕坐标系时要取Y轴坐标的相反数。
最终,我们可以得到顶点 A 在屏幕坐标系下的坐标为:
(0.5 * 250, 0.5 * 300 * -1) + (250, 300) = (375, 150)