AnimTestbed/3rdparty/ozz-animation/test/animation/offline/animation_optimizer_tests.cc

673 lines
22 KiB
C++

//----------------------------------------------------------------------------//
// //
// 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(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(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(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(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();
}
}