第六篇:坐标系与变换:3D空间操作指南

第六篇:坐标系与变换:3D空间操作指南

引言

在3D世界中,坐标系和变换是构建虚拟空间的数学基础。Three.js提供了完整的变换系统,从简单的位移到复杂的矩阵运算。本文将深入解析3D空间的数学原理,并通过Vue3实现一个功能完备的变换编辑器,让你彻底掌握三维空间的操控艺术。


在这里插入图片描述

1. 3D坐标系系统
1.1 坐标系类型
3D坐标系
世界坐标系
局部坐标系
视图坐标系
绝对原点
相对物体原点
相机为中心
  • 世界坐标系:场景的全局参考系,原点(0,0,0)固定
  • 局部坐标系:以物体自身为原点的坐标系
  • 视图坐标系:以相机为原点的坐标系(相机空间)
1.2 坐标转换流程
ObjectWorldCameraScreen模型变换视图变换投影变换ObjectWorldCameraScreen

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 解决方案
  1. 改变旋转顺序

    cube.rotation.order = 'ZYX'; // 使用ZYX顺序避免万向节锁
    
  2. 使用四元数(推荐):

    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 对象层级示例
场景
汽车组
车身
车轮组
前轮1
前轮2
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. 总结

通过本文,你已掌握:

  1. 三大坐标系系统及其转换关系
  2. 位置/旋转/缩放的数学原理与API操作
  3. 欧拉角与万向节锁的解决方案
  4. 四元数的概念与球面插值技术
  5. 矩阵变换的底层原理与应用
  6. Vue3实现功能完备的变换编辑器
  7. 层级对象的变换继承关系

核心原理:Three.js中的所有变换最终都通过4x4变换矩阵计算,矩阵结合了位置、旋转和缩放信息,并通过矩阵乘法实现层级变换的累积。


下一篇预告

第七篇:动画基础:requestAnimationFrame循环
你将学习:

  • 动画循环原理与性能优化
  • 使用GSAP实现高级动画曲线
  • 骨骼动画与变形动画技术
  • 物理动画与碰撞检测
  • 动画混合与状态机管理
  • Vue3实现3D动画编辑器

准备好让你的3D场景动起来了吗?让我们探索Three.js的动画魔法世界!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二川bro

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值