基础知识
欧拉角
欧拉角Eulerian angles用来确定定点转动刚体位置的3个一组独立角参量,由章动角 θ、旋进角(即进动角)ψ和自转角j组成。为欧拉首先提出而得名。
四元素(quaternion)
四元数是简单的超复数。 复数是由实数加上虚数单位 i 组成,其中i^2 = -1。 相似地,四元数都是由实数加上三个虚数单位 i、j、k 组成,而且它们有如下的关系: i^2 = j^2 = k^2 = -1, i^0 = j^0 = k^0 = 1 , 每个四元数都是 1、i、j 和 k 的线性组合,即是四元数一般可表示为a + bk+ cj + di,其中a、b、c 、d是实数。
矩阵
介绍
简单来说,矩阵就是有确定的行数和列数的一组数。例如,一个 2x3 的矩阵:
在 3D 图形中,我们最经常使用 4x4 的矩阵。这能够让我们变换 (x,y,z,w)的向量。通过将向量和矩阵相乘的方式完成。
矩阵 x 向量(位置不要变)= 变换后的向量
平移矩阵
下面这种是最简单易懂的变换矩阵。如下:
X,Y,Z就是你想要给你位置添加的值。
所以如果你想将向量 (10,10,10,1) 在X轴移动10个单位,我们得到:
并且我们得到了(20,10,10,1)齐次向量!记住,这个 1 指的是位置,不是方向。所以我们的转换并没有修改我们处理的是位置的事实。
让我们来看下如果对这个指向-z轴方向的向量(0,0,-1,0)沿X轴平移10个单位会发生什么:
结果还是 (0,0,-1,0)方向,这很好,因为移动一个方向是没有意义的。
单位矩阵
这个矩阵很特殊,它什么也不做。但是我还是要提一下它,就好像你要知道 A X 1.0 = A 一样是很重要的。
伸缩矩阵
伸缩矩阵也很容易:
所以如果你想对向量的每一个方向都放大2倍:
并且 w 同样没有变化,你获取会问:”伸缩方向“是什么意思呢?当然,大部分情况下你不会做这种事情,但是在非常特殊的情况下,还是有用的。
旋转矩阵
这个矩阵比较复杂,可以参考Matrices and Quaternions FAQ,Rotation tutorials
复合变换
所以现在我们知道如何旋转、平移、伸缩我们的向量。如果能够结合这些变换,将是很好的。这是通过矩阵的乘法完成的。比如:
TransformedVector = TranslationMatrix * RotationMatrix * ScaleMatrix * OriginalVector;
注意:这行代码事实上是先伸缩,然后旋转,最后平移的。这就是矩阵乘法的使用方式。顺序改变是会影响最终的结果的。比如:
点(x0,y0)经过矩阵变换后得到(x,y),那这个变换顺序是怎么样的呢?
变换顺序:T(-10, 10) -> R(theta) -> T(10, 10)。这一种,把运算式子写出来如M' = T(10,10) x R(θ) x T(-10,-10) x M,然后在按照从右边到左边的顺序(T(-10,-10)->R(θ)->T(10,10))去理解,改变的是坐标位置,坐标系不变。
变换顺序:T(10, 10) -> R(theta) -> T(-10, 10)。第二种方式去理解矩阵变换,就得改变变换的空间想象,这个时候改变得是坐标系,不变的是坐标位置,即坐标位置相对于它所在的坐标系里一直是不变的
矩阵类型
模型矩阵
这种模型,它们的顶点的 X,Y,Z坐标是相对对象的中心点确定的:也就是说,如果一个顶点的坐标是(0,0,0),那么它就是对象的中心点.
我们想要移动这个模型,或许是因为玩家想通过键盘或者鼠标来控制它。很简单,上面已经讲过:translate*rotation*scale
,就这样。你在每一帧都对这个模型上的每一个顶点做矩阵变换,那么所有的东西都会移动。不会移动的点就是世界的中心点。
现在你所有的顶点都在世界空间上。这就是下图黑色箭头的含义:我们从模型空间(所有顶点都是相对模型的中心点定义的)转化为了世界空间(所有的顶点都是相对世界的中心点确定的)
我们可以总结为下图:
视图矩阵
让我们再一次引用 Futurama:“引擎并不会移动船。船一直呆着不动,引擎让整个世界绕着船动。”
当我们思考这个的时候,同样的道理也适用于照相机。如果你想换个角度看山,你既可以移动相机,也可以移动山。虽然在现实生活中不太实际,但是在图形学中是相当简单和方便的。
所以最开始的时候你的照相机是在世界空间的中心。为了移动世界,你其实引入了另一个矩阵。假设说你沿着X轴往右把你的相机移动了3个单位,就等价于你将整个世界(包括里面的网格)向左移动了3个单位。
我们还是用图片来描述一下这个:我们从世界空间(所有的顶点都是相对世界中心定义的)到相机空间(所有的顶点都是相对相机定义的)。
下面就是复合图:
还没完,别着急,哈哈
投影矩阵
我们现在到了相机世界。这意味着经过所有的这些变换,如果一个顶点恰好是 x = 0 并且 y = 0,它应该在屏幕中间渲染。但是我们不能够只使用x,y坐标来决定一个对象应该放置在屏幕的位置。它离相机的距离(z)也是有用的。对于x,y坐标类似的顶点,拥有最大的z坐标的顶点会比其他的在更中间。
下面这个叫做透视投影:
最后一次:
我们从相机空间(所有的顶点都是相对相机定义的)到齐次空间(所有的顶点都是定义在一个小方块里面,所有在小方块里面的东西都是在屏幕上的)
最后一个图:
下面的另一张图可以让我们更好地了解投影过程中到底发生了什么。在投影之前,我们有蓝色的立方体,在相机空间,我们用红色的图形来代表相机的视景体:相机真正能够看到的那部分场景。
将所有的一切乘以投影矩阵,我们有了如下的效果:
在这张图中,视景体是一个完整的立方体(所有的轴都在-1到1之间,有点难看出来),并且所有的蓝色对象都同样变形啦。离照相机近的对象会大一些,远一点的会小一些。就跟现实生活一样!
让我们看看从视景体的后面看是什么效果:
上面看到的是正方形的图片,所以需要执行另一个数学变换(这个是自动计算的,不需要自己手动执行)去适应屏幕的正式宽高。
矩阵变换
Three.js使用矩阵来表达 3D 变换---平移(位置)、渲染、伸缩。每一个 Object3D 的实例都有一个矩阵来存储对象的位置、旋转、伸缩。接下来我们看下如何对一个对象进行转换。
常用属性和 matrixAutoUpdate
有两种方式来更新对象的变换
-
更改对象的位置,四元数,和伸缩属性,three.js 会根据这些属性重新计算对象的矩阵:
object.position.copy(start_position); object.quaternion.copy(quaternion);
默认情况下,
matrixAutoUpdate
属性是设置为true
的,矩阵会自动重新计算。如果对象是静态的,或者你希望自己手动控制什么时候重新计算,可以通过将属性设置为false
来获取更好的性能。object.matrixAutoUpdate = false
同时在改变任何属性之后,手动更新矩阵:
object.updateMatrix();
-
直接修改对象的矩阵。Matrix4提供了各种修改矩阵的方法:
object.matrix.setRotationFromQuaternion(quaternion); object.matrix.setPosition(start_position); object.matrixAutoUpdate = false;
注意在这种情况下
matrixAutoUpdate
必须设置成false
。并且你要确定不要调用updateMatrix
方法。调用updateMatrix
会阻断对矩阵的手动更改,会根据位置、伸缩等属性重新计算矩阵。
对象和世界矩阵
一个对象的矩阵存储着对象相对父对象的变换。要获得对象在世界坐标的变换,你需要访问 Object3D.matrixWorld
当无论是父对象或者是子对象的变换改变的时候,你都可以通过调用 updateMatrixWorld来更新子对象的 matrixWorld
旋转和四元数
Three.js提供了两种方式来表示 3D 旋转:欧拉角 和 四元数,当然也包括用于两者之间转换的方法。欧拉角会有“万向节锁”的问题,导致某些配置失去一定的自由度(阻止物体绕一个轴旋转)。因为这个原因,对象旋转总是存储在对象的四元数里。
参考
- three.js matrix transformation
- matrices