Skip to main content

fyrox_animation/machine/node/
blend.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! Various animation blending nodes.
22
23use crate::{
24    core::{
25        pool::{Handle, Pool},
26        reflect::prelude::*,
27        visitor::{Visit, VisitResult, Visitor},
28    },
29    machine::{
30        node::AnimationEventCollectionStrategy, node::BasePoseNode, AnimationPoseSource, Parameter,
31        ParameterContainer, PoseNode, PoseWeight,
32    },
33    Animation, AnimationContainer, AnimationEvent, AnimationPose, EntityId,
34};
35use fyrox_core::uuid::{uuid, Uuid};
36use fyrox_core::TypeUuidProvider;
37use std::cmp::Ordering;
38use std::{
39    cell::{Cell, Ref, RefCell},
40    ops::{Deref, DerefMut},
41};
42
43/// Weighted proxy for animation pose. It has an input pose source and a weight, that tells in which proportion
44/// the pose should be blended into final pose.
45#[derive(Default, Debug, Visit, Clone, Reflect, PartialEq)]
46pub struct BlendPose<T: EntityId> {
47    /// Weight of the pose.
48    pub weight: PoseWeight,
49
50    /// A source of animation pose.
51    #[reflect(hidden)]
52    pub pose_source: Handle<PoseNode<T>>,
53}
54
55impl<T: EntityId> TypeUuidProvider for BlendPose<T> {
56    fn type_uuid() -> Uuid {
57        uuid!("b01d7639-7b39-4eaf-87e6-29fd5221951b")
58    }
59}
60
61impl<T: EntityId> BlendPose<T> {
62    /// Creates new instance of blend pose with given weight and animation pose.
63    pub fn new(weight: PoseWeight, pose_source: Handle<PoseNode<T>>) -> Self {
64        Self {
65            weight,
66            pose_source,
67        }
68    }
69
70    /// Specialized constructor that creates blend pose with constant weight.
71    /// `weight` should be positive.
72    pub fn with_constant_weight(weight: f32, pose_source: Handle<PoseNode<T>>) -> Self {
73        Self {
74            weight: PoseWeight::Constant(weight),
75            pose_source,
76        }
77    }
78
79    /// Specialized constructor that creates blend pose with parametrized weight.
80    /// `param_id` must be name of Weight parameter in machine.
81    pub fn with_param_weight(param_id: &str, pose_source: Handle<PoseNode<T>>) -> Self {
82        Self {
83            weight: PoseWeight::Parameter(param_id.to_owned()),
84            pose_source,
85        }
86    }
87}
88
89/// Animation blend node. It takes multiple input poses and mixes them together into single pose with specified
90/// weights. Could be used to mix hit and run animations for example - once your character got hit, you set some
91/// significant weight for hit animation (0.8 for example) and lower weight for run animation (0.2) and it will
92/// look like your character got wounded while it still running (probably you should decrease speed here too).
93/// Weights can be parametrized, which means that you can dynamically change them in runtime. In our example we
94/// can decrease weight of hit animation over time and increase weight of run animation, so character will recover
95/// from his wounds.
96#[derive(Default, Debug, Visit, Clone, Reflect, PartialEq)]
97pub struct BlendAnimations<T: EntityId> {
98    /// Base node.
99    pub base: BasePoseNode<T>,
100
101    /// A list of pose sources. See [`BlendPose`] docs for more info.
102    pub pose_sources: Vec<BlendPose<T>>,
103
104    /// Output pose of the node, contains final result of blending all input poses.
105    #[visit(skip)]
106    #[reflect(hidden)]
107    pub output_pose: RefCell<AnimationPose<T>>,
108}
109
110impl<T: EntityId> Deref for BlendAnimations<T> {
111    type Target = BasePoseNode<T>;
112
113    fn deref(&self) -> &Self::Target {
114        &self.base
115    }
116}
117
118impl<T: EntityId> DerefMut for BlendAnimations<T> {
119    fn deref_mut(&mut self) -> &mut Self::Target {
120        &mut self.base
121    }
122}
123
124impl<T: EntityId> BlendAnimations<T> {
125    /// Creates new animation blend node with given poses.
126    pub fn new(poses: Vec<BlendPose<T>>) -> Self {
127        Self {
128            base: Default::default(),
129            pose_sources: poses,
130            output_pose: Default::default(),
131        }
132    }
133
134    /// Returns a set of handles to children pose nodes.
135    pub fn children(&self) -> Vec<Handle<PoseNode<T>>> {
136        self.pose_sources.iter().map(|s| s.pose_source).collect()
137    }
138}
139
140impl<T: EntityId> AnimationPoseSource<T> for BlendAnimations<T> {
141    fn eval_pose(
142        &self,
143        nodes: &Pool<PoseNode<T>>,
144        params: &ParameterContainer,
145        animations: &AnimationContainer<T>,
146        dt: f32,
147    ) -> Ref<AnimationPose<T>> {
148        self.output_pose.borrow_mut().reset();
149        for blend_pose in self.pose_sources.iter() {
150            let weight = match blend_pose.weight {
151                PoseWeight::Constant(value) => value,
152                PoseWeight::Parameter(ref param_id) => {
153                    if let Some(Parameter::Weight(weight)) = params.get(param_id) {
154                        *weight
155                    } else {
156                        0.0
157                    }
158                }
159            };
160
161            if let Ok(pose_source) = nodes
162                .try_borrow(blend_pose.pose_source)
163                .map(|pose_source| pose_source.eval_pose(nodes, params, animations, dt))
164            {
165                self.output_pose
166                    .borrow_mut()
167                    .blend_with(&pose_source, weight);
168            }
169        }
170        self.output_pose.borrow()
171    }
172
173    fn pose(&self) -> Ref<AnimationPose<T>> {
174        self.output_pose.borrow()
175    }
176
177    fn collect_animation_events(
178        &self,
179        nodes: &Pool<PoseNode<T>>,
180        params: &ParameterContainer,
181        animations: &AnimationContainer<T>,
182        strategy: AnimationEventCollectionStrategy,
183    ) -> Vec<(Handle<Animation<T>>, AnimationEvent)> {
184        match strategy {
185            AnimationEventCollectionStrategy::All => {
186                let mut events = Vec::new();
187                for pose in self.pose_sources.iter() {
188                    if let Ok(source) = nodes.try_borrow(pose.pose_source) {
189                        events.extend(
190                            source.collect_animation_events(nodes, params, animations, strategy),
191                        );
192                    }
193                }
194                events
195            }
196            AnimationEventCollectionStrategy::MaxWeight => {
197                if let Some((pose, _)) = self
198                    .pose_sources
199                    .iter()
200                    .filter_map(|s| s.weight.value(params).map(|w| (s, w)))
201                    .max_by(|(_, w1), (_, w2)| w1.partial_cmp(w2).unwrap_or(Ordering::Equal))
202                {
203                    if let Ok(pose_source) = nodes.try_borrow(pose.pose_source) {
204                        return pose_source
205                            .collect_animation_events(nodes, params, animations, strategy);
206                    }
207                }
208
209                Default::default()
210            }
211            AnimationEventCollectionStrategy::MinWeight => {
212                if let Some((pose, _)) = self
213                    .pose_sources
214                    .iter()
215                    .filter_map(|s| s.weight.value(params).map(|w| (s, w)))
216                    .min_by(|(_, w1), (_, w2)| w1.partial_cmp(w2).unwrap_or(Ordering::Equal))
217                {
218                    if let Ok(pose_source) = nodes.try_borrow(pose.pose_source) {
219                        return pose_source
220                            .collect_animation_events(nodes, params, animations, strategy);
221                    }
222                }
223
224                Default::default()
225            }
226        }
227    }
228}
229
230/// An animation pose with specific blend time. Blend time tells the engine how many time it should use to perform
231/// blending to this pose.
232#[derive(Default, Debug, Visit, Clone, Reflect, PartialEq)]
233pub struct IndexedBlendInput<T: EntityId> {
234    /// Blend time tells the engine how many time it should use to perform blending to this pose.
235    pub blend_time: f32,
236
237    /// A handle to pose node source.
238    #[reflect(hidden)]
239    pub pose_source: Handle<PoseNode<T>>,
240}
241
242impl<T: EntityId> TypeUuidProvider for IndexedBlendInput<T> {
243    fn type_uuid() -> Uuid {
244        uuid!("92fcc992-9a68-4152-8449-657546faa286")
245    }
246}
247
248/// A node that switches between given animations using index and smoothly blends from one animation to another
249/// while switching. It is very useful for situations when you need to switch between different animations. For
250/// example you could have an `aim` state, it is suitable for any weapon (you don't need to create a ton of states
251/// like `aim_rifle`, `aim_pistol`, etc), but actual weapon holding animation should be different based on actual
252/// weapon a character is holding. In this case you create a BlendAnimationsByIndex node, add a few inputs where
253/// each input uses different weapon holding animation and in your game all you need to do is to set an index
254/// parameter in the machine parameters. The node will automatically perform smooth transition between different
255/// animations.
256#[derive(Default, Debug, Visit, Clone, Reflect, PartialEq)]
257pub struct BlendAnimationsByIndex<T: EntityId> {
258    /// Base node.
259    pub base: BasePoseNode<T>,
260
261    /// A name of index parameter that will be used to switch between input poses.
262    pub index_parameter: String,
263
264    /// A set of input poses.
265    pub inputs: Vec<IndexedBlendInput<T>>,
266
267    /// Index of a previously active input pose.
268    #[reflect(hidden)]
269    pub prev_index: Cell<Option<u32>>,
270
271    /// Current blend time.
272    #[reflect(hidden)]
273    pub blend_time: Cell<f32>,
274
275    /// Output pose of the node.
276    #[visit(skip)]
277    #[reflect(hidden)]
278    pub output_pose: RefCell<AnimationPose<T>>,
279}
280
281impl<T: EntityId> Deref for BlendAnimationsByIndex<T> {
282    type Target = BasePoseNode<T>;
283
284    fn deref(&self) -> &Self::Target {
285        &self.base
286    }
287}
288
289impl<T: EntityId> DerefMut for BlendAnimationsByIndex<T> {
290    fn deref_mut(&mut self) -> &mut Self::Target {
291        &mut self.base
292    }
293}
294
295impl<T: EntityId> BlendAnimationsByIndex<T> {
296    /// Creates new [`BlendAnimationsByIndex`] node using given index parameter name and a set of inputs.
297    pub fn new(index_parameter: String, inputs: Vec<IndexedBlendInput<T>>) -> Self {
298        Self {
299            base: Default::default(),
300            index_parameter,
301            inputs,
302            output_pose: RefCell::new(Default::default()),
303            prev_index: Cell::new(None),
304            blend_time: Cell::new(0.0),
305        }
306    }
307
308    /// Return a set of handle of children nodes.
309    pub fn children(&self) -> Vec<Handle<PoseNode<T>>> {
310        self.inputs.iter().map(|s| s.pose_source).collect()
311    }
312}
313
314impl<T: EntityId> AnimationPoseSource<T> for BlendAnimationsByIndex<T> {
315    fn eval_pose(
316        &self,
317        nodes: &Pool<PoseNode<T>>,
318        params: &ParameterContainer,
319        animations: &AnimationContainer<T>,
320        dt: f32,
321    ) -> Ref<AnimationPose<T>> {
322        self.output_pose.borrow_mut().reset();
323
324        if let Some(&Parameter::Index(current_index)) = params.get(&self.index_parameter) {
325            let mut applied = false;
326
327            if let Some(prev_index) = self.prev_index.get() {
328                if prev_index != current_index {
329                    if let (Some(prev_input), Some(current_input)) = (
330                        self.inputs.get(prev_index as usize),
331                        self.inputs.get(current_index as usize),
332                    ) {
333                        self.blend_time
334                            .set((self.blend_time.get() + dt).min(current_input.blend_time));
335
336                        let interpolator = self.blend_time.get() / current_input.blend_time;
337
338                        self.output_pose.borrow_mut().blend_with(
339                            &nodes[prev_input.pose_source].eval_pose(nodes, params, animations, dt),
340                            1.0 - interpolator,
341                        );
342                        self.output_pose.borrow_mut().blend_with(
343                            &nodes[current_input.pose_source]
344                                .eval_pose(nodes, params, animations, dt),
345                            interpolator,
346                        );
347
348                        if interpolator >= 1.0 {
349                            self.prev_index.set(Some(current_index));
350                            self.blend_time.set(0.0);
351                        }
352
353                        applied = true;
354                    }
355                }
356            } else {
357                self.prev_index.set(Some(current_index));
358            }
359
360            if !applied {
361                // Immediately jump to target pose (if any).
362                self.blend_time.set(0.0);
363
364                if let Some(current_input) = self.inputs.get(current_index as usize) {
365                    nodes[current_input.pose_source]
366                        .eval_pose(nodes, params, animations, dt)
367                        .clone_into(&mut self.output_pose.borrow_mut());
368                }
369            }
370        }
371
372        self.output_pose.borrow()
373    }
374
375    fn pose(&self) -> Ref<AnimationPose<T>> {
376        self.output_pose.borrow()
377    }
378
379    fn collect_animation_events(
380        &self,
381        nodes: &Pool<PoseNode<T>>,
382        params: &ParameterContainer,
383        animations: &AnimationContainer<T>,
384        strategy: AnimationEventCollectionStrategy,
385    ) -> Vec<(Handle<Animation<T>>, AnimationEvent)> {
386        if let Some(&Parameter::Index(current_index)) = params.get(&self.index_parameter) {
387            if let Some(prev_index) = self.prev_index.get() {
388                if prev_index != current_index {
389                    if let (Some(prev_input), Some(current_input)) = (
390                        self.inputs.get(prev_index as usize),
391                        self.inputs.get(current_index as usize),
392                    ) {
393                        let interpolator = self.blend_time.get() / current_input.blend_time;
394
395                        match strategy {
396                            AnimationEventCollectionStrategy::All => {
397                                let mut events = Vec::new();
398                                for input in [prev_input, current_input] {
399                                    if let Ok(source) = nodes.try_borrow(input.pose_source) {
400                                        events.extend(source.collect_animation_events(
401                                            nodes, params, animations, strategy,
402                                        ));
403                                    }
404                                }
405                                return events;
406                            }
407                            AnimationEventCollectionStrategy::MaxWeight => {
408                                let input = if interpolator < 0.5 {
409                                    prev_input
410                                } else {
411                                    current_input
412                                };
413
414                                if let Ok(pose_source) = nodes.try_borrow(input.pose_source) {
415                                    return pose_source.collect_animation_events(
416                                        nodes, params, animations, strategy,
417                                    );
418                                }
419                            }
420                            AnimationEventCollectionStrategy::MinWeight => {
421                                let input = if interpolator < 0.5 {
422                                    current_input
423                                } else {
424                                    prev_input
425                                };
426
427                                if let Ok(pose_source) = nodes.try_borrow(input.pose_source) {
428                                    return pose_source.collect_animation_events(
429                                        nodes, params, animations, strategy,
430                                    );
431                                }
432                            }
433                        }
434                    }
435                } else {
436                    // In case where the transition is done, all the strategies does the same - just collects events
437                    // from active pose node.
438                    if let Some(current_input) = self.inputs.get(current_index as usize) {
439                        if let Ok(pose_source) = nodes.try_borrow(current_input.pose_source) {
440                            return pose_source
441                                .collect_animation_events(nodes, params, animations, strategy);
442                        }
443                    }
444                }
445            }
446        }
447
448        Default::default()
449    }
450}