Add animation controller system, material assets and skeleton visualization

- .animctrl state machine asset (params/transitions/blend trees, 1D+2D
  directional blending) with Unity Animator-style editor panel: state
  list, transitions, params, directional pad preview driving live
  CrossFade on the selected entity
- Split animation evaluation out of SceneRenderer into
  AnimationSystem/AnimationEvaluator; skin palettes with rest-pose
  fallback and blend-weight epsilon filtering
- MaterialAsset (.material) with reload-safe preview tile pool
  (DropMaterial clears dirty refs before instance destruction)
- EditorGizmoPass skeleton wires + viewport joint overlay; camera
  projection rework (baked Vulkan Y-flip, ortho support)
- Editor/game build split via HELLOVULKAN_EDITOR option
This commit is contained in:
2026-10-07 01:50:04 +08:00
parent a941207ae3
commit cc837bc605
71 changed files with 8198 additions and 548 deletions
+129 -1
View File
@@ -169,6 +169,25 @@ namespace Engine::Serialization {
return ref;
}
// 材质覆盖路径解析:guid 反查优先(资产移动不断引用),path
// 兜底;解析成功即预载——"收集期只查缓存"不变量在此保证
//(游戏形态无编辑器,场景读后预载是唯一自动加载点),未加载
// 成功也返回路径(收集期回退内建材质,不丢实体)
std::string ResolveMaterialPath(Assets::AssetManager& assets,
u64 guid, const std::string& path)
{
std::string resolved;
if (guid != 0) resolved = assets.ResolveGUID(guid);
if (resolved.empty()) resolved = path;
if (resolved.empty()) return {};
if (!assets.IsMaterialLoaded(resolved)) {
assets.LoadMaterialAsset(resolved);
}
assets.GetOrCreateGUID(resolved);
return resolved;
}
// ---- 中间结构:解析全部成功才碰 registry ----
// comps 持 Components 节点(仅在键存在且为 map 时赋值——
// yaml-cpp 新版对僵尸节点做 operator= 会抛异常)
@@ -229,6 +248,25 @@ namespace Engine::Serialization {
out << YAML::Key << "Path" << YAML::Value << path;
out << YAML::EndMap; // Model(flow)
out << YAML::Key << "Active" << YAML::Value << mr->active;
// 材质覆盖表(UE Material Slot):seq of {Slot,Guid,Path}
if (!mr->materialOverrides.empty()) {
out << YAML::Key << "MaterialOverrides"
<< YAML::Value << YAML::BeginSeq;
for (const auto& [slot, mpath] :
mr->materialOverrides) {
out << YAML::BeginMap;
out << YAML::Key << "Slot" << YAML::Value << slot;
if (const u64 guid = assets.GetOrCreateGUID(mpath);
guid != 0) {
out << YAML::Key << "Guid"
<< YAML::Value << guid;
}
out << YAML::Key << "Path"
<< YAML::Value << mpath;
out << YAML::EndMap;
}
out << YAML::EndSeq;
}
out << YAML::EndMap; // MeshRenderer
}
else {
@@ -244,7 +282,31 @@ namespace Engine::Serialization {
out << YAML::Key << "Playing" << YAML::Value << a->playing;
out << YAML::Key << "Loop" << YAML::Value << a->loop;
out << YAML::Key << "Speed" << YAML::Value << a->speed;
out << YAML::Key << "FadeDuration" << YAML::Value
<< a->fadeDuration;
out << YAML::Key << "Active" << YAML::Value << a->active;
// Controller 模式(Unity Animator 语义):mode/路径/参数
// 实例值落盘;状态机播放头瞬态不落盘(重启从 entry 开始)
out << YAML::Key << "Mode" << YAML::Value
<< static_cast<int>(a->mode);
out << YAML::Key << "ControllerPath" << YAML::Value
<< a->controllerPath;
if (!a->floatParams.empty()) {
out << YAML::Key << "FloatParams" << YAML::Value
<< YAML::BeginMap;
for (const auto& [k, v] : a->floatParams) {
out << YAML::Key << k << YAML::Value << v;
}
out << YAML::EndMap;
}
if (!a->boolParams.empty()) {
out << YAML::Key << "BoolParams" << YAML::Value
<< YAML::BeginMap;
for (const auto& [k, v] : a->boolParams) {
out << YAML::Key << k << YAML::Value << v;
}
out << YAML::EndMap;
}
// 外部动画源(编辑器 attach 列表):clip 本体运行时并入
// Model 资产不落盘,加载端按此列表幂等重放
out << YAML::Key << "Attached" << YAML::Value << YAML::Flow
@@ -261,6 +323,17 @@ namespace Engine::Serialization {
<< YAML::BeginMap;
out << YAML::Key << "Primary" << YAML::Value << c->primary;
out << YAML::Key << "Active" << YAML::Value << c->active;
// 投影参数(Camera 成员):老场景缺键时读侧回退默认值
out << YAML::Key << "ProjectionMode" << YAML::Value
<< static_cast<int>(c->camera.GetProjectionMode());
out << YAML::Key << "FovDeg" << YAML::Value
<< c->camera.GetFovDeg();
out << YAML::Key << "NearZ" << YAML::Value
<< c->camera.GetNearZ();
out << YAML::Key << "FarZ" << YAML::Value
<< c->camera.GetFarZ();
out << YAML::Key << "OrthoSize" << YAML::Value
<< c->camera.GetOrthoSize();
out << YAML::EndMap;
}
@@ -598,8 +671,25 @@ namespace Engine::Serialization {
ref.Valid()) {
handle.AddComponent<ECS::MeshRendererComponent>(
ECS::MeshRendererComponent{ref});
handle.GetComponent<ECS::MeshRendererComponent>().active =
auto& mrComp =
handle.GetComponent<ECS::MeshRendererComponent>();
mrComp.active =
n["Active"].as<bool>(true); // 缺省 true 向前兼容
// 材质覆盖表:逐项解析(guid 反查 → path 兜底)
// 并预载;加载失败保留路径(收集期回退内建材质)
if (const YAML::Node ovs = n["MaterialOverrides"]) {
for (const auto& ov : ovs) {
const u32 slot = ov["Slot"].as<u32>(
0xFFFFFFFFu);
const auto opath = ResolveMaterialPath(
assets, ov["Guid"].as<u64>(0),
ov["Path"].as<std::string>(""));
if (slot == 0xFFFFFFFFu || opath.empty()) {
continue;
}
mrComp.materialOverrides[slot] = opath;
}
}
}
else {
ENGINE_WARN("SceneSerializer: model unresolved "
@@ -616,7 +706,33 @@ namespace Engine::Serialization {
a.playing = n["Playing"].as<bool>(a.playing);
a.loop = n["Loop"].as<bool>(a.loop);
a.speed = n["Speed"].as<f32>(a.speed);
a.fadeDuration =
n["FadeDuration"].as<f32>(a.fadeDuration); // 缺键回退 0.3
a.active = n["Active"].as<bool>(true);
// Controller 模式缺键回退 Direct(老场景零迁移);
// mode 越界值也视为 Direct
if (const int m = n["Mode"].as<int>(0); m == 1) {
a.mode = ECS::AnimationComponent::AnimMode::Controller;
}
a.controllerPath = n["ControllerPath"].as<std::string>("");
// Controller 预载(收集期只查缓存的不变量在此保证;
// 解析失败保留路径,System Find 落空 → rest 兜底)
if (!a.controllerPath.empty() &&
!assets.IsControllerLoaded(a.controllerPath)) {
assets.LoadController(a.controllerPath);
}
if (const YAML::Node fp = n["FloatParams"]) {
for (const auto& it : fp) {
a.floatParams[it.first.as<std::string>("")] =
it.second.as<float>(0.0f);
}
}
if (const YAML::Node bp = n["BoolParams"]) {
for (const auto& it : bp) {
a.boolParams[it.first.as<std::string>("")] =
it.second.as<bool>(false);
}
}
if (const YAML::Node att = n["Attached"]) {
for (const auto& it : att) {
a.attachedAnimPaths.push_back(
@@ -629,6 +745,18 @@ namespace Engine::Serialization {
auto& c = handle.AddComponent<ECS::CameraComponent>();
c.primary = n["Primary"].as<bool>(false);
c.active = n["Active"].as<bool>(true);
// 投影参数缺省回退默认值(Version 向后兼容:老场景无新键
// 可载)。ProjectionMode 越界值视为透视
if (const int pm = n["ProjectionMode"].as<int>(0);
pm == 1) {
c.camera.SetProjectionMode(
Engine::Camera::ProjectionMode::Orthographic);
}
c.camera.SetFovDeg(n["FovDeg"].as<f32>(c.camera.GetFovDeg()));
c.camera.SetClip(n["NearZ"].as<f32>(c.camera.GetNearZ()),
n["FarZ"].as<f32>(c.camera.GetFarZ()));
c.camera.SetOrthoSize(
n["OrthoSize"].as<f32>(c.camera.GetOrthoSize()));
}
if (const YAML::Node n = comps["Light"]) {