Object3D:变换相关的内容
希望读者朋友可以了解一些基础的计算机图形学知识
1. 有关变换属性
总共有三类变换:平移,旋转以及缩放。
对于一个新创建的3D对象来说,需要提供一系列的初始顶点,可以通过矩阵运算(变换),移动这些顶点,从而达到平移,旋转以及缩放对象的目的。
在Three.js中,对于上述三类变换,都有对应的属性。
-
平移变换:
position:Vector3 -
缩放变换:
scale:Vector3 -
旋转变换:
rotation:Euler和quaternion:Quaternion,分别表示欧拉角和四元数。在three.js中,最终表示旋转的,仍然是四元数,用户可以设置欧拉角,three.js会把设置的欧拉角转换成四元数。下面是关于这部分的源码。
function onRotationChange() {
quaternion.setFromEuler( rotation, false );
}
rotation._onChange( onRotationChange ); // 当欧拉角被设置时,会同时设置对应四元数
// 在生成变换矩阵时,旋转变换使用的是四元数而不是欧拉角
updateMatrix() {
// compose方法:通过position,quternion,scale生成变换矩阵
this.matrix.compose( this.position, this.quaternion, this.scale );
this.matrixWorldNeedsUpdate = true;
}
2. matrix(局部变换矩阵)和matrixWorld(世界/全局变换矩阵)
在复杂的场景中,往往不止一个对象,如果所有的对象都直接用世界坐标进行描述,结果会非常复杂。
举个例子,对同一场景的两种表述:
-
太阳在(0, 0, 0)的位置,地球绕着太阳转(圆周),月球绕着地球转(圆周)。
-
太阳在(0, 0, 0)的位置,地球绕着太阳转(圆周),月球绕着太阳转(??)。
对于第二种表述,乍一看没有什么问题,但是,如果要通过代码计算某一时刻月球的位置,并不容易,因为相对于太阳来说,月球的运动轨迹并不是简单的圆周,如果直接在世界坐标系下描述,需要使用大量旋转和位移的组合。
对于第一种表述,我们很容易就可以用代码描述了,地球绕着太阳所在的一个固定的轴旋转,月球绕着地球所在的一个固定的轴旋转。
对于three.js来说,position,rotate,quaternion,scale,描述的都是一种相对与父对象的相对变换。
上面的例子中,月球的父对象是地球,地球的父对象是太阳。通过子对象相对于父对象来描述一个场景,显然是更简单的。我们可以通过一个局部的变换矩阵matrix,把子对象的顶点,移动到父对象的坐标空间。
例如,把月球(初始的顶点),通过变换矩阵,变换为地球中的坐标。
同样的,继续把这个坐标变换为太阳空间下的坐标。
可以看到,对于月球坐标的变换,就是把从月球到祖先对象的变换矩阵,一级一级的乘起来,最终,得到一个全局的变换矩阵。
即,
这种有父子关系的相对坐标的变换,在计算机图形学中,称为矩阵堆栈,感兴趣的读者可以自行学习一下。
Three.js中,因为一个对象可以有多个子对象,所以,用树的方式组织结构,但在计算世界变换时,用到的思想是一样的。
Object3D有两个更新世界矩阵的方法updateMatrixWorld和updateWorldMatrix
updateMatrixWorld用来更新一个对象的世界矩阵,以及它的后代对象的世界矩阵。
updateWorldMatrix用来更新一个对象的世界矩阵,通过两个参数来控制是否更新祖先或后代对象的世界矩阵。
updateMatrixWorld( force ) {
if ( this.matrixAutoUpdate ) this.updateMatrix(); // 先更新自己的局部矩阵
if ( this.matrixWorldNeedsUpdate || force ) {
if ( this.parent === null ) {
this.matrixWorld.copy( this.matrix );
} else {
// 核心!!!!!!
// 父对象的世界矩阵叉乘自己的局部矩阵 得到自己的世界矩阵
this.matrixWorld.multiplyMatrices( this.parent.matrixWorld, this.matrix );
}
this.matrixWorldNeedsUpdate = false;
force = true;
}
// update children
const children = this.children;
for ( let i = 0, l = children.length; i < l; i ++ ) {
const child = children[ i ];
if ( child.matrixWorldAutoUpdate === true || force === true ) {
child.updateMatrixWorld( force );
}
}
}
updateWorldMatrix( updateParents, updateChildren ) {
// 这部分代码和上面大致相同,只是在上面的代码前面加上了递归更新祖先对象的世界矩阵
const parent = this.parent;
if ( updateParents === true && parent !== null && parent.matrixWorldAutoUpdate === true ) {
parent.updateWorldMatrix( true, false );
}
if ( this.matrixAutoUpdate ) this.updateMatrix();
if ( this.parent === null ) {
this.matrixWorld.copy( this.matrix );
} else {
// 核心!!!!和上面是一样
this.matrixWorld.multiplyMatrices( this.parent.matrixWorld, this.matrix );
}
// update children
if ( updateChildren === true ) {
const children = this.children;
for ( let i = 0, l = children.length; i < l; i ++ ) {
const child = children[ i ];
if ( child.matrixWorldAutoUpdate === true ) {
child.updateWorldMatrix( false, true );
}
}
}
}
3. localToWorld() 和 worldToLocal()
有了上面说到的世界变换,就可以把局部坐标转换为世界坐标。
这两部分的源码如下:
localToWorld( vector ) {
return vector.applyMatrix4( this.matrixWorld );
}
// 注:矩阵的invert()用来求逆矩阵
worldToLocal( vector ) {
return vector.applyMatrix4( _m1.copy( this.matrixWorld ).invert() );
}
4. 从局部空间坐标到视觉空间坐标:modelViewMatrix
有了前面说过的matrixWorld,可以把一个坐标转换到世界空间的坐标。对于一个相机来说,它也有三个正交坐标轴,以这三个坐标轴为基的空间,称作视觉空间,为了后续继续计算,我们需要把坐标转换到视觉空间。这个转换,也可以用一个视觉矩阵实现(其实就是相机三个基向量构成的矩阵的逆矩阵),记作
因此,要把一个局部空间的坐标,转换到视觉空间,就要用到modelViewMatrix,
5. 总结
Object3D对于平移、旋转和缩放都提供了相应的方法,这些方法仅修改对象的局部变换,在调用更新世界变换矩阵的方法时,通过一系列局部变换矩阵的叉乘,得到世界变换矩阵。
将世界空间的坐标转换到视图空间,就是进行了一次基变换。
还有一个Object3D中的变换矩阵:normalMatrix,它是modelViewMatrix的逆转置矩阵,用来对法向量进行变换。对于法向量来说,如果同样使用modelViewMatrix进行变换,变换后,法向量与面就不再垂直,需要使用它的逆转置矩阵,才能保持垂直关系。