Browse Source

Implement Animation Compression

Roughly based on https://github.com/godotengine/godot-proposals/issues/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)
reduz 3 years ago
parent
commit
a69541da4c

+ 9 - 0
core/math/quaternion.cpp

@@ -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.");

+ 3 - 0
core/math/quaternion.h

@@ -72,6 +72,9 @@ public:
 	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);

+ 26 - 1
core/math/vector3.h

@@ -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 {
@@ -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;

+ 4 - 0
core/variant/variant_call.cpp

@@ -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 */
 
@@ -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 */
 

+ 14 - 0
doc/classes/Animation.xml

@@ -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" />
@@ -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" />

+ 10 - 0
doc/classes/Quaternion.xml

@@ -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>

+ 11 - 0
doc/classes/Vector3.xml

@@ -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>

+ 59 - 49
editor/animation_track_editor.cpp

@@ -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.
@@ -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.
@@ -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 {
@@ -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);
 				}
 
@@ -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 {
@@ -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;
@@ -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();
 			}
 		}
 	}
@@ -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);
@@ -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"));

+ 25 - 4
editor/import/resource_importer_scene.cpp

@@ -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;
@@ -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: {
@@ -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));
@@ -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;
 				}

+ 1 - 0
editor/import/resource_importer_scene.h

@@ -261,6 +261,7 @@ public:
 	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;

+ 1608 - 39
scene/resources/animation.cpp

@@ -30,13 +30,40 @@
 
 #include "animation.h"
 
