-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make VariableCurve
into curves
#13105
Conversation
Curve
with bevy_animation
Curve
with bevy_animation
Hey cool stuff. However I think it would be important to keep a baked animation data at all times. With curves it's really conveniant to work and modify animations which I would love but to actually play them having to compute curve values every frame would be loss of performence compared to baked animation. |
This is pretty much a direct drop-in replacement for how animation currently works in Bevy (which is to say, there is no baking). It also doesn't really preclude things like baking |
I believe right now the transform values are lerped between previous and next key which will always be the case because of variable frame durations. The lerp function comes from the glam crate which is using SIMd making this operation very fast. |
Actually, I think that One advantage of this is that if we do ever actually want to bake anything, the |
I was looking at lib.rs in |
Ah; |
Ok I see, we should probably look into code optimization on this topic later. Even the tweened keyframe function is doing a few unecessary copies :
The keyframe update functions should be very fast compared to data management functions which can take there time. But yeah lots of things to do ^^ Having curves is already a huge plus. Would be interesting to reimplement all the actual curve evaluations in SIMd as well |
Curve
with bevy_animation
VariableCurve
into curves
# Objective Finish what we started in #14630. The Curve RFC is [here](https://github.com/bevyengine/rfcs/blob/main/rfcs/80-curve-trait.md). ## Solution This contains the rest of the library from my branch. The main things added here are: - Bulk sampling / resampling methods on `Curve` itself - Data structures supporting the above - The `cores` submodule that those data structures use to encapsulate sample interpolation The weirdest thing in here is probably `ChunkedUnevenCore` in `cores`, which is not used by anything in the Curve library itself but which is required for efficient storage of glTF animation curves. (See #13105.) We can move it into a different PR if we want to; I don't have strong feelings either way. ## Testing New tests related to resampling are included. As I write this, I realize we could use some tests in `cores` itself, so I will add some on this branch before too long. --------- Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: Robert Walter <[email protected]>
Very cool! Obligatory note that we cannot merge this without profiling. |
Well, I did profile it, but as noted in the description, more profiling would definitely be great. |
Really cool, please accept the merge this will make the logic behind blended mask nodes. Way easier to implement |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't gotten very far yet. To be continued and here's at least one comment. Already looks really cool! :)
/// The time of the last keyframe for this animation curve. If the curve is constant, None | ||
/// is returned instead. | ||
#[inline] | ||
pub fn duration(&self) -> Option<f32> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like the doc comment and the name of the function are not aligned. In my head duration
is more like the length of the domain whereas the comment and the implementation are about the end_time
or however you want to call it. If the curve implicitly will always start at t = 0
then we should note that somewhere.
The same review comment applies to the other enums and their duration
methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. I expect duration()
to return a std::time::duration
like Time.delta()
would.
Maybe last_keyframe()
would be more appropriate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks really good to me. Only a few minor things. Great work! 💚
cubic_spline_interpolation( | ||
first[idx + width], | ||
first[idx + (width * 2)], | ||
second[idx + width], | ||
second[idx], | ||
s, | ||
step_between, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really super deep into the code but comparing with the other occurrence of this function call we have
cubic_spline_interpolation(u[1], u[2], v[0], v[1], s, t1 - t0)
so in my mind I would think that we follow a similar pattern here. So to me it looks like the args for second got switched up and should be
cubic_spline_interpolation(
first[idx + width],
first[idx + (width * 2)],
second[idx],
second[idx + width],
s,
step_between,
)
since I think that second[idx + width]
is the end value and second[idx]
is the in tangent. But maybe I'm missing something, just making sure that this is intended
.map(|c| { | ||
VariableCurve::Translation(TranslationCurve::Linear(c)) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering why clippy isn't complaining but (nit-picky and opinionated!!) I think this can be
.map(|c| { | |
VariableCurve::Translation(TranslationCurve::Linear(c)) | |
}) | |
.map(TranslationCurve::Linear) | |
.map(VariableCurve::Translation) |
saving us the indentation, a line of code and generally easier read (probably just to me). This is basically applicaable everywhere below
| InterpolationDatum::RightTail((_, v)) => v[1], | ||
|
||
InterpolationDatum::Between((t0, u), (t1, v), s) => { | ||
cubic_spline_interpolation(u[1], u[2], v[0], v[1], s, t1 - t0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like this could use
- a) well named variables for
u[1]
,u[2]
,v[0]
andv[1]
to add some context again - b) a normal comment explaining what the values are
for future developers that look at the code in 1+ year even though (!!) it's already perfectly explained a few lines below in the new
functions. imo we can't guarantee that people will find the other docs and if they don't this is harder to figure out.
if values.is_empty() { | ||
return Err(ChunkedUnevenCoreError::ZeroWidth); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could move this up a bit as it is cheap to check and we could potentially skip filter_sort_dedup_times
and exit early.
fn sample_iter_unchecked<'a>(&self, t: f32) -> impl Iterator<Item = T> | ||
where | ||
Self: 'a; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the lifetime for here?
continue; | ||
} | ||
|
||
let maybe_curve: Option<VariableCurve> = if let Some(outputs) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel this has a lot of cyclomatic complexity - if we do
let Some(outputs) = reader.read_outputs() else {
warn!("Animations without a sampler output are not supported");
return Err(GltfError::MissingAnimationSampler(animation.index()));
};
we could achieve the same thing but with more readability?
The same review comment applies to the following let ... = if let Some(...) = ...
passages.
Subsumed by #15434. |
Objective
Presently, the main notion of curve in
bevy_animation
isVariableCurve
, which is essentially a way of organizing an imported glTF animation. This RFC demonstrates a reorganization of this data so that eachTransform
component andMorphWeights
curve is actually aCurve
, with its glTF interpolation modes reified by curve structs that implement them on the underlying data buffers.This has several advantages:
Solution
VariableCurve
, along with the code that loads it and uses it, has been completely overhauled.VariableCurve
is still an enum, but it is split up in a different way:Each of the
Transform
component curve types here is actually aCurve
, but they are still enums, broken down by mode of interpolation; for example, here isRotationCurve
:Some of the curve representations that appear here are new, such as
SteppedKeyframeCurve
andCubicKeyframeCurve
— these belong tobevy_animation
, and are built on the data structures frombevy::math::curve::cores
, which handles the data storage and access patterns. Others, likeUnevenSampleAutoCurve
andConstantCurve
, are taken "off-the-shelf" from the Curve API itself.Its implementation of
Curve<Quat>
essentially just matches over the variants. (The last one requires special handling to do quaternion normalization, but that's about it.)Allocation-free
Curve<Vec<T>>
Since the definitions implicit in the Curve trait would require that anything that looks like a
Curve<Vec<T>>
allocates to produce owned output, this PR introduces an offshoot trait which mitigates this problem —IterableCurve
:This is used in concert with the core data structures from the Curve API to sample from keyframes valued in morph weights without ever allocating, all backed by a contiguous buffer of output data.
Performance
This is probably one of the biggest concerns about substantial changes to
VariableCurve
, so let me be proactive in addressing this.First of all,
VariableCurve
has not changed in size (still 64 bytes); this is because the backing data for every curve is at most a pair of vectors — 48 bytes in total, plus 16 bytes for enum discriminants. This is important for caching reasons.Secondly, proactive measures have been taken to ensure that the curves are designed with good cache locality properties, internally using a
Vec<f32>
for keyframe times paired with a contiguous buffer of sample dataVec<T>
which is sliced up to actually perform sampling. One nice thing is thatbevy_animation
is not doing any very fancy gymnastics here; it's mostly just using thebevy_math::curve::core
APIs as someone would in user-space. TheIterableCurve
abstraction mentioned above allows these niceties to extend to the case of morph weights without allocation concerns.Finally, preliminary performance data from tracing looks fine (huge grain of salt — just my machine, on this one example, etc.). On my machine, the difference (if any) on
many_foxes
appears to be basically the same, with the PR branch running within +-5% ofmain
onanimate_targets
.One thing that I really want is a broader base of examples to pull from for performance benchmarking our animation systems, to get a better idea of the potential impact under more realistic circumstances.
Future direction
I think it's unlikely that this representation will serve as the be-all and end-all for encoding character animations — to the contrary, it's likely in the future that
bevy_animation
will want to include things like compression, at which point these curve constructions will no longer (in an ideal world) see much direct use. On the other hand, I believe that the Curve API itself might provide some value in accomplishing feats like compression, and it would be especially nice if the tools for such things could actually be made reusable across domains. To me, this change helps facilitate that, in addition to just providing a nicer internal representation for glTF animations.