模型坐标系是用来描述模型内部构造的。它的原点是(0,0,0)
在模型坐标系下,定义的坐标,本文称之为模型坐标。当然你也可以有自己的命名。
物理建模的模型,由一个个小的三角形组成,每个三角形都有三个顶点组成。顶点的模型坐标是基于模型中心点的。中心点的坐标为(0,0,0)。
上图是个简单的正方形的模型,正方形长和宽为20。中心点在正方形的中心,中心点模型坐标为(0,0,0),则其中四个顶点的模型坐标为
(-10,10,0)、(10,10,0)、(-10,-10,0)、(10,-10,0)
这些顶点的模型坐标不会随着模型具体的位置的变化而变化。
你可以把自己想成一个模型,你身高2米,当自己是个纸片人时,不考虑z坐标。你的中心点在两脚之间,你的头的模型坐标为(0,2,0)。当你的位置从上海移动到北京时,你的头的模型坐标仍然为(0,2,0)。不可能你人到了北京,你的头距离你的脚就3米了。
世界坐标系是描述整个3D场景的。它的原点是(0,0,0)
我们将模型放置到世界坐标系中,默认是放到原点的。
这里切记,并不是说模型坐标的原点在(0,0,0),模型放在世界坐标系中,就是放在世界坐标系的中心点。虽然都是(0,0,0),但是意义是不一样的。
更多时候我们需要把模型放在世界坐标系原点以外的地方,那么这时候就需要进行变换。这些变换包括平移、旋转和缩放,我们称之为模型变换(model matrix)。图形学中用矩阵去描述这些变换。
你可以想象一下,孙悟空出厂设置在世界中心点,他跑步,翻跟头、放大、缩小着去取经了,他的世界坐标不断在变化着。
现在再看模型的这些顶点,在模型坐标系中它有自己的模型坐标,在世界坐标系中,它又有自己的世界坐标。
仍以上面的正方形模型为例。如果我们把模型沿世界坐标系的x轴移动10个单位,那么各顶点的世界坐标为
(0,10,0)、(20,10,0)、(0,-10,0)、(20,-10,0)
但是各顶点的模型坐标仍为
(-10,10,0)、(10,10,0)、(-10,-10,0)、(10,-10,0)
如何验证顶点的模型坐标不会变化这个问题呢,我们写一个简单的代码。
const box = new THREE.PlaneGeometry(20, 20);
const mesh = new THREE.Mesh(box, new THREE.MeshBasicMaterial({ color: new THREE.Color(0xffffff) }));
我们没有对这个正方形的模型进行任何的变换,它默认是在世界坐标的(0,0,0)位置。我们看到顶点坐标是这样的。
然后我们对正方形沿着x轴方向移动10个单位(mesh.translateX(10),我们打印看一下,顶点坐标仍是上图中显示的那样。
那问题来了,我们如何知道顶点的世界坐标呢???模型坐标与模型的世界变换矩阵相乘就得到了世界坐标
我们可以手动计算,代码如下:
const array = mesh.geometry.getAttribute('position').array;
const size = mesh.geometry.getAttribute('position').count;
mesh.updateMatrixWorld();
for (let i = 0; i < size; i += 1) {
console.log(
new THREE.Vector3(array[i * 3 + 0], array[i * 3 + 1], array[i * 3 + 2]).applyMatrix4(mesh.matrixWorld),
);
}
打印结果如下:
matrix:局部变换矩阵。我们可以理解为模型和模型父级坐标的变换。如果该模型没有父级,则模型坐标乘以matrix就是世界坐标了。
matrixWorld:世界坐标矩阵。若这个模型没有父级,那它和matrix是一样的。模型坐标乘以matrixWorld就是世界坐标。
我们用代码去验证:
const box = new THREE.PlaneGeometry(20, 20);
const mesh = new THREE.Mesh(box, new THREE.MeshBasicMaterial({ color: new THREE.Color(0xffffff) }));
mesh.translateX(10);
scene.add(mesh);
打印mesh,可以看到当mesh没有父级时matrix和matrixWorld相同
我们将模型放置在一个父级中再放入到场景,执行代码
const box = new THREE.PlaneGeometry(20, 20);
const mesh = new THREE.Mesh(box, new THREE.MeshBasicMaterial({ color: new THREE.Color(0xffffff) }));
mesh.translateX(10);
const group = new THREE.Group();
group.translateX(10);
group.add(mesh);
我们看到下图中,模型的世界变换矩阵为(group.matrix)*(mesh.matrix)。世界变换矩阵是两次变换矩阵相乘的结果。
最后用一张图来描述模型坐标系到世界坐标系的转换,这张图来自opengl的学习文档
把模型通过旋转、平移、缩放等操作,放置到世界坐标系下,现在模型每个顶点处于世界坐标系下,变化矩阵*顶点的模型坐标,就得到了模型顶点在世界坐标系下的世界坐标。
在3D场景中,我们引入了相机的概念。
相机的三要素:原点(0,0,0),朝向z轴的负方向,向上方向,也就是threejs中相机的up方向为y轴正方向。这三个要素构成了观察坐标系
一般我们的相机的位置坐标不会设置在原点,这样相机就做了平移的变化。如我们把相机位置设置在(0,0,1000)的位置,我们查看相机的局部变换矩阵,这里查看相机的变换矩阵。
在此改变基础上,我再修改相机的up方向为(1,0,0),即up方向为x轴的正方向。我们再查看相机的变换矩阵
了解旋转矩阵的可以看出来,相机进行了一个旋转-90度且平移1000的操作。所以改变相机的up方向也会更新相机的变换矩阵。这两个改变同样可以在threejs相机对象的position和rotation属性中查看到。
我们想一下,相机向左动3个单位,世界坐标不变,那是不是和相机不动,世界坐标整体向右移动3个单位的效果是一样的。其他方向同样,旋转也同样逻辑。所以,要想获取到各个顶点在观察坐标系中的观察坐标,只需要执行如下操作
视图矩阵的逆矩阵*各顶点世界坐标
到目前为止,我们已经得到了一个以相机为中心的观察坐标系了。但是观察坐标系中不是所有的模型都可以看到的。实际生活中,相机能够拍到范围是有限的,这就需要根据一些参数(比如人眼能看到的最近距离,以及最远距离,以及摄像机视锥体垂直视野角度)去裁剪观察坐标系可视范围。这个裁剪范围是由投影矩阵决定的。
一共有两种投影方式,透视投影和正交投影,透视投影更加符合生活,有近大远小的效果。而正交投影并不会有这样的效果。
如上图,投影就是物体到近平面的投影,正交投影比较简单,透视投影原理其实就是相似三角形。是缩放和平移变换。
上图中主要是x,y的转换,z的转换是个线性的,具体透视矩阵如何推导,这篇文章中不会去推导。本文主要讲各种坐标的意义以及坐标的转换。
裁剪矩阵变换如下(存储在threejs 的camera projectMatrix中)
注意:这里不做坐标转化,我们先将裁剪空间转为标准化空间后再做坐标转换。
接下来裁剪空间需要进一步转换称标准化设备空间,所以需要进一步的进行新一轮的坐标变换
这样我们就得到一个标准立方体了,也就是标准化设备空间。标准化设备矩阵是如下:
我们通过以下方式获得标准化设备坐标:
标准化设备矩阵*观察坐标
将标准化设备空间转换到屏幕坐标,这样物体就在屏幕上的合适位置显示了。
屏幕变换矩阵如下:
经过这样的转换,我们就可以在屏幕上看到场景中的模型了。
为了更好的理解这些坐标的作用,我现在用planeGeometry绘制一个正方形,边长为1,然后正方形的世界坐标为(10,10,0.1),我们去计算一下正方形的屏幕坐标。
1. 正方形的模型坐标和模型坐标系如下:
2. 设置正方形坐标为(10,10,0.1),模型变换矩阵为 (m1)
3. 设置相机的坐标为(0,0,50),则观察坐标系和观察矩阵如下,注意,在坐标转换过程中我们用视图的逆矩阵(m2)
4. 设置透视相机,fov为60,near:0.1 far:1000,canvasWidth:1607,canvasHeight:915。 计算裁剪矩阵
注意,near和far在裁剪坐标系中为-0.01和-1000,所以在下图计算时请注意看前面的符号
5. 将4的裁剪空间,转成标准化空间[-1,1]的立体空间中,坐标转换如下:(m3)
6. 屏幕坐标系以及坐标矩阵转换,坐标转换如下:(m4)
dom.getBoundingClientRect().left = 0,dom.getBoundingClientRect().left = 79
经过 m1、m2、m3、m4四次矩阵变换,position最终转换为屏幕坐标(961,378)
然后去电脑屏幕上验证一下确实是(961,378)
如何将一个世界坐标下的模型,绘制到屏幕上呢?
屏幕变换*标准设备变换*视图变换*模型变换*模型坐标