-
-
Notifications
You must be signed in to change notification settings - Fork 21.4k
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
Implement animation compression #54050
Conversation
87a752f
to
12feb72
Compare
Thanks for documenting the compression format. I would put a version byte (or uint32_t) header at the start of the encoding (you could start with 0x1). If someone changes or updates the encoding format in the future (maybe you need a few extra bytes of precision sometime in the future), there is an easy(er) check and migration path for any data stored in a previous encoding version to a newer one. |
Makes sense, but I suppose I can just put it in the Compression dictionary that gets serialized. Will add it before getting the PR out of draft. |
12feb72
to
587fbc9
Compare
587fbc9
to
45e2ddb
Compare
Quaternion Animation::_uncompress_quaternion(const Vector3i &p_value) const { | ||
Vector3 axis = Vector3::octahedron_decode(Vector2(float(p_value.x) / 65535.0, float(p_value.y) / 65535.0)); | ||
float angle = (float(p_value.z) / 65535.0) * 2.0 * Math_PI; | ||
return Quaternion(axis, angle); |
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.
As another observation, this stuff may be useful in other places in the engine (encode / compress) a Quaternion to Vector3i.. (Networking for example). It may be worth considering organizing the generic compression/encoding funcs to be available & accessible outside of the Animation class.
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 am not super sure about this, but be it the case at some point I suppose it can moved. Would wait until there is demand to avoid cluttering the core APIs.
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 guess I'm just curious where the line is drawn on something being considered API clutter then? Vector3 (now) in the PR exposes a new octahedron_decode
and octahedron_encode
(Vector3->Vector2
) in core API. Why is Quaternion->Vector3i
considered API clutter when the former is not? The Quaternion->Vector3i
seems very useful elsewhere, especially as it should be used for encoding Quaternions in say netcode or other serializations.
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.
Because I am going to implement octahedral encoding and decoding in vertex arrays in a subsequent PR. If this were the case for the other encodes definitely they would be exposed in core. I just prefer the conservative approach of only providing commonly used functions in core API.
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.
Because I am going to implement octahedral encoding and decoding in vertex arrays in a subsequent PR.
That's great, but why not design modularly now and then you won't have to refactor later? uncompress_quaternion
just really shouldn't be a member method of Animation
. It's PURELY functional, which is a good indication it shouldn't be a member method at all. Why would you need an Animation
object to use this compression when it's a pure function and reads no data in the Animation
object itself? :(
About exposing it publicly in the engine, I comment because we are already doing this in our network module (no one encodes orientations over the wire with 4 floats). But now when we migrate to 4.0, we will still need to duplicate this functionality because the functionality provided by the engine will be specifically encapsulated by the Animation
object.
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.
Well, its not super difficult to do this manually need it be, the main thing that matters is the octahedron compression, so it feels like for now this may be enough. May change my mind later if I see more demand.
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.
To clarify, I am very obsessive of not putting in core stuff that is easy to workaround or that is not used frequently and I am not convinced about that. If I am mistaken on omitting something, generally time proves me wrong but if nobody uses something and it adds bloat, then time makes it very difficult to fix that bloat.
@fire Its the same with the remove buttons, seems like a bug in ScrollContainer, unrelated to this PR. |
I saw that the compressed track is not on by default. Does this mean #51341 (default to 30fps) needs work? |
@fire It would be nice to do some performance measurements on how fast the compressed track is vs the regular one, if the performance is not affected as much we could make it default, but I think this is likely better in a subsequent PR. NOTE Sorry I misunderstood you, yes letting aside setting it on by default, it should be fine to default to 30fps with this PR, since if size starts becoming a problem, compression can turned on. |
|
Roughly based on godotengine/godot-proposals#3375 (used format is slightly different). * Implement bitwidth based animation compression (see animation.h for format). * Can compress imported animations up to 10 times. * Compression format opens the door to streaming. * Works transparently (happens all inside animation.h)
45e2ddb
to
a69541d
Compare
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.
Reviewed together with @fire
I don't think there is anything we should do about this right now, since it's not called at runtime.... but _fetch_compressed_by_index
appears to be linear time complexity.
It may be good to note that fetching all indices using a function like position_track_get_key
in a loop on a compressed track would take O(N^2) time total.
Had a discussion in rocket chat that we should document how to calculate the tolerance and when to use uncompressed.
real_t r = ((real_t)1) / Math::sqrt(1 - w * w); | ||
return Vector3(x * r, y * r, z * r); |
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.
Does this return NaN for the identity quaternion? Should it return 0,0,0 or an arbitrary axis instead?
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 think w should never be negative, but it may be possible this is the case due to numerical precision, so a MAX(1-w*w,0.0) might be in order for a separate PR.
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.
even if w is exactly +1.0 or -1.0, the issue is that (1-w*w) may become 0.0 which would cause a division by zero if Math::sqrt(1 - w * w)==0
.
that is to say, r
may become NaN
perhaps real_t r = MAX(0.0, ....);
would avoid both issues.
(I'm a bit curious how identity quaternion is currently handled in this code / why this encoding doesn't cause trouble when converting such a NaN axis to octahedral compression)
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 think the problem is that, if w is 1.0 (or -1.0), then whatever axis is does not really matter because upon rebuilding of axis/angle axis is multiplied by 0. I am not sure if we should add a special check to these functions for this case.
for (uint32_t i = 0; i < compression.pages.size(); i++) { | ||
if (compression.pages[i].time_offset > p_time) { | ||
break; |
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.
This should use binary search in case a given animation track has a large number of pages.
_fetch_compressed
would be called in the inner loop of animation playback at runtime so it should be fast.
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.
Note: unlike _fetch_compressed_by_index
, I think this would be important to optimize since it's used heavily at runtime.
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 think for the time being there will not be as many pages to make it worth implementing binary search. At some point, animation streaming should be implemented (for very large animations) and I think that will be the best time to do and test this optimization.
uint32_t frame = 0; | ||
}; | ||
|
||
float split_tolerance = 1.5; |
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.
Should some of your test data from
https://chat.godotengine.org/channel/animation?msg=bY8KpPJ7tfkQNpGzv
be included here to explain how this magic number was chosen?
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 just tried compressing a large animation to find the best value, but its not like it makes a huge difference.
Thanks! |
Fixup to #54050, CI's GCC builds didn't catch it.
_FORCE_INLINE_ Vector2 octahedron_encode() const { | ||
Vector3 n = *this; | ||
n /= Math::abs(n.x) + Math::abs(n.y) + Math::abs(n.z); | ||
Vector2 o; | ||
if (n.z >= 0.0) { | ||
o.x = n.x; | ||
o.y = n.y; | ||
} else { | ||
o.x = (1.0 - Math::abs(n.y)) * (n.x >= 0.0 ? 1.0 : -1.0); | ||
o.y = (1.0 - Math::abs(n.x)) * (n.y >= 0.0 ? 1.0 : -1.0); | ||
} | ||
o.x = o.x * 0.5 + 0.5; | ||
o.y = o.y * 0.5 + 0.5; | ||
return o; | ||
} | ||
|
||
static _FORCE_INLINE_ Vector3 octahedron_decode(const Vector2 &p_oct) { | ||
Vector2 f(p_oct.x * 2.0 - 1.0, p_oct.y * 2.0 - 1.0); | ||
Vector3 n(f.x, f.y, 1.0f - Math::abs(f.x) - Math::abs(f.y)); | ||
float t = CLAMP(-n.z, 0.0, 1.0); | ||
n.x += n.x >= 0 ? -t : t; | ||
n.y += n.y >= 0 ? -t : t; | ||
return n.normalized(); | ||
} |
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.
Roughly based on godotengine/godot-proposals#3375 (used format is slightly different).
Algorithm is described in the comments of animation.h
Why bitwidth over curve fitting?
I experimented with octahedral and spherical mapping. While spherical mapping has the advantage of less discontinuity when wrapping around, the compressor does a good job of isolating the transients (border crossings), so it matters little in practice, as they don't happen as often.
NOTE Seems to work in everything I tested, but try at your own risk.