From e5161439dd9c2940b9058e590482f7e4b6b921c3 Mon Sep 17 00:00:00 2001 From: atdunbg Date: Sat, 3 Oct 2026 00:47:11 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20TransformComponent=20=E6=97=8B?= =?UTF-8?q?=E8=BD=AC=E5=AD=98=E5=82=A8=E6=94=B9=E5=9B=9B=E5=85=83=E6=95=B0?= =?UTF-8?q?=EF=BC=88UE=20FTransform=20=E8=AF=AD=E4=B9=89=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 欧拉角内部存储每帧矩阵↔欧拉往返,是 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 显示数值),热路径不经过。 --- App/editor/ViewportPanel.cpp | 41 +++----------- App/main.cpp | 3 +- Engine/include/Engine/ECS/Components.h | 55 ++++++++++++++++--- Engine/include/Engine/Physics/PhysicsWorld.h | 3 +- Engine/src/Physics/PhysicsWorld.cpp | 57 +++++--------------- Engine/src/Renderer/Camera.cpp | 2 +- 6 files changed, 71 insertions(+), 90 deletions(-) diff --git a/App/editor/ViewportPanel.cpp b/App/editor/ViewportPanel.cpp index e754fce..b5235bc 100644 --- a/App/editor/ViewportPanel.cpp +++ b/App/editor/ViewportPanel.cpp @@ -23,13 +23,11 @@ #include #include -#include -// GTX 扩展(实验性)需显式开启:matrix_decompose(矩阵→SRT 分解)、 -// quaternion(eulerAngles 四元数→欧拉角) +// GTX 扩展(实验性)需显式开启:matrix_decompose(矩阵→SRT 分解, +// 旋转以四元数输出,直接写回 TransformComponent) #define GLM_ENABLE_EXPERIMENTAL #include -#include using namespace Engine; // u32 / f32(Engine/Core/Types.h) @@ -148,43 +146,16 @@ std::optional 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); } } diff --git a/App/main.cpp b/App/main.cpp index 8af314e..3414394 100644 --- a/App/main.cpp +++ b/App/main.cpp @@ -26,7 +26,8 @@ #include #include -#include +#include +#include #include diff --git a/Engine/include/Engine/ECS/Components.h b/Engine/include/Engine/ECS/Components.h index cece77c..3cb3d4e 100644 --- a/Engine/include/Engine/ECS/Components.h +++ b/Engine/include/Engine/ECS/Components.h @@ -15,6 +15,9 @@ #include #include +#include +#include +#include #include 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 diff --git a/Engine/include/Engine/Physics/PhysicsWorld.h b/Engine/include/Engine/Physics/PhysicsWorld.h index c90fa45..8196220 100644 --- a/Engine/include/Engine/Physics/PhysicsWorld.h +++ b/Engine/include/Engine/Physics/PhysicsWorld.h @@ -33,6 +33,7 @@ #include #include +#include #include #include #include @@ -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}; diff --git a/Engine/src/Physics/PhysicsWorld.cpp b/Engine/src/Physics/PhysicsWorld.cpp index 7b57d72..b875696 100644 --- a/Engine/src/Physics/PhysicsWorld.cpp +++ b/Engine/src/Physics/PhysicsWorld.cpp @@ -73,46 +73,15 @@ namespace Engine { } }; - // 引擎 Compose() 的旋转合成是 Ry·Rx·Rz(YXZ 序,见 Components.h)—— - // 物理写回必须按同约定分解欧拉角,否则体素会随写回帧帧漂移。 - // R = Ry(y)·Rx(x)·Rz(z) 的矩阵展开(0 基): - // M12 = -sx → x = asin(-M12);否则 y = atan2(M02, M22),z = atan2(M10, M11) - 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 - }; + // Jolt ↔ glm 四元数直转(TransformComponent 旋转存 glm::quat, + // UE FTransform 语义):物理同步/写回不再经欧拉往返 → 姿态 + // 零漂移(分量序:Jolt ctor (x,y,z,w),glm ctor (w,x,y,z)) + JPH::Quat ToJolt(const glm::quat &q) { + return {q.x, q.y, q.z, q.w}; } - // 与 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}; + glm::quat ToGlm(const JPH::Quat &q) { + return {q.GetW(), q.GetX(), q.GetY(), q.GetZ()}; } } // namespace @@ -390,14 +359,14 @@ namespace Engine { if (rb.type == ECS::RigidBodyComponent::BodyType::Dynamic) { bi.SetGravityFactor(id, rb.gravityScale); } else if (rec.lastPos != tr.GetPosition() || - rec.lastRot != tr.GetRotation()) { + rec.lastRot != tr.GetRotationQuat()) { // 编辑器拖动静态/运动学体 → 传送 body bi.SetPositionAndRotation(id, JPH::RVec3(tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z), - EulerYXZToQuat(tr.GetRotation()), + ToJolt(tr.GetRotationQuat()), JPH::EActivation::DontActivate); rec.lastPos = tr.GetPosition(); - rec.lastRot = tr.GetRotation(); + rec.lastRot = tr.GetRotationQuat(); } } } @@ -509,7 +478,7 @@ namespace Engine { JPH::BodyCreationSettings settings( shape, JPH::RVec3(tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z), - EulerYXZToQuat(tr.GetRotation()), + ToJolt(tr.GetRotationQuat()), dynamic ? JPH::EMotionType::Dynamic : (kinematic @@ -542,7 +511,7 @@ namespace Engine { rec.mass = rb.mass; rec.isSensor = rb.isSensor; rec.lastPos = tr.GetPosition(); - rec.lastRot = tr.GetRotation(); + rec.lastRot = tr.GetRotationQuat(); m_Bodies[entityBits] = rec; rb.bodyId = id.GetIndexAndSequenceNumber(); @@ -625,7 +594,7 @@ namespace Engine { const JPH::RVec3 pos = bi.GetPosition(id); const JPH::Quat rot = bi.GetRotation(id); 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); rb.velocity = glm::vec3(vel.GetX(), vel.GetY(), vel.GetZ()); diff --git a/Engine/src/Renderer/Camera.cpp b/Engine/src/Renderer/Camera.cpp index 093b3cb..a7b35bd 100644 --- a/Engine/src/Renderer/Camera.cpp +++ b/Engine/src/Renderer/Camera.cpp @@ -46,7 +46,7 @@ namespace Engine { // 基向量由实体旋转决定(本地 -Z 为视线方向;应用顺序 Y→X, // 与 TransformComponent::Compose 一致;roll 不参与 FPS 相机) - const glm::vec3& rot = transform.GetRotation(); + const glm::vec3 rot = transform.GetRotation(); // 派生欧拉(度) const glm::mat3 r = static_cast( glm::rotate(glm::mat4(1.0f), glm::radians(rot.y), glm::vec3(0, 1, 0)) *