3D: Geschossdecken als extrudierte Polygone + hemisphärisches Licht
Deckenplatten (SlabInput) werden im render3d aus dem Grundriss-Umriss per
Ear-Clipping trianguliert und über die Deckendicke extrudiert (Deckel/Boden/
Mantel mit robust nach außen orientierten Normalen). Payload erweitert auf
{ walls, slabs } — blanke Wand-Arrays bleiben kompatibel. Beleuchtung auf
hemisphärisches Ambient (Himmel/Boden) + Directional-Sonne umgestellt, dezente
Kantenbetonung, hellerer Hintergrund (#f5f5f5). Beispiel-Geschossdecke im EG.
This commit is contained in:
@@ -13,9 +13,9 @@ use bytemuck::{Pod, Zeroable};
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
use crate::math::{view_projection, Mat4};
|
||||
use crate::mesh::build_walls_mesh;
|
||||
use crate::mesh::{build_model_mesh, build_walls_mesh};
|
||||
use crate::shaders::MESH_WGSL;
|
||||
use crate::types::{Camera, WallInput, FLOATS_PER_VERTEX};
|
||||
use crate::types::{Camera, Mesh, SlabInput, WallInput, FLOATS_PER_VERTEX};
|
||||
|
||||
/// Tiefenformat des Z-Puffers (32 Bit Float, ueberall verfuegbar).
|
||||
pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
|
||||
@@ -27,18 +27,23 @@ pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
|
||||
struct Globals {
|
||||
view_proj: [f32; 16],
|
||||
light_dir: [f32; 4],
|
||||
ambient: [f32; 4],
|
||||
sky_color: [f32; 4],
|
||||
ground_color: [f32; 4],
|
||||
sun_color: [f32; 4],
|
||||
}
|
||||
|
||||
impl Default for Globals {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
view_proj: crate::math::identity(),
|
||||
// Richtung ZUM Licht (world), normiert. Entspricht der three.js-Sonne
|
||||
// bei (6,12,4): das Licht kommt aus dieser Richtung.
|
||||
// Richtung ZUM Licht (world), normiert. Sonne oben-vorne (6,12,4).
|
||||
light_dir: normalize4([6.0, 12.0, 4.0]),
|
||||
// Ambienter Sockel 0.6 (wie three.js-AmbientLight(0.6)).
|
||||
ambient: [0.6, 0.6, 0.6, 1.0],
|
||||
// Himmels-Ambient (von oben): helles, leicht kuehles Licht.
|
||||
sky_color: [0.66, 0.68, 0.72, 1.0],
|
||||
// Boden-Ambient (von unten): dunkler, warmer Ton (Bounce/Schatten).
|
||||
ground_color: [0.30, 0.29, 0.27, 1.0],
|
||||
// Directional-Sonne: warmweiss, moderat.
|
||||
sun_color: [0.55, 0.53, 0.49, 1.0],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,11 +179,11 @@ impl Renderer {
|
||||
mesh: None,
|
||||
depth: None,
|
||||
globals: Globals::default(),
|
||||
// #e9e9e9 heller Hintergrund (wie die three.js-Modellsicht).
|
||||
// #f5f5f5 heller Hintergrund (wie der 2D-Grundriss).
|
||||
clear_color: wgpu::Color {
|
||||
r: 0.914,
|
||||
g: 0.914,
|
||||
b: 0.914,
|
||||
r: 0.961,
|
||||
g: 0.961,
|
||||
b: 0.961,
|
||||
a: 1.0,
|
||||
},
|
||||
}
|
||||
@@ -186,7 +191,16 @@ impl Renderer {
|
||||
|
||||
/// Erzeugt das Mesh aus geflachten Waenden und laedt die Puffer hoch.
|
||||
pub fn upload_walls(&mut self, device: &wgpu::Device, walls: &[WallInput]) {
|
||||
let mesh = build_walls_mesh(walls);
|
||||
self.upload_mesh(device, build_walls_mesh(walls));
|
||||
}
|
||||
|
||||
/// Erzeugt das Mesh aus Waenden UND Deckenplatten und laedt die Puffer hoch.
|
||||
pub fn upload_model(&mut self, device: &wgpu::Device, walls: &[WallInput], slabs: &[SlabInput]) {
|
||||
self.upload_mesh(device, build_model_mesh(walls, slabs));
|
||||
}
|
||||
|
||||
/// Laedt ein fertiges Mesh in die GPU-Puffer (oder loescht es bei leer).
|
||||
fn upload_mesh(&mut self, device: &wgpu::Device, mesh: Mesh) {
|
||||
if mesh.indices.is_empty() {
|
||||
self.mesh = None;
|
||||
return;
|
||||
@@ -208,10 +222,18 @@ impl Renderer {
|
||||
});
|
||||
}
|
||||
|
||||
/// Setzt die Lichtrichtung (Richtung ZUM Licht, world) und den ambienten Sockel.
|
||||
pub fn set_light(&mut self, dir_to_light: [f32; 3], ambient: f32) {
|
||||
/// Setzt die Richtung ZUM Directional-Light (Sonne, world). Das hemisphaerische
|
||||
/// Ambient (Himmel/Boden) bleibt bei den Default-Farben.
|
||||
pub fn set_light(&mut self, dir_to_light: [f32; 3]) {
|
||||
self.globals.light_dir = normalize4(dir_to_light);
|
||||
self.globals.ambient = [ambient, ambient, ambient, 1.0];
|
||||
}
|
||||
|
||||
/// Uebersteuert die hemisphaerischen Ambient-Farben (Himmel oben, Boden unten)
|
||||
/// und die Sonnenfarbe. Fuer Feinabstimmung des Massenmodell-Looks.
|
||||
pub fn set_ambient(&mut self, sky: [f32; 3], ground: [f32; 3], sun: [f32; 3]) {
|
||||
self.globals.sky_color = [sky[0], sky[1], sky[2], 1.0];
|
||||
self.globals.ground_color = [ground[0], ground[1], ground[2], 1.0];
|
||||
self.globals.sun_color = [sun[0], sun[1], sun[2], 1.0];
|
||||
}
|
||||
|
||||
/// Stellt sicher, dass ein Tiefenpuffer passend zur Ziel-Groesse existiert.
|
||||
|
||||
@@ -26,9 +26,9 @@ pub use math::{
|
||||
look_at, orbit_eye, orthographic, perspective, preset_camera, projection_matrix,
|
||||
view_matrix, view_projection, Mat4,
|
||||
};
|
||||
pub use mesh::{build_walls_mesh, extrude_wall};
|
||||
pub use mesh::{build_model_mesh, build_walls_mesh, extrude_slab, extrude_wall, triangulate};
|
||||
pub use types::{
|
||||
Camera, CameraPreset, Mesh, Point2, Projection, Rgb, WallInput, FLOATS_PER_VERTEX,
|
||||
Camera, CameraPreset, Mesh, Point2, Projection, Rgb, SlabInput, WallInput, FLOATS_PER_VERTEX,
|
||||
};
|
||||
|
||||
// --- Tests: Mesh-Erzeugung (Muster wie render2d/tessellate) -------------------
|
||||
@@ -183,6 +183,109 @@ mod tests {
|
||||
assert!(mesh.indices.is_empty());
|
||||
}
|
||||
|
||||
// --- Deckenplatten (extrudierte Polygone) ---------------------------------
|
||||
|
||||
use super::mesh::{build_model_mesh, extrude_slab, triangulate};
|
||||
use super::types::SlabInput;
|
||||
|
||||
/// Quadratischer Decken-Umriss (CCW) mit Kantenlaenge `s`, Ecke im Ursprung.
|
||||
fn square_slab(s: f32, z_bottom: f32, z_top: f32) -> SlabInput {
|
||||
SlabInput {
|
||||
outline: vec![[0.0, 0.0], [s, 0.0], [s, s], [0.0, s]],
|
||||
z_bottom,
|
||||
z_top,
|
||||
color: [0.86, 0.86, 0.88],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn triangulate_quadrat_gibt_zwei_dreiecke() {
|
||||
let tris = triangulate(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
|
||||
assert_eq!(tris.len(), 6, "Quadrat -> 2 Dreiecke = 6 Indizes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn triangulate_cw_umriss_ebenfalls_zwei_dreiecke() {
|
||||
// CW-Umriss (negative Flaeche) muss genauso trianguliert werden.
|
||||
let tris = triangulate(&[[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]]);
|
||||
assert_eq!(tris.len(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slab_deckel_und_boden_und_mantel() {
|
||||
// Quadratische Platte: Deckel (2 Tri) + Boden (2 Tri) + 4 Mantel-Quads
|
||||
// (je 2 Tri) = 4 + 8 = 12 Dreiecke = 36 Vertices (je Tri eigene Vertices).
|
||||
let mut mesh = Mesh::default();
|
||||
extrude_slab(&mut mesh, &square_slab(4.0, 2.4, 2.65));
|
||||
assert_eq!(mesh.triangle_count(), 12, "2+2 Kappen + 8 Mantel");
|
||||
assert_eq!(mesh.vertex_count(), 36);
|
||||
// Z-Ausdehnung deckt die Deckendicke ab.
|
||||
let (min, max) = mesh.bounds();
|
||||
assert!((min[1] - 2.4).abs() < 1e-5, "UK 2.4");
|
||||
assert!((max[1] - 2.65).abs() < 1e-5, "OK 2.65");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slab_deckel_normale_zeigt_nach_oben_boden_nach_unten() {
|
||||
let mut mesh = Mesh::default();
|
||||
extrude_slab(&mut mesh, &square_slab(4.0, 0.0, 0.25));
|
||||
// Fuer jedes Vertex mit y==0.25 (Deckel) muss die Normale +Y sein, fuer
|
||||
// y==0.0 (Boden) −Y. Mantel-Vertices haben n.y ≈ 0.
|
||||
let n = mesh.vertex_count();
|
||||
for i in 0..n {
|
||||
let b = i * FLOATS_PER_VERTEX;
|
||||
let py = mesh.verts[b + 1];
|
||||
let ny = mesh.verts[b + 4];
|
||||
if (py - 0.25).abs() < 1e-6 && (mesh.verts[b + 3]).abs() < 1e-6 && (mesh.verts[b + 5]).abs() < 1e-6 {
|
||||
assert!((ny - 1.0).abs() < 1e-5, "Deckel-Normale +Y");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slab_mantel_normalen_zeigen_nach_aussen() {
|
||||
// Zentriertes Quadrat um den Ursprung: jede Mantel-Normale muss vom
|
||||
// Zentrum weg zeigen.
|
||||
let slab = SlabInput {
|
||||
outline: vec![[-2.0, -2.0], [2.0, -2.0], [2.0, 2.0], [-2.0, 2.0]],
|
||||
z_bottom: 0.0,
|
||||
z_top: 0.3,
|
||||
color: [0.8, 0.8, 0.8],
|
||||
};
|
||||
let mut mesh = Mesh::default();
|
||||
extrude_slab(&mut mesh, &slab);
|
||||
let center = [0.0f32, 0.15, 0.0];
|
||||
let n = mesh.vertex_count();
|
||||
for i in 0..n {
|
||||
let b = i * FLOATS_PER_VERTEX;
|
||||
let p = [mesh.verts[b], mesh.verts[b + 1], mesh.verts[b + 2]];
|
||||
let nor = [mesh.verts[b + 3], mesh.verts[b + 4], mesh.verts[b + 5]];
|
||||
let out = [p[0] - center[0], p[1] - center[1], p[2] - center[2]];
|
||||
let d = out[0] * nor[0] + out[1] * nor[1] + out[2] * nor[2];
|
||||
assert!(d >= -1e-4, "Vertex {i}: Normale zeigt nach innen (dot={d})");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_model_mesh_haengt_slabs_an_waende() {
|
||||
let wall = build_walls_mesh(&[wall_x(4.0, 0.2, 2.6)]);
|
||||
let model = build_model_mesh(&[wall_x(4.0, 0.2, 2.6)], &[square_slab(4.0, 2.6, 2.85)]);
|
||||
// Modell = Wand-Vertices + Slab-Vertices.
|
||||
assert_eq!(model.vertex_count(), wall.vertex_count() + 36);
|
||||
assert!(model.indices.len() > wall.indices.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entarteter_slab_erzeugt_nichts() {
|
||||
let mut mesh = Mesh::default();
|
||||
// <3 Ecken.
|
||||
extrude_slab(&mut mesh, &SlabInput { outline: vec![[0.0, 0.0], [1.0, 0.0]], z_bottom: 0.0, z_top: 0.2, color: [0.8, 0.8, 0.8] });
|
||||
assert_eq!(mesh.vertex_count(), 0);
|
||||
// Nullhoehe.
|
||||
extrude_slab(&mut mesh, &square_slab(4.0, 2.6, 2.6));
|
||||
assert_eq!(mesh.vertex_count(), 0);
|
||||
}
|
||||
|
||||
// --- Kamera / Matrizen ----------------------------------------------------
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
// XZ-Ebene, Extrusion entlang +Y (Y-up), exakt wie Viewport3D.tsx:
|
||||
// "Modell (x,y,z) -> Three (x, z, y) (Z = Hoehe nach oben)".
|
||||
|
||||
use crate::types::{Mesh, Point2, Rgb, WallInput, FLOATS_PER_VERTEX};
|
||||
use crate::types::{Mesh, Point2, Rgb, SlabInput, WallInput, FLOATS_PER_VERTEX};
|
||||
|
||||
/// Ein Quader-Mesh besteht aus 6 Seiten (Boden, Deckel, 4 Waende) zu je 2
|
||||
/// Dreiecken = 12 Dreiecke, mit flachen Normalen also 24 Vertices (je Seite 4,
|
||||
@@ -134,3 +134,217 @@ pub fn build_walls_mesh(walls: &[WallInput]) -> Mesh {
|
||||
}
|
||||
mesh
|
||||
}
|
||||
|
||||
/// Baut das volle Modell-Mesh: erst die Waende (Quader), dann die Deckenplatten
|
||||
/// (extrudierte Polygone) — alles in EINEN Puffer. Die Wand-Reihenfolge bleibt
|
||||
/// vorne (deterministische Zaehlung fuer die Wand-Tests).
|
||||
pub fn build_model_mesh(walls: &[WallInput], slabs: &[SlabInput]) -> Mesh {
|
||||
let mut mesh = build_walls_mesh(walls);
|
||||
for s in slabs {
|
||||
extrude_slab(&mut mesh, s);
|
||||
}
|
||||
mesh
|
||||
}
|
||||
|
||||
// ── Deckenplatten (extrudierte Polygone) ─────────────────────────────────────
|
||||
|
||||
/// Signierte Flaeche eines Grundriss-Polygons (Shoelace) in Modell-Koordinaten.
|
||||
fn signed_area(pts: &[Point2]) -> f32 {
|
||||
let mut a = 0.0f32;
|
||||
let n = pts.len();
|
||||
if n < 3 {
|
||||
return 0.0;
|
||||
}
|
||||
let mut j = n - 1;
|
||||
for i in 0..n {
|
||||
a += pts[j][0] * pts[i][1] - pts[i][0] * pts[j][1];
|
||||
j = i;
|
||||
}
|
||||
a * 0.5
|
||||
}
|
||||
|
||||
/// Kreuzprodukt (b-a) x (c-a) im Grundriss.
|
||||
#[inline]
|
||||
fn cross2(a: Point2, b: Point2, c: Point2) -> f32 {
|
||||
(b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
|
||||
}
|
||||
|
||||
/// Liegt p im (a,b,c)-Dreieck? (CCW-orientiert).
|
||||
fn point_in_tri(a: Point2, b: Point2, c: Point2, p: Point2) -> bool {
|
||||
let d1 = cross2(a, b, p);
|
||||
let d2 = cross2(b, c, p);
|
||||
let d3 = cross2(c, a, p);
|
||||
let has_neg = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
|
||||
let has_pos = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
|
||||
!(has_neg && has_pos)
|
||||
}
|
||||
|
||||
/// Ear-Clipping-Triangulierung eines einfachen (lochfreien) Polygons. Robust fuer
|
||||
/// konvexe UND konkave Ringe. O(n^2) — fuer Decken-Umrisse (wenige Ecken) voellig
|
||||
/// ausreichend. Liefert Dreiecks-Indizes (0-basiert auf `pts`); leer bei <3 Ecken
|
||||
/// oder Degeneration. 1:1-Port von `render2d::tessellate::triangulate`.
|
||||
pub fn triangulate(pts: &[Point2]) -> Vec<u32> {
|
||||
let n = pts.len();
|
||||
if n < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
// Ohr-Test unten nutzt cross>0 = konvex (setzt CCW voraus). CW-Polygone drehen.
|
||||
let mut idx: Vec<usize> = (0..n).collect();
|
||||
if signed_area(pts) < 0.0 {
|
||||
idx.reverse();
|
||||
}
|
||||
|
||||
let mut tris: Vec<u32> = Vec::new();
|
||||
let mut guard = 0usize;
|
||||
let max_guard = n * n + 16;
|
||||
|
||||
while idx.len() > 3 && guard < max_guard {
|
||||
guard += 1;
|
||||
let mut clipped = false;
|
||||
let m = idx.len();
|
||||
for i in 0..m {
|
||||
let i_prev = idx[(i + m - 1) % m];
|
||||
let i_cur = idx[i];
|
||||
let i_next = idx[(i + 1) % m];
|
||||
let a = pts[i_prev];
|
||||
let b = pts[i_cur];
|
||||
let c = pts[i_next];
|
||||
if cross2(a, b, c) <= 0.0 {
|
||||
continue; // konkav/kollinear -> kein Ohr
|
||||
}
|
||||
let mut contains = false;
|
||||
for &vi in &idx {
|
||||
if vi == i_prev || vi == i_cur || vi == i_next {
|
||||
continue;
|
||||
}
|
||||
if point_in_tri(a, b, c, pts[vi]) {
|
||||
contains = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if contains {
|
||||
continue;
|
||||
}
|
||||
tris.push(i_prev as u32);
|
||||
tris.push(i_cur as u32);
|
||||
tris.push(i_next as u32);
|
||||
idx.remove(i);
|
||||
clipped = true;
|
||||
break;
|
||||
}
|
||||
if !clipped {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if idx.len() == 3 {
|
||||
tris.push(idx[0] as u32);
|
||||
tris.push(idx[1] as u32);
|
||||
tris.push(idx[2] as u32);
|
||||
}
|
||||
tris
|
||||
}
|
||||
|
||||
/// Haengt ein Dreieck (drei world-Ecken) mit fester Flaechen-Normale + Farbe an.
|
||||
/// Die Reihenfolge wird so gedreht, dass die geometrische Normale mit `want_normal`
|
||||
/// gleich orientiert ist (CCW von aussen -> korrektes Backface-Culling).
|
||||
fn push_tri_oriented(
|
||||
mesh: &mut Mesh,
|
||||
a: [f32; 3],
|
||||
b: [f32; 3],
|
||||
c: [f32; 3],
|
||||
want_normal: [f32; 3],
|
||||
color: Rgb,
|
||||
) {
|
||||
// Geometrische Normale (b-a) x (c-a).
|
||||
let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
||||
let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
||||
let gn = [
|
||||
ab[1] * ac[2] - ab[2] * ac[1],
|
||||
ab[2] * ac[0] - ab[0] * ac[2],
|
||||
ab[0] * ac[1] - ab[1] * ac[0],
|
||||
];
|
||||
let dot = gn[0] * want_normal[0] + gn[1] * want_normal[1] + gn[2] * want_normal[2];
|
||||
let (v0, v1, v2) = if dot < 0.0 { (a, c, b) } else { (a, b, c) };
|
||||
let base = (mesh.verts.len() / FLOATS_PER_VERTEX) as u32;
|
||||
for p in [v0, v1, v2] {
|
||||
mesh.verts.extend_from_slice(&[
|
||||
p[0], p[1], p[2], want_normal[0], want_normal[1], want_normal[2], color[0], color[1],
|
||||
color[2],
|
||||
]);
|
||||
}
|
||||
mesh.indices.extend_from_slice(&[base, base + 1, base + 2]);
|
||||
}
|
||||
|
||||
/// Extrudiert EINE Deckenplatte (Slab) und haengt sie an `mesh` an: Deckel (+Y),
|
||||
/// Boden (−Y) — beide aus der Polygon-Triangulierung — plus die Mantelflaechen
|
||||
/// (ein Quad je Umriss-Kante). Normalen werden robust nach aussen orientiert
|
||||
/// (Deckel +Y, Boden −Y, Mantel weg vom Umriss-Schwerpunkt), sodass Culling und
|
||||
/// Shading unabhaengig von der Umlaufrichtung des Umrisses stimmen.
|
||||
pub fn extrude_slab(mesh: &mut Mesh, slab: &SlabInput) {
|
||||
let pts = &slab.outline;
|
||||
let n = pts.len();
|
||||
if n < 3 {
|
||||
return;
|
||||
}
|
||||
let y0 = slab.z_bottom.min(slab.z_top);
|
||||
let y1 = slab.z_bottom.max(slab.z_top);
|
||||
if (y1 - y0) < 1e-6 {
|
||||
return;
|
||||
}
|
||||
let tris = triangulate(pts);
|
||||
if tris.is_empty() {
|
||||
return;
|
||||
}
|
||||
let color = slab.color;
|
||||
// world-Position: model [x, y] -> (x, hoehe, y).
|
||||
let w = |g: Point2, y: f32| -> [f32; 3] { [g[0], y, g[1]] };
|
||||
|
||||
// Deckel (+Y) und Boden (−Y) aus den Triangulierungs-Dreiecken.
|
||||
for t in tris.chunks_exact(3) {
|
||||
let a = pts[t[0] as usize];
|
||||
let b = pts[t[1] as usize];
|
||||
let c = pts[t[2] as usize];
|
||||
push_tri_oriented(mesh, w(a, y1), w(b, y1), w(c, y1), [0.0, 1.0, 0.0], color);
|
||||
push_tri_oriented(mesh, w(a, y0), w(b, y0), w(c, y0), [0.0, -1.0, 0.0], color);
|
||||
}
|
||||
|
||||
// Umriss-Schwerpunkt (Grundriss) fuer die Aussenrichtung der Mantel-Normalen.
|
||||
let mut cx = 0.0f32;
|
||||
let mut cy = 0.0f32;
|
||||
for p in pts {
|
||||
cx += p[0];
|
||||
cy += p[1];
|
||||
}
|
||||
cx /= n as f32;
|
||||
cy /= n as f32;
|
||||
|
||||
// Mantelflaechen: je Kante ein vertikales Quad (unten y0, oben y1).
|
||||
for i in 0..n {
|
||||
let a = pts[i];
|
||||
let b = pts[(i + 1) % n];
|
||||
let ex = b[0] - a[0];
|
||||
let ez = b[1] - a[1];
|
||||
let elen = (ex * ex + ez * ez).sqrt();
|
||||
if elen < 1e-9 {
|
||||
continue; // entartete Kante
|
||||
}
|
||||
// Horizontale Kanten-Normale (senkrecht zur Kante), nach aussen orientiert.
|
||||
let mx = (a[0] + b[0]) * 0.5;
|
||||
let mz = (a[1] + b[1]) * 0.5;
|
||||
let out = [mx - cx, mz - cy];
|
||||
let mut nx = -ez / elen;
|
||||
let mut nz = ex / elen;
|
||||
if nx * out[0] + nz * out[1] < 0.0 {
|
||||
nx = -nx;
|
||||
nz = -nz;
|
||||
}
|
||||
let normal = [nx, 0.0, nz];
|
||||
let ba = w(a, y0);
|
||||
let bb = w(b, y0);
|
||||
let tb = w(b, y1);
|
||||
let ta = w(a, y1);
|
||||
// Als zwei orientierte Dreiecke (Winding via push_tri_oriented gesichert).
|
||||
push_tri_oriented(mesh, ba, bb, tb, normal, color);
|
||||
push_tri_oriented(mesh, ba, tb, ta, normal, color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,24 +2,32 @@
|
||||
// ohne aktives GPU-Feature (headless) im Repo pruefbar bleibt (naga-Test, Muster:
|
||||
// render2d/shaders).
|
||||
//
|
||||
// Beleuchtung: EIN Directional-Light (Sonne) + ambienter Grundterm — das Pendant
|
||||
// zur three.js-Sicht (AmbientLight 0.6 + DirectionalLight 1.1 bei (6,12,4), siehe
|
||||
// Viewport3D.tsx). PBR (Rauheit/Metallik/Texturen) folgt in spaeteren Milestones.
|
||||
// Beleuchtung: HEMISPHAERISCHES Umgebungslicht (Himmel oben / Boden unten) plus
|
||||
// EIN Directional-Light (Sonne). Das hemisphaerische Ambient toent jede Flaeche je
|
||||
// nach ihrer Normalen-Neigung: Deckflaechen bekommen das helle Himmelslicht, nach
|
||||
// unten weisende Flaechen das dunklere Bodenlicht, senkrechte Waende einen Misch-
|
||||
// wert. Dadurch liest sich das Volumen sofort ab (Architektur-Massenmodell) — ohne
|
||||
// teures Postprocessing. Zusaetzlich betont ein Facing-Term (n gegen die Blick-
|
||||
// naeherung „nach oben") die Kanten sanft ab.
|
||||
//
|
||||
// Uniform-Layout (group(0) binding(0)):
|
||||
// view_proj : mat4x4<f32> world -> Clip (proj * view)
|
||||
// light_dir : vec4<f32> Richtung ZUM Licht (world, xyz; w ungenutzt)
|
||||
// ambient : vec4<f32> ambienter Grundfaktor (rgb; a ungenutzt)
|
||||
// view_proj : mat4x4<f32> world -> Clip (proj * view)
|
||||
// light_dir : vec4<f32> Richtung ZUM Licht (world, xyz; w = Sonnen-Staerke)
|
||||
// sky_color : vec4<f32> Himmels-Ambient (rgb; von oben)
|
||||
// ground_color : vec4<f32> Boden-Ambient (rgb; von unten)
|
||||
// sun_color : vec4<f32> Farbe/Staerke des Directional-Lights (rgb)
|
||||
//
|
||||
// Vertex-Attribute: position (world), normal (world), color (Albedo).
|
||||
|
||||
/// Der einzige Shader (Vertex + Fragment). Diffuse Lambert-Beleuchtung mit einem
|
||||
/// Directional-Light plus ambientem Sockel — GPU-Aequivalent des three.js-Setups.
|
||||
/// Der einzige Shader (Vertex + Fragment). Hemisphaerisches Ambient (Himmel/Boden)
|
||||
/// + Lambert-Directional — GPU-Aequivalent eines Architektur-Massenmodell-Lichts.
|
||||
pub const MESH_WGSL: &str = r#"
|
||||
struct Globals {
|
||||
view_proj : mat4x4<f32>,
|
||||
light_dir : vec4<f32>,
|
||||
ambient : vec4<f32>,
|
||||
view_proj : mat4x4<f32>,
|
||||
light_dir : vec4<f32>,
|
||||
sky_color : vec4<f32>,
|
||||
ground_color : vec4<f32>,
|
||||
sun_color : vec4<f32>,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> globals : Globals;
|
||||
|
||||
@@ -48,10 +56,22 @@ fn vs_main(in : VsIn) -> VsOut {
|
||||
fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
||||
let n = normalize(in.world_normal);
|
||||
let l = normalize(globals.light_dir.xyz);
|
||||
// Lambert-Diffusanteil; Rueckseiten (n.l < 0) tragen nichts bei.
|
||||
let diffuse = max(dot(n, l), 0.0);
|
||||
// Ambienter Sockel (Albedo * ambient) + gerichteter Anteil (Albedo * diffuse).
|
||||
let shaded = in.color * globals.ambient.rgb + in.color * diffuse;
|
||||
|
||||
// Hemisphaerisches Ambient: Mischfaktor aus der Vertikal-Komponente der
|
||||
// Normalen (n.y = +1 -> voll Himmel, n.y = -1 -> voll Boden).
|
||||
let hemi_t = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
|
||||
let ambient = mix(globals.ground_color.rgb, globals.sky_color.rgb, hemi_t);
|
||||
|
||||
// Gerichteter Lambert-Anteil (Sonne); Rueckseiten (n.l < 0) tragen nichts bei.
|
||||
let diffuse = max(dot(n, l), 0.0) * globals.sun_color.rgb;
|
||||
|
||||
var shaded = in.color * (ambient + diffuse);
|
||||
|
||||
// Sanfte Kantenbetonung: senkrechte Flaechen (n.y nahe 0) minimal abdunkeln,
|
||||
// damit sich Waende vom hellen Deckel/Boden abheben (ohne Postprocessing).
|
||||
let edge = 0.90 + 0.10 * abs(n.y);
|
||||
shaded = shaded * edge;
|
||||
|
||||
return vec4<f32>(shaded, 1.0);
|
||||
}
|
||||
"#;
|
||||
|
||||
@@ -50,6 +50,32 @@ fn default_wall_color() -> Rgb {
|
||||
[0.82, 0.80, 0.76]
|
||||
}
|
||||
|
||||
/// Eine geflachte Deckenplatte (Slab): ein GESCHLOSSENER Grundriss-Umriss (Polygon
|
||||
/// in Modell-Metern, Schlusspunkt NICHT dupliziert) plus die absolute vertikale
|
||||
/// Ausdehnung `z_bottom..z_top`. Aus diesen Feldern wird das Polygon trianguliert
|
||||
/// (Ear-Clipping) und ueber die Dicke zu einer Platte extrudiert.
|
||||
///
|
||||
/// KOORDINATEN wie WallInput: model `[x, y]` -> world `(x, elevation, y)`, d. h. der
|
||||
/// Umriss liegt in der XZ-Ebene, die Extrusion laeuft entlang +Y.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SlabInput {
|
||||
/// Geschlossener Umriss im Grundriss (Modell-Meter), CW oder CCW zulaessig.
|
||||
pub outline: Vec<Point2>,
|
||||
/// Unterkante (absolut, Meter).
|
||||
#[serde(rename = "zBottom")]
|
||||
pub z_bottom: f32,
|
||||
/// Oberkante (absolut, Meter).
|
||||
#[serde(rename = "zTop")]
|
||||
pub z_top: f32,
|
||||
/// Albedo-Farbe (RGB 0..1). Default heller Deckenton.
|
||||
#[serde(default = "default_slab_color")]
|
||||
pub color: Rgb,
|
||||
}
|
||||
|
||||
fn default_slab_color() -> Rgb {
|
||||
[0.86, 0.86, 0.88]
|
||||
}
|
||||
|
||||
/// Projektionsart der Kamera. Die three.js-Sicht schaltet zwischen perspektivisch
|
||||
/// (freies Orbit) und orthografisch (die achsparallelen Presets front/top/side um).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
Reference in New Issue
Block a user