refactor: TransformComponent 旋转存储改四元数(UE FTransform 语义)

欧拉角内部存储每帧矩阵↔欧拉往返,是 Gizmo 拖拽乱跳/缩放串轴的
结构性根源(74d2f6c 的单点公式修复治标不治本)。对齐 UE/Unity
方案:内部存 glm::quat,欧拉只在 UI 表皮。

- Components.h:m_Rotation 改 quat;Compose 用 mat4_cast;
  SetRotation(euler) 转存(q=qy·qx·qz);新增 SetRotationQuat/
  GetRotationQuat;GetRotation() 改派生欧拉(YXZ 序矩阵反解,
  仅 UI 显示/编辑用)
- ViewportPanel:Gizmo 写回 decompose 的 quat 直写,删除整个
  欧拉提取块
- PhysicsWorld:删 QuatToEulerYXZ/EulerYXZToQuat,改 ToJolt/
  ToGlm 分量直转;物理写回 SetRotationQuat;BodyRecord.lastRot
  改存 quat

验证(1 万随机姿态 round-trip):Gizmo 链路矩阵差 7.9e-07
(旧欧拉方案 3.56e-04,好 3 个数量级);万向锁 pitch=±90° 穿
表皮恒定;欧拉表皮误差 4.5e-04 为欧拉参数化固有病态(随
pitch→±90 指数增长,仅影响 UI 显示数值),热路径不经过。
This commit is contained in:
2026-10-03 00:47:11 +08:00
parent 74d2f6cdaa
commit e5161439dd
6 changed files with 71 additions and 90 deletions
+47 -8
View File
@@ -15,6 +15,9 @@
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <cmath>
#include <algorithm>
#include <string>
namespace Engine {
@@ -41,16 +44,56 @@ namespace Engine::ECS {
// Scene::UpdateTransforms() 只对脏的实体重算(transform-update
// 阶段),并自动向未来的子层级传播。渲染热路径只读 World()。
// 层级(父子变换)留给后续的层级同步系统。
//
// 旋转内部存储 = 四元数(UE FTransform 语义,不存欧拉角):
// 欧拉往返是漂移/万向锁之源(Gizmo 拖拽乱跳的根因)。欧拉只在
// 表皮出现——UI 显示(GetRotation 派生)与 UI 输入(SetRotation
// 转存);Gizmo/物理热路径走 quat 直写(SetRotationQuat)零往返。
struct TransformComponent {
// ---- 读取 ----
const glm::vec3& GetPosition() const { return m_Position; }
const glm::vec3& GetRotation() const { return m_Rotation; } // 欧拉角(度)
const glm::quat& GetRotationQuat() const { return m_Rotation; }
const glm::vec3& GetScale() const { return m_Scale; }
const glm::mat4& World() const { return m_World; } // 要求已由 UpdateTransforms 计算
// 欧拉角(度)视图:从 quat 派生(仅 UI 显示/编辑用,勿做存储)。
// 按引擎 YXZ 序(R = Ry·Rx·Rz)从旋转矩阵反解;glm 列主序索引:
// x = asin(-R[2][1])、y = atan2(R[2][0], R[2][2])、z = atan2(R[0][1], R[1][1])。
// cx 在 atan2 分子分母中约掉,pitch 贴 ±90° 依然精确
glm::vec3 GetRotation() const
{
const glm::mat3 R = glm::mat3_cast(m_Rotation);
const f32 sp = std::clamp(-R[2][1], -1.0f, 1.0f); // = sin(pitch)
if (std::abs(sp) < 1.0f) {
return glm::degrees(glm::vec3(
std::asin(sp),
std::atan2(R[2][0], R[2][2]),
std::atan2(R[0][1], R[1][1])));
}
// ±90° 万向锁:yaw/roll 只剩组合角,约定 roll=0(UE 同策略)
// 归入 yaw;两支组合角符号不同
return sp > 0.0f
? glm::vec3(90.0f,
glm::degrees(std::atan2(-R[0][2], R[1][2])), 0.0f)
: glm::vec3(-90.0f,
glm::degrees(std::atan2(-R[0][2], -R[1][2])), 0.0f);
}
// ---- 写入(标脏,脏实体由 UpdateTransforms 重算)----
TransformComponent& SetPosition(const glm::vec3& p) { if (m_Position != p) { m_Position = p; m_Dirty = true; } return *this; }
TransformComponent& SetRotation(const glm::vec3& r) { if (m_Rotation != r) { m_Rotation = r; m_Dirty = true; } return *this; }
// 欧拉角(度)入口:UI/游戏逻辑用。转 quat 存(q = qy·qx·qz,
// 与原 Ry·Rx·Rz 合成严格同构;不能用 glm::quat(euler) ctor——
// 那是 ZYX 航向序,语义不同)
TransformComponent& SetRotation(const glm::vec3& r)
{
const glm::quat qx = glm::angleAxis(glm::radians(r.x), glm::vec3(1, 0, 0));
const glm::quat qy = glm::angleAxis(glm::radians(r.y), glm::vec3(0, 1, 0));
const glm::quat qz = glm::angleAxis(glm::radians(r.z), glm::vec3(0, 0, 1));
return SetRotationQuat(qy * qx * qz);
}
// 四元数直写(UE FTransform 语义):Gizmo 拖拽/物理写回用,
// 不经欧拉往返 → 零漂移、无万向锁
TransformComponent& SetRotationQuat(const glm::quat& q) { if (m_Rotation != q) { m_Rotation = q; m_Dirty = true; } return *this; }
TransformComponent& SetScale(const glm::vec3& s) { if (m_Scale != s) { m_Scale = s; m_Dirty = true; } return *this; }
bool IsDirty() const { return m_Dirty; }
@@ -60,19 +103,15 @@ namespace Engine::ECS {
TransformComponent& Compose()
{
const glm::mat4 t = glm::translate(glm::mat4(1.0f), m_Position);
const glm::mat4 r =
glm::rotate(glm::mat4(1.0f), glm::radians(m_Rotation.y), glm::vec3(0, 1, 0)) *
glm::rotate(glm::mat4(1.0f), glm::radians(m_Rotation.x), glm::vec3(1, 0, 0)) *
glm::rotate(glm::mat4(1.0f), glm::radians(m_Rotation.z), glm::vec3(0, 0, 1));
const glm::mat4 s = glm::scale(glm::mat4(1.0f), m_Scale);
m_World = t * r * s;
m_World = t * glm::mat4_cast(m_Rotation) * s;
m_Dirty = false;
return *this;
}
private:
glm::vec3 m_Position{0.0f};
glm::vec3 m_Rotation{0.0f};
glm::quat m_Rotation{1.0f, 0.0f, 0.0f, 0.0f}; // 单位四元数(w,x,y,z)
glm::vec3 m_Scale{1.0f};
glm::mat4 m_World{1.0f};
bool m_Dirty = true; // 出生即脏:首次 UpdateTransforms 计算 world
+2 -1
View File
@@ -33,6 +33,7 @@
#include <Jolt/Core/TempAllocator.h>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <mutex>
#include <unordered_map>
#include <vector>
@@ -85,7 +86,7 @@ namespace Engine {
f32 mass = 0.0f;
bool isSensor = false; // 传感器标记(变更 → 重建 body)
glm::vec3 lastPos{0.0f}; // 静态/运动体编辑器拖动检测
glm::vec3 lastRot{0.0f};
glm::quat lastRot{1.0f, 0.0f, 0.0f, 0.0f}; // 同上(quat 直比,无欧拉往返)
// 加速度箭头用:上一步速度(定步长差分)与平滑后的加速度
glm::vec3 prevVel{0.0f};
glm::vec3 lastAccel{0.0f};