Three 中矩阵的计算和应用过程
来源: Day4: Three.js 什麼!空間被扭曲了?我願稱你為最強——矩陣
https://codepen.io/umas-sunavan/pen/JjvNYwY
下面两段代码有什么区别?
// cube.matrix 为单位阵
const translationMatrix = new THREE.Matrix4().makeTranslation(5,0,0)
const scaleMatrix = new THREE.Matrix4().makeScale(2,1,1)
cube.applyMatrix4(translationMatrix.multiply(scaleMatrix))
// 结果:cube.matrix.elements = [2,0,0,0,0,1,0,0,0,0,1,0,5,0,0,1]
// 即数学上:|2|0|0|5|
// |0|1|0|0|
// |0|0|1|0|
// |0|0|0|1|
// cube.matrix 为单位阵
const translationMatrix = new THREE.Matrix4().makeTranslation(5,0,0)
const scaleMatrix = new THREE.Matrix4().makeScale(2,1,1)
cube.applyMatrix4(translationMatrix)
cube.applyMatrix4(scaleMatrix)
// 结果:cube.matrix.elements = [2,0,0,0,0,1,0,0,0,0,1,0,10,0,0,1]
// 即数学上:|2|0|0|10|
// |0|1|0| 0|
// |0|0|1| 0|
// |0|0|0| 1|
问题 1:效果为什么不同?applyMatrix4()做了什么?
因为运算顺序不同导致结果不同。Object3D.applyMatrix4()
方法是这样实现的:
// github.com/mrdoob/three.js/blob/64c8d59ac9f57ac6e8737d32f01340ec304c5364/src/core/Object3D.js#L124
applyMatrix4( matrix ) {
if ( this.matrixAutoUpdate ) this.updateMatrix();
this.matrix.premultiply( matrix );
this.matrix.decompose( this.position, this.quaternion, this.scale );
}
注意:applyMatrix4() 是左乘矩阵
所以上述两个代码区别在于:
- 第一个是 C = (T·S)·C = T·S·C
- 第二个是 C = S·(T·C) = S·T·C
矩阵乘法不支持交换律,所以结果完全不同。
问题 2:为什么是左乘?(矩阵主序问题)
这个问题可以直接看这篇文章:WebGL 矩阵 vs 数学中的矩阵
数学上的 4 维矩阵通常使用行主序表示和参与运算,但 OpenGL 使用列主序。
数学上变换矩阵习惯写为:
为便于观察,我们表示为(A 角标矩阵)
|11|12|13|14|
|21|22|23|24|
|31|32|33|34|
|41|42|43|44|
Threejs 中是这样声明:
const m = new THREE.Matrix4();
m.set(11, 12, 13, 14,
21, 22, 23, 24,
31, 32, 33, 34,
41, 42, 43, 44)
// 或通过行主序数组
m.fromArray([11, 12, 13, 14,
21, 22, 23, 24,
31, 32, 33, 34,
41, 42, 43, 44],0)
// 存储结果是列主序的:
console.log(m.elements)
// [11,12,13,14,21,22,23,24,31,32,33,34,41,42,43,44]
所以根据矩阵转置乘法法则:(AB)T = BTAT
即数学上: C' = C(SRT)
OpenGL 上: C'T = (SRT)TCT = TTRTSTCT
即 C' = (TRS)·C 写成代码就是
// 缩放旋转平移
cube.applyMatrix4( new THREE.matrix4().multiply(translationM).multiply(rotaMZ).multiply(scaleM) )
Three 结合 Tween 的简单动画
以下所有代码中,cube
代表被运动的物体
平移有几种写法?
const gclock = new THREE.Clock();
function animate() {
let t = gclock.getDelta() // v 是单位时间
// case 1: 设置cube的position
let pos = new THREE.Vector3(t,0,0)
cube.position.set(...pos)
// case 2: 沿特定轴平移,对Object3D本身操作
cube.translateOnAxis(new THREE.Vector3(12,12,12).normalize(),t)
// case 3: 先对空矩阵设定makeTranslation,连乘组成TRS,对cube应用TRS变换。
// 每个t都对起始点变换,变化值不累加
let Tm = new THREE.Matrix4().makeTranslation(v, 0, 0) // Tm 矩阵表示单次偏移矩阵变换,也就是每个t都会变换。
let TRS = new THREE.Matrix4().multiply(Tm) // 单次变化值
cube.applyMatrix4(TRS)
// bad case 3: 每次修改了cube.matrix再做multiply,变化值累加,平移运动随时间越来越快。
let Tm = new THREE.Matrix4().makeTranslation(v, 0, 0)
let mat = new THREE.Matrix4().copy(cube.matrix).multiply(Tm) // 累计值
cube.applyMatrix4(mat)
}
旋转有几种写法?
原点出发的轴线:
// 物体绕设定轴公转运动,设定轴是从原点出发指空间中某点。
const gclock = new THREE.Clock();
function animate() {
let v = gclock.getDelta()
let specAxes = new THREE.Vector3(12,12,12) // 旋转轴
let Rm = new THREE.Matrix4().makeRotationAxis(specAxes.normalize(),v * Math.PI / 20)
cube.applyMatrix4(Rm)
}
任意轴线:先平移再旋转再平移逆变换
// cube位置为 (3, 0, 0),以下函数让cube绕(1.5, 0, 0)公转。
// 做法是让旋转中心平移至(1.5, 0, 0),再进行旋转,再乘平移逆矩阵
cube.position.set(3, 0, 0)
const gclock = new THREE.Clock();
function animate() {
let v = gclock.getDelta()
let Tm = new THREE.Matrix4().makeTranslation(1.5, 0, 0) // 平移
let Rm = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0,1,0).normalize(),v * Math.PI / 5)
let TRS = new THREE.Matrix4().multiply(Tm).multiply(Rm).multiply(Tm.invert()) // 单次变化值
cube.applyMatrix4(TRS)
}
和以上效果完全相同的实现方式:
// cube位置为 (3, 0, 0),以下函数让cube绕(1.5, 0, 0)公转。
// 做法是让cube中心平移至(1.5, 0, 0),再进行旋转,再乘平移逆矩阵
cube.position.set(3, 0, 0)
const gclock = new THREE.Clock();
function animate() {
let v = gclock.getDelta()
// 平移
let Tm = new THREE.Matrix4().makeTranslation(-1.5, 0, 0) // 以cube为基准,轴心向-x方向平移1.5
let Rm = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0,1,0).normalize(),v * Math.PI / 5)
let TRS = new THREE.Matrix4().premultiply(Tm).premultiply(Rm).premultiply(Tm.invert())
cube.applyMatrix4(TRS)
}
目前看来第一种 mutiply 实现方式似乎更符合人的直觉,相当于对旋转轴进行变换
- (旋转轴 0,1,0) 平移变换 (至 1.5, 0, 0)
- 旋转变换
- 平移变换逆矩阵
TRS = T·R·T-1
这几个动画方式有什么区别?(单位量还是累积量)
【错误写法】
// 这个写法不能实现连续自转
const gclock = new THREE.Clock();
function animate() {
let v = gclock.getDelta() // v是单位值,每次执行animate都是0.1s左右
let matrix = new THREE.Matrix4().copy(cube.matrix)
matrix.makeRotationY(Math.PI / 10 * v); // 注意:makeRotationY在累计值才有效果,每次只转一单位效果不累加。
cube.setRotationFromMatrix(matrix)
}
function render() {
if (!renderer) return;
renderer.render(scene, camera);
requestAnimationFrame(render);
animate()
}
【正确写法】
// 这个写法可以实现连续公转(无自转)。公转速度为 Math.PI / 10 * v
// 注意这个例子与上一个只有一行不同。
const gclock = new THREE.Clock();
function animate() {
let v = gclock.getDelta() // v是单位值,每次执行animate都是0.1s左右
let matrix = new THREE.Matrix4().copy(cube.matrix)
matrix.makeRotationY(Math.PI / 10 * v); // 注意:makeRotationY在累计值才有效果,每次只转一单位效果不累加。
cube.applyMatrix4(matrix)
}
function render() {
if (!renderer) return;
renderer.render(scene, camera);
requestAnimationFrame(render);
animate()
}
【正确写法】
// 这个写法可以实现 1000ms 内自转 π/2 弧度。
let progress = { value: 0 }
new TWEEN.Tween(progress)
.to({ value: 1 },1000)
.easing(TWEEN.Easing.Linear.None)
.onUpdate((progress) => {
const v = progress.value // 此处v是进度值,从0至1
// matrix 实现 cube 自转
let matrix = new THREE.Matrix4()
matrix.makeRotationY(Math.PI / 2 * v);
cube.setRotationFromMatrix(matrix)
}).start()
function render() {
if (!renderer) return;
renderer.render(scene, camera);
requestAnimationFrame(render);
TWEEN.update()
}
这个写法可以在绕(0,1,0)旋转同时自转面向原来的方向
function animate() {
let v = gclock.getDelta()
let matrix = new THREE.Matrix4().copy(cube.matrix)
matrix.makeRotationY(Math.PI / 10 * v);
cube.setRotationFromMatrix(matrix)
cube.applyMatrix4(matrix) // 但如果去掉这行看不出任何效果
}
复合运动
// 两个平移叠加
function animate() {
let v = gclock.getDelta()
let T1 = new THREE.Matrix4().makeTranslation(v, 0, 0)
let T2 = new THREE.Matrix4().makeTranslation(0, v, 0)
let M = new THREE.Matrix4()
M.multiplyMatrices(T2, T1)
cube.applyMatrix4(M)
}
参考资料
// 世界平移-自身旋转-世界平移的逆(表现为原地旋转)
public worldTranslation_SelfRotate() {
let worldPosZero = this.getWorldPosition(this.cubeArr[0]);//本质上就是Object3D里的getWorldPosition() 获取世界坐标
let worldPosOne = this.getWorldPosition(this.cubeArr[1]);
let worldOffset = worldPosZero.sub(worldPosOne);
let goZeroMatrix = new Matrix4().makeTranslation(worldOffset.x, worldOffset.y, worldOffset.z);
this.cubeArr[1].matrix.premultiply(goZeroMatrix);
let angle = 30 * Math.PI / 180;
let absAngle = Math.abs(angle);
this.cubeArr[1].matrix.multiply(new THREE.Matrix4().makeRotationZ(absAngle));
let mat4I = new THREE.Matrix4();
mat4I.copy(goZeroMatrix).invert();// mat4I.getInverse(goZeroMatrix);getInverse函数已弃用
this.cubeArr[1].matrix.premultiply(mat4I);
}
// 自身平移-自身旋转-自身平移的逆(表现为绕轴旋转)
public SelfTranslation_SelfRotate() {
let worldPosZero = this.getWorldPosition(this.cubeArr[0]);
let worldPosOne = this.getWorldPosition(this.cubeArr[1]);
let worldOffset = worldPosZero.sub(worldPosOne);
// 世界空间转局部空间
let itemBaseVec = this.getBasisVec(this.cubeArr[1].matrix);//本质上就是extractBasis() 获取基坐标系
let localOffset = this.vectorChangBasic(worldOffset,itemBaseVec);
let goZeroMatrix = new Matrix4().makeTranslation(localOffset.x, localOffset.y, localOffset.z);
this.cubeArr[1].matrix.multiply(goZeroMatrix);
let angle = 30 * Math.PI / 180;
let absAngle = Math.abs(angle);
this.cubeArr[1].matrix.multiply(new THREE.Matrix4().makeRotationZ(absAngle));
let mat4I = new THREE.Matrix4();
mat4I.copy(goZeroMatrix).invert();// mat4I.getInverse(goZeroMatrix);getInverse函数已弃用
this.cubeArr[1].matrix.multiply(mat4I);
}
private vectorChangBasic(needChangeVec: Vector3, baseVec: baseVectorObj) {
// 世界空间的相对位置,转成基于baseVec坐标系下的位置
let newVec: Vector3 = new Vector3();
newVec.x = needChangeVec.dot(baseVec.x);
newVec.y = needChangeVec.dot(baseVec.y);
newVec.z = needChangeVec.dot(baseVec.z);
return newVec;
}