579 lines
21 KiB
C++
579 lines
21 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/runtime/animation.h"
|
|
#include "ozz/animation/runtime/local_to_model_job.h"
|
|
#include "ozz/animation/runtime/sampling_job.h"
|
|
#include "ozz/animation/runtime/skeleton.h"
|
|
|
|
#include "ozz/animation/offline/animation_builder.h"
|
|
#include "ozz/animation/offline/animation_optimizer.h"
|
|
#include "ozz/animation/offline/raw_animation.h"
|
|
#include "ozz/animation/offline/raw_animation_utils.h"
|
|
|
|
#include "ozz/base/memory/unique_ptr.h"
|
|
|
|
#include "ozz/base/io/archive.h"
|
|
#include "ozz/base/io/stream.h"
|
|
#include "ozz/base/log.h"
|
|
|
|
#include "ozz/base/maths/math_ex.h"
|
|
#include "ozz/base/maths/simd_math.h"
|
|
#include "ozz/base/maths/soa_transform.h"
|
|
#include "ozz/base/maths/vec_float.h"
|
|
|
|
#include "ozz/options/options.h"
|
|
|
|
#include "framework/application.h"
|
|
#include "framework/imgui.h"
|
|
#include "framework/profile.h"
|
|
#include "framework/renderer.h"
|
|
#include "framework/utils.h"
|
|
|
|
#include <algorithm>
|
|
|
|
// Skeleton and animation file can be specified as an option.
|
|
OZZ_OPTIONS_DECLARE_STRING(skeleton, "Path to the runtime skeleton file.",
|
|
"media/skeleton.ozz", false)
|
|
|
|
OZZ_OPTIONS_DECLARE_STRING(animation, "Path to the raw animation file.",
|
|
"media/animation_raw.ozz", false)
|
|
|
|
namespace {
|
|
|
|
// Loads a raw animation from a file.
|
|
bool LoadAnimation(const char* _filename,
|
|
ozz::animation::offline::RawAnimation* _animation) {
|
|
assert(_filename && _animation);
|
|
ozz::log::Out() << "Loading raw animation archive: " << _filename << "."
|
|
<< std::endl;
|
|
ozz::io::File file(_filename, "rb");
|
|
if (!file.opened()) {
|
|
ozz::log::Err() << "Failed to open animation file " << _filename << "."
|
|
<< std::endl;
|
|
return false;
|
|
}
|
|
ozz::io::IArchive archive(&file);
|
|
if (!archive.TestTag<ozz::animation::offline::RawAnimation>()) {
|
|
ozz::log::Err() << "Failed to load raw animation instance from file "
|
|
<< _filename << "." << std::endl;
|
|
return false;
|
|
}
|
|
|
|
// Once the tag is validated, reading cannot fail.
|
|
archive >> *_animation;
|
|
|
|
// Ensure animation is valid.
|
|
return _animation->Validate();
|
|
}
|
|
} // namespace
|
|
|
|
class OptimizeSampleApplication : public ozz::sample::Application {
|
|
public:
|
|
OptimizeSampleApplication()
|
|
: selected_display_(eRuntimeAnimation),
|
|
optimize_(true),
|
|
joint_setting_enable_(true),
|
|
joint_(0),
|
|
error_record_med_(64),
|
|
error_record_max_(64),
|
|
joint_error_record_(64) {}
|
|
|
|
protected:
|
|
// Updates current animation time and skeleton pose.
|
|
virtual bool OnUpdate(float _dt, float) {
|
|
// Updates current animation time.
|
|
controller_.Update(*animation_rt_, _dt);
|
|
|
|
// Prepares sampling job.
|
|
ozz::animation::SamplingJob sampling_job;
|
|
sampling_job.cache = &cache_;
|
|
sampling_job.ratio = controller_.time_ratio();
|
|
|
|
// Samples optimized animation (_according to the display mode).
|
|
sampling_job.animation = animation_rt_.get();
|
|
sampling_job.output = make_span(locals_rt_);
|
|
if (!sampling_job.Run()) {
|
|
return false;
|
|
}
|
|
|
|
// Also samples non-optimized animation, from the raw animation.
|
|
if (!SampleRawAnimation(raw_animation_,
|
|
controller_.time_ratio() * raw_animation_.duration,
|
|
make_span(locals_raw_))) {
|
|
return false;
|
|
}
|
|
|
|
// Computes difference between the optimized and non-optimized animations
|
|
// in local space, and rebinds it to the bind pose.
|
|
{
|
|
const ozz::span<const ozz::math::SoaTransform>& bind_poses =
|
|
skeleton_.joint_bind_poses();
|
|
const ozz::math::SoaTransform* bind_pose = bind_poses.begin();
|
|
const ozz::math::SoaTransform* locals_raw = locals_raw_.data();
|
|
const ozz::math::SoaTransform* locals_rt = locals_rt_.data();
|
|
ozz::math::SoaTransform* locals_diff = locals_diff_.data();
|
|
for (; bind_pose < bind_poses.end();
|
|
++locals_raw, ++locals_rt, ++locals_diff, ++bind_pose) {
|
|
assert(locals_raw < array_end(locals_raw_) &&
|
|
locals_rt < array_end(locals_rt_) &&
|
|
locals_diff < array_end(locals_diff_) &&
|
|
bind_pose < bind_poses.end());
|
|
|
|
// Computes difference.
|
|
const ozz::math::SoaTransform diff = {
|
|
locals_rt->translation - locals_raw->translation,
|
|
locals_rt->rotation * Conjugate(locals_raw->rotation),
|
|
locals_rt->scale / locals_raw->scale};
|
|
|
|
// Rebinds to the bind pose in the diff buffer.
|
|
locals_diff->translation = bind_pose->translation + diff.translation;
|
|
locals_diff->rotation = bind_pose->rotation * diff.rotation;
|
|
locals_diff->scale = bind_pose->scale * diff.scale;
|
|
}
|
|
}
|
|
|
|
// Converts from local space to model space matrices.
|
|
ozz::animation::LocalToModelJob ltm_job;
|
|
ltm_job.skeleton = &skeleton_;
|
|
|
|
// Optimized samples.
|
|
ltm_job.input = make_span(locals_rt_);
|
|
ltm_job.output = make_span(models_rt_);
|
|
if (!ltm_job.Run()) {
|
|
return false;
|
|
}
|
|
|
|
// Non-optimized samples (from the raw animation).
|
|
ltm_job.input = make_span(locals_raw_);
|
|
ltm_job.output = make_span(models_raw_);
|
|
if (!ltm_job.Run()) {
|
|
return false;
|
|
}
|
|
|
|
// Difference between optimized and non-optimized samples.
|
|
ltm_job.input = make_span(locals_diff_);
|
|
ltm_job.output = make_span(models_diff_);
|
|
if (!ltm_job.Run()) {
|
|
return false;
|
|
}
|
|
|
|
// Computes the absolute error, aka the difference between the raw and
|
|
// runtime model space transformation.
|
|
const size_t num_joints = models_rt_.size();
|
|
float errors_sq[ozz::animation::Skeleton::kMaxJoints];
|
|
for (size_t i = 0; i < num_joints; ++i) {
|
|
// Computes error based on the translation difference.
|
|
errors_sq[i] = ozz::math::GetX(ozz::math::Length3Sqr(
|
|
models_rt_[i].cols[3] - models_raw_[i].cols[3]));
|
|
}
|
|
|
|
std::sort(errors_sq, errors_sq + models_rt_.size());
|
|
error_record_med_.Push(std::sqrt(errors_sq[num_joints / 2]) * 1000.f);
|
|
error_record_max_.Push(std::sqrt(errors_sq[num_joints - 1]) * 1000.f);
|
|
joint_error_record_.Push(std::sqrt(errors_sq[joint_]) * 1000.f);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SampleRawAnimation(
|
|
const ozz::animation::offline::RawAnimation& _animation, float _time,
|
|
ozz::span<ozz::math::SoaTransform> _locals) {
|
|
// Ensure output is big enough.
|
|
if (_locals.size() * 4 < _animation.tracks.size() &&
|
|
locals_raw_aos_.size() * 4 < _animation.tracks.size()) {
|
|
return false;
|
|
}
|
|
|
|
// Sample raw animation and converts AoS transforms to SoA transform array.
|
|
assert(_animation.Validate() && "Animation should be valid.");
|
|
bool success = true;
|
|
for (int i = 0; success && i < _animation.num_tracks(); i += 4) {
|
|
ozz::math::SimdFloat4 translations[4];
|
|
ozz::math::SimdFloat4 rotations[4];
|
|
ozz::math::SimdFloat4 scales[4];
|
|
|
|
// Works on 4 consecutive tracks, or what remains to be processed if it's
|
|
// lower than 4.
|
|
const int jmax = ozz::math::Min(_animation.num_tracks() - i, 4);
|
|
for (int j = 0; success && j < jmax; ++j) {
|
|
// Samples raw animation.
|
|
ozz::math::Transform transform;
|
|
success &= SampleTrack(_animation.tracks[i + j], _time, &transform);
|
|
|
|
// Convert transform to AoS SimdFloat4 values.
|
|
translations[j] =
|
|
ozz::math::simd_float4::Load3PtrU(&transform.translation.x);
|
|
rotations[j] = ozz::math::simd_float4::LoadPtrU(&transform.rotation.x);
|
|
scales[j] = ozz::math::simd_float4::Load3PtrU(&transform.scale.x);
|
|
}
|
|
// Fills remaining transforms.
|
|
for (int j = jmax; j < 4; ++j) {
|
|
translations[j] = ozz::math::simd_float4::zero();
|
|
rotations[j] = ozz::math::simd_float4::w_axis();
|
|
scales[j] = ozz::math::simd_float4::one();
|
|
}
|
|
// Stores AoS keyframes to the SoA output.
|
|
ozz::math::SoaTransform& output = _locals[i / 4];
|
|
ozz::math::Transpose4x3(translations, &output.translation.x);
|
|
ozz::math::Transpose4x4(rotations, &output.rotation.x);
|
|
ozz::math::Transpose4x3(scales, &output.scale.x);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
// Selects model space matrices according to the display mode.
|
|
ozz::span<const ozz::math::Float4x4> models() const {
|
|
switch (selected_display_) {
|
|
case eRuntimeAnimation:
|
|
return make_span(models_rt_);
|
|
case eRawAnimation:
|
|
return make_span(models_raw_);
|
|
case eAbsoluteError:
|
|
return make_span(models_diff_);
|
|
default: {
|
|
assert(false && "Invalid display mode");
|
|
return make_span(models_rt_);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Samples animation, transforms to model space and renders.
|
|
virtual bool OnDisplay(ozz::sample::Renderer* _renderer) {
|
|
bool success = true;
|
|
|
|
const ozz::span<const ozz::math::Float4x4> transforms = models();
|
|
|
|
// Renders posture.
|
|
success &= _renderer->DrawPosture(skeleton_, transforms,
|
|
ozz::math::Float4x4::identity());
|
|
|
|
if (joint_setting_enable_) {
|
|
// Renders an axes with targetted joint transform.
|
|
const ozz::math::Float4x4 axes_scale = ozz::math::Float4x4::Scaling(
|
|
ozz::math::simd_float4::Load1(joint_setting_.distance));
|
|
success &= _renderer->DrawAxes(transforms[joint_] * axes_scale);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
virtual bool OnInitialize() {
|
|
// Imports offline skeleton from a binary file.
|
|
if (!ozz::sample::LoadSkeleton(OPTIONS_skeleton, &skeleton_)) {
|
|
return false;
|
|
}
|
|
|
|
// Imports offline animation from a binary file.
|
|
// Invalid animations are rejected by the load function.
|
|
if (!LoadAnimation(OPTIONS_animation, &raw_animation_)) {
|
|
return false;
|
|
}
|
|
|
|
const int num_joints = skeleton_.num_joints();
|
|
const int num_soa_joints = skeleton_.num_soa_joints();
|
|
|
|
// Finds the joint where the object should be attached.
|
|
for (int i = 0; i < num_joints; i++) {
|
|
if (std::strstr(skeleton_.joint_names()[i], "L Finger2Nub")) {
|
|
joint_ = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Builds the runtime animation from the raw one.
|
|
if (!BuildAnimations()) {
|
|
return false;
|
|
}
|
|
|
|
// Allocates runtime buffers.
|
|
locals_rt_.resize(num_soa_joints);
|
|
models_rt_.resize(num_joints);
|
|
locals_raw_aos_.resize(num_joints);
|
|
locals_raw_.resize(num_soa_joints);
|
|
models_raw_.resize(num_joints);
|
|
locals_diff_.resize(num_soa_joints);
|
|
models_diff_.resize(num_joints);
|
|
|
|
// Allocates a cache that matches animation requirements.
|
|
cache_.Resize(num_joints);
|
|
|
|
return true;
|
|
}
|
|
|
|
virtual bool OnGui(ozz::sample::ImGui* _im_gui) {
|
|
char label[64];
|
|
// Exposes animation runtime playback controls.
|
|
{
|
|
static bool open = true;
|
|
ozz::sample::ImGui::OpenClose occ(_im_gui, "Animation control", &open);
|
|
if (open) {
|
|
controller_.OnGui(*animation_rt_, _im_gui);
|
|
}
|
|
}
|
|
|
|
// Exposes optimizer's tolerances.
|
|
{
|
|
static bool open = true;
|
|
ozz::sample::ImGui::OpenClose ocb(_im_gui, "Optimization tolerances",
|
|
&open);
|
|
if (open) {
|
|
bool rebuild = false;
|
|
|
|
rebuild |= _im_gui->DoCheckBox("Enable optimizations", &optimize_);
|
|
|
|
std::sprintf(label, "Tolerance: %0.2f mm", setting_.tolerance * 1000);
|
|
rebuild |= _im_gui->DoSlider(label, 0.f, .1f, &setting_.tolerance, .5f,
|
|
optimize_);
|
|
|
|
std::sprintf(label, "Distance: %0.2f mm", setting_.distance * 1000);
|
|
rebuild |= _im_gui->DoSlider(label, 0.f, 1.f, &setting_.distance, .5f,
|
|
optimize_);
|
|
|
|
rebuild |= _im_gui->DoCheckBox("Enable joint setting",
|
|
&joint_setting_enable_, optimize_);
|
|
|
|
std::sprintf(label, "%s (%d)", skeleton_.joint_names()[joint_], joint_);
|
|
rebuild |=
|
|
_im_gui->DoSlider(label, 0, skeleton_.num_joints() - 1, &joint_,
|
|
1.f, joint_setting_enable_ && optimize_);
|
|
|
|
std::sprintf(label, "Tolerance: %0.2f mm",
|
|
joint_setting_.tolerance * 1000);
|
|
rebuild |= _im_gui->DoSlider(label, 0.f, .1f, &joint_setting_.tolerance,
|
|
.5f, joint_setting_enable_ && optimize_);
|
|
|
|
std::sprintf(label, "Distance: %0.2f mm",
|
|
joint_setting_.distance * 1000);
|
|
rebuild |= _im_gui->DoSlider(label, 0.f, 1.f, &joint_setting_.distance,
|
|
.5f, joint_setting_enable_ && optimize_);
|
|
|
|
if (rebuild) {
|
|
// Invalidates the cache in case the new animation has the same
|
|
// address as the previous one. Other cases (like changing animation)
|
|
// are automatic handled by the cache. See SamplingCache::Invalidate
|
|
// for more details.
|
|
cache_.Invalidate();
|
|
|
|
// Rebuilds a new runtime animation.
|
|
if (!BuildAnimations()) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
{
|
|
static bool open = true;
|
|
ozz::sample::ImGui::OpenClose ocb(_im_gui, "Memory size", &open);
|
|
if (open) {
|
|
std::sprintf(label, "Original: %dKB",
|
|
static_cast<int>(raw_animation_.size() >> 10));
|
|
_im_gui->DoLabel(label);
|
|
|
|
std::sprintf(label, "Optimized: %dKB (%.1f:1)",
|
|
static_cast<int>(raw_optimized_animation_.size() >> 10),
|
|
static_cast<float>(raw_animation_.size()) /
|
|
raw_optimized_animation_.size());
|
|
_im_gui->DoLabel(label);
|
|
|
|
std::sprintf(
|
|
label, "Compressed: %dKB (%.1f:1)",
|
|
static_cast<int>(animation_rt_->size() >> 10),
|
|
static_cast<float>(raw_animation_.size()) / animation_rt_->size());
|
|
_im_gui->DoLabel(label);
|
|
}
|
|
}
|
|
|
|
// Selects display mode.
|
|
static bool open_mode = true;
|
|
ozz::sample::ImGui::OpenClose mode(_im_gui, "Display mode", &open_mode);
|
|
if (open_mode) {
|
|
_im_gui->DoRadioButton(eRuntimeAnimation, "Runtime animation",
|
|
&selected_display_);
|
|
_im_gui->DoRadioButton(eRawAnimation, "Raw animation",
|
|
&selected_display_);
|
|
_im_gui->DoRadioButton(eAbsoluteError, "Absolute error",
|
|
&selected_display_);
|
|
}
|
|
|
|
// Show absolute error.
|
|
{
|
|
char szLabel[64];
|
|
static bool error_open = true;
|
|
ozz::sample::ImGui::OpenClose oc_stats(_im_gui, "Absolute error",
|
|
&error_open);
|
|
if (error_open) {
|
|
{
|
|
std::sprintf(szLabel, "Median error: %.2fmm",
|
|
*error_record_med_.cursor());
|
|
const ozz::sample::Record::Statistics error_stats =
|
|
error_record_med_.GetStatistics();
|
|
_im_gui->DoGraph(szLabel, 0.f, error_stats.max, error_stats.latest,
|
|
error_record_med_.cursor(),
|
|
error_record_med_.record_begin(),
|
|
error_record_med_.record_end());
|
|
}
|
|
{
|
|
std::sprintf(szLabel, "Maximum error: %.2fmm",
|
|
*error_record_max_.cursor());
|
|
const ozz::sample::Record::Statistics error_stats =
|
|
error_record_max_.GetStatistics();
|
|
_im_gui->DoGraph(szLabel, 0.f, error_stats.max, error_stats.latest,
|
|
error_record_max_.cursor(),
|
|
error_record_max_.record_begin(),
|
|
error_record_max_.record_end());
|
|
}
|
|
{
|
|
std::sprintf(szLabel, "Joint %d error: %.2fmm", joint_,
|
|
*joint_error_record_.cursor());
|
|
const ozz::sample::Record::Statistics error_stats =
|
|
joint_error_record_.GetStatistics();
|
|
_im_gui->DoGraph(szLabel, 0.f, error_stats.max, error_stats.latest,
|
|
joint_error_record_.cursor(),
|
|
joint_error_record_.record_begin(),
|
|
joint_error_record_.record_end());
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
virtual void OnDestroy() {}
|
|
|
|
bool BuildAnimations() {
|
|
// Instantiate an animation builder.
|
|
ozz::animation::offline::AnimationBuilder animation_builder;
|
|
|
|
// Builds the optimized animation.
|
|
if (optimize_) {
|
|
ozz::animation::offline::AnimationOptimizer optimizer;
|
|
|
|
// Setup global optimization settings.
|
|
optimizer.setting = setting_;
|
|
|
|
// Setup joint specific optimization settings.
|
|
if (joint_setting_enable_) {
|
|
optimizer.joints_setting_override[joint_] = joint_setting_;
|
|
} else {
|
|
optimizer.joints_setting_override.clear();
|
|
}
|
|
|
|
if (!optimizer(raw_animation_, skeleton_, &raw_optimized_animation_)) {
|
|
return false;
|
|
}
|
|
} else {
|
|
// Builds runtime animation from the brute one.
|
|
raw_optimized_animation_ = raw_animation_;
|
|
}
|
|
|
|
// Builds runtime animation from the optimized one.
|
|
animation_rt_ = animation_builder(raw_optimized_animation_);
|
|
|
|
// Check if building runtime animation was successful.
|
|
if (!animation_rt_) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
virtual void GetSceneBounds(ozz::math::Box* _bound) const {
|
|
ozz::sample::ComputePostureBounds(models(), _bound);
|
|
}
|
|
|
|
private:
|
|
// Selects which animation is displayed.
|
|
enum DisplayMode {
|
|
eRuntimeAnimation,
|
|
eRawAnimation,
|
|
eAbsoluteError,
|
|
};
|
|
int selected_display_;
|
|
|
|
// Select whether optimization should be performed.
|
|
bool optimize_;
|
|
|
|
// Imported non-optimized animation.
|
|
ozz::animation::offline::RawAnimation raw_animation_;
|
|
|
|
// Optimized raw animation.
|
|
ozz::animation::offline::RawAnimation raw_optimized_animation_;
|
|
|
|
// Optimizer global settings.
|
|
ozz::animation::offline::AnimationOptimizer::Setting setting_;
|
|
|
|
// Optimizer joint specific settings.
|
|
bool joint_setting_enable_;
|
|
int joint_;
|
|
ozz::animation::offline::AnimationOptimizer::Setting joint_setting_;
|
|
|
|
// Playback animation controller. This is a utility class that helps with
|
|
// controlling animation playback time.
|
|
ozz::sample::PlaybackController controller_;
|
|
|
|
// Runtime skeleton.
|
|
ozz::animation::Skeleton skeleton_;
|
|
|
|
// Sampling cache, shared across optimized and non-optimized animations. This
|
|
// is not optimal, but it's not an issue either.
|
|
ozz::animation::SamplingCache cache_;
|
|
|
|
// Runtime optimized animation.
|
|
ozz::unique_ptr<ozz::animation::Animation> animation_rt_;
|
|
|
|
// Buffer of local and model space transformations as sampled from the
|
|
// rutime (optimized and compressed) animation.
|
|
ozz::vector<ozz::math::SoaTransform> locals_rt_;
|
|
ozz::vector<ozz::math::Float4x4> models_rt_;
|
|
|
|
// Buffer of local and model space transformations as sampled from the
|
|
// non-optimized (raw) animation.
|
|
// Sampling the raw animation results in AoS data, meaning we have to
|
|
// allocate AoS data and do the SoA conversion by hand.
|
|
ozz::vector<ozz::math::Transform> locals_raw_aos_;
|
|
ozz::vector<ozz::math::SoaTransform> locals_raw_;
|
|
ozz::vector<ozz::math::Float4x4> models_raw_;
|
|
|
|
// Buffer of local and model space transformations storing samples from the
|
|
// difference between optimized and non-optimized animations.
|
|
ozz::vector<ozz::math::SoaTransform> locals_diff_;
|
|
ozz::vector<ozz::math::Float4x4> models_diff_;
|
|
|
|
// Record of accuracy errors produced by animation compression and
|
|
// optimization.
|
|
ozz::sample::Record error_record_med_;
|
|
ozz::sample::Record error_record_max_;
|
|
ozz::sample::Record joint_error_record_;
|
|
};
|
|
|
|
int main(int _argc, const char** _argv) {
|
|
const char* title = "Ozz-animation sample: Animation keyframe optimization";
|
|
return OptimizeSampleApplication().Run(_argc, _argv, "1.0", title);
|
|
}
|