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
+61 -11
View File
@@ -13,6 +13,8 @@
#include <Engine/Core/AssetRef.h>
#include <Engine/Renderer/Camera.h>
#include <entt/entt.hpp>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
@@ -40,22 +42,29 @@ namespace Engine::ECS {
std::string name;
};
// 世界变换:UE USceneComponent 的 encapsulation 模式——
// SRT 分量私有,所有写入口标脏;world 矩阵由帧循环的
// Scene::UpdateTransforms() 只对脏的实体重算(transform-update
// 阶段),并自动向未来的子层级传播。渲染热路径只读 World()。
// 层级(父子变换)留给后续的层级同步系统。
// 变换(UE FTransform 语义):TRS 分量 = 相对父的 **local** 值
//(无父实体 local 即 world,旧场景零迁移)。分量私有,所有写入口
// 标脏;world 缓存(SRT 三分量 + 矩阵)由帧循环的
// Scene::UpdateTransforms() 从根 DFS 只对脏子树重算(transform-
// update 阶段)。子 world = 父 world SRT 组合而非矩阵乘法——
// 非均匀父缩放下组合式仍是干净 SRT(矩阵分解会引入剪切)。
// 渲染热路径只读 World();灯光/相机/物理/Gizmo 读 world 三分量。
//
// 旋转内部存储 = 四元数(UE FTransform 语义,不存欧拉角):
// 欧拉往返是漂移/万向锁之源(Gizmo 拖拽乱跳的根因)。欧拉只在
// 表皮出现——UI 显示(GetRotation 派生)与 UI 输入(SetRotation
// 转存);Gizmo/物理热路径走 quat 直写(SetRotationQuat)零往返。
struct TransformComponent {
// ---- 读取 ----
// ---- local 读取(相对父;UI/游戏逻辑用)----
const glm::vec3& GetPosition() const { return m_Position; }
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 计算
// ---- world 缓存读取(要求已由 UpdateTransforms 计算)----
const glm::mat4& World() const { return m_World; }
const glm::vec3& GetWorldPosition() const { return m_WorldPosition; }
const glm::quat& GetWorldRotationQuat() const { return m_WorldRotation; }
const glm::vec3& GetWorldScale() const { return m_WorldScale; }
// 欧拉角(度)视图:从 quat 派生(仅 UI 显示/编辑用,勿做存储)。
// 按引擎 YXZ 序(R = Ry·Rx·Rz)从旋转矩阵反解;glm 列主序索引:
@@ -120,25 +129,66 @@ namespace Engine::ECS {
bool IsDirty() const { return m_Dirty; }
// 显式标脏:层级系统改父/挂载后触发 world 重算用
//(local 值未变但 world 缓存已失效)
TransformComponent& MarkDirty() { m_Dirty = true; return *this; }
// SRT 分量 → world 矩阵(T * R * S),清除脏标记。
// 由 Scene::UpdateTransforms 调用(或层级系统),勿手动
// 根实体版(world = local);由 Scene::UpdateTransforms 调用,
// 勿手动
TransformComponent& Compose()
{
const glm::mat4 t = glm::translate(glm::mat4(1.0f), m_Position);
const glm::mat4 s = glm::scale(glm::mat4(1.0f), m_Scale);
m_World = t * glm::mat4_cast(m_Rotation) * s;
m_WorldPosition = m_Position;
m_WorldRotation = m_Rotation;
m_WorldScale = m_Scale;
RebuildWorldMatrix();
m_Dirty = false;
return *this;
}
// 子实体版:world = 父 world SRT 组合(UE FTransform 语义):
// wPos = pPos + pRot·(pScale⊙lPos) wRot = pRot·lRot wScl = pScale⊙lScl
// 组合式(非矩阵分解)保证非均匀父缩放下结果仍是干净 SRT。
// 要求父已 Compose(DFS 父先于子保证)。由 Scene 调用,勿手动
TransformComponent& ComposeFromParent(const TransformComponent& parent)
{
m_WorldPosition = parent.m_WorldPosition +
parent.m_WorldRotation * (parent.m_WorldScale * m_Position);
m_WorldRotation = parent.m_WorldRotation * m_Rotation;
m_WorldScale = parent.m_WorldScale * m_Scale;
RebuildWorldMatrix();
m_Dirty = false;
return *this;
}
private:
void RebuildWorldMatrix()
{
const glm::mat4 t = glm::translate(glm::mat4(1.0f), m_WorldPosition);
const glm::mat4 s = glm::scale(glm::mat4(1.0f), m_WorldScale);
m_World = t * glm::mat4_cast(m_WorldRotation) * s;
}
// ---- local TRS(相对父)----
glm::vec3 m_Position{0.0f};
glm::quat m_Rotation{1.0f, 0.0f, 0.0f, 0.0f}; // 单位四元数(w,x,y,z)
glm::vec3 m_Scale{1.0f};
// ---- world 缓存(UpdateTransforms 重算)----
glm::vec3 m_WorldPosition{0.0f};
glm::quat m_WorldRotation{1.0f, 0.0f, 0.0f, 0.0f};
glm::vec3 m_WorldScale{1.0f};
glm::mat4 m_World{1.0f};
bool m_Dirty = true; // 出生即脏:首次 UpdateTransforms 计算 world
};
// 层级链接(UE Actor attach):父句柄 + 子表。惰性创建——
// 只有参与父子关系的实体才有本组件,独立实体零存储成本。
// entt 句柄仅在 registry 存续期有效;序列化换 UUID,加载后重链
struct HierarchyComponent {
entt::entity parent = entt::null; // entt::null = 根实体
std::vector<entt::entity> children; // 挂载顺序
};
// 网格渲染体:引用共享的 Model 资产。
// 存中性 AssetRef(Core 层纯值类型,Unity InstanceID 思路)——
// ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。
+26 -2
View File
@@ -52,7 +52,8 @@ namespace Engine::ECS {
// 返回句柄表皮:e.AddComponent<T>() / e.GetComponent<T>() / e.Destroy()
EntityHandle CreateEntity(std::string name = "new entity");
void DestroyEntity(Entity e); // 延迟:帧尾统一销毁
// 延迟:帧尾统一销毁。层级级联(UE 语义):销毁父 → 子树随葬
void DestroyEntity(Entity e);
void FlushDestroyQueue(); // 每帧末尾由 Application 调用
// 整场景换血(序列化加载):立即销毁全部实体并清空延迟队列。
@@ -66,10 +67,33 @@ namespace Engine::ECS {
// 帧循环统一重算脏 transform 的 world 矩阵(UE transform-update
// 阶段模式)——由 Application 在 OnUpdate 之后、渲染之前调用,
// 用户代码永远不需要手动 Compose
// 用户代码永远不需要手动 Compose。层级语义:从根 DFS,脏实体
// 或脏祖先的后代才重算(父先于子,ComposeFromParent 组合)
void UpdateTransforms();
bool IsAlive(Entity e) const { return m_Registry.valid(e); }
// ---- 层级(UE Actor attach 语义)----
// 改父/挂根。keepWorld=true(编辑器拖拽语义):换算 local 保持
// world 位姿不变;false(序列化加载语义):local 已就位只建链。
// 防环(新父是 child 后代 → 拒绝);成功返回 true
bool SetParent(Entity child, Entity parent, bool keepWorld = true);
// 父句柄;根/悬空/无效实体返回 entt::null
Entity GetParent(Entity e) const;
// e 是否在 ancestor 的子树内(含相等)。防环检测/面板用
bool IsDescendantOf(Entity e, Entity ancestor) const;
// 世界位姿写入(UE SetActorLocation 语义):world SRT → 沿父链
// 反解 local。物理写回/Gizmo 拖拽用
void SetWorldPose(Entity e, const glm::vec3& worldPos,
const glm::quat& worldRot, const glm::vec3& worldScale);
// 沿父链自顶向下补算脏 world(层级写读 world 前调用——
// UpdateTransforms 之外的时机读取/换算 world 前保证缓存新鲜)
void EnsureWorldFresh(Entity e);
// 按稳定 UUID 查实体(序列化/跨引用场景);找不到返回 entt::null
Entity FindEntityByUUID(UUID uuid);
@@ -51,8 +51,9 @@ namespace Engine {
// 一次性对齐版)。环绕 pivot 已失效,清除待 MMB 重建
void AlignTo(const ECS::TransformComponent& target)
{
m_Transform.SetPosition(target.GetPosition());
m_Transform.SetRotationQuat(target.GetRotationQuat());
// world 位姿复制(目标实体可能在层级子树下)
m_Transform.SetPosition(target.GetWorldPosition());
m_Transform.SetRotationQuat(target.GetWorldRotationQuat());
m_Transform.Compose();
m_Controller.ResetOrbit();
}
@@ -30,8 +30,9 @@ namespace Engine::Assets { class AssetManager; }
namespace Engine::Serialization {
// 当前场景文件格式版本(字段/结构变更时递增)
inline constexpr int kSceneVersion = 1;
// 当前场景文件格式版本(字段/结构变更时递增)。
// 2 = 层级(可选键 Parent: <uuid>,缺省 = 根实体,旧档零迁移)
inline constexpr int kSceneVersion = 2;
class SceneSerializer {
public: