动画优化

This commit is contained in:
Rowland 2026-07-27 09:50:32 +08:00
parent 2512e3ade6
commit 3ba7b090d1
64 changed files with 732808 additions and 126 deletions

View File

@ -1,5 +1,6 @@
#include "MetaCorePlatform/MetaCoreWindow.h"
#include "MetaCorePhysics/MetaCorePhysics.h"
#include "MetaCoreAnimation/MetaCoreAnimation.h"
#include "MetaCoreDelivery/MetaCoreBuildPipeline.h"
#include "MetaCoreDelivery/MetaCoreCrashReporter.h"
#include "MetaCoreFoundation/MetaCoreGeneratedReflection.h"
@ -517,14 +518,26 @@ int main(int argc, char* argv[]) {
MetaCore::MetaCorePhysicsWorld physicsWorld(scene, physicsSettings,
[&physicsMeshes](MetaCore::MetaCoreAssetGuid guid)->std::optional<MetaCore::MetaCorePhysicsMeshData>{const auto it=physicsMeshes.find(guid);return it==physicsMeshes.end()?std::nullopt:std::optional<MetaCore::MetaCorePhysicsMeshData>(it->second);},
[&physicsMaterials](MetaCore::MetaCoreAssetGuid guid)->std::optional<MetaCore::MetaCorePhysicsMaterialDocument>{const auto it=physicsMaterials.find(guid);return it==physicsMaterials.end()?std::nullopt:std::optional<MetaCore::MetaCorePhysicsMaterialDocument>(it->second);});
MetaCore::MetaCoreAnimationAssetLibrary animationAssets(projectRoot / "Assets");
MetaCore::MetaCoreAnimationRuntime animationRuntime(scene,
[&animationAssets](MetaCore::MetaCoreAssetGuid guid) { return animationAssets.LoadClip(guid); },
[&animationAssets](MetaCore::MetaCoreAssetGuid guid) { return animationAssets.LoadController(guid); },
[&animationAssets](MetaCore::MetaCoreAssetGuid guid) { return animationAssets.LoadAvatar(guid); },
[&animationAssets](MetaCore::MetaCoreAssetGuid guid) { return animationAssets.LoadTimeline(guid); }, &physicsWorld);
MetaCore::MetaCoreScriptRuntime scriptRuntime(
scene, scriptRegistry,
[&output, &errors](std::uint32_t level, std::string_view category, std::string_view message) {
std::ostream& stream = level >= 2U ? errors : output;
stream << "MetaCorePlayer Script[" << category << "]: " << message << '\n';
}, &runtimeInput, &physicsWorld
}, &runtimeInput, &physicsWorld, &animationRuntime
);
scriptRuntime.Start();
animationRuntime.SetEventCallback([&scriptRuntime](const MetaCore::MetaCoreAnimationRuntimeEvent& event) {
MetaCoreScriptValueV1 name{}; name.Type = MetaCoreScriptAbiValue_String;
std::snprintf(name.Text, sizeof(name.Text), "%s", event.Name.c_str());
scriptRuntime.PublishToObject(event.ObjectId, "MetaCore.Animation." + event.Type, {name});
});
animationRuntime.Start();
physicsWorld.SetEventCallback([&scriptRuntime](const MetaCore::MetaCorePhysicsContactEvent& event) {
const auto topic = [&]() -> const char* {
using E = MetaCore::MetaCorePhysicsEventType;
@ -663,6 +676,7 @@ int main(int argc, char* argv[]) {
fixedAccumulatorSeconds += std::clamp(frameDeltaSeconds, 0.0, physicsSettings.MaxFrameDelta);
std::uint32_t fixedSteps = 0;
while (fixedAccumulatorSeconds >= physicsSettings.FixedTimeStep && fixedSteps < physicsSettings.MaxFixedStepsPerFrame) {
animationRuntime.FixedUpdate(physicsSettings.FixedTimeStep);
scriptRuntime.FixedUpdate(physicsSettings.FixedTimeStep);
physicsWorld.Step(physicsSettings.FixedTimeStep);
fixedAccumulatorSeconds -= physicsSettings.FixedTimeStep;
@ -670,6 +684,7 @@ int main(int argc, char* argv[]) {
}
if (fixedSteps == physicsSettings.MaxFixedStepsPerFrame && fixedAccumulatorSeconds >= physicsSettings.FixedTimeStep)
fixedAccumulatorSeconds = 0.0;
animationRuntime.Update(frameDeltaSeconds);
scriptRuntime.Update(frameDeltaSeconds);
const auto [windowWidth, windowHeight] = window.GetWindowSize();
viewportRenderer.SetViewportRect(MetaCore::MetaCoreViewportRect{
@ -738,6 +753,7 @@ int main(int argc, char* argv[]) {
}
physicsWorld.Stop();
animationRuntime.Stop();
scriptRuntime.Stop();
runtimeInput.Unsubscribe(inputSubscription);
(void)runtimeInput.SaveOverrides(inputOverridePath);

View File

@ -618,6 +618,14 @@ target_include_directories(MetaCorePhysics PUBLIC Source/MetaCorePhysics/Public
target_link_libraries(MetaCorePhysics PUBLIC MetaCoreFoundation MetaCoreScene glm::glm PRIVATE ${BULLET_LIBRARIES})
target_compile_options(MetaCorePhysics PRIVATE ${METACORE_COMMON_WARNINGS})
add_library(MetaCoreAnimation STATIC
Source/MetaCoreAnimation/Public/MetaCoreAnimation/MetaCoreAnimation.h
Source/MetaCoreAnimation/Private/MetaCoreAnimation.cpp
)
target_include_directories(MetaCoreAnimation PUBLIC Source/MetaCoreAnimation/Public PRIVATE third_party)
target_link_libraries(MetaCoreAnimation PUBLIC MetaCoreFoundation MetaCoreScene MetaCorePhysics glm::glm)
target_compile_options(MetaCoreAnimation PRIVATE ${METACORE_COMMON_WARNINGS})
add_library(MetaCoreScripting STATIC
Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScriptAbi.h
Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScripting.h
@ -627,7 +635,7 @@ target_include_directories(MetaCoreScripting
PUBLIC Source/MetaCoreScripting/Public
PRIVATE third_party
)
target_link_libraries(MetaCoreScripting PUBLIC MetaCoreFoundation MetaCoreScene MetaCoreRuntimeInput MetaCorePhysics glm::glm)
target_link_libraries(MetaCoreScripting PUBLIC MetaCoreFoundation MetaCoreScene MetaCoreRuntimeInput MetaCorePhysics MetaCoreAnimation glm::glm)
if(UNIX AND NOT APPLE)
target_link_libraries(MetaCoreScripting PRIVATE dl)
endif()
@ -644,7 +652,7 @@ target_include_directories(MetaCoreDelivery
PRIVATE third_party
)
target_link_libraries(MetaCoreDelivery
PUBLIC MetaCoreFoundation MetaCoreRendering MetaCoreScene MetaCoreScripting MetaCoreRuntimeInput MetaCoreRuntimeData
PUBLIC MetaCoreFoundation MetaCoreRendering MetaCoreScene MetaCoreScripting MetaCoreAnimation MetaCoreRuntimeInput MetaCoreRuntimeData
)
target_compile_options(MetaCoreDelivery PRIVATE ${METACORE_COMMON_WARNINGS})
if(crashpad_FOUND)
@ -699,6 +707,7 @@ target_link_libraries(MetaCoreRender
MetaCoreRendering
MetaCorePlatform
MetaCoreScene
MetaCoreAnimation
glm::glm
imgui::imgui
)
@ -1004,6 +1013,17 @@ if(METACORE_BUILD_TESTS)
target_compile_options(MetaCorePhysicsTests PRIVATE ${METACORE_COMMON_WARNINGS})
add_test(NAME MetaCorePhysicsTests COMMAND MetaCorePhysicsTests)
add_executable(MetaCoreAnimationTests tests/MetaCoreAnimationTests.cpp)
target_link_libraries(MetaCoreAnimationTests PRIVATE MetaCoreAnimation)
target_compile_options(MetaCoreAnimationTests PRIVATE ${METACORE_COMMON_WARNINGS})
add_test(NAME MetaCoreAnimationTests COMMAND MetaCoreAnimationTests)
add_executable(MetaCoreAnimationBenchmark tests/MetaCoreAnimationBenchmark.cpp)
target_link_libraries(MetaCoreAnimationBenchmark PRIVATE MetaCoreAnimation)
target_compile_options(MetaCoreAnimationBenchmark PRIVATE ${METACORE_COMMON_WARNINGS})
add_test(NAME MetaCoreAnimationBenchmark COMMAND MetaCoreAnimationBenchmark)
set_tests_properties(MetaCoreAnimationBenchmark PROPERTIES LABELS "benchmark")
add_executable(MetaCorePhysicsBenchmark tests/MetaCorePhysicsBenchmark.cpp)
target_link_libraries(MetaCorePhysicsBenchmark PRIVATE MetaCorePhysics)
target_compile_options(MetaCorePhysicsBenchmark PRIVATE ${METACORE_COMMON_WARNINGS})

View File

@ -0,0 +1,35 @@
{
"guid": "42ccd313-d6a6-3318-ac6c-573b81167e42",
"layers": [
{
"additive_reference": "00000000-0000-0000-0000-000000000000",
"additive_reference_time": 0.0,
"default": "Default",
"id": "Base",
"mask": [],
"mode": 0,
"name": "Base Layer",
"states": [
{
"id": "Default",
"loop": true,
"motion": {
"clip": "72333b36-0de9-ede4-494b-ab15fea15ad1",
"samples": [],
"type": 0,
"x": "",
"y": ""
},
"name": "Armature|mixamo.com|Layer0",
"speed": 1.0
}
],
"transitions": [],
"weight": 1.0
}
],
"name": "Default Armature|mixamo.com|Layer0",
"parameters": [],
"type": "MetaCoreAnimatorController",
"version": 1
}

View File

@ -0,0 +1,35 @@
{
"guid": "5897ce7b-4d75-322b-6c8e-f87a8274f335",
"layers": [
{
"additive_reference": "00000000-0000-0000-0000-000000000000",
"additive_reference_time": 0.0,
"default": "Default",
"id": "Base",
"mask": [],
"mode": 0,
"name": "Base Layer",
"states": [
{
"id": "Default",
"loop": true,
"motion": {
"clip": "24e509ba-3a8b-1a3b-0c74-39c2e57770ad",
"samples": [],
"type": 0,
"x": "",
"y": ""
},
"name": "mixamo.com",
"speed": 1.0
}
],
"transitions": [],
"weight": 1.0
}
],
"name": "Default mixamo.com",
"parameters": [],
"type": "MetaCoreAnimatorController",
"version": 1
}

View File

@ -0,0 +1,383 @@
#include "MetaCoreAnimation/MetaCoreAnimation.h"
#include "MetaCorePhysics/MetaCorePhysics.h"
#include "MetaCoreFoundation/MetaCoreHash.h"
#include "MetaCoreScene/MetaCoreComponents.h"
#include "MetaCoreScene/MetaCoreScene.h"
#include <nlohmann/json.hpp>
#include <glm/common.hpp>
#include <glm/gtc/quaternion.hpp>
#include <algorithm>
#include <cmath>
#include <fstream>
#include <limits>
#include <set>
#include <unordered_set>
namespace MetaCore {
namespace {
using Json = nlohmann::json;
float Clamp01(float value) { return std::clamp(std::isfinite(value) ? value : 0.0F, 0.0F, 1.0F); }
float SafeSpeed(float value) { return std::isfinite(value) ? std::clamp(value, 0.0F, 100.0F) : 0.0F; }
glm::quat SafeNormalize(glm::quat value) {
const float length = glm::length(value);
return length > 0.000001F && std::isfinite(length) ? glm::normalize(value) : glm::quat(1, 0, 0, 0);
}
MetaCoreAnimationTransform BlendTransform(const MetaCoreAnimationTransform& a, const MetaCoreAnimationTransform& b, float weight) {
weight = Clamp01(weight);
return {glm::mix(a.Translation, b.Translation, weight), SafeNormalize(glm::slerp(a.Rotation, b.Rotation, weight)),
glm::mix(a.Scale, b.Scale, weight)};
}
MetaCoreAnimationPose BlendPose(const MetaCoreAnimationPose& a, const MetaCoreAnimationPose& b, float weight) {
MetaCoreAnimationPose result = a;
for (const auto& [path, value] : b.Nodes) {
const auto it = result.Nodes.find(path);
result.Nodes[path] = it == result.Nodes.end() ? BlendTransform({}, value, weight) : BlendTransform(it->second, value, weight);
}
return result;
}
void ApplyLayer(MetaCoreAnimationPose& base, const MetaCoreAnimationPose& layer, float weight,
MetaCoreAnimatorLayerBlendMode mode, const std::vector<std::string>& mask, const MetaCoreAnimationPose* reference = nullptr) {
const auto allowed = [&mask](std::string_view path) {
if (mask.empty()) return true;
return std::any_of(mask.begin(), mask.end(), [path](const std::string& root) {
return path == root || (path.size() > root.size() && path.starts_with(root) && path[root.size()] == '/');
});
};
for (const auto& [path, value] : layer.Nodes) {
if (!allowed(path)) continue;
auto& target = base.Nodes[path];
if (mode == MetaCoreAnimatorLayerBlendMode::Override) target = BlendTransform(target, value, weight);
else {
MetaCoreAnimationTransform delta = value;
if (reference != nullptr) if (const auto it = reference->Nodes.find(path); it != reference->Nodes.end()) {
delta.Translation -= it->second.Translation;
delta.Rotation = SafeNormalize(glm::inverse(it->second.Rotation) * value.Rotation);
delta.Scale /= glm::max(glm::abs(it->second.Scale), glm::vec3(0.000001F));
}
target.Translation += delta.Translation * weight;
target.Rotation = SafeNormalize(target.Rotation * glm::slerp(glm::quat(1, 0, 0, 0), delta.Rotation, Clamp01(weight)));
target.Scale *= glm::mix(glm::vec3(1.0F), delta.Scale, Clamp01(weight));
}
}
}
template<class Key> std::pair<std::size_t, std::size_t> KeyInterval(const std::vector<Key>& keys, float time) {
if (keys.size() < 2 || time <= keys.front().Time) return {0, 0};
if (time >= keys.back().Time) return {keys.size() - 1, keys.size() - 1};
const auto upper = std::upper_bound(keys.begin(), keys.end(), time, [](float t, const Key& key) { return t < key.Time; });
return {static_cast<std::size_t>(upper - keys.begin() - 1), static_cast<std::size_t>(upper - keys.begin())};
}
float KeyAlpha(float time, float a, float b) { return b > a ? Clamp01((time - a) / (b - a)) : 0.0F; }
glm::vec3 Hermite(glm::vec3 p0, glm::vec3 m0, glm::vec3 p1, glm::vec3 m1, float t, float duration) {
const float t2=t*t, t3=t2*t;
return (2*t3-3*t2+1)*p0 + (t3-2*t2+t)*(m0*duration) + (-2*t3+3*t2)*p1 + (t3-t2)*(m1*duration);
}
glm::quat Hermite(glm::quat p0, glm::quat m0, glm::quat p1, glm::quat m1, float t, float duration) {
const float t2=t*t, t3=t2*t;
return SafeNormalize((2*t3-3*t2+1)*p0 + (t3-2*t2+t)*(m0*duration) + (-2*t3+3*t2)*p1 + (t3-t2)*(m1*duration));
}
Json Guid(const MetaCoreAssetGuid& value) { return value.ToString(); }
MetaCoreAssetGuid ParseGuid(const Json& value) {
return value.is_string() ? MetaCoreAssetGuid::Parse(value.get<std::string>()).value_or(MetaCoreAssetGuid{}) : MetaCoreAssetGuid{};
}
Json V3(glm::vec3 v) { return Json::array({v.x,v.y,v.z}); }
glm::vec3 PV3(const Json& j, glm::vec3 fallback={}) { return j.is_array()&&j.size()==3 ? glm::vec3(j[0].get<float>(),j[1].get<float>(),j[2].get<float>()) : fallback; }
Json Q(glm::quat q) { return Json::array({q.w,q.x,q.y,q.z}); }
glm::quat PQ(const Json& j, glm::quat fallback={1,0,0,0}) { return j.is_array()&&j.size()==4 ? SafeNormalize({j[0].get<float>(),j[1].get<float>(),j[2].get<float>(),j[3].get<float>()}) : fallback; }
glm::quat PQTangent(const Json& j) { return j.is_array()&&j.size()==4 ? glm::quat(j[0].get<float>(),j[1].get<float>(),j[2].get<float>(),j[3].get<float>()) : glm::quat(0,0,0,0); }
Json Transform(const MetaCoreAnimationTransform& v) { return {{"t",V3(v.Translation)},{"r",Q(v.Rotation)},{"s",V3(v.Scale)}}; }
MetaCoreAnimationTransform PTransform(const Json& j) { MetaCoreAnimationTransform v; if(j.is_object()){v.Translation=PV3(j.value("t",Json{}));v.Rotation=PQ(j.value("r",Json{}));v.Scale=PV3(j.value("s",Json{}),{1,1,1});}return v; }
bool WriteJson(const std::filesystem::path& path, const Json& json) {
std::error_code ec; std::filesystem::create_directories(path.parent_path(), ec);
const auto temporary = path.string()+".tmp"; std::ofstream out(temporary, std::ios::trunc);
if(!out)return false; out<<json.dump(2); out.close(); if(!out)return false;
std::filesystem::rename(temporary,path,ec); if(ec){std::filesystem::remove(temporary);return false;} return true;
}
std::optional<Json> ReadJson(const std::filesystem::path& path) { try { std::ifstream in(path); if(!in)return std::nullopt; return Json::parse(in); } catch(...) { return std::nullopt; } }
Json ClipJson(const MetaCoreAnimationClipDocument& d) {
Json channels=Json::array(); for(const auto& c:d.Channels){Json v={{"path",c.NodePath},{"target",c.Target},{"interpolation",c.Interpolation}};v["v3"]=Json::array();for(const auto& k:c.Vec3Keys)v["v3"].push_back({{"time",k.Time},{"value",V3(k.Value)},{"in",V3(k.InTangent)},{"out",V3(k.OutTangent)}});v["q"]=Json::array();for(const auto& k:c.QuatKeys)v["q"].push_back({{"time",k.Time},{"value",Q(k.Value)},{"in",Q(k.InTangent)},{"out",Q(k.OutTangent)}});channels.push_back(std::move(v));}
Json events=Json::array();for(const auto&e:d.Events)events.push_back({{"id",e.StableId},{"time",e.Time},{"name",e.Name},{"type",e.ValueType},{"bool",e.BoolValue},{"int",e.IntValue},{"float",e.FloatValue},{"text",e.TextValue},{"object",e.ObjectValue},{"asset",Guid(e.AssetValue)}});
return {{"type","MetaCoreAnimationClip"},{"version",d.Version},{"guid",Guid(d.AssetGuid)},{"source_model",Guid(d.SourceModelGuid)},
{"stable_key",d.StableImportKey},{"name",d.Name},{"skeleton",d.SkeletonSignature},{"duration",d.Duration},{"loop",d.Loop},{"root",d.RootNodePath},{"channels",channels},{"events",events}};
}
std::optional<MetaCoreAnimationClipDocument> ParseClip(const Json& j) { try { if(j.value("type","")!="MetaCoreAnimationClip")return std::nullopt;MetaCoreAnimationClipDocument d;d.Version=j.value("version",0U);if(d.Version!=MetaCoreAnimationClipVersion)return std::nullopt;d.AssetGuid=ParseGuid(j.value("guid",Json{}));d.SourceModelGuid=ParseGuid(j.value("source_model",Json{}));d.StableImportKey=j.value("stable_key","");d.Name=j.value("name","");d.SkeletonSignature=j.value("skeleton","");d.Duration=j.value("duration",0.F);d.Loop=j.value("loop",true);d.RootNodePath=j.value("root","");for(const auto&v:j.value("channels",Json::array())){MetaCoreAnimationChannelDocument c;c.NodePath=v.value("path","");c.Target=v.value("target",MetaCoreAnimationChannelTarget::Translation);c.Interpolation=v.value("interpolation",MetaCoreAnimationInterpolation::Linear);for(const auto&k:v.value("v3",Json::array()))c.Vec3Keys.push_back({k.value("time",0.F),PV3(k.value("value",Json{})),PV3(k.value("in",Json{})),PV3(k.value("out",Json{}))});for(const auto&k:v.value("q",Json::array()))c.QuatKeys.push_back({k.value("time",0.F),PQ(k.value("value",Json{})),PQTangent(k.value("in",Json{})),PQTangent(k.value("out",Json{}))});d.Channels.push_back(std::move(c));}for(const auto&v:j.value("events",Json::array())){MetaCoreAnimationEventDocument e;e.StableId=v.value("id","");e.Time=v.value("time",0.F);e.Name=v.value("name","");e.ValueType=v.value("type",MetaCoreAnimationEventValueType::None);e.BoolValue=v.value("bool",false);e.IntValue=v.value("int",std::int64_t{});e.FloatValue=v.value("float",0.);e.TextValue=v.value("text","");e.ObjectValue=v.value("object",MetaCoreId{});e.AssetValue=ParseGuid(v.value("asset",Json{}));d.Events.push_back(std::move(e));}if(!MetaCoreValidateAnimationClip(d))return std::nullopt;return d;}catch(...){return std::nullopt;} }
Json AvatarJson(const MetaCoreAvatarDocument&d){Json b=Json::array();for(const auto&v:d.Bones)b.push_back({{"path",v.Path},{"parent",v.Parent},{"bind",Transform(v.BindPose)},{"required",v.Required}});Json m=Json::array();for(const auto&v:d.Mappings)m.push_back({{"source",v.SourcePath},{"target",v.TargetPath}});return{{"type","MetaCoreAvatar"},{"version",d.Version},{"guid",Guid(d.AssetGuid)},{"model",Guid(d.ModelGuid)},{"skeleton",d.SkeletonSignature},{"root",d.RootBonePath},{"scale",d.UniformScale},{"bones",b},{"mappings",m}};}
std::optional<MetaCoreAvatarDocument> ParseAvatar(const Json&j){try{if(j.value("type","")!="MetaCoreAvatar")return std::nullopt;MetaCoreAvatarDocument d;d.Version=j.value("version",0U);if(d.Version!=MetaCoreAvatarVersion)return std::nullopt;d.AssetGuid=ParseGuid(j.value("guid",Json{}));d.ModelGuid=ParseGuid(j.value("model",Json{}));d.SkeletonSignature=j.value("skeleton","");d.RootBonePath=j.value("root","");d.UniformScale=j.value("scale",1.F);for(const auto&v:j.value("bones",Json::array()))d.Bones.push_back({v.value("path",""),v.value("parent",-1),PTransform(v.value("bind",Json{})),v.value("required",false)});for(const auto&v:j.value("mappings",Json::array()))d.Mappings.push_back({v.value("source",""),v.value("target","")});if(!MetaCoreValidateAvatar(d))return std::nullopt;return d;}catch(...){return std::nullopt;}}
Json ControllerJson(const MetaCoreAnimatorControllerDocument&d){Json p=Json::array();for(const auto&v:d.Parameters)p.push_back({{"id",v.Id},{"type",v.Type},{"bool",v.BoolDefault},{"int",v.IntDefault},{"float",v.FloatDefault}});Json layers=Json::array();for(const auto&l:d.Layers){Json states=Json::array();for(const auto&s:l.States){Json samples=Json::array();for(const auto&v:s.Motion.Samples)samples.push_back({{"id",v.StableId},{"clip",Guid(v.ClipGuid)},{"threshold",v.Threshold},{"position",V3({v.Position,0})},{"speed",v.Speed}});states.push_back({{"id",s.Id},{"name",s.Name},{"speed",s.Speed},{"loop",s.Loop},{"motion",{{"type",s.Motion.Type},{"clip",Guid(s.Motion.ClipGuid)},{"x",s.Motion.ParameterX},{"y",s.Motion.ParameterY},{"samples",samples}}}});}Json transitions=Json::array();for(const auto&t:l.Transitions){Json conditions=Json::array();for(const auto&c:t.Conditions)conditions.push_back({{"parameter",c.ParameterId},{"mode",c.Mode},{"bool",c.BoolValue},{"int",c.IntValue},{"float",c.FloatValue}});transitions.push_back({{"id",t.Id},{"source",t.SourceStateId},{"target",t.TargetStateId},{"priority",t.Priority},{"has_exit",t.HasExitTime},{"exit",t.ExitTime},{"duration",t.Duration},{"fixed",t.FixedDuration},{"offset",t.TargetOffset},{"interrupt",t.CanInterrupt},{"self",t.AllowSelfTransition},{"conditions",conditions}});}layers.push_back({{"id",l.Id},{"name",l.Name},{"mode",l.BlendMode},{"weight",l.Weight},{"default",l.DefaultStateId},{"additive_reference",Guid(l.AdditiveReferenceClipGuid)},{"additive_reference_time",l.AdditiveReferenceTime},{"mask",l.MaskBonePaths},{"states",states},{"transitions",transitions}});}return{{"type","MetaCoreAnimatorController"},{"version",d.Version},{"guid",Guid(d.AssetGuid)},{"name",d.Name},{"parameters",p},{"layers",layers}};}
std::optional<MetaCoreAnimatorControllerDocument> ParseController(const Json&j){try{if(j.value("type","")!="MetaCoreAnimatorController")return std::nullopt;MetaCoreAnimatorControllerDocument d;d.Version=j.value("version",0U);if(d.Version!=MetaCoreAnimatorControllerVersion)return std::nullopt;d.AssetGuid=ParseGuid(j.value("guid",Json{}));d.Name=j.value("name","");for(const auto&v:j.value("parameters",Json::array()))d.Parameters.push_back({v.value("id",""),v.value("type",MetaCoreAnimatorParameterType::Float),v.value("bool",false),v.value("int",std::int64_t{}),v.value("float",0.F)});for(const auto&v:j.value("layers",Json::array())){MetaCoreAnimatorLayerDocument l;l.Id=v.value("id","");l.Name=v.value("name","");l.BlendMode=v.value("mode",MetaCoreAnimatorLayerBlendMode::Override);l.Weight=v.value("weight",1.F);l.DefaultStateId=v.value("default","");l.AdditiveReferenceClipGuid=ParseGuid(v.value("additive_reference",Json{}));l.AdditiveReferenceTime=v.value("additive_reference_time",0.F);l.MaskBonePaths=v.value("mask",std::vector<std::string>{});for(const auto&sv:v.value("states",Json::array())){MetaCoreAnimatorStateDocument s;s.Id=sv.value("id","");s.Name=sv.value("name","");s.Speed=sv.value("speed",1.F);s.Loop=sv.value("loop",true);const auto&m=sv.at("motion");s.Motion.Type=m.value("type",MetaCoreAnimatorMotionType::Clip);s.Motion.ClipGuid=ParseGuid(m.value("clip",Json{}));s.Motion.ParameterX=m.value("x","");s.Motion.ParameterY=m.value("y","");for(const auto&x:m.value("samples",Json::array())){const auto pos=PV3(x.value("position",Json{}));s.Motion.Samples.push_back({x.value("id",""),ParseGuid(x.value("clip",Json{})),x.value("threshold",0.F),{pos.x,pos.y},x.value("speed",1.F)});}l.States.push_back(std::move(s));}for(const auto&tv:v.value("transitions",Json::array())){MetaCoreAnimatorTransitionDocument t;t.Id=tv.value("id","");t.SourceStateId=tv.value("source","");t.TargetStateId=tv.value("target","");t.Priority=tv.value("priority",0);t.HasExitTime=tv.value("has_exit",false);t.ExitTime=tv.value("exit",1.F);t.Duration=tv.value("duration",.15F);t.FixedDuration=tv.value("fixed",true);t.TargetOffset=tv.value("offset",0.F);t.CanInterrupt=tv.value("interrupt",true);t.AllowSelfTransition=tv.value("self",false);for(const auto&cv:tv.value("conditions",Json::array()))t.Conditions.push_back({cv.value("parameter",""),cv.value("mode",MetaCoreAnimatorConditionMode::If),cv.value("bool",true),cv.value("int",std::int64_t{}),cv.value("float",0.F)});l.Transitions.push_back(std::move(t));}d.Layers.push_back(std::move(l));}if(!MetaCoreValidateAnimatorController(d))return std::nullopt;return d;}catch(...){return std::nullopt;}}
Json TimelineJson(const MetaCoreTimelineDocument&d){Json tracks=Json::array();for(const auto&t:d.Tracks){Json keys=Json::array();for(const auto&k:t.Keys)keys.push_back({{"time",k.Time},{"transform",Transform(k.Transform)},{"bool",k.BoolValue},{"text",k.TextValue},{"in",k.InTangent},{"out",k.OutTangent},{"interpolation",k.Interpolation}});tracks.push_back({{"id",t.Id},{"slot",t.BindingSlot},{"type",t.Type},{"parameter",t.AnimatorParameter},{"clip",Guid(t.AnimationClipGuid)},{"keys",keys}});}return{{"type","MetaCoreTimeline"},{"version",d.Version},{"guid",Guid(d.AssetGuid)},{"name",d.Name},{"duration",d.Duration},{"tracks",tracks}};}
std::optional<MetaCoreTimelineDocument> ParseTimeline(const Json&j){try{if(j.value("type","")!="MetaCoreTimeline")return std::nullopt;MetaCoreTimelineDocument d;d.Version=j.value("version",0U);if(d.Version!=MetaCoreTimelineVersion)return std::nullopt;d.AssetGuid=ParseGuid(j.value("guid",Json{}));d.Name=j.value("name","");d.Duration=j.value("duration",0.F);for(const auto&v:j.value("tracks",Json::array())){MetaCoreTimelineTrackDocument t;t.Id=v.value("id","");t.BindingSlot=v.value("slot","");t.Type=v.value("type",MetaCoreTimelineTrackType::Transform);t.AnimatorParameter=v.value("parameter","");t.AnimationClipGuid=ParseGuid(v.value("clip",Json{}));for(const auto&x:v.value("keys",Json::array()))t.Keys.push_back({x.value("time",0.F),PTransform(x.value("transform",Json{})),x.value("bool",false),x.value("text",""),x.value("in",0.F),x.value("out",0.F),x.value("interpolation",MetaCoreTimelineInterpolation::Linear)});t.Keys.shrink_to_fit();d.Tracks.push_back(std::move(t));}if(!MetaCoreValidateTimeline(d))return std::nullopt;return d;}catch(...){return std::nullopt;}}
}
glm::vec3 MetaCoreSampleAnimationVec3(const std::vector<MetaCoreAnimationVec3Key>& keys,float time,MetaCoreAnimationInterpolation interpolation,glm::vec3 fallback){if(keys.empty())return fallback;const auto[a,b]=KeyInterval(keys,time);if(a==b||interpolation==MetaCoreAnimationInterpolation::Step)return keys[a].Value;const float t=KeyAlpha(time,keys[a].Time,keys[b].Time);return interpolation==MetaCoreAnimationInterpolation::CubicSpline?Hermite(keys[a].Value,keys[a].OutTangent,keys[b].Value,keys[b].InTangent,t,keys[b].Time-keys[a].Time):glm::mix(keys[a].Value,keys[b].Value,t);}
glm::quat MetaCoreSampleAnimationQuat(const std::vector<MetaCoreAnimationQuatKey>& keys,float time,MetaCoreAnimationInterpolation interpolation,glm::quat fallback){if(keys.empty())return fallback;const auto[a,b]=KeyInterval(keys,time);if(a==b||interpolation==MetaCoreAnimationInterpolation::Step)return SafeNormalize(keys[a].Value);const float t=KeyAlpha(time,keys[a].Time,keys[b].Time);return interpolation==MetaCoreAnimationInterpolation::CubicSpline?SafeNormalize(Hermite(keys[a].Value,keys[a].OutTangent,keys[b].Value,keys[b].InTangent,t,keys[b].Time-keys[a].Time)):SafeNormalize(glm::slerp(keys[a].Value,keys[b].Value,t));}
MetaCoreAnimationPose MetaCoreSampleAnimationClip(const MetaCoreAnimationClipDocument&clip,float time){MetaCoreAnimationPose pose;time=std::max(0.F,std::isfinite(time)?time:0.F);if(clip.Duration>0)time=clip.Loop&&time>clip.Duration?std::fmod(time,clip.Duration):std::min(time,clip.Duration);for(const auto&channel:clip.Channels){auto&v=pose.Nodes[channel.NodePath];if(channel.Target==MetaCoreAnimationChannelTarget::Translation)v.Translation=MetaCoreSampleAnimationVec3(channel.Vec3Keys,time,channel.Interpolation);else if(channel.Target==MetaCoreAnimationChannelTarget::Scale)v.Scale=MetaCoreSampleAnimationVec3(channel.Vec3Keys,time,channel.Interpolation,{1,1,1});else v.Rotation=MetaCoreSampleAnimationQuat(channel.QuatKeys,time,channel.Interpolation);}return pose;}
bool MetaCoreValidateAnimationClip(const MetaCoreAnimationClipDocument&d,std::vector<std::string>*issues){std::vector<std::string>local;auto add=[&](std::string s){local.push_back(std::move(s));};if(d.Version!=MetaCoreAnimationClipVersion)add("unsupported clip version");if(!std::isfinite(d.Duration)||d.Duration<0)add("invalid duration");std::set<std::pair<std::string,int>>seen;for(const auto&c:d.Channels){if(c.NodePath.empty())add("empty channel path");if(!seen.emplace(c.NodePath,(int)c.Target).second)add("duplicate channel");float last=-1;const auto check=[&](const auto&keys){for(const auto&k:keys){if(!std::isfinite(k.Time)||k.Time<last)add("unsorted/non-finite keys");last=k.Time;}};check(c.Vec3Keys);check(c.QuatKeys);}std::unordered_set<std::string>ids;for(const auto&e:d.Events){if(e.StableId.empty()||!ids.insert(e.StableId).second)add("duplicate/empty event id");if(e.Time<0||e.Time>d.Duration)add("event outside clip");}if(issues)*issues=local;return local.empty();}
bool MetaCoreValidateAvatar(const MetaCoreAvatarDocument& d, std::vector<std::string>* issues) {
std::vector<std::string> validationIssues;
if (d.Version != MetaCoreAvatarVersion) validationIssues.push_back("unsupported avatar version");
if (!std::isfinite(d.UniformScale) || d.UniformScale <= 0) validationIssues.push_back("invalid scale");
std::unordered_set<std::string> paths;
std::unordered_set<std::string> targets;
bool parentRangeValid = true;
for (const auto& bone : d.Bones) {
if (bone.Path.empty() || !paths.insert(bone.Path).second)
validationIssues.push_back("duplicate/empty bone path");
if (bone.Parent < -1 || bone.Parent >= static_cast<int>(d.Bones.size())) {
validationIssues.push_back("invalid parent index");
parentRangeValid = false;
}
if (!std::isfinite(glm::length(bone.BindPose.Translation)) ||
!std::isfinite(glm::length(bone.BindPose.Rotation)) ||
!std::isfinite(glm::length(bone.BindPose.Scale)) ||
std::abs(bone.BindPose.Scale.x * bone.BindPose.Scale.y * bone.BindPose.Scale.z) < 0.000001F)
validationIssues.push_back("non-invertible bind transform");
}
// glTF does not require parents to precede children in the node array.
// Validate the graph itself instead of rejecting a valid non-topological order.
if (parentRangeValid) {
for (std::size_t boneIndex = 0; boneIndex < d.Bones.size(); ++boneIndex) {
std::unordered_set<int> chain;
int cursor = static_cast<int>(boneIndex);
while (cursor >= 0) {
if (!chain.insert(cursor).second) {
validationIssues.push_back("cyclic bone hierarchy");
break;
}
cursor = d.Bones[static_cast<std::size_t>(cursor)].Parent;
}
}
}
for (const auto& mapping : d.Mappings)
if (mapping.SourcePath.empty() || mapping.TargetPath.empty() ||
!targets.insert(mapping.TargetPath).second ||
!paths.contains(mapping.SourcePath) || !paths.contains(mapping.TargetPath))
validationIssues.push_back("invalid/duplicate mapping");
if (!d.RootBonePath.empty() && !paths.contains(d.RootBonePath))
validationIssues.push_back("missing root bone");
if (issues) *issues = validationIssues;
return validationIssues.empty();
}
bool MetaCoreValidateAnimatorController(const MetaCoreAnimatorControllerDocument&d,std::vector<std::string>*issues){std::vector<std::string>v;if(d.Version!=MetaCoreAnimatorControllerVersion)v.push_back("unsupported controller version");std::unordered_set<std::string>params;for(const auto&p:d.Parameters)if(p.Id.empty()||!params.insert(p.Id).second)v.push_back("duplicate/empty parameter");std::unordered_set<std::string>layers;for(const auto&l:d.Layers){if(l.Id.empty()||!layers.insert(l.Id).second)v.push_back("duplicate/empty layer");if(!std::isfinite(l.Weight)||l.Weight<0||l.Weight>1)v.push_back("invalid layer weight");std::unordered_set<std::string>states;for(const auto&s:l.States){if(s.Id.empty()||!states.insert(s.Id).second)v.push_back("duplicate/empty state");if(!std::isfinite(s.Speed)||s.Speed<0)v.push_back("invalid state speed");if(s.Motion.Type!=MetaCoreAnimatorMotionType::Clip&&s.Motion.Samples.empty())v.push_back("empty blend tree");std::unordered_set<std::string>samples;for(const auto&sample:s.Motion.Samples)if(sample.StableId.empty()||!samples.insert(sample.StableId).second||!std::isfinite(sample.Speed)||sample.Speed<0)v.push_back("invalid/duplicate blend sample");}if(!states.contains(l.DefaultStateId))v.push_back("missing default state");std::unordered_set<std::string>transitions;for(const auto&t:l.Transitions){if(t.Id.empty()||!transitions.insert(t.Id).second)v.push_back("duplicate/empty transition");if(!states.contains(t.SourceStateId)||!states.contains(t.TargetStateId))v.push_back("transition state missing");if(t.SourceStateId==t.TargetStateId&&!t.AllowSelfTransition)v.push_back("self transition disabled");if(!std::isfinite(t.Duration)||t.Duration<0||!std::isfinite(t.ExitTime)||!std::isfinite(t.TargetOffset))v.push_back("invalid transition time");for(const auto&c:t.Conditions)if(!params.contains(c.ParameterId))v.push_back("condition parameter missing");}}if(issues)*issues=v;return v.empty();}
bool MetaCoreValidateTimeline(const MetaCoreTimelineDocument&d,std::vector<std::string>*issues){std::vector<std::string>v;if(d.Version!=MetaCoreTimelineVersion)v.push_back("unsupported timeline version");if(!std::isfinite(d.Duration)||d.Duration<0)v.push_back("invalid duration");std::unordered_set<std::string>ids;for(const auto&t:d.Tracks){if(t.Id.empty()||!ids.insert(t.Id).second)v.push_back("duplicate/empty track");if(t.BindingSlot.empty()&&t.Type!=MetaCoreTimelineTrackType::Signal)v.push_back("missing binding");float last=-1;for(const auto&k:t.Keys){if(!std::isfinite(k.Time)||k.Time<last||k.Time<0||k.Time>d.Duration||!std::isfinite(k.InTangent)||!std::isfinite(k.OutTangent))v.push_back("invalid key time");last=k.Time;}}if(issues)*issues=v;return v.empty();}
bool MetaCoreWriteAnimationClip(const std::filesystem::path&p,const MetaCoreAnimationClipDocument&d){return MetaCoreValidateAnimationClip(d)&&WriteJson(p,ClipJson(d));}std::optional<MetaCoreAnimationClipDocument> MetaCoreReadAnimationClip(const std::filesystem::path&p){auto j=ReadJson(p);return j?ParseClip(*j):std::nullopt;}
bool MetaCoreWriteAvatar(const std::filesystem::path&p,const MetaCoreAvatarDocument&d){return MetaCoreValidateAvatar(d)&&WriteJson(p,AvatarJson(d));}std::optional<MetaCoreAvatarDocument> MetaCoreReadAvatar(const std::filesystem::path&p){auto j=ReadJson(p);return j?ParseAvatar(*j):std::nullopt;}
bool MetaCoreWriteAnimatorController(const std::filesystem::path&p,const MetaCoreAnimatorControllerDocument&d){return MetaCoreValidateAnimatorController(d)&&WriteJson(p,ControllerJson(d));}std::optional<MetaCoreAnimatorControllerDocument> MetaCoreReadAnimatorController(const std::filesystem::path&p){auto j=ReadJson(p);return j?ParseController(*j):std::nullopt;}
bool MetaCoreWriteTimeline(const std::filesystem::path&p,const MetaCoreTimelineDocument&d){return MetaCoreValidateTimeline(d)&&WriteJson(p,TimelineJson(d));}std::optional<MetaCoreTimelineDocument> MetaCoreReadTimeline(const std::filesystem::path&p){auto j=ReadJson(p);return j?ParseTimeline(*j):std::nullopt;}
MetaCoreAssetGuid MetaCoreBuildDeterministicAnimationGuid(std::string_view key){const auto a=MetaCoreHashString(key);const auto b=MetaCoreHashCombine(a,MetaCoreHashString("MetaCoreAnimation"));MetaCoreAssetGuid guid;for(std::size_t i=0;i<8;++i){guid.Bytes[i]=static_cast<std::uint8_t>((a>>(i*8))&0xffU);guid.Bytes[i+8]=static_cast<std::uint8_t>((b>>(i*8))&0xffU);}return guid;}
MetaCoreAnimationAssetLibrary::MetaCoreAnimationAssetLibrary(std::filesystem::path root) { if (!root.empty()) Scan(root); }
void MetaCoreAnimationAssetLibrary::Scan(const std::filesystem::path& root) {
Clips_.clear(); Avatars_.clear(); Controllers_.clear(); Timelines_.clear();
std::error_code error; if (!std::filesystem::is_directory(root, error)) return;
for (const auto& entry : std::filesystem::recursive_directory_iterator(root, error)) {
if (error || !entry.is_regular_file()) continue;
const auto extension = entry.path().extension();
if (extension == ".mcanim") { if (auto value=MetaCoreReadAnimationClip(entry.path())) Clips_.insert_or_assign(value->AssetGuid,entry.path()); }
else if (extension == ".mcavatar") { if (auto value=MetaCoreReadAvatar(entry.path())) Avatars_.insert_or_assign(value->AssetGuid,entry.path()); }
else if (extension == ".mcanimator") { if (auto value=MetaCoreReadAnimatorController(entry.path())) Controllers_.insert_or_assign(value->AssetGuid,entry.path()); }
else if (extension == ".mctimeline") { if (auto value=MetaCoreReadTimeline(entry.path())) Timelines_.insert_or_assign(value->AssetGuid,entry.path()); }
}
}
std::optional<MetaCoreAnimationClipDocument> MetaCoreAnimationAssetLibrary::LoadClip(MetaCoreAssetGuid guid) const { const auto i=Clips_.find(guid);return i==Clips_.end()?std::nullopt:MetaCoreReadAnimationClip(i->second); }
std::optional<MetaCoreAvatarDocument> MetaCoreAnimationAssetLibrary::LoadAvatar(MetaCoreAssetGuid guid) const { const auto i=Avatars_.find(guid);return i==Avatars_.end()?std::nullopt:MetaCoreReadAvatar(i->second); }
std::optional<MetaCoreAnimatorControllerDocument> MetaCoreAnimationAssetLibrary::LoadController(MetaCoreAssetGuid guid) const { const auto i=Controllers_.find(guid);return i==Controllers_.end()?std::nullopt:MetaCoreReadAnimatorController(i->second); }
std::optional<MetaCoreTimelineDocument> MetaCoreAnimationAssetLibrary::LoadTimeline(MetaCoreAssetGuid guid) const { const auto i=Timelines_.find(guid);return i==Timelines_.end()?std::nullopt:MetaCoreReadTimeline(i->second); }
std::optional<MetaCoreAnimationClipDocument> MetaCoreAnimationAssetLibrary::FindClip(MetaCoreAssetGuid model,std::string_view key) const { for(const auto&[guid,path]:Clips_){(void)guid;if(auto value=MetaCoreReadAnimationClip(path);value&&value->SourceModelGuid==model&&(value->Name==key||value->StableImportKey==key))return value;}return std::nullopt; }
std::optional<MetaCoreAvatarDocument> MetaCoreAnimationAssetLibrary::FindAvatar(MetaCoreAssetGuid model) const { for(const auto&[guid,path]:Avatars_){(void)guid;if(auto value=MetaCoreReadAvatar(path);value&&value->ModelGuid==model)return value;}return std::nullopt; }
namespace {
using ParameterValue=std::variant<bool,std::int64_t,float>;
struct LayerRuntime{std::string State;float Time=0;std::string Previous;float PreviousTime=0;float TransitionTime=0;float TransitionDuration=0;bool InTransition=false;};
struct AnimatorRuntime{MetaCoreAnimatorControllerDocument Controller;std::unordered_map<std::string,ParameterValue> Parameters;std::unordered_map<std::string,MetaCoreAnimatorParameterType> ParameterTypes;std::unordered_set<std::string> Triggers;std::vector<LayerRuntime> Layers;MetaCoreAnimationPose Pose;MetaCoreRootMotionDelta RootMotion;MetaCoreAnimatorDebugSnapshot Debug;bool Playing=false;};
struct TimelineRuntime{MetaCoreTimelineDocument Timeline;float Time=0;float PreviousTime=0;float Speed=1;bool Playing=false;};
const MetaCoreAnimatorStateDocument* FindState(const MetaCoreAnimatorLayerDocument&l,std::string_view id){const auto it=std::find_if(l.States.begin(),l.States.end(),[&](const auto&s){return s.Id==id;});return it==l.States.end()?nullptr:&*it;}
float ParamFloat(const AnimatorRuntime&i,std::string_view id){const auto it=i.Parameters.find(std::string(id));if(it==i.Parameters.end())return 0;if(const auto*v=std::get_if<float>(&it->second))return*v;if(const auto*v=std::get_if<std::int64_t>(&it->second))return(float)*v;return std::get<bool>(it->second)?1.F:0.F;}
bool Condition(const AnimatorRuntime&i,const MetaCoreAnimatorConditionDocument&c){const auto it=i.Parameters.find(c.ParameterId);if(c.Mode==MetaCoreAnimatorConditionMode::If||c.Mode==MetaCoreAnimatorConditionMode::IfNot){const bool value=i.Triggers.contains(c.ParameterId)||(it!=i.Parameters.end()&&std::visit([](auto v){return v!=0;},it->second));return c.Mode==MetaCoreAnimatorConditionMode::If?value:!value;}const float value=ParamFloat(i,c.ParameterId);if(c.Mode==MetaCoreAnimatorConditionMode::Greater)return value>c.FloatValue;if(c.Mode==MetaCoreAnimatorConditionMode::Less)return value<c.FloatValue;if(c.Mode==MetaCoreAnimatorConditionMode::NotEqual)return std::abs(value-c.FloatValue)>0.00001F;return std::abs(value-c.FloatValue)<=0.00001F;}
glm::vec2 ProjectToSampleHull(glm::vec2 point,std::vector<const MetaCoreAnimatorBlendSampleDocument*> samples){if(samples.size()<2)return samples.empty()?point:samples.front()->Position;std::sort(samples.begin(),samples.end(),[](auto*a,auto*b){if(a->Position.x!=b->Position.x)return a->Position.x<b->Position.x;if(a->Position.y!=b->Position.y)return a->Position.y<b->Position.y;return a->StableId<b->StableId;});const auto cross=[](glm::vec2 a,glm::vec2 b,glm::vec2 c){const auto ab=b-a,ac=c-a;return ab.x*ac.y-ab.y*ac.x;};std::vector<glm::vec2>hull;for(auto*s:samples){while(hull.size()>=2&&cross(hull[hull.size()-2],hull.back(),s->Position)<=0)hull.pop_back();hull.push_back(s->Position);}const auto lower=hull.size();for(auto it=samples.rbegin()+1;it!=samples.rend();++it){while(hull.size()>lower&&cross(hull[hull.size()-2],hull.back(),(*it)->Position)<=0)hull.pop_back();hull.push_back((*it)->Position);}if(hull.size()>1)hull.pop_back();if(hull.size()<2)return hull.empty()?point:hull.front();bool inside=true;for(std::size_t i=0;i<hull.size();++i)if(cross(hull[i],hull[(i+1)%hull.size()],point)<-0.00001F){inside=false;break;}if(inside)return point;glm::vec2 closest=hull.front();float best=std::numeric_limits<float>::max();for(std::size_t i=0;i<hull.size();++i){const auto a=hull[i],b=hull[(i+1)%hull.size()],edge=b-a;const float length2=glm::dot(edge,edge);const float t=length2>0?glm::clamp(glm::dot(point-a,edge)/length2,0.F,1.F):0;const auto candidate=a+edge*t;const float distance=glm::dot(point-candidate,point-candidate);if(distance<best){best=distance;closest=candidate;}}return closest;}
std::vector<std::pair<MetaCoreAssetGuid,float>> RawMotionWeights(const MetaCoreAnimatorMotionDocument&m,const AnimatorRuntime&i){if(m.Type==MetaCoreAnimatorMotionType::Clip)return{{m.ClipGuid,1}};if(m.Samples.empty())return{};std::vector<const MetaCoreAnimatorBlendSampleDocument*>samples;for(const auto&s:m.Samples)samples.push_back(&s);std::sort(samples.begin(),samples.end(),[](auto*a,auto*b){return a->StableId<b->StableId;});if(m.Type==MetaCoreAnimatorMotionType::BlendTree1D){const float x=ParamFloat(i,m.ParameterX);std::sort(samples.begin(),samples.end(),[](auto*a,auto*b){return a->Threshold==b->Threshold?a->StableId<b->StableId:a->Threshold<b->Threshold;});if(x<=samples.front()->Threshold)return{{samples.front()->ClipGuid,1}};if(x>=samples.back()->Threshold)return{{samples.back()->ClipGuid,1}};for(std::size_t n=1;n<samples.size();++n)if(x<=samples[n]->Threshold){const float d=samples[n]->Threshold-samples[n-1]->Threshold;const float w=d>0?(x-samples[n-1]->Threshold)/d:0;return{{samples[n-1]->ClipGuid,1-w},{samples[n]->ClipGuid,w}};}}glm::vec2 p{ParamFloat(i,m.ParameterX),ParamFloat(i,m.ParameterY)};if(m.Type==MetaCoreAnimatorMotionType::BlendTree2DCartesian)p=ProjectToSampleHull(p,samples);std::vector<std::pair<MetaCoreAssetGuid,float>>result;float total=0;for(auto*s:samples){float distance=glm::length(p-s->Position);if(m.Type==MetaCoreAnimatorMotionType::BlendTree2DDirectional){const float pLength=glm::length(p),sLength=glm::length(s->Position);const float angular=pLength>.00001F&&sLength>.00001F?std::acos(glm::clamp(glm::dot(p/pLength,s->Position/sLength),-1.F,1.F)):0;distance=angular+std::abs(pLength-sLength);}if(distance<0.00001F)return{{s->ClipGuid,1}};const float w=1.F/distance;result.emplace_back(s->ClipGuid,w);total+=w;}for(auto&v:result)v.second/=total;return result;}
std::vector<std::pair<MetaCoreAssetGuid,float>> MotionWeights(const MetaCoreAnimatorMotionDocument&m,const AnimatorRuntime&i){auto raw=RawMotionWeights(m,i);std::vector<std::pair<MetaCoreAssetGuid,float>>result;result.reserve(raw.size());for(const auto&value:raw){const auto found=std::find_if(result.begin(),result.end(),[&](const auto&existing){return existing.first==value.first;});if(found==result.end())result.push_back(value);else found->second+=value.second;}return result;}
}
class MetaCoreAnimationRuntime::Impl {
public:
MetaCoreScene& Scene;MetaCoreAnimationClipProvider Clips;MetaCoreAnimatorControllerProvider Controllers;MetaCoreAvatarProvider Avatars;MetaCoreTimelineProvider Timelines;MetaCorePhysicsWorld* Physics=nullptr;MetaCoreAnimationEventCallback Events;bool Running=false;
std::unordered_map<MetaCoreId,AnimatorRuntime> Animators;std::unordered_map<MetaCoreId,TimelineRuntime> Directors;std::unordered_set<MetaCoreId> DisabledAnimators;
std::unordered_map<MetaCoreAssetGuid,MetaCoreAnimationClipDocument,MetaCoreAssetGuidHasher> ClipCache;
Impl(MetaCoreScene&s,MetaCoreAnimationClipProvider c,MetaCoreAnimatorControllerProvider co,MetaCoreAvatarProvider a,MetaCoreTimelineProvider t,MetaCorePhysicsWorld*p):Scene(s),Clips(std::move(c)),Controllers(std::move(co)),Avatars(std::move(a)),Timelines(std::move(t)),Physics(p){}
const MetaCoreAnimationClipDocument* GetClip(MetaCoreAssetGuid guid){const auto cached=ClipCache.find(guid);if(cached!=ClipCache.end())return&cached->second;if(!Clips)return nullptr;auto loaded=Clips(guid);if(!loaded)return nullptr;return&ClipCache.emplace(guid,std::move(*loaded)).first->second;}
void Emit(MetaCoreId id,std::string type,std::string name,MetaCoreAnimationEventDocument value={}){if(Events)Events({id,std::move(type),std::move(name),std::move(value)});}
bool ValidateRuntimeAssets(MetaCoreId id,const MetaCoreAnimatorComponent&component,const MetaCoreAnimatorControllerDocument&controller){const auto fail=[&](std::string issue){Emit(id,"AnimationError",std::move(issue));DisabledAnimators.insert(id);return false;};if(component.RootMotionMode==MetaCoreRootMotionMode::CharacterController&&component.UpdateMode!=MetaCoreAnimatorUpdateMode::Fixed)return fail("CharacterControllerRootMotionRequiresFixedUpdate");std::optional<MetaCoreAvatarDocument>avatar;if(component.AvatarAssetGuid.IsValid()){avatar=Avatars?Avatars(component.AvatarAssetGuid):std::nullopt;if(!avatar||!MetaCoreValidateAvatar(*avatar))return fail("InvalidOrMissingAvatar:"+component.AvatarAssetGuid.ToString());}for(const auto&layer:controller.Layers)for(const auto&state:layer.States){std::vector<MetaCoreAssetGuid>guids;if(state.Motion.Type==MetaCoreAnimatorMotionType::Clip)guids.push_back(state.Motion.ClipGuid);else for(const auto&sample:state.Motion.Samples)guids.push_back(sample.ClipGuid);for(const auto guid:guids){const auto*clip=GetClip(guid);if(!clip)return fail("MissingClip:"+guid.ToString());if(avatar&&!clip->SkeletonSignature.empty()&&!avatar->SkeletonSignature.empty()&&clip->SkeletonSignature!=avatar->SkeletonSignature&&!avatar->Mappings.empty())return fail("SkeletonSignatureMismatch:"+guid.ToString());}}return true;}
void Prune(){for(auto it=Animators.begin();it!=Animators.end();){auto object=Scene.FindGameObject(it->first);const bool keep=object&&object.HasComponent<MetaCoreAnimatorComponent>()&&object.GetComponent<MetaCoreAnimatorComponent>().Enabled&&object.GetComponent<MetaCoreAnimatorComponent>().ControllerAssetGuid==it->second.Controller.AssetGuid;if(keep){++it;continue;}if(object&&object.HasComponent<MetaCoreAnimationRuntimePoseComponent>())object.RemoveComponent<MetaCoreAnimationRuntimePoseComponent>();it=Animators.erase(it);}for(auto it=Directors.begin();it!=Directors.end();){auto object=Scene.FindGameObject(it->first);const bool keep=object&&object.HasComponent<MetaCoreTimelineDirectorComponent>()&&object.GetComponent<MetaCoreTimelineDirectorComponent>().Enabled&&object.GetComponent<MetaCoreTimelineDirectorComponent>().TimelineAssetGuid==it->second.Timeline.AssetGuid;if(keep)++it;else it=Directors.erase(it);}}
void Sync(){for(auto o:Scene.GetGameObjects()){if(o.HasComponent<MetaCoreAnimatorComponent>()){const auto&c=o.GetComponent<MetaCoreAnimatorComponent>();if(c.Enabled&&c.ControllerAssetGuid.IsValid()&&!Animators.contains(o.GetId())&&!DisabledAnimators.contains(o.GetId())&&Controllers){auto d=Controllers(c.ControllerAssetGuid);if(!d){Emit(o.GetId(),"AnimationError","MissingController:"+c.ControllerAssetGuid.ToString());DisabledAnimators.insert(o.GetId());}else if(ValidateRuntimeAssets(o.GetId(),c,*d)){AnimatorRuntime r;r.Controller=*d;r.Debug.ObjectId=o.GetId();r.Debug.Playing=c.PlayOnStart;r.Playing=c.PlayOnStart;for(const auto&p:d->Parameters){r.ParameterTypes[p.Id]=p.Type;if(p.Type==MetaCoreAnimatorParameterType::Bool||p.Type==MetaCoreAnimatorParameterType::Trigger)r.Parameters[p.Id]=p.BoolDefault;else if(p.Type==MetaCoreAnimatorParameterType::Int)r.Parameters[p.Id]=p.IntDefault;else r.Parameters[p.Id]=p.FloatDefault;}for(const auto&l:d->Layers){LayerRuntime layerRuntime;layerRuntime.State=l.DefaultStateId;r.Layers.push_back(std::move(layerRuntime));}Animators.emplace(o.GetId(),std::move(r));}}}if(o.HasComponent<MetaCoreTimelineDirectorComponent>()){const auto&c=o.GetComponent<MetaCoreTimelineDirectorComponent>();if(c.Enabled&&c.TimelineAssetGuid.IsValid()&&!Directors.contains(o.GetId())&&Timelines){auto d=Timelines(c.TimelineAssetGuid);if(d)Directors.emplace(o.GetId(),TimelineRuntime{*d,0,0,SafeSpeed(c.Speed),c.PlayOnStart});}}}}
MetaCoreAnimationPose SampleMotion(const MetaCoreAnimatorMotionDocument&m,const AnimatorRuntime&i,float time){MetaCoreAnimationPose result;float accumulated=0;for(const auto&[guid,w]:MotionWeights(m,i)){const auto*clip=GetClip(guid);if(!clip)continue;auto pose=MetaCoreSampleAnimationClip(*clip,time);result=accumulated==0?pose:BlendPose(result,pose,w/(accumulated+w));accumulated+=w;}return result;}
MetaCoreAnimationPose RetargetPose(const MetaCoreAnimationPose& source, const MetaCoreAvatarDocument& avatar) {
if (avatar.Mappings.empty()) return source;
MetaCoreAnimationPose result = source;
const auto findBone = [&avatar](std::string_view path) -> const MetaCoreAvatarBoneDocument* {
const auto it = std::find_if(avatar.Bones.begin(), avatar.Bones.end(),
[path](const auto& bone) { return bone.Path == path; });
return it == avatar.Bones.end() ? nullptr : &*it;
};
for (const auto& mapping : avatar.Mappings) {
const auto sampled = source.Nodes.find(mapping.SourcePath);
const auto* sourceBone = findBone(mapping.SourcePath);
const auto* targetBone = findBone(mapping.TargetPath);
if (sampled == source.Nodes.end() || sourceBone == nullptr || targetBone == nullptr) continue;
MetaCoreAnimationTransform target = targetBone->BindPose;
const auto rotationDelta = SafeNormalize(glm::inverse(sourceBone->BindPose.Rotation) * sampled->second.Rotation);
target.Rotation = SafeNormalize(targetBone->BindPose.Rotation * rotationDelta);
target.Translation += (sampled->second.Translation - sourceBone->BindPose.Translation) * avatar.UniformScale;
const glm::vec3 safeSourceScale = glm::max(glm::abs(sourceBone->BindPose.Scale), glm::vec3(0.000001F));
target.Scale *= sampled->second.Scale / safeSourceScale;
result.Nodes[mapping.TargetPath] = target;
}
return result;
}
float MotionDuration(const MetaCoreAnimatorMotionDocument&m,const AnimatorRuntime&i){float d=0;for(const auto&[guid,w]:MotionWeights(m,i)){(void)w;if(const auto*c=GetClip(guid))d=std::max(d,c->Duration);}return d;}
void ClipEvents(MetaCoreId id,const MetaCoreAnimatorMotionDocument&m,const AnimatorRuntime&i,float previous,float current,bool loop,int wraps=0){for(const auto&[guid,w]:MotionWeights(m,i)){(void)w;const auto*c=GetClip(guid);if(!c||c->Duration<=0)continue;const int actualWraps=loop?std::max(wraps,current<previous?1:0):0;for(const auto&e:c->Events){int occurrences=0;if(actualWraps==0)occurrences=e.Time>previous&&e.Time<=current?1:0;else occurrences=(e.Time>previous?1:0)+std::max(0,actualWraps-1)+(e.Time<=current?1:0);for(int occurrence=0;occurrence<occurrences;++occurrence)Emit(id,"ClipEvent",e.Name,e);}}}
void EvaluateAnimator(MetaCoreId id, AnimatorRuntime& i, double delta, bool fixed) {
auto object = Scene.FindGameObject(id);
if (!object || !object.HasComponent<MetaCoreAnimatorComponent>() || !i.Playing) return;
const auto& component = object.GetComponent<MetaCoreAnimatorComponent>();
if ((component.UpdateMode == MetaCoreAnimatorUpdateMode::Fixed) != fixed) return;
const float dt = static_cast<float>(std::max(0.0, delta)) * SafeSpeed(component.Speed);
MetaCoreAnimationPose finalPose;
MetaCoreAnimationPose cycleStartPose, cycleEndPose;
int baseLayerWraps = 0;
i.Debug.Layers.clear();
for (std::size_t n = 0; n < i.Controller.Layers.size() && n < i.Layers.size(); ++n) {
const auto& layer = i.Controller.Layers[n];
auto& runtime = i.Layers[n];
const auto* state = FindState(layer, runtime.State);
if (!state) continue;
const float sourceDuration = MotionDuration(state->Motion, i);
const float sourcePrevious = runtime.Time;
const float sourceAdvance = dt * SafeSpeed(state->Speed);
const float sourceRawTime = runtime.Time + sourceAdvance;
int sourceWraps = 0;
runtime.Time = sourceRawTime;
if (sourceDuration > 0.0F) {
if (state->Loop) {
sourceWraps = static_cast<int>(std::floor(sourceRawTime / sourceDuration));
runtime.Time = std::fmod(sourceRawTime, sourceDuration);
} else runtime.Time = std::min(runtime.Time, sourceDuration);
}
if(n==0&&sourceWraps>0){baseLayerWraps=sourceWraps;cycleStartPose=SampleMotion(state->Motion,i,0.0F);cycleEndPose=SampleMotion(state->Motion,i,std::max(0.0F,sourceDuration-0.00001F));}
std::vector<const MetaCoreAnimatorTransitionDocument*> eligible;
for (const auto& transition : layer.Transitions) {
const bool exitReady = !transition.HasExitTime || sourceDuration <= 0.0F || sourceWraps > 0 ||
runtime.Time / sourceDuration >= transition.ExitTime;
if (transition.SourceStateId == state->Id &&
(transition.AllowSelfTransition || transition.TargetStateId != state->Id) && exitReady &&
std::all_of(transition.Conditions.begin(), transition.Conditions.end(),
[&](const auto& condition) { return Condition(i, condition); })) eligible.push_back(&transition);
}
std::sort(eligible.begin(), eligible.end(), [](auto* a, auto* b) {
return a->Priority == b->Priority ? a->Id < b->Id : a->Priority > b->Priority;
});
bool transitioned = false;
if (!eligible.empty() && (!runtime.InTransition || eligible.front()->CanInterrupt)) {
ClipEvents(id, state->Motion, i, sourcePrevious, runtime.Time, state->Loop, sourceWraps);
const auto& transition = *eligible.front();
runtime.Previous = runtime.State;
runtime.PreviousTime = runtime.Time;
runtime.State = transition.TargetStateId;
const auto* target = FindState(layer, runtime.State);
runtime.Time = target ? MotionDuration(target->Motion, i) * Clamp01(transition.TargetOffset) : 0.0F;
runtime.TransitionTime = 0.0F;
runtime.TransitionDuration = std::max(0.0F, transition.FixedDuration ? transition.Duration : transition.Duration * sourceDuration);
runtime.InTransition = runtime.TransitionDuration > 0.0F;
for (const auto& condition : transition.Conditions) i.Triggers.erase(condition.ParameterId);
Emit(id, "StateExit", state->Id);
Emit(id, "StateEnter", runtime.State);
state = target;
transitioned = true;
}
if (!state) continue;
auto pose = SampleMotion(state->Motion, i, runtime.Time);
if (runtime.InTransition) {
runtime.TransitionTime += dt;
const auto* old = FindState(layer, runtime.Previous);
if (old) {
if (!transitioned) {
const float oldPrevious = runtime.PreviousTime;
const float oldDuration = MotionDuration(old->Motion, i);
runtime.PreviousTime += dt * SafeSpeed(old->Speed);
int oldWraps=0;
if (oldDuration > 0.0F) {if(old->Loop){oldWraps=static_cast<int>(std::floor(runtime.PreviousTime/oldDuration));runtime.PreviousTime=std::fmod(runtime.PreviousTime,oldDuration);}else runtime.PreviousTime=std::min(runtime.PreviousTime,oldDuration);}
ClipEvents(id, old->Motion, i, oldPrevious, runtime.PreviousTime, old->Loop, oldWraps);
}
pose = BlendPose(SampleMotion(old->Motion, i, runtime.PreviousTime), pose,
runtime.TransitionDuration > 0.0F ? runtime.TransitionTime / runtime.TransitionDuration : 1.0F);
}
if (runtime.TransitionTime >= runtime.TransitionDuration) runtime.InTransition = false;
}
if (!transitioned) ClipEvents(id, state->Motion, i, sourcePrevious, runtime.Time, state->Loop, sourceWraps);
if (n == 0) finalPose = std::move(pose);
else {
MetaCoreAnimationPose reference;
const MetaCoreAnimationPose* referencePtr = nullptr;
if (layer.BlendMode == MetaCoreAnimatorLayerBlendMode::Additive && layer.AdditiveReferenceClipGuid.IsValid()) {
if (const auto* clip = GetClip(layer.AdditiveReferenceClipGuid)) {
reference = MetaCoreSampleAnimationClip(*clip, layer.AdditiveReferenceTime);
referencePtr = &reference;
}
}
ApplyLayer(finalPose, pose, Clamp01(layer.Weight), layer.BlendMode, layer.MaskBonePaths, referencePtr);
}
const float activeDuration = MotionDuration(state->Motion, i);
i.Debug.Layers.push_back({layer.Id, runtime.State, runtime.InTransition ? runtime.Previous : "",
activeDuration > 0.0F ? runtime.Time / activeDuration : 0.0F,
runtime.InTransition ? Clamp01(runtime.TransitionTime / runtime.TransitionDuration) : 0.0F, layer.Weight});
}
if (component.AvatarAssetGuid.IsValid() && Avatars) {
if (const auto avatar = Avatars(component.AvatarAssetGuid)) {finalPose = RetargetPose(finalPose, *avatar);if(baseLayerWraps>0){cycleStartPose=RetargetPose(cycleStartPose,*avatar);cycleEndPose=RetargetPose(cycleEndPose,*avatar);}}
}
// Publish a transient pose for render sync without changing scene revision.
MetaCoreAnimationRuntimePoseComponent runtimePose;
runtimePose.Nodes.reserve(finalPose.Nodes.size());
for (const auto& [path, value] : finalPose.Nodes) runtimePose.Nodes.push_back({path, 0, value.Translation, value.Rotation, value.Scale});
auto& registry = Scene.GetRegistry();
const auto entity = object.GetEntityHandle();
if (registry.any_of<MetaCoreAnimationRuntimePoseComponent>(entity)) registry.replace<MetaCoreAnimationRuntimePoseComponent>(entity, std::move(runtimePose));
else registry.emplace<MetaCoreAnimationRuntimePoseComponent>(entity, std::move(runtimePose));
// Extract root delta after the final blended pose.
i.RootMotion={};if(component.RootMotionMode!=MetaCoreRootMotionMode::Ignore&&!finalPose.Nodes.empty()){const std::string root=component.AvatarAssetGuid.IsValid()&&Avatars?Avatars(component.AvatarAssetGuid).value_or(MetaCoreAvatarDocument{}).RootBonePath:"";if(!root.empty()){const auto current=finalPose.Nodes.find(root);const auto previous=i.Pose.Nodes.find(root);if(current!=finalPose.Nodes.end()&&previous!=i.Pose.Nodes.end()){glm::vec3 translationDelta=current->second.Translation-previous->second.Translation;glm::quat rotationDelta=SafeNormalize(glm::inverse(previous->second.Rotation)*current->second.Rotation);const auto cycleStart=cycleStartPose.Nodes.find(root),cycleEnd=cycleEndPose.Nodes.find(root);if(baseLayerWraps>0&&cycleStart!=cycleStartPose.Nodes.end()&&cycleEnd!=cycleEndPose.Nodes.end()){translationDelta+=(cycleEnd->second.Translation-cycleStart->second.Translation)*static_cast<float>(baseLayerWraps);const auto cycleRotation=SafeNormalize(glm::inverse(cycleStart->second.Rotation)*cycleEnd->second.Rotation);for(int wrap=0;wrap<baseLayerWraps;++wrap)rotationDelta=SafeNormalize(cycleRotation*rotationDelta);}i.RootMotion.Translation=translationDelta*component.RootMotionTranslationMask;const glm::vec3 rotationDeltaEuler=glm::eulerAngles(rotationDelta)*component.RootMotionRotationMask;i.RootMotion.Rotation=SafeNormalize(glm::quat(rotationDeltaEuler));const bool dynamicConflict=object.HasComponent<MetaCoreRigidBodyComponent>()&&object.GetComponent<MetaCoreRigidBodyComponent>().BodyType==MetaCoreRigidBodyType::Dynamic;if(dynamicConflict){Emit(id,"AnimationError","RootMotionDynamicRigidBodyConflict");}else if(component.RootMotionMode==MetaCoreRootMotionMode::ApplyTransform){auto&t=object.GetComponent<MetaCoreTransformComponent>();t.Position+=i.RootMotion.Translation;t.RotationEulerDegrees+=glm::degrees(rotationDeltaEuler);}else if(component.RootMotionMode==MetaCoreRootMotionMode::CharacterController&&fixed&&Physics){(void)Physics->CharacterMove(id,i.RootMotion.Translation);}}}}i.Pose=std::move(finalPose);i.Debug.Playing=true;i.Debug.LastRootMotion=i.RootMotion;}
MetaCoreId Binding(const MetaCoreTimelineDirectorComponent&c,std::string_view slot){const auto it=std::find_if(c.Bindings.begin(),c.Bindings.end(),[&](const auto&b){return b.Slot==slot;});return it==c.Bindings.end()?0:it->ObjectId;}
MetaCoreAnimationTransform TimelineTransform(const std::vector<MetaCoreTimelineKeyDocument>&keys,float time){if(keys.empty())return{};const auto[a,b]=KeyInterval(keys,time);if(a==b||keys[a].Interpolation==MetaCoreTimelineInterpolation::Step)return keys[a].Transform;float t=KeyAlpha(time,keys[a].Time,keys[b].Time);if(keys[a].Interpolation==MetaCoreTimelineInterpolation::CubicHermite){const float t2=t*t,t3=t2*t,dt=std::max(0.F,keys[b].Time-keys[a].Time);t=Clamp01((-2.F*t3+3.F*t2)+(t3-2.F*t2+t)*keys[a].OutTangent*dt+(t3-t2)*keys[b].InTangent*dt);}return BlendTransform(keys[a].Transform,keys[b].Transform,t);}
void EvaluateTimelines(double delta, bool postAnimation){for(auto&[id,r]:Directors){auto director=Scene.FindGameObject(id);if(!director||!director.HasComponent<MetaCoreTimelineDirectorComponent>()||(!r.Playing&&!postAnimation))continue;const auto&c=director.GetComponent<MetaCoreTimelineDirectorComponent>();bool wrapped=false;if(!postAnimation){r.PreviousTime=r.Time;r.Time+=static_cast<float>(delta)*r.Speed;if(r.Timeline.Duration>0&&r.Time>r.Timeline.Duration){if(c.Loop){r.Time=std::fmod(r.Time,r.Timeline.Duration);wrapped=true;}else{r.Time=r.Timeline.Duration;r.Playing=false;}}}for(const auto&t:r.Timeline.Tracks){const bool transformTrack=t.Type==MetaCoreTimelineTrackType::Transform;if(transformTrack!=postAnimation)continue;const MetaCoreId targetId=Binding(c,t.BindingSlot);auto target=Scene.FindGameObject(targetId);if(t.Type==MetaCoreTimelineTrackType::Signal){for(const auto&k:t.Keys)if((!wrapped&&k.Time>r.PreviousTime&&k.Time<=r.Time)||(wrapped&&(k.Time>r.PreviousTime||k.Time<=r.Time)))Emit(targetId,"TimelineSignal",k.TextValue);continue;}if(!target)continue;if(transformTrack&&target.HasComponent<MetaCoreTransformComponent>()){const bool rootMotionConflict=target.HasComponent<MetaCoreAnimatorComponent>()&&target.GetComponent<MetaCoreAnimatorComponent>().RootMotionMode!=MetaCoreRootMotionMode::Ignore&&target.GetComponent<MetaCoreAnimatorComponent>().RootMotionMode!=MetaCoreRootMotionMode::ExtractOnly;if(rootMotionConflict){Emit(targetId,"AnimationError","TimelineRootMotionTransformConflict");continue;}const auto v=TimelineTransform(t.Keys,r.Time);auto&x=target.GetComponent<MetaCoreTransformComponent>();x.Position=v.Translation;x.RotationEulerDegrees=glm::degrees(glm::eulerAngles(v.Rotation));x.Scale=v.Scale;}else if(t.Type==MetaCoreTimelineTrackType::Activation){const auto interval=KeyInterval(t.Keys,r.Time);if(!t.Keys.empty()&&target.HasComponent<MetaCoreActiveComponent>())target.GetComponent<MetaCoreActiveComponent>().Active=t.Keys[interval.first].BoolValue;}else if(t.Type==MetaCoreTimelineTrackType::Animator&&!t.Keys.empty()){const auto interval=KeyInterval(t.Keys,r.Time);if(t.AnimatorParameter.empty())Play(targetId,t.Keys[interval.first].TextValue);else SetFloat(targetId,t.AnimatorParameter,t.Keys[interval.first].Transform.Translation.x);}}}}
bool Play(MetaCoreId id,std::string_view state,float normalized=0){auto it=Animators.find(id);if(it==Animators.end())return false;bool found=false;for(std::size_t n=0;n<it->second.Controller.Layers.size();++n)if(const auto*s=FindState(it->second.Controller.Layers[n],state)){it->second.Layers[n].State=s->Id;it->second.Layers[n].Time=MotionDuration(s->Motion,it->second)*Clamp01(normalized);it->second.Layers[n].InTransition=false;found=true;}if(found){it->second.Playing=true;it->second.Debug.Playing=true;}return found;}
bool Set(MetaCoreId id,std::string_view p,ParameterValue value){auto it=Animators.find(id);if(it==Animators.end())return false;const std::string key(p);const auto type=it->second.ParameterTypes.find(key);if(type==it->second.ParameterTypes.end())return false;const bool matches=(std::holds_alternative<bool>(value)&&type->second==MetaCoreAnimatorParameterType::Bool)||(std::holds_alternative<std::int64_t>(value)&&type->second==MetaCoreAnimatorParameterType::Int)||(std::holds_alternative<float>(value)&&type->second==MetaCoreAnimatorParameterType::Float);if(!matches)return false;it->second.Parameters[key]=std::move(value);return true;}
bool SetFloat(MetaCoreId id, std::string_view parameter, float value) { return Set(id, parameter, value); }
};
MetaCoreAnimationRuntime::MetaCoreAnimationRuntime(MetaCoreScene&s,MetaCoreAnimationClipProvider c,MetaCoreAnimatorControllerProvider co,MetaCoreAvatarProvider a,MetaCoreTimelineProvider t,MetaCorePhysicsWorld*p):Impl_(new Impl(s,std::move(c),std::move(co),std::move(a),std::move(t),p)){}
MetaCoreAnimationRuntime::~MetaCoreAnimationRuntime(){delete Impl_;}
void MetaCoreAnimationRuntime::Start(){if(Impl_->Running)return;Impl_->Running=true;Impl_->Prune();Impl_->Sync();}
void MetaCoreAnimationRuntime::Update(double d){if(!Impl_->Running)return;Impl_->Prune();Impl_->Sync();Impl_->EvaluateTimelines(d,false);for(auto&[id,i]:Impl_->Animators)Impl_->EvaluateAnimator(id,i,d,false);Impl_->EvaluateTimelines(0.0,true);}
void MetaCoreAnimationRuntime::FixedUpdate(double d){if(!Impl_->Running)return;Impl_->Prune();Impl_->Sync();for(auto&[id,i]:Impl_->Animators)Impl_->EvaluateAnimator(id,i,d,true);}
void MetaCoreAnimationRuntime::Stop(){Impl_->Running=false;for(auto object:Impl_->Scene.GetGameObjects())if(object.HasComponent<MetaCoreAnimationRuntimePoseComponent>())object.RemoveComponent<MetaCoreAnimationRuntimePoseComponent>();Impl_->Animators.clear();Impl_->Directors.clear();Impl_->DisabledAnimators.clear();Impl_->ClipCache.clear();}
void MetaCoreAnimationRuntime::SetEventCallback(MetaCoreAnimationEventCallback c){Impl_->Events=std::move(c);}
bool MetaCoreAnimationRuntime::SetBool(MetaCoreId id,std::string_view p,bool v){return Impl_->Set(id,p,v);}bool MetaCoreAnimationRuntime::SetInt(MetaCoreId id,std::string_view p,std::int64_t v){return Impl_->Set(id,p,v);}bool MetaCoreAnimationRuntime::SetFloat(MetaCoreId id,std::string_view p,float v){return Impl_->Set(id,p,std::isfinite(v)?v:0.F);}bool MetaCoreAnimationRuntime::SetTrigger(MetaCoreId id,std::string_view p){auto it=Impl_->Animators.find(id);if(it==Impl_->Animators.end())return false;const std::string key(p);const auto type=it->second.ParameterTypes.find(key);if(type==it->second.ParameterTypes.end()||type->second!=MetaCoreAnimatorParameterType::Trigger)return false;it->second.Triggers.insert(key);return true;}
bool MetaCoreAnimationRuntime::Play(MetaCoreId id,std::string_view s,float n){return Impl_->Play(id,s,n);}bool MetaCoreAnimationRuntime::CrossFade(MetaCoreId id,std::string_view s,float d,float n){auto it=Impl_->Animators.find(id);if(it==Impl_->Animators.end())return false;for(std::size_t x=0;x<it->second.Controller.Layers.size();++x)if(const auto*target=FindState(it->second.Controller.Layers[x],s)){auto&r=it->second.Layers[x];r.Previous=r.State;r.PreviousTime=r.Time;r.State=target->Id;r.Time=Impl_->MotionDuration(target->Motion,it->second)*Clamp01(n);r.TransitionTime=0;r.TransitionDuration=std::max(0.F,d);r.InTransition=r.TransitionDuration>0;it->second.Playing=true;it->second.Debug.Playing=true;return true;}return false;}
std::optional<MetaCoreAnimatorDebugSnapshot> MetaCoreAnimationRuntime::GetDebugSnapshot(MetaCoreId id)const{const auto it=Impl_->Animators.find(id);return it==Impl_->Animators.end()?std::nullopt:std::optional(it->second.Debug);}const MetaCoreAnimationPose* MetaCoreAnimationRuntime::GetPose(MetaCoreId id)const{const auto it=Impl_->Animators.find(id);return it==Impl_->Animators.end()?nullptr:&it->second.Pose;}std::optional<MetaCoreRootMotionDelta> MetaCoreAnimationRuntime::ConsumeRootMotion(MetaCoreId id){auto it=Impl_->Animators.find(id);if(it==Impl_->Animators.end())return std::nullopt;auto result=it->second.RootMotion;it->second.RootMotion={};return result;}
bool MetaCoreAnimationRuntime::TimelinePlay(MetaCoreId id){auto it=Impl_->Directors.find(id);if(it==Impl_->Directors.end())return false;it->second.Playing=true;return true;}bool MetaCoreAnimationRuntime::TimelinePause(MetaCoreId id){auto it=Impl_->Directors.find(id);if(it==Impl_->Directors.end())return false;it->second.Playing=false;return true;}bool MetaCoreAnimationRuntime::TimelineStop(MetaCoreId id){auto it=Impl_->Directors.find(id);if(it==Impl_->Directors.end())return false;it->second.Playing=false;it->second.Time=0;return true;}bool MetaCoreAnimationRuntime::TimelineSeek(MetaCoreId id,float s,bool emit){auto it=Impl_->Directors.find(id);if(it==Impl_->Directors.end())return false;it->second.PreviousTime=it->second.Time;it->second.Time=std::clamp(s,0.F,it->second.Timeline.Duration);if(emit){const auto director=Impl_->Scene.FindGameObject(id);if(director&&director.HasComponent<MetaCoreTimelineDirectorComponent>())for(const auto&track:it->second.Timeline.Tracks)if(track.Type==MetaCoreTimelineTrackType::Signal)for(const auto&key:track.Keys)if(key.Time>it->second.PreviousTime&&key.Time<=it->second.Time)Impl_->Emit(Impl_->Binding(director.GetComponent<MetaCoreTimelineDirectorComponent>(),track.BindingSlot),"TimelineSignal",key.TextValue);}return true;}bool MetaCoreAnimationRuntime::TimelineSetSpeed(MetaCoreId id,float s){auto it=Impl_->Directors.find(id);if(it==Impl_->Directors.end())return false;it->second.Speed=SafeSpeed(s);return true;}
} // namespace MetaCore

View File

@ -0,0 +1,338 @@
#pragma once
#include "MetaCoreFoundation/MetaCoreAssetGuid.h"
#include "MetaCoreFoundation/MetaCoreId.h"
#include <glm/gtc/quaternion.hpp>
#include <glm/vec2.hpp>
#include <glm/vec3.hpp>
#include <cstdint>
#include <filesystem>
#include <functional>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <variant>
#include <vector>
namespace MetaCore {
class MetaCoreScene;
class MetaCorePhysicsWorld;
// Version 2 stores Clip TRS and Avatar Bind Pose in MetaCore's Z-up basis.
// Version 1 used raw glTF Y-up data and must be regenerated from the source.
inline constexpr std::uint32_t MetaCoreAnimationClipVersion = 2U;
inline constexpr std::uint32_t MetaCoreAvatarVersion = 2U;
inline constexpr std::uint32_t MetaCoreAnimatorControllerVersion = 1U;
inline constexpr std::uint32_t MetaCoreTimelineVersion = 1U;
enum class MetaCoreAnimationInterpolation : std::uint8_t { Step = 0, Linear, CubicSpline };
enum class MetaCoreAnimationChannelTarget : std::uint8_t { Translation = 0, Rotation, Scale };
enum class MetaCoreAnimatorParameterType : std::uint8_t { Bool = 0, Int, Float, Trigger };
enum class MetaCoreAnimatorConditionMode : std::uint8_t {
If = 0, IfNot, Greater, Less, Equals, NotEqual
};
enum class MetaCoreAnimatorMotionType : std::uint8_t { Clip = 0, BlendTree1D, BlendTree2DCartesian, BlendTree2DDirectional };
enum class MetaCoreAnimatorLayerBlendMode : std::uint8_t { Override = 0, Additive };
enum class MetaCoreAnimationEventValueType : std::uint8_t { None = 0, Bool, Int, Float, String, Object, Asset };
enum class MetaCoreTimelineTrackType : std::uint8_t { Transform = 0, Animator, Activation, Signal };
enum class MetaCoreTimelineInterpolation : std::uint8_t { Step = 0, Linear, CubicHermite };
struct MetaCoreAnimationTransform {
glm::vec3 Translation{0.0F};
glm::quat Rotation{1.0F, 0.0F, 0.0F, 0.0F};
glm::vec3 Scale{1.0F};
};
struct MetaCoreAnimationVec3Key {
float Time = 0.0F;
glm::vec3 Value{0.0F};
glm::vec3 InTangent{0.0F};
glm::vec3 OutTangent{0.0F};
};
struct MetaCoreAnimationQuatKey {
float Time = 0.0F;
glm::quat Value{1.0F, 0.0F, 0.0F, 0.0F};
glm::quat InTangent{0.0F, 0.0F, 0.0F, 0.0F};
glm::quat OutTangent{0.0F, 0.0F, 0.0F, 0.0F};
};
struct MetaCoreAnimationChannelDocument {
std::string NodePath{};
MetaCoreAnimationChannelTarget Target = MetaCoreAnimationChannelTarget::Translation;
MetaCoreAnimationInterpolation Interpolation = MetaCoreAnimationInterpolation::Linear;
std::vector<MetaCoreAnimationVec3Key> Vec3Keys{};
std::vector<MetaCoreAnimationQuatKey> QuatKeys{};
};
struct MetaCoreAnimationEventDocument {
std::string StableId{};
float Time = 0.0F;
std::string Name{};
MetaCoreAnimationEventValueType ValueType = MetaCoreAnimationEventValueType::None;
bool BoolValue = false;
std::int64_t IntValue = 0;
double FloatValue = 0.0;
std::string TextValue{};
MetaCoreId ObjectValue = 0;
MetaCoreAssetGuid AssetValue{};
};
struct MetaCoreAnimationClipDocument {
std::uint32_t Version = MetaCoreAnimationClipVersion;
MetaCoreAssetGuid AssetGuid{};
MetaCoreAssetGuid SourceModelGuid{};
std::string StableImportKey{};
std::string Name{};
std::string SkeletonSignature{};
float Duration = 0.0F;
bool Loop = true;
std::string RootNodePath{};
std::vector<MetaCoreAnimationChannelDocument> Channels{};
std::vector<MetaCoreAnimationEventDocument> Events{};
};
struct MetaCoreAvatarBoneDocument {
std::string Path{};
std::int32_t Parent = -1;
MetaCoreAnimationTransform BindPose{};
bool Required = false;
};
struct MetaCoreAvatarMappingDocument {
std::string SourcePath{};
std::string TargetPath{};
};
struct MetaCoreAvatarDocument {
std::uint32_t Version = MetaCoreAvatarVersion;
MetaCoreAssetGuid AssetGuid{};
MetaCoreAssetGuid ModelGuid{};
std::string SkeletonSignature{};
std::string RootBonePath{};
float UniformScale = 1.0F;
std::vector<MetaCoreAvatarBoneDocument> Bones{};
std::vector<MetaCoreAvatarMappingDocument> Mappings{};
};
struct MetaCoreAnimatorParameterDocument {
std::string Id{};
MetaCoreAnimatorParameterType Type = MetaCoreAnimatorParameterType::Float;
bool BoolDefault = false;
std::int64_t IntDefault = 0;
float FloatDefault = 0.0F;
};
struct MetaCoreAnimatorBlendSampleDocument {
std::string StableId{};
MetaCoreAssetGuid ClipGuid{};
float Threshold = 0.0F;
glm::vec2 Position{0.0F};
float Speed = 1.0F;
};
struct MetaCoreAnimatorMotionDocument {
MetaCoreAnimatorMotionType Type = MetaCoreAnimatorMotionType::Clip;
MetaCoreAssetGuid ClipGuid{};
std::string ParameterX{};
std::string ParameterY{};
std::vector<MetaCoreAnimatorBlendSampleDocument> Samples{};
};
struct MetaCoreAnimatorStateDocument {
std::string Id{};
std::string Name{};
MetaCoreAnimatorMotionDocument Motion{};
float Speed = 1.0F;
bool Loop = true;
};
struct MetaCoreAnimatorConditionDocument {
std::string ParameterId{};
MetaCoreAnimatorConditionMode Mode = MetaCoreAnimatorConditionMode::If;
bool BoolValue = true;
std::int64_t IntValue = 0;
float FloatValue = 0.0F;
};
struct MetaCoreAnimatorTransitionDocument {
std::string Id{};
std::string SourceStateId{};
std::string TargetStateId{};
std::int32_t Priority = 0;
bool HasExitTime = false;
float ExitTime = 1.0F;
float Duration = 0.15F;
bool FixedDuration = true;
float TargetOffset = 0.0F;
bool CanInterrupt = true;
bool AllowSelfTransition = false;
std::vector<MetaCoreAnimatorConditionDocument> Conditions{};
};
struct MetaCoreAnimatorLayerDocument {
std::string Id{"Base"};
std::string Name{"Base Layer"};
MetaCoreAnimatorLayerBlendMode BlendMode = MetaCoreAnimatorLayerBlendMode::Override;
float Weight = 1.0F;
std::string DefaultStateId{};
MetaCoreAssetGuid AdditiveReferenceClipGuid{};
float AdditiveReferenceTime = 0.0F;
std::vector<std::string> MaskBonePaths{};
std::vector<MetaCoreAnimatorStateDocument> States{};
std::vector<MetaCoreAnimatorTransitionDocument> Transitions{};
};
struct MetaCoreAnimatorControllerDocument {
std::uint32_t Version = MetaCoreAnimatorControllerVersion;
MetaCoreAssetGuid AssetGuid{};
std::string Name{};
std::vector<MetaCoreAnimatorParameterDocument> Parameters{};
std::vector<MetaCoreAnimatorLayerDocument> Layers{};
};
struct MetaCoreTimelineKeyDocument {
float Time = 0.0F;
MetaCoreAnimationTransform Transform{};
bool BoolValue = false;
std::string TextValue{};
float InTangent = 0.0F;
float OutTangent = 0.0F;
MetaCoreTimelineInterpolation Interpolation = MetaCoreTimelineInterpolation::Linear;
};
struct MetaCoreTimelineTrackDocument {
std::string Id{};
std::string BindingSlot{};
MetaCoreTimelineTrackType Type = MetaCoreTimelineTrackType::Transform;
std::string AnimatorParameter{};
MetaCoreAssetGuid AnimationClipGuid{};
std::vector<MetaCoreTimelineKeyDocument> Keys{};
};
struct MetaCoreTimelineDocument {
std::uint32_t Version = MetaCoreTimelineVersion;
MetaCoreAssetGuid AssetGuid{};
std::string Name{};
float Duration = 0.0F;
std::vector<MetaCoreTimelineTrackDocument> Tracks{};
};
struct MetaCoreAnimationPose {
std::unordered_map<std::string, MetaCoreAnimationTransform> Nodes{};
};
struct MetaCoreRootMotionDelta {
glm::vec3 Translation{0.0F};
glm::quat Rotation{1.0F, 0.0F, 0.0F, 0.0F};
};
struct MetaCoreAnimationRuntimeEvent {
MetaCoreId ObjectId = 0;
std::string Type{};
std::string Name{};
MetaCoreAnimationEventDocument Value{};
};
struct MetaCoreAnimatorDebugLayer {
std::string LayerId{};
std::string StateId{};
std::string TargetStateId{};
float NormalizedTime = 0.0F;
float TransitionWeight = 0.0F;
float Weight = 1.0F;
};
struct MetaCoreAnimatorDebugSnapshot {
MetaCoreId ObjectId = 0;
bool Playing = false;
std::vector<MetaCoreAnimatorDebugLayer> Layers{};
MetaCoreRootMotionDelta LastRootMotion{};
std::vector<std::string> Diagnostics{};
};
using MetaCoreAnimationClipProvider = std::function<std::optional<MetaCoreAnimationClipDocument>(MetaCoreAssetGuid)>;
using MetaCoreAvatarProvider = std::function<std::optional<MetaCoreAvatarDocument>(MetaCoreAssetGuid)>;
using MetaCoreAnimatorControllerProvider = std::function<std::optional<MetaCoreAnimatorControllerDocument>(MetaCoreAssetGuid)>;
using MetaCoreTimelineProvider = std::function<std::optional<MetaCoreTimelineDocument>(MetaCoreAssetGuid)>;
using MetaCoreAnimationEventCallback = std::function<void(const MetaCoreAnimationRuntimeEvent&)>;
[[nodiscard]] glm::vec3 MetaCoreSampleAnimationVec3(const std::vector<MetaCoreAnimationVec3Key>& keys,
float time, MetaCoreAnimationInterpolation interpolation, glm::vec3 fallback = glm::vec3(0.0F));
[[nodiscard]] glm::quat MetaCoreSampleAnimationQuat(const std::vector<MetaCoreAnimationQuatKey>& keys,
float time, MetaCoreAnimationInterpolation interpolation, glm::quat fallback = glm::quat(1.0F, 0.0F, 0.0F, 0.0F));
[[nodiscard]] MetaCoreAnimationPose MetaCoreSampleAnimationClip(const MetaCoreAnimationClipDocument& clip, float time);
[[nodiscard]] bool MetaCoreValidateAnimationClip(const MetaCoreAnimationClipDocument& document, std::vector<std::string>* issues = nullptr);
[[nodiscard]] bool MetaCoreValidateAvatar(const MetaCoreAvatarDocument& document, std::vector<std::string>* issues = nullptr);
[[nodiscard]] bool MetaCoreValidateAnimatorController(const MetaCoreAnimatorControllerDocument& document, std::vector<std::string>* issues = nullptr);
[[nodiscard]] bool MetaCoreValidateTimeline(const MetaCoreTimelineDocument& document, std::vector<std::string>* issues = nullptr);
[[nodiscard]] bool MetaCoreWriteAnimationClip(const std::filesystem::path& path, const MetaCoreAnimationClipDocument& document);
[[nodiscard]] std::optional<MetaCoreAnimationClipDocument> MetaCoreReadAnimationClip(const std::filesystem::path& path);
[[nodiscard]] bool MetaCoreWriteAvatar(const std::filesystem::path& path, const MetaCoreAvatarDocument& document);
[[nodiscard]] std::optional<MetaCoreAvatarDocument> MetaCoreReadAvatar(const std::filesystem::path& path);
[[nodiscard]] bool MetaCoreWriteAnimatorController(const std::filesystem::path& path, const MetaCoreAnimatorControllerDocument& document);
[[nodiscard]] std::optional<MetaCoreAnimatorControllerDocument> MetaCoreReadAnimatorController(const std::filesystem::path& path);
[[nodiscard]] bool MetaCoreWriteTimeline(const std::filesystem::path& path, const MetaCoreTimelineDocument& document);
[[nodiscard]] std::optional<MetaCoreTimelineDocument> MetaCoreReadTimeline(const std::filesystem::path& path);
[[nodiscard]] MetaCoreAssetGuid MetaCoreBuildDeterministicAnimationGuid(std::string_view key);
class MetaCoreAnimationRuntime {
public:
MetaCoreAnimationRuntime(MetaCoreScene& scene, MetaCoreAnimationClipProvider clips,
MetaCoreAnimatorControllerProvider controllers, MetaCoreAvatarProvider avatars = {},
MetaCoreTimelineProvider timelines = {}, MetaCorePhysicsWorld* physics = nullptr);
~MetaCoreAnimationRuntime();
MetaCoreAnimationRuntime(const MetaCoreAnimationRuntime&) = delete;
MetaCoreAnimationRuntime& operator=(const MetaCoreAnimationRuntime&) = delete;
void Start();
void Update(double deltaSeconds);
void FixedUpdate(double deltaSeconds);
void Stop();
void SetEventCallback(MetaCoreAnimationEventCallback callback);
[[nodiscard]] bool SetBool(MetaCoreId objectId, std::string_view parameter, bool value);
[[nodiscard]] bool SetInt(MetaCoreId objectId, std::string_view parameter, std::int64_t value);
[[nodiscard]] bool SetFloat(MetaCoreId objectId, std::string_view parameter, float value);
[[nodiscard]] bool SetTrigger(MetaCoreId objectId, std::string_view parameter);
[[nodiscard]] bool Play(MetaCoreId objectId, std::string_view stateId, float normalizedTime = 0.0F);
[[nodiscard]] bool CrossFade(MetaCoreId objectId, std::string_view stateId, float duration, float normalizedTime = 0.0F);
[[nodiscard]] std::optional<MetaCoreAnimatorDebugSnapshot> GetDebugSnapshot(MetaCoreId objectId) const;
[[nodiscard]] const MetaCoreAnimationPose* GetPose(MetaCoreId objectId) const;
[[nodiscard]] std::optional<MetaCoreRootMotionDelta> ConsumeRootMotion(MetaCoreId objectId);
[[nodiscard]] bool TimelinePlay(MetaCoreId directorId);
[[nodiscard]] bool TimelinePause(MetaCoreId directorId);
[[nodiscard]] bool TimelineStop(MetaCoreId directorId);
[[nodiscard]] bool TimelineSeek(MetaCoreId directorId, float seconds, bool emitSignals = false);
[[nodiscard]] bool TimelineSetSpeed(MetaCoreId directorId, float speed);
private:
class Impl;
Impl* Impl_ = nullptr;
};
// GUID-indexed file provider shared by Editor, Player and Cook validation.
class MetaCoreAnimationAssetLibrary {
public:
explicit MetaCoreAnimationAssetLibrary(std::filesystem::path root = {});
void Scan(const std::filesystem::path& root);
[[nodiscard]] std::optional<MetaCoreAnimationClipDocument> LoadClip(MetaCoreAssetGuid guid) const;
[[nodiscard]] std::optional<MetaCoreAvatarDocument> LoadAvatar(MetaCoreAssetGuid guid) const;
[[nodiscard]] std::optional<MetaCoreAnimatorControllerDocument> LoadController(MetaCoreAssetGuid guid) const;
[[nodiscard]] std::optional<MetaCoreTimelineDocument> LoadTimeline(MetaCoreAssetGuid guid) const;
[[nodiscard]] std::optional<MetaCoreAnimationClipDocument> FindClip(MetaCoreAssetGuid sourceModelGuid, std::string_view nameOrStableKey) const;
[[nodiscard]] std::optional<MetaCoreAvatarDocument> FindAvatar(MetaCoreAssetGuid modelGuid) const;
private:
std::unordered_map<MetaCoreAssetGuid, std::filesystem::path, MetaCoreAssetGuidHasher> Clips_{};
std::unordered_map<MetaCoreAssetGuid, std::filesystem::path, MetaCoreAssetGuidHasher> Avatars_{};
std::unordered_map<MetaCoreAssetGuid, std::filesystem::path, MetaCoreAssetGuidHasher> Controllers_{};
std::unordered_map<MetaCoreAssetGuid, std::filesystem::path, MetaCoreAssetGuidHasher> Timelines_{};
};
} // namespace MetaCore

View File

@ -8,6 +8,7 @@
#include "MetaCoreFoundation/MetaCoreGeneratedReflection.h"
#include "MetaCoreRuntimeInput/MetaCoreInputMap.h"
#include "MetaCorePhysics/MetaCorePhysics.h"
#include "MetaCoreAnimation/MetaCoreAnimation.h"
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h"
#include "MetaCoreRendering/MetaCoreRendering.h"
#include "MetaCoreScene/MetaCoreScenePackage.h"
@ -36,6 +37,52 @@
namespace MetaCore {
namespace {
std::optional<std::string> MetaCoreValidateSceneAnimationForCook(const MetaCoreSceneDocument& scene, const std::filesystem::path& assetsRoot) {
MetaCoreAnimationAssetLibrary library(assetsRoot);
for (const auto& object : scene.GameObjects) {
if (object.Animator) {
const auto& animator = *object.Animator;
if (!animator.ControllerAssetGuid.IsValid()) return "Animator has not been migrated to a Controller asset: object " + std::to_string(object.Id);
const auto controller = library.LoadController(animator.ControllerAssetGuid);
if (!controller) return "Animator Controller is missing, corrupt, or version-incompatible: " + animator.ControllerAssetGuid.ToString();
for (const auto& layer : controller->Layers) for (const auto& state : layer.States) {
if (state.Motion.Type == MetaCoreAnimatorMotionType::Clip && !library.LoadClip(state.Motion.ClipGuid)) return "Animation Clip is missing: " + state.Motion.ClipGuid.ToString();
for (const auto& sample : state.Motion.Samples) if (!library.LoadClip(sample.ClipGuid)) return "Blend Tree Clip is missing: " + sample.ClipGuid.ToString();
}
for (const auto& layer : controller->Layers) if (layer.AdditiveReferenceClipGuid.IsValid() && !library.LoadClip(layer.AdditiveReferenceClipGuid))
return "Additive reference Clip is missing: " + layer.AdditiveReferenceClipGuid.ToString();
const auto avatar = animator.AvatarAssetGuid.IsValid() ? library.LoadAvatar(animator.AvatarAssetGuid) : std::nullopt;
if (animator.AvatarAssetGuid.IsValid() && !avatar) return "Animation Avatar is missing: " + animator.AvatarAssetGuid.ToString();
if (avatar) for (const auto& layer : controller->Layers) for (const auto& state : layer.States) {
std::vector<MetaCoreAssetGuid> clipGuids;
if (state.Motion.Type == MetaCoreAnimatorMotionType::Clip) clipGuids.push_back(state.Motion.ClipGuid);
else for (const auto& sample : state.Motion.Samples) clipGuids.push_back(sample.ClipGuid);
for (const auto guid : clipGuids) if (const auto clip = library.LoadClip(guid); clip && !clip->SkeletonSignature.empty() && !avatar->SkeletonSignature.empty() && clip->SkeletonSignature != avatar->SkeletonSignature && !avatar->Mappings.empty())
return "Animation skeleton signature mismatch: clip " + guid.ToString() + " avatar " + animator.AvatarAssetGuid.ToString();
}
if (animator.RootMotionMode == MetaCoreRootMotionMode::CharacterController && animator.UpdateMode != MetaCoreAnimatorUpdateMode::Fixed)
return "CharacterController Root Motion requires Fixed update mode: object " + std::to_string(object.Id);
if (animator.RootMotionMode != MetaCoreRootMotionMode::Ignore && object.RigidBody && object.RigidBody->BodyType == MetaCoreRigidBodyType::Dynamic)
return "Root Motion cannot drive a Dynamic RigidBody: object " + std::to_string(object.Id);
}
if (object.TimelineDirector && object.TimelineDirector->TimelineAssetGuid.IsValid()) {
const auto timeline = library.LoadTimeline(object.TimelineDirector->TimelineAssetGuid);
if (!timeline) return "Timeline asset is missing, corrupt, or version-incompatible: " + object.TimelineDirector->TimelineAssetGuid.ToString();
for (const auto& track : timeline->Tracks) {
if (track.AnimationClipGuid.IsValid() && !library.LoadClip(track.AnimationClipGuid)) return "Timeline Clip is missing: " + track.AnimationClipGuid.ToString();
if (track.Type != MetaCoreTimelineTrackType::Transform) continue;
const auto binding = std::find_if(object.TimelineDirector->Bindings.begin(), object.TimelineDirector->Bindings.end(), [&](const auto& value){ return value.Slot == track.BindingSlot; });
if (binding == object.TimelineDirector->Bindings.end()) return "Timeline binding is missing: slot " + track.BindingSlot;
const auto target = std::find_if(scene.GameObjects.begin(), scene.GameObjects.end(), [&](const auto& value){ return value.Id == binding->ObjectId; });
if (target == scene.GameObjects.end()) return "Timeline binding target is missing: object " + std::to_string(binding->ObjectId);
if (target->Animator && target->Animator->RootMotionMode != MetaCoreRootMotionMode::Ignore && target->Animator->RootMotionMode != MetaCoreRootMotionMode::ExtractOnly)
return "Timeline Transform track conflicts with applied Root Motion: object " + std::to_string(binding->ObjectId);
}
}
}
return std::nullopt;
}
using json = nlohmann::json;
[[nodiscard]] bool CopyTree(const std::filesystem::path& source, const std::filesystem::path& target);
@ -786,27 +833,43 @@ MetaCoreBuildResult MetaCoreBuildPipeline::Run(const MetaCoreBuildRequest& reque
MetaCoreRegisterRuntimeInputGeneratedTypes(inputRegistry);
MetaCoreRegisterPhysicsGeneratedTypes(inputRegistry);
const auto runtimeProjectPath = request.ProjectRoot / project->RuntimeDirectory / "ProjectRuntime.mcruntimecfg";
const auto runtimeProject = MetaCoreReadRuntimeProjectDocument(runtimeProjectPath, inputRegistry);
auto runtimeProject = MetaCoreReadRuntimeProjectDocument(runtimeProjectPath, inputRegistry);
if (!runtimeProject) return fail(MetaCoreBuildErrorCode::CookFailed, "Runtime project configuration is missing or unreadable", runtimeProjectPath);
if (runtimeProject->InputMapPath.empty() || !IsSafeRelativePath(runtimeProject->InputMapPath))
return fail(MetaCoreBuildErrorCode::UnsafePath, "Runtime input map path is empty or unsafe", runtimeProjectPath);
const auto inputMapPath = request.ProjectRoot / runtimeProject->InputMapPath;
const auto inputMap = MetaCoreReadInputMap(inputMapPath, inputRegistry);
// Runtime project documents created before the input-map field was added do
// not contain InputMapPath. Editor and Player already use this canonical
// default, so Cook must make the same version-compatible choice instead of
// misclassifying a valid legacy document as an unsafe path.
const bool legacyInputMapPath = runtimeProject->InputMapPath.empty();
const auto inputMapRelativePath = legacyInputMapPath
? std::filesystem::path("Runtime/Input.mcruntime")
: runtimeProject->InputMapPath;
runtimeProject->InputMapPath = inputMapRelativePath;
if (!IsSafeRelativePath(inputMapRelativePath))
return fail(MetaCoreBuildErrorCode::UnsafePath, "Runtime input map path is unsafe", inputMapRelativePath);
const auto inputMapPath = request.ProjectRoot / inputMapRelativePath;
auto inputMap = MetaCoreReadInputMap(inputMapPath, inputRegistry);
if (!inputMap && legacyInputMapPath) inputMap = MetaCoreBuildDefaultInputMap();
if (!inputMap) return fail(MetaCoreBuildErrorCode::CookFailed, "Runtime input map is missing or unreadable", inputMapPath);
const auto inputIssues = MetaCoreValidateInputMap(*inputMap);
if (std::any_of(inputIssues.begin(), inputIssues.end(), [](const auto& issue) { return issue.Error; }))
return fail(MetaCoreBuildErrorCode::CookFailed, "Runtime input map validation failed", inputMapPath);
const auto physicsRelativePath = runtimeProject->PhysicsSettingsPath.empty() ? std::filesystem::path("Runtime/Physics.mcruntime") : runtimeProject->PhysicsSettingsPath;
const bool legacyPhysicsPath = runtimeProject->PhysicsSettingsPath.empty();
const auto physicsRelativePath = legacyPhysicsPath ? std::filesystem::path("Runtime/Physics.mcruntime") : runtimeProject->PhysicsSettingsPath;
runtimeProject->PhysicsSettingsPath = physicsRelativePath;
if (!IsSafeRelativePath(physicsRelativePath)) return fail(MetaCoreBuildErrorCode::UnsafePath, "Runtime physics settings path is unsafe", physicsRelativePath);
const auto physicsPath = request.ProjectRoot / physicsRelativePath;
auto physicsSettings = MetaCoreReadPhysicsSettings(physicsPath, inputRegistry);
if (!physicsSettings && legacyPhysicsPath) physicsSettings = MetaCoreBuildDefaultPhysicsSettings();
if (!physicsSettings) return fail(MetaCoreBuildErrorCode::CookFailed, "Runtime physics settings are missing or unreadable", physicsPath);
std::vector<std::string> physicsIssues;
if (!MetaCoreValidatePhysicsSettings(*physicsSettings, &physicsIssues)) return fail(MetaCoreBuildErrorCode::CookFailed, "Runtime physics settings validation failed", physicsPath);
const auto renderingRelativePath = runtimeProject->RenderingSettingsPath.empty() ? std::filesystem::path("Runtime/Rendering.mcruntime") : runtimeProject->RenderingSettingsPath;
const bool legacyRenderingPath = runtimeProject->RenderingSettingsPath.empty();
const auto renderingRelativePath = legacyRenderingPath ? std::filesystem::path("Runtime/Rendering.mcruntime") : runtimeProject->RenderingSettingsPath;
runtimeProject->RenderingSettingsPath = renderingRelativePath;
if (!IsSafeRelativePath(renderingRelativePath)) return fail(MetaCoreBuildErrorCode::UnsafePath, "Runtime rendering settings path is unsafe", renderingRelativePath);
const auto renderingPath = request.ProjectRoot / renderingRelativePath;
auto renderingSettings = MetaCoreReadRenderSettings(renderingPath, inputRegistry);
if (!renderingSettings && legacyRenderingPath) renderingSettings = MetaCoreBuildDefaultRenderSettings();
if (!renderingSettings) return fail(MetaCoreBuildErrorCode::CookFailed, "Runtime rendering settings are missing, unreadable, or newer than this engine", renderingPath);
std::vector<std::string> renderingIssues;
if (!MetaCoreValidateRenderSettings(*renderingSettings, &renderingIssues)) return fail(MetaCoreBuildErrorCode::CookFailed, "Runtime rendering settings validation failed", renderingPath);
@ -823,15 +886,19 @@ MetaCoreBuildResult MetaCoreBuildPipeline::Run(const MetaCoreBuildRequest& reque
if (startupSource.extension() == ".json") {
const auto registry = MetaCoreBuildScenePackageTypeRegistry();
const auto scene = MetaCoreSceneSerializer::LoadSceneFromJson(absoluteStartup, registry);
if (scene) if (const auto issue = MetaCoreValidateScenePhysicsForCook(*scene, cookablePhysicsMeshes))
return fail(MetaCoreBuildErrorCode::CookFailed, *issue, absoluteStartup);
if (scene) {
if (const auto issue = MetaCoreValidateScenePhysicsForCook(*scene, cookablePhysicsMeshes)) return fail(MetaCoreBuildErrorCode::CookFailed, *issue, absoluteStartup);
if (const auto issue = MetaCoreValidateSceneAnimationForCook(*scene, request.ProjectRoot / "Assets")) return fail(MetaCoreBuildErrorCode::CookFailed, *issue, absoluteStartup);
}
if (!scene || !MetaCoreWriteScenePackage(cookedScene, *scene)) return fail(MetaCoreBuildErrorCode::CookFailed, "Failed to cook startup scene", absoluteStartup);
startupSceneDocument = *scene;
} else {
if (const auto scene = MetaCoreReadScenePackage(absoluteStartup); scene.has_value()) {
if (const auto issue = MetaCoreValidateScenePhysicsForCook(*scene, cookablePhysicsMeshes))
return fail(MetaCoreBuildErrorCode::CookFailed, *issue, absoluteStartup);
else startupSceneDocument = *scene;
if (const auto issue = MetaCoreValidateSceneAnimationForCook(*scene, request.ProjectRoot / "Assets"))
return fail(MetaCoreBuildErrorCode::CookFailed, *issue, absoluteStartup);
startupSceneDocument = *scene;
}
std::filesystem::copy_file(absoluteStartup, cookedScene, std::filesystem::copy_options::overwrite_existing, error);
if (error) return fail(MetaCoreBuildErrorCode::CookFailed, "Failed to copy startup scene", absoluteStartup);
@ -892,6 +959,15 @@ MetaCoreBuildResult MetaCoreBuildPipeline::Run(const MetaCoreBuildRequest& reque
if (std::filesystem::is_directory(request.ProjectRoot / project->RuntimeDirectory) &&
!CopyTree(request.ProjectRoot / project->RuntimeDirectory, staging / "Content" / "Runtime"))
return fail(MetaCoreBuildErrorCode::CookFailed, "Failed to collect project runtime configuration", request.ProjectRoot / project->RuntimeDirectory);
// Cook upgrades legacy runtime configuration only inside the package. An
// explicitly configured missing file still fails above; absent fields from
// older documents receive the same defaults as Editor and Player.
const auto stagedRuntimeRoot = staging / "Content" / "Runtime";
if (!MetaCoreWriteRuntimeProjectDocument(stagedRuntimeRoot / "ProjectRuntime.mcruntimecfg", *runtimeProject, inputRegistry) ||
!MetaCoreWriteInputMap(staging / "Content" / inputMapRelativePath, *inputMap, inputRegistry) ||
!MetaCoreWritePhysicsSettings(staging / "Content" / physicsRelativePath, *physicsSettings, inputRegistry) ||
!MetaCoreWriteRenderSettings(staging / "Content" / renderingRelativePath, *renderingSettings, inputRegistry))
return fail(MetaCoreBuildErrorCode::CookFailed, "Failed to stage migrated runtime configuration", stagedRuntimeRoot);
if (cancelled()) { std::filesystem::remove_all(staging, error); return fail(MetaCoreBuildErrorCode::Cancelled, "Build cancelled"); }
report(MetaCoreBuildStage::Stage, "Staging executable and runtime dependencies");

View File

@ -12,6 +12,7 @@
#include "MetaCoreFoundation/MetaCoreProject.h"
#include "MetaCorePlatform/MetaCoreInput.h"
#include "MetaCorePhysics/MetaCorePhysics.h"
#include "MetaCoreAnimation/MetaCoreAnimation.h"
#include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h"
#include "MetaCoreRuntimeInput/MetaCoreRuntimeInteraction.h"
#include "MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h"
@ -3011,7 +3012,24 @@ void MetaCoreDrawAnimatorComponentInspector(MetaCoreEditorContext& editorContext
auto& animator = gameObject.GetComponent<MetaCoreAnimatorComponent>();
const auto modelDocument = MetaCoreLoadAnimatorSourceModelDocument(editorContext, animator);
std::string sourceLabel = animator.SourceModelPath.empty() ? "<none>" : animator.SourceModelPath;
ImGui::TextWrapped("动画控制器: %s", animator.ControllerAssetGuid.IsValid() ? animator.ControllerAssetGuid.ToString().c_str() : "<旧版,播放时迁移>");
ImGui::TextWrapped("骨架配置: %s", animator.AvatarAssetGuid.IsValid() ? animator.AvatarAssetGuid.ToString().c_str() : "<无>");
int updateMode = static_cast<int>(animator.UpdateMode);
const char* updateModes[] = {"普通更新", "固定更新", "不受时间缩放"};
if (ImGui::Combo("更新模式", &updateMode, updateModes, 3)) { animator.UpdateMode=static_cast<MetaCoreAnimatorUpdateMode>(updateMode);editorContext.GetScene().IncrementRevision(); }
int rootMode = static_cast<int>(animator.RootMotionMode);
const char* rootModes[] = {"忽略", "仅提取", "应用到变换", "角色控制器"};
if (ImGui::Combo("根运动", &rootMode, rootModes, 4)) { animator.RootMotionMode=static_cast<MetaCoreRootMotionMode>(rootMode);editorContext.GetScene().IncrementRevision(); }
if (animator.RootMotionMode != MetaCoreRootMotionMode::Ignore) {
ImGui::DragFloat3("位移轴遮罩", &animator.RootMotionTranslationMask.x, 0.05F, 0.0F, 1.0F);
ImGui::DragFloat3("旋转轴遮罩", &animator.RootMotionRotationMask.x, 0.05F, 0.0F, 1.0F);
}
if (auto* runtime = editorContext.GetActiveAnimationRuntime()) if (auto debug = runtime->GetDebugSnapshot(gameObject.GetId())) {
ImGui::SeparatorText("运行时状态");
for (const auto& layer : debug->Layers) ImGui::Text("%s: %s 归一化时间=%.3f 过渡=%.2f", layer.LayerId.c_str(), layer.StateId.c_str(), layer.NormalizedTime, layer.TransitionWeight);
}
std::string sourceLabel = animator.SourceModelPath.empty() ? "<无>" : animator.SourceModelPath;
if (animator.SourceModelAssetGuid.IsValid()) {
sourceLabel += " [" + animator.SourceModelAssetGuid.ToString() + "]";
}
@ -3037,7 +3055,7 @@ void MetaCoreDrawAnimatorComponentInspector(MetaCoreEditorContext& editorContext
ImGui::TextDisabled("%s: 混合值", label);
}
if (ImGui::Checkbox(label, &value)) {
(void)editorContext.ExecuteSnapshotCommand("修改 Animator", [&]() {
(void)editorContext.ExecuteSnapshotCommand("修改动画器", [&]() {
MetaCoreApplyValueToSelectedObjects(
editorContext,
[member](MetaCoreGameObject& selectedObject) -> bool* {
@ -3078,7 +3096,7 @@ void MetaCoreDrawAnimatorComponentInspector(MetaCoreEditorContext& editorContext
if (ImGui::Selectable(label.c_str(), selected)) {
const auto newClipIndex = static_cast<std::int32_t>(index);
const std::string newClipName = clip.Name;
(void)editorContext.ExecuteSnapshotCommand("修改 Animator 动画片段", [&]() {
(void)editorContext.ExecuteSnapshotCommand("修改动画片段", [&]() {
MetaCoreForEachSelectedGameObject(editorContext, [&](MetaCoreGameObject& selectedObject) {
if (!selectedObject.HasComponent<MetaCoreAnimatorComponent>()) {
return;
@ -3112,7 +3130,7 @@ void MetaCoreDrawAnimatorComponentInspector(MetaCoreEditorContext& editorContext
[](float lhs, float rhs) { return MetaCoreNearlyEqual(lhs, rhs); }
).value_or(animator.Speed);
ImGui::DragFloat("速度", &speed, 0.05F, 0.0F, 8.0F);
MetaCoreTrackComponentInspectorEdit(editorContext, "修改 Animator", true);
MetaCoreTrackComponentInspectorEdit(editorContext, "修改动画器", true);
if (ImGui::IsItemEdited()) {
MetaCoreApplyValueToSelectedObjects(
editorContext,
@ -3181,6 +3199,21 @@ void MetaCoreDrawAnimatorComponentInspector(MetaCoreEditorContext& editorContext
}
}
void MetaCoreDrawTimelineDirectorInspector(MetaCoreEditorContext& editorContext, MetaCoreGameObject& object) {
auto& director = object.GetComponent<MetaCoreTimelineDirectorComponent>();
ImGui::Checkbox("启用##Timeline", &director.Enabled);
ImGui::TextWrapped("时间线: %s", director.TimelineAssetGuid.IsValid() ? director.TimelineAssetGuid.ToString().c_str() : "<无>");
ImGui::Checkbox("开始时播放##Timeline", &director.PlayOnStart);
ImGui::Checkbox("循环##Timeline", &director.Loop);
ImGui::DragFloat("速度##Timeline", &director.Speed, 0.05F, 0.0F, 100.0F);
ImGui::Text("绑定槽: %d", static_cast<int>(director.Bindings.size()));
if (auto* runtime=editorContext.GetActiveAnimationRuntime()) {
if(ImGui::Button("播放##Timeline")) (void)runtime->TimelinePlay(object.GetId()); ImGui::SameLine();
if(ImGui::Button("暂停##Timeline")) (void)runtime->TimelinePause(object.GetId()); ImGui::SameLine();
if(ImGui::Button("停止##Timeline")) (void)runtime->TimelineStop(object.GetId());
}
}
void MetaCoreDrawScriptComponentInspector(MetaCoreEditorContext& editorContext, MetaCoreGameObject& gameObject) {
if (!gameObject.HasComponent<MetaCoreScriptComponent>()) {
return;
@ -4791,6 +4824,14 @@ template <typename T>
if (fieldId == "enabled") {
prefabObjectIterator->Animator->Enabled = animator.Enabled;
changed = true;
} else if (fieldId == "controller_asset_guid") {
prefabObjectIterator->Animator->ControllerAssetGuid = animator.ControllerAssetGuid; changed = true;
} else if (fieldId == "avatar_asset_guid") {
prefabObjectIterator->Animator->AvatarAssetGuid = animator.AvatarAssetGuid; changed = true;
} else if (fieldId == "update_mode") {
prefabObjectIterator->Animator->UpdateMode = animator.UpdateMode; changed = true;
} else if (fieldId == "root_motion_mode") {
prefabObjectIterator->Animator->RootMotionMode = animator.RootMotionMode; changed = true;
} else if (fieldId == "source_model_asset_guid") {
prefabObjectIterator->Animator->SourceModelAssetGuid = animator.SourceModelAssetGuid;
changed = true;
@ -4938,6 +4979,10 @@ template <typename T>
animator.Enabled = prefabObject->Animator->Enabled;
return true;
}
if (fieldId == "controller_asset_guid") { animator.ControllerAssetGuid=prefabObject->Animator->ControllerAssetGuid;return true; }
if (fieldId == "avatar_asset_guid") { animator.AvatarAssetGuid=prefabObject->Animator->AvatarAssetGuid;return true; }
if (fieldId == "update_mode") { animator.UpdateMode=prefabObject->Animator->UpdateMode;return true; }
if (fieldId == "root_motion_mode") { animator.RootMotionMode=prefabObject->Animator->RootMotionMode;return true; }
if (fieldId == "source_model_asset_guid") {
animator.SourceModelAssetGuid = prefabObject->Animator->SourceModelAssetGuid;
return true;
@ -5101,6 +5146,9 @@ bool MetaCoreDeserializeComponentValue(
[[nodiscard]] bool MetaCoreIsPackagePath(const std::filesystem::path& path) {
const std::string extension = path.extension().string();
if (extension == ".mcanim" || extension == ".mcavatar" || extension == ".mcanimator" || extension == ".mctimeline") {
return true;
}
return extension == ".mcscene" || extension == ".mcprefab" || extension == ".mcasset" || extension == ".mcui";
}
@ -5155,7 +5203,9 @@ bool MetaCoreDeserializeComponentValue(
}
[[nodiscard]] bool MetaCoreIsJsonSourceAssetPath(const std::filesystem::path& path) {
return MetaCoreIsScenePath(path) || MetaCoreIsPrefabPath(path) || MetaCoreIsMaterialPath(path) || MetaCoreIsUiPath(path);
const auto extension = path.extension();
return MetaCoreIsScenePath(path) || MetaCoreIsPrefabPath(path) || MetaCoreIsMaterialPath(path) || MetaCoreIsUiPath(path) ||
extension == ".mcanim" || extension == ".mcavatar" || extension == ".mcanimator" || extension == ".mctimeline";
}
[[nodiscard]] bool MetaCoreIsGeneratedProjectPath(const std::filesystem::path& relativePath) {
@ -5410,6 +5460,7 @@ void MetaCoreWriteEmbeddedTextureRuntimePackages(
}
const std::string extension = path.extension().string();
if (extension == ".mcanim" || extension == ".mcavatar" || extension == ".mcanimator" || extension == ".mctimeline") return "AnimationAssetImporter";
if (extension == ".png" || extension == ".jpg" || extension == ".jpeg" || extension == ".tga" ||
extension == ".ppm" || extension == ".pnm" || extension == ".pgm" ||
extension == ".ktx" || extension == ".hdr") {
@ -5450,6 +5501,10 @@ void MetaCoreWriteEmbeddedTextureRuntimePackages(
}
const std::string extension = path.extension().string();
if (extension == ".mcanim") return "animation_clip";
if (extension == ".mcavatar") return "animation_avatar";
if (extension == ".mcanimator") return "animator_controller";
if (extension == ".mctimeline") return "timeline";
if (extension == ".mcscene") {
return "scene";
}
@ -9307,6 +9362,8 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(
std::string importedAssetTypeName = "MetaCoreImportedAssetDocument";
if (metadata.ImporterId == "GltfModelImporter") {
std::vector<std::vector<std::byte>> meshPayloads;
std::vector<MetaCoreAnimationClipDocument> animationClips;
MetaCoreAvatarDocument animationAvatar;
MetaCoreModelAssetDocument importedAsset =
MetaCoreBuildImportedGltfAssetDocument(
absoluteSourcePath,
@ -9314,7 +9371,9 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(
metadata.SourceHash,
meshPayloads,
existingModelImportSettings,
cancelToken.get()
cancelToken.get(),
&animationClips,
&animationAvatar
);
if (cancelled()) {
return false;
@ -9327,6 +9386,8 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(
return false;
}
MetaCoreApplyStableSubAssetGuids(previousSubAssets, importedAsset);
animationAvatar.ModelGuid = metadata.AssetGuid;
for (auto& clip : animationClips) clip.SourceModelGuid = metadata.AssetGuid;
MetaCoreFinalizeImportReport(importedAsset.ImportReport);
MetaCoreUpdateModelSubAssetMetadata(importedAsset, metadata);
metadata.LastImportResult = importedAsset.ImportReport.Result;
@ -9352,6 +9413,26 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(
if (!PackageService_->WritePackage(absolutePackagePath, std::move(assetPackage))) {
return false;
}
const auto animationDirectory = absoluteSourcePath.parent_path() / (absoluteSourcePath.stem().string() + "_Animations");
bool wroteAnimationArtifacts = MetaCoreWriteAvatar(
animationDirectory / (animationAvatar.AssetGuid.ToString() + ".mcavatar"),
animationAvatar
);
for (const auto& clip : animationClips) {
wroteAnimationArtifacts =
MetaCoreWriteAnimationClip(animationDirectory / (clip.AssetGuid.ToString() + ".mcanim"), clip) &&
wroteAnimationArtifacts;
}
if (!wroteAnimationArtifacts) {
metadata.LastImportResult = MetaCoreModelImportResult::Error;
metadata.LastImportMessages.push_back(MetaCoreImportMessageDocument{
MetaCoreImportMessageSeverity::Error,
"animation_artifact_write_failed",
"无法写入或校验生成的 Avatar/Animation Clip"
});
(void)MetaCoreWriteMetaJson(absoluteMetaPath, metadata);
return false;
}
if (MetaCoreWriteMetaJson(absoluteMetaPath, metadata)) {
MetaCoreWriteGeneratedTextureRuntimePackages(
*PackageService_,
@ -9565,6 +9646,33 @@ public:
return false;
}
// Legacy Animator v1 migration is transactional with respect to the scene
// document: resolve and atomically write every deterministic controller first,
// then rewrite component references as one in-memory commit.
bool migratedLegacyAnimators = false;
{
struct PendingAnimationMigration { std::size_t ObjectIndex=0; MetaCoreAssetGuid Controller{}; MetaCoreAssetGuid Avatar{}; };
std::vector<PendingAnimationMigration> pending;
bool migrationFailed = false;
const auto projectRoot = AssetDatabaseService_->GetProjectDescriptor().RootPath;
MetaCoreAnimationAssetLibrary animationAssets(projectRoot / "Assets");
for (std::size_t index = 0; index < sceneDocument->GameObjects.size(); ++index) {
auto& data = sceneDocument->GameObjects[index];
if (!data.Animator || data.Animator->ControllerAssetGuid.IsValid() || !data.Animator->SourceModelAssetGuid.IsValid()) continue;
const auto clip = animationAssets.FindClip(data.Animator->SourceModelAssetGuid, data.Animator->ClipName);
if (!clip) { migrationFailed = true; editorContext.AddConsoleMessage(MetaCoreLogLevel::Error,"Animation","旧 Animator 迁移失败:找不到 Clipobject="+data.Name+" model="+data.Animator->SourceModelAssetGuid.ToString()); break; }
const std::string key=data.Animator->SourceModelAssetGuid.ToString()+"|"+clip->StableImportKey+"|"+(data.Animator->Loop?"loop":"once");
MetaCoreAnimatorControllerDocument controller;controller.AssetGuid=MetaCoreBuildDeterministicAnimationGuid(key);controller.Name="Migrated "+clip->Name;
MetaCoreAnimatorStateDocument state;state.Id="Default";state.Name=clip->Name;state.Motion.ClipGuid=clip->AssetGuid;state.Loop=data.Animator->Loop;
MetaCoreAnimatorLayerDocument layer;layer.DefaultStateId=state.Id;layer.States.push_back(std::move(state));controller.Layers.push_back(std::move(layer));
const auto controllerPath=projectRoot/"Assets"/"Animations"/"Migrated"/(controller.AssetGuid.ToString()+".mcanimator");
if(!MetaCoreWriteAnimatorController(controllerPath,controller)){migrationFailed=true;editorContext.AddConsoleMessage(MetaCoreLogLevel::Error,"Animation","旧 Animator 迁移失败:无法原子写入 "+controllerPath.string());break;}
const auto avatar=animationAssets.FindAvatar(data.Animator->SourceModelAssetGuid);
pending.push_back({index,controller.AssetGuid,avatar?avatar->AssetGuid:MetaCoreAssetGuid{}});
}
if(!migrationFailed)for(const auto&change:pending){auto&animator=*sceneDocument->GameObjects[change.ObjectIndex].Animator;animator.ControllerAssetGuid=change.Controller;animator.AvatarAssetGuid=change.Avatar;migratedLegacyAnimators=true;}
}
MetaCoreEditorStateSnapshot editorStateSnapshot;
editorStateSnapshot.SceneSnapshot.Environment = sceneDocument->Environment;
editorStateSnapshot.SceneSnapshot.GameObjects = sceneDocument->GameObjects;
@ -9591,7 +9699,8 @@ public:
ReflectionRegistry_->GetTypeRegistry(),
editorContext.GetScene().CaptureSnapshot()
).value_or(std::vector<std::byte>{});
SetDirtyState(false);
SetDirtyState(migratedLegacyAnimators);
if(migratedLegacyAnimators)editorContext.AddConsoleMessage(MetaCoreLogLevel::Info,"Animation","旧 Animator 已迁移为稳定 GUID Controller场景已标记为待保存。");
editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Scene", "已加载场景: " + MetaCorePathToPortableString(CurrentScenePath_));
if (ModuleRegistry_ != nullptr) {
@ -10084,6 +10193,16 @@ public:
},
MetaCoreDrawAnimatorComponentInspector
});
Descriptors_.push_back(MetaCoreComponentDescriptor{
"TimelineDirector", "时间线导演",
[](const MetaCoreGameObject& object){return object.HasComponent<MetaCoreTimelineDirectorComponent>();},
[](MetaCoreGameObject& object){if(object.HasComponent<MetaCoreTimelineDirectorComponent>())return false;object.AddComponent<MetaCoreTimelineDirectorComponent>();return true;},
[](MetaCoreGameObject& object){if(!object.HasComponent<MetaCoreTimelineDirectorComponent>())return false;object.RemoveComponent<MetaCoreTimelineDirectorComponent>();return true;},
[](MetaCoreGameObject& object){if(!object.HasComponent<MetaCoreTimelineDirectorComponent>())return false;object.GetComponent<MetaCoreTimelineDirectorComponent>()={};return true;},
[](const MetaCoreGameObject& object,const MetaCoreTypeRegistry& registry)->std::optional<std::vector<std::byte>>{if(!object.HasComponent<MetaCoreTimelineDirectorComponent>())return std::nullopt;return MetaCoreSerializeComponentValue(object.GetComponent<MetaCoreTimelineDirectorComponent>(),registry);},
[](MetaCoreGameObject& object,std::span<const std::byte> payload,const MetaCoreTypeRegistry& registry){MetaCoreTimelineDirectorComponent value;if(!MetaCoreDeserializeComponentValue(payload,value,registry))return false;if(object.HasComponent<MetaCoreTimelineDirectorComponent>())object.GetComponent<MetaCoreTimelineDirectorComponent>()=value;else object.AddComponent<MetaCoreTimelineDirectorComponent>(value);return true;},
MetaCoreDrawTimelineDirectorInspector
});
Descriptors_.push_back(MetaCoreComponentDescriptor{
"Script",
"脚本",
@ -10746,9 +10865,11 @@ public:
[&editorContext](std::uint32_t level, std::string_view category, std::string_view message) {
const MetaCoreLogLevel mapped = level >= 2U ? MetaCoreLogLevel::Error : level == 1U ? MetaCoreLogLevel::Warning : MetaCoreLogLevel::Info;
editorContext.AddConsoleMessage(mapped, std::string(category), std::string(message));
}, Input_ ? &Input_->GetRuntime() : nullptr, Physics_ ? Physics_->GetOrCreate(editorContext) : nullptr
}, Input_ ? &Input_->GetRuntime() : nullptr, Physics_ ? Physics_->GetOrCreate(editorContext) : nullptr,
editorContext.GetActiveAnimationRuntime()
);
Runtime_->Start();
editorContext.SetActiveScriptRuntime(Runtime_.get());
if (Physics_ && Physics_->GetOrCreate(editorContext)) {
Physics_->GetOrCreate(editorContext)->SetEventCallback([this](const MetaCorePhysicsContactEvent& event) {
if (!Runtime_) return;
@ -10801,7 +10922,8 @@ public:
if (Runtime_ != nullptr) Runtime_->LateUpdate(deltaSeconds);
}
void OnPlayStop(MetaCoreEditorContext&) override {
void OnPlayStop(MetaCoreEditorContext& editorContext) override {
editorContext.SetActiveScriptRuntime(nullptr);
if (Input_) {
Input_->GetRuntime().Unsubscribe(InputSubscription_);
Input_->GetInteraction().SetEventCallback({});
@ -10854,81 +10976,61 @@ public:
class MetaCoreAnimationPlayModeComponentSystem final : public MetaCoreIPlayModeComponentSystem {
public:
explicit MetaCoreAnimationPlayModeComponentSystem(std::shared_ptr<MetaCorePhysicsPlayModeComponentSystem> physics)
: Physics_(std::move(physics)) {}
[[nodiscard]] std::string GetSystemId() const override { return "MetaCore.PlayMode.Animation"; }
void OnPlayStart(MetaCoreEditorContext& editorContext) override {
bool changed = false;
for (MetaCoreGameObject gameObject : editorContext.GetScene().GetGameObjects()) {
if (!gameObject.HasComponent<MetaCoreAnimatorComponent>()) {
continue;
const auto assets = editorContext.GetModuleRegistry().ResolveService<MetaCoreIAssetDatabaseService>();
if (!assets || !assets->HasProject()) return;
const auto projectRoot = assets->GetProjectDescriptor().RootPath;
Library_ = std::make_unique<MetaCoreAnimationAssetLibrary>(projectRoot / "Assets");
bool migrated = false;
for (auto object : editorContext.GetScene().GetGameObjects()) {
if (!object.HasComponent<MetaCoreAnimatorComponent>()) continue;
auto& component = object.GetComponent<MetaCoreAnimatorComponent>();
if (component.ControllerAssetGuid.IsValid() || !component.SourceModelAssetGuid.IsValid()) continue;
auto clip = Library_->FindClip(component.SourceModelAssetGuid, component.ClipName);
if (!clip) continue;
const std::string migrationKey = component.SourceModelAssetGuid.ToString() + "|" + clip->StableImportKey + "|" + (component.Loop ? "loop" : "once");
MetaCoreAnimatorControllerDocument controller;
controller.AssetGuid = MetaCoreBuildDeterministicAnimationGuid(migrationKey);
controller.Name = "Migrated " + clip->Name;
MetaCoreAnimatorStateDocument state; state.Id = "Default"; state.Name = clip->Name; state.Motion.ClipGuid = clip->AssetGuid; state.Loop = component.Loop;
MetaCoreAnimatorLayerDocument layer; layer.DefaultStateId = state.Id; layer.States.push_back(std::move(state)); controller.Layers.push_back(std::move(layer));
const auto path = projectRoot / "Assets" / "Animations" / "Migrated" / (controller.AssetGuid.ToString() + ".mcanimator");
if (!MetaCoreWriteAnimatorController(path, controller)) continue;
component.ControllerAssetGuid = controller.AssetGuid;
if (auto avatar = Library_->FindAvatar(component.SourceModelAssetGuid)) component.AvatarAssetGuid = avatar->AssetGuid;
migrated = true;
}
if (migrated) { editorContext.GetScene().IncrementRevision(); Library_->Scan(projectRoot / "Assets"); }
Runtime_ = std::make_unique<MetaCoreAnimationRuntime>(editorContext.GetScene(),
[this](MetaCoreAssetGuid guid) { return Library_->LoadClip(guid); },
[this](MetaCoreAssetGuid guid) { return Library_->LoadController(guid); },
[this](MetaCoreAssetGuid guid) { return Library_->LoadAvatar(guid); },
[this](MetaCoreAssetGuid guid) { return Library_->LoadTimeline(guid); },
Physics_ ? Physics_->GetOrCreate(editorContext) : nullptr);
Runtime_->SetEventCallback([&editorContext](const MetaCoreAnimationRuntimeEvent& event) {
editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Animation", event.Type + ": " + event.Name);
if (auto* scripts = editorContext.GetActiveScriptRuntime()) {
MetaCoreScriptValueV1 name{}; name.Type = MetaCoreScriptAbiValue_String;
std::snprintf(name.Text, sizeof(name.Text), "%s", event.Name.c_str());
scripts->PublishToObject(event.ObjectId, "MetaCore.Animation." + event.Type, {name});
}
auto& animator = gameObject.GetComponent<MetaCoreAnimatorComponent>();
animator.RuntimeTimeSeconds = 0.0F;
animator.RuntimePlaying = animator.Enabled && animator.PlayOnStart;
changed = true;
}
if (changed) {
editorContext.GetScene().IncrementRevision();
}
});
Runtime_->Start();
editorContext.SetActiveAnimationRuntime(Runtime_.get());
}
void Update(MetaCoreEditorContext& editorContext, double deltaSeconds) override {
if (deltaSeconds <= 0.0) {
return;
}
void FixedUpdate(MetaCoreEditorContext&, double deltaSeconds) override { if (Runtime_) Runtime_->FixedUpdate(deltaSeconds); }
void Update(MetaCoreEditorContext&, double deltaSeconds) override { if (Runtime_) Runtime_->Update(deltaSeconds); }
bool changed = false;
for (MetaCoreGameObject gameObject : editorContext.GetScene().GetGameObjects()) {
if (!gameObject.HasComponent<MetaCoreAnimatorComponent>()) {
continue;
}
auto& animator = gameObject.GetComponent<MetaCoreAnimatorComponent>();
if (!animator.Enabled || !animator.RuntimePlaying) {
continue;
}
const float speed = std::max(0.0F, animator.Speed);
if (speed <= 0.0F) {
continue;
}
const auto modelDocument = MetaCoreLoadAnimatorSourceModelDocument(editorContext, animator);
const float durationSeconds = MetaCoreGetAnimatorClipDuration(modelDocument, animator.ClipIndex);
float nextTime = animator.RuntimeTimeSeconds + static_cast<float>(deltaSeconds) * speed;
if (durationSeconds > 0.0F) {
if (animator.Loop) {
nextTime = std::fmod(std::max(0.0F, nextTime), durationSeconds);
} else if (nextTime >= durationSeconds) {
nextTime = durationSeconds;
animator.RuntimePlaying = false;
}
}
animator.RuntimeTimeSeconds = std::max(0.0F, nextTime);
changed = true;
}
if (changed) {
editorContext.GetScene().IncrementRevision();
}
}
void OnPlayStop(MetaCoreEditorContext& editorContext) override {
bool changed = false;
for (MetaCoreGameObject gameObject : editorContext.GetScene().GetGameObjects()) {
if (!gameObject.HasComponent<MetaCoreAnimatorComponent>()) {
continue;
}
auto& animator = gameObject.GetComponent<MetaCoreAnimatorComponent>();
animator.RuntimePlaying = false;
animator.RuntimeTimeSeconds = 0.0F;
changed = true;
}
if (changed) {
editorContext.GetScene().IncrementRevision();
}
}
void OnPlayStop(MetaCoreEditorContext& editorContext) override { editorContext.SetActiveAnimationRuntime(nullptr); if (Runtime_) Runtime_->Stop(); Runtime_.reset(); Library_.reset(); }
private:
std::unique_ptr<MetaCoreAnimationAssetLibrary> Library_{};
std::unique_ptr<MetaCoreAnimationRuntime> Runtime_{};
std::shared_ptr<MetaCorePhysicsPlayModeComponentSystem> Physics_{};
};
class MetaCoreBuiltinPlayModeComponentSystemRegistry final : public MetaCoreIPlayModeComponentSystemRegistry {
@ -10940,13 +11042,13 @@ public:
Systems_.clear();
auto input = std::make_shared<MetaCoreInputPlayModeComponentSystem>();
RegisterSystem(input);
RegisterSystem(std::make_shared<MetaCoreAnimationPlayModeComponentSystem>());
RegisterSystem(std::make_shared<MetaCoreRotatorPlayModeComponentSystem>());
auto physics = std::make_shared<MetaCorePhysicsPlayModeComponentSystem>();
input->SetPhysicsWorldGetter([weakPhysics = std::weak_ptr<MetaCorePhysicsPlayModeComponentSystem>(physics)] {
const auto value = weakPhysics.lock(); return value ? value->GetWorld() : nullptr;
});
RegisterSystem(std::make_shared<MetaCoreAnimationPlayModeComponentSystem>(physics));
RegisterSystem(std::make_shared<MetaCoreScriptPlayModeComponentSystem>(input, physics));
RegisterSystem(std::make_shared<MetaCoreRotatorPlayModeComponentSystem>());
RegisterSystem(std::move(physics));
}
@ -11440,6 +11542,8 @@ std::optional<std::filesystem::path> MetaCoreBuiltinPrefabService::CreatePrefabF
if (gameObject.HasComponent<MetaCoreLightComponent>()) data.Light = gameObject.GetComponent<MetaCoreLightComponent>();
if (gameObject.HasComponent<MetaCoreRotatorComponent>()) data.Rotator = gameObject.GetComponent<MetaCoreRotatorComponent>();
if (gameObject.HasComponent<MetaCoreAnimatorComponent>()) data.Animator = gameObject.GetComponent<MetaCoreAnimatorComponent>();
if (gameObject.HasComponent<MetaCoreActiveComponent>()) data.Active = gameObject.GetComponent<MetaCoreActiveComponent>();
if (gameObject.HasComponent<MetaCoreTimelineDirectorComponent>()) data.TimelineDirector = gameObject.GetComponent<MetaCoreTimelineDirectorComponent>();
if (gameObject.HasComponent<MetaCoreScriptComponent>()) data.Script = gameObject.GetComponent<MetaCoreScriptComponent>();
if (gameObject.HasComponent<MetaCoreUiRendererComponent>()) data.UiRenderer = gameObject.GetComponent<MetaCoreUiRendererComponent>();
if (gameObject.HasComponent<MetaCoreMeshRendererComponent>()) data.MeshRenderer = gameObject.GetComponent<MetaCoreMeshRendererComponent>();
@ -11516,6 +11620,8 @@ std::optional<MetaCoreId> MetaCoreBuiltinPrefabService::InstantiatePrefabDocumen
if (sourceObject.Light.has_value()) clonedObject.AddComponent<MetaCoreLightComponent>(*sourceObject.Light);
if (sourceObject.Rotator.has_value()) clonedObject.AddComponent<MetaCoreRotatorComponent>(*sourceObject.Rotator);
if (sourceObject.Animator.has_value()) clonedObject.AddComponent<MetaCoreAnimatorComponent>(*sourceObject.Animator);
if (sourceObject.Active.has_value()) clonedObject.AddComponent<MetaCoreActiveComponent>(*sourceObject.Active);
if (sourceObject.TimelineDirector.has_value()) clonedObject.AddComponent<MetaCoreTimelineDirectorComponent>(*sourceObject.TimelineDirector);
if (sourceObject.Script.has_value()) clonedObject.AddComponent<MetaCoreScriptComponent>(*sourceObject.Script);
if (sourceObject.UiRenderer.has_value()) clonedObject.AddComponent<MetaCoreUiRendererComponent>(*sourceObject.UiRenderer);
if (sourceObject.MeshRenderer.has_value()) clonedObject.AddComponent<MetaCoreMeshRendererComponent>(*sourceObject.MeshRenderer);
@ -11637,6 +11743,8 @@ bool MetaCoreBuiltinPrefabService::ApplySelectedPrefabInstance(MetaCoreEditorCon
if (gameObject.HasComponent<MetaCoreLightComponent>()) data.Light = gameObject.GetComponent<MetaCoreLightComponent>();
if (gameObject.HasComponent<MetaCoreRotatorComponent>()) data.Rotator = gameObject.GetComponent<MetaCoreRotatorComponent>();
if (gameObject.HasComponent<MetaCoreAnimatorComponent>()) data.Animator = gameObject.GetComponent<MetaCoreAnimatorComponent>();
if (gameObject.HasComponent<MetaCoreActiveComponent>()) data.Active = gameObject.GetComponent<MetaCoreActiveComponent>();
if (gameObject.HasComponent<MetaCoreTimelineDirectorComponent>()) data.TimelineDirector = gameObject.GetComponent<MetaCoreTimelineDirectorComponent>();
if (gameObject.HasComponent<MetaCoreScriptComponent>()) data.Script = gameObject.GetComponent<MetaCoreScriptComponent>();
if (gameObject.HasComponent<MetaCoreUiRendererComponent>()) data.UiRenderer = gameObject.GetComponent<MetaCoreUiRendererComponent>();
if (gameObject.HasComponent<MetaCoreMeshRendererComponent>()) data.MeshRenderer = gameObject.GetComponent<MetaCoreMeshRendererComponent>();
@ -11884,6 +11992,73 @@ std::optional<MetaCoreId> MetaCoreBuiltinSceneEditingService::InstantiateModelAs
return std::nullopt;
}
MetaCoreAssetGuid defaultControllerGuid{};
MetaCoreAssetGuid defaultAvatarGuid{};
if (!importedDocument->Animations.empty()) {
const auto projectRoot=AssetDatabaseService_->GetProjectDescriptor().RootPath;
MetaCoreAnimationAssetLibrary animationAssets(projectRoot/"Assets");
const auto& sourceAnimation=importedDocument->Animations.front();
auto clip=animationAssets.FindClip(assetGuid,sourceAnimation.StableImportKey);
if(!clip)clip=animationAssets.FindClip(assetGuid,sourceAnimation.Name);
// Older/partially imported model packages can contain animation metadata
// without the derived .mcanim files because the former importer ignored
// artifact write failures. Recover them once from the glTF source instead
// of making an otherwise valid model impossible to instantiate.
if (!clip) {
const std::filesystem::path sourceRelativePath = !assetRecord->SourcePath.empty()
? assetRecord->SourcePath
: assetRecord->RelativePath;
const std::filesystem::path sourcePath = projectRoot / sourceRelativePath;
if (MetaCoreIsGltfModelPath(sourcePath) && std::filesystem::is_regular_file(sourcePath)) {
std::vector<std::vector<std::byte>> ignoredMeshPayloads;
std::vector<MetaCoreAnimationClipDocument> recoveredClips;
MetaCoreAvatarDocument recoveredAvatar;
(void)MetaCoreBuildImportedGltfAssetDocument(
sourcePath,
sourceRelativePath,
assetRecord->SourceHash,
ignoredMeshPayloads,
importedDocument->ImportSettings,
nullptr,
&recoveredClips,
&recoveredAvatar
);
recoveredAvatar.ModelGuid = assetGuid;
const auto animationDirectory =
sourcePath.parent_path() / (sourcePath.stem().string() + "_Animations");
bool recovered = MetaCoreWriteAvatar(
animationDirectory / (recoveredAvatar.AssetGuid.ToString() + ".mcavatar"),
recoveredAvatar
);
for (auto& recoveredClip : recoveredClips) {
recoveredClip.SourceModelGuid = assetGuid;
recovered = MetaCoreWriteAnimationClip(
animationDirectory / (recoveredClip.AssetGuid.ToString() + ".mcanim"),
recoveredClip
) && recovered;
}
if (recovered) {
animationAssets.Scan(projectRoot / "Assets");
clip = animationAssets.FindClip(assetGuid, sourceAnimation.StableImportKey);
if (!clip) clip = animationAssets.FindClip(assetGuid, sourceAnimation.Name);
editorContext.AddConsoleMessage(
MetaCoreLogLevel::Info,
"Animation",
"已恢复旧导入缺失的动画派生资产: " + sourceAnimation.StableImportKey
);
}
}
}
if(!clip){editorContext.AddConsoleMessage(MetaCoreLogLevel::Error,"Animation","实例化模型失败:找不到稳定动画 Clip "+sourceAnimation.StableImportKey);return std::nullopt;}
const std::string key=assetGuid.ToString()+"|"+clip->StableImportKey+"|loop";
MetaCoreAnimatorControllerDocument controller;controller.AssetGuid=MetaCoreBuildDeterministicAnimationGuid(key);controller.Name="Default "+clip->Name;
MetaCoreAnimatorStateDocument state;state.Id="Default";state.Name=clip->Name;state.Motion.ClipGuid=clip->AssetGuid;state.Loop=true;
MetaCoreAnimatorLayerDocument layer;layer.DefaultStateId=state.Id;layer.States.push_back(std::move(state));controller.Layers.push_back(std::move(layer));
const auto path=projectRoot/"Assets"/"Animations"/"Generated"/(controller.AssetGuid.ToString()+".mcanimator");
if(!std::filesystem::exists(path)&&!MetaCoreWriteAnimatorController(path,controller)){editorContext.AddConsoleMessage(MetaCoreLogLevel::Error,"Animation","实例化模型失败:无法创建默认 Controller "+path.string());return std::nullopt;}
defaultControllerGuid=controller.AssetGuid;if(const auto avatar=animationAssets.FindAvatar(assetGuid))defaultAvatarGuid=avatar->AssetGuid;
}
MetaCoreId instantiatedRootId = 0;
const bool instantiated = editorContext.ExecuteSnapshotCommand("实例化层级模型", [&]() {
MetaCoreId externalParentId = forcedParentId.value_or(editorContext.GetActiveObjectId());
@ -11910,6 +12085,8 @@ std::optional<MetaCoreId> MetaCoreBuiltinSceneEditingService::InstantiateModelAs
if (!importedDocument->Animations.empty() && !modelRootObject.HasComponent<MetaCoreAnimatorComponent>()) {
MetaCoreAnimatorComponent animator;
animator.ControllerAssetGuid = defaultControllerGuid;
animator.AvatarAssetGuid = defaultAvatarGuid;
animator.SourceModelAssetGuid = assetGuid;
animator.SourceModelPath = resolvedModelPath.generic_string();
animator.ClipIndex = 0;

View File

@ -20,6 +20,7 @@
#include "MetaCoreRuntimeInput/MetaCoreInputMap.h"
#include "MetaCorePlatform/MetaCoreWindow.h"
#include "MetaCorePhysics/MetaCorePhysics.h"
#include "MetaCoreAnimation/MetaCoreAnimation.h"
#include "MetaCoreRendering/MetaCoreRendering.h"
#include "MetaCoreFoundation/MetaCoreAssetRegistry.h"
#include "MetaCoreRender/MetaCoreEditorViewportRenderer.h"
@ -3504,6 +3505,97 @@ private:
bool Dirty_ = false;
};
class MetaCoreAnimationToolsPanelProvider final : public MetaCoreIEditorPanelProvider {
public:
std::string GetPanelId() const override { return "AnimationTools"; }
std::string GetPanelTitle() const override { return "动画工具"; }
bool IsOpenByDefault() const override { return false; }
void DrawPanel(MetaCoreEditorContext& editorContext) override {
const auto assets = editorContext.GetModuleRegistry().ResolveService<MetaCoreIAssetDatabaseService>();
if (!assets || !assets->HasProject()) { ImGui::TextDisabled("没有打开项目。"); return; }
const auto root = assets->GetProjectDescriptor().RootPath;
if (root != Root_) { Root_ = root; Refresh(); }
if (ImGui::Button("刷新资源")) Refresh();
ImGui::SameLine(); ImGui::TextDisabled("%zu 个动画资产", Paths_.size());
if (ImGui::BeginListBox("##AnimationAssets", ImVec2(-1.0F, 105.0F))) {
for (std::size_t index = 0; index < Paths_.size(); ++index) {
const bool selected = index == Selected_;
if (ImGui::Selectable(std::filesystem::relative(Paths_[index], Root_).generic_string().c_str(), selected)) Load(index);
}
ImGui::EndListBox();
}
if (Selected_ >= Paths_.size()) { DrawRuntimeDebugger(editorContext); return; }
ImGui::Separator();
if (Controller_) DrawController(editorContext);
else if (Avatar_) DrawAvatar(editorContext);
else if (Clip_) DrawClip(editorContext);
else if (Timeline_) DrawTimeline(editorContext);
DrawRuntimeDebugger(editorContext);
}
private:
template<class Document, class Writer>
void SaveDocument(MetaCoreEditorContext& context, const Document& document, Writer writer, bool valid, const char* label) {
if (!valid) { context.AddConsoleMessage(MetaCoreLogLevel::Error, "Animation", std::string(label) + " 校验失败,未保存。"); return; }
if (!writer(Paths_[Selected_], document)) { context.AddConsoleMessage(MetaCoreLogLevel::Error, "Animation", std::string(label) + " 原子保存失败。"); return; }
Dirty_ = false;
}
static bool EditString(const char* label, std::string& value) {
std::array<char, 256> buffer{}; std::snprintf(buffer.data(), buffer.size(), "%s", value.c_str());
if (!ImGui::InputText(label, buffer.data(), buffer.size())) return false; value = buffer.data(); return true;
}
void Refresh() {
Paths_.clear(); std::error_code error; const auto assets = Root_ / "Assets";
if (std::filesystem::is_directory(assets, error)) for (const auto& entry : std::filesystem::recursive_directory_iterator(assets, error)) {
if (error || !entry.is_regular_file()) continue; const auto extension = entry.path().extension();
if (extension == ".mcanim" || extension == ".mcavatar" || extension == ".mcanimator" || extension == ".mctimeline") Paths_.push_back(entry.path());
}
std::sort(Paths_.begin(), Paths_.end()); Selected_ = Paths_.size(); ClearDocuments();
}
void ClearDocuments() { Clip_.reset(); Avatar_.reset(); Controller_.reset(); Timeline_.reset(); Dirty_ = false; }
void Load(std::size_t index) {
Selected_ = index; ClearDocuments(); if (index >= Paths_.size()) return; const auto& path = Paths_[index];
if (path.extension() == ".mcanim") Clip_ = MetaCoreReadAnimationClip(path);
else if (path.extension() == ".mcavatar") Avatar_ = MetaCoreReadAvatar(path);
else if (path.extension() == ".mcanimator") Controller_ = MetaCoreReadAnimatorController(path);
else if (path.extension() == ".mctimeline") Timeline_ = MetaCoreReadTimeline(path);
}
void DrawController(MetaCoreEditorContext& context) {
auto& document = *Controller_; ImGui::Text("动画控制器%s", Dirty_ ? " *" : "");
Dirty_ |= EditString("名称", document.Name);
if (ImGui::Button("保存动画控制器")) SaveDocument(context, document, MetaCoreWriteAnimatorController, MetaCoreValidateAnimatorController(document), "动画控制器");
ImGui::SameLine(); if (ImGui::Button("添加参数")) { document.Parameters.push_back({"Parameter" + std::to_string(document.Parameters.size() + 1), MetaCoreAnimatorParameterType::Float}); Dirty_ = true; }
ImGui::SameLine(); if (ImGui::Button("添加动画层")) { MetaCoreAnimatorLayerDocument layer; layer.Id = "Layer" + std::to_string(document.Layers.size() + 1); layer.Name = layer.Id; document.Layers.push_back(std::move(layer)); Dirty_ = true; }
if (ImGui::CollapsingHeader("参数", ImGuiTreeNodeFlags_DefaultOpen)) for (std::size_t i=0;i<document.Parameters.size();++i) { ImGui::PushID(static_cast<int>(i)); Dirty_|=EditString("ID",document.Parameters[i].Id);int type=static_cast<int>(document.Parameters[i].Type);if(ImGui::Combo("类型",&type,"布尔\0整数\0浮点\0触发器\0")){document.Parameters[i].Type=static_cast<MetaCoreAnimatorParameterType>(type);Dirty_=true;}ImGui::PopID(); }
if (document.Layers.empty()) return; LayerIndex_ = std::min(LayerIndex_, static_cast<int>(document.Layers.size()-1));
if (ImGui::BeginCombo("动画层", document.Layers[static_cast<std::size_t>(LayerIndex_)].Name.c_str())) { for(std::size_t i=0;i<document.Layers.size();++i)if(ImGui::Selectable(document.Layers[i].Name.c_str(),LayerIndex_==static_cast<int>(i)))LayerIndex_=static_cast<int>(i);ImGui::EndCombo(); }
auto& layer=document.Layers[static_cast<std::size_t>(LayerIndex_)];Dirty_|=ImGui::SliderFloat("动画层权重",&layer.Weight,0,1);int mode=static_cast<int>(layer.BlendMode);if(ImGui::Combo("混合模式",&mode,"覆盖\0叠加\0")){layer.BlendMode=static_cast<MetaCoreAnimatorLayerBlendMode>(mode);Dirty_=true;}
if(ImGui::Button("添加状态")){MetaCoreAnimatorStateDocument state;state.Id="State"+std::to_string(layer.States.size()+1);state.Name=state.Id;layer.States.push_back(state);if(layer.DefaultStateId.empty())layer.DefaultStateId=state.Id;Dirty_=true;}ImGui::SameLine();if(ImGui::Button("连接前两个状态")&&layer.States.size()>=2){MetaCoreAnimatorTransitionDocument transition;transition.Id="Transition"+std::to_string(layer.Transitions.size()+1);transition.SourceStateId=layer.States[0].Id;transition.TargetStateId=layer.States[1].Id;layer.Transitions.push_back(std::move(transition));Dirty_=true;}
for(std::size_t stateIndex=0;stateIndex<layer.States.size();++stateIndex){auto&state=layer.States[stateIndex];ImGui::PushID(static_cast<int>(10000+stateIndex));if(ImGui::TreeNode(state.Name.c_str())){Dirty_|=EditString("状态 ID",state.Id);Dirty_|=EditString("显示名",state.Name);Dirty_|=ImGui::DragFloat("速度",&state.Speed,.01F,0,100);Dirty_|=ImGui::Checkbox("循环",&state.Loop);int motion=static_cast<int>(state.Motion.Type);if(ImGui::Combo("动作类型",&motion,"动画片段\0" "一维混合树\0" "二维笛卡尔混合\0" "二维方向混合\0")){state.Motion.Type=static_cast<MetaCoreAnimatorMotionType>(motion);Dirty_=true;}std::string currentClip=state.Motion.ClipGuid.IsValid()?state.Motion.ClipGuid.ToString():"<选择动画片段>";if(ImGui::BeginCombo("动画片段",currentClip.c_str())){for(const auto&path:Paths_)if(path.extension()==".mcanim")if(const auto clip=MetaCoreReadAnimationClip(path))if(ImGui::Selectable(clip->Name.c_str(),clip->AssetGuid==state.Motion.ClipGuid)){state.Motion.ClipGuid=clip->AssetGuid;Dirty_=true;}ImGui::EndCombo();}if(state.Motion.Type!=MetaCoreAnimatorMotionType::Clip){Dirty_|=EditString("参数 X",state.Motion.ParameterX);if(state.Motion.Type!=MetaCoreAnimatorMotionType::BlendTree1D)Dirty_|=EditString("参数 Y",state.Motion.ParameterY);}if(ImGui::Button("设为默认")){layer.DefaultStateId=state.Id;Dirty_=true;}ImGui::TreePop();}ImGui::PopID();}
ImGui::BeginChild("AnimatorGraph",ImVec2(-1,220),true,ImGuiWindowFlags_HorizontalScrollbar);const ImVec2 origin=ImGui::GetCursorScreenPos();auto*draw=ImGui::GetWindowDrawList();for(std::size_t i=0;i<layer.States.size();++i){const ImVec2 p{origin.x+25.0F+static_cast<float>(i%4)*165.0F,origin.y+25.0F+static_cast<float>(i/4)*80.0F};const ImVec2 q{p.x+130,p.y+48};draw->AddRectFilled(p,q,layer.States[i].Id==layer.DefaultStateId?IM_COL32(52,110,68,255):IM_COL32(52,62,78,255),6);draw->AddRect(p,q,IM_COL32(180,190,210,255),6);draw->AddText({p.x+8,p.y+15},IM_COL32_WHITE,layer.States[i].Name.c_str());}for(const auto&transition:layer.Transitions){const auto source=std::find_if(layer.States.begin(),layer.States.end(),[&](const auto&s){return s.Id==transition.SourceStateId;});const auto target=std::find_if(layer.States.begin(),layer.States.end(),[&](const auto&s){return s.Id==transition.TargetStateId;});if(source==layer.States.end()||target==layer.States.end())continue;const auto a=static_cast<std::size_t>(source-layer.States.begin()),b=static_cast<std::size_t>(target-layer.States.begin());draw->AddLine({origin.x+155+static_cast<float>(a%4)*165,origin.y+49+static_cast<float>(a/4)*80},{origin.x+25+static_cast<float>(b%4)*165,origin.y+49+static_cast<float>(b/4)*80},IM_COL32(240,190,70,255),2);}ImGui::Dummy(ImVec2(680,190));ImGui::EndChild();
}
void DrawAvatar(MetaCoreEditorContext& context) {
auto& document=*Avatar_;ImGui::Text("骨架映射%s",Dirty_?" *":"");Dirty_|=EditString("根骨骼",document.RootBonePath);Dirty_|=ImGui::DragFloat("统一比例",&document.UniformScale,.01F,.001F,100.F);
if(ImGui::Button("按稳定路径自动映射")){document.Mappings.clear();for(const auto&bone:document.Bones)document.Mappings.push_back({bone.Path,bone.Path});Dirty_=true;}ImGui::SameLine();if(ImGui::Button("保存骨架配置"))SaveDocument(context,document,MetaCoreWriteAvatar,MetaCoreValidateAvatar(document),"骨架配置");
ImGui::Columns(2,"AvatarTrees",true);ImGui::TextUnformatted("骨架 / 绑定姿势");for(const auto&bone:document.Bones)ImGui::BulletText("%s 父索引=%d",bone.Path.c_str(),bone.Parent);ImGui::NextColumn();ImGui::TextUnformatted("源骨骼 -> 目标骨骼");for(const auto&mapping:document.Mappings)ImGui::BulletText("%s -> %s",mapping.SourcePath.c_str(),mapping.TargetPath.c_str());ImGui::Columns(1);
}
void DrawClip(MetaCoreEditorContext& context) {
auto& document=*Clip_;if(PreviewPlaying_)PreviewTime_=document.Duration>0?std::fmod(PreviewTime_+ImGui::GetIO().DeltaTime,document.Duration):0;ImGui::Text("动画片段预览:%s",document.Name.c_str());if(ImGui::Button(PreviewPlaying_?"暂停":"播放"))PreviewPlaying_=!PreviewPlaying_;ImGui::SameLine();if(ImGui::Button("逐帧"))PreviewTime_=std::min(document.Duration,PreviewTime_+1.F/60.F);ImGui::SliderFloat("时间",&PreviewTime_,0,document.Duration);const auto pose=MetaCoreSampleAnimationClip(document,PreviewTime_);ImGui::Text("姿势节点 %zu根节点 %s",pose.Nodes.size(),document.RootNodePath.c_str());
if(const auto selected=context.GetSelectedGameObject();selected&&selected.HasComponent<MetaCoreAnimatorComponent>()&&context.GetActiveAnimationRuntime()==nullptr){MetaCoreAnimationRuntimePoseComponent preview;preview.Nodes.reserve(pose.Nodes.size());for(const auto&[path,value]:pose.Nodes)preview.Nodes.push_back({path,0,value.Translation,value.Rotation,value.Scale});auto&registry=context.GetScene().GetRegistry();const auto entity=selected.GetEntityHandle();if(registry.any_of<MetaCoreAnimationRuntimePoseComponent>(entity))registry.replace<MetaCoreAnimationRuntimePoseComponent>(entity,std::move(preview));else registry.emplace<MetaCoreAnimationRuntimePoseComponent>(entity,std::move(preview));ImGui::TextDisabled("姿势已输出到场景真实模型视口(不修改场景修订号)。");}
if(const auto root=pose.Nodes.find(document.RootNodePath);root!=pose.Nodes.end())ImGui::Text("根运动轨迹点:%.3f, %.3f, %.3f",root->second.Translation.x,root->second.Translation.y,root->second.Translation.z);for(const auto&event:document.Events)ImGui::BulletText("%.3fs %s [%s]",event.Time,event.Name.c_str(),event.StableId.c_str());
}
void DrawTimeline(MetaCoreEditorContext& context) {
auto& document=*Timeline_;ImGui::Text("工业时间线%s",Dirty_?" *":"");Dirty_|=EditString("名称",document.Name);Dirty_|=ImGui::DragFloat("时长",&document.Duration,.05F,0,86400);
if(ImGui::Button("添加变换轨")){MetaCoreTimelineTrackDocument track;track.Id="Track"+std::to_string(document.Tracks.size()+1);track.BindingSlot="Object";track.Keys.resize(2);track.Keys[1].Time=document.Duration;document.Tracks.push_back(std::move(track));Dirty_=true;}ImGui::SameLine();if(ImGui::Button("保存时间线"))SaveDocument(context,document,MetaCoreWriteTimeline,MetaCoreValidateTimeline(document),"时间线");
for(std::size_t i=0;i<document.Tracks.size();++i){auto&track=document.Tracks[i];ImGui::PushID(static_cast<int>(i));if(ImGui::TreeNode(track.Id.c_str())){Dirty_|=EditString("轨道 ID",track.Id);Dirty_|=EditString("绑定槽",track.BindingSlot);int type=static_cast<int>(track.Type);if(ImGui::Combo("类型",&type,"变换轨\0动画控制轨\0激活轨\0信号轨\0")){track.Type=static_cast<MetaCoreTimelineTrackType>(type);Dirty_=true;}for(auto&key:track.Keys){ImGui::PushID(&key);Dirty_|=ImGui::DragFloat("关键帧",&key.Time,.01F,0,document.Duration);ImGui::SameLine();ImGui::TextDisabled("%s",key.TextValue.c_str());ImGui::PopID();}ImGui::TreePop();}ImGui::PopID();}
}
void DrawRuntimeDebugger(MetaCoreEditorContext& context) {
if(!ImGui::CollapsingHeader("播放模式状态调试器",ImGuiTreeNodeFlags_DefaultOpen))return;const auto*runtime=context.GetActiveAnimationRuntime();const auto objectId=context.GetSelectedObjectId();if(!runtime||objectId==0){ImGui::TextDisabled("进入播放模式并选择动画控制器对象。");return;}const auto snapshot=runtime->GetDebugSnapshot(objectId);if(!snapshot){ImGui::TextDisabled("所选对象没有活动动画控制器。");return;}for(const auto&layer:snapshot->Layers)ImGui::Text("%s: %s 归一化时间 %.3f 过渡 %.3f 权重 %.2f",layer.LayerId.c_str(),layer.StateId.c_str(),layer.NormalizedTime,layer.TransitionWeight,layer.Weight);ImGui::Text("根运动 %.3f %.3f %.3f",snapshot->LastRootMotion.Translation.x,snapshot->LastRootMotion.Translation.y,snapshot->LastRootMotion.Translation.z);
}
std::filesystem::path Root_{};std::vector<std::filesystem::path>Paths_{};std::size_t Selected_=0;std::optional<MetaCoreAnimationClipDocument>Clip_{};std::optional<MetaCoreAvatarDocument>Avatar_{};std::optional<MetaCoreAnimatorControllerDocument>Controller_{};std::optional<MetaCoreTimelineDocument>Timeline_{};int LayerIndex_=0;float PreviewTime_=0;bool PreviewPlaying_=false;bool Dirty_=false;
};
class MetaCorePhysicsSettingsPanelProvider final : public MetaCoreIEditorPanelProvider {
public:
std::string GetPanelId() const override { return "PhysicsSettings"; }
@ -6700,12 +6792,12 @@ void DrawMissingAnimatorPrompt(MetaCoreEditorContext& editorContext, MetaCoreGam
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.18F, 0.16F, 0.10F, 1.0F));
ImGui::BeginChild("MissingAnimatorPrompt", ImVec2(0, 58), true, ImGuiWindowFlags_NoScrollbar);
ImGui::Text("该模型包含 %d 个动画 Clip但当前根对象没有 Animator", static_cast<int>(modelDocument->Animations.size()));
ImGui::Text("该模型包含 %d 个动画片段,但当前根对象没有动画器", static_cast<int>(modelDocument->Animations.size()));
if (ImGui::Button("添加动画器并播放", ImVec2(-1, 24))) {
const MetaCoreId objectId = selectedObject.GetId();
const MetaCoreAssetRecord sourceRecord = *sourceAsset;
const MetaCoreModelAssetDocument sourceDocument = *modelDocument;
(void)editorContext.ExecuteSnapshotCommand("添加 Animator", [&]() {
(void)editorContext.ExecuteSnapshotCommand("添加动画器", [&]() {
MetaCoreGameObject object = editorContext.GetScene().FindGameObject(objectId);
if (!object || object.HasComponent<MetaCoreAnimatorComponent>()) {
return false;
@ -10121,6 +10213,7 @@ public:
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreScenePanelProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreRuntimeUiPanelProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreInputMapPanelProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreAnimationToolsPanelProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCorePhysicsSettingsPanelProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreRenderingSettingsPanelProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreRmlUiVisualEditorPanelProvider>());

View File

@ -218,6 +218,12 @@ private:
}
if (lhs.Animator.has_value()) {
if (lhs.Animator->Enabled != rhs.Animator->Enabled ||
lhs.Animator->ControllerAssetGuid != rhs.Animator->ControllerAssetGuid ||
lhs.Animator->AvatarAssetGuid != rhs.Animator->AvatarAssetGuid ||
lhs.Animator->UpdateMode != rhs.Animator->UpdateMode ||
lhs.Animator->RootMotionMode != rhs.Animator->RootMotionMode ||
!MetaCoreNearlyEqualVec3(lhs.Animator->RootMotionTranslationMask, rhs.Animator->RootMotionTranslationMask) ||
!MetaCoreNearlyEqualVec3(lhs.Animator->RootMotionRotationMask, rhs.Animator->RootMotionRotationMask) ||
lhs.Animator->SourceModelAssetGuid != rhs.Animator->SourceModelAssetGuid ||
lhs.Animator->SourceModelPath != rhs.Animator->SourceModelPath ||
lhs.Animator->ClipIndex != rhs.Animator->ClipIndex ||
@ -229,6 +235,13 @@ private:
}
}
if (lhs.Active.has_value() != rhs.Active.has_value() || (lhs.Active && lhs.Active->Active != rhs.Active->Active)) return false;
if (lhs.TimelineDirector.has_value() != rhs.TimelineDirector.has_value()) return false;
if (lhs.TimelineDirector && (lhs.TimelineDirector->Enabled != rhs.TimelineDirector->Enabled ||
lhs.TimelineDirector->TimelineAssetGuid != rhs.TimelineDirector->TimelineAssetGuid ||
lhs.TimelineDirector->PlayOnStart != rhs.TimelineDirector->PlayOnStart || lhs.TimelineDirector->Loop != rhs.TimelineDirector->Loop ||
!MetaCoreNearlyEqual(lhs.TimelineDirector->Speed, rhs.TimelineDirector->Speed) || lhs.TimelineDirector->Bindings.size() != rhs.TimelineDirector->Bindings.size())) return false;
if (lhs.Script.has_value() != rhs.Script.has_value()) {
return false;
}

View File

@ -1,4 +1,5 @@
#include "MetaCoreGltfImporter.h"
#include "MetaCoreAnimation/MetaCoreAnimation.h"
#include "MetaCoreFoundation/MetaCoreHash.h"
#include "MetaCoreLocalCgltf.h"
@ -20,6 +21,7 @@
#include <cmath>
#include <algorithm>
#include <optional>
#include <unordered_map>
namespace MetaCore {
@ -88,6 +90,27 @@ void MetaCoreSetImportedNodeTransformFromCgltfNode(MetaCoreImportedGltfNodeDocum
}
}
[[nodiscard]] glm::quat MetaCoreGltfToEngineBasisRotation() {
// Keep this in lockstep with MetaCoreFilamentSceneBridge's asset pivot:
// glTF is Y-up, while MetaCore runtime poses and Root Motion are Z-up.
return glm::angleAxis(glm::radians(-90.0F), glm::vec3(1.0F, 0.0F, 0.0F));
}
[[nodiscard]] glm::vec3 MetaCoreGltfToEngineTranslation(glm::vec3 value) {
return MetaCoreGltfToEngineBasisRotation() * value;
}
[[nodiscard]] glm::quat MetaCoreGltfToEngineRotation(glm::quat value, bool normalizeResult = true) {
const glm::quat basis = MetaCoreGltfToEngineBasisRotation();
const glm::quat converted = basis * value * glm::inverse(basis);
return normalizeResult ? glm::normalize(converted) : converted;
}
[[nodiscard]] glm::vec3 MetaCoreGltfToEngineScale(glm::vec3 value) {
// A +/-90 degree X basis change permutes the Y/Z scale axes.
return {value.x, value.z, value.y};
}
[[nodiscard]] MetaCoreAssetGuid MetaCoreMakeDeterministicGeneratedAssetGuid(
const std::filesystem::path& relativeSourcePath,
std::string_view generatedKind,
@ -149,11 +172,13 @@ void MetaCoreImportGltfAnimationMetadata(MetaCoreModelAssetDocument& document, c
}
document.Animations.reserve(data.animations_count);
std::unordered_map<std::string, std::size_t> animationNameOccurrences;
for (std::size_t animationIndex = 0; animationIndex < data.animations_count; ++animationIndex) {
const cgltf_animation& animation = data.animations[animationIndex];
MetaCoreImportedGltfAnimationDocument animationDocument;
animationDocument.Name = MetaCoreGltfDisplayName(animation.name, "Animation_" + std::to_string(animationIndex));
animationDocument.StableImportKey = MetaCoreBuildStableImportKey("animation", animationDocument.Name, animationIndex);
const std::size_t nameOccurrence = animationNameOccurrences[animationDocument.Name]++;
animationDocument.StableImportKey = MetaCoreBuildStableImportKey("animation", animationDocument.Name, nameOccurrence);
animationDocument.ChannelCount = static_cast<std::int32_t>(animation.channels_count);
animationDocument.SamplerCount = static_cast<std::int32_t>(animation.samplers_count);
@ -462,7 +487,9 @@ struct MetaCoreGltfMaterialScalarPresence {
std::uint64_t sourceHash,
std::vector<std::vector<std::byte>>& outMeshPayloads,
MetaCoreModelImportSettingsDocument importSettings,
const std::atomic_bool* cancelRequested
const std::atomic_bool* cancelRequested,
std::vector<MetaCoreAnimationClipDocument>* outAnimationClips,
MetaCoreAvatarDocument* outAvatar
) {
MetaCoreModelAssetDocument document;
const auto cancelled = [&]() { return cancelRequested != nullptr && cancelRequested->load(); };
@ -1110,6 +1137,131 @@ struct MetaCoreGltfMaterialScalarPresence {
MetaCoreImportGltfAnimationMetadata(document, *data);
if (outAvatar != nullptr) {
*outAvatar = {};
outAvatar->AssetGuid = MetaCoreMakeDeterministicGeneratedAssetGuid(relativeSourcePath, "avatar", "Skeleton", 0);
outAvatar->ModelGuid = MetaCoreMakeDeterministicGeneratedAssetGuid(relativeSourcePath, "model", relativeSourcePath.filename().string(), 0);
std::string signature;
for (std::size_t index = 0; index < document.Nodes.size(); ++index) {
const auto& node = document.Nodes[index];
MetaCoreAvatarBoneDocument bone;
bone.Path = node.StableNodePath;
bone.Parent = node.ParentIndex;
bone.BindPose.Translation = MetaCoreGltfToEngineTranslation(node.Position);
bone.BindPose.Rotation = MetaCoreGltfToEngineRotation(glm::quat(glm::radians(node.RotationEulerDegrees)));
bone.BindPose.Scale = MetaCoreGltfToEngineScale(node.Scale);
outAvatar->Bones.push_back(std::move(bone));
signature += node.StableNodePath + ":" + std::to_string(node.ParentIndex) + ";";
if (node.ParentIndex < 0 && outAvatar->RootBonePath.empty()) outAvatar->RootBonePath = node.StableNodePath;
}
outAvatar->SkeletonSignature = std::to_string(MetaCoreHashString(signature));
}
if (outAnimationClips != nullptr) {
outAnimationClips->clear();
for (std::size_t animationIndex = 0; animationIndex < data->animations_count; ++animationIndex) {
const cgltf_animation& animation = data->animations[animationIndex];
MetaCoreAnimationClipDocument clip;
clip.Name = document.Animations[animationIndex].Name;
clip.StableImportKey = document.Animations[animationIndex].StableImportKey;
// The stable key already contains a same-name occurrence. Do not include the
// source array index: unrelated glTF animation reordering must preserve GUIDs.
clip.AssetGuid = MetaCoreMakeDeterministicGeneratedAssetGuid(relativeSourcePath, "animation", clip.StableImportKey, 0);
clip.Duration = document.Animations[animationIndex].DurationSeconds;
clip.SkeletonSignature = outAvatar != nullptr ? outAvatar->SkeletonSignature : std::string{};
clip.RootNodePath = outAvatar != nullptr ? outAvatar->RootBonePath : std::string{};
for (std::size_t channelIndex = 0; channelIndex < animation.channels_count; ++channelIndex) {
const cgltf_animation_channel& source = animation.channels[channelIndex];
if (source.target_node == nullptr || source.sampler == nullptr || source.sampler->input == nullptr || source.sampler->output == nullptr) continue;
const std::ptrdiff_t nodeIndex = source.target_node - data->nodes;
if (nodeIndex < 0 || static_cast<std::size_t>(nodeIndex) >= document.Nodes.size()) continue;
MetaCoreAnimationChannelDocument channel;
channel.NodePath = document.Nodes[static_cast<std::size_t>(nodeIndex)].StableNodePath;
channel.Interpolation = source.sampler->interpolation == cgltf_interpolation_type_step ? MetaCoreAnimationInterpolation::Step :
source.sampler->interpolation == cgltf_interpolation_type_cubic_spline ? MetaCoreAnimationInterpolation::CubicSpline : MetaCoreAnimationInterpolation::Linear;
channel.Target = source.target_path == cgltf_animation_path_type_rotation ? MetaCoreAnimationChannelTarget::Rotation :
source.target_path == cgltf_animation_path_type_scale ? MetaCoreAnimationChannelTarget::Scale : MetaCoreAnimationChannelTarget::Translation;
const std::size_t keyCount = source.sampler->input->count;
const std::size_t stride = channel.Interpolation == MetaCoreAnimationInterpolation::CubicSpline ? 3U : 1U;
for (std::size_t keyIndex = 0; keyIndex < keyCount; ++keyIndex) {
float time = 0.0F; if (!cgltf_accessor_read_float(source.sampler->input, keyIndex, &time, 1)) continue;
if (channel.Target == MetaCoreAnimationChannelTarget::Rotation) {
float value[4]{0,0,0,1}, in[4]{}, out[4]{};
cgltf_accessor_read_float(source.sampler->output, keyIndex * stride + (stride == 3 ? 1 : 0), value, 4);
if (stride == 3) { cgltf_accessor_read_float(source.sampler->output,keyIndex*3,in,4);cgltf_accessor_read_float(source.sampler->output,keyIndex*3+2,out,4); }
channel.QuatKeys.push_back({
time,
MetaCoreGltfToEngineRotation({value[3],value[0],value[1],value[2]}),
MetaCoreGltfToEngineRotation({in[3],in[0],in[1],in[2]}, false),
MetaCoreGltfToEngineRotation({out[3],out[0],out[1],out[2]}, false)
});
} else {
float value[3]{}, in[3]{}, out[3]{};
cgltf_accessor_read_float(source.sampler->output, keyIndex * stride + (stride == 3 ? 1 : 0), value, 3);
if (stride == 3) { cgltf_accessor_read_float(source.sampler->output,keyIndex*3,in,3);cgltf_accessor_read_float(source.sampler->output,keyIndex*3+2,out,3); }
const glm::vec3 keyValue{value[0],value[1],value[2]};
const glm::vec3 inTangent{in[0],in[1],in[2]};
const glm::vec3 outTangent{out[0],out[1],out[2]};
if (channel.Target == MetaCoreAnimationChannelTarget::Scale) {
channel.Vec3Keys.push_back({
time,
MetaCoreGltfToEngineScale(keyValue),
MetaCoreGltfToEngineScale(inTangent),
MetaCoreGltfToEngineScale(outTangent)
});
} else {
channel.Vec3Keys.push_back({
time,
MetaCoreGltfToEngineTranslation(keyValue),
MetaCoreGltfToEngineTranslation(inTangent),
MetaCoreGltfToEngineTranslation(outTangent)
});
}
}
}
clip.Channels.push_back(std::move(channel));
}
// glTF clips are sparse: a joint commonly animates only rotation.
// Runtime poses, however, are complete local transforms. Fill every
// unauthored component from the imported bind/local pose so starting
// animation cannot collapse joint offsets or reset node scale.
for (const auto& node : document.Nodes) {
const auto hasTarget = [&](MetaCoreAnimationChannelTarget target) {
return std::any_of(clip.Channels.begin(), clip.Channels.end(), [&](const auto& channel) {
return channel.NodePath == node.StableNodePath && channel.Target == target;
});
};
if (!hasTarget(MetaCoreAnimationChannelTarget::Translation)) {
MetaCoreAnimationChannelDocument channel;
channel.NodePath = node.StableNodePath;
channel.Target = MetaCoreAnimationChannelTarget::Translation;
channel.Interpolation = MetaCoreAnimationInterpolation::Step;
channel.Vec3Keys.push_back({0.0F, MetaCoreGltfToEngineTranslation(node.Position)});
clip.Channels.push_back(std::move(channel));
}
if (!hasTarget(MetaCoreAnimationChannelTarget::Rotation)) {
MetaCoreAnimationChannelDocument channel;
channel.NodePath = node.StableNodePath;
channel.Target = MetaCoreAnimationChannelTarget::Rotation;
channel.Interpolation = MetaCoreAnimationInterpolation::Step;
channel.QuatKeys.push_back({
0.0F,
MetaCoreGltfToEngineRotation(glm::quat(glm::radians(node.RotationEulerDegrees)))
});
clip.Channels.push_back(std::move(channel));
}
if (!hasTarget(MetaCoreAnimationChannelTarget::Scale)) {
MetaCoreAnimationChannelDocument channel;
channel.NodePath = node.StableNodePath;
channel.Target = MetaCoreAnimationChannelTarget::Scale;
channel.Interpolation = MetaCoreAnimationInterpolation::Step;
channel.Vec3Keys.push_back({0.0F, MetaCoreGltfToEngineScale(node.Scale)});
clip.Channels.push_back(std::move(channel));
}
}
outAnimationClips->push_back(std::move(clip));
}
}
cgltf_free(data);
MetaCoreFinalizeImportReport(document.ImportReport);
MetaCoreApplyModelImportSettings(document);

View File

@ -1,6 +1,7 @@
#pragma once
#include "MetaCoreFoundation/MetaCoreAssetTypes.h"
#include "MetaCoreAnimation/MetaCoreAnimation.h"
#include <filesystem>
#include <vector>
#include <cstddef>
@ -14,7 +15,9 @@ namespace MetaCore {
std::uint64_t sourceHash,
std::vector<std::vector<std::byte>>& outMeshPayloads,
MetaCoreModelImportSettingsDocument importSettings = {},
const std::atomic_bool* cancelRequested = nullptr
const std::atomic_bool* cancelRequested = nullptr,
std::vector<MetaCoreAnimationClipDocument>* outAnimationClips = nullptr,
MetaCoreAvatarDocument* outAvatar = nullptr
);
} // namespace MetaCore

View File

@ -17,6 +17,8 @@
#include <vector>
namespace MetaCore {
class MetaCoreAnimationRuntime;
class MetaCoreScriptRuntime;
class MetaCoreWindow;
class MetaCoreInput;
@ -157,6 +159,10 @@ public:
[[nodiscard]] bool GetShowPhysicsDebug() const;
void SetShowPhysicsDebug(bool show);
void SetActivePhysicsWorld(MetaCorePhysicsWorld* world);
void SetActiveAnimationRuntime(MetaCoreAnimationRuntime* runtime) { ActiveAnimationRuntime_ = runtime; }
[[nodiscard]] MetaCoreAnimationRuntime* GetActiveAnimationRuntime() const { return ActiveAnimationRuntime_; }
void SetActiveScriptRuntime(MetaCoreScriptRuntime* runtime) { ActiveScriptRuntime_ = runtime; }
[[nodiscard]] MetaCoreScriptRuntime* GetActiveScriptRuntime() const { return ActiveScriptRuntime_; }
[[nodiscard]] MetaCorePhysicsWorld* GetPhysicsWorldForDebug();
[[nodiscard]] MetaCoreReparentTransformRule GetReparentTransformRule() const;
void SetReparentTransformRule(MetaCoreReparentTransformRule rule);
@ -226,6 +232,8 @@ private:
bool ShowWorldOrigin_ = true;
bool ShowPhysicsDebug_ = false;
MetaCorePhysicsWorld* ActivePhysicsWorld_ = nullptr;
MetaCoreAnimationRuntime* ActiveAnimationRuntime_ = nullptr;
MetaCoreScriptRuntime* ActiveScriptRuntime_ = nullptr;
std::unique_ptr<MetaCorePhysicsWorld> PhysicsPreviewWorld_{};
MetaCoreReparentTransformRule ReparentTransformRule_ = MetaCoreReparentTransformRule::KeepWorld;
MetaCoreEditorCommandService CommandService_{};

View File

@ -2584,6 +2584,18 @@ public:
continue;
}
if (!animationState.PoseNodes.empty()) {
if (!animationState.PoseMappingValid) { animator->resetBoneMatrices(); continue; }
for (const MetaCoreAnimationRuntimePoseNode& poseNode : animationState.PoseNodes) {
if (poseNode.ObjectId == 0) continue;
const glm::mat4 local = glm::translate(glm::mat4(1.0F), poseNode.Translation) *
glm::mat4_cast(poseNode.Rotation) * glm::scale(glm::mat4(1.0F), poseNode.Scale);
UpdateTransform(poseNode.ObjectId, local, local);
}
animator->updateBoneMatrices();
continue;
}
const std::size_t animationCount = animator->getAnimationCount();
const bool validClip =
animationState.ClipIndex >= 0 &&

View File

@ -11,6 +11,7 @@
#include <algorithm>
#include <cmath>
#include <unordered_map>
namespace MetaCore {
@ -32,6 +33,20 @@ namespace {
return path;
}
[[nodiscard]] std::optional<std::string> MetaCoreBuildPathBelowRoot(
const MetaCoreScene& scene, MetaCoreGameObject object, MetaCoreId rootId
) {
std::vector<std::string> segments;
while (object && object.GetId() != rootId) {
segments.push_back(object.GetName());
object = object.GetParentId() == 0 ? MetaCoreGameObject{} : scene.FindGameObject(object.GetParentId());
}
if (!object || object.GetId() != rootId || segments.empty()) return std::nullopt;
std::string path;
for (auto iterator=segments.rbegin();iterator!=segments.rend();++iterator) { if(!path.empty())path+="/";path+=*iterator; }
return path;
}
[[nodiscard]] float MetaCoreSafeAspectRatio(float width, float height) {
return height > 0.0001F ? std::max(0.0001F, width / height) : 1.0F;
}
@ -165,18 +180,27 @@ MetaCoreSceneRenderSyncSnapshot MetaCoreSceneRenderSync::BuildSnapshot(const Met
}
}
bool hierarchyActive = true;
for (MetaCoreGameObject current = gameObject; current; ) {
if (current.HasComponent<MetaCoreActiveComponent>() && !current.GetComponent<MetaCoreActiveComponent>().Active) {
hierarchyActive = false;
break;
}
current = current.GetParentId() == 0 ? MetaCoreGameObject{} : scene.FindGameObject(current.GetParentId());
}
snapshot.Renderables.push_back(MetaCoreRenderSyncRenderable{
objectId,
parentId,
gameObject.GetName(),
localMatrix,
worldMatrix,
meshRenderer.Visible,
meshRenderer.Visible && hierarchyActive,
meshRenderer.MeshSource,
meshRenderer.BuiltinMesh,
meshRenderer.MeshAssetGuid,
meshRenderer.SourceModelAssetGuid,
meshRenderer.SourceModelPath,
meshRenderer.SourceNodePath,
meshRenderer.ModelNodeIndex,
hostRoot.GetId(),
hostRoot.GetName(),
@ -266,7 +290,7 @@ MetaCoreSceneRenderSyncSnapshot MetaCoreSceneRenderSync::BuildSnapshot(const Met
if (sourceModelPath.empty() && gameObject.HasComponent<MetaCoreModelRootTag>()) {
sourceModelPath = gameObject.GetComponent<MetaCoreModelRootTag>().SourceModelPath;
}
snapshot.Animations.push_back(MetaCoreRenderSyncAnimationState{
MetaCoreRenderSyncAnimationState state{
objectId,
animator.SourceModelAssetGuid,
sourceModelPath,
@ -276,7 +300,21 @@ MetaCoreSceneRenderSyncSnapshot MetaCoreSceneRenderSync::BuildSnapshot(const Met
animator.RuntimePlaying,
animator.Loop,
animator.Speed
});
};
if (gameObject.HasComponent<MetaCoreAnimationRuntimePoseComponent>()) {
state.PoseNodes = gameObject.GetComponent<MetaCoreAnimationRuntimePoseComponent>().Nodes;
std::unordered_map<std::string,std::vector<MetaCoreId>> pathTargets;
for(const auto candidate:scene.GetGameObjects()) {
if(const auto path=MetaCoreBuildPathBelowRoot(scene,candidate,objectId))pathTargets[*path].push_back(candidate.GetId());
if(candidate.HasComponent<MetaCoreMeshRendererComponent>()) {
const auto&sourcePath=candidate.GetComponent<MetaCoreMeshRendererComponent>().SourceNodePath;
if(!sourcePath.empty()){auto&ids=pathTargets[sourcePath];if(std::find(ids.begin(),ids.end(),candidate.GetId())==ids.end())ids.push_back(candidate.GetId());}
}
}
for(auto&node:state.PoseNodes){if(const auto target=pathTargets.find(node.Path);target!=pathTargets.end()&&target->second.size()==1)node.ObjectId=target->second.front();else state.PoseMappingValid=false;}
if(!state.PoseMappingValid)for(auto&node:state.PoseNodes)node.ObjectId=0;
}
snapshot.Animations.push_back(std::move(state));
}
if(gameObject.HasComponent<MetaCoreReflectionProbeComponent>())snapshot.ReflectionProbes.push_back({objectId,gameObject.GetComponent<MetaCoreReflectionProbeComponent>(),worldMatrix});
if(gameObject.HasComponent<MetaCoreLodGroupComponent>())snapshot.LodGroups.push_back({objectId,gameObject.GetComponent<MetaCoreLodGroupComponent>(),worldMatrix});

View File

@ -31,6 +31,7 @@ struct MetaCoreRenderSyncRenderable {
MetaCoreAssetGuid MeshAssetGuid{};
MetaCoreAssetGuid SourceModelAssetGuid{};
std::string SourceModelPath{};
std::string SourceNodePath{};
std::int32_t ModelNodeIndex = -1;
MetaCoreId HostRootId = 0;
@ -107,6 +108,8 @@ struct MetaCoreRenderSyncAnimationState {
bool Playing = false;
bool Loop = true;
float Speed = 1.0F;
std::vector<MetaCoreAnimationRuntimePoseNode> PoseNodes{};
bool PoseMappingValid = true;
};
template<typename T> struct MetaCoreRenderSyncComponent { MetaCoreId ObjectId=0; T Component{}; glm::mat4 WorldMatrix{1.0F}; };

View File

@ -289,6 +289,10 @@ std::vector<MetaCoreId> MetaCoreScene::DuplicateGameObjects(const std::vector<Me
if (sourceObject.HasComponent<MetaCoreAnimatorComponent>())
clonedObject.AddComponent<MetaCoreAnimatorComponent>(sourceObject.GetComponent<MetaCoreAnimatorComponent>());
if (sourceObject.HasComponent<MetaCoreActiveComponent>())
clonedObject.AddComponent<MetaCoreActiveComponent>(sourceObject.GetComponent<MetaCoreActiveComponent>());
if (sourceObject.HasComponent<MetaCoreTimelineDirectorComponent>())
clonedObject.AddComponent<MetaCoreTimelineDirectorComponent>(sourceObject.GetComponent<MetaCoreTimelineDirectorComponent>());
if (sourceObject.HasComponent<MetaCoreScriptComponent>())
clonedObject.AddComponent<MetaCoreScriptComponent>(sourceObject.GetComponent<MetaCoreScriptComponent>());
@ -474,6 +478,10 @@ MetaCoreSceneSnapshot MetaCoreScene::CaptureSnapshot() const {
data.Rotator = obj.GetComponent<MetaCoreRotatorComponent>();
if (obj.HasComponent<MetaCoreAnimatorComponent>())
data.Animator = obj.GetComponent<MetaCoreAnimatorComponent>();
if (obj.HasComponent<MetaCoreActiveComponent>())
data.Active = obj.GetComponent<MetaCoreActiveComponent>();
if (obj.HasComponent<MetaCoreTimelineDirectorComponent>())
data.TimelineDirector = obj.GetComponent<MetaCoreTimelineDirectorComponent>();
if (obj.HasComponent<MetaCoreScriptComponent>())
data.Script = obj.GetComponent<MetaCoreScriptComponent>();
if (obj.HasComponent<MetaCoreUiRendererComponent>())
@ -526,6 +534,10 @@ void MetaCoreScene::RestoreSnapshot(const MetaCoreSceneSnapshot& snapshot) {
obj.AddComponent<MetaCoreRotatorComponent>(data.Rotator.value());
if (data.Animator.has_value())
obj.AddComponent<MetaCoreAnimatorComponent>(data.Animator.value());
if (data.Active.has_value())
obj.AddComponent<MetaCoreActiveComponent>(data.Active.value());
if (data.TimelineDirector.has_value())
obj.AddComponent<MetaCoreTimelineDirectorComponent>(data.TimelineDirector.value());
if (data.Script.has_value())
obj.AddComponent<MetaCoreScriptComponent>(data.Script.value());
if (data.UiRenderer.has_value())

View File

@ -184,16 +184,30 @@ static json GameObjectDataToJson(const MetaCoreGameObjectData& obj, const MetaCo
const auto& animator = obj.Animator.value();
for (const auto& animatorField : animatorDesc->Fields) {
if (animatorField.Name == "Enabled") animatorJson["Enabled"] = animator.Enabled;
else if (animatorField.Name == "SourceModelAssetGuid") animatorJson["SourceModelAssetGuid"] = GuidToString(animator.SourceModelAssetGuid);
else if (animatorField.Name == "SourceModelPath") animatorJson["SourceModelPath"] = animator.SourceModelPath;
else if (animatorField.Name == "ClipIndex") animatorJson["ClipIndex"] = animator.ClipIndex;
else if (animatorField.Name == "ClipName") animatorJson["ClipName"] = animator.ClipName;
else if (animatorField.Name == "ControllerAssetGuid") animatorJson["ControllerAssetGuid"] = GuidToString(animator.ControllerAssetGuid);
else if (animatorField.Name == "AvatarAssetGuid") animatorJson["AvatarAssetGuid"] = GuidToString(animator.AvatarAssetGuid);
else if (animatorField.Name == "UpdateMode") animatorJson["UpdateMode"] = static_cast<int>(animator.UpdateMode);
else if (animatorField.Name == "RootMotionMode") animatorJson["RootMotionMode"] = static_cast<int>(animator.RootMotionMode);
else if (animatorField.Name == "RootMotionTranslationMask") animatorJson["RootMotionTranslationMask"] = Vec3ToJson(animator.RootMotionTranslationMask);
else if (animatorField.Name == "RootMotionRotationMask") animatorJson["RootMotionRotationMask"] = Vec3ToJson(animator.RootMotionRotationMask);
else if (animatorField.Name == "SourceModelAssetGuid" && !animator.ControllerAssetGuid.IsValid()) animatorJson["SourceModelAssetGuid"] = GuidToString(animator.SourceModelAssetGuid);
else if (animatorField.Name == "SourceModelPath" && !animator.ControllerAssetGuid.IsValid()) animatorJson["SourceModelPath"] = animator.SourceModelPath;
else if (animatorField.Name == "ClipIndex" && !animator.ControllerAssetGuid.IsValid()) animatorJson["ClipIndex"] = animator.ClipIndex;
else if (animatorField.Name == "ClipName" && !animator.ControllerAssetGuid.IsValid()) animatorJson["ClipName"] = animator.ClipName;
else if (animatorField.Name == "PlayOnStart") animatorJson["PlayOnStart"] = animator.PlayOnStart;
else if (animatorField.Name == "Loop") animatorJson["Loop"] = animator.Loop;
else if (animatorField.Name == "Loop" && !animator.ControllerAssetGuid.IsValid()) animatorJson["Loop"] = animator.Loop;
else if (animatorField.Name == "Speed") animatorJson["Speed"] = animator.Speed;
}
objJson["Animator"] = std::move(animatorJson);
}
} else if (objField.Name == "Active" && obj.Active.has_value()) {
objJson["Active"] = {{"Active", obj.Active->Active}};
} else if (objField.Name == "TimelineDirector" && obj.TimelineDirector.has_value()) {
const auto& director = *obj.TimelineDirector;
json bindings = json::array();
for (const auto& binding : director.Bindings) bindings.push_back({{"Slot", binding.Slot}, {"ObjectId", binding.ObjectId}});
objJson["TimelineDirector"] = {{"Enabled", director.Enabled}, {"TimelineAssetGuid", GuidToString(director.TimelineAssetGuid)},
{"PlayOnStart", director.PlayOnStart}, {"Loop", director.Loop}, {"Speed", director.Speed}, {"Bindings", std::move(bindings)}};
} else if (objField.Name == "Script" && obj.Script.has_value()) {
json scriptJson = json::object();
json instancesJson = json::array();
@ -415,6 +429,12 @@ static MetaCoreGameObjectData JsonToGameObjectData(const json& objJson, const Me
MetaCoreAnimatorComponent animator;
for (const auto& animatorField : animatorDesc->Fields) {
if (animatorField.Name == "Enabled" && animatorJson.contains("Enabled")) animator.Enabled = animatorJson["Enabled"].get<bool>();
else if (animatorField.Name == "ControllerAssetGuid" && animatorJson.contains("ControllerAssetGuid")) animator.ControllerAssetGuid = StringToGuid(animatorJson["ControllerAssetGuid"].get<std::string>());
else if (animatorField.Name == "AvatarAssetGuid" && animatorJson.contains("AvatarAssetGuid")) animator.AvatarAssetGuid = StringToGuid(animatorJson["AvatarAssetGuid"].get<std::string>());
else if (animatorField.Name == "UpdateMode" && animatorJson.contains("UpdateMode")) animator.UpdateMode = static_cast<MetaCoreAnimatorUpdateMode>(animatorJson["UpdateMode"].get<int>());
else if (animatorField.Name == "RootMotionMode" && animatorJson.contains("RootMotionMode")) animator.RootMotionMode = static_cast<MetaCoreRootMotionMode>(animatorJson["RootMotionMode"].get<int>());
else if (animatorField.Name == "RootMotionTranslationMask" && animatorJson.contains("RootMotionTranslationMask")) animator.RootMotionTranslationMask = JsonToVec3(animatorJson["RootMotionTranslationMask"]);
else if (animatorField.Name == "RootMotionRotationMask" && animatorJson.contains("RootMotionRotationMask")) animator.RootMotionRotationMask = JsonToVec3(animatorJson["RootMotionRotationMask"]);
else if (animatorField.Name == "SourceModelAssetGuid" && animatorJson.contains("SourceModelAssetGuid")) animator.SourceModelAssetGuid = StringToGuid(animatorJson["SourceModelAssetGuid"].get<std::string>());
else if (animatorField.Name == "SourceModelPath" && animatorJson.contains("SourceModelPath")) animator.SourceModelPath = animatorJson["SourceModelPath"].get<std::string>();
else if (animatorField.Name == "ClipIndex" && animatorJson.contains("ClipIndex")) animator.ClipIndex = animatorJson["ClipIndex"].get<std::int32_t>();
@ -425,6 +445,20 @@ static MetaCoreGameObjectData JsonToGameObjectData(const json& objJson, const Me
}
obj.Animator = animator;
}
} else if (objField.Name == "Active" && objJson.contains("Active")) {
MetaCoreActiveComponent active;
active.Active = objJson["Active"].value("Active", true);
obj.Active = active;
} else if (objField.Name == "TimelineDirector" && objJson.contains("TimelineDirector")) {
const auto& value = objJson["TimelineDirector"];
MetaCoreTimelineDirectorComponent director;
director.Enabled = value.value("Enabled", true);
if (value.contains("TimelineAssetGuid")) director.TimelineAssetGuid = StringToGuid(value["TimelineAssetGuid"].get<std::string>());
director.PlayOnStart = value.value("PlayOnStart", true);
director.Loop = value.value("Loop", false);
director.Speed = value.value("Speed", 1.0F);
for (const auto& binding : value.value("Bindings", json::array())) director.Bindings.push_back({binding.value("Slot", ""), binding.value("ObjectId", MetaCoreId{})});
obj.TimelineDirector = std::move(director);
} else if (objField.Name == "Script" && objJson.contains("Script")) {
const auto& scriptJson = objJson["Script"];
MetaCoreScriptComponent script;

View File

@ -6,6 +6,7 @@
#include "MetaCoreRendering/MetaCoreRendering.h"
#include <glm/vec3.hpp>
#include <glm/gtc/quaternion.hpp>
#include <cstdint>
#include <optional>
@ -69,6 +70,21 @@ enum class MetaCoreRigidBodyType {
Dynamic
};
MC_ENUM()
enum class MetaCoreAnimatorUpdateMode {
Normal = 0,
Fixed,
Unscaled
};
MC_ENUM()
enum class MetaCoreRootMotionMode {
Ignore = 0,
ExtractOnly,
ApplyTransform,
CharacterController
};
MC_ENUM()
enum class MetaCorePhysicsConstraintType {
Fixed = 0,
@ -89,6 +105,13 @@ struct MetaCoreTransformComponent {
glm::vec3 Scale{1.0F, 1.0F, 1.0F};
};
MC_STRUCT()
struct MetaCoreActiveComponent {
MC_GENERATED_BODY()
MC_PROPERTY()
bool Active = true;
};
MC_STRUCT()
struct MetaCoreCameraComponent {
MC_GENERATED_BODY()
@ -245,6 +268,21 @@ struct MetaCoreAnimatorComponent {
MC_PROPERTY()
bool Enabled = true;
MC_PROPERTY()
MetaCoreAssetGuid ControllerAssetGuid{};
MC_PROPERTY()
MetaCoreAssetGuid AvatarAssetGuid{};
MC_PROPERTY()
MetaCoreAnimatorUpdateMode UpdateMode = MetaCoreAnimatorUpdateMode::Normal;
MC_PROPERTY()
MetaCoreRootMotionMode RootMotionMode = MetaCoreRootMotionMode::Ignore;
MC_PROPERTY()
glm::vec3 RootMotionTranslationMask{1.0F, 1.0F, 1.0F};
MC_PROPERTY()
glm::vec3 RootMotionRotationMask{0.0F, 0.0F, 1.0F};
// Legacy v1 fields. Readers retain these for one migration version; new runtime
// evaluation uses ControllerAssetGuid exclusively.
MC_PROPERTY()
MetaCoreAssetGuid SourceModelAssetGuid{};
MC_PROPERTY()
std::string SourceModelPath{};
@ -259,10 +297,50 @@ struct MetaCoreAnimatorComponent {
MC_PROPERTY()
float Speed = 1.0F;
// Transient editor-preview controls. MetaCoreAnimationRuntime owns all Play/Player state.
bool RuntimePlaying = false;
float RuntimeTimeSeconds = 0.0F;
};
MC_STRUCT()
struct MetaCoreTimelineBinding {
MC_GENERATED_BODY()
MC_PROPERTY()
std::string Slot{};
MC_PROPERTY()
MetaCoreId ObjectId = 0;
};
MC_STRUCT()
struct MetaCoreTimelineDirectorComponent {
MC_GENERATED_BODY()
MC_PROPERTY()
bool Enabled = true;
MC_PROPERTY()
MetaCoreAssetGuid TimelineAssetGuid{};
MC_PROPERTY()
bool PlayOnStart = true;
MC_PROPERTY()
bool Loop = false;
MC_PROPERTY()
float Speed = 1.0F;
MC_PROPERTY()
std::vector<MetaCoreTimelineBinding> Bindings{};
};
// Runtime-only pose output. It is intentionally absent from MetaCoreGameObjectData
// and all serializers, so animation evaluation never dirties persisted scene state.
struct MetaCoreAnimationRuntimePoseNode {
std::string Path{};
MetaCoreId ObjectId = 0;
glm::vec3 Translation{0.0F};
glm::quat Rotation{1.0F, 0.0F, 0.0F, 0.0F};
glm::vec3 Scale{1.0F};
};
struct MetaCoreAnimationRuntimePoseComponent {
std::vector<MetaCoreAnimationRuntimePoseNode> Nodes{};
};
MC_STRUCT()
struct MetaCoreScriptInstanceData {
MC_GENERATED_BODY()

View File

@ -134,6 +134,10 @@ struct MetaCoreGameObjectData {
MC_PROPERTY()
std::optional<MetaCoreAnimatorComponent> Animator;
MC_PROPERTY()
std::optional<MetaCoreActiveComponent> Active;
MC_PROPERTY()
std::optional<MetaCoreTimelineDirectorComponent> TimelineDirector;
MC_PROPERTY()
std::optional<MetaCoreScriptComponent> Script;
MC_PROPERTY()
std::optional<MetaCoreUiRendererComponent> UiRenderer;

View File

@ -1,6 +1,7 @@
#include "MetaCoreScripting/MetaCoreScripting.h"
#include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h"
#include "MetaCorePhysics/MetaCorePhysics.h"
#include "MetaCoreAnimation/MetaCoreAnimation.h"
#include "MetaCoreFoundation/MetaCoreHash.h"
#include <nlohmann/json.hpp>
@ -451,10 +452,11 @@ struct MetaCoreScriptRuntime::Impl {
bool Running = false;
MetaCoreRuntimeInput* Input = nullptr;
MetaCorePhysicsWorld* Physics = nullptr;
MetaCoreAnimationRuntime* Animation = nullptr;
Impl(MetaCoreScriptRuntime& owner, MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger,
MetaCoreRuntimeInput* input, MetaCorePhysicsWorld* physics)
: Owner(owner), Scene(scene), Registry(registry), Logger(std::move(logger)), Input(input), Physics(physics) {}
MetaCoreRuntimeInput* input, MetaCorePhysicsWorld* physics, MetaCoreAnimationRuntime* animation)
: Owner(owner), Scene(scene), Registry(registry), Logger(std::move(logger)), Input(input), Physics(physics), Animation(animation) {}
void Log(std::uint32_t level, std::string_view category, std::string_view text) const { if (Logger) Logger(level, category, text); }
Entry* FindEntry(MetaCoreId object, std::uint64_t instance) {
@ -560,8 +562,8 @@ struct MetaCoreScriptRuntime::Impl {
};
MetaCoreScriptRuntime::MetaCoreScriptRuntime(MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger,
MetaCoreRuntimeInput* input, MetaCorePhysicsWorld* physics)
: Impl_(std::make_unique<Impl>(*this, scene, registry, std::move(logger), input, physics)) {}
MetaCoreRuntimeInput* input, MetaCorePhysicsWorld* physics, MetaCoreAnimationRuntime* animation)
: Impl_(std::make_unique<Impl>(*this, scene, registry, std::move(logger), input, physics, animation)) {}
MetaCoreScriptRuntime::~MetaCoreScriptRuntime() { Stop(); }
void MetaCoreScriptRuntime::Start() { if (Impl_->Running) return; Impl_->Running = true; ++WorldGeneration_; for (auto o : Impl_->Scene.GetGameObjects()) if (o.HasComponent<MetaCoreScriptComponent>()) for (auto& i : o.GetComponent<MetaCoreScriptComponent>().Instances) if (i.Enabled) Impl_->Ensure(o, i); }
void MetaCoreScriptRuntime::FixedUpdate(double delta) { if (!Impl_->Running) return; Time_.FixedDeltaSeconds = std::max(0.0, delta); Impl_->Dispatch(Impl::Phase::Fixed, Time_.FixedDeltaSeconds); }
@ -585,6 +587,7 @@ void MetaCoreScriptRuntime::Publish(std::string topic, std::vector<MetaCoreScrip
void MetaCoreScriptRuntime::PublishToObject(MetaCoreId object, std::string topic, std::vector<MetaCoreScriptValueV1> fields) { Impl_->Events.push_back({object, std::move(topic), std::move(fields)}); }
MetaCoreRuntimeInput* MetaCoreScriptRuntime::GetRuntimeInput() const { return Impl_->Input; }
MetaCorePhysicsWorld* MetaCoreScriptRuntime::GetPhysicsWorld() const { return Impl_->Physics; }
MetaCoreAnimationRuntime* MetaCoreScriptRuntime::GetAnimationRuntime() const { return Impl_->Animation; }
std::uint64_t MetaCoreScriptRuntime::ScheduleTimer(MetaCoreId object, std::uint64_t instance, double delay, double interval, bool unscaled, std::function<void()> callback) { const auto id = Impl_->NextTimer++; Impl_->Timers.push_back({id, object, instance, std::max(0.0, delay), std::max(0.0, interval), unscaled, std::move(callback)}); return id; }
void MetaCoreScriptRuntime::CancelTimer(std::uint64_t id) { std::erase_if(Impl_->Timers, [id](const auto& t) { return t.Id == id; }); }
bool MetaCoreScriptRuntime::IsHandleValid(MetaCoreScriptObjectHandleV1 handle) const { return handle.WorldGeneration == WorldGeneration_ && handle.ObjectId != 0 && static_cast<bool>(Impl_->Scene.FindGameObject(handle.ObjectId)); }
@ -669,7 +672,21 @@ bool HPhysicsJump(void* c,MetaCoreScriptObjectHandleV1 h,double speed) noexcept
bool HPhysicsGrounded(void* c,MetaCoreScriptObjectHandleV1 h,bool* out) noexcept { try { auto* r=static_cast<MetaCoreScriptRuntime*>(c); if(!r||!out||!r->IsHandleValid(h)||!r->GetPhysicsWorld())return false; *out=r->GetPhysicsWorld()->IsCharacterGrounded(h.ObjectId); return true;}catch(...){return false;} }
bool HPhysicsConstraintEnabled(void* c,MetaCoreScriptObjectHandleV1 h,bool enabled) noexcept { try { auto* r=static_cast<MetaCoreScriptRuntime*>(c); return r&&r->IsHandleValid(h)&&r->GetPhysicsWorld()&&r->GetPhysicsWorld()->SetConstraintEnabled(h.ObjectId,enabled);}catch(...){return false;} }
bool HPhysicsConstraintMotor(void* c,MetaCoreScriptObjectHandleV1 h,bool enabled,double velocity,double impulse) noexcept { try { auto* r=static_cast<MetaCoreScriptRuntime*>(c); return r&&r->IsHandleValid(h)&&r->GetPhysicsWorld()&&r->GetPhysicsWorld()->SetConstraintMotor(h.ObjectId,enabled,static_cast<float>(velocity),static_cast<float>(impulse));}catch(...){return false;} }
const MetaCoreScriptHostApiV1& HostApi() { static const MetaCoreScriptHostApiV1 api{METACORE_SCRIPT_ABI_MAJOR, METACORE_SCRIPT_ABI_MINOR, METACORE_SCRIPT_HOST_CAPABILITIES, HValid, HHas, HGet, HSet, HGetField, HSetField, HLog, HPublish, HSubscribe, HUnsubscribe, HSchedule, HCancel, HInputAction, HInputAxis, HInputContext, HInputRebind, HInputReset, HPhysicsRay, HPhysicsShape, HPhysicsOverlap, HPhysicsForce, HPhysicsImpulse, HPhysicsGetVelocity, HPhysicsSetVelocity, HPhysicsWake, HPhysicsMove, HPhysicsJump, HPhysicsGrounded, HPhysicsConstraintEnabled, HPhysicsConstraintMotor}; return api; }
MetaCoreAnimationRuntime* HAnimation(void* c, MetaCoreScriptObjectHandleV1 h) { auto* r=static_cast<MetaCoreScriptRuntime*>(c);return r&&r->IsHandleValid(h)?r->GetAnimationRuntime():nullptr; }
bool HAnimationBool(void*c,MetaCoreScriptObjectHandleV1 h,const char*p,bool v) noexcept{try{auto*a=HAnimation(c,h);return a&&p&&a->SetBool(h.ObjectId,p,v);}catch(...){return false;}}
bool HAnimationInt(void*c,MetaCoreScriptObjectHandleV1 h,const char*p,std::int64_t v) noexcept{try{auto*a=HAnimation(c,h);return a&&p&&a->SetInt(h.ObjectId,p,v);}catch(...){return false;}}
bool HAnimationFloat(void*c,MetaCoreScriptObjectHandleV1 h,const char*p,double v) noexcept{try{auto*a=HAnimation(c,h);return a&&p&&a->SetFloat(h.ObjectId,p,static_cast<float>(v));}catch(...){return false;}}
bool HAnimationTrigger(void*c,MetaCoreScriptObjectHandleV1 h,const char*p) noexcept{try{auto*a=HAnimation(c,h);return a&&p&&a->SetTrigger(h.ObjectId,p);}catch(...){return false;}}
bool HAnimationPlay(void*c,MetaCoreScriptObjectHandleV1 h,const char*s,double n) noexcept{try{auto*a=HAnimation(c,h);return a&&s&&a->Play(h.ObjectId,s,static_cast<float>(n));}catch(...){return false;}}
bool HAnimationCrossFade(void*c,MetaCoreScriptObjectHandleV1 h,const char*s,double d,double n) noexcept{try{auto*a=HAnimation(c,h);return a&&s&&a->CrossFade(h.ObjectId,s,static_cast<float>(d),static_cast<float>(n));}catch(...){return false;}}
bool HAnimationState(void*c,MetaCoreScriptObjectHandleV1 h,MetaCoreScriptAnimatorStateV1*out) noexcept{try{auto*a=HAnimation(c,h);if(!a||!out)return false;auto state=a->GetDebugSnapshot(h.ObjectId);if(!state||state->Layers.empty())return false;*out={};std::snprintf(out->StateId,sizeof(out->StateId),"%s",state->Layers.front().StateId.c_str());out->NormalizedTime=state->Layers.front().NormalizedTime;out->TransitionWeight=state->Layers.front().TransitionWeight;out->Playing=state->Playing;return true;}catch(...){return false;}}
bool HTimelinePlay(void*c,MetaCoreScriptObjectHandleV1 h) noexcept{try{auto*a=HAnimation(c,h);return a&&a->TimelinePlay(h.ObjectId);}catch(...){return false;}}
bool HTimelinePause(void*c,MetaCoreScriptObjectHandleV1 h) noexcept{try{auto*a=HAnimation(c,h);return a&&a->TimelinePause(h.ObjectId);}catch(...){return false;}}
bool HTimelineStop(void*c,MetaCoreScriptObjectHandleV1 h) noexcept{try{auto*a=HAnimation(c,h);return a&&a->TimelineStop(h.ObjectId);}catch(...){return false;}}
bool HTimelineSeek(void*c,MetaCoreScriptObjectHandleV1 h,double s) noexcept{try{auto*a=HAnimation(c,h);return a&&a->TimelineSeek(h.ObjectId,static_cast<float>(s));}catch(...){return false;}}
bool HTimelineSpeed(void*c,MetaCoreScriptObjectHandleV1 h,double s) noexcept{try{auto*a=HAnimation(c,h);return a&&a->TimelineSetSpeed(h.ObjectId,static_cast<float>(s));}catch(...){return false;}}
bool HAnimationConsumeRootMotion(void*c,MetaCoreScriptObjectHandleV1 h,MetaCoreScriptRootMotionV1*out) noexcept{try{auto*a=HAnimation(c,h);if(!a||!out)return false;const auto delta=a->ConsumeRootMotion(h.ObjectId);if(!delta)return false;out->Translation[0]=delta->Translation.x;out->Translation[1]=delta->Translation.y;out->Translation[2]=delta->Translation.z;out->Rotation[0]=delta->Rotation.w;out->Rotation[1]=delta->Rotation.x;out->Rotation[2]=delta->Rotation.y;out->Rotation[3]=delta->Rotation.z;return true;}catch(...){return false;}}
const MetaCoreScriptHostApiV1& HostApi() { static const MetaCoreScriptHostApiV1 api{METACORE_SCRIPT_ABI_MAJOR, METACORE_SCRIPT_ABI_MINOR, METACORE_SCRIPT_HOST_CAPABILITIES, HValid, HHas, HGet, HSet, HGetField, HSetField, HLog, HPublish, HSubscribe, HUnsubscribe, HSchedule, HCancel, HInputAction, HInputAxis, HInputContext, HInputRebind, HInputReset, HPhysicsRay, HPhysicsShape, HPhysicsOverlap, HPhysicsForce, HPhysicsImpulse, HPhysicsGetVelocity, HPhysicsSetVelocity, HPhysicsWake, HPhysicsMove, HPhysicsJump, HPhysicsGrounded, HPhysicsConstraintEnabled, HPhysicsConstraintMotor, HAnimationBool, HAnimationInt, HAnimationFloat, HAnimationTrigger, HAnimationPlay, HAnimationCrossFade, HAnimationState, HTimelinePlay, HTimelinePause, HTimelineStop, HTimelineSeek, HTimelineSpeed, HAnimationConsumeRootMotion}; return api; }
} // namespace
bool MetaCoreGenerateScriptProject(const std::filesystem::path& root, std::string_view projectName, const std::filesystem::path& sdkRoot, std::string* error) {

View File

@ -12,15 +12,16 @@
extern "C" {
inline constexpr std::uint32_t METACORE_SCRIPT_ABI_MAJOR = 1U;
inline constexpr std::uint32_t METACORE_SCRIPT_ABI_MINOR = 2U;
inline constexpr std::uint32_t METACORE_SCRIPT_ABI_MINOR = 4U;
inline constexpr std::uint64_t METACORE_SCRIPT_CAP_PROPERTIES = 1ULL << 0U;
inline constexpr std::uint64_t METACORE_SCRIPT_CAP_EVENTS = 1ULL << 1U;
inline constexpr std::uint64_t METACORE_SCRIPT_CAP_TIMERS = 1ULL << 2U;
inline constexpr std::uint64_t METACORE_SCRIPT_CAP_INPUT = 1ULL << 3U;
inline constexpr std::uint64_t METACORE_SCRIPT_CAP_PHYSICS = 1ULL << 4U;
inline constexpr std::uint64_t METACORE_SCRIPT_CAP_ANIMATION = 1ULL << 5U;
inline constexpr std::uint64_t METACORE_SCRIPT_HOST_CAPABILITIES =
METACORE_SCRIPT_CAP_PROPERTIES | METACORE_SCRIPT_CAP_EVENTS | METACORE_SCRIPT_CAP_TIMERS | METACORE_SCRIPT_CAP_INPUT |
METACORE_SCRIPT_CAP_PHYSICS;
METACORE_SCRIPT_CAP_PHYSICS | METACORE_SCRIPT_CAP_ANIMATION;
enum MetaCoreScriptAbiValueType : std::uint32_t {
MetaCoreScriptAbiValue_None = 0,
@ -76,6 +77,18 @@ struct MetaCoreScriptPhysicsQueryShapeV1 {
double Height;
};
struct MetaCoreScriptAnimatorStateV1 {
char StateId[128];
double NormalizedTime;
double TransitionWeight;
bool Playing;
};
struct MetaCoreScriptRootMotionV1 {
double Translation[3];
double Rotation[4]; // w, x, y, z
};
struct MetaCoreScriptExecutionContextV1;
using MetaCoreScriptTimerCallbackV1 = const char* (*)(void*, const MetaCoreScriptExecutionContextV1*) noexcept;
using MetaCoreScriptEventCallbackV1 = const char* (*)(void*, const MetaCoreScriptExecutionContextV1*, const char*, const MetaCoreScriptValueV1*, std::size_t) noexcept;
@ -114,6 +127,19 @@ struct MetaCoreScriptHostApiV1 {
bool (*PhysicsCharacterGrounded)(void*, MetaCoreScriptObjectHandleV1, bool*) noexcept;
bool (*PhysicsSetConstraintEnabled)(void*, MetaCoreScriptObjectHandleV1, bool) noexcept;
bool (*PhysicsSetConstraintMotor)(void*, MetaCoreScriptObjectHandleV1, bool, double, double) noexcept;
bool (*AnimationSetBool)(void*, MetaCoreScriptObjectHandleV1, const char*, bool) noexcept;
bool (*AnimationSetInt)(void*, MetaCoreScriptObjectHandleV1, const char*, std::int64_t) noexcept;
bool (*AnimationSetFloat)(void*, MetaCoreScriptObjectHandleV1, const char*, double) noexcept;
bool (*AnimationSetTrigger)(void*, MetaCoreScriptObjectHandleV1, const char*) noexcept;
bool (*AnimationPlay)(void*, MetaCoreScriptObjectHandleV1, const char*, double) noexcept;
bool (*AnimationCrossFade)(void*, MetaCoreScriptObjectHandleV1, const char*, double, double) noexcept;
bool (*AnimationGetState)(void*, MetaCoreScriptObjectHandleV1, MetaCoreScriptAnimatorStateV1*) noexcept;
bool (*TimelinePlay)(void*, MetaCoreScriptObjectHandleV1) noexcept;
bool (*TimelinePause)(void*, MetaCoreScriptObjectHandleV1) noexcept;
bool (*TimelineStop)(void*, MetaCoreScriptObjectHandleV1) noexcept;
bool (*TimelineSeek)(void*, MetaCoreScriptObjectHandleV1, double) noexcept;
bool (*TimelineSetSpeed)(void*, MetaCoreScriptObjectHandleV1, double) noexcept;
bool (*AnimationConsumeRootMotion)(void*, MetaCoreScriptObjectHandleV1, MetaCoreScriptRootMotionV1*) noexcept;
};
struct MetaCoreScriptExecutionContextV1 {

View File

@ -12,6 +12,7 @@
#include <vector>
namespace MetaCore {
class MetaCoreAnimationRuntime;
enum class MetaCoreScriptFieldType {
Bool = 0,
@ -118,7 +119,7 @@ public:
class MetaCoreScriptRuntime {
public:
MetaCoreScriptRuntime(MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger = {},
MetaCoreRuntimeInput* input = nullptr, MetaCorePhysicsWorld* physics = nullptr);
MetaCoreRuntimeInput* input = nullptr, MetaCorePhysicsWorld* physics = nullptr, MetaCoreAnimationRuntime* animation = nullptr);
~MetaCoreScriptRuntime();
void Start();
@ -138,6 +139,7 @@ public:
void PublishToObject(MetaCoreId objectId, std::string topic, std::vector<MetaCoreScriptValueV1> fields = {});
[[nodiscard]] MetaCoreRuntimeInput* GetRuntimeInput() const;
[[nodiscard]] MetaCorePhysicsWorld* GetPhysicsWorld() const;
[[nodiscard]] MetaCoreAnimationRuntime* GetAnimationRuntime() const;
std::uint64_t ScheduleTimer(MetaCoreId objectId, std::uint64_t instanceId, double delay, double interval, bool unscaled, std::function<void()> callback);
void CancelTimer(std::uint64_t timerId);

View File

@ -0,0 +1,17 @@
# emit_after_seconds data_point_id value_type payload
0.00 cube.position vec3 0.0 0.5 0.0
0.00 cube.visible bool true
0.00 cube.base_color vec3 0.4 0.6 0.9
0.00 valve.visible bool true
0.00 tank.base_color vec3 0.35 0.65 0.90
0.00 alarm.intensity double 0.0
0.50 cube.position vec3 1.5 0.5 0.0
0.50 cube.base_color vec3 0.8 0.6 0.5
0.50 tank.base_color vec3 0.20 0.80 0.25
0.50 alarm.intensity double 2.0
1.00 cube.visible bool false
1.00 valve.visible bool false
1.50 cube.position vec3 -1.5 0.5 0.0
1.50 cube.visible bool true
1.50 valve.visible bool true
1.50 alarm.intensity double 0.0

View File

@ -0,0 +1,58 @@
[
{
"compiled_sha256": "f7cb87c6d31a5dfe1779dbedda64f141ec7f33d4e4f16d738f34c74e7895c693",
"cook_key": "98e1fa5d2906f1fa982107b3e395eacedc4fcc80e194e039b134d42db8e50292",
"file": "pbr.filamat",
"source_sha256": "418c899f336635d12a1cff1e8a746c64e5a676638925a0f076858a85d2c85579",
"template_id": "metacore.pbr",
"template_version": 1
},
{
"compiled_sha256": "4f6abb4a484bbb6563cc2c2714fd2499347a1f04d1577a67da30b40c2fed3abf",
"cook_key": "ee7229b2c23c75972f5c2f08e3fc9bc56adc8709adfaeae04900abb20e6a7e91",
"file": "unlit.filamat",
"source_sha256": "a0f01bd6851210fbf96140cbd4cbe9e8a80391e67fc4918cc9f01be474ef069d",
"template_id": "metacore.unlit",
"template_version": 1
},
{
"compiled_sha256": "6ebb8b2bb6957069e9e644c24911ca90e0ee1f99b54fbc9add20ef5de5e73230",
"cook_key": "a47c39e765a150442e386352d047f53d89f9d7ec434ed8676af018dbd4771ebe",
"file": "transparent.filamat",
"source_sha256": "4cb283be0476ae63c1997fc3a3f2414118c4fb6b9dff6860e0dea38d87358fc5",
"template_id": "metacore.transparent",
"template_version": 1
},
{
"compiled_sha256": "d46844699e73c363ebd39bbab323edd5cdf11f6f53a54639c724e0ad74aec6fa",
"cook_key": "10c2c7b2c34cc18016c4d73c80a7d8157fe3f5d14bbae70438d178a2146e9426",
"file": "decal.filamat",
"source_sha256": "cfc7fd4397c4086daa69e55e7a9ec9d958ce5722b109bbe59281d631e01bd753",
"template_id": "metacore.decal",
"template_version": 1
},
{
"compiled_sha256": "ab753f15f54d303119588a5cc6fa396769b1bab8262da3dfe3be4794272edcd1",
"cook_key": "60e3df9dc23924bddac11d05375dc93758be02d6277ce750ae8a59353f193906",
"file": "terrain.filamat",
"source_sha256": "41bf468737eeab2ca07c63bae756ec2917fefd60b65d83a32ef31631c8e8ef30",
"template_id": "metacore.terrain",
"template_version": 1
},
{
"compiled_sha256": "47a2bb71172388c2d02d9d2d6e783722e3ca807fd0218e362b3b2480b847ee32",
"cook_key": "fe614bfa274a7e31a9608aca704fed0d5b331fc0cdcfc08f348c062246c9284b",
"file": "particle.filamat",
"source_sha256": "bb153954e23a9adf348898f3d32bfb8256566d61bd9548bab55cf12779ed08b6",
"template_id": "metacore.particle",
"template_version": 1
},
{
"compiled_sha256": "94b8a06134d06d496eaf5422a58993d1e8b5d29eb5e326be3515f6cda95618d0",
"cook_key": "d7f450b22d24ff0d7652dc2528b9b04d2ab03a24b43f252d98eed09a29f9bd7d",
"file": "line.filamat",
"source_sha256": "ed04d10a025bb5d2119d727c3cb73dd16460d94acfb2febef9981b0ea7774804",
"template_id": "metacore.line",
"template_version": 1
}
]

View File

@ -0,0 +1,281 @@
{
"build_id": "20260722T094256364Z",
"configuration": "Development",
"engine_version": "1.0.0",
"files": [
{
"path": "Content/Assets/0e6c871d-52c4-4af3-ae1b-575b4eede7dc_097cd96858153eb4.mccooked",
"required": true,
"role": "asset",
"sha256": "54adcf9934655bdd09cf280a5891c8c832df7e1a749abab5ea70ceb20a78a6d2",
"size": 7919
},
{
"path": "Content/Assets/75068577-37e3-4a9d-954a-54251c667933_6ce8a8b3807d68c9.mccooked",
"required": true,
"role": "asset",
"sha256": "33048ff5dab5b6a82bd453fed55d2991cbee6d5a9037c1e11877b0f1bbaaad5c",
"size": 1036
},
{
"path": "Content/Assets/7e3d10d5-d46f-4622-9615-25ce0abef1e7_f4236ee9e4658574.mccooked",
"required": true,
"role": "asset",
"sha256": "4d2b5251a2ab1f2becff64309cf6a0119df03bbbf6aed524b2cfb35bf0ba6712",
"size": 136
},
{
"path": "Content/Assets/83429142-44ec-4b15-bf3e-bac36bc8fc7c_74a8e34b32d57f49.mccooked",
"required": true,
"role": "asset",
"sha256": "df9ee4db676121ba83e3e75fd14b5815764f32906b9894f0fe4b70f0e54d75b8",
"size": 1440
},
{
"path": "Content/Assets/9bd0aa89-1f68-4a4d-b9ed-1fcf1dc6180d_58f989d519fac13e.mccooked",
"required": true,
"role": "asset",
"sha256": "715d49df1d2d19f3c4cc8d47aa57b829550ae1374018da78df7b6ea81ba768c4",
"size": 162
},
{
"path": "Content/Assets/CookManifest.bin",
"required": true,
"role": "asset",
"sha256": "ee0fc94b07d976eee6f4d97fac029041c7b53f8c2b65cfc22cab17bdede91a93",
"size": 2352
},
{
"path": "Content/Assets/d4f241ff-2970-45f8-a6d7-5a4441980a89_552cc0be8542c78b.mccooked",
"required": true,
"role": "asset",
"sha256": "7772917c29810ee2d1e1fc30742bd1f5fe0c6ab93ec1b0b877a48d1217dc8292",
"size": 1440
},
{
"path": "Content/Assets/e85ce6d3-12a9-475a-a224-9da4035ce22a_ec0723e3020e0bf5.mccooked",
"required": true,
"role": "asset",
"sha256": "80a95d1bb663ff0a5e81a319f5234b125b7c1f5e4be084bdeac4acc6a48f2d59",
"size": 1936
},
{
"path": "Content/Physics/9cd50d41-df30-26ff-da7e-e01bb57f7135.mccollision",
"required": true,
"role": "content",
"sha256": "9a74b2290145b5d82d5e4b898fef58774ab6362f59d62effff3e2616d8a148e4",
"size": 448
},
{
"path": "Content/Physics/a718a4d3-f5ce-7f3a-6fe6-877661f2ea55.mccollision",
"required": true,
"role": "content",
"sha256": "0947ed4346e9ea1ee17114e046818cab3a3a78383231e1720436d51d118b6a0f",
"size": 88
},
{
"path": "Content/Runtime/Bindings.mcruntime",
"required": true,
"role": "runtime_config",
"sha256": "3e35327671dd79981e653348ae3c09f0d256a65610166acc25f4a100bfd71134",
"size": 1316
},
{
"path": "Content/Runtime/DataSources.mcruntime",
"required": true,
"role": "runtime_config",
"sha256": "72b5958c338f5486fe46bc94981748d9265df48e204fac5d19d7c1dbbf15d846",
"size": 1487
},
{
"path": "Content/Runtime/Diagnostics.mcruntimestate",
"required": true,
"role": "runtime_config",
"sha256": "b20338c44262960e2642b53133c2abdebcd14bbea94554b6a1f3cb2d9851a70b",
"size": 1609
},
{
"path": "Content/Runtime/Input.mcruntime",
"required": true,
"role": "runtime_config",
"sha256": "475c31d37aae7bd9de8484c95c7ac521da7287c484ca5e4eabf0cc636a3b0e6a",
"size": 3777
},
{
"path": "Content/Runtime/Physics.mcruntime",
"required": true,
"role": "runtime_config",
"sha256": "ce69a566e1d7f8ed96b16f03ffe4472cea17df4f7fd7708e917aa675a717457d",
"size": 887
},
{
"path": "Content/Runtime/ProjectRuntime.mcruntimecfg",
"required": true,
"role": "runtime_config",
"sha256": "3d428a31613ee8f93fde13dcc76d6b40df29856543ac03dee6e72e832f6a0e50",
"size": 507
},
{
"path": "Content/Runtime/Rendering.mcruntime",
"required": true,
"role": "runtime_config",
"sha256": "7d4f311f2ad3539e971848c05c287627319c697e6e7fec36c40db5582d2c997e",
"size": 608
},
{
"path": "Content/Runtime/RuntimeReplay.mcstream",
"required": true,
"role": "runtime_config",
"sha256": "63dad81dd38c910aa3146bfc278b4af03600f586b54eeddd081c92c806defcb7",
"size": 590
},
{
"path": "Content/Scenes/Main.mcscene",
"required": true,
"role": "scene",
"sha256": "faaa3d5f8d6f4f9dc4c193111f2a55d5b795f84399571380ad56eb4a1a4b7a41",
"size": 1854
},
{
"path": "Engine/Fonts/Roboto-Medium.ttf",
"required": true,
"role": "font",
"sha256": "f205cc511821ea56078a105557fcea6253129404d411c997e1866fbd006abb68",
"size": 172064
},
{
"path": "Engine/Materials/Templates/decal.filamat",
"required": true,
"role": "engine_material",
"sha256": "d46844699e73c363ebd39bbab323edd5cdf11f6f53a54639c724e0ad74aec6fa",
"size": 13559
},
{
"path": "Engine/Materials/Templates/line.filamat",
"required": true,
"role": "engine_material",
"sha256": "94b8a06134d06d496eaf5422a58993d1e8b5d29eb5e326be3515f6cda95618d0",
"size": 13945
},
{
"path": "Engine/Materials/Templates/particle.filamat",
"required": true,
"role": "engine_material",
"sha256": "47a2bb71172388c2d02d9d2d6e783722e3ca807fd0218e362b3b2480b847ee32",
"size": 13944
},
{
"path": "Engine/Materials/Templates/pbr.filamat",
"required": true,
"role": "engine_material",
"sha256": "f7cb87c6d31a5dfe1779dbedda64f141ec7f33d4e4f16d738f34c74e7895c693",
"size": 131004
},
{
"path": "Engine/Materials/Templates/templates.manifest.json",
"required": true,
"role": "engine_material",
"sha256": "fb536493f25d72a24eb4e7f0e58b1414f2b0f857eb7ad884b4a034305a906d2d",
"size": 2574
},
{
"path": "Engine/Materials/Templates/terrain.filamat",
"required": true,
"role": "engine_material",
"sha256": "ab753f15f54d303119588a5cc6fa396769b1bab8262da3dfe3be4794272edcd1",
"size": 130925
},
{
"path": "Engine/Materials/Templates/transparent.filamat",
"required": true,
"role": "engine_material",
"sha256": "6ebb8b2bb6957069e9e644c24911ca90e0ee1f99b54fbc9add20ef5de5e73230",
"size": 13561
},
{
"path": "Engine/Materials/Templates/unlit.filamat",
"required": true,
"role": "engine_material",
"sha256": "4f6abb4a484bbb6563cc2c2714fd2499347a1f04d1577a67da30b40c2fed3abf",
"size": 13382
},
{
"path": "Engine/Materials/grid.filamat",
"required": true,
"role": "engine_material",
"sha256": "d5ee20841d2728a0ef4709c9db2d1f321908cf2ed8855cae7dbbf2f827128071",
"size": 14081
},
{
"path": "Engine/Materials/render_debug.filamat",
"required": true,
"role": "engine_material",
"sha256": "7eaa579b02016f36353e700011578a1218dc7592b0a34eb37423671e3ecbcbe7",
"size": 20403
},
{
"path": "Engine/Materials/rml_ui.filamat",
"required": true,
"role": "engine_material",
"sha256": "5de2707bec486f0483d36c7d3fdc6db3dc6959d90b59358bb46fe9b9abc6e3d1",
"size": 14589
},
{
"path": "Engine/Materials/uiBlit.filamat",
"required": true,
"role": "engine_material",
"sha256": "c752428c62326072c3a42166b3d85f2d2471b183512cd603f80ed2c823301d18",
"size": 17926
},
{
"path": "MetaCorePlayer",
"required": true,
"role": "player",
"sha256": "24af2d52222e96d570a899e62c4c3ed1094c7646eb2f184240592e59ff30cb66",
"size": 136078360
},
{
"path": "Project/MetaCore.runtimeproject",
"required": true,
"role": "runtime_project",
"sha256": "fe2d4e953aa2052437934d51114a2a20fc92ce095598c836e3877f5b1539b93b",
"size": 390
},
{
"path": "lib/libc++.so.1",
"required": true,
"role": "runtime_library",
"sha256": "09d9cbcb21aa1d9b3ee6251e4061127fb87775baaf7ef08d08fbbf048dfe51b5",
"size": 983088
},
{
"path": "lib/libc++abi.so.1",
"required": true,
"role": "runtime_library",
"sha256": "03f655e4cee52258bfd8cdcafc0f07ab492b8eeb74fbe6d363d5c69de4380562",
"size": 219832
},
{
"path": "lib/libunwind.so.1",
"required": true,
"role": "runtime_library",
"sha256": "ff8bb59f87b293a0bf4377f3229a7573ef88d9b2d5737928c37aff521b1a5bdf",
"size": 51664
}
],
"format_version": 1,
"git_commit": "2512e3ade63ac0ebe4c33110c15eef0daf2f133f",
"graphics_backend": "OpenGL",
"package_format_version": 1,
"project_name": "MetaCoreTest",
"project_version": "0.1.0",
"runtime_abi_major": 1,
"runtime_abi_minor": 0,
"system_dependencies": [
"Ubuntu 22.04 x86_64",
"glibc >= 2.35",
"OpenGL/GLX",
"X11"
],
"target_platform": "Linux-x86_64"
}

View File

@ -0,0 +1,12 @@
{
"build_id": "20260722T094256364Z",
"content_root": "Content",
"format_version": 1,
"release_manifest": "Manifest/MetaCore.release.json",
"runtime_abi_major": 1,
"runtime_abi_minor": 0,
"runtime_config": "Content/Runtime/ProjectRuntime.mcruntimecfg",
"script_modules": [],
"startup_scene": "Content/Scenes/Main.mcscene",
"ui_manifest": "Content/Runtime/Ui.mcruntime"
}

View File

@ -304,14 +304,33 @@ R7.1R7.4 的代码交付范围已经接通;问题 7 的功能清单在本
### 8. 动画系统
当前 Clip 播放基础之上,需要逐步增加:
关闭状态:**实现完成,签收待补(暂不关闭)**。
- [ ] Animator Controller 和状态机。
- [ ] Cross Fade、Blend Tree、Layer 和 Mask。
- [ ] 骨骼 Avatar、动画重定向和 Root Motion。
- [ ] 动画事件、参数驱动和脚本控制 API。
- [ ] 动画预览、时间轴和状态调试器。
- [ ] 机械装配动画和 Timeline 类编排能力。
- [x] Animator Controller 和状态机。
- [x] Cross Fade、1D/2D Blend Tree、Override/Additive Layer 和骨骼 Mask。
- [x] 通用骨骼 Avatar、Bind Pose 差值重定向和四种 Root Motion 模式。
- [x] 动画事件、参数驱动、Trigger 语义及原生脚本 Animation Capability。
- [x] 动画/Avatar/Controller/Timeline 编辑面板、模型预览和 Play Mode 状态调试器。
- [x] Transform、Animator、激活和 Signal 多轨工业 Timeline。
R8.1R8.5 已落地的工程边界:
- 新增 Editor/Player 共用的 `MetaCoreAnimation` 模块,以及带 Schema/Generator Version 的 `.mcanim`、`.mcavatar`、`.mcanimator`、`.mctimeline`。glTF 重导入以 Stable Import Key 保持 Clip GUID导入完整 TRS、Step/Linear/Cubic Spline、骨架层级和 Bind Pose。
- Scene 组件只保存 Controller/Avatar GUID、启动和更新策略、Root Motion 设置运行实例、参数、状态、Pose、事件及 Timeline 时间均在运行时模块中。编辑态预览写入瞬态 Pose不修改 Scene Revision 或持久化组件。
- 状态机实现确定性 Transition 排序、Exit Time、秒数/归一化 Cross Fade、中断与 Trigger 消费;混合实现 1D、2D Cartesian 边界投影、2D Directional、Layer、Mask 和 Additive 参考 Pose。首版仍按计划拒绝负速度、嵌套状态机和任意可编程混合节点。
- Avatar 以稳定骨骼路径和显式映射为准校验层级、映射、Bind Transform 与骨架签名Root Motion 支持轴遮罩,并检测 CharacterController/Fixed Update、Dynamic RigidBody 和 Timeline Transform 轨冲突。
- 动画事件、状态进入/退出和 Timeline Signal 共用确定性事件队列,在动画求值后转发脚本事件总线;脚本 ABI 可设置 Bool/Int/Float/Trigger、Play/CrossFade、查询状态/归一化时间、消费 Root Motion并控制 Timeline Play/Pause/Stop/Seek/Speed。
- Render Sync 传递“稳定节点路径 → 最终 Pose”Filament 桥只更新节点 Transform 和 Bone Matrix缺失/重复路径或签名错误会禁用实例并输出上下文诊断。Editor Play Mode 与独立 Player 使用同一资产库、运行时及更新顺序。
- 旧 Scene Animator 首次加载时会确定性创建单状态 Controller并在所有 Controller 写入成功后一次性提交组件改写;新保存只写 Controller 结构。Prefab Capture/Apply/Revert、复制、Undo 快照和 Cook 依赖扫描均已覆盖 Animator 与 Timeline 新字段。
- Cook 会验证 Controller/Clip/Avatar/Timeline 依赖、格式版本、循环/损坏资产、骨架签名和非法 Root Motion 组合;旧 Runtime 配置缺失的新字段只在交付暂存区生成默认配置,显式配置但缺失的文件仍使 Cook 失败。
2026-07-22 验证结果Linux Development 全量构建成功CTest 11/11 通过Delivery、Scripting、Runtime Input、Physics、Animation、Render、Smoke、Asset Pipeline 及基准);动画参考场景为 100 实例 × 100 骨骼 × 2 Layer180 帧 CPU p95 **2.11 ms**,低于 4 ms 门禁clean Development 包已生成且 `MetaCoreBuildTool validate` 通过。
尚未满足、因此不能正式关闭问题 8 的签收项:
- 基准尚未证明“稳态逐帧零堆分配”,也尚未建立以签收值 120% 为阈值的长期内存回归门禁。
- 计划中的完整端到端资产场景、Editor/Player Pose Hash 对照、动画最终画面黄金图回归,以及 Controller 图编辑器的复制粘贴和资产级 Undo/Redo 尚未形成自动化验收。
- 当前执行环境无图形会话,交付包完整性验证已通过,但独立 Player 实机启动在 `glfwInit` 处被环境阻断;仍需在 Linux x86_64 图形主机完成最终画面与交互签收。
### 9. UI 系统

View File

@ -0,0 +1,71 @@
#include "MetaCoreAnimation/MetaCoreAnimation.h"
#include "MetaCoreScene/MetaCoreScene.h"
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <iostream>
#include <vector>
int main() {
using namespace MetaCore;
const auto clipGuid = MetaCoreBuildDeterministicAnimationGuid("benchmark/clip");
const auto controllerGuid = MetaCoreBuildDeterministicAnimationGuid("benchmark/controller");
MetaCoreAnimationClipDocument clip;
clip.AssetGuid = clipGuid;
clip.Duration = 1.0F;
for (int bone = 0; bone < 100; ++bone) {
MetaCoreAnimationChannelDocument channel;
channel.NodePath = "Root/Bone_" + std::to_string(bone);
channel.Target = MetaCoreAnimationChannelTarget::Translation;
channel.Vec3Keys = {{0.0F, {0, 0, 0}}, {1.0F, {static_cast<float>(bone) * 0.001F, 0, 0}}};
clip.Channels.push_back(std::move(channel));
}
MetaCoreAnimatorControllerDocument controller;
controller.AssetGuid = controllerGuid;
controller.Parameters.push_back({"Speed", MetaCoreAnimatorParameterType::Float, false, 0, 0.5F});
for (int layerIndex = 0; layerIndex < 2; ++layerIndex) {
MetaCoreAnimatorStateDocument state;
state.Id = "Move";
state.Motion.Type = MetaCoreAnimatorMotionType::BlendTree1D;
state.Motion.ParameterX = "Speed";
state.Motion.Samples = {{"slow", clipGuid, 0.0F}, {"fast", clipGuid, 1.0F}};
MetaCoreAnimatorLayerDocument layer;
layer.Id = "Layer_" + std::to_string(layerIndex);
layer.DefaultStateId = state.Id;
layer.BlendMode = layerIndex == 0 ? MetaCoreAnimatorLayerBlendMode::Override : MetaCoreAnimatorLayerBlendMode::Additive;
layer.States.push_back(std::move(state));
controller.Layers.push_back(std::move(layer));
}
MetaCoreScene scene;
for (int index = 0; index < 100; ++index) {
auto object = scene.CreateGameObject("Animated_" + std::to_string(index));
MetaCoreAnimatorComponent animator;
animator.ControllerAssetGuid = controllerGuid;
object.AddComponent<MetaCoreAnimatorComponent>(animator);
}
MetaCoreAnimationRuntime runtime(scene,
[&](MetaCoreAssetGuid guid) -> std::optional<MetaCoreAnimationClipDocument> { return guid == clipGuid ? std::optional(clip) : std::nullopt; },
[&](MetaCoreAssetGuid guid) -> std::optional<MetaCoreAnimatorControllerDocument> { return guid == controllerGuid ? std::optional(controller) : std::nullopt; });
runtime.Start();
for (int warmup = 0; warmup < 30; ++warmup) runtime.Update(1.0 / 60.0);
std::vector<double> samples;
samples.reserve(180);
for (int frame = 0; frame < 180; ++frame) {
const auto begin = std::chrono::steady_clock::now();
runtime.Update(1.0 / 60.0);
samples.push_back(std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - begin).count());
}
std::sort(samples.begin(), samples.end());
const double p95 = samples[static_cast<std::size_t>(samples.size() * 0.95)];
std::cout << "AnimationBenchmark instances=100 bones=100 layers=2 p95_ms=" << p95 << '\n';
if (p95 > 4.0) {
std::cerr << "Animation CPU p95 exceeds the Linux x86_64 4 ms gate\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

View File

@ -0,0 +1,77 @@
#include "MetaCoreAnimation/MetaCoreAnimation.h"
#include "MetaCoreScene/MetaCoreScene.h"
#include <cmath>
#include <cstdlib>
#include <filesystem>
#include <iostream>
namespace {
void Expect(bool value, const char* message) { if (!value) { std::cerr << "MetaCoreAnimationTests: " << message << '\n'; std::exit(1); } }
bool Near(float a,float b,float e=.001F){return std::abs(a-b)<=e;}
}
int main() {
using namespace MetaCore;
const auto clipGuid=MetaCoreBuildDeterministicAnimationGuid("clip/walk");
const auto controllerGuid=MetaCoreBuildDeterministicAnimationGuid("controller/test");
const auto timelineGuid=MetaCoreBuildDeterministicAnimationGuid("timeline/test");
MetaCoreAnimationClipDocument clip;clip.AssetGuid=clipGuid;clip.Name="Walk";clip.Duration=1;clip.RootNodePath="Root";
MetaCoreAnimationChannelDocument channel;channel.NodePath="Root";channel.Target=MetaCoreAnimationChannelTarget::Translation;
channel.Vec3Keys={{0,{0,0,0}},{1,{10,0,0}}};clip.Channels.push_back(channel);
clip.Events.push_back({"half",.5F,"Footstep"});
Expect(MetaCoreValidateAnimationClip(clip),"valid clip rejected");
Expect(Near(MetaCoreSampleAnimationClip(clip,.25F).Nodes.at("Root").Translation.x,2.5F),"linear clip sampling failed");
Expect(Near(MetaCoreSampleAnimationClip(clip,1.0F).Nodes.at("Root").Translation.x,10.0F),"clip end pose was not held");
channel.Interpolation=MetaCoreAnimationInterpolation::Step;Expect(Near(MetaCoreSampleAnimationVec3(channel.Vec3Keys,.75F,channel.Interpolation).x,0),"step sampling failed");
MetaCoreAnimatorControllerDocument controller;controller.AssetGuid=controllerGuid;
controller.Parameters.push_back({"Run",MetaCoreAnimatorParameterType::Trigger});
MetaCoreAnimatorStateDocument idle;idle.Id="Idle";idle.Motion.ClipGuid=clipGuid;
MetaCoreAnimatorStateDocument run=idle;run.Id="Run";
MetaCoreAnimatorLayerDocument layer;layer.DefaultStateId="Idle";layer.States={idle,run};
MetaCoreAnimatorTransitionDocument transition;transition.Id="to-run";transition.SourceStateId="Idle";transition.TargetStateId="Run";transition.Conditions.push_back({"Run",MetaCoreAnimatorConditionMode::If});layer.Transitions.push_back(transition);controller.Layers.push_back(layer);
Expect(MetaCoreValidateAnimatorController(controller),"valid controller rejected");
MetaCoreTimelineDocument timeline;timeline.AssetGuid=timelineGuid;timeline.Duration=1;
MetaCoreTimelineTrackDocument track;track.Id="move";track.BindingSlot="Part";track.Type=MetaCoreTimelineTrackType::Transform;
MetaCoreTimelineKeyDocument k0,k1;k1.Time=1;k1.Transform.Translation={4,0,0};track.Keys={k0,k1};timeline.Tracks.push_back(track);
MetaCoreTimelineTrackDocument signal;signal.Id="signal";signal.BindingSlot="Part";signal.Type=MetaCoreTimelineTrackType::Signal;MetaCoreTimelineKeyDocument signalKey;signalKey.Time=.25F;signalKey.TextValue="ClampClosed";signal.Keys.push_back(signalKey);timeline.Tracks.push_back(signal);
Expect(MetaCoreValidateTimeline(timeline),"valid timeline rejected");
MetaCoreAvatarDocument runtimeAvatar;runtimeAvatar.AssetGuid=MetaCoreBuildDeterministicAnimationGuid("avatar/runtime");runtimeAvatar.RootBonePath="Target";runtimeAvatar.UniformScale=1.0F;
MetaCoreAvatarBoneDocument sourceBone;sourceBone.Path="Root";runtimeAvatar.Bones.push_back(sourceBone);
MetaCoreAvatarBoneDocument targetBone;targetBone.Path="Target";targetBone.BindPose.Translation={2,0,0};runtimeAvatar.Bones.push_back(targetBone);
runtimeAvatar.Mappings.push_back({"Root","Target"});Expect(MetaCoreValidateAvatar(runtimeAvatar),"runtime avatar rejected");
const auto temp=std::filesystem::temp_directory_path()/"metacore_animation_tests";std::error_code ec;std::filesystem::create_directories(temp,ec);
Expect(MetaCoreWriteAnimationClip(temp/"Walk.mcanim",clip)&&MetaCoreReadAnimationClip(temp/"Walk.mcanim").has_value(),"clip round trip failed");
Expect(MetaCoreWriteAnimatorController(temp/"Test.mcanimator",controller)&&MetaCoreReadAnimatorController(temp/"Test.mcanimator").has_value(),"controller round trip failed");
Expect(MetaCoreWriteTimeline(temp/"Test.mctimeline",timeline)&&MetaCoreReadTimeline(temp/"Test.mctimeline").has_value(),"timeline round trip failed");
MetaCoreScene scene;auto animated=scene.CreateGameObject("Animated");MetaCoreAnimatorComponent animator;animator.ControllerAssetGuid=controllerGuid;animator.AvatarAssetGuid=runtimeAvatar.AssetGuid;animator.RootMotionMode=MetaCoreRootMotionMode::ExtractOnly;animated.AddComponent<MetaCoreAnimatorComponent>(animator);
auto part=scene.CreateGameObject("Part");auto directorObject=scene.CreateGameObject("Director");MetaCoreTimelineDirectorComponent director;director.TimelineAssetGuid=timelineGuid;director.Bindings.push_back({"Part",part.GetId()});directorObject.AddComponent<MetaCoreTimelineDirectorComponent>(director);
int eventCount=0;MetaCoreAnimationRuntime runtime(scene,[&](MetaCoreAssetGuid guid)->std::optional<MetaCoreAnimationClipDocument>{return guid==clipGuid?std::optional(clip):std::nullopt;},[&](MetaCoreAssetGuid guid)->std::optional<MetaCoreAnimatorControllerDocument>{return guid==controllerGuid?std::optional(controller):std::nullopt;},[&](MetaCoreAssetGuid guid)->std::optional<MetaCoreAvatarDocument>{return guid==runtimeAvatar.AssetGuid?std::optional(runtimeAvatar):std::nullopt;},[&](MetaCoreAssetGuid guid)->std::optional<MetaCoreTimelineDocument>{return guid==timelineGuid?std::optional(timeline):std::nullopt;});
runtime.SetEventCallback([&](const auto&){++eventCount;});runtime.Start();runtime.Update(.5);
Expect(runtime.GetPose(animated.GetId())!=nullptr&&Near(runtime.GetPose(animated.GetId())->Nodes.at("Root").Translation.x,5),"runtime pose failed");
Expect(Near(runtime.GetPose(animated.GetId())->Nodes.at("Target").Translation.x,7),"bind-pose retargeting failed");
Expect(Near(part.GetComponent<MetaCoreTransformComponent>().Position.x,2),"timeline transform failed");
Expect(eventCount==2,"clip/timeline event dispatch failed");Expect(!runtime.SetBool(animated.GetId(),"Run",true),"trigger accepted through bool API");Expect(runtime.SetTrigger(animated.GetId(),"Run"),"trigger rejected");runtime.Update(.01);
const auto debug=runtime.GetDebugSnapshot(animated.GetId());Expect(debug&&debug->Layers.front().StateId=="Run","state transition failed");runtime.Stop();
MetaCoreScene loopScene;auto loopObject=loopScene.CreateGameObject("Loop");MetaCoreAnimatorComponent loopAnimator;loopAnimator.ControllerAssetGuid=controllerGuid;loopObject.AddComponent<MetaCoreAnimatorComponent>(loopAnimator);
MetaCoreAnimatorControllerDocument loopController=controller;loopController.Parameters.clear();loopController.Layers.front().States.resize(1);loopController.Layers.front().States.front().Id="Idle";loopController.Layers.front().DefaultStateId="Idle";loopController.Layers.front().Transitions.clear();
int loopEvents=0;MetaCoreAnimationRuntime loopRuntime(loopScene,[&](MetaCoreAssetGuid guid)->std::optional<MetaCoreAnimationClipDocument>{return guid==clipGuid?std::optional(clip):std::nullopt;},[&](MetaCoreAssetGuid guid)->std::optional<MetaCoreAnimatorControllerDocument>{return guid==controllerGuid?std::optional(loopController):std::nullopt;});loopRuntime.SetEventCallback([&](const auto&event){if(event.Type=="ClipEvent")++loopEvents;});loopRuntime.Start();loopRuntime.Update(2.6);Expect(loopEvents==3,"multi-loop event crossing was not dispatched exactly once per crossing");loopRuntime.Stop();
MetaCoreScene stoppedScene;auto stopped=stoppedScene.CreateGameObject("Stopped");MetaCoreAnimatorComponent stoppedAnimator;stoppedAnimator.ControllerAssetGuid=controllerGuid;stoppedAnimator.PlayOnStart=false;stopped.AddComponent<MetaCoreAnimatorComponent>(stoppedAnimator);MetaCoreAnimationRuntime stoppedRuntime(stoppedScene,[&](MetaCoreAssetGuid guid)->std::optional<MetaCoreAnimationClipDocument>{return guid==clipGuid?std::optional(clip):std::nullopt;},[&](MetaCoreAssetGuid guid)->std::optional<MetaCoreAnimatorControllerDocument>{return guid==controllerGuid?std::optional(loopController):std::nullopt;});stoppedRuntime.Start();stoppedRuntime.Update(.25);Expect(stoppedRuntime.GetPose(stopped.GetId())&&stoppedRuntime.GetPose(stopped.GetId())->Nodes.empty(),"PlayOnStart=false advanced pose");Expect(stoppedRuntime.Play(stopped.GetId(),"Idle"),"manual Play failed");stoppedRuntime.Update(.25);Expect(!stoppedRuntime.GetPose(stopped.GetId())->Nodes.empty(),"manual Play did not start animator");stoppedRuntime.Stop();
MetaCoreAvatarDocument avatar;avatar.AssetGuid=MetaCoreBuildDeterministicAnimationGuid("avatar");avatar.RootBonePath="Root";avatar.Bones.push_back({"Root",-1});
Expect(MetaCoreValidateAvatar(avatar),"valid generic avatar rejected");avatar.Bones.push_back({"Root",0});Expect(!MetaCoreValidateAvatar(avatar),"duplicate/cyclic avatar accepted");
MetaCoreAvatarDocument unorderedAvatar;unorderedAvatar.AssetGuid=MetaCoreBuildDeterministicAnimationGuid("avatar/unordered");unorderedAvatar.RootBonePath="Root";
unorderedAvatar.Bones.push_back({"Root/Child",1});unorderedAvatar.Bones.push_back({"Root",-1});
Expect(MetaCoreValidateAvatar(unorderedAvatar),"valid glTF child-before-parent ordering rejected");
unorderedAvatar.Bones[1].Parent=0;Expect(!MetaCoreValidateAvatar(unorderedAvatar),"cyclic avatar hierarchy accepted");
std::filesystem::remove_all(temp,ec);
std::cout<<"MetaCoreAnimationTests passed\n";return 0;
}

View File

@ -4513,7 +4513,7 @@ void MetaCoreTestGltfAnimationMetadataImport() {
{
const std::vector<float> values{
0.0F, 0.5F, 1.0F,
0.0F, 0.0F, 0.0F, 1.0F, 0.0F, 0.0F, 2.0F, 0.0F, 0.0F,
0.0F, 0.0F, 0.0F, 0.0F, 1.0F, 0.0F, 0.0F, 2.0F, 0.0F,
0.0F, 2.5F,
0.0F, 0.0F, 0.0F, 0.0F, 1.0F, 0.0F
};
@ -4542,27 +4542,33 @@ void MetaCoreTestGltfAnimationMetadataImport() {
<< " {\"bufferView\": 2, \"componentType\": 5126, \"count\": 2, \"type\": \"SCALAR\"},\n"
<< " {\"bufferView\": 3, \"componentType\": 5126, \"count\": 2, \"type\": \"VEC3\"}\n"
<< " ],\n"
<< " \"nodes\": [{\"name\": \"Root\", \"children\": [1]}, {\"name\": \"Joint\"}],\n"
<< " \"nodes\": [{\"name\": \"Root\", \"children\": [1]}, {\"name\": \"Joint\", \"translation\": [0, 1, 0]}],\n"
<< " \"scenes\": [{\"nodes\": [0]}],\n"
<< " \"scene\": 0,\n"
<< " \"animations\": [\n"
<< " {\"name\": \"Walk\", \"samplers\": [{\"input\": 0, \"output\": 1, \"interpolation\": \"LINEAR\"}], \"channels\": [{\"sampler\": 0, \"target\": {\"node\": 1, \"path\": \"translation\"}}]},\n"
<< " {\"name\": \"Armature|mixamo.com|Layer0\", \"samplers\": [{\"input\": 0, \"output\": 1, \"interpolation\": \"LINEAR\"}], \"channels\": [{\"sampler\": 0, \"target\": {\"node\": 1, \"path\": \"translation\"}}]},\n"
<< " {\"name\": \"Look\", \"samplers\": [{\"input\": 2, \"output\": 3, \"interpolation\": \"LINEAR\"}], \"channels\": [{\"sampler\": 0, \"target\": {\"node\": 0, \"path\": \"translation\"}}]}\n"
<< " ]\n"
<< "}\n";
}
std::vector<std::vector<std::byte>> meshPayloads;
std::vector<MetaCore::MetaCoreAnimationClipDocument> animationClips;
MetaCore::MetaCoreAvatarDocument animationAvatar;
const MetaCore::MetaCoreModelAssetDocument document = MetaCore::MetaCoreBuildImportedGltfAssetDocument(
gltfPath,
std::filesystem::path("Assets") / "AnimatedMetadata.gltf",
1234,
meshPayloads
meshPayloads,
{},
nullptr,
&animationClips,
&animationAvatar
);
MetaCoreExpect(document.Animations.size() == 2, "导入器应记录两个 glTF animation");
MetaCoreExpect(document.Animations[0].Name == "Walk", "第一个 animation 名称应保留");
MetaCoreExpect(document.Animations[0].StableImportKey == "animation:Walk:0", "第一个 animation stable key 应稳定");
MetaCoreExpect(document.Animations[0].Name == "Armature|mixamo.com|Layer0", "第一个 animation 名称应保留特殊字符");
MetaCoreExpect(document.Animations[0].StableImportKey == "animation:Armature|mixamo.com|Layer0:0", "第一个 animation stable key 应稳定");
MetaCoreExpect(std::abs(document.Animations[0].DurationSeconds - 1.0F) <= 0.0001F, "第一个 animation duration 应来自最大 key time");
MetaCoreExpect(document.Animations[0].ChannelCount == 1, "第一个 animation channel 数应正确");
MetaCoreExpect(document.Animations[0].SamplerCount == 1, "第一个 animation sampler 数应正确");
@ -4570,6 +4576,45 @@ void MetaCoreTestGltfAnimationMetadataImport() {
MetaCoreExpect(document.Animations[1].Name == "Look", "第二个 animation 名称应保留");
MetaCoreExpect(std::abs(document.Animations[1].DurationSeconds - 2.5F) <= 0.0001F, "第二个 animation duration 应正确");
MetaCoreExpect(document.Animations[1].TargetNodeIndices.size() == 1 && document.Animations[1].TargetNodeIndices[0] == 0, "第二个 animation target node 应正确");
MetaCoreExpect(animationClips.size() == 2, "导入器应生成两个运行时 Clip");
MetaCoreExpect(!animationClips[0].Channels.empty() && animationClips[0].Channels[0].Vec3Keys.size() == 3, "运行时 Clip 应保留完整关键帧");
const glm::vec3 convertedTranslation = animationClips[0].Channels[0].Vec3Keys.back().Value;
MetaCoreExpectVec3Near(convertedTranslation, glm::vec3(0.0F, 0.0F, -2.0F), "动画位移应转换到 MetaCore Z-up 空间");
const MetaCore::MetaCoreAnimationPose sampledPose =
MetaCore::MetaCoreSampleAnimationClip(animationClips[0], 0.5F);
MetaCoreExpect(sampledPose.Nodes.contains("Root"), "稀疏动画应补齐未动画根节点的 Bind Pose");
MetaCoreExpect(sampledPose.Nodes.contains("Root/Joint"), "动画 Pose 应包含目标骨骼");
if (const auto sampledJoint = sampledPose.Nodes.find("Root/Joint"); sampledJoint != sampledPose.Nodes.end()) {
MetaCoreExpectVec3Near(sampledJoint->second.Translation, glm::vec3(0.0F, 0.0F, -1.0F), "骨骼动画位移应保持 Z-up");
MetaCoreExpectVec3Near(sampledJoint->second.Scale, glm::vec3(1.0F), "未动画 Scale 应继承 Bind Pose");
}
const auto jointBone = std::find_if(animationAvatar.Bones.begin(), animationAvatar.Bones.end(), [](const auto& bone) {
return bone.Path == "Root/Joint";
});
MetaCoreExpect(jointBone != animationAvatar.Bones.end(), "Avatar 应包含 Joint");
if (jointBone != animationAvatar.Bones.end()) {
MetaCoreExpectVec3Near(jointBone->BindPose.Translation, glm::vec3(0.0F, 0.0F, -1.0F), "Bind Pose 应与动画使用相同 Z-up 空间");
}
const MetaCore::MetaCoreAssetGuid modelGuid = MetaCore::MetaCoreAssetGuid::Generate();
animationAvatar.ModelGuid = modelGuid;
const std::filesystem::path animationDirectory = tempDirectory / "AnimatedMetadata_Animations";
MetaCoreExpect(
MetaCore::MetaCoreWriteAvatar(animationDirectory / (animationAvatar.AssetGuid.ToString() + ".mcavatar"), animationAvatar),
"导入器生成的 Avatar 应可写入"
);
for (auto& animationClip : animationClips) {
animationClip.SourceModelGuid = modelGuid;
MetaCoreExpect(
MetaCore::MetaCoreWriteAnimationClip(
animationDirectory / (animationClip.AssetGuid.ToString() + ".mcanim"),
animationClip
),
"包含 Mixamo 特殊名称的动画 Clip 应可写入"
);
}
MetaCore::MetaCoreAnimationAssetLibrary animationLibrary(tempDirectory);
const auto mixamoClip = animationLibrary.FindClip(modelGuid, "animation:Armature|mixamo.com|Layer0:0");
MetaCoreExpect(mixamoClip.has_value(), "资产库应按完整 Stable Import Key 找到 Mixamo Clip");
std::filesystem::remove_all(tempDirectory);
}