前言
在Threejs中,旋转、平移、缩放一个物体的方法相信大家已经耳熟能详,具体操作如下,
- 平移:调用mesh的translate(X/Y/Z)方法
- 旋转:调用mesh的rotate(X/Y/Z)方法
- 缩放:修改mesh的scale属性
js
// 创建场景
const scene = new THREE.Scene();
// 创建相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
// 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 创建几何体
const geometry = new THREE.BoxGeometry();
// 创建材质
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
// 创建网格
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 平移
cube.translateX(0.01);
cube.translateY(0.01);
cube.translateZ(0.01);
// 旋转
cube.rotateX(0.01);
cube.rotateY(0.01);
cube.rotateZ(0.01);
// 缩放
cube.scale.set(2,2,2)
// 渲染
renderer.render(scene, camera);
那么这些Api操作是如何实现的呢?如果我需要同时平移、旋转、和缩放能否一步操作就可以解决呢?
这些问题的答案其实总结下来就俩个字矩阵,本质上是矩阵运算,接下来我们来分析一下具体的实现原理。
平移
数学原理
假定初始坐标为(x,y,z,1), 那么平移操作就是初始矩阵*模型矩阵(modelMatrix), 这个模型矩阵本质上是平移矩阵,旋转矩阵和缩放矩阵的复合矩阵,第四列的a,b,c分别代表沿着x,y,z的平移量
WebGL fragmentShader
让顶点的x移动0.1,y移动0.2,z移动0.3
js
attribute vec4 a_Position;
//列主序平移矩阵
mat4 m4=mat4(
1.0, 0.0, 0.0,0.0,
0.0, 1.0, 0.0,0.0,
0.0, 0.0, 1.0,0.0,
0.1, 0.2, 0.3,1.0
);
void main(){
gl_Position = m4*a_Position;
}
ThreeJs 源码解析
js
cube.translate(1,2,3);//沿着x轴方向平移1,y轴方向平移2,z轴方向平3
第一步
js
// 源码地址:https://github.com/mrdoob/three.js/blob/dev/src/core/BufferGeometry.js
const _m1 = /*@__PURE__*/ new Matrix4();
.......
translate( x, y, z ) {
// translate geometry
_m1.makeTranslation( x, y, z );
this.applyMatrix4( _m1 );
return this;
}
- m1 :该物体当前模型矩阵(对该物体的平移,旋转,缩放都会修改这个矩阵)
- 调用模型矩阵的makeTranslation方法,修改模型矩阵
js
// 源码地址 https://github.com/mrdoob/three.js/blob/dev/src/math/Matrix4.js
makeTranslation( x, y, z ) {
if ( x.isVector3 ) {
this.set(
1, 0, 0, x.x,
0, 1, 0, x.y,
0, 0, 1, x.z,
0, 0, 0, 1
);
} else {
this.set(
1, 0, 0, x,
0, 1, 0, y,
0, 0, 1, z,
0, 0, 0, 1
);
}
return this;
}
这边会有一个判断,makeTranslation支持传入一个Vector3向量,这个我们暂时不用关心,继续往下看,大家有没有发现一个规律,这个set的右侧x,y,z和我上述说的数学原理中的模型矩阵是对应关系的,这就说明 平移的本质就是对矩阵变换
第二步 调用applyMatrix4 即 上文的this.applyMatrix4( _m1 );
js
// 源码地址:https://github.com/mrdoob/three.js/blob/dev/src/core/BufferGeometry.js
applyMatrix4( matrix ) {
const position = this.attributes.position;
if ( position !== undefined ) {
position.applyMatrix4( matrix );
position.needsUpdate = true;
}
......
}
applyMatrix4方法也很好理解,就是将之前模型矩阵和现在平移后所得的矩阵相乘,得出此次平移后最终的模型矩阵。
旋转
旋转和平移相似,唯一不同就是旋转矩阵和平移矩阵的在模型矩阵中的位置不同
数学原理
一个物体沿着x,y,z 轴分别旋转α, β, γ 度数, 可以看出该矩阵和平移矩阵的区别就是,旋转矩阵是左上角的3*3,平移矩阵是最右侧那一列
WebGL fragmentShader
物体沿z轴旋转60度
glsl
attribute vec4 a_Position;
float angle = radians(60.0);
float cosB = cos(angle);
float sinB = sin(angle);
mat4 m4 = mat4 (
cosB, sinB, 0.0, 0.0,
-sinB, cosB, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
);
void main(){
gl_Position = m4*a_Position;
}
ThreeJs 源码解析
js
// 旋转
cube.rotateZ(Math.PI/4);//沿着z轴旋转π/4
第一步
js
// 源码地址:https://github.com/mrdoob/three.js/blob/dev/src/core/BufferGeometry.js
const _m1 = /*@__PURE__*/ new Matrix4();
.......
rotateZ( angle ) {
// rotate geometry around world z-axis
_m1.makeRotationZ( angle );
this.applyMatrix4( _m1 );
return this;
}
- m1 :该物体当前模型矩阵(对该物体的平移,旋转,缩放都会修改这个矩阵)
- 调用模型矩阵的makeRotationZ方法,修改模型矩阵
js
// 源码地址 https://github.com/mrdoob/three.js/blob/dev/src/math/Matrix4.js
makeRotationZ( theta ) {
const c = Math.cos( theta ), s = Math.sin( theta );
this.set(
c, - s, 0, 0,
s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
);
return this;
}
makeRotationZ中set本质就是上面数学原理中的构造沿着z轴旋转的旋转矩阵
PS: 需要注意的点是在threejs矩阵的存储方式默认是列优先,而在我们大学线代中的矩阵是行优先。就是将我数学原理中的矩阵的行看成three中的列即可
第二步 调用applyMatrix4 即 上文的this.applyMatrix4( _m1 );
js
// 源码地址:https://github.com/mrdoob/three.js/blob/dev/src/core/BufferGeometry.js
applyMatrix4( matrix ) {
const position = this.attributes.position;
if ( position !== undefined ) {
position.applyMatrix4( matrix );
position.needsUpdate = true;
}
......
}
原理同上,将此次旋转所得的矩阵和之前的模型矩阵相乘,得出现在的模型矩阵
缩放
缩放也同上,唯一不同就是缩放矩阵
数学原理
下面公式表示沿着x,y,z轴分别缩放a,b,c倍
WebGL fragmentShader
x轴向缩放2,y轴向缩放3,z轴向缩放4
glsl
attribute vec4 a_Position;
// 声明缩放矩阵
mat4 m4 = mat4(
2.0, 0.0, 0.0,0.0,
0.0, 3.0, 0.0,0.0,
0.0, 0.0, 4.0,0.0,
0.0, 0.0, 0.0,1.0
);
void main(){
gl_Position = m4*a_Position;
}
ThreeJs 源码解析
js
// 缩放
cube.scale.set(1, 2, 3) // x轴缩放1倍,y轴缩放2倍,z轴缩放3倍
第一步
js
// 源码地址:https://github.com/mrdoob/three.js/blob/dev/src/core/BufferGeometry.js
const _m1 = /*@__PURE__*/ new Matrix4();
.......
scale( x, y, z ) {
// scale geometry
_m1.makeScale( x, y, z );
this.applyMatrix4( _m1 );
return this;
}
- m1 :该物体当前模型矩阵(对该物体的平移,旋转,缩放都会修改这个矩阵)
- 调用模型矩阵的makeRotationZ方法,修改模型矩阵
js
// 源码地址 https://github.com/mrdoob/three.js/blob/dev/src/math/Matrix4.js
makeScale( x, y, z ) {
this.set(
x, 0, 0, 0,
0, y, 0, 0,
0, 0, z, 0,
0, 0, 0, 1
);
return this;
}
makeScale中set本质就是上面数学原理中的构造缩放矩阵
第二步 调用applyMatrix4 即 上文的this.applyMatrix4( _m1 );
js
// 源码地址:https://github.com/mrdoob/three.js/blob/dev/src/core/BufferGeometry.js
applyMatrix4( matrix ) {
const position = this.attributes.position;
if ( position !== undefined ) {
position.applyMatrix4( matrix );
position.needsUpdate = true;
}
......
}
原理同上,将此次缩放所得的矩阵和之前的模型矩阵相乘,得出现在的模型矩阵
总结
现在我在前言中提出的问题其实就迎刃而解了
【如果我需要同时平移、旋转、和缩放能否一步操作就可以解决呢?】
答: 构造平移,旋转,缩放的复合矩阵;再乘以该mesh现在的模型矩阵