Skip to content

Commit

Permalink
First root motion extraction tooling implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
guillaumeblanc committed Mar 31, 2024
1 parent bbab4fb commit 09dbad5
Show file tree
Hide file tree
Showing 18 changed files with 444 additions and 280 deletions.
13 changes: 13 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
Next release
----------------------

* Library
- [offline] Implements root motion extraction through`ozz::animation::offline::MotionExtraction` utility. Root motion defines how a character moves during an animation. The utility extracts the motion (position and rotation) from a root joint of the animation into separate tracks, and removes (bake) that motion from the original animation. User code is expected to reapply motion at runtime by moving the character transform, hence reconstructing the original animation.

* Tools
- Adds motion track extraction to \*2ozz. Configuration (json) is extended with a animations.tracks.motion object that exposes root motion extraction settings. See [src/animation/offline/tools/reference.json#L67]().
- Breaking \*2ozz json configuration change. \*2ozz json configuration animations.tracks is no longer an array, but an array. See [src/animation/offline/tools/reference.json#L51]().

* Samples
- Adds motion extraction sample, showcasing root motion extraction parameters.

Release version 0.15.0
----------------------

Expand Down
18 changes: 10 additions & 8 deletions include/ozz/animation/offline/motion_extractor.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class OZZ_ANIMOFFLINE_DLL MotionExtractor {
RawQuaternionTrack* _motion_rotation,
RawAnimation* _output) const;

// Index of the joint that will be used as root joint to extract root motion.
// Index of the joint that will be used as root to extract motion.
int root_joint = 0;

// Defines the reference transform to use while extracting root motion.
Expand All @@ -58,15 +58,17 @@ class OZZ_ANIMOFFLINE_DLL MotionExtractor {
};

struct Settings {
bool x, y, z;
Reference reference;
bool bake;
bool x, y, z; // Extract X, Y, Z components
Reference reference; // Extracting reference
bool bake; // Bake extracted data to output animation
};

Settings position_settings = {true, false, true, Reference::kSkeleton,
true}; // X and Z projection
Settings rotation_settings = {false, true, false, Reference::kSkeleton,
true}; // Y / Yaw only
Settings position_settings = {true, false, true, // X and Z projection
Reference::kFirstFrame, // Reference
true}; // Bake extracted position
Settings rotation_settings = {false, true, false, // Y / Yaw only
Reference::kFirstFrame, // Reference
true}; // Bake extracted rotation
};
} // namespace offline
} // namespace animation
Expand Down
Binary file added media/bin/pab_jog_motion_track.ozz
Binary file not shown.
Binary file added media/bin/pab_jog_no_motion.ozz
Binary file not shown.
Binary file added media/bin/pab_run_motion_track.ozz
Binary file not shown.
Binary file added media/bin/pab_run_no_motion.ozz
Binary file not shown.
Binary file added media/bin/pab_walk_motion_track.ozz
Binary file not shown.
Binary file added media/bin/pab_walk_no_motion.ozz
Binary file not shown.
24 changes: 11 additions & 13 deletions samples/user_channel/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,17 @@
{
"filename":"robot_animation.ozz",
"tracks":
[
{
"properties": // User-channel property track must be imported.
[
{
"type" : "float1", // Type of the property.
"joint_name":"thumb2", // Get the property from this node
"property_name":"grasp", // Name of the custom property
"filename":"robot_track_grasp.ozz" // Output filename
}
]
}
]
{
"properties": // User-channel property track must be imported.
[
{
"type" : "float1", // Type of the property.
"joint_name":"thumb2", // Get the property from this node
"property_name":"grasp", // Name of the custom property
"filename":"robot_track_grasp.ozz" // Output filename
}
]
}
}
]
}
3 changes: 3 additions & 0 deletions src/animation/offline/fbx/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ set(build_configurations
"/fbx/pab/crackhead.fbx\;{\"skeleton\":{\"filename\":\"pab_skeleton.ozz\",\"import\":{\"enable\":false}},\"animations\":[{\"filename\":\"pab_crackhead.ozz\"},{\"filename\":\"pab_crackhead_additive.ozz\",\"additive\":true}]}\;output:pab_crackhead_additive.ozz\;output:pab_crackhead.ozz\;depend:pab_skeleton.ozz"
"/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/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"

# 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
61 changes: 30 additions & 31 deletions src/animation/offline/motion_extractor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ bool MotionExtractor::operator()(const RawAnimation& _input,
return false;
}

// All outputs are expected to be valid.
if (!_output || !_motion_position || !_motion_rotation) {
return false;
}

// Animation must match skeleton.
if (_input.num_tracks() != _skeleton.num_joints()) {
return false;
Expand All @@ -105,9 +110,7 @@ bool MotionExtractor::operator()(const RawAnimation& _input,
}

// Copy output animation
if (_output) {
*_output = _input;
}
*_output = _input;

// Track to extract motion from
const auto& input_track = _input.tracks[root_joint];
Expand All @@ -119,34 +122,30 @@ bool MotionExtractor::operator()(const RawAnimation& _input,
GetJointLocalRestPose(_skeleton, root_joint), input_track);

// Copies root position
if (_motion_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});
}
_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
if (_motion_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});
}
_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 rotation from the animation, aka bake it.
Expand Down Expand Up @@ -193,9 +192,9 @@ bool MotionExtractor::operator()(const RawAnimation& _input,

// Validate outputs
bool success = true;
if (_motion_position) success &= _motion_position->Validate();
if (_motion_rotation) success &= _motion_rotation->Validate();
if (_output) success &= _output->Validate();
success &= _motion_position->Validate();
success &= _motion_rotation->Validate();
success &= _output->Validate();

return success;
}
Expand Down
87 changes: 46 additions & 41 deletions src/animation/offline/tools/import2ozz_anim.cc
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,13 @@ namespace {
void DisplaysOptimizationstatistics(const RawAnimation& _non_optimized,
const RawAnimation& _optimized) {
size_t opt_translations = 0, opt_rotations = 0, opt_scales = 0;
for (size_t i = 0; i < _optimized.tracks.size(); ++i) {
const RawAnimation::JointTrack& track = _optimized.tracks[i];
for (auto& track : _optimized.tracks) {
opt_translations += track.translations.size();
opt_rotations += track.rotations.size();
opt_scales += track.scales.size();
}
size_t non_opt_translations = 0, non_opt_rotations = 0, non_opt_scales = 0;
for (size_t i = 0; i < _non_optimized.tracks.size(); ++i) {
const RawAnimation::JointTrack& track = _non_optimized.tracks[i];
for (auto& track : _non_optimized.tracks) {
non_opt_translations += track.translations.size();
non_opt_rotations += track.rotations.size();
non_opt_scales += track.scales.size();
Expand Down Expand Up @@ -177,10 +175,7 @@ bool Export(OzzImporter& _importer, const RawAnimation& _input_animation,
optimizer.setting.distance = tolerances["distance"].asFloat();

// Builds per joint settings.
const Json::Value& joints_config = tolerances["override"];
for (Json::ArrayIndex i = 0; i < joints_config.size(); ++i) {
const Json::Value& joint_config = joints_config[i];

for (auto& joint_config : tolerances["override"]) {
// Prepares setting.
AnimationOptimizer::Setting setting;
setting.tolerance = joint_config["tolerance"].asFloat();
Expand Down Expand Up @@ -314,24 +309,22 @@ bool Export(OzzImporter& _importer, const RawAnimation& _input_animation,
return true;
} // namespace

bool ProcessAnimation(OzzImporter& _importer, const char* _animation_name,
bool ProcessAnimation(OzzImporter& _importer, const char* _clip_name,
const Skeleton& _skeleton, const Json::Value& _config,
const ozz::Endianness _endianness) {
RawAnimation animation;

ozz::log::Log() << "Extracting animation \"" << _animation_name << "\""
RawAnimation* _animation) {
ozz::log::Log() << "Extracting animation \"" << _clip_name << "\""
<< std::endl;

if (!_importer.Import(_animation_name, _skeleton,
_config["sampling_rate"].asFloat(), &animation)) {
ozz::log::Err() << "Failed to import animation \"" << _animation_name
<< "\"" << std::endl;
if (!_importer.Import(_clip_name, _skeleton,
_config["sampling_rate"].asFloat(), _animation)) {
ozz::log::Err() << "Failed to import animation \"" << _clip_name << "\""
<< std::endl;
return false;
}

// Give animation a name
animation.name = _animation_name;
return Export(_importer, animation, _skeleton, _config, _endianness);
_animation->name = _clip_name;
return true;
}
} // namespace

Expand All @@ -354,11 +347,11 @@ bool ImportAnimations(const Json::Value& _config, OzzImporter* _importer,
}

// Get all available animation names.
const OzzImporter::AnimationNames& import_animation_names =
const OzzImporter::AnimationNames& import_clip_names =
_importer->GetAnimationNames();

// Are there animations available
if (import_animation_names.empty()) {
if (import_clip_names.empty()) {
ozz::log::Err() << "No animation found." << std::endl;
return true;
}
Expand All @@ -377,8 +370,7 @@ bool ImportAnimations(const Json::Value& _config, OzzImporter* _importer,

// Loop though all existing animations, and export those who match
// configuration.
for (Json::ArrayIndex i = 0; i < animations_config.size(); ++i) {
const Json::Value& animation_config = animations_config[i];
for (auto& animation_config : animations_config) {
const char* clip_match = animation_config["clip"].asCString();

if (*clip_match == 0) {
Expand All @@ -389,30 +381,44 @@ bool ImportAnimations(const Json::Value& _config, OzzImporter* _importer,
}

size_t num_not_clip_animation = 0, num_valid_animation = 0;
for (size_t j = 0; j < import_animation_names.size(); ++j) {
const char* animation_name = import_animation_names[j].c_str();
if (!strmatch(animation_name, clip_match)) {
for (size_t j = 0; j < import_clip_names.size(); ++j) {
const char* clip_name = import_clip_names[j].c_str();
if (!strmatch(clip_name, clip_match)) {
continue;
}
++num_not_clip_animation;
if (ProcessAnimation(*_importer, animation_name, *skeleton,
animation_config, _endianness)) {

// Animation
RawAnimation animation;
if (ProcessAnimation(*_importer, clip_name, *skeleton, animation_config,
&animation)) {
++num_valid_animation;
}

size_t num_valid_track = 0;
// Tracks
const Json::Value& tracks_config = animation_config["tracks"];
for (Json::ArrayIndex t = 0; t < tracks_config.size(); ++t) {
if (ProcessTracks(*_importer, animation_name, *skeleton,
tracks_config[t], _endianness)) {
++num_valid_track;
}

// Properties
for (auto& property : tracks_config["properties"]) {
success &= ProcessImportTrack(*_importer, clip_name, *skeleton,
property, _endianness);
}

// Motion
if (success) {
RawAnimation baked_animation = animation;
const Json::Value& motion = tracks_config["motion"];
success &=
ProcessMotionTrack(*_importer, clip_name, animation, *skeleton,
motion, _endianness, &baked_animation);
animation = std::move(baked_animation);
}

if (num_valid_track != tracks_config.size()) {
ozz::log::Log() << "One of track failed when import: \""
<< animation_name << "\"" << std::endl;
success = false;
// Writes animation last, as it can have been modified by track
// processing.
if (success) {
success = Export(*_importer, animation, *skeleton, animation_config,
_endianness);
}
}
// Don't display any message if no animation is supposed to be imported.
Expand All @@ -422,9 +428,8 @@ bool ImportAnimations(const Json::Value& _config, OzzImporter* _importer,
}

if (num_valid_animation != num_not_clip_animation) {
ozz::log::Log()
<< "One of animation failed when import, animation index: \"" << i
<< "\"" << std::endl;
ozz::log::Log() << "Animation with clip name \"" << clip_match
<< "\" failed when import" << std::endl;
success = false;
}
}
Expand Down
Loading

0 comments on commit 09dbad5

Please sign in to comment.