Compare commits

..

20 Commits

Author SHA1 Message Date
sindoring 5a957d2ebb test(M00): refine Goldshire camera framing 2026-07-12 02:49:19 +04:00
sindoring 5092995d0c Merge pull request 'test(M00): require WMO-ready pose captures' (#10) from work/sindo-main-codex/m00-wmo-readiness into master
Reviewed-on: #10
2026-07-12 01:36:02 +03:00
sindoring 4908eb2e31 test(M00): require WMO-ready pose captures 2026-07-12 02:16:09 +04:00
sindoring eca76c5ed1 Merge pull request 'test(M00): record Goldshire pose evidence' (#9) from work/sindo-main-codex/m00-camera-pose-evidence into master
Reviewed-on: #9
2026-07-12 01:00:40 +03:00
sindoring 03891edeb2 test(M00): record Goldshire pose evidence 2026-07-12 01:59:42 +04:00
sindoring 657a1d888a Merge pull request 'test(M00): add camera pose sweep' (#8) from work/sindo-main-codex/m00-camera-pose-sweep into master
Reviewed-on: #8
2026-07-12 00:47:17 +03:00
sindoring b48195d08d test(M00): add camera pose sweep 2026-07-12 01:46:02 +04:00
sindoring 8d4641a43b Merge pull request 'fix(renderer): drain threaded loads on shutdown' (#7) from work/sindo-main-codex/m00-capture-shutdown into master
Reviewed-on: #7
2026-07-12 00:39:21 +03:00
sindoring ad9ba7af4e fix(renderer): drain threaded loads on shutdown 2026-07-12 01:28:26 +04:00
sindoring f54850718a Merge pull request 'test(M00): add empirical checkpoint FOV sweep' (#6) from work/sindo-main-codex/m00-fov-sweep into master
Reviewed-on: #6
2026-07-11 23:57:40 +03:00
sindoring 5103eed014 test(M00): add empirical checkpoint FOV sweep
Work-Package: M00-QAR-FOV-SWEEP-001
Agent: sindo-main-codex
Tests: comparator filter self-test, FOV capture dry-run, bounded real sweep, repository gates
Fidelity: projection sweep remains inconclusive without reproducible reference yaw and pitch
2026-07-12 00:51:58 +04:00
sindoring 9d34a47765 Merge pull request 'test(M00): diagnose checkpoint camera occluders' (#5) from work/sindo-main-codex/m00-camera-occluders into master
Reviewed-on: #5
2026-07-11 23:04:13 +03:00
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
22 changed files with 1638 additions and 15 deletions
@@ -0,0 +1,78 @@
# M00-QAR-CAMERA-FRAMING-REFINEMENT-001 — Goldshire pitch/FOV refinement
<!-- OPENWC_CLAIM:M00-QAR-CAMERA-FRAMING-REFINEMENT-001:sindo-main-codex:2026-07-14 -->
## Ownership
- Target: M00
- Program: QAR
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m00-camera-framing-refinement`
- Lease expires UTC: 2026-07-14
- Integrator: milestone integrator
## Outcome
Jointly refine Goldshire checkpoint pitch and FOV against a WMO-ready build 12340 reference.
## Non-goals
- Changing runtime player camera defaults.
- Changing manifest values without an interior optimum and human approval.
- Optimizing yaw beyond the ready-scene coarse result.
## Paths
- Exclusive: joint FOV option in camera-pose sweep and framing evidence
- Shared/hotspots: camera-pose operational documentation and renderer module findings
- Generated/ignored: candidate PNGs and ranking JSON under `user://`
## Contracts and data
- Public API/events: additive `-CameraFovValues`; scalar `-CameraFov` remains compatible
- Schema/format version: ranking schema remains version 1 with existing per-candidate FOV field
- Migration/compatibility: scalar sweeps retain existing candidate paths
- Consumers: M00 fidelity workflow
## Dependencies
- Requires: WMO-ready 8-second capture contract
- Blocks: Goldshire manifest framing decision
- External state: private build 12340 reference remains outside Git
## Verification
- Commands: joint-grid plan regression, real ready-scene sweep, comparator self-test, repository gates
- Fixtures: private Goldshire Inn reference
- Fidelity evidence: joint pitch/FOV ranking and human inspection
- Performance budget: offline diagnostic
## Documentation deliverables
- Inline public API docs: PowerShell parameter
- Module specification: framing finding
- Data-flow diagram: FOV joins pose grid
- Sequence/state/dependency diagrams: unchanged
- Source map/status updates: operational guide
## Simplicity and naming
- Important names introduced: `CameraFovValues`, `effectiveCameraFovValues`
- Simplest considered solution: one additional loop around the existing grid
- Rejected complexity/abstractions: generic optimizer
- Unavoidable complexity and justification: projection and pitch jointly affect framing
- Measured optimization evidence: bounded 1x5x3 grid
## Status
- State: ready-for-review
- Done: additive joint FOV grid, compatibility-preserving paths, plan regression, ready-scene 15-candidate ranking and human inspection
- Next: add approved landmark/region scoring before any manifest camera calibration
- Blocked by:
## Handoff
- Commit: this work-package commit
- Results: numeric best FOV 62/yaw 10/pitch -25 at mean 0.078843 and ratio 0.667721 was visually rejected as grass-dominated; FOV 38/yaw 10/pitch -10 better matched building scale at mean 0.084220
- Remaining risks: full-frame metric does not preserve landmark framing; no manifest values were changed
- Documentation updated: `docs/CAMERA_POSE_SWEEP.md`, `docs/modules/world-renderer.md`
@@ -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-CAMERA-POSE-EVIDENCE-001 — Goldshire camera pose evidence
<!-- OPENWC_CLAIM:M00-QAR-CAMERA-POSE-EVIDENCE-001:sindo-main-codex:2026-07-14 -->
## Ownership
- Target: M00
- Program: QAR
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m00-camera-pose-evidence`
- Lease expires UTC: 2026-07-14
- Integrator: milestone integrator
## Outcome
Produce coarse-to-fine yaw/pitch evidence for the build 12340 Goldshire Inn reference and classify whether framing dominates the paired-image gap.
## Non-goals
- Changing manifest camera defaults before human approval.
- Treating minimum perceptual error as proof of exact camera parity.
- Committing proprietary reference images or generated candidates.
## Paths
- Exclusive: camera-pose evidence record
- Shared/hotspots: renderer baseline and module fidelity findings
- Generated/ignored: all sweep PNGs and JSON under `user://`
## Contracts and data
- Public API/events: none
- Schema/format version: unchanged
- Migration/compatibility: none
- Consumers: M00 fidelity review
## Dependencies
- Requires: merged camera-pose sweep and local build 12340 reference
- Blocks: classification of the Goldshire WMO paired gap
- External state: `sources/OpenWCReferenceCheckpoints/goldshire_inn_large_wmo.jpg`
## Verification
- Commands: coarse and fine real sweeps, ranking inspection, repository gates
- Fixtures: private Goldshire Inn build 12340 screenshot
- Fidelity evidence: ranked mean perceptual error and changed-pixel ratio
- Performance budget: offline diagnostic only
## Documentation deliverables
- Inline public API docs: unchanged
- Module specification: fidelity finding and remaining risk
- Data-flow diagram: unchanged
- Sequence/state/dependency diagrams: unchanged
- Source map/status updates: baseline evidence
## Simplicity and naming
- Important names introduced: none
- Simplest considered solution: use the merged bounded grid runner
- Rejected complexity/abstractions: optimizer or renderer changes
- Unavoidable complexity and justification: real images must be rendered for each candidate
- Measured optimization evidence: single pass per candidate
## Status
- State: ready-for-review
- Done: private reference dimensions verified; coarse, extended and limit sweeps completed; representative candidates inspected; runner failure handling hardened
- Next: investigate Goldshire Inn spatial/placement composition separately; do not change manifest yaw/pitch from this metric
- Blocked by:
## Handoff
- Commit: this work-package commit
- Results: zero offset mean error 0.099632; coarse best (-10,-20) 0.077575; metric decreased monotonically to (0,-60) 0.063574/changed ratio 0.549556, while visual inspection showed only grass and no inn
- Remaining risks: full-frame color error cannot register a missing landmark; WMO placement/streaming at this checkpoint remains unresolved
- Documentation updated: `docs/CAMERA_POSE_SWEEP.md`, `docs/modules/world-renderer.md`
@@ -0,0 +1,78 @@
# M00-QAR-CAMERA-POSE-SWEEP-001 — Empirical camera pose sweep
<!-- OPENWC_CLAIM:M00-QAR-CAMERA-POSE-SWEEP-001:sindo-main-codex:2026-07-14 -->
## Ownership
- Target: M00
- Program: QAR
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m00-camera-pose-sweep`
- Lease expires UTC: 2026-07-14
- Integrator: milestone integrator
## Outcome
Recover reproducible checkpoint framing by sweeping bounded yaw/pitch offsets and ranking paired-image error.
## Non-goals
- Changing runtime player camera defaults.
- Claiming an exact original-client camera pose from perceptual score alone.
- Adding computer-vision registration or image warping.
## Paths
- Exclusive: camera-pose CLI and sweep orchestration
- Shared/hotspots: renderer baseline documentation
- Generated/ignored: local candidate PNGs and JSON reports
## Contracts and data
- Public API/events: additive camera yaw/pitch offset and single-pass capture options; new PowerShell sweep command
- Schema/format version: capture report schema remains version 1 with additive camera-offset fields
- Migration/compatibility: existing capture commands retain zero offsets and two passes
- Consumers: M00 paired-fidelity workflow
## Dependencies
- Requires: build 12340 reference image, capture tool, paired-image comparator
- Blocks: reproducible manual-reference framing
- External state: original screenshots remain outside Git
## Verification
- Commands: capture dry-run offset regression, comparator self-test, bounded local sweep when a display/reference is available, repository gates
- Fixtures: one named M00 checkpoint
- Fidelity evidence: ranked yaw/pitch candidates with explicit human-approval requirement
- Performance budget: offline diagnostic; single-pass mode avoids redundant warm capture
## Documentation deliverables
- Inline public API docs: capture and sweep usage
- Module specification: camera-pose data flow, verification and source map
- Data-flow diagram: updated pose-sweep flow
- Sequence/state/dependency diagrams: unchanged
- Source map/status updates: baseline workflow and findings
## Simplicity and naming
- Important names introduced: `camera_yaw_offset_degrees`, `camera_pitch_offset_degrees`, `single_pass`
- Simplest considered solution: bounded grid over existing capture/comparator tools
- Rejected complexity/abstractions: feature matching, optimizer framework, image transforms
- Unavoidable complexity and justification: original-client camera angles are not exposed by tested APIs
- Measured optimization evidence: single-pass mode halves captures per candidate
## Status
- State: ready-for-review
- Done: additive capture offsets, single-pass calibration mode, bounded grid runner, ranked JSON contract, grid-plan regression and documentation
- Next: run a coarse-to-fine real sweep with the private build 12340 reference directory, then obtain human framing approval
- Blocked by:
## Handoff
- Commit: this work-package commit
- Results: dry-run reported yaw 12.50/pitch -7.50; comparator self-test passed; a 3x2 plan produced six unique candidates; full seven-checkpoint M00 dry-run passed with default zero offsets
- Remaining risks: no private reference directory was available in this worktree, so real perceptual ranking and human approval remain external evidence
- Documentation updated: `docs/CAMERA_POSE_SWEEP.md`; renderer module verification, risk and source map
@@ -0,0 +1,78 @@
# M00-QAR-CAPTURE-SHUTDOWN-001 — Capture shutdown drain
<!-- OPENWC_CLAIM:M00-QAR-CAPTURE-SHUTDOWN-001:sindo-main-codex:2026-07-14 -->
## Ownership
- Target: M00
- Program: QAR
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m00-capture-shutdown`
- Lease expires UTC: 2026-07-14
- Integrator: milestone integrator
## Outcome
Eliminate the reproducible capture-tool ObjectDB leak by completing owned threaded resource requests and draining deferred scene deletion before process exit.
## Non-goals
- Refactoring renderer resource ownership.
- Hiding genuine RID or worker-task leaks.
- Changing capture images, metrics, or streaming behavior.
## Paths
- Exclusive: capture tool shutdown sequence
- Shared/hotspots: renderer baseline documentation
- Generated/ignored: verbose Godot logs
## Contracts and data
- Public API/events: none
- Schema/format version: unchanged
- Migration/compatibility: none
- Consumers: M00 baseline runner
## Dependencies
- Requires: reproducible verbose leak diagnostic
- Blocks: clean M00 capture shutdown evidence
- External state: none
## Verification
- Commands: identical verbose dry-run before/after, M00 dry-run, repository gates
- Fixtures: ADT-boundary filtered capture
- Fidelity evidence: no visual behavior change
- Performance budget: blocking resource completion is restricted to shutdown; two SceneTree drain frames only
## Documentation deliverables
- Inline public API docs: shutdown rationale comment
- Module specification: recovery/known-risk update
- Data-flow diagram: unchanged
- Sequence/state/dependency diagrams: shutdown sequence documented in baseline
- Source map/status updates: baseline evidence
## Simplicity and naming
- Important names introduced: none
- Simplest considered solution: await two SceneTree frames after `queue_free` and finish registered in-flight ResourceLoader requests during loader shutdown
- Rejected complexity/abstractions: resource registry or manual child traversal
- Unavoidable complexity and justification: deferred deletion requires frame drain
- Measured optimization evidence: not applicable
## Status
- State: ready-for-review
- Done: one leaked RefCounted reproduced; isolation attributed it to `StreamingWorldLoader`; active threaded tile request was confirmed as the retained object; all owned threaded resource registries now finish in-progress requests before clearing; two identical verbose ADT-boundary runs and the full M00 dry-run completed without ObjectDB leak diagnostics
- Next: integrator review and merge
- Blocked by:
## Handoff
- Commit: this work-package commit
- Results: verbose filtered capture returned one report and zero `Leaked instance`/`ObjectDB instances leaked` lines on two consecutive runs; `run_render_baseline.ps1 -DryRun` passed all gates and seven checkpoints
- Remaining risks:
- Documentation updated: `docs/modules/world-renderer.md` shutdown sequence, ownership contract, recovery table and known-risk status
@@ -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-FOV-SWEEP-001 — Empirical camera FOV sweep
<!-- OPENWC_CLAIM:M00-QAR-FOV-SWEEP-001:sindo-main-codex:2026-07-14 -->
## Ownership
- Target: M00
- Program: QAR
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m00-fov-sweep`
- Lease expires UTC: 2026-07-14
- Integrator: milestone integrator
## Outcome
Support a bounded empirical FOV sweep when build 12340 does not expose a readable camera FOV CVar.
## Non-goals
- Claiming exact WoW FOV from a single manually aimed screenshot.
- Changing runtime player camera defaults.
- Automating yaw/pitch registration or image warping.
## Paths
- Exclusive: FOV/filter CLI additions in checkpoint tools
- Shared/hotspots: renderer baseline documentation
- Generated/ignored: local sweep PNGs and JSON reports
## Contracts and data
- Public API/events: additive `--camera-fov` and comparator `--only` CLI options
- Schema/format version: unchanged
- Migration/compatibility: existing commands retain defaults
- Consumers: M00 fidelity workflow
## Dependencies
- Requires: build 12340 reference JPG and paired-image comparator
- Blocks: empirical projection ranking
- External state: original screenshots remain outside Git
## Verification
- Commands: synthetic comparator test, capture dry-run with override, bounded real sweep
- Fixtures: `elwynn_adt_boundary` reference
- Fidelity evidence: ranked perceptual metrics with manual-direction limitation
- Performance budget: offline diagnostic
## Documentation deliverables
- Inline public API docs: CLI headers
- Module specification: verification/source map if needed
- Data-flow diagram: FOV sweep flow
- Sequence/state/dependency diagrams: not applicable
- Source map/status updates: baseline findings
## Simplicity and naming
- Important names introduced: `camera_fov_override`, `only_filter`
- Simplest considered solution: reuse capture and comparator
- Rejected complexity/abstractions: computer vision registration framework
- Unavoidable complexity and justification: multiple captures are required because client FOV is inaccessible
- Measured optimization evidence: not applicable
## Status
- State: ready
- Done: client FOV access audit, capture override, comparator filter/regression, dedicated-camera ownership fix and bounded sweep
- Next: integrator review; obtain reproducible yaw/pitch/zoom metadata before changing normative FOV
- Blocked by:
## Handoff
- Commit: branch HEAD
- Results: corrected ranking 26=0.079588, 38=0.079633, 50=0.084353, 62=0.088360, 86=0.097993; plateau is inconclusive
- Remaining risks: build 12340 FOV is inaccessible through tested APIs; manual direction/framing dominates metrics
- 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`
@@ -0,0 +1,78 @@
# M00-QAR-WMO-READINESS-001 — Goldshire WMO readiness
<!-- OPENWC_CLAIM:M00-QAR-WMO-READINESS-001:sindo-main-codex:2026-07-14 -->
## Ownership
- Target: M00
- Program: QAR
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m00-wmo-readiness`
- Lease expires UTC: 2026-07-14
- Integrator: milestone integrator
## Outcome
Prevent pose calibration against an under-loaded WMO scene and replace invalid Goldshire evidence with a baseline-ready comparison.
## Non-goals
- Changing runtime streaming budgets.
- Claiming all WMO queues must globally drain before a checkpoint.
- Changing manifest camera pose without valid paired evidence.
## Paths
- Exclusive: pose-sweep readiness default and Goldshire timing evidence
- Shared/hotspots: renderer baseline/module fidelity findings
- Generated/ignored: timing and sweep images under `user://`
## Contracts and data
- Public API/events: pose sweep default wait becomes 8 seconds, matching the M00 manifest
- Schema/format version: unchanged
- Migration/compatibility: explicit `-WaitSeconds` remains supported
- Consumers: M00 paired-fidelity workflow
## Dependencies
- Requires: Goldshire pose evidence and private build 12340 reference
- Blocks: valid Goldshire camera-pose ranking
- External state: private reference and generated captures remain outside Git
## Verification
- Commands: 2/8/15 second timing audit, ready-scene 3x3 pose sweep, baseline dry-run, repository gates
- Fixtures: private Goldshire Inn screenshot
- Fidelity evidence: visual WMO presence plus streaming snapshot and paired metrics
- Performance budget: offline diagnostic
## Documentation deliverables
- Inline public API docs: PowerShell default
- Module specification: corrected readiness/fidelity finding
- Data-flow diagram: readiness precondition
- Sequence/state/dependency diagrams: unchanged
- Source map/status updates: camera-pose operational guide
## Simplicity and naming
- Important names introduced: none
- Simplest considered solution: reuse the manifest's measured 8-second stabilization default
- Rejected complexity/abstractions: global queue barrier that would include unrelated distant work
- Unavoidable complexity and justification: WMO publication is asynchronous
- Measured optimization evidence: 2/8/15 second snapshots
## Status
- State: ready-for-review
- Done: timing audit invalidated the 2-second evidence; sweep default now matches the 8-second manifest; ready-scene 3x3 grid and human inspection completed
- Next: jointly refine negative pitch and FOV around the ready-scene Goldshire candidate
- Blocked by:
## Handoff
- Commit: this work-package commit
- Results: WMO instances 9/113/328 at 2/8/15 seconds; inn absent at 2 and visible at 8/15; ready grid improved mean error from 0.101402 at (0,0) to 0.087952 at (10,-10)
- Remaining risks: global WMO queues still contain unrelated work; best pitch is at the tested boundary and building scale indicates unresolved FOV/framing
- Documentation updated: `docs/CAMERA_POSE_SWEEP.md`, `docs/modules/world-renderer.md`
+57
View File
@@ -0,0 +1,57 @@
# Renderer Camera Pose Sweep
This offline M00 diagnostic recovers reproducible checkpoint framing when an original-client screenshot has no recorded camera yaw or pitch. It ranks a bounded grid; it does not change renderer or player-camera defaults and does not prove the exact build 12340 camera pose without human approval.
## Camera contract
`capture_render_checkpoints.gd` accepts additive `--camera-yaw-offset` and `--camera-pitch-offset` values in degrees. Yaw rotates the no-roll checkpoint basis around Godot world `Vector3.UP`; pitch then rotates around the camera-local right axis. Zero offsets preserve the manifest target exactly. `--single-pass` captures only `cold_process` and is intended for pose calibration, not performance baselines.
## Workflow
```powershell
.\tools\sweep_render_checkpoint_camera_pose.ps1 `
-ReferenceDirectory 'D:\private-fixtures\wow-3.3.5a-checkpoints' `
-Checkpoint elwynn_adt_boundary `
-YawOffsets -15,-10,-5,0,5,10,15 `
-PitchOffsets -10,-5,0,5,10 `
-CameraFovValues 38,50,62 `
-ViewportWidth 1280 -ViewportHeight 960 `
-WaitSeconds 8
```
Each candidate receives its own output directory and comparison report. `-CameraFovValues` adds FOV as another bounded grid dimension; omit it to retain the scalar `-CameraFov` workflow and legacy candidate paths. The viewport must exactly match the reference image dimensions; the runner fails on `size_mismatch` instead of inventing a score. The default 8-second wait matches the M00 manifest and is a readiness precondition: lowering it requires evidence that the checkpoint's WMO/M2 content has already appeared. `ranking.json` sorts candidates by mean perceptual error and then changed-pixel ratio. Use `-PlanOnly` to validate the Cartesian grid and output paths without rendering. Run a coarse grid first, then a finer grid around the best candidate.
```mermaid
flowchart LR
Y[Yaw offsets in degrees] --> G[Bounded Cartesian grid]
P[Pitch offsets in degrees] --> G
G --> C[Single-pass checkpoint captures]
R[Original-client reference] --> D[Perceptual comparator]
C --> D
D --> J[ranking.json]
J --> H[Human framing approval]
```
## Interpretation and recovery
The smallest error is a candidate, not automatic fidelity approval. Geometry, materials, lighting and FOV can move the perceptual optimum away from the true original-client pose. Inspect the best images manually and retain the original reference outside Git. A missing matching reference or candidate is a hard error; correct the checkpoint filter or filenames and rerun. Generated PNG and JSON outputs are disposable local evidence.
## Verification
The headless capture dry-run must report requested yaw and pitch offsets. Comparator `--self-test` covers paired metrics. Sweep `-PlanOnly` covers deterministic grid expansion and output naming without requiring a display. A real ranked sweep requires a display and a private build 12340 reference image.
## Goldshire Inn evidence — 2026-07-12
The private `2560x1440` build 12340 `goldshire_inn_large_wmo` reference was evaluated at FOV 62 degrees. A coarse 5x5 grid over yaw/pitch `[-20, -10, 0, 10, 20]` reduced mean error from `0.099632` at `(0, 0)` to `0.077575` at `(-10, -20)`. Extending pitch produced `0.070901` at `(0, -40)` and `0.063574` at `(0, -60)` with changed-pixel ratio `0.549556`.
These values are invalid camera-pose evidence because the sweep used a 2-second wait. A subsequent readiness audit showed 9 WMO instances and no inn after 2 seconds, 113 instances with the inn visible after 8 seconds, and 328 instances after 15 seconds. The apparent monotonic improvement rewarded grass while the landmark was still absent; it does not demonstrate a spatial/placement defect. The manifest pose remains unchanged pending a ready-scene rerun.
The ready-scene 3x3 rerun at FOV 62 degrees ranked `(yaw=10, pitch=-10)` first with mean error `0.087952` and changed-pixel ratio `0.665527`, compared with `0.101402`/`0.702489` at zero offsets. Human inspection confirms that the inn is present and the direction is plausible. Pitch remains on the tested boundary and the building scale differs from the reference, so these offsets are coarse evidence only; a joint pitch/FOV refinement is required before changing manifest camera values.
## Goldshire joint pitch/FOV refinement — 2026-07-12
A WMO-ready joint grid fixed yaw at 10 degrees and evaluated pitch `[-25, -20, -15, -10, -5]` across FOV `[38, 50, 62]`. Full-frame error ranked FOV 62/pitch -25 first at `0.078843` with changed-pixel ratio `0.667721`. Human inspection rejects it as a manifest calibration: the view is dominated by grass and crops the inn. FOV 38/pitch -10 is closer to the reference building scale but ranks only `0.084220`, and horizontal landmark alignment remains different.
The grid therefore has no approved joint optimum. Full-frame color error continues to trade landmark framing against large terrain regions. The manifest remains unchanged. The next registration method must score an approved inn/road/tree landmark region or explicit landmark coordinates rather than the entire image.
During this run the sweep orchestration was hardened in two ways: expected comparator exit code `1` is now collected through an explicit child process, and viewport dimensions are explicit. A reference/candidate `size_mismatch` now fails before ranking instead of producing an empty metric.
+74
View File
@@ -120,3 +120,77 @@ 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.
## Empirical FOV sweep
В build 12340 `GetCVar("cameraFoV")` возвращает `nil`, а `/console cameraFoV` и `ConsoleExec("cameraFoV")` не выдают значения. Поэтому capture tool поддерживает additive `--camera-fov`, а comparator — `--only`, позволяющий ограничить sweep одним reference checkpoint.
```mermaid
flowchart LR
F[Candidate vertical FOV] --> C[Dedicated checkpoint camera]
C --> P[Filtered checkpoint PNGs]
R[One original-client reference] --> D[Perceptual comparator --only]
P --> D
D --> M[Ranked metrics]
```
Первый sweep обнаружил capture defect: разные FOV иногда создавали одинаковые hashes, потому что scene camera могла перехватить viewport после входа в tree. Capture теперь вызывает `camera.make_current()` после добавления world и перед каждым checkpoint. После исправления ADT-boundary ranking стал: `26° → 0.079588`, `38° → 0.079633`, `50° → 0.084353`, `62° → 0.088360`, `86° → 0.097993` mean error. Plateau `2638°` и несовпавший changed-pixel optimum показывают доминирование manual look direction/framing; эти данные не обосновывают изменение нормативного manifest FOV `62°`. Для настоящей калибровки reference capture должен сохранять воспроизводимые yaw/pitch/zoom или независимый projection fixture.
+19 -4
View File
@@ -141,6 +141,9 @@ sequenceDiagram
Stream->>Budget: enqueue finalize operations
Budget->>Render: attach bounded terrain/M2/WMO/liquid work
Stream->>Render: evict outside retention range
Stream->>Worker: shutdown: wait for WorkerThreadPool tasks
Stream->>Stream: shutdown: finish registered ResourceLoader requests
Stream->>Render: clear queues, nodes, caches and RIDs
```
## Ownership, threading and resources
@@ -149,7 +152,7 @@ sequenceDiagram
- Worker tasks не должны менять SceneTree и shared Resource concurrently.
- Parsed/grouped results передаются обратно через guarded result queues.
- Mesh/material/node/RID finalization выполняется main thread и ограничивается exported budgets.
- Shutdown/map switch обязан отменить/дождаться jobs и освободить RIDs; M00 всё ещё фиксирует leaked-resource risk.
- Shutdown/map switch обязан дождаться WorkerThreadPool jobs и зарегистрированных threaded ResourceLoader requests до очистки очередей, nodes, caches и RIDs.
- Cache resources считаются immutable после публикации.
## Errors, cancellation and recovery
@@ -162,7 +165,7 @@ sequenceDiagram
| Main-thread hitch | Named section timing | Frame spike, work remains queued | `HITCH` log | Lower budget/fix finalize path |
| D3D12 descriptor exhaustion | Rendering backend error | Render failure/fallback backend | Godot error + baseline report | Dedup resources/fix settings |
| Teleport/map change | Focus/session transition | Old jobs become stale | Target/session generation | Cancel/drop stale results |
| Shutdown leak | Godot leak/RID diagnostics | Resource retained | Shutdown report | Ownership cleanup before DONE |
| Shutdown leak | Godot leak/RID diagnostics | Resource retained | Verbose shutdown report | Drain owned worker and resource requests before clearing registries |
## Configuration and capabilities
@@ -192,7 +195,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, camera-pose grid plan.
- 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,7 +226,14 @@ 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.
- D3D12 descriptor and shutdown RID/resource issues remain.
- 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.
- Empirical FOV sweep выявил, что checkpoint camera должна явно вызывать `make_current()`; после исправления projection ranking остаётся inconclusive из-за неизвестного manual yaw/pitch/framing reference.
- Camera-pose sweep can now rank bounded yaw/pitch grids without changing manifest defaults; perceptual ranking remains diagnostic and requires human framing approval.
- The first Goldshire pose grid was invalidated by WMO readiness: the inn is absent after 2 seconds but visible after the manifest-standard 8-second wait. Pose comparisons must stabilize asynchronous checkpoint content first.
- A ready-scene Goldshire 3x3 grid improved mean error from `0.101402` at zero offsets to `0.087952` at yaw `10`/pitch `-10`; pitch/FOV refinement remains required before manifest calibration.
- Joint Goldshire pitch/FOV refinement had no human-approved optimum: full-frame error preferred grass-heavy FOV 62/pitch -25, while FOV 38/pitch -10 better matched building scale. Landmark/region scoring is required before calibration.
- D3D12 descriptor issues remain; the capture-path anonymous `RefCounted` shutdown leak is regression-covered by a clean verbose dry-run, while other RID/resource diagnostics still require independent evidence.
- M2/WMO/material/particle/ribbon/portal parity incomplete.
- Public API is mostly exported configuration rather than stable contracts.
@@ -242,10 +252,15 @@ 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 |
| `tools/sweep_render_checkpoint_camera_pose.ps1` | Offline yaw/pitch capture grid and paired-error ranking |
| `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
- [`../../RENDER.md`](../../RENDER.md)
- [`../RENDER_BASELINE.md`](../RENDER_BASELINE.md)
- [`../CAMERA_POSE_SWEEP.md`](../CAMERA_POSE_SWEEP.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
- [`../GODOT_BEST_PRACTICES.md`](../GODOT_BEST_PRACTICES.md)
@@ -2073,7 +2073,7 @@ func _wait_for_tile_tasks() -> void:
var path: String = String(pending.get("path", ""))
if not path.is_empty():
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_tile_loading_tasks.clear()
@@ -2094,7 +2094,7 @@ func _wait_for_tile_tasks() -> void:
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_m2_mesh_load_requests.clear()
_m2_mesh_finalize_queue.clear()
@@ -2105,7 +2105,7 @@ func _wait_for_tile_tasks() -> void:
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_m2_animation_load_requests.clear()
_m2_animation_finalize_queue.clear()
@@ -2115,7 +2115,7 @@ func _wait_for_tile_tasks() -> void:
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_wmo_scene_load_requests.clear()
@@ -2124,7 +2124,7 @@ func _wait_for_tile_tasks() -> void:
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_wmo_render_load_requests.clear()
_wmo_render_missing_cache.clear()
@@ -2137,7 +2137,7 @@ func _wait_for_tile_tasks() -> void:
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_terrain_upgrade_tasks.clear()
@@ -2148,7 +2148,7 @@ func _wait_for_tile_tasks() -> void:
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_terrain_control_splat_cache_tasks.clear()
@@ -2162,7 +2162,7 @@ func _wait_for_tile_tasks() -> void:
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_terrain_splat_cache_tasks.clear()
_terrain_splat_result_mutex.lock()
+26 -2
View File
@@ -2,6 +2,7 @@ extends SceneTree
const DEFAULT_MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
const M2_NATIVE_ANIMATED_BUILDER := preload("res://addons/mpq_extractor/loaders/m2_native_animated_builder.gd")
const SHUTDOWN_DRAIN_FRAMES := 2
func _initialize() -> void:
@@ -22,6 +23,10 @@ func _capture_async() -> void:
var measure_seconds := float(_arg(args, "--measure", str(manifest.get("default_measure_seconds", 3.0))))
var revision := _arg(args, "--revision", "worktree")
var cache_state := _arg(args, "--cache-state", "existing")
var camera_fov_override := float(_arg(args, "--camera-fov", str(manifest.get("camera_fov", 62.0))))
var camera_yaw_offset_degrees := float(_arg(args, "--camera-yaw-offset", "0.0"))
var camera_pitch_offset_degrees := float(_arg(args, "--camera-pitch-offset", "0.0"))
var single_pass := args.has("--single-pass")
var headless := DisplayServer.get_name().to_lower() == "headless"
var dry_run := args.has("--dry-run") or headless
var viewport: Array = manifest.get("viewport", [1280, 900])
@@ -55,7 +60,7 @@ func _capture_async() -> void:
var camera := Camera3D.new()
camera.name = "CheckpointCamera"
camera.current = true
camera.fov = float(manifest.get("camera_fov", 62.0))
camera.fov = camera_fov_override
camera.far = 50000.0
camera.position = _vector3(first.get("camera", [0.0, 0.0, 0.0]))
(world as Node3D).add_child(camera)
@@ -65,6 +70,7 @@ func _capture_async() -> void:
get_root().add_child(world)
await process_frame
await process_frame
camera.make_current()
var player := world.get_node_or_null("ThirdPersonPlayer") as Node3D
if player != null:
@@ -82,6 +88,9 @@ func _capture_async() -> void:
"created_utc": Time.get_datetime_string_from_system(true, true),
"dry_run": dry_run,
"cache_state": cache_state,
"camera_fov": camera_fov_override,
"camera_yaw_offset_degrees": camera_yaw_offset_degrees,
"camera_pitch_offset_degrees": camera_pitch_offset_degrees,
"environment": _environment_metadata(),
"comparison_budgets": manifest.get("comparison_budgets", {}),
"cache_contract": manifest.get("cache_contract", {}),
@@ -90,6 +99,8 @@ func _capture_async() -> void:
}
var captured := 0
var passes := ["cold_process", "warm_revisit"]
if single_pass:
passes = ["cold_process"]
if dry_run:
passes = ["dry_run"]
@@ -110,6 +121,8 @@ func _capture_async() -> void:
camera.global_position = _vector3(checkpoint.get("camera", [0.0, 0.0, 0.0]))
_orient_camera_without_roll(camera, _vector3(checkpoint.get("target", [0.0, 0.0, 0.0])))
_apply_camera_pose_offsets(camera, camera_yaw_offset_degrees, camera_pitch_offset_degrees)
camera.make_current()
if player != null:
player.global_position = _vector3(checkpoint.get("player", checkpoint.get("target", [0.0, 0.0, 0.0])))
_set_sky_time(world, float(checkpoint.get("time_hours", 13.0)))
@@ -117,11 +130,13 @@ func _capture_async() -> void:
world.call("_refresh_streaming_targets_at", camera.global_position, true)
if dry_run:
print("RENDER_CHECKPOINT dry_run name=%s coverage=%s camera=%s target=%s time=%.2f" % [
print("RENDER_CHECKPOINT dry_run name=%s coverage=%s camera=%s target=%s yaw_offset=%.2f pitch_offset=%.2f time=%.2f" % [
checkpoint_name,
str(checkpoint.get("coverage", [])),
camera.global_position,
_vector3(checkpoint.get("target", [0.0, 0.0, 0.0])),
camera_yaw_offset_degrees,
camera_pitch_offset_degrees,
float(checkpoint.get("time_hours", 13.0)),
])
(report["results"] as Array).append(_result_record(checkpoint, pass_name, 0.0, {}, ""))
@@ -163,6 +178,10 @@ func _capture_async() -> void:
print("RENDER_BASELINE report=%s results=%d" % [report_path, captured])
world.queue_free()
# SceneTree deletion is deferred. Allow the world and its queued children
# to run their shutdown paths before engine teardown.
for unused_frame in SHUTDOWN_DRAIN_FRAMES:
await process_frame
if captured <= 0:
push_error("No checkpoints selected. --only filter was: %s" % only)
quit(1)
@@ -206,6 +225,11 @@ func _orient_camera_without_roll(camera: Camera3D, target_position: Vector3) ->
camera.global_basis = Basis(right, corrected_up, -forward)
func _apply_camera_pose_offsets(camera: Camera3D, yaw_offset_degrees: float, pitch_offset_degrees: float) -> void:
camera.global_rotate(Vector3.UP, deg_to_rad(yaw_offset_degrees))
camera.rotate_object_local(Vector3.RIGHT, deg_to_rad(pitch_offset_degrees))
func _result_record(checkpoint: Dictionary, pass_name: String, load_time_ms: float, metrics: Dictionary, sha256: String) -> Dictionary:
return {
"name": checkpoint.get("name", "checkpoint"),
+21 -1
View File
@@ -56,7 +56,7 @@ func _parse_arguments(raw_arguments: PackedStringArray) -> Dictionary:
parsed.self_test = true
index += 1
continue
if argument not in ["--reference", "--candidate", "--output", "--pixel-threshold", "--mean-threshold", "--changed-ratio-threshold"]:
if argument not in ["--reference", "--candidate", "--output", "--only", "--pixel-threshold", "--mean-threshold", "--changed-ratio-threshold"]:
return {"error": "unknown argument: %s" % argument}
if index + 1 >= raw_arguments.size():
return {"error": "missing value for %s" % argument}
@@ -76,10 +76,13 @@ func _compare_directories(reference_directory: String, candidate_directory: Stri
reference_directory = ProjectSettings.globalize_path(reference_directory)
candidate_directory = ProjectSettings.globalize_path(candidate_directory)
var reference_files := _reference_image_file_names(reference_directory)
var only_filter := String(options.get("only", "")).to_lower()
var results: Array[Dictionary] = []
var failed_count := 0
var missing_count := 0
for file_name in reference_files:
if not only_filter.is_empty() and not file_name.get_basename().to_lower().contains(only_filter):
continue
var reference_path := reference_directory.path_join(file_name)
var checkpoint_name := file_name.get_basename()
var candidate_file_names := _candidate_file_names(candidate_directory, checkpoint_name)
@@ -203,6 +206,7 @@ func _run_self_test() -> int:
var changed_directory := root.path_join("changed")
for directory_path in [reference_directory, identical_directory, changed_directory]:
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(directory_path))
_clear_directory_files(directory_path)
var reference_image := Image.create(2, 2, false, Image.FORMAT_RGBA8)
reference_image.fill(Color(0.25, 0.5, 0.75, 1.0))
var changed_image := reference_image.duplicate()
@@ -225,5 +229,21 @@ func _run_self_test() -> int:
if not identical_report.passed or changed_report.passed:
push_error("RENDER_CHECKPOINT_DIFF SELF_TEST: expected identical pass and changed failure")
return 1
if reference_image.save_jpg(reference_directory.path_join("ignored.jpg"), 1.0) != OK:
return 1
var filtered_options := options.duplicate()
filtered_options["only"] = "synthetic"
var filtered_report := _compare_directories(reference_directory, identical_directory, filtered_options)
if not filtered_report.passed or filtered_report.compared_count != 2:
push_error("RENDER_CHECKPOINT_DIFF SELF_TEST: --only must exclude unrelated references")
return 1
print("RENDER_CHECKPOINT_DIFF SELF_TEST PASS")
return 0
func _clear_directory_files(directory_path: String) -> void:
var directory := DirAccess.open(directory_path)
if directory == null:
return
for file_name in directory.get_files():
directory.remove(file_name)
+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", ".",
@@ -0,0 +1,143 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ReferenceDirectory,
[Parameter(Mandatory = $true)]
[string]$Checkpoint,
[string]$GodotPath,
[string]$Output = "user://render_camera_pose_sweep",
[double[]]$YawOffsets = @(-10.0, 0.0, 10.0),
[double[]]$PitchOffsets = @(-10.0, 0.0, 10.0),
[double]$CameraFov = 62.0,
[double[]]$CameraFovValues = @(),
[int]$ViewportWidth = 1280,
[int]$ViewportHeight = 900,
[double]$WaitSeconds = 8.0,
[double]$MeasureSeconds = 0.1,
[switch]$PlanOnly
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
if (-not $GodotPath) {
$godotCommand = Get-Command godot -ErrorAction SilentlyContinue
if ($godotCommand) {
$GodotPath = $godotCommand.Source
} else {
$GodotPath = Join-Path $env:TEMP "godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe"
}
}
if (-not (Test-Path -LiteralPath $GodotPath)) {
throw "Godot executable not found: $GodotPath"
}
if (-not (Test-Path -LiteralPath $ReferenceDirectory)) {
throw "Reference directory not found: $ReferenceDirectory"
}
$invariantCulture = [Globalization.CultureInfo]::InvariantCulture
$isJointFovSweep = $CameraFovValues.Count -gt 0
$effectiveCameraFovValues = if ($isJointFovSweep) { $CameraFovValues } else { @($CameraFov) }
$waitSecondsText = $WaitSeconds.ToString($invariantCulture)
$measureSecondsText = $MeasureSeconds.ToString($invariantCulture)
$absoluteOutput = if ($Output.StartsWith("user://")) {
Join-Path $env:APPDATA "Godot\app_userdata\OpenWC\$($Output.Substring(7))"
} else {
[IO.Path]::GetFullPath((Join-Path $repoRoot $Output))
}
$ranking = [Collections.Generic.List[object]]::new()
Push-Location $repoRoot
try {
foreach ($cameraFovValue in $effectiveCameraFovValues) {
foreach ($yawOffset in $YawOffsets) {
foreach ($pitchOffset in $PitchOffsets) {
$cameraFovText = $cameraFovValue.ToString("0.###", $invariantCulture)
$yawText = $yawOffset.ToString("0.###", $invariantCulture)
$pitchText = $pitchOffset.ToString("0.###", $invariantCulture)
$candidateName = "yaw_$($yawText.Replace('-', 'n').Replace('.', '_'))__pitch_$($pitchText.Replace('-', 'n').Replace('.', '_'))"
if ($isJointFovSweep) {
$candidateName = "fov_$($cameraFovText.Replace('.', '_'))__$candidateName"
}
$candidateOutput = "$Output/$candidateName"
$comparisonReport = Join-Path $absoluteOutput "$candidateName.json"
if ($PlanOnly) {
$ranking.Add([pscustomobject]@{
yaw_offset_degrees = $yawOffset
pitch_offset_degrees = $pitchOffset
camera_fov_degrees = $cameraFovValue
candidate_directory = $candidateOutput
})
continue
}
$captureArguments = @(
"--path", ".", "--script", "res://src/tools/capture_render_checkpoints.gd", "--",
"--output", $candidateOutput, "--only", $Checkpoint, "--single-pass",
"--camera-fov", $cameraFovText,
"--camera-yaw-offset", $yawText, "--camera-pitch-offset", $pitchText,
"--viewport-width", $ViewportWidth, "--viewport-height", $ViewportHeight,
"--wait", $waitSecondsText, "--measure", $measureSecondsText
)
$captureProcess = Start-Process -FilePath $GodotPath -ArgumentList $captureArguments -Wait -PassThru
if ($captureProcess.ExitCode -ne 0) {
throw "Capture failed for yaw=$yawText pitch=$pitchText"
}
$comparisonArguments = @(
"--headless", "--path", ".", "--script", "res://src/tools/compare_render_checkpoints.gd", "--",
"--reference", $ReferenceDirectory, "--candidate", $candidateOutput,
"--only", $Checkpoint, "--output", $comparisonReport
)
$comparisonProcess = Start-Process -FilePath $GodotPath -ArgumentList $comparisonArguments -Wait -PassThru -NoNewWindow
if ($comparisonProcess.ExitCode -notin @(0, 1)) {
throw "Comparison failed for yaw=$yawText pitch=$pitchText"
}
$comparison = Get-Content -Raw -LiteralPath $comparisonReport | ConvertFrom-Json
if ($comparison.compared_count -lt 1) {
throw "No paired image was compared for yaw=$yawText pitch=$pitchText"
}
$metricResults = @($comparison.results | Where-Object { $null -ne $_.mean_perceptual_error })
if ($metricResults.Count -ne $comparison.compared_count) {
$statuses = ($comparison.results.status | Sort-Object -Unique) -join ","
throw "Comparison produced no numeric metric for yaw=$yawText pitch=$pitchText statuses=$statuses"
}
$meanError = ($metricResults | Measure-Object -Property mean_perceptual_error -Average).Average
$changedRatio = ($metricResults | Measure-Object -Property changed_pixel_ratio -Average).Average
$ranking.Add([pscustomobject]@{
yaw_offset_degrees = $yawOffset
pitch_offset_degrees = $pitchOffset
camera_fov_degrees = $cameraFovValue
mean_perceptual_error = $meanError
changed_pixel_ratio = $changedRatio
candidate_directory = $candidateOutput
comparison_report = $comparisonReport
})
}
}
}
$rankedCandidates = if ($PlanOnly) {
@($ranking)
} else {
@($ranking | Sort-Object mean_perceptual_error, changed_pixel_ratio)
}
$rankingPath = Join-Path $absoluteOutput "ranking.json"
$rankingDocument = [ordered]@{
schema_version = 1
checkpoint = $Checkpoint
reference_directory = [IO.Path]::GetFullPath($ReferenceDirectory)
viewport = @($ViewportWidth, $ViewportHeight)
wait_seconds = $WaitSeconds
measure_seconds = $MeasureSeconds
candidates = $rankedCandidates
}
New-Item -ItemType Directory -Force -Path $absoluteOutput | Out-Null
$rankingDocument | ConvertTo-Json -Depth 6 | Set-Content -Encoding UTF8 -LiteralPath $rankingPath
$rankedCandidates | Format-Table camera_fov_degrees, yaw_offset_degrees, pitch_offset_degrees, mean_perceptual_error, changed_pixel_ratio
$completionKind = if ($PlanOnly) { "plan" } else { "ranking" }
Write-Output "Camera pose sweep $completionKind completed: $rankingPath"
} finally {
Pop-Location
}