注意:这章是我自己结合资料总结的,有的理解不一定是正确的,因为我也是计算机图形学初学者
1. 齐次坐标
在之前的文章中我们直接就使用的齐次坐标(x, y, z, w)但是并没有解释为什么要多一个w分量,为什么要使用齐次坐标
简单来说,齐次坐标的出现是为了能在二维世界中描述三维世界独有的东西深度。
平常使用的欧氏空间(或笛卡尔空间),两条平行的直线是永远不会相交。但是在某个视角下的三维图像中,平行的直线也可能相交,这是因为三维世界投射到二维世界有透视关系。
加上的这个维度w既能让图像有透视关系(近大远小),同时也能保证能够还原到原始的点(正确表示位置关系)。
2. 左手坐标系和右手坐标系
左手坐标系和右手坐标系的区别,很简单就是z轴指向的位置,如果正方向是朝屏幕外的就是右手坐标,朝屏幕里的就是左手坐标。
3. webgl的坐标系系
在很多教程或者网上的文章中,你一定看到了很多坐标表示方法,一会左手坐标一会右手坐标。对于初学者,比如我。
实际上webgl没有固定说要使用什么坐标系,左手还是右手坐标系取决于你对于顶点的变换是怎么操作的。
4. 裁剪坐标和NDC
在WebGL学习(六)三维世界中,介绍了可视空间(投影变换),当时只是把它理解为一种特殊的图形变换。
实际上这里面的内容还挺多。
4.1. 裁剪空间和裁剪坐标
很好理解,显示在屏幕的画面不可能全部都能看见,在合适的地方剪裁图像,这个剪裁范围就是裁剪空间,显然这个空间内的坐标就是裁剪坐标。
当顶点着色器处理完成之后(比如设置MVP矩阵(模型(Model)矩阵、视图(View)矩阵、投影(Projection)矩阵)),物体就会被放入裁剪空间(进行剪裁)。
4.2. NDC
NDC(归一化设备坐标系),他的作用就是将坐标归一化到下面这个立方体中。裁剪空间没有固定的范围(或者说是[-w, w]),所以每个不同的图像都要进行单独的处理来判断哪些顶点需要被看见,对于计算机来说,计算起来费劲,不如归一到一个固定的范围,这样计算会更方便和精确。
归一的方法就是透视除法,也就是给坐标除以一个w。
4.3. 透视除法
为什么给每个坐标除以一个w就能实现归一化了呢?
这里要结合上面的第一节,齐次坐标是什么。
上面图中有两个相似三角形△AEF和△ABC,如果我们要计算线段BC那么根据相似同比例:
这里的EF就是裁剪之后的线段,此时它的w = 3,如果要转到NDC,也就是 BC所在的截面,只需要除以3就可以了。
上图也是一种思考方式,二维笛卡尔坐标可以看做是w=1的平面。也可以当做是NDC坐标所在的空间,也可以看成上面的BC线段所在的那个截面。如果我要将w=3时的平面上的点(3,6,3)归一化到NDC,显然只需要除以w=3就行了。
这样操作之后不会改变本身的透视关系,还能归一到一个固定的空间内。
5. 一步步探索坐标系的变化
5.1. 简单的画点图形
// 就简单的画两个三角形,一前一后
// 先画的蓝色
// 后画的红色
var pc = new Float32Array([ // Vertex coordinates and color
0.0, 0.5, -0.1, 0.0, 0.0, 1.0, // The front blue one
-0.5, -0.5, -0.1, 0.0, 0.0, 1.0,
0.5, -0.5, -0.1, 1.0, 1.0, 0.0,
0.5, 0.4, -0.5, 1.0, 1.0, 0.0, // The red triangle is behind
-0.5, 0.4, -0.5, 1.0, 0.0, 0.0,
0.0, -0.6, -0.5, 1.0, 0.0, 0.0,
]);
蓝色三角形的顶点z=-0.1,红色z=-0.5,假设坐标系默认是右手坐标,那么蓝色应该在红色前面。事实上不是这样:
可以看到,红色在蓝色前面。这其实说明webgl并没有默认的左手或者右手系统。看着就像webgl是按照先后顺序渲染的图像,后定义的顶点会覆盖在前一个图像上。
交换顶点定义顺序:
var pc = new Float32Array([
0.5, 0.4, -0.5, 1.0, 1.0, 0.0, // 红色渲染在后
-0.5, 0.4, -0.5, 1.0, 0.0, 0.0,
0.0, -0.6, -0.5, 1.0, 0.0, 0.0,
0.0, 0.5, -0.1, 0.0, 0.0, 1.0, // 蓝色渲染在前
-0.5, -0.5, -0.1, 0.0, 0.0, 1.0,
0.5, -0.5, -0.1, 1.0, 1.0, 0.0,
]);
所以webgl在默认设置的时候,没有什么左右坐标系,就是单纯的后画的覆盖前面的。
5.2. 加入深度测试
上面的代码如果加上了隐藏面消除(深度测试):
// ....
gl.enable(gl.DEPTH_TEST);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
//...
var pc = new Float32Array([
0.5, 0.4, -0.5, 1.0, 1.0, 0.0, //先画红色
-0.5, 0.4, -0.5, 1.0, 0.0, 0.0,
0.0, -0.6, -0.5, 1.0, 0.0, 0.0,
0.0, 0.5, -0.1, 0.0, 0.0, 1.0, // 再画蓝色
-0.5, -0.5, -0.1, 0.0, 0.0, 1.0,
0.5, -0.5, -0.1, 1.0, 1.0, 0.0,
]);
神奇的是,无论你怎么交换顶点绘制的先后顺序,红色将总是在蓝色前面。
你可能觉得正确了,但是红色z=-0.5蓝色z=-0.1,在右手坐标中,红色应该在蓝色后面。
5.3. 为什么会这样
有个值得注意的点:
在webgl中,投影变换矩阵的公式都是基于左手坐标系(z正方向朝屏幕内部)推导的
这样做的好处是:
- 符合经验,z越大离观察点越远
- 提高计算精度,下图可以看出,z值越大深度变化就越不明显。如果使用右手坐标系,z越大越靠近观察点,这会导致离观察人越近的物体,深度精度会越差,就会出现
Z-Fighting(深度冲突)。
上图可以看出横坐标
z值越小,深度变化越明显(斜率越高),也就越准确。
5.4. 理论基础(用正投影来解释)
之前我们没有探究正投影是怎么计算出来的,这里我们需要用到正投影矩阵公式:
这个公式值得注意的是,其推导过程是基于左手坐标系的(这个很关键),为什么使用左手坐标系,前面已经解释了。
没有设置可视空间的情况就相当于,给顶点乘了一个单位矩阵(因为乘以单位矩阵相当于没有做任何变换)。
现在假设这个正投影矩阵根据参数计算出来的结果是一个单位矩阵,那么我们可以反解出参数的值:
我们来看一下效果:
没有任何变化,这其实说明默认情况下,webgl就是设置了一个单位矩阵正投影。
要想正确的显示,正投影应该这么设置
// 伪代码
ortho(left: -1, right: 1, bottom: -1, top: 1, near: -1, far: 1)
可以看到这两种设置唯一的区别就是near<far了。
我们来用treejs上面的例子来看看这个例子我改了下源码,让near可以大于far
这里我们设置
near=5、far=35.3,显然near < far这是正确的设置,前后关系也能正确显示。我们来设置near > far的情况
这次
near=31.6、far=13.1,视点位置没变,但是可以发现物体前后关系发生了变化,球体挡住了立方体。
5.5. 梳理总结
现在有了理论基础,现在我们来总结一下刚才发生了什么,我们可以得到什么:
1、在没有开启深度测试时,webgl似乎按照后画的覆盖先画的顺序,并没有理会z值。
2、开启深度测试后,虽然能够不受先后定义顺序的影响,显示固定的前后关系,但是似乎和右手坐标系相反
3、凑巧的是,设置了一个单位矩阵正投影后,webgl的表现和第二条一样。
4、如果我将正投影矩阵的设置成near < far,显示就正确了。
下面我将做解释,再回过头看上面的现象就能够理解了:
1、 默认情况下,webgl设置了一个单位矩阵正投影,但是没设置深度测试的时候不会进行隐藏面消除,所以看起来就是后绘制的总是会覆盖前面的。(解释第1条)
2、由于投影矩阵是基于左手坐标系推导的,所以做出的变换相当于将图像放入了左手坐标系。因此默认情况下z值越小的越靠近我们。(解释2、3条)
3、当设置为near < far时,此时的投影矩阵变成了这样:
可以看到第三行出现了负数,当顶点左乘这个矩阵时,z被设置成了负值。这不就是翻转了z轴吗。根据第2点解释,默认情况下顶点被放入了左手坐标,而现在又将z轴翻转了一次,最后呈现的就是右手坐标系。(解释第4条)
6. 总结
上面写了一大堆,可能云里雾里的,我也是看了很多资料,从迷迷糊糊到有点通了。这篇文章我也修改了很多次,现在我再总结一下:
webgl没有什么左手/右手坐标系,都是人为设置的参考,z越大越远还是反过来,全靠人为定义。而习惯上,将默认情况看做右手坐标系,也就是z正方向朝屏幕外。- 投影矩阵是基于
左手坐标系推导的。所以投影变换的时候相当于将图像放入了左手坐标系。 - 默认情况下
webgl设置了一个单位矩阵正投影,打开深度测试后,就会根据左手坐标系的规则剔除隐藏面。 - 无论是
顶点着色器阶段还是后面的剪裁、NDC转化,都没有设置坐标系的操作。
总之,不是webgl知道该变成什么坐标系,而是人为编程计算故意设置的,一个习惯性预设而已。
就像汽车左舵的国家道路相对于右舵的国家就是反的,一个道理。只是人为定义的而已。