render: extract cached M2 animation resource observer

This commit is contained in:
2026-07-18 10:23:03 +04:00
parent 3519f183bb
commit df87619220
17 changed files with 1065 additions and 197 deletions
+33 -138
View File
@@ -73,6 +73,9 @@ const M2_STATIC_BATCH_MATERIALIZER_SCRIPT := preload(
const M2_STATIC_BUILD_RESOURCE_OBSERVER_SCRIPT := preload(
"res://src/render/m2/m2_static_build_resource_observer.gd"
)
const M2_CACHED_ANIMATION_RESOURCE_OBSERVER_SCRIPT := preload(
"res://src/render/m2/m2_cached_animation_resource_observer.gd"
)
const M2_RUNTIME_MESH_FINALIZER_SCRIPT := preload(
"res://src/render/m2/m2_runtime_mesh_finalizer.gd"
)
@@ -283,6 +286,9 @@ var _m2_build_dispatch_planner := M2_BUILD_DISPATCH_PLANNER_SCRIPT.new()
var _m2_static_build_resource_observer := (
M2_STATIC_BUILD_RESOURCE_OBSERVER_SCRIPT.new()
)
var _m2_animation_resource_observer := (
M2_CACHED_ANIMATION_RESOURCE_OBSERVER_SCRIPT.new()
)
var _m2_unique_placement_registry := (
M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT.new()
)
@@ -4350,20 +4356,34 @@ func _process_m2_build_jobs() -> void:
var groups := _m2_build_queue_state.groups_for(key)
var transforms: Array = groups.get(rel_path, [])
var offset := _m2_build_queue_state.transform_offset_for(key)
var animated_prototype: Node3D = null
var resource_snapshot: RefCounted
if enable_m2_animated_instances:
animated_prototype = _get_or_load_m2_native_animated_prototype(rel_path)
if animated_prototype == null:
animated_prototype = _get_or_load_m2_animated_prototype(rel_path)
var animation_request_pending := (
enable_m2_animated_instances
and _m2_animation_load_pipeline_state.has_request(normalized_rel)
)
var resource_snapshot: RefCounted = M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
normalized_rel,
animated_prototype,
animation_request_pending
)
var native_animated_prototype := (
_get_or_load_m2_native_animated_prototype(rel_path)
)
if native_animated_prototype != null:
resource_snapshot = M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
normalized_rel,
native_animated_prototype,
false
)
else:
resource_snapshot = _m2_animation_resource_observer.observe(
normalized_rel,
m2_cache_dir,
m2_animated_max_primitives,
m2_animated_allowlist_patterns,
m2_animated_denylist_patterns,
_m2_prototype_cache_state,
_m2_animation_load_pipeline_state,
debug_streaming
)
else:
resource_snapshot = M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
normalized_rel,
null,
false
)
var batch_plan: Dictionary = {}
var batch_count := 0
if not bool(resource_snapshot.call("animation_request_pending")):
@@ -4875,75 +4895,6 @@ func _is_m2_native_animation_candidate(normalized_rel: String) -> bool:
return normalized_rel.to_lower().contains("gryphonroost")
func _get_or_load_m2_animated_prototype(rel_path: String) -> Node3D:
var normalized_rel := _normalize_m2_rel_path(rel_path)
if normalized_rel.is_empty():
return null
var cached_animated := _m2_prototype_cache_state.find_animated_prototype(
normalized_rel
)
if cached_animated != null:
return cached_animated
if _m2_prototype_cache_state.is_animation_static(normalized_rel):
return null
if _m2_animation_load_pipeline_state.has_request(normalized_rel):
return null
_request_m2_animation_load(normalized_rel)
return null
func _request_m2_animation_load(normalized_rel: String) -> void:
if normalized_rel.is_empty() or _m2_animation_load_pipeline_state.has_request(normalized_rel):
return
var cache_res_path := _find_m2_animated_glb_cache_path(normalized_rel)
if cache_res_path.is_empty():
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
return
var err := ResourceLoader.load_threaded_request(
cache_res_path,
"",
false,
ResourceLoader.CACHE_MODE_REUSE)
if err == OK or err == ERR_BUSY:
_m2_animation_load_pipeline_state.remember_request(normalized_rel, cache_res_path)
else:
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
func _find_m2_animated_glb_cache_path(normalized_rel: String) -> String:
if not _is_m2_animation_allowed(normalized_rel):
return ""
if _is_m2_animation_denied(normalized_rel):
return ""
for cache_res_path in _get_m2_cache_resource_paths(normalized_rel, [".glb"]):
if not ResourceLoader.exists(cache_res_path):
continue
if _glb_cache_is_safe_for_runtime_animation(cache_res_path):
return cache_res_path
return ""
func _is_m2_animation_allowed(normalized_rel: String) -> bool:
if m2_animated_allowlist_patterns.is_empty():
return false
var lower := normalized_rel.to_lower()
for pattern in m2_animated_allowlist_patterns:
var needle := String(pattern).strip_edges().to_lower()
if not needle.is_empty() and lower.contains(needle):
return true
return false
func _is_m2_animation_denied(normalized_rel: String) -> bool:
var lower := normalized_rel.to_lower()
for pattern in m2_animated_denylist_patterns:
var needle := String(pattern).strip_edges().to_lower()
if not needle.is_empty() and lower.contains(needle):
return true
return false
func _get_or_load_m2_material_prototype(normalized_rel: String) -> Node3D:
if normalized_rel.is_empty():
return null
@@ -4967,62 +4918,6 @@ func _get_or_load_m2_material_prototype(normalized_rel: String) -> Node3D:
return null
func _glb_cache_is_safe_for_runtime_animation(cache_res_path: String) -> bool:
var abs_path := ProjectSettings.globalize_path(cache_res_path)
if not FileAccess.file_exists(abs_path):
return false
var file := FileAccess.open(abs_path, FileAccess.READ)
if file == null or file.get_length() < 20:
return false
var magic := file.get_32()
var version := file.get_32()
file.get_32()
if magic != 0x46546c67 or version != 2:
return false
var json_len := int(file.get_32())
var chunk_type := file.get_32()
if json_len <= 0 or chunk_type != 0x4e4f534a:
return false
var json_text := file.get_buffer(json_len).get_string_from_utf8()
var parsed = JSON.parse_string(json_text)
if not (parsed is Dictionary):
return false
var gltf := parsed as Dictionary
var animations: Array = gltf.get("animations", [])
if animations.is_empty():
return false
var primitive_count := _glb_primitive_count(gltf)
if m2_animated_max_primitives > 0 and primitive_count > m2_animated_max_primitives:
return false
var schema := _glb_animation_schema(gltf)
if schema == "pivot_prefix_v1":
return true
# Older simple critter GLBs were generated before the schema marker existed.
# Accept them only after allowlist/denylist filtering and primitive-count checks.
if schema.is_empty():
return true
if debug_streaming:
print("M2_ANIM_REJECT schema=%s primitives=%d cache=%s" % [schema, primitive_count, cache_res_path])
return false
func _glb_primitive_count(gltf: Dictionary) -> int:
var count := 0
var meshes: Array = gltf.get("meshes", [])
for mesh_variant in meshes:
if not (mesh_variant is Dictionary):
continue
var primitives: Array = (mesh_variant as Dictionary).get("primitives", [])
count += primitives.size()
return count
func _glb_animation_schema(gltf: Dictionary) -> String:
var asset: Dictionary = gltf.get("asset", {})
var extras: Dictionary = asset.get("extras", {})
return String(extras.get("openwc_m2_anim_schema", ""))
func _get_or_load_wmo_prototype(rel_path: String) -> Node3D:
var normalized_rel := rel_path.replace("\\", "/")
if normalized_rel.is_empty():