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