render: extract cached M2 animation resource observer
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
class_name M2CachedAnimationResourceObserver
|
||||
extends RefCounted
|
||||
|
||||
## Observes or requests the cached-GLB animated phase of one M2 build step.
|
||||
|
||||
const M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_build_resource_snapshot.gd"
|
||||
)
|
||||
|
||||
|
||||
## Returns a snapshot containing the exact cached prototype or pending state.
|
||||
## Invalid composition and terminal failures produce a non-pending snapshot.
|
||||
func observe(
|
||||
normalized_relative_path: String,
|
||||
cache_directory: String,
|
||||
maximum_primitive_count: int,
|
||||
allowlist_patterns: PackedStringArray,
|
||||
denylist_patterns: PackedStringArray,
|
||||
prototype_cache_state: RefCounted,
|
||||
load_pipeline_state: RefCounted,
|
||||
debug_logging_enabled: bool = false
|
||||
) -> RefCounted:
|
||||
var empty_snapshot: RefCounted = M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
|
||||
normalized_relative_path,
|
||||
null,
|
||||
false
|
||||
)
|
||||
if (
|
||||
normalized_relative_path.is_empty()
|
||||
or prototype_cache_state == null
|
||||
or load_pipeline_state == null
|
||||
):
|
||||
return empty_snapshot
|
||||
|
||||
var cached_prototype := prototype_cache_state.call(
|
||||
"find_animated_prototype",
|
||||
normalized_relative_path
|
||||
) as Node3D
|
||||
if cached_prototype != null:
|
||||
return M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
|
||||
normalized_relative_path,
|
||||
cached_prototype,
|
||||
false
|
||||
)
|
||||
if bool(prototype_cache_state.call(
|
||||
"is_animation_static",
|
||||
normalized_relative_path
|
||||
)):
|
||||
return empty_snapshot
|
||||
if bool(load_pipeline_state.call("has_request", normalized_relative_path)):
|
||||
return M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
|
||||
normalized_relative_path,
|
||||
null,
|
||||
true
|
||||
)
|
||||
|
||||
var cache_resource_path := find_eligible_glb_cache_path(
|
||||
normalized_relative_path,
|
||||
cache_directory,
|
||||
maximum_primitive_count,
|
||||
allowlist_patterns,
|
||||
denylist_patterns,
|
||||
debug_logging_enabled
|
||||
)
|
||||
if cache_resource_path.is_empty():
|
||||
prototype_cache_state.call(
|
||||
"mark_animation_static",
|
||||
normalized_relative_path
|
||||
)
|
||||
return empty_snapshot
|
||||
|
||||
var request_error := ResourceLoader.load_threaded_request(
|
||||
cache_resource_path,
|
||||
"",
|
||||
false,
|
||||
ResourceLoader.CACHE_MODE_REUSE
|
||||
)
|
||||
if request_error == OK or request_error == ERR_BUSY:
|
||||
load_pipeline_state.call(
|
||||
"remember_request",
|
||||
normalized_relative_path,
|
||||
cache_resource_path
|
||||
)
|
||||
return M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
|
||||
normalized_relative_path,
|
||||
null,
|
||||
true
|
||||
)
|
||||
|
||||
prototype_cache_state.call("mark_animation_static", normalized_relative_path)
|
||||
return empty_snapshot
|
||||
|
||||
|
||||
## Returns the first eligible historical `.glb` cache candidate.
|
||||
func find_eligible_glb_cache_path(
|
||||
normalized_relative_path: String,
|
||||
cache_directory: String,
|
||||
maximum_primitive_count: int,
|
||||
allowlist_patterns: PackedStringArray,
|
||||
denylist_patterns: PackedStringArray,
|
||||
debug_logging_enabled: bool = false
|
||||
) -> String:
|
||||
if not is_animation_path_allowed(
|
||||
normalized_relative_path,
|
||||
allowlist_patterns,
|
||||
denylist_patterns
|
||||
):
|
||||
return ""
|
||||
for cache_resource_path in cache_resource_paths(
|
||||
cache_directory,
|
||||
normalized_relative_path
|
||||
):
|
||||
if not ResourceLoader.exists(cache_resource_path):
|
||||
continue
|
||||
if glb_cache_is_safe_for_runtime_animation(
|
||||
cache_resource_path,
|
||||
maximum_primitive_count,
|
||||
debug_logging_enabled
|
||||
):
|
||||
return cache_resource_path
|
||||
return ""
|
||||
|
||||
|
||||
## Applies the historical stripped, case-insensitive allow/deny substring policy.
|
||||
func is_animation_path_allowed(
|
||||
normalized_relative_path: String,
|
||||
allowlist_patterns: PackedStringArray,
|
||||
denylist_patterns: PackedStringArray
|
||||
) -> bool:
|
||||
if normalized_relative_path.is_empty() or allowlist_patterns.is_empty():
|
||||
return false
|
||||
var lowercase_path := normalized_relative_path.to_lower()
|
||||
var allowed := false
|
||||
for pattern in allowlist_patterns:
|
||||
var needle := String(pattern).strip_edges().to_lower()
|
||||
if not needle.is_empty() and lowercase_path.contains(needle):
|
||||
allowed = true
|
||||
break
|
||||
if not allowed:
|
||||
return false
|
||||
for pattern in denylist_patterns:
|
||||
var needle := String(pattern).strip_edges().to_lower()
|
||||
if not needle.is_empty() and lowercase_path.contains(needle):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
## Returns historical nested/lowercase/basename `.glb` candidates without I/O.
|
||||
func cache_resource_paths(
|
||||
cache_directory: String,
|
||||
relative_path: String
|
||||
) -> PackedStringArray:
|
||||
var normalized := relative_path.replace("\\", "/")
|
||||
var lowercase := normalized.to_lower()
|
||||
var stems := [
|
||||
normalized.get_basename(),
|
||||
lowercase.get_basename(),
|
||||
normalized.get_file().get_basename(),
|
||||
lowercase.get_file().get_basename(),
|
||||
]
|
||||
var result := PackedStringArray()
|
||||
for stem in stems:
|
||||
if stem.is_empty():
|
||||
continue
|
||||
var cache_resource_path := cache_directory.path_join(stem + ".glb")
|
||||
if not result.has(cache_resource_path):
|
||||
result.append(cache_resource_path)
|
||||
return result
|
||||
|
||||
|
||||
## Checks animation presence, primitive limit and accepted OpenWC schema.
|
||||
func glb_cache_is_safe_for_runtime_animation(
|
||||
cache_resource_path: String,
|
||||
maximum_primitive_count: int,
|
||||
debug_logging_enabled: bool = false
|
||||
) -> bool:
|
||||
var gltf := read_glb_json(cache_resource_path)
|
||||
if gltf.is_empty():
|
||||
return false
|
||||
var animations: Array = gltf.get("animations", [])
|
||||
if animations.is_empty():
|
||||
return false
|
||||
var primitive_count := glb_primitive_count(gltf)
|
||||
if maximum_primitive_count > 0 and primitive_count > maximum_primitive_count:
|
||||
return false
|
||||
var schema := glb_animation_schema(gltf)
|
||||
if schema == "pivot_prefix_v1" or schema.is_empty():
|
||||
return true
|
||||
if debug_logging_enabled:
|
||||
print(
|
||||
"M2_ANIM_REJECT schema=%s primitives=%d cache=%s"
|
||||
% [schema, primitive_count, cache_resource_path]
|
||||
)
|
||||
return false
|
||||
|
||||
|
||||
## Reads the JSON chunk of a version-2 GLB or returns an empty Dictionary.
|
||||
func read_glb_json(cache_resource_path: String) -> Dictionary:
|
||||
var absolute_path := ProjectSettings.globalize_path(cache_resource_path)
|
||||
if not FileAccess.file_exists(absolute_path):
|
||||
return {}
|
||||
var file := FileAccess.open(absolute_path, FileAccess.READ)
|
||||
if file == null or file.get_length() < 20:
|
||||
return {}
|
||||
var magic := file.get_32()
|
||||
var version := file.get_32()
|
||||
file.get_32()
|
||||
if magic != 0x46546c67 or version != 2:
|
||||
return {}
|
||||
var json_length := int(file.get_32())
|
||||
var chunk_type := file.get_32()
|
||||
if json_length <= 0 or chunk_type != 0x4e4f534a:
|
||||
return {}
|
||||
var parsed: Variant = JSON.parse_string(
|
||||
file.get_buffer(json_length).get_string_from_utf8()
|
||||
)
|
||||
return parsed as Dictionary if parsed is Dictionary else {}
|
||||
|
||||
|
||||
## Counts all glTF Mesh primitives using the historical safety rule.
|
||||
func glb_primitive_count(gltf: Dictionary) -> int:
|
||||
var count := 0
|
||||
var meshes: Array = gltf.get("meshes", [])
|
||||
for mesh_variant in meshes:
|
||||
if mesh_variant is Dictionary:
|
||||
var primitives: Array = (mesh_variant as Dictionary).get(
|
||||
"primitives",
|
||||
[]
|
||||
)
|
||||
count += primitives.size()
|
||||
return count
|
||||
|
||||
|
||||
## Returns the OpenWC animation schema marker from parsed glTF metadata.
|
||||
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", ""))
|
||||
@@ -0,0 +1 @@
|
||||
uid://d0ptej41k2smt
|
||||
Reference in New Issue
Block a user