2021-11-11 21:22:24 +01:00
|
|
|
//----------------------------------------------------------------------------//
|
|
|
|
// //
|
|
|
|
// 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. //
|
|
|
|
// //
|
|
|
|
//----------------------------------------------------------------------------//
|
|
|
|
|
2023-03-26 11:44:29 +02:00
|
|
|
#include "framework/application.h"
|
|
|
|
#include "framework/imgui.h"
|
|
|
|
#include "framework/renderer.h"
|
|
|
|
#include "framework/utils.h"
|
2021-11-11 21:22:24 +01:00
|
|
|
#include "ozz/animation/runtime/animation.h"
|
|
|
|
#include "ozz/animation/runtime/blending_job.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/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"
|
|
|
|
|
|
|
|
// Skeleton archive can be specified as an option.
|
|
|
|
OZZ_OPTIONS_DECLARE_STRING(skeleton,
|
|
|
|
"Path to the skeleton (ozz archive format).",
|
|
|
|
"media/skeleton.ozz", false)
|
|
|
|
|
|
|
|
// First animation archive can be specified as an option.
|
|
|
|
OZZ_OPTIONS_DECLARE_STRING(animation1,
|
|
|
|
"Path to the first animation (ozz archive format).",
|
|
|
|
"media/animation1.ozz", false)
|
|
|
|
|
|
|
|
// Second animation archive can be specified as an option.
|
|
|
|
OZZ_OPTIONS_DECLARE_STRING(animation2,
|
|
|
|
"Path to the second animation (ozz archive format).",
|
|
|
|
"media/animation2.ozz", false)
|
|
|
|
|
|
|
|
// Third animation archive can be specified as an option.
|
|
|
|
OZZ_OPTIONS_DECLARE_STRING(animation3,
|
2024-03-17 12:47:11 +01:00
|
|
|
"Path to the third animation (ozz archive format).",
|
2021-11-11 21:22:24 +01:00
|
|
|
"media/animation3.ozz", false)
|
|
|
|
|
|
|
|
class BlendSampleApplication : public ozz::sample::Application {
|
|
|
|
public:
|
|
|
|
BlendSampleApplication()
|
|
|
|
: blend_ratio_(.3f),
|
|
|
|
manual_(false),
|
|
|
|
threshold_(ozz::animation::BlendingJob().threshold) {}
|
|
|
|
|
|
|
|
protected:
|
|
|
|
// Updates current animation time and skeleton pose.
|
|
|
|
virtual bool OnUpdate(float _dt, float) {
|
|
|
|
// Updates blending parameters and synchronizes animations if control mode
|
|
|
|
// is not manual.
|
|
|
|
if (!manual_) {
|
|
|
|
UpdateRuntimeParameters();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Updates and samples all animations to their respective local space
|
|
|
|
// transform buffers.
|
|
|
|
for (int i = 0; i < kNumLayers; ++i) {
|
|
|
|
Sampler& sampler = samplers_[i];
|
|
|
|
|
|
|
|
// Updates animations time.
|
|
|
|
sampler.controller.Update(sampler.animation, _dt);
|
|
|
|
|
|
|
|
// Early out if this sampler weight makes it irrelevant during blending.
|
|
|
|
if (samplers_[i].weight <= 0.f) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Setup sampling job.
|
|
|
|
ozz::animation::SamplingJob sampling_job;
|
|
|
|
sampling_job.animation = &sampler.animation;
|
2023-03-26 11:44:29 +02:00
|
|
|
sampling_job.context = &sampler.context;
|
2021-11-11 21:22:24 +01:00
|
|
|
sampling_job.ratio = sampler.controller.time_ratio();
|
|
|
|
sampling_job.output = make_span(sampler.locals);
|
|
|
|
|
|
|
|
// Samples animation.
|
|
|
|
if (!sampling_job.Run()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Blends animations.
|
|
|
|
// Blends the local spaces transforms computed by sampling all animations
|
|
|
|
// (1st stage just above), and outputs the result to the local space
|
|
|
|
// transform buffer blended_locals_
|
|
|
|
|
|
|
|
// Prepares blending layers.
|
|
|
|
ozz::animation::BlendingJob::Layer layers[kNumLayers];
|
|
|
|
for (int i = 0; i < kNumLayers; ++i) {
|
|
|
|
layers[i].transform = make_span(samplers_[i].locals);
|
|
|
|
layers[i].weight = samplers_[i].weight;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Setups blending job.
|
|
|
|
ozz::animation::BlendingJob blend_job;
|
|
|
|
blend_job.threshold = threshold_;
|
|
|
|
blend_job.layers = layers;
|
2023-03-26 11:44:29 +02:00
|
|
|
blend_job.rest_pose = skeleton_.joint_rest_poses();
|
2021-11-11 21:22:24 +01:00
|
|
|
blend_job.output = make_span(blended_locals_);
|
|
|
|
|
|
|
|
// Blends.
|
|
|
|
if (!blend_job.Run()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Converts from local space to model space matrices.
|
|
|
|
// Gets the output of the blending stage, and converts it to model space.
|
|
|
|
|
|
|
|
// Setup local-to-model conversion job.
|
|
|
|
ozz::animation::LocalToModelJob ltm_job;
|
|
|
|
ltm_job.skeleton = &skeleton_;
|
|
|
|
ltm_job.input = make_span(blended_locals_);
|
|
|
|
ltm_job.output = make_span(models_);
|
|
|
|
|
|
|
|
// Runs ltm job.
|
|
|
|
if (!ltm_job.Run()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Computes blending weight and synchronizes playback speed when the "manual"
|
|
|
|
// option is off.
|
|
|
|
void UpdateRuntimeParameters() {
|
|
|
|
// Computes weight parameters for all samplers.
|
|
|
|
const float kNumIntervals = kNumLayers - 1;
|
|
|
|
const float kInterval = 1.f / kNumIntervals;
|
|
|
|
for (int i = 0; i < kNumLayers; ++i) {
|
|
|
|
const float med = i * kInterval;
|
|
|
|
const float x = blend_ratio_ - med;
|
|
|
|
const float y = ((x < 0.f ? x : -x) + kInterval) * kNumIntervals;
|
|
|
|
samplers_[i].weight = ozz::math::Max(0.f, y);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Synchronizes animations.
|
|
|
|
// First computes loop cycle duration. Selects the 2 samplers that define
|
|
|
|
// interval that contains blend_ratio_.
|
|
|
|
// Uses a maximum value smaller that 1.f (-epsilon) to ensure that
|
|
|
|
// (relevant_sampler + 1) is always valid.
|
|
|
|
const int relevant_sampler =
|
|
|
|
static_cast<int>((blend_ratio_ - 1e-3f) * (kNumLayers - 1));
|
|
|
|
assert(relevant_sampler + 1 < kNumLayers);
|
|
|
|
Sampler& sampler_l = samplers_[relevant_sampler];
|
|
|
|
Sampler& sampler_r = samplers_[relevant_sampler + 1];
|
|
|
|
|
|
|
|
// Interpolates animation durations using their respective weights, to
|
|
|
|
// find the loop cycle duration that matches blend_ratio_.
|
|
|
|
const float loop_duration =
|
|
|
|
sampler_l.animation.duration() * sampler_l.weight +
|
|
|
|
sampler_r.animation.duration() * sampler_r.weight;
|
|
|
|
|
|
|
|
// Finally finds the speed coefficient for all samplers.
|
|
|
|
const float inv_loop_duration = 1.f / loop_duration;
|
|
|
|
for (int i = 0; i < kNumLayers; ++i) {
|
|
|
|
Sampler& sampler = samplers_[i];
|
|
|
|
const float speed = sampler.animation.duration() * inv_loop_duration;
|
|
|
|
sampler.controller.set_playback_speed(speed);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Samples animation, transforms to model space and renders.
|
|
|
|
virtual bool OnDisplay(ozz::sample::Renderer* _renderer) {
|
|
|
|
return _renderer->DrawPosture(skeleton_, make_span(models_),
|
|
|
|
ozz::math::Float4x4::identity());
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual bool OnInitialize() {
|
|
|
|
// Reading skeleton.
|
|
|
|
if (!ozz::sample::LoadSkeleton(OPTIONS_skeleton, &skeleton_)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const int num_joints = skeleton_.num_joints();
|
|
|
|
const int num_soa_joints = skeleton_.num_soa_joints();
|
|
|
|
|
|
|
|
// Reading animations.
|
|
|
|
const char* filenames[] = {OPTIONS_animation1, OPTIONS_animation2,
|
|
|
|
OPTIONS_animation3};
|
|
|
|
static_assert(OZZ_ARRAY_SIZE(filenames) == kNumLayers, "Arrays mistmatch.");
|
|
|
|
for (int i = 0; i < kNumLayers; ++i) {
|
|
|
|
Sampler& sampler = samplers_[i];
|
|
|
|
|
|
|
|
if (!ozz::sample::LoadAnimation(filenames[i], &sampler.animation)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Allocates sampler runtime buffers.
|
|
|
|
sampler.locals.resize(num_soa_joints);
|
|
|
|
|
2023-03-26 11:44:29 +02:00
|
|
|
// Allocates a context that matches animation requirements.
|
|
|
|
sampler.context.Resize(num_joints);
|
2021-11-11 21:22:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Allocates local space runtime buffers of blended data.
|
|
|
|
blended_locals_.resize(num_soa_joints);
|
|
|
|
|
|
|
|
// Allocates model space runtime buffers of blended data.
|
|
|
|
models_.resize(num_joints);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void OnDestroy() {}
|
|
|
|
|
|
|
|
virtual bool OnGui(ozz::sample::ImGui* _im_gui) {
|
|
|
|
// Exposes blending parameters.
|
|
|
|
{
|
|
|
|
static bool open = true;
|
|
|
|
ozz::sample::ImGui::OpenClose oc(_im_gui, "Blending parameters", &open);
|
|
|
|
if (open) {
|
|
|
|
if (_im_gui->DoCheckBox("Manual settings", &manual_) && !manual_) {
|
|
|
|
// Check-box state was changed, reset parameters.
|
|
|
|
for (int i = 0; i < kNumLayers; ++i) {
|
|
|
|
Sampler& sampler = samplers_[i];
|
|
|
|
sampler.controller.Reset();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
char label[64];
|
2024-03-17 12:47:11 +01:00
|
|
|
std::snprintf(label, sizeof(label), "Blend ratio: %.2f", blend_ratio_);
|
2021-11-11 21:22:24 +01:00
|
|
|
_im_gui->DoSlider(label, 0.f, 1.f, &blend_ratio_, 1.f, !manual_);
|
|
|
|
|
|
|
|
for (int i = 0; i < kNumLayers; ++i) {
|
|
|
|
Sampler& sampler = samplers_[i];
|
2024-03-17 12:47:11 +01:00
|
|
|
std::snprintf(label, sizeof(label), "Weight %d: %.2f", i, sampler.weight);
|
2021-11-11 21:22:24 +01:00
|
|
|
_im_gui->DoSlider(label, 0.f, 1.f, &sampler.weight, 1.f, manual_);
|
|
|
|
}
|
|
|
|
|
2024-03-17 12:47:11 +01:00
|
|
|
std::snprintf(label, sizeof(label), "Threshold: %.2f", threshold_);
|
2021-11-11 21:22:24 +01:00
|
|
|
_im_gui->DoSlider(label, .01f, 1.f, &threshold_);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Exposes animations runtime playback controls.
|
|
|
|
{
|
|
|
|
static bool oc_open = true;
|
|
|
|
ozz::sample::ImGui::OpenClose oc(_im_gui, "Animation control", &oc_open);
|
|
|
|
if (oc_open) {
|
|
|
|
static bool open[] = {true, true, true};
|
|
|
|
static_assert(OZZ_ARRAY_SIZE(open) == kNumLayers,
|
|
|
|
"Arrays size mismatch");
|
|
|
|
const char* oc_names[] = {"Animation 1", "Animation 2", "Animation 3"};
|
|
|
|
static_assert(OZZ_ARRAY_SIZE(oc_names) == kNumLayers,
|
|
|
|
"Arrays size mismatch");
|
|
|
|
for (int i = 0; i < kNumLayers; ++i) {
|
|
|
|
Sampler& sampler = samplers_[i];
|
|
|
|
ozz::sample::ImGui::OpenClose loc(_im_gui, oc_names[i], nullptr);
|
|
|
|
if (open[i]) {
|
|
|
|
sampler.controller.OnGui(sampler.animation, _im_gui, manual_);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void GetSceneBounds(ozz::math::Box* _bound) const {
|
|
|
|
ozz::sample::ComputePostureBounds(make_span(models_), _bound);
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
// Runtime skeleton.
|
|
|
|
ozz::animation::Skeleton skeleton_;
|
|
|
|
|
|
|
|
// Global blend ratio in range [0,1] that controls all blend parameters and
|
|
|
|
// synchronizes playback speeds. A value of 0 gives full weight to the first
|
|
|
|
// animation, and 1 to the last.
|
|
|
|
float blend_ratio_;
|
|
|
|
|
|
|
|
// Switch to manual control of animations and blending parameters.
|
|
|
|
bool manual_;
|
|
|
|
|
|
|
|
// The number of layers to blend.
|
|
|
|
enum {
|
|
|
|
kNumLayers = 3,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Sampler structure contains all the data required to sample a single
|
|
|
|
// animation.
|
|
|
|
struct Sampler {
|
|
|
|
// Constructor, default initialization.
|
|
|
|
Sampler() : weight(1.f) {}
|
|
|
|
|
|
|
|
// Playback animation controller. This is a utility class that helps with
|
|
|
|
// controlling animation playback time.
|
|
|
|
ozz::sample::PlaybackController controller;
|
|
|
|
|
|
|
|
// Blending weight for the layer.
|
|
|
|
float weight;
|
|
|
|
|
|
|
|
// Runtime animation.
|
|
|
|
ozz::animation::Animation animation;
|
|
|
|
|
2023-03-26 11:44:29 +02:00
|
|
|
// Sampling context.
|
|
|
|
ozz::animation::SamplingJob::Context context;
|
2021-11-11 21:22:24 +01:00
|
|
|
|
|
|
|
// Buffer of local transforms as sampled from animation_.
|
|
|
|
ozz::vector<ozz::math::SoaTransform> locals;
|
|
|
|
} samplers_[kNumLayers]; // kNumLayers animations to blend.
|
|
|
|
|
2023-03-26 11:44:29 +02:00
|
|
|
// Blending job rest pose threshold.
|
2021-11-11 21:22:24 +01:00
|
|
|
float threshold_;
|
|
|
|
|
|
|
|
// Buffer of local transforms which stores the blending result.
|
|
|
|
ozz::vector<ozz::math::SoaTransform> blended_locals_;
|
|
|
|
|
|
|
|
// Buffer of model space matrices. These are computed by the local-to-model
|
|
|
|
// job after the blending stage.
|
|
|
|
ozz::vector<ozz::math::Float4x4> models_;
|
|
|
|
};
|
|
|
|
|
|
|
|
int main(int _argc, const char** _argv) {
|
|
|
|
const char* title = "Ozz-animation sample: Animation blending";
|
|
|
|
return BlendSampleApplication().Run(_argc, _argv, "1.2", title);
|
|
|
|
}
|