§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 轴不转

关键点:

  1. 单位是弧度(radians),不是角度(degrees)。转换公式:弧度 = 角度 × π / 180。
  2. 顺序很重要:Babylon.js 默认使用 YXZ 顺序(先绕 Y 轴,再绕 X 轴,最后绕 Z 轴)。这意味着 rotation = (π/2, π/2, 0) 不等于 (π/2, 0, π/2)。
  3. 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);
}

关键特性:

  1. 默认值是 (0, 0, 0, 1),表示"无旋转"。注意 w=1,不是 0!
  2. 必须是单位四元数:x² + y² + z² + w² = 1。如果不是,旋转会缩放物体。
  3. 没有直观的几何意义:你不能像欧拉角那样说"绕 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 rotationQuaternion is then automatically updated and the property rotation is 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 内部做了这些事:

  1. 计算从物体到目标的向量:direction = target - position(Space.WORLD 时取的是 getAbsolutePosition(),Space.LOCAL 时取 position)
  2. 由方向向量直接反解两个角:yaw = -atan2(z, x) + π/2、pitch = -atan2(y, √(x² + z²)),不经过矩阵,也不做归一化
  3. 把 yawCor / pitchCor / rollCor 加到三个角上,组成一次 YawPitchRoll 旋转
  4. 写入当前生效的那种旋转模式: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;

关键点:

  1. 自动归一化轴向量:你传入的 axis 不需要预先归一化。
  2. 切换为四元数模式:调用后 rotation 被清零,后续必须用 rotationQuaternion。
  3. 空间选择: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 观察要点

  1. 欧拉角 vs 四元数:在 X 轴旋转时,两者效果完全相同。但当你尝试同时旋转多个轴时,欧拉角会出现万向锁,四元数保持平滑。
  2. lookAt 的动态性:目标点移动时,立方体实时跟随朝向。适合 AI 敌人追踪玩家。
  3. 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 优化性能。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