Files
DOSSIER-STANDALONE/docs/design/wgpu-3d-renderer-briefing.md

14 KiB
Raw Permalink Blame History

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. 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.rsMat4 (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 render13/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.