首页 / Three.js 入门教程 / 坐标系与变换

Three.js 入门教程

坐标系与变换

本教程共 40 篇 · 第 9 篇 · 更新于 2026-08-14 · 约 7 分钟阅读

Three.js坐标系变换positionrotationscalelookAtObject3D

本节目标:理解 three.js 的右手坐标系、世界坐标与局部坐标的区别,掌握 Object3D 的 position / rotation / scale 三大变换,学会用 lookAt 控制朝向。

三维坐标系

3D 空间用三个互相垂直的轴描述位置:X、Y、Z。三个轴交于原点 (0, 0, 0)。任何位置都能写成 (x, y, z) 这样的坐标。

右手坐标系

three.js 使用右手坐标系。在默认视角(相机未旋转)下:

  • X 轴指向屏幕右方。
  • Y 轴指向屏幕上方。
  • Z 轴指向屏幕外,朝向你。

用右手就能记住:拇指 X、食指 Y、中指 Z,三根手指两两垂直。这是 three.js 官方手册与社区确认的约定。

默认视角下各轴方向可以记成一张表:

正方向负方向
X屏幕右方屏幕左方
Y屏幕上方屏幕下方
Z屏幕外(朝向你)屏幕里
Note

坐标系约定并不统一:CSS 的 Y 轴向下,很多建模软件的 Z 轴向上。导入模型或对接其他工具时,经常要做轴转换,这是新手最容易懵的地方。

世界坐标与局部坐标

场景定义世界空间,原点在场景中心。直接加进场景的物体,位置是相对世界空间说的。

但每个物体还有一个局部空间:物体的坐标原点就在它自己身上。几何体的顶点,是在局部空间里描述的。一个 2×2×2 的立方体,顶点坐标在局部空间里写,不管这个立方体被搬到世界空间的哪里。

两者什么时候有区别?当物体有父子关系时:

parent.add(child);
child.position.set(2, 0, 0);

child.position 永远是 (2, 0, 0)——这是相对 parent 的局部坐标。但 parent 一旦旋转,child 在世界空间里的实际位置就会绕圈变化。这个”子随父动”的规则是场景图的基础。

Object3D:变换三件套

网格、灯光、相机、场景,都继承自 Object3D 基类。基类定义了三个变换属性,合称 TRS:

  • position:位置(平移),类型是 Vector3。
  • rotation:旋转,类型是 Euler。
  • scale:缩放,类型是 Vector3。

材质和几何体没有这些属性——它们不是场景里的”物体”,没有位置可言。

position:平移

cube.position.x = 2;
cube.position.set(2, 0, 0); // 一次性设置三个轴

默认值是 (0, 0, 0)。位置单位约定为米:1 个 three.js 单位 = 1 米。

默认视角下(相机未旋转):+X 右、+Y 上、+Z 朝屏幕外,加负号则方向相反。比如 position.set(0, 1, -3) 就是把物体放到上方 1 米、屏幕里 3 米处。

scale:缩放

cube.scale.set(2, 2, 2); // 整体放大一倍
cube.scale.set(1, 2, 1); // 只拉高 Y 轴

默认值是 (1, 1, 1)。三个轴同一个值是均匀缩放,不一样是非均匀缩放。负值会把物体镜像翻转,日常基本用不到。

Note

缩放改变的是画出来的大小,几何体的顶点数据不变。相机和灯光不建议缩放,行为会很奇怪。

rotation:旋转

cube.rotation.x = Math.PI / 2; // 绕 X 轴转 90°
cube.rotation.y += THREE.MathUtils.degToRad(45);

rotation 是 Euler 角,单位是弧度,不是度。180° 等于 π,90° 等于 π/2。记不住换算就用 THREE.MathUtils.degToRad(角度)

three.js 里唯一用”度”的角度参数是透视相机的 fov,其他全是弧度。

旋转有一个特点:绕三个轴的顺序会影响结果。先转 X 再转 Y,和先转 Y 再转 X,最终朝向可能不同。three.js 默认按 XYZ 顺序计算,日常使用不用管,但要知道有这回事。

Tip

three.js 还有一套四元数(Quaternion)旋转,存在 object.quaternion。改 rotation 会自动同步到 quaternion。四元数没有万向锁问题,做复杂动画时再学。

父子关系与场景图

add 把物体挂到另一个物体下面,就形成了父子关系:

