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).
14 KiB
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, dieclippedBand(p1, p2, offA, offB, startCut, endCut)liefert (~Z. 2237; Funktion insrc/model/geometry.ts:87). Extrudiert wird umdepth = zTop - zBottom. - Die Bandform kommt aus Achse + Dicke:
wallBand/wallCorners(geometry.ts:53/:70) versetzen die Achse umthickness/2entlang der Links-NormaleleftNormal(u) = (-u.y, u.x)(geometry.ts:17), CCW-Umlauf. ExtrudeGeometryliegt in der XY-Ebene und waechst entlang +Z; three.js dreht das Prisma daher um +90 Grad um X und setzt es auftopY(~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 absolutezBottom/zTop(auswall.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), dieclippedBandan L-Ecken auf Gehrung zieht (miterLine,joins.ts:101). M1 laesst das noch weg (stumpfe Enden).
Oeffnungen (Fenster/Tueren)
addOpeningMeshes()(~Z. 2056–2169) + Segmentierung inaddWallMeshes(~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 alsBoxGeometry; Glas semitransparent, Tuerfluegel umswingAnglegedreht.
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.outlinealsExtrudeGeometry, Tiefe = Deckenstaerke, waechst nach unten vonzTop(ceilingVerticalExtent,wall.ts:80).
Raeume
- Nicht als eigenstaendige 3D-Koerper gerendert (2D-Grundriss-Repraesentation).
Kontext/Gelaende
buildContext()(~Z. 1651–1718). Terrain/importierte Meshes als roheBufferGeometry(Positions/Indices, Koordinaten-Swap wie oben); Hoehenlinien alsLineSegments. Dazu einGridHelper(~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) undOrthographicCamera(-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.
- front — Richtung
- 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 mitdepthTest: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 wiewallCorners. Extrusion in world: XZ-Grundriss, +Y vonbase_elevationbis+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),perspectiveundorthographic— beide auf Clip-Z in [0,1] (wgpu-Konvention, NICHT [-1,1]).- Fuenf Presets (
preset_camera): front/top/side orthografisch achsparallel, iso/persp perspektivisch.topmit 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 inmath.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 zuglam(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.mdM2 (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— Featuresdefault(serde-only) /render(wgpu) /window(winit-Spike).[[bin]] spike3dmitrequired-features = ["window"].src/types.rs— serde-only Eingabe:WallInput { start, end, thickness, height, base_elevation, color },Camera(+Projection),CameraPreset; AusgabeMesh(interleaved[pos.xyz, normal.xyz, color.rgb]+ Indizes) mitvertex_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(Featurerender) —Renderer: eine Pipeline mit Tiefenpuffer, View-Projektions-Uniform, Backface-Culling.upload_walls→ GPU-Buffers,render(camera, viewport).src/bin/spike3d.rs(Featurewindow) — 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_elevationverschiebt 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_atsetzt Ziel auf view-z=-dist; Perspektive klemmt z in [0,1];orbit_eyehaelt den Abstand; Presets setzen die richtige Projektionsart. - Mit
--features render: WGSL headless vianaga(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
- M2 starten:
computeJoins/clippedBand-Port für gehrte, mehrschichtige Waende (die Bandmath ist im Web bereits verifiziert — gleiche Tests portieren). - Oeffnungs-Segmentierung (M3) auf demselben Extrusions-Kern.
- 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.