From 08bdb67885a19e877d8a945472aa9c174191558e Mon Sep 17 00:00:00 2001 From: Guillaume Blanc Date: Tue, 23 Apr 2024 22:45:22 +0200 Subject: [PATCH] First motion blending sample implementation. --- samples/CMakeLists.txt | 1 + samples/framework/motion_utils.cc | 52 ++- samples/framework/motion_utils.h | 30 +- samples/motion_blend/CMakeLists.txt | 74 ++++ samples/motion_blend/README.md | 9 + samples/motion_blend/sample_motion_blend.cc | 420 ++++++++++++++++++++ 6 files changed, 561 insertions(+), 25 deletions(-) create mode 100644 samples/motion_blend/CMakeLists.txt create mode 100644 samples/motion_blend/README.md create mode 100644 samples/motion_blend/sample_motion_blend.cc diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt index a32c269fb..206781f17 100644 --- a/samples/CMakeLists.txt +++ b/samples/CMakeLists.txt @@ -12,6 +12,7 @@ add_subdirectory(blend) add_subdirectory(foot_ik) add_subdirectory(look_at) add_subdirectory(millipede) +add_subdirectory(motion_blend) add_subdirectory(motion_extraction) add_subdirectory(motion_playback) add_subdirectory(multithread) diff --git a/samples/framework/motion_utils.cc b/samples/framework/motion_utils.cc index 966a68409..8e152e451 100644 --- a/samples/framework/motion_utils.cc +++ b/samples/framework/motion_utils.cc @@ -103,6 +103,27 @@ bool SampleMotion(const MotionTrack& _tracks, float _ratio, return true; } +void MotionDeltaAccumulator::Update(const ozz::math::Transform& _delta) { + Update(_delta, ozz::math::Quaternion::identity()); +} + +void MotionDeltaAccumulator::Update(const ozz::math::Transform& _delta, + const ozz::math::Quaternion& _rotation) { + // Accumulates rotation. + rotation_accum_ = Normalize(rotation_accum_ * _rotation); + + // Updates current transform. + current.translation = current.translation + + TransformVector(rotation_accum_, _delta.translation); + current.rotation = Normalize(current.rotation * _delta.rotation * _rotation); +} +void MotionDeltaAccumulator::Teleport(const ozz::math::Transform& _origin) { + current = _origin; + + // Resets rotation accumulator. + rotation_accum_ = ozz::math::Quaternion::identity(); +} + void MotionAccumulator::Update(const ozz::math::Transform& _new) { return Update(_new, ozz::math::Quaternion::identity()); } @@ -110,19 +131,12 @@ void MotionAccumulator::Update(const ozz::math::Transform& _new) { // Accumulates motion deltas (new transform - last). void MotionAccumulator::Update(const ozz::math::Transform& _new, const ozz::math::Quaternion& _delta_rotation) { - // Accumulates rotation. - // Normalizes to avoid accumulating error. - rotation_accum_ = Normalize(rotation_accum_ * _delta_rotation); - - // Computes delta translation. - const auto delta_p = _new.translation - last.translation; - current.translation = - current.translation + TransformVector(rotation_accum_, delta_p); + // Computes delta. + delta.translation = _new.translation - last.translation; + delta.rotation = Conjugate(last.rotation) * _new.rotation; - // Computes delta rotation. - // Normalizes to avoid accumulating a denormalization error. - const auto delta_r = Conjugate(last.rotation) * _new.rotation; - current.rotation = Normalize(current.rotation * delta_r * _delta_rotation); + // Updates current transform based on computed delta. + MotionDeltaAccumulator::Update(delta, _delta_rotation); // Next time, delta will be computed from the _new transform. last = _new; @@ -133,11 +147,13 @@ void MotionAccumulator::ResetOrigin(const ozz::math::Transform& _origin) { } void MotionAccumulator::Teleport(const ozz::math::Transform& _origin) { + MotionDeltaAccumulator::Teleport(_origin); + // Resets current transform to new _origin - current = last = _origin; + last = current; - // Resets rotation accumulator. - rotation_accum_ = ozz::math::Quaternion::identity(); + // No delta between last and current + delta = ozz::math::Transform::identity(); } bool MotionSampler::Update(const MotionTrack& _motion, float _ratio, @@ -154,10 +170,12 @@ bool MotionSampler::Update(const MotionTrack& _motion, float _ratio, int _loops, // motion done during the loop(s). // Uses a local accumulator to accumulate motion during loops. - MotionAccumulator local_accumulator{last, last}; + MotionAccumulator local_accumulator; + local_accumulator.Teleport(last); for (; _loops; _loops > 0 ? --_loops : ++_loops) { - // Samples motion at loop end (or begin depending on playback direction). + // Samples motion at loop end (or begin depending on playback + // direction). if (!SampleMotion(_motion, _loops > 0 ? 1.f : 0.f, &sample)) { return false; } diff --git a/samples/framework/motion_utils.h b/samples/framework/motion_utils.h index fe1e4ac87..bb7093564 100644 --- a/samples/framework/motion_utils.h +++ b/samples/framework/motion_utils.h @@ -52,18 +52,35 @@ struct MotionTrack { // and _track must be non-nullptr. bool LoadMotionTrack(const char* _filename, MotionTrack* _track); +struct MotionDeltaAccumulator { + void Update(const ozz::math::Transform& _delta); + + void Update(const ozz::math::Transform& _delta, + const ozz::math::Quaternion& _rotation); + + // Teleports accumulator to a new transform. This also resets the origin, so + // next delta is computed from the new origin. + void Teleport(const ozz::math::Transform& _origin); + + // Character's current transform. + ozz::math::Transform current = ozz::math::Transform::identity(); + + // Accumulated rotation (since last teleport). + ozz::math::Quaternion rotation_accum_ = ozz::math::Quaternion::identity(); +}; + // Helper object that manages motion accumulation to compute character's // transform. -struct MotionAccumulator { +struct MotionAccumulator : public MotionDeltaAccumulator { // Accumulates motion delta (new - last) and updates current transform. void Update(const ozz::math::Transform& _new); // Accumulates motion delta (new - last) and updates current transform. - // _delta_rotation is the rotation to pply to deform the path since last + // _delta_rotation is the rotation to apply to deform the path since last // update. Hence, user is responsible for taking care of applying delta time // if he wants to achieve a specific angular speed. void Update(const ozz::math::Transform& _new, - const ozz::math::Quaternion& _delta_rotation); + const ozz::math::Quaternion& _rotation); // Tells the accumulator that the _new transform is the new origin. // This is useful when animation loops, so next delta is computed from the new @@ -77,11 +94,8 @@ struct MotionAccumulator { // Last value sample from the motion track, used to compute delta. ozz::math::Transform last = ozz::math::Transform::identity(); - // Character's current transform. - ozz::math::Transform current = ozz::math::Transform::identity(); - - // Accumulated rotation (since last teleport). - ozz::math::Quaternion rotation_accum_ = ozz::math::Quaternion::identity(); + // Delta transformation between last and current frame. + ozz::math::Transform delta = ozz::math::Transform::identity(); }; // Helper object samples a motion track to update a MotionAccumulator. diff --git a/samples/motion_blend/CMakeLists.txt b/samples/motion_blend/CMakeLists.txt new file mode 100644 index 000000000..52c9c358b --- /dev/null +++ b/samples/motion_blend/CMakeLists.txt @@ -0,0 +1,74 @@ +# Generates sample data + +add_custom_command( + DEPENDS $<$:BUILD_DATA> + "${CMAKE_CURRENT_LIST_DIR}/README.md" + "${ozz_media_directory}/bin/pab_skeleton.ozz" + "${ozz_media_directory}/bin/pab_walk_no_motion.ozz" + "${ozz_media_directory}/bin/pab_jog_no_motion.ozz" + "${ozz_media_directory}/bin/pab_jog_no_motion.ozz" + "${ozz_media_directory}/bin/pab_walk_motion_track.ozz" + "${ozz_media_directory}/bin/pab_jog_motion_track.ozz" + "${ozz_media_directory}/bin/pab_jog_motion_track.ozz" + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/README.md" + "${CMAKE_CURRENT_BINARY_DIR}/media/skeleton.ozz" + "${CMAKE_CURRENT_BINARY_DIR}/media/animation1.ozz" + "${CMAKE_CURRENT_BINARY_DIR}/media/animation2.ozz" + "${CMAKE_CURRENT_BINARY_DIR}/media/animation3.ozz" + "${CMAKE_CURRENT_BINARY_DIR}/media/motion1.ozz" + "${CMAKE_CURRENT_BINARY_DIR}/media/motion2.ozz" + "${CMAKE_CURRENT_BINARY_DIR}/media/motion3.ozz" + COMMAND ${CMAKE_COMMAND} -E make_directory media + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_LIST_DIR}/README.md" . + COMMAND ${CMAKE_COMMAND} -E copy "${ozz_media_directory}/bin/pab_skeleton.ozz" "./media/skeleton.ozz" + COMMAND ${CMAKE_COMMAND} -E copy "${ozz_media_directory}/bin/pab_walk_no_motion.ozz" "./media/animation1.ozz" + COMMAND ${CMAKE_COMMAND} -E copy "${ozz_media_directory}/bin/pab_jog_no_motion.ozz" "./media/animation2.ozz" + COMMAND ${CMAKE_COMMAND} -E copy "${ozz_media_directory}/bin/pab_run_no_motion.ozz" "./media/animation3.ozz" + COMMAND ${CMAKE_COMMAND} -E copy "${ozz_media_directory}/bin/pab_walk_motion_track.ozz" "./media/motion1.ozz" + COMMAND ${CMAKE_COMMAND} -E copy "${ozz_media_directory}/bin/pab_jog_motion_track.ozz" "./media/motion2.ozz" + COMMAND ${CMAKE_COMMAND} -E copy "${ozz_media_directory}/bin/pab_run_motion_track.ozz" "./media/motion3.ozz" + VERBATIM) + +# Adds sample executable +add_executable(sample_motion_blend +sample_motion_blend.cc + "${CMAKE_CURRENT_BINARY_DIR}/README.md" + "${CMAKE_CURRENT_BINARY_DIR}/media/skeleton.ozz" + "${CMAKE_CURRENT_BINARY_DIR}/media/animation1.ozz" + "${CMAKE_CURRENT_BINARY_DIR}/media/animation2.ozz" + "${CMAKE_CURRENT_BINARY_DIR}/media/animation3.ozz" + "${CMAKE_CURRENT_BINARY_DIR}/media/motion1.ozz" + "${CMAKE_CURRENT_BINARY_DIR}/media/motion2.ozz" + "${CMAKE_CURRENT_BINARY_DIR}/media/motion3.ozz") + +target_link_libraries(sample_motion_blend + sample_framework) +target_copy_shared_libraries(sample_motion_blend) + +set_target_properties(sample_motion_blend + PROPERTIES FOLDER "samples") + +if(EMSCRIPTEN) + # Resource files are embedded to the output file with emscripten + set_target_properties(sample_motion_blend + PROPERTIES LINK_FLAGS "--embed-file media --embed-file README.md") + + install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/sample_motion_blend.html + ${CMAKE_CURRENT_BINARY_DIR}/sample_motion_blend.js + ${CMAKE_CURRENT_BINARY_DIR}/sample_motion_blend.wasm + DESTINATION bin/samples/motion_blend) +else() + install(TARGETS sample_motion_blend DESTINATION bin/samples/motion_blend) + install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/media DESTINATION bin/samples/motion_blend) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/README.md DESTINATION bin/samples/motion_blend) +endif(EMSCRIPTEN) + +add_test(NAME sample_motion_blend COMMAND sample_motion_blend "--max_idle_loops=${ozz_sample_testing_loops}" $<$:--norender>) +add_test(NAME sample_motion_blend_path COMMAND sample_motion_blend "--skeleton=media/skeleton.ozz" "--animation1=media/animation1.ozz" "--animation2=media/animation2.ozz" "--animation3=media/animation3.ozz" "--max_idle_loops=${ozz_sample_testing_loops}" $<$:--norender>) +add_test(NAME sample_motion_blend_invalid_skeleton_path COMMAND sample_motion_blend "--skeleton=media/bad_skeleton.ozz" $<$:--norender>) +set_tests_properties(sample_motion_blend_invalid_skeleton_path PROPERTIES WILL_FAIL true) +add_test(NAME sample_motion_blend_invalid_animation_path COMMAND sample_motion_blend "--animation1=media/bad_animation.ozz" $<$:--norender>) +set_tests_properties(sample_motion_blend_invalid_animation_path PROPERTIES WILL_FAIL true) +add_test(NAME sample_motion_blend_invalid_motion_path COMMAND sample_motion_blend "--motion2=media/bad_animation.ozz" $<$:--norender>) +set_tests_properties(sample_motion_blend_invalid_motion_path PROPERTIES WILL_FAIL true) diff --git a/samples/motion_blend/README.md b/samples/motion_blend/README.md new file mode 100644 index 000000000..11b42f603 --- /dev/null +++ b/samples/motion_blend/README.md @@ -0,0 +1,9 @@ +# Ozz-animation sample: Animation blending + +## Description + +## Concept + +## Sample usage + +## Implementation diff --git a/samples/motion_blend/sample_motion_blend.cc b/samples/motion_blend/sample_motion_blend.cc new file mode 100644 index 000000000..8ee3a0e65 --- /dev/null +++ b/samples/motion_blend/sample_motion_blend.cc @@ -0,0 +1,420 @@ +//----------------------------------------------------------------------------// +// // +// ozz-animation is hosted at http://github.com/guillaumeblanc/ozz-animation // +// and distributed under the MIT License (MIT). // +// // +// Copyright (c) Guillaume Blanc // +// // +// Permission is hereby granted, free of charge, to any person obtaining a // +// copy of this software and associated documentation files (the "Software"), // +// to deal in the Software without restriction, including without limitation // +// the rights to use, copy, modify, merge, publish, distribute, sublicense, // +// and/or sell copies of the Software, and to permit persons to whom the // +// Software is furnished to do so, subject to the following conditions: // +// // +// The above copyright notice and this permission notice shall be included in // +// all copies or substantial portions of the Software. // +// // +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // +// DEALINGS IN THE SOFTWARE. // +// // +//----------------------------------------------------------------------------// + +#include "framework/application.h" +#include "framework/imgui.h" +#include "framework/motion_utils.h" +#include "framework/renderer.h" +#include "framework/utils.h" +#include "ozz/animation/runtime/animation.h" +#include "ozz/animation/runtime/blending_job.h" +#include "ozz/animation/runtime/local_to_model_job.h" +#include "ozz/animation/runtime/sampling_job.h" +#include "ozz/animation/runtime/skeleton.h" +#include "ozz/base/log.h" +#include "ozz/base/maths/math_ex.h" +#include "ozz/base/maths/simd_math.h" +#include "ozz/base/maths/soa_transform.h" +#include "ozz/base/maths/vec_float.h" +#include "ozz/options/options.h" + +// Skeleton archive can be specified as an option. +OZZ_OPTIONS_DECLARE_STRING(skeleton, + "Path to the skeleton (ozz archive format).", + "media/skeleton.ozz", false) + +// First animation archive can be specified as an option. +OZZ_OPTIONS_DECLARE_STRING(animation1, + "Path to the first animation (ozz archive format).", + "media/animation1.ozz", false) + +// Second animation archive can be specified as an option. +OZZ_OPTIONS_DECLARE_STRING(animation2, + "Path to the second animation (ozz archive format).", + "media/animation2.ozz", false) + +// Third animation archive can be specified as an option. +OZZ_OPTIONS_DECLARE_STRING(animation3, + "Path to the third animation (ozz archive format).", + "media/animation3.ozz", false) + +// First motion track archive can be specified as an option. +OZZ_OPTIONS_DECLARE_STRING(motion1, + "Path to the first motion (ozz archive format).", + "media/motion1.ozz", false) + +// Second motion track archive can be specified as an option. +OZZ_OPTIONS_DECLARE_STRING(motion2, + "Path to the first motion (ozz archive format).", + "media/motion2.ozz", false) + +// Third motion track archive can be specified as an option. +OZZ_OPTIONS_DECLARE_STRING(motion3, + "Path to the first motion (ozz archive format).", + "media/motion3.ozz", false) + +class MotionBlendSampleApplication : public ozz::sample::Application { + protected: + // Updates current animation time and skeleton pose. + virtual bool OnUpdate(float _dt, float) { + // Updates blending parameters and synchronizes animations if control mode + // is not manual. + if (!manual_) { + UpdateRuntimeParameters(); + } + + // Updates and samples all animations to their respective local space + // transform buffers. + for (auto& sampler : samplers_) { + // Updates animations time. + int loops = sampler.controller.Update(sampler.animation, _dt); + + // Updates motion. + //----------------------------------------------------------------------- + + // Updates motion accumulator. + if (!sampler.motion_sampler.Update( + sampler.motion_track, sampler.controller.time_ratio(), loops)) { + return false; + } + + // Updates animation + //----------------------------------------------------------------------- + + // Early out if this sampler weight makes it irrelevant during animation + // blending. + if (sampler.weight <= 0.f) { + continue; + } + + // Setup sampling job. + ozz::animation::SamplingJob sampling_job; + sampling_job.animation = &sampler.animation; + sampling_job.context = &sampler.context; + sampling_job.ratio = sampler.controller.time_ratio(); + sampling_job.output = make_span(sampler.locals); + + // Samples animation. + if (!sampling_job.Run()) { + return false; + } + } + + // Blends motion. + //------------------------------------------------------------------------- + + ozz::math::Transform delta = ozz::math::Transform::identity(); + delta.rotation = {0, 0, 0, 0}; + for (const auto& sampler : samplers_) { + delta.translation = + delta.translation + + sampler.motion_sampler.delta.translation * sampler.weight; + delta.rotation = delta.rotation + + sampler.motion_sampler.delta.rotation * sampler.weight; + } + delta.rotation = + NormalizeSafe(delta.rotation, ozz::math::Quaternion::identity()); + accumulator_.Update(delta, FrameRotation(_dt)); + + // Updates the character transform matrix. + const auto& transform = accumulator_.current; + transform_ = ozz::math::Float4x4::FromAffine( + transform.translation, transform.rotation, transform.scale); + + // Blends animations. + //------------------------------------------------------------------------- + // Blends the local spaces transforms computed by sampling all animations + // (1st stage just above), and outputs the result to the local space + // transform buffer blended_locals_ + + // Prepares blending layers. + ozz::animation::BlendingJob::Layer layers[kNumLayers]; + for (int i = 0; i < kNumLayers; ++i) { + layers[i].transform = make_span(samplers_[i].locals); + layers[i].weight = samplers_[i].weight; + } + + // Setups blending job. + ozz::animation::BlendingJob blend_job; + blend_job.layers = layers; + blend_job.rest_pose = skeleton_.joint_rest_poses(); + blend_job.output = make_span(blended_locals_); + + // Blends. + if (!blend_job.Run()) { + return false; + } + + // Converts from local space to model space matrices. + // Gets the output of the blending stage, and converts it to model space. + + // Setup local-to-model conversion job. + ozz::animation::LocalToModelJob ltm_job; + ltm_job.skeleton = &skeleton_; + ltm_job.input = make_span(blended_locals_); + ltm_job.output = make_span(models_); + + // Runs ltm job. + if (!ltm_job.Run()) { + return false; + } + + return true; + } + + // Compute rotation to apply for the given _duration + ozz::math::Quaternion FrameRotation(float _duration) const { + const float angle = angular_velocity_ * _duration; + return ozz::math::Quaternion::FromEuler({angle, 0, 0}); + } + + // Computes blending weight and synchronizes playback speed when the "manual" + // option is off. + void UpdateRuntimeParameters() { + // Computes weight parameters for all samplers. + const float kNumIntervals = kNumLayers - 1; + const float kInterval = 1.f / kNumIntervals; + for (int i = 0; i < kNumLayers; ++i) { + const float med = i * kInterval; + const float x = blend_ratio_ - med; + const float y = ((x < 0.f ? x : -x) + kInterval) * kNumIntervals; + samplers_[i].weight = ozz::math::Max(0.f, y); + } + + // Synchronizes animations. + // First computes loop cycle duration. Selects the 2 samplers that define + // interval that contains blend_ratio_. + // Uses a maximum value smaller that 1.f (-epsilon) to ensure that + // (relevant_sampler + 1) is always valid. + const int relevant_sampler = + static_cast((blend_ratio_ - 1e-3f) * (kNumLayers - 1)); + assert(relevant_sampler + 1 < kNumLayers); + Sampler& sampler_l = samplers_[relevant_sampler]; + Sampler& sampler_r = samplers_[relevant_sampler + 1]; + + // Interpolates animation durations using their respective weights, to + // find the loop cycle duration that matches blend_ratio_. + const float loop_duration = + sampler_l.animation.duration() * sampler_l.weight + + sampler_r.animation.duration() * sampler_r.weight; + + // Finally finds the speed coefficient for all samplers. + const float inv_loop_duration = 1.f / loop_duration; + for (auto& sampler : samplers_) { + const float speed = sampler.animation.duration() * inv_loop_duration; + sampler.controller.set_playback_speed(speed); + } + } + + virtual bool OnDisplay(ozz::sample::Renderer* _renderer) { + // Renders character at transform_ location. + return _renderer->DrawPosture(skeleton_, make_span(models_), transform_); + } + + virtual bool OnInitialize() { + // Reading skeleton. + if (!ozz::sample::LoadSkeleton(OPTIONS_skeleton, &skeleton_)) { + return false; + } + + const int num_joints = skeleton_.num_joints(); + const int num_soa_joints = skeleton_.num_soa_joints(); + + // Reading animations. + const char* animations[] = {OPTIONS_animation1, OPTIONS_animation2, + OPTIONS_animation3}; + const char* motions[] = {OPTIONS_motion1, OPTIONS_motion2, OPTIONS_motion3}; + static_assert(OZZ_ARRAY_SIZE(animations) == kNumLayers && + OZZ_ARRAY_SIZE(motions) == kNumLayers, + "Arrays mismatch."); + for (int i = 0; i < kNumLayers; ++i) { + Sampler& sampler = samplers_[i]; + + if (!ozz::sample::LoadAnimation(animations[i], &sampler.animation)) { + return false; + } + + if (!ozz::sample::LoadMotionTrack(motions[i], &sampler.motion_track)) { + return false; + } + + // Allocates sampler runtime buffers. + sampler.locals.resize(num_soa_joints); + + // Allocates a context that matches animation requirements. + sampler.context.Resize(num_joints); + } + + // Allocates local space runtime buffers of blended data. + blended_locals_.resize(num_soa_joints); + + // Allocates model space runtime buffers of blended data. + models_.resize(num_joints); + + return true; + } + + virtual void OnDestroy() {} + + virtual bool OnGui(ozz::sample::ImGui* _im_gui) { + char label[64]; + + // Exposes blending parameters. + { + static bool open = true; + ozz::sample::ImGui::OpenClose oc(_im_gui, "Blending parameters", &open); + if (open) { + if (_im_gui->DoCheckBox("Manual settings", &manual_) && !manual_) { + // Check-box state was changed, reset parameters. + for (auto& sampler : samplers_) { + sampler.controller.Reset(); + } + } + + std::snprintf(label, sizeof(label), "Blend ratio: %.2f", blend_ratio_); + _im_gui->DoSlider(label, 0.f, 1.f, &blend_ratio_, 1.f, !manual_); + + for (int i = 0; i < kNumLayers; ++i) { + Sampler& sampler = samplers_[i]; + std::snprintf(label, sizeof(label), "Weight %d: %.2f", i, + sampler.weight); + _im_gui->DoSlider(label, 0.f, 1.f, &sampler.weight, 1.f, manual_); + } + } + } + + // Exposes animations runtime playback controls. + { + static bool oc_open = false; + ozz::sample::ImGui::OpenClose oc(_im_gui, "Animation control", &oc_open); + if (oc_open) { + static bool open[] = {true, true, true}; + static_assert(OZZ_ARRAY_SIZE(open) == kNumLayers, + "Arrays size mismatch"); + const char* oc_names[] = {"Animation 1", "Animation 2", "Animation 3"}; + static_assert(OZZ_ARRAY_SIZE(oc_names) == kNumLayers, + "Arrays size mismatch"); + for (int i = 0; i < kNumLayers; ++i) { + Sampler& sampler = samplers_[i]; + ozz::sample::ImGui::OpenClose loc(_im_gui, oc_names[i], nullptr); + if (open[i]) { + sampler.controller.OnGui(sampler.animation, _im_gui, manual_); + } + } + } + } + + { + static bool open = true; + ozz::sample::ImGui::OpenClose oc(_im_gui, "Motion control", &open); + if (open) { + // _im_gui->DoCheckBox("Use motion position", &apply_motion_position_); + // _im_gui->DoCheckBox("Use motion rotation", &apply_motion_rotation_); + std::snprintf(label, sizeof(label), "Angular vel: %.0f deg/s", + angular_velocity_ * 180.f / ozz::math::kPi); + _im_gui->DoSlider(label, -ozz::math::kPi_2, ozz::math::kPi_2, + &angular_velocity_); + if (_im_gui->DoButton("Teleport")) { + for (auto& sampler : samplers_) { + sampler.motion_sampler.Teleport(ozz::math::Transform::identity()); + } + accumulator_.Teleport(ozz::math::Transform::identity()); + } + } + } + + return true; + } + + virtual void GetSceneBounds(ozz::math::Box* _bound) const { + ozz::sample::ComputeSkeletonBounds(skeleton_, transform_, _bound); + } + + private: + // Runtime skeleton. + ozz::animation::Skeleton skeleton_; + + // Global blend ratio in range [0,1] that controls all blend parameters and + // synchronizes playback speeds. A value of 0 gives full weight to the first + // animation, and 1 to the last. + float blend_ratio_ = .3f; + + // Switch to manual control of animations and blending parameters. + bool manual_ = false; + + // The number of layers to blend. + enum { + kNumLayers = 3, + }; + + // Sampler structure contains all the data required to sample a single + // animation. + struct Sampler { + // Playback animation controller. + ozz::sample::PlaybackController controller; + + // Blending weight for the layer. + float weight = 1.f; + + // Runtime animation. + ozz::animation::Animation animation; + + // Sampling context. + ozz::animation::SamplingJob::Context context; + + // Runtime motion track. + ozz::sample::MotionTrack motion_track; + + // Motion sampling & accumulator. + ozz::sample::MotionSampler motion_sampler; + + // Buffer of local transforms as sampled from animation_. + ozz::vector locals; + } samplers_[kNumLayers]; // kNumLayers animations to blend. + + // Buffer of local transforms which stores the blending result. + ozz::vector blended_locals_; + + // Buffer of model space matrices. These are computed by the local-to-model + // job after the blending stage. + ozz::vector models_; + + // Uses a delta accumulator to accumulate the blended delta motion of the + // frame. + ozz::sample::MotionDeltaAccumulator accumulator_; + + // Rotation deformation, rad/s + float angular_velocity_ = ozz::math::kPi_4; + + // Character transform. + ozz::math::Float4x4 transform_; +}; + +int main(int _argc, const char** _argv) { + const char* title = "Ozz-animation sample: Motion blending"; + return MotionBlendSampleApplication().Run(_argc, _argv, "1.2", title); +}