第六篇:坐标系与变换:3D空间操作指南
引言
在3D世界中,坐标系和变换是构建虚拟空间的数学基础。Three.js提供了完整的变换系统,从简单的位移到复杂的矩阵运算。本文将深入解析3D空间的数学原理,并通过Vue3实现一个功能完备的变换编辑器,让你彻底掌握三维空间的操控艺术。
1. 3D坐标系系统
1.1 坐标系类型
- 世界坐标系:场景的全局参考系,原点(0,0,0)固定
- 局部坐标系:以物体自身为原点的坐标系
- 视图坐标系:以相机为原点的坐标系(相机空间)
1.2 坐标转换流程
2. 基础变换操作
2.1 位置(Position)
// 设置立方体位置
cube.position.set(2, 3, -1);
// 相对移动
cube.translateX(1); // X轴移动1单位
cube.translateOnAxis(new THREE.Vector3(0,1,0), 2); // 沿Y轴移动2单位
2.2 旋转(Rotation)
// 欧拉角旋转 (弧度制)
cube.rotation.set(Math.PI/4, 0, 0); // 绕X轴旋转45度
// 轴角旋转
const axis = new THREE.Vector3(1,1,0).normalize();
cube.rotateOnAxis(axis, Math.PI/6); // 绕(1,1,0)轴旋转30度
2.3 缩放(Scale)
// 均匀缩放
cube.scale.set(2, 2, 2); // 放大2倍
// 非均匀缩放
cube.scale.set(1, 2, 1); // Y轴拉伸
3. 欧拉角与万向节锁
3.1 欧拉角原理
欧拉角使用三个角度表示旋转:
- Pitch(俯仰角):绕X轴旋转
- Yaw(偏航角):绕Y轴旋转
- Roll(翻滚角):绕Z轴旋转
// Three.js默认旋转顺序:YXZ
cube.rotation.set(y, x, z);
3.2 万向节锁问题
当俯仰角为±90°时,偏航和翻滚轴重合,失去一个自由度:
graph LR
A[正常状态] --> B[三轴独立]
C[俯仰90°] --> D[偏航与翻滚轴重合]
复现问题:
cube.rotation.x = Math.PI/2; // 俯仰90°
cube.rotation.y = 0.5; // 偏航
cube.rotation.z = 0.3; // 实际效果与单独旋转Y轴相同
3.3 解决方案
-
改变旋转顺序:
cube.rotation.order = 'ZYX'; // 使用ZYX顺序避免万向节锁
-
使用四元数(推荐):
const quaternion = new THREE.Quaternion(); quaternion.setFromEuler(new THREE.Euler(0, 0.5, 0.3, 'XYZ')); cube.quaternion.copy(quaternion);
4. 四元数:高级旋转表示
4.1 四元数概念
四元数由1个实部和3个虚部组成:
$ q = w + xi + yj + zk $
在Three.js中表示为:
const quat = new THREE.Quaternion(x, y, z, w);
4.2 四元数操作
// 从轴角创建
const axis = new THREE.Vector3(0, 1, 0);
const angle = Math.PI/4;
quaternion.setFromAxisAngle(axis, angle);
// 球面插值(Slerp)
const startQuat = new THREE.Quaternion();
const endQuat = new THREE.Quaternion().setFromAxisAngle(axis, Math.PI/2);
const currentQuat = new THREE.Quaternion();
currentQuat.slerpQuaternions(startQuat, endQuat, 0.5); // 50%插值
// 应用旋转
cube.quaternion.copy(currentQuat);
4.3 欧拉角与四元数转换
// 欧拉角 -> 四元数
const euler = new THREE.Euler(Math.PI/6, 0, 0);
const quat = new THREE.Quaternion();
quat.setFromEuler(euler);
// 四元数 -> 欧拉角
const newEuler = new THREE.Euler();
newEuler.setFromQuaternion(quat);
5. 矩阵变换:底层原理
5.1 变换矩阵结构
4x4齐次坐标矩阵:
[sx00tx0sy0ty00sztz0001]×[r11r12r130r21r22r230r31r32r3300001]
\begin{bmatrix}
s_x & 0 & 0 & t_x \\
0 & s_y & 0 & t_y \\
0 & 0 & s_z & t_z \\
0 & 0 & 0 & 1 \\
\end{bmatrix}
\times
\begin{bmatrix}
r_{11} & r_{12} & r_{13} & 0 \\
r_{21} & r_{22} & r_{23} & 0 \\
r_{31} & r_{32} & r_{33} & 0 \\
0 & 0 & 0 & 1 \\
\end{bmatrix}
sx0000sy0000sz0txtytz1×r11r21r310r12r22r320r13r23r3300001
5.2 矩阵操作API
const matrix = new THREE.Matrix4();
// 构建变换矩阵
matrix.compose(
new THREE.Vector3(2, 0, 0), // 位置
new THREE.Quaternion(), // 旋转
new THREE.Vector3(1, 1, 1) // 缩放
);
// 应用矩阵到物体
cube.applyMatrix4(matrix);
// 矩阵分解
const position = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
const scale = new THREE.Vector3();
matrix.decompose(position, quaternion, scale);
5.3 模型视图矩阵
// 获取模型矩阵(局部->世界)
const modelMatrix = cube.matrixWorld;
// 获取视图矩阵(世界->相机)
const viewMatrix = camera.matrixWorldInverse;
// 模型视图矩阵 = 视图矩阵 × 模型矩阵
const modelViewMatrix = viewMatrix.clone().multiply(modelMatrix);
6. Vue3实战:物体变换编辑器
6.1 项目结构
src/
├── components/
│ ├── TransformEditor.vue // 变换控制面板
│ ├── GizmoController.vue // 3D操控器
│ └── MatrixDisplay.vue // 矩阵可视化
└── App.vue
6.2 变换控制核心代码
<!-- TransformEditor.vue -->
<script setup>
import { ref, reactive, watch } from 'vue';
const transform = reactive({
position: { x: 0, y: 0, z: 0 },
rotationEuler: { x: 0, y: 0, z: 0 },
rotationQuat: { x: 0, y: 0, z: 0, w: 1 },
scale: { x: 1, y: 1, z: 1 },
useQuaternion: false
});
// 更新物体变换
const updateObject = (obj) => {
// 位置
obj.position.set(
transform.position.x,
transform.position.y,
transform.position.z
);
// 旋转
if (transform.useQuaternion) {
obj.quaternion.set(
transform.rotationQuat.x,
transform.rotationQuat.y,
transform.rotationQuat.z,
transform.rotationQuat.w
);
} else {
obj.rotation.set(
THREE.MathUtils.degToRad(transform.rotationEuler.x),
THREE.MathUtils.degToRad(transform.rotationEuler.y),
THREE.MathUtils.degToRad(transform.rotationEuler.z)
);
}
// 缩放
obj.scale.set(
transform.scale.x,
transform.scale.y,
transform.scale.z
);
};
// 从物体同步数据
const syncFromObject = (obj) => {
transform.position = { ...obj.position };
transform.scale = { ...obj.scale };
if (transform.useQuaternion) {
transform.rotationQuat = {
x: obj.quaternion.x,
y: obj.quaternion.y,
z: obj.quaternion.z,
w: obj.quaternion.w
};
} else {
const euler = new THREE.Euler();
euler.setFromQuaternion(obj.quaternion, obj.rotation.order);
transform.rotationEuler = {
x: THREE.MathUtils.radToDeg(euler.x),
y: THREE.MathUtils.radToDeg(euler.y),
z: THREE.MathUtils.radToDeg(euler.z)
};
}
};
// 监听变换变化
watch(transform, () => {
emit('update', transform);
}, { deep: true });
</script>
<template>
<div class="transform-editor">
<h3>位置</h3>
<VectorControl v-model="transform.position" />
<h3>旋转</h3>
<div class="rotation-mode">
<label>
<input type="checkbox" v-model="transform.useQuaternion">
使用四元数
</label>
</div>
<template v-if="!transform.useQuaternion">
<VectorControl
v-model="transform.rotationEuler"
:labels="['Pitch (X)', 'Yaw (Y)', 'Roll (Z)']"
:min="-180" :max="180"
/>
<GimbalLockWarning :rotation="transform.rotationEuler" />
</template>
<template v-else>
<QuaternionControl v-model="transform.rotationQuat" />
</template>
<h3>缩放</h3>
<VectorControl v-model="transform.scale" :min="0.01" :max="5" />
</div>
</template>
6.3 四元数控制组件
<!-- QuaternionControl.vue -->
<script setup>
import { ref, computed } from 'vue';
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
const quat = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
});
// 轴角表示法
const axisAngle = computed({
get: () => {
const axis = new THREE.Vector3();
const angle = new THREE.Quaternion(
quat.value.x,
quat.value.y,
quat.value.z,
quat.value.w
).angleTo(new THREE.Quaternion());
return {
axis: { x: axis.x, y: axis.y, z: axis.z },
angle: THREE.MathUtils.radToDeg(angle)
};
},
set: (val) => {
const axis = new THREE.Vector3(val.axis.x, val.axis.y, val.axis.z);
const quaternion = new THREE.Quaternion();
quaternion.setFromAxisAngle(
axis.normalize(),
THREE.MathUtils.degToRad(val.angle)
);
quat.value = {
x: quaternion.x,
y: quaternion.y,
z: quaternion.z,
w: quaternion.w
};
}
});
// 欧拉角表示(仅用于显示)
const eulerDisplay = computed(() => {
const euler = new THREE.Euler();
euler.setFromQuaternion(
new THREE.Quaternion(
quat.value.x,
quat.value.y,
quat.value.z,
quat.value.w
)
);
return {
x: THREE.MathUtils.radToDeg(euler.x).toFixed(1),
y: THREE.MathUtils.radToDeg(euler.y).toFixed(1),
z: THREE.MathUtils.radToDeg(euler.z).toFixed(1)
};
});
</script>
<template>
<div class="quaternion-control">
<div class="axis-angle">
<h4>轴角表示</h4>
<VectorControl
v-model="axisAngle.axis"
:min="-1" :max="1" :step="0.01"
:labels="['轴X', '轴Y', '轴Z']"
/>
<ParamRange
label="角度"
v-model="axisAngle.angle"
:min="0" :max="360" :step="1"
/>
</div>
<div class="euler-display">
<h4>等效欧拉角</h4>
<p>X: {{ eulerDisplay.x }}°</p>
<p>Y: {{ eulerDisplay.y }}°</p>
<p>Z: {{ eulerDisplay.z }}°</p>
</div>
</div>
</template>
6.4 3D操控器组件
<!-- GizmoController.vue -->
<script setup>
import { ref, onMounted } from 'vue';
import * as THREE from 'three';
import { TransformControls } from 'three/addons/controls/TransformControls.js';
const props = defineProps(['object', 'camera', 'renderer']);
const emit = defineEmits(['change']);
let transformControls = ref(null);
onMounted(() => {
// 创建变换控制器
transformControls.value = new TransformControls(
props.camera,
props.renderer.domElement
);
// 附加到物体
transformControls.value.attach(props.object);
scene.add(transformControls.value);
// 监听变换事件
transformControls.value.addEventListener('change', () => {
emit('change', props.object);
});
// 模式切换
transformControls.value.addEventListener('dragging-changed', (event) => {
orbitControls.enabled = !event.value;
});
});
// 设置变换模式
const setMode = (mode) => {
transformControls.value.setMode(mode); // 'translate'/'rotate'/'scale'
};
</script>
<template>
<div class="gizmo-controls">
<button @click="setMode('translate')">位移</button>
<button @click="setMode('rotate')">旋转</button>
<button @click="setMode('scale')">缩放</button>
<button @click="transformControls.reset()">重置</button>
</div>
</template>
6.5 矩阵可视化组件
<!-- MatrixDisplay.vue -->
<script setup>
import { computed } from 'vue';
const props = defineProps(['matrix']);
// 格式化矩阵显示
const matrixRows = computed(() => {
const rows = [];
const flat = props.matrix.elements;
for (let i = 0; i < 4; i++) {
rows.push([
flat[i * 4].toFixed(2),
flat[i * 4 + 1].toFixed(2),
flat[i * 4 + 2].toFixed(2),
flat[i * 4 + 3].toFixed(2)
]);
}
return rows;
});
</script>
<template>
<div class="matrix-display">
<h3>变换矩阵</h3>
<table>
<tr v-for="(row, i) in matrixRows" :key="i">
<td v-for="(cell, j) in row" :key="j">{{ cell }}</td>
</tr>
</table>
</div>
</template>
7. 变换组合与层级关系
7.1 对象层级示例
7.2 层级变换代码
// 创建汽车组
const carGroup = new THREE.Group();
scene.add(carGroup);
// 添加车身(相对汽车组)
const body = new THREE.Mesh(bodyGeo, bodyMat);
body.position.set(0, 0.5, 0);
carGroup.add(body);
// 添加车轮组(相对汽车组)
const wheelsGroup = new THREE.Group();
carGroup.add(wheelsGroup);
// 添加车轮(相对车轮组)
const wheel1 = new THREE.Mesh(wheelGeo, wheelMat);
wheel1.position.set(1, 0.3, 1);
wheelsGroup.add(wheel1);
// 旋转车轮组(所有车轮同时旋转)
wheelsGroup.rotation.y = 0.1;
// 移动整个汽车组
carGroup.position.x = 5;
7.3 世界坐标计算
// 获取车轮的世界位置
const wheelWorldPos = new THREE.Vector3();
wheel1.getWorldPosition(wheelWorldPos);
// 获取车轮的世界旋转
const wheelWorldQuat = new THREE.Quaternion();
wheel1.getWorldQuaternion(wheelWorldQuat);
8. 常见问题解答
Q1:为什么物体旋转后移动方向不对?
- 使用局部坐标系移动:
// 沿物体自身Z轴移动 cube.translateZ(1); // 沿世界坐标系Y轴移动 cube.position.y += 1;
Q2:如何让物体始终面向相机?
function update() {
// 计算物体指向相机的方向
const direction = new THREE.Vector3();
camera.getWorldPosition(direction);
object.getWorldPosition(tempVector);
direction.sub(tempVector);
// 应用旋转
object.quaternion.setFromUnitVectors(
new THREE.Vector3(0, 0, 1), // 物体默认朝向
direction.normalize()
);
}
Q3:如何实现平滑过渡动画?
// 使用GSAP实现缓动
import gsap from 'gsap';
gsap.to(cube.position, {
x: 5,
duration: 2,
ease: "power2.out"
});
// 四元数球面插值
const startQuat = cube.quaternion.clone();
const endQuat = new THREE.Quaternion().setFromEuler(new Euler(0, Math.PI, 0));
gsap.to({ t: 0 }, {
t: 1,
duration: 1,
onUpdate: (tween) => {
const t = tween.targets()[0].t;
cube.quaternion.slerpQuaternions(startQuat, endQuat, t);
}
});
9. 总结
通过本文,你已掌握:
- 三大坐标系系统及其转换关系
- 位置/旋转/缩放的数学原理与API操作
- 欧拉角与万向节锁的解决方案
- 四元数的概念与球面插值技术
- 矩阵变换的底层原理与应用
- Vue3实现功能完备的变换编辑器
- 层级对象的变换继承关系
核心原理:Three.js中的所有变换最终都通过4x4变换矩阵计算,矩阵结合了位置、旋转和缩放信息,并通过矩阵乘法实现层级变换的累积。
下一篇预告
第七篇:动画基础:requestAnimationFrame循环
你将学习:
- 动画循环原理与性能优化
- 使用GSAP实现高级动画曲线
- 骨骼动画与变形动画技术
- 物理动画与碰撞检测
- 动画混合与状态机管理
- Vue3实现3D动画编辑器
准备好让你的3D场景动起来了吗?让我们探索Three.js的动画魔法世界!