feat: parent-child transform hierarchy (UE Actor attach semantics)

- TransformComponent TRS = local (relative to parent); cached world SRT
  composed hierarchically (parent pos/rot/scale, no matrix decomposition
  shear under non-uniform parent scale)
- HierarchyComponent (parent + children, lazily attached); Scene::
  SetParent (keepWorld, cycle-safe) / GetParent / IsDescendantOf /
  SetWorldPose (world->local inverse, used by physics writeback & gizmo)
- UpdateTransforms: DFS from roots, recompose only dirty-or-dirty-
  ancestor subtrees (parents before children)
- DestroyEntity: descendants buried with parent (UE cascade)
- Serialization v2: optional Parent uuid key, two-pass relink on load
- Hierarchy panel: tree view, drag-drop reparent / drop-to-root detach,
  Create Child / Detach context menu; Inspector shows parent name
- Consumers moved to world-space reads: lights, camera pos, physics
  bodies/characters/joints/collider baking, gizmo, F-focus, AlignTo
This commit is contained in:
2026-10-04 15:22:25 +08:00
parent 97408e506c
commit ee5ab8e94e
13 changed files with 550 additions and 135 deletions
+51 -40
View File
@@ -417,28 +417,29 @@ namespace Engine {
// 编辑器拖动 Dynamic 体(暂停期/物理未步进的帧)→ 传送 +
// 清速度(UE 编辑器语义:编辑器操作权威于物理 body,拖动 =
// 重新放置)。物理模拟中 Writeback 每步同步 lastPos/lastRot
// → 本比对只在真正拖动帧为真,不会打断自由模拟
if (rec.lastPos != tr.GetPosition() ||
rec.lastRot != tr.GetRotationQuat()) {
// → 本比对只在真正拖动帧为真,不会打断自由模拟。
// body 是世界系:读 world 缓存(层级实体取父链组合结果)
if (rec.lastPos != tr.GetWorldPosition() ||
rec.lastRot != tr.GetWorldRotationQuat()) {
bi.SetPositionAndRotation(
id,
JPH::RVec3(tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z),
ToJolt(tr.GetRotationQuat()),
JPH::RVec3(tr.GetWorldPosition().x, tr.GetWorldPosition().y, tr.GetWorldPosition().z),
ToJolt(tr.GetWorldRotationQuat()),
JPH::EActivation::Activate);
bi.SetLinearVelocity(id, JPH::Vec3(0.0f, 0.0f, 0.0f));
bi.SetAngularVelocity(id, JPH::Vec3(0.0f, 0.0f, 0.0f));
rec.lastPos = tr.GetPosition();
rec.lastRot = tr.GetRotationQuat();
rec.lastPos = tr.GetWorldPosition();
rec.lastRot = tr.GetWorldRotationQuat();
}
} else if (rec.lastPos != tr.GetPosition() ||
rec.lastRot != tr.GetRotationQuat()) {
} else if (rec.lastPos != tr.GetWorldPosition() ||
rec.lastRot != tr.GetWorldRotationQuat()) {
// 编辑器拖动静态/运动学体 → 传送 body
bi.SetPositionAndRotation(id,
JPH::RVec3(tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z),
ToJolt(tr.GetRotationQuat()),
JPH::RVec3(tr.GetWorldPosition().x, tr.GetWorldPosition().y, tr.GetWorldPosition().z),
ToJolt(tr.GetWorldRotationQuat()),
JPH::EActivation::DontActivate);
rec.lastPos = tr.GetPosition();
rec.lastRot = tr.GetRotationQuat();
rec.lastPos = tr.GetWorldPosition();
rec.lastRot = tr.GetWorldRotationQuat();
}
}
@@ -519,8 +520,8 @@ namespace Engine {
if (tr1 == nullptr || tr2 == nullptr) {
return;
}
const glm::vec3 s1 = tr1->GetScale();
const glm::vec3 s2 = tr2->GetScale();
const glm::vec3 s1 = tr1->GetWorldScale();
const glm::vec3 s2 = tr2->GetWorldScale();
// 双写锁读位姿/COM + 创建(SyncScene 在主线程,与 Update 不并发)
JPH::BodyLockWrite lock1(m_System->GetBodyLockInterface(), b1.id);
@@ -642,22 +643,22 @@ namespace Engine {
if (found == m_Characters.end()) {
CharacterEntry entry;
entry.cc = std::make_unique<CharacterController>(
*m_System, p, tr.GetPosition());
entry.lastPos = tr.GetPosition();
*m_System, p, tr.GetWorldPosition());
entry.lastPos = tr.GetWorldPosition();
m_Characters.emplace(bits, std::move(entry));
continue;
}
if (found->second.cc->GetParams() != p) {
// 参数变更 → 重建(位置沿用当前 Transform)
found->second.cc = std::make_unique<CharacterController>(
*m_System, p, tr.GetPosition());
found->second.lastPos = tr.GetPosition();
*m_System, p, tr.GetWorldPosition());
found->second.lastPos = tr.GetWorldPosition();
continue;
}
if (found->second.lastPos != tr.GetPosition()) {
if (found->second.lastPos != tr.GetWorldPosition()) {
// 编辑器拖动实体 → 传送角色(角色是唯一位置源,反向覆盖)
found->second.cc->Teleport(tr.GetPosition());
found->second.lastPos = tr.GetPosition();
found->second.cc->Teleport(tr.GetWorldPosition());
found->second.lastPos = tr.GetWorldPosition();
}
}
}
@@ -672,8 +673,12 @@ namespace Engine {
continue;
}
entry.cc->Update(dt, gravity, *m_TempAllocator);
const glm::vec3 p = entry.cc->GetPosition(); // 原点 = 脚底
tr->SetPosition(p); // 标脏 → UpdateTransforms 重算 world
const glm::vec3 p = entry.cc->GetPosition(); // 原点 = 脚底(世界系)
// 写回走 world→local 换算(层级实体挂父下仍正确;标脏 →
// UpdateTransforms 重算 world)
m_Scene.SetWorldPose(
static_cast<entt::entity>(bits), p,
tr->GetWorldRotationQuat(), tr->GetWorldScale());
entry.lastPos = p;
}
}
@@ -693,8 +698,9 @@ namespace Engine {
return nullptr;
}
// 缩放烘进顶点:Jolt shape 无缩放语义,body 只吃 pos/rot
const glm::vec3 s = tr.GetScale();
// 缩放烘进顶点:Jolt shape 无缩放语义,body 只吃 pos/rot。
// 取 world 缩放(层级实体 = 父链累计)
const glm::vec3 s = tr.GetWorldScale();
// 动态/运动体 → ConvexHull(MeshShape 三角网格太贵,Jolt 不允许动)
if (rb.type != ECS::RigidBodyComponent::BodyType::Static) {
@@ -833,23 +839,23 @@ namespace Engine {
shape = BuildMeshShape(entityBits, rb, tr);
if (shape == nullptr) {
shape = new JPH::BoxShape(JPH::Vec3(
std::max(rb.boxHalfExtents.x * tr.GetScale().x, 0.01f),
std::max(rb.boxHalfExtents.y * tr.GetScale().y, 0.01f),
std::max(rb.boxHalfExtents.z * tr.GetScale().z, 0.01f)));
std::max(rb.boxHalfExtents.x * tr.GetWorldScale().x, 0.01f),
std::max(rb.boxHalfExtents.y * tr.GetWorldScale().y, 0.01f),
std::max(rb.boxHalfExtents.z * tr.GetWorldScale().z, 0.01f)));
}
} else {
shape = new JPH::BoxShape(JPH::Vec3(
std::max(rb.boxHalfExtents.x * tr.GetScale().x, 0.01f),
std::max(rb.boxHalfExtents.y * tr.GetScale().y, 0.01f),
std::max(rb.boxHalfExtents.z * tr.GetScale().z, 0.01f)));
std::max(rb.boxHalfExtents.x * tr.GetWorldScale().x, 0.01f),
std::max(rb.boxHalfExtents.y * tr.GetWorldScale().y, 0.01f),
std::max(rb.boxHalfExtents.z * tr.GetWorldScale().z, 0.01f)));
}
const bool dynamic = rb.type == ECS::RigidBodyComponent::BodyType::Dynamic;
const bool kinematic = rb.type == ECS::RigidBodyComponent::BodyType::Kinematic;
JPH::BodyCreationSettings settings(
shape,
JPH::RVec3(tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z),
ToJolt(tr.GetRotationQuat()),
JPH::RVec3(tr.GetWorldPosition().x, tr.GetWorldPosition().y, tr.GetWorldPosition().z),
ToJolt(tr.GetWorldRotationQuat()),
dynamic
? JPH::EMotionType::Dynamic
: (kinematic
@@ -882,8 +888,8 @@ namespace Engine {
rec.compound = rb.compound;
rec.mass = rb.mass;
rec.isTrigger = rb.isTrigger;
rec.lastPos = tr.GetPosition();
rec.lastRot = tr.GetRotationQuat();
rec.lastPos = tr.GetWorldPosition();
rec.lastRot = tr.GetWorldRotationQuat();
m_Bodies[entityBits] = rec;
rb.bodyId = id.GetIndexAndSequenceNumber();
@@ -1053,8 +1059,13 @@ namespace Engine {
const JPH::BodyID id(rb.bodyId);
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.SetRotationQuat(ToGlm(rot));
// body 姿态是世界系:写回经 world→local 换算(挂父层级实体
// 不破坏相对位姿;根实体 = 直写)。标脏 → UpdateTransforms 重算
m_Scene.SetWorldPose(
e,
glm::vec3(pos.GetX(), pos.GetY(), pos.GetZ()),
ToGlm(rot),
tr.GetWorldScale());
const JPH::Vec3 vel = bi.GetLinearVelocity(id);
rb.velocity = glm::vec3(vel.GetX(), vel.GetY(), vel.GetZ());
@@ -1063,8 +1074,8 @@ namespace Engine {
// 位移误判为编辑器拖动 → teleport 回旧位置死循环
const u32 bits = static_cast<u32>(entt::to_integral(e));
if (auto it = m_Bodies.find(bits); it != m_Bodies.end()) {
it->second.lastPos = tr.GetPosition();
it->second.lastRot = tr.GetRotationQuat();
it->second.lastPos = tr.GetWorldPosition();
it->second.lastRot = tr.GetWorldRotationQuat();
}
}
}