From 56e2fe38451f481e1eabfc8451abd9627bfe3d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tur=C3=A1nszki=20J=C3=A1nos?= Date: Fri, 12 Aug 2022 18:41:38 +0200 Subject: [PATCH] editor: animation editing --- Editor/AnimationWindow.cpp | 408 +++++++++++++++++++++++++++++++++++- Editor/AnimationWindow.h | 7 + Editor/ComponentsWindow.cpp | 8 + Editor/IconDefinitions.h | 1 + Editor/OptionsWindow.cpp | 36 ++-- WickedEngine/wiGUI.cpp | 21 ++ WickedEngine/wiGUI.h | 2 + WickedEngine/wiScene.cpp | 65 +++--- WickedEngine/wiScene.h | 1 + WickedEngine/wiVersion.cpp | 2 +- 10 files changed, 500 insertions(+), 51 deletions(-) diff --git a/Editor/AnimationWindow.cpp b/Editor/AnimationWindow.cpp index f790acad2..338dd323c 100644 --- a/Editor/AnimationWindow.cpp +++ b/Editor/AnimationWindow.cpp @@ -8,14 +8,28 @@ using namespace wi::scene; void AnimationWindow::Create(EditorComponent* _editor) { editor = _editor; - wi::gui::Window::Create(ICON_ANIMATION " Animation", wi::gui::Window::WindowControls::COLLAPSE); - SetSize(XMFLOAT2(520, 140)); + wi::gui::Window::Create(ICON_ANIMATION " Animation", wi::gui::Window::WindowControls::COLLAPSE | wi::gui::Window::WindowControls::CLOSE); + SetSize(XMFLOAT2(520, 400)); + + closeButton.SetTooltip("Delete Animation"); + OnClose([=](wi::gui::EventArgs args) { + + wi::Archive& archive = editor->AdvanceHistory(); + archive << EditorComponent::HISTORYOP_COMPONENT_DATA; + editor->RecordEntity(archive, entity); + + editor->GetCurrentScene().animations.Remove(entity); + + editor->RecordEntity(archive, entity); + + editor->optionsWnd.RefreshEntityTree(); + }); float x = 80; float y = 0; float hei = 18; float wid = 200; - float step = hei + 2; + float step = hei + 4; modeComboBox.Create("Sampling: "); modeComboBox.SetSize(XMFLOAT2(wid, hei)); @@ -41,7 +55,7 @@ void AnimationWindow::Create(EditorComponent* _editor) { sampler.mode = AnimationComponent::AnimationSampler::Mode::LINEAR; } - else if(animationdata->keyframe_data.size() != animationdata->keyframe_times.size() * 3 * 3) + else if (animationdata->keyframe_data.size() != animationdata->keyframe_times.size() * 3 * 3) { sampler.mode = AnimationComponent::AnimationSampler::Mode::LINEAR; } @@ -141,6 +155,291 @@ void AnimationWindow::Create(EditorComponent* _editor) speedSlider.SetTooltip("Set the animation speed."); AddWidget(&speedSlider); + startInput.Create("Start"); + startInput.SetDescription("Start time: "); + startInput.SetSize(XMFLOAT2(wid, hei)); + startInput.SetPos(XMFLOAT2(x, y += step)); + startInput.OnInputAccepted([&](wi::gui::EventArgs args) { + AnimationComponent* animation = editor->GetCurrentScene().animations.GetComponent(entity); + if (animation != nullptr) + { + animation->start = args.fValue; + } + }); + startInput.SetTooltip("Set the animation start in seconds. This will be the loop's starting point."); + AddWidget(&startInput); + + endInput.Create("End"); + endInput.SetDescription("End time: "); + endInput.SetSize(XMFLOAT2(wid, hei)); + endInput.SetPos(XMFLOAT2(x, y += step)); + endInput.OnInputAccepted([&](wi::gui::EventArgs args) { + AnimationComponent* animation = editor->GetCurrentScene().animations.GetComponent(entity); + if (animation != nullptr) + { + animation->end = args.fValue; + } + }); + endInput.SetTooltip("Set the animation end in seconds. This is relative to 0, not the animation start."); + AddWidget(&endInput); + + + recordCombo.Create("Record: "); + recordCombo.selected_font.anim.typewriter.looped = true; + recordCombo.selected_font.anim.typewriter.time = 2; + recordCombo.selected_font.anim.typewriter.character_start = 1; + recordCombo.SetSize(XMFLOAT2(wid, hei)); + recordCombo.SetPos(XMFLOAT2(x, y += step)); + recordCombo.AddItem("..."); + recordCombo.AddItem("Transform " ICON_TRANSLATE " " ICON_ROTATE " " ICON_SCALE); + recordCombo.AddItem("Position " ICON_TRANSLATE); + recordCombo.AddItem("Rotation " ICON_ROTATE); + recordCombo.AddItem("Scale " ICON_SCALE); + recordCombo.AddItem("Morph weights " ICON_MESH); + recordCombo.OnSelect([&](wi::gui::EventArgs args) { + if (args.iValue == 0) + return; + + wi::scene::Scene& scene = editor->GetCurrentScene(); + + AnimationComponent* animation = scene.animations.GetComponent(entity); + if (animation != nullptr) + { + const float current_time = animation->timer; + + wi::vector paths; + + switch (args.iValue) + { + default: + case 1: + paths.push_back(AnimationComponent::AnimationChannel::Path::TRANSLATION); + paths.push_back(AnimationComponent::AnimationChannel::Path::ROTATION); + paths.push_back(AnimationComponent::AnimationChannel::Path::SCALE); + break; + case 2: + paths.push_back(AnimationComponent::AnimationChannel::Path::TRANSLATION); + break; + case 3: + paths.push_back(AnimationComponent::AnimationChannel::Path::ROTATION); + break; + case 4: + paths.push_back(AnimationComponent::AnimationChannel::Path::SCALE); + break; + case 5: + paths.push_back(AnimationComponent::AnimationChannel::Path::WEIGHTS); + break; + } + + for (auto path : paths) + { + for (auto& selected : editor->translator.selected) + { + int channelIndex = -1; + for (int i = 0; i < (int)animation->channels.size(); ++i) + { + // Search for channel for this path and target: + auto& channel = animation->channels[i]; + if (channel.path == path && channel.target == selected.entity) + { + channelIndex = i; + break; + } + } + if (channelIndex < 0) + { + // No channel found for this path and target, create it: + channelIndex = (int)animation->channels.size(); + auto& channel = animation->channels.emplace_back(); + channel.samplerIndex = (int)animation->samplers.size(); + channel.target = selected.entity; + channel.path = path; + auto& sam = animation->samplers.emplace_back(); + Entity animation_data_entity = CreateEntity(); + scene.animation_datas.Create(animation_data_entity); + sam.data = animation_data_entity; + } + auto& channel = animation->channels[channelIndex]; + + AnimationDataComponent* animation_data = scene.animation_datas.GetComponent(animation->samplers[channel.samplerIndex].data); + if (animation_data != nullptr) + { + animation_data->keyframe_times.push_back(current_time); + + switch (channel.path) + { + case wi::scene::AnimationComponent::AnimationChannel::TRANSLATION: + { + const TransformComponent* transform = scene.transforms.GetComponent(channel.target); + if (transform != nullptr) + { + animation_data->keyframe_data.push_back(transform->translation_local.x); + animation_data->keyframe_data.push_back(transform->translation_local.y); + animation_data->keyframe_data.push_back(transform->translation_local.z); + } + else + { + animation_data->keyframe_times.pop_back(); + animation->channels.pop_back(); + } + } + break; + case wi::scene::AnimationComponent::AnimationChannel::ROTATION: + { + const TransformComponent* transform = scene.transforms.GetComponent(channel.target); + if (transform != nullptr) + { + animation_data->keyframe_data.push_back(transform->rotation_local.x); + animation_data->keyframe_data.push_back(transform->rotation_local.y); + animation_data->keyframe_data.push_back(transform->rotation_local.z); + animation_data->keyframe_data.push_back(transform->rotation_local.w); + } + else + { + animation_data->keyframe_times.pop_back(); + animation->channels.pop_back(); + } + } + break; + case wi::scene::AnimationComponent::AnimationChannel::SCALE: + { + const TransformComponent* transform = scene.transforms.GetComponent(channel.target); + if (transform != nullptr) + { + animation_data->keyframe_data.push_back(transform->scale_local.x); + animation_data->keyframe_data.push_back(transform->scale_local.y); + animation_data->keyframe_data.push_back(transform->scale_local.z); + } + else + { + animation_data->keyframe_times.pop_back(); + animation->channels.pop_back(); + } + } + break; + case wi::scene::AnimationComponent::AnimationChannel::WEIGHTS: + { + const MeshComponent* mesh = scene.meshes.GetComponent(channel.target); + if (mesh == nullptr && scene.objects.Contains(selected.entity)) + { + // Also try query mesh of selected object: + ObjectComponent* object = scene.objects.GetComponent(selected.entity); + mesh = scene.meshes.GetComponent(object->meshID); + channel.target = selected.entity; + } + if (mesh != nullptr && !mesh->targets.empty()) + { + for (const MeshComponent::MeshMorphTarget& morph : mesh->targets) + { + animation_data->keyframe_data.push_back(morph.weight); + } + } + else + { + animation_data->keyframe_times.pop_back(); + animation->channels.pop_back(); + } + } + break; + default: + break; + } + } + } + } + } + recordCombo.SetSelectedWithoutCallback(0); + RefreshKeyframesList(); + }); + recordCombo.SetTooltip("Record selected entities' specified channels into the animation at the current time."); + AddWidget(&recordCombo); + + + keyframesList.Create("Keyframes"); + keyframesList.SetSize(XMFLOAT2(wid, 200)); + keyframesList.SetPos(XMFLOAT2(x, y += step)); + keyframesList.OnSelect([=](wi::gui::EventArgs args) { + wi::scene::Scene& scene = editor->GetCurrentScene(); + AnimationComponent* animation = scene.animations.GetComponent(entity); + if (animation != nullptr) + { + uint32_t channelIndex = args.userdata & 0xFFFFFFFF; + uint32_t timeIndex = uint32_t(args.userdata >> 32ull) & 0xFFFFFFFF; + if (animation->channels.size() > channelIndex) + { + const AnimationComponent::AnimationChannel& channel = animation->channels[channelIndex]; + const AnimationComponent::AnimationSampler& sam = animation->samplers[channel.samplerIndex]; + const AnimationDataComponent* animation_data = scene.animation_datas.GetComponent(sam.data); + if (animation_data != nullptr && animation_data->keyframe_times.size() > timeIndex) + { + wi::vector tmp = animation_data->keyframe_times; + std::sort(tmp.begin(), tmp.end()); + float time = tmp[timeIndex]; + animation->timer = time; + } + } + } + }); + keyframesList.OnDelete([=](wi::gui::EventArgs args) { + wi::scene::Scene& scene = editor->GetCurrentScene(); + AnimationComponent* animation = scene.animations.GetComponent(entity); + if (animation != nullptr) + { + uint32_t channelIndex = args.userdata & 0xFFFFFFFF; + uint32_t timeIndex = uint32_t(args.userdata >> 32ull) & 0xFFFFFFFF; + if (animation->channels.size() > channelIndex) + { + const AnimationComponent::AnimationChannel& channel = animation->channels[channelIndex]; + const AnimationComponent::AnimationSampler& sam = animation->samplers[channel.samplerIndex]; + AnimationDataComponent* animation_data = scene.animation_datas.GetComponent(sam.data); + if (animation_data != nullptr && animation_data->keyframe_times.size() > timeIndex) + { + // specific keyframe deletion: + switch (channel.path) + { + case AnimationComponent::AnimationChannel::Path::TRANSLATION: + animation_data->keyframe_times.erase(animation_data->keyframe_times.begin() + timeIndex); + animation_data->keyframe_data.erase(animation_data->keyframe_data.begin() + timeIndex * 3, animation_data->keyframe_data.begin() + timeIndex * 3 + 3); + break; + case AnimationComponent::AnimationChannel::Path::ROTATION: + animation_data->keyframe_times.erase(animation_data->keyframe_times.begin() + timeIndex); + animation_data->keyframe_data.erase(animation_data->keyframe_data.begin() + timeIndex * 4, animation_data->keyframe_data.begin() + timeIndex * 4 + 4); + break; + case AnimationComponent::AnimationChannel::Path::SCALE: + animation_data->keyframe_times.erase(animation_data->keyframe_times.begin() + timeIndex); + animation_data->keyframe_data.erase(animation_data->keyframe_data.begin() + timeIndex * 3, animation_data->keyframe_data.begin() + timeIndex * 3 + 3); + break; + case AnimationComponent::AnimationChannel::Path::WEIGHTS: + { + MeshComponent* mesh = scene.meshes.GetComponent(channel.target); + if (mesh == nullptr && scene.objects.Contains(channel.target)) + { + // Also try query mesh of selected object: + ObjectComponent* object = scene.objects.GetComponent(channel.target); + mesh = scene.meshes.GetComponent(object->meshID); + } + if (mesh != nullptr) + { + animation_data->keyframe_times.erase(animation_data->keyframe_times.begin() + timeIndex); + animation_data->keyframe_data.erase(animation_data->keyframe_data.begin() + timeIndex * mesh->targets.size(), animation_data->keyframe_data.begin() + timeIndex * mesh->targets.size() + mesh->targets.size()); + } + } + break; + default: + break; + } + } + else + { + // entire channel deletion: + animation->channels.erase(animation->channels.begin() + channelIndex); + } + } + } + RefreshKeyframesList(); + }); + AddWidget(&keyframesList); + SetMinimized(true); @@ -150,7 +449,17 @@ void AnimationWindow::Create(EditorComponent* _editor) void AnimationWindow::SetEntity(Entity entity) { - this->entity = entity; + if (this->entity != entity) + { + wi::scene::Scene& scene = editor->GetCurrentScene(); + + AnimationComponent* animation = scene.animations.GetComponent(entity); + if (animation != nullptr || IsCollapsed()) + { + this->entity = entity; + RefreshKeyframesList(); + } + } } void AnimationWindow::Update() @@ -178,18 +487,95 @@ void AnimationWindow::Update() playButton.SetText("Play"); } - for (const AnimationComponent::AnimationChannel& channel : animation.channels) + if(!animation.samplers.empty()) { - assert(channel.samplerIndex < (int)animation.samplers.size()); - AnimationComponent::AnimationSampler& sampler = animation.samplers[channel.samplerIndex]; - modeComboBox.SetSelectedByUserdataWithoutCallback(sampler.mode); - break; // feed back the first sampler's mode into the gui + modeComboBox.SetSelectedByUserdataWithoutCallback(animation.samplers[0].mode); + } + else + { + modeComboBox.SetSelectedByUserdataWithoutCallback(AnimationComponent::AnimationSampler().mode); } loopedCheckBox.SetCheck(animation.IsLooped()); - timerSlider.SetRange(0, animation.GetLength()); + timerSlider.SetRange(animation.start, animation.end); timerSlider.SetValue(animation.timer); amountSlider.SetValue(animation.amount); speedSlider.SetValue(animation.speed); + startInput.SetValue(animation.start); + endInput.SetValue(animation.end); +} + + +void AnimationWindow::RefreshKeyframesList() +{ + Scene& scene = editor->GetCurrentScene(); + + if (!scene.animations.Contains(entity)) + { + SetEntity(INVALID_ENTITY); + return; + } + + AnimationComponent& animation = *scene.animations.GetComponent(entity); + + keyframesList.ClearItems(); + uint32_t channelIndex = 0; + for (const AnimationComponent::AnimationChannel& channel : animation.channels) + { + wi::gui::TreeList::Item item; + switch (channel.path) + { + default: + case wi::scene::AnimationComponent::AnimationChannel::TRANSLATION: + item.name += ICON_TRANSLATE " "; + break; + case wi::scene::AnimationComponent::AnimationChannel::ROTATION: + item.name += ICON_ROTATE " "; + break; + case wi::scene::AnimationComponent::AnimationChannel::SCALE: + item.name += ICON_SCALE " "; + break; + case wi::scene::AnimationComponent::AnimationChannel::WEIGHTS: + item.name += ICON_MESH " "; + break; + } + const NameComponent* name = scene.names.GetComponent(channel.target); + if (name == nullptr) + { + item.name += "[no_name] " + std::to_string(channel.target); + } + else if (name->name.empty()) + { + item.name += "[name_empty] " + std::to_string(channel.target); + } + else + { + item.name += name->name; + } + + item.userdata = 0ull; + item.userdata |= channelIndex & 0xFFFFFFFF; + item.userdata |= uint64_t(0xFFFFFFFF) << 32ull; // invalid time index, means entire channel + keyframesList.AddItem(item); + + auto& sam = animation.samplers[channel.samplerIndex]; + AnimationDataComponent* animation_data = scene.animation_datas.GetComponent(sam.data); + if (animation_data != nullptr) + { + uint32_t timeIndex = 0; + for (float time : animation_data->keyframe_times) + { + wi::gui::TreeList::Item item2; + item2.name = std::to_string(time); + item2.level = 1; + item2.userdata = 0ull; + item2.userdata |= channelIndex & 0xFFFFFFFF; + item2.userdata |= uint64_t(timeIndex & 0xFFFFFFFF) << 32ull; + keyframesList.AddItem(item2); + timeIndex++; + } + } + channelIndex++; + } } diff --git a/Editor/AnimationWindow.h b/Editor/AnimationWindow.h index 7830f9a66..8c13052e8 100644 --- a/Editor/AnimationWindow.h +++ b/Editor/AnimationWindow.h @@ -19,7 +19,14 @@ public: wi::gui::Slider timerSlider; wi::gui::Slider amountSlider; wi::gui::Slider speedSlider; + wi::gui::TextInputField startInput; + wi::gui::TextInputField endInput; + + wi::gui::ComboBox recordCombo; + wi::gui::TreeList keyframesList; void Update(); + + void RefreshKeyframesList(); }; diff --git a/Editor/ComponentsWindow.cpp b/Editor/ComponentsWindow.cpp index 957505797..f5900d21a 100644 --- a/Editor/ComponentsWindow.cpp +++ b/Editor/ComponentsWindow.cpp @@ -54,6 +54,7 @@ void ComponentsWindow::Create(EditorComponent* _editor) newComponentCombo.AddItem("Decal " ICON_DECAL, 11); newComponentCombo.AddItem("Weather " ICON_WEATHER, 12); newComponentCombo.AddItem("Force Field " ICON_FORCE, 13); + newComponentCombo.AddItem("Animation " ICON_ANIMATION, 14); newComponentCombo.OnSelect([=](wi::gui::EventArgs args) { newComponentCombo.SetSelectedWithoutCallback(0); if (editor->translator.selected.empty()) @@ -125,6 +126,10 @@ void ComponentsWindow::Create(EditorComponent* _editor) if (scene.forces.Contains(entity)) return; break; + case 14: + if (scene.animations.Contains(entity)) + return; + break; default: return; } @@ -186,6 +191,9 @@ void ComponentsWindow::Create(EditorComponent* _editor) case 13: scene.forces.Create(entity); break; + case 14: + scene.animations.Create(entity); + break; default: break; } diff --git a/Editor/IconDefinitions.h b/Editor/IconDefinitions.h index f8ac08256..1faab282f 100644 --- a/Editor/IconDefinitions.h +++ b/Editor/IconDefinitions.h @@ -44,3 +44,4 @@ #define ICON_UPRIGHT_DOWNLEFT ICON_FA_UP_RIGHT_AND_DOWN_LEFT_FROM_CENTER #define ICON_CIRCLE ICON_FA_CIRCLE #define ICON_SQUARE ICON_FA_SQUARE_FULL +#define ICON_CUBE ICON_FA_CUBE diff --git a/Editor/OptionsWindow.cpp b/Editor/OptionsWindow.cpp index 4c81c1757..a96e77a41 100644 --- a/Editor/OptionsWindow.cpp +++ b/Editor/OptionsWindow.cpp @@ -117,21 +117,22 @@ void OptionsWindow::Create(EditorComponent* _editor) newCombo.selected_font.anim.typewriter.time = 2; newCombo.selected_font.anim.typewriter.character_start = 1; newCombo.AddItem("...", ~0ull); - newCombo.AddItem("Transform", 0); - newCombo.AddItem("Material", 1); - newCombo.AddItem("Point Light", 2); - newCombo.AddItem("Spot Light", 3); - newCombo.AddItem("Directional Light", 4); - newCombo.AddItem("Environment Probe", 5); - newCombo.AddItem("Force", 6); - newCombo.AddItem("Decal", 7); - newCombo.AddItem("Sound", 8); - newCombo.AddItem("Weather", 9); - newCombo.AddItem("Emitter", 10); - newCombo.AddItem("HairParticle", 11); - newCombo.AddItem("Camera", 12); - newCombo.AddItem("Cube Object", 13); - newCombo.AddItem("Plane Object", 14); + newCombo.AddItem("Transform " ICON_TRANSFORM, 0); + newCombo.AddItem("Material " ICON_MATERIAL, 1); + newCombo.AddItem("Point Light " ICON_POINTLIGHT, 2); + newCombo.AddItem("Spot Light " ICON_SPOTLIGHT, 3); + newCombo.AddItem("Directional Light " ICON_DIRECTIONALLIGHT, 4); + newCombo.AddItem("Environment Probe " ICON_ENVIRONMENTPROBE, 5); + newCombo.AddItem("Force " ICON_FORCE, 6); + newCombo.AddItem("Decal " ICON_DECAL, 7); + newCombo.AddItem("Sound " ICON_SOUND, 8); + newCombo.AddItem("Weather " ICON_WEATHER, 9); + newCombo.AddItem("Emitter " ICON_EMITTER, 10); + newCombo.AddItem("HairParticle " ICON_HAIR, 11); + newCombo.AddItem("Camera " ICON_CAMERA, 12); + newCombo.AddItem("Cube Object " ICON_CUBE, 13); + newCombo.AddItem("Plane Object " ICON_SQUARE, 14); + newCombo.AddItem("Animation " ICON_ANIMATION, 15); newCombo.OnSelect([&](wi::gui::EventArgs args) { newCombo.SetSelectedWithoutCallback(0); const EditorComponent::EditorScene& editorscene = editor->GetCurrentEditorScene(); @@ -226,6 +227,11 @@ void OptionsWindow::Create(EditorComponent* _editor) pick.entity = scene.Entity_CreatePlane("plane"); pick.subsetIndex = 0; break; + case 15: + pick.entity = CreateEntity(); + scene.animations.Create(pick.entity); + scene.names.Create(pick.entity) = "animation"; + break; default: break; } diff --git a/WickedEngine/wiGUI.cpp b/WickedEngine/wiGUI.cpp index 41357c57c..9d3eaa3dc 100644 --- a/WickedEngine/wiGUI.cpp +++ b/WickedEngine/wiGUI.cpp @@ -4135,6 +4135,23 @@ namespace wi::gui clicked = true; } + if (onDelete && state == FOCUS && wi::input::Press(wi::input::KEYBOARD_BUTTON_DELETE)) + { + int index = 0; + for (auto& item : items) + { + if (item.selected) + { + EventArgs args; + args.iValue = index; + args.sValue = items[index].name; + args.userdata = items[index].userdata; + onDelete(args); + } + index++; + } + } + bool click_down = false; if (wi::input::Down(wi::input::MOUSE_BUTTON_LEFT)) { @@ -4360,6 +4377,10 @@ namespace wi::gui { onSelect = func; } + void TreeList::OnDelete(std::function func) + { + onDelete = func; + } void TreeList::AddItem(const Item& item) { items.push_back(item); diff --git a/WickedEngine/wiGUI.h b/WickedEngine/wiGUI.h index 2298e9f2a..91ab63eca 100644 --- a/WickedEngine/wiGUI.h +++ b/WickedEngine/wiGUI.h @@ -672,6 +672,7 @@ namespace wi::gui }; protected: std::function onSelect; + std::function onDelete; int item_highlight = -1; int opener_highlight = -1; @@ -701,6 +702,7 @@ namespace wi::gui void SetTheme(const Theme& theme, int id = -1) override; void OnSelect(std::function func); + void OnDelete(std::function func); ScrollBar scrollbar; }; diff --git a/WickedEngine/wiScene.cpp b/WickedEngine/wiScene.cpp index 455bc9a6a..4c35427cf 100644 --- a/WickedEngine/wiScene.cpp +++ b/WickedEngine/wiScene.cpp @@ -2737,10 +2737,11 @@ namespace wi::scene for (size_t i = 0; i < animations.GetCount(); ++i) { AnimationComponent& animation = animations[i]; - if (!animation.IsPlaying() && animation.timer == 0.0f) + if (!animation.IsPlaying() && animation.last_update_time == animation.timer) { continue; } + animation.last_update_time = animation.timer; for (const AnimationComponent::AnimationChannel& channel : animation.channels) { @@ -2752,25 +2753,42 @@ namespace wi::scene continue; } - int keyLeft = 0; - int keyRight = 0; + float timeFirst = std::numeric_limits::max(); + float timeLast = std::numeric_limits::min(); + int keyLeft = 0; float timeLeft = std::numeric_limits::min(); + int keyRight = 0; float timeRight = std::numeric_limits::max(); - if (animationdata->keyframe_times.back() < animation.timer) + // search for usable keyframes: + for (int k = 0; k < (int)animationdata->keyframe_times.size(); ++k) { - // Rightmost keyframe is already outside animation, so just snap to last keyframe: - keyLeft = keyRight = (int)animationdata->keyframe_times.size() - 1; + const float time = animationdata->keyframe_times[k]; + if (time < timeFirst) + { + timeFirst = time; + } + if (time > timeLast) + { + timeLast = time; + } + if (time <= animation.timer && time > timeLeft) + { + timeLeft = time; + keyLeft = k; + } + if (time >= animation.timer && time < timeRight) + { + timeRight = time; + keyRight = k; + } } - else + if (animation.timer < timeFirst || animation.timer > timeLast) { - // Search for the right keyframe (greater/equal to anim time): - while (animationdata->keyframe_times[keyRight++] < animation.timer) {} - keyRight--; - - // Left keyframe is just near right: - keyLeft = std::max(0, keyRight - 1); + // timer is outside range of keyframes, don't update animation: + continue; } - float left = animationdata->keyframe_times[keyLeft]; + const float left = animationdata->keyframe_times[keyLeft]; + const float right = animationdata->keyframe_times[keyRight]; TransformComponent transform; @@ -2800,26 +2818,27 @@ namespace wi::scene default: case AnimationComponent::AnimationSampler::Mode::STEP: { - // Nearest neighbor method (snap to left): + // Nearest neighbor method: + const int key = wi::math::InverseLerp(timeLeft, timeRight, animation.timer) > 0.5f ? keyRight : keyLeft; switch (channel.path) { default: case AnimationComponent::AnimationChannel::Path::TRANSLATION: { assert(animationdata->keyframe_data.size() == animationdata->keyframe_times.size() * 3); - transform.translation_local = ((const XMFLOAT3*)animationdata->keyframe_data.data())[keyLeft]; + transform.translation_local = ((const XMFLOAT3*)animationdata->keyframe_data.data())[key]; } break; case AnimationComponent::AnimationChannel::Path::ROTATION: { assert(animationdata->keyframe_data.size() == animationdata->keyframe_times.size() * 4); - transform.rotation_local = ((const XMFLOAT4*)animationdata->keyframe_data.data())[keyLeft]; + transform.rotation_local = ((const XMFLOAT4*)animationdata->keyframe_data.data())[key]; } break; case AnimationComponent::AnimationChannel::Path::SCALE: { assert(animationdata->keyframe_data.size() == animationdata->keyframe_times.size() * 3); - transform.scale_local = ((const XMFLOAT3*)animationdata->keyframe_data.data())[keyLeft]; + transform.scale_local = ((const XMFLOAT3*)animationdata->keyframe_data.data())[key]; } break; case AnimationComponent::AnimationChannel::Path::WEIGHTS: @@ -2827,7 +2846,7 @@ namespace wi::scene assert(animationdata->keyframe_data.size() == animationdata->keyframe_times.size() * animation.morph_weights_temp.size()); for (size_t j = 0; j < animation.morph_weights_temp.size(); ++j) { - animation.morph_weights_temp[j] = animationdata->keyframe_data[keyLeft * animation.morph_weights_temp.size() + j]; + animation.morph_weights_temp[j] = animationdata->keyframe_data[key * animation.morph_weights_temp.size() + j]; } } break; @@ -2844,7 +2863,6 @@ namespace wi::scene } else { - float right = animationdata->keyframe_times[keyRight]; t = (animation.timer - left) / (right - left); } @@ -2888,7 +2906,7 @@ namespace wi::scene for (size_t j = 0; j < animation.morph_weights_temp.size(); ++j) { float vLeft = animationdata->keyframe_data[keyLeft * animation.morph_weights_temp.size() + j]; - float vRight = animationdata->keyframe_data[keyLeft * animation.morph_weights_temp.size() + j]; + float vRight = animationdata->keyframe_data[keyRight * animation.morph_weights_temp.size() + j]; float vAnim = wi::math::Lerp(vLeft, vRight, t); animation.morph_weights_temp[j] = vAnim; } @@ -2907,7 +2925,6 @@ namespace wi::scene } else { - float right = animationdata->keyframe_times[keyRight]; t = (animation.timer - left) / (right - left); } @@ -2961,8 +2978,8 @@ namespace wi::scene { float vLeft = animationdata->keyframe_data[(keyLeft * animation.morph_weights_temp.size() + j) * 3 + 1]; float vLeftTanOut = animationdata->keyframe_data[(keyLeft * animation.morph_weights_temp.size() + j) * 3 + 2]; - float vRightTanIn = animationdata->keyframe_data[(keyLeft * animation.morph_weights_temp.size() + j) * 3 + 0]; - float vRight = animationdata->keyframe_data[(keyLeft * animation.morph_weights_temp.size() + j) * 3 + 1]; + float vRightTanIn = animationdata->keyframe_data[(keyRight * animation.morph_weights_temp.size() + j) * 3 + 0]; + float vRight = animationdata->keyframe_data[(keyRight * animation.morph_weights_temp.size() + j) * 3 + 1]; float vAnim = (2 * t3 - 3 * t2 + 1) * vLeft + (t3 - 2 * t2 + t) * vLeftTanOut + (-2 * t3 + 3 * t2) * vRight + (t3 - t2) * vRightTanIn; animation.morph_weights_temp[j] = vAnim; } diff --git a/WickedEngine/wiScene.h b/WickedEngine/wiScene.h index 6d680345d..0c247963d 100644 --- a/WickedEngine/wiScene.h +++ b/WickedEngine/wiScene.h @@ -1142,6 +1142,7 @@ namespace wi::scene // Non-serialzied attributes: wi::vector morph_weights_temp; + float last_update_time = 0; inline bool IsPlaying() const { return _flags & PLAYING; } inline bool IsLooped() const { return _flags & LOOPED; } diff --git a/WickedEngine/wiVersion.cpp b/WickedEngine/wiVersion.cpp index 99eaf735a..21916f638 100644 --- a/WickedEngine/wiVersion.cpp +++ b/WickedEngine/wiVersion.cpp @@ -9,7 +9,7 @@ namespace wi::version // minor features, major updates, breaking compatibility changes const int minor = 71; // minor bug fixes, alterations, refactors, updates - const int revision = 6; + const int revision = 7; const std::string version_string = std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(revision);