Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 657a1d888a | |||
| b48195d08d | |||
| 8d4641a43b | |||
| ad9ba7af4e | |||
| f54850718a |
@@ -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,39 @@
|
||||
# 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 `
|
||||
-WaitSeconds 2
|
||||
```
|
||||
|
||||
Each candidate receives its own output directory and comparison report. `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.
|
||||
@@ -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, five-point coordinate calibration, 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.
|
||||
@@ -226,7 +229,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
- 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.
|
||||
- D3D12 descriptor and shutdown RID/resource issues remain.
|
||||
- Camera-pose sweep can now rank bounded yaw/pitch grids without changing manifest defaults; perceptual ranking remains diagnostic and requires human framing approval.
|
||||
- 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.
|
||||
|
||||
@@ -245,6 +249,7 @@ 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 |
|
||||
@@ -253,5 +258,6 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
|
||||
- [`../../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()
|
||||
|
||||
@@ -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:
|
||||
@@ -23,6 +24,9 @@ func _capture_async() -> void:
|
||||
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])
|
||||
@@ -85,6 +89,8 @@ func _capture_async() -> void:
|
||||
"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", {}),
|
||||
@@ -93,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"]
|
||||
|
||||
@@ -113,6 +121,7 @@ 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])))
|
||||
@@ -121,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, {}, ""))
|
||||
@@ -167,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)
|
||||
@@ -210,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"),
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
[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]$WaitSeconds = 2.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
|
||||
$cameraFovText = $CameraFov.ToString($invariantCulture)
|
||||
$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 ($yawOffset in $YawOffsets) {
|
||||
foreach ($pitchOffset in $PitchOffsets) {
|
||||
$yawText = $yawOffset.ToString("0.###", $invariantCulture)
|
||||
$pitchText = $pitchOffset.ToString("0.###", $invariantCulture)
|
||||
$candidateName = "yaw_$($yawText.Replace('-', 'n').Replace('.', '_'))__pitch_$($pitchText.Replace('-', 'n').Replace('.', '_'))"
|
||||
$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 = $CameraFov
|
||||
candidate_directory = $candidateOutput
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
& $GodotPath --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 `
|
||||
--wait $waitSecondsText --measure $measureSecondsText
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Capture failed for yaw=$yawText pitch=$pitchText"
|
||||
}
|
||||
|
||||
& $GodotPath --headless --path . --script res://src/tools/compare_render_checkpoints.gd -- `
|
||||
--reference $ReferenceDirectory --candidate $candidateOutput `
|
||||
--only $Checkpoint --output $comparisonReport
|
||||
if ($LASTEXITCODE -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"
|
||||
}
|
||||
$meanError = ($comparison.results | Measure-Object -Property mean_perceptual_error -Average).Average
|
||||
$changedRatio = ($comparison.results | Measure-Object -Property changed_pixel_ratio -Average).Average
|
||||
$ranking.Add([pscustomobject]@{
|
||||
yaw_offset_degrees = $yawOffset
|
||||
pitch_offset_degrees = $pitchOffset
|
||||
camera_fov_degrees = $CameraFov
|
||||
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)
|
||||
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 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
|
||||
}
|
||||
Reference in New Issue
Block a user