render: extract animated M2 materializer

This commit is contained in:
2026-07-18 01:48:55 +04:00
parent 9ab4c0762d
commit 456f3e2334
16 changed files with 725 additions and 78 deletions
@@ -0,0 +1,120 @@
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)