refactor(M03): extract M2 animated scene finalizer

This commit is contained in:
2026-07-18 01:14:17 +04:00
parent 03ba129a52
commit 1b450dc580
11 changed files with 581 additions and 95 deletions
@@ -0,0 +1,115 @@
class_name M2AnimatedSceneFinalizer
extends RefCounted
## Instantiates and validates detached animated M2 scene candidates and repairs
## their surface materials from an existing static material prototype.
## The caller owns ResourceLoader I/O, permits and prototype cache adoption.
## Instantiates one PackedScene as a detached Node3D candidate. Unsupported
## Resources return null; a created non-Node3D root is freed before return.
func instantiate_candidate(resource: Resource) -> Node3D:
if not (resource is PackedScene):
return null
var candidate_root := (resource as PackedScene).instantiate()
if candidate_root is Node3D:
return candidate_root as Node3D
if candidate_root != null:
candidate_root.free()
return null
## Applies the historical depth-first source-to-target material mapping. A null
## source or target leaves the candidate unchanged.
func repair_materials(animated_root: Node3D, material_source_root: Node3D) -> void:
if animated_root == null or material_source_root == null:
return
var source_meshes := mesh_instances_in_subtree(material_source_root)
var target_meshes := mesh_instances_in_subtree(animated_root)
if source_meshes.is_empty() or target_meshes.is_empty():
return
var fallback_material := _first_mesh_material(source_meshes)
for target_index in target_meshes.size():
var target := target_meshes[target_index]
if target == null or target.mesh == null:
continue
var source := source_meshes[mini(target_index, source_meshes.size() - 1)]
for surface_index in target.mesh.get_surface_count():
var material := _mesh_instance_surface_material(source, surface_index)
if material == null:
material = fallback_material
if material != null:
target.set_surface_override_material(surface_index, material)
## Accepts a detached candidate only when it contains an AnimationPlayer.
## Rejected candidates are freed synchronously. A successful result transfers
## the exact prototype reference and its AnimationPlayer count to the caller.
func finalize_candidate(candidate_root: Node3D) -> Dictionary:
if candidate_root == null:
return {}
var animation_players := animation_players_in_subtree(candidate_root)
if animation_players.is_empty():
candidate_root.free()
return {}
return {
"prototype": candidate_root,
"animation_player_count": animation_players.size(),
}
## Returns MeshInstance3D descendants in depth-first preorder, including root.
func mesh_instances_in_subtree(root: Node) -> Array[MeshInstance3D]:
var mesh_instances: Array[MeshInstance3D] = []
if root != null:
_collect_mesh_instances(root, mesh_instances)
return mesh_instances
## Returns AnimationPlayer descendants in depth-first preorder, including root.
func animation_players_in_subtree(root: Node) -> Array[AnimationPlayer]:
var animation_players: Array[AnimationPlayer] = []
if root != null:
_collect_animation_players(root, animation_players)
return animation_players
func _collect_mesh_instances(node: Node, result: Array[MeshInstance3D]) -> void:
if node is MeshInstance3D:
result.append(node as MeshInstance3D)
for child in node.get_children():
_collect_mesh_instances(child, result)
func _collect_animation_players(node: Node, result: Array[AnimationPlayer]) -> void:
if node is AnimationPlayer:
result.append(node as AnimationPlayer)
for child in node.get_children():
_collect_animation_players(child, result)
func _first_mesh_material(meshes: Array[MeshInstance3D]) -> Material:
for mesh_instance in meshes:
if mesh_instance == null or mesh_instance.mesh == null:
continue
for surface_index in mesh_instance.mesh.get_surface_count():
var material := _mesh_instance_surface_material(mesh_instance, surface_index)
if material != null:
return material
return null
func _mesh_instance_surface_material(
mesh_instance: MeshInstance3D,
surface_index: int
) -> Material:
if mesh_instance == null or mesh_instance.mesh == null:
return null
if surface_index < mesh_instance.get_surface_override_material_count():
var override_material := mesh_instance.get_surface_override_material(surface_index)
if override_material != null:
return override_material
if surface_index < mesh_instance.mesh.get_surface_count():
return mesh_instance.mesh.surface_get_material(surface_index)
return null
@@ -0,0 +1 @@
uid://cjjim3v675w66