# 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 Fremd-Tool-Hinweise im Code, in Kommentaren oder der Historie. Kommentare deutsch, Identifier englisch. Kein Commit ohne Absprache. ## 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.