mirror of
https://github.com/FyroxEngine/Fyrox.git
synced 2026-10-11 22:50:08 +00:00
curve fetching optimization for animation system
- gives 2x-3x performance boost on average - essentially turns binary search of a span into simple indexing using stored index value from the previous frame (so called "fetch hint")
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
//! Track data container is a flexible source of data for numeric parameters, it is built using a set
|
||||
//! of parametric curves. See [`TrackDataContainer`] docs for more info.
|
||||
|
||||
use crate::track::HintContainer;
|
||||
use crate::{
|
||||
core::{
|
||||
algebra::{Vector2, Vector3, Vector4},
|
||||
@@ -159,7 +160,7 @@ impl TrackDataContainer {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn fetch_vector2(&self, time: f32) -> Option<TrackValue> {
|
||||
fn fetch_vector2(&self, time: f32, fetch_hints: &mut HintContainer) -> Option<TrackValue> {
|
||||
if self.curves.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
@@ -167,29 +168,39 @@ impl TrackDataContainer {
|
||||
// SAFETY: The indices are guaranteed to be correct by the above check.
|
||||
unsafe {
|
||||
Some(TrackValue::Vector2(Vector2::new(
|
||||
self.curves.get_unchecked(0).value_at(time),
|
||||
self.curves.get_unchecked(1).value_at(time),
|
||||
self.curves
|
||||
.get_unchecked(0)
|
||||
.value_at(time, &mut fetch_hints[0]),
|
||||
self.curves
|
||||
.get_unchecked(1)
|
||||
.value_at(time, &mut fetch_hints[1]),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn fetch_vector3(&self, time: f32) -> Option<TrackValue> {
|
||||
fn fetch_vector3(&self, time: f32, fetch_hints: &mut HintContainer) -> Option<TrackValue> {
|
||||
if self.curves.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
Some(TrackValue::Vector3(Vector3::new(
|
||||
self.curves.get_unchecked(0).value_at(time),
|
||||
self.curves.get_unchecked(1).value_at(time),
|
||||
self.curves.get_unchecked(2).value_at(time),
|
||||
self.curves
|
||||
.get_unchecked(0)
|
||||
.value_at(time, &mut fetch_hints[0]),
|
||||
self.curves
|
||||
.get_unchecked(1)
|
||||
.value_at(time, &mut fetch_hints[1]),
|
||||
self.curves
|
||||
.get_unchecked(2)
|
||||
.value_at(time, &mut fetch_hints[2]),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn fetch_vector4(&self, time: f32) -> Option<TrackValue> {
|
||||
fn fetch_vector4(&self, time: f32, fetch_hints: &mut HintContainer) -> Option<TrackValue> {
|
||||
if self.curves.len() < 4 {
|
||||
return None;
|
||||
}
|
||||
@@ -197,16 +208,28 @@ impl TrackDataContainer {
|
||||
// SAFETY: The indices are guaranteed to be correct by the above check.
|
||||
unsafe {
|
||||
Some(TrackValue::Vector4(Vector4::new(
|
||||
self.curves.get_unchecked(0).value_at(time),
|
||||
self.curves.get_unchecked(1).value_at(time),
|
||||
self.curves.get_unchecked(2).value_at(time),
|
||||
self.curves.get_unchecked(3).value_at(time),
|
||||
self.curves
|
||||
.get_unchecked(0)
|
||||
.value_at(time, &mut fetch_hints[0]),
|
||||
self.curves
|
||||
.get_unchecked(1)
|
||||
.value_at(time, &mut fetch_hints[1]),
|
||||
self.curves
|
||||
.get_unchecked(2)
|
||||
.value_at(time, &mut fetch_hints[2]),
|
||||
self.curves
|
||||
.get_unchecked(3)
|
||||
.value_at(time, &mut fetch_hints[3]),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn fetch_quaternion_euler(&self, time: f32) -> Option<TrackValue> {
|
||||
fn fetch_quaternion_euler(
|
||||
&self,
|
||||
time: f32,
|
||||
fetch_hints: &mut HintContainer,
|
||||
) -> Option<TrackValue> {
|
||||
if self.curves.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
@@ -219,9 +242,9 @@ impl TrackDataContainer {
|
||||
|
||||
// Convert Euler angles to quaternion
|
||||
let (x, y, z) = (
|
||||
x_curve.value_at(time),
|
||||
y_curve.value_at(time),
|
||||
z_curve.value_at(time),
|
||||
x_curve.value_at(time, &mut fetch_hints[0]),
|
||||
y_curve.value_at(time, &mut fetch_hints[1]),
|
||||
z_curve.value_at(time, &mut fetch_hints[1]),
|
||||
);
|
||||
|
||||
Some(TrackValue::UnitQuaternion(quat_from_euler(
|
||||
@@ -232,7 +255,7 @@ impl TrackDataContainer {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn fetch_quaternion(&self, time: f32) -> Option<TrackValue> {
|
||||
fn fetch_quaternion(&self, time: f32, fetch_hints: &mut HintContainer) -> Option<TrackValue> {
|
||||
if self.curves.len() < 4 {
|
||||
return None;
|
||||
}
|
||||
@@ -246,10 +269,10 @@ impl TrackDataContainer {
|
||||
|
||||
// Convert Euler angles to quaternion
|
||||
let (x, y, z, w) = (
|
||||
x_curve.value_at(time),
|
||||
y_curve.value_at(time),
|
||||
z_curve.value_at(time),
|
||||
w_curve.value_at(time),
|
||||
x_curve.value_at(time, &mut fetch_hints[0]),
|
||||
y_curve.value_at(time, &mut fetch_hints[1]),
|
||||
z_curve.value_at(time, &mut fetch_hints[2]),
|
||||
w_curve.value_at(time, &mut fetch_hints[3]),
|
||||
);
|
||||
|
||||
Some(TrackValue::UnitQuaternion(UnitQuaternion::from_quaternion(
|
||||
@@ -261,14 +284,16 @@ impl TrackDataContainer {
|
||||
/// Tries to get a value at a given time. The method could fail if the internal set of curves is malformed
|
||||
/// and cannot produce a desired value (for example, [`Vector3`] can be fetched only if the amount of curves
|
||||
/// is 3).
|
||||
pub fn fetch(&self, time: f32) -> Option<TrackValue> {
|
||||
pub fn fetch(&self, time: f32, fetch_hints: &mut HintContainer) -> Option<TrackValue> {
|
||||
match self.kind {
|
||||
TrackValueKind::Real => Some(TrackValue::Real(self.curves.first()?.value_at(time))),
|
||||
TrackValueKind::Vector2 => self.fetch_vector2(time),
|
||||
TrackValueKind::Vector3 => self.fetch_vector3(time),
|
||||
TrackValueKind::Vector4 => self.fetch_vector4(time),
|
||||
TrackValueKind::UnitQuaternionEuler => self.fetch_quaternion_euler(time),
|
||||
TrackValueKind::UnitQuaternion => self.fetch_quaternion(time),
|
||||
TrackValueKind::Real => Some(TrackValue::Real(
|
||||
self.curves.first()?.value_at(time, &mut fetch_hints[0]),
|
||||
)),
|
||||
TrackValueKind::Vector2 => self.fetch_vector2(time, fetch_hints),
|
||||
TrackValueKind::Vector3 => self.fetch_vector3(time, fetch_hints),
|
||||
TrackValueKind::Vector4 => self.fetch_vector4(time, fetch_hints),
|
||||
TrackValueKind::UnitQuaternionEuler => self.fetch_quaternion_euler(time, fetch_hints),
|
||||
TrackValueKind::UnitQuaternion => self.fetch_quaternion(time, fetch_hints),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#![allow(clippy::doc_lazy_continuation)]
|
||||
#![allow(mismatched_lifetime_syntaxes)]
|
||||
|
||||
use crate::track::TrackBinding;
|
||||
use crate::track::{HintContainer, TrackBinding};
|
||||
use crate::{
|
||||
core::{
|
||||
algebra::{UnitQuaternion, Vector3},
|
||||
@@ -504,7 +504,7 @@ impl<T: EntityId> Animation<T> {
|
||||
tracks
|
||||
.iter()
|
||||
.find(|track| track.value_binding() == &ValueBinding::Position)
|
||||
.and_then(|track| track.fetch(time))
|
||||
.and_then(|track| track.fetch(time, &mut HintContainer::default()))
|
||||
.and_then(|value| {
|
||||
if let TrackValue::Vector3(position) = value.value {
|
||||
Some(position)
|
||||
@@ -519,7 +519,7 @@ impl<T: EntityId> Animation<T> {
|
||||
tracks
|
||||
.iter()
|
||||
.find(|track| track.value_binding() == &ValueBinding::Rotation)
|
||||
.and_then(|track| track.fetch(time))
|
||||
.and_then(|track| track.fetch(time, &mut HintContainer::default()))
|
||||
.and_then(|value| {
|
||||
if let TrackValue::UnitQuaternion(rotation) = value.value {
|
||||
Some(rotation)
|
||||
@@ -898,12 +898,13 @@ impl<T: EntityId> Animation<T> {
|
||||
|
||||
self.pose.reset();
|
||||
for track in tracks_data.tracks.iter() {
|
||||
let Some(binding) = self.track_bindings.get(&track.id()) else {
|
||||
let Some(binding) = self.track_bindings.get_mut(&track.id()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if binding.is_enabled() {
|
||||
if let Some(bound_value) = track.fetch(self.time_position) {
|
||||
if let Some(bound_value) = track.fetch(self.time_position, &mut binding.fetch_hints)
|
||||
{
|
||||
self.pose.add_to_node_pose(binding.target(), bound_value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,11 @@ use crate::{
|
||||
};
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// A set of indices of keys, where each index points on the right side of a curve span. Each index
|
||||
/// corresponds in the array to a curve in the curve set. It is used to remember last curve fetching
|
||||
/// position to eliminate searching of a span for most cases.
|
||||
pub type HintContainer = [usize; 4];
|
||||
|
||||
/// Track binding contains a handle to a target object that will be animated by an animation track.
|
||||
/// Additionally, the binding could be disabled to temporarily prevent animation from affecting the
|
||||
/// target.
|
||||
@@ -39,6 +44,9 @@ pub struct TrackBinding<T: EntityId> {
|
||||
/// A target bound to a track. The actual track id is stored as a key in hash map of bindings in
|
||||
/// the animation.
|
||||
pub target: T,
|
||||
#[visit(skip)]
|
||||
#[reflect(hidden)]
|
||||
pub(super) fetch_hints: HintContainer,
|
||||
}
|
||||
|
||||
impl<T: EntityId> Default for TrackBinding<T> {
|
||||
@@ -46,6 +54,7 @@ impl<T: EntityId> Default for TrackBinding<T> {
|
||||
Self {
|
||||
enabled: true,
|
||||
target: Default::default(),
|
||||
fetch_hints: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,6 +65,7 @@ impl<T: EntityId> TrackBinding<T> {
|
||||
Self {
|
||||
enabled: true,
|
||||
target,
|
||||
fetch_hints: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,8 +181,8 @@ impl Track {
|
||||
}
|
||||
|
||||
/// Tries to get a new property value at a given time position.
|
||||
pub fn fetch(&self, time: f32) -> Option<BoundValue> {
|
||||
self.frames.fetch(time).map(|v| BoundValue {
|
||||
pub fn fetch(&self, time: f32, fetch_hints: &mut HintContainer) -> Option<BoundValue> {
|
||||
self.frames.fetch(time, fetch_hints).map(|v| BoundValue {
|
||||
binding: self.binding.clone(),
|
||||
value: v,
|
||||
})
|
||||
|
||||
+57
-22
@@ -251,22 +251,57 @@ impl Curve {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn fetch_at<I>(&self, location: f32, interpolator: I) -> f32
|
||||
fn fetch_at<I>(&self, location: f32, hint: &mut usize, interpolator: I) -> f32
|
||||
where
|
||||
I: FnOnce(&CurveKey, &CurveKey, f32) -> f32,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn interpolate_keys<I>(
|
||||
left: &CurveKey,
|
||||
right: &CurveKey,
|
||||
location: f32,
|
||||
interpolator: I,
|
||||
) -> f32
|
||||
where
|
||||
I: FnOnce(&CurveKey, &CurveKey, f32) -> f32,
|
||||
{
|
||||
let t = (location - left.location) / (right.location - left.location);
|
||||
interpolator(left, right, t)
|
||||
}
|
||||
|
||||
if let (Some(first), Some(last)) = (self.keys.first(), self.keys.last()) {
|
||||
if location <= first.location {
|
||||
*hint = 0;
|
||||
first.value
|
||||
} else if location >= last.location {
|
||||
*hint = self.keys().len().saturating_sub(1);
|
||||
last.value
|
||||
} else {
|
||||
// Try to predict the span using the given hint.
|
||||
if let (Some(predicted_left), Some(predicted_right)) = (
|
||||
self.keys.get((*hint).saturating_sub(1)),
|
||||
self.keys.get(*hint),
|
||||
) {
|
||||
if location >= predicted_left.location && location < predicted_right.location {
|
||||
return interpolate_keys(
|
||||
predicted_left,
|
||||
predicted_right,
|
||||
location,
|
||||
interpolator,
|
||||
);
|
||||
} else {
|
||||
// TODO: Usually curve fetching is continuous, so we should also check the
|
||||
// next or previous span here, this will completely eliminate binary search
|
||||
// below on case when playback time step is lower than mid-key time distance.
|
||||
}
|
||||
}
|
||||
|
||||
// Use binary search for multiple spans.
|
||||
let pos = self.keys.partition_point(|k| k.location < location);
|
||||
let left = self.keys.get(pos.saturating_sub(1)).unwrap();
|
||||
let right = self.keys.get(pos).unwrap();
|
||||
let t = (location - left.location) / (right.location - left.location);
|
||||
interpolator(left, right, t)
|
||||
*hint = self.keys.partition_point(|k| k.location < location);
|
||||
let span_end = *hint;
|
||||
let left = self.keys.get(span_end.saturating_sub(1)).unwrap();
|
||||
let right = self.keys.get(span_end).unwrap();
|
||||
interpolate_keys(left, right, location, interpolator)
|
||||
}
|
||||
} else {
|
||||
0.0
|
||||
@@ -274,13 +309,13 @@ impl Curve {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn value_at(&self, location: f32) -> f32 {
|
||||
self.fetch_at(location, |a, b, t| a.interpolate(b, t))
|
||||
pub fn value_at(&self, location: f32, hint: &mut usize) -> f32 {
|
||||
self.fetch_at(location, hint, |a, b, t| a.interpolate(b, t))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn angle_at(&self, location: f32) -> f32 {
|
||||
self.fetch_at(location, |a, b, t| a.interpolate_angles(b, t))
|
||||
pub fn angle_at(&self, location: f32, hint: &mut usize) -> f32 {
|
||||
self.fetch_at(location, hint, |a, b, t| a.interpolate_angles(b, t))
|
||||
}
|
||||
|
||||
pub fn bounds(&self) -> Rect<f32> {
|
||||
@@ -396,21 +431,21 @@ mod test {
|
||||
let mut curve = Curve::default();
|
||||
|
||||
// Test fetching from empty curve.
|
||||
assert_eq!(curve.value_at(0.0), 0.0);
|
||||
assert_eq!(curve.value_at(0.0, &mut 0), 0.0);
|
||||
|
||||
curve.add_key(CurveKey::new(0.0, 1.0, CurveKeyKind::Linear));
|
||||
|
||||
// One-key curves must always return its single key value.
|
||||
assert_eq!(curve.value_at(-1.0), 1.0);
|
||||
assert_eq!(curve.value_at(1.0), 1.0);
|
||||
assert_eq!(curve.value_at(0.0), 1.0);
|
||||
assert_eq!(curve.value_at(-1.0, &mut 0), 1.0);
|
||||
assert_eq!(curve.value_at(1.0, &mut 0), 1.0);
|
||||
assert_eq!(curve.value_at(0.0, &mut 0), 1.0);
|
||||
|
||||
curve.add_key(CurveKey::new(1.0, 0.0, CurveKeyKind::Linear));
|
||||
|
||||
// Two-key curves must always use interpolation.
|
||||
assert_eq!(curve.value_at(-1.0), 1.0);
|
||||
assert_eq!(curve.value_at(2.0), 0.0);
|
||||
assert_eq!(curve.value_at(0.5), 0.5);
|
||||
assert_eq!(curve.value_at(-1.0, &mut 0), 1.0);
|
||||
assert_eq!(curve.value_at(2.0, &mut 0), 0.0);
|
||||
assert_eq!(curve.value_at(0.5, &mut 0), 0.5);
|
||||
|
||||
// Add one more key and do more checks.
|
||||
curve.add_key(CurveKey::new(2.0, 1.0, CurveKeyKind::Linear));
|
||||
@@ -420,15 +455,15 @@ mod test {
|
||||
assert!(curve.keys[1].location <= curve.keys[2].location);
|
||||
|
||||
// Check generic out-of-bounds fetching.
|
||||
assert_eq!(curve.value_at(-1.0), 1.0); // Left side oob
|
||||
assert_eq!(curve.value_at(3.0), 1.0); // Right side oob.
|
||||
assert_eq!(curve.value_at(-1.0, &mut 0), 1.0); // Left side oob
|
||||
assert_eq!(curve.value_at(3.0, &mut 0), 1.0); // Right side oob.
|
||||
|
||||
// Check edge cases.
|
||||
assert_eq!(curve.value_at(0.0), 1.0); // Left edge.
|
||||
assert_eq!(curve.value_at(2.0), 1.0); // Right edge.
|
||||
assert_eq!(curve.value_at(0.0, &mut 0), 1.0); // Left edge.
|
||||
assert_eq!(curve.value_at(2.0, &mut 0), 1.0); // Right edge.
|
||||
|
||||
// Check interpolation.
|
||||
assert_eq!(curve.value_at(0.5), 0.5);
|
||||
assert_eq!(curve.value_at(0.5, &mut 0), 0.5);
|
||||
|
||||
// Check id.
|
||||
let id = Uuid::new_v4();
|
||||
|
||||
@@ -260,7 +260,9 @@ impl ScriptTrait for FlyingCameraController {
|
||||
.min(1.0);
|
||||
*self.target_velocity = new_normalized_velocity.scale(
|
||||
*self.speed
|
||||
* self.acceleration_curve.value_at(self.acceleration_coeff)
|
||||
* self
|
||||
.acceleration_curve
|
||||
.value_at(self.acceleration_coeff, &mut 0)
|
||||
* context.dt,
|
||||
);
|
||||
} else {
|
||||
@@ -270,7 +272,9 @@ impl ScriptTrait for FlyingCameraController {
|
||||
if let Some(normalized_velocity) = self.target_velocity.try_normalize(f32::EPSILON) {
|
||||
*self.target_velocity = normalized_velocity.scale(
|
||||
*self.speed
|
||||
* self.deceleration_curve.value_at(self.acceleration_coeff)
|
||||
* self
|
||||
.deceleration_curve
|
||||
.value_at(self.acceleration_coeff, &mut 0)
|
||||
* context.dt,
|
||||
);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user