From 1aa9a4905ea823bfa4192c718148ea874799d060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tur=C3=A1nszki=20J=C3=A1nos?= Date: Thu, 22 Sep 2022 18:13:46 +0200 Subject: [PATCH] Animation retargeting (#570) --- Editor/AnimationWindow.cpp | 225 ++++++++++++++++++++++++++++++++++++- Editor/AnimationWindow.h | 2 + features.txt | 2 + 3 files changed, 228 insertions(+), 1 deletion(-) diff --git a/Editor/AnimationWindow.cpp b/Editor/AnimationWindow.cpp index 196bb880d..635fd4e00 100644 --- a/Editor/AnimationWindow.cpp +++ b/Editor/AnimationWindow.cpp @@ -5,11 +5,70 @@ using namespace wi::ecs; using namespace wi::scene; +XMMATRIX ComputeWorldMatrixRecursive(Scene& scene, Entity entity, XMMATRIX localMatrix) +{ + HierarchyComponent* hier = scene.hierarchy.GetComponent(entity); + if (hier != nullptr) + { + Entity parentID = hier->parentID; + while (parentID != INVALID_ENTITY) + { + TransformComponent* transform_parent = scene.transforms.GetComponent(parentID); + if (transform_parent == nullptr) + break; + + localMatrix *= transform_parent->GetLocalMatrix(); + + const HierarchyComponent* hier_recursive = scene.hierarchy.GetComponent(parentID); + if (hier_recursive != nullptr) + { + parentID = hier_recursive->parentID; + } + else + { + parentID = INVALID_ENTITY; + } + } + } + return localMatrix; +} +XMMATRIX ComputeInverseParentMatrixRecursive(Scene& scene, Entity entity) +{ + XMMATRIX inverseParentMatrix = XMMatrixIdentity(); + + HierarchyComponent* hier = scene.hierarchy.GetComponent(entity); + if (hier != nullptr) + { + Entity parentID = hier->parentID; + while (parentID != INVALID_ENTITY) + { + TransformComponent* transform_parent = scene.transforms.GetComponent(parentID); + if (transform_parent == nullptr) + break; + + inverseParentMatrix *= transform_parent->GetLocalMatrix(); + + const HierarchyComponent* hier_recursive = scene.hierarchy.GetComponent(parentID); + if (hier_recursive != nullptr) + { + parentID = hier_recursive->parentID; + } + else + { + parentID = INVALID_ENTITY; + } + } + + inverseParentMatrix = XMMatrixInverse(nullptr, inverseParentMatrix); + } + return inverseParentMatrix; +} + void AnimationWindow::Create(EditorComponent* _editor) { editor = _editor; wi::gui::Window::Create(ICON_ANIMATION " Animation", wi::gui::Window::WindowControls::COLLAPSE | wi::gui::Window::WindowControls::CLOSE); - SetSize(XMFLOAT2(520, 480)); + SetSize(XMFLOAT2(520, 500)); closeButton.SetTooltip("Delete AnimationComponent"); OnClose([=](wi::gui::EventArgs args) { @@ -901,6 +960,148 @@ void AnimationWindow::Create(EditorComponent* _editor) AddWidget(&keyframesList); + retargetCombo.Create("Retarget: "); + retargetCombo.SetSize(XMFLOAT2(wid, hei)); + retargetCombo.selected_font.anim.typewriter.looped = true; + retargetCombo.selected_font.anim.typewriter.time = 2; + retargetCombo.selected_font.anim.typewriter.character_start = 1; + retargetCombo.SetTooltip("Make a copy of this animation and retarget it for a humanoid.\nThis will only work for bones in this animation that are part of a humanoid."); + retargetCombo.SetInvalidSelectionText("..."); + retargetCombo.OnSelect([=](wi::gui::EventArgs args) { + retargetCombo.SetSelectedWithoutCallback(-1); + wi::scene::Scene& scene = editor->GetCurrentScene(); + const AnimationComponent* animation_source = scene.animations.GetComponent(entity); + if (animation_source == nullptr) + return; + const HumanoidComponent* humanoid_dest = scene.humanoids.GetComponent((Entity)args.userdata); + if (humanoid_dest == nullptr) + return; + + bool retarget_valid = false; + Scene retarget_scene; + Entity retarget_entity = CreateEntity(); + AnimationComponent& animation = retarget_scene.animations.Create(retarget_entity); + animation = *animation_source; + animation.channels.clear(); + animation.samplers.clear(); + + NameComponent name; + name.name += "[retarget]"; + const NameComponent* name_source = scene.names.GetComponent(entity); + if (name_source != nullptr) + { + name.name += " " + name_source->name; + } + scene.names.Create(retarget_entity) = name; + + TransformComponent transform; + const TransformComponent* transform_source = scene.transforms.GetComponent(entity); + if (transform_source != nullptr) + { + transform = *transform_source; + } + scene.transforms.Create(retarget_entity) = transform; + + scene.Component_Attach(retarget_entity, (Entity)args.userdata); + + for (auto& channel : animation_source->channels) + { + bool found = false; + for (size_t i = 0; (i < scene.humanoids.GetCount()) && !found; ++i) + { + const HumanoidComponent& humanoid_source = scene.humanoids[i]; + for (size_t humanoidBoneIndex = 0; humanoidBoneIndex < arraysize(humanoid_source.bones); ++humanoidBoneIndex) + { + Entity bone_source = humanoid_source.bones[humanoidBoneIndex]; + if (bone_source == channel.target) + { + retarget_valid = true; + found = true; + Entity bone_dest = humanoid_dest->bones[humanoidBoneIndex]; + + auto& retarget_channel = animation.channels.emplace_back(); + retarget_channel = channel; + retarget_channel.target = bone_dest; + retarget_channel.samplerIndex = (int)animation.samplers.size(); + + auto& sampler = animation_source->samplers[channel.samplerIndex]; + + auto& retarget_sampler = animation.samplers.emplace_back(); + retarget_sampler = sampler; + retarget_sampler.backwards_compatibility_data = {}; + + Entity retarget_animation_data_entity = CreateEntity(); + auto& retarget_animation_data = retarget_scene.animation_datas.Create(retarget_animation_data_entity); + retarget_sampler.data = retarget_animation_data_entity; + + auto& animation_data = scene.animation_datas.Contains(sampler.data) ? *scene.animation_datas.GetComponent(sampler.data) : sampler.backwards_compatibility_data; + retarget_animation_data = animation_data; + + TransformComponent* transform_source = scene.transforms.GetComponent(bone_source); + TransformComponent* transform_dest = scene.transforms.GetComponent(bone_dest); + if (transform_source != nullptr && transform_dest != nullptr) + { + XMMATRIX bindMatrix = ComputeWorldMatrixRecursive(scene, bone_source, transform_source->GetLocalMatrix()); + XMMATRIX inverseBindMatrix = XMMatrixInverse(nullptr, bindMatrix); + XMMATRIX targetMatrix = ComputeWorldMatrixRecursive(scene, bone_dest, transform_dest->GetLocalMatrix()); + XMMATRIX inverseParentMatrix = ComputeInverseParentMatrixRecursive(scene, bone_dest); + XMVECTOR S, R, T; // matrix decompose destinations + + switch (channel.path) + { + case AnimationComponent::AnimationChannel::Path::SCALE: + for (size_t offset = 0; offset < retarget_animation_data.keyframe_data.size(); offset += 3) + { + XMFLOAT3* data = (XMFLOAT3*)&retarget_animation_data.keyframe_data[offset]; + TransformComponent transform = *transform_source; + transform.scale_local = *data; + XMMATRIX localMatrix = inverseBindMatrix * ComputeWorldMatrixRecursive(scene, bone_source, transform.GetLocalMatrix()); + localMatrix = targetMatrix * localMatrix * inverseParentMatrix; + XMMatrixDecompose(&S, &R, &T, localMatrix); + XMStoreFloat3(data, S); + } + break; + case AnimationComponent::AnimationChannel::Path::ROTATION: + for (size_t offset = 0; offset < retarget_animation_data.keyframe_data.size(); offset += 4) + { + XMFLOAT4* data = (XMFLOAT4*)&retarget_animation_data.keyframe_data[offset]; + TransformComponent transform = *transform_source; + transform.rotation_local = *data; + XMMATRIX localMatrix = inverseBindMatrix * ComputeWorldMatrixRecursive(scene, bone_source, transform.GetLocalMatrix()); + localMatrix = targetMatrix * localMatrix * inverseParentMatrix; + XMMatrixDecompose(&S, &R, &T, localMatrix); + XMStoreFloat4(data, R); + } + break; + case AnimationComponent::AnimationChannel::Path::TRANSLATION: + for (size_t offset = 0; offset < retarget_animation_data.keyframe_data.size(); offset += 3) + { + XMFLOAT3* data = (XMFLOAT3*)&retarget_animation_data.keyframe_data[offset]; + TransformComponent transform = *transform_source; + transform.translation_local = *data; + XMMATRIX localMatrix = inverseBindMatrix * ComputeWorldMatrixRecursive(scene, bone_source, transform.GetLocalMatrix()); + localMatrix = targetMatrix * localMatrix * inverseParentMatrix; + XMMatrixDecompose(&S, &R, &T, localMatrix); + XMStoreFloat3(data, T); + } + break; + default: + break; + } + } + break; + } + } + } + } + if (retarget_valid) + { + scene.Merge(retarget_scene); + editor->optionsWnd.RefreshEntityTree(); + } + }); + AddWidget(&retargetCombo); + SetMinimized(true); SetVisible(false); @@ -1104,6 +1305,27 @@ void AnimationWindow::RefreshKeyframesList() } channelIndex++; } + + retargetCombo.ClearItems(); + for (size_t i = 0; i < scene.humanoids.GetCount(); ++i) + { + std::string item = ICON_HUMANOID " "; + Entity entity = scene.humanoids.GetEntity(i); + const NameComponent* name = scene.names.GetComponent(entity); + if (name == nullptr) + { + item += "[no_name] " + std::to_string(entity); + } + else if (name->name.empty()) + { + item += "[name_empty] " + std::to_string(entity); + } + else + { + item += name->name; + } + retargetCombo.AddItem(item, (uint64_t)entity); + } } @@ -1165,5 +1387,6 @@ void AnimationWindow::ResizeLayout() add(startInput); add(endInput); add(recordCombo); + add(retargetCombo); add_fullwidth(keyframesList); } diff --git a/Editor/AnimationWindow.h b/Editor/AnimationWindow.h index b82cde5f7..c1ca004fe 100644 --- a/Editor/AnimationWindow.h +++ b/Editor/AnimationWindow.h @@ -26,6 +26,8 @@ public: wi::gui::ComboBox recordCombo; wi::gui::TreeList keyframesList; + wi::gui::ComboBox retargetCombo; + void Update(); void RefreshKeyframesList(); diff --git a/features.txt b/features.txt index 51b9eb027..bbd418ef2 100644 --- a/features.txt +++ b/features.txt @@ -84,6 +84,8 @@ HDR display output Dynamic Diffuse Global Illumination (DDGI) Procedural terrain generator Expressions +Humanoid rig +Animation retargeting GLTF 2.0 - KHR extensions supported: KHR_materials_unlit