94 lines
3.3 KiB
GDScript
94 lines
3.3 KiB
GDScript
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
|