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
+6 -35
View File
@@ -23,13 +23,11 @@
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <cmath>
// GTX 扩展(实验性)需显式开启:matrix_decompose(矩阵→SRT 分解)、
// quaternion(eulerAngles 四元数→欧拉角)
// GTX 扩展(实验性)需显式开启:matrix_decompose(矩阵→SRT 分解,
// 旋转以四元数输出,直接写回 TransformComponent)
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/matrix_decompose.hpp>
#include <glm/gtx/quaternion.hpp>
using namespace Engine; // u32 / f32(Engine/Core/Types.h)
@@ -148,43 +146,16 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
gizmo.dragging = ImGuizmo::IsUsing();
// 拖拽生效:分解 world 矩阵写回 SRT(Set* 标脏 →
// UpdateTransforms 重算 → 渲染下帧生效)
// UpdateTransforms 重算 → 渲染下帧生效)。旋转用
// decompose 出的 quat 直写(UE FTransform 语义)——
// 不经欧拉往返,拖拽零漂移、无万向锁
if (ImGuizmo::IsUsing()) {
glm::vec3 scale, transl, skew;
glm::quat rot;
glm::vec4 persp;
if (glm::decompose(m, scale, rot, transl, skew, persp)) {
// 旋转写回:glm::eulerAngles 的序约定(XYZ 系)
// 与 Compose 的 YXZ 不一致,复合旋转下往返失真
// (拖拽乱跳、缩放串轴)。改为按引擎 YXZ 序
//(R = Ry·Rx·Rz)从旋转矩阵直接提取欧拉角,
// 保证 decompose 出的 R 与 Compose 重组的 R
// 严格相等,拖拽零漂移
const glm::mat3 R = glm::mat3_cast(rot);
const f32 sp = -R[2][1]; // = sin(pitch)
glm::vec3 euler;
if (std::abs(sp) < 1.0f) {
// 常规区(float32 下覆盖全部非奇异姿态):
// 由 R = Ry·Rx·Rz 展开反解,cx 在 atan2
// 分子分母中约掉,pitch→±90° 依然精确
euler = glm::vec3(
glm::degrees(std::asin(sp)),
glm::degrees(std::atan2(R[2][0], R[2][2])),
glm::degrees(std::atan2(R[0][1], R[1][1])));
} else {
// float 真奇异(|pitch|=90°):yaw/roll 只
// 剩组合角,约定 roll=0(UE 同策略)归入
// yaw;±90° 两支组合角符号不同
const f32 pitch = sp > 0 ? 90.0f : -90.0f;
const f32 yaw = sp > 0
? glm::degrees(std::atan2(
-R[0][2], R[1][2]))
: glm::degrees(std::atan2(
-R[0][2], -R[1][2]));
euler = glm::vec3(pitch, yaw, 0.0f);
}
tr->SetPosition(transl)
.SetRotation(euler)
.SetRotationQuat(rot)
.SetScale(scale);
}
}