Nativer 3D-wgpu-Renderer (render3d, M0+M1): Wand-Extrusion, Kamera, Licht

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).
This commit is contained in:
2026-07-02 00:29:02 +02:00
parent 6d4b4e66c1
commit 48bbcaed1e
11 changed files with 4379 additions and 0 deletions
+265
View File
@@ -0,0 +1,265 @@
# 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. 16791684): `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. 19392039, 22372271).
- **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. 22662271). 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. 19912015). 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. 20562169) + Segmentierung in `addWallMeshes` (~Z.
19682035). **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. 23572409). `stairGeometry()` (`geometry/stair.ts`)
liefert Trittflaechen (Footprint + Steig-Hoehe) + optionalen Podest-Umriss; jede
Stufe als extrudierter Block (`ExtrudeGeometry`), Hoehe = `stairVerticalExtent`.
### Decken/Platten
- `addCeilingMesh()` (~Z. 22902347). `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. 16511718). 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. 21762206), `MeshStandardMaterial` (Weiss-/Textur-Modus + Terrain, PBR:
`roughness`/`metalness`/`aoMap`, ~Z. 445491, 20012015), `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.
15661633) 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. 391413): Mitteltaste orbit, Shift+Mitte pan, Rad zoom
(linke/rechte Taste fuer Auswahl/Kontextmenue umgewidmet).
### Griffe / Gizmos
- Editier-Griffe (`SphereGeometry`, ~Z. 711814): 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.