坐标系与变换
本教程共 40 篇 · 第 9 篇 · 更新于 2026-08-14 · 约 7 分钟阅读
本节目标:理解 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 顺序计算,日常使用不用管,但要知道有这回事。
Tipthree.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 核心,不需要额外引入。