From 6a743324f2350883396297ae96aaf08f66bdaee7 Mon Sep 17 00:00:00 2001 From: Guillaume Blanc Date: Tue, 26 Mar 2024 21:33:05 +0100 Subject: [PATCH] Adds raw track sampling utility --- .vscode/settings.json | 66 +++ .../ozz/animation/offline/motion_extractor.h | 78 ++++ include/ozz/animation/offline/raw_track.h | 2 +- .../ozz/animation/offline/raw_track_utils.h | 47 +++ samples/motion/CMakeLists.txt | 45 ++ samples/motion/README.md | 9 + samples/motion/sample_motion.cc | 384 ++++++++++++++++++ src/animation/offline/CMakeLists.txt | 2 + src/animation/offline/motion_extractor.cc | 168 ++++++++ src/animation/offline/raw_track_utils.cc | 136 +++++++ src/animation/offline/track_builder.cc | 6 +- src/animation/offline/track_optimizer.cc | 2 - test/animation/offline/CMakeLists.txt | 9 + .../offline/motion_extractor_tests.cc | 100 +++++ .../offline/raw_track_utils_tests.cc | 170 ++++++++ 15 files changed, 1217 insertions(+), 7 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 include/ozz/animation/offline/motion_extractor.h create mode 100644 include/ozz/animation/offline/raw_track_utils.h create mode 100644 samples/motion/CMakeLists.txt create mode 100644 samples/motion/README.md create mode 100644 samples/motion/sample_motion.cc create mode 100644 src/animation/offline/motion_extractor.cc create mode 100644 src/animation/offline/raw_track_utils.cc create mode 100644 test/animation/offline/motion_extractor_tests.cc create mode 100644 test/animation/offline/raw_track_utils_tests.cc diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..f8879b2f1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,66 @@ +{ + "cmake.configureOnOpen": true, + "files.associations": { + "__bit_reference": "cpp", + "__config": "cpp", + "__hash_table": "cpp", + "__locale": "cpp", + "__node_handle": "cpp", + "__split_buffer": "cpp", + "__threading_support": "cpp", + "__tree": "cpp", + "__verbose_abort": "cpp", + "array": "cpp", + "bitset": "cpp", + "cctype": "cpp", + "charconv": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "complex": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "execution": "cpp", + "forward_list": "cpp", + "fstream": "cpp", + "future": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "ios": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "limits": "cpp", + "list": "cpp", + "locale": "cpp", + "map": "cpp", + "mutex": "cpp", + "new": "cpp", + "optional": "cpp", + "ostream": "cpp", + "queue": "cpp", + "ratio": "cpp", + "set": "cpp", + "sstream": "cpp", + "stack": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "string": "cpp", + "string_view": "cpp", + "tuple": "cpp", + "typeinfo": "cpp", + "unordered_map": "cpp", + "unordered_set": "cpp", + "valarray": "cpp", + "variant": "cpp", + "vector": "cpp", + "algorithm": "cpp" + } +} \ No newline at end of file diff --git a/include/ozz/animation/offline/motion_extractor.h b/include/ozz/animation/offline/motion_extractor.h new file mode 100644 index 000000000..6c4d2d112 --- /dev/null +++ b/include/ozz/animation/offline/motion_extractor.h @@ -0,0 +1,78 @@ +//----------------------------------------------------------------------------// +// // +// 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. // +// // +//----------------------------------------------------------------------------// + +#ifndef OZZ_OZZ_ANIMATION_OFFLINE_MOTION_EXTRACTOR_H_ +#define OZZ_OZZ_ANIMATION_OFFLINE_MOTION_EXTRACTOR_H_ + +#include "ozz/animation/offline/export.h" + +namespace ozz { +namespace animation { +class Skeleton; +namespace offline { + +// Forward declare offline types. +struct RawAnimation; +struct RawFloat3Track; +struct RawQuaternionTrack; + +class OZZ_ANIMOFFLINE_DLL MotionExtractor { + public: + bool operator()(const RawAnimation& _input, const Skeleton& _skeleton, + RawFloat3Track* _motion_position, + RawQuaternionTrack* _motion_rotation, + RawAnimation* _output) const; + + struct PositionComponents { + bool x = true; + bool y = true; + bool z = true; + bool all() const { return x && y && z; } + } position_components; + + struct RotationComponents { + bool x = false; + bool y = true; + bool z = false; + bool all() const { return x && y && z; } + } rotation_components; + + // Defines teh reference transform to use while extracting root motion. + enum class Reference { + kIdentity, // Identity / global reference + kSkeleton, // Use skeleton rest pose root bone transform + kFirstFrame, // Uses root transform of the animation's first frame + } reference = Reference::kFirstFrame; + + bool bake_position = true; + bool bake_rotation = true; + bool bake_scale = true; +}; +} // namespace offline +} // namespace animation +} // namespace ozz +#endif // OZZ_OZZ_ANIMATION_OFFLINE_MOTION_EXTRACTOR_H_ diff --git a/include/ozz/animation/offline/raw_track.h b/include/ozz/animation/offline/raw_track.h index d3eb6e6b6..dfcd229bc 100644 --- a/include/ozz/animation/offline/raw_track.h +++ b/include/ozz/animation/offline/raw_track.h @@ -43,7 +43,7 @@ namespace offline { struct RawTrackInterpolation { enum Value { kStep, // All values following this key, up to the next key, are equal. - kLinear, // All value between this key and the next are linearly + kLinear, // All values between this key and the next are linearly // interpolated. }; }; diff --git a/include/ozz/animation/offline/raw_track_utils.h b/include/ozz/animation/offline/raw_track_utils.h new file mode 100644 index 000000000..b88ab4af7 --- /dev/null +++ b/include/ozz/animation/offline/raw_track_utils.h @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------------// +// // +// 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. // +// // +//----------------------------------------------------------------------------// + +#ifndef OZZ_OZZ_ANIMATION_OFFLINE_RAW_TRACK_UTILS_H_ +#define OZZ_OZZ_ANIMATION_OFFLINE_RAW_TRACK_UTILS_H_ + +#include "ozz/animation/offline/export.h" +#include "ozz/animation/offline/raw_track.h" + +namespace ozz { +namespace animation { +namespace offline { + +// Samples a RawTrack. This function shall be used for offline purpose. +// Returns false if track is invalid. +template +OZZ_ANIMOFFLINE_DLL bool SampleTrack(const _RawTrack& _track, float _ratio, + typename _RawTrack::ValueType* _value); + +} // namespace offline +} // namespace animation +} // namespace ozz +#endif // OZZ_OZZ_ANIMATION_OFFLINE_RAW_TRACK_UTILS_H_ diff --git a/samples/motion/CMakeLists.txt b/samples/motion/CMakeLists.txt new file mode 100644 index 000000000..72b59d8fd --- /dev/null +++ b/samples/motion/CMakeLists.txt @@ -0,0 +1,45 @@ + +add_custom_command( + DEPENDS $<$:BUILD_DATA> + "${CMAKE_CURRENT_LIST_DIR}/README.md" + "${ozz_media_directory}/bin/pab_skeleton.ozz" + "${ozz_media_directory}/bin/pab_atlas_raw.ozz" + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/README.md" + "${CMAKE_CURRENT_BINARY_DIR}/media/skeleton.ozz" + "${CMAKE_CURRENT_BINARY_DIR}/media/raw_animation.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_atlas_raw.ozz" "./media/raw_animation.ozz" + VERBATIM) + +add_executable(sample_motion +sample_motion.cc + "${CMAKE_CURRENT_BINARY_DIR}/README.md" + "${CMAKE_CURRENT_BINARY_DIR}/media/skeleton.ozz" + "${CMAKE_CURRENT_BINARY_DIR}/media/raw_animation.ozz") +target_link_libraries(sample_motion + sample_framework) +target_copy_shared_libraries(sample_motion) + +set_target_properties(sample_motion + PROPERTIES FOLDER "samples") + +if(EMSCRIPTEN) + # Resource files are embedded to the output file with emscripten + set_target_properties(sample_motion + PROPERTIES LINK_FLAGS "--embed-file media --embed-file README.md") + + install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/sample_motion.html + ${CMAKE_CURRENT_BINARY_DIR}/sample_motion.js + ${CMAKE_CURRENT_BINARY_DIR}/sample_motion.wasm + DESTINATION bin/samples/motion) +else() + install(TARGETS sample_motion DESTINATION bin/samples/motion) + install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/media DESTINATION bin/samples/motion) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/README.md DESTINATION bin/samples/motion) +endif(EMSCRIPTEN) + +add_test(NAME sample_motion COMMAND sample_motion "--max_idle_loops=${ozz_sample_testing_loops}" $<$:--norender>) + diff --git a/samples/motion/README.md b/samples/motion/README.md new file mode 100644 index 000000000..67b8a396d --- /dev/null +++ b/samples/motion/README.md @@ -0,0 +1,9 @@ +# Ozz-animation sample: Animation motion + +## Description + +## Concept + +## Sample usage + +## Implementation diff --git a/samples/motion/sample_motion.cc b/samples/motion/sample_motion.cc new file mode 100644 index 000000000..7f9c3cc3d --- /dev/null +++ b/samples/motion/sample_motion.cc @@ -0,0 +1,384 @@ +//----------------------------------------------------------------------------// +// // +// 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/renderer.h" +#include "framework/utils.h" +#include "ozz/animation/offline/animation_builder.h" +#include "ozz/animation/offline/animation_optimizer.h" +#include "ozz/animation/offline/motion_extractor.h" +#include "ozz/animation/offline/raw_animation.h" +#include "ozz/animation/offline/raw_track.h" +#include "ozz/animation/offline/track_builder.h" +#include "ozz/animation/offline/track_optimizer.h" +#include "ozz/animation/runtime/animation.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/animation/runtime/track.h" +#include "ozz/animation/runtime/track_sampling_job.h" +#include "ozz/base/log.h" +#include "ozz/base/maths/box.h" +#include "ozz/base/maths/simd_math.h" +#include "ozz/base/maths/soa_transform.h" +#include "ozz/base/maths/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) + +// Animation archive can be specified as an option. +OZZ_OPTIONS_DECLARE_STRING(animation, + "Path to the animation (ozz archive format).", + "media/raw_animation.ozz", false) + +struct MotionCache { + void Update(ozz::math::Transform _current) { + delta.translation = _current.translation - last.translation; + delta.rotation = Conjugate(last.rotation) * _current.rotation; + delta.scale = _current.scale / last.scale; + last = _current; + } + + ozz::math::Transform last = ozz::math::Transform::identity(); + ozz::math::Transform delta = ozz::math::Transform::identity(); +}; + +class MotionSampleApplication : public ozz::sample::Application { + public: + MotionSampleApplication() {} + + protected: + // Updates current animation time and skeleton pose. + virtual bool OnUpdate(float _dt, float) { + // Updates current animation time. + controller_.Update(animation_, _dt); + + // Get position from motion track + ozz::math::Float3 position; + ozz::animation::Float3TrackSamplingJob position_sampler; + position_sampler.track = &motion_position_; + position_sampler.result = &position; + position_sampler.ratio = controller_.time_ratio(); + if (!position_sampler.Run()) { + return false; + } + + // Get rotation from motion track + ozz::math::Quaternion rotation; + ozz::animation::QuaternionTrackSamplingJob rotation_sampler; + rotation_sampler.track = &motion_rotation_; + rotation_sampler.result = &rotation; + rotation_sampler.ratio = controller_.time_ratio(); + if (!rotation_sampler.Run()) { + return false; + } + + // Build character transform + transform_ = ozz::math::Float4x4::identity(); + + if (apply_motion_position_) { + transform_ = + transform_ * ozz::math::Float4x4::Translation( + ozz::math::simd_float4::Load3PtrU(&position.x)); + } + if (apply_motion_rotation_) { + transform_ = + transform_ * ozz::math::Float4x4::FromQuaternion( + ozz::math::simd_float4::LoadPtrU(&rotation.x)); + } + + // Samples optimized animation at t = animation_time_. + ozz::animation::SamplingJob sampling_job; + sampling_job.animation = &animation_; + sampling_job.context = &context_; + sampling_job.ratio = controller_.time_ratio(); + sampling_job.output = make_span(locals_); + if (!sampling_job.Run()) { + return false; + } + + // Converts from local space to model space matrices. + ozz::animation::LocalToModelJob ltm_job; + ltm_job.skeleton = &skeleton_; + ltm_job.input = make_span(locals_); + ltm_job.output = make_span(models_); + if (!ltm_job.Run()) { + return false; + } + + return true; + } + + // Samples animation, transforms to model space and renders. + virtual bool OnDisplay(ozz::sample::Renderer* _renderer) { + bool success = true; + success &= + _renderer->DrawPosture(skeleton_, make_span(models_), transform_); + + success &= + _renderer->DrawBoxIm(ozz::math::Box(ozz::math::Float3(-.3f, 0, -.2f), + ozz::math::Float3(.3f, 1.8f, .2f)), + transform_, ozz::sample::kWhite); + + const float at = controller_.time_ratio(); + const float step = 1.f / (animation_.duration() * 30.f); + success &= DrawMotion(_renderer, motion_position_, 0.f, at, step, + ozz::sample::kGreen); + success &= DrawMotion(_renderer, motion_position_, at, 1.f, step, + ozz::sample::kRed); + return success; + } + + bool ExtractMotion() { + // Position + ozz::animation::offline::RawFloat3Track raw_motion_position; + raw_motion_position.name = "motion_position"; + + // Rotation + ozz::animation::offline::RawQuaternionTrack raw_motion_rotation; + raw_motion_rotation.name = "motion_rotation"; + + ozz::animation::offline::RawAnimation baked_animation; + if (!motion_extractor_(raw_animation_, skeleton_, &raw_motion_position, + &raw_motion_rotation, &baked_animation)) { + return false; + } + + // Track optimization and runtime bulding + { + ozz::animation::offline::TrackOptimizer optimizer; + ozz::animation::offline::RawFloat3Track raw_track_position_opt; + if (!optimizer(raw_motion_position, &raw_track_position_opt)) { + return false; + } + raw_motion_position = raw_track_position_opt; + + ozz::animation::offline::RawQuaternionTrack raw_track_rotation_opt; + if (!optimizer(raw_motion_rotation, &raw_track_rotation_opt)) { + return false; + } + raw_motion_rotation = raw_track_rotation_opt; + + // Build runtime tracks + ozz::animation::offline::TrackBuilder track_builder; + auto motion_p = track_builder(raw_motion_position); + auto motion_r = track_builder(raw_motion_rotation); + if (!motion_p || !motion_r) { + return false; + } + motion_position_ = std::move(*motion_p); + motion_rotation_ = std::move(*motion_r); + } + + // Track optimization and runtime bulding + { + ozz::animation::offline::RawAnimation baked_animation_opt; + ozz::animation::offline::AnimationOptimizer optimizer; + if (!optimizer(baked_animation, skeleton_, &baked_animation_opt)) { + return false; + } + + ozz::animation::offline::AnimationBuilder builder; + auto animation = builder(baked_animation_opt); + if (!animation) { + return false; + } + animation_ = std::move(*animation); + + // Animation was changed, context needs to know. + context_.Invalidate(); + } + + return true; + } + + bool DrawMotion(ozz::sample::Renderer* _renderer, + const ozz::animation::Float3Track& _track, float _from, + float _to, float _step, ozz::sample::Color _color) { + if (_from > _to || _step <= 0.f) { + return false; + } + + ozz::math::Float3 value; + ozz::animation::Float3TrackSamplingJob motion_sampler; + motion_sampler.track = &_track; + motion_sampler.result = &value; + + ozz::vector points; + for (float t = _from; t <= _to + _step; t += _step) { + motion_sampler.ratio = ozz::math::Min(t, _to); + if (!motion_sampler.Run()) { + return false; + } + points.push_back(value); + } + + return _renderer->DrawLineStrip(make_span(points), _color, + ozz::math::Float4x4::identity()); + } + + virtual bool OnInitialize() { + // Reading skeleton. + if (!ozz::sample::LoadSkeleton(OPTIONS_skeleton, &skeleton_)) { + return false; + } + + // Reading animation. + if (!ozz::sample::LoadRawAnimation(OPTIONS_animation, &raw_animation_)) { + return false; + } + + if (!ExtractMotion()) { + return false; + } + + // Skeleton and animation needs to match. + if (skeleton_.num_joints() != animation_.num_tracks()) { + return false; + } + + // Allocates runtime buffers. + const int num_soa_joints = skeleton_.num_soa_joints(); + locals_.resize(num_soa_joints); + const int num_joints = skeleton_.num_joints(); + models_.resize(num_joints); + + // Allocates a context that matches animation requirements. + context_.Resize(num_joints); + + return true; + } + + virtual void OnDestroy() {} + + virtual bool OnGui(ozz::sample::ImGui* _im_gui) { + // Exposes animation runtime playback controls. + { + static bool open = true; + ozz::sample::ImGui::OpenClose oc(_im_gui, "Animation control", &open); + if (open) { + controller_.OnGui(animation_, _im_gui); + } + } + + { + bool rebuild = false; + static bool open = true; + ozz::sample::ImGui::OpenClose oc(_im_gui, "Motion extraction", &open); + _im_gui->DoLabel("Position components"); + rebuild |= + _im_gui->DoCheckBox("x", &motion_extractor_.position_components.x); + rebuild |= + _im_gui->DoCheckBox("y", &motion_extractor_.position_components.y); + rebuild |= + _im_gui->DoCheckBox("z", &motion_extractor_.position_components.z); + + _im_gui->DoLabel("Rotation components"); + rebuild |= + _im_gui->DoCheckBox("x", &motion_extractor_.rotation_components.x); + rebuild |= + _im_gui->DoCheckBox("y", &motion_extractor_.rotation_components.y); + rebuild |= + _im_gui->DoCheckBox("z", &motion_extractor_.rotation_components.z); + + _im_gui->DoLabel("Reference"); + int ref = static_cast(motion_extractor_.reference); + rebuild |= _im_gui->DoRadioButton(0, "Identity", &ref); + rebuild |= _im_gui->DoRadioButton(1, "Skeleton", &ref); + rebuild |= _im_gui->DoRadioButton(2, "First frame", &ref); + motion_extractor_.reference = + static_cast(ref); + + if (rebuild) { + if (!ExtractMotion()) { + return false; + } + } + } + + { + 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_); + } + } + return true; + } + + virtual void GetSceneBounds(ozz::math::Box* _bound) const { + ozz::sample::ComputePostureBounds(make_span(models_), transform_, _bound); + } + + private: + // Playback animation controller. This is a utility class that helps with + // controlling animation playback time. + ozz::sample::PlaybackController controller_; + + // Store extractor to expose parameters to GUI. + // In a real use case, no need to store it. + ozz::animation::offline::MotionExtractor motion_extractor_; + + // Runtime skeleton. + ozz::animation::Skeleton skeleton_; + + // Original animation. + ozz::animation::offline::RawAnimation raw_animation_; + + // Runtime animation. + ozz::animation::Animation animation_; + + // Runtime motion track + ozz::animation::Float3Track motion_position_; + ozz::animation::QuaternionTrack motion_rotation_; + + // Sampling context. + ozz::animation::SamplingJob::Context context_; + + // Character transform. + ozz::math::Float4x4 transform_; + + // Buffer of local transforms as sampled from animation_. + ozz::vector locals_; + + // Buffer of model space matrices. + ozz::vector models_; + + bool apply_motion_position_ = true; + bool apply_motion_rotation_ = true; +}; + +int main(int _argc, const char** _argv) { + const char* title = "Ozz-animation sample: Root motion extraction"; + return MotionSampleApplication().Run(_argc, _argv, "1.0", title); +} diff --git a/src/animation/offline/CMakeLists.txt b/src/animation/offline/CMakeLists.txt index 7dc853f62..5f83ca119 100644 --- a/src/animation/offline/CMakeLists.txt +++ b/src/animation/offline/CMakeLists.txt @@ -20,6 +20,8 @@ add_library(ozz_animation_offline skeleton_builder.cc ${PROJECT_SOURCE_DIR}/include/ozz/animation/offline/raw_track.h raw_track.cc + ${PROJECT_SOURCE_DIR}/include/ozz/animation/offline/raw_track_utils.h + raw_track_utils.cc ${PROJECT_SOURCE_DIR}/include/ozz/animation/offline/track_builder.h track_builder.cc ${PROJECT_SOURCE_DIR}/include/ozz/animation/offline/track_optimizer.h diff --git a/src/animation/offline/motion_extractor.cc b/src/animation/offline/motion_extractor.cc new file mode 100644 index 000000000..5bd2f54b4 --- /dev/null +++ b/src/animation/offline/motion_extractor.cc @@ -0,0 +1,168 @@ +//----------------------------------------------------------------------------// +// // +// 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 "ozz/animation/offline/motion_extractor.h" + +#include + +#include "ozz/animation/offline/raw_animation.h" +#include "ozz/animation/offline/raw_animation_utils.h" +#include "ozz/animation/offline/raw_track.h" +#include "ozz/animation/runtime/skeleton.h" + +// TEMP +#include "ozz/base/maths/soa_transform.h" + +namespace ozz { +namespace animation { +namespace offline { + +namespace { +ozz::math::Transform BuildReference(MotionExtractor::Reference _reference, + const Skeleton& _skeleton, + const RawAnimation::JointTrack& _track) { + auto ref = ozz::math::Transform::identity(); + switch (_reference) { + case MotionExtractor::Reference::kSkeleton: { + // Extract the reference transform from skeleton rest pose. + const auto& soa_root = _skeleton.joint_rest_poses()[0]; + math::SimdFloat4 translation[4]; + math::SimdFloat4 rotation[4]; + math::SimdFloat4 scale[4]; + math::Transpose3x4(&soa_root.translation.x, translation); + math::Transpose4x4(&soa_root.rotation.x, rotation); + math::Transpose3x4(&soa_root.scale.x, scale); + + math::Store3PtrU(translation[0], &ref.translation.x); + math::StorePtrU(rotation[0], &ref.rotation.x); + math::Store3PtrU(scale[0], &ref.scale.x); + } break; + case MotionExtractor::Reference::kFirstFrame: { + // Extract the reference transform from animation first frame. + if (!_track.translations.empty()) { + ref.translation = _track.translations[0].value; + } + if (!_track.rotations.empty()) { + ref.rotation = _track.rotations[0].value; + } + if (!_track.scales.empty()) { + ref.scale = _track.scales[0].value; + } + } break; + default: + break; + } + return ref; +} +} // namespace + +bool MotionExtractor::operator()(const RawAnimation& _input, + const Skeleton& _skeleton, + RawFloat3Track* _motion_position, + RawQuaternionTrack* _motion_rotation, + RawAnimation* _output) const { + if (&_input == _output) { + return false; + } + + // Validate animation. + if (!_input.Validate()) { + return false; + } + + if (_output) { + *_output = _input; + } + + // Track to extract motion from + const int track_index = 0; + + // Baking reference + auto ref = BuildReference(reference, _skeleton, _input.tracks[track_index]); + + // Extract root position + if (_motion_position) { + // Copy the position keyframes + _motion_position->keyframes.clear(); + for (const auto& src_key : _input.tracks[track_index].translations) { + const typename RawFloat3Track::Keyframe key = { + ozz::animation::offline::RawTrackInterpolation::kLinear, + src_key.time / _input.duration, + {position_components.x ? src_key.value.x : 0, + position_components.y ? src_key.value.y : 0, + position_components.z ? src_key.value.z : 0}}; + _motion_position->keyframes.push_back(key); + } + } + + // Extract root rotation + if (_motion_rotation) { + _motion_rotation->keyframes.clear(); + for (auto& joint_key : _output->tracks[track_index].rotations) { + // Decompose the source keyframe into components, extract root rotation + // and rebuild joint rotations + const auto decomp = ToEuler(joint_key.value * Conjugate(ref.rotation)); + const auto motion_q = + math::Quaternion::FromEuler(rotation_components.y ? decomp.x : 0, + rotation_components.x ? decomp.y : 0, + rotation_components.z ? decomp.z : 0); + joint_key.value = Conjugate(motion_q) * joint_key.value; + + const typename RawQuaternionTrack::Keyframe key = { + ozz::animation::offline::RawTrackInterpolation::kLinear, + joint_key.time / _input.duration, motion_q}; + _motion_rotation->keyframes.push_back(key); + } + } + + // When root motion is applied, then root rotation is applied before joint + // translation. Hence joint's translation should be corrected to support this + // order. + assert(_output->tracks[track_index].translations.size() == + _motion_position->keyframes.size()); + for (size_t i = 0; i < _output->tracks[track_index].translations.size(); + i++) { + const auto& motion_q = + _motion_rotation->keyframes[i].value; // TODO interpolate + + const auto& motion_key = _motion_position->keyframes[i]; + auto& joint_key = _output->tracks[track_index].translations[i]; + joint_key.value = TransformVector(Conjugate(motion_q), + joint_key.value - motion_key.value); + } + // Validate outputs + bool success = true; + if (_motion_position) success &= _motion_position->Validate(); + if (_motion_rotation) success &= _motion_rotation->Validate(); + if (_output) success &= _output->Validate(); + + return success; +} + +} // namespace offline +} // namespace animation +} // namespace ozz diff --git a/src/animation/offline/raw_track_utils.cc b/src/animation/offline/raw_track_utils.cc new file mode 100644 index 000000000..8d0b721dc --- /dev/null +++ b/src/animation/offline/raw_track_utils.cc @@ -0,0 +1,136 @@ +//----------------------------------------------------------------------------// +// // +// 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 "ozz/animation/offline/raw_track_utils.h" + +#include +#include + +// Needs runtime track to access TrackPolicy. +#include "ozz/animation/runtime/track.h" + +namespace ozz { +namespace animation { +namespace offline { + +namespace { + +// Less comparator, used by search algorithm to walk through track sorted +// keyframes +template +bool TrackLess(const _Key& _left, const _Key& _right) { + return _left.ratio < _right.ratio; +} + +template +typename _Key::ValueType TrackLerp(const _Key& _left, const _Key& _right, + float _alpha) { + if (_left.interpolation == RawTrackInterpolation::kStep && _alpha < 1.f) { + return _left.value; + } + return animation::internal::TrackPolicy::Lerp( + _left.value, _right.value, _alpha); +} + +template <> +math::Quaternion TrackLerp(const RawQuaternionTrack::Keyframe& _left, + const RawQuaternionTrack::Keyframe& _right, + float _alpha) { + if (_left.interpolation == RawTrackInterpolation::kStep && _alpha < 1.f) { + return _left.value; + } + // Fix quaternion interpolation to always use the shortest path. + const auto& lq = _left.value; + auto rq = _right.value; + const float dot = lq.x * rq.x + lq.y * rq.y + lq.z * rq.z + lq.w * rq.w; + if (dot < 0.f) { + rq = -rq; + } + return animation::internal::TrackPolicy::Lerp(lq, rq, + _alpha); +} + +template +typename _Keyframes::value_type::ValueType _SampleTrack( + const _Keyframes& _keyframes, float _ratio) { + using Keyframe = typename _Keyframes::value_type; + if (_keyframes.size() == 0) { + // Return identity if there's no key for this track. + return animation::internal::TrackPolicy< + typename Keyframe::ValueType>::identity(); + } else if (_ratio <= _keyframes.front().ratio) { + // Returns the first keyframe if _time is before the first keyframe. + return _keyframes.front().value; + } else if (_ratio >= _keyframes.back().ratio) { + // Returns the last keyframe if _time is before the last keyframe. + return _keyframes.back().value; + } else { + // Needs to interpolate the 2 keyframes before and after _time. + assert(_keyframes.size() >= 2); + + // First find the 2 keys. + typename _Keyframes::value_type cmp; + cmp.ratio = _ratio; + auto it = std::lower_bound(array_begin(_keyframes), array_end(_keyframes), + cmp, TrackLess); + assert(it > array_begin(_keyframes) && it < array_end(_keyframes)); + + // Then interpolate them at t = _time. + const auto& right = it[0]; + const auto& left = it[-1]; + const float alpha = (_ratio - left.ratio) / (right.ratio - left.ratio); + return TrackLerp(left, right, alpha); + } +} +} // namespace + +template +bool SampleTrack(const _RawTrack& _track, float _ratio, + typename _RawTrack::ValueType* _value) { + if (!_track.Validate()) { + return false; + } + + *_value = _SampleTrack(_track.keyframes, _ratio); + return true; +} + +// Explicitly instantiate supported raw tracks. +template bool SampleTrack(const RawFloatTrack& _track, float _ratio, + float* _value); +template bool SampleTrack(const RawFloat2Track& _track, float _ratio, + math::Float2* _value); +template bool SampleTrack(const RawFloat3Track& _track, float _ratio, + math::Float3* _value); +template bool SampleTrack(const RawFloat4Track& _track, float _ratio, + math::Float4* _value); +template bool SampleTrack(const RawQuaternionTrack& _track, float _ratio, + math::Quaternion* _value); + +} // namespace offline +} // namespace animation +} // namespace ozz diff --git a/src/animation/offline/track_builder.cc b/src/animation/offline/track_builder.cc index 9ea73f019..20dca62b2 100644 --- a/src/animation/offline/track_builder.cc +++ b/src/animation/offline/track_builder.cc @@ -32,11 +32,9 @@ #include #include -#include "ozz/base/memory/allocator.h" - #include "ozz/animation/offline/raw_track.h" - #include "ozz/animation/runtime/track.h" +#include "ozz/base/memory/allocator.h" namespace ozz { namespace animation { @@ -185,7 +183,7 @@ void Fixup( // Ensures quaternions are all on the same hemisphere. if (i == 0) { if (src_key.w < 0.f) { - src_key = -src_key; // Q an -Q are the same rotation. + src_key = -src_key; // Q and -Q are the same rotation. } } else { RawQuaternionTrack::ValueType& prev_key = _keyframes->at(i - 1).value; diff --git a/src/animation/offline/track_optimizer.cc b/src/animation/offline/track_optimizer.cc index 0158f0c1d..472552f7e 100644 --- a/src/animation/offline/track_optimizer.cc +++ b/src/animation/offline/track_optimizer.cc @@ -54,8 +54,6 @@ struct Adapter { typedef typename _KeyFrame::ValueType ValueType; typedef typename animation::internal::TrackPolicy Policy; - Adapter() {} - bool Decimable(const _KeyFrame& _key) const { // RawTrackInterpolation::kStep keyframes aren't optimized, as steps can't // be interpolated. diff --git a/test/animation/offline/CMakeLists.txt b/test/animation/offline/CMakeLists.txt index d1f72da81..cf088959f 100644 --- a/test/animation/offline/CMakeLists.txt +++ b/test/animation/offline/CMakeLists.txt @@ -121,6 +121,15 @@ target_copy_shared_libraries(test_raw_track_archive) set_target_properties(test_raw_track_archive PROPERTIES FOLDER "ozz/tests/animation_offline") add_test(NAME test_raw_track_archive COMMAND test_raw_track_archive) +add_executable(test_raw_track_utils + raw_track_utils_tests.cc) +target_link_libraries(test_raw_track_utils + ozz_animation_offline + gtest) +target_copy_shared_libraries(test_raw_track_utils) +set_target_properties(test_raw_track_utils PROPERTIES FOLDER "ozz/tests/animation_offline") +add_test(NAME test_raw_track_utils COMMAND test_raw_track_utils) + # ozz_animation_offline fuse tests set_source_files_properties(${PROJECT_BINARY_DIR}/src_fused/ozz_animation_offline.cc PROPERTIES GENERATED 1) add_executable(test_fuse_animation_offline diff --git a/test/animation/offline/motion_extractor_tests.cc b/test/animation/offline/motion_extractor_tests.cc new file mode 100644 index 000000000..841dcfd6c --- /dev/null +++ b/test/animation/offline/motion_extractor_tests.cc @@ -0,0 +1,100 @@ +//----------------------------------------------------------------------------// +// // +// 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 "gtest/gtest.h" +#include "ozz/animation/offline/motion_extractor.h" +#include "ozz/animation/offline/raw_animation.h" +#include "ozz/animation/offline/raw_skeleton.h" +#include "ozz/animation/offline/raw_track.h" +#include "ozz/animation/offline/skeleton_builder.h" +#include "ozz/animation/runtime/skeleton.h" +#include "ozz/base/maths/math_constant.h" +#include "ozz/base/memory/unique_ptr.h" + +using ozz::animation::Skeleton; +using ozz::animation::offline::MotionExtractor; +using ozz::animation::offline::RawAnimation; +using ozz::animation::offline::RawFloat3Track; +using ozz::animation::offline::RawQuaternionTrack; +using ozz::animation::offline::RawSkeleton; +using ozz::animation::offline::SkeletonBuilder; + +TEST(Error, MotionExtractor) { + MotionExtractor extractor; + + { // Same input and output. + RawAnimation input; + Skeleton skeleton; + EXPECT_TRUE(input.Validate()); + + RawFloat3Track motion_position; + RawQuaternionTrack motion_rotation; + + // Builds animation + EXPECT_FALSE( + extractor(input, skeleton, &motion_position, &motion_rotation, &input)); + } + + { // Valid + RawAnimation input; + Skeleton skeleton; + EXPECT_TRUE(input.Validate()); + + RawFloat3Track motion_position; + RawQuaternionTrack motion_rotation; + RawAnimation output; + + // Builds animation + EXPECT_TRUE(extractor(input, skeleton, &motion_position, &motion_rotation, + &output)); + EXPECT_TRUE(extractor(input, skeleton, nullptr, &motion_rotation, &output)); + EXPECT_TRUE(extractor(input, skeleton, &motion_position, nullptr, &output)); + EXPECT_TRUE(extractor(input, skeleton, nullptr, &motion_rotation, nullptr)); + EXPECT_TRUE(extractor(input, skeleton, &motion_position, nullptr, nullptr)); + EXPECT_TRUE(extractor(input, skeleton, nullptr, nullptr, nullptr)); + } +} + +TEST(Name, MotionExtractor) { + MotionExtractor extractor; + + RawAnimation input; + Skeleton skeleton; + input.name = "Test_Animation"; + input.duration = 1.f; + + ASSERT_TRUE(input.Validate()); + + RawFloat3Track motion_position; + RawQuaternionTrack motion_rotation; + RawAnimation output; + EXPECT_TRUE( + extractor(input, skeleton, &motion_position, &motion_rotation, &output)); + EXPECT_STRCASEEQ(output.name.c_str(), "Test_Animation"); +} + +TEST(Extract, MotionExtractor) {} diff --git a/test/animation/offline/raw_track_utils_tests.cc b/test/animation/offline/raw_track_utils_tests.cc new file mode 100644 index 000000000..872930a6e --- /dev/null +++ b/test/animation/offline/raw_track_utils_tests.cc @@ -0,0 +1,170 @@ +//----------------------------------------------------------------------------// +// // +// 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 "gtest/gtest.h" +#include "ozz/animation/offline/raw_track.h" +#include "ozz/animation/offline/raw_track_utils.h" +#include "ozz/base/maths/gtest_math_helper.h" + +using ozz::animation::offline::RawFloat2Track; +using ozz::animation::offline::RawFloat3Track; +using ozz::animation::offline::RawFloat4Track; +using ozz::animation::offline::RawFloatTrack; +using ozz::animation::offline::RawQuaternionTrack; +using ozz::animation::offline::RawTrackInterpolation; + +TEST(Invalid, RawTrackUtils) { + { + RawFloatTrack raw_track; + raw_track.keyframes.resize(1); + raw_track.keyframes[0].ratio = 99.f; + EXPECT_FALSE(raw_track.Validate()); + float out; + EXPECT_FALSE(ozz::animation::offline::SampleTrack(raw_track, 0.f, &out)); + } +} + +TEST(SampleFloat, RawTrackUtils) { + { // Empty + RawFloatTrack raw_track; + EXPECT_TRUE(raw_track.Validate()); + float out; + ASSERT_TRUE(ozz::animation::offline::SampleTrack(raw_track, 0.f, &out)); + EXPECT_FLOAT_EQ(out, 0.f); + } + + { // Single key + RawFloatTrack raw_track; + raw_track.keyframes.push_back({RawTrackInterpolation::kLinear, .46f, 46.f}); + EXPECT_TRUE(raw_track.Validate()); + + float out; + ASSERT_TRUE(ozz::animation::offline::SampleTrack(raw_track, 0.f, &out)); + EXPECT_FLOAT_EQ(out, 46.f); + + ASSERT_TRUE(ozz::animation::offline::SampleTrack(raw_track, 1.f, &out)); + EXPECT_FLOAT_EQ(out, 46.f); + } + + { // Mixed interpolation + RawFloatTrack raw_track; + raw_track.keyframes.push_back({RawTrackInterpolation::kLinear, 0.f, 1.f}); + raw_track.keyframes.push_back({RawTrackInterpolation::kStep, .2f, 2.f}); + raw_track.keyframes.push_back({RawTrackInterpolation::kLinear, .5f, 3.f}); + raw_track.keyframes.push_back({RawTrackInterpolation::kLinear, .75f, 3.f}); + + EXPECT_TRUE(raw_track.Validate()); + float out; + + ASSERT_TRUE(ozz::animation::offline::SampleTrack(raw_track, 0.f, &out)); + EXPECT_FLOAT_EQ(out, 1.f); + + ASSERT_TRUE(ozz::animation::offline::SampleTrack(raw_track, 1.f, &out)); + EXPECT_FLOAT_EQ(out, 3.f); + + ASSERT_TRUE(ozz::animation::offline::SampleTrack(raw_track, .1f, &out)); + EXPECT_FLOAT_EQ(out, 1.5f); + + ASSERT_TRUE(ozz::animation::offline::SampleTrack(raw_track, .2f, &out)); + EXPECT_FLOAT_EQ(out, 2.f); + + ASSERT_TRUE(ozz::animation::offline::SampleTrack(raw_track, .25f, &out)); + EXPECT_FLOAT_EQ(out, 2.f); // Step interpolation + + ASSERT_TRUE(ozz::animation::offline::SampleTrack(raw_track, .5f, &out)); + EXPECT_FLOAT_EQ(out, 3.f); + } +} + +TEST(SampleFloat2, RawTrackUtils) { + { // Empty + RawFloat2Track raw_track; + EXPECT_TRUE(raw_track.Validate()); + ozz::math::Float2 out; + ASSERT_TRUE(ozz::animation::offline::SampleTrack(raw_track, 0.f, &out)); + EXPECT_FLOAT2_EQ(out, 0.f, 0.f); + } +} + +TEST(SampleFloat3, RawTrackUtils) { + { // Empty + RawFloat3Track raw_track; + EXPECT_TRUE(raw_track.Validate()); + ozz::math::Float3 out; + ASSERT_TRUE(ozz::animation::offline::SampleTrack(raw_track, 0.f, &out)); + EXPECT_FLOAT3_EQ(out, 0.f, 0.f, 0.f); + } +} + +TEST(SampleFloat4, RawTrackUtils) { + { // Empty + RawFloat4Track raw_track; + EXPECT_TRUE(raw_track.Validate()); + ozz::math::Float4 out; + ASSERT_TRUE(ozz::animation::offline::SampleTrack(raw_track, 0.f, &out)); + EXPECT_FLOAT4_EQ(out, 0.f, 0.f, 0.f, 0.f); + } +} + +TEST(SampleQauternion, RawTrackUtils) { + { // Empty + RawQuaternionTrack raw_track; + EXPECT_TRUE(raw_track.Validate()); + + ozz::math::Quaternion out; + ASSERT_TRUE(ozz::animation::offline::SampleTrack(raw_track, 0.f, &out)); + EXPECT_QUATERNION_EQ(out, 0.f, 0.f, 0.f, 1.f); + } + + { // NLerp + RawQuaternionTrack raw_track; + raw_track.keyframes.push_back( + {RawTrackInterpolation::kLinear, 0.f, + ozz::math::Quaternion(.70710677f, 0.f, 0.f, .70710677f)}); + raw_track.keyframes.push_back( + {RawTrackInterpolation::kLinear, 1.f, + ozz::math::Quaternion(0.f, .70710677f, 0.f, .70710677f)}); + + ozz::math::Quaternion out; + ASSERT_TRUE(ozz::animation::offline::SampleTrack(raw_track, .2f, &out)); + EXPECT_QUATERNION_EQ(out, .6172133f, .1543033f, 0.f, .7715167f); + } + + { // NLerp opposite + RawQuaternionTrack raw_track; + raw_track.keyframes.push_back( + {RawTrackInterpolation::kLinear, 0.f, + ozz::math::Quaternion(.70710677f, 0.f, 0.f, .70710677f)}); + raw_track.keyframes.push_back( + {RawTrackInterpolation::kLinear, 1.f, + ozz::math::Quaternion(0.f, -.70710677f, 0.f, -.70710677f)}); + + ozz::math::Quaternion out; + ASSERT_TRUE(ozz::animation::offline::SampleTrack(raw_track, .2f, &out)); + EXPECT_QUATERNION_EQ(out, .6172133f, .1543033f, 0.f, .7715167f); + } +} \ No newline at end of file