一、行主序、列主序
概念参考行主序 列主序
以线性代数中描述的矩阵为标准,行主序就是依次按行存储,而列主序就是依次按列存储。在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
相对应的计算结果如下:
image.png
因此可以认为,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轴的旋转,这是表示旋转最常用的方式;第六个方法是一种基于轴和角度表示旋转的替代方法。
3.compose
image.png//使用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)); //效果同上
image.png
就是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);
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);
其运行结果为:
image.png
表明后乘方法multiply()的结果就是A∗B
反过来,使用前乘方法A.premultiply(B);
,结果就是B∗A
5.逆矩阵
var matrix = new THREE.Matrix4();
var inverseMatrix = new THREE.Matrix4();
matrix.getInverse(inverseMatrix);
6.例子
image.png参考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即可。
7.Object3D.matrix和matrixWorld
仍然以UnityShader精要笔记二 数学基础中MVP的例子:
image.png就是有个奶牛叫妞妞,她有自己的坐标空间即模型空间,在这个空间里,她的鼻子坐标是(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);
}
image.png
image.png
显然,能看出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);
image.png回到我们的农场游戏。现在我们需要把妞妞的鼻子从世界空间变换到观察空间中。为此我们需要知道世界坐标系下摄像机的变换信息。这同样可以通过摄像机面板中的Transform组件得到:(1,1,1)的缩放,(30,0,0)的旋转,(0,10,-10)的平移。
image.png
可以看到结果与例子中的Z值是相反的,这是因为unity用的左手坐标系,而threejs是右手坐标系。
8.camera相关的matrix
摄像机Cameras 有两个额外的四维矩阵:
- Camera.matrixWorldInverse: 视图矩阵 - 摄像机世界坐标变换的逆矩阵。
- Camera.projectionMatrix: 投影矩阵 - 表示将场景中的信息投影到裁剪空间。
9.更多的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 ();
};
网友评论