Skip to content

Commit

Permalink
Implement Animation Compression
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
reduz committed Oct 21, 2021
1 parent 9c9ec63 commit 45e2ddb
Show file tree
Hide file tree
Showing 12 changed files with 1,859 additions and 95 deletions.
9 changes: 9 additions & 0 deletions core/math/quaternion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,15 @@ Quaternion::operator String() const {
return "(" + String::num_real(x, false) + ", " + String::num_real(y, false) + ", " + String::num_real(z, false) + ", " + String::num_real(w, false) + ")";
}

Vector3 Quaternion::get_axis() const {
real_t r = ((real_t)1) / Math::sqrt(1 - w * w);
return Vector3(x * r, y * r, z * r);
}

float Quaternion::get_angle() const {
return 2 * Math::acos(w);
}

Quaternion::Quaternion(const Vector3 &p_axis, real_t p_angle) {
#ifdef MATH_CHECKS
ERR_FAIL_COND_MSG(!p_axis.is_normalized(), "The axis Vector3 must be normalized.");
Expand Down
3 changes: 3 additions & 0 deletions core/math/quaternion.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ class Quaternion {
Quaternion slerpni(const Quaternion &p_to, const real_t &p_weight) const;
Quaternion cubic_slerp(const Quaternion &p_b, const Quaternion &p_pre_a, const Quaternion &p_post_b, const real_t &p_weight) const;

Vector3 get_axis() const;
float get_angle() const;

_FORCE_INLINE_ void get_axis_angle(Vector3 &r_axis, real_t &r_angle) const {
r_angle = 2 * Math::acos(w);
real_t r = ((real_t)1) / Math::sqrt(1 - w * w);
Expand Down
27 changes: 26 additions & 1 deletion core/math/vector3.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@
#define VECTOR3_H

#include "core/math/math_funcs.h"
#include "core/math/vector2.h"
#include "core/math/vector3i.h"
#include "core/string/ustring.h"

class Basis;

struct Vector3 {
Expand Down Expand Up @@ -103,6 +103,31 @@ struct Vector3 {
Vector3 cubic_interpolate(const Vector3 &p_b, const Vector3 &p_pre_a, const Vector3 &p_post_b, const real_t p_weight) const;
Vector3 move_toward(const Vector3 &p_to, const real_t p_delta) const;

_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();
}

_FORCE_INLINE_ Vector3 cross(const Vector3 &p_b) const;
_FORCE_INLINE_ real_t dot(const Vector3 &p_b) const;
Basis outer(const Vector3 &p_b) const;
Expand Down
4 changes: 4 additions & 0 deletions core/variant/variant_call.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1581,6 +1581,8 @@ static void _register_variant_builtin_methods() {
bind_method(Vector3, bounce, sarray("n"), varray());
bind_method(Vector3, reflect, sarray("n"), varray());
bind_method(Vector3, sign, sarray(), varray());
bind_method(Vector3, octahedron_encode, sarray(), varray());
bind_static_method(Vector3, octahedron_decode, sarray("uv"), varray());

/* Vector3i */

Expand Down Expand Up @@ -1617,6 +1619,8 @@ static void _register_variant_builtin_methods() {
bind_method(Quaternion, slerpni, sarray("to", "weight"), varray());
bind_method(Quaternion, cubic_slerp, sarray("b", "pre_a", "post_b", "weight"), varray());
bind_method(Quaternion, get_euler, sarray(), varray());
bind_method(Quaternion, get_axis, sarray(), varray());
bind_method(Quaternion, get_angle, sarray(), varray());

/* Color */

Expand Down
14 changes: 14 additions & 0 deletions doc/classes/Animation.xml
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,14 @@
Clear the animation (clear all tracks and reset all).
</description>
</method>
<method name="compress">
<return type="void" />
<argument index="0" name="page_size" type="int" default="8192" />
<argument index="1" name="fps" type="int" default="120" />
<argument index="2" name="split_tolerance" type="float" default="4.0" />
<description>
</description>
</method>
<method name="copy_track">
<return type="void" />
<argument index="0" name="track_idx" type="int" />
Expand Down Expand Up @@ -370,6 +378,12 @@
Insert a generic key in a given track.
</description>
</method>
<method name="track_is_compressed" qualifiers="const">
<return type="bool" />
<argument index="0" name="track_idx" type="int" />
<description>
</description>
</method>
<method name="track_is_enabled" qualifiers="const">
<return type="bool" />
<argument index="0" name="track_idx" type="int" />
Expand Down
10 changes: 10 additions & 0 deletions doc/classes/Quaternion.xml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@
Returns the dot product of two quaternions.
</description>
</method>
<method name="get_angle" qualifiers="const">
<return type="float" />
<description>
</description>
</method>
<method name="get_axis" qualifiers="const">
<return type="Vector3" />
<description>
</description>
</method>
<method name="get_euler" qualifiers="const">
<return type="Vector3" />
<description>
Expand Down
11 changes: 11 additions & 0 deletions doc/classes/Vector3.xml
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,17 @@
Returns the vector scaled to unit length. Equivalent to [code]v / v.length()[/code].
</description>
</method>
<method name="octahedron_decode" qualifiers="static">
<return type="Vector3" />
<argument index="0" name="uv" type="Vector2" />
<description>
</description>
</method>
<method name="octahedron_encode" qualifiers="const">
<return type="Vector2" />
<description>
</description>
</method>
<method name="operator !=" qualifiers="operator">
<return type="bool" />
<description>
Expand Down
108 changes: 59 additions & 49 deletions editor/animation_track_editor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2037,7 +2037,7 @@ void AnimationTrackEdit::_notification(int p_what) {
update_mode_rect.position.y = int(get_size().height - update_icon->get_height()) / 2;
update_mode_rect.size = update_icon->get_size();

if (animation->track_get_type(track) == Animation::TYPE_VALUE) {
if (!animation->track_is_compressed(track) && animation->track_get_type(track) == Animation::TYPE_VALUE) {
draw_texture(update_icon, update_mode_rect.position);
}
// Make it easier to click.
Expand Down Expand Up @@ -2079,7 +2079,7 @@ void AnimationTrackEdit::_notification(int p_what) {
interp_mode_rect.position.y = int(get_size().height - icon->get_height()) / 2;
interp_mode_rect.size = icon->get_size();

if (animation->track_get_type(track) == Animation::TYPE_VALUE || animation->track_get_type(track) == Animation::TYPE_BLEND_SHAPE || animation->track_get_type(track) == Animation::TYPE_POSITION_3D || animation->track_get_type(track) == Animation::TYPE_SCALE_3D || animation->track_get_type(track) == Animation::TYPE_ROTATION_3D) {
if (!animation->track_is_compressed(track) && (animation->track_get_type(track) == Animation::TYPE_VALUE || animation->track_get_type(track) == Animation::TYPE_BLEND_SHAPE || animation->track_get_type(track) == Animation::TYPE_POSITION_3D || animation->track_get_type(track) == Animation::TYPE_SCALE_3D || animation->track_get_type(track) == Animation::TYPE_ROTATION_3D)) {
draw_texture(icon, interp_mode_rect.position);
}
// Make it easier to click.
Expand All @@ -2089,7 +2089,7 @@ void AnimationTrackEdit::_notification(int p_what) {
ofs += icon->get_width() + hsep;
interp_mode_rect.size.x += hsep;

if (animation->track_get_type(track) == Animation::TYPE_VALUE || animation->track_get_type(track) == Animation::TYPE_BLEND_SHAPE || animation->track_get_type(track) == Animation::TYPE_POSITION_3D || animation->track_get_type(track) == Animation::TYPE_SCALE_3D || animation->track_get_type(track) == Animation::TYPE_ROTATION_3D) {
if (!animation->track_is_compressed(track) && (animation->track_get_type(track) == Animation::TYPE_VALUE || animation->track_get_type(track) == Animation::TYPE_BLEND_SHAPE || animation->track_get_type(track) == Animation::TYPE_POSITION_3D || animation->track_get_type(track) == Animation::TYPE_SCALE_3D || animation->track_get_type(track) == Animation::TYPE_ROTATION_3D)) {
draw_texture(down_icon, Vector2(ofs, int(get_size().height - down_icon->get_height()) / 2));
interp_mode_rect.size.x += down_icon->get_width();
} else {
Expand All @@ -2112,7 +2112,7 @@ void AnimationTrackEdit::_notification(int p_what) {
loop_mode_rect.position.y = int(get_size().height - icon->get_height()) / 2;
loop_mode_rect.size = icon->get_size();

if (animation->track_get_type(track) == Animation::TYPE_VALUE || animation->track_get_type(track) == Animation::TYPE_BLEND_SHAPE || animation->track_get_type(track) == Animation::TYPE_POSITION_3D || animation->track_get_type(track) == Animation::TYPE_SCALE_3D || animation->track_get_type(track) == Animation::TYPE_ROTATION_3D) {
if (!animation->track_is_compressed(track) && (animation->track_get_type(track) == Animation::TYPE_VALUE || animation->track_get_type(track) == Animation::TYPE_BLEND_SHAPE || animation->track_get_type(track) == Animation::TYPE_POSITION_3D || animation->track_get_type(track) == Animation::TYPE_SCALE_3D || animation->track_get_type(track) == Animation::TYPE_ROTATION_3D)) {
draw_texture(icon, loop_mode_rect.position);
}

Expand All @@ -2122,7 +2122,7 @@ void AnimationTrackEdit::_notification(int p_what) {
ofs += icon->get_width() + hsep;
loop_mode_rect.size.x += hsep;

if (animation->track_get_type(track) == Animation::TYPE_VALUE || animation->track_get_type(track) == Animation::TYPE_BLEND_SHAPE || animation->track_get_type(track) == Animation::TYPE_POSITION_3D || animation->track_get_type(track) == Animation::TYPE_SCALE_3D || animation->track_get_type(track) == Animation::TYPE_ROTATION_3D) {
if (!animation->track_is_compressed(track) && (animation->track_get_type(track) == Animation::TYPE_VALUE || animation->track_get_type(track) == Animation::TYPE_BLEND_SHAPE || animation->track_get_type(track) == Animation::TYPE_POSITION_3D || animation->track_get_type(track) == Animation::TYPE_SCALE_3D || animation->track_get_type(track) == Animation::TYPE_ROTATION_3D)) {
draw_texture(down_icon, Vector2(ofs, int(get_size().height - down_icon->get_height()) / 2));
loop_mode_rect.size.x += down_icon->get_width();
} else {
Expand All @@ -2137,7 +2137,7 @@ void AnimationTrackEdit::_notification(int p_what) {
{
// Erase.

Ref<Texture2D> icon = get_theme_icon(SNAME("Remove"), SNAME("EditorIcons"));
Ref<Texture2D> icon = get_theme_icon(animation->track_is_compressed(track) ? SNAME("Lock") : SNAME("Remove"), SNAME("EditorIcons"));

remove_rect.position.x = ofs + ((get_size().width - ofs) - icon->get_width()) / 2;
remove_rect.position.y = int(get_size().height - icon->get_height()) / 2;
Expand Down Expand Up @@ -2709,60 +2709,63 @@ void AnimationTrackEdit::gui_input(const Ref<InputEvent> &p_event) {

// Check keyframes.

float scale = timeline->get_zoom_scale();
int limit = timeline->get_name_limit();
int limit_end = get_size().width - timeline->get_buttons_width();
// Left Border including space occupied by keyframes on t=0.
int limit_start_hitbox = limit - type_icon->get_width();

if (pos.x >= limit_start_hitbox && pos.x <= limit_end) {
int key_idx = -1;
float key_distance = 1e20;
if (!animation->track_is_compressed(track)) { // Selecting compressed keyframes for editing is not possible.

// Select should happen in the opposite order of drawing for more accurate overlap select.
for (int i = animation->track_get_key_count(track) - 1; i >= 0; i--) {
Rect2 rect = get_key_rect(i, scale);
float offset = animation->track_get_key_time(track, i) - timeline->get_value();
offset = offset * scale + limit;
rect.position.x += offset;

if (rect.has_point(pos)) {
if (is_key_selectable_by_distance()) {
float distance = ABS(offset - pos.x);
if (key_idx == -1 || distance < key_distance) {
float scale = timeline->get_zoom_scale();
int limit = timeline->get_name_limit();
int limit_end = get_size().width - timeline->get_buttons_width();
// Left Border including space occupied by keyframes on t=0.
int limit_start_hitbox = limit - type_icon->get_width();

if (pos.x >= limit_start_hitbox && pos.x <= limit_end) {
int key_idx = -1;
float key_distance = 1e20;

// Select should happen in the opposite order of drawing for more accurate overlap select.
for (int i = animation->track_get_key_count(track) - 1; i >= 0; i--) {
Rect2 rect = get_key_rect(i, scale);
float offset = animation->track_get_key_time(track, i) - timeline->get_value();
offset = offset * scale + limit;
rect.position.x += offset;

if (rect.has_point(pos)) {
if (is_key_selectable_by_distance()) {
float distance = ABS(offset - pos.x);
if (key_idx == -1 || distance < key_distance) {
key_idx = i;
key_distance = distance;
}
} else {
// First one does it.
key_idx = i;
key_distance = distance;
break;
}
} else {
// First one does it.
key_idx = i;
break;
}
}
}

if (key_idx != -1) {
if (mb->is_command_pressed() || mb->is_shift_pressed()) {
if (editor->is_key_selected(track, key_idx)) {
emit_signal(SNAME("deselect_key"), key_idx);
if (key_idx != -1) {
if (mb->is_command_pressed() || mb->is_shift_pressed()) {
if (editor->is_key_selected(track, key_idx)) {
emit_signal(SNAME("deselect_key"), key_idx);
} else {
emit_signal(SNAME("select_key"), key_idx, false);
moving_selection_attempt = true;
select_single_attempt = -1;
moving_selection_from_ofs = (mb->get_position().x - limit) / timeline->get_zoom_scale();
}
} else {
emit_signal(SNAME("select_key"), key_idx, false);
if (!editor->is_key_selected(track, key_idx)) {
emit_signal(SNAME("select_key"), key_idx, true);
select_single_attempt = -1;
} else {
select_single_attempt = key_idx;
}

moving_selection_attempt = true;
select_single_attempt = -1;
moving_selection_from_ofs = (mb->get_position().x - limit) / timeline->get_zoom_scale();
}
} else {
if (!editor->is_key_selected(track, key_idx)) {
emit_signal(SNAME("select_key"), key_idx, true);
select_single_attempt = -1;
} else {
select_single_attempt = key_idx;
}

moving_selection_attempt = true;
moving_selection_from_ofs = (mb->get_position().x - limit) / timeline->get_zoom_scale();
accept_event();
}
accept_event();
}
}
}
Expand Down Expand Up @@ -2994,6 +2997,9 @@ void AnimationTrackEdit::set_in_group(bool p_enable) {
}

void AnimationTrackEdit::append_to_selection(const Rect2 &p_box, bool p_deselection) {
if (animation->track_is_compressed(track)) {
return; // Compressed keyframes can't be edited
}
// Left Border including space occupied by keyframes on t=0.
int limit_start_hitbox = timeline->get_name_limit() - type_icon->get_width();
Rect2 select_rect(limit_start_hitbox, 0, get_size().width - timeline->get_name_limit() - timeline->get_buttons_width(), get_size().height);
Expand Down Expand Up @@ -3337,6 +3343,10 @@ void AnimationTrackEditor::_timeline_changed(float p_new_pos, bool p_drag, bool
}

void AnimationTrackEditor::_track_remove_request(int p_track) {
if (animation->track_is_compressed(p_track)) {
EditorNode::get_singleton()->show_warning(TTR("Compressed tracks can't be edited or removed. Re-import the animation with compression disabled in order to edit."));
return;
}
int idx = p_track;
if (idx >= 0 && idx < animation->get_track_count()) {
undo_redo->create_action(TTR("Remove Anim Track"));
Expand Down
29 changes: 25 additions & 4 deletions editor/import/resource_importer_scene.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,13 @@ Node *ResourceImporterScene::_post_fix_node(Node *p_node, Node *p_root, Map<Ref<
}
}
}

bool use_compression = node_settings["compression/enabled"];
int anim_compression_page_size = node_settings["compression/page_size"];

if (use_compression) {
_compress_animations(ap, anim_compression_page_size);
}
}

return p_node;
Expand Down Expand Up @@ -1149,6 +1156,15 @@ void ResourceImporterScene::_optimize_animations(AnimationPlayer *anim, float p_
}
}

void ResourceImporterScene::_compress_animations(AnimationPlayer *anim, int p_page_size_kb) {
List<StringName> anim_names;
anim->get_animation_list(&anim_names);
for (const StringName &E : anim_names) {
Ref<Animation> a = anim->get_animation(E);
a->compress(p_page_size_kb * 1024);
}
}

void ResourceImporterScene::get_internal_import_options(InternalImportCategory p_category, List<ImportOption> *r_options) const {
switch (p_category) {
case INTERNAL_IMPORT_CATEGORY_NODE: {
Expand Down Expand Up @@ -1212,6 +1228,8 @@ void ResourceImporterScene::get_internal_import_options(InternalImportCategory p
r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "optimizer/max_linear_error"), 0.05));
r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "optimizer/max_angular_error"), 0.01));
r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "optimizer/max_angle"), 22));
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "compression/enabled", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compression/page_size", PROPERTY_HINT_RANGE, "4,512,1,suffix:kb"), 8));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "import_tracks/position", PROPERTY_HINT_ENUM, "IfPresent,IfPresentForAll,Never"), 1));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "import_tracks/rotation", PROPERTY_HINT_ENUM, "IfPresent,IfPresentForAll,Never"), 1));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "import_tracks/scale", PROPERTY_HINT_ENUM, "IfPresent,IfPresentForAll,Never"), 1));
Expand Down Expand Up @@ -1320,13 +1338,16 @@ bool ResourceImporterScene::get_internal_option_visibility(InternalImportCategor
}
} break;
case INTERNAL_IMPORT_CATEGORY_ANIMATION_NODE: {
if (p_option.begins_with("animation/optimizer/") && p_option != "animation/optimizer/enabled" && !bool(p_options["animation/optimizer/enabled"])) {
if (p_option.begins_with("optimizer/") && p_option != "optimizer/enabled" && !bool(p_options["optimizer/enabled"])) {
return false;
}
if (p_option.begins_with("compression/") && p_option != "compression/enabled" && !bool(p_options["compression/enabled"])) {
return false;
}

if (p_option.begins_with("animation/slice_")) {
int max_slice = p_options["animation/slices/amount"];
int slice = p_option.get_slice("/", 1).get_slice("_", 1).to_int() - 1;
if (p_option.begins_with("slice_")) {
int max_slice = p_options["slices/amount"];
int slice = p_option.get_slice("_", 1).to_int() - 1;
if (slice >= max_slice) {
return false;
}
Expand Down
1 change: 1 addition & 0 deletions editor/import/resource_importer_scene.h
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ class ResourceImporterScene : public ResourceImporter {
Ref<Animation> _save_animation_to_file(Ref<Animation> anim, bool p_save_to_file, String p_save_to_path, bool p_keep_custom_tracks);
void _create_clips(AnimationPlayer *anim, const Array &p_clips, bool p_bake_all);
void _optimize_animations(AnimationPlayer *anim, float p_max_lin_error, float p_max_ang_error, float p_max_angle);
void _compress_animations(AnimationPlayer *anim, int p_page_size_kb);

Node *pre_import(const String &p_source_file);
virtual Error import(const String &p_source_file, const String &p_save_path, const Map<StringName, Variant> &p_options, List<String> *r_platform_variants, List<String> *r_gen_files = nullptr, Variant *r_metadata = nullptr) override;
Expand Down
Loading

0 comments on commit 45e2ddb

Please sign in to comment.