Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad9ba7af4e | |||
| f54850718a | |||
| 5103eed014 | |||
| 9d34a47765 | |||
| aea7787b9b |
@@ -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-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-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`
|
||||||
@@ -158,3 +158,39 @@ flowchart LR
|
|||||||
Измеренный 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.
|
Измеренный 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` остаётся строгим режимом для действительно неснятых точек.
|
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 `26–38°` и несовпавший changed-pixel optimum показывают доминирование manual look direction/framing; эти данные не обосновывают изменение нормативного manifest FOV `62°`. Для настоящей калибровки reference capture должен сохранять воспроизводимые yaw/pitch/zoom или независимый projection fixture.
|
||||||
|
|||||||
@@ -141,6 +141,9 @@ sequenceDiagram
|
|||||||
Stream->>Budget: enqueue finalize operations
|
Stream->>Budget: enqueue finalize operations
|
||||||
Budget->>Render: attach bounded terrain/M2/WMO/liquid work
|
Budget->>Render: attach bounded terrain/M2/WMO/liquid work
|
||||||
Stream->>Render: evict outside retention range
|
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
|
## Ownership, threading and resources
|
||||||
@@ -149,7 +152,7 @@ sequenceDiagram
|
|||||||
- Worker tasks не должны менять SceneTree и shared Resource concurrently.
|
- Worker tasks не должны менять SceneTree и shared Resource concurrently.
|
||||||
- Parsed/grouped results передаются обратно через guarded result queues.
|
- Parsed/grouped results передаются обратно через guarded result queues.
|
||||||
- Mesh/material/node/RID finalization выполняется main thread и ограничивается exported budgets.
|
- 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 после публикации.
|
- Cache resources считаются immutable после публикации.
|
||||||
|
|
||||||
## Errors, cancellation and recovery
|
## 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 |
|
| 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 |
|
| 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 |
|
| 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
|
## Configuration and capabilities
|
||||||
|
|
||||||
@@ -224,7 +227,9 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
- Original-client paired fidelity evidence incomplete.
|
- Original-client paired fidelity evidence incomplete.
|
||||||
- Первый paired run выявил coordinate/placement mismatch: несколько совпадающих server-derived camera positions оказываются под terrain или внутри WMO/rocks OpenWC.
|
- Первый paired run выявил coordinate/placement mismatch: несколько совпадающих server-derived camera positions оказываются под terrain или внутри WMO/rocks OpenWC.
|
||||||
- Terrain-height probe исключил under-terrain состояние для всех пяти точек; waterfall exact-XZ miss классифицирован как TriangleMesh seam/edge и подтверждён nearby sample в 2 units.
|
- Terrain-height probe исключил under-terrain состояние для всех пяти точек; waterfall exact-XZ miss классифицирован как TriangleMesh seam/edge и подтверждён nearby sample в 2 units.
|
||||||
- D3D12 descriptor and shutdown RID/resource issues remain.
|
- 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 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.
|
||||||
|
|
||||||
@@ -245,6 +250,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
| `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 |
|
||||||
| `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 |
|
||||||
|
|
||||||
## Related decisions and references
|
## Related decisions and references
|
||||||
|
|
||||||
|
|||||||
@@ -2073,7 +2073,7 @@ func _wait_for_tile_tasks() -> void:
|
|||||||
var path: String = String(pending.get("path", ""))
|
var path: String = String(pending.get("path", ""))
|
||||||
if not path.is_empty():
|
if not path.is_empty():
|
||||||
var status := ResourceLoader.load_threaded_get_status(path)
|
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)
|
ResourceLoader.load_threaded_get(path)
|
||||||
_tile_loading_tasks.clear()
|
_tile_loading_tasks.clear()
|
||||||
|
|
||||||
@@ -2094,7 +2094,7 @@ func _wait_for_tile_tasks() -> void:
|
|||||||
if path.is_empty():
|
if path.is_empty():
|
||||||
continue
|
continue
|
||||||
var status := ResourceLoader.load_threaded_get_status(path)
|
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)
|
ResourceLoader.load_threaded_get(path)
|
||||||
_m2_mesh_load_requests.clear()
|
_m2_mesh_load_requests.clear()
|
||||||
_m2_mesh_finalize_queue.clear()
|
_m2_mesh_finalize_queue.clear()
|
||||||
@@ -2105,7 +2105,7 @@ func _wait_for_tile_tasks() -> void:
|
|||||||
if path.is_empty():
|
if path.is_empty():
|
||||||
continue
|
continue
|
||||||
var status := ResourceLoader.load_threaded_get_status(path)
|
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)
|
ResourceLoader.load_threaded_get(path)
|
||||||
_m2_animation_load_requests.clear()
|
_m2_animation_load_requests.clear()
|
||||||
_m2_animation_finalize_queue.clear()
|
_m2_animation_finalize_queue.clear()
|
||||||
@@ -2115,7 +2115,7 @@ func _wait_for_tile_tasks() -> void:
|
|||||||
if path.is_empty():
|
if path.is_empty():
|
||||||
continue
|
continue
|
||||||
var status := ResourceLoader.load_threaded_get_status(path)
|
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)
|
ResourceLoader.load_threaded_get(path)
|
||||||
_wmo_scene_load_requests.clear()
|
_wmo_scene_load_requests.clear()
|
||||||
|
|
||||||
@@ -2124,7 +2124,7 @@ func _wait_for_tile_tasks() -> void:
|
|||||||
if path.is_empty():
|
if path.is_empty():
|
||||||
continue
|
continue
|
||||||
var status := ResourceLoader.load_threaded_get_status(path)
|
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)
|
ResourceLoader.load_threaded_get(path)
|
||||||
_wmo_render_load_requests.clear()
|
_wmo_render_load_requests.clear()
|
||||||
_wmo_render_missing_cache.clear()
|
_wmo_render_missing_cache.clear()
|
||||||
@@ -2137,7 +2137,7 @@ func _wait_for_tile_tasks() -> void:
|
|||||||
if path.is_empty():
|
if path.is_empty():
|
||||||
continue
|
continue
|
||||||
var status := ResourceLoader.load_threaded_get_status(path)
|
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)
|
ResourceLoader.load_threaded_get(path)
|
||||||
_terrain_upgrade_tasks.clear()
|
_terrain_upgrade_tasks.clear()
|
||||||
|
|
||||||
@@ -2148,7 +2148,7 @@ func _wait_for_tile_tasks() -> void:
|
|||||||
if path.is_empty():
|
if path.is_empty():
|
||||||
continue
|
continue
|
||||||
var status := ResourceLoader.load_threaded_get_status(path)
|
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)
|
ResourceLoader.load_threaded_get(path)
|
||||||
_terrain_control_splat_cache_tasks.clear()
|
_terrain_control_splat_cache_tasks.clear()
|
||||||
|
|
||||||
@@ -2162,7 +2162,7 @@ func _wait_for_tile_tasks() -> void:
|
|||||||
if path.is_empty():
|
if path.is_empty():
|
||||||
continue
|
continue
|
||||||
var status := ResourceLoader.load_threaded_get_status(path)
|
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)
|
ResourceLoader.load_threaded_get(path)
|
||||||
_terrain_splat_cache_tasks.clear()
|
_terrain_splat_cache_tasks.clear()
|
||||||
_terrain_splat_result_mutex.lock()
|
_terrain_splat_result_mutex.lock()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ extends SceneTree
|
|||||||
|
|
||||||
const DEFAULT_MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
|
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 M2_NATIVE_ANIMATED_BUILDER := preload("res://addons/mpq_extractor/loaders/m2_native_animated_builder.gd")
|
||||||
|
const SHUTDOWN_DRAIN_FRAMES := 2
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
func _initialize() -> void:
|
||||||
@@ -22,6 +23,7 @@ func _capture_async() -> void:
|
|||||||
var measure_seconds := float(_arg(args, "--measure", str(manifest.get("default_measure_seconds", 3.0))))
|
var measure_seconds := float(_arg(args, "--measure", str(manifest.get("default_measure_seconds", 3.0))))
|
||||||
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 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])
|
||||||
@@ -55,7 +57,7 @@ func _capture_async() -> void:
|
|||||||
var camera := Camera3D.new()
|
var camera := Camera3D.new()
|
||||||
camera.name = "CheckpointCamera"
|
camera.name = "CheckpointCamera"
|
||||||
camera.current = true
|
camera.current = true
|
||||||
camera.fov = float(manifest.get("camera_fov", 62.0))
|
camera.fov = camera_fov_override
|
||||||
camera.far = 50000.0
|
camera.far = 50000.0
|
||||||
camera.position = _vector3(first.get("camera", [0.0, 0.0, 0.0]))
|
camera.position = _vector3(first.get("camera", [0.0, 0.0, 0.0]))
|
||||||
(world as Node3D).add_child(camera)
|
(world as Node3D).add_child(camera)
|
||||||
@@ -65,6 +67,7 @@ func _capture_async() -> void:
|
|||||||
get_root().add_child(world)
|
get_root().add_child(world)
|
||||||
await process_frame
|
await process_frame
|
||||||
await process_frame
|
await process_frame
|
||||||
|
camera.make_current()
|
||||||
|
|
||||||
var player := world.get_node_or_null("ThirdPersonPlayer") as Node3D
|
var player := world.get_node_or_null("ThirdPersonPlayer") as Node3D
|
||||||
if player != null:
|
if player != null:
|
||||||
@@ -82,6 +85,7 @@ func _capture_async() -> void:
|
|||||||
"created_utc": Time.get_datetime_string_from_system(true, true),
|
"created_utc": Time.get_datetime_string_from_system(true, true),
|
||||||
"dry_run": dry_run,
|
"dry_run": dry_run,
|
||||||
"cache_state": cache_state,
|
"cache_state": cache_state,
|
||||||
|
"camera_fov": camera_fov_override,
|
||||||
"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", {}),
|
||||||
@@ -110,6 +114,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])))
|
||||||
|
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])))
|
||||||
_set_sky_time(world, float(checkpoint.get("time_hours", 13.0)))
|
_set_sky_time(world, float(checkpoint.get("time_hours", 13.0)))
|
||||||
@@ -163,6 +168,10 @@ func _capture_async() -> void:
|
|||||||
print("RENDER_BASELINE report=%s results=%d" % [report_path, captured])
|
print("RENDER_BASELINE report=%s results=%d" % [report_path, captured])
|
||||||
|
|
||||||
world.queue_free()
|
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:
|
if captured <= 0:
|
||||||
push_error("No checkpoints selected. --only filter was: %s" % only)
|
push_error("No checkpoints selected. --only filter was: %s" % only)
|
||||||
quit(1)
|
quit(1)
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func _parse_arguments(raw_arguments: PackedStringArray) -> Dictionary:
|
|||||||
parsed.self_test = true
|
parsed.self_test = true
|
||||||
index += 1
|
index += 1
|
||||||
continue
|
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}
|
return {"error": "unknown argument: %s" % argument}
|
||||||
if index + 1 >= raw_arguments.size():
|
if index + 1 >= raw_arguments.size():
|
||||||
return {"error": "missing value for %s" % argument}
|
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)
|
reference_directory = ProjectSettings.globalize_path(reference_directory)
|
||||||
candidate_directory = ProjectSettings.globalize_path(candidate_directory)
|
candidate_directory = ProjectSettings.globalize_path(candidate_directory)
|
||||||
var reference_files := _reference_image_file_names(reference_directory)
|
var reference_files := _reference_image_file_names(reference_directory)
|
||||||
|
var only_filter := String(options.get("only", "")).to_lower()
|
||||||
var results: Array[Dictionary] = []
|
var results: Array[Dictionary] = []
|
||||||
var failed_count := 0
|
var failed_count := 0
|
||||||
var missing_count := 0
|
var missing_count := 0
|
||||||
for file_name in reference_files:
|
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 reference_path := reference_directory.path_join(file_name)
|
||||||
var checkpoint_name := file_name.get_basename()
|
var checkpoint_name := file_name.get_basename()
|
||||||
var candidate_file_names := _candidate_file_names(candidate_directory, checkpoint_name)
|
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")
|
var changed_directory := root.path_join("changed")
|
||||||
for directory_path in [reference_directory, identical_directory, changed_directory]:
|
for directory_path in [reference_directory, identical_directory, changed_directory]:
|
||||||
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(directory_path))
|
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)
|
var reference_image := Image.create(2, 2, false, Image.FORMAT_RGBA8)
|
||||||
reference_image.fill(Color(0.25, 0.5, 0.75, 1.0))
|
reference_image.fill(Color(0.25, 0.5, 0.75, 1.0))
|
||||||
var changed_image := reference_image.duplicate()
|
var changed_image := reference_image.duplicate()
|
||||||
@@ -225,5 +229,21 @@ func _run_self_test() -> int:
|
|||||||
if not identical_report.passed or changed_report.passed:
|
if not identical_report.passed or changed_report.passed:
|
||||||
push_error("RENDER_CHECKPOINT_DIFF SELF_TEST: expected identical pass and changed failure")
|
push_error("RENDER_CHECKPOINT_DIFF SELF_TEST: expected identical pass and changed failure")
|
||||||
return 1
|
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")
|
print("RENDER_CHECKPOINT_DIFF SELF_TEST PASS")
|
||||||
return 0
|
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)
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user