+#include "core/io/marshalls.h"
 #include "core/math/geometry_3d.h"
 #include "scene/scene_string_names.h"
 
 bool Animation::_set(const StringName &p_name, const Variant &p_value) {
 	String name = p_name;
 
-	if (name.begins_with("tracks/")) {
+	if (p_name == SNAME("_compression")) {
+		ERR_FAIL_COND_V(tracks.size() > 0, false); //can only set compression if no tracks exist
+		Dictionary comp = p_value;
+		ERR_FAIL_COND_V(!comp.has("fps"), false);
+		ERR_FAIL_COND_V(!comp.has("bounds"), false);
+		ERR_FAIL_COND_V(!comp.has("pages"), false);
+		ERR_FAIL_COND_V(!comp.has("format_version"), false);
+		uint32_t format_version = comp["format_version"];
+		ERR_FAIL_COND_V(format_version > Compression::FORMAT_VERSION, false); // version does not match this supported version
+		compression.fps = comp["fps"];
+		Array bounds = comp["bounds"];
+		compression.bounds.resize(bounds.size());
+		for (int i = 0; i < bounds.size(); i++) {
+			compression.bounds[i] = bounds[i];
+		}
+		Array pages = comp["pages"];
+		compression.pages.resize(pages.size());
+		for (int i = 0; i < pages.size(); i++) {
+			Dictionary page = pages[i];
+			ERR_FAIL_COND_V(!page.has("data"), false);
+			ERR_FAIL_COND_V(!page.has("time_offset"), false);
+			compression.pages[i].data = page["data"];
+			compression.pages[i].time_offset = page["time_offset"];
+		}
+		compression.enabled = true;
+		return true;
+	} else if (name.begins_with("tracks/")) {
 		int track = name.get_slicec('/', 1).to_int();
 		String what = name.get_slicec('/', 2);
 
@@ -72,6 +99,34 @@ bool Animation::_set(const StringName &p_name, const Variant &p_value) {
 
 		if (what == "path") {
 			track_set_path(track, p_value);
+		} else if (what == "compressed_track") {
+			int index = p_value;
+			ERR_FAIL_COND_V(!compression.enabled, false);
+			ERR_FAIL_UNSIGNED_INDEX_V((uint32_t)index, compression.bounds.size(), false);
+			Track *t = tracks[track];
+			t->interpolation = INTERPOLATION_LINEAR; //only linear supported
+			switch (t->type) {
+				case TYPE_POSITION_3D: {
+					PositionTrack *tt = static_cast<PositionTrack *>(t);
+					tt->compressed_track = index;
+				} break;
+				case TYPE_ROTATION_3D: {
+					RotationTrack *rt = static_cast<RotationTrack *>(t);
+					rt->compressed_track = index;
+				} break;
+				case TYPE_SCALE_3D: {
+					ScaleTrack *st = static_cast<ScaleTrack *>(t);
+					st->compressed_track = index;
+				} break;
+				case TYPE_BLEND_SHAPE: {
+					BlendShapeTrack *bst = static_cast<BlendShapeTrack *>(t);
+					bst->compressed_track = index;
+				} break;
+				default: {
+					return false;
+				}
+			}
+			return true;
 		} else if (what == "interp") {
 			track_set_interpolation_type(track, InterpolationType(p_value.operator int()));
 		} else if (what == "loop_wrap") {
@@ -369,7 +424,30 @@ bool Animation::_set(const StringName &p_name, const Variant &p_value) {
 bool Animation::_get(const StringName &p_name, Variant &r_ret) const {
 	String name = p_name;
 
-	if (name == "length") {
+	if (p_name == SNAME("_compression")) {
+		ERR_FAIL_COND_V(!compression.enabled, false);
+		Dictionary comp;
+		comp["fps"] = compression.fps;
+		Array bounds;
+		bounds.resize(compression.bounds.size());
+		for (uint32_t i = 0; i < compression.bounds.size(); i++) {
+			bounds[i] = compression.bounds[i];
+		}
+		comp["bounds"] = bounds;
+		Array pages;
+		pages.resize(compression.pages.size());
+		for (uint32_t i = 0; i < compression.pages.size(); i++) {
+			Dictionary page;
+			page["data"] = compression.pages[i].data;
+			page["time_offset"] = compression.pages[i].time_offset;
+			pages[i] = page;
+		}
+		comp["pages"] = pages;
+		comp["format_version"] = Compression::FORMAT_VERSION;
+
+		r_ret = comp;
+		return true;
+	} else if (name == "length") {
 		r_ret = length;
 	} else if (name == "loop") {
 		r_ret = loop;
@@ -414,6 +492,34 @@ bool Animation::_get(const StringName &p_name, Variant &r_ret) const {
 
 		} else if (what == "path") {
 			r_ret = track_get_path(track);
+		} else if (what == "compressed_track") {
+			ERR_FAIL_COND_V(!compression.enabled, false);
+			Track *t = tracks[track];
+			switch (t->type) {
+				case TYPE_POSITION_3D: {
+					PositionTrack *tt = static_cast<PositionTrack *>(t);
+					r_ret = tt->compressed_track;
+				} break;
+				case TYPE_ROTATION_3D: {
+					RotationTrack *rt = static_cast<RotationTrack *>(t);
+					r_ret = rt->compressed_track;
+				} break;
+				case TYPE_SCALE_3D: {
+					ScaleTrack *st = static_cast<ScaleTrack *>(t);
+					r_ret = st->compressed_track;
+				} break;
+				case TYPE_BLEND_SHAPE: {
+					BlendShapeTrack *bst = static_cast<BlendShapeTrack *>(t);
+					r_ret = bst->compressed_track;
+				} break;
+				default: {
+					r_ret = Variant();
+					ERR_FAIL_V(false);
+				}
+			}
+
+			return true;
+
 		} else if (what == "interp") {
 			r_ret = track_get_interpolation_type(track);
 		} else if (what == "loop_wrap") {
@@ -692,14 +798,21 @@ bool Animation::_get(const StringName &p_name, Variant &r_ret) const {
 }
 
 void Animation::_get_property_list(List<PropertyInfo> *p_list) const {
+	if (compression.enabled) {
+		p_list->push_back(PropertyInfo(Variant::DICTIONARY, "_compression", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL));
+	}
 	for (int i = 0; i < tracks.size(); i++) {
 		p_list->push_back(PropertyInfo(Variant::STRING, "tracks/" + itos(i) + "/type", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL));
-		p_list->push_back(PropertyInfo(Variant::NODE_PATH, "tracks/" + itos(i) + "/path", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL));
-		p_list->push_back(PropertyInfo(Variant::INT, "tracks/" + itos(i) + "/interp", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL));
-		p_list->push_back(PropertyInfo(Variant::BOOL, "tracks/" + itos(i) + "/loop_wrap", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL));
 		p_list->push_back(PropertyInfo(Variant::BOOL, "tracks/" + itos(i) + "/imported", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL));
 		p_list->push_back(PropertyInfo(Variant::BOOL, "tracks/" + itos(i) + "/enabled", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL));
-		p_list->push_back(PropertyInfo(Variant::ARRAY, "tracks/" + itos(i) + "/keys", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL));
+		p_list->push_back(PropertyInfo(Variant::NODE_PATH, "tracks/" + itos(i) + "/path", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL));
+		if (track_is_compressed(i)) {
+			p_list->push_back(PropertyInfo(Variant::INT, "tracks/" + itos(i) + "/compressed_track", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL));
+		} else {
+			p_list->push_back(PropertyInfo(Variant::INT, "tracks/" + itos(i) + "/interp", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL));
+			p_list->push_back(PropertyInfo(Variant::BOOL, "tracks/" + itos(i) + "/loop_wrap", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL));
+			p_list->push_back(PropertyInfo(Variant::ARRAY, "tracks/" + itos(i) + "/keys", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL));
+		}
 	}
 }
 
@@ -765,21 +878,25 @@ void Animation::remove_track(int p_track) {
 	switch (t->type) {
 		case TYPE_POSITION_3D: {
 			PositionTrack *tt = static_cast<PositionTrack *>(t);
+			ERR_FAIL_COND_MSG(tt->compressed_track >= 0, "Compressed tracks can't be manually removed. Call clear() to get rid of compression first.");
 			_clear(tt->positions);
 
 		} break;
 		case TYPE_ROTATION_3D: {
 			RotationTrack *rt = static_cast<RotationTrack *>(t);
+			ERR_FAIL_COND_MSG(rt->compressed_track >= 0, "Compressed tracks can't be manually removed. Call clear() to get rid of compression first.");
 			_clear(rt->rotations);
 
 		} break;
 		case TYPE_SCALE_3D: {
 			ScaleTrack *st = static_cast<ScaleTrack *>(t);
+			ERR_FAIL_COND_MSG(st->compressed_track >= 0, "Compressed tracks can't be manually removed. Call clear() to get rid of compression first.");
 			_clear(st->scales);
 
 		} break;
 		case TYPE_BLEND_SHAPE: {
 			BlendShapeTrack *bst = static_cast<BlendShapeTrack *>(t);
+			ERR_FAIL_COND_MSG(bst->compressed_track >= 0, "Compressed tracks can't be manually removed. Call clear() to get rid of compression first.");
 			_clear(bst->blend_shapes);
 
 		} break;
@@ -907,6 +1024,8 @@ int Animation::position_track_insert_key(int p_track, double p_time, const Vecto
 
 	PositionTrack *tt = static_cast<PositionTrack *>(t);
 
+	ERR_FAIL_COND_V(tt->compressed_track >= 0, -1);
+
 	TKey<Vector3> tkey;
 	tkey.time = p_time;
 	tkey.value = p_position;
@@ -922,6 +1041,19 @@ Error Animation::position_track_get_key(int p_track, int p_key, Vector3 *r_posit
 
 	PositionTrack *tt = static_cast<PositionTrack *>(t);
 	ERR_FAIL_COND_V(t->type != TYPE_POSITION_3D, ERR_INVALID_PARAMETER);
+
+	if (tt->compressed_track >= 0) {
+		Vector3i key;
+		double time;
+		bool fetch_success = _fetch_compressed_by_index<3>(tt->compressed_track, p_key, key, time);
+		if (!fetch_success) {
+			return ERR_INVALID_PARAMETER;
+		}
+
+		*r_position = _uncompress_pos_scale(tt->compressed_track, key);
+		return OK;
+	}
+
 	ERR_FAIL_INDEX_V(p_key, tt->positions.size(), ERR_INVALID_PARAMETER);
 
 	*r_position = tt->positions[p_key].value;
@@ -936,6 +1068,14 @@ Error Animation::position_track_interpolate(int p_track, double p_time, Vector3
 
 	PositionTrack *tt = static_cast<PositionTrack *>(t);
 
+	if (tt->compressed_track >= 0) {
+		if (_pos_scale_interpolate_compressed(tt->compressed_track, p_time, *r_interpolation)) {
+			return OK;
+		} else {
+			return ERR_UNAVAILABLE;
+		}
+	}
+
 	bool ok = false;
 
 	Vector3 tk = _interpolate(tt->positions, p_time, tt->interpolation, tt->loop_wrap, &ok);
@@ -956,6 +1096,8 @@ int Animation::rotation_track_insert_key(int p_track, double p_time, const Quate
 
 	RotationTrack *rt = static_cast<RotationTrack *>(t);
 
+	ERR_FAIL_COND_V(rt->compressed_track >= 0, -1);
+
 	TKey<Quaternion> tkey;
 	tkey.time = p_time;
 	tkey.value = p_rotation;
@@ -971,6 +1113,19 @@ Error Animation::rotation_track_get_key(int p_track, int p_key, Quaternion *r_ro
 
 	RotationTrack *rt = static_cast<RotationTrack *>(t);
 	ERR_FAIL_COND_V(t->type != TYPE_ROTATION_3D, ERR_INVALID_PARAMETER);
+
+	if (rt->compressed_track >= 0) {
+		Vector3i key;
+		double time;
+		bool fetch_success = _fetch_compressed_by_index<3>(rt->compressed_track, p_key, key, time);
+		if (!fetch_success) {
+			return ERR_INVALID_PARAMETER;
+		}
+
+		*r_rotation = _uncompress_quaternion(key);
+		return OK;
+	}
+
 	ERR_FAIL_INDEX_V(p_key, rt->rotations.size(), ERR_INVALID_PARAMETER);
 
 	*r_rotation = rt->rotations[p_key].value;
@@ -985,6 +1140,14 @@ Error Animation::rotation_track_interpolate(int p_track, double p_time, Quaterni
 
 	RotationTrack *rt = static_cast<RotationTrack *>(t);
 
+	if (rt->compressed_track >= 0) {
+		if (_rotation_interpolate_compressed(rt->compressed_track, p_time, *r_interpolation)) {
+			return OK;
+		} else {
+			return ERR_UNAVAILABLE;
+		}
+	}
+
 	bool ok = false;
 
 	Quaternion tk = _interpolate(rt->rotations, p_time, rt->interpolation, rt->loop_wrap, &ok);
@@ -1005,6 +1168,8 @@ int Animation::scale_track_insert_key(int p_track, double p_time, const Vector3
 
 	ScaleTrack *st = static_cast<ScaleTrack *>(t);
 
+	ERR_FAIL_COND_V(st->compressed_track >= 0, -1);
+
 	TKey<Vector3> tkey;
 	tkey.time = p_time;
 	tkey.value = p_scale;
@@ -1020,6 +1185,19 @@ Error Animation::scale_track_get_key(int p_track, int p_key, Vector3 *r_scale) c
 
 	ScaleTrack *st = static_cast<ScaleTrack *>(t);
 	ERR_FAIL_COND_V(t->type != TYPE_SCALE_3D, ERR_INVALID_PARAMETER);
+
+	if (st->compressed_track >= 0) {
+		Vector3i key;
+		double time;
+		bool fetch_success = _fetch_compressed_by_index<3>(st->compressed_track, p_key, key, time);
+		if (!fetch_success) {
+			return ERR_INVALID_PARAMETER;
+		}
+
+		*r_scale = _uncompress_pos_scale(st->compressed_track, key);
+		return OK;
+	}
+
 	ERR_FAIL_INDEX_V(p_key, st->scales.size(), ERR_INVALID_PARAMETER);
 
 	*r_scale = st->scales[p_key].value;
@@ -1034,6 +1212,14 @@ Error Animation::scale_track_interpolate(int p_track, double p_time, Vector3 *r_
 
 	ScaleTrack *st = static_cast<ScaleTrack *>(t);
 
+	if (st->compressed_track >= 0) {
+		if (_pos_scale_interpolate_compressed(st->compressed_track, p_time, *r_interpolation)) {
+			return OK;
+		} else {
+			return ERR_UNAVAILABLE;
+		}
+	}
+
 	bool ok = false;
 
 	Vector3 tk = _interpolate(st->scales, p_time, st->interpolation, st->loop_wrap, &ok);
@@ -1052,6 +1238,8 @@ int Animation::blend_shape_track_insert_key(int p_track, double p_time, float p_
 
 	BlendShapeTrack *st = static_cast<BlendShapeTrack *>(t);
 
+	ERR_FAIL_COND_V(st->compressed_track >= 0, -1);
+
 	TKey<float> tkey;
 	tkey.time = p_time;
 	tkey.value = p_blend_shape;
@@ -1065,11 +1253,24 @@ Error Animation::blend_shape_track_get_key(int p_track, int p_key, float *r_blen
 	ERR_FAIL_INDEX_V(p_track, tracks.size(), ERR_INVALID_PARAMETER);
 	Track *t = tracks[p_track];
 
-	BlendShapeTrack *st = static_cast<BlendShapeTrack *>(t);
+	BlendShapeTrack *bst = static_cast<BlendShapeTrack *>(t);
 	ERR_FAIL_COND_V(t->type != TYPE_BLEND_SHAPE, ERR_INVALID_PARAMETER);
-	ERR_FAIL_INDEX_V(p_key, st->blend_shapes.size(), ERR_INVALID_PARAMETER);
 
-	*r_blend_shape = st->blend_shapes[p_key].value;
+	if (bst->compressed_track >= 0) {
+		Vector3i key;
+		double time;
+		bool fetch_success = _fetch_compressed_by_index<1>(bst->compressed_track, p_key, key, time);
+		if (!fetch_success) {
+			return ERR_INVALID_PARAMETER;
+		}
+
+		*r_blend_shape = _uncompress_blend_shape(key);
+		return OK;
+	}
+
+	ERR_FAIL_INDEX_V(p_key, bst->blend_shapes.size(), ERR_INVALID_PARAMETER);
+
+	*r_blend_shape = bst->blend_shapes[p_key].value;
 
 	return OK;
 }
@@ -1079,11 +1280,19 @@ Error Animation::blend_shape_track_interpolate(int p_track, double p_time, float
 	Track *t = tracks[p_track];
 	ERR_FAIL_COND_V(t->type != TYPE_BLEND_SHAPE, ERR_INVALID_PARAMETER);
 
-	BlendShapeTrack *st = static_cast<BlendShapeTrack *>(t);
+	BlendShapeTrack *bst = static_cast<BlendShapeTrack *>(t);
+
+	if (bst->compressed_track >= 0) {
+		if (_blend_shape_interpolate_compressed(bst->compressed_track, p_time, *r_interpolation)) {
+			return OK;
+		} else {
+			return ERR_UNAVAILABLE;
+		}
+	}
 
 	bool ok = false;
 
-	float tk = _interpolate(st->blend_shapes, p_time, st->interpolation, st->loop_wrap, &ok);
+	float tk = _interpolate(bst->blend_shapes, p_time, bst->interpolation, bst->loop_wrap, &ok);
 
 	if (!ok) {
 		return ERR_UNAVAILABLE;
@@ -1105,24 +1314,36 @@ void Animation::track_remove_key(int p_track, int p_idx) {
 	switch (t->type) {
 		case TYPE_POSITION_3D: {
 			PositionTrack *tt = static_cast<PositionTrack *>(t);
+
+			ERR_FAIL_COND(tt->compressed_track >= 0);
+
 			ERR_FAIL_INDEX(p_idx, tt->positions.size());
 			tt->positions.remove(p_idx);
 
 		} break;
 		case TYPE_ROTATION_3D: {
 			RotationTrack *rt = static_cast<RotationTrack *>(t);
+
+			ERR_FAIL_COND(rt->compressed_track >= 0);
+
 			ERR_FAIL_INDEX(p_idx, rt->rotations.size());
 			rt->rotations.remove(p_idx);
 
 		} break;
 		case TYPE_SCALE_3D: {
 			ScaleTrack *st = static_cast<ScaleTrack *>(t);
+
+			ERR_FAIL_COND(st->compressed_track >= 0);
+
 			ERR_FAIL_INDEX(p_idx, st->scales.size());
 			st->scales.remove(p_idx);
 
 		} break;
 		case TYPE_BLEND_SHAPE: {
 			BlendShapeTrack *bst = static_cast<BlendShapeTrack *>(t);
+
+			ERR_FAIL_COND(bst->compressed_track >= 0);
+
 			ERR_FAIL_INDEX(p_idx, bst->blend_shapes.size());
 			bst->blend_shapes.remove(p_idx);
 
@@ -1169,6 +1390,21 @@ int Animation::track_find_key(int p_track, double p_time, bool p_exact) const {
 	switch (t->type) {
 		case TYPE_POSITION_3D: {
 			PositionTrack *tt = static_cast<PositionTrack *>(t);
+
+			if (tt->compressed_track >= 0) {
+				double time;
+				double time_next;
+				Vector3i key;
+				Vector3i key_next;
+				uint32_t key_index;
+				bool fetch_compressed_success = _fetch_compressed<3>(tt->compressed_track, p_time, key, time, key_next, time_next, &key_index);
+				ERR_FAIL_COND_V(!fetch_compressed_success, -1);
+				if (p_exact && time != p_time) {
+					return -1;
+				}
+				return key_index;
+			}
+
 			int k = _find(tt->positions, p_time);
 			if (k < 0 || k >= tt->positions.size()) {
 				return -1;
@@ -1181,6 +1417,21 @@ int Animation::track_find_key(int p_track, double p_time, bool p_exact) const {
 		} break;
 		case TYPE_ROTATION_3D: {
 			RotationTrack *rt = static_cast<RotationTrack *>(t);
+
+			if (rt->compressed_track >= 0) {
+				double time;
+				double time_next;
+				Vector3i key;
+				Vector3i key_next;
+				uint32_t key_index;
+				bool fetch_compressed_success = _fetch_compressed<3>(rt->compressed_track, p_time, key, time, key_next, time_next, &key_index);
+				ERR_FAIL_COND_V(!fetch_compressed_success, -1);
+				if (p_exact && time != p_time) {
+					return -1;
+				}
+				return key_index;
+			}
+
 			int k = _find(rt->rotations, p_time);
 			if (k < 0 || k >= rt->rotations.size()) {
 				return -1;
@@ -1193,6 +1444,21 @@ int Animation::track_find_key(int p_track, double p_time, bool p_exact) const {
 		} break;
 		case TYPE_SCALE_3D: {
 			ScaleTrack *st = static_cast<ScaleTrack *>(t);
+
+			if (st->compressed_track >= 0) {
+				double time;
+				double time_next;
+				Vector3i key;
+				Vector3i key_next;
+				uint32_t key_index;
+				bool fetch_compressed_success = _fetch_compressed<3>(st->compressed_track, p_time, key, time, key_next, time_next, &key_index);
+				ERR_FAIL_COND_V(!fetch_compressed_success, -1);
+				if (p_exact && time != p_time) {
+					return -1;
+				}
+				return key_index;
+			}
+
 			int k = _find(st->scales, p_time);
 			if (k < 0 || k >= st->scales.size()) {
 				return -1;
@@ -1205,6 +1471,21 @@ int Animation::track_find_key(int p_track, double p_time, bool p_exact) const {
 		} break;
 		case TYPE_BLEND_SHAPE: {
 			BlendShapeTrack *bst = static_cast<BlendShapeTrack *>(t);
+
+			if (bst->compressed_track >= 0) {
+				double time;
+				double time_next;
+				Vector3i key;
+				Vector3i key_next;
+				uint32_t key_index;
+				bool fetch_compressed_success = _fetch_compressed<1>(bst->compressed_track, p_time, key, time, key_next, time_next, &key_index);
+				ERR_FAIL_COND_V(!fetch_compressed_success, -1);
+				if (p_exact && time != p_time) {
+					return -1;
+				}
+				return key_index;
+			}
+
 			int k = _find(bst->blend_shapes, p_time);
 			if (k < 0 || k >= bst->blend_shapes.size()) {
 				return -1;
@@ -1392,18 +1673,30 @@ int Animation::track_get_key_count(int p_track) const {
 	switch (t->type) {
 		case TYPE_POSITION_3D: {
 			PositionTrack *tt = static_cast<PositionTrack *>(t);
+			if (tt->compressed_track >= 0) {
+				return _get_compressed_key_count(tt->compressed_track);
+			}
 			return tt->positions.size();
 		} break;
 		case TYPE_ROTATION_3D: {
 			RotationTrack *rt = static_cast<RotationTrack *>(t);
+			if (rt->compressed_track >= 0) {
+				return _get_compressed_key_count(rt->compressed_track);
+			}
 			return rt->rotations.size();
 		} break;
 		case TYPE_SCALE_3D: {
 			ScaleTrack *st = static_cast<ScaleTrack *>(t);
+			if (st->compressed_track >= 0) {
+				return _get_compressed_key_count(st->compressed_track);
+			}
 			return st->scales.size();
 		} break;
 		case TYPE_BLEND_SHAPE: {
 			BlendShapeTrack *bst = static_cast<BlendShapeTrack *>(t);
+			if (bst->compressed_track >= 0) {
+				return _get_compressed_key_count(bst->compressed_track);
+			}
 			return bst->blend_shapes.size();
 		} break;
 		case TYPE_VALUE: {
@@ -1438,28 +1731,24 @@ Variant Animation::track_get_key_value(int p_track, int p_key_idx) const {
 
 	switch (t->type) {
 		case TYPE_POSITION_3D: {
-			PositionTrack *tt = static_cast<PositionTrack *>(t);
-			ERR_FAIL_INDEX_V(p_key_idx, tt->positions.size(), Variant());
-
-			return tt->positions[p_key_idx].value;
+			Vector3 value;
+			position_track_get_key(p_track, p_key_idx, &value);
+			return value;
 		} break;
 		case TYPE_ROTATION_3D: {
-			RotationTrack *rt = static_cast<RotationTrack *>(t);
-			ERR_FAIL_INDEX_V(p_key_idx, rt->rotations.size(), Variant());
-
-			return rt->rotations[p_key_idx].value;
+			Quaternion value;
+			rotation_track_get_key(p_track, p_key_idx, &value);
+			return value;
 		} break;
 		case TYPE_SCALE_3D: {
-			ScaleTrack *st = static_cast<ScaleTrack *>(t);
-			ERR_FAIL_INDEX_V(p_key_idx, st->scales.size(), Variant());
-
-			return st->scales[p_key_idx].value;
+			Vector3 value;
+			scale_track_get_key(p_track, p_key_idx, &value);
+			return value;
 		} break;
 		case TYPE_BLEND_SHAPE: {
-			BlendShapeTrack *bst = static_cast<BlendShapeTrack *>(t);
-			ERR_FAIL_INDEX_V(p_key_idx, bst->blend_shapes.size(), Variant());
-
-			return bst->blend_shapes[p_key_idx].value;
+			float value;
+			blend_shape_track_get_key(p_track, p_key_idx, &value);
+			return value;
 		} break;
 		case TYPE_VALUE: {
 			ValueTrack *vt = static_cast<ValueTrack *>(t);
@@ -1520,21 +1809,49 @@ double Animation::track_get_key_time(int p_track, int p_key_idx) const {
 	switch (t->type) {
 		case TYPE_POSITION_3D: {
 			PositionTrack *tt = static_cast<PositionTrack *>(t);
+			if (tt->compressed_track >= 0) {
+				Vector3i value;
+				double time;
+				bool fetch_compressed_success = _fetch_compressed_by_index<3>(tt->compressed_track, p_key_idx, value, time);
+				ERR_FAIL_COND_V(!fetch_compressed_success, false);
+				return time;
+			}
 			ERR_FAIL_INDEX_V(p_key_idx, tt->positions.size(), -1);
 			return tt->positions[p_key_idx].time;
 		} break;
 		case TYPE_ROTATION_3D: {
 			RotationTrack *rt = static_cast<RotationTrack *>(t);
+			if (rt->compressed_track >= 0) {
+				Vector3i value;
+				double time;
+				bool fetch_compressed_success = _fetch_compressed_by_index<3>(rt->compressed_track, p_key_idx, value, time);
+				ERR_FAIL_COND_V(!fetch_compressed_success, false);
+				return time;
+			}
 			ERR_FAIL_INDEX_V(p_key_idx, rt->rotations.size(), -1);
 			return rt->rotations[p_key_idx].time;
 		} break;
 		case TYPE_SCALE_3D: {
 			ScaleTrack *st = static_cast<ScaleTrack *>(t);
+			if (st->compressed_track >= 0) {
+				Vector3i value;
+				double time;
+				bool fetch_compressed_success = _fetch_compressed_by_index<3>(st->compressed_track, p_key_idx, value, time);
+				ERR_FAIL_COND_V(!fetch_compressed_success, false);
+				return time;
+			}
 			ERR_FAIL_INDEX_V(p_key_idx, st->scales.size(), -1);
 			return st->scales[p_key_idx].time;
 		} break;
 		case TYPE_BLEND_SHAPE: {
 			BlendShapeTrack *bst = static_cast<BlendShapeTrack *>(t);
+			if (bst->compressed_track >= 0) {
+				Vector3i value;
+				double time;
+				bool fetch_compressed_success = _fetch_compressed_by_index<1>(bst->compressed_track, p_key_idx, value, time);
+				ERR_FAIL_COND_V(!fetch_compressed_success, false);
+				return time;
+			}
 			ERR_FAIL_INDEX_V(p_key_idx, bst->blend_shapes.size(), -1);
 			return bst->blend_shapes[p_key_idx].time;
 		} break;
@@ -1580,6 +1897,7 @@ void Animation::track_set_key_time(int p_track, int p_key_idx, double p_time) {
 	switch (t->type) {
 		case TYPE_POSITION_3D: {
 			PositionTrack *tt = static_cast<PositionTrack *>(t);
+			ERR_FAIL_COND(tt->compressed_track >= 0);
 			ERR_FAIL_INDEX(p_key_idx, tt->positions.size());
 			TKey<Vector3> key = tt->positions[p_key_idx];
 			key.time = p_time;
@@ -1589,6 +1907,7 @@ void Animation::track_set_key_time(int p_track, int p_key_idx, double p_time) {
 		}
 		case TYPE_ROTATION_3D: {
 			RotationTrack *tt = static_cast<RotationTrack *>(t);
+			ERR_FAIL_COND(tt->compressed_track >= 0);
 			ERR_FAIL_INDEX(p_key_idx, tt->rotations.size());
 			TKey<Quaternion> key = tt->rotations[p_key_idx];
 			key.time = p_time;
@@ -1598,6 +1917,7 @@ void Animation::track_set_key_time(int p_track, int p_key_idx, double p_time) {
 		}
 		case TYPE_SCALE_3D: {
 			ScaleTrack *tt = static_cast<ScaleTrack *>(t);
+			ERR_FAIL_COND(tt->compressed_track >= 0);
 			ERR_FAIL_INDEX(p_key_idx, tt->scales.size());
 			TKey<Vector3> key = tt->scales[p_key_idx];
 			key.time = p_time;
@@ -1607,6 +1927,7 @@ void Animation::track_set_key_time(int p_track, int p_key_idx, double p_time) {
 		}
 		case TYPE_BLEND_SHAPE: {
 			BlendShapeTrack *tt = static_cast<BlendShapeTrack *>(t);
+			ERR_FAIL_COND(tt->compressed_track >= 0);
 			ERR_FAIL_INDEX(p_key_idx, tt->blend_shapes.size());
 			TKey<float> key = tt->blend_shapes[p_key_idx];
 			key.time = p_time;
@@ -1671,21 +1992,33 @@ real_t Animation::track_get_key_transition(int p_track, int p_key_idx) const {
 	switch (t->type) {
 		case TYPE_POSITION_3D: {
 			PositionTrack *tt = static_cast<PositionTrack *>(t);
+			if (tt->compressed_track >= 0) {
+				return 1.0;
+			}
 			ERR_FAIL_INDEX_V(p_key_idx, tt->positions.size(), -1);
 			return tt->positions[p_key_idx].transition;
 		} break;
 		case TYPE_ROTATION_3D: {
 			RotationTrack *rt = static_cast<RotationTrack *>(t);
+			if (rt->compressed_track >= 0) {
+				return 1.0;
+			}
 			ERR_FAIL_INDEX_V(p_key_idx, rt->rotations.size(), -1);
 			return rt->rotations[p_key_idx].transition;
 		} break;
 		case TYPE_SCALE_3D: {
 			ScaleTrack *st = static_cast<ScaleTrack *>(t);
+			if (st->compressed_track >= 0) {
+				return 1.0;
+			}
 			ERR_FAIL_INDEX_V(p_key_idx, st->scales.size(), -1);
 			return st->scales[p_key_idx].transition;
 		} break;
 		case TYPE_BLEND_SHAPE: {
 			BlendShapeTrack *bst = static_cast<BlendShapeTrack *>(t);
+			if (bst->compressed_track >= 0) {
+				return 1.0;
+			}
 			ERR_FAIL_INDEX_V(p_key_idx, bst->blend_shapes.size(), -1);
 			return bst->blend_shapes[p_key_idx].transition;
 		} break;
@@ -1715,6 +2048,35 @@ real_t Animation::track_get_key_transition(int p_track, int p_key_idx) const {
 	ERR_FAIL_V(0);
 }
 
+bool Animation::track_is_compressed(int p_track) const {
+	ERR_FAIL_INDEX_V(p_track, tracks.size(), false);
+	Track *t = tracks[p_track];
+
+	switch (t->type) {
+		case TYPE_POSITION_3D: {
+			PositionTrack *tt = static_cast<PositionTrack *>(t);
+			return tt->compressed_track >= 0;
+		} break;
+		case TYPE_ROTATION_3D: {
+			RotationTrack *rt = static_cast<RotationTrack *>(t);
+			return rt->compressed_track >= 0;
+		} break;
+		case TYPE_SCALE_3D: {
+			ScaleTrack *st = static_cast<ScaleTrack *>(t);
+			return st->compressed_track >= 0;
+		} break;
+		case TYPE_BLEND_SHAPE: {
+			BlendShapeTrack *bst = static_cast<BlendShapeTrack *>(t);
+			return bst->compressed_track >= 0;
+		} break;
+		default: {
+			return false; //animation does not really use transitions
+		} break;
+	}
+
+	ERR_FAIL_V(false);
+}
+
 void Animation::track_set_key_value(int p_track, int p_key_idx, const Variant &p_value) {
 	ERR_FAIL_INDEX(p_track, tracks.size());
 	Track *t = tracks[p_track];
@@ -1723,6 +2085,7 @@ void Animation::track_set_key_value(int p_track, int p_key_idx, const Variant &p
 		case TYPE_POSITION_3D: {
 			ERR_FAIL_COND((p_value.get_type() != Variant::VECTOR3) && (p_value.get_type() != Variant::VECTOR3I));
 			PositionTrack *tt = static_cast<PositionTrack *>(t);
+			ERR_FAIL_COND(tt->compressed_track >= 0);
 			ERR_FAIL_INDEX(p_key_idx, tt->positions.size());
 
 			tt->positions.write[p_key_idx].value = p_value;
@@ -1731,6 +2094,7 @@ void Animation::track_set_key_value(int p_track, int p_key_idx, const Variant &p
 		case TYPE_ROTATION_3D: {
 			ERR_FAIL_COND((p_value.get_type() != Variant::QUATERNION) && (p_value.get_type() != Variant::BASIS));
 			RotationTrack *rt = static_cast<RotationTrack *>(t);
+			ERR_FAIL_COND(rt->compressed_track >= 0);
 			ERR_FAIL_INDEX(p_key_idx, rt->rotations.size());
 
 			rt->rotations.write[p_key_idx].value = p_value;
@@ -1739,6 +2103,7 @@ void Animation::track_set_key_value(int p_track, int p_key_idx, const Variant &p
 		case TYPE_SCALE_3D: {
 			ERR_FAIL_COND((p_value.get_type() != Variant::VECTOR3) && (p_value.get_type() != Variant::VECTOR3I));
 			ScaleTrack *st = static_cast<ScaleTrack *>(t);
+			ERR_FAIL_COND(st->compressed_track >= 0);
 			ERR_FAIL_INDEX(p_key_idx, st->scales.size());
 
 			st->scales.write[p_key_idx].value = p_value;
@@ -1747,6 +2112,7 @@ void Animation::track_set_key_value(int p_track, int p_key_idx, const Variant &p
 		case TYPE_BLEND_SHAPE: {
 			ERR_FAIL_COND((p_value.get_type() != Variant::FLOAT) && (p_value.get_type() != Variant::INT));
 			BlendShapeTrack *bst = static_cast<BlendShapeTrack *>(t);
+			ERR_FAIL_COND(bst->compressed_track >= 0);
 			ERR_FAIL_INDEX(p_key_idx, bst->blend_shapes.size());
 
 			bst->blend_shapes.write[p_key_idx].value = p_value;
@@ -1820,21 +2186,25 @@ void Animation::track_set_key_transition(int p_track, int p_key_idx, real_t p_tr
 	switch (t->type) {
 		case TYPE_POSITION_3D: {
 			PositionTrack *tt = static_cast<PositionTrack *>(t);
+			ERR_FAIL_COND(tt->compressed_track >= 0);
 			ERR_FAIL_INDEX(p_key_idx, tt->positions.size());
 			tt->positions.write[p_key_idx].transition = p_transition;
 		} break;
 		case TYPE_ROTATION_3D: {
 			RotationTrack *rt = static_cast<RotationTrack *>(t);
+			ERR_FAIL_COND(rt->compressed_track >= 0);
 			ERR_FAIL_INDEX(p_key_idx, rt->rotations.size());
 			rt->rotations.write[p_key_idx].transition = p_transition;
 		} break;
 		case TYPE_SCALE_3D: {
 			ScaleTrack *st = static_cast<ScaleTrack *>(t);
+			ERR_FAIL_COND(st->compressed_track >= 0);
 			ERR_FAIL_INDEX(p_key_idx, st->scales.size());
 			st->scales.write[p_key_idx].transition = p_transition;
 		} break;
 		case TYPE_BLEND_SHAPE: {
 			BlendShapeTrack *bst = static_cast<BlendShapeTrack *>(t);
+			ERR_FAIL_COND(bst->compressed_track >= 0);
 			ERR_FAIL_INDEX(p_key_idx, bst->blend_shapes.size());
 			bst->blend_shapes.write[p_key_idx].transition = p_transition;
 		} break;
@@ -2334,26 +2704,50 @@ void Animation::track_get_key_indices_in_range(int p_track, double p_time, doubl
 			switch (t->type) {
 				case TYPE_POSITION_3D: {
 					const PositionTrack *tt = static_cast<const PositionTrack *>(t);
-					_track_get_key_indices_in_range(tt->positions, from_time, length, p_indices);
-					_track_get_key_indices_in_range(tt->positions, 0, to_time, p_indices);
+					if (tt->compressed_track >= 0) {
+						_get_compressed_key_indices_in_range<3>(tt->compressed_track, from_time, length, p_indices);
+						_get_compressed_key_indices_in_range<3>(tt->compressed_track, 0, to_time, p_indices);
+
+					} else {
+						_track_get_key_indices_in_range(tt->positions, from_time, length, p_indices);
+						_track_get_key_indices_in_range(tt->positions, 0, to_time, p_indices);
+					}
 
 				} break;
 				case TYPE_ROTATION_3D: {
 					const RotationTrack *rt = static_cast<const RotationTrack *>(t);
-					_track_get_key_indices_in_range(rt->rotations, from_time, length, p_indices);
-					_track_get_key_indices_in_range(rt->rotations, 0, to_time, p_indices);
+					if (rt->compressed_track >= 0) {
+						_get_compressed_key_indices_in_range<3>(rt->compressed_track, from_time, length, p_indices);
+						_get_compressed_key_indices_in_range<3>(rt->compressed_track, 0, to_time, p_indices);
+
+					} else {
+						_track_get_key_indices_in_range(rt->rotations, from_time, length, p_indices);
+						_track_get_key_indices_in_range(rt->rotations, 0, to_time, p_indices);
+					}
 
 				} break;
 				case TYPE_SCALE_3D: {
 					const ScaleTrack *st = static_cast<const ScaleTrack *>(t);
-					_track_get_key_indices_in_range(st->scales, from_time, length, p_indices);
-					_track_get_key_indices_in_range(st->scales, 0, to_time, p_indices);
+					if (st->compressed_track >= 0) {
+						_get_compressed_key_indices_in_range<3>(st->compressed_track, from_time, length, p_indices);
+						_get_compressed_key_indices_in_range<3>(st->compressed_track, 0, to_time, p_indices);
+
+					} else {
+						_track_get_key_indices_in_range(st->scales, from_time, length, p_indices);
+						_track_get_key_indices_in_range(st->scales, 0, to_time, p_indices);
+					}
 
 				} break;
 				case TYPE_BLEND_SHAPE: {
 					const BlendShapeTrack *bst = static_cast<const BlendShapeTrack *>(t);
-					_track_get_key_indices_in_range(bst->blend_shapes, from_time, length, p_indices);
-					_track_get_key_indices_in_range(bst->blend_shapes, 0, to_time, p_indices);
+					if (bst->compressed_track >= 0) {
+						_get_compressed_key_indices_in_range<1>(bst->compressed_track, from_time, length, p_indices);
+						_get_compressed_key_indices_in_range<1>(bst->compressed_track, 0, to_time, p_indices);
+
+					} else {
+						_track_get_key_indices_in_range(bst->blend_shapes, from_time, length, p_indices);
+						_track_get_key_indices_in_range(bst->blend_shapes, 0, to_time, p_indices);
+					}
 
 				} break;
 				case TYPE_VALUE: {
@@ -2408,22 +2802,38 @@ void Animation::track_get_key_indices_in_range(int p_track, double p_time, doubl
 	switch (t->type) {
 		case TYPE_POSITION_3D: {
 			const PositionTrack *tt = static_cast<const PositionTrack *>(t);
-			_track_get_key_indices_in_range(tt->positions, from_time, to_time, p_indices);
+			if (tt->compressed_track >= 0) {
+				_get_compressed_key_indices_in_range<3>(tt->compressed_track, from_time, to_time - from_time, p_indices);
+			} else {
+				_track_get_key_indices_in_range(tt->positions, from_time, to_time, p_indices);
+			}
 
 		} break;
 		case TYPE_ROTATION_3D: {
 			const RotationTrack *rt = static_cast<const RotationTrack *>(t);
-			_track_get_key_indices_in_range(rt->rotations, from_time, to_time, p_indices);
+			if (rt->compressed_track >= 0) {
+				_get_compressed_key_indices_in_range<3>(rt->compressed_track, from_time, to_time - from_time, p_indices);
+			} else {
+				_track_get_key_indices_in_range(rt->rotations, from_time, to_time, p_indices);
+			}
 
 		} break;
 		case TYPE_SCALE_3D: {
 			const ScaleTrack *st = static_cast<const ScaleTrack *>(t);
-			_track_get_key_indices_in_range(st->scales, from_time, to_time, p_indices);
+			if (st->compressed_track >= 0) {
+				_get_compressed_key_indices_in_range<3>(st->compressed_track, from_time, to_time - from_time, p_indices);
+			} else {
+				_track_get_key_indices_in_range(st->scales, from_time, to_time, p_indices);
+			}
 
 		} break;
 		case TYPE_BLEND_SHAPE: {
 			const BlendShapeTrack *bst = static_cast<const BlendShapeTrack *>(t);
-			_track_get_key_indices_in_range(bst->blend_shapes, from_time, to_time, p_indices);
+			if (bst->compressed_track >= 0) {
+				_get_compressed_key_indices_in_range<1>(bst->compressed_track, from_time, to_time - from_time, p_indices);
+			} else {
+				_track_get_key_indices_in_range(bst->blend_shapes, from_time, to_time, p_indices);
+			}
 
 		} break;
 		case TYPE_VALUE: {
@@ -3064,6 +3474,8 @@ void Animation::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("track_set_interpolation_loop_wrap", "track_idx", "interpolation"), &Animation::track_set_interpolation_loop_wrap);
 	ClassDB::bind_method(D_METHOD("track_get_interpolation_loop_wrap", "track_idx"), &Animation::track_get_interpolation_loop_wrap);
 
+	ClassDB::bind_method(D_METHOD("track_is_compressed", "track_idx"), &Animation::track_is_compressed);
+
 	ClassDB::bind_method(D_METHOD("value_track_set_update_mode", "track_idx", "mode"), &Animation::value_track_set_update_mode);
 	ClassDB::bind_method(D_METHOD("value_track_get_update_mode", "track_idx"), &Animation::value_track_get_update_mode);
 
@@ -3110,6 +3522,8 @@ void Animation::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("clear"), &Animation::clear);
 	ClassDB::bind_method(D_METHOD("copy_track", "track_idx", "to_animation"), &Animation::copy_track);
 
+	ClassDB::bind_method(D_METHOD("compress", "page_size", "fps", "split_tolerance"), &Animation::compress, DEFVAL(8192), DEFVAL(120), DEFVAL(4.0));
+
 	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "length", PROPERTY_HINT_RANGE, "0.001,99999,0.001"), "set_length", "get_length");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "loop"), "set_loop", "has_loop");
 	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "step", PROPERTY_HINT_RANGE, "0,4096,0.001"), "set_step", "get_step");
@@ -3143,6 +3557,10 @@ void Animation::clear() {
 	tracks.clear();
 	loop = false;
 	length = 1;
+	compression.enabled = false;
+	compression.bounds.clear();
+	compression.pages.clear();
+	compression.fps = 120;
 	emit_changed();
 	emit_signal(SceneStringNames::get_singleton()->tracks_changed);
 }
@@ -3447,6 +3865,9 @@ void Animation::_blend_shape_track_optimize(int p_idx, real_t p_allowed_linear_e
 
 void Animation::optimize(real_t p_allowed_linear_err, real_t p_allowed_angular_err, real_t p_max_optimizable_angle) {
 	for (int i = 0; i < tracks.size(); i++) {
+		if (track_is_compressed(i)) {
+			continue; //not possible to optimize compressed track
+		}
 		if (tracks[i]->type == TYPE_POSITION_3D) {
 			_position_track_optimize(i, p_allowed_linear_err, p_allowed_angular_err);
 		} else if (tracks[i]->type == TYPE_ROTATION_3D) {
@@ -3459,6 +3880,1154 @@ void Animation::optimize(real_t p_allowed_linear_err, real_t p_allowed_angular_e
 	}
 }
 
+#define print_animc(m_str)
+//#define print_animc(m_str) print_line(m_str);
+
+struct AnimationCompressionDataState {
+	enum {
+		MIN_OPTIMIZE_PACKETS = 5,
+		MAX_PACKETS = 16
+	};
+
+	uint32_t components = 3;
+	LocalVector<uint8_t> data; //commited packets
+	struct PacketData {
+		int32_t data[3] = { 0, 0, 0 };
+		uint32_t frame = 0;
+	};
+
+	float split_tolerance = 1.5;
+
+	LocalVector<PacketData> temp_packets;
+
+	//used for rollback if the new frame does not fit
+	int32_t validated_packet_count = -1;
+
+	static int32_t _compute_delta16_signed(int32_t p_from, int32_t p_to) {
+		int32_t delta = p_to - p_from;
+		if (delta > 32767) {
+			return delta - 65536; // use wrap around
+		} else if (delta < -32768) {
+			return 65536 + delta; // use wrap around
+		}
+		return delta;
+	}
+
+	static uint32_t _compute_shift_bits_signed(int32_t p_delta) {
+		if (p_delta == 0) {
+			return 0;
+		} else if (p_delta < 0) {
+			p_delta = ABS(p_delta) - 1;
+			if (p_delta == 0) {
+				return 1;
+			}
+		}
+		return nearest_shift(p_delta);
+	}
+
+	void _compute_max_shifts(uint32_t p_from, uint32_t p_to, uint32_t *max_shifts, uint32_t &max_frame_delta_shift) const {
+		for (uint32_t j = 0; j < components; j++) {
+			max_shifts[j] = 0;
+		}
+		max_frame_delta_shift = 0;
+
+		for (uint32_t i = p_from + 1; i <= p_to; i++) {
+			int32_t frame_delta = temp_packets[i].frame - temp_packets[i - 1].frame;
+			max_frame_delta_shift = MAX(max_frame_delta_shift, nearest_shift(frame_delta));
+			for (uint32_t j = 0; j < components; j++) {
+				int32_t diff = _compute_delta16_signed(temp_packets[i - 1].data[j], temp_packets[i].data[j]);
+				uint32_t shift = _compute_shift_bits_signed(diff);
+				max_shifts[j] = MAX(shift, max_shifts[j]);
+			}
+		}
+	}
+
+	bool insert_key(uint32_t p_frame, const Vector3i &p_key) {
+		if (temp_packets.size() == MAX_PACKETS) {
+			commit_temp_packets();
+		}
+		PacketData packet;
+		packet.frame = p_frame;
+		for (int i = 0; i < 3; i++) {
+			ERR_FAIL_COND_V(p_key[i] > 65535, false); // Sanity check
+			packet.data[i] = p_key[i];
+		}
+
+		temp_packets.push_back(packet);
+
+		if (temp_packets.size() >= MIN_OPTIMIZE_PACKETS) {
+			uint32_t max_shifts[3] = { 0, 0, 0 }; // Base sizes, 16 bit
+			uint32_t max_frame_delta_shift = 0;
+			// Compute the average shift before the packet was added
+			_compute_max_shifts(0, temp_packets.size() - 2, max_shifts, max_frame_delta_shift);
+
+			float prev_packet_size_avg = 0;
+			prev_packet_size_avg = float(1 << max_frame_delta_shift);
+			for (uint32_t i = 0; i < components; i++) {
+				prev_packet_size_avg += float(1 << max_shifts[i]);
+			}
+			prev_packet_size_avg /= float(1 + components);
+
+			_compute_max_shifts(temp_packets.size() - 2, temp_packets.size() - 1, max_shifts, max_frame_delta_shift);
+
+			float new_packet_size_avg = 0;
+			new_packet_size_avg = float(1 << max_frame_delta_shift);
+			for (uint32_t i = 0; i < components; i++) {
+				new_packet_size_avg += float(1 << max_shifts[i]);
+			}
+			new_packet_size_avg /= float(1 + components);
+
+			print_animc("packet count: " + rtos(temp_packets.size() - 1) + " size avg " + rtos(prev_packet_size_avg) + " new avg " + rtos(new_packet_size_avg));
+			float ratio = (prev_packet_size_avg < new_packet_size_avg) ? (new_packet_size_avg / prev_packet_size_avg) : (prev_packet_size_avg / new_packet_size_avg);
+
+			if (ratio > split_tolerance) {
+				print_animc("split!");
+				temp_packets.resize(temp_packets.size() - 1);
+				commit_temp_packets();
+				temp_packets.push_back(packet);
+			}
+		}
+
+		return temp_packets.size() == 1; // First key
+	}
+
+	uint32_t get_temp_packet_size() const {
+		if (temp_packets.size() == 0) {
+			return 0;
+		} else if (temp_packets.size() == 1) {
+			return components == 1 ? 4 : 8; // 1 component packet is 16 bits and 16 bits unused. 3 component packets is 48 bits and 16 bits unused
+		}
+		uint32_t max_shifts[3] = { 0, 0, 0 }; //base sizes, 16 bit
+		uint32_t max_frame_delta_shift = 0;
+
+		_compute_max_shifts(0, temp_packets.size() - 1, max_shifts, max_frame_delta_shift);
+
+		uint32_t size_bits = 16; //base value (all 4 bits of shift sizes for x,y,z,time)
+		size_bits += max_frame_delta_shift * (temp_packets.size() - 1); //times
+		for (uint32_t j = 0; j < components; j++) {
+			size_bits += 16; //base value
+			uint32_t shift = max_shifts[j];
+			if (shift > 0) {
+				shift += 1; //if not zero, add sign bit
+			}
+			size_bits += shift * (temp_packets.size() - 1);
+		}
+		if (size_bits % 8 != 0) { //wrap to 8 bits
+			size_bits += 8 - (size_bits % 8);
+		}
+		uint32_t size_bytes = size_bits / 8; //wrap to words
+		if (size_bytes % 4 != 0) {
+			size_bytes += 4 - (size_bytes % 4);
+		}
+		return size_bytes;
+	}
+
+	static void _push_bits(LocalVector<uint8_t> &data, uint32_t &r_buffer, uint32_t &r_bits_used, uint32_t p_value, uint32_t p_bits) {
+		r_buffer |= p_value << r_bits_used;
+		r_bits_used += p_bits;
+		while (r_bits_used >= 8) {
+			uint8_t byte = r_buffer & 0xFF;
+			data.push_back(byte);
+			r_buffer >>= 8;
+			r_bits_used -= 8;
+		}
+	}
+
+	void commit_temp_packets() {
+		if (temp_packets.size() == 0) {
+			return; //nohing to do
+		}
+#define DEBUG_PACKET_PUSH
+#ifdef DEBUG_PACKET_PUSH
+#ifndef _MSC_VER
+#warning Debugging packet push, disable this code in production to gain a bit more import performance.
+#endif
+		uint32_t debug_packet_push = get_temp_packet_size();
+		uint32_t debug_data_size = data.size();
+#endif
+		// Store header
+
+		uint8_t header[8];
+		uint32_t header_bytes = 0;
+		for (uint32_t i = 0; i < components; i++) {
+			encode_uint16(temp_packets[0].data[i], &header[header_bytes]);
+			header_bytes += 2;
+		}
+
+		uint32_t max_shifts[3] = { 0, 0, 0 }; //base sizes, 16 bit
+		uint32_t max_frame_delta_shift = 0;
+
+		if (temp_packets.size() > 1) {
+			_compute_max_shifts(0, temp_packets.size() - 1, max_shifts, max_frame_delta_shift);
+			uint16_t shift_header = (max_frame_delta_shift - 1) << 12;
+			for (uint32_t i = 0; i < components; i++) {
+				shift_header |= max_shifts[i] << (4 * i);
+			}
+
+			encode_uint16(shift_header, &header[header_bytes]);
+			header_bytes += 2;
+		}
+
+		while (header_bytes % 4 != 0) {
+			header[header_bytes++] = 0;
+		}
+
+		for (uint32_t i = 0; i < header_bytes; i++) {
+			data.push_back(header[i]);
+		}
+
+		if (temp_packets.size() == 1) {
+			temp_packets.clear();
+			validated_packet_count = 0;
+			return; //only header stored, nothing else to do
+		}
+
+		uint32_t bit_buffer = 0;
+		uint32_t bits_used = 0;
+
+		for (uint32_t i = 1; i < temp_packets.size(); i++) {
+			uint32_t frame_delta = temp_packets[i].frame - temp_packets[i - 1].frame;
+			_push_bits(data, bit_buffer, bits_used, frame_delta, max_frame_delta_shift);
+
+			for (uint32_t j = 0; j < components; j++) {
+				if (max_shifts[j] == 0) {
+					continue; // Zero delta, do not store
+				}
+				int32_t delta = _compute_delta16_signed(temp_packets[i - 1].data[j], temp_packets[i].data[j]);
+
+				ERR_FAIL_COND(delta < -32768 || delta > 32767); //sanity check
+
+				uint16_t deltau;
+				if (delta < 0) {
+					deltau = (ABS(delta) - 1) | (1 << max_shifts[j]);
+				} else {
+					deltau = delta;
+				}
+				_push_bits(data, bit_buffer, bits_used, deltau, max_shifts[j] + 1); // Include sign bit
+			}
+		}
+		if (bits_used != 0) {
+			ERR_FAIL_COND(bit_buffer > 0xFF); // Sanity check
+			data.push_back(bit_buffer);
+		}
+
+		while (data.size() % 4 != 0) {
+			data.push_back(0); //pad to align with 4
+		}
+
+		temp_packets.clear();
+		validated_packet_count = 0;
+
+#ifdef DEBUG_PACKET_PUSH
+		ERR_FAIL_COND((data.size() - debug_data_size) != debug_packet_push);
+#endif
+	}
+};
+
+struct AnimationCompressionTimeState {
+	struct Packet {
+		uint32_t frame;
+		uint32_t offset;
+		uint32_t count;
+	};
+
+	LocalVector<Packet> packets;
+	//used for rollback
+	int32_t key_index = 0;
+	int32_t validated_packet_count = 0;
+	int32_t validated_key_index = -1;
+	bool needs_start_frame = false;
+};
+
+Vector3i Animation::_compress_key(uint32_t p_track, const AABB &p_bounds, int32_t p_key, float p_time) {
+	Vector3i values;
+	TrackType tt = track_get_type(p_track);
+	switch (tt) {
+		case TYPE_POSITION_3D: {
+			Vector3 pos;
+			if (p_key >= 0) {
+				position_track_get_key(p_track, p_key, &pos);
+			} else {
+				position_track_interpolate(p_track, p_time, &pos);
+			}
+			pos = (pos - p_bounds.position) / p_bounds.size;
+			for (int j = 0; j < 3; j++) {
+				values[j] = CLAMP(int32_t(pos[j] * 65535.0), 0, 65535);
+			}
+		} break;
+		case TYPE_ROTATION_3D: {
+			Quaternion rot;
+			if (p_key >= 0) {
+				rotation_track_get_key(p_track, p_key, &rot);
+			} else {
+				rotation_track_interpolate(p_track, p_time, &rot);
+			}
+			Vector3 axis = rot.get_axis();
+			float angle = rot.get_angle();
+			angle = Math::fposmod(double(angle), double(Math_PI * 2.0));
+			Vector2 oct = axis.octahedron_encode();
+			Vector3 rot_norm(oct.x, oct.y, angle / (Math_PI * 2.0)); // high resolution rotation in 0-1 angle.
+
+			for (int j = 0; j < 3; j++) {
+				values[j] = CLAMP(int32_t(rot_norm[j] * 65535.0), 0, 65535);
+			}
+		} break;
+		case TYPE_SCALE_3D: {
+			Vector3 scale;
+			if (p_key >= 0) {
+				scale_track_get_key(p_track, p_key, &scale);
+			} else {
+				scale_track_interpolate(p_track, p_time, &scale);
+			}
+			scale = (scale - p_bounds.position) / p_bounds.size;
+			for (int j = 0; j < 3; j++) {
+				values[j] = CLAMP(int32_t(scale[j] * 65535.0), 0, 65535);
+			}
+		} break;
+		case TYPE_BLEND_SHAPE: {
+			float blend;
+			if (p_key >= 0) {
+				blend_shape_track_get_key(p_track, p_key, &blend);
+			} else {
+				blend_shape_track_interpolate(p_track, p_time, &blend);
+			}
+
+			blend = (blend / float(Compression::BLEND_SHAPE_RANGE)) * 0.5 + 0.5;
+			values[0] = CLAMP(int32_t(blend * 65535.0), 0, 65535);
+		} break;
+		default: {
+			ERR_FAIL_V(Vector3i()); //sanity check
+		} break;
+	}
+
+	return values;
+}
+
+struct AnimationCompressionBufferBitsRead {
+	uint32_t buffer = 0;
+	uint32_t used = 0;
+	const uint8_t *src_data = nullptr;
+
+	_FORCE_INLINE_ uint32_t read(uint32_t p_bits) {
+		uint32_t output = 0;
+		uint32_t written = 0;
+		while (p_bits > 0) {
+			if (used == 0) {
+				used = 8;
+				buffer = *src_data;
+				src_data++;
+			}
+			uint32_t to_write = MIN(used, p_bits);
+			output |= (buffer & ((1 << to_write) - 1)) << written;
+			buffer >>= to_write;
+			used -= to_write;
+			p_bits -= to_write;
+			written += to_write;
+		}
+		return output;
+	}
+};
+
+void Animation::compress(uint32_t p_page_size, uint32_t p_fps, float p_split_tolerance) {
+	ERR_FAIL_COND_MSG(compression.enabled, "This animation is already compressed");
+
+	p_split_tolerance = CLAMP(p_split_tolerance, 1.1, 8.0);
+	compression.pages.clear();
+
+	uint32_t base_page_size = 0; // Before compressing pages, compute how large the "end page" datablock is.
+	LocalVector<uint32_t> tracks_to_compress;
+	LocalVector<AABB> track_bounds;
+	const uint32_t time_packet_size = 4;
+
+	const uint32_t track_header_size = 4 + 4 + 4; // pointer to time (4 bytes), amount of time keys (4 bytes) pointer to track data (4 bytes)
+
+	for (int i = 0; i < get_track_count(); i++) {
+		TrackType type = track_get_type(i);
+		if (type != TYPE_POSITION_3D && type != TYPE_ROTATION_3D && type != TYPE_SCALE_3D && type != TYPE_BLEND_SHAPE) {
+			continue;
+		}
+		if (track_get_key_count(i) == 0) {
+			continue; //do not compress, no keys
+		}
+		base_page_size += track_header_size; //pointer to beginning of each track timeline and amount of time keys
+		base_page_size += time_packet_size; //for end of track time marker
+		base_page_size += (type == TYPE_BLEND_SHAPE) ? 4 : 8; // at least the end of track packet (at much 8 bytes). This could be less, but have to be pessimistic.
+		tracks_to_compress.push_back(i);
+
+		AABB bounds;
+
+		if (type == TYPE_POSITION_3D) {
+			AABB aabb;
+			int kcount = track_get_key_count(i);
+			for (int j = 0; j < kcount; j++) {
+				Vector3 pos;
+				position_track_get_key(i, j, &pos);
+				if (j == 0) {
+					aabb.position = pos;
+				} else {
+					aabb.expand_to(pos);
+				}
+			}
+			for (int j = 0; j < 3; j++) {
+				//cant have zero
+				if (aabb.size[j] < CMP_EPSILON) {
+					aabb.size[j] = CMP_EPSILON;
+				}
+			}
+			bounds = aabb;
+		}
+		if (type == TYPE_SCALE_3D) {
+			AABB aabb;
+			int kcount = track_get_key_count(i);
+			for (int j = 0; j < kcount; j++) {
+				Vector3 scale;
+				scale_track_get_key(i, j, &scale);
+				if (j == 0) {
+					aabb.position = scale;
+				} else {
+					aabb.expand_to(scale);
+				}
+			}
+			for (int j = 0; j < 3; j++) {
+				//cant have zero
+				if (aabb.size[j] < CMP_EPSILON) {
+					aabb.size[j] = CMP_EPSILON;
+				}
+			}
+			bounds = aabb;
+		}
+
+		track_bounds.push_back(bounds);
+	}
+
+	if (tracks_to_compress.size() == 0) {
+		return; //nothing to compress
+	}
+
+	print_animc("Anim Compression:");
+	print_animc("-----------------");
+	print_animc("Tracks to compress: " + itos(tracks_to_compress.size()));
+
+	uint32_t current_frame = 0;
+	uint32_t base_page_frame = 0;
+	double frame_len = 1.0 / double(p_fps);
+	const uint32_t max_frames_per_page = 65536;
+
+	print_animc("Frame Len: " + rtos(frame_len));
+
+	LocalVector<AnimationCompressionDataState> data_tracks;
+	LocalVector<AnimationCompressionTimeState> time_tracks;
+
+	data_tracks.resize(tracks_to_compress.size());
+	time_tracks.resize(tracks_to_compress.size());
+
+	for (uint32_t i = 0; i < data_tracks.size(); i++) {
+		data_tracks[i].split_tolerance = p_split_tolerance;
+		if (track_get_type(tracks_to_compress[i]) == TYPE_BLEND_SHAPE) {
+			data_tracks[i].components = 1;
+		} else {
+			data_tracks[i].components = 3;
+		}
+	}
+
+	while (true) {
+		// Begin by finding the keyframe in all tracks with the time closest to the current time
+		const uint32_t FRAME_MAX = 0xFFFFFFFF;
+		const int32_t NO_TRACK_FOUND = -1;
+		uint32_t best_frame = FRAME_MAX;
+		uint32_t best_invalid_frame = FRAME_MAX;
+		int32_t best_frame_track = NO_TRACK_FOUND; // Default is -1, which means all keyframes for this page are exhausted.
+		bool start_frame = false;
+
+		for (uint32_t i = 0; i < tracks_to_compress.size(); i++) {
+			uint32_t uncomp_track = tracks_to_compress[i];
+
+			if (time_tracks[i].key_index == track_get_key_count(uncomp_track)) {
+				if (time_tracks[i].needs_start_frame) {
+					start_frame = true;
+					best_frame = base_page_frame;
+					best_frame_track = i;
+					time_tracks[i].needs_start_frame = false;
+					break;
+				} else {
+					continue; // This track is exhausted (all keys were added already), don't consider.
+				}
+			}
+
+			uint32_t key_frame = double(track_get_key_time(uncomp_track, time_tracks[i].key_index)) / frame_len;
+
+			if (time_tracks[i].needs_start_frame && key_frame > base_page_frame) {
+				start_frame = true;
+				best_frame = base_page_frame;
+				best_frame_track = i;
+				time_tracks[i].needs_start_frame = false;
+				break;
+			}
+
+			ERR_FAIL_COND(key_frame < base_page_frame); // Sanity check, should never happen
+
+			if (key_frame - base_page_frame >= max_frames_per_page) {
+				// Invalid because beyond the max frames allowed per page
+				best_invalid_frame = MIN(best_invalid_frame, key_frame);
+			} else if (key_frame < best_frame) {
+				best_frame = key_frame;
+				best_frame_track = i;
+			}
+		}
+
+		print_animc("*KEY*: Current Frame: " + itos(current_frame) + " Best Frame: " + rtos(best_frame) + " Best Track: " + itos(best_frame_track) + " Start: " + String(start_frame ? "true" : "false"));
+
+		if (!start_frame && best_frame > current_frame) {
+			// Any case where the current frame advanced, either because nothing was found or because something was found greater than the current one.
+			print_animc("\tAdvance Condition.");
+			bool rollback = false;
+
+			// The frame has advanced, time to validate the previous frame
+			uint32_t current_page_size = base_page_size;
+			for (uint32_t i = 0; i < data_tracks.size(); i++) {
+				uint32_t track_size = data_tracks[i].data.size(); // track size
+				track_size += data_tracks[i].get_temp_packet_size(); // Add the temporary data
+				if (track_size > Compression::MAX_DATA_TRACK_SIZE) {
+					rollback = true; //track to large, time track can't point to keys any longer, because key offset is 12 bits
+					break;
+				}
+				current_page_size += track_size;
+			}
+			for (uint32_t i = 0; i < time_tracks.size(); i++) {
+				current_page_size += time_tracks[i].packets.size() * 4; // time packet is 32 bits
+			}
+
+			if (!rollback && current_page_size > p_page_size) {
+				rollback = true;
+			}
+
+			print_animc("\tCurrent Page Size: " + itos(current_page_size) + "/" + itos(p_page_size) + " Rollback? " + String(rollback ? "YES!" : "no"));
+
+			if (rollback) {
+				// Not valid any longer, so rollback and commit page
+
+				for (uint32_t i = 0; i < data_tracks.size(); i++) {
+					data_tracks[i].temp_packets.resize(data_tracks[i].validated_packet_count);
+				}
+				for (uint32_t i = 0; i < time_tracks.size(); i++) {
+					time_tracks[i].key_index = time_tracks[i].validated_key_index; //rollback key
+					time_tracks[i].packets.resize(time_tracks[i].validated_packet_count);
+				}
+
+			} else {
+				// All valid, so save rollback information
+				for (uint32_t i = 0; i < data_tracks.size(); i++) {
+					data_tracks[i].validated_packet_count = data_tracks[i].temp_packets.size();
+				}
+				for (uint32_t i = 0; i < time_tracks.size(); i++) {
+					time_tracks[i].validated_key_index = time_tracks[i].key_index;
+					time_tracks[i].validated_packet_count = time_tracks[i].packets.size();
+				}
+
+				// Accept this frame as the frame being processed (as long as it exists)
+				if (best_frame != FRAME_MAX) {
+					current_frame = best_frame;
+					print_animc("\tValidated, New Current Frame: " + itos(current_frame));
+				}
+			}
+
+			if (rollback || best_frame == FRAME_MAX) {
+				// Commit the page if had to rollback or if no track was found
+				print_animc("\tCommiting page..");
+
+				// The end frame for the page depends entirely on whether its valid or
+				// no more keys were found.
+				// If not valid, then the end frame is the current frame (as this means the current frame is being rolled back
+				// If valid, then the end frame is the next invalid one (in case more frames exist), or the current frame in case no more frames exist.
+				uint32_t page_end_frame = (rollback || best_frame == FRAME_MAX) ? current_frame : best_invalid_frame;
+
+				print_animc("\tEnd Frame: " + itos(page_end_frame) + ", " + rtos(page_end_frame * frame_len) + "s");
+
+				// Add finalizer frames and commit pending tracks
+				uint32_t finalizer_local_frame = page_end_frame - base_page_frame;
+
+				uint32_t total_page_size = 0;
+
+				for (uint32_t i = 0; i < data_tracks.size(); i++) {
+					if (data_tracks[i].temp_packets.size() == 0 || (data_tracks[i].temp_packets[data_tracks[i].temp_packets.size() - 1].frame) < finalizer_local_frame) {
+						// Add finalizer frame if it makes sense
+						Vector3i values = _compress_key(tracks_to_compress[i], track_bounds[i], -1, page_end_frame * frame_len);
+
+						bool first_key = data_tracks[i].insert_key(finalizer_local_frame, values);
+						if (first_key) {
+							AnimationCompressionTimeState::Packet p;
+							p.count = 1;
+							p.frame = finalizer_local_frame;
+							p.offset = data_tracks[i].data.size();
+							time_tracks[i].packets.push_back(p);
+						} else {
+							ERR_FAIL_COND(time_tracks[i].packets.size() == 0);
+							time_tracks[i].packets[time_tracks[i].packets.size() - 1].count++;
+						}
+					}
+
+					data_tracks[i].commit_temp_packets();
+					total_page_size += data_tracks[i].data.size();
+					total_page_size += time_tracks[i].packets.size() * 4;
+					total_page_size += track_header_size;
+
+					print_animc("\tTrack " + itos(i) + " time packets: " + itos(time_tracks[i].packets.size()) + " Packet data: " + itos(data_tracks[i].data.size()));
+				}
+
+				print_animc("\tTotal page Size: " + itos(total_page_size) + "/" + itos(p_page_size));
+
+				// Create Page
+				Vector<uint8_t> page_data;
+				page_data.resize(total_page_size);
+				{
+					uint8_t *page_ptr = page_data.ptrw();
+					uint32_t base_offset = data_tracks.size() * track_header_size;
+
+					for (uint32_t i = 0; i < data_tracks.size(); i++) {
+						encode_uint32(base_offset, page_ptr + (track_header_size * i + 0));
+						uint16_t *key_time_ptr = (uint16_t *)(page_ptr + base_offset);
+						for (uint32_t j = 0; j < time_tracks[i].packets.size(); j++) {
+							key_time_ptr[j * 2 + 0] = uint16_t(time_tracks[i].packets[j].frame);
+							uint16_t ptr = time_tracks[i].packets[j].offset / 4;
+							ptr |= (time_tracks[i].packets[j].count - 1) << 12;
+							key_time_ptr[j * 2 + 1] = ptr;
+							base_offset += 4;
+						}
+						encode_uint32(time_tracks[i].packets.size(), page_ptr + (track_header_size * i + 4));
+						encode_uint32(base_offset, page_ptr + (track_header_size * i + 8));
+						memcpy(page_ptr + base_offset, data_tracks[i].data.ptr(), data_tracks[i].data.size());
+						base_offset += data_tracks[i].data.size();
+
+						//reset track
+						data_tracks[i].data.clear();
+						data_tracks[i].temp_packets.clear();
+						data_tracks[i].validated_packet_count = -1;
+
+						time_tracks[i].needs_start_frame = true; //Not required the first time, but from now on it is.
+						time_tracks[i].packets.clear();
+						time_tracks[i].validated_key_index = -1;
+						time_tracks[i].validated_packet_count = 0;
+					}
+				}
+
+				Compression::Page page;
+				page.data = page_data;
+				page.time_offset = base_page_frame * frame_len;
+				compression.pages.push_back(page);
+
+				if (!rollback && best_invalid_frame == FRAME_MAX) {
+					break; // No more pages to add.
+				}
+
+				current_frame = page_end_frame;
+				base_page_frame = page_end_frame;
+
+				continue; // Start over
+			}
+		}
+
+		// A key was found for the current frame and all is ok
+
+		uint32_t comp_track = best_frame_track;
+		Vector3i values;
+
+		if (start_frame) {
+			// Interpolate
+			values = _compress_key(tracks_to_compress[comp_track], track_bounds[comp_track], -1, base_page_frame * frame_len);
+		} else {
+			uint32_t key = time_tracks[comp_track].key_index;
+			values = _compress_key(tracks_to_compress[comp_track], track_bounds[comp_track], key);
+			time_tracks[comp_track].key_index++; //goto next key (but could be rolled back if beyond page size).
+		}
+
+		bool first_key = data_tracks[comp_track].insert_key(best_frame - base_page_frame, values);
+		if (first_key) {
+			AnimationCompressionTimeState::Packet p;
+			p.count = 1;
+			p.frame = best_frame - base_page_frame;
+			p.offset = data_tracks[comp_track].data.size();
+			time_tracks[comp_track].packets.push_back(p);
+		} else {
+			ERR_CONTINUE(time_tracks[comp_track].packets.size() == 0);
+			time_tracks[comp_track].packets[time_tracks[comp_track].packets.size() - 1].count++;
+		}
+	}
+
+	compression.bounds = track_bounds;
+	compression.fps = p_fps;
+	compression.enabled = true;
+
+	for (uint32_t i = 0; i < tracks_to_compress.size(); i++) {
+		Track *t = tracks[tracks_to_compress[i]];
+		t->interpolation = INTERPOLATION_LINEAR; //only linear supported
+		switch (t->type) {
+			case TYPE_POSITION_3D: {
+				PositionTrack *tt = static_cast<PositionTrack *>(t);
+				tt->positions.clear();
+				tt->compressed_track = i;
+			} break;
+			case TYPE_ROTATION_3D: {
+				RotationTrack *rt = static_cast<RotationTrack *>(t);
+				rt->rotations.clear();
+				rt->compressed_track = i;
+			} break;
+			case TYPE_SCALE_3D: {
+				ScaleTrack *st = static_cast<ScaleTrack *>(t);
+				st->scales.clear();
+				st->compressed_track = i;
+				print_line("Scale Bounds " + itos(i) + ": " + track_bounds[i]);
+			} break;
+			case TYPE_BLEND_SHAPE: {
+				BlendShapeTrack *bst = static_cast<BlendShapeTrack *>(t);
+				bst->blend_shapes.clear();
+				bst->compressed_track = i;
+			} break;
+			default: {
+			}
+		}
+	}
+#if 1
+	uint32_t orig_size = 0;
+	for (int i = 0; i < get_track_count(); i++) {
+		switch (track_get_type(i)) {
+			case TYPE_SCALE_3D:
+			case TYPE_POSITION_3D: {
+				orig_size += sizeof(TKey<Vector3>) * track_get_key_count(i);
+			} break;
+			case TYPE_ROTATION_3D: {
+				orig_size += sizeof(TKey<Quaternion>) * track_get_key_count(i);
+			} break;
+			case TYPE_BLEND_SHAPE: {
+				orig_size += sizeof(TKey<float>) * track_get_key_count(i);
+			} break;
+			default: {
+			}
+		}
+	}
+
+	uint32_t new_size = 0;
+	for (uint32_t i = 0; i < compression.pages.size(); i++) {
+		new_size += compression.pages[i].data.size();
+	}
+
+	print_line("Original size: " + itos(orig_size) + " - Compressed size: " + itos(new_size) + " " + String::num(float(new_size) / float(orig_size) * 100, 2) + "% pages: " + itos(compression.pages.size()));
+#endif
+}
+
+bool Animation::_rotation_interpolate_compressed(uint32_t p_compressed_track, double p_time, Quaternion &r_ret) const {
+	Vector3i current;
+	Vector3i next;
+	double time_current;
+	double time_next;
+
+	if (!_fetch_compressed<3>(p_compressed_track, p_time, current, time_current, next, time_next)) {
+		return false; //some sort of problem
+	}
+
+	if (time_current >= p_time || time_current == time_next) {
+		r_ret = _uncompress_quaternion(current);
+	} else if (p_time >= time_next) {
+		r_ret = _uncompress_quaternion(next);
+	} else {
+		double c = (p_time - time_current) / (time_next - time_current);
+		Quaternion from = _uncompress_quaternion(current);
+		Quaternion to = _uncompress_quaternion(next);
+		r_ret = from.slerp(to, c);
+	}
+
+	return true;
+}
+
+bool Animation::_pos_scale_interpolate_compressed(uint32_t p_compressed_track, double p_time, Vector3 &r_ret) const {
+	Vector3i current;
+	Vector3i next;
+	double time_current;
+	double time_next;
+
+	if (!_fetch_compressed<3>(p_compressed_track, p_time, current, time_current, next, time_next)) {
+		return false; //some sort of problem
+	}
+
+	if (time_current >= p_time || time_current == time_next) {
+		r_ret = _uncompress_pos_scale(p_compressed_track, current);
+	} else if (p_time >= time_next) {
+		r_ret = _uncompress_pos_scale(p_compressed_track, next);
+	} else {
+		double c = (p_time - time_current) / (time_next - time_current);
+		Vector3 from = _uncompress_pos_scale(p_compressed_track, current);
+		Vector3 to = _uncompress_pos_scale(p_compressed_track, next);
+		r_ret = from.lerp(to, c);
+	}
+
+	return true;
+}
+bool Animation::_blend_shape_interpolate_compressed(uint32_t p_compressed_track, double p_time, float &r_ret) const {
+	Vector3i current;
+	Vector3i next;
+	double time_current;
+	double time_next;
+
+	if (!_fetch_compressed<1>(p_compressed_track, p_time, current, time_current, next, time_next)) {
+		return false; //some sort of problem
+	}
+
+	if (time_current >= p_time || time_current == time_next) {
+		r_ret = _uncompress_blend_shape(current);
+	} else if (p_time >= time_next) {
+		r_ret = _uncompress_blend_shape(next);
+	} else {
+		float c = (p_time - time_current) / (time_next - time_current);
+		float from = _uncompress_blend_shape(current);
+		float to = _uncompress_blend_shape(next);
+		r_ret = Math::lerp(from, to, c);
+	}
+
+	return true;
+}
+
+template <uint32_t COMPONENTS>
+bool Animation::_fetch_compressed(uint32_t p_compressed_track, double p_time, Vector3i &r_current_value, double &r_current_time, Vector3i &r_next_value, double &r_next_time, uint32_t *key_index) const {
+	ERR_FAIL_COND_V(!compression.enabled, false);
+	ERR_FAIL_INDEX_V(p_compressed_track, compression.bounds.size(), false);
+	p_time = CLAMP(p_time, 0, length);
+	if (key_index) {
+		*key_index = 0;
+	}
+
+	double frame_to_sec = 1.0 / double(compression.fps);
+
+	int32_t page_index = -1;
+	for (uint32_t i = 0; i < compression.pages.size(); i++) {
+		if (compression.pages[i].time_offset > p_time) {
+			break;
+		}
+		page_index = i;
+	}
+
+	ERR_FAIL_COND_V(page_index == -1, false); //should not happen
+
+	double page_base_time = compression.pages[page_index].time_offset;
+	const uint8_t *page_data = compression.pages[page_index].data.ptr();
+#ifndef _MSC_VER
+#warning Little endian assumed. No major big endian hardware exists any longer, but in case it does it will need to be supported
+#endif
+	const uint32_t *indices = (const uint32_t *)page_data;
+	const uint16_t *time_keys = (const uint16_t *)&page_data[indices[p_compressed_track * 3 + 0]];
+	uint32_t time_key_count = indices[p_compressed_track * 3 + 1];
+
+	int32_t packet_idx = 0;
+	double packet_time = double(time_keys[0]) * frame_to_sec + page_base_time;
+	uint32_t base_frame = time_keys[0];
+
+	for (uint32_t i = 1; i < time_key_count; i++) {
+		uint32_t f = time_keys[i * 2 + 0];
+		double frame_time = double(f) * frame_to_sec + page_base_time;
+
+		if (frame_time > p_time) {
+			break;
+		}
+
+		if (key_index) {
+			(*key_index) += (time_keys[(i - 1) * 2 + 1] >> 12) + 1;
+		}
+
+		packet_idx = i;
+		packet_time = frame_time;
+		base_frame = f;
+	}
+
+	const uint8_t *data_keys_base = (const uint8_t *)&page_data[indices[p_compressed_track * 3 + 2]];
+
+	uint16_t time_key_data = time_keys[packet_idx * 2 + 1];
+	uint32_t data_offset = (time_key_data & 0xFFF) * 4; // lower 12 bits
+	uint32_t data_count = (time_key_data >> 12) + 1;
+
+	const uint16_t *data_key = (const uint16_t *)(data_keys_base + data_offset);
+
+	uint16_t decode[COMPONENTS];
+	uint16_t decode_next[COMPONENTS];
+
+	for (uint32_t i = 0; i < COMPONENTS; i++) {
+		decode[i] = data_key[i];
+		decode_next[i] = data_key[i];
+	}
+
+	double next_time = packet_time;
+
+	if (p_time > packet_time) { // If its equal or less, then don't bother
+		if (data_count > 1) {
+			//decode forward
+			uint32_t bit_width[COMPONENTS];
+			for (uint32_t i = 0; i < COMPONENTS; i++) {
+				bit_width[i] = (data_key[COMPONENTS] >> (i * 4)) & 0xF;
+			}
+
+			uint32_t frame_bit_width = (data_key[COMPONENTS] >> 12) + 1;
+
+			AnimationCompressionBufferBitsRead buffer;
+
+			buffer.src_data = (const uint8_t *)&data_key[COMPONENTS + 1];
+
+			for (uint32_t i = 1; i < data_count; i++) {
+				uint32_t frame_delta = buffer.read(frame_bit_width);
+				base_frame += frame_delta;
+
+				for (uint32_t j = 0; j < COMPONENTS; j++) {
+					if (bit_width[j] == 0) {
+						continue; // do none
+					}
+					uint32_t valueu = buffer.read(bit_width[j] + 1);
+					bool sign = valueu & (1 << bit_width[j]);
+					int16_t value = valueu & ((1 << bit_width[j]) - 1);
+					if (sign) {
+						value = -value - 1;
+					}
+
+					decode_next[j] += value;
+				}
+
+				next_time = double(base_frame) * frame_to_sec + page_base_time;
+				if (p_time < next_time) {
+					break;
+				}
+
+				packet_time = next_time;
+
+				for (uint32_t j = 0; j < COMPONENTS; j++) {
+					decode[j] = decode_next[j];
+				}
+
+				if (key_index) {
+					(*key_index)++;
+				}
+			}
+		}
+
+		if (p_time > next_time) { // > instead of >= because if its equal, then it will be properly interpolated anyway
+			// So, the last frame found still has a time that is less than the required frame,
+			// will have to interpolate with the first frame of the next timekey.
+
+			if ((uint32_t)packet_idx < time_key_count - 1) { // Sanity check but should not matter much, otherwise current next packet is last packet
+
+				uint16_t time_key_data_next = time_keys[(packet_idx + 1) * 2 + 1];
+				uint32_t data_offset_next = (time_key_data_next & 0xFFF) * 4; // Lower 12 bits
+
+				const uint16_t *data_key_next = (const uint16_t *)(data_keys_base + data_offset_next);
+				base_frame = time_keys[(packet_idx + 1) * 2 + 0];
+				next_time = double(base_frame) * frame_to_sec + page_base_time;
+				for (uint32_t i = 0; i < COMPONENTS; i++) {
+					decode_next[i] = data_key_next[i];
+				}
+			}
+		}
+	}
+
+	r_current_time = packet_time;
+	r_next_time = next_time;
+
+	for (uint32_t i = 0; i < COMPONENTS; i++) {
+		r_current_value[i] = decode[i];
+		r_next_value[i] = decode_next[i];
+	}
+
+	return true;
+}
+
+template <uint32_t COMPONENTS>
+void Animation::_get_compressed_key_indices_in_range(uint32_t p_compressed_track, double p_time, double p_delta, List<int> *r_indices) const {
+	ERR_FAIL_COND(!compression.enabled);
+	ERR_FAIL_INDEX(p_compressed_track, compression.bounds.size());
+
+	double frame_to_sec = 1.0 / double(compression.fps);
+	uint32_t key_index = 0;
+
+	for (uint32_t p = 0; p < compression.pages.size(); p++) {
+		if (compression.pages[p].time_offset >= p_time + p_delta) {
+			// Page beyond range
+			return;
+		}
+
+		// Page within range
+
+		uint32_t page_index = p;
+
+		double page_base_time = compression.pages[page_index].time_offset;
+		const uint8_t *page_data = compression.pages[page_index].data.ptr();
+#ifndef _MSC_VER
+#warning Little endian assumed. No major big endian hardware exists any longer, but in case it does it will need to be supported
+#endif
+		const uint32_t *indices = (const uint32_t *)page_data;
+		const uint16_t *time_keys = (const uint16_t *)&page_data[indices[p_compressed_track * 3 + 0]];
+		uint32_t time_key_count = indices[p_compressed_track * 3 + 1];
+
+		for (uint32_t i = 0; i < time_key_count; i++) {
+			uint32_t f = time_keys[i * 2 + 0];
+			double frame_time = f * frame_to_sec + page_base_time;
+			if (frame_time >= p_time + p_delta) {
+				return;
+			} else if (frame_time >= p_time) {
+				r_indices->push_back(key_index);
+			}
+
+			key_index++;
+
+			const uint8_t *data_keys_base = (const uint8_t *)&page_data[indices[p_compressed_track * 3 + 2]];
+
+			uint16_t time_key_data = time_keys[i * 2 + 1];
+			uint32_t data_offset = (time_key_data & 0xFFF) * 4; // lower 12 bits
+			uint32_t data_count = (time_key_data >> 12) + 1;
+
+			const uint16_t *data_key = (const uint16_t *)(data_keys_base + data_offset);
+
+			if (data_count > 1) {
+				//decode forward
+				uint32_t bit_width[COMPONENTS];
+				for (uint32_t j = 0; j < COMPONENTS; j++) {
+					bit_width[j] = (data_key[COMPONENTS] >> (j * 4)) & 0xF;
+				}
+
+				uint32_t frame_bit_width = (data_key[COMPONENTS] >> 12) + 1;
+
+				AnimationCompressionBufferBitsRead buffer;
+
+				buffer.src_data = (const uint8_t *)&data_key[COMPONENTS + 1];
+
+				for (uint32_t j = 1; j < data_count; j++) {
+					uint32_t frame_delta = buffer.read(frame_bit_width);
+					f += frame_delta;
+
+					frame_time = f * frame_to_sec + page_base_time;
+					if (frame_time >= p_time + p_delta) {
+						return;
+					} else if (frame_time >= p_time) {
+						r_indices->push_back(key_index);
+					}
+
+					for (uint32_t k = 0; k < COMPONENTS; k++) {
+						if (bit_width[k] == 0) {
+							continue; // do none
+						}
+						buffer.read(bit_width[k] + 1); // skip
+					}
+
+					key_index++;
+				}
+			}
+		}
+	}
+}
+
+int Animation::_get_compressed_key_count(uint32_t p_compressed_track) const {
+	ERR_FAIL_COND_V(!compression.enabled, -1);
+	ERR_FAIL_UNSIGNED_INDEX_V(p_compressed_track, compression.bounds.size(), -1);
+
+	int key_count = 0;
+
+	for (uint32_t i = 0; i < compression.pages.size(); i++) {
+		const uint8_t *page_data = compression.pages[i].data.ptr();
+#ifndef _MSC_VER
+#warning Little endian assumed. No major big endian hardware exists any longer, but in case it does it will need to be supported
+#endif
+		const uint32_t *indices = (const uint32_t *)page_data;
+		const uint16_t *time_keys = (const uint16_t *)&page_data[indices[p_compressed_track * 3 + 0]];
+		uint32_t time_key_count = indices[p_compressed_track * 3 + 1];
+
+		for (uint32_t j = 0; j < time_key_count; j++) {
+			key_count += (time_keys[j * 2 + 1] >> 12) + 1;
+		}
+	}
+
+	return key_count;
+}
+
+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);
+}
+Vector3 Animation::_uncompress_pos_scale(uint32_t p_compressed_track, const Vector3i &p_value) const {
+	Vector3 pos_norm(float(p_value.x) / 65535.0, float(p_value.y) / 65535.0, float(p_value.z) / 65535.0);
+	return compression.bounds[p_compressed_track].position + pos_norm * compression.bounds[p_compressed_track].size;
+}
+float Animation::_uncompress_blend_shape(const Vector3i &p_value) const {
+	float bsn = float(p_value.x) / 65535.0;
+	return (bsn * 2.0 - 1.0) * float(Compression::BLEND_SHAPE_RANGE);
+}
+
+template <uint32_t COMPONENTS>
+bool Animation::_fetch_compressed_by_index(uint32_t p_compressed_track, int p_index, Vector3i &r_value, double &r_time) const {
+	ERR_FAIL_COND_V(!compression.enabled, false);
+	ERR_FAIL_INDEX_V(p_compressed_track, compression.bounds.size(), false);
+
+	for (uint32_t i = 0; i < compression.pages.size(); i++) {
+		const uint8_t *page_data = compression.pages[i].data.ptr();
+#ifndef _MSC_VER
+#warning Little endian assumed. No major big endian hardware exists any longer, but in case it does it will need to be supported
+#endif
+		const uint32_t *indices = (const uint32_t *)page_data;
+		const uint16_t *time_keys = (const uint16_t *)&page_data[indices[p_compressed_track * 3 + 0]];
+		uint32_t time_key_count = indices[p_compressed_track * 3 + 1];
+		const uint8_t *data_keys_base = (const uint8_t *)&page_data[indices[p_compressed_track * 3 + 2]];
+
+		for (uint32_t j = 0; j < time_key_count; j++) {
+			uint32_t subkeys = (time_keys[j * 2 + 1] >> 12) + 1;
+			if ((uint32_t)p_index < subkeys) {
+				uint16_t data_offset = (time_keys[j * 2 + 1] & 0xFFF) * 4;
+
+				const uint16_t *data_key = (const uint16_t *)(data_keys_base + data_offset);
+
+				uint16_t frame = time_keys[j * 2 + 0];
+				uint16_t decode[COMPONENTS];
+
+				for (uint32_t k = 0; k < COMPONENTS; k++) {
+					decode[k] = data_key[k];
+				}
+
+				if (p_index > 0) {
+					uint32_t bit_width[COMPONENTS];
+					for (uint32_t k = 0; k < COMPONENTS; k++) {
+						bit_width[k] = (data_key[COMPONENTS] >> (k * 4)) & 0xF;
+					}
+					uint32_t frame_bit_width = (data_key[COMPONENTS] >> 12) + 1;
+
+					AnimationCompressionBufferBitsRead buffer;
+					buffer.src_data = (const uint8_t *)&data_key[COMPONENTS + 1];
+
+					for (int k = 0; k < p_index; k++) {
+						uint32_t frame_delta = buffer.read(frame_bit_width);
+						frame += frame_delta;
+						for (uint32_t l = 0; l < COMPONENTS; l++) {
+							if (bit_width[l] == 0) {
+								continue; // do none
+							}
+							uint32_t valueu = buffer.read(bit_width[l] + 1);
+							bool sign = valueu & (1 << bit_width[l]);
+							int16_t value = valueu & ((1 << bit_width[l]) - 1);
+							if (sign) {
+								value = -value - 1;
+							}
+
+							decode[l] += value;
+						}
+					}
+				}
+
+				r_time = compression.pages[i].time_offset + double(frame) / double(compression.fps);
+				for (uint32_t l = 0; l < COMPONENTS; l++) {
+					r_value[l] = decode[l];
+				}
+
+				return true;
+
+			} else {
+				p_index -= subkeys;
+			}
+		}
+	}
+
+	return false;
+}
+
 Animation::Animation() {}
 
 Animation::~Animation() {

+ 90 - 2
scene/resources/animation.h

@@ -32,6 +32,7 @@
 #define ANIMATION_H
 
 #include "core/io/resource.h"
+#include "core/templates/local_vector.h"
 
 #define ANIM_MIN_LENGTH 0.001
 
@@ -98,7 +99,7 @@ private:
 
 	struct PositionTrack : public Track {
 		Vector<TKey<Vector3>> positions;
-
+		int32_t compressed_track = -1;
 		PositionTrack() { type = TYPE_POSITION_3D; }
 	};
 
@@ -106,7 +107,7 @@ private:
 
 	struct RotationTrack : public Track {
 		Vector<TKey<Quaternion>> rotations;
-
+		int32_t compressed_track = -1;
 		RotationTrack() { type = TYPE_ROTATION_3D; }
 	};
 
@@ -114,6 +115,7 @@ private:
 
 	struct ScaleTrack : public Track {
 		Vector<TKey<Vector3>> scales;
+		int32_t compressed_track = -1;
 		ScaleTrack() { type = TYPE_SCALE_3D; }
 	};
 
@@ -121,6 +123,7 @@ private:
 
 	struct BlendShapeTrack : public Track {
 		Vector<TKey<float>> blend_shapes;
+		int32_t compressed_track = -1;
 		BlendShapeTrack() { type = TYPE_BLEND_SHAPE; }
 	};
 
@@ -230,6 +233,89 @@ private:
 	real_t step = 0.1;
 	bool loop = false;
 
+	/* Animation compression page format (version 1):
+	 *
+	 * Animation uses bitwidth based compression separated into small pages. The intention is that pages fit easily in the cache, so decoding is cache efficient.
+	 * The page-based nature also makes future animation streaming from disk possible.
+	 *
+	 * Actual format:
+	 *
+	 * num_compressed_tracks = bounds.size()
+	 * header : (x num_compressed_tracks)
+	 * -------
+	 * timeline_keys_offset : uint32_t - offset to time keys
+	 * timeline_size : uint32_t - amount of time keys
+	 * data_keys_offset : uint32_t offset to key data
+	 *
+	 * time key (uint32_t):
+	 * ------------------
+	 * frame : bits 0-15 - time offset of key, computed as: page.time_offset + frame * (1.0/fps)
+	 * data_key_offset : bits 16-27 - offset to key data, computed as: data_keys_offset * 4 + data_key_offset
+	 * data_key_count : bits 28-31 - amount of data keys pointed to, computed as: data_key_count+1 (max 16)
+	 *
+	 * data key:
+	 * ---------
+	 * X / Blend Shape : uint16_t - X coordinate of XYZ vector key, or Blend Shape value. If Blend shape, Y and Z are not present and can be ignored.
+	 * Y : uint16_t
+	 * Z : uint16_t
+	 * If data_key_count+1 > 1 (if more than 1 key is stored):
+	 * data_bitwidth : uint16_t - This is only present if data_key_count > 1. Contains delta bitwidth information.
+	 *    X / Blend Shape delta bitwidth: bits 0-3 -
+	 * if 0, nothing is present for X (use the first key-value for subsequent keys),
+	 * else assume the number of bits present for each element (+ 1 for sign). Assumed always 16 bits, delta max signed 15 bits, with underflow and overflow supported.
+	 *    Y delta bitwidth : bits 4-7
+	 *    Z delta bitwidth : bits 8-11
+	 *    FRAME delta bitwidth : 12-15 bits - always present (obviously), actual bitwidth is FRAME+1
+	 * Data key is 4 bytes long for Blend Shapes, 8 bytes long for pos/rot/scale.
+	 *
+	 * delta keys:
+	 * -----------
+	 * Compressed format is packed in the following format after the data key, containing delta keys one after the next in a tightly bit packed fashion.
+	 * FRAME bits -> X / Blend Shape Bits (if bitwidth > 0) -> Y Bits (if not Blend Shape and Y Bitwidth > 0) -> Z Bits (if not Blend Shape and Z Bitwidth > 0)
+	 *
+	 * data key format:
+	 * ----------------
+	 * Decoding keys means starting from the base key and going key by key applying deltas until the proper position is reached needed for interpolation.
+	 * Resulting values are uint32_t
+	 * data for X / Blend Shape, Y and Z must be normalized first: unorm = float(data) / 65535.0
+	 * **Blend Shape**: (unorm * 2.0 - 1.0) * Compression::BLEND_SHAPE_RANGE
+	 * **Pos/Scale**: unorm_vec3 * bounds[track].size + bounds[track].position
+	 * **Rotation**: Quaternion(Vector3::octahedron_decode(unorm_vec3.xy),unorm_vec3.z * Math_PI * 2.0)
+	 * **Frame**: page.time_offset + frame * (1.0/fps)
+	 */
+
+	struct Compression {
+		enum {
+			MAX_DATA_TRACK_SIZE = 16384,
+			BLEND_SHAPE_RANGE = 8, // - 8.0 to 8.0
+			FORMAT_VERSION = 1
+		};
+		struct Page {
+			Vector<uint8_t> data;
+			double time_offset;
+		};
+
+		uint32_t fps = 120;
+		LocalVector<Page> pages;
+		LocalVector<AABB> bounds; //used by position and scale tracks (which contain index to track and index to bounds).
+		bool enabled = false;
+	} compression;
+
+	Vector3i _compress_key(uint32_t p_track, const AABB &p_bounds, int32_t p_key = -1, float p_time = 0.0);
+	bool _rotation_interpolate_compressed(uint32_t p_compressed_track, double p_time, Quaternion &r_ret) const;
+	bool _pos_scale_interpolate_compressed(uint32_t p_compressed_track, double p_time, Vector3 &r_ret) const;
+	bool _blend_shape_interpolate_compressed(uint32_t p_compressed_track, double p_time, float &r_ret) const;
+	template <uint32_t COMPONENTS>
+	bool _fetch_compressed(uint32_t p_compressed_track, double p_time, Vector3i &r_current_value, double &r_current_time, Vector3i &r_next_value, double &r_next_time, uint32_t *key_index = nullptr) const;
+	template <uint32_t COMPONENTS>
+	bool _fetch_compressed_by_index(uint32_t p_compressed_track, int p_index, Vector3i &r_value, double &r_time) const;
+	int _get_compressed_key_count(uint32_t p_compressed_track) const;
+	template <uint32_t COMPONENTS>
+	void _get_compressed_key_indices_in_range(uint32_t p_compressed_track, double p_time, double p_delta, List<int> *r_indices) const;
+	_FORCE_INLINE_ Quaternion _uncompress_quaternion(const Vector3i &p_value) const;
+	_FORCE_INLINE_ Vector3 _uncompress_pos_scale(uint32_t p_compressed_track, const Vector3i &p_value) const;
+	_FORCE_INLINE_ float _uncompress_blend_shape(const Vector3i &p_value) const;
+
 	// bind helpers
 private:
 	Vector<int> _value_track_get_key_indices(int p_track, double p_time, double p_delta) const {
@@ -306,6 +392,7 @@ public:
 	Variant track_get_key_value(int p_track, int p_key_idx) const;
 	double track_get_key_time(int p_track, int p_key_idx) const;
 	real_t track_get_key_transition(int p_track, int p_key_idx) const;
+	bool track_is_compressed(int p_track) const;
 
 	int position_track_insert_key(int p_track, double p_time, const Vector3 &p_position);
 	Error position_track_get_key(int p_track, int p_key, Vector3 *r_position) const;
@@ -376,6 +463,7 @@ public:
 	void clear();
 
 	void optimize(real_t p_allowed_linear_err = 0.05, real_t p_allowed_angular_err = 0.01, real_t p_max_optimizable_angle = Math_PI * 0.125);
+	void compress(uint32_t p_page_size = 8192, uint32_t p_fps = 120, float p_split_tolerance = 4.0); // 4.0 seems to be the split tolerance sweet spot from many tests
 
 	Animation();
 	~Animation();