《Babylonjs入门》第 7 章 旋转的真相
§7.1 本章要做成什么样
最终效果:一个"旋转调试器",展示同一个立方体用三种不同方式旋转的效果对比:欧拉角(Euler angles)、四元数(Quaternion)和 lookAt。读者可以拖动滑块改变角度,直观看到万向锁现象以及四元数如何避免它。

直达链接:http://localhost:18000/?ch=07
为什么这章很重要:第 6 章我们学会了向量,但向量只能表示方向和位移,不能表示"绕某个轴转多少度"。旋转是 3D 编程中最容易踩坑的部分——你以为只是改个角度,结果物体乱飞、相机翻转、动画卡顿。本章揭开旋转的数学本质,让你知道该用哪个 API。
§7.2 欧拉角:直觉与陷阱
7.2.1 什么是欧拉角
欧拉角(Euler Angles):用三个角度分别表示绕 X、Y、Z 轴的旋转。在 Babylon.js 中,这是 mesh.rotation 属性,类型是 Vector3。
import { Vector3 } from '@babylonjs/core'
// 创建一个立方体
const box = MeshBuilder.CreateBox('box', { size: 1 }, scene)
// 绕 Y 轴旋转 45 度(注意:单位是弧度,不是角度!)
box.rotation = new Vector3(0, Math.PI / 4, 0)
// 或者分别设置每个轴
box.rotation.x = Math.PI / 6 // 绕 X 轴转 30°
box.rotation.y = Math.PI / 4 // 绕 Y 轴转 45°
box.rotation.z = 0 // Z 轴不转
关键点:
- 单位是弧度(radians),不是角度(degrees)。转换公式:
弧度 = 角度 × π / 180。 - 顺序很重要:Babylon.js 默认使用 YXZ 顺序(先绕 Y 轴,再绕 X 轴,最后绕 Z 轴)。这意味着
rotation = (π/2, π/2, 0)不等于(π/2, 0, π/2)。 rotation是局部空间的旋转,相对于父对象。换用rotationQuaternion并不会把它变成世界空间——四元数描述的同样是这个节点相对父对象的朝向;世界空间的朝向只能从世界矩阵里读(getWorldMatrix(),第 8 章讲父子链时用到)。想绕世界轴转,用的是rotate(axis, angle, Space.WORLD),见 §7.6。
源码位置:Meshes/transformNode.pure.d.ts:139-143
/**
* Gets or sets the rotation property : a Vector3 defining the rotation value in radians
* around each local axis X, Y, Z (default is (0.0, 0.0, 0.0)).
* If rotation quaternion is set, this Vector3 will be ignored and copy from the quaternion
*/
get rotation(): Vector3;
set rotation(newRotation: Vector3);
注意注释最后一句:如果设置了 rotationQuaternion,rotation 会被忽略。这是第一个坑。
7.2.2 万向锁(Gimbal Lock):欧拉角的致命缺陷
万向锁现象:当某个轴的旋转达到 ±90° 时,另外两个轴会重合,导致失去一个自由度。你无法再独立控制三个方向的旋转。
经典案例:第一人称射击游戏的相机。当你抬头到 90°(直视天空)时,左右转动鼠标会让相机绕 Z 轴翻滚,而不是绕 Y 轴水平转动。这就是万向锁。
数学解释:假设旋转顺序是 YXZ。当你把 X 轴转到 90° 时,原来的 Z 轴现在和世界空间的 Y 轴对齐了。此时再转 Y 或 Z,效果是一样的——你失去了区分两者的能力。
可视化实验:
// 创建一个参考坐标系
const axes = MeshBuilder.CreateBox('axes', { size: 0.1 }, scene)
axes.scaling = new Vector3(5, 0.1, 0.1) // X 轴(红色长条)
// 逐渐增加 X 轴旋转
box.rotation.x += 0.01 // 每帧转一点点
// 当 x 接近 π/2 (90°) 时,你会发现:
// - 修改 rotation.y 和 rotation.z 的效果变得相同
// - 物体出现"跳跃"式的翻转
解决方案:不要用欧拉角处理连续旋转或插值动画。改用四元数。
这个比喻到这就断了:很多人说"万向锁就像陀螺仪卡住",但这不准确。万向锁不是机械故障,而是数学表示的固有局限。即使你用完美的硬件,欧拉角依然会在 90° 时丢失自由度。
§7.3 四元数:旋转的正确打开方式
7.3.1 四元数的结构
四元数(Quaternion):用四个数字 (x, y, z, w) 表示旋转。其中 (x, y, z) 是旋转轴的方向,w 与旋转角度相关。
从源码看(Maths/math.vector.pure.d.ts:2487-2533):
export declare class Quaternion implements Tensor<Tuple<number, 4>, Quaternion>, IQuaternionLike {
get x(): number;
get y(): number;
get z(): number;
get w(): number;
constructor(x?: number, y?: number, z?: number, w?: number);
}
关键特性:
- 默认值是
(0, 0, 0, 1),表示"无旋转"。注意w=1,不是 0! - 必须是单位四元数:
x² + y² + z² + w² = 1。如果不是,旋转会缩放物体。 - 没有直观的几何意义:你不能像欧拉角那样说"绕 Y 轴转 30°",必须通过 API 计算。
7.3.2 创建四元数
方法 1:从欧拉角转换
import { Quaternion } from '@babylonjs/core'
// 从欧拉角(弧度)创建四元数
const quat = Quaternion.FromEulerAngles(0, Math.PI / 4, 0) // 绕 Y 轴转 45°
// 写入已有四元数(避免分配新对象)
const existingQuat = new Quaternion()
Quaternion.FromEulerAnglesToRef(0, Math.PI / 4, 0, existingQuat)
源码位置:math.vector.pure.d.ts:3061-3071
static FromEulerAngles(x: number, y: number, z: number): Quaternion;
static FromEulerAnglesToRef<T extends Quaternion>(x: number, y: number, z: number, result: T): T;
方法 2:从旋转轴和角度
// 绕 Y 轴旋转 45°
const axis = new Vector3(0, 1, 0)
const angle = Math.PI / 4
const quat = Quaternion.RotationAxis(axis, angle)
方法 3:从两个方向向量
// 创建从"向上"旋转到"目标方向"的四元数
const from = Vector3.Up()
const to = new Vector3(1, 1, 0).normalize()
const quat = new Quaternion()
Quaternion.FromUnitVectorsToRef(from, to, quat)
源码位置:math.vector.pure.d.ts:3096
static FromUnitVectorsToRef<T extends Quaternion>(
vecFrom: DeepImmutable<Vector3>,
vecTo: DeepImmutable<Vector3>,
result: T,
epsilon?: number
): T;
方法 4:从三个正交轴构建旋转矩阵对应的四元数
// 给定三个互相垂直的单位向量(右、上、前)
const right = new Vector3(1, 0, 0)
const up = new Vector3(0, 1, 0)
const forward = new Vector3(0, 0, 1)
const quat = Quaternion.RotationQuaternionFromAxis(right, up, forward)
源码位置:math.vector.pure.d.ts:3143-3153
static RotationQuaternionFromAxis(
axis1: DeepImmutable<Vector3>,
axis2: DeepImmutable<Vector3>,
axis3: DeepImmutable<Vector3>
): Quaternion;
7.3.3 应用四元数到物体
const box = MeshBuilder.CreateBox('box', { size: 1 }, scene)
// 方式 1:直接赋值
box.rotationQuaternion = Quaternion.FromEulerAngles(0, Math.PI / 4, 0)
// 方式 2:原地修改(推荐,避免 GC)
if (!box.rotationQuaternion) {
box.rotationQuaternion = new Quaternion()
}
Quaternion.FromEulerAnglesToRef(0, Math.PI / 4, 0, box.rotationQuaternion)
重要规则(源码注释强调):
Meshes/transformNode.pure.d.ts:150-152: “If set, only the rotationQuaternion is then used to compute the node rotation (ie. node.rotation will be ignored)”
一旦你设置了 rotationQuaternion,就不要再碰 rotation 属性。两者混用会导致不可预测的行为。如果你想用四元数,就把 rotation 设为 (0, 0, 0) 并忘记它。
§7.4 rotation vs rotationQuaternion:二选一
7.4.1 何时用哪个
| 特性 | rotation(欧拉角) | rotationQuaternion(四元数) |
|---|---|---|
| 可读性 | 好,(0, π/2, 0) 容易理解 | 差,(0, 0.38, 0, 0.92) 看不懂 |
| 性能 | 稍慢(内部要转成四元数) | 快(直接使用) |
| 插值平滑 | 会出现万向锁 | 完美平滑(用球面插值) |
| 累加旋转 | 顺序敏感,容易出错 | 安全(四元数乘法) |
| 适用场景 | 静态角度、简单旋转 | 动画、相机、物理模拟 |
黄金法则:
- 简单场景(如"让牌子转 90°"):用
rotation。 - 复杂场景(如"相机跟随角色"、“动画混合”):用
rotationQuaternion。 - 一个物体只认其中一个:不是"别同时改两者",而是改了一个另一个就不参与合成——
rotationQuaternion一旦存在,rotation立刻失效(下一节实测)。所以决定用哪个要从建这个物体时就定下来,别中途换轨。 - 要四元数就显式给一个带朝向的四元数:
box.rotationQuaternion = Quaternion.RotationYawPitchRoll(box.rotation.y, box.rotation.x, box.rotation.z)(静态方法在Maths/math.vector.pure.d.ts:3105;参数顺序是 yaw/pitch/roll,对应rotation的 y/x/z)。别写new Quaternion()——它的默认值是(0, 0, 0, 1)(math.vector.pure.js:3909),即单位四元数 = 不旋转:物体当场转回初始朝向,而box.rotation里的数字还原样读得出来,看起来就像"我明明设了 45°怎么没转"。另一个办法是调rotate(),它会自动切到四元数并把rotation清零。lookAt()不会替你切模式(§7.5.4)。
7.4.2 静默失效陷阱:两种模式互不相干
网上常见的说法是"设置 rotation 时 Babylon 会自动更新内部的 rotationQuaternion,反之亦然"。实测(v9.30)不是这样:rotation 和 rotationQuaternion 是两份互相独立的存储,改其中一个不会替你算出另一个。合成世界矩阵时引擎二选一(源码 Meshes/transformNode.pure.js:965-967):
rotationQuaternion不是 null → 用四元数,此时你怎么改rotation都不生效rotationQuaternion是 null → 用rotation走 YawPitchRoll,rotationQuaternion保持 null,引擎不会凭空创建一个
const box = MeshBuilder.CreateBox('box', { size: 1 }, scene)
box.rotation.y = Math.PI / 4
console.log(box.rotationQuaternion) // null ← 引擎没有替你生成四元数
box.rotationQuaternion = new Quaternion() // 从这一刻起切到四元数模式,同时画面立刻回到"没旋转过"(默认值就是单位四元数 (0,0,0,1))
box.rotation.y = 0 // 之后这一行毫无效果
注意 rotationQuaternion 这个公开属性在你没赋值时恒为 null;"内部的"四元数不外露,你读不到、也不需要读。
所以真正的坑不是性能,而是静默失效:一旦某处代码创建了四元数(你自己赋值的、rotate() 触发的、decompose() 带出来的),你继续在 rotation 上写的角度就全被吃掉——画面一动不动,控制台一个错都不报。
// ❌ 已经是四元数模式,还去改 rotation:不报错,也不生效
box.rotationQuaternion = new Quaternion()
box.rotation.y += 0.1
// ✅ 选定一种模式并一直用它
box.rotation.y += 0.1 // 欧拉角模式
// 或
const deltaQuat = Quaternion.RotationYawPitchRoll(0.1, 0.1, 0.1)
box.rotationQuaternion!.multiplyInPlace(deltaQuat) // 四元数模式
唯一会把两者"串起来"的公开入口是 rotate() 方法(7.6 节):它会主动创建 rotationQuaternion 并把 rotation 清零,transformNode.pure.d.ts:404 的注释说的就是这个方法:
“Note that the property
rotationQuaternionis then automatically updated and the propertyrotationis set to (0,0,0) and no longer used.”
(另有一个默认关闭的开关 reIntegrateRotationIntoRotationQuaternion,打开后引擎会在每帧把 rotation 折进四元数并清零它。零基础阶段不要碰,本书一律不使用。)
§7.5 lookAt:让物体看向目标
7.5.1 基本用法
lookAt:让物体的正面朝向某个目标点。这是最常用的旋转 API 之一。
先划一条界线:lookAt 属于 TransformNode(网格、变换节点),相机没有这个方法。 v9 的 Camera 上不存在 lookAt,写 camera.lookAt(target) 会直接抛 cam.lookAt is not a function;相机要改注视点用的是同一族里的 setTarget()。
const camera = new ArcRotateCamera('camera-main', -Math.PI / 2, Math.PI / 3, 11.5, Vector3.Zero(), scene)
const target = new Vector3(3, 0, 3)
// 相机看向目标:setTarget,不是 lookAt
camera.setTarget(target)
// 网格/变换节点才用 lookAt
const enemy = MeshBuilder.CreateBox('enemy', { size: 1 }, scene)
const playerPos = new Vector3(5, 0, 0)
enemy.lookAt(playerPos)
源码位置:Meshes/transformNode.pure.d.ts:287
lookAt(
targetPoint: Vector3,
yawCor?: number, // 偏航角修正(Y 轴)
pitchCor?: number, // 俯仰角修正(X 轴)
rollCor?: number, // 翻滚角修正(Z 轴)
space?: Space // 目标点的空间(默认 LOCAL)
): TransformNode;
7.5.2 修正参数
yawCor、pitchCor、rollCor 用于微调朝向。例如,如果你的模型正面不是沿 Z 轴,而是沿 X 轴,你需要修正:
// 模型的"正面"实际上是 +X 方向,需要绕 Y 轴转 90° 修正
enemy.lookAt(playerPos, Math.PI / 2, 0, 0)
7.5.3 空间参数
space 参数决定 targetPoint 是在局部空间还是世界空间:
// 目标点在世界空间(默认)
enemy.lookAt(playerPos, 0, 0, 0, Space.WORLD)
// 目标点在敌人的局部空间(罕见)
enemy.lookAt(localTarget, 0, 0, 0, Space.LOCAL)
7.5.4 底层原理
lookAt 内部做了这些事:
- 计算从物体到目标的向量:
direction = target - position(Space.WORLD时取的是getAbsolutePosition(),Space.LOCAL时取position) - 由方向向量直接反解两个角:
yaw = -atan2(z, x) + π/2、pitch = -atan2(y, √(x² + z²)),不经过矩阵,也不做归一化 - 把
yawCor / pitchCor / rollCor加到三个角上,组成一次 YawPitchRoll 旋转 - 写入当前生效的那种旋转模式:
rotationQuaternion已经存在就就地更新它,否则写rotation
关键就在第 4 步:lookAt 不会把你切到四元数模式。实测(v9.30):
const box = MeshBuilder.CreateBox('m', { size: 1 }, scene)
box.position = new Vector3(4, 0, 0)
box.lookAt(Vector3.Zero())
console.log(box.rotationQuaternion) // null ← 没有凭空创建四元数
console.log(box.rotation) // (0, -π/2, 0) ← 朝向写进了欧拉角
想让 lookAt 产出四元数,必须先把模式定下来:box.rotationQuaternion = new Quaternion(),之后再 lookAt。这一点和第 7.6 节的 rotate() 恰好相反——rotate() 会真的创建 rotationQuaternion 并把 rotation 清零。
§7.6 rotate 和 Rotate(Axis):增量旋转
7.6.1 rotate 方法
rotate:在当前旋转基础上,再绕某个轴旋转一定角度。
const box = MeshBuilder.CreateBox('box', { size: 1 }, scene)
// 绕世界空间的 Y 轴旋转 45°
box.rotate(Vector3.Up(), Math.PI / 4, Space.WORLD)
// 绕局部空间的 X 轴旋转 30°
box.rotate(Vector3.Right(), Math.PI / 6, Space.LOCAL)
源码位置:transformNode.pure.d.ts:401-411
/**
* Rotates the mesh around the axis vector for the passed angle (amount) expressed in radians, in the given space.
* space (default LOCAL) can be either Space.LOCAL, either Space.WORLD.
* Note that the property `rotationQuaternion` is then automatically updated and the property `rotation` is set to (0,0,0) and no longer used.
* The passed axis is also normalized.
*/
rotate(axis: Vector3, amount: number, space?: Space): TransformNode;
关键点:
- 自动归一化轴向量:你传入的
axis不需要预先归一化。 - 切换为四元数模式:调用后
rotation被清零,后续必须用rotationQuaternion。 - 空间选择:
Space.LOCAL绕物体自身的轴旋转,Space.WORLD绕世界坐标系的轴旋转。
7.6.2 rotateAround:绕任意点旋转
rotateAround:绕空间中任意一点旋转,而不只是物体中心。
// 绕点 (5, 0, 0) 的 Y 轴旋转 45°
box.rotateAround(new Vector3(5, 0, 0), Vector3.Up(), Math.PI / 4)
源码位置:transformNode.pure.d.ts:412-422
这在制作"行星绕太阳公转"或"门绕铰链转动"时非常有用。
7.6.3 addRotation:累积欧拉角
如果你坚持用欧拉角,但又想避免手动处理顺序问题,可以用 addRotation:
// 依次绕局部 X、Z、Y 轴旋转
box.addRotation(Math.PI / 6, 0, 0) // 先绕 X
.addRotation(0, 0, Math.PI / 4) // 再绕 Z
.addRotation(0, Math.PI / 3, 0) // 最后绕 Y
源码位置:transformNode.pure.d.ts:433-449
注释说明:
“Under the hood, only quaternions are used. So it’s a little faster if you use .rotationQuaternion because it doesn’t need to translate them back to Euler angles.”
内部其实是用四元数实现的,所以性能略低于直接用四元数。
§7.7 综合实战:旋转调试器
7.7.1 场景设计
- 三个相同的立方体,分别用欧拉角、四元数、
lookAt控制 - GUI 滑块控制旋转角度
- 实时显示当前旋转值
- 演示万向锁现象
7.7.2 核心代码
import { Engine, Scene, ArcRotateCamera, HemisphericLight, Vector3, Quaternion, StandardMaterial, Color3, Space } from '@babylonjs/core'
import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder'
import { AdvancedDynamicTexture, Slider, TextBlock } from '@babylonjs/gui'
export function createRotationDebugger(canvas: HTMLCanvasElement) {
const engine = new Engine(canvas, true)
const scene = new Scene(engine)
// 相机和灯光
const camera = new ArcRotateCamera('camera-main', -Math.PI / 2, Math.PI / 3, 18, Vector3.Zero(), scene)
camera.attachControl()
const light = new HemisphericLight('light-hemi', new Vector3(0, 1, 0), scene)
// 创建三个立方体
const boxEuler = MeshBuilder.CreateBox('mesh-euler', { size: 1 }, scene)
boxEuler.position = new Vector3(-3, 0, 0)
const boxQuat = MeshBuilder.CreateBox('mesh-quat', { size: 1 }, scene)
boxQuat.position = new Vector3(0, 0, 0)
const boxLookAt = MeshBuilder.CreateBox('mesh-look-at', { size: 1 }, scene)
boxLookAt.position = new Vector3(3, 0, 0)
// 创建目标点(用于 lookAt)
const target = MeshBuilder.CreateSphere('mesh-target', { diameter: 0.3 }, scene)
target.position = new Vector3(3, 2, 2)
// mesh.material 的静态类型是 Material,上面没有 diffuseColor(TS2339)
// 所以先落到一个 StandardMaterial 局部变量上,设完属性再挂给网格
const matTarget = new StandardMaterial('mat-target', scene)
matTarget.diffuseColor = new Color3(1, 0, 0)
target.material = matTarget
// GUI 控制
const gui = AdvancedDynamicTexture.CreateFullscreenUI('gui')
// 欧拉角滑块
const sliderX = new Slider('sliderX')
sliderX.minimum = 0
sliderX.maximum = Math.PI * 2
sliderX.value = 0
sliderX.top = '50px'
sliderX.left = '50px'
sliderX.width = '200px'
gui.addControl(sliderX)
const labelX = new TextBlock('labelX', 'Euler X: 0°')
labelX.top = '80px'
labelX.left = '50px'
gui.addControl(labelX)
// 更新逻辑
sliderX.onValueChangedObservable.add((value) => {
// 方式 1:欧拉角
boxEuler.rotation = new Vector3(value, 0, 0)
// 方式 2:四元数(等价效果)
const quat = Quaternion.FromEulerAngles(value, 0, 0)
if (!boxQuat.rotationQuaternion) {
boxQuat.rotationQuaternion = new Quaternion()
}
boxQuat.rotationQuaternion.copyFrom(quat)
// 方式 3:lookAt(让立方体看向移动的目标)
target.position = new Vector3(3, Math.sin(value) * 2, Math.cos(value) * 2)
boxLookAt.lookAt(target.position)
// 更新标签
const degrees = Math.round(value * 180 / Math.PI)
labelX.text = `Euler X: ${degrees}°`
})
// 渲染循环
engine.runRenderLoop(() => {
scene.render()
})
return { engine, scene }
}
7.7.3 观察要点
- 欧拉角 vs 四元数:在 X 轴旋转时,两者效果完全相同。但当你尝试同时旋转多个轴时,欧拉角会出现万向锁,四元数保持平滑。
- lookAt 的动态性:目标点移动时,立方体实时跟随朝向。适合 AI 敌人追踪玩家。
- Inspector 中的差异:选中
boxEuler,Inspector 显示rotation值;选中boxQuat,显示rotationQuaternion值。
§7.8 预期输出与自查
运行本章代码后,你应该看到:
| 现象 | 说明 |
|---|---|
| 三个立方体并排显示 | 分别对应三种旋转方式 |
| 拖动滑块,左侧立方体绕 X 轴旋转 | 欧拉角方式 |
| 中间立方体同步旋转 | 四元数方式,效果相同 |
| 右侧立方体始终面向红色小球 | lookAt 方式 |
| 红色小球随滑块上下移动 | 作为 lookAt 的目标点 |
Inspector 中 boxQuat 的 rotation 为 (0,0,0) | 确认使用了四元数模式 |
自查清单:
- 能解释为什么
rotation和rotationQuaternion不能混用 - 知道万向锁发生的条件(某轴旋转 ±90°)
- 能用
Quaternion.FromEulerAngles()创建四元数 - 知道
lookAt只写入当前生效的旋转模式(默认仍是rotation),要用四元数得自己先建rotationQuaternion - 知道
rotate()方法的space参数影响旋转轴的解释
§7.9 本章的坑
7-01|混用 rotation 和 rotationQuaternion 导致旋转失效。
如果你先设置了 box.rotationQuaternion = quat,然后又写 box.rotation.x = 0.1,后者会被前者覆盖(因为四元数优先级更高)。→ 二选一:要么全程用 rotation,要么全程用 rotationQuaternion。
7-02|角度单位混淆:用了角度而不是弧度。
box.rotation = new Vector3(90, 0, 0) 会让物体转 90 弧度(约 5156°),而不是 90°。→ 始终用弧度:Math.PI / 2 表示 90°。如果需要角度输入,用 angle * Math.PI / 180 转换。
7-03|调用 rotate() 后继续修改 rotation 属性。
rotate() 会自动将 rotation 清零并切换到四元数模式。之后你再写 box.rotation.y += 0.1 不会有任何效果(或者行为不可预测)。→ 调用 rotate() 后,只用 rotationQuaternion。
7-04|lookAt 的目标点和物体在同一位置导致 NaN。
如果 target === position,方向向量是零向量,归一化后变成 NaN。物体会随机翻转。→ 检查距离:if (target.subtract(position).lengthSquared() > 0.001) { mesh.lookAt(target) }。
7-05|四元数未归一化导致物体缩放。
如果你手动构造四元数(如 new Quaternion(1, 2, 3, 4))而没有归一化,物体会被非均匀缩放。→ 始终用 API 创建四元数,或创建后调用 quat.normalize()。
7-06|addRotation 的顺序与直觉相反。
mesh.addRotation(x, 0, 0).addRotation(0, y, 0) 是先绕 X 再绕 Y,但因为是在局部空间,第二次旋转的 Y 轴已经被第一次旋转改变了。→ 用四元数乘法代替:quat1.multiply(quat2) 更可控。
§7.10 章节挑战
挑战 1:实现第三人称相机跟随
玩家位置 playerPos,相机应该在玩家后方 5 单位、上方 2 单位的位置,并始终看向玩家。用 lookAt 实现。
提示:相机位置 = playerPos + new Vector3(0, 2, 5),然后 camera.setTarget(playerPos)。相机没有 lookAt 可用。
挑战 2:避免万向锁的相机控制
实现一个相机,可以用鼠标控制俯仰(pitch)和偏航(yaw),但不能出现万向锁。限制俯仰角度在 -89° 到 +89° 之间。
提示:用四元数累积旋转:camera.rotationQuaternion = Quaternion.RotationYawPitchRoll(yaw, pitch, 0)。俯仰限制:pitch = Math.max(-Math.PI/2 + 0.01, Math.min(Math.PI/2 - 0.01, pitch))。
挑战 3:行星公转动画
创建一个太阳(中心球体)和一个行星(小球),让行星绕太阳公转,同时行星自身也在自转。用 rotateAround 实现公转,用 rotate 实现自转。
提示:
// 每帧
planet.rotateAround(sun.position, Vector3.Up(), deltaTime * 0.5) // 公转
planet.rotate(Vector3.Up(), deltaTime * 2) // 自转
挑战 4:实现" billboard "效果
让一个平面(如血条背景)始终面向相机,无论相机怎么移动。用 lookAt 每帧更新。
提示:在 scene.onBeforeRenderObservable.add(() => { billboard.lookAt(camera.position) }) 中更新。注意性能:如果有很多 billboard,考虑用 GPU instancing(第 11 章)。
§7.11 本章小结
- 欧拉角用三个角度表示旋转,直观但有万向锁缺陷。在 Babylon.js 中是
mesh.rotation属性,单位是弧度,顺序是 YXZ。 - 万向锁发生在某轴旋转 ±90° 时,导致另外两个轴重合,失去一个自由度。不能用欧拉角做连续旋转或插值动画。
- 四元数用四个数字
(x, y, z, w)表示旋转,没有万向锁问题,适合动画和物理模拟。创建方法包括FromEulerAngles、RotationAxis、FromUnitVectorsToRef。 rotation和rotationQuaternion互斥:设置后者后前者被忽略。rotate()会自动切到四元数模式;lookAt不会,它只写入当前已经生效的那一种。lookAt让物体朝向目标点,内部把方向向量反解成 yaw/pitch 后写入当前旋转模式。支持修正参数和空间选择;相机改用setTarget()。rotate方法做增量旋转,支持局部空间和世界空间。调用后rotation被清零。addRotation累积欧拉角旋转,内部用四元数实现,比手动处理顺序安全但性能略低。
§7.12 术语卡
| 中文 | English | 解释 |
|---|---|---|
| 欧拉角 | Euler Angles | 用三个角度(绕 X/Y/Z 轴)表示旋转 |
| 四元数 | Quaternion | 用四个数字表示旋转,避免万向锁 |
| 万向锁 | Gimbal Lock | 欧拉角在某轴旋转 90° 时丢失自由度的现象 |
| 弧度 | Radian | 角度单位,2π 弧度 = 360° |
| 偏航角 | Yaw | 绕 Y 轴的旋转(水平转动) |
| 俯仰角 | Pitch | 绕 X 轴的旋转(上下抬头) |
| 翻滚角 | Roll | 绕 Z 轴的旋转(侧倾) |
| 单位四元数 | Unit Quaternion | 长度为 1 的四元数,表示纯旋转无缩放 |
| 球面插值 | Spherical Interpolation (Slerp) | 在两个四元数之间平滑过渡的方法 |
| 局部空间 | Local Space | 相对于物体自身坐标系的旋转 |
| 世界空间 | World Space | 相对于场景全局坐标系的旋转 |
下一章预告:第 8 章《场景图与变换》将讲解 TransformNode、父子层级、局部/世界矩阵的继承关系。理解了本章的旋转基础后,你将明白为什么父物体旋转时子物体会跟着转,以及如何用 freezeWorldMatrix 优化性能。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)