384 lines
61 KiB
C++
384 lines
61 KiB
C++
#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
|