35299307d6
Bündelt den über mehrere Sessions gewachsenen, uncommitteten Stand in
einem Basis-Commit, damit Folge-Features isoliert darauf aufsetzen.
Verifikation: tsc --noEmit sauber, vitest 600/600 grün.
Enthalten (Details in PENDENZEN.md ✅-Liste / HANDOVER.md):
- truck-Integration: Profil-Extrusion + Verjüngung + Boolean-CSG (csgrs),
Crate src-tauri/trucksolid, Werkzeug `extrude`, ExtrudedSolid-Modell.
- kernel2d-Port nach Rust/WASM (Phasen 1–5, Diff-Harness).
- render3d 3D-Live-Schnitt = 2D-Schnitt: geschichteter Bodenaufbau,
Prioritäts-Verschneidung (section_boolean.rs), einstellbare
Schichttrennlinien, per-Hatch-Strichstärke, relativeToWall-Orientierung.
- Interop-Export IFC4/STL/OBJ (Loch-Ausschnitt wallMeshCut), Schnellexport.
- Projektdatei .obp + OS-Lock (lock.rs, LockConflictDialog).
- Layout-Blätter (Modell/Editor/Panel/PDF), Ausschnitte, Override-Engine,
Tragwerk-Stützen (Column), BIM-Tree-Panel.
- Bauteil-Typsystem (Tür/Fenster/Treppe-Typen), Betontreppe mit schräger
Laufplatte, Text-/Textbox-Werkzeug, Mess-Werkzeug, 2D/3D-Griffe für
Öffnungen/Treppen, Snap-Symbol-Restyle.
1421 lines
64 KiB
Rust
1421 lines
64 KiB
Rust
// wgpu-Pipeline des nativen 3D-Renderers (nur mit Feature "render").
|
||
//
|
||
// Eine Pipeline: extrudierte Wand-Meshes mit Tiefenpuffer, View-Projektions-
|
||
// Matrix-Uniform und einem Directional-Light im Fragment-Shader. Backface-Culling
|
||
// ist aktiv (die Extrusion liefert konsistent nach aussen zeigende Flaechen, siehe
|
||
// mesh.rs) — das haelt das Innere der Waende korrekt verdeckt.
|
||
//
|
||
// Der Renderer ist fenster-/surface-agnostisch: er bekommt `Device`, `Queue` und
|
||
// ein `TextureView` (Surface- oder Offscreen-Textur) plus die aktuelle Kamera und
|
||
// zeichnet dort hinein. Die Fenster-Anbindung (winit) liegt separat im Spike-Bin.
|
||
|
||
use bytemuck::{Pod, Zeroable};
|
||
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::math::{view_projection, Mat4};
|
||
use crate::mesh::{
|
||
build_scene_mesh, build_scene_mesh_textured, build_walls_mesh, build_walls_mesh_textured,
|
||
};
|
||
use crate::section::SectionPlane;
|
||
use crate::section_fill::{
|
||
build_cut_cap_lines, build_cut_caps, CAP_FLOATS_PER_VERTEX, CUT_LINE_FLOATS_PER_VERTEX,
|
||
};
|
||
use crate::shaders::{CAP_WGSL, GRID_WGSL, MESH_TEXTURED_WGSL, MESH_WGSL};
|
||
use crate::types::{
|
||
Camera, Mesh, MeshInput, SlabInput, TexturedMesh, WallInput, FLOATS_PER_VERTEX,
|
||
TEXTURED_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,
|
||
/// Echte Bild-Textur (prozedurales Schachbrett) auf den Wandflaechen,
|
||
/// beleuchtet wie `Shaded` (eigene Pipeline + Textur-Bind-Group group 1,
|
||
/// siehe `textured_pipeline`).
|
||
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).
|
||
pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
|
||
|
||
/// MSAA-Samples fuer Farb- und Tiefenziel. 4x ist auf allen gaengigen Backends
|
||
/// (Vulkan/Metal/DX12/GL) fuer die ueblichen Oberflaechenformate verfuegbar und
|
||
/// gleicht die Treppenstufen an Wandkanten/Silhouetten spuerbar aus.
|
||
const SAMPLE_COUNT: u32 = 4;
|
||
|
||
/// Uniform-Block, 1:1 zum WGSL-`Globals`-Struct. std140-kompatibel (mat4/vec4 auf
|
||
/// 16 Byte ausgerichtet).
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Pod, Zeroable)]
|
||
struct Globals {
|
||
view_proj: [f32; 16],
|
||
light_dir: [f32; 4],
|
||
sky_color: [f32; 4],
|
||
ground_color: [f32; 4],
|
||
sun_color: [f32; 4],
|
||
/// x = Darstellungsart (0 = shaded/Material, 1 = weiss/Clay, 2 = hidden);
|
||
/// y = Live-Schnitt aktiv (0/1). Siehe shaders.rs.
|
||
mode: [f32; 4],
|
||
/// Live-Schnittebene: xyz = Ebenennormale (= Blickrichtung, normiert),
|
||
/// w = -dot(normal, point). Ein Fragment mit `dot(world, xyz) + w > 0` liegt
|
||
/// hinter der Ebene und wird (bei aktivem Schnitt, mode.y > 0.5) verworfen.
|
||
section_plane: [f32; 4],
|
||
}
|
||
|
||
impl Default for Globals {
|
||
fn default() -> Self {
|
||
Self {
|
||
view_proj: crate::math::identity(),
|
||
// Richtung ZUM Licht (world), normiert. Sonne oben-vorne (6,12,4).
|
||
light_dir: normalize4([6.0, 12.0, 4.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],
|
||
mode: [0.0, 0.0, 0.0, 0.0],
|
||
// Default: kein Schnitt (mode.y = 0). Neutrale Ebene.
|
||
section_plane: [0.0, 0.0, 1.0, 0.0],
|
||
}
|
||
}
|
||
}
|
||
|
||
fn normalize4(v: [f32; 3]) -> [f32; 4] {
|
||
let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-9);
|
||
[v[0] / l, v[1] / l, v[2] / l, 0.0]
|
||
}
|
||
|
||
/// GPU-seitige Puffer eines hochgeladenen Meshes.
|
||
struct MeshBuffers {
|
||
vbo: wgpu::Buffer,
|
||
ibo: wgpu::Buffer,
|
||
index_count: u32,
|
||
}
|
||
|
||
/// GPU-seitige Puffer des TEXTURIERTEN Wand-Meshes (`RenderStyle::Textured`).
|
||
/// Eigenes Vertex-Layout `[pos vec3, normal vec3, uv vec2]`
|
||
/// (`TEXTURED_FLOATS_PER_VERTEX`), sonst wie `MeshBuffers`. Zweiter, additiver
|
||
/// Puffer neben `MeshBuffers` — der Alt-Pfad bleibt bitgleich unangetastet.
|
||
struct TexturedMeshBuffers {
|
||
vbo: wgpu::Buffer,
|
||
/// Zweiter Vertex-Buffer: EIN f32 (Material-Textur-Ebene) je Vertex,
|
||
/// @location(3) im `MESH_TEXTURED_WGSL`. Immer gesetzt (Fallback: `-1` je
|
||
/// Vertex), damit das Pipeline-Layout stets zwei Vertex-Buffer erwartet.
|
||
layer_vbo: wgpu::Buffer,
|
||
ibo: wgpu::Buffer,
|
||
index_count: u32,
|
||
}
|
||
|
||
/// Kantenlaenge der prozeduralen Test-Textur (Schachbrett), in Texeln.
|
||
const TEXTURE_SIZE: u32 = 256;
|
||
|
||
/// Erzeugt ein 256x256-RGBA-Schachbrett IM CODE (kein Asset, keine `image`-Crate):
|
||
/// 8x8 Felder abwechselnd hell/dunkel, mit einem duennen Raster als Kachel-Grenze.
|
||
/// Rueckgabe = flaches RGBA8-Byte-Array (`4 * TEXTURE_SIZE * TEXTURE_SIZE`), direkt
|
||
/// fuer `queue.write_texture` (Zeilen sind `4 * TEXTURE_SIZE` Byte breit, ohne
|
||
/// Padding, weil `256 * 4 = 1024` bereits ein Vielfaches von 256 ist). Bewusst
|
||
/// selbststaendig, damit der Spike ohne Dateipfade/Asset-Pipeline laeuft.
|
||
fn build_checker_texture() -> Vec<u8> {
|
||
let size = TEXTURE_SIZE as usize;
|
||
// 8 Felder je Kante -> jedes Feld 32 Texel breit. Das macht 1 Textur-Kachel
|
||
// (= 1 m Welt, siehe UV-Massstab) in ein 8x8-Schachbrett sichtbar.
|
||
let cell = size / 8;
|
||
let mut data = vec![0u8; size * size * 4];
|
||
// Zwei Grautoene als Schachbrett + eine dunkle Rasterlinie an den Feldgrenzen.
|
||
let light = [220u8, 220, 224, 255];
|
||
let dark = [150u8, 152, 160, 255];
|
||
let line = [70u8, 72, 80, 255];
|
||
for y in 0..size {
|
||
for x in 0..size {
|
||
let checker = ((x / cell) + (y / cell)) % 2 == 0;
|
||
// Duenne Rasterlinie (2 Texel) exakt auf den Feldkanten — betont die
|
||
// weltmassstaebliche Kachelung an Gehrungen/Ecken sichtbar.
|
||
let on_line = (x % cell) < 2 || (y % cell) < 2;
|
||
let px = if on_line {
|
||
line
|
||
} else if checker {
|
||
light
|
||
} else {
|
||
dark
|
||
};
|
||
let o = (y * size + x) * 4;
|
||
data[o..o + 4].copy_from_slice(&px);
|
||
}
|
||
}
|
||
data
|
||
}
|
||
|
||
/// GPU-seitiger Vertex-Buffer einer LineList (kein Indexpuffer). Format
|
||
/// [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,
|
||
vertex_count: u32,
|
||
}
|
||
|
||
/// GPU-seitiger Vertex-Buffer der Schnittflaechen-Kappen (Cut-Caps, TriangleList,
|
||
/// kein Indexpuffer). Format `[pos vec3, uv vec2]` (`CAP_FLOATS_PER_VERTEX`) —
|
||
/// die (u,v)-Ebenenkoordinaten treiben die prozedurale Schraffur (CAP_WGSL).
|
||
struct CapBuffers {
|
||
vbo: wgpu::Buffer,
|
||
vertex_count: u32,
|
||
}
|
||
|
||
/// Der native 3D-Renderer: haelt die Pipeline, das Uniform (View-Projektion +
|
||
/// Licht) und das aktuell hochgeladene Mesh. Ein Tiefenpuffer wird passend zur
|
||
/// Ziel-Groesse (neu) angelegt.
|
||
pub struct Renderer {
|
||
pipeline: wgpu::RenderPipeline,
|
||
/// 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
|
||
/// Mesh-Pipeline und rendert in denselben Pass/Tiefenpuffer.
|
||
grid_pipeline: wgpu::RenderPipeline,
|
||
/// Wie `grid_pipeline` (LineList, GRID_WGSL), aber OHNE Tiefentest
|
||
/// (`depth_compare: Always`, kein Depth-Write): die Auswahl-Highlight-Linien
|
||
/// liegen immer sichtbar obenauf und scheinen auch hinter Waenden durch.
|
||
highlight_pipeline: wgpu::RenderPipeline,
|
||
/// Schnittflaechen-Kappen (TriangleList, CAP_WGSL, eigenes Vertex-Layout
|
||
/// [pos vec3, uv vec2]). Teilt sich die `Globals`-Bind-Group; leichter
|
||
/// Depth-Bias zur Kamera hin gegen Z-Fighting mit den an der Ebene
|
||
/// per-`discard` gekappten Wandflaechen. Backface-Culling AUS (CullMode::None).
|
||
cap_pipeline: wgpu::RenderPipeline,
|
||
/// Schnitt-Umrisslinien-Pipeline (Cut-Cap-Kanten, P2): TriangleList von
|
||
/// Ribbon-Quads (`section_fill::build_cut_cap_lines`), Vertex-Layout [pos vec3,
|
||
/// color vec3] wie die Grid/Highlight-LineList — teilt darum das `grid_module`
|
||
/// (GRID_WGSL). KEIN Tiefentest (`depth_compare: Always`, kein Write) wie das
|
||
/// Highlight: die Fugenlinien liegen (leicht zur Kamera versetzt) sichtbar auf
|
||
/// den Cut-Caps. Sie werden NICHT an der Schnittebene weggeclippt (sie SIND die
|
||
/// Schnittflaeche) — der Kamera-Lift in `build_cut_cap_lines` haelt sie vor der
|
||
/// `depth > 0`-Kappung von GRID_WGSL.
|
||
cut_line_pipeline: wgpu::RenderPipeline,
|
||
/// Texturierte Wand-Pipeline (`RenderStyle::Textured`, TriangleList,
|
||
/// MESH_TEXTURED_WGSL, Vertex-Layout [pos vec3, normal vec3, uv vec2]). Bindet
|
||
/// group 0 (Globals) UND group 1 (Bild-Textur). Depth-/MSAA-/Farbziel
|
||
/// BITIDENTISCH zur Haupt-`pipeline` (sonst inkompatibler Render-Pass), nur
|
||
/// Shader/Layout unterscheiden sich.
|
||
textured_pipeline: wgpu::RenderPipeline,
|
||
/// Bind-Group des Material-Textur-Arrays (group 1): Array-View + Sampler.
|
||
/// Wird bei `set_material_textures` neu gebaut (mehr Ebenen); bis dahin haelt
|
||
/// sie das 1-Ebenen-Array mit dem Fallback-Schachbrett.
|
||
texture_bind_group: wgpu::BindGroup,
|
||
/// Das Textur-Array selbst. Start: 1 Ebene (Schachbrett). `set_material_textures`
|
||
/// ersetzt es durch ein Array `[Schachbrett, Material0, Material1, …]`.
|
||
texture: wgpu::Texture,
|
||
/// Bind-Group-Layout (group 1), festgehalten fuer den Neubau der Bind-Group
|
||
/// beim Hochladen von Material-Texturen.
|
||
texture_bind_group_layout: wgpu::BindGroupLayout,
|
||
/// Sampler (Repeat/Linear), festgehalten fuer den Bind-Group-Neubau.
|
||
texture_sampler: wgpu::Sampler,
|
||
/// Anzahl gueltiger Ebenen im Textur-Array (>= 1). 1 = nur Schachbrett; nach
|
||
/// `set_material_textures` = 1 + Anzahl Material-Karten. Wandert je Frame nach
|
||
/// `mode.z` (Klemmung der Ebenen-Auswahl im Shader).
|
||
material_layer_count: u32,
|
||
/// Ob die Schachbrett-Texel (Ebene 0) schon hochgeladen wurden. Beim ersten
|
||
/// `render` einmalig gesetzt (lazy Upload, weil `new` keine Queue bekommt).
|
||
texture_uploaded: bool,
|
||
bind_group: wgpu::BindGroup,
|
||
uniform: wgpu::Buffer,
|
||
mesh: Option<MeshBuffers>,
|
||
/// Texturiertes Wand-Mesh (`[pos, normal, uv]`) — zweiter, additiver Puffer
|
||
/// neben `mesh`. Nur im Stil `Textured` gezeichnet; bei `upload_*` zusammen mit
|
||
/// `mesh` erzeugt. None = kein Mesh geladen.
|
||
textured_mesh: Option<TexturedMeshBuffers>,
|
||
/// 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
|
||
/// zusaetzlich `grid_visible` gesetzt ist.
|
||
grid: Option<LineBuffers>,
|
||
/// Auswahl-Highlight-Linien (Kanten des selektierten Bauteils, LineList) als
|
||
/// LineBuffers. None = keine Auswahl -> nichts wird gezeichnet. Ueber
|
||
/// `set_highlight_lines` gesetzt/geloescht; im Render-Pass als ALLERLETZTES
|
||
/// (ohne Tiefentest) gezeichnet.
|
||
highlight: Option<LineBuffers>,
|
||
/// Schnittflaechen-Kappen (Cut-Caps) des Live-Schnitts. None = kein Schnitt
|
||
/// aktiv / keine Schnittgeometrie. Ueber `set_cut_caps` gesetzt/geloescht,
|
||
/// gezeichnet NACH den Flaechen, VOR Kanten/Grid/Highlight.
|
||
caps: Option<CapBuffers>,
|
||
/// Schnitt-Umrisslinien (Cut-Cap-Kanten, P2) als Ribbon-TriangleList
|
||
/// (`[pos, color]`). None = kein Schnitt aktiv / keine Kanten. Ueber
|
||
/// `set_cut_lines` gesetzt/geloescht, gezeichnet NACH den Caps.
|
||
cut_lines: Option<LineBuffers>,
|
||
/// Ob das Bodengitter im naechsten `render` gezeichnet wird. Default false —
|
||
/// bestehende Aufrufer ohne `set_ground_grid` bekommen KEIN Gitter.
|
||
grid_visible: bool,
|
||
/// Aktuelle Darstellungsart (Default `Shaded`). Einzige Quelle der Wahrheit;
|
||
/// `set_white_mode` bildet auf `Shaded`/`White` ab.
|
||
style: RenderStyle,
|
||
depth: Option<DepthTarget>,
|
||
msaa: Option<MsaaTarget>,
|
||
/// Farbformat des Render-Ziels (Surface), fuer die MSAA-Zwischentextur.
|
||
color_format: wgpu::TextureFormat,
|
||
globals: Globals,
|
||
/// Loeschfarbe (Hintergrund). Default heller Grauton wie die three.js-Sicht.
|
||
pub clear_color: wgpu::Color,
|
||
}
|
||
|
||
/// Tiefen-Textur samt View, an eine bestimmte Groesse gebunden.
|
||
struct DepthTarget {
|
||
view: wgpu::TextureView,
|
||
width: u32,
|
||
height: u32,
|
||
}
|
||
|
||
/// Multisampled Farb-Zwischentextur samt View, an eine bestimmte Groesse
|
||
/// gebunden. Wird jeden Frame in das eigentliche Ziel (Surface-View) aufgeloest.
|
||
struct MsaaTarget {
|
||
view: wgpu::TextureView,
|
||
width: u32,
|
||
height: u32,
|
||
}
|
||
|
||
impl Renderer {
|
||
/// Erzeugt Pipeline + Layout fuer ein gegebenes Farbformat (Surface).
|
||
pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
|
||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||
label: Some("mesh.wgsl"),
|
||
source: wgpu::ShaderSource::Wgsl(MESH_WGSL.into()),
|
||
});
|
||
|
||
let bind_group_layout =
|
||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||
label: Some("globals.layout"),
|
||
entries: &[wgpu::BindGroupLayoutEntry {
|
||
binding: 0,
|
||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||
ty: wgpu::BindingType::Buffer {
|
||
ty: wgpu::BufferBindingType::Uniform,
|
||
has_dynamic_offset: false,
|
||
min_binding_size: wgpu::BufferSize::new(
|
||
std::mem::size_of::<Globals>() as u64
|
||
),
|
||
},
|
||
count: None,
|
||
}],
|
||
});
|
||
|
||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||
label: Some("3d.layout"),
|
||
bind_group_layouts: &[Some(&bind_group_layout)],
|
||
immediate_size: 0,
|
||
});
|
||
|
||
// Vertex-Layout: [pos vec3, normal vec3, color vec3], stride 9*4.
|
||
let vertex_layout = wgpu::VertexBufferLayout {
|
||
array_stride: (FLOATS_PER_VERTEX * 4) as u64,
|
||
step_mode: wgpu::VertexStepMode::Vertex,
|
||
attributes: &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3, 2 => Float32x3],
|
||
};
|
||
|
||
// Mesh-Pipeline-Fabrik: identische Beschreibung fuer die Standard-Flaechen
|
||
// (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),
|
||
vertex: wgpu::VertexState {
|
||
module: &module,
|
||
entry_point: Some("vs_main"),
|
||
buffers: &[vertex_layout.clone()],
|
||
compilation_options: Default::default(),
|
||
},
|
||
fragment: Some(wgpu::FragmentState {
|
||
module: &module,
|
||
entry_point: Some("fs_main"),
|
||
targets: &[Some(wgpu::ColorTargetState {
|
||
format: color_format,
|
||
blend: Some(wgpu::BlendState::REPLACE),
|
||
write_mask: wgpu::ColorWrites::ALL,
|
||
})],
|
||
compilation_options: Default::default(),
|
||
}),
|
||
primitive: wgpu::PrimitiveState {
|
||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||
// Aussen zeigende Flaechen sind CCW (mesh.rs) -> Rueckseiten cullen.
|
||
front_face: wgpu::FrontFace::Ccw,
|
||
cull_mode: Some(wgpu::Face::Back),
|
||
..Default::default()
|
||
},
|
||
depth_stencil: Some(wgpu::DepthStencilState {
|
||
format: DEPTH_FORMAT,
|
||
depth_write_enabled: Some(true),
|
||
depth_compare: Some(wgpu::CompareFunction::Less),
|
||
stencil: wgpu::StencilState::default(),
|
||
bias,
|
||
}),
|
||
multisample: wgpu::MultisampleState {
|
||
count: SAMPLE_COUNT,
|
||
mask: !0,
|
||
alpha_to_coverage_enabled: false,
|
||
},
|
||
multiview_mask: 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
|
||
// dasselbe Depth-/MSAA-/Farbziel wie die Mesh-Pipeline, aber LineList-
|
||
// Topologie, ein eigener unlit-Shader (konstante Vertexfarbe) und ein
|
||
// schlankeres Vertex-Layout [pos vec3, color vec3].
|
||
let grid_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||
label: Some("grid.wgsl"),
|
||
source: wgpu::ShaderSource::Wgsl(GRID_WGSL.into()),
|
||
});
|
||
let grid_vertex_layout = wgpu::VertexBufferLayout {
|
||
array_stride: (GRID_FLOATS_PER_VERTEX * 4) as u64,
|
||
step_mode: wgpu::VertexStepMode::Vertex,
|
||
attributes: &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3],
|
||
};
|
||
// Linien-Pipeline-Fabrik: identische Beschreibung (LineList, GRID_WGSL,
|
||
// gemeinsame Bind-Group/Farbziel/MSAA) fuer das Bodengitter/die Kanten
|
||
// (tiefengetestet) UND die Auswahl-Highlight-Linien (ohne Tiefentest, siehe
|
||
// unten). Nur der `depth_stencil`-Zustand unterscheidet sich.
|
||
let make_line_pipeline = |label: &str, depth: wgpu::DepthStencilState| {
|
||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||
label: Some(label),
|
||
layout: Some(&pipeline_layout),
|
||
vertex: wgpu::VertexState {
|
||
module: &grid_module,
|
||
entry_point: Some("vs_main"),
|
||
buffers: &[grid_vertex_layout.clone()],
|
||
compilation_options: Default::default(),
|
||
},
|
||
fragment: Some(wgpu::FragmentState {
|
||
module: &grid_module,
|
||
entry_point: Some("fs_main"),
|
||
targets: &[Some(wgpu::ColorTargetState {
|
||
format: color_format,
|
||
blend: Some(wgpu::BlendState::REPLACE),
|
||
write_mask: wgpu::ColorWrites::ALL,
|
||
})],
|
||
compilation_options: Default::default(),
|
||
}),
|
||
primitive: wgpu::PrimitiveState {
|
||
topology: wgpu::PrimitiveTopology::LineList,
|
||
// Linien haben keine Vorder-/Rueckseite -> kein Culling.
|
||
cull_mode: None,
|
||
..Default::default()
|
||
},
|
||
depth_stencil: Some(depth),
|
||
multisample: wgpu::MultisampleState {
|
||
count: SAMPLE_COUNT,
|
||
mask: !0,
|
||
alpha_to_coverage_enabled: false,
|
||
},
|
||
multiview_mask: None,
|
||
cache: None,
|
||
})
|
||
};
|
||
// Grid/Kanten: normaler Tiefentest (Less), schreibt Tiefe -> das Modell
|
||
// verdeckt Gitter/verdeckte Kanten korrekt.
|
||
let grid_pipeline = make_line_pipeline(
|
||
"grid.pipeline",
|
||
wgpu::DepthStencilState {
|
||
format: DEPTH_FORMAT,
|
||
depth_write_enabled: Some(true),
|
||
depth_compare: Some(wgpu::CompareFunction::Less),
|
||
stencil: wgpu::StencilState::default(),
|
||
bias: wgpu::DepthBiasState::default(),
|
||
},
|
||
);
|
||
// Auswahl-Highlight: KEIN Tiefentest (`Always`) und KEIN Tiefen-Write —
|
||
// die Auswahl-Umrisslinien werden unabhaengig von der Szenentiefe immer
|
||
// gezeichnet und scheinen so auch hinter Waenden durch (liegen optisch oben,
|
||
// weil sie als letztes gezeichnet werden).
|
||
let highlight_pipeline = make_line_pipeline(
|
||
"highlight.pipeline",
|
||
wgpu::DepthStencilState {
|
||
format: DEPTH_FORMAT,
|
||
depth_write_enabled: Some(false),
|
||
depth_compare: Some(wgpu::CompareFunction::Always),
|
||
stencil: wgpu::StencilState::default(),
|
||
bias: wgpu::DepthBiasState::default(),
|
||
},
|
||
);
|
||
|
||
// Schnitt-Umrisslinien-Pipeline (Cut-Cap-Kanten, P2): dasselbe schlanke
|
||
// LineList-Vertex-Layout [pos vec3, color vec3] und `grid_module` (GRID_WGSL)
|
||
// wie Grid/Highlight, aber als TriangleList (die Kanten sind Ribbon-Quads,
|
||
// damit die Strichstaerke als Welt-Breite durchkommt). Depth wie Highlight
|
||
// (Always, kein Write) -> immer sichtbar auf den Caps.
|
||
let cut_line_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||
label: Some("cut_line.pipeline"),
|
||
layout: Some(&pipeline_layout),
|
||
vertex: wgpu::VertexState {
|
||
module: &grid_module,
|
||
entry_point: Some("vs_main"),
|
||
buffers: &[grid_vertex_layout.clone()],
|
||
compilation_options: Default::default(),
|
||
},
|
||
fragment: Some(wgpu::FragmentState {
|
||
module: &grid_module,
|
||
entry_point: Some("fs_main"),
|
||
targets: &[Some(wgpu::ColorTargetState {
|
||
format: color_format,
|
||
blend: Some(wgpu::BlendState::REPLACE),
|
||
write_mask: wgpu::ColorWrites::ALL,
|
||
})],
|
||
compilation_options: Default::default(),
|
||
}),
|
||
primitive: wgpu::PrimitiveState {
|
||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||
// Ribbons sind flach in der Schnittebene, aus beiden Seiten sichtbar.
|
||
cull_mode: None,
|
||
..Default::default()
|
||
},
|
||
depth_stencil: Some(wgpu::DepthStencilState {
|
||
format: DEPTH_FORMAT,
|
||
depth_write_enabled: Some(false),
|
||
depth_compare: Some(wgpu::CompareFunction::Always),
|
||
stencil: wgpu::StencilState::default(),
|
||
bias: wgpu::DepthBiasState::default(),
|
||
}),
|
||
multisample: wgpu::MultisampleState {
|
||
count: SAMPLE_COUNT,
|
||
mask: !0,
|
||
alpha_to_coverage_enabled: false,
|
||
},
|
||
multiview_mask: None,
|
||
cache: None,
|
||
});
|
||
|
||
// Schnittflaechen-Kappen-Pipeline: eigener Shader (CAP_WGSL), eigenes
|
||
// schlankes Vertex-Layout [pos vec3, uv vec2], TriangleList. Dieselbe
|
||
// Bind-Group (Globals) und dasselbe Depth-/MSAA-/Farbziel wie oben.
|
||
let cap_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||
label: Some("cap.wgsl"),
|
||
source: wgpu::ShaderSource::Wgsl(CAP_WGSL.into()),
|
||
});
|
||
// Vertex-Layout: [pos vec3, uv vec2, hatch vec4(pattern, angle_rad, scale,
|
||
// line_weight_mm)], stride 9*4 (`CAP_FLOATS_PER_VERTEX`). Das dritte Attribut
|
||
// treibt die musterspezifische Schraffur im Cap-Fragment-Shader (CAP_WGSL);
|
||
// `line_weight_mm` setzt die Musterlinien-Breite je Schraffur.
|
||
let cap_vertex_layout = wgpu::VertexBufferLayout {
|
||
array_stride: (CAP_FLOATS_PER_VERTEX * 4) as u64,
|
||
step_mode: wgpu::VertexStepMode::Vertex,
|
||
attributes: &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x2, 2 => Float32x4],
|
||
};
|
||
let cap_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||
label: Some("cap.pipeline"),
|
||
layout: Some(&pipeline_layout),
|
||
vertex: wgpu::VertexState {
|
||
module: &cap_module,
|
||
entry_point: Some("vs_main"),
|
||
buffers: &[cap_vertex_layout],
|
||
compilation_options: Default::default(),
|
||
},
|
||
fragment: Some(wgpu::FragmentState {
|
||
module: &cap_module,
|
||
entry_point: Some("fs_main"),
|
||
targets: &[Some(wgpu::ColorTargetState {
|
||
format: color_format,
|
||
blend: Some(wgpu::BlendState::REPLACE),
|
||
write_mask: wgpu::ColorWrites::ALL,
|
||
})],
|
||
compilation_options: Default::default(),
|
||
}),
|
||
primitive: wgpu::PrimitiveState {
|
||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||
// Die Cap ist eine flache Flaeche und soll aus BEIDEN Richtungen
|
||
// sichtbar sein (das Winding der Rueckprojektion ist nicht
|
||
// kamerakonsistent garantiert) -> kein Backface-Culling.
|
||
cull_mode: None,
|
||
..Default::default()
|
||
},
|
||
depth_stencil: Some(wgpu::DepthStencilState {
|
||
format: DEPTH_FORMAT,
|
||
depth_write_enabled: Some(true),
|
||
depth_compare: Some(wgpu::CompareFunction::Less),
|
||
stencil: wgpu::StencilState::default(),
|
||
// Leicht ZUR Kamera ziehen (negativer Bias, Gegenteil des
|
||
// hidden_face_pipeline-Bias): die Caps liegen exakt auf der
|
||
// Schnittebene, an der die Wandflaechen weggeschnitten werden —
|
||
// der negative Bias vermeidet Z-Fighting mit an der Grenze
|
||
// gerade noch stehen gebliebenen Wandfragmenten.
|
||
bias: wgpu::DepthBiasState {
|
||
constant: -4,
|
||
slope_scale: -1.0,
|
||
clamp: 0.0,
|
||
},
|
||
}),
|
||
multisample: wgpu::MultisampleState {
|
||
count: SAMPLE_COUNT,
|
||
mask: !0,
|
||
alpha_to_coverage_enabled: false,
|
||
},
|
||
multiview_mask: None,
|
||
cache: None,
|
||
});
|
||
|
||
// ── Texturierter Wand-Pfad (RenderStyle::Textured) ──────────────────────
|
||
// Bild-Textur-Bind-Group-Layout (group 1): sampelbare 2D-Float-Textur +
|
||
// Filter-Sampler. `Globals` bleibt group 0 (oben) UND unveraendert.
|
||
let texture_bind_group_layout =
|
||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||
label: Some("texture.layout"),
|
||
entries: &[
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 0,
|
||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||
ty: wgpu::BindingType::Texture {
|
||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||
view_dimension: wgpu::TextureViewDimension::D2Array,
|
||
multisampled: false,
|
||
},
|
||
count: None,
|
||
},
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 1,
|
||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||
count: None,
|
||
},
|
||
],
|
||
});
|
||
|
||
// Pipeline-Layout des texturierten Pfades: group 0 = Globals (wie oben),
|
||
// group 1 = Textur. Reihenfolge = Bindungs-Index, deshalb beide hier.
|
||
let textured_pipeline_layout =
|
||
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||
label: Some("3d.textured.layout"),
|
||
bind_group_layouts: &[Some(&bind_group_layout), Some(&texture_bind_group_layout)],
|
||
immediate_size: 0,
|
||
});
|
||
let textured_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||
label: Some("mesh_textured.wgsl"),
|
||
source: wgpu::ShaderSource::Wgsl(MESH_TEXTURED_WGSL.into()),
|
||
});
|
||
// Vertex-Layout: [pos vec3, normal vec3, uv vec2], stride 8*4.
|
||
let textured_vertex_layout = wgpu::VertexBufferLayout {
|
||
array_stride: (TEXTURED_FLOATS_PER_VERTEX * 4) as u64,
|
||
step_mode: wgpu::VertexStepMode::Vertex,
|
||
attributes: &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3, 2 => Float32x2],
|
||
};
|
||
// ZWEITER Vertex-Buffer: die Material-Textur-Ebene je Vertex (@location(3),
|
||
// 1 f32). Separat gehalten, damit das Haupt-`[pos,normal,uv]`-Layout (und
|
||
// die Geometrie-Paritaet zum Shaded-Pfad) unveraendert bleibt.
|
||
let textured_layer_layout = wgpu::VertexBufferLayout {
|
||
array_stride: 4,
|
||
step_mode: wgpu::VertexStepMode::Vertex,
|
||
attributes: &wgpu::vertex_attr_array![3 => Float32],
|
||
};
|
||
// WICHTIG: Depth-Format, MSAA (`SAMPLE_COUNT`), Color-Target-Format,
|
||
// Topologie und Culling BITIDENTISCH zur Haupt-`pipeline` (make_mesh_pipeline)
|
||
// — die texturierten Waende laufen im GLEICHEN Render-Pass, deshalb muss der
|
||
// Pipeline-State kompatibel sein (sonst wgpu-Validierungspanik). Einziger
|
||
// Unterschied: Shader/Vertex-Layout und das zusaetzliche group-1-Layout.
|
||
let textured_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||
label: Some("mesh.textured.pipeline"),
|
||
layout: Some(&textured_pipeline_layout),
|
||
vertex: wgpu::VertexState {
|
||
module: &textured_module,
|
||
entry_point: Some("vs_main"),
|
||
buffers: &[textured_vertex_layout, textured_layer_layout],
|
||
compilation_options: Default::default(),
|
||
},
|
||
fragment: Some(wgpu::FragmentState {
|
||
module: &textured_module,
|
||
entry_point: Some("fs_main"),
|
||
targets: &[Some(wgpu::ColorTargetState {
|
||
format: color_format,
|
||
blend: Some(wgpu::BlendState::REPLACE),
|
||
write_mask: wgpu::ColorWrites::ALL,
|
||
})],
|
||
compilation_options: Default::default(),
|
||
}),
|
||
primitive: wgpu::PrimitiveState {
|
||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||
front_face: wgpu::FrontFace::Ccw,
|
||
cull_mode: Some(wgpu::Face::Back),
|
||
..Default::default()
|
||
},
|
||
depth_stencil: Some(wgpu::DepthStencilState {
|
||
format: DEPTH_FORMAT,
|
||
depth_write_enabled: Some(true),
|
||
depth_compare: Some(wgpu::CompareFunction::Less),
|
||
stencil: wgpu::StencilState::default(),
|
||
bias: wgpu::DepthBiasState::default(),
|
||
}),
|
||
multisample: wgpu::MultisampleState {
|
||
count: SAMPLE_COUNT,
|
||
mask: !0,
|
||
alpha_to_coverage_enabled: false,
|
||
},
|
||
multiview_mask: None,
|
||
cache: None,
|
||
});
|
||
|
||
// Prozedurale 256x256-Schachbrett-Textur ANLEGEN (kein Asset). RGBA8-srgb,
|
||
// damit die im Shader gesampelten Werte im linearen Raum landen (die
|
||
// Beleuchtung rechnet linear) — analog zum srgb-Surface-Format. Das
|
||
// Hochladen der Texel (queue.write_texture) passiert einmalig beim ersten
|
||
// `render` (siehe `texture_uploaded`), weil `new` bewusst KEINE Queue
|
||
// bekommt — die Signatur bleibt fuer die bestehenden Aufrufer (web.rs,
|
||
// spike3d) unveraendert.
|
||
// Textur-ARRAY (D2Array). Start: EINE Ebene (Fallback-Schachbrett).
|
||
// `set_material_textures` ersetzt das Array spaeter durch mehrere Ebenen.
|
||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||
label: Some("materials.texture_array"),
|
||
size: wgpu::Extent3d {
|
||
width: TEXTURE_SIZE,
|
||
height: TEXTURE_SIZE,
|
||
depth_or_array_layers: 1,
|
||
},
|
||
mip_level_count: 1,
|
||
sample_count: 1,
|
||
dimension: wgpu::TextureDimension::D2,
|
||
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||
view_formats: &[],
|
||
});
|
||
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor {
|
||
dimension: Some(wgpu::TextureViewDimension::D2Array),
|
||
..Default::default()
|
||
});
|
||
// Repeat (weltmassstaebliches UV kachelt) + Linear (weiche Filterung).
|
||
let texture_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||
label: Some("materials.sampler"),
|
||
address_mode_u: wgpu::AddressMode::Repeat,
|
||
address_mode_v: wgpu::AddressMode::Repeat,
|
||
address_mode_w: wgpu::AddressMode::Repeat,
|
||
mag_filter: wgpu::FilterMode::Linear,
|
||
min_filter: wgpu::FilterMode::Linear,
|
||
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
|
||
..Default::default()
|
||
});
|
||
let texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||
label: Some("materials.bind"),
|
||
layout: &texture_bind_group_layout,
|
||
entries: &[
|
||
wgpu::BindGroupEntry {
|
||
binding: 0,
|
||
resource: wgpu::BindingResource::TextureView(&texture_view),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 1,
|
||
resource: wgpu::BindingResource::Sampler(&texture_sampler),
|
||
},
|
||
],
|
||
});
|
||
|
||
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
|
||
label: Some("globals.buffer"),
|
||
size: std::mem::size_of::<Globals>() as u64,
|
||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||
mapped_at_creation: false,
|
||
});
|
||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||
label: Some("globals.bind"),
|
||
layout: &bind_group_layout,
|
||
entries: &[wgpu::BindGroupEntry {
|
||
binding: 0,
|
||
resource: uniform.as_entire_binding(),
|
||
}],
|
||
});
|
||
|
||
Self {
|
||
pipeline,
|
||
hidden_face_pipeline,
|
||
grid_pipeline,
|
||
highlight_pipeline,
|
||
cap_pipeline,
|
||
cut_line_pipeline,
|
||
textured_pipeline,
|
||
texture_bind_group,
|
||
texture,
|
||
texture_bind_group_layout,
|
||
texture_sampler,
|
||
material_layer_count: 1,
|
||
texture_uploaded: false,
|
||
bind_group,
|
||
uniform,
|
||
mesh: None,
|
||
textured_mesh: None,
|
||
edges: None,
|
||
grid: None,
|
||
highlight: None,
|
||
caps: None,
|
||
cut_lines: None,
|
||
grid_visible: false,
|
||
style: RenderStyle::Shaded,
|
||
depth: None,
|
||
msaa: None,
|
||
color_format,
|
||
globals: Globals::default(),
|
||
// #f5f5f5 heller Hintergrund (wie der 2D-Grundriss).
|
||
clear_color: wgpu::Color {
|
||
r: 0.961,
|
||
g: 0.961,
|
||
b: 0.961,
|
||
a: 1.0,
|
||
},
|
||
}
|
||
}
|
||
|
||
/// Erzeugt das Mesh aus geflachten Waenden und laedt die Puffer hoch.
|
||
pub fn upload_walls(&mut self, device: &wgpu::Device, walls: &[WallInput]) {
|
||
self.upload_mesh(device, build_walls_mesh(walls), build_walls_mesh_textured(walls));
|
||
}
|
||
|
||
/// Erzeugt das Mesh aus Waenden, Deckenplatten UND rohen Kontext-Meshes
|
||
/// (Terrain/importierte Volumen, siehe `MeshInput`) und laedt die Puffer hoch.
|
||
pub fn upload_model(
|
||
&mut self,
|
||
device: &wgpu::Device,
|
||
walls: &[WallInput],
|
||
slabs: &[SlabInput],
|
||
meshes: &[MeshInput],
|
||
) {
|
||
self.upload_mesh(
|
||
device,
|
||
build_scene_mesh(walls, slabs, meshes),
|
||
build_scene_mesh_textured(walls, slabs, meshes),
|
||
);
|
||
}
|
||
|
||
/// 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, tex_mesh: TexturedMesh) {
|
||
if mesh.indices.is_empty() {
|
||
self.mesh = None;
|
||
self.textured_mesh = None;
|
||
self.edges = None;
|
||
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 {
|
||
label: Some("mesh.vbo"),
|
||
contents: bytemuck::cast_slice(&mesh.verts),
|
||
usage: wgpu::BufferUsages::VERTEX,
|
||
});
|
||
let ibo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||
label: Some("mesh.ibo"),
|
||
contents: bytemuck::cast_slice(&mesh.indices),
|
||
usage: wgpu::BufferUsages::INDEX,
|
||
});
|
||
self.mesh = Some(MeshBuffers {
|
||
vbo,
|
||
ibo,
|
||
index_count: mesh.indices.len() as u32,
|
||
});
|
||
|
||
// Texturiertes Pendant (planare Welt-UV je Vertex + Material-Ebenen-Puffer,
|
||
// siehe `build_scene_mesh_textured`) in zwei Vertex-Puffer hochladen. Die
|
||
// Geometrie bleibt bitgleich zum Alt-Pfad; der Stil `Textured` hat ohne
|
||
// Re-Meshing seine Puffer bereit.
|
||
let tvbo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||
label: Some("mesh.textured.vbo"),
|
||
contents: bytemuck::cast_slice(&tex_mesh.verts),
|
||
usage: wgpu::BufferUsages::VERTEX,
|
||
});
|
||
let tibo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||
label: Some("mesh.textured.ibo"),
|
||
contents: bytemuck::cast_slice(&tex_mesh.indices),
|
||
usage: wgpu::BufferUsages::INDEX,
|
||
});
|
||
// Material-Ebenen-Puffer (1 f32 je Vertex). Fehlt er (kein Material-Bau),
|
||
// mit `-1` je Vertex fuellen -> alle Vertices auf das Fallback-Schachbrett.
|
||
let vertex_count = tex_mesh.verts.len() / TEXTURED_FLOATS_PER_VERTEX;
|
||
let layer_data: Vec<f32> = if tex_mesh.layers.len() == vertex_count {
|
||
tex_mesh.layers
|
||
} else {
|
||
vec![-1.0; vertex_count]
|
||
};
|
||
let tlvbo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||
label: Some("mesh.textured.layer.vbo"),
|
||
contents: bytemuck::cast_slice(&layer_data),
|
||
usage: wgpu::BufferUsages::VERTEX,
|
||
});
|
||
self.textured_mesh = Some(TexturedMeshBuffers {
|
||
vbo: tvbo,
|
||
layer_vbo: tlvbo,
|
||
ibo: tibo,
|
||
index_count: tex_mesh.indices.len() as u32,
|
||
});
|
||
}
|
||
|
||
/// 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);
|
||
}
|
||
|
||
/// 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];
|
||
}
|
||
|
||
/// Darstellungsart setzen (shaded/white/textured/wireframe/hidden). Einzige
|
||
/// 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) {
|
||
self.style = if white {
|
||
RenderStyle::White
|
||
} else {
|
||
RenderStyle::Shaded
|
||
};
|
||
}
|
||
|
||
/// Setzt/aktualisiert das Referenz-Bodengitter (grid.rs). Erzeugt den Linien-
|
||
/// Vertex-Buffer aus den Parametern neu und merkt sich die Sichtbarkeit; der
|
||
/// naechste `render` zeichnet das Gitter (bzw. nichts bei `visible == false`).
|
||
/// Ohne Aufruf bleibt das Gitter unsichtbar (Default), damit bestehende
|
||
/// Aufrufer unveraendert bleiben.
|
||
pub fn set_ground_grid(
|
||
&mut self,
|
||
device: &wgpu::Device,
|
||
visible: bool,
|
||
elevation: f32,
|
||
extent: f32,
|
||
spacing: f32,
|
||
) {
|
||
self.grid_visible = visible;
|
||
let verts = build_ground_grid(elevation, extent, spacing);
|
||
if verts.is_empty() {
|
||
self.grid = None;
|
||
return;
|
||
}
|
||
let vbo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||
label: Some("grid.vbo"),
|
||
contents: bytemuck::cast_slice(&verts),
|
||
usage: wgpu::BufferUsages::VERTEX,
|
||
});
|
||
self.grid = Some(LineBuffers {
|
||
vbo,
|
||
vertex_count: (verts.len() / GRID_FLOATS_PER_VERTEX) as u32,
|
||
});
|
||
}
|
||
|
||
/// Setzt/loescht die Auswahl-Highlight-Linien (Kanten des selektierten Bauteils).
|
||
/// `verts` = interleaved LineList-Vertices `[px,py,pz, r,g,b, ...]` (world,
|
||
/// je 2 Vertices = 1 Segment), exakt wie Grid-/Edge-Vertices
|
||
/// (`GRID_FLOATS_PER_VERTEX`). Leeres Slice -> Highlight loeschen. Analog
|
||
/// `set_ground_grid`, aber ohne Sichtbarkeits-Flag: vorhanden = gezeichnet.
|
||
pub fn set_highlight_lines(&mut self, device: &wgpu::Device, verts: &[f32]) {
|
||
if verts.is_empty() {
|
||
self.highlight = None;
|
||
return;
|
||
}
|
||
let vbo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||
label: Some("highlight.vbo"),
|
||
contents: bytemuck::cast_slice(verts),
|
||
usage: wgpu::BufferUsages::VERTEX,
|
||
});
|
||
self.highlight = Some(LineBuffers {
|
||
vbo,
|
||
vertex_count: (verts.len() / GRID_FLOATS_PER_VERTEX) as u32,
|
||
});
|
||
}
|
||
|
||
/// Aktiviert/deaktiviert den Live-Schnitt und setzt die Schnittebene. `active`
|
||
/// steuert `mode.y` (der Mesh-/Grid-Shader kappt die Geometrie nur bei > 0.5);
|
||
/// `point`/`normal` (world-Meter, Normale = Blickrichtung) fuellen
|
||
/// `section_plane = [n, -dot(n, point)]`. Nur die Uniform-Werte — die Cap-
|
||
/// Geometrie setzt der Aufrufer separat via `set_cut_caps`.
|
||
pub fn set_section_plane(&mut self, active: bool, point: [f32; 3], normal: [f32; 3]) {
|
||
let n = crate::math::normalize(normal);
|
||
let w = -(n[0] * point[0] + n[1] * point[1] + n[2] * point[2]);
|
||
self.globals.section_plane = [n[0], n[1], n[2], w];
|
||
self.globals.mode[1] = if active { 1.0 } else { 0.0 };
|
||
}
|
||
|
||
/// Setzt/loescht die Schnittflaechen-Kappen (Cut-Caps). `verts` = interleaved
|
||
/// `[px,py,pz, u,v, ...]` (world-Meter + Ebenenkoordinaten, TriangleList,
|
||
/// `CAP_FLOATS_PER_VERTEX`), wie `section_fill::build_cut_caps` liefert. Leeres
|
||
/// Slice loescht die Caps. Wirkt erst beim naechsten `render`.
|
||
pub fn set_cut_caps(&mut self, device: &wgpu::Device, verts: &[f32]) {
|
||
if verts.is_empty() {
|
||
self.caps = None;
|
||
return;
|
||
}
|
||
let vbo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||
label: Some("caps.vbo"),
|
||
contents: bytemuck::cast_slice(verts),
|
||
usage: wgpu::BufferUsages::VERTEX,
|
||
});
|
||
self.caps = Some(CapBuffers {
|
||
vbo,
|
||
vertex_count: (verts.len() / CAP_FLOATS_PER_VERTEX) as u32,
|
||
});
|
||
}
|
||
|
||
/// Setzt/loescht die Schnitt-Umrisslinien (Cut-Cap-Kanten, P2). `verts` =
|
||
/// interleaved `[px,py,pz, r,g,b, ...]` (world-Meter + Farbe, TriangleList von
|
||
/// Ribbon-Quads, `CUT_LINE_FLOATS_PER_VERTEX`), wie `section_fill::
|
||
/// build_cut_cap_lines` liefert. Leeres Slice loescht die Linien. Wirkt erst
|
||
/// beim naechsten `render`.
|
||
pub fn set_cut_lines(&mut self, device: &wgpu::Device, verts: &[f32]) {
|
||
if verts.is_empty() {
|
||
self.cut_lines = None;
|
||
return;
|
||
}
|
||
let vbo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||
label: Some("cut_lines.vbo"),
|
||
contents: bytemuck::cast_slice(verts),
|
||
usage: wgpu::BufferUsages::VERTEX,
|
||
});
|
||
self.cut_lines = Some(LineBuffers {
|
||
vbo,
|
||
vertex_count: (verts.len() / CUT_LINE_FLOATS_PER_VERTEX) as u32,
|
||
});
|
||
}
|
||
|
||
/// Komfort: Schnittebene setzen UND die Cap-Geometrie aus den (gecachten)
|
||
/// Waenden/Decken neu bauen — bei `active == false` werden Uniform-Flag und
|
||
/// Caps geloescht. Buendelt `set_section_plane` + `build_cut_caps` +
|
||
/// `set_cut_caps`, damit `web.rs` nur EINEN Aufruf braucht.
|
||
pub fn update_section(
|
||
&mut self,
|
||
device: &wgpu::Device,
|
||
active: bool,
|
||
point: [f32; 3],
|
||
normal: [f32; 3],
|
||
walls: &[WallInput],
|
||
slabs: &[SlabInput],
|
||
) {
|
||
self.set_section_plane(active, point, normal);
|
||
if active {
|
||
let plane = SectionPlane::new(point, normal);
|
||
let caps = build_cut_caps(&plane, walls, slabs);
|
||
self.set_cut_caps(device, &caps);
|
||
// P2: die Schnitt-Umrisslinien (Fugen-/Materiallinien) je ueberlebendem
|
||
// Cut-Cap-Rechteck aus DERSELBEN, dominanz-verschnittenen Rechteck-Menge.
|
||
let lines = build_cut_cap_lines(&plane, walls, slabs);
|
||
self.set_cut_lines(device, &lines);
|
||
} else {
|
||
self.set_cut_caps(device, &[]);
|
||
self.set_cut_lines(device, &[]);
|
||
}
|
||
}
|
||
|
||
/// Laedt die Material-Farbkarten fuer den Stil `Textured` als Textur-ARRAY hoch.
|
||
/// `rgba` = dicht gepackte RGBA8-Bytes von `layer_count` Ebenen, jede exakt
|
||
/// `TEXTURE_SIZE × TEXTURE_SIZE` (der Aufrufer skaliert/dekodiert die Bilder
|
||
/// JS-seitig, siehe `web::set_material_textures`/`Wasm3DViewport`). Baut ein
|
||
/// neues Array `[Schachbrett (Ebene 0), Material0 (Ebene 1), …]`, laedt es hoch
|
||
/// und ersetzt die group-1-Bind-Group. Ein Wand-Band verweist per
|
||
/// `WallInput::material_index` (1-basiert) auf seine Ebene; Baender ohne
|
||
/// Material (`-1`) bleiben beim Schachbrett (Ebene 0). `layer_count == 0` (oder
|
||
/// leere/zu kleine Daten) setzt auf das reine Schachbrett-Array zurueck.
|
||
pub fn set_material_textures(
|
||
&mut self,
|
||
device: &wgpu::Device,
|
||
queue: &wgpu::Queue,
|
||
rgba: &[u8],
|
||
layer_count: u32,
|
||
) {
|
||
let bytes_per_layer = (TEXTURE_SIZE * TEXTURE_SIZE * 4) as usize;
|
||
// Ungueltige/leere Eingabe -> reines Schachbrett-Array (1 Ebene).
|
||
let valid = layer_count > 0 && rgba.len() >= layer_count as usize * bytes_per_layer;
|
||
let material_layers = if valid { layer_count } else { 0 };
|
||
let total_layers = material_layers + 1; // Ebene 0 = Schachbrett
|
||
|
||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||
label: Some("materials.texture_array"),
|
||
size: wgpu::Extent3d {
|
||
width: TEXTURE_SIZE,
|
||
height: TEXTURE_SIZE,
|
||
depth_or_array_layers: total_layers,
|
||
},
|
||
mip_level_count: 1,
|
||
sample_count: 1,
|
||
dimension: wgpu::TextureDimension::D2,
|
||
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||
view_formats: &[],
|
||
});
|
||
|
||
// Ebene 0: Schachbrett-Fallback.
|
||
let checker = build_checker_texture();
|
||
let write_layer = |layer: u32, data: &[u8]| {
|
||
queue.write_texture(
|
||
wgpu::TexelCopyTextureInfo {
|
||
texture: &texture,
|
||
mip_level: 0,
|
||
origin: wgpu::Origin3d {
|
||
x: 0,
|
||
y: 0,
|
||
z: layer,
|
||
},
|
||
aspect: wgpu::TextureAspect::All,
|
||
},
|
||
data,
|
||
wgpu::TexelCopyBufferLayout {
|
||
offset: 0,
|
||
bytes_per_row: Some(4 * TEXTURE_SIZE),
|
||
rows_per_image: Some(TEXTURE_SIZE),
|
||
},
|
||
wgpu::Extent3d {
|
||
width: TEXTURE_SIZE,
|
||
height: TEXTURE_SIZE,
|
||
depth_or_array_layers: 1,
|
||
},
|
||
);
|
||
};
|
||
write_layer(0, &checker);
|
||
for i in 0..material_layers {
|
||
let start = i as usize * bytes_per_layer;
|
||
write_layer(i + 1, &rgba[start..start + bytes_per_layer]);
|
||
}
|
||
|
||
let view = texture.create_view(&wgpu::TextureViewDescriptor {
|
||
dimension: Some(wgpu::TextureViewDimension::D2Array),
|
||
..Default::default()
|
||
});
|
||
self.texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||
label: Some("materials.bind"),
|
||
layout: &self.texture_bind_group_layout,
|
||
entries: &[
|
||
wgpu::BindGroupEntry {
|
||
binding: 0,
|
||
resource: wgpu::BindingResource::TextureView(&view),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 1,
|
||
resource: wgpu::BindingResource::Sampler(&self.texture_sampler),
|
||
},
|
||
],
|
||
});
|
||
self.texture = texture;
|
||
self.material_layer_count = total_layers;
|
||
// Ebene 0 (Schachbrett) ist bereits geladen -> den Lazy-Upload ueberspringen.
|
||
self.texture_uploaded = true;
|
||
}
|
||
|
||
/// Stellt sicher, dass ein Tiefenpuffer passend zur Ziel-Groesse existiert.
|
||
fn ensure_depth(&mut self, device: &wgpu::Device, w: u32, h: u32) {
|
||
let ok = matches!(&self.depth, Some(d) if d.width == w && d.height == h);
|
||
if ok {
|
||
return;
|
||
}
|
||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||
label: Some("depth"),
|
||
size: wgpu::Extent3d {
|
||
width: w.max(1),
|
||
height: h.max(1),
|
||
depth_or_array_layers: 1,
|
||
},
|
||
mip_level_count: 1,
|
||
// Muss zur Sample-Zahl der Farb-/Pipeline-Ziele passen (MSAA).
|
||
sample_count: SAMPLE_COUNT,
|
||
dimension: wgpu::TextureDimension::D2,
|
||
format: DEPTH_FORMAT,
|
||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||
view_formats: &[],
|
||
});
|
||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||
self.depth = Some(DepthTarget {
|
||
view,
|
||
width: w.max(1),
|
||
height: h.max(1),
|
||
});
|
||
}
|
||
|
||
/// Stellt sicher, dass eine multisampled Farb-Zwischentextur passend zur
|
||
/// Ziel-Groesse existiert (wird jeden Frame in die Surface-View aufgeloest).
|
||
fn ensure_msaa(&mut self, device: &wgpu::Device, w: u32, h: u32) {
|
||
let ok = matches!(&self.msaa, Some(m) if m.width == w && m.height == h);
|
||
if ok {
|
||
return;
|
||
}
|
||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||
label: Some("msaa.color"),
|
||
size: wgpu::Extent3d {
|
||
width: w.max(1),
|
||
height: h.max(1),
|
||
depth_or_array_layers: 1,
|
||
},
|
||
mip_level_count: 1,
|
||
sample_count: SAMPLE_COUNT,
|
||
dimension: wgpu::TextureDimension::D2,
|
||
format: self.color_format,
|
||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||
view_formats: &[],
|
||
});
|
||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||
self.msaa = Some(MsaaTarget {
|
||
view,
|
||
width: w.max(1),
|
||
height: h.max(1),
|
||
});
|
||
}
|
||
|
||
/// Zeichnet einen Frame und loest ihn in `target` auf (Surface- oder
|
||
/// Offscreen-Textur). Intern wird in eine multisampled Zwischentextur (MSAA)
|
||
/// gerendert und am Ende der Pass in `target` aufgeloest. Die Kamera liefert
|
||
/// die View-Projektions-Matrix; `viewport` bestimmt das Seitenverhaeltnis und
|
||
/// die Tiefen-/MSAA-Puffer-Groesse.
|
||
pub fn render(
|
||
&mut self,
|
||
device: &wgpu::Device,
|
||
queue: &wgpu::Queue,
|
||
target: &wgpu::TextureView,
|
||
camera: &Camera,
|
||
viewport: (u32, u32),
|
||
) {
|
||
let (w, h) = viewport;
|
||
let aspect = if h > 0 { w as f32 / h as f32 } else { 1.0 };
|
||
let vp: Mat4 = view_projection(camera, aspect);
|
||
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();
|
||
// `mode.z` = Anzahl gueltiger Textur-Array-Ebenen (Klemmung im Textur-Shader).
|
||
self.globals.mode[2] = self.material_layer_count as f32;
|
||
queue.write_buffer(&self.uniform, 0, bytemuck::bytes_of(&self.globals));
|
||
|
||
// Prozedurale Schachbrett-Textur einmalig hochladen (lazy, weil `new` keine
|
||
// Queue bekommt — Signatur unveraendert). Danach steht sie fuer den Stil
|
||
// `Textured` bereit; kostet ab dem zweiten Frame nichts mehr.
|
||
if !self.texture_uploaded {
|
||
let texel_data = build_checker_texture();
|
||
queue.write_texture(
|
||
wgpu::TexelCopyTextureInfo {
|
||
texture: &self.texture,
|
||
mip_level: 0,
|
||
origin: wgpu::Origin3d::ZERO,
|
||
aspect: wgpu::TextureAspect::All,
|
||
},
|
||
&texel_data,
|
||
wgpu::TexelCopyBufferLayout {
|
||
offset: 0,
|
||
bytes_per_row: Some(4 * TEXTURE_SIZE),
|
||
rows_per_image: Some(TEXTURE_SIZE),
|
||
},
|
||
wgpu::Extent3d {
|
||
width: TEXTURE_SIZE,
|
||
height: TEXTURE_SIZE,
|
||
depth_or_array_layers: 1,
|
||
},
|
||
);
|
||
self.texture_uploaded = true;
|
||
}
|
||
|
||
self.ensure_depth(device, w, h);
|
||
self.ensure_msaa(device, w, h);
|
||
let depth_view = &self.depth.as_ref().unwrap().view;
|
||
let msaa_view = &self.msaa.as_ref().unwrap().view;
|
||
|
||
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||
label: Some("3d.encoder"),
|
||
});
|
||
{
|
||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||
label: Some("3d.pass"),
|
||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||
// In die multisampled Zwischentextur rendern; wgpu loest sie am
|
||
// Ende der Pass automatisch in `target` (Surface-View) auf.
|
||
view: msaa_view,
|
||
// Ziel ist eine 2D-Textur (kein 3D-Volumen) -> kein Depth-Slice.
|
||
depth_slice: None,
|
||
resolve_target: Some(target),
|
||
ops: wgpu::Operations {
|
||
load: wgpu::LoadOp::Clear(self.clear_color),
|
||
// Der multisampled Inhalt selbst wird nach dem Aufloesen nicht
|
||
// mehr gebraucht — Discard spart Bandbreite.
|
||
store: wgpu::StoreOp::Discard,
|
||
},
|
||
})],
|
||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||
view: depth_view,
|
||
depth_ops: Some(wgpu::Operations {
|
||
load: wgpu::LoadOp::Clear(1.0),
|
||
store: wgpu::StoreOp::Store,
|
||
}),
|
||
stencil_ops: None,
|
||
}),
|
||
timestamp_writes: None,
|
||
occlusion_query_set: 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 self.style == RenderStyle::Textured {
|
||
// Texturierter Pfad: eigene Pipeline + zweiter Vertex-Puffer
|
||
// ([pos, normal, uv]) + zusaetzliche Textur-Bind-Group (group 1).
|
||
// Globals bleiben group 0. Fehlt der texturierte Puffer (kein
|
||
// Mesh geladen), wird nichts gezeichnet.
|
||
if let Some(t) = &self.textured_mesh {
|
||
pass.set_pipeline(&self.textured_pipeline);
|
||
pass.set_bind_group(0, &self.bind_group, &[]);
|
||
pass.set_bind_group(1, &self.texture_bind_group, &[]);
|
||
pass.set_vertex_buffer(0, t.vbo.slice(..));
|
||
pass.set_vertex_buffer(1, t.layer_vbo.slice(..));
|
||
pass.set_index_buffer(t.ibo.slice(..), wgpu::IndexFormat::Uint32);
|
||
pass.draw_indexed(0..t.index_count, 0, 0..1);
|
||
}
|
||
} else if let Some(m) = &self.mesh {
|
||
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_vertex_buffer(0, m.vbo.slice(..));
|
||
pass.set_index_buffer(m.ibo.slice(..), wgpu::IndexFormat::Uint32);
|
||
pass.draw_indexed(0..m.index_count, 0, 0..1);
|
||
}
|
||
}
|
||
|
||
// 1b) Schnittflaechen-Kappen (Cut-Caps) NACH den Flaechen, VOR den
|
||
// Kanten: sie fuellen die an der Schnittebene weggeschnittene Wunde
|
||
// mit der Diagonalschraffur (CAP_WGSL). Werden selbst NICHT gekappt.
|
||
// Nur vorhanden, wenn der Live-Schnitt aktiv ist (set_cut_caps).
|
||
if let Some(c) = &self.caps {
|
||
pass.set_pipeline(&self.cap_pipeline);
|
||
pass.set_bind_group(0, &self.bind_group, &[]);
|
||
pass.set_vertex_buffer(0, c.vbo.slice(..));
|
||
pass.draw(0..c.vertex_count, 0..1);
|
||
}
|
||
|
||
// 1c) Schnitt-Umrisslinien (Cut-Cap-Kanten, P2) NACH den Caps: die
|
||
// Material-/Fugenlinien je ueberlebendem Cut-Rechteck (Ribbon-
|
||
// TriangleList, eigener Stil je Schicht). Ohne Tiefentest (Always)
|
||
// liegen sie sichtbar auf den Caps; der Kamera-Lift in
|
||
// `build_cut_cap_lines` haelt sie vor der Ebenen-Kappung.
|
||
if let Some(cl) = &self.cut_lines {
|
||
pass.set_pipeline(&self.cut_line_pipeline);
|
||
pass.set_bind_group(0, &self.bind_group, &[]);
|
||
pass.set_vertex_buffer(0, cl.vbo.slice(..));
|
||
pass.draw(0..cl.vertex_count, 0..1);
|
||
}
|
||
|
||
// 2) Modell-Kanten (wireframe/hidden) — LineList-Pipeline (wie Grid).
|
||
// 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 let Some(g) = &self.grid {
|
||
pass.set_pipeline(&self.grid_pipeline);
|
||
pass.set_bind_group(0, &self.bind_group, &[]);
|
||
pass.set_vertex_buffer(0, g.vbo.slice(..));
|
||
pass.draw(0..g.vertex_count, 0..1);
|
||
}
|
||
}
|
||
|
||
// 4) Auswahl-Highlight-Linien als ALLERLETZTES — ohne Tiefentest
|
||
// (`highlight_pipeline`: depth_compare Always, kein Depth-Write),
|
||
// damit die Auswahl immer sichtbar obenauf liegt und auch hinter
|
||
// Waenden durchscheint. Nur wenn per `set_highlight_lines` gesetzt.
|
||
if let Some(hl) = &self.highlight {
|
||
pass.set_pipeline(&self.highlight_pipeline);
|
||
pass.set_bind_group(0, &self.bind_group, &[]);
|
||
pass.set_vertex_buffer(0, hl.vbo.slice(..));
|
||
pass.draw(0..hl.vertex_count, 0..1);
|
||
}
|
||
}
|
||
queue.submit(std::iter::once(encoder.finish()));
|
||
}
|
||
}
|