refactor(M03): extract M2 placement transform resolver

This commit is contained in:
2026-07-16 23:51:59 +04:00
parent 91f0ba8433
commit 0a09d7bc1e
9 changed files with 573 additions and 90 deletions
+1
View File
@@ -16,6 +16,7 @@
| Terrain chunk LOD planner | Implemented extraction | [`terrain-chunk-lod-planner.md`](terrain-chunk-lod-planner.md) |
| Terrain chunk geometry queue planner | Implemented extraction | [`terrain-chunk-geometry-queue-planner.md`](terrain-chunk-geometry-queue-planner.md) |
| M2 unique placement registry | Implemented extraction | [`m2-unique-placement-registry.md`](m2-unique-placement-registry.md) |
| M2 placement transform resolver | Implemented | [`m2-placement-transform-resolver.md`](m2-placement-transform-resolver.md) |
| Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) |
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
@@ -0,0 +1,184 @@
# M2 Placement Transform Resolver
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M03 / `M03-RND-M2-PLACEMENT-TRANSFORM-001` |
| Owners | Pure ADT M2 placement basis and model-specific origin compensation |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-placement-transform`, 2026-07-16 |
| Profiles/capabilities | Existing static M2 paths; four calibrated model exceptions |
## Purpose
Resolve the unscaled Godot-space orientation and optional local origin offset
used by ADT M2 worker grouping, placeholder rendering and instance creation.
## Non-goals
- Convert WoW/server/ADT coordinates or own placement positions.
- Apply final scale clamps, build transforms, group placements or create Nodes.
- Add, generalize or configure model-specific corrections.
- Change M2 materials, animation, caches, visibility or quality profiles.
- Claim general build-12340 placement parity.
## Context and boundaries
```mermaid
flowchart LR
ADT[ADT placement rotation/path/scale] --> Resolver[M2PlacementTransformResolver]
Resolver --> Basis[Unscaled Basis]
Resolver --> Offset[Local origin offset]
Basis --> Group[Worker grouping]
Basis --> Placeholder[Placeholder MultiMesh path]
Basis --> Instance[Instance path]
Offset --> Group
Offset --> Placeholder
Offset --> Instance
Group --> Loader[StreamingWorldLoader-owned queues/build]
```
Allowed dependencies are `Vector3`, `Basis`, scalar math and path-string
normalization. Node, SceneTree, RenderingServer, ResourceLoader, tasks, caches,
files, gameplay, network and editor UI are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `resolve_basis(rotation_radians, relative_m2_path)` | Pure query | Return unscaled regular or calibrated placement orientation | Worker/main thread; call-local result | Unknown paths use `Basis.from_euler` |
| `resolve_origin_offset(rotation_radians, relative_m2_path, scale_value)` | Pure query | Compensate the measured tall-waterfall twist anchor | Worker/main thread; call-local result | Regular/unanchored paths return zero; scale clamps to `0.0001` for compensation |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Godot Euler rotation in radians | ADT loader placement record | Resolver | Scalar copy | One call |
| Input | Relative M2 path | ADT MMDX/name table | Resolver allowlist | Caller string | One call |
| Input | Raw placement scale | ADT placement record | Offset compensation | Scalar copy | One call |
| Output | Unscaled `Basis` | Resolver | Group/placeholder/instance adapter | Value copy | One call |
| Output | Local `Vector3` offset | Resolver | Transform origin adapter | Value copy | One call |
Callers preserve their historical basis scale minima: grouping/instance use
`0.0001`, while placeholders use `0.01`.
## Data flow
```mermaid
flowchart TD
BasisCall[resolve_basis] --> ModelName[Normalize separators/case/basename]
ModelName --> Waterfall{Waterfall sheet?}
Waterfall -->|yes| WaterfallBasis[+90 degree world yaw * Euler * local-axis twist]
Waterfall -->|no| Cliff{Elwynn cliff rock?}
Cliff -->|yes| CliffBasis[Corrected Y/BACK/RIGHT composition]
Cliff -->|no| Regular[Basis.from_euler]
OffsetCall[resolve_origin_offset] --> Anchored{Tall waterfall anchor?}
Anchored -->|no| Zero[Vector3.ZERO]
Anchored -->|yes| Compensation[(base anchor - twisted anchor) * clamped scale]
```
## Lifecycle/state
The resolver is stateless. It may be shared between the main thread and worker
calls because each result is computed solely from value inputs. Construction,
world reset and shutdown require no clear, cancellation or resource release.
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Resolver as M2PlacementTransformResolver
participant Consumer as Group/placeholder/instance path
Loader->>Resolver: resolve_basis(rotation, path)
Resolver-->>Loader: unscaled Basis
Loader->>Resolver: resolve_origin_offset(rotation, path, scale)
Resolver-->>Loader: local offset
Loader->>Consumer: compose historical scaled Transform3D
```
## Ownership, threading and resources
- The resolver retains no mutable state or engine resources.
- The loader owns placement records, final transforms, tasks, queues, caches,
MultiMesh/Node/Mesh/material/RID creation and cleanup.
- Pure methods are safe for current worker grouping and main-thread consumers.
- Returned `Basis`/`Vector3` values have no lifetime coupling to the resolver.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty/unknown path | Allowlist miss | Regular Euler basis and zero offset | Contract fixture | Expected fallback |
| Mixed case/backslashes/MDX | Normalization | Same calibrated model match | Contract fixture | None required |
| Zero/negative scale for anchored offset | Numeric clamp | Use `0.0001` compensation scale | Contract fixture | Caller may supply valid ADT scale later |
| World reset/shutdown | No retained state | No action | Existing shutdown verifier | New calls remain independent |
There is no asynchronous operation or cancellation inside the resolver.
## Configuration and capabilities
The resolver introduces no settings. The preserved constants and allowlists are:
- world yaw `PI` for `ElwynnCliffRock01/02`;
- world yaw `PI/2` for `NewWaterfall` and `ElwynnTallWaterfall01`;
- measured local axes/twist signs for both waterfall sheets;
- measured tall-waterfall anchor `(-2.667799, 89.62273, 0.00129)`.
## Persistence, cache and migration
No persistence, schema or cache format participates. Existing ADT/M2 cache
versions and rebuild instructions remain unchanged.
## Diagnostics and observability
The resolver emits no logs. The contract verifier compares formulas/constants,
checks all three loader consumers and records bounded synthetic timing. Visual
and asset-backed evidence remains in the renderer checkpoint workflow.
## Verification
- `verify_m2_placement_transform_resolver.gd`: regular fallback, path
normalization, both cliff rocks, both waterfalls, twist anchor, negative-scale
compensation, three loader adapters, source boundary and 10,000-pair timing.
- M2 unique/dedupe, terrain, facade, internal-access, manifest, shutdown,
scheduler, streaming and coordinate regressions remain required.
- Fidelity evidence is exact extraction of existing calibrated formulas. The
private asset/camera comparison that motivated them is not reproduced here.
## Extension points
- The next package may compose this resolver into a pure placement grouper.
- New model corrections require separate measured fidelity evidence; this module
is not a generic override registry.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Consistent transform rules across three M2 paths | Implemented | Formula/source/timing contract | Asset-backed visual recheck pending |
| General ADT M2 placement parity | Partial | Existing checkpoint notes | Broader fixture coverage required |
## Known gaps and risks
- Four model-specific exceptions are empirical compatibility rules.
- Most placements use raw Godot Euler behavior; independent client-wide
transform validation is incomplete.
- Callers intentionally retain different final scale clamps.
- Grouping, build, cache and render ownership remain in the streamer.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_placement_transform_resolver.gd` | Pure basis/offset rules and calibrated allowlists |
| `src/scenes/streaming/streaming_world_loader.gd` | Three adapters and final Transform3D/resource ownership |
| `src/tools/verify_m2_placement_transform_resolver.gd` | Formula, source and performance regression |
## Related decisions and references
- [`m2-unique-placement-registry.md`](m2-unique-placement-registry.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+12 -2
View File
@@ -5,9 +5,9 @@
| Field | Value |
|---|---|
| Status | Partial |
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; `M03-RND-M2-UNIQUE-REGISTRY-001` |
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 unique/transform packages |
| Owners | Renderer workstream / milestone integrator |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-unique-registry`, 2026-07-16 |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-placement-transform`, 2026-07-16 |
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
## Purpose
@@ -48,6 +48,8 @@ flowchart LR
ChunkQueue --> Loader
Loader --> M2Registry[M2UniquePlacementRegistry]
M2Registry --> Loader
Loader --> M2Transform[M2PlacementTransformResolver]
M2Transform --> Loader
Native --> Parsed[Parsed tile/model data]
Parsed --> Loader
Loader --> Scene[SceneTree nodes]
@@ -115,6 +117,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
| `TerrainChunkLodPlanner.plan` | Internal pure terrain query | Maps populated parsed chunk indices to desired LOD 0/1/2 | Synchronous per target refresh | Invalid/disabled input returns empty for existing tile-LOD fallback |
| `TerrainChunkGeometryQueuePlanner.plan` | Internal pure terrain query | Produces chunk create/remove requests from current and desired state | Synchronous per queue rebuild | Invalid focus returns empty arrays |
| `M2UniquePlacementRegistry.reserve/release/clear` | Internal M2 service | Owns positive cross-tile ADT placement-ID reservations | Renderer thread/map session | Invalid/unkeyed/non-owner inputs preserve documented fallback |
| `M2PlacementTransformResolver.resolve_basis/resolve_origin_offset` | Internal pure M2 service | Resolves regular and calibrated model-specific ADT placement transforms | Worker/main thread; stateless | Unknown paths use regular basis and zero offset |
Публичным contract не считаются `StreamingWorldLoader` methods/properties,
внутренние dictionaries, queues, job records и generated node names. Scene-owned
@@ -140,6 +143,7 @@ loader configuration remains transitional composition data, not a caller API.
| Internal plan | Parsed chunks, typed focus and immutable chunk-LOD policy | Loader / `TerrainChunkLodPlanner` | Loader desired state | Fresh dictionary; no retained resources | One target refresh |
| Internal plan | Current/desired chunk state and typed focus | Loader / `TerrainChunkGeometryQueuePlanner` | Loader chunk queues | Fresh request arrays; no retained resources | One queue rebuild |
| Internal registry | Tile key and M2 placement unique IDs | Loader / `M2UniquePlacementRegistry` | Filtered grouping input and tile retry state | Registry-owned strings; fresh result arrays | Map session |
| Internal transform | Rotation/path/scale | Loader / `M2PlacementTransformResolver` | Group/placeholder/instance transforms | Value-only Basis/Vector3 | One placement |
| Output | Desired tile/detail operations | Streamer plan application | Finalize queues | Loader-owned | Cross-frame |
| Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach |
| Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand |
@@ -185,6 +189,7 @@ flowchart TD
ChunkQueue --> B
R --> M2Registry[M2UniquePlacementRegistry reserve]
M2Registry --> M2
M2Transform[M2PlacementTransformResolver] --> M2
R --> B[RenderBudgetScheduler permits]
B --> Terrain[Terrain attach/upgrade]
B --> M2[M2 group/MultiMesh attach]
@@ -299,6 +304,8 @@ sequenceDiagram
- `M2UniquePlacementRegistry` owns only positive unique-key to tile-key strings.
The loader retains owned/skipped tile arrays, candidate retry, grouping/build
tasks, caches and all MultiMesh/Node/Mesh/material/RID side effects.
- `M2PlacementTransformResolver` is stateless and retains no resources. The
loader owns final transforms and every grouping/build/render side effect.
- Rendered-ground query results and diagnostic snapshots are caller-owned values;
the facade never returns Mesh, Node, tile-state or queue references.
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
@@ -384,6 +391,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
- M2 unique placement contract: unkeyed pass-through, within-call/cross-tile
dedupe, owner release/retry, clear, detached diagnostics, loader boundary and
bounded reserve/release timing without a world scene.
- M2 placement transform contract: regular/cliffrock/waterfall formulas, path
normalization, scale compensation, all three consumers and bounded timing.
- Budget scheduler contract: exact lane exhaustion, shared chunk removal/create
priority, independent lanes, frame reset, invalid limits, terminal cancellation,
dependency boundaries and bounded permit timing without loading a world scene.
@@ -414,6 +423,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| ADT streaming/terrain | Partial | M00 checkpoints and current scenes | Fidelity/performance gaps remain |
| Static M2 placement | Partial | MultiMesh/dedupe probes | Full materials/animation/effects |
| M2 unique placement registry | Implemented extraction | Scene-free ownership/lifecycle/timing contract and historical `uid:11785` smoke | Group/build/tasks/finalization and asset-backed p95/p99 remain pending |
| M2 placement transform resolver | Implemented extraction | Scene-free formula/source/timing contract across three consumers | Asset-backed visual recheck and general placement parity pending |
| WMO rendering | Partial | Cached group rendering | Portals/rooms/material parity |
| Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity |
| Sky/light | Partial | DBC controller/logs | Zone transition paired validation |