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/glm.hpp>
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
#include <cmath>
// GTX 扩展(实验性)需显式开启:matrix_decompose(矩阵→SRT 分解)、 // GTX 扩展(实验性)需显式开启:matrix_decompose(矩阵→SRT 分解,
// quaternion(eulerAngles 四元数→欧拉角) // 旋转以四元数输出,直接写回 TransformComponent)
#define GLM_ENABLE_EXPERIMENTAL #define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/matrix_decompose.hpp> #include <glm/gtx/matrix_decompose.hpp>
#include <glm/gtx/quaternion.hpp>
using namespace Engine; // u32 / f32(Engine/Core/Types.h) 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(); gizmo.dragging = ImGuizmo::IsUsing();
// 拖拽生效:分解 world 矩阵写回 SRT(Set* 标脏 → // 拖拽生效:分解 world 矩阵写回 SRT(Set* 标脏 →
// UpdateTransforms 重算 → 渲染下帧生效) // UpdateTransforms 重算 → 渲染下帧生效)。旋转用
// decompose 出的 quat 直写(UE FTransform 语义)——
// 不经欧拉往返,拖拽零漂移、无万向锁
if (ImGuizmo::IsUsing()) { if (ImGuizmo::IsUsing()) {
glm::vec3 scale, transl, skew; glm::vec3 scale, transl, skew;
glm::quat rot; glm::quat rot;
glm::vec4 persp; glm::vec4 persp;
if (glm::decompose(m, scale, rot, transl, skew, 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) tr->SetPosition(transl)
.SetRotation(euler) .SetRotationQuat(rot)
.SetScale(scale); .SetScale(scale);
} }
} }
+1
View File
@@ -27,6 +27,7 @@
#include <Engine/Renderer/Model.h> #include <Engine/Renderer/Model.h>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <GLFW/glfw3.h> #include <GLFW/glfw3.h>
+47 -8
View File
@@ -15,6 +15,9 @@
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <cmath>
#include <algorithm>
#include <string> #include <string>
namespace Engine { namespace Engine {
@@ -41,16 +44,56 @@ namespace Engine::ECS {
// Scene::UpdateTransforms() 只对脏的实体重算(transform-update // Scene::UpdateTransforms() 只对脏的实体重算(transform-update
// 阶段),并自动向未来的子层级传播。渲染热路径只读 World()。 // 阶段),并自动向未来的子层级传播。渲染热路径只读 World()。
// 层级(父子变换)留给后续的层级同步系统。 // 层级(父子变换)留给后续的层级同步系统。
//
// 旋转内部存储 = 四元数(UE FTransform 语义,不存欧拉角):
// 欧拉往返是漂移/万向锁之源(Gizmo 拖拽乱跳的根因)。欧拉只在
// 表皮出现——UI 显示(GetRotation 派生)与 UI 输入(SetRotation
// 转存);Gizmo/物理热路径走 quat 直写(SetRotationQuat)零往返。
struct TransformComponent { struct TransformComponent {
// ---- 读取 ---- // ---- 读取 ----
const glm::vec3& GetPosition() const { return m_Position; } 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::vec3& GetScale() const { return m_Scale; }
const glm::mat4& World() const { return m_World; } // 要求已由 UpdateTransforms 计算 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 重算)---- // ---- 写入(标脏,脏实体由 UpdateTransforms 重算)----
TransformComponent& SetPosition(const glm::vec3& p) { if (m_Position != p) { m_Position = p; m_Dirty = true; } return *this; } 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; } TransformComponent& SetScale(const glm::vec3& s) { if (m_Scale != s) { m_Scale = s; m_Dirty = true; } return *this; }
bool IsDirty() const { return m_Dirty; } bool IsDirty() const { return m_Dirty; }
@@ -60,19 +103,15 @@ namespace Engine::ECS {
TransformComponent& Compose() TransformComponent& Compose()
{ {
const glm::mat4 t = glm::translate(glm::mat4(1.0f), m_Position); 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); 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; m_Dirty = false;
return *this; return *this;
} }
private: private:
glm::vec3 m_Position{0.0f}; 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::vec3 m_Scale{1.0f};
glm::mat4 m_World{1.0f}; glm::mat4 m_World{1.0f};
bool m_Dirty = true; // 出生即脏:首次 UpdateTransforms 计算 world bool m_Dirty = true; // 出生即脏:首次 UpdateTransforms 计算 world
+2 -1
View File
@@ -33,6 +33,7 @@
#include <Jolt/Core/TempAllocator.h> #include <Jolt/Core/TempAllocator.h>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <mutex> #include <mutex>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
@@ -85,7 +86,7 @@ namespace Engine {
f32 mass = 0.0f; f32 mass = 0.0f;
bool isSensor = false; // 传感器标记(变更 → 重建 body) bool isSensor = false; // 传感器标记(变更 → 重建 body)
glm::vec3 lastPos{0.0f}; // 静态/运动体编辑器拖动检测 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 prevVel{0.0f};
glm::vec3 lastAccel{0.0f}; glm::vec3 lastAccel{0.0f};
+14 -45
View File
@@ -73,47 +73,16 @@ namespace Engine {
} }
}; };
// 引擎 Compose() 的旋转合成是 Ry·Rx·Rz(YXZ 序,见 Components.h)—— // Jolt ↔ glm 四元数直转(TransformComponent 旋转存 glm::quat,
// 物理写回必须按同约定分解欧拉角,否则体素会随写回帧帧漂移。 // UE FTransform 语义):物理同步/写回不再经欧拉往返 → 姿态
// R = Ry(y)·Rx(x)·Rz(z) 的矩阵展开(0 基): // 零漂移(分量序:Jolt ctor (x,y,z,w),glm ctor (w,x,y,z))
// M12 = -sx → x = asin(-M12);否则 y = atan2(M02, M22),z = atan2(M10, M11) JPH::Quat ToJolt(const glm::quat &q) {
glm::vec3 QuatToEulerYXZ(const JPH::Quat &q) {
const glm::quat gq(q.GetW(), q.GetX(), q.GetY(), q.GetZ());
const glm::mat3 m = glm::mat3(gq); // glm 列主序:m[col][row]
// 数学记法 M[row][col]:M12 = m[2][1],M02 = m[2][0],M10 = m[0][1] …
const f32 m12 = m[2][1];
const f32 m02 = m[2][0];
const f32 m22 = m[2][2];
const f32 m10 = m[0][1];
const f32 m11 = m[1][1];
const f32 m01 = m[1][0];
const f32 sinX = std::clamp(-m12, -1.0f, 1.0f);
const f32 x = std::asin(sinX);
if (std::abs(sinX) < 0.9999f) {
return {
glm::degrees(x),
glm::degrees(std::atan2(m02, m22)),
glm::degrees(std::atan2(m10, m11))
};
}
// 万向锁(x=±90°):z 并入 y,取 z=0,y = atan2(M01, M02)
return {
glm::degrees(x),
glm::degrees(std::atan2(m01, m02)),
0.0f
};
}
// 与 QuatToEulerYXZ 互逆:q = qy(y)·qx(x)·qz(z) ≡ Ry·Rx·Rz
//(不能用 glm::quat(euler) ctor——那是 ZYX 航向序,语义不同)
JPH::Quat EulerYXZToQuat(const glm::vec3 &eulerDeg) {
const glm::quat qx = glm::angleAxis(glm::radians(eulerDeg.x), glm::vec3(1, 0, 0));
const glm::quat qy = glm::angleAxis(glm::radians(eulerDeg.y), glm::vec3(0, 1, 0));
const glm::quat qz = glm::angleAxis(glm::radians(eulerDeg.z), glm::vec3(0, 0, 1));
const glm::quat q = qy * qx * qz;
return {q.x, q.y, q.z, q.w}; return {q.x, q.y, q.z, q.w};
} }
glm::quat ToGlm(const JPH::Quat &q) {
return {q.GetW(), q.GetX(), q.GetY(), q.GetZ()};
}
} // namespace } // namespace
// ---- 碰撞事件监听:回调在 Update 内多线程并发,只读 body、加锁入队 ---- // ---- 碰撞事件监听:回调在 Update 内多线程并发,只读 body、加锁入队 ----
@@ -390,14 +359,14 @@ namespace Engine {
if (rb.type == ECS::RigidBodyComponent::BodyType::Dynamic) { if (rb.type == ECS::RigidBodyComponent::BodyType::Dynamic) {
bi.SetGravityFactor(id, rb.gravityScale); bi.SetGravityFactor(id, rb.gravityScale);
} else if (rec.lastPos != tr.GetPosition() || } else if (rec.lastPos != tr.GetPosition() ||
rec.lastRot != tr.GetRotation()) { rec.lastRot != tr.GetRotationQuat()) {
// 编辑器拖动静态/运动学体 → 传送 body // 编辑器拖动静态/运动学体 → 传送 body
bi.SetPositionAndRotation(id, bi.SetPositionAndRotation(id,
JPH::RVec3(tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z), JPH::RVec3(tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z),
EulerYXZToQuat(tr.GetRotation()), ToJolt(tr.GetRotationQuat()),
JPH::EActivation::DontActivate); JPH::EActivation::DontActivate);
rec.lastPos = tr.GetPosition(); rec.lastPos = tr.GetPosition();
rec.lastRot = tr.GetRotation(); rec.lastRot = tr.GetRotationQuat();
} }
} }
} }
@@ -509,7 +478,7 @@ namespace Engine {
JPH::BodyCreationSettings settings( JPH::BodyCreationSettings settings(
shape, shape,
JPH::RVec3(tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z), JPH::RVec3(tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z),
EulerYXZToQuat(tr.GetRotation()), ToJolt(tr.GetRotationQuat()),
dynamic dynamic
? JPH::EMotionType::Dynamic ? JPH::EMotionType::Dynamic
: (kinematic : (kinematic
@@ -542,7 +511,7 @@ namespace Engine {
rec.mass = rb.mass; rec.mass = rb.mass;
rec.isSensor = rb.isSensor; rec.isSensor = rb.isSensor;
rec.lastPos = tr.GetPosition(); rec.lastPos = tr.GetPosition();
rec.lastRot = tr.GetRotation(); rec.lastRot = tr.GetRotationQuat();
m_Bodies[entityBits] = rec; m_Bodies[entityBits] = rec;
rb.bodyId = id.GetIndexAndSequenceNumber(); rb.bodyId = id.GetIndexAndSequenceNumber();
@@ -625,7 +594,7 @@ namespace Engine {
const JPH::RVec3 pos = bi.GetPosition(id); const JPH::RVec3 pos = bi.GetPosition(id);
const JPH::Quat rot = bi.GetRotation(id); const JPH::Quat rot = bi.GetRotation(id);
tr.SetPosition(glm::vec3(pos.GetX(), pos.GetY(), pos.GetZ())); tr.SetPosition(glm::vec3(pos.GetX(), pos.GetY(), pos.GetZ()));
tr.SetRotation(QuatToEulerYXZ(rot)); tr.SetRotationQuat(ToGlm(rot));
const JPH::Vec3 vel = bi.GetLinearVelocity(id); const JPH::Vec3 vel = bi.GetLinearVelocity(id);
rb.velocity = glm::vec3(vel.GetX(), vel.GetY(), vel.GetZ()); rb.velocity = glm::vec3(vel.GetX(), vel.GetY(), vel.GetZ());
+1 -1
View File
@@ -46,7 +46,7 @@ namespace Engine {
// 基向量由实体旋转决定(本地 -Z 为视线方向;应用顺序 Y→X, // 基向量由实体旋转决定(本地 -Z 为视线方向;应用顺序 Y→X,
// 与 TransformComponent::Compose 一致;roll 不参与 FPS 相机) // 与 TransformComponent::Compose 一致;roll 不参与 FPS 相机)
const glm::vec3& rot = transform.GetRotation(); const glm::vec3 rot = transform.GetRotation(); // 派生欧拉(度)
const glm::mat3 r = static_cast<glm::mat3>( const glm::mat3 r = static_cast<glm::mat3>(
glm::rotate(glm::mat4(1.0f), glm::radians(rot.y), glm::rotate(glm::mat4(1.0f), glm::radians(rot.y),
glm::vec3(0, 1, 0)) * glm::vec3(0, 1, 0)) *