++版权声明:本系列文章为新中地webgis讲师原创,转载请获得授权并注明出处!++
坐标系的分类
WebGL 是如何把 3D 世界中的模型(物体)渲染到屏幕上的呢? 这其中的最大难点就是`坐标系的变换。
在坐标系转换过程中都涉及"哪些坐标系?"、"他们存在的意义是什么?"、"其中的计算逻辑又是什么?"等问题,本文将详细的总结webgl当中的坐标系。
WebGL 坐标系分为如下几类:
模型坐标系 -- 世界坐标系 -- 观察坐标系(又称相机坐标系、视图坐标系) -- 裁剪坐标系(**<font style="color:rgb(255, 80, 44);background-color:rgb(255, 245, 245);">gl_Position</font>**接收的值) -- NDC 坐标系 -- 屏幕坐标系。
其中,裁剪坐标系之前的这几个坐标系,我们都可以使用 JavaScript 控制。从裁剪坐标系到 NDC 坐标系,这一个步骤是 顶点着色器的最后自动完成的,我们无法干预。
其中前面四步我们都已经实现过了。

-
CPU 中将模型坐标转换成裁剪坐标
-
顶点在模型坐标系中的坐标经过模型变换,转换到世界坐标系中。
-
然后通过摄像机观察这个世界,将物体从世界坐标系中转换到观察坐标系。
-
之后进行投影变换,将物体从观察坐标系中转换到裁剪坐标系。
具体代码实现:
我们一般会计算一个几何体的顶点,这个顶点就是模型坐标,然后这个顶点经过mvp变换,然后赋值给gl_Position,这个gl_Position所对应的坐标,我们称之为裁剪坐标。
gl_Position=u_ProjMatrix*u_ViewMatrix*u_ModelMatrix*vec4(a_Position,1.0);
模型坐标系
这个概念最好理解,假如我们有一个箱子,它的长宽高为10,那么我们能够很轻松的计算出,以这个箱子中心为原点的箱子顶点的坐标,比如箱子左上角的模型坐标就是(5,5,5),一般来说,这种模型坐标系遵循右手坐标系。

世界坐标系
我们创建好的模型需要放在世界中的位置,比如我在地球上的坐标是(650000,500000,500000)以地球为原点,建立的坐标系,对我来说就是一个世界坐标
世界坐标系也是遵循右手坐标系,X 轴水平向右,Y 轴垂直向上,Z 轴指向屏幕外面。

具体到代码实现中就是
javascript
vec4 position=modelMatrix*a_Position;
观察坐标系
观察坐标系是将世界空间坐标转化为用户视野前方的坐标而产生的结果。**人眼或者摄像机**看到的世界中的物体**相对于他自身的位置所参照的坐标系就叫观察坐标系**。
在我们日常生活中,精准描述一个街道,我们一般用经纬度来表示,但是如果有人问你:某某街道在什么位置?如果我们告诉他世界坐标:某某街道在东经 M 度,北纬 N 度,我想他会打你。
一般我们都会用这样易于理解的描述:在<font style="color:rgb(255, 80, 44);background-color:rgb(255, 245, 245);">前面多远,往左或右走多远</font>。
这种坐标就称为观察坐标,也叫相机坐标,他是以人眼/摄像机为原点而建立的坐标系。

具体到代码实现:
javascript
vec4 position=viewMatrix*modelMatrix*a_Position;
裁剪坐标系
裁剪坐标系遵循`左手坐标系`。
相机坐标系观察的空间是整个 3D 世界,而裁剪坐标系是希望所有的坐标都落在一个特定的范围内,超出这个范围的顶点坐标都将被裁剪掉,被裁剪掉的坐标就不会显示,这就是裁剪坐标系的由来。
具体到代码实现
javascript
vec4 position=projMatrix*viewMatrix*modelMatrix*a_Position;
细心的同学可能会发现,之前我们讲过投影矩阵,它们分别是正交投影与透视投影,它们是如何让物体变换到上图中的立方体空间中的呢?

正交投影变换
首先看一下正交投影,假设我们的点在左图中的长方体中,第一步,这个长方体会平移到世界坐标原点

- translate
所以正交投影会有一个平移分量

- scale

所以正交投影的缩放分量推导为

为什么是**(f-n)****? **
按照正常传参的情况, f的值比n的值大. 比如: f=10, n=1
f表示的是远面和相机的距离
n表示的是近面和相机的距离
正交投影矩阵 = 缩放矩阵 x 位移矩阵

由于上面的矩阵是在左手坐标系下计算的, 要映射到裁剪空间需要把z反转, 最终的正交投影矩阵如下

透视投影变换
透视投影也需要把物体变换到世界原点,也需要缩放为一个立方体,所以正交投影其实是透视投影的一个分量
透视投影在正交投影的基础上,还需要把金字塔的棱锥变为长方体,即我们需要一个远面压缩矩阵
这需要用来我们坐标的第四个分量,齐次坐标。
++齐次坐标的作用++
假如有一条无限长的列车轨道,虽然我们知道在物理上,两条轨道是平行线,是不会相交的,但是在我们的眼睛里,两条轨道在无穷远的位置会相交在一起,这就是透视空间。

那么,如何用数学的方法来表示透视空间下轨道上的点呢。

这是齐次坐标的第一个作用:可以表示无穷远的点。
在透视投影下,齐次坐标可以用来处理透视投影矩阵的远面压缩矩阵,当应用投影矩阵后,顶点的坐标会被转换为齐次坐标,然后通过透视除法(除以w)得到NDC(裁剪)坐标。
代码示例
-
远处的物体:w更大 → x/y分量被压缩更明显 → 产生近大远小效果
gl_Position.xyz /= gl_Position.w
另外一个作用是齐次坐标可以用来处理更复杂的变换,比如将缩放和平移结合起来。

透视投影矩阵推导
深入研究-透视投影中深度与距离的对应关系,在后续内容中会详细介绍。
我让ai推导了这个对应关系,我们只需要关注结果即可:



让n=1,f=100,将公式用graphToy进行观测

我们可以看到zView的值在-1到-∞之间时,深度值在前期会变化的很快,但是后期会变化的很慢
以此得到结论:

所以我们在使用透视投影相机的时候,尽量让near/far的比例合适,这样可以避免一些遮挡问题,因为webgl中是通过深度检测来判断物体的前后顺序的。
比较合适的比例:

我们还可以在shader中调试深度值
javascript
// 片段着色器
gl_FragColor = vec4(vec3(gl_FragCoord.z), 1.0);
-
纯白表示far平面
-
纯黑表示near平面
-
出现明显色阶断裂说明精度不足
NDC坐标系
(Normalized device coordinates(归一化设备坐标系))
上面的裁剪坐标已经是我们能通过编程得到的最细一层次的坐标了,一个物体的模型坐标经过模型,视图,投影变换之后,得到一个四阶的裁剪坐标 vec4(x,y,z,w),接着,gpu会接受到这个四阶坐标,并对x,y,z分别除以w分量,这一步叫做透视除法,经过这一步转换而来的坐标将会被归一化到一个立方体范围内,如下图所示:

NDC 坐标系遵循<font style="color:rgb(255, 80, 44);background-color:rgb(255, 245, 245);">左手坐标系</font>,Z 轴朝向屏幕里面,Z轴值越小,越靠近我们的眼睛,我们之前的例子验证过webgl的深度检测这一点。
屏幕坐标系
有了 NDC 坐标之后,GPU 会执行最后一步变换操作,`视口变换`,这个过程会将所有在【-1, 1】之间的坐标映射到屏幕空间中,并被变换成片段。这一步主要由我们的viewPort来完成,具体到代码实现
gl.viewport(0,0,canvas.width,canvas.height)
其中的转换公式为,具体的步骤就是将x轴的 -1,1 转换为startX,startX + viewWidth
将y轴的-1,1转换为startY,startY + viewHeight
