Visual LabINTERACTIVE LEARNING
OpenGL 与 3D精校教程

Three.js 场景、矩阵与拾取

穿过 Three.js 抽象层,理解场景渲染、局部/世界矩阵、法线贴图和射线拾取。

Three.js矩阵Raycaster
进阶预计 30 分钟查看源文 ↗

three的运行模式,代码运行逻辑

Object3D的矩阵

Matrix

📌 提示

是记录group本身相对于parent的变换,是对group进行了一些变换操作后更新到matrix中的,类似于相对路径的概念

matrixWorld

📌 提示

matrixWorld是group相对于worldspace的变换,类似于绝对路径的概念

如何实现更新

简述

📌 提示

使用parent的matrixWorld与自身的matrix乘积得到自身的matrixWorld,而parent的matrixWorld也是这样不断的累积祖先的变换的结果,所以最外层的matrix和matrixWorld相同 更为深入的理解,group实际是出于组织和管理3D物体的需求而人为创造的一个分组(即不包含顶点),主要作用是对其子元素批量进行一个相同的矩阵变换。 不管套了多少个group,物体的matrixWorld始终表示的都是其在场景世界中的绝对变换(即不套group,只要物体应用了这个矩阵得到的也是相同的变换-位置、缩放、旋转) 对外层group应用一个变换操作,最终都会自动执行上面所说的更新操作(即parent的matrixWorld与自身的matrix乘积得到自身的matrixWorld),从而实现模型位置的变换

📌 提示

与矩阵更新相关的4个方法和3个属性 三个方法:

  1. updateMatrix 把matrix分解到各个分量上,并标记matrixWorld要被更新
  2. updateMatrixWorld 在scene中就会调用的方法,用来开启更新
  3. updateWorldMatrix 一个方法方便递归去调用更新自己的matrixWorld,和matrix ,
  4. applyMatrix4 把矩阵应用到物体的matrix上,并且更新本地矩阵 三个属性:
  5. matrixAutoUpdate 标记是否自动把matrix分解到几个分量上
  6. matrixWorldAutoUpdate 标记当一个祖先节点的matrixWorld没有更新时,是否需要更新自己的matrixWorld
  7. matrixWorldNeedsUpdate 标记当前节点的**matrixWorld**是否需要更新
updateMatrix

📌 提示

  1. 主要做了2件事:
  2. 把当前的局部矩阵matrix 分解到position/scale/rotate三个分量
  3. matrixWorldNeedsUpdate标记为true
  1. updateMatrix() {

    this.matrix.compose( this.position, this.quaternion, this.scale );

    this.matrixWorldNeedsUpdate = true;

}

updateMatrixWorld

📌 提示

  1. 作用
  2. matrixAutoUpdate为true,则调用updateMatrix 更新本地matrix
  3. matrixWorldNeedsUpdate或者参数 force 为true,则需要更新自身的matrixWorld
  4. 如果更新了自身的matrixWorld,那么一定要去更新所有的后代节点,force 设置为true
  5. 对所有children递归调用updateMatrixWorld( force )
  6. 如果自身更新了,因为matrixWorld已经改变了,那么children的matrixWorld也一定要被更新
  7. 如果自身没有被更新,那么只去更新后代组件中matrixWorldAutoUpdate 设置为true的
  1. 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;
    // 自身更新结束,标记为false

    force = true;
    //如果有被更新,那么后代一定要被更新

    }

    // update children

    const children = this.children;

    for ( let i = 0, l = children.length; i < l; i ++ ) {

    const child = children[ i ];
    // 如果自身没有被更新,那么只去更新后代组件中matrixWorldAutoUpdate 设置为true的
    if ( child.matrixWorldAutoUpdate === true || force === true ) {

     child.updateMatrixWorld( force );  
    

    }

    }

}

updateWorldMatrix

📌 提示

作用:

  1. 一定会更新自己的matrixWorld,如果设置了matrixAutoUpdate ,则也会更新自己的matrix
  2. 如果符合条件,并且matrixWorldAutoUpdate ,则会递归更新父元素matrixWorld
  3. 符合条件且matrixWorldAutoUpdate ,递归调用去更新后代的 matrixWorld
updateWorldMatrix( updateParents, updateChildren ) {

    const parent = this.parent;

    if ( updateParents === true && parent !== null && parent.matrixWorldAutoUpdate === true ) {
     //父元素更新的条件
      parent.updateWorldMatrix( true, false ); // 不更新子元素了

    }

    if ( this.matrixAutoUpdate ) this.updateMatrix();  
    // 如果设置 了matrixAutoUpdate,那么更新一下当前局部矩阵到几个分量中
    
    if ( this.parent === null ) {
      // 最外层,matrix 与matrixWorld相同
      this.matrixWorld.copy( this.matrix );

    } else {
       // 否则当前的matrixWorld 为父元素的matrixWorld 左乘 当前的局部矩阵
      this.matrixWorld.multiplyMatrices( this.parent.matrixWorld, this.matrix );

    }

    // update children
   
    if ( updateChildren === true ) {

      const children = this.children;
     // 如果子元素的需要更新,把 matrixWorldAutoUpdate 的子元素的matrixWorld也更新了
      for ( let i = 0, l = children.length; i < l; i ++ ) {

        const child = children[ i ];

        if ( child.matrixWorldAutoUpdate === true ) {

          child.updateWorldMatrix( false, true );

        }

      }

    }

  }

