class_name M2AnimatedInstanceMaterializer extends RefCounted ## Duplicates one ordered animated M2 batch, applies instance render settings, ## starts playback and attaches a non-empty batch to the supplied parent. ## All calls mutate Godot scene objects and therefore belong on the main thread. const M2_ANIMATED_SCENE_FINALIZER_SCRIPT := preload( "res://src/render/m2/m2_animated_scene_finalizer.gd" ) const M2_ANIMATION_PLAYBACK_CONTROLLER_SCRIPT := preload( "res://src/render/m2/m2_animation_playback_controller.gd" ) var _animated_scene_finalizer := M2_ANIMATED_SCENE_FINALIZER_SCRIPT.new() var _animation_playback_controller := M2_ANIMATION_PLAYBACK_CONTROLLER_SCRIPT.new() ## Returns the attached batch root and detached diagnostic entries. Invalid or ## empty input, and a batch whose duplicates all fail, returns an empty result. ## The caller owns the returned/attached batch through `m2_parent_root`. func materialize_batch( m2_parent_root: Node3D, relative_path: String, prototype: Node3D, transforms: Array, start_index: int, instance_count: int, batch_serial: int, visibility_range_end: float, visibility_range_margin: float, cast_shadows: bool, native_animator_script: Script, collect_native_diagnostics: bool ) -> Dictionary: if ( m2_parent_root == null or transforms.is_empty() or instance_count <= 0 or prototype == null ): return {} var model_name := relative_path.get_file().get_basename() var batch_root := Node3D.new() batch_root.name = "%s_anim_%d" % [model_name, batch_serial] var diagnostic_entries: Array[Dictionary] = [] for batch_offset in instance_count: var instance_index := start_index + batch_offset var instance := prototype.duplicate( Node.DUPLICATE_SIGNALS | Node.DUPLICATE_GROUPS | Node.DUPLICATE_SCRIPTS ) as Node3D if instance == null: continue instance.name = "%s_%d" % [model_name, instance_index] instance.transform = transforms[instance_index] _apply_visibility_range_recursive( instance, visibility_range_end, visibility_range_margin ) _apply_shadow_cast_recursive(instance, cast_shadows) _animation_playback_controller.copy_native_animator_data( prototype, instance, native_animator_script ) batch_root.add_child(instance) var animation_players := _animated_scene_finalizer.animation_players_in_subtree( instance ) var native_diagnostics := _animation_playback_controller.start_instance_playback( instance, relative_path, instance_index, native_animator_script, animation_players, collect_native_diagnostics ) for native_diagnostic in native_diagnostics: diagnostic_entries.append({ "instance_index": instance_index, "state": native_diagnostic, }) if batch_root.get_child_count() <= 0: batch_root.queue_free() return {} m2_parent_root.add_child(batch_root) return { "batch_root": batch_root, "native_diagnostics": diagnostic_entries, } func _apply_visibility_range_recursive( node: Node, range_end: float, range_end_margin: float ) -> void: if range_end <= 0.0: return if node is GeometryInstance3D: var geometry := node as GeometryInstance3D geometry.visibility_range_end = range_end geometry.visibility_range_end_margin = range_end_margin for child in node.get_children(): _apply_visibility_range_recursive(child, range_end, range_end_margin) func _apply_shadow_cast_recursive(node: Node, cast_shadows: bool) -> void: if node is GeometryInstance3D: var geometry := node as GeometryInstance3D geometry.cast_shadow = ( GeometryInstance3D.SHADOW_CASTING_SETTING_ON if cast_shadows else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF ) for child in node.get_children(): _apply_shadow_cast_recursive(child, cast_shadows)