十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity旋转系统全解析:从欧拉角、四元数到万向节锁实战避坑

Unity旋转系统全解析:从欧拉角、四元数到万向节锁实战避坑 1. 项目概述为什么Unity旋转是开发者的“必修课”在Unity开发中旋转Rotation是一个看似基础实则暗藏玄机的核心概念。无论是让角色转头看向敌人让飞船在太空中翻滚还是让UI图标平滑地指向目标旋转操作无处不在。然而许多开发者包括一些有经验的同行都曾在这个“坑”里栽过跟头。你可能会遇到物体莫名其妙地“抽搐”旋转或者当角度达到某个特定值比如90度时整个旋转逻辑突然崩溃——这就是臭名昭著的“万向节锁”Gimbal Lock。更令人困惑的是Unity提供了Transform.Rotate、transform.rotation、transform.localEulerAngles、transform.localRotation等一系列属性与方法它们看起来功能相似但在底层实现和使用场景上却天差地别。选择错误轻则导致动画不平滑重则引发难以调试的物理或逻辑错误。因此深入理解Unity的旋转系统不仅仅是掌握几个API更是构建稳定、可预测的3D交互体验的基石。本文将从一个实战开发者的角度彻底拆解这些旋转相关的核心概念、方法、陷阱以及最佳实践让你不仅能“知其然”更能“知其所以然”从此对旋转操作胸有成竹。2. 核心概念辨析四元数、欧拉角与旋转矩阵在深入具体API之前我们必须先理解支撑Unity旋转系统的三大数学基础欧拉角、四元数和旋转矩阵。这是解开所有旋转谜团的第一把钥匙。2.1 欧拉角人类友好的“角度描述符”欧拉角是我们最直观的理解方式。它用三个绕特定坐标轴通常是X, Y, Z的连续旋转角度来描述一个朝向。在Unity的Inspector面板中你看到的Rotation字段X, Y, Z就是欧拉角表示。优点直观易懂(0, 90, 0)表示绕Y轴旋转90度非常符合人类的思维习惯。易于插值对单个角度进行线性插值Lerp很简单可以实现简单的旋转动画。致命缺点万向节锁Gimbal Lock这是欧拉角最著名的缺陷。当第二个旋转轴通常是Y轴旋转到±90度时第一个旋转轴X和第三个旋转轴Z的旋转平面会重合失去一个自由度。此时你无法单独通过X或Z的旋转来达成某些朝向物体会产生“抽搐”或不受控的旋转。这是欧拉角表示法固有的数学缺陷无法避免。歧义性同一个最终朝向可能对应多组不同的欧拉角值例如(0, 90, 0)和(180, 270, 180)可能表示相同的朝向这在进行旋转比较或差值计算时会带来问题。注意Inspector面板中显示的欧拉角是Unity从物体当前的四元数旋转状态转换而来的一个“人类可读”的表示。你修改它Unity会在底层将其转换为四元数进行存储。这个转换过程可能引入万向节锁问题。2.2 四元数引擎内部的“旋转本质”四元数Quaternion是Unity内部用于表示和计算旋转的真正数据结构。它由一个标量w和一个三维向量x, y, z组成形式上不像角度那样直观。优点无万向节锁这是四元数最大的优势。它能平滑、唯一地表示所有三维旋转没有奇点。计算高效且稳定对于旋转的组合乘法、插值Slerp, Lerp等操作四元数比欧拉角或旋转矩阵更高效数值稳定性更好。适合存储和传输只需要存储4个浮点数比旋转矩阵的9个数更节省空间。缺点不直观你无法直接看出一个四元数(0.707, 0, 0.707, 0)具体代表绕哪个轴转了多少度。需要特定API操作不能直接对x, y, z, w分量进行加减来修改旋转必须使用Quaternion类提供的方法如AngleAxis,LookRotation,Slerp等。核心理解你可以把transform.rotation和transform.localRotation属性理解为物体当前旋转状态的四元数“快照”。你赋值或读取的就是一个Quaternion结构体。2.3 旋转矩阵空间变换的“数学工具”旋转矩阵是一个3x3的矩阵。在图形学中它用于将一个点或向量从一个坐标系变换到另一个坐标系。虽然Unity开发者很少直接操作旋转矩阵但它是理解世界空间World Space和本地空间Local Space变换的基础。与四元数的关系四元数和3x3旋转矩阵可以相互转换。Unity在需要将旋转应用于顶点变换渲染管线时会在底层将四元数转换为旋转矩阵。实操心得作为应用层开发者我们的主战场是理解欧拉角用于直观设置和调试和四元数用于实际逻辑和存储。旋转矩阵更多是底层渲染和物理引擎关心的内容。3. 关键API深度解析Rotate, rotation, localEulerAngles, localRotation理解了底层表示我们再来逐一拆解Unity提供的这几个最常用也最易混淆的API。它们可以分为两大类增量旋转方法和旋转状态属性。3.1 增量旋转方法Transform.RotateTransform.Rotate是一个方法它的作用是基于物体当前的旋转状态叠加一个新的旋转。// 语法示例 void Update() { // 每帧绕世界坐标系的Y轴旋转10度 transform.Rotate(0, 10 * Time.deltaTime, 0, Space.World); // 每帧绕自身坐标系的X轴旋转30度 transform.Rotate(30 * Time.deltaTime, 0, 0, Space.Self); }核心参数解析前三个参数 (float xAngle, float yAngle, float zAngle)通常是以欧拉角形式指定的旋转角度。注意这里的顺序默认是绕Z轴、再绕X轴、最后绕Y轴与Inspector的XYZ顺序不同但Rotate的重载会处理这个映射。更推荐使用Vector3参数的重载。Space space这是关键它决定了旋转所参照的坐标系。Space.Self默认值绕物体自身的坐标系轴旋转。例如一个倾斜的飞机绕其自身的“机头”方向本地Z轴旋转。Space.World绕世界坐标系的轴旋转。无论物体自身如何倾斜旋转轴始终与世界坐标系的X、Y、Z轴对齐。使用场景与避坑指南连续动画Rotate非常适合实现连续的旋转动画如风扇叶片旋转、行星自转/公转。避免在物理更新中使用在FixedUpdate中频繁使用Rotate修改旋转可能会与物理引擎如Rigidbody的插值计算冲突导致物体抖动。对于物理对象应优先考虑对Rigidbody的rotation属性赋值或使用力/扭矩。注意旋转顺序Rotate方法内部会将传入的欧拉角转换为一个四元数增量然后与当前旋转四元数相乘。这个转换过程如果传入的角度值很大同样可能遭遇由欧拉角表示法引入的中间计算问题尽管最终结果由四元数存储是稳定的。3.2 旋转状态属性世界空间transform.rotationtransform.rotation是一个属性它获取或设置物体在世界空间中的最终旋转状态其类型是Quaternion。// 获取当前世界旋转 Quaternion worldRot transform.rotation; // 设置一个特定的世界旋转例如让物体正面朝向世界原点 transform.rotation Quaternion.LookRotation(Vector3.zero - transform.position, Vector3.up);核心特性绝对量它代表的是物体相对于世界坐标系原点的绝对朝向。四元数直接操作四元数无万向节锁问题前提是你用四元数方法创建它。受父物体影响如果一个物体有父物体那么它的rotation是其所有父级旋转叠加后的结果。使用场景需要精确控制物体在世界中的朝向时。例如让一个导弹始终指向世界空间中的某个目标。进行旋转插值Slerp时。Quaternion.Slerp需要在两个四元数间进行rotation属性是完美的输入。复制或保存一个物体的全局朝向时。3.3 旋转状态属性本地空间transform.localRotationtransform.localRotation同样是一个Quaternion类型的属性。它描述的是物体相对于其直接父物体坐标系的旋转。如果物体没有父物体位于层级根部那么localRotation和rotation是相同的。// 假设物体是某个父物体的子级 // 让子物体相对于父物体坐标系绕本地Y轴旋转90度 transform.localRotation Quaternion.Euler(0, 90, 0);核心特性相对量表示与父物体的相对旋转关系。层级关系的关键这是构建复杂层级动画如角色关节、机械臂的基础。你只需要关心当前关节相对于上一级关节的旋转而不需要计算复杂的全局变换。Inspector的映射在Inspector面板中修改Rotation字段Unity底层实际上修改的是localRotation如果物体有父节点。使用场景构建层级化模型动画角色手臂的旋转、车门相对于车身的开合。预制体Prefab的默认朝向预制体保存的旋转信息就是其根节点的localRotation。当你想忽略所有父级旋转只关注本地偏移时。3.4 欧拉角接口transform.localEulerAnglestransform.localEulerAngles是一个Vector3类型的属性。它返回或接受一个欧拉角表示的旋转但这个旋转是相对于父物体的本地旋转。它是localRotation的欧拉角形式“视图”。// 读取本地欧拉角可能不是你在Inspector里输入的原值因为存在转换 Vector3 euler transform.localEulerAngles; Debug.Log($当前本地欧拉角: {euler}); // 设置本地欧拉角可能导致万向节锁 transform.localEulerAngles new Vector3(0, 90, 0);核心特性与巨大陷阱“双向转换器”它是一个便捷的“接口”。当你读取它时Unity将内部存储的localRotation四元数转换为欧拉角。当你写入它时Unity将你提供的欧拉角转换为四元数再赋值给localRotation。万向节锁的导火索这是引发万向节锁问题最常见的操作直接对localEulerAngles的某个分量如eulerAngles.x进行加减或赋值Unity需要执行“四元数 - 欧拉角 - 修改 - 欧拉角 - 四元数”的完整转换链条。当欧拉角值处于或经过万向节锁区域Y轴接近±90度时这个转换就会产生剧烈跳变导致物体旋转异常。值的不确定性读取出来的欧拉角值可能不是你之前设置的那个值。例如设置(0, 450, 0)读出来可能是(180, 90, 180)因为它们代表相同的四元数朝向。Unity会将其规范到一个默认范围通常0-360度。重要警告绝对不要在连续动画或每帧更新中通过修改eulerAngles或localEulerAngles的分量来实现旋转这是导致旋转抖动和万向节锁问题的头号原因。正确的做法是使用四元数运算或Rotate方法。那么eulerAngles世界欧拉角呢同理transform.eulerAngles是rotation的欧拉角表示。它存在同样的转换问题和万向节锁风险且因为它受所有父物体影响行为更难以预测在实际代码中应尽量避免使用。4. 万向节锁现象、原理与彻底规避方案万向节锁不是Unity的Bug而是使用欧拉角表示三维旋转时固有的数学缺陷。但Unity的API设计特别是eulerAngles属性让开发者很容易踩中这个坑。4.1 现象还原你的物体为什么“发疯”了假设你正在制作一个第一人称摄像机控制器。代码逻辑是鼠标X移动控制左右看绕世界Y轴旋转鼠标Y移动控制上下看绕本地X轴旋转。// 一个存在万向节锁风险的典型错误代码 float mouseX Input.GetAxis(Mouse X) * sensitivity; float mouseY Input.GetAxis(Mouse Y) * sensitivity; // 错误做法直接累加欧拉角 Vector3 currentEuler transform.localEulerAngles; currentEuler.y mouseX; // 左右看 currentEuler.x - mouseY; // 上下看 transform.localEulerAngles currentEuler; // 致命行当玩家向上看到极限摄像机本地X轴旋转接近90度即抬头看天时再移动鼠标左右看会发现摄像机不仅左右转还会产生剧烈的、不受控的翻滚绕Z轴旋转。这就是万向节锁的典型表现一个自由度丢失另一个自由度的转动被“锁”进了错误的轴上。4.2 原理深入坐标系重合的灾难从数学上看欧拉角用三个顺序的绕轴旋转来描述朝向比如常见的“Y-X-Z”顺序Unity默认。当第二次旋转绕X轴达到90度时第一次旋转Y轴的旋转平面和第三次旋转Z轴的旋转平面变得完全一致。此时原本应该由Y和Z轴旋转独立控制的两种旋转变成了绕同一个轴的旋转你失去了区分它们的能力。系统在将你的操作意图修改Y角转换回四元数时会产生歧义和突变。4.3 终极规避方案拥抱四元数解决方案的核心是永远在四元数领域进行旋转的组合和插值仅在最终需要显示或设置初始值时才与欧拉角进行一次性转换。方案一使用四元数乘法进行增量旋转替代错误的欧拉角累加float mouseX Input.GetAxis(Mouse X) * sensitivity; float mouseY Input.GetAxis(Mouse Y) * sensitivity; // 1. 创建分别代表绕Y轴和X轴旋转的四元数增量 Quaternion yaw Quaternion.AngleAxis(mouseX, Vector3.up); // 绕世界Y轴旋转 Quaternion pitch Quaternion.AngleAxis(-mouseY, Vector3.right); // 绕本地X轴旋转注意坐标系 // 2. 关键正确组合旋转顺序。对于FPS摄像机通常先Yaw影响全局方向再Pitch本地附加。 // 旋转顺序当前旋转 * 新的偏航 * 新的俯仰 transform.rotation transform.rotation * yaw; // 应用偏航 transform.Rotate(-mouseY, 0, 0, Space.Self); // 使用Rotate应用俯仰Space.Self是关键 // 或者完全用四元数transform.rotation transform.rotation * yaw * pitch; // 但需要注意pitch四元数应该基于物体当前旋转后的本地右轴生成这稍复杂。 // 对于FPS摄像机上述Rotate方法更简洁安全。方案二使用Quaternion.Euler进行安全的一次性设置如果你确实需要从一个欧拉角值来设置旋转比如从数据文件读取使用Quaternion.Euler函数一次性生成四元数然后赋值给rotation或localRotation。// 安全一次性从欧拉角创建四元数 Vector3 eulerFromData new Vector3(30, 45, 0); transform.localRotation Quaternion.Euler(eulerFromData); // 危险分多次设置欧拉角分量模拟每帧修改 // transform.localEulerAngles new Vector3(transform.localEulerAngles.x 1, 45, 0); // 不要这样做方案三对于复杂朝向控制使用Quaternion.LookRotation当你的目标是让物体看向某个点或对准某个方向时Quaternion.LookRotation是最佳工具。它直接根据向前方向和向上方向计算四元数完全绕过欧拉角。// 让物体的正前方Z轴指向targetPosition Vector3 direction (targetPosition - transform.position).normalized; transform.rotation Quaternion.LookRotation(direction, Vector3.up); // 让物体的正前方指向目标同时约束物体的顶部朝向世界Y轴 transform.rotation Quaternion.LookRotation(direction, Vector3.up);实操心得养成条件反射——看到代码里出现对eulerAngles.x/y/z的或直接赋值就要立刻警醒思考能否用Rotate、四元数乘法或LookRotation来替代。对于摄像机旋转、平滑跟随等动态旋转逻辑四元数Slerp或Lerp是平滑过渡的黄金标准。5. 实战应用与最佳实践指南理解了原理和陷阱我们来看几个高频实战场景下的正确操作姿势。5.1 场景一平滑的摄像机跟随与旋转需求让一个第三人称摄像机平滑地跟随玩家并允许鼠标控制环绕观察。public class SmoothCameraController : MonoBehaviour { public Transform target; // 跟随的目标 public float distance 5.0f; public float currentX 0.0f; public float currentY 0.0f; public float sensitivityX 4.0f; public float sensitivityY 1.0f; public float smoothTime 0.3f; private Vector3 currentVelocity Vector3.zero; void Update() { // 1. 获取鼠标输入累积角度这里累积的是欧拉角度值但仅用于计算 currentX Input.GetAxis(Mouse X) * sensitivityX; currentY - Input.GetAxis(Mouse Y) * sensitivityY; currentY Mathf.Clamp(currentY, -80, 80); // 限制俯仰角避免翻转 // 2. 使用四元数根据角度计算朝向安全 Quaternion rotation Quaternion.Euler(currentY, currentX, 0); // 3. 计算期望的摄像机位置 Vector3 negDistance new Vector3(0.0f, 0.0f, -distance); Vector3 targetPosition target.position rotation * negDistance; // 4. 平滑移动到目标位置和旋转 transform.position Vector3.SmoothDamp(transform.position, targetPosition, ref currentVelocity, smoothTime); transform.rotation rotation; // 直接应用计算好的四元数旋转 } }最佳实践分离数据与表现将累积的旋转角度currentX,currentY作为逻辑数据存储。它们只是数字不代表物体的实时旋转状态。一次性转换在Update中使用Quaternion.Euler将累积的角度一次性转换为四元数。这个转换过程是瞬时的只要currentY不恰好被设置在±90度万向节锁点转换就是稳定且唯一的。我们通过Mathf.Clamp将其限制在-80, 80的安全区间完美避开了万向节锁。直接赋值将计算好的四元数直接赋给transform.rotation干净利落。5.2 场景二物体绕轴持续旋转需求让一个风车叶片或行星绕某个轴持续旋转。// 方法A使用Rotate (简单直接) void Update() { // 绕自身Y轴旋转 transform.Rotate(0, 90 * Time.deltaTime, 0, Space.Self); // 或者绕世界Y轴旋转 // transform.Rotate(0, 90 * Time.deltaTime, 0, Space.World); } // 方法B使用四元数乘法 (性能稍优更数学化) public Vector3 axis Vector3.up; // 旋转轴 public float degreesPerSecond 90f; void Update() { Quaternion deltaRot Quaternion.AngleAxis(degreesPerSecond * Time.deltaTime, axis); transform.rotation transform.rotation * deltaRot; // 注意乘法顺序这里是局部旋转 }最佳实践对于这种简单的持续旋转Transform.Rotate方法可读性更高代码更简洁是首选。明确指定Space参数是关键。5.3 场景三从当前旋转平滑过渡到目标旋转需求让一个物体如敌人头部平滑地转向玩家。public Transform player; public float turnSpeed 5f; // 插值速度 void Update() { if (player null) return; // 1. 计算目标朝向从当前物体指向玩家 Vector3 directionToPlayer (player.position - transform.position).normalized; // 2. 使用LookRotation计算目标旋转的四元数 Quaternion targetRotation Quaternion.LookRotation(directionToPlayer, Vector3.up); // 3. 使用四元数球面线性插值(Slerp)进行平滑旋转 // Slerp能保证旋转角速度均匀适合旋转插值。Lerp是线性插值在旋转上可能不平滑。 transform.rotation Quaternion.Slerp(transform.rotation, targetRotation, turnSpeed * Time.deltaTime); // 注意上面的插值方式不是匀速的速度会逐渐变慢。如需恒定角速度需要更复杂的处理。 }最佳实践朝向计算Quaternion.LookRotation是计算“看向某方向”旋转的标准方法比手动计算欧拉角可靠得多。平滑过渡Quaternion.Slerp是旋转插值的标准工具。Time.deltaTime与speed结合可以控制过渡的快慢。记住Slerp的第三个参数t的范围是[0,1]代表插值完成的比例因此通常用speed * Time.deltaTime作为增量使其与帧率无关。5.4 场景四处理层级模型如机械臂的旋转需求控制一个多关节的机械臂每个关节只应相对于其父关节旋转。// 假设层级Base - ArmSegment1 - ArmSegment2 public class RobotArmSegment : MonoBehaviour { // 本地旋转的欧拉角范围限制在Inspector中设置用于直观调试 public Vector3 minLocalEuler new Vector3(-90, -90, 0); public Vector3 maxLocalEuler new Vector3(90, 90, 0); private float currentHingeAngle 0f; // 逻辑上存储的关节角度 public void RotateHinge(float deltaAngle) { // 1. 更新逻辑角度 currentHingeAngle deltaAngle; currentHingeAngle Mathf.Clamp(currentHingeAngle, minLocalEuler.y, maxLocalEuler.y); // 2. 一次性将逻辑角度转换为四元数并赋值给localRotation // 这里假设绕本地Y轴旋转铰链关节 Quaternion newLocalRot Quaternion.Euler(0, currentHingeAngle, 0); transform.localRotation newLocalRot; // 关键操作localRotation // 绝对错误做法transform.localEulerAngles new Vector3(0, deltaAngle, 0); } }最佳实践使用localRotation对于层级中的子物体永远优先考虑localRotation。它直接定义了与父物体的相对关系逻辑清晰。分离数据与表现将旋转角度存储在普通的浮点变量中如currentHingeAngle进行逻辑计算和限制Clamp。一次性转换在需要更新视觉表现时使用Quaternion.Euler将逻辑角度一次性转换为四元数再赋值给localRotation。避免在每帧中反复读取和修改localEulerAngles。6. 常见问题排查与性能优化即使理解了原理在实际开发中仍会遇到各种奇怪的问题。下面是一些常见问题的排查清单和优化建议。6.1 问题排查速查表问题现象可能原因解决方案物体旋转时突然剧烈抖动或翻转1. 在Update中直接修改eulerAngles或localEulerAngles的分量。2. 旋转值进入了万向节锁区域如Pitch接近±90度。1.立即停止修改eulerAngles分量。改用Rotate方法或四元数运算。2. 对于摄像机等将俯仰角限制在安全范围如-85到85度。3. 使用Quaternion.LookRotation或Quaternion.RotateTowards。旋转动画不平滑有卡顿1. 在Update中使用transform.rotation targetRotation直接硬切。2. 使用Quaternion.Lerp/Slerp时第三个参数t的计算方式不对导致帧率相关的不平滑。1. 使用Quaternion.Slerp或Quaternion.RotateTowards进行插值。2. 确保插值因子与Time.deltaTime结合Quaternion.Slerp(currentRot, targetRot, rotateSpeed * Time.deltaTime)。子物体的旋转不符合预期乱转1. 错误地使用了rotation而非localRotation。2. 父物体的旋转影响了子物体的本地轴方向导致Rotate(..., Space.Self)行为异常。1. 检查代码对于层级中的物体确认你是否真的需要世界旋转(rotation)。大部分情况下应使用localRotation。2. 考虑使用Rotate(..., Space.World)来获得确定性的绕世界轴旋转或者使用四元数乘法明确指定旋转轴。Rigidbody物理物体的旋转不受控或抖动在Update中使用transform.Rotate或修改rotation来驱动物理物体与物理引擎的模拟冲突。1. 对于动态Rigidbody应在FixedUpdate中通过rigidbody.MoveRotation(Quaternion)来设置旋转。2. 或者对Rigidbody施加扭矩rigidbody.AddTorque()。3. 如果必须使用transform确保Rigidbody的Collision Detection设置为Continuous或Continuous Dynamic并可能需将Interpolation开启。从Inspector输入的旋转值在代码中读出来不一样欧拉角的多值性。Inspector输入的值被Unity规范化了例如370度会被显示为10度。理解这是欧拉角的特性。如果需要进行精确的角度比较或计算不要依赖eulerAngles的精确值。应该直接比较四元数使用Quaternion.Angle计算夹角或在逻辑层存储你自己的角度变量。6.2 性能优化与编码习惯减少不必要的旋转计算如果旋转不需要每帧都变化将其计算放在Start、OnEnable或由事件触发而不是Update中。缓存Transform引用对于频繁访问的transform在Start或Awake中缓存它。Transform是Unity引擎层面的对象通过C#访问有一定开销。private Transform myTransform; void Awake() { myTransform transform; } void Update() { // 使用 myTransform 而不是 transform myTransform.Rotate(...); }慎用Find和GetComponent避免在Update中通过GameObject.Find或GetComponent来查找其他物体以获取其旋转。应在初始化时完成查找并缓存结果。理解四元数乘法的顺序四元数乘法不满足交换律。q1 * q2表示先进行q2旋转再进行q1旋转。常见的约定是parentRotation * localRotation得到子物体的世界旋转。在组合旋转时务必明确顺序。为旋转操作编写工具函数如果你的项目中有大量类似的旋转逻辑如朝向平滑、角度限制将其封装成静态工具类。这能提高代码复用性、可读性并便于集中优化。7. 总结与核心心法回顾全文Unity的旋转系统可以概括为一句核心心法“用欧拉角思考用四元数计算”。思考与设计阶段大胆使用欧拉角。在Inspector面板中设置初始值在头脑中构思“绕X轴转30度再绕Y轴转45度”这完全没问题因为欧拉角直观。编码与运行时阶段坚决使用四元数。将欧拉角视为一次性的“输入参数”通过Quaternion.Euler、Quaternion.AngleAxis、Quaternion.LookRotation等函数将其转换为四元数。之后所有的旋转叠加、插值、存储都应在四元数的领域内完成。绝对禁区永远不要对transform.eulerAngles或transform.localEulerAngles的.x,.y,.z分量进行连续的、帧级的读写操作。这是万向节锁和各类旋转怪象的根源。最后分享一个我个人调试旋转问题的小技巧当旋转行为异常时不要只盯着代码看。在Scene视图中将坐标轴模式切换为Local然后观察物体的本地坐标轴红X、绿Y、蓝Z是否按你期望的方向旋转。这能帮你快速判断是旋转逻辑错误还是坐标系Space选择错误。记住Space.Self是绕这些本地轴旋转而Space.World是绕Scene视图里永远固定不动的世界轴旋转。分清这两者很多问题就迎刃而解了。
返回列表