Merge pull request 'test(M00): add camera pose sweep' (#8) from work/sindo-main-codex/m00-camera-pose-sweep into master
Reviewed-on: #8
This commit was merged in pull request #8.
This commit is contained in:
@@ -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,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.
|
||||||
@@ -195,7 +195,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
|
|
||||||
## Verification
|
## 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.
|
- 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 ещё не закрыты.
|
- 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.
|
- Performance budgets: M00 report records cold/warm p95 and max hitch; no final acceptance threshold yet.
|
||||||
@@ -229,6 +229,7 @@ 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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
- 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.
|
- 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.
|
- M2/WMO/material/particle/ribbon/portal parity incomplete.
|
||||||
- Public API is mostly exported configuration rather than stable contracts.
|
- Public API is mostly exported configuration rather than stable contracts.
|
||||||
@@ -248,6 +249,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
| `tools/run_render_baseline.ps1` | Unified M00 baseline runner |
|
| `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/compare_render_checkpoints.gd` | Offline JPG/PNG paired-image perceptual metrics and JSON pass/fail report |
|
||||||
| `src/tools/capture_render_checkpoints.gd` | Deterministic no-roll checkpoint camera, performance and visual capture |
|
| `src/tools/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/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_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 |
|
| `src/tools/probe_render_camera_occluders.gd` | Scene-tree placement containment and camera-to-target AABB intersection report |
|
||||||
@@ -256,5 +258,6 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
|
|
||||||
- [`../../RENDER.md`](../../RENDER.md)
|
- [`../../RENDER.md`](../../RENDER.md)
|
||||||
- [`../RENDER_BASELINE.md`](../RENDER_BASELINE.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)
|
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
||||||
- [`../GODOT_BEST_PRACTICES.md`](../GODOT_BEST_PRACTICES.md)
|
- [`../GODOT_BEST_PRACTICES.md`](../GODOT_BEST_PRACTICES.md)
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ func _capture_async() -> void:
|
|||||||
var revision := _arg(args, "--revision", "worktree")
|
var revision := _arg(args, "--revision", "worktree")
|
||||||
var cache_state := _arg(args, "--cache-state", "existing")
|
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_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 headless := DisplayServer.get_name().to_lower() == "headless"
|
||||||
var dry_run := args.has("--dry-run") or headless
|
var dry_run := args.has("--dry-run") or headless
|
||||||
var viewport: Array = manifest.get("viewport", [1280, 900])
|
var viewport: Array = manifest.get("viewport", [1280, 900])
|
||||||
@@ -86,6 +89,8 @@ func _capture_async() -> void:
|
|||||||
"dry_run": dry_run,
|
"dry_run": dry_run,
|
||||||
"cache_state": cache_state,
|
"cache_state": cache_state,
|
||||||
"camera_fov": camera_fov_override,
|
"camera_fov": camera_fov_override,
|
||||||
|
"camera_yaw_offset_degrees": camera_yaw_offset_degrees,
|
||||||
|
"camera_pitch_offset_degrees": camera_pitch_offset_degrees,
|
||||||
"environment": _environment_metadata(),
|
"environment": _environment_metadata(),
|
||||||
"comparison_budgets": manifest.get("comparison_budgets", {}),
|
"comparison_budgets": manifest.get("comparison_budgets", {}),
|
||||||
"cache_contract": manifest.get("cache_contract", {}),
|
"cache_contract": manifest.get("cache_contract", {}),
|
||||||
@@ -94,6 +99,8 @@ func _capture_async() -> void:
|
|||||||
}
|
}
|
||||||
var captured := 0
|
var captured := 0
|
||||||
var passes := ["cold_process", "warm_revisit"]
|
var passes := ["cold_process", "warm_revisit"]
|
||||||
|
if single_pass:
|
||||||
|
passes = ["cold_process"]
|
||||||
if dry_run:
|
if dry_run:
|
||||||
passes = ["dry_run"]
|
passes = ["dry_run"]
|
||||||
|
|
||||||
@@ -114,6 +121,7 @@ func _capture_async() -> void:
|
|||||||
|
|
||||||
camera.global_position = _vector3(checkpoint.get("camera", [0.0, 0.0, 0.0]))
|
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])))
|
_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()
|
camera.make_current()
|
||||||
if player != null:
|
if player != null:
|
||||||
player.global_position = _vector3(checkpoint.get("player", checkpoint.get("target", [0.0, 0.0, 0.0])))
|
player.global_position = _vector3(checkpoint.get("player", checkpoint.get("target", [0.0, 0.0, 0.0])))
|
||||||
@@ -122,11 +130,13 @@ func _capture_async() -> void:
|
|||||||
world.call("_refresh_streaming_targets_at", camera.global_position, true)
|
world.call("_refresh_streaming_targets_at", camera.global_position, true)
|
||||||
|
|
||||||
if dry_run:
|
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,
|
checkpoint_name,
|
||||||
str(checkpoint.get("coverage", [])),
|
str(checkpoint.get("coverage", [])),
|
||||||
camera.global_position,
|
camera.global_position,
|
||||||
_vector3(checkpoint.get("target", [0.0, 0.0, 0.0])),
|
_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)),
|
float(checkpoint.get("time_hours", 13.0)),
|
||||||
])
|
])
|
||||||
(report["results"] as Array).append(_result_record(checkpoint, pass_name, 0.0, {}, ""))
|
(report["results"] as Array).append(_result_record(checkpoint, pass_name, 0.0, {}, ""))
|
||||||
@@ -215,6 +225,11 @@ func _orient_camera_without_roll(camera: Camera3D, target_position: Vector3) ->
|
|||||||
camera.global_basis = Basis(right, corrected_up, -forward)
|
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:
|
func _result_record(checkpoint: Dictionary, pass_name: String, load_time_ms: float, metrics: Dictionary, sha256: String) -> Dictionary:
|
||||||
return {
|
return {
|
||||||
"name": checkpoint.get("name", "checkpoint"),
|
"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