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
+13 -44
View File
@@ -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());
+1 -1
View File
@@ -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::mat3>(
glm::rotate(glm::mat4(1.0f), glm::radians(rot.y),
glm::vec3(0, 1, 0)) *