48bbcaed1e
Eigenstaendige Crate wie render2d (render/window-Stufung, serde-only Mesh- schicht headless testbar). Wand-Extrusion (Band via Links-Normale, Quader mit nach aussen zeigenden Normalen), handgerechnete Mat4 (perspektiv+ortho, wgpu- Clip-Z [0,1], 5 Kamera-Presets), Directional-Light + Tiefenpuffer + Backface- Culling. Orbit-Spike (cargo run --features window --bin spike3d). Plus Port- Briefing mit M2..M9-Milestones (three.js-Viewport-Bestandsaufnahme).
266 lines
14 KiB
Markdown
266 lines
14 KiB
Markdown
# Briefing — Nativer wgpu-3D-Renderer (M0 Design + M1 Spike)
|
||
|
||
> Schwester-Dokument zu `wgpu-2d-renderer-briefing.md`. Beschreibt den Weg vom
|
||
> jetzigen three.js-3D-View (WebGL im WebKitGTK-Webview) zu einer nativen
|
||
> wgpu-Engine (Rust). Dieses Dokument ist der ANFANG: M0 (Bestandsaufnahme +
|
||
> Port-Plan) und M1 (entkoppelter Standalone-Spike). Noch NICHT die Migration.
|
||
|
||
## Commit-/Spuren-Regel
|
||
Wie im ganzen Repo: keine Hinweise auf KI-Werkzeuge — nicht im Code, in Kommentaren
|
||
oder in der Historie. Kommentare deutsch, Identifier englisch. Kein Commit ohne
|
||
Ansage der Hauptinstanz.
|
||
|
||
## Warum
|
||
Der 3D-View laeuft heute als three.js/WebGL im Tauri-Webview (WebKitGTK). Wie beim
|
||
2D-Plan bremst dieser Compositor unter Linux (siehe `wgpu-2d-renderer-briefing.md`
|
||
und Memo `webkitgtk-bottleneck`). Die schwere 3D-Grafik soll daher **nativ mit wgpu**
|
||
gerendert werden (Rust), die Webview macht nur noch UI-Chrome. Das umgeht den
|
||
Webview-Compositor komplett und teilt sich die Toolchain mit dem 2D-Renderer
|
||
(`src-tauri/render2d/`, gleiche Feature-Stufung, gleiche Test-Muster).
|
||
|
||
---
|
||
|
||
## 1. Bestandsaufnahme — was der three.js-View rendert
|
||
|
||
Referenz: `src/viewport/Viewport3D.tsx` (analysiert), plus die Geometrie-Grundlage
|
||
in `src/model/geometry.ts`, `src/model/wall.ts`, `src/model/joins.ts`,
|
||
`src/geometry/opening.ts`, `src/geometry/stair.ts`. Zeilenangaben beziehen sich auf
|
||
den Stand der Analyse.
|
||
|
||
### Koordinaten-Konvention (verbindlich)
|
||
Das Modell ist 2D in Metern (`x`, `y`) plus Hoehe `z`. Die 3D-Welt ist **Y-up**:
|
||
|
||
```
|
||
world.x = model.x
|
||
world.y = Hoehe (z)
|
||
world.z = model.y
|
||
```
|
||
|
||
D.h. **der Grundriss liegt in der XZ-Ebene, die Extrusion laeuft entlang +Y.**
|
||
Belegt u.a. in `Viewport3D.tsx`:
|
||
- Kommentar (~Z. 473): „Modell (x,y,z) → Three (x, z, y) (Z = Hoehe nach oben)".
|
||
- Kontext-Mesh (~Z. 1679–1684): `verts[i]=pos.x; verts[i+1]=pos.z (Hoehe); verts[i+2]=pos.y`.
|
||
- Wand-Griffe (~Z. 764): `new THREE.Vector3(wall.start.x, zBottom, wall.start.y)`.
|
||
- Workplane-Raycast (~Z. 627): `{ x: hit.x, y: hit.z }` (Three → Modell).
|
||
|
||
Diese Konvention ist in `render3d` 1:1 uebernommen (`types.rs`, `mesh.rs`).
|
||
|
||
### Waende (der Kern)
|
||
- Funktion `addWallMeshes()` / `addLayerPrism()` (~Z. 1939–2039, 2237–2271).
|
||
- **Mesh-Weg:** `THREE.ExtrudeGeometry` (~Z. 2248) ueber die 2D-Bandform, die
|
||
`clippedBand(p1, p2, offA, offB, startCut, endCut)` liefert (~Z. 2237; Funktion in
|
||
`src/model/geometry.ts:87`). Extrudiert wird um `depth = zTop - zBottom`.
|
||
- Die Bandform kommt aus Achse + Dicke: `wallBand`/`wallCorners`
|
||
(`geometry.ts:53`/`:70`) versetzen die Achse um `thickness/2` entlang der
|
||
**Links-Normale** `leftNormal(u) = (-u.y, u.x)` (`geometry.ts:17`), CCW-Umlauf.
|
||
- `ExtrudeGeometry` liegt in der XY-Ebene und waechst entlang +Z; three.js dreht das
|
||
Prisma daher um +90 Grad um X und setzt es auf `topY` (~Z. 2266–2271). In wgpu
|
||
extrudieren wir direkt in world (XZ-Grundriss, +Y-Hoehe) und sparen die Drehung.
|
||
- **Hoehe/Basis:** `wallVerticalExtent(project, wall)` (`wall.ts:56`) liefert
|
||
absolute `zBottom`/`zTop` (aus `wall.bottom`/`wall.top`-Ankern bzw. Geschoss-
|
||
`baseElevation + wall.height`).
|
||
- **Mehrschichtig:** je `wt.layers`-Schicht ein eigenes Prisma mit Dicken-Offset
|
||
(~Z. 1991–2015). M1 extrudiert vereinfacht EINE Schicht (Gesamtdicke).
|
||
- **Ecken/Gehrung:** `computeJoins()` (`joins.ts:45`) berechnet Schnittlinien
|
||
(`startCut`/`endCut`), die `clippedBand` an L-Ecken auf Gehrung zieht
|
||
(`miterLine`, `joins.ts:101`). M1 laesst das noch weg (stumpfe Enden).
|
||
|
||
### Oeffnungen (Fenster/Tueren)
|
||
- `addOpeningMeshes()` (~Z. 2056–2169) + Segmentierung in `addWallMeshes` (~Z.
|
||
1968–2035). **Kein CSG/Boolean:** die Wand wird entlang der Achse in Segmente
|
||
zerlegt (`openingInterval`, `geometry/opening.ts:31`), und je Oeffnung entstehen
|
||
bis zu drei Prismen: Wand DAVOR, **Bruestung** unter dem Fenster (`sillRel`),
|
||
**Sturz** ueber der Oeffnung (`headRel`). Rahmen/Fluegel als `BoxGeometry`;
|
||
Glas semitransparent, Tuerfluegel um `swingAngle` gedreht.
|
||
|
||
### Treppen
|
||
- `addStairMeshes()` (~Z. 2357–2409). `stairGeometry()` (`geometry/stair.ts`)
|
||
liefert Trittflaechen (Footprint + Steig-Hoehe) + optionalen Podest-Umriss; jede
|
||
Stufe als extrudierter Block (`ExtrudeGeometry`), Hoehe = `stairVerticalExtent`.
|
||
|
||
### Decken/Platten
|
||
- `addCeilingMesh()` (~Z. 2290–2347). `ceiling.outline` als `ExtrudeGeometry`, Tiefe
|
||
= Deckenstaerke, waechst nach unten von `zTop` (`ceilingVerticalExtent`, `wall.ts:80`).
|
||
|
||
### Raeume
|
||
- Nicht als eigenstaendige 3D-Koerper gerendert (2D-Grundriss-Repraesentation).
|
||
|
||
### Kontext/Gelaende
|
||
- `buildContext()` (~Z. 1651–1718). Terrain/importierte Meshes als rohe
|
||
`BufferGeometry` (Positions/Indices, Koordinaten-Swap wie oben); Hoehenlinien als
|
||
`LineSegments`. Dazu ein `GridHelper` (~Z. 421) auf OKFF-Hoehe.
|
||
|
||
### Materialien
|
||
- `MeshLambertMaterial` (Waende/Oeffnungen/Treppen, per Komponente eingefaerbt,
|
||
~Z. 2176–2206), `MeshStandardMaterial` (Weiss-/Textur-Modus + Terrain, PBR:
|
||
`roughness`/`metalness`/`aoMap`, ~Z. 445–491, 2001–2015), `MeshBasicMaterial`
|
||
(Hidden-Line-Flaechen + immer-oben-Marker), `LineBasicMaterial` (Kanten/2D-
|
||
Zeichnungen). Render-Modi: shaded / white / textured / wireframe / hidden-line.
|
||
|
||
### Beleuchtung
|
||
- `AmbientLight(0xffffff, 0.6)` (~Z. 416) + `DirectionalLight(0xffffff, 1.1)` bei
|
||
`(6, 12, 4)` (~Z. 417). **Keine Schatten** konfiguriert. Keine Hemisphere/Point-
|
||
Lights.
|
||
|
||
### Kamera + Presets
|
||
- Zwei Kameras: `PerspectiveCamera(fov, 1, 0.1, 1000)` (~Z. 367) und
|
||
`OrthographicCamera(-1,1,1,-1, 0.1, 5000)` (~Z. 374). `applyView3d()` (~Z.
|
||
1566–1633) setzt fuenf Presets:
|
||
- **front** — Richtung `(0,0,1)`, orthografisch.
|
||
- **side** — Richtung `(1,0,0)`, orthografisch.
|
||
- **top** — Richtung `(0,1,~0)`, orthografisch (Rotation gesperrt).
|
||
- **iso** — Richtung `(1,1,1)` normiert, orthografisch.
|
||
- **perspective** — Richtung `(0.62,0.5,0.7)` normiert, perspektivisch.
|
||
- Umschalten perspektiv/ortho ueber `active = perspective ? camera : orthoCamera`
|
||
(~Z. 1602); Ortho-Frustum aus den Modell-Bounds (`updateOrthoFrustum`, ~Z. 1522).
|
||
- **OrbitControls** (~Z. 391–413): Mitteltaste orbit, Shift+Mitte pan, Rad zoom
|
||
(linke/rechte Taste fuer Auswahl/Kontextmenue umgewidmet).
|
||
|
||
### Griffe / Gizmos
|
||
- Editier-Griffe (`SphereGeometry`, ~Z. 711–814): Endpunkt (orange), Hoehe (blau),
|
||
Verschieben (gruen); `depthTest:false` (immer sichtbar). Drag ueber Workplane-
|
||
Raycast. Fuer den nativen Renderer spaeter relevant (eigener Overlay-Pass).
|
||
|
||
### Schnittebene
|
||
- **Nicht implementiert:** keine `renderer.clippingPlanes` / `localClippingEnabled`.
|
||
Schnitte laufen aktuell 2D. Fuer wgpu ein eigenständiger spaeterer Milestone
|
||
(Clip-Distances im Shader oder Stencil-Capping).
|
||
|
||
### Tiefe / Culling
|
||
- Tiefentest three.js-Standard aktiv. Backface-Culling per Default (Ausnahme:
|
||
Terrain/Import `DoubleSide`). Diverse Overlays mit `depthTest:false`.
|
||
|
||
---
|
||
|
||
## 2. Port-Plan nach wgpu
|
||
|
||
### Datenfluss
|
||
Web-Modell → **geflachte Eingabe** (`WallInput`, spaeter Oeffnungen/Treppen/Decken)
|
||
→ `render3d`-Mesh-Erzeugung → GPU-Buffers → Draw. Analog zum 2D-Pfad (`Scene` →
|
||
Tessellierung → Buffers). Die Eingabe ist bewusst serde-only und GPU-frei, damit die
|
||
Mesh-Logik headless testbar bleibt.
|
||
|
||
### Mesh-Erzeugung (Waende extrudieren)
|
||
- Band aus Achse + Dicke ueber die Links-Normale (`(-u.y, u.x) * thickness/2`,
|
||
CCW), exakt wie `wallCorners`. Extrusion in world: XZ-Grundriss, +Y von
|
||
`base_elevation` bis `+height`.
|
||
- Ein Quader = 6 Seiten, je eigene Vertices mit Flaechen-Normale (flaches Shading,
|
||
korrektes Backface-Culling). 24 Vertices / 36 Indizes je Wand.
|
||
- Spaeter: mehrschichtige Waende (je Schicht ein Prisma), Gehrung
|
||
(`computeJoins`/`clippedBand`-Port), Oeffnungs-Segmentierung (Bruestung/Sturz).
|
||
|
||
### Kamera (View/Projektion, Presets)
|
||
- `look_at` (right-handed, Kamera blickt entlang -Z im View-Raum), `perspective`
|
||
und `orthographic` — beide auf **Clip-Z in [0,1]** (wgpu-Konvention, NICHT [-1,1]).
|
||
- Fuenf Presets (`preset_camera`): front/top/side orthografisch achsparallel,
|
||
iso/persp perspektivisch. `top` mit up=-Z, damit Modell-Y im Bild nach unten
|
||
zeigt (wie die 2D-Sicht).
|
||
- Orbit-Kamera aus Yaw/Pitch/Distanz (`orbit_eye`, Pitch geklemmt gegen Pol-Flip).
|
||
|
||
### Beleuchtung
|
||
- Zunaechst EIN Directional-Light (Richtung ZUM Licht) + ambienter Sockel im
|
||
Fragment-Shader (WGSL) — das GPU-Aequivalent zu `AmbientLight(0.6)` +
|
||
`DirectionalLight(1.1)@(6,12,4)`. Diffuses Lambert. PBR (Rauheit/Metallik/
|
||
Texturen/AO) spaeter.
|
||
|
||
### Tiefenpuffer + Culling
|
||
- `Depth32Float`-Attachment, `depth_compare = Less`, `depth_write = true`.
|
||
- `front_face = Ccw`, `cull_mode = Back` (die Extrusion liefert konsistent nach
|
||
aussen zeigende CCW-Flaechen).
|
||
|
||
### Matrix-Mathematik
|
||
- Handgerechnet (kein `glam`) in der serde-only Schicht — begruendet in `math.rs`:
|
||
die Standard-Schicht soll wie in render2d ohne Zusatz-Crates headless test-/baubar
|
||
bleiben; der Umfang (perspective/ortho/look_at + Orbit) ist klein und exakt
|
||
testbar. Ein spaeterer Wechsel zu `glam` (nur in der GPU-Schicht) bleibt moeglich,
|
||
ohne die Kamera-Tests anzufassen. Alles spalten-major, direkt als Uniform ladbar.
|
||
|
||
### Milestones M2..Mn
|
||
- **M2 — Mehrschichtige Waende + Gehrung:** Port von `computeJoins`/`miterLine` +
|
||
`clippedBand` → gehrte Bandformen je Schicht; Farben/Materialien je Komponente.
|
||
- **M3 — Oeffnungen:** Achsen-Segmentierung (Bruestung/Sturz) + Rahmen/Glas/Fluegel
|
||
als eigene Meshes; Tuerschwenk-Winkel.
|
||
- **M4 — Treppen + Decken:** Port von `stairGeometry`/`ceilingVerticalExtent`.
|
||
- **M5 — Kontext/Gelaende:** rohe Terrain-/Import-Meshes + Hoehenlinien + Grid.
|
||
- **M6 — Materialien (PBR):** `MeshStandardMaterial`-Aequivalent (roughness/
|
||
metalness/albedo/AO-Textur); Render-Modi shaded/white/textured/wireframe/hidden.
|
||
- **M7 — Schnittebene:** Clip-Distances im Shader oder Stencil-Capping (Feature, das
|
||
der three.js-View gar nicht hat — echter Mehrwert).
|
||
- **M8 — Griffe/Gizmos + Picking:** Overlay-Pass (immer-oben) + GPU-/Ray-Picking.
|
||
- **M9 — Tauri-Integration:** Surface unter der Webview (raw-window-handle,
|
||
Z-Order, Input-Routing) — siehe `wgpu-2d-renderer-briefing.md` M2 (gleiches
|
||
Integrations-Problem; einmal loesen, fuer 2D+3D nutzen). Kamera-Presets/Orbit-
|
||
Input aus der Webview an den Renderer.
|
||
|
||
---
|
||
|
||
## 3. Was in `src-tauri/render3d/` steht (M1)
|
||
|
||
Neue, eigenstaendige Crate (eigener leerer `[workspace]`-Block, wie render2d), damit
|
||
`cargo test`/`build` unabhaengig vom Tauri-Workspace laufen. Feature-Stufung 1:1 wie
|
||
render2d:
|
||
|
||
- `Cargo.toml` — Features `default` (serde-only) / `render` (wgpu) / `window`
|
||
(winit-Spike). `[[bin]] spike3d` mit `required-features = ["window"]`.
|
||
- `src/types.rs` — serde-only Eingabe: `WallInput { start, end, thickness, height,
|
||
base_elevation, color }`, `Camera` (+ `Projection`), `CameraPreset`; Ausgabe
|
||
`Mesh` (interleaved `[pos.xyz, normal.xyz, color.rgb]` + Indizes) mit
|
||
`vertex_count`/`triangle_count`/`bounds`. Koordinaten-Konvention dokumentiert.
|
||
- `src/mesh.rs` — Wand-Extrusion: `extrude_wall`/`build_walls_mesh`. Band ueber
|
||
Links-Normale, Quader mit sechs eigenen Seiten, nach aussen zeigende Normalen.
|
||
- `src/math.rs` — `Mat4` (spalten-major), `perspective`/`orthographic` (Clip-Z
|
||
[0,1]), `look_at`, `view_projection`, `orbit_eye`, `preset_camera` (fuenf Presets).
|
||
- `src/shaders.rs` — WGSL (`MESH_WGSL`): View-Projektion-Uniform + Directional-
|
||
Light + ambienter Sockel im Fragment-Shader.
|
||
- `src/gpu.rs` (Feature `render`) — `Renderer`: eine Pipeline mit Tiefenpuffer,
|
||
View-Projektions-Uniform, Backface-Culling. `upload_walls` → GPU-Buffers,
|
||
`render(camera, viewport)`.
|
||
- `src/bin/spike3d.rs` (Feature `window`) — winit-Fenster mit Demo-Raum (5
|
||
extrudierte Waende) + **Orbit-Kamera** (linke Maustaste dreht Yaw/Pitch, Rad
|
||
zoomt Abstand). Matrix-getrieben, kein Re-Meshing beim Kamera-Wechsel.
|
||
- `src/lib.rs` — Modul-Deklarationen, Re-Exports, Tests.
|
||
|
||
### Tests (`cargo test`, default-Feature)
|
||
Muster wie render2d/`glPlanCompile.test.ts`:
|
||
- Quader-Zaehlung (eine Wand → 24 Vertices / 36 Indizes / 12 Dreiecke).
|
||
- Mehrere Waende addieren sich.
|
||
- Bounding-Box deckt Laenge/Dicke/Hoehe ab; `base_elevation` verschiebt in Y.
|
||
- Deckel-Normale = +Y; **alle Mantel-Normalen zeigen nach aussen** (Dot mit
|
||
„Vertex − Zentrum" ≥ 0 → Backface-Culling korrekt).
|
||
- Diagonale Wand; degenerierte Wand (Start==Ende) erzeugt nichts (kein Absturz).
|
||
- Kamera: `look_at` setzt Ziel auf view-z=-dist; Perspektive klemmt z in [0,1];
|
||
`orbit_eye` haelt den Abstand; Presets setzen die richtige Projektionsart.
|
||
- Mit `--features render`: WGSL headless via `naga` (Parser + Validator) validiert.
|
||
|
||
---
|
||
|
||
## 4. Build-/Test-Ergebnis
|
||
|
||
Alle Gates gruen (Toolchain: cargo 1.96, wgpu 22, winit 0.30):
|
||
- `cargo test` (default) — **12/12** gruen (Mesh + Kamera).
|
||
- `cargo test --features render` — **13/13** gruen (inkl. WGSL-naga-Validierung).
|
||
- `cargo build` (default), `--features render`, `--features window` — je gruen,
|
||
**keine Warnungen**.
|
||
- Trace-Scan sauber (keine KI-Spuren).
|
||
- Web-Gates unberuehrt (nur `src-tauri/` + `docs/` angefasst; `src/` nur gelesen).
|
||
|
||
**Visuelle Fenster-Verifikation** ist headless NICHT moeglich. Auf einer aktiven
|
||
Display-Session pruefbar mit:
|
||
|
||
```
|
||
cargo run --features window --bin spike3d
|
||
```
|
||
|
||
Erwartet: ein Raum aus extrudierten Waenden mit diffuser Beleuchtung; linke
|
||
Maustaste dreht die Orbit-Kamera, das Rad zoomt.
|
||
|
||
---
|
||
|
||
## 5. Naechste Schritte
|
||
1. **M2** starten: `computeJoins`/`clippedBand`-Port für gehrte, mehrschichtige
|
||
Waende (die Bandmath ist im Web bereits verifiziert — gleiche Tests portieren).
|
||
2. Oeffnungs-Segmentierung (M3) auf demselben Extrusions-Kern.
|
||
3. Die **Tauri-Integration (M9)** gemeinsam mit dem 2D-Renderer loesen (ein Surface-
|
||
Unterbau, ein Input-Routing) — das ist der eigentliche Engpass, nicht das
|
||
Rendering. Erst standalone spiken (dieser Stand), dann unter die Webview.
|