7815385b3b
Work-Package: M01-RND-STREAMING-FOCUS-001 Agent: sindo-main-codex
167 lines
6.1 KiB
GDScript
167 lines
6.1 KiB
GDScript
extends SceneTree
|
|
|
|
## Reports published scene-tree geometry around calibrated renderer cameras.
|
|
## Usage: godot --headless --path . --script res://src/tools/probe_render_camera_occluders.gd --
|
|
## [--wait 3.0] [--output user://render_camera_occluders/report.json]
|
|
|
|
const MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
|
|
const MAX_REPORTED_INTERSECTIONS := 20
|
|
|
|
|
|
func _initialize() -> void:
|
|
_run_async.call_deferred()
|
|
|
|
|
|
func _run_async() -> void:
|
|
var arguments := OS.get_cmdline_user_args()
|
|
var wait_seconds := float(_argument(arguments, "--wait", "3.0"))
|
|
var output_path := _argument(arguments, "--output", "user://render_camera_occluders/report.json")
|
|
var only_filter := _argument(arguments, "--only", "").to_lower()
|
|
var manifest := _load_json(MANIFEST_PATH)
|
|
var packed_scene: PackedScene = load(String(manifest.get("scene", "")))
|
|
if packed_scene == null:
|
|
push_error("CAMERA_OCCLUDER_PROBE: cannot load streaming scene")
|
|
quit(1)
|
|
return
|
|
var world := packed_scene.instantiate() as Node3D
|
|
var camera := Camera3D.new()
|
|
camera.name = "OccluderProbeCamera"
|
|
camera.current = true
|
|
world.add_child(camera)
|
|
world.set("streaming_focus_source_path", NodePath(camera.name))
|
|
world.set("debug_streaming", false)
|
|
get_root().add_child(world)
|
|
await process_frame
|
|
await process_frame
|
|
|
|
var results: Array[Dictionary] = []
|
|
for checkpoint_variant in manifest.get("checkpoints", []):
|
|
if not (checkpoint_variant is Dictionary):
|
|
continue
|
|
var checkpoint: Dictionary = checkpoint_variant
|
|
if not checkpoint.has("reference_wow_camera"):
|
|
continue
|
|
var checkpoint_name := String(checkpoint.get("name", "checkpoint"))
|
|
if not only_filter.is_empty() and not checkpoint_name.to_lower().contains(only_filter):
|
|
continue
|
|
var camera_position := _vector3(checkpoint.get("camera", []))
|
|
var target_position := _vector3(checkpoint.get("target", []))
|
|
camera.global_position = camera_position
|
|
if world.has_method("refresh_streaming_focus"):
|
|
world.call("refresh_streaming_focus", true)
|
|
await create_timer(maxf(0.1, wait_seconds)).timeout
|
|
var geometry_nodes: Array[Node3D] = []
|
|
_collect_geometry_nodes(world, geometry_nodes)
|
|
var containing: Array[Dictionary] = []
|
|
var intersecting: Array[Dictionary] = []
|
|
for geometry_node in geometry_nodes:
|
|
var world_aabb := _geometry_world_aabb(geometry_node)
|
|
if world_aabb.size.is_zero_approx():
|
|
continue
|
|
var record := _geometry_record(geometry_node, world_aabb, camera_position)
|
|
if world_aabb.has_point(camera_position):
|
|
containing.append(record)
|
|
var intersection = world_aabb.intersects_segment(camera_position, target_position)
|
|
if intersection != null:
|
|
record["intersection_distance"] = camera_position.distance_to(intersection as Vector3)
|
|
intersecting.append(record)
|
|
intersecting.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
|
return float(a.intersection_distance) < float(b.intersection_distance))
|
|
if intersecting.size() > MAX_REPORTED_INTERSECTIONS:
|
|
intersecting.resize(MAX_REPORTED_INTERSECTIONS)
|
|
var result := {
|
|
"name": checkpoint_name,
|
|
"geometry_node_count": geometry_nodes.size(),
|
|
"camera_containing_geometry": containing,
|
|
"segment_intersecting_geometry": intersecting,
|
|
}
|
|
results.append(result)
|
|
print("CAMERA_OCCLUDERS name=%s geometry=%d containing=%d segment=%d" % [
|
|
checkpoint_name, geometry_nodes.size(), containing.size(), intersecting.size()])
|
|
|
|
var report := {
|
|
"schema_version": 1,
|
|
"created_utc": Time.get_datetime_string_from_system(true, true),
|
|
"wait_seconds": wait_seconds,
|
|
"coverage": "scene_tree_only; RenderingServer RID-only instances are excluded",
|
|
"results": results,
|
|
}
|
|
if not _write_json(output_path, report):
|
|
quit(1)
|
|
return
|
|
world.queue_free()
|
|
quit(0 if not results.is_empty() else 1)
|
|
|
|
|
|
func _collect_geometry_nodes(node: Node, output: Array[Node3D]) -> void:
|
|
if node is MeshInstance3D:
|
|
var mesh_instance := node as MeshInstance3D
|
|
if mesh_instance.mesh != null and not mesh_instance.name.begins_with("TileLOD"):
|
|
output.append(mesh_instance)
|
|
elif node is MultiMeshInstance3D:
|
|
var multimesh_instance := node as MultiMeshInstance3D
|
|
if multimesh_instance.multimesh != null:
|
|
output.append(multimesh_instance)
|
|
for child in node.get_children():
|
|
_collect_geometry_nodes(child, output)
|
|
|
|
|
|
func _geometry_world_aabb(node: Node3D) -> AABB:
|
|
if node is MeshInstance3D:
|
|
return node.global_transform * (node as MeshInstance3D).mesh.get_aabb()
|
|
if node is MultiMeshInstance3D:
|
|
return node.global_transform * (node as MultiMeshInstance3D).multimesh.get_aabb()
|
|
return AABB()
|
|
|
|
|
|
func _geometry_record(node: Node3D, world_aabb: AABB, camera_position: Vector3) -> Dictionary:
|
|
var node_path := String(node.get_path())
|
|
var category := "geometry"
|
|
if node_path.contains("/M2s/") or node is MultiMeshInstance3D:
|
|
category = "m2"
|
|
elif node_path.contains("/WMOs/"):
|
|
category = "wmo"
|
|
return {
|
|
"category": category,
|
|
"node_path": node_path,
|
|
"distance_to_center": camera_position.distance_to(world_aabb.get_center()),
|
|
"aabb_position": _vector3_array(world_aabb.position),
|
|
"aabb_size": _vector3_array(world_aabb.size),
|
|
}
|
|
|
|
|
|
func _vector3(value_variant) -> Vector3:
|
|
if not (value_variant is Array) or value_variant.size() != 3:
|
|
return Vector3.ZERO
|
|
return Vector3(float(value_variant[0]), float(value_variant[1]), float(value_variant[2]))
|
|
|
|
|
|
func _vector3_array(value: Vector3) -> Array[float]:
|
|
return [value.x, value.y, value.z]
|
|
|
|
|
|
func _argument(arguments: PackedStringArray, name: String, default_value: String) -> String:
|
|
var index := arguments.find(name)
|
|
if index >= 0 and index + 1 < arguments.size():
|
|
return arguments[index + 1]
|
|
return default_value
|
|
|
|
|
|
func _load_json(path: String) -> Dictionary:
|
|
var file := FileAccess.open(path, FileAccess.READ)
|
|
if file == null:
|
|
return {}
|
|
var parsed = JSON.parse_string(file.get_as_text())
|
|
return parsed if parsed is Dictionary else {}
|
|
|
|
|
|
func _write_json(path: String, value: Dictionary) -> bool:
|
|
var absolute_path := ProjectSettings.globalize_path(path)
|
|
if DirAccess.make_dir_recursive_absolute(absolute_path.get_base_dir()) != OK:
|
|
return false
|
|
var file := FileAccess.open(absolute_path, FileAccess.WRITE)
|
|
if file == null:
|
|
return false
|
|
file.store_string(JSON.stringify(value, " "))
|
|
return true
|