Object3D:变换相关的内容

1,128 阅读5分钟

Object3D:变换相关的内容

希望读者朋友可以了解一些基础的计算机图形学知识

1. 有关变换属性

总共有三类变换平移旋转以及缩放

对于一个新创建的3D对象来说,需要提供一系列的初始顶点,可以通过矩阵运算(变换),移动这些顶点,从而达到平移,旋转以及缩放对象的目的。

在Three.js中,对于上述三类变换,都有对应的属性。

  • 平移变换:position:Vector3

  • 缩放变换:scale:Vector3

  • 旋转变换:rotation:Eulerquaternion: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来说,positionrotatequaternionscale,描述的都是一种相对与对象的相对变换。

上面的例子中,月球的父对象是地球,地球的父对象是太阳。通过子对象相对于父对象来描述一个场景,显然是更简单的。我们可以通过一个局部的变换矩阵matrix,把子对象的顶点,移动到父对象的坐标空间。

例如,把月球(初始的顶点),通过变换矩阵,变换为地球中的坐标。

Vertex地球=Matrix月球×Vertex月球{Vertex}_{地球}= {Matrix}_{月球}\times{Vertex}_{月球}

同样的,继续把这个坐标变换为太阳空间下的坐标。

Vertex太阳=Matrix地球×Vertex地球=Matrix地球×Matrix月球×Vertex月球{Vertex}_{太阳}= {Matrix}_{地球}\times{Vertex}_{地球}={Matrix}_{地球}\times{Matrix}_{月球}\times{Vertex}_{月球}

可以看到,对于月球坐标的变换,就是把从月球到祖先对象的变换矩阵,一级一级的乘起来,最终,得到一个全局的变换矩阵。

即,WorldMatrix×Matrix=WorldMatrix{WorldMatrix}_{父}\times{Matrix}_{子}={WorldMatrix}_{子}

这种有父子关系的相对坐标的变换,在计算机图形学中,称为矩阵堆栈,感兴趣的读者可以自行学习一下。

Three.js中,因为一个对象可以有多个子对象,所以,用的方式组织结构,但在计算世界变换时,用到的思想是一样的。

Object3D有两个更新世界矩阵的方法updateMatrixWorldupdateWorldMatrix

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()

有了上面说到的世界变换,就可以把局部坐标转换为世界坐标。

Vertex世界=WorldMatrix×Vertex局部{Vertex}_{世界}={WorldMatrix}\times{Vertex}_{局部}

Vertex局部=WorldMatrix1×Vertex世界{Vertex}_{局部}={WorldMatrix}^{-1}\times{Vertex}_{世界}

这两部分的源码如下:

localToWorld( vector ) {
  return vector.applyMatrix4( this.matrixWorld );
}

// 注:矩阵的invert()用来求逆矩阵
worldToLocal( vector ) {
  return vector.applyMatrix4( _m1.copy( this.matrixWorld ).invert() );
}

4. 从局部空间坐标到视觉空间坐标:modelViewMatrix

有了前面说过的matrixWorld,可以把一个坐标转换到世界空间的坐标。对于一个相机来说,它也有三个正交坐标轴,以这三个坐标轴为基的空间,称作视觉空间,为了后续继续计算,我们需要把坐标转换到视觉空间。这个转换,也可以用一个视觉矩阵实现(其实就是相机三个基向量构成的矩阵的逆矩阵),记作ViewMatrix{ViewMatrix}

因此,要把一个局部空间的坐标,转换到视觉空间,就要用到modelViewMatrixModelViewMatrix=ViewMatrix×WorldMatrix{ModelViewMatrix}={ViewMatrix}\times{WorldMatrix}

5. 总结

Object3D对于平移、旋转和缩放都提供了相应的方法,这些方法仅修改对象的局部变换,在调用更新世界变换矩阵的方法时,通过一系列局部变换矩阵的叉乘,得到世界变换矩阵。

将世界空间的坐标转换到视图空间,就是进行了一次基变换

还有一个Object3D中的变换矩阵:normalMatrix,它是modelViewMatrix逆转置矩阵,用来对法向量进行变换。对于法向量来说,如果同样使用modelViewMatrix进行变换,变换后,法向量与面就不再垂直,需要使用它的逆转置矩阵,才能保持垂直关系。