美文网首页
three.js 笔记七 Matrix

three.js 笔记七 Matrix

作者: 合肥黑 | 来源:发表于2023-01-05 17:59 被阅读0次
一、行主序、列主序

概念参考行主序 列主序

以线性代数中描述的矩阵为标准,行主序就是依次按行存储,而列主序就是依次按列存储。在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);

其运行结果为:

image.png
在网上找一个在线矩阵计算器,比如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

参考Three.js 克隆其他模型的矩阵 Matrix4

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的例子:

就是有个奶牛叫妞妞,她有自己的坐标空间即模型空间,在这个空间里,她的鼻子坐标是(0,2,4),最后如何显示在屏幕上呢?首先,转化为齐次坐标(0,2,4,1)。顶点变换的第一步就是将顶点坐标从模型空间变换到世界空间,这个变换通常叫做模型变换(model transform)。根据Transform的信息,妞妞进行了(2,2,2)的缩放,(0,150,0)的旋转以及(5,0,25)的平移。根据之前的知识,要先缩放再旋转再平移:

image.png

我们使用代码来验证一下:

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。
但是,我打印一下,发现这个值不对:

image.png
那没办法,我们自己用矩阵乘法计算:
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)的平移。

image.png
image.png

可以看到结果与例子中的Z值是相反的,这是因为unity用的左手坐标系,而threejs是右手坐标系。

8.camera相关的matrix

摄像机Cameras 有两个额外的四维矩阵:

  • Camera.matrixWorldInverse: 视图矩阵 - 摄像机世界坐标变换的逆矩阵。
  • Camera.projectionMatrix: 投影矩阵 - 表示将场景中的信息投影到裁剪空间。
9.更多的API

参考
three.js 数学方法之Matrix4

四、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事件更新

参考Three.js自适应窗口变化渲染

  • 发生场景:当窗口大小发生变化时,会出现局部空白区域。
  • 解决方法:重新获取浏览器窗口新的宽高尺寸,然后通过新的宽高尺寸更新相机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 ();
};

相关文章

网友评论

      本文标题:three.js 笔记七 Matrix

      本文链接:https://www.haomeiwen.com/subject/fhfbdrtx.html