一、行主序、列主序
概念参考行主序 列主序
以线性代数中描述的矩阵为标准,行主序就是依次按行存储,而列主序就是依次按列存储。在threeJS中:
var A = new THREE.Matrix4();
A.set(1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12,
13, 14, 15, 16);
console.log(A);
var B = new THREE.Matrix4();
B.set(16, 15, 14, 13,
12, 11, 10, 9,
8, 7, 6, 5,
4, 3, 2, 1);
console.log(B);
var C = new THREE.Matrix4();
C.multiplyMatrices (A, B);
console.log(C);
其运行结果为:
在网上找一个在线矩阵计算器,比如http://www.yunsuan.info/matrixcomputations/solvematrixmultiplication.html
相对应的计算结果如下:
因此可以认为,threejs矩阵内部储存形式为列主序,表达和描述的仍然是线性代数中行主序,set()函数就是以行主序接受矩阵参数的。
二、如何根据变换设计自己的矩阵
概念性的东西,可以参考
线性代数笔记三 线性变换和矩阵乘法
图形学笔记一 仿射变换和齐次坐标
1.向量或点的缩放平移等操作
这部分比较好处理,例子可以参考
three.js 之 Matrix
2.坐标系的转化
冯乐乐讲MVP的例子也很好,可以参考
UnityShader精要笔记二 数学基础
核心思路就是以世界坐标为中转,应用坐标的变换等价于基变换。
比如模型坐标系转世界坐标系,就是模型空间任意一点计算出其在世界坐标系的位置。即模型每个点动了,整个模型也动了。
而世界坐标系转观察坐标系,则是先用观察坐标系转世界坐标系之后求逆,这样快速运算。所以说,世界坐标系是用来中转的,世界中心点不会动。
三、THREEJS封装的矩阵API
1.平移
var vector = new THREE.Vector3(20, 20, 0);
var matrix = new THREE.Matrix4();
matrix.makeTranslation(10, 40, 0);
vector.applyMatrix4(matrix);
2.旋转
matrix.makeRotationX(angle);
matrix.makeRotationY(angle);
matrix.makeRotationZ(angle);
matrix.makeRotationAxis(axis, angle);
matrix.makeRotationFromEuler(euler);
matrix.makeRotationFromQuaternion(quaternion);
前三个方法分别代表的是绕X、Y、Z三个轴旋转,无需赘述。
第四个方法是前三个方法的整合版,第一个参数表示的是代表xyz的THREE.Vector3,第二个参数是旋转的弧度。下面两行代码是等价的:
matrix.makeRotationX(Math.PI);
matrix.makeRotationAxis(new THREE.Vector3(1, 0, 0), Math.PI);
第五个方法表示围绕x、y和z轴的旋转,这是表示旋转最常用的方式;第六个方法是一种基于轴和角度表示旋转的替代方法。
关于旋转,可以参考
Cocos 3.x 四元数 rotateAroundLocal
Three.js欧拉对象Euler和四元数Quaternion
构造函数:Euler(x,y,z,order)
参数xyz分别表示绕xyz轴旋转的角度值,角度单位是弧度。参数order表示旋转顺序,默认值XYZ,也可以设置为YXZ、YZX等值
// 创建一个欧拉对象,表示绕着xyz轴分别旋转45度,0度,90度
var Euler = new THREE.Euler( Math.PI/4,0, Math.PI/2);
四元数的方法.setFromAxisAngle(axis, angle)通过旋转轴axis和旋转角度angle设置四元数数据,也就是x、y、z和w四个分量。
var quaternion = new THREE.Quaternion();
// 旋转轴new THREE.Vector3(0,1,0)
// 旋转角度Math.PI/2
quaternion.setFromAxisAngle(new THREE.Vector3(0,1,0),Math.PI/2)
console.log('查看四元数结构',quaternion);
四元数乘法.multiply()
对象的一个旋转可以用一个四元数表示,两次连续旋转可以理解为两次旋转对应的四元数对象进行乘法运算。
// 四元数q1、q2分别表示一个旋转,两个四元数进行乘法运算,相乘结果保存在q2中
// 在q1表示的旋转基础在进行q2表示的旋转操作
q1.quaternion.multiply( q2 );
欧拉、四元数和矩阵转化
欧拉对象、四元数对象和旋转矩阵可以相关转化,都可以表示旋转变换。
//通过矩阵对象Matrix4的.makeRotationFromQuaternion(q)方法可以把四元数转化对应的矩阵对象。
Matrix4.makeRotationFromQuaternion(q)
//通过欧拉对象设置四元数对象
quaternion.setFromEuler(Euler)
//四元数转化为欧拉对象
Euler.setFromQuaternion(quaternion)
Object3D对象角度属性.rotation的值是欧拉对象Euler,四元数属性.quaternion的值是四元数对象Quaternion。
执行Object3D对象旋转方法,会同时改变对象的角度属性和四元数属性。四元数属性和位置.position、缩放属性.scale一样会转化为对象的本地矩阵属性.matrix,本地矩阵属性值包含了旋转矩阵、缩放矩阵、平移矩阵。
Object3D对象角度属性.rotation和四元数属性.quaternion是相互关联的一个改变会同时改变另一个。
// 一个网格模型对象,基类是Object3D
var mesh = new THREE.Mesh()
// 绕z轴旋转
mesh.rotateZ(Math.PI)
console.log('查看角度属性rotation',mesh.rotation);
console.log('查看四元数属性quaternion',mesh.quaternion);
3.compose
//使用make系列的方法操作
Object3D.applyMatrix(new THREE.Matrix4().makeScale(2,1,1));
Object3D.applyMatrix(new THREE.Matrix4().makeTranslation(0,4,0));
Object3D.applyMatrix(new THREE.Matrix4().makeRotationZ(Math.PI/6));
//使用compose方法操作
var matrix = new THREE.Matrix4();
var trans = new THREE.Vector3(0,4,0);
var rotat = new THREE.Quaternion().setFromEuler(new THREE.Euler(0,0,Math.PI/6));
var scale = new THREE.Vector3(2,1,1);
Object3D.applyMatrix4(matrix.compose(trans, rotat, scale)); //效果同上
就是compose的逆过程。随便举个例子。
var matrix = new THREE.Matrix4().set(1,2,3,4,2,3,4,5,3,4,5,6,4,5,6,7);
var trans = new THREE.Vector3();
var rotat = new THREE.Quaternion();
var scale = new THREE.Vector3();
matrix.decompose(trans, rotat, scale);
//返回Vector3 {x: 4, y: 5, z: 6} 因为是随便写的,所以只有平移变量不需计算就可以看出来的
console.log(trans);
//返回Quaternion {_x: 0.05565363763555474, _y: -0.11863820054057297
//, _z: 0.051265314875937947, _w: 0.7955271896092125}
console.log(rotat);
//返回Vector3 {x: 3.7416573867739413, y: 5.385164807134504, z: 7.0710678118654755}
console.log(scale);
如何通过矩阵设置Object3D对象位置呢,参考108 THREE.JS 使用矩阵对3D对象进行位置设置
//最后先将模型移动到中心位置
var inverseM = new THREE.Matrix4();
inverseM.getInverse(centerM);
matrix.multiply(inverseM);
//将矩阵赋值给模型
cube.matrix = matrix;
//使用矩阵更新模型的信息
cube.matrix.decompose(cube.position, cube.quaternion, cube.scale);
还有个注意点,就是threejs的decompose是X轴方向的负缩放,有可能与其它3D库的matrix.decompose不同(比如是Z轴方向的负缩放)
测试的matrix数据如下:
rawData
(16) [5.053215498074303e-16, -1, 6.123233995736772e-17, 0, -1, -5.053215498074303e-16, -1.2246467991473532e-16, 0, -1.2246467991473535e-16, 6.123233995736766e-17, 1, 0, 1282.7820737078252, 3307.4240662137445, 3.128811373451107e-13, 1]
4.相乘
之前用过的matrix.multiplyMatrices(matrixA, matrixB),表示 将矩阵设置为matrixA * matrixB的结果。
threejs矩阵还有前乘和后乘的区别,也很容易混淆。
在threeJS中矩阵的后乘方法为multiply():
var A = new THREE.Matrix4();
A.set(1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12,
13, 14, 15, 16);
var B = new THREE.Matrix4();
B.set(16, 15, 14, 13,
12, 11, 10, 9,
8, 7, 6, 5,
4, 3, 2, 1);
A.multiply(B);
console.log(A);
console.log(B);
其运行结果为:
表明A.multiply(B)
不会改变B的值,会改变A的值,相当于A=A*B
,即后乘方法multiply()的结果就是把传入的参数放自己后面去乘。
反过来,使用前乘方法A.premultiply(B);
,结果就是B∗A
5.逆矩阵
let m4 = new THREE.Matrix4();
m4.elements = [10, 8, 3, 0, 15, 7, 2, 0, 10, 6, 1, 0, 0, 0, 0, 1];
let m3 = new THREE.Matrix4();
//执行这行,会将m4的逆矩阵设置给m3
m3.getInverse(m4);
6.例子
参考Three.js中的矩阵,做出如图的旋转效果:
var box_geometry = new THREE.BoxGeometry();
var sphere_geometry = new THREE.SphereGeometry(0.5, 32, 32);
var cylinder_geometry = new THREE.CylinderGeometry(0.1, 0.1, 0.5);
var material = new THREE.MeshLambertMaterial({color: new THREE.Color(0.9, 0.55, 0.4)});
var box = new THREE.Mesh(box_geometry, material);
var sphere = new THREE.Mesh(sphere_geometry, material);
var cylinder = new THREE.Mesh(cylinder_geometry, material);
scene.add(box);
scene.add(sphere);
scene.add(cylinder);
box.matrixAutoUpdate = false;
sphere.matrixAutoUpdate = false;
cylinder.matrixAutoUpdate = false;
var sphere_matrix = new THREE.Matrix4().makeTranslation(0.0, 1.0, 0.0);
sphere_matrix.multiply(new THREE.Matrix4().makeRotationZ(-Math.PI * 0.25));
sphere.applyMatrix(sphere_matrix);
var cylinder_matrix = sphere_matrix.clone();
cylinder_matrix.multiply(new THREE.Matrix4().makeTranslation(0.0, 0.75, 0.0));
cylinder.applyMatrix(cylinder_matrix);
注意这个例子中只给了部分代码,由于使用的是MeshLambertMaterial,需要添加光照才能看到几何体,当然也可以换其它material:
// 环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 1); // 创建环境光
this.scene.add(ambientLight); // 将环境光添加到场景
如果因为threejs版本问题,applyMatrix报undefined,改成applyMatrix4即可。
也可以参考这个例子,确认一下multiply的用法,上面有提到结论:
表明
A.multiply(B)
不会改变B的值,会改变A的值,相当于A=A*B
var sphere_matrix = new THREE.Matrix4().makeTranslation(0.0, 1.0, 0.0);
sphere.applyMatrix(sphere_matrix);
var cylinder_matrix = sphere_matrix.clone();
cylinder_matrix.multiply(new THREE.Matrix4().makeTranslation(0.0, 0.75, 0.0));
cylinder.applyMatrix(cylinder_matrix);
这段代码和之前的功能是一样的:
var box = new THREE.Mesh(box_geometry, material);
var sphere = new THREE.Mesh(sphere_geometry, material);
sphere.position.y += 1;
var cylinder = new THREE.Mesh(cylinder_geometry, material);
cylinder.position.y += 1.75;
scene.add(box);
scene.add(sphere);
scene.add(cylinder);
可以看出,cylinder_matrix使用multiply,在之前的平移矩阵上,再移动了0.75
但是,如果矩阵中还有其他值,使用乘法,都会改变:
let ttt = new THREE.Matrix4().set(1,2,3,4,2,3,4,5,3,4,5,6,4,5,6,7);
ttt.multiply(new THREE.Matrix4().makeTranslation(10,20,30));
console.log("ttt:",ttt.elements);
let ttt2 = new THREE.Matrix4().set(1,2,3,4,2,3,4,5,3,4,5,6,4,5,6,7);
ttt2.premultiply(new THREE.Matrix4().makeTranslation(10,20,30));
console.log("ttt2:",ttt2.elements);
结果:
ttt: (16) [1, 2, 3, 4, 2, 3, 4, 5, 3, 4, 5, 6, 144, 205, 266, 327]
ttt2: (16) [41, 82, 123, 4, 52, 103, 154, 5, 63, 124, 185, 6, 74, 145, 216, 7]
spher: (16) [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1] cylinder: (16) [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1.75, 0, 1]
7.Object3D.matrix和matrixWorld
仍然以UnityShader精要笔记二 数学基础中MVP的例子:
就是有个奶牛叫妞妞,她有自己的坐标空间即模型空间,在这个空间里,她的鼻子坐标是(0,2,4),最后如何显示在屏幕上呢?首先,转化为齐次坐标(0,2,4,1)。顶点变换的第一步就是将顶点坐标从模型空间变换到世界空间,这个变换通常叫做模型变换(model transform)。根据Transform的信息,妞妞进行了(2,2,2)的缩放,(0,150,0)的旋转以及(5,0,25)的平移。根据之前的知识,要先缩放再旋转再平移:
我们使用代码来验证一下:
var geometry = new THREE.BoxGeometry(1, 1, 1);
var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
var cube = new THREE.Mesh(geometry, material);
cube.scale.set(2, 2, 2);
cube.rotateY(150 * Math.PI / 180);
cube.position.set(5, 0, 25);
var vec = new THREE.Vector4(0, 2, 4, 1);
vec.applyMatrix4(cube.matrix);
console.log("vec:", vec);
现在打印出来的是0,2,4,1 这是因为matrix并没有立即生效,可以手动调用cube.updateMatrix()
,关于更新的问题后面再说,现在先换一个打印方式:
function animate() {
requestAnimationFrame(animate);
// cube.rotation.x += 0.01;
// cube.rotation.y += 0.01;
renderer.render(scene, camera);
var vec = new THREE.Vector4(0, 2, 4, 1);
vec.applyMatrix4(cube.matrix);
console.log("vec:", vec);
}
//animate();
打印结果与例子中计算结果一致:vec: Vector4 {x: 9, y: 4, z: 18.07179676972449, w: 1}
这也说明,cube.matrix是由模型坐标系转向其父容器世界坐标系的。现在继续做测试,把cube再添加一个父容器:
var geometry = new THREE.BoxGeometry(1, 1, 1);
var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
var cube = new THREE.Mesh(geometry, material);
cube.scale.set(2, 2, 2);
cube.rotateY(150 * Math.PI / 180);
cube.position.set(5, 0, 25);
var vec = new THREE.Vector4(0, 2, 4, 1);
vec.applyMatrix4(cube.matrix);
var cubeParent = new THREE.Object3D();
cubeParent.position.set(3, 0, 0);
cubeParent.add(cube);
scene.add(cubeParent);
然后打印的地方,把cube.matrix和cube.matrixWorld都打印:
function animate() {
requestAnimationFrame(animate);
// cube.rotation.x += 0.01;
// cube.rotation.y += 0.01;
renderer.render(scene, camera);
var vec = new THREE.Vector4(0, 2, 4, 1);
vec.applyMatrix4(cube.matrix);
console.log("vec:", vec);
console.log("matrix:", cube.matrix);
console.log("matrixWorld:", cube.matrixWorld);
}
显然,能看出cube.matrixWorld是把嵌套的父容器也考虑进去,一步到位,直接转到世界坐标系。
cube.modelViewMatrix
表示对象相对于相机坐标系的变换。也就是matrixWorld左乘相机的matrixWorldInverse。
但是,我打印一下,发现这个值不对:
那没办法,我们自己用矩阵乘法计算:
camera.rotateX(30 * Math.PI / 180);
camera.position.set(0, 10, -10);
function animate() {
requestAnimationFrame(animate);
// cube.rotation.x += 0.01;
// cube.rotation.y += 0.01;
renderer.render(scene, camera);
var vec = new THREE.Vector4(0, 2, 4, 1);
vec.applyMatrix4(cube.matrix);
console.log("vec:", vec);
console.log("matrix:", cube.matrix);
console.log("matrixWorld:", cube.matrixWorld);
var vec2 = new THREE.Vector4(0, 2, 4, 1);
let m = cube.matrixWorld.clone();
m.premultiply(camera.matrixWorldInverse);
vec2.applyMatrix4(m);
console.log("vec2:", vec2);
回到我们的农场游戏。现在我们需要把妞妞的鼻子从世界空间变换到观察空间中。为此我们需要知道世界坐标系下摄像机的变换信息。这同样可以通过摄像机面板中的Transform组件得到:(1,1,1)的缩放,(30,0,0)的旋转,(0,10,-10)的平移。
可以看到结果与例子中的Z值是相反的,这是因为unity用的左手坐标系,而threejs是右手坐标系。
注,这里用的是premultiply,可以参考之前的结论:
表明A.multiply(B)不会改变B的值,会改变A的值,相当于A=A*B
8.camera相关的matrix
摄像机Cameras 有两个额外的四维矩阵:
- Camera.matrixWorldInverse: 视图矩阵 - 摄像机世界坐标变换的逆矩阵。
- Camera.projectionMatrix: 投影矩阵 - 表示将场景中的信息投影到裁剪空间。
9.makeScale会清理其它数据
let ttt = new THREE.Matrix4().set(1,2,3,4,2,3,4,5,3,4,5,6,4,5,6,7);
ttt.makeScale(2,2,2);
console.log("ttt:",ttt.elements);
let ttt2 = new THREE.Matrix4().set(1,2,3,4,2,3,4,5,3,4,5,6,4,5,6,7);
ttt2.makeTranslation(1,2,3);
console.log("ttt2:",ttt2.elements);
let ttt3 = new THREE.Matrix4().set(1,2,3,4,2,3,4,5,3,4,5,6,4,5,6,7);
ttt3.compose(new THREE.Vector3(2,2,2),new THREE.Quaternion(),new THREE.Vector3(1,2,3));
console.log("ttt3:",ttt3.elements);
结果如下:
ttt: (16) [2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1]
ttt2: (16) [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 3, 1]
ttt3: (16) [1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 3, 0, 2, 2, 2, 1]
也就是说,如果不想清理之前的数据,可以使用compose进行一次性转换。
10.更多的API
四、THREEJS来更新对象的变换
参考
three.js 之 Matrix
学习ThreeJS 04 更新机制
1.更改对象的位置,四元数,和伸缩属性,three.js 会根据这些属性重新计算对象的矩阵:
object.position.copy(start_position);
object.quaternion.copy(quaternion);
默认情况下,matrixAutoUpdate 属性是设置为 true 的,矩阵会自动重新计算(如果它们已添加到场景中,或者是已添加到场景中的另一个对象的子节点)。
var object1 = new THREE.Object3D();
var object2 = new THREE.Object3D();
object1.add( object2 );
//object1 和 object2 会自动更新它们的矩阵
scene.add( object1 );
如果对象是静态的,或者你希望自己手动控制什么时候重新计算,可以通过将属性设置为 false 来获取更好的性能。
object.matrixAutoUpdate = false
同时在改变任何属性之后,手动更新矩阵:
object.updateMatrix();
2.直接修改对象的矩阵
object.matrix.setRotationFromQuaternion(quaternion);
object.matrix.setPosition(start_position);
object.matrixAutoUpdate = false;
注意在这种情况下 matrixAutoUpdate
必须设置成 false
。并且你要确定不要调用 updateMatrix
方法。调用 updateMatrix
会阻断对矩阵的手动更改,会根据位置、伸缩等属性重新计算矩阵。
3.matrixWorldNeedsUpdate
参考https://sogrey.top/Three.js-start/cores/#Object3D
matrixWorldNeedsUpdate : Boolean
当这个属性设置了之后,它将计算在那一帧中的matrixWorld,并将这个值重置为false。默认值为false。
五、窗口 resize事件更新
- 发生场景:当窗口大小发生变化时,会出现局部空白区域。
- 解决方法:重新获取浏览器窗口新的宽高尺寸,然后通过新的宽高尺寸更新相机Camera和渲染器WebGLRenderer的参数。
- 要注意一下,Three.js自适应渲染不一定就是窗口变化,本质上还是你要渲染的区域宽高尺寸变化了;更进一步变化是视图矩阵.matrixWorldInverse和投影矩阵.projectionMatrix的变化。
// onresize 事件会在窗口被调整大小时发生
window.οnresize=function(){
// 重置渲染器输出画布canvas尺寸
renderer.setSize(window.innerWidth,window.innerHeight);
// 全屏情况下:设置观察范围长宽比aspect为窗口宽高比
camera.aspect = window.innerWidth/window.innerHeight;
// 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix
// 但是不会每渲染一帧,就通过相机的属性计算投影矩阵(节约计算资源)
// 如果相机的一些属性发生了变化,需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
camera.updateProjectionMatrix ();
};