Skip to content
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

Merged
merged 1 commit into from
Oct 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Comment on lines +193 to +194
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Contributor

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)

Copy link
Member Author

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.

}

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();
}
Comment on lines +106 to +129
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These methods seem pretty specific, is Vector3 the best place for them?


reduz marked this conversation as resolved.
Show resolved Hide resolved
_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