[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 }