动画优化
This commit is contained in:
parent
2512e3ade6
commit
3ba7b090d1
@ -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);
|
||||
|
||||
@ -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})
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
383
Source/MetaCoreAnimation/Private/MetaCoreAnimation.cpp
Normal file
383
Source/MetaCoreAnimation/Private/MetaCoreAnimation.cpp
Normal 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
|
||||
@ -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
|
||||
@ -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");
|
||||
|
||||
@ -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 迁移失败:找不到 Clip,object="+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;
|
||||
|
||||
@ -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®istry=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>());
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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_{};
|
||||
|
||||
@ -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 &&
|
||||
|
||||
@ -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});
|
||||
|
||||
@ -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}; };
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
||||
}
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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"
|
||||
}
|
||||
Binary file not shown.
@ -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"
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -304,14 +304,33 @@ R7.1–R7.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.1–R8.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 Layer,180 帧 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 系统
|
||||
|
||||
|
||||
71
tests/MetaCoreAnimationBenchmark.cpp
Normal file
71
tests/MetaCoreAnimationBenchmark.cpp
Normal 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;
|
||||
}
|
||||
77
tests/MetaCoreAnimationTests.cpp
Normal file
77
tests/MetaCoreAnimationTests.cpp
Normal 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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user