Three.js 场景、矩阵与拾取
穿过 Three.js 抽象层,理解场景渲染、局部/世界矩阵、法线贴图和射线拾取。
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个属性 三个方法:
updateMatrix把matrix分解到各个分量上,并标记matrixWorld要被更新updateMatrixWorld在scene中就会调用的方法,用来开启更新updateWorldMatrix一个方法方便递归去调用更新自己的matrixWorld,和matrix ,applyMatrix4把矩阵应用到物体的matrix上,并且更新本地矩阵 三个属性:matrixAutoUpdate标记是否自动把matrix分解到几个分量上matrixWorldAutoUpdate标记当一个祖先节点的matrixWorld没有更新时,是否需要更新自己的matrixWorldmatrixWorldNeedsUpdate标记当前节点的**matrixWorld**是否需要更新
updateMatrix
📌 提示
- 主要做了2件事:
- 把当前的局部矩阵matrix 分解到position/scale/rotate三个分量
- 把
matrixWorldNeedsUpdate标记为true
updateMatrix() {
this.matrix.compose( this.position, this.quaternion, this.scale );
this.matrixWorldNeedsUpdate = true;
}
updateMatrixWorld
📌 提示
- 作用
- 如
matrixAutoUpdate为true,则调用updateMatrix更新本地matrixmatrixWorldNeedsUpdate或者参数 force 为true,则需要更新自身的matrixWorld- 如果更新了自身的matrixWorld,那么一定要去更新所有的后代节点,
force设置为true- 对所有children递归调用
updateMatrixWorld( force )- 如果自身更新了,因为matrixWorld已经改变了,那么children的matrixWorld也一定要被更新
- 如果自身没有被更新,那么只去更新后代组件中matrixWorldAutoUpdate 设置为true的
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;
// 自身更新结束,标记为falseforce = 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
📌 提示
作用:
- 一定会更新自己的matrixWorld,如果设置了
matrixAutoUpdate,则也会更新自己的matrix- 如果符合条件,并且
matrixWorldAutoUpdate,则会递归更新父元素matrixWorld- 符合条件且
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的原理

- 屏幕点击的位置是屏幕空间,需要经过计算转换到投影空间(会自动构建一个z坐标0.5),然后还要经过转换到视图空间,再转换到世界空间中
- 求与三角面交点时,要把光线转换到mesh的局部空间中计算
- 得到的交点,返回前会被转换成世界坐标的点,但是法线仍然时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坐标的原理
- 理解uv插值
可以尝试一个uv坐标之间使用超过范围的值的,实验一下会如何采样
- 理解uv展开
也就是uv坐标并非是标准的[0,1]范围的情况
Object3D的add和attach的区别
- 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;
}
- attach方法,添加一个对象,且可以保持object对象的
matrixWorld,object的本地矩阵(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;
}