Unravel Engine C++ Reference
Loading...
Searching...
No Matches
animation_player.cpp
Go to the documentation of this file.
1#include "animation_player.h"
2#include <hpp/utility/overload.hpp>
3namespace unravel
4{
5
6namespace
7{
15template<typename T>
16auto interpolate(const std::vector<animation_channel::key<T>>& keys, animation_player::seconds_t time) -> T
17{
18 if(keys.empty())
19 {
20 return {}; // Return default value if there are no keys
21 }
22
23 // Do binary search for keyframe
24 int high = (int)keys.size(), low = -1;
25 while(high - low > 1)
26 {
27 int probe = (high + low) / 2;
28 if(keys[probe].time < time)
29 {
30 low = probe;
31 }
32 else
33 {
34 high = probe;
35 }
36 }
37
38 if(low == -1)
39 {
40 // Before first key, return first key
41 return keys.front().value;
42 }
43
44 if(high == (int)keys.size())
45 {
46 // Beyond last key, return last key
47 return keys.back().value;
48 }
49
50 const auto& key1 = keys[low];
51 const auto& key2 = keys[low + 1];
52
53 // Compute the interpolation factor (0.0 to 1.0)
54 float factor = (time.count() - key1.time.count()) / (key2.time.count() - key1.time.count());
55
56 // Perform the interpolation
57 if constexpr(std::is_same_v<T, math::vec3>)
58 {
59 return math::lerp(key1.value, key2.value, factor);
60 }
61 else if constexpr(std::is_same_v<T, math::quat>)
62 {
63 return math::slerp(key1.value, key2.value, factor);
64 }
65
66 return {};
67}
68
69} // namespace
70auto animation_player::get_layer(size_t index) -> animation_layer&
71{
72 if(index >= layers_.size())
73 {
74 layers_.resize(index + 1);
75 }
76 return layers_[index];
77}
78void animation_player::clear(size_t layer_idx)
79{
80 auto& layer = get_layer(layer_idx);
81 layer = {};
82}
83
84void animation_player::blend_to(size_t layer_idx,
86 seconds_t duration,
87 bool loop,
88 bool phase_sync,
89 const blend_easing_t& easing)
90{
91 auto& layer = get_layer(layer_idx);
92 if(!clip)
93 {
94 if(layer.current_state.state.clip)
95 {
96 layer.current_state = {};
97 }
98 return;
99 }
100
101 // Already blending TO this clip - nothing to do (but keep loop flag fresh).
102 if(layer.target_state.state.clip == clip)
103 {
104 layer.target_state.state.loop = loop;
105 return;
106 }
107
108 // Caller wants to go back to the clip we're currently blending AWAY from.
109 if(layer.current_state.state.clip == clip)
110 {
111 // No active crossfade? Already fully on this clip, nothing to do.
112 if(!layer.target_state.state.clip)
113 {
114 layer.current_state.state.loop = loop;
115 return;
116 }
117
118 // Active crossfade - reverse direction. Swap current<->target so we
119 // are now blending BACK toward the caller's requested clip, and
120 // flip the blend progress so the visible pose stays continuous.
121 std::swap(layer.current_state, layer.target_state);
122 layer.current_state.state.loop = loop;
123
124 if(auto* bot = std::get_if<blend_over_time>(&layer.blending_state.state))
125 {
126 float progress_before = 0.0f;
127 if(bot->duration.count() > 0.0f)
128 {
129 progress_before = float(bot->elapsed.count() / bot->duration.count());
130 progress_before = std::clamp(progress_before, 0.0f, 1.0f);
131 }
132
133 if(progress_before > 1e-4f)
134 {
135 // Stretch `duration` so remaining wall-clock time == caller's
136 // requested duration, while starting at mirrored progress
137 // (1 - progress_before) for visual continuity.
138 auto stretched = seconds_t(duration.count() / progress_before);
139 bot->duration = stretched;
140 bot->elapsed = seconds_t(stretched.count() * (1.0f - progress_before));
141 }
142 else
143 {
144 bot->duration = duration;
145 bot->elapsed = seconds_t(0);
146 }
147 }
148 else
149 {
150 // Shouldn't happen given target was set, but be safe.
151 layer.blending_state.state = blend_over_time{duration};
152 }
153 layer.blending_state.easing = easing;
154 return;
155 }
156
157 // Fresh blend: start a new crossfade from current to the requested clip.
158 layer.target_state.state.clip = clip;
159 layer.target_state.state.loop = loop;
160
161 auto phase = phase_sync ? layer.current_state.state.get_progress() : 0.0f;
162 layer.target_state.state.set_progress(phase);
163
164 if(duration > clip.get()->duration)
165 {
166 duration = clip.get()->duration;
167 }
168
169 layer.blending_state.state = blend_over_time{duration};
170 layer.blending_state.easing = easing;
171}
172
173void animation_player::set_blend_space(size_t layer_idx, const std::shared_ptr<blend_space_def>& blend_space, bool loop)
174{
175 auto& layer = get_layer(layer_idx);
176
177 layer.current_state.state.loop = loop;
178
179 if(layer.current_state.state.blend_space == blend_space)
180 {
181 return;
182 }
183
184 layer.current_state.state.blend_space = blend_space;
185 layer.current_state.state.elapsed = seconds_t(0);
186 // Clear target state if any
187 layer.target_state = {};
188 layer.blending_state = {};
189}
190
191void animation_player::set_blend_space_parameters(size_t layer_idx, const std::vector<float>& params)
192{
193 auto& layer = get_layer(layer_idx);
194 layer.current_state.parameters = params;
195}
196
198{
199 if(playing_)
200 {
201 return false;
202 }
203 playing_ = true;
204 paused_ = false;
205
206 return true;
207}
208
210{
211 paused_ = true;
212}
213
215{
216 paused_ = false;
217}
218
220{
221 playing_ = false;
222 paused_ = false;
223
224 for(auto& layer : layers_)
225 {
226 layer.current_state.state.elapsed = seconds_t(0);
227 layer.target_state.state.elapsed = seconds_t(0);
228 }
229}
230
231auto animation_player::update_time(seconds_t delta_time, bool force) -> bool
232{
233 if(!is_playing())
234 {
235 return false;
236 }
237
238 bool any_valid = false;
239 for(auto& layer : layers_)
240 {
241 any_valid |= layer.current_state.is_valid();
242 any_valid |= layer.target_state.is_valid();
243
244 if(any_valid)
245 {
246 break;
247 }
248 }
249
250 if(!any_valid)
251 {
252 return false;
253 }
254
255 // Update times
256 if(playing_ && !paused_)
257 {
258 for(auto& layer : layers_)
259 {
260 update_state(delta_time, layer.current_state.state);
261
262 update_state(delta_time, layer.target_state.state);
263
264 // update overtime parameters
265 hpp::visit(hpp::overload(
266 [&](blend_over_time& state)
267 {
268 state.elapsed += delta_time;
269 },
270 [](auto& state)
271 {
272
273 }),
274 layer.blending_state.state);
275 }
276
277 return true;
278 }
279 return false;
280}
281
282void animation_player::update_poses(const animation_pose& ref_pose, animation_retargeting_mode retargeting_mode, const update_callback_t& set_transform_callback)
283{
284 if(layers_.empty())
285 {
286 return;
287 }
288
289 for(auto& layer : layers_)
290 {
291 // Update current layer
292 update_pose(layer.current_state, retargeting_mode);
293
294 // Update target layer
295 if(update_pose(layer.target_state, retargeting_mode))
296 {
297 // Compute blend factor
298 float blend_progress = get_blend_progress(layer);
299 float blend_factor = compute_blend_factor(layer, blend_progress);
300
301 // Blend poses
302 blend_poses(layer.current_state.pose, layer.target_state.pose, blend_factor, layer.blend_pose);
303
304 // Check if blending is finished
305 if(blend_progress >= 1.0f)
306 {
307 // Switch to target animation or blend space
308 layer.current_state = layer.target_state;
309 layer.target_state = {};
310 layer.blending_state = {};
311 }
312 }
313 }
314
315 if(layers_.size() == 1)
316 {
317 auto final_pose = layers_.front().get_final_pose();
318
319 // Apply the final pose using the callback
320 for(const auto& node : final_pose->nodes)
321 {
322 set_transform_callback(node.desc, node.transform, final_pose->motion_result);
323 }
324 }
325 else
326 {
327 animation_pose final_pose{};
328 blend_poses_additive(*layers_[0].get_final_pose(), *layers_[1].get_final_pose(), ref_pose, 1.0f, final_pose);
329
330 for(size_t i = 2; i < layers_.size(); ++i)
331 {
332 animation_pose next_final_pose{};
333 blend_poses_additive(final_pose, *layers_[i].get_final_pose(), ref_pose, 1.0f, next_final_pose);
334
335 final_pose = next_final_pose;
336 }
337
338 // Apply the final pose using the callback
339 for(const auto& node : final_pose.nodes)
340 {
341 set_transform_callback(node.desc, node.transform, final_pose.motion_result);
342 }
343 }
344}
345
346auto animation_player::update_pose(animation_layer_state& layer, animation_retargeting_mode retargeting_mode) -> bool
347{
348 auto& state = layer.state;
349 auto& pose = layer.pose;
350 auto& parameters = layer.parameters;
351
352 if(state.blend_space)
353 {
354 // Compute blending weights based on current parameters (e.g., speed and direction)
355 state.blend_space->compute_blend(parameters, state.blend_clips);
356
357 // Sample animations and blend poses
358 state.blend_poses.resize(state.blend_clips.size());
359 for(size_t i = 0; i < state.blend_clips.size(); ++i)
360 {
361 const auto& clip_weight_pair = state.blend_clips[i];
362 sample_animation(clip_weight_pair.first.get().get(), state.elapsed, retargeting_mode, state.blend_poses[i]);
363 }
364
365 // Blend all poses based on their weights
366 pose.nodes.clear();
367 if(!state.blend_poses.empty())
368 {
369 // Initialize with the first pose
370 pose = state.blend_poses[0];
371 float total_weight = state.blend_clips[0].second;
372
373 for(size_t i = 1; i < state.blend_poses.size(); ++i)
374 {
376 state.blend_poses[i],
377 state.blend_clips[i].second / (total_weight + state.blend_clips[i].second),
378 pose);
379 total_weight += state.blend_clips[i].second;
380 }
381 }
382 return true;
383 }
384
385 if(state.clip)
386 {
387 sample_animation(state.clip.get().get(), state.elapsed, retargeting_mode, pose);
388 return true;
389 }
390
391 return false;
392}
393
394void animation_player::update_state(seconds_t delta_time, animation_state& state)
395{
396 if(state.clip)
397 {
398 state.elapsed += delta_time;
399 auto target_anim = state.clip.get();
400 if(target_anim)
401 {
402 if(state.elapsed > target_anim->duration)
403 {
404 if(state.loop)
405 {
406 state.elapsed = seconds_t(std::fmod(state.elapsed.count(), target_anim->duration.count()));
407 }
408 else
409 {
410 state.elapsed = target_anim->duration;
411 }
412 }
413 }
414
415 }
416}
417
418auto animation_player::get_blend_progress(const animation_layer& layer) const -> float
419{
420 return hpp::visit(hpp::overload(
421 [](const hpp::monostate& state)
422 {
423 return 0.0f;
424 },
425 [](const auto& state)
426 {
427 return state.get_progress();
428 }),
429 layer.blending_state.state);
430}
431
432auto animation_player::compute_blend_factor(const animation_layer& layer, float normalized_blend_time) noexcept -> float
433{
434 float blend_factor = 0.0f;
435
436 // Apply the easing function
437 blend_factor = layer.blending_state.easing(normalized_blend_time);
438
439 // Check if blending is complete
440 if(normalized_blend_time >= 1.0f)
441 {
442 // Blending complete
443 blend_factor = 1.0f;
444 }
445
446 return blend_factor;
447}
448
449void animation_player::sample_animation(const animation_clip* anim_clip,
450 seconds_t time,
451 animation_retargeting_mode retargeting_mode,
452 animation_pose& pose) const noexcept
453{
454 if(!anim_clip)
455 {
456 return;
457 }
458 pose.nodes.clear();
459 pose.nodes.reserve(anim_clip->channels.size());
460
461 for(const auto& channel : anim_clip->channels)
462 {
463 math::vec3 position = interpolate(channel.position_keys, time);
464 math::quat rotation = interpolate(channel.rotation_keys, time);
465 math::vec3 scaling = interpolate(channel.scaling_keys, time);
466
467 auto& node = pose.nodes.emplace_back();
468
469 // Store both index and name - callback will resolve based on retargeting mode
470 node.desc.index = channel.node_index;
471 node.desc.name = channel.node_name;
472 node.transform.set_position(position);
473 node.transform.set_rotation(rotation);
474 node.transform.set_scale(scaling);
475
476
477 // Root motion comparison based on retargeting mode
478 bool is_root_position_node = false;
479 bool is_root_rotation_node = false;
480
481 if(retargeting_mode == animation_retargeting_mode::name_based)
482 {
483 // Name-based comparison
484 if(node.desc.name == anim_clip->root_motion.position_node_name)
485 {
486 is_root_position_node = true;
487 }
488 if(node.desc.name == anim_clip->root_motion.rotation_node_name)
489 {
490 is_root_rotation_node = true;
491 }
492 }
493 else
494 {
495 // Index-based comparison (default)
496 if(int(node.desc.index) == anim_clip->root_motion.position_node_index)
497 {
498 is_root_position_node = true;
499 }
500 if(int(node.desc.index) == anim_clip->root_motion.rotation_node_index)
501 {
502 is_root_rotation_node = true;
503 }
504 }
505
506 if(is_root_position_node)
507 {
508 pose.motion_result.root_position_node_index = anim_clip->root_motion.position_node_index;
509 pose.motion_result.root_position_node_name = anim_clip->root_motion.position_node_name;
510
511 const auto& clip_start_pos = channel.position_keys.front().value;
512 const auto& clip_end_pos = channel.position_keys.back().value;
513
514 pose.motion_result.root_position_weights = {1.0f, 1.0f, 1.0f};
515 pose.motion_result.bone_position_weights = {0.0f, 0.0f, 0.0f};
516
517 if(pose.motion_state.root_position_time == seconds_t(0))
518 {
519 pose.motion_state.root_position_time = time;
520 pose.motion_state.root_position_at_time = clip_start_pos;
521 }
522
523 auto delta_position = position - pose.motion_state.root_position_at_time;
524
525 if(time < pose.motion_state.root_position_time)
526 {
527 auto loop_pos_offset = clip_end_pos - clip_start_pos;
528 delta_position = delta_position + loop_pos_offset;
529 }
530
531
532
533 if(anim_clip->root_motion.keep_position_y)
534 {
535 pose.motion_result.root_position_weights.y = 0.0f;
536 pose.motion_result.bone_position_weights.y = 1.0f;
537
538 }
539
540 if(anim_clip->root_motion.keep_position_xz)
541 {
542 pose.motion_result.root_position_weights.x = 0.0f;
543 pose.motion_result.root_position_weights.z = 0.0f;
544
545 pose.motion_result.bone_position_weights.x = 1.0f;
546 pose.motion_result.bone_position_weights.z = 1.0f;
547 }
548
549 if(anim_clip->root_motion.keep_in_place)
550 {
551 pose.motion_result.root_position_weights.y = 0.0f;
552 pose.motion_result.root_position_weights.x = 0.0f;
553 pose.motion_result.root_position_weights.z = 0.0f;
554
555 pose.motion_result.bone_position_weights.y = 1.0f;
556 pose.motion_result.bone_position_weights.x = 0.0f;
557 pose.motion_result.bone_position_weights.z = 0.0f;
558 }
559
560 pose.motion_state.root_position_time = time;
561 pose.motion_state.root_position_at_time = position;
562 pose.motion_result.root_transform_delta.set_position(delta_position);
563 }
564
565 if(is_root_rotation_node)
566 {
567 pose.motion_result.root_rotation_node_index = anim_clip->root_motion.rotation_node_index;
568 pose.motion_result.root_rotation_node_name = anim_clip->root_motion.rotation_node_name;
569
570 const auto& clip_start_rotation = channel.rotation_keys.front().value;
571 const auto& clip_end_rotation = channel.rotation_keys.back().value;
572
573 pose.motion_result.root_rotation_weight = {1.0f};
574 pose.motion_result.bone_rotation_weight = {0.0f};
575
576 if(pose.motion_state.root_rotation_time == seconds_t(0))
577 {
578 pose.motion_state.root_rotation_time = time;
579
580 pose.motion_state.root_rotation_at_time = clip_start_rotation;
581 }
582
583 auto delta_rotation = rotation * glm::inverse(pose.motion_state.root_rotation_at_time);
584
585 if(time < pose.motion_state.root_rotation_time)
586 {
587 auto loop_rotation_offset = clip_end_rotation * glm::inverse(clip_start_rotation);
588 delta_rotation = loop_rotation_offset * delta_rotation;
589 }
590
591 if(anim_clip->root_motion.keep_rotation)
592 {
593 pose.motion_result.root_rotation_weight = 0.0f;
594 pose.motion_result.bone_rotation_weight = 1.0f;
595
596 }
597
598 if(anim_clip->root_motion.keep_in_place)
599 {
600 pose.motion_result.root_rotation_weight = 0.0f;
601 pose.motion_result.bone_rotation_weight = 1.0f;
602 }
603
604 pose.motion_state.root_rotation_time = time;
605 pose.motion_state.root_rotation_at_time = rotation;
606 pose.motion_result.root_transform_delta.set_rotation(delta_rotation);
607 }
608 }
609}
610
612{
613 return playing_ && !paused_;
614}
615
616auto animation_player::is_paused() const -> bool
617{
618 return paused_;
619}
620
621} // namespace unravel
auto play() -> bool
Starts or resumes the animation playback.
void pause()
Pauses the animation playback.
animation_clip::seconds_t seconds_t
void clear(size_t layer_idx)
void set_blend_space(size_t layer_idx, const std::shared_ptr< blend_space_def > &blend_space, bool loop=true)
void stop()
Stops the animation playback and resets the time.
std::function< void(const animation_pose::node_desc &desc, const math::transform &abs, const animation_pose::root_motion_result &motion_result)> update_callback_t
auto is_playing() const -> bool
Returns whether the animation is currently playing.
void blend_to(size_t layer_idx, const asset_handle< animation_clip > &clip, seconds_t duration=seconds_t(0.3), bool loop=true, bool phase_sync=false, const blend_easing_t &easing=math::linearInterpolation< float >)
Blends to the animation over the specified time with the specified easing.
void set_blend_space_parameters(size_t layer_idx, const std::vector< float > &params)
void resume()
Resumes the animation playback.
auto update_time(seconds_t delta_time, bool force=false) -> bool
Updates the animation player, advancing the animation time and applying transformations.
void update_poses(const animation_pose &ref_pose, animation_retargeting_mode retargeting_mode, const update_callback_t &set_transform_callback)
auto is_paused() const -> bool
Returns whether the animation is currently paused.
math::vec3 position
Definition defaults.cpp:52
uint16_t index
transform_snapshot pose
phase_t phase
std::function< float(float)> blend_easing_t
void blend_poses(const animation_pose &pose1, const animation_pose &pose2, float factor, animation_pose &result_pose)
animation_retargeting_mode
Animation retargeting mode for bone matching.
@ name_based
Flexible, works with different skeletons (uses cached hash map)
void blend_poses_additive(const animation_pose &base, const animation_pose &additive, const animation_pose &ref_pose, float weight, animation_pose &result)
std::vector< math::quat > rotation
Thread-safe handle to an asset.
auto get(bool wait=true) const -> std::shared_ptr< T >
Gets the shared pointer to the asset.
animation_clip::seconds_t elapsed