Last active
May 25, 2025 02:36
-
-
Save lyuma/0bb0d9306812905053c03ebf31f47bcf to your computer and use it in GitHub Desktop.
Records position and rotation changes to a node tree.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # node_animation_recorder.gd | |
| # Copyright (c) 2025- Lyuma and V-Sekai developers | |
| # | |
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |
| # of this software and associated documentation files (the "Software"), to deal | |
| # in the Software without restriction, including without limitation the rights | |
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| # copies of the Software, and to permit persons to whom the Software is | |
| # furnished to do so, subject to the following conditions: | |
| # | |
| # The above copyright notice and this permission notice shall be included in all | |
| # copies or substantial portions of the Software. | |
| # | |
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| # SOFTWARE. | |
| extends Node | |
| class_name NodeAnimationRecorder | |
| @export var initial_anim: Animation: | |
| set(value): | |
| initial_anim = value | |
| if is_node_ready(): | |
| initialize_anim() | |
| @export var anim_root: NodePath = ^".." | |
| @export var initial_timestamp: float = -1 | |
| @export_custom(PROPERTY_HINT_GLOBAL_SAVE_FILE, "*.anim") var animation_path: String | |
| @export var save_interval: float = -1 | |
| @export var node_paths: Array[NodePath] | |
| @export var extra_properties: PackedStringArray | |
| var anim: Animation | |
| var root_node: Node | |
| var node_path_cache: Dictionary[Node, NodePath] | |
| var node_3d_cache: Dictionary[Node3D, NodePath] | |
| var node_pos_eq_timestamp: Dictionary[Node3D, float] | |
| var node_rot_eq_timestamp: Dictionary[Node3D, float] | |
| var node_pos_track_cache: Dictionary[NodePath, int] | |
| var node_rot_track_cache: Dictionary[NodePath, int] | |
| var node_position_cache: Dictionary[Node3D, Vector3] | |
| var node_rotation_cache: Dictionary[Node3D, Quaternion] | |
| var node_extra_eq_timestamp: Dictionary[Node3D, PackedFloat64Array] | |
| var node_extra_track_cache: Dictionary[NodePath, PackedInt64Array] | |
| var node_extra_value_cache: Dictionary[Node3D, Array] | |
| var timestamp: float | |
| var last_save: float | |
| func _child_node_added(node: Node): | |
| add_node(node, root_node.get_path_to(node)) | |
| func add_node(node: Node, node_path: NodePath): | |
| if node_path_cache.has(node): | |
| return | |
| node.child_entered_tree.connect(_child_node_added) | |
| var node_3d := node as Node3D | |
| if node_3d != null: | |
| node_3d_cache[node_3d] = node_path | |
| if not node_pos_track_cache.has(node_path): | |
| var pos_track_idx := anim.add_track(Animation.TYPE_POSITION_3D) | |
| node_pos_track_cache[node_path] = pos_track_idx | |
| anim.track_set_path(pos_track_idx, node_path) | |
| var pos := node_3d.position | |
| anim.position_track_insert_key(pos_track_idx, timestamp, pos) | |
| node_position_cache[node_3d] = pos | |
| if not node_rot_track_cache.has(node_path): | |
| var rot_track_idx := anim.add_track(Animation.TYPE_ROTATION_3D) | |
| node_rot_track_cache[node_path] = rot_track_idx | |
| anim.track_set_path(rot_track_idx, node_path) | |
| var rot := node_3d.quaternion | |
| anim.rotation_track_insert_key(rot_track_idx, timestamp, rot) | |
| node_rotation_cache[node_3d] = rot | |
| if not node_extra_track_cache.has(node_path): | |
| var extra_track_cache: PackedInt64Array | |
| var extra_value_cache: Array | |
| var extra_eq_timestamp: PackedFloat64Array | |
| extra_track_cache.resize(len(extra_properties)) | |
| extra_value_cache.resize(len(extra_properties)) | |
| extra_eq_timestamp.resize(len(extra_properties)) | |
| var has_extra: bool = false | |
| for i in range(len(extra_properties)): | |
| extra_track_cache[i] = -1 | |
| extra_eq_timestamp[i] = -1 | |
| var prop := extra_properties[i] | |
| if typeof(node.get(prop)) != TYPE_NIL: | |
| var extra_track_idx := anim.add_track(Animation.TYPE_VALUE) | |
| extra_track_cache[i] = extra_track_idx | |
| anim.track_set_path(extra_track_idx, str(node_path) + ":" + prop) | |
| var val: Variant = node.get(prop) | |
| anim.track_insert_key(extra_track_idx, timestamp, val) | |
| extra_value_cache[i] = val | |
| has_extra = true | |
| if has_extra: | |
| node_extra_eq_timestamp[node_3d] = extra_eq_timestamp | |
| node_extra_track_cache[node_path] = extra_track_cache | |
| node_extra_value_cache[node_3d] = extra_value_cache | |
| else: | |
| node_extra_track_cache[node_path] = PackedInt64Array() | |
| for child_node in node.get_children(): | |
| add_node(child_node, NodePath(str(node_path) + "/" + str(child_node.name))) | |
| func _ready(): | |
| initialize_anim() | |
| func initialize_anim(): | |
| node_path_cache.clear() | |
| node_pos_track_cache.clear() | |
| node_rot_track_cache.clear() | |
| if initial_anim == null: | |
| anim = Animation.new() | |
| else: | |
| anim = initial_anim.duplicate() as Animation | |
| if initial_timestamp == -1: | |
| timestamp = anim.length | |
| else: | |
| timestamp = initial_timestamp | |
| last_save = timestamp | |
| root_node = get_node(anim_root) | |
| # Cache existing animation tracks | |
| for track_idx in range(anim.get_track_count()): | |
| var typ := anim.track_get_type(track_idx) | |
| var node_path := anim.track_get_path(track_idx) | |
| if typ == Animation.TYPE_POSITION_3D: | |
| node_pos_track_cache[node_path] = track_idx | |
| if typ == Animation.TYPE_ROTATION_3D: | |
| node_rot_track_cache[node_path] = track_idx | |
| # Now add existing nodes from the scene. | |
| for node_path in node_paths: | |
| var node: Node = get_node(node_path) | |
| add_node(node, root_node.get_path_to(node)) | |
| func _process(delta: float): | |
| timestamp += delta | |
| anim.length = timestamp | |
| for node_3d in node_3d_cache: | |
| var node_path := node_3d_cache[node_3d] | |
| var pos_idx := node_pos_track_cache[node_path] | |
| var rot_idx := node_rot_track_cache[node_path] | |
| var pos: Vector3 = node_3d.position | |
| if node_3d not in node_position_cache or not node_position_cache[node_3d].is_equal_approx(pos): | |
| if node_pos_eq_timestamp.has(node_3d) and node_pos_eq_timestamp[node_3d] != -1: | |
| # print("POS " + str(pos_idx) + ": " + str(node_pos_eq_timestamp[node_3d]) + " flat" + " count=" + str(anim.track_get_key_count(pos_idx))) | |
| anim.position_track_insert_key(pos_idx, node_pos_eq_timestamp[node_3d], node_position_cache[node_3d]) | |
| node_position_cache[node_3d] = pos | |
| # print("POS " + str(pos_idx) + ": " + str(timestamp) + " count=" + str(anim.track_get_key_count(pos_idx))) | |
| anim.position_track_insert_key(pos_idx, timestamp, pos) | |
| node_pos_eq_timestamp[node_3d] = -1 | |
| else: | |
| node_pos_eq_timestamp[node_3d] = timestamp | |
| var rot: Quaternion = node_3d.quaternion | |
| if node_3d not in node_rotation_cache or not node_rotation_cache[node_3d].is_equal_approx(rot): | |
| if node_rot_eq_timestamp.has(node_3d) and node_rot_eq_timestamp[node_3d] != -1: | |
| # print("rot " + str(rot_idx) + ": " + str(node_rot_eq_timestamp[node_3d]) + " flat" + " count=" + str(anim.track_get_key_count(rot_idx))) | |
| anim.rotation_track_insert_key(rot_idx, node_rot_eq_timestamp[node_3d], node_rotation_cache[node_3d]) | |
| node_rotation_cache[node_3d] = rot | |
| # print("rot " + str(rot_idx) + ": " + str(timestamp) + " count=" + str(anim.track_get_key_count(rot_idx))) | |
| anim.rotation_track_insert_key(rot_idx, timestamp, rot) | |
| node_rot_eq_timestamp[node_3d] = -1 | |
| else: | |
| node_rot_eq_timestamp[node_3d] = timestamp | |
| if node_extra_track_cache.get(node_path): | |
| var extra_track_cache: PackedInt64Array = node_extra_track_cache[node_path] | |
| var extra_value_cache: Array = node_extra_value_cache[node_3d] | |
| var extra_eq_timestamp: PackedFloat64Array = node_extra_eq_timestamp[node_3d] | |
| for i in range(len(extra_track_cache)): | |
| var track_idx := extra_track_cache[i] | |
| var val: Variant = node_3d.get(extra_properties[i]) | |
| var equal: bool = val == extra_value_cache[i] | |
| match typeof(val): | |
| TYPE_VECTOR2, TYPE_VECTOR3, TYPE_VECTOR4, TYPE_BASIS, \ | |
| TYPE_TRANSFORM2D, TYPE_TRANSFORM3D, TYPE_AABB, TYPE_COLOR, \ | |
| TYPE_RECT2, TYPE_QUATERNION, TYPE_PROJECTION, TYPE_PLANE: | |
| equal = val.is_equal_approx(extra_value_cache[i]) | |
| TYPE_FLOAT: | |
| equal = is_equal_approx(val as float, extra_value_cache[i]) | |
| if not equal: | |
| if extra_eq_timestamp[i] != -1: | |
| anim.track_insert_key(track_idx, extra_eq_timestamp[i], extra_value_cache[i]) | |
| extra_value_cache[i] = val | |
| anim.track_insert_key(track_idx, timestamp, val) | |
| extra_eq_timestamp[i] = -1 | |
| else: | |
| extra_eq_timestamp[i] = timestamp | |
| if save_interval >= 0 and last_save + save_interval < timestamp: | |
| ResourceSaver.save(anim, animation_path + "tmp.anim", ResourceSaver.FLAG_COMPRESS) | |
| DirAccess.open("res://").rename_absolute(animation_path + "tmp.anim", animation_path) | |
| last_save = timestamp | |
| func _exit_tree() -> void: | |
| OS.delay_msec(100) | |
| pass # ResourceSaver.save(anim, animation_path, ResourceSaver.FLAG_COMPRESS) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment