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 17, 2024
1 parent 7a50ee5 commit 9250205
Show file tree
Hide file tree
Showing 19 changed files with 169 additions and 90 deletions.
29 changes: 19 additions & 10 deletions include/ozz/animation/offline/motion_extractor.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
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.
2 changes: 1 addition & 1 deletion samples/framework/motion_utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ bool DrawMotion(ozz::sample::Renderer* _renderer,
ozz::vector<ozz::math::Float3> 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<int>(std::floor(_t) - std::floor(_prev));
bool success =
sampler.Update(_motion_track, _t - std::floor(_t), loop, _rot);
points.push_back(sampler.current.translation);
Expand Down
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 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<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, "Animation", &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, "Animation", &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
2 changes: 1 addition & 1 deletion samples/motion_playback/sample_motion_playback.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>(trace_.size()) > trace_size_) {
trace_.erase(trace_.begin(), trace_.end() - trace_size_);
}
}
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 9250205

Please sign in to comment.