//----------------------------------------------------------------------------// // // // 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/animation_optimizer.h" #include "gtest/gtest.h" #include "ozz/base/maths/math_constant.h" #include "ozz/base/memory/unique_ptr.h" #include "ozz/animation/offline/animation_builder.h" #include "ozz/animation/offline/raw_animation.h" #include "ozz/animation/offline/raw_skeleton.h" #include "ozz/animation/offline/skeleton_builder.h" #include "ozz/animation/runtime/skeleton.h" using ozz::animation::Skeleton; using ozz::animation::offline::AnimationOptimizer; using ozz::animation::offline::RawAnimation; using ozz::animation::offline::RawSkeleton; using ozz::animation::offline::SkeletonBuilder; TEST(Error, AnimationOptimizer) { AnimationOptimizer optimizer; { // nullptr output. RawAnimation input; Skeleton skeleton; EXPECT_TRUE(input.Validate()); // Builds animation EXPECT_FALSE(optimizer(input, skeleton, nullptr)); } { // Invalid input animation. RawSkeleton raw_skeleton; raw_skeleton.roots.resize(1); SkeletonBuilder skeleton_builder; ozz::unique_ptr skeleton(skeleton_builder(raw_skeleton)); ASSERT_TRUE(skeleton); RawAnimation input; input.duration = -1.f; EXPECT_FALSE(input.Validate()); // Builds animation RawAnimation output; output.duration = -1.f; output.tracks.resize(1); EXPECT_FALSE(optimizer(input, *skeleton, &output)); EXPECT_FLOAT_EQ(output.duration, RawAnimation().duration); EXPECT_EQ(output.num_tracks(), 0); } { // Invalid skeleton. Skeleton skeleton; RawAnimation input; input.tracks.resize(1); EXPECT_TRUE(input.Validate()); // Builds animation RawAnimation output; EXPECT_FALSE(optimizer(input, skeleton, &output)); EXPECT_FLOAT_EQ(output.duration, RawAnimation().duration); EXPECT_EQ(output.num_tracks(), 0); } } TEST(Name, AnimationOptimizer) { // Prepares a skeleton. RawSkeleton raw_skeleton; SkeletonBuilder skeleton_builder; ozz::unique_ptr skeleton(skeleton_builder(raw_skeleton)); ASSERT_TRUE(skeleton); AnimationOptimizer optimizer; RawAnimation input; input.name = "Test_Animation"; input.duration = 1.f; ASSERT_TRUE(input.Validate()); RawAnimation output; ASSERT_TRUE(optimizer(input, *skeleton, &output)); EXPECT_EQ(output.num_tracks(), 0); EXPECT_STRCASEEQ(output.name.c_str(), "Test_Animation"); } TEST(Optimize, AnimationOptimizer) { // Prepares a skeleton. RawSkeleton raw_skeleton; raw_skeleton.roots.resize(1); raw_skeleton.roots[0].children.resize(1); raw_skeleton.roots[0].children[0].children.resize(1); raw_skeleton.roots[0].children[0].children[0].children.resize(2); SkeletonBuilder skeleton_builder; ozz::unique_ptr skeleton(skeleton_builder(raw_skeleton)); ASSERT_TRUE(skeleton); // Disable non hierarchical optimizations AnimationOptimizer optimizer; // Disables vertex distance. optimizer.setting.distance = 0.f; RawAnimation input; input.duration = 1.f; input.tracks.resize(5); // Translations on track 0. { RawAnimation::TranslationKey key = {0.f, ozz::math::Float3(4.f, 0.f, 0.f)}; input.tracks[0].translations.push_back(key); } // Translations on track 1. { RawAnimation::TranslationKey key = {0.f, ozz::math::Float3(0.f, 0.f, 0.f)}; input.tracks[1].translations.push_back(key); } // Translations on track 2. { RawAnimation::TranslationKey key = {0.f, ozz::math::Float3(5.f, 0.f, 0.f)}; input.tracks[2].translations.push_back(key); } { RawAnimation::TranslationKey key = {.1f, ozz::math::Float3(6.f, 0.f, 0.f)}; input.tracks[2].translations.push_back(key); } { // Creates an variation. RawAnimation::TranslationKey key = {.2f, ozz::math::Float3(7.1f, 0.f, 0.f)}; input.tracks[2].translations.push_back(key); } { RawAnimation::TranslationKey key = {.3f, ozz::math::Float3(8.f, 0.f, 0.f)}; input.tracks[2].translations.push_back(key); } // Translations on track 3. { RawAnimation::TranslationKey key = {0.f, ozz::math::Float3(16.f, 0.f, 0.f)}; input.tracks[3].translations.push_back(key); } // Translations on track 4. { RawAnimation::TranslationKey key = {0.f, ozz::math::Float3(32.f, 0.f, 0.f)}; input.tracks[4].translations.push_back(key); } ASSERT_TRUE(input.Validate()); // Small translation tolerance -> all key maintained { RawAnimation output; optimizer.setting.tolerance = .01f; ASSERT_TRUE(optimizer(input, *skeleton, &output)); EXPECT_EQ(output.num_tracks(), 5); const RawAnimation::JointTrack::Translations& translations = output.tracks[2].translations; ASSERT_EQ(translations.size(), 4u); EXPECT_FLOAT_EQ(translations[0].time, 0.f); EXPECT_FLOAT_EQ(translations[1].time, .1f); EXPECT_FLOAT_EQ(translations[2].time, .2f); EXPECT_FLOAT_EQ(translations[3].time, .3f); } // High translation tolerance -> all key interpolated { RawAnimation output; optimizer.setting.tolerance = .1f; ASSERT_TRUE(optimizer(input, *skeleton, &output)); EXPECT_EQ(output.num_tracks(), 5); const RawAnimation::JointTrack::Translations& translations = output.tracks[2].translations; ASSERT_EQ(translations.size(), 2u); EXPECT_FLOAT_EQ(translations[0].time, 0.f); EXPECT_FLOAT_EQ(translations[1].time, .3f); } // Introduces a 10x scaling upstream that amplifies error // Scaling on track 0 { RawAnimation::ScaleKey key = {0.f, ozz::math::Float3(10.f, 0.f, 0.f)}; input.tracks[0].scales.push_back(key); } // High translation tolerance -> keys aren't interpolated because of scale // effect. { RawAnimation output; optimizer.setting.tolerance = .1f; ASSERT_TRUE(optimizer(input, *skeleton, &output)); EXPECT_EQ(output.num_tracks(), 5); const RawAnimation::JointTrack::Translations& translations = output.tracks[2].translations; ASSERT_EQ(translations.size(), 4u); } // Very high tolerance { RawAnimation output; optimizer.setting.tolerance = 1.f; ASSERT_TRUE(optimizer(input, *skeleton, &output)); EXPECT_EQ(output.num_tracks(), 5); const RawAnimation::JointTrack::Translations& translations = output.tracks[2].translations; ASSERT_EQ(translations.size(), 2u); } // Introduces a -10x scaling upstream that amplifies error // Scaling on track 0 { input.tracks[0].scales[0].value = ozz::math::Float3(0.f, -10.f, 0.f); } // High translation tolerance -> keys aren't interpolated because of scale // effect. { RawAnimation output; optimizer.setting.tolerance = .1f; ASSERT_TRUE(optimizer(input, *skeleton, &output)); EXPECT_EQ(output.num_tracks(), 5); const RawAnimation::JointTrack::Translations& translations = output.tracks[2].translations; ASSERT_EQ(translations.size(), 4u); EXPECT_FLOAT_EQ(translations[0].time, 0.f); EXPECT_FLOAT_EQ(translations[1].time, .1f); EXPECT_FLOAT_EQ(translations[2].time, .2f); EXPECT_FLOAT_EQ(translations[3].time, .3f); } // Very high tolerance { RawAnimation output; optimizer.setting.tolerance = 1.f; ASSERT_TRUE(optimizer(input, *skeleton, &output)); EXPECT_EQ(output.num_tracks(), 5); const RawAnimation::JointTrack::Translations& translations = output.tracks[2].translations; ASSERT_EQ(translations.size(), 2u); EXPECT_FLOAT_EQ(translations[0].time, 0.f); EXPECT_FLOAT_EQ(translations[1].time, .3f); } // Compenstate scale on next joint { RawAnimation::ScaleKey key = {0.f, ozz::math::Float3(.1f, 0.f, 0.f)}; input.tracks[1].scales.push_back(key); } // High translation tolerance -> keys ar interpolated because of scale // compensation. { RawAnimation output; optimizer.setting.tolerance = 1.f; ASSERT_TRUE(optimizer(input, *skeleton, &output)); EXPECT_EQ(output.num_tracks(), 5); const RawAnimation::JointTrack::Translations& translations = output.tracks[2].translations; ASSERT_EQ(translations.size(), 2u); } // Remove scaling compensation { input.tracks[1].scales.clear(); } // Introduces a .1x scaling upstream that amplifies error // Scaling on track 0 { input.tracks[0].scales[0].value = ozz::math::Float3(0.f, 0.f, .1f); } // High translation tolerance -> keys aren't interpolated because of scale // effect. { RawAnimation output; optimizer.setting.tolerance = .001f; ASSERT_TRUE(optimizer(input, *skeleton, &output)); EXPECT_EQ(output.num_tracks(), 5); const RawAnimation::JointTrack::Translations& translations = output.tracks[2].translations; ASSERT_EQ(translations.size(), 4u); EXPECT_FLOAT_EQ(translations[0].time, 0.f); EXPECT_FLOAT_EQ(translations[1].time, .1f); EXPECT_FLOAT_EQ(translations[2].time, .2f); EXPECT_FLOAT_EQ(translations[3].time, .3f); } // Very high translation tolerance { RawAnimation output; optimizer.setting.tolerance = .01f; ASSERT_TRUE(optimizer(input, *skeleton, &output)); EXPECT_EQ(output.num_tracks(), 5); const RawAnimation::JointTrack::Translations& translations = output.tracks[2].translations; ASSERT_EQ(translations.size(), 2u); EXPECT_FLOAT_EQ(translations[0].time, 0.f); EXPECT_FLOAT_EQ(translations[1].time, .3f); } // Remove scaling { input.tracks[0].scales.clear(); } // Rotations on track 0. { RawAnimation::RotationKey key = { 0.f, ozz::math::Quaternion::FromEuler(0.f, 0.f, 0.f)}; input.tracks[0].rotations.push_back(key); } { // Include error const float angle_error = 2.5e-3f; // creates an arc of .1m at 40m. RawAnimation::RotationKey key = { .1f, ozz::math::Quaternion::FromEuler(ozz::math::kPi_4 + angle_error, 0.f, 0.f)}; input.tracks[0].rotations.push_back(key); } { RawAnimation::RotationKey key = { .2f, ozz::math::Quaternion::FromEuler(ozz::math::kPi_2, 0.f, 0.f)}; input.tracks[0].rotations.push_back(key); } // Big enough tolerance -> keys rejected { RawAnimation output; optimizer.setting.tolerance = .3f; optimizer.setting.distance = 40.f; ASSERT_TRUE(optimizer(input, *skeleton, &output)); EXPECT_EQ(output.num_tracks(), 5); const RawAnimation::JointTrack::Rotations& rotations = output.tracks[0].rotations; ASSERT_EQ(rotations.size(), 2u); const RawAnimation::JointTrack::Translations& translations = output.tracks[2].translations; ASSERT_EQ(translations.size(), 2u); } // Small enough tolerance -> keys rejected { RawAnimation output; optimizer.setting.tolerance = .05f; optimizer.setting.distance = 40.f; ASSERT_TRUE(optimizer(input, *skeleton, &output)); EXPECT_EQ(output.num_tracks(), 5); const RawAnimation::JointTrack::Rotations& rotations = output.tracks[0].rotations; ASSERT_EQ(rotations.size(), 3u); const RawAnimation::JointTrack::Translations& translations = output.tracks[2].translations; ASSERT_EQ(translations.size(), 4u); } // Back to default optimizer.setting = AnimationOptimizer::Setting(); // Small translation tolerance -> all key maintained { RawAnimation output; optimizer.setting.tolerance = .01f; ASSERT_TRUE(optimizer(input, *skeleton, &output)); EXPECT_EQ(output.num_tracks(), 5); const RawAnimation::JointTrack::Rotations& rotations = output.tracks[0].rotations; ASSERT_EQ(rotations.size(), 3u); const RawAnimation::JointTrack::Translations& translations = output.tracks[2].translations; ASSERT_EQ(translations.size(), 4u); } // Introduces a .1x scaling upstream that lowers error // Scaling on track 0 { RawAnimation::ScaleKey key = {0.f, ozz::math::Float3(0.f, .1f, 0.f)}; input.tracks[1].scales.push_back(key); } // Small translation tolerance, but scaled down -> keys rejected { RawAnimation output; optimizer.setting.tolerance = .011f; ASSERT_TRUE(optimizer(input, *skeleton, &output)); EXPECT_EQ(output.num_tracks(), 5); const RawAnimation::JointTrack::Rotations& rotations = output.tracks[0].rotations; ASSERT_EQ(rotations.size(), 2u); const RawAnimation::JointTrack::Translations& translations = output.tracks[2].translations; ASSERT_EQ(translations.size(), 2u); } // More vertex distance -> keys are maintained (translation unaffected) { RawAnimation output; optimizer.setting.tolerance = .01f; optimizer.setting.distance = 1.f; ASSERT_TRUE(optimizer(input, *skeleton, &output)); EXPECT_EQ(output.num_tracks(), 5); const RawAnimation::JointTrack::Rotations& rotations = output.tracks[0].rotations; ASSERT_EQ(rotations.size(), 3u); const RawAnimation::JointTrack::Translations& translations = output.tracks[2].translations; ASSERT_EQ(translations.size(), 2u); } // Remove scaling { input.tracks[2].scales.clear(); } } TEST(OptimizeOverride, AnimationOptimizer) { // Prepares a skeleton. RawSkeleton raw_skeleton; raw_skeleton.roots.resize(1); raw_skeleton.roots[0].children.resize(1); raw_skeleton.roots[0].children[0].children.resize(1); raw_skeleton.roots[0].children[0].children[0].children.resize(2); SkeletonBuilder skeleton_builder; ozz::unique_ptr skeleton(skeleton_builder(raw_skeleton)); ASSERT_TRUE(skeleton); // Disable non hierarchical optimizations AnimationOptimizer optimizer; const AnimationOptimizer::Setting loose_setting(1e-2f, // 1cm 1e-3f); // 1mm // Disables vertex distance. optimizer.setting.distance = 0.f; RawAnimation input; input.duration = 1.f; input.tracks.resize(5); // Translations on track 0. { RawAnimation::TranslationKey key = {0.f, ozz::math::Float3(.4f, 0.f, 0.f)}; input.tracks[0].translations.push_back(key); } // Rotations on track 0. { RawAnimation::RotationKey key = { 0.f, ozz::math::Quaternion::FromEuler(0.f, 0.f, 0.f)}; input.tracks[1].rotations.push_back(key); } { // Includes an error that const float angle_error = 1e-3f; // creates an arc of 1mm at 1m. RawAnimation::RotationKey key = { .1f, ozz::math::Quaternion::FromEuler(ozz::math::kPi_4 + angle_error, 0.f, 0.f)}; input.tracks[1].rotations.push_back(key); } { RawAnimation::RotationKey key = { .2f, ozz::math::Quaternion::FromEuler(ozz::math::kPi_2, 0.f, 0.f)}; input.tracks[1].rotations.push_back(key); } // Translations on track 1. { RawAnimation::TranslationKey key = {0.f, ozz::math::Float3(0.f, 0.f, 0.f)}; input.tracks[1].translations.push_back(key); } // Translations on track 2. { RawAnimation::TranslationKey key = {0.f, ozz::math::Float3(.05f, 0.f, 0.f)}; input.tracks[2].translations.push_back(key); } { RawAnimation::TranslationKey key = {.1f, ozz::math::Float3(.06f, 0.f, 0.f)}; input.tracks[2].translations.push_back(key); } { // Creates a variation. const float trans_err = 5e-4f; RawAnimation::TranslationKey key = { .2f, ozz::math::Float3(.07f + trans_err, 0.f, 0.f)}; input.tracks[2].translations.push_back(key); } { RawAnimation::TranslationKey key = {.3f, ozz::math::Float3(.08f, 0.f, 0.f)}; input.tracks[2].translations.push_back(key); } // Translations on track 3. { RawAnimation::TranslationKey key = {0.f, ozz::math::Float3(.16f, 0.f, 0.f)}; input.tracks[3].translations.push_back(key); } // Translations on track 4. { RawAnimation::TranslationKey key = {0.f, ozz::math::Float3(.32f, 0.f, 0.f)}; input.tracks[4].translations.push_back(key); } ASSERT_TRUE(input.Validate()); // Default global tolerances { RawAnimation output; optimizer.setting = loose_setting; ASSERT_TRUE(optimizer(input, *skeleton, &output)); ASSERT_EQ(output.num_tracks(), 5); const RawAnimation::JointTrack::Rotations& rotations = output.tracks[1].rotations; ASSERT_EQ(rotations.size(), 2u); EXPECT_FLOAT_EQ(rotations[0].time, 0.f); EXPECT_FLOAT_EQ(rotations[1].time, .2f); const RawAnimation::JointTrack::Translations& translations = output.tracks[2].translations; ASSERT_EQ(translations.size(), 2u); EXPECT_FLOAT_EQ(translations[0].time, 0.f); EXPECT_FLOAT_EQ(translations[1].time, .3f); } // Overriding root has no effect on its child, even with small tolerance. { RawAnimation output; optimizer.setting = loose_setting; const AnimationOptimizer::Setting joint_override(1e-6f, 1e6f); optimizer.joints_setting_override[0] = joint_override; ASSERT_TRUE(optimizer(input, *skeleton, &output)); ASSERT_EQ(output.num_tracks(), 5); const RawAnimation::JointTrack::Rotations& rotations = output.tracks[1].rotations; EXPECT_EQ(rotations.size(), 2u); const RawAnimation::JointTrack::Translations& translations = output.tracks[2].translations; EXPECT_EQ(translations.size(), 2u); optimizer.joints_setting_override.clear(); } // Overriding a joint has effect on itself. { RawAnimation output; optimizer.setting = loose_setting; const AnimationOptimizer::Setting joint_override(1e-3f, // 1mm 1e-2f); // 1cm optimizer.joints_setting_override[1] = joint_override; ASSERT_TRUE(optimizer(input, *skeleton, &output)); EXPECT_EQ(output.num_tracks(), 5); const RawAnimation::JointTrack::Rotations& rotations = output.tracks[1].rotations; EXPECT_EQ(rotations.size(), 2u); const RawAnimation::JointTrack::Translations& translations = output.tracks[2].translations; EXPECT_EQ(translations.size(), 2u); optimizer.joints_setting_override.clear(); } // Overriding leaf has effect up to the root though. { RawAnimation output; optimizer.setting = loose_setting; const AnimationOptimizer::Setting joint_override(1e-3f, // 1mm 10.f); // 10m optimizer.joints_setting_override[2] = joint_override; ASSERT_TRUE(optimizer(input, *skeleton, &output)); EXPECT_EQ(output.num_tracks(), 5); const RawAnimation::JointTrack::Rotations& rotations = output.tracks[1].rotations; ASSERT_EQ(rotations.size(), 3u); const RawAnimation::JointTrack::Translations& translations = output.tracks[2].translations; ASSERT_EQ(translations.size(), 2u); optimizer.joints_setting_override.clear(); } // Scale at root affects rotation and translation. { RawAnimation::ScaleKey key = {0.f, ozz::math::Float3(.1f, 2.f, .1f)}; input.tracks[0].scales.push_back(key); RawAnimation output; optimizer.setting = loose_setting; const AnimationOptimizer::Setting joint_override(1.e-3f, // > 1mm 1.f); // 1m optimizer.joints_setting_override[1] = joint_override; optimizer.joints_setting_override[2] = joint_override; ASSERT_TRUE(optimizer(input, *skeleton, &output)); EXPECT_EQ(output.num_tracks(), 5); const RawAnimation::JointTrack::Rotations& rotations = output.tracks[1].rotations; EXPECT_EQ(rotations.size(), 3u); const RawAnimation::JointTrack::Translations& translations = output.tracks[2].translations; EXPECT_EQ(translations.size(), 3u); optimizer.joints_setting_override.clear(); input.tracks[0].scales.clear(); } // Scale at leaf doesn't affect anything but the leaf. { RawAnimation::ScaleKey key = {0.f, ozz::math::Float3(.1f, 2.f, .1f)}; input.tracks[4].scales.push_back(key); RawAnimation output; optimizer.setting = loose_setting; const AnimationOptimizer::Setting joint_override(1e-3f, // < 1mm .5f); // .5m optimizer.joints_setting_override[1] = joint_override; ASSERT_TRUE(optimizer(input, *skeleton, &output)); EXPECT_EQ(output.num_tracks(), 5); const RawAnimation::JointTrack::Rotations& rotations = output.tracks[1].rotations; EXPECT_EQ(rotations.size(), 2u); const RawAnimation::JointTrack::Translations& translations = output.tracks[2].translations; EXPECT_EQ(translations.size(), 2u); optimizer.joints_setting_override.clear(); input.tracks[4].scales.clear(); } }