飞书画板

applyMatrix4
applyMatrix4( matrix ) {

    if ( this.matrixAutoUpdate ) this.updateMatrix();

    this.matrix.premultiply( matrix ); 
    // 这里为什么是 this.matrix 右乘matrix ?
    this.matrix.decompose( this.position, this.quaternion, this.scale );
  }

矩阵的左乘与右乘是指:矩阵在表达式的位置是左边还是右边

A x B : 矩阵A左乘矩阵B,或者矩阵B右乘矩阵A

  //Matrix4
 // 矩阵 左乘 矩阵m
multiply( m ) { 
    return this.multiplyMatrices( this, m ); 
 }
// 矩阵 右乘 矩阵m
premultiply( m ) {
   return this.multiplyMatrices( m, this );
}
multiplyMatrices( a, b ) {} // 矩阵 a x b  a 左乘 b

mesh直接变换和变换geometry的区别

geometry变换会直接修改顶点数据/法线数据

normalMap中的normalScale

normalScale的作用是把,法线贴图中不垂直于表面的法线的不垂直程度放大,如下图代码所示,采样得到的mapN的xy分别放大normalScale倍,如果是垂直于表面的法向量,xy均为0,只有不垂直的法向量,xy大小才能被放大,也就是会改变法向量的方向,使其效果更显著

intersectObjects的原理

飞书画板

  1. 屏幕点击的位置是屏幕空间,需要经过计算转换到投影空间(会自动构建一个z坐标0.5),然后还要经过转换到视图空间,再转换到世界空间中
  2. 求与三角面交点时,要把光线转换到mesh的局部空间中计算
  3. 得到的交点,返回前会被转换成世界坐标的点,但是法线仍然时mesh局部空间的法线
// 局部空间的点转换到世界空间
point .applyMatrix4( object.matrixWorld );

// convert ray to local space of mesh
const _ray = new Ray();
_inverseMatrix.copy( matrixWorld ).invert(); // 世界矩阵的逆矩阵
_ray.copy( raycaster.ray ).applyMatrix4( _inverseMatrix ); //复制原来的射线,应用上世界矩阵的逆矩阵

ray.applyMatrix4(_inverseMatrix) //一个矩阵即可把射线转换到某个空间

// 把投影坐标的vector3转换回世界坐标
Vec3.unproject( camera )
// 本地空间转换到世界空间的的法线矩阵  
const normalWorld  = new Matrix3().getNormalMatrix(matrixWorld )
normal.applyMatrix3(normalWorld) 
// 底层实现是 this.setFromMatrix4( matrix4 ).invert().transpose(); 
// 世界矩阵的左上角3x3矩阵的逆矩阵的转置矩阵,移除了位移和非均匀缩放影响

// 如果从世界空间转换回来,使用逆矩阵即可

uv坐标的原理

  1. 理解uv插值

可以尝试一个uv坐标之间使用超过范围的值的,实验一下会如何采样

  1. 理解uv展开

也就是uv坐标并非是标准的[0,1]范围的情况

Object3D的add和attach的区别

  1. add方法,可以添加多个object,且会保持object的本地矩阵(Matrix)不变,去更新object的世界矩阵(matrixWorld)
add( object ) {

    if ( arguments.length > 1 ) {

      for ( let i = 0; i < arguments.length; i ++ ) {

        this.add( arguments[ i ] );

      }

      return this;

    }
    if ( object === this ) {

      console.error( 'THREE.Object3D.add: object can\'t be added as a child of itself.', object );
      return this;
    }

    if ( object && object.isObject3D ) {

      object.removeFromParent();
      object.parent = this;
      this.children.push( object );
      object.dispatchEvent( _addedEvent );
      _childaddedEvent.child = object;
      this.dispatchEvent( _childaddedEvent );
      _childaddedEvent.child = null;

    } else {

      console.error( 'THREE.Object3D.add: object not an instance of THREE.Object3D.', object );

    }
    return this;

  }
  1. attach方法,添加一个对象,且可以保持object对象的matrixWorldobject的本地矩阵(Matrix)会被重新计算
attach( object ) {

    // adds object as a child of this, while maintaining the object's world transform

    // Note: This method does not support scene graphs having non-uniformly-scaled nodes(s)

    // Mpw x Ml = Mw
    // Mnpw x Mt x Ml = Mw
    // 要保持最后的世界矩阵相等,即Mw不变,就有 Mnpw x Mt x Ml = Mpw x Ml 
    // 得,Mt = Mpw x Mnpw-1
    // 
    this.updateWorldMatrix( true, false );

    _m1.copy( this.matrixWorld ).invert();

    if ( object.parent !== null ) {

      object.parent.updateWorldMatrix( true, false );

      _m1.multiply( object.parent.matrixWorld ); // 这就是Mt

    }

    object.applyMatrix4( _m1 ); // 把Mt应用到Ml上得到新的Ml
    // 亦即,新得parent的matrixWorld和 新的Ml累积,得到的matrixWorld可以保持不变

    object.removeFromParent();
    object.parent = this;
    this.children.push( object );

    object.updateWorldMatrix( false, true );

    object.dispatchEvent( _addedEvent );

    _childaddedEvent.child = object;
    this.dispatchEvent( _childaddedEvent );
    _childaddedEvent.child = null;

    return this;

  }