Nordstern-3D: View-Styles wireframe + hidden-line + schönere Schattierung

- Neue set_render_style(style)-Methode: shaded/white/wireframe/hidden
  (textured fällt vorerst auf shaded zurück, bis die Textur-Pipeline
  steht). Voll durchverdrahtet vom TopBar-Darstellungs-Dropdown zum
  Nordstern-Viewport (setRenderStyle statt nur setWhiteMode).
- Feature-Edge-Extraktion (edges.rs): Kanten aus dem Mesh, dedupliziert
  + Crease-Erkennung (nur Silhouette/Knickkanten, keine Triangulierungs-
  Diagonalen) → sauberes Architektur-Drahtgitter. Reuse der Grid-
  LineList-Pipeline.
- wireframe = nur Kanten; hidden = flach-weisse Flächen (Depth-Bias) +
  sichtbare Kanten obenauf (klassischer Hidden-Line-Look).
- Grundschattierung: dezentes Gegenlicht (Fill-Light), damit abgewandte
  Flächen nicht mehr in Schwarz absaufen.
This commit is contained in:
2026-07-04 06:05:11 +02:00
parent 85a721f03b
commit 6ef57b6b6b
7 changed files with 540 additions and 87 deletions
+250
View File
@@ -0,0 +1,250 @@
// Modell-Kanten (Feature-Edges) fuer die Draufsicht-Stile "wireframe" und
// "hidden" des 3D-Renderers.
//
// Leitet aus einem fertigen Dreiecks-`Mesh` (types::Mesh, interleaved
// [pos, normal, color]) eine schlanke Kantenliste ab und liefert sie im
// GLEICHEN Vertex-Format wie das Bodengitter (grid.rs): interleaved
// [px,py,pz, r,g,b], je 2 Vertices = 1 Liniensegment (LineList). Dadurch teilt
// sich die Kanten-Zeichnung Shader (GRID_WGSL) und Vertex-Layout mit dem Grid;
// die GPU-Schicht (gpu.rs) laedt die Vertices einmalig bei `set_model` in einen
// Linien-Vertex-Buffer und cached sie (nicht je Frame).
//
// FEATURE-EDGE-STATT-ALLE-DREIECKSKANTEN: Wuerde man jede Dreieckskante zeichnen,
// erschienen auch die Triangulierungs-Diagonalen jeder rechteckigen Flaeche
// (jede Wand-/Deckenseite ist aus 2 Dreiecken) — ein unruhiges Gitter. Statt
// dessen dedupliziert diese Schicht geteilte Kanten UND unterdrueckt Kanten
// zwischen zwei koplanaren Dreiecken (Flaechen-Innendiagonalen): gezeichnet
// werden nur Silhouetten-/Randkanten (Kante gehoert nur einem Dreieck) und
// Knickkanten ("Crease": die beiden angrenzenden Dreiecke stehen deutlich
// zueinander gewinkelt). Ergebnis ist ein sauberer Architektur-Drahtgitter-/
// Hidden-Line-Look statt einer Dreiecks-Suppe.
//
// GPU-frei (nur `Vec<f32>`), damit headless per `cargo test` pruefbar (Muster:
// grid.rs / mesh.rs / section.rs).
use std::collections::HashMap;
use crate::types::{Mesh, FLOATS_PER_VERTEX};
/// f32 je Kanten-Vertex im interleaved Puffer: 3 Position (world) + 3 Farbe.
/// Bewusst identisch zu `grid::GRID_FLOATS_PER_VERTEX`, damit dieselbe
/// LineList-Pipeline/derselbe Shader (GRID_WGSL) beide zeichnen kann.
pub const EDGE_FLOATS_PER_VERTEX: usize = 6;
/// Farbe der Modell-Kanten (RGB 0..1). Dezentes, dunkles Anthrazit — deutlich
/// genug fuer den Hidden-Line-Look, ohne hart schwarz zu wirken.
pub const EDGE_COLOR: [f32; 3] = [0.16, 0.16, 0.18];
/// Quantisierungsfaktor fuer den Kanten-Schluessel: Positionen werden auf 0.1 mm
/// (1e4 pro Meter) gerundet, damit geometrisch zusammenfallende Endpunkte aus
/// verschiedenen (pro Flaeche duplizierten) Vertices denselben Schluessel teilen.
const QUANT: f32 = 1.0e4;
/// Kosinus des Crease-Winkels: liegen die Normalen zweier angrenzender Dreiecke
/// enger beieinander (dot > COS), gelten sie als koplanar → die geteilte Kante
/// wird NICHT gezeichnet (Flaechen-Innendiagonale). Darunter = Knickkante →
/// zeichnen. cos(15°) ≈ 0.966: Ebenen-Diagonalen (dot≈1) fallen weg, echte
/// Bauteil-Kanten (≥ ~90°) bleiben.
const CREASE_COS: f32 = 0.966;
/// Ganzzahliger Positions-Schluessel (quantisiert) fuer die Kanten-Deduplizierung.
type Key = [i64; 3];
fn key_of(p: [f32; 3]) -> Key {
[
(p[0] * QUANT).round() as i64,
(p[1] * QUANT).round() as i64,
(p[2] * QUANT).round() as i64,
]
}
/// Sammlung je Kante: die (erste gesehene) Position beider Endpunkte plus die
/// Normalen aller angrenzenden Dreiecke — daraus entscheidet sich Rand/Knick.
struct EdgeRec {
p0: [f32; 3],
p1: [f32; 3],
normals: Vec<[f32; 3]>,
}
fn sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn normalize(v: [f32; 3]) -> [f32; 3] {
let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
if l <= 1e-12 {
[0.0, 0.0, 0.0]
} else {
[v[0] / l, v[1] / l, v[2] / l]
}
}
fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
/// Baut die Feature-Edge-LineList aus dem Mesh (siehe Moduldoc).
///
/// Ausgabe: interleaved `[px,py,pz, r,g,b, ...]`, je 2 Vertices = 1 Segment.
/// Leerer Vektor bei leerem/degeneriertem Mesh.
pub fn build_mesh_edges(mesh: &Mesh) -> Vec<f32> {
if mesh.indices.len() < 3 {
return Vec::new();
}
let pos = |i: u32| -> [f32; 3] {
let b = i as usize * FLOATS_PER_VERTEX;
[mesh.verts[b], mesh.verts[b + 1], mesh.verts[b + 2]]
};
// Kanten-Schluessel (kanonisch sortiertes Endpunkt-Paar) -> Datensatz.
let mut edges: HashMap<(Key, Key), EdgeRec> = HashMap::new();
for tri in mesh.indices.chunks_exact(3) {
let (a, b, c) = (pos(tri[0]), pos(tri[1]), pos(tri[2]));
// Geometrische Flaechennormale (robuster als die pro-Vertex-Normalen).
let n = normalize(cross(sub(b, a), sub(c, a)));
if n == [0.0, 0.0, 0.0] {
continue; // entartetes Dreieck ueberspringen
}
for (u, v) in [(a, b), (b, c), (c, a)] {
let (ku, kv) = (key_of(u), key_of(v));
if ku == kv {
continue; // Null-Laengen-Kante
}
// Kanonische Reihenfolge, damit u->v und v->u denselben Schluessel haben.
let (key, p0, p1) = if ku <= kv {
((ku, kv), u, v)
} else {
((kv, ku), v, u)
};
edges
.entry(key)
.or_insert_with(|| EdgeRec {
p0,
p1,
normals: Vec::new(),
})
.normals
.push(n);
}
}
let mut verts: Vec<f32> = Vec::new();
let mut push = |p: [f32; 3]| {
verts.push(p[0]);
verts.push(p[1]);
verts.push(p[2]);
verts.push(EDGE_COLOR[0]);
verts.push(EDGE_COLOR[1]);
verts.push(EDGE_COLOR[2]);
};
for rec in edges.values() {
if should_draw(&rec.normals) {
push(rec.p0);
push(rec.p1);
}
}
verts
}
/// Entscheidet, ob eine Kante gezeichnet wird: Randkante (nur ein Dreieck) oder
/// Knickkante (zwei angrenzende Dreiecke stehen ueber dem Crease-Winkel zuein-
/// ander). Koplanare geteilte Kanten (Flaechendiagonalen) werden unterdrueckt.
fn should_draw(normals: &[[f32; 3]]) -> bool {
match normals.len() {
0 => false,
1 => true, // Silhouette/Rand: gehoert nur einem Dreieck
_ => {
// Zeichnen, sobald irgendein Normalen-Paar deutlich abknickt.
for i in 0..normals.len() {
for j in (i + 1)..normals.len() {
if dot(normals[i], normals[j]) < CREASE_COS {
return true;
}
}
}
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Hilfsbau: Mesh aus (pos, dummy-normal, dummy-color) + Indizes.
fn mesh(positions: &[[f32; 3]], indices: &[u32]) -> Mesh {
let mut verts = Vec::new();
for p in positions {
verts.extend_from_slice(&[p[0], p[1], p[2], 0.0, 1.0, 0.0, 0.8, 0.8, 0.8]);
}
Mesh {
verts,
indices: indices.to_vec(),
}
}
fn segment_count(v: &[f32]) -> usize {
v.len() / EDGE_FLOATS_PER_VERTEX / 2
}
#[test]
fn single_triangle_has_three_boundary_edges() {
let m = mesh(
&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
&[0, 1, 2],
);
let e = build_mesh_edges(&m);
assert_eq!(segment_count(&e), 3);
}
#[test]
fn coplanar_quad_drops_shared_diagonal() {
// Ebenes Quad aus 2 Dreiecken in der XZ-Ebene: geteilte Diagonale ist
// koplanar -> weg; es bleiben die 4 Umriss-Kanten.
let m = mesh(
&[
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 0.0, 1.0],
[0.0, 0.0, 1.0],
],
&[0, 1, 2, 0, 2, 3],
);
let e = build_mesh_edges(&m);
assert_eq!(segment_count(&e), 4);
}
#[test]
fn folded_quad_keeps_crease() {
// Zwei Dreiecke im 90°-Knick teilen die Kante (0,0,0)-(1,0,0):
// Normalen stehen senkrecht -> Knickkante bleibt erhalten.
let m = mesh(
&[
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 0.0, 1.0], // erstes Dreieck in XZ (Normale +Y)
[1.0, 1.0, 0.0], // zweites Dreieck kippt nach oben (Normale +Z)
],
&[0, 1, 2, 0, 3, 1],
);
let e = build_mesh_edges(&m);
// geteilte Knickkante + je 2 Randkanten pro Dreieck = 5 Segmente.
assert_eq!(segment_count(&e), 5);
}
#[test]
fn empty_mesh_yields_no_edges() {
let m = Mesh::default();
assert!(build_mesh_edges(&m).is_empty());
}
}
+164 -17
View File
@@ -12,12 +12,66 @@
use bytemuck::{Pod, Zeroable}; use bytemuck::{Pod, Zeroable};
use wgpu::util::DeviceExt; use wgpu::util::DeviceExt;
use crate::edges::{build_mesh_edges, EDGE_FLOATS_PER_VERTEX};
use crate::grid::{build_ground_grid, GRID_FLOATS_PER_VERTEX}; use crate::grid::{build_ground_grid, GRID_FLOATS_PER_VERTEX};
use crate::math::{view_projection, Mat4}; use crate::math::{view_projection, Mat4};
use crate::mesh::{build_scene_mesh, build_walls_mesh}; use crate::mesh::{build_scene_mesh, build_walls_mesh};
use crate::shaders::{GRID_WGSL, MESH_WGSL}; use crate::shaders::{GRID_WGSL, MESH_WGSL};
use crate::types::{Camera, Mesh, MeshInput, SlabInput, WallInput, FLOATS_PER_VERTEX}; use crate::types::{Camera, Mesh, MeshInput, SlabInput, WallInput, FLOATS_PER_VERTEX};
/// Darstellungsart des 3D-Renderers (Oberleiste: shaded/white/textured/
/// wireframe/hidden). Bestimmt, welche Pipelines je Frame gezeichnet werden und
/// welchen `mode.x`-Wert der Mesh-Shader bekommt.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RenderStyle {
/// Beleuchtete Bauteilfarben (Default).
Shaded,
/// Clay-Look: einheitlich helles Material, beleuchtet.
White,
/// Mangels Textur-Pipeline VORERST identisch zu `Shaded` behandelt (kein Fake).
Textured,
/// Nur Modell-Kanten (Feature-Edges), keine gefuellten Flaechen.
Wireframe,
/// Hidden-Line: flach-weisse Flaechen (fuellen den Depth-Buffer) + dunkle
/// Kanten obenauf, tiefengetestet -> nur sichtbare Kanten erscheinen.
Hidden,
}
impl RenderStyle {
/// Parst den API-String; unbekannte Werte fallen auf `Shaded` zurueck.
pub fn from_str_or_shaded(s: &str) -> Self {
match s {
"white" => RenderStyle::White,
"textured" => RenderStyle::Textured,
"wireframe" => RenderStyle::Wireframe,
"hidden" => RenderStyle::Hidden,
_ => RenderStyle::Shaded,
}
}
/// `mode.x`-Wert fuer den Mesh-Shader (siehe shaders.rs): 0 = Material/shaded
/// (auch textured), 1 = weiss/Clay, 2 = flach-weisse Hidden-Line-Flaechen.
fn mesh_mode(self) -> f32 {
match self {
RenderStyle::White => 1.0,
RenderStyle::Hidden => 2.0,
// Shaded, Textured, Wireframe (Flaechen werden bei Wireframe eh nicht
// gezeichnet) -> Material.
_ => 0.0,
}
}
/// Werden in diesem Stil gefuellte Flaechen gezeichnet?
fn draws_faces(self) -> bool {
!matches!(self, RenderStyle::Wireframe)
}
/// Werden in diesem Stil Modell-Kanten gezeichnet?
fn draws_edges(self) -> bool {
matches!(self, RenderStyle::Wireframe | RenderStyle::Hidden)
}
}
/// Tiefenformat des Z-Puffers (32 Bit Float, ueberall verfuegbar). /// Tiefenformat des Z-Puffers (32 Bit Float, ueberall verfuegbar).
pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
@@ -69,8 +123,10 @@ struct MeshBuffers {
index_count: u32, index_count: u32,
} }
/// GPU-seitiger Vertex-Buffer des Bodengitters (LineList, kein Indexpuffer). /// GPU-seitiger Vertex-Buffer einer LineList (kein Indexpuffer). Format
struct GridBuffers { /// [pos vec3, color vec3] — genutzt fuer das Bodengitter UND die Modell-Kanten
/// (identisches Layout/Shader, siehe grid.rs / edges.rs).
struct LineBuffers {
vbo: wgpu::Buffer, vbo: wgpu::Buffer,
vertex_count: u32, vertex_count: u32,
} }
@@ -80,19 +136,32 @@ struct GridBuffers {
/// Ziel-Groesse (neu) angelegt. /// Ziel-Groesse (neu) angelegt.
pub struct Renderer { pub struct Renderer {
pipeline: wgpu::RenderPipeline, pipeline: wgpu::RenderPipeline,
/// Eigene, schlanke Linien-Pipeline (LineList, unlit) fuer das Bodengitter. /// Wie `pipeline`, aber mit positivem Depth-Bias (Flaechen leicht nach hinten
/// geschoben). Nur fuer den Hidden-Line-Stil: so gewinnen die danach an ihrer
/// wahren Tiefe gezeichneten Kanten den Less-Test und liegen sauber obenauf,
/// waehrend verdeckte Kanten weiterhin weggeschnitten werden (kein Z-Fighting).
hidden_face_pipeline: wgpu::RenderPipeline,
/// Eigene, schlanke Linien-Pipeline (LineList, unlit) fuer das Bodengitter
/// UND die Modell-Kanten (wireframe/hidden — identisches Layout/Shader).
/// Teilt sich das `Globals`-Uniform/Bind-Group (nur View-Projektion) mit der /// Teilt sich das `Globals`-Uniform/Bind-Group (nur View-Projektion) mit der
/// Mesh-Pipeline und rendert in denselben Pass/Tiefenpuffer. /// Mesh-Pipeline und rendert in denselben Pass/Tiefenpuffer.
grid_pipeline: wgpu::RenderPipeline, grid_pipeline: wgpu::RenderPipeline,
bind_group: wgpu::BindGroup, bind_group: wgpu::BindGroup,
uniform: wgpu::Buffer, uniform: wgpu::Buffer,
mesh: Option<MeshBuffers>, mesh: Option<MeshBuffers>,
/// Modell-Kanten (Feature-Edges, edges.rs) als LineList. Einmalig bei
/// `set_model`/`upload_*` erzeugt und gecacht (nicht je Frame). Gezeichnet in
/// den Stilen wireframe/hidden.
edges: Option<LineBuffers>,
/// Bodengitter-Vertices (None = nicht erzeugt). Wird nur gezeichnet, wenn /// Bodengitter-Vertices (None = nicht erzeugt). Wird nur gezeichnet, wenn
/// zusaetzlich `grid_visible` gesetzt ist. /// zusaetzlich `grid_visible` gesetzt ist.
grid: Option<GridBuffers>, grid: Option<LineBuffers>,
/// Ob das Bodengitter im naechsten `render` gezeichnet wird. Default false — /// Ob das Bodengitter im naechsten `render` gezeichnet wird. Default false —
/// bestehende Aufrufer ohne `set_ground_grid` bekommen KEIN Gitter. /// bestehende Aufrufer ohne `set_ground_grid` bekommen KEIN Gitter.
grid_visible: bool, grid_visible: bool,
/// Aktuelle Darstellungsart (Default `Shaded`). Einzige Quelle der Wahrheit;
/// `set_white_mode` bildet auf `Shaded`/`White` ab.
style: RenderStyle,
depth: Option<DepthTarget>, depth: Option<DepthTarget>,
msaa: Option<MsaaTarget>, msaa: Option<MsaaTarget>,
/// Farbformat des Render-Ziels (Surface), fuer die MSAA-Zwischentextur. /// Farbformat des Render-Ziels (Surface), fuer die MSAA-Zwischentextur.
@@ -155,13 +224,17 @@ impl Renderer {
attributes: &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3, 2 => Float32x3], attributes: &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3, 2 => Float32x3],
}; };
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { // Mesh-Pipeline-Fabrik: identische Beschreibung fuer die Standard-Flaechen
label: Some("mesh.pipeline"), // (kein Bias) und die Hidden-Line-Flaechen (positiver Depth-Bias, damit die
// spaeter gezeichneten Kanten obenauf liegen). Nur `bias` unterscheidet sich.
let make_mesh_pipeline = |label: &str, bias: wgpu::DepthBiasState| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&pipeline_layout), layout: Some(&pipeline_layout),
vertex: wgpu::VertexState { vertex: wgpu::VertexState {
module: &module, module: &module,
entry_point: Some("vs_main"), entry_point: Some("vs_main"),
buffers: &[vertex_layout], buffers: &[vertex_layout.clone()],
compilation_options: Default::default(), compilation_options: Default::default(),
}, },
fragment: Some(wgpu::FragmentState { fragment: Some(wgpu::FragmentState {
@@ -186,7 +259,7 @@ impl Renderer {
depth_write_enabled: Some(true), depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::Less), depth_compare: Some(wgpu::CompareFunction::Less),
stencil: wgpu::StencilState::default(), stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(), bias,
}), }),
multisample: wgpu::MultisampleState { multisample: wgpu::MultisampleState {
count: SAMPLE_COUNT, count: SAMPLE_COUNT,
@@ -195,7 +268,20 @@ impl Renderer {
}, },
multiview_mask: None, multiview_mask: None,
cache: None, cache: None,
}); })
};
let pipeline = make_mesh_pipeline("mesh.pipeline", wgpu::DepthBiasState::default());
// Hidden-Line-Flaechen leicht nach hinten schieben (Depth-Bias auf
// Dreiecke wirkt zuverlaessig — anders als bei Linien), damit die Kanten
// an ihrer wahren Tiefe den Less-Test gewinnen.
let hidden_face_pipeline = make_mesh_pipeline(
"mesh.hidden.pipeline",
wgpu::DepthBiasState {
constant: 4,
slope_scale: 1.0,
clamp: 0.0,
},
);
// Bodengitter-Pipeline: dieselbe Bind-Group (Globals/View-Projektion) und // Bodengitter-Pipeline: dieselbe Bind-Group (Globals/View-Projektion) und
// dasselbe Depth-/MSAA-/Farbziel wie die Mesh-Pipeline, aber LineList- // dasselbe Depth-/MSAA-/Farbziel wie die Mesh-Pipeline, aber LineList-
@@ -268,12 +354,15 @@ impl Renderer {
Self { Self {
pipeline, pipeline,
hidden_face_pipeline,
grid_pipeline, grid_pipeline,
bind_group, bind_group,
uniform, uniform,
mesh: None, mesh: None,
edges: None,
grid: None, grid: None,
grid_visible: false, grid_visible: false,
style: RenderStyle::Shaded,
depth: None, depth: None,
msaa: None, msaa: None,
color_format, color_format,
@@ -305,12 +394,32 @@ impl Renderer {
self.upload_mesh(device, build_scene_mesh(walls, slabs, meshes)); self.upload_mesh(device, build_scene_mesh(walls, slabs, meshes));
} }
/// Laedt ein fertiges Mesh in die GPU-Puffer (oder loescht es bei leer). /// Laedt ein fertiges Mesh in die GPU-Puffer (oder loescht es bei leer) und
/// erzeugt ZUGLEICH die gecachten Modell-Kanten (Feature-Edges, edges.rs) fuer
/// die Stile wireframe/hidden — einmalig hier, nicht je Frame.
fn upload_mesh(&mut self, device: &wgpu::Device, mesh: Mesh) { fn upload_mesh(&mut self, device: &wgpu::Device, mesh: Mesh) {
if mesh.indices.is_empty() { if mesh.indices.is_empty() {
self.mesh = None; self.mesh = None;
self.edges = None;
return; return;
} }
// Kanten aus der Mesh-Geometrie ableiten und (falls vorhanden) hochladen.
let edge_verts = build_mesh_edges(&mesh);
self.edges = if edge_verts.is_empty() {
None
} else {
let vbo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("edges.vbo"),
contents: bytemuck::cast_slice(&edge_verts),
usage: wgpu::BufferUsages::VERTEX,
});
Some(LineBuffers {
vbo,
vertex_count: (edge_verts.len() / EDGE_FLOATS_PER_VERTEX) as u32,
})
};
let vbo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { let vbo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("mesh.vbo"), label: Some("mesh.vbo"),
contents: bytemuck::cast_slice(&mesh.verts), contents: bytemuck::cast_slice(&mesh.verts),
@@ -342,10 +451,21 @@ impl Renderer {
self.globals.sun_color = [sun[0], sun[1], sun[2], 1.0]; self.globals.sun_color = [sun[0], sun[1], sun[2], 1.0];
} }
/// Darstellungsart umschalten: `true` = weisses Clay-Material (Beleuchtung /// Darstellungsart setzen (shaded/white/textured/wireframe/hidden). Einzige
/// bleibt aktiv, siehe shaders.rs), `false` = normale Bauteil-/Vertexfarbe. /// Quelle der Wahrheit; `render` leitet daraus Pipeline-Wahl und `mode.x` ab.
pub fn set_render_style(&mut self, style: RenderStyle) {
self.style = style;
}
/// Rueckwaerts-kompatibler Schalter: `true` bildet auf `White`, `false` auf
/// `Shaded` ab (kein separater Zustand mehr — vermeidet Widerspruch zum
/// `style`-Feld). Neue Aufrufer nutzen `set_render_style`.
pub fn set_white_mode(&mut self, white: bool) { pub fn set_white_mode(&mut self, white: bool) {
self.globals.mode[0] = if white { 1.0 } else { 0.0 }; self.style = if white {
RenderStyle::White
} else {
RenderStyle::Shaded
};
} }
/// Setzt/aktualisiert das Referenz-Bodengitter (grid.rs). Erzeugt den Linien- /// Setzt/aktualisiert das Referenz-Bodengitter (grid.rs). Erzeugt den Linien-
@@ -372,7 +492,7 @@ impl Renderer {
contents: bytemuck::cast_slice(&verts), contents: bytemuck::cast_slice(&verts),
usage: wgpu::BufferUsages::VERTEX, usage: wgpu::BufferUsages::VERTEX,
}); });
self.grid = Some(GridBuffers { self.grid = Some(LineBuffers {
vbo, vbo,
vertex_count: (verts.len() / GRID_FLOATS_PER_VERTEX) as u32, vertex_count: (verts.len() / GRID_FLOATS_PER_VERTEX) as u32,
}); });
@@ -453,6 +573,9 @@ impl Renderer {
let aspect = if h > 0 { w as f32 / h as f32 } else { 1.0 }; let aspect = if h > 0 { w as f32 / h as f32 } else { 1.0 };
let vp: Mat4 = view_projection(camera, aspect); let vp: Mat4 = view_projection(camera, aspect);
self.globals.view_proj = vp; self.globals.view_proj = vp;
// `mode.x` fuer den Mesh-Shader aus dem aktuellen Stil ableiten (Material/
// Clay/Hidden-Flaechen). Wireframe zeichnet keine Flaechen -> Wert egal.
self.globals.mode[0] = self.style.mesh_mode();
queue.write_buffer(&self.uniform, 0, bytemuck::bytes_of(&self.globals)); queue.write_buffer(&self.uniform, 0, bytemuck::bytes_of(&self.globals));
self.ensure_depth(device, w, h); self.ensure_depth(device, w, h);
@@ -493,16 +616,40 @@ impl Renderer {
multiview_mask: None, multiview_mask: None,
}); });
// 1) Gefuellte Flaechen — in allen Stilen ausser "wireframe". Im
// Hidden-Line-Stil die leicht nach hinten gebiaste Pipeline, damit
// die folgenden Kanten sauber obenauf liegen.
if self.style.draws_faces() {
if let Some(m) = &self.mesh { if let Some(m) = &self.mesh {
pass.set_pipeline(&self.pipeline); let face_pipeline = if self.style == RenderStyle::Hidden {
&self.hidden_face_pipeline
} else {
&self.pipeline
};
pass.set_pipeline(face_pipeline);
pass.set_bind_group(0, &self.bind_group, &[]); pass.set_bind_group(0, &self.bind_group, &[]);
pass.set_vertex_buffer(0, m.vbo.slice(..)); pass.set_vertex_buffer(0, m.vbo.slice(..));
pass.set_index_buffer(m.ibo.slice(..), wgpu::IndexFormat::Uint32); pass.set_index_buffer(m.ibo.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..m.index_count, 0, 0..1); pass.draw_indexed(0..m.index_count, 0, 0..1);
} }
}
// Bodengitter NACH dem Modell zeichnen (gemeinsamer Tiefenpuffer -> // 2) Modell-Kanten (wireframe/hidden) — LineList-Pipeline (wie Grid).
// das Modell verdeckt das Gitter korrekt). Nur wenn sichtbar + vorhanden. // Im Hidden-Stil obenauf, tiefengetestet gegen die (gebiasten)
// Flaechen -> verdeckte Kanten fallen weg. Im Wireframe ohne
// Flaechen -> alle Kanten sichtbar.
if self.style.draws_edges() {
if let Some(e) = &self.edges {
pass.set_pipeline(&self.grid_pipeline);
pass.set_bind_group(0, &self.bind_group, &[]);
pass.set_vertex_buffer(0, e.vbo.slice(..));
pass.draw(0..e.vertex_count, 0..1);
}
}
// 3) Bodengitter NACH dem Modell zeichnen (gemeinsamer Tiefenpuffer ->
// das Modell verdeckt das Gitter korrekt). Unabhaengig vom Stil, nur
// wenn sichtbar + vorhanden.
if self.grid_visible { if self.grid_visible {
if let Some(g) = &self.grid { if let Some(g) = &self.grid {
pass.set_pipeline(&self.grid_pipeline); pass.set_pipeline(&self.grid_pipeline);
+1
View File
@@ -14,6 +14,7 @@
// Standard-Build (`cargo test`/`cargo build` ohne Features) enthaelt nur die // Standard-Build (`cargo test`/`cargo build` ohne Features) enthaelt nur die
// GPU-freien Schichten und ist damit unabhaengig von einer Display-Session. // GPU-freien Schichten und ist damit unabhaengig von einer Display-Session.
pub mod edges;
pub mod grid; pub mod grid;
pub mod math; pub mod math;
pub mod mesh; pub mod mesh;
+36 -6
View File
@@ -16,9 +16,16 @@
// sky_color : vec4<f32> Himmels-Ambient (rgb; von oben) // sky_color : vec4<f32> Himmels-Ambient (rgb; von oben)
// ground_color : vec4<f32> Boden-Ambient (rgb; von unten) // ground_color : vec4<f32> Boden-Ambient (rgb; von unten)
// sun_color : vec4<f32> Farbe/Staerke des Directional-Lights (rgb) // sun_color : vec4<f32> Farbe/Staerke des Directional-Lights (rgb)
// mode : vec4<f32> x = Darstellungsart (0 = shaded/Material, 1 = weiss/Clay) // mode : vec4<f32> x = Darstellungsart (0 = shaded/Material,
// 1 = weiss/Clay, 2 = hidden-Line-Flaechen/flach-weiss)
// //
// Vertex-Attribute: position (world), normal (world), color (Albedo). // Vertex-Attribute: position (world), normal (world), color (Albedo).
//
// FILL-LIGHT: Zusaetzlich zum Haupt-Directional (Sonne) wirkt ein schwaches
// GEGEN-Fuelllicht aus der entgegengesetzten Azimut-Richtung (leicht von oben),
// abgeleitet aus der Sonnenrichtung — es hebt die vom Hauptlicht abgewandten
// Flaechen an, damit sie nicht in reinem Schwarz absaufen. Dezent (≈ 0.28 der
// Sonne, leicht kuehl), physikalisch als Himmels-/Bounce-Fuellung plausibel.
/// Der einzige Shader (Vertex + Fragment). Hemisphaerisches Ambient (Himmel/Boden) /// Der einzige Shader (Vertex + Fragment). Hemisphaerisches Ambient (Himmel/Boden)
/// + Lambert-Directional — GPU-Aequivalent eines Architektur-Massenmodell-Lichts. /// + Lambert-Directional — GPU-Aequivalent eines Architektur-Massenmodell-Lichts.
@@ -58,6 +65,7 @@ fn vs_main(in : VsIn) -> VsOut {
fn fs_main(in : VsOut) -> @location(0) vec4<f32> { fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
let n = normalize(in.world_normal); let n = normalize(in.world_normal);
let l = normalize(globals.light_dir.xyz); let l = normalize(globals.light_dir.xyz);
let style = globals.mode.x;
// Hemisphaerisches Ambient: Mischfaktor aus der Vertikal-Komponente der // Hemisphaerisches Ambient: Mischfaktor aus der Vertikal-Komponente der
// Normalen (n.y = +1 -> voll Himmel, n.y = -1 -> voll Boden). // Normalen (n.y = +1 -> voll Himmel, n.y = -1 -> voll Boden).
@@ -67,18 +75,40 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
// Gerichteter Lambert-Anteil (Sonne); Rueckseiten (n.l < 0) tragen nichts bei. // Gerichteter Lambert-Anteil (Sonne); Rueckseiten (n.l < 0) tragen nichts bei.
let diffuse = max(dot(n, l), 0.0) * globals.sun_color.rgb; let diffuse = max(dot(n, l), 0.0) * globals.sun_color.rgb;
// Weiss-Modus (Clay-Look): Material-/Vertexfarbe durch ein einheitliches // Schwaches Gegen-Fuelllicht: aus entgegengesetztem Azimut, leicht von oben,
// Hellgrau ersetzen, Beleuchtung (Ambient + Directional) bleibt aktiv — so // aus der Sonnenrichtung abgeleitet — hebt abgewandte Flaechen an (kein
// liest sich die Volumetrie ohne Bauteil-Farben ab (Vectorworks „white"). // reines Schwarz). Dezent (0.28) und leicht kuehl getoent.
let albedo = mix(in.color, vec3<f32>(0.82, 0.82, 0.80), globals.mode.x); let fill_l = normalize(vec3<f32>(-l.x, abs(l.y) * 0.35 + 0.15, -l.z));
let fill = max(dot(n, fill_l), 0.0) * globals.sun_color.rgb
* vec3<f32>(0.24, 0.27, 0.30);
var shaded = albedo * (ambient + diffuse); // Weiss-Modus (Clay, style==1) bzw. Hidden-Flaechen (style==2): Material-/
// Vertexfarbe durch ein einheitliches Hell ersetzen. Clay = warmes Hellgrau,
// Hidden = nahezu Weiss (damit die dunklen Kanten obenauf klar lesbar sind).
let clay = vec3<f32>(0.82, 0.82, 0.80);
let paper = vec3<f32>(0.97, 0.97, 0.97);
var albedo = in.color;
if (style > 1.5) {
albedo = paper;
} else if (style > 0.5) {
albedo = clay;
}
var shaded = albedo * (ambient + diffuse + fill);
// Sanfte Kantenbetonung: senkrechte Flaechen (n.y nahe 0) minimal abdunkeln, // Sanfte Kantenbetonung: senkrechte Flaechen (n.y nahe 0) minimal abdunkeln,
// damit sich Waende vom hellen Deckel/Boden abheben (ohne Postprocessing). // damit sich Waende vom hellen Deckel/Boden abheben (ohne Postprocessing).
let edge = 0.90 + 0.10 * abs(n.y); let edge = 0.90 + 0.10 * abs(n.y);
shaded = shaded * edge; shaded = shaded * edge;
// Hidden-Line-Flaechen: sehr flach halten (fast unlit, nahezu weiss), damit
// die dunklen Modell-Kanten die Form tragen und die Flaechen nur den Depth-
// Buffer fuellen (verdeckte Kanten wegschneiden). Leichte Hemi-Toenung bleibt,
// damit die Silhouette auch ohne Kante nicht voellig verschwindet.
if (style > 1.5) {
shaded = albedo * (0.88 + 0.12 * hemi_t);
}
return vec4<f32>(shaded, 1.0); return vec4<f32>(shaded, 1.0);
} }
"#; "#;
+21 -1
View File
@@ -320,9 +320,29 @@ impl WebModelRenderer {
Ok(()) Ok(())
} }
/// Darstellungsart der Oberleiste setzen. Gueltige Werte:
/// - `"shaded"` — beleuchtete Bauteilfarben (Default).
/// - `"white"` — Clay-Look: einheitlich helles Material, beleuchtet
/// (identisch zum bisherigen `set_render_mode_white(true)`).
/// - `"textured"` — mangels Textur-Pipeline VORERST identisch zu `"shaded"`
/// behandelt (KEIN Fake-Stub); Platzhalter fuer eine spaetere Textur-Schicht.
/// - `"wireframe"`— nur Modell-Kanten (Feature-Edges), keine Flaechen.
/// - `"hidden"` — Hidden-Line: flach-weisse Flaechen fuellen den Depth-
/// Buffer, dunkle Kanten liegen tiefengetestet obenauf (nur sichtbare).
/// Unbekannte Werte fallen auf `"shaded"` zurueck. Wirkt erst beim naechsten
/// `render`. Loest `set_render_mode_white` ab (das bleibt kompatibel).
pub fn set_render_style(&mut self, style: &str) -> Result<(), JsValue> {
self.renderer
.set_render_style(crate::gpu::RenderStyle::from_str_or_shaded(style));
Ok(())
}
/// Darstellungsart umschalten: `true` = weisses Clay-Material (beleuchtet, /// Darstellungsart umschalten: `true` = weisses Clay-Material (beleuchtet,
/// ohne Bauteil-Farben — Vectorworks „white"), `false` = normale Material- /// ohne Bauteil-Farben — Vectorworks „white"), `false` = normale Material-
/// farbe (shaded, Default). Wirkt erst beim naechsten `render`. /// farbe (shaded, Default). Wirkt erst beim naechsten `render`. RUECKWAERTS-
/// KOMPATIBEL — bildet intern auf denselben `render_style`-Zustand ab wie
/// `set_render_style` (true→„white", false→„shaded"), sodass es keine zwei
/// widersprechenden Zustaende gibt.
pub fn set_render_mode_white(&mut self, white: bool) { pub fn set_render_mode_white(&mut self, white: bool) {
self.renderer.set_white_mode(white); self.renderer.set_white_mode(white);
} }
+15 -15
View File
@@ -6,12 +6,13 @@
// INTEGRATIONS-SPIKE (Umfang bewusst schmal): Massenmodell (Wände mit Öffnungs- // INTEGRATIONS-SPIKE (Umfang bewusst schmal): Massenmodell (Wände mit Öffnungs-
// Teilquadern + Deckenplatten, alle Geschosse gestapelt) + Orbit-Navigation + // Teilquadern + Deckenplatten, alle Geschosse gestapelt) + Orbit-Navigation +
// Kamera-Presets (`view3d`, von der Oberleiste getrieben — s. u.) + Darstellungs- // Kamera-Presets (`view3d`, von der Oberleiste getrieben — s. u.) + Darstellungs-
// art (shaded/white, `renderMode`, ebenfalls Oberleiste) + ein-/ausblendbares // art (volle `renderMode`-Union der Oberleiste — shaded/white/textured/
// Boden-Referenzraster (`gridVisible`/`groundElevation`, Overlay-Umschalter im // wireframe/hidden — via `set_render_style` in Rust; "textured" faellt dort
// Viewport selbst). Auswahl/Raycast, Griffe/Zeichnen sowie die übrigen // vorerst auf "shaded" zurueck) + ein-/ausblendbares Boden-Referenzraster
// Darstellungsarten der three.js-Sicht (texturiert/Drahtgitter/Kanten, HLR) // (`gridVisible`/`groundElevation`, Overlay-Umschalter im Viewport selbst).
// folgen später — bis dahin gilt: RECHTS unterdrückt weiterhin nur das // Auswahl/Raycast sowie Griffe/Zeichnen der three.js-Sicht folgen später —
// Browser-Menü (kein Kontextmenü im Engine-Pfad). // bis dahin gilt: RECHTS unterdrückt weiterhin nur das Browser-Menü (kein
// Kontextmenü im Engine-Pfad).
// //
// KAMERA-PRESETS: `view3d`/`renderMode` kommen als Props von der Oberleiste // KAMERA-PRESETS: `view3d`/`renderMode` kommen als Props von der Oberleiste
// (TopBar → App.tsx → Viewport3D-Umschalter, dieselbe Quelle wie die three.js- // (TopBar → App.tsx → Viewport3D-Umschalter, dieselbe Quelle wie die three.js-
@@ -170,9 +171,8 @@ export function Wasm3DViewport({
project: Project; project: Project;
/** Kanonischer Blickwinkel (Oberleiste) — s. Datei-Kommentar. */ /** Kanonischer Blickwinkel (Oberleiste) — s. Datei-Kommentar. */
view3d?: View3d; view3d?: View3d;
/** Darstellungsart (Oberleiste): nur `shaded`/`white` wirken hier (die /** Darstellungsart (Oberleiste) alle fünf Werte wirken hier, s.
* übrigen Modi der three.js-Sicht sind im Engine-Pfad noch nicht gebaut, * Datei-Kommentar. */
* s. Datei-Kommentar) alles ausser `white` fällt auf shaded zurück. */
renderMode?: RenderMode; renderMode?: RenderMode;
/** Boden-Referenzraster sichtbar? Umgeschaltet über den Overlay-Button unten /** Boden-Referenzraster sichtbar? Umgeschaltet über den Overlay-Button unten
* rechts im Viewport (`onToggleGrid`) Single Source of Truth ist der * rechts im Viewport (`onToggleGrid`) Single Source of Truth ist der
@@ -188,7 +188,7 @@ export function Wasm3DViewport({
const { const {
render, render,
updateModel, updateModel,
setWhiteMode, setRenderStyle,
applyViewPreset, applyViewPreset,
setGroundGrid, setGroundGrid,
ready, ready,
@@ -229,14 +229,14 @@ export function Wasm3DViewport({
redrawRef.current(); redrawRef.current();
}, [ready, view3d, applyViewPreset, render]); }, [ready, view3d, applyViewPreset, render]);
// Darstellungsart (shaded/weiss) an den Renderer weiterreichen, sobald sie // Darstellungsart (volle RenderMode-Union) an den Renderer weiterreichen,
// wechselt oder der Renderer bereit wird — dann mit der aktuell aktiven // sobald sie wechselt oder der Renderer bereit wird — dann mit der aktuell
// Kamera (Praeset ODER Orbit) neu zeichnen. // aktiven Kamera (Praeset ODER Orbit) neu zeichnen.
useEffect(() => { useEffect(() => {
if (!ready) return; if (!ready) return;
setWhiteMode(renderMode === "white"); setRenderStyle(renderMode);
redrawRef.current(); redrawRef.current();
}, [ready, renderMode, setWhiteMode]); }, [ready, renderMode, setRenderStyle]);
// Boden-Referenzraster: bei Bereitschaft oder Wechsel von Sichtbarkeit/ // Boden-Referenzraster: bei Bereitschaft oder Wechsel von Sichtbarkeit/
// Geschoss-Hoehe neu setzen + zeichnen. Fixe Ausdehnung/Abstand (40 m / // Geschoss-Hoehe neu setzen + zeichnen. Fixe Ausdehnung/Abstand (40 m /
+13 -8
View File
@@ -37,7 +37,10 @@ export interface Camera3d {
/** Minimal-Typ des WASM-Exports (das echte .d.ts entsteht erst mit `build:engine3d`). */ /** Minimal-Typ des WASM-Exports (das echte .d.ts entsteht erst mit `build:engine3d`). */
interface WebModelRendererLike { interface WebModelRendererLike {
set_model(json: string): void; set_model(json: string): void;
set_render_mode_white(white: boolean): void; /** Darstellungsart setzen `style` "shaded"|"white"|"wireframe"|"hidden"|
* "textured" (deckt die volle `RenderMode`-Union der Oberleiste ab;
* "textured" faellt engine-seitig vorerst auf "shaded" zurueck). */
set_render_style(style: string): void;
set_view_preset(preset: string): void; set_view_preset(preset: string): void;
/** Boden-Referenzraster in der Welt-XZ-Ebene ein-/ausblenden (Rust-seitig, /** Boden-Referenzraster in der Welt-XZ-Ebene ein-/ausblenden (Rust-seitig,
* `web::set_ground_grid`). `visible=false` zeichnet kein Raster; sonst ein * `web::set_ground_grid`). `visible=false` zeichnet kein Raster; sonst ein
@@ -142,17 +145,19 @@ export function useWasm3dRenderer(
}, []); }, []);
/** /**
* Darstellungsart umschalten: `true` = weisses Clay-Material (beleuchtet, * Darstellungsart umschalten reicht die volle `RenderMode`-Union der
* ohne Bauteil-Farben), `false` = normale Material-/Vertexfarbe (shaded, * Oberleiste ("shaded"/"white"/"textured"/"wireframe"/"hidden") an Rust
* Default). Wirkt erst beim nächsten `render`. * durch (`web::set_render_style`); die Engine entscheidet je Stil, wie
* gezeichnet wird ("textured" faellt dort vorerst auf "shaded" zurueck).
* Wirkt erst beim nächsten `render`.
*/ */
const setWhiteMode = useCallback((white: boolean) => { const setRenderStyle = useCallback((style: string) => {
const r = rendererRef.current; const r = rendererRef.current;
if (!r) return; if (!r) return;
try { try {
r.set_render_mode_white(white); r.set_render_style(style);
} catch (e) { } catch (e) {
console.warn("render3d set_render_mode_white fehlgeschlagen:", e); console.warn("render3d set_render_style fehlgeschlagen:", e);
} }
}, []); }, []);
@@ -242,7 +247,7 @@ export function useWasm3dRenderer(
return { return {
render, render,
updateModel, updateModel,
setWhiteMode, setRenderStyle,
applyViewPreset, applyViewPreset,
setGroundGrid, setGroundGrid,
ready, ready,