Skip to content

Commit

Permalink
Expose motion looping correction setting.
Browse files Browse the repository at this point in the history
  • Loading branch information
guillaumeblanc committed Apr 16, 2024
1 parent 7a50ee5 commit 2b41aa7
Show file tree
Hide file tree
Showing 17 changed files with 160 additions and 82 deletions.
16 changes: 12 additions & 4 deletions include/ozz/animation/offline/motion_extractor.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,22 @@ class OZZ_ANIMOFFLINE_DLL MotionExtractor {
bool x, y, z; // Extract X, Y, Z components
Reference reference; // Extracting reference
bool bake; // Bake extracted data to output animation
bool loop; // Distributes begin - end difference to make animation
// loopable.
};

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
true}; // Loop rosition
};
} // namespace offline
} // namespace animation
Expand Down
Binary file modified media/bin/pab_atlas_motion_track.ozz
Binary file not shown.
Binary file modified media/bin/pab_atlas_no_motion.ozz
Binary file not shown.
Binary file modified media/bin/pab_jog_motion_track.ozz
Binary file not shown.
Binary file modified media/bin/pab_jog_no_motion.ozz
Binary file not shown.
Binary file modified media/bin/pab_run_motion_track.ozz
Binary file not shown.
Binary file modified media/bin/pab_run_no_motion.ozz
Binary file not shown.
Binary file modified media/bin/pab_walk_motion_track.ozz
Binary file not shown.
Binary file modified media/bin/pab_walk_no_motion.ozz
Binary file not shown.
102 changes: 58 additions & 44 deletions samples/motion_extraction/sample_motion_extraction.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<int>(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<ozz::animation::offline::MotionExtractor::Reference>(
ref);
static bool position = true;
ozz::sample::ImGui::OpenClose ocp(_im_gui, "Position", &position);
{
ozz::sample::ImGui::OpenClose ocp(_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 ocp(_im_gui, "Reference", nullptr);
int ref =
static_cast<int>(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<ozz::animation::offline::MotionExtractor::Reference>(
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<int>(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<ozz::animation::offline::MotionExtractor::Reference>(
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<int>(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<ozz::animation::offline::MotionExtractor::Reference>(
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()) {
Expand Down
8 changes: 4 additions & 4 deletions samples/motion_playback/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ add_custom_command(
DEPENDS $<$<BOOL:${ozz_build_fbx}>: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"
"${CMAKE_CURRENT_BINARY_DIR}/media/motion.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_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
Expand Down
4 changes: 2 additions & 2 deletions src/animation/offline/fbx/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
105 changes: 78 additions & 27 deletions src/animation/offline/motion_extractor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/animation/offline/tools/import2ozz.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
2 changes: 2 additions & 0 deletions src/animation/offline/tools/import2ozz_config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down
1 change: 1 addition & 0 deletions src/animation/offline/tools/import2ozz_track.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 2 additions & 0 deletions src/animation/offline/tools/reference.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 2b41aa7

Please sign in to comment.