scene.add(parent);
parent.add(child);

父级变换,子级跟着变。这个层级结构叫场景图(scene graph),是 three.js 组织场景的基本方式,细节在第 20 章展开。想按名字找物体,可以用 scene.getObjectByName('名字')

lookAt:面向目标

lookAt 让物体朝向世界空间里的一个点:

cube.lookAt(0, 0, 0);         // 面向原点
cube.lookAt(camera.position); // 面向相机

相机用它时,镜头对准目标;普通网格用它时,物体的 +Z 轴转向目标。第 6 章的相机跟随,用的就是它。常见用途:让广告牌、指示箭头始终朝向观察者。

获取世界坐标

子物体的 position 是局部坐标。想要它在世界空间里的实际位置,用 getWorldPosition

const worldPos = new THREE.Vector3();
child.getWorldPosition(worldPos); // 结果写入 worldPos

getWorldPosition 需要一个目标 Vector3 来接收结果。在动画循环里打印它,能看到子物体的世界坐标随父级旋转不断变化,而 child.position 始终是 (2, 0, 0)——这就是局部坐标和世界坐标最直观的差别。

另外,lookAt 默认以 +Y 方向为”上”。做飞行、相机跟随这类效果时,如果物体倒立了,可以通过 object.up 指定新的上方向。

可运行示例:父子变换与 lookAt

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>9. 坐标系与变换</title>
  <style>
    * { margin: 0; padding: 0; }
    body { overflow: hidden; background: #1a1a2e; }
    canvas { display: block; }
    #info {
      position: fixed; left: 12px; top: 12px; color: #fff;
      font: 13px/1.7 sans-serif; background: rgba(0, 0, 0, 0.5);
      padding: 8px 12px; border-radius: 6px;
    }
  </style>
</head>
<body>
  <div id="info">
    红/绿/蓝线:X / Y / Z 轴(AxesHelper)<br />
    绿色:父立方体,绕 Y 轴自转<br />
    橙色:子立方体,局部坐标 (2, 0, 0),随父级公转<br />
    蓝色:始终面向相机的立方体(lookAt)
  </div>

  <script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@0.185.0/build/three.module.js",
      "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.185.0/examples/jsm/"
    }
  }
  </script>

  <script type="module">
  import * as THREE from 'three';

  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0x1a1a2e);

  // 坐标轴辅助线:红 X / 绿 Y / 蓝 Z,长度 3
  const axes = new THREE.AxesHelper(3);
  scene.add(axes);

  // 父立方体
  const parent = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshBasicMaterial({ color: 0x69db7c })
  );
  scene.add(parent);

  // 子立方体:挂在父级下,局部坐标 (2, 0, 0)
  const child = new THREE.Mesh(
    new THREE.BoxGeometry(0.6, 0.6, 0.6),
    new THREE.MeshBasicMaterial({ color: 0xffa94d })
  );
  child.position.set(2, 0, 0);
  parent.add(child);

  // 面向相机的立方体
  const facing = new THREE.Mesh(
    new THREE.BoxGeometry(0.5, 0.5, 0.5),
    new THREE.MeshBasicMaterial({ color: 0x4dabf7 })
  );
  facing.position.set(0, 2.2, 0);
  scene.add(facing);

  const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 100);
  camera.position.set(4, 3, 6);
  camera.lookAt(0, 0, 0);

  const renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(innerWidth, innerHeight);
  document.body.append(renderer.domElement);

  const clock = new THREE.Clock();
  function animate() {
    requestAnimationFrame(animate);
    const delta = clock.getDelta();

    // 父级旋转:子级跟着绕 Y 轴公转
    parent.rotation.y += 0.8 * delta;

    // 子级在局部空间里再自转
    child.rotation.z += 2 * delta;

    // lookAt:让立方体永远面向相机
    facing.lookAt(camera.position);

    renderer.render(scene, camera);
  }
  animate();
  </script>
</body>
</html>

运行后观察橙色小立方体:它一边跟着绿色父立方体公转,一边自己旋转。它的局部坐标一直是 (2, 0, 0),但世界坐标每帧都在变。蓝色立方体无论相机怎么摆(可以改 camera.position.set 试试),正面始终朝向你。

Tip

调试变换时,用 AxesHelper 显示坐标轴,一眼就能看出物体往哪个方向偏。示例里的红绿蓝线就是它,属于 three.js 核心,不需要额外引入。