refactor(M03): extract M2 runtime rebuild classifier

This commit is contained in:
2026-07-17 10:01:29 +04:00
parent 324e64dd6e
commit 19ca8f48e8
9 changed files with 600 additions and 52 deletions
@@ -0,0 +1,93 @@
class_name M2RuntimeMeshRebuildClassifier
extends RefCounted
## Memoizes whether raw M2 billboard or UV-rotation metadata requires replacing
## a stale cached runtime mesh. Owns no parser, Mesh, material, Node or RID.
const UV_ROTATION_SPEED_EPSILON := 0.000001
const MAXIMUM_TEXTURE_STAGES := 4
var _rebuild_required_by_normalized_path: Dictionary = {}
## Returns the first memoized rebuild decision for [param normalized_relative_path].
## [param raw_m2_data] is borrowed and not mutated. The decision remains cached
## until [method clear], matching the loader map/shutdown reset lifetime.
func needs_runtime_mesh_rebuild(
normalized_relative_path: String,
raw_m2_data: Dictionary) -> bool:
if _rebuild_required_by_normalized_path.has(normalized_relative_path):
return bool(_rebuild_required_by_normalized_path[normalized_relative_path])
var rebuild_required := (
_raw_data_has_billboards(raw_m2_data)
or _raw_data_has_uv_rotation(raw_m2_data)
)
_rebuild_required_by_normalized_path[normalized_relative_path] = rebuild_required
return rebuild_required
## Clears every memoized path decision. No Mesh or resource is retained or freed.
func clear() -> void:
_rebuild_required_by_normalized_path.clear()
## Returns the number of memoized normalized paths.
func cached_path_count() -> int:
return _rebuild_required_by_normalized_path.size()
## Returns a detached normalized-path to boolean decision snapshot.
func diagnostic_snapshot() -> Dictionary:
return _rebuild_required_by_normalized_path.duplicate()
func _raw_data_has_billboards(raw_m2_data: Dictionary) -> bool:
for batch_variant in raw_m2_data.get("batches", []):
if not (batch_variant is Dictionary):
continue
var batch: Dictionary = batch_variant
if (
bool(batch.get("has_billboard", false))
or int(batch.get("billboard_vertex_count", 0)) > 0
):
return true
return false
func _raw_data_has_uv_rotation(raw_m2_data: Dictionary) -> bool:
var texture_transforms: Array = raw_m2_data.get("texture_transforms", [])
if texture_transforms.is_empty():
return false
var texture_transform_combos: PackedInt32Array = raw_m2_data.get(
"texture_transform_combos",
PackedInt32Array()
)
if texture_transform_combos.is_empty():
return false
for batch_variant in raw_m2_data.get("batches", []):
if not (batch_variant is Dictionary):
continue
var batch: Dictionary = batch_variant
var texture_count := maxi(1, int(batch.get("texture_count", 1)))
var base_combo_index := int(batch.get("texture_transform_combo_index", -1))
for texture_stage in mini(texture_count, MAXIMUM_TEXTURE_STAGES):
var combo_index := base_combo_index + texture_stage
if combo_index < 0 or combo_index >= texture_transform_combos.size():
continue
var transform_index := int(texture_transform_combos[combo_index])
if transform_index < 0 or transform_index >= texture_transforms.size():
continue
var texture_transform: Dictionary = (
texture_transforms[transform_index]
if texture_transforms[transform_index] is Dictionary
else {}
)
var rotation: Vector4 = texture_transform.get(
"rotation",
Vector4(1.0, 0.0, 0.0, 1.0)
)
if not rotation.is_equal_approx(Vector4(1.0, 0.0, 0.0, 1.0)):
return true
if absf(float(texture_transform.get("rotation_speed", 0.0))) > UV_ROTATION_SPEED_EPSILON:
return true
return false
@@ -0,0 +1 @@
uid://breu8vpmm88en