Compare commits

..

8 Commits

Author SHA1 Message Date
sindoring aea7787b9b test(M00): diagnose checkpoint camera occluders
Work-Package: M00-QAR-CAMERA-OCCLUDERS-001
Agent: sindo-main-codex
Tests: five-point camera occluder probe, coordination and documentation gates
Fidelity: no camera containment found; manual target and FOV mismatch recorded
2026-07-11 22:23:33 +04:00
sindoring 230620089f Merge pull request 'test(M00): diagnose waterfall tile ownership' (#4) from work/sindo-main-codex/m00-waterfall-tile-ownership into master
Reviewed-on: #4
2026-07-11 21:11:21 +03:00
sindoring 42fdf40282 test(M00): diagnose waterfall tile ownership
Work-Package: M00-QAR-TILE-OWNERSHIP-001
Agent: sindo-main-codex
Tests: strict five-point terrain probe, coordination and documentation gates
Fidelity: tile 30_49 ownership is healthy; exact ray miss classified as triangle seam
2026-07-11 22:06:22 +04:00
sindoring f8538ba2cf Merge pull request 'test(M00): probe renderer terrain clearance' (#3) from work/sindo-main-codex/m00-terrain-height into master
Reviewed-on: #3
2026-07-11 20:48:57 +03:00
sindoring d233a41ce8 test(M00): probe renderer terrain clearance
Work-Package: M00-QAR-TERRAIN-HEIGHT-001
Agent: sindo-main-codex
Tests: five-point terrain probe, coordination and documentation gates
Fidelity: four cameras confirmed above terrain; waterfall tile missing terrain mesh recorded
2026-07-11 19:38:23 +04:00
sindoring dfc10312f8 Merge pull request 'test(M00): add renderer coordinate calibration' (#2) from work/sindo-main-codex/m00-coordinate-calibration into master
Reviewed-on: #2
2026-07-11 18:12:57 +03:00
sindoring 8e8ea32ba3 test(M00): add renderer coordinate calibration
Work-Package: M00-QAR-COORD-CALIBRATION-001
Agent: sindo-main-codex
Tests: coordinate probe, M00 dry-run, coordination and documentation gates
Fidelity: five build 12340 camera points round-trip within 0.000015 units
2026-07-11 19:09:46 +04:00
sindoring 19df5b7968 Merge pull request 'test(M00): add checkpoint perceptual comparison' (#1) from work/sindo-main-codex/m00-checkpoint-diff into master
Reviewed-on: #1
2026-07-11 17:57:04 +03:00
11 changed files with 887 additions and 1 deletions
@@ -0,0 +1,78 @@
# M00-QAR-CAMERA-OCCLUDERS-001 — Camera occluder diagnostic
<!-- OPENWC_CLAIM:M00-QAR-CAMERA-OCCLUDERS-001:sindo-main-codex:2026-07-13 -->
## Ownership
- Target: M00
- Program: QAR
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m00-camera-occluders`
- Lease expires UTC: 2026-07-13
- Integrator: milestone integrator
## Outcome
Report scene-tree M2/WMO geometry that contains calibrated cameras or intersects each camera-to-target segment.
## Non-goals
- Changing placements, cameras, FOV, culling, or renderer behavior.
- Claiming full coverage of RenderingServer RID-only instances.
- Implementing a production visibility or collision service.
## Paths
- Exclusive: `src/tools/probe_render_camera_occluders.gd`
- Shared/hotspots: renderer baseline documentation
- Generated/ignored: local JSON probe reports
## Contracts and data
- Public API/events: headless diagnostic CLI only
- Schema/format version: report schema 1
- Migration/compatibility: none
- Consumers: M00 fidelity workflow
## Dependencies
- Requires: calibrated five-point manifest and streaming scene
- Blocks: placement versus camera-composition classification
- External state: local extracted/cache data
## Verification
- Commands: camera occluder probe, coordination and documentation gates
- Fixtures: five calibrated camera/target segments
- Fidelity evidence: scene-tree bounds at paired build 12340 viewpoints
- Performance budget: offline diagnostic
## Documentation deliverables
- Inline public API docs: probe header and output fields
- Module specification: verification/source map
- Data-flow diagram: occluder probe flow
- Sequence/state/dependency diagrams: synchronous diagnostic; not applicable
- Source map/status updates: baseline findings
## Simplicity and naming
- Important names introduced: `camera_containing_geometry`, `segment_intersecting_geometry`
- Simplest considered solution: transformed published AABBs
- Rejected complexity/abstractions: GPU visibility readback or new BVH
- Unavoidable complexity and justification: RID-only geometry cannot be named by this probe
- Measured optimization evidence: not applicable
## Status
- State: ready
- Done: five-point scene-tree AABB probe, containment/intersection classification and documentation
- Next: integrator review; calibrate reproducible reference camera direction/FOV separately
- Blocked by:
## Handoff
- Commit: branch HEAD
- Results: zero containing geometry at all five cameras; expected WMO/liquid target intersections; ADT/dense segments unobstructed
- Remaining risks: RID-only instances are excluded; manual reference direction and FOV were not recorded exactly
- Documentation updated: `docs/RENDER_BASELINE.md`, `docs/modules/world-renderer.md`
@@ -0,0 +1,78 @@
# M00-QAR-COORD-CALIBRATION-001 — Renderer coordinate calibration
<!-- OPENWC_CLAIM:M00-QAR-COORD-CALIBRATION-001:sindo-main-codex:2026-07-13 -->
## Ownership
- Target: M00
- Program: QAR
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m00-coordinate-calibration`
- Lease expires UTC: 2026-07-13
- Integrator: milestone integrator
## Outcome
Record the five observed build 12340 camera positions as non-proprietary golden coordinates and determine whether the existing WoW/Godot position formula round-trips them.
## Non-goals
- Introducing the M01 production `CoordinateMapper` contract.
- Changing terrain, placement, WMO, liquid, or streaming behavior.
- Claiming camera composition parity from coordinate round-trip alone.
## Paths
- Exclusive: `src/tools/verify_render_coordinate_calibration.gd`
- Shared/hotspots: `src/tools/render_baseline_manifest.json`, renderer baseline documentation
- Generated/ignored: original-client screenshots and local reports
## Contracts and data
- Public API/events: none; headless diagnostic only
- Schema/format version: additive checkpoint calibration metadata, manifest schema remains 1
- Migration/compatibility: existing capture consumers ignore additive fields
- Consumers: M00 fidelity workflow and future M01 golden fixtures
## Dependencies
- Requires: five accepted original-client build 12340 viewpoints
- Blocks: diagnosis of paired camera/placement mismatch
- External state: screenshots remain outside Git
## Verification
- Commands: coordinate calibration probe, baseline manifest, coordination and documentation gates
- Fixtures: five `reference_wow_camera` values in renderer manifest
- Fidelity evidence: positions were observed in build 12340 during the paired session
- Performance budget: negligible headless arithmetic
## Documentation deliverables
- Inline public API docs: diagnostic script header
- Module specification: renderer verification/source map update
- Data-flow diagram: baseline calibration flow update
- Sequence/state/dependency diagrams: not applicable; stateless synchronous probe
- Source map/status updates: renderer module and baseline document
## Simplicity and naming
- Important names introduced: `reference_wow_camera`, `maximum_round_trip_error`
- Simplest considered solution: direct formula probe over manifest fixtures
- Rejected complexity/abstractions: production domain mapper before M01
- Unavoidable complexity and justification: none
- Measured optimization evidence: not applicable
## Status
- State: ready
- Done: five golden camera points, headless round-trip probe, runner integration and documentation
- Next: integrator review; diagnose terrain height/placement/composition separately
- Blocked by:
## Handoff
- Commit: branch HEAD
- Results: five points passed with maximum mapping/round-trip error 0.000015
- Remaining risks: correct position arithmetic does not prove terrain height, placement or camera direction parity
- Documentation updated: `docs/RENDER_BASELINE.md`, `docs/modules/world-renderer.md`
@@ -0,0 +1,78 @@
# M00-QAR-TERRAIN-HEIGHT-001 — Terrain height diagnostic
<!-- OPENWC_CLAIM:M00-QAR-TERRAIN-HEIGHT-001:sindo-main-codex:2026-07-13 -->
## Ownership
- Target: M00
- Program: QAR
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m00-terrain-height`
- Lease expires UTC: 2026-07-13
- Integrator: milestone integrator
## Outcome
Measure rendered terrain height and camera clearance at the five build 12340 golden checkpoints without changing renderer behavior.
## Non-goals
- Adding a runtime terrain-query API or collision system.
- Changing terrain geometry, coordinate mapping, placements, or cameras.
- Implementing the M01 CoordinateMapper.
## Paths
- Exclusive: `src/tools/probe_render_terrain_height.gd`
- Shared/hotspots: renderer baseline documentation and runner
- Generated/ignored: local JSON probe reports
## Contracts and data
- Public API/events: headless diagnostic CLI only
- Schema/format version: report schema 1
- Migration/compatibility: none
- Consumers: M00 fidelity diagnosis
## Dependencies
- Requires: calibrated renderer manifest and active terrain meshes
- Blocks: classification of under-terrain camera gaps
- External state: local extracted/cache data
## Verification
- Commands: terrain probe, M00 dry-run, coordination and documentation gates
- Fixtures: five calibrated manifest checkpoints
- Fidelity evidence: camera clearance against rendered OpenWC terrain
- Performance budget: offline diagnostic only
## Documentation deliverables
- Inline public API docs: script CLI header
- Module specification: verification/source map
- Data-flow diagram: terrain probe flow
- Sequence/state/dependency diagrams: synchronous diagnostic; not applicable
- Source map/status updates: baseline and renderer module
## Simplicity and naming
- Important names introduced: `terrain_height`, `camera_clearance`
- Simplest considered solution: CPU ray against already loaded mesh
- Rejected complexity/abstractions: new parser, physics collision, runtime query service
- Unavoidable complexity and justification: tile-local ray transform is required by mesh ownership
- Measured optimization evidence: not applicable
## Status
- State: ready
- Done: active-mesh terrain probe, four clearance measurements, isolated waterfall missing-mesh confirmation and documentation
- Next: integrator review; placement/composition diagnosis for four points and tile 30_49 ownership diagnosis remain separate packages
- Blocked by:
## Handoff
- Commit: branch HEAD
- Results: four cameras are 12.034..90.178 units above terrain; waterfall tile has no accessible terrain mesh after 10-second settle
- Remaining risks: mesh ray does not measure WMO/M2 occlusion; waterfall missing mesh requires streaming ownership investigation
- Documentation updated: `docs/RENDER_BASELINE.md`, `docs/modules/world-renderer.md`
@@ -0,0 +1,78 @@
# M00-QAR-TILE-OWNERSHIP-001 — Waterfall terrain tile ownership
<!-- OPENWC_CLAIM:M00-QAR-TILE-OWNERSHIP-001:sindo-main-codex:2026-07-13 -->
## Ownership
- Target: M00
- Program: QAR
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m00-waterfall-tile-ownership`
- Lease expires UTC: 2026-07-13
- Integrator: milestone integrator
## Outcome
Identify the runtime transition that prevents waterfall tile `30_49` from exposing a terrain mesh to the height probe.
## Non-goals
- Changing streaming budgets or terrain behavior.
- Rebuilding caches or modifying extracted assets.
- Adding a production terrain query service.
## Paths
- Exclusive: terrain probe runtime ownership diagnostics
- Shared/hotspots: renderer baseline documentation
- Generated/ignored: local probe reports and caches
## Contracts and data
- Public API/events: additive diagnostic JSON fields
- Schema/format version: terrain report remains schema 1
- Migration/compatibility: additive fields only
- Consumers: M00 fidelity diagnosis
## Dependencies
- Requires: merged terrain-height probe and local cache inventory
- Blocks: waterfall terrain ownership classification
- External state: local extracted/cache data
## Verification
- Commands: isolated waterfall probe, coordination and documentation gates
- Fixtures: checkpoint tile `30_49`
- Fidelity evidence: runtime state correlated with build 12340 waterfall viewpoint
- Performance budget: offline diagnostic
## Documentation deliverables
- Inline public API docs: diagnostic output fields
- Module specification: verification/source map if behavior changes
- Data-flow diagram: update ownership transition if needed
- Sequence/state/dependency diagrams: document observed tile transition
- Source map/status updates: baseline findings
## Simplicity and naming
- Important names introduced: `available`, `queued_index`, `loading`, `state_present`, `mesh_source`
- Simplest considered solution: inspect existing loader registries read-only
- Rejected complexity/abstractions: new tracing framework
- Unavoidable complexity and justification: none
- Measured optimization evidence: not applicable
## Status
- State: ready
- Done: raw/cache inventory, isolated runtime state, mesh AABB/local probe and nearby sampling diagnosis
- Next: integrator review; placement/composition remains the actual paired-camera gap
- Blocked by:
## Handoff
- Commit: branch HEAD
- Results: tile 30_49 ownership and meshes are healthy; exact ray misses a triangle seam/edge, while a 2-unit offset samples terrain at 113.872
- Remaining risks: nearby estimate is diagnostic and must not become a gameplay terrain-query contract
- Documentation updated: `docs/RENDER_BASELINE.md`, `docs/modules/world-renderer.md`
+59
View File
@@ -120,3 +120,62 @@ Baseline пока не имеет approved парных кадров ориги
Первый paired run после калибровки создал десять сравнений (пять reference × cold/warm), без missing pairs. Все пары ожидаемо превысили строгий tolerance: mean perceptual error `0.0707..0.1746`, changed-pixel ratio `0.5504..0.8187`. Human inspection показал, что это пока не чистая material/lighting ошибка: при тех же WoW-derived координатах OpenWC terrain-overview camera находится под terrain/placements, WMO camera — внутри таверны, liquid camera — внутри скал; ADT и dense-M2 композиции также существенно смещены. До исправления coordinate/placement mismatch эти значения являются gap evidence, а не основанием расширять tolerance.
Capture tool строит camera basis явно из target и world-up. Это исключает неоднозначный roll `look_at` при автоматической съёмке. `ViewportTexture.get_image()` сохраняется без дополнительного vertical flip для Godot 4.6.1.
## Coordinate calibration
Пять принятых build 12340 viewpoints записаны в manifest как `reference_wow_camera`. Они содержат только числовые world coordinates и не включают proprietary данные. Headless probe:
```powershell
godot --headless --path . --script res://src/tools/verify_render_coordinate_calibration.gd
```
```mermaid
flowchart LR
O[Observed build 12340 WoW XYZ] --> W[WoW to Godot formula]
W --> G[Manifest Godot camera XYZ]
G --> R[Godot to WoW round-trip]
R --> E[Maximum numeric error]
```
На пяти точках maximum mapping/round-trip error равен `0.000015`. Это исключает текущую формулу `gx = center - wy`, `gy = wz`, `gz = center - wx` как источник крупного paired mismatch. Результат не доказывает renderer parity: следующая диагностика должна отдельно проверить terrain height, placement transforms и фактическое camera direction/FOV. Production `CoordinateMapper` остаётся задачей M01; M00 probe не создаёт второй публичный coordinate contract.
## Terrain height diagnostic
Rendered terrain проверяется без нового runtime API: offline probe использует уже загруженный tile mesh, строит `TriangleMesh` и выполняет вертикальный ray в tile-local space.
```powershell
godot --headless --path . --script res://src/tools/probe_render_terrain_height.gd -- --wait 2
```
```mermaid
flowchart LR
C[Calibrated camera XZ] --> S[Streaming tile state]
S --> M[Active terrain mesh]
M --> T[Tile-local TriangleMesh ray]
T --> H[Terrain height and camera clearance]
```
Измеренный clearance: terrain overview `89.044`, ADT boundary `44.788`, dense M2 `90.178`, large WMO `12.034`, waterfall примерно `76.128` Godot units. Следовательно, все пять камер находятся над rendered terrain; visual obstruction принадлежит placements/WMO/composition, а не terrain height.
Waterfall XZ сначала давал `no_intersection`, хотя tile `30_49` был available, полностью загружен, имел `control_splat_cache` quality mesh и LOD0 mesh, а probe находился внутри mesh AABB. Ray со смещением `2.0` units пересёк тот же mesh на высоте `113.872`; точная XZ попала на triangle seam/edge numerical miss. Probe теперь сообщает `sampled_nearby`, distance и source tile вместо ложного streaming ownership gap. `--require-all` остаётся строгим режимом для действительно неснятых точек.
## Camera occluder diagnostic
Scene-tree placement composition проверяется transformed AABB без изменения renderer:
```powershell
godot --headless --path . --script res://src/tools/probe_render_camera_occluders.gd -- --wait 3
```
```mermaid
flowchart LR
C[Calibrated camera] --> A[Published Mesh/MultiMesh AABBs]
T[Manifest target] --> S[Camera-to-target segment]
A --> P[Camera containment test]
A --> I[Segment intersection test]
S --> I
P --> J[JSON occluder report]
I --> J
```
Ни одна из пяти камер не находится внутри опубликованной scene-tree geometry. Terrain-overview segment пересекает четыре Stormwind WMO groups, large-WMO segment — три Goldshire Inn groups, waterfall segment — liquid surface; ADT boundary и dense-M2 segments не пересекают placement AABB. Поэтому прежнее визуальное впечатление «камера внутри WMO/placements» не подтверждается. Основной paired gap сейчас — неточно воспроизведённые manual look direction/target/FOV reference-кадров: например, автоматический Goldshire target направляет луч через фасад внутрь WMO, тогда как reference был вручную скадрирован на весь фасад. Probe охватывает только scene-tree MeshInstance/MultiMesh; RID-only instances не имеют доступного semantic path и явно исключены из coverage.
+6 -1
View File
@@ -192,7 +192,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
## Verification
- Unit/contract tests: material mapping, unique-ID dedupe, placement probes, baseline manifest, synthetic perceptual checkpoint diff.
- Unit/contract tests: material mapping, unique-ID dedupe, placement probes, baseline manifest, five-point coordinate calibration, synthetic perceptual checkpoint diff.
- Integration/E2E: Eastern Kingdoms/Kalimdor streaming scenes and seven cold/warm checkpoints.
- Fidelity evidence: пять локальных build 12340 reference JPG откалибровали terrain/ADT/M2/WMO/liquid viewpoints; automated paired-image metrics exist, но synthetic animation/dusk и полный human approval ещё не закрыты.
- Performance budgets: M00 report records cold/warm p95 and max hitch; no final acceptance threshold yet.
@@ -223,6 +223,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
- Direct camera path remains until M01/M03.
- Original-client paired fidelity evidence incomplete.
- Первый paired run выявил coordinate/placement mismatch: несколько совпадающих server-derived camera positions оказываются под terrain или внутри WMO/rocks OpenWC.
- Terrain-height probe исключил under-terrain состояние для всех пяти точек; waterfall exact-XZ miss классифицирован как TriangleMesh seam/edge и подтверждён nearby sample в 2 units.
- Camera-occluder probe не нашёл camera containment в пяти точках; paired mismatch локализован прежде всего в manual look direction/target/FOV calibration, с явным ограничением по RID-only geometry.
- D3D12 descriptor and shutdown RID/resource issues remain.
- M2/WMO/material/particle/ribbon/portal parity incomplete.
- Public API is mostly exported configuration rather than stable contracts.
@@ -242,6 +244,9 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `tools/run_render_baseline.ps1` | Unified M00 baseline runner |
| `src/tools/compare_render_checkpoints.gd` | Offline JPG/PNG paired-image perceptual metrics and JSON pass/fail report |
| `src/tools/capture_render_checkpoints.gd` | Deterministic no-roll checkpoint camera, performance and visual capture |
| `src/tools/verify_render_coordinate_calibration.gd` | Build 12340 camera-coordinate golden point round-trip diagnostic |
| `src/tools/probe_render_terrain_height.gd` | Offline active-mesh terrain height and camera-clearance report |
| `src/tools/probe_render_camera_occluders.gd` | Scene-tree placement containment and camera-to-target AABB intersection report |
## Related decisions and references
+166
View File
@@ -0,0 +1,166 @@
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("camera_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_targets_at"):
world.call("_refresh_streaming_targets_at", camera_position, 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
+250
View File
@@ -0,0 +1,250 @@
extends SceneTree
## Measures camera clearance against the active rendered terrain mesh.
## Usage: godot --path . --script res://src/tools/probe_render_terrain_height.gd --
## [--wait 3.0] [--output user://render_terrain_height/report.json]
const MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
const TILE_SIZE := 533.33333
const RAY_HEIGHT := 5000.0
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_terrain_height/report.json")
var only_filter := _argument(arguments, "--only", "").to_lower()
var require_all := arguments.has("--require-all")
var manifest := _load_json(MANIFEST_PATH)
if manifest.is_empty():
quit(1)
return
var packed_scene: PackedScene = load(String(manifest.get("scene", "")))
if packed_scene == null:
push_error("TERRAIN_HEIGHT_PROBE: cannot load streaming scene")
quit(1)
return
var world := packed_scene.instantiate() as Node3D
if world == null:
push_error("TERRAIN_HEIGHT_PROBE: streaming scene root is not Node3D")
quit(1)
return
var camera := Camera3D.new()
camera.current = true
world.add_child(camera)
world.set("camera_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
if not only_filter.is_empty() and not String(checkpoint.get("name", "")).to_lower().contains(only_filter):
continue
var camera_position := _vector3(checkpoint.get("camera", []))
camera.global_position = camera_position
if world.has_method("_refresh_streaming_targets_at"):
world.call("_refresh_streaming_targets_at", camera_position, true)
await create_timer(maxf(0.1, wait_seconds)).timeout
var terrain_sample := _sample_terrain(world, camera_position)
terrain_sample["name"] = checkpoint.get("name", "checkpoint")
terrain_sample["camera_y"] = camera_position.y
if terrain_sample.has("terrain_height"):
terrain_sample["camera_clearance"] = camera_position.y - float(terrain_sample.terrain_height)
results.append(terrain_sample)
var report := {
"schema_version": 1,
"created_utc": Time.get_datetime_string_from_system(true, true),
"wait_seconds": wait_seconds,
"results": results,
}
if not _write_json(output_path, report):
quit(1)
return
var sampled_count := 0
for result in results:
if result.has("terrain_height"):
sampled_count += 1
print("TERRAIN_HEIGHT name=%s status=%s camera_y=%.3f terrain=%s clearance=%s" % [
result.get("name", "checkpoint"),
result.get("status", "unknown"),
float(result.get("camera_y", 0.0)),
str(result.get("terrain_height", "n/a")),
str(result.get("camera_clearance", "n/a")),
])
print("TERRAIN_HEIGHT_PROBE sampled=%d total=%d report=%s" % [sampled_count, results.size(), output_path])
world.queue_free()
var failed := sampled_count == 0 or (require_all and sampled_count != results.size())
quit(1 if failed else 0)
func _sample_terrain(world: Node3D, world_position: Vector3) -> Dictionary:
var tile_coordinate := Vector2i(
int(floor(world_position.x / TILE_SIZE)),
int(floor(world_position.z / TILE_SIZE)))
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 := Vector2i(
int(floor(sample_position.x / TILE_SIZE)),
int(floor(sample_position.z / TILE_SIZE)))
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 _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
return Vector3(float(value_variant[0]), float(value_variant[1]), float(value_variant[2]))
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
+5
View File
@@ -43,6 +43,7 @@
"name": "elwynn_terrain_overview",
"coverage": ["terrain"],
"camera": [16680.0, 180.0, 26220.0],
"reference_wow_camera": [-9153.334, 386.666, 180.0],
"target": [16800.0, 62.0, 26400.0],
"player": [16800.0, 58.0, 26400.0],
"time_hours": 13.0
@@ -51,6 +52,7 @@
"name": "elwynn_adt_boundary",
"coverage": ["adt_boundary"],
"camera": [17020.0, 105.0, 26590.0],
"reference_wow_camera": [-9523.334, 46.666, 105.0],
"target": [17066.666, 62.0, 26666.666],
"player": [17060.0, 58.0, 26660.0],
"time_hours": 13.0
@@ -59,6 +61,7 @@
"name": "goldshire_dense_m2",
"coverage": ["dense_m2"],
"camera": [16956.666, 150.0, 26466.666],
"reference_wow_camera": [-9400.0, 110.0, 150.0],
"target": [17015.0, 62.0, 26525.0],
"player": [17015.0, 58.0, 26525.0],
"time_hours": 13.0
@@ -67,6 +70,7 @@
"name": "goldshire_inn_large_wmo",
"coverage": ["large_wmo"],
"camera": [17013.666, 72.0, 26451.666],
"reference_wow_camera": [-9385.0, 53.0, 72.0],
"target": [17042.27, 66.0, 26530.91],
"player": [17042.27, 58.0, 26530.91],
"time_hours": 13.0
@@ -75,6 +79,7 @@
"name": "elwynn_waterfall_liquid",
"coverage": ["liquid"],
"camera": [16481.666, 190.0, 26366.666],
"reference_wow_camera": [-9300.0, 585.0, 190.0],
"target": [16518.84, 68.0, 26427.27],
"player": [16518.84, 55.0, 26427.27],
"time_hours": 13.0
@@ -0,0 +1,88 @@
extends SceneTree
## Verifies observed build 12340 camera coordinates against the current renderer
## WoW X/Y/Z to Godot X/Y/Z convention. This diagnostic does not define the
## future M01 production CoordinateMapper contract.
const MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
const WOW_WORLD_CENTER := 17066.666
const MAXIMUM_POSITION_ERROR := 0.002
func _initialize() -> void:
var manifest := _load_manifest()
if manifest.is_empty():
quit(1)
return
var failures: Array[String] = []
var calibrated_count := 0
var maximum_round_trip_error := 0.0
for checkpoint_variant in manifest.get("checkpoints", []):
if not (checkpoint_variant is Dictionary):
continue
var checkpoint: Dictionary = checkpoint_variant
var reference_wow_camera_variant = checkpoint.get("reference_wow_camera", null)
if not (reference_wow_camera_variant is Array):
continue
var reference_wow_camera: Array = reference_wow_camera_variant
var checkpoint_name := String(checkpoint.get("name", "checkpoint"))
if reference_wow_camera.size() != 3:
failures.append("%s reference_wow_camera must contain three coordinates" % checkpoint_name)
continue
var expected_godot_camera := _array_to_vector3(checkpoint.get("camera", []))
var reference_wow_position := _array_to_vector3(reference_wow_camera)
var mapped_godot_position := _wow_to_godot(reference_wow_position)
var round_trip_wow_position := _godot_to_wow(mapped_godot_position)
var mapping_error := mapped_godot_position.distance_to(expected_godot_camera)
var round_trip_error := round_trip_wow_position.distance_to(reference_wow_position)
maximum_round_trip_error = maxf(maximum_round_trip_error, maxf(mapping_error, round_trip_error))
if mapping_error > MAXIMUM_POSITION_ERROR:
failures.append("%s maps %.6f units away from manifest camera" % [checkpoint_name, mapping_error])
if round_trip_error > MAXIMUM_POSITION_ERROR:
failures.append("%s round-trip error is %.6f" % [checkpoint_name, round_trip_error])
calibrated_count += 1
if calibrated_count < 5:
failures.append("expected at least five build 12340 camera calibrations, found %d" % calibrated_count)
if not failures.is_empty():
for failure in failures:
push_error("RENDER_COORDINATE_CALIBRATION: %s" % failure)
quit(1)
return
print("RENDER_COORDINATE_CALIBRATION PASS points=%d maximum_error=%.6f" % [calibrated_count, maximum_round_trip_error])
quit(0)
func _wow_to_godot(wow_position: Vector3) -> Vector3:
return Vector3(
WOW_WORLD_CENTER - wow_position.y,
wow_position.z,
WOW_WORLD_CENTER - wow_position.x
)
func _godot_to_wow(godot_position: Vector3) -> Vector3:
return Vector3(
WOW_WORLD_CENTER - godot_position.z,
WOW_WORLD_CENTER - godot_position.x,
godot_position.y
)
func _array_to_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 _load_manifest() -> Dictionary:
var manifest_file := FileAccess.open(MANIFEST_PATH, FileAccess.READ)
if manifest_file == null:
push_error("Cannot open renderer baseline manifest: %s" % MANIFEST_PATH)
return {}
var parsed = JSON.parse_string(manifest_file.get_as_text())
if not (parsed is Dictionary):
push_error("Renderer baseline manifest is not a JSON object")
return {}
return parsed
+1
View File
@@ -46,6 +46,7 @@ try {
Invoke-GodotStep "render-materials" @("--headless", "--path", ".", "--script", "res://src/tools/verify_render_materials.gd")
Invoke-GodotStep "m2-unique-dedupe" @("--headless", "--path", ".", "--script", "res://src/tools/verify_m2_unique_dedupe.gd")
Invoke-GodotStep "baseline-manifest" @("--headless", "--path", ".", "--script", "res://src/tools/verify_render_baseline_manifest.gd")
Invoke-GodotStep "coordinate-calibration" @("--headless", "--path", ".", "--script", "res://src/tools/verify_render_coordinate_calibration.gd")
$captureArgs = @(
"--path", ".",