diff --git a/include/ozz/animation/offline/motion_extractor.h b/include/ozz/animation/offline/motion_extractor.h index 263917dcb..a28a7788d 100644 --- a/include/ozz/animation/offline/motion_extractor.h +++ b/include/ozz/animation/offline/motion_extractor.h @@ -64,23 +64,32 @@ class OZZ_ANIMOFFLINE_DLL MotionExtractor { // Defines the reference transform to use while extracting root motion. enum class Reference { - kIdentity, // Identity / global reference - kSkeleton, // Use skeleton rest pose root bone transform - kAnimation, // Uses root transform of the animation's first frame + kIdentity, // Identity / global reference. + kSkeleton, // Use skeleton rest pose root bone transform. + kAnimation, // Uses root transform of the animation's first frame. }; struct Settings { - bool x, y, z; // Extract X, Y, Z components - Reference reference; // Extracting reference - bool bake; // Bake extracted data to output animation + bool x, y, z; // Extract X, Y, Z components. + Reference reference; // Extracting reference. + bool bake; // Bake extracted data to output animation. + bool loop; // Makes end transformation equal to begin to make animation + // loopable. Difference between end and begin is distributed all + // along animation duration. }; - Settings position_settings = {true, false, true, // X and Z projection + Settings position_settings = {true, + false, + true, // X and Z projection Reference::kSkeleton, // Reference - true}; // Bake extracted position - Settings rotation_settings = {false, true, false, // Y / Yaw only + true, // Bake extracted position + false}; // Don't loop position + Settings rotation_settings = {false, + true, + false, // Y / Yaw only Reference::kSkeleton, // Reference - true}; // Bake extracted rotation + true, // Bake extracted rotation + false}; // Don't loop rotation }; } // namespace offline } // namespace animation diff --git a/media/bin/pab_atlas_motion_track.ozz b/media/bin/pab_atlas_motion_track.ozz index 72b6b0254..34ad3adb8 100644 Binary files a/media/bin/pab_atlas_motion_track.ozz and b/media/bin/pab_atlas_motion_track.ozz differ diff --git a/media/bin/pab_atlas_no_motion.ozz b/media/bin/pab_atlas_no_motion.ozz index 3513069dd..cc9a20e2b 100644 Binary files a/media/bin/pab_atlas_no_motion.ozz and b/media/bin/pab_atlas_no_motion.ozz differ diff --git a/media/bin/pab_jog_motion_track.ozz b/media/bin/pab_jog_motion_track.ozz index ba426a85e..dc3cdabab 100644 Binary files a/media/bin/pab_jog_motion_track.ozz and b/media/bin/pab_jog_motion_track.ozz differ diff --git a/media/bin/pab_jog_no_motion.ozz b/media/bin/pab_jog_no_motion.ozz index a256451f2..5808b5abd 100644 Binary files a/media/bin/pab_jog_no_motion.ozz and b/media/bin/pab_jog_no_motion.ozz differ diff --git a/media/bin/pab_run_motion_track.ozz b/media/bin/pab_run_motion_track.ozz index 2437f4976..59f98e026 100644 Binary files a/media/bin/pab_run_motion_track.ozz and b/media/bin/pab_run_motion_track.ozz differ diff --git a/media/bin/pab_run_no_motion.ozz b/media/bin/pab_run_no_motion.ozz index 9c9014ac7..08e61b193 100644 Binary files a/media/bin/pab_run_no_motion.ozz and b/media/bin/pab_run_no_motion.ozz differ diff --git a/media/bin/pab_walk_motion_track.ozz b/media/bin/pab_walk_motion_track.ozz index 1e57fb879..ecf2a42c2 100644 Binary files a/media/bin/pab_walk_motion_track.ozz and b/media/bin/pab_walk_motion_track.ozz differ diff --git a/media/bin/pab_walk_no_motion.ozz b/media/bin/pab_walk_no_motion.ozz index 834a265e7..0aea61b50 100644 Binary files a/media/bin/pab_walk_no_motion.ozz and b/media/bin/pab_walk_no_motion.ozz differ diff --git a/samples/framework/motion_utils.cc b/samples/framework/motion_utils.cc index 3af55a785..6bd1cd88e 100644 --- a/samples/framework/motion_utils.cc +++ b/samples/framework/motion_utils.cc @@ -216,7 +216,7 @@ bool DrawMotion(ozz::sample::Renderer* _renderer, ozz::vector points; auto sample = [&sampler, &_motion_track, &points]( float _t, float _prev, const ozz::math::Quaternion& _rot) { - int loop = std::floor(_t) - std::floor(_prev); + int loop = static_cast(std::floor(_t) - std::floor(_prev)); bool success = sampler.Update(_motion_track, _t - std::floor(_t), loop, _rot); points.push_back(sampler.current.translation); diff --git a/samples/motion_extraction/sample_motion_extraction.cc b/samples/motion_extraction/sample_motion_extraction.cc index cc826875a..7cdc0b508 100644 --- a/samples/motion_extraction/sample_motion_extraction.cc +++ b/samples/motion_extraction/sample_motion_extraction.cc @@ -147,9 +147,10 @@ class MotionSampleApplication : public ozz::sample::Application { // Draw motion tracks. const float at = controller_.time_ratio(); - success &= ozz::sample::DrawMotion(_renderer, motion_track_, 0.f, at, 1.f, - animation_.duration(), transform_, - ozz::math::Quaternion::identity()); + const float step = 1.f / (animation_.duration() * 60.f); + success &= + ozz::sample::DrawMotion(_renderer, motion_track_, 0.f, at, 1.f, step, + transform_, ozz::math::Quaternion::identity()); return success; } @@ -268,51 +269,64 @@ class MotionSampleApplication : public ozz::sample::Application { static bool open = true; ozz::sample::ImGui::OpenClose oc(_im_gui, "Motion extraction", &open); { - ozz::sample::ImGui::OpenClose ocp(_im_gui, "Position", nullptr); - _im_gui->DoLabel("Components"); - rebuild |= - _im_gui->DoCheckBox("x", &motion_extractor_.position_settings.x); - rebuild |= - _im_gui->DoCheckBox("y", &motion_extractor_.position_settings.y); - rebuild |= - _im_gui->DoCheckBox("z", &motion_extractor_.position_settings.z); - - _im_gui->DoLabel("Reference"); - int ref = - static_cast(motion_extractor_.position_settings.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_.position_settings.reference = - static_cast( - ref); + static bool position = true; + ozz::sample::ImGui::OpenClose ocp(_im_gui, "Position", &position); + { + ozz::sample::ImGui::OpenClose occ(_im_gui, "Components", nullptr); + rebuild |= + _im_gui->DoCheckBox("x", &motion_extractor_.position_settings.x); + rebuild |= + _im_gui->DoCheckBox("y", &motion_extractor_.position_settings.y); + rebuild |= + _im_gui->DoCheckBox("z", &motion_extractor_.position_settings.z); + } + + { + ozz::sample::ImGui::OpenClose ocr(_im_gui, "Reference", nullptr); + int ref = + static_cast(motion_extractor_.position_settings.reference); + rebuild |= _im_gui->DoRadioButton(0, "Identity", &ref); + rebuild |= _im_gui->DoRadioButton(1, "Skeleton", &ref); + rebuild |= _im_gui->DoRadioButton(2, "Animation", &ref); + motion_extractor_.position_settings.reference = + static_cast( + ref); + } + rebuild |= _im_gui->DoCheckBox( + "Bake", &motion_extractor_.position_settings.bake); + rebuild |= _im_gui->DoCheckBox( + "Loop", &motion_extractor_.position_settings.loop); } { - ozz::sample::ImGui::OpenClose ocp(_im_gui, "Rotation", nullptr); - _im_gui->DoLabel("Components"); - rebuild |= _im_gui->DoCheckBox("x / pitch", - &motion_extractor_.rotation_settings.x); - rebuild |= _im_gui->DoCheckBox("y / yaw", - &motion_extractor_.rotation_settings.y); - rebuild |= _im_gui->DoCheckBox("z / roll", - &motion_extractor_.rotation_settings.z); - - _im_gui->DoLabel("Reference"); - int ref = - static_cast(motion_extractor_.rotation_settings.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_.rotation_settings.reference = - static_cast( - ref); - } + static bool rotation = true; + ozz::sample::ImGui::OpenClose ocp(_im_gui, "Rotation", &rotation); + { + ozz::sample::ImGui::OpenClose ocp(_im_gui, "Components", nullptr); + rebuild |= _im_gui->DoCheckBox( + "x / pitch", &motion_extractor_.rotation_settings.x); + rebuild |= _im_gui->DoCheckBox( + "y / yaw", &motion_extractor_.rotation_settings.y); + rebuild |= _im_gui->DoCheckBox( + "z / roll", &motion_extractor_.rotation_settings.z); + } - rebuild |= _im_gui->DoCheckBox("Bake position", - &motion_extractor_.position_settings.bake); - rebuild |= _im_gui->DoCheckBox("Bake rotation", - &motion_extractor_.rotation_settings.bake); + { + ozz::sample::ImGui::OpenClose ocp(_im_gui, "Reference", nullptr); + int ref = + static_cast(motion_extractor_.rotation_settings.reference); + rebuild |= _im_gui->DoRadioButton(0, "Identity", &ref); + rebuild |= _im_gui->DoRadioButton(1, "Skeleton", &ref); + rebuild |= _im_gui->DoRadioButton(2, "Animation", &ref); + motion_extractor_.rotation_settings.reference = + static_cast( + ref); + } + rebuild |= _im_gui->DoCheckBox( + "Bake", &motion_extractor_.rotation_settings.bake); + rebuild |= _im_gui->DoCheckBox( + "Loop", &motion_extractor_.rotation_settings.loop); + } if (rebuild) { if (!ExtractMotion()) { diff --git a/samples/motion_playback/CMakeLists.txt b/samples/motion_playback/CMakeLists.txt index 225a7fc02..236776fc3 100644 --- a/samples/motion_playback/CMakeLists.txt +++ b/samples/motion_playback/CMakeLists.txt @@ -3,8 +3,8 @@ 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_no_motion.ozz" - "${ozz_media_directory}/bin/pab_atlas_motion_track.ozz" + "${ozz_media_directory}/bin/pab_jog_no_motion.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/animation.ozz" @@ -12,8 +12,8 @@ add_custom_command( 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_no_motion.ozz" "./media/animation.ozz" - COMMAND ${CMAKE_COMMAND} -E copy "${ozz_media_directory}/bin/pab_atlas_motion_track.ozz" "./media/motion.ozz" + COMMAND ${CMAKE_COMMAND} -E copy "${ozz_media_directory}/bin/pab_jog_no_motion.ozz" "./media/animation.ozz" + COMMAND ${CMAKE_COMMAND} -E copy "${ozz_media_directory}/bin/pab_jog_motion_track.ozz" "./media/motion.ozz" VERBATIM) add_executable(sample_motion_playback diff --git a/samples/motion_playback/sample_motion_playback.cc b/samples/motion_playback/sample_motion_playback.cc index 9302ef6cc..79e255401 100644 --- a/samples/motion_playback/sample_motion_playback.cc +++ b/samples/motion_playback/sample_motion_playback.cc @@ -88,7 +88,7 @@ class MotionPlaybackSampleApplication : public ozz::sample::Application { if (controller_.playing()) { trace_.push_back(transform.translation); - if (trace_.size() > trace_size_) { + if (static_cast(trace_.size()) > trace_size_) { trace_.erase(trace_.begin(), trace_.end() - trace_size_); } } diff --git a/src/animation/offline/fbx/CMakeLists.txt b/src/animation/offline/fbx/CMakeLists.txt index e87dba170..629cb0140 100644 --- a/src/animation/offline/fbx/CMakeLists.txt +++ b/src/animation/offline/fbx/CMakeLists.txt @@ -48,8 +48,8 @@ set(build_configurations "/fbx/pab/hand.fbx\;{\"skeleton\":{\"filename\":\"pab_skeleton.ozz\",\"import\":{\"enable\":false}},\"animations\":[{\"clip\":\"curl\",\"filename\":\"pab_curl_additive.ozz\",\"additive\":true,\"additive_reference\":\"skeleton\"},{\"clip\":\"splay\",\"filename\":\"pab_splay_additive.ozz\",\"additive\":true,\"additive_reference\":\"skeleton\"}]}\;output:pab_curl_additive.ozz\;output:pab_splay_additive.ozz\;depend:pab_skeleton.ozz" # Library data with root motion - "/fbx/pab/atlas.fbx\;{\"skeleton\":{\"filename\":\"pab_skeleton.ozz\",\"import\":{\"enable\":false}},\"animations\":[{\"filename\":\"pab_atlas_no_motion.ozz\",\"tracks\":{\"motion\":{\"enable\":true,\"filename\":\"pab_atlas_motion_track.ozz\"}}}]}\;output:pab_atlas_no_motion.ozz\;output:pab_atlas_motion_track.ozz\;depend:pab_skeleton.ozz" - "/fbx/pab/locomotions.fbx\;{\"skeleton\":{\"filename\":\"pab_skeleton.ozz\",\"import\":{\"enable\":false}},\"animations\":[{\"filename\":\"pab_*_no_motion.ozz\",\"tracks\":{\"motion\":{\"enable\":true,\"filename\":\"pab_*_motion_track.ozz\"}}}]}\;output:pab_walk_no_motion.ozz\;output:pab_jog_no_motion.ozz\;output:pab_run_no_motion.ozz\;output:pab_walk_motion_track.ozz\;output:pab_jog_motion_track.ozz\;output:pab_run_motion_track.ozz\;depend:pab_skeleton.ozz" + "/fbx/pab/atlas.fbx\;{\"skeleton\":{\"filename\":\"pab_skeleton.ozz\",\"import\":{\"enable\":false}},\"animations\":[{\"filename\":\"pab_atlas_no_motion.ozz\",\"tracks\":{\"motion\":{\"enable\":true,\"rotation\":{\"loop\":true},\"filename\":\"pab_atlas_motion_track.ozz\"}}}]}\;output:pab_atlas_no_motion.ozz\;output:pab_atlas_motion_track.ozz\;depend:pab_skeleton.ozz" + "/fbx/pab/locomotions.fbx\;{\"skeleton\":{\"filename\":\"pab_skeleton.ozz\",\"import\":{\"enable\":false}},\"animations\":[{\"filename\":\"pab_*_no_motion.ozz\",\"tracks\":{\"motion\":{\"enable\":true,\"rotation\":{\"loop\":true},\"filename\":\"pab_*_motion_track.ozz\"}}}]}\;output:pab_walk_no_motion.ozz\;output:pab_jog_no_motion.ozz\;output:pab_run_no_motion.ozz\;output:pab_walk_motion_track.ozz\;output:pab_jog_motion_track.ozz\;output:pab_run_motion_track.ozz\;depend:pab_skeleton.ozz" # Robot user channels "/fbx/robot.fbx\;\;config_file:${PROJECT_SOURCE_DIR}/samples/user_channel/config.json\;output:robot_skeleton.ozz\;output:robot_animation.ozz\;output:robot_track_grasp.ozz" diff --git a/src/animation/offline/motion_extractor.cc b/src/animation/offline/motion_extractor.cc index f1eab4e33..525620e1b 100644 --- a/src/animation/offline/motion_extractor.cc +++ b/src/animation/offline/motion_extractor.cc @@ -116,37 +116,51 @@ bool MotionExtractor::operator()(const RawAnimation& _input, const auto& input_track = _input.tracks[root_joint]; auto& output_track = _output->tracks[root_joint]; - // Baking reference + // Compute extraction reference auto ref = BuildReference(position_settings.reference, rotation_settings.reference, GetJointLocalRestPose(_skeleton, root_joint), input_track); - // Copies root position - _motion_position->keyframes.clear(); - for (const auto& joint_key : input_track.translations) { - // Takes expected components only. - const math::Float3 mask{1.f * position_settings.x, - 1.f * position_settings.y, - 1.f * position_settings.z}; - const math::Float3 motion_p = (joint_key.value - ref.translation) * mask; - _motion_position->keyframes.push_back( - {ozz::animation::offline::RawTrackInterpolation::kLinear, - joint_key.time / _input.duration, motion_p}); - } - - // Copies root rotation - _motion_rotation->keyframes.clear(); - for (const auto& joint_key : input_track.rotations) { - // Decompose rotation to take expected components only. - const math::Float3 mask{1.f * rotation_settings.y, // Yaw - 1.f * rotation_settings.x, // Pitch - 1.f * rotation_settings.z}; // Roll - const auto euler = ToEuler(joint_key.value * Conjugate(ref.rotation)); - const auto motion_q = math::Quaternion::FromEuler(euler * mask); - _motion_rotation->keyframes.push_back( - {ozz::animation::offline::RawTrackInterpolation::kLinear, - joint_key.time / _input.duration, motion_q}); - } + // Extract root motion + // ----------------------------------------------------------------------------- + + // Copy function, used to copy aniamtion keyframes to motion keyframes. + auto extract = [duration = _input.duration](const auto& _keframes, + auto _extract, auto& output) { + output.clear(); + for (const auto& joint_key : _keframes) { + const auto& motion = _extract(joint_key.value); + output.push_back({ozz::animation::offline::RawTrackInterpolation::kLinear, + joint_key.time / duration, motion}); + } + }; + + // Copies root position, selecting only expecting components. + const math::Float3 position_mask{1.f * position_settings.x, + 1.f * position_settings.y, + 1.f * position_settings.z}; + extract( + input_track.translations, + [&mask = position_mask, &ref = ref.translation](const auto& _joint) { + return (_joint - ref) * mask; + }, + _motion_position->keyframes); + + // Copies root rotation, selecting only expecting decomposed rotation + // components. + const math::Float3 rotation_mask{1.f * rotation_settings.y, // Yaw + 1.f * rotation_settings.x, // Pitch + 1.f * rotation_settings.z}; // Roll + extract( + input_track.rotations, + [&mask = rotation_mask, &ref = ref.rotation](const auto& _joint) { + const auto euler = ToEuler(_joint * Conjugate(ref)); + return math::Quaternion::FromEuler(euler * mask); + }, + _motion_rotation->keyframes); + + // Bake + // ----------------------------------------------------------------------------- // Extract root motion rotation from the animation, aka bake it. if (rotation_settings.bake) { @@ -169,7 +183,44 @@ bool MotionExtractor::operator()(const RawAnimation& _input, } } + // Loopify + // ----------------------------------------------------------------------------- + // Distributes the difference between the first and last keyframes all along + // animation duration, so tha animation can loop. + auto loopify = [](auto& _keyframes, auto _diff, auto _lerp) { + if (_keyframes.size() < 2) { + return; + } + const auto delta = _diff(_keyframes.front().value, _keyframes.back().value); + for (size_t i = 0; i < _keyframes.size(); i++) { + const float alpha = i / (_keyframes.size() - 1.f); + auto& value = _keyframes[i].value; + value = _lerp(value, delta, alpha); + } + }; + + // Loopify translations + if (rotation_settings.loop) { + loopify( + _motion_rotation->keyframes, + [](auto _front, auto _back) { return _front * Conjugate(_back); }, + [](auto _value, auto _delta, float _alpha) { + return NLerp(math::Quaternion::identity(), _delta, _alpha) * _value; + }); + } + + // Loopify rotations + if (position_settings.loop) { + loopify( + _motion_position->keyframes, + [](auto _front, auto _back) { return _front - _back; }, + [](auto _value, auto _delta, float _alpha) { + return _delta * _alpha + _value; + }); + } + // Fixup animation translations. + // ----------------------------------------------------------------------------- // When root motion is applied, then root rotation is applied before joint // translation. Hence joint's translation should be corrected to support this // new composition order. diff --git a/src/animation/offline/tools/import2ozz.cc b/src/animation/offline/tools/import2ozz.cc index f11899663..0de72232b 100644 --- a/src/animation/offline/tools/import2ozz.cc +++ b/src/animation/offline/tools/import2ozz.cc @@ -161,7 +161,7 @@ int OzzImporter::operator()(int _argc, const char** _argv) { << std::endl; return EXIT_FAILURE; } - ozz::log::Log() << "Existing importer successfully." << std::endl; + ozz::log::Log() << "Exiting importer successfully." << std::endl; return EXIT_SUCCESS; } diff --git a/src/animation/offline/tools/import2ozz_config.cc b/src/animation/offline/tools/import2ozz_config.cc index b35030b34..c7f21dacf 100644 --- a/src/animation/offline/tools/import2ozz_config.cc +++ b/src/animation/offline/tools/import2ozz_config.cc @@ -341,6 +341,8 @@ bool SanitizeTrackMotionComponent(Json::Value& _root, } MakeDefault(_root, "bake", true, "Bake extracted motion into animation."); + MakeDefault(_root, "loop", false, + "Distributes begin - end difference to make animation loopable."); MakeDefault(_root, "raw", false, "Outputs raw track."); MakeDefault(_root, "optimize", true, "Activates keyframes optimization."); diff --git a/src/animation/offline/tools/import2ozz_track.cc b/src/animation/offline/tools/import2ozz_track.cc index 245b98953..d1ab7ea2b 100644 --- a/src/animation/offline/tools/import2ozz_track.cc +++ b/src/animation/offline/tools/import2ozz_track.cc @@ -351,6 +351,7 @@ ozz::animation::offline::MotionExtractor::Settings ProcessMotionTrackSettings( "Reference should have been checked during config validation"); settings.bake = _config["bake"].asBool(); + settings.loop = _config["loop"].asBool(); return settings; } diff --git a/src/animation/offline/tools/reference.json b/src/animation/offline/tools/reference.json index 51bd82483..88e5ce953 100644 --- a/src/animation/offline/tools/reference.json +++ b/src/animation/offline/tools/reference.json @@ -75,6 +75,7 @@ "components" : "xz", // Components to import, can be any composition of x, y and z. "reference" : "skeleton", // Root motion extraction reference pose, can be identity, skeleton or animation. "bake" : true, // Bake extracted motion into animation. + "loop" : false, // Distributes begin - end difference to make animation loopable. "raw" : false, // Outputs raw track. "optimize" : true, // Activates keyframes optimization. "optimization_tolerance" : 0.001 // Optimization tolerance for the optimized track @@ -85,6 +86,7 @@ "components" : "y", // Components to import, can be any composition of x, y and z. "reference" : "skeleton", // Root motion extraction reference pose, can be identity, skeleton or animation. "bake" : true, // Bake extracted motion into animation. + "loop" : false, // Distributes begin - end difference to make animation loopable. "raw" : false, // Outputs raw track. "optimize" : true, // Activates keyframes optimization. "optimization_tolerance" : 0.001 // Optimization tolerance for the optimized track