rnd(M03): add rendered ground query facade
This commit is contained in:
@@ -5,9 +5,7 @@ extends SceneTree
|
||||
## [--wait 3.0] [--output user://render_terrain_height/report.json]
|
||||
|
||||
const MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
|
||||
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
|
||||
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
|
||||
const RAY_HEIGHT := 5000.0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
@@ -61,7 +59,15 @@ func _run_async() -> void:
|
||||
camera.global_position = camera_position
|
||||
render_facade.call("refresh_streaming_focus", true)
|
||||
await create_timer(maxf(0.1, wait_seconds)).timeout
|
||||
var terrain_sample := _sample_terrain(world, camera_position)
|
||||
var typed_camera_position = GODOT_WORLD_POSITION_SCRIPT.new(
|
||||
camera_position.x,
|
||||
camera_position.y,
|
||||
camera_position.z
|
||||
)
|
||||
var terrain_sample: Dictionary = render_facade.call(
|
||||
"renderer_ground_query_snapshot",
|
||||
typed_camera_position
|
||||
)
|
||||
terrain_sample["name"] = checkpoint.get("name", "checkpoint")
|
||||
terrain_sample["camera_y"] = camera_position.y
|
||||
if terrain_sample.has("terrain_height"):
|
||||
@@ -94,137 +100,6 @@ func _run_async() -> void:
|
||||
quit(1 if failed else 0)
|
||||
|
||||
|
||||
func _sample_terrain(world: Node3D, world_position: Vector3) -> Dictionary:
|
||||
var tile_coordinate := _adt_tile_vector2i(world_position)
|
||||
var tile_states: Dictionary = world.get("_tile_states")
|
||||
var highest_terrain_height := -INF
|
||||
var intersected_tile_key := ""
|
||||
var ready_mesh_count := 0
|
||||
for tile_y in range(tile_coordinate.y - 1, tile_coordinate.y + 2):
|
||||
for tile_x in range(tile_coordinate.x - 1, tile_coordinate.x + 2):
|
||||
var tile_key := "%d_%d" % [tile_x, tile_y]
|
||||
if not tile_states.has(tile_key):
|
||||
continue
|
||||
var state: Dictionary = tile_states[tile_key]
|
||||
var terrain_height_variant = _intersect_terrain_state(state, world_position)
|
||||
if terrain_height_variant == null:
|
||||
continue
|
||||
ready_mesh_count += 1
|
||||
var terrain_height := float(terrain_height_variant)
|
||||
if terrain_height > highest_terrain_height:
|
||||
highest_terrain_height = terrain_height
|
||||
intersected_tile_key = tile_key
|
||||
if not is_finite(highest_terrain_height):
|
||||
var missing_result := {
|
||||
"status": "no_intersection" if ready_mesh_count > 0 else "mesh_not_ready",
|
||||
"tile": "%d_%d" % [tile_coordinate.x, tile_coordinate.y],
|
||||
}
|
||||
missing_result.merge(_tile_runtime_diagnostic(world, String(missing_result.tile), world_position), true)
|
||||
if bool(missing_result.get("quality_mesh_present", false)) or bool(missing_result.get("tile_lod_mesh_present", false)):
|
||||
missing_result["status"] = "no_intersection"
|
||||
missing_result.merge(_nearest_terrain_sample(world, world_position), true)
|
||||
if missing_result.get("nearest_sample_distance", null) != null:
|
||||
missing_result["status"] = "sampled_nearby"
|
||||
missing_result["terrain_height"] = float(missing_result.nearest_sample_height)
|
||||
return missing_result
|
||||
return {
|
||||
"status": "sampled",
|
||||
"tile": intersected_tile_key,
|
||||
"terrain_height": highest_terrain_height,
|
||||
}
|
||||
|
||||
|
||||
func _intersect_terrain_state(state: Dictionary, world_position: Vector3):
|
||||
var terrain_mesh: Mesh = state.get("quality_terrain_mesh", null)
|
||||
if terrain_mesh == null:
|
||||
terrain_mesh = state.get("tile_lod_mesh", null)
|
||||
if terrain_mesh == null:
|
||||
return null
|
||||
var triangle_mesh := terrain_mesh.generate_triangle_mesh()
|
||||
if triangle_mesh == null:
|
||||
return null
|
||||
var tile_root := state.get("root", null) as Node3D
|
||||
if tile_root == null:
|
||||
return null
|
||||
var inverse_transform := tile_root.global_transform.affine_inverse()
|
||||
var local_ray_origin := inverse_transform * Vector3(world_position.x, RAY_HEIGHT, world_position.z)
|
||||
var local_ray_direction := inverse_transform.basis * Vector3.DOWN
|
||||
var intersection: Dictionary = triangle_mesh.intersect_ray(local_ray_origin, local_ray_direction)
|
||||
if intersection.is_empty():
|
||||
return null
|
||||
var local_hit: Vector3 = intersection.get("position", Vector3.ZERO)
|
||||
var world_hit := tile_root.global_transform * local_hit
|
||||
return world_hit.y
|
||||
|
||||
|
||||
func _tile_runtime_diagnostic(world: Node3D, tile_key: String, world_position: Vector3) -> Dictionary:
|
||||
var available_tiles: Dictionary = world.get("_available_tiles")
|
||||
var loading_tasks: Dictionary = world.get("_tile_loading_tasks")
|
||||
var tile_states: Dictionary = world.get("_tile_states")
|
||||
var load_queue: Array = world.get("_tile_load_queue")
|
||||
var queued_index := -1
|
||||
for index in load_queue.size():
|
||||
var request: Dictionary = load_queue[index]
|
||||
if String(request.get("key", "")) == tile_key:
|
||||
queued_index = index
|
||||
break
|
||||
var diagnostic := {
|
||||
"available": available_tiles.has(tile_key),
|
||||
"queued_index": queued_index,
|
||||
"loading": loading_tasks.has(tile_key),
|
||||
"state_present": tile_states.has(tile_key),
|
||||
"load_queue_size": load_queue.size(),
|
||||
}
|
||||
if tile_states.has(tile_key):
|
||||
var state: Dictionary = tile_states[tile_key]
|
||||
var quality_mesh: Mesh = state.get("quality_terrain_mesh", null)
|
||||
var tile_lod_mesh: Mesh = state.get("tile_lod_mesh", null)
|
||||
diagnostic["quality_mesh_present"] = quality_mesh != null
|
||||
diagnostic["tile_lod_mesh_present"] = tile_lod_mesh != null
|
||||
diagnostic["quality_source"] = String(state.get("quality_terrain_source", ""))
|
||||
diagnostic["tile_lod"] = int(state.get("tile_lod", -1))
|
||||
var tile_root := state.get("root", null) as Node3D
|
||||
if tile_root != null:
|
||||
var local_position := tile_root.global_transform.affine_inverse() * world_position
|
||||
diagnostic["tile_root_position"] = _vector3_array(tile_root.global_position)
|
||||
diagnostic["probe_local_position"] = _vector3_array(local_position)
|
||||
var diagnostic_mesh := quality_mesh if quality_mesh != null else tile_lod_mesh
|
||||
if diagnostic_mesh != null:
|
||||
var mesh_aabb := diagnostic_mesh.get_aabb()
|
||||
diagnostic["mesh_aabb_position"] = _vector3_array(mesh_aabb.position)
|
||||
diagnostic["mesh_aabb_size"] = _vector3_array(mesh_aabb.size)
|
||||
return diagnostic
|
||||
|
||||
|
||||
func _nearest_terrain_sample(world: Node3D, world_position: Vector3) -> Dictionary:
|
||||
var tile_states: Dictionary = world.get("_tile_states")
|
||||
for radius in [2.0, 5.0, 10.0, 20.0, 40.0]:
|
||||
for offset in [Vector2(radius, 0.0), Vector2(-radius, 0.0), Vector2(0.0, radius), Vector2(0.0, -radius)]:
|
||||
var sample_position := world_position + Vector3(offset.x, 0.0, offset.y)
|
||||
var sample_tile := _adt_tile_vector2i(sample_position)
|
||||
var sample_key := "%d_%d" % [sample_tile.x, sample_tile.y]
|
||||
if not tile_states.has(sample_key):
|
||||
continue
|
||||
var height_variant = _intersect_terrain_state(tile_states[sample_key], sample_position)
|
||||
if height_variant != null:
|
||||
return {
|
||||
"nearest_sample_distance": radius,
|
||||
"nearest_sample_height": float(height_variant),
|
||||
"nearest_sample_tile": sample_key,
|
||||
}
|
||||
return {"nearest_sample_distance": null}
|
||||
|
||||
|
||||
func _adt_tile_vector2i(world_position: Vector3) -> Vector2i:
|
||||
var typed_world_position = GODOT_WORLD_POSITION_SCRIPT.new(world_position.x, world_position.y, world_position.z)
|
||||
var tile_coordinate = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(typed_world_position)
|
||||
return Vector2i(tile_coordinate.tile_x, tile_coordinate.tile_y)
|
||||
|
||||
|
||||
func _vector3_array(value: Vector3) -> Array[float]:
|
||||
return [value.x, value.y, value.z]
|
||||
|
||||
|
||||
func _vector3(value_variant) -> Vector3:
|
||||
if not (value_variant is Array) or value_variant.size() != 3:
|
||||
return Vector3.ZERO
|
||||
|
||||
@@ -23,7 +23,6 @@ const REQUIRED_MAPPER_CALLS := {
|
||||
"res://src/render/streaming/streaming_target_planner.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local"],
|
||||
"res://src/scenes/player/third_person_wow_controller.gd": ["adt_tile_local_to_godot"],
|
||||
"res://src/gameplay/terrain/adt_terrain_query.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local", "adt_tile_local_to_chunk"],
|
||||
"res://src/tools/probe_render_terrain_height.gd": ["godot_to_adt_tile"],
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +60,7 @@ func _initialize() -> void:
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("COORDINATE_CONVERSION_BOUNDARIES PASS files=%d native_boundary=1 oracle=1 consumers=6 classifier_cases=6" % source_paths.size())
|
||||
print("COORDINATE_CONVERSION_BOUNDARIES PASS files=%d native_boundary=1 oracle=1 consumers=5 classifier_cases=6" % source_paths.size())
|
||||
quit(0)
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ extends SceneTree
|
||||
const STREAMING_WORLD_LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
const GAMEPLAY_ROOT := "res://src/gameplay"
|
||||
const SOURCE_ROOTS: Array[String] = ["res://src", "res://addons"]
|
||||
const RENDERER_DIAGNOSTIC_PATHS: Array[String] = [
|
||||
"res://src/tools/probe_render_terrain_height.gd",
|
||||
]
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
@@ -20,6 +23,8 @@ func _initialize() -> void:
|
||||
consumer_paths_by_path[gameplay_path] = true
|
||||
for editor_source_path in editor_source_paths:
|
||||
consumer_paths_by_path[editor_source_path] = true
|
||||
for renderer_diagnostic_path in RENDERER_DIAGNOSTIC_PATHS:
|
||||
consumer_paths_by_path[renderer_diagnostic_path] = true
|
||||
|
||||
for consumer_path_variant in consumer_paths_by_path:
|
||||
var consumer_path := String(consumer_path_variant)
|
||||
@@ -33,10 +38,11 @@ func _initialize() -> void:
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("RENDERER_INTERNAL_ACCESS PASS private_symbols=%d gameplay=%d editor_sources=%d" % [
|
||||
print("RENDERER_INTERNAL_ACCESS PASS private_symbols=%d gameplay=%d editor_sources=%d renderer_tools=%d" % [
|
||||
private_renderer_symbols.size(),
|
||||
gameplay_paths.size(),
|
||||
editor_source_paths.size(),
|
||||
RENDERER_DIAGNOSTIC_PATHS.size(),
|
||||
])
|
||||
quit(0)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ extends SceneTree
|
||||
const WORLD_RENDER_FACADE_SCRIPT := preload("res://src/render/world_render_facade.gd")
|
||||
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
|
||||
const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd")
|
||||
const RENDERED_GROUND_SAMPLE_SCRIPT := preload("res://src/render/terrain/rendered_ground_sample.gd")
|
||||
|
||||
const RUNTIME_SCENE_PATHS: Array[String] = [
|
||||
"res://src/scenes/streaming/eastern_kingdoms_streaming.tscn",
|
||||
@@ -15,12 +16,19 @@ const TOOL_PATHS: Array[String] = [
|
||||
"res://src/tools/probe_render_camera_occluders.gd",
|
||||
"res://src/tools/probe_render_terrain_height.gd",
|
||||
]
|
||||
const STREAMING_WORLD_LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
class StreamingWorldLoaderDouble extends Node:
|
||||
var current_focus: StreamingFocus
|
||||
var refresh_count := 0
|
||||
var metrics := {"tiles": 2, "queues": {"tile": 1}}
|
||||
var ground_sample: RefCounted = RENDERED_GROUND_SAMPLE_SCRIPT.available(42.25)
|
||||
var ground_snapshot := {
|
||||
"status": "sampled",
|
||||
"terrain_height": 42.25,
|
||||
"diagnostic": {"state_present": true},
|
||||
}
|
||||
|
||||
func set_streaming_focus(streaming_focus: StreamingFocus) -> void:
|
||||
current_focus = streaming_focus
|
||||
@@ -32,9 +40,16 @@ class StreamingWorldLoaderDouble extends Node:
|
||||
func render_baseline_snapshot() -> Dictionary:
|
||||
return metrics
|
||||
|
||||
func sample_rendered_ground_height(_godot_world_position: GodotWorldPosition) -> RefCounted:
|
||||
return ground_sample
|
||||
|
||||
func render_ground_query_snapshot(_godot_world_position: GodotWorldPosition) -> Dictionary:
|
||||
return ground_snapshot
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_rendered_ground_sample_contract(failures)
|
||||
await _verify_delegation_and_snapshot_isolation(failures)
|
||||
_verify_repository_wiring(failures)
|
||||
|
||||
@@ -44,10 +59,26 @@ func _initialize() -> void:
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("WORLD_RENDER_FACADE PASS delegation=3 runtime_scenes=2 tools=3")
|
||||
print("WORLD_RENDER_FACADE PASS delegation=5 ground_contract=2 runtime_scenes=2 tools=3")
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_rendered_ground_sample_contract(failures: Array[String]) -> void:
|
||||
var non_finite_sample = RENDERED_GROUND_SAMPLE_SCRIPT.available(INF)
|
||||
_expect_true(not non_finite_sample.is_available, "non-finite rendered ground is unavailable", failures)
|
||||
_expect_true(
|
||||
non_finite_sample.failure_code == &"render_terrain_height_not_finite",
|
||||
"non-finite rendered ground failure code",
|
||||
failures
|
||||
)
|
||||
var default_unavailable_sample = RENDERED_GROUND_SAMPLE_SCRIPT.unavailable(&"")
|
||||
_expect_true(
|
||||
default_unavailable_sample.failure_code == &"render_terrain_unavailable",
|
||||
"empty rendered ground failure code normalizes",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_delegation_and_snapshot_isolation(failures: Array[String]) -> void:
|
||||
var test_root := Node.new()
|
||||
var loader := StreamingWorldLoaderDouble.new()
|
||||
@@ -82,6 +113,24 @@ func _verify_delegation_and_snapshot_isolation(failures: Array[String]) -> void:
|
||||
(snapshot["queues"] as Dictionary)["tile"] = 999
|
||||
_expect_true(int(loader.metrics.tiles) == 2, "top-level metrics are detached", failures)
|
||||
_expect_true(int(loader.metrics.queues.tile) == 1, "nested metrics are detached", failures)
|
||||
|
||||
var query_position = GODOT_WORLD_POSITION_SCRIPT.new(100.0, 200.0, 300.0)
|
||||
var ground_sample: RefCounted = facade.sample_ground_height(query_position)
|
||||
_expect_true(ground_sample.is_available, "rendered ground sample delegates", failures)
|
||||
_expect_near(ground_sample.height_units, 42.25, "rendered ground height", failures)
|
||||
loader.ground_sample = RENDERED_GROUND_SAMPLE_SCRIPT.unavailable(&"render_terrain_mesh_not_ready")
|
||||
var unavailable_ground_sample: RefCounted = facade.sample_ground_height(query_position)
|
||||
_expect_true(not unavailable_ground_sample.is_available, "unavailable ground sample delegates", failures)
|
||||
_expect_true(
|
||||
unavailable_ground_sample.failure_code == &"render_terrain_mesh_not_ready",
|
||||
"ground failure code delegates",
|
||||
failures
|
||||
)
|
||||
var ground_snapshot: Dictionary = facade.renderer_ground_query_snapshot(query_position)
|
||||
ground_snapshot["terrain_height"] = 999.0
|
||||
(ground_snapshot["diagnostic"] as Dictionary)["state_present"] = false
|
||||
_expect_near(float(loader.ground_snapshot.terrain_height), 42.25, "ground snapshot top-level detached", failures)
|
||||
_expect_true(bool(loader.ground_snapshot.diagnostic.state_present), "ground snapshot nested detached", failures)
|
||||
test_root.queue_free()
|
||||
|
||||
|
||||
@@ -98,6 +147,44 @@ func _verify_repository_wiring(failures: Array[String]) -> void:
|
||||
_expect_true(not tool_source.contains('world.call("refresh_streaming_focus"'), "%s avoids direct streamer refresh" % tool_path, failures)
|
||||
_expect_true(not tool_source.contains('world.set("streaming_focus_source_path"'), "%s avoids direct streamer focus config" % tool_path, failures)
|
||||
|
||||
var terrain_probe_source := _read_text("res://src/tools/probe_render_terrain_height.gd", failures)
|
||||
_expect_true(
|
||||
terrain_probe_source.contains('"renderer_ground_query_snapshot"'),
|
||||
"terrain probe delegates ground diagnostics",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
not terrain_probe_source.contains('world.get("_'),
|
||||
"terrain probe avoids private streamer state",
|
||||
failures
|
||||
)
|
||||
for forbidden_probe_text in [
|
||||
"_tile_states",
|
||||
"_tile_load_queue",
|
||||
"_tile_loading_tasks",
|
||||
"_available_tiles",
|
||||
"generate_triangle_mesh",
|
||||
]:
|
||||
_expect_true(
|
||||
not terrain_probe_source.contains(forbidden_probe_text),
|
||||
"terrain probe omits %s" % forbidden_probe_text,
|
||||
failures
|
||||
)
|
||||
|
||||
var loader_source := _read_text(STREAMING_WORLD_LOADER_PATH, failures)
|
||||
for required_loader_text in [
|
||||
"func sample_rendered_ground_height(godot_world_position: GodotWorldPosition)",
|
||||
"func render_ground_query_snapshot(godot_world_position: GodotWorldPosition)",
|
||||
"RENDER_GROUND_QUERY_RAY_HEIGHT_UNITS := 5000.0",
|
||||
"for radius_units in [2.0, 5.0, 10.0, 20.0, 40.0]",
|
||||
"generate_triangle_mesh()",
|
||||
]:
|
||||
_expect_true(
|
||||
loader_source.contains(required_loader_text),
|
||||
"streamer ground query retains %s" % required_loader_text,
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _read_text(path: String, failures: Array[String]) -> String:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
|
||||
Reference in New Issue
Block a user