Unravel Engine C++ Reference
Loading...
Searching...
No Matches
animation_blend_space.cpp
Go to the documentation of this file.
1#include "animation_player.h"
2#include <hpp/utility/overload.hpp>
3namespace unravel
4{
5
6// Computes an additive blend between a base and an additive transform,
7// using a reference transform. The additive transform is assumed to be authored
8// relative to the reference pose. The result is:
9// result = base + weight * (additive - ref)
10// For rotations, we compute the delta rotation and then slerp from identity.
12 const math::transform& additive,
13 const math::transform& ref,
14 float weight) -> math::transform
15{
16 math::transform result;
17 // Translation: base + weight*(additive - ref)
18 result.set_translation(base.get_translation() + weight * (additive.get_translation() - ref.get_translation()));
19
20 // Rotation: Compute delta = additive.rotation * inverse(ref.rotation)
21 math::quat additive_delta = additive.get_rotation() * glm::inverse(ref.get_rotation());
22 // Interpolate from identity to the delta
23 math::quat weighted_delta = math::slerp(math::identity<math::quat>(), additive_delta, weight);
24 // Apply the weighted delta to the base rotation
25 result.set_rotation(math::normalize(weighted_delta * base.get_rotation()));
26
27 // Scale: base + weight*(additive - ref)
28 result.set_scale(base.get_scale() + weight * (additive.get_scale() - ref.get_scale()));
29
30 return result;
31}
32
45 const animation_pose& additive,
46 const animation_pose& ref_pose,
47 float weight,
48 animation_pose& result)
49{
50 result.nodes.clear();
51 // Reserve based on the ref pose since it is the most complete.
52 result.nodes.reserve(ref_pose.nodes.size());
53
54 // Blend the root transform delta using additive blending.
58 weight);
59 // For weights, you might choose to leave them as-is or blend them differently.
62
65
68
69 // We'll use indices to iterate through base and additive poses.
70 size_t i_base = 0;
71 size_t i_add = 0;
72
73 // Iterate over each node in the reference pose.
74 for(const auto& ref_node : ref_pose.nodes)
75 {
76 animation_pose::node blended_node;
77 blended_node.desc = ref_node.desc;
78
79 // Default to the ref node's transform if no corresponding node is found.
80 math::transform base_transform = ref_node.transform;
81 math::transform additive_transform = ref_node.transform;
82
83 // Advance the base index until we find a node with an index >= ref_node.desc.index.
84 while(i_base < base.nodes.size() && base.nodes[i_base].desc.index < ref_node.desc.index)
85 {
86 ++i_base;
87 }
88 // If we found an exact match in the base pose, use its transform.
89 if(i_base < base.nodes.size() && base.nodes[i_base].desc.index == ref_node.desc.index)
90 {
91 base_transform = base.nodes[i_base].transform;
92 }
93
94 // Do the same for the additive pose.
95 while(i_add < additive.nodes.size() && additive.nodes[i_add].desc.index < ref_node.desc.index)
96 {
97 ++i_add;
98 }
99 if(i_add < additive.nodes.size() && additive.nodes[i_add].desc.index == ref_node.desc.index)
100 {
101 additive_transform = additive.nodes[i_add].transform;
102 }
103
104 // Blend additively:
105 // The idea is that the additive animation was authored as an offset relative to the reference pose.
106 // So the delta is (additive_transform - ref_node.transform) and we add that (scaled by weight) onto base.
107 blended_node.transform = blend_additive(base_transform, additive_transform, ref_node.transform, weight);
108
109 result.nodes.push_back(blended_node);
110 }
111}
112
114 const animation_pose& additive,
115 const animation_pose& ref_pose,
116 float weight,
117 animation_pose& result)
118{
119 blend_poses_by_node_index_sorted_additive(base, additive, ref_pose, weight, result);
120}
121
122auto blend(const math::transform& lhs, const math::transform& rhs, float factor) -> math::transform
123{
124 math::transform result;
125 result.set_translation(math::lerp(lhs.get_translation(), rhs.get_translation(), factor));
126 result.set_rotation(math::slerp(lhs.get_rotation(), rhs.get_rotation(), factor));
127 result.set_scale(math::lerp(lhs.get_scale(), rhs.get_scale(), factor));
128 return result;
129}
130
133{
135 result.root_transform_delta = blend(r1.root_transform_delta, r2.root_transform_delta, factor);
136 result.root_position_weights = r1.root_position_weights * r2.root_position_weights;
137 result.bone_position_weights = r1.bone_position_weights * r2.bone_position_weights;
138
139 result.root_rotation_weight = r1.root_rotation_weight * r2.root_rotation_weight;
140 result.bone_rotation_weight = r1.bone_rotation_weight * r2.bone_rotation_weight;
141
142 if(r1.root_position_node_index == -1)
143 {
144 result.root_position_node_index = r2.root_position_node_index;
145 }
146 else if(r2.root_position_node_index == -1)
147 {
148 result.root_position_node_index = r1.root_position_node_index;
149 }
150 else
151 {
152 result.root_position_node_index = factor < 0.5f ? r1.root_position_node_index : r2.root_position_node_index;
153 }
154
155 if(r1.root_rotation_node_index == -1)
156 {
157 result.root_rotation_node_index = r2.root_rotation_node_index;
158 }
159 else if(r2.root_rotation_node_index == -1)
160 {
161 result.root_rotation_node_index = r1.root_rotation_node_index;
162 }
163 else
164 {
165 result.root_rotation_node_index = factor < 0.5f ? r1.root_rotation_node_index : r2.root_rotation_node_index;
166 }
167 return result;
168}
169
176 const animation_pose& pose2,
177 float factor,
178 animation_pose& result)
179{
180 result.nodes.clear();
181 result.nodes.reserve(pose1.nodes.size() + pose2.nodes.size());
182
183 size_t i1 = 0;
184 size_t i2 = 0;
185
186 result.motion_result = blend(pose1.motion_result, pose2.motion_result, factor);
187
188 while(i1 < pose1.nodes.size() && i2 < pose2.nodes.size())
189 {
190 const auto& node1 = pose1.nodes[i1];
191 const auto& node2 = pose2.nodes[i2];
192
193 if(node1.desc.index < node2.desc.index)
194 {
195 // node1 is "missing" in pose2, so copy node1
196 result.nodes.push_back(node1);
197 i1++;
198 }
199 else if(node1.desc.index > node2.desc.index)
200 {
201 // node2 is "missing" in pose1, so copy node2
202 result.nodes.push_back(node2);
203 i2++;
204 }
205 else
206 {
207 auto& node = result.nodes.emplace_back();
208 // node1.index == node2.index -> blend
209 node.desc = node1.desc;
210 node.transform = blend(node1.transform, node2.transform, factor);
211
212 i1++;
213 i2++;
214 }
215 }
216
217 // Copy the remaining nodes in pose1
218 while(i1 < pose1.nodes.size())
219 {
220 result.nodes.push_back(pose1.nodes[i1]);
221 i1++;
222 }
223
224 // Copy the remaining nodes in pose2
225 while(i2 < pose2.nodes.size())
226 {
227 result.nodes.push_back(pose2.nodes[i2]);
228 i2++;
229 }
230}
231
232void blend_poses(const animation_pose& pose1, const animation_pose& pose2, float factor, animation_pose& result_pose)
233{
234 blend_poses_by_node_index_sorted(pose1, pose2, factor, result_pose);
235}
236
237void blend_poses_by_node_index_sorted_multiway(const std::vector<animation_pose>& poses,
238 const std::vector<float>& weights,
239 animation_pose& result)
240{
241 // 1) If there's only 1 pose, it's trivial
242 if(poses.size() == 1)
243 {
244 result = poses[0];
245 return;
246 }
247
248 // We'll assume each pose is sorted by node.index in ascending order
249 // We'll keep a pointer array "idx[]" for each pose
250 size_t k = poses.size();
251 std::vector<size_t> idx(k, 0);
252
253 result.nodes.clear();
254
255 // We'll do a loop while there's at least one pose not at end
256 while(true)
257 {
258 // 2) Among all poses that are not finished, find the smallest node.index
259 size_t min_index = (size_t)-1; // sentinel for "none"
260 bool all_finished = true;
261
262 // Collect all unique indices that appear for this iteration
263 // e.g. we might see multiple poses have the same current index
264 // or some might have bigger ones
265 for(size_t p = 0; p < k; ++p)
266 {
267 if(idx[p] < poses[p].nodes.size())
268 {
269 all_finished = false;
270 size_t node_index = poses[p].nodes[idx[p]].desc.index;
271 if(min_index == (size_t)-1 || node_index < min_index)
272 {
273 min_index = node_index;
274 }
275 }
276 }
277
278 // If all are finished, break
279 if(all_finished)
280 {
281 break;
282 }
283
284 // 3) Collect transforms from all poses that have this minIndex
285 float total_weight{0.0f};
286 math::transform accum_transform{}; // maybe identity or something
287 bool first_transform_set{false};
288
289 for(size_t p = 0; p < k; ++p)
290 {
291 if(idx[p] < poses[p].nodes.size())
292 {
293 const auto& node = poses[p].nodes[idx[p]];
294 if(node.desc.index == min_index)
295 {
296 // We want to blend node.transform into accumTransform
297 float w = weights[p];
298 if(!first_transform_set)
299 {
300 accum_transform = node.transform; // first
301 total_weight = w;
302 first_transform_set = true;
303
304 result.motion_result = poses[p].motion_result;
305 }
306 else
307 {
308 // Blend accumTransform w/ node.transform
309 float factor = w / (total_weight + w);
310 accum_transform = blend(accum_transform, node.transform, factor);
311 result.motion_result = blend(result.motion_result, poses[p].motion_result, factor);
312
313 total_weight += w;
314 }
315 // advance pointer in pose p
316 idx[p]++;
317 }
318 }
319 }
320
321 // 4) Push the final blended node into result
322 auto& out = result.nodes.emplace_back();
323 out.desc.index = min_index;
324 out.transform = accum_transform;
325 // name can be optional or from whichever pose you prefer
326 // out.name = ?
327 }
328
329 // Now result has the union of all node_index across all poses, blended by weight.
330}
331
332void blend_poses(const std::vector<animation_pose>& poses,
333 const std::vector<float>& weights,
334 animation_pose& result_pose)
335{
336 blend_poses_by_node_index_sorted_multiway(poses, weights, result_pose);
337}
338
340{
341 points_.emplace_back(blend_space_point{params, clip});
342 parameter_count_ = params.size(); // Ensure all points have the same number of parameters
343}
344
346 std::vector<std::pair<asset_handle<animation_clip>, float>>& out_clips) const
347{
348 // Clear the output vector
349 out_clips.clear();
350
351 if(parameter_count_ == 1)
352 {
353 // 1D linear interpolation
354 // (see example below)
355 compute_blend_1d(current_params, out_clips);
356 return;
357 }
358 if(parameter_count_ == 2)
359 {
360 compute_blend_2d(current_params, out_clips);
361 return;
362 }
363
364 // Implement 3D or N-D as needed
365}
366
367void blend_space_def::compute_blend_1d(const parameters_t& current_params,
368 std::vector<std::pair<asset_handle<animation_clip>, float>>& out_clips) const
369{
370 // current_params[0] is the single parameter (e.g., "speed")
371 float param = current_params[0];
372
373 // Gather all unique parameter values from points_
374 std::set<float> unique_values;
375 for(const auto& point : points_)
376 {
377 unique_values.insert(point.parameters[0]);
378 }
379
380 // Turn into a sorted vector
381 std::vector<float> sorted_values(unique_values.begin(), unique_values.end());
382
383 // If there's only 1 or 0 unique values, there's no blending, just use that clip
384 if(sorted_values.size() <= 1)
385 {
386 if(!points_.empty())
387 {
388 // Assume they're all the same param -> 100% weight on the first
389 out_clips.emplace_back(points_.front().clip, 1.0f);
390 }
391 return;
392 }
393
394 // 1) Find which interval param falls into
395 // e.g. if sortedValues = [0.0, 2.0, 5.0], and param = 1.5
396 // that’s between indices 0 and 1
397 auto find_index_1d = [&](float p)
398 {
399 for(size_t i = 0; i < sorted_values.size() - 1; ++i)
400 {
401 if(p >= sorted_values[i] && p <= sorted_values[i + 1])
402 return i;
403 }
404 // clamp if out of range
405 return sorted_values.size() - 2;
406 };
407
408 size_t idx = find_index_1d(param);
409 float v0 = sorted_values[idx];
410 float v1 = sorted_values[idx + 1];
411
412 // 2) Interpolation factor
413 float t = 0.0f;
414 if(fabs(v1 - v0) > 1e-5f) // avoid divide by zero
415 t = (param - v0) / (v1 - v0);
416
417 t = math::clamp(t, 0.0f, 1.0f);
418 // 3) Find the exact clip(s) that correspond to v0 and v1
419 // We’ll pick the *closest* clip for each param value (since we might have multiple points at the same param)
420 const blend_space_point* p0 = nullptr;
421 const blend_space_point* p1 = nullptr;
422
423 // We'll store whichever points match v0 and v1
424 // (If you had multiple clips at the same param, you'd either pick one or store them all—depends on design.)
425 for(const auto& point : points_)
426 {
427 if(fabs(point.parameters[0] - v0) < 1e-5f)
428 p0 = &point;
429 if(fabs(point.parameters[0] - v1) < 1e-5f)
430 p1 = &point;
431 }
432
433 // 4) If we found both endpoints, output them with weight
434 // Typically you'll have 2 clips if param is within range,
435 // or if param < v0 or param > v1 you'll effectively clamp to one clip.
436 if(p0 && p1 && p0 != p1)
437 {
438 float w0 = 1.0f - t;
439 float w1 = t;
440 // Add them if both weights are > 0, or clamp if out of range
441 out_clips.emplace_back(p0->clip, w0);
442 out_clips.emplace_back(p1->clip, w1);
443 }
444 else if(p0) // param is out of range or there's only one valid endpoint
445 {
446 // 100% to p0
447 out_clips.emplace_back(p0->clip, 1.0f);
448 }
449 else if(p1)
450 {
451 // 100% to p1
452 out_clips.emplace_back(p1->clip, 1.0f);
453 }
454}
455
456void blend_space_def::compute_blend_2d(const parameters_t& current_params,
457 std::vector<std::pair<asset_handle<animation_clip>, float>>& out_clips) const
458{
459 // Clear the output vector
460 out_clips.clear();
461
462 // For simplicity, we'll handle a 2D blend space with bilinear interpolation
463 if(parameter_count_ != 2)
464 {
465 // Implement support for other dimensions as needed
466 return;
467 }
468
469 // Find the four closest points for bilinear interpolation
470 // This involves finding the rectangle (grid cell) that the current parameters fall into
471
472 // Collect all parameter values along each axis
473 std::set<float> param0_values;
474 std::set<float> param1_values;
475 for(const auto& point : points_)
476 {
477 param0_values.insert(point.parameters[0]);
478 param1_values.insert(point.parameters[1]);
479 }
480
481 // Convert sets to vectors for indexing
482 std::vector<float> param0_vector(param0_values.begin(), param0_values.end());
483 std::vector<float> param1_vector(param1_values.begin(), param1_values.end());
484
485 // Find indices along each axis
486 auto find_index = [](const std::vector<float>& values, float param) -> size_t
487 {
488 for(size_t i = 0; i < values.size() - 1; ++i)
489 {
490 if(param >= values[i] && param <= values[i + 1])
491 {
492 return i;
493 }
494 }
495 return values.size() - 2; // Return last index if beyond range
496 };
497
498 size_t index0 = find_index(param0_vector, current_params[0]);
499 size_t index1 = find_index(param1_vector, current_params[1]);
500
501 // Get the parameter values at the grid corners
502 float p00 = param0_vector[index0];
503 float p01 = param0_vector[index0 + 1];
504 float p10 = param1_vector[index1];
505 float p11 = param1_vector[index1 + 1];
506
507 // Collect the four corner points
508 std::array<const blend_space_point*, 4> corner_points = {nullptr, nullptr, nullptr, nullptr};
509
510 for(const auto& point : points_)
511 {
512 const auto& params = point.parameters;
513 if(params[0] == p00 && params[1] == p10)
514 corner_points[0] = &point; // Bottom-left
515 if(params[0] == p01 && params[1] == p10)
516 corner_points[1] = &point; // Bottom-right
517 if(params[0] == p00 && params[1] == p11)
518 corner_points[2] = &point; // Top-left
519 if(params[0] == p01 && params[1] == p11)
520 corner_points[3] = &point; // Top-right
521 }
522
523 // Ensure all corner points are found
524 for(const auto* cp : corner_points)
525 {
526 if(!cp)
527 return; // Cannot interpolate without all corner points
528 }
529
530 // Compute interpolation factors
531 float tx = (current_params[0] - p00) / (p01 - p00);
532 float ty = (current_params[1] - p10) / (p11 - p10);
533
534 // Compute weights
535 float w_bl = (1 - tx) * (1 - ty); // Bottom-left
536 float w_br = tx * (1 - ty); // Bottom-right
537 float w_tl = (1 - tx) * ty; // Top-left
538 float w_tr = tx * ty; // Top-right
539
540 // Output the clips and their weights
541 out_clips.emplace_back(corner_points[0]->clip, w_bl);
542 out_clips.emplace_back(corner_points[1]->clip, w_br);
543 out_clips.emplace_back(corner_points[2]->clip, w_tl);
544 out_clips.emplace_back(corner_points[3]->clip, w_tr);
545}
546
548{
549 return parameter_count_;
550}
551
552} // namespace unravel
General purpose transformation class designed to maintain each component of the transformation separa...
Definition transform.hpp:27
void set_scale(const vec3_t &scale) noexcept
Set the scale component.
void set_translation(const vec3_t &position) noexcept
Set the translation component.
void set_rotation(const quat_t &rotation) noexcept
Set the rotation component.
void add_clip(const parameters_t &params, const asset_handle< animation_clip > &clip)
void compute_blend(const parameters_t &current_params, std::vector< std::pair< asset_handle< animation_clip >, float > > &out_clips) const
std::vector< parameter_t > parameters_t
auto get_parameter_count() const -> size_t
void blend_poses_by_node_index_sorted_additive(const animation_pose &base, const animation_pose &additive, const animation_pose &ref_pose, float weight, animation_pose &result)
void blend_poses_by_node_index_sorted_multiway(const std::vector< animation_pose > &poses, const std::vector< float > &weights, animation_pose &result)
void blend_poses(const animation_pose &pose1, const animation_pose &pose2, float factor, animation_pose &result_pose)
void blend_poses_by_node_index_sorted(const animation_pose &pose1, const animation_pose &pose2, float factor, animation_pose &result)
void blend_poses_additive(const animation_pose &base, const animation_pose &additive, const animation_pose &ref_pose, float weight, animation_pose &result)
auto blend_additive(const math::transform &base, const math::transform &additive, const math::transform &ref, float weight) -> math::transform
auto blend(const math::transform &lhs, const math::transform &rhs, float factor) -> math::transform
Represents a handle to an asset, providing access and management functions.
std::vector< node > nodes
root_motion_result motion_result