Nordstern-3D: Boden-Referenzraster + bessere Kamera-Steuerung

- Boden-Grid-Fläche auf y = baseElevation des aktiven Geschosses,
  ein-/ausschaltbar (Overlay-Button im Viewport, State in viewSlice
  grid3dVisible). Eigene LineList-Pipeline in der Engine (grid.rs,
  GRID_WGSL, geteilte View-Projection/Depth mit der Mesh-Pipeline),
  neue set_ground_grid(visible, elevation, extent, spacing)-Methode.
  Minor-Linien dezent, jede 5. als Major betont.
- Kamera-Steuerung überarbeitet (vorher nur Mitte=Orbit, Pan auf
  Shift+Mitte versteckt): Links=Orbit, Mitte/Rechts=Pan (bewegen),
  Rad=Zoom zum Cursor. Laptop-freundlich, konsistent mit der
  2D-Plan-Navigation (dort Mitte=Pan).
This commit is contained in:
2026-07-04 05:54:16 +02:00
parent d0b94a75ec
commit d4065c5a41
12 changed files with 567 additions and 21 deletions
+116 -1
View File
@@ -12,9 +12,10 @@
use bytemuck::{Pod, Zeroable};
use wgpu::util::DeviceExt;
use crate::grid::{build_ground_grid, GRID_FLOATS_PER_VERTEX};
use crate::math::{view_projection, Mat4};
use crate::mesh::{build_scene_mesh, build_walls_mesh};
use crate::shaders::MESH_WGSL;
use crate::shaders::{GRID_WGSL, MESH_WGSL};
use crate::types::{Camera, Mesh, MeshInput, SlabInput, WallInput, FLOATS_PER_VERTEX};
/// Tiefenformat des Z-Puffers (32 Bit Float, ueberall verfuegbar).
@@ -68,14 +69,30 @@ struct MeshBuffers {
index_count: u32,
}
/// GPU-seitiger Vertex-Buffer des Bodengitters (LineList, kein Indexpuffer).
struct GridBuffers {
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,
/// Eigene, schlanke Linien-Pipeline (LineList, unlit) fuer das Bodengitter.
/// Teilt sich das `Globals`-Uniform/Bind-Group (nur View-Projektion) mit der
/// Mesh-Pipeline und rendert in denselben Pass/Tiefenpuffer.
grid_pipeline: wgpu::RenderPipeline,
bind_group: wgpu::BindGroup,
uniform: wgpu::Buffer,
mesh: Option<MeshBuffers>,
/// Bodengitter-Vertices (None = nicht erzeugt). Wird nur gezeichnet, wenn
/// zusaetzlich `grid_visible` gesetzt ist.
grid: Option<GridBuffers>,
/// Ob das Bodengitter im naechsten `render` gezeichnet wird. Default false —
/// bestehende Aufrufer ohne `set_ground_grid` bekommen KEIN Gitter.
grid_visible: bool,
depth: Option<DepthTarget>,
msaa: Option<MsaaTarget>,
/// Farbformat des Render-Ziels (Surface), fuer die MSAA-Zwischentextur.
@@ -180,6 +197,60 @@ impl Renderer {
cache: None,
});
// 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],
};
let grid_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("grid.pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &grid_module,
entry_point: Some("vs_main"),
buffers: &[grid_vertex_layout],
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(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,
});
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("globals.buffer"),
size: std::mem::size_of::<Globals>() as u64,
@@ -197,9 +268,12 @@ impl Renderer {
Self {
pipeline,
grid_pipeline,
bind_group,
uniform,
mesh: None,
grid: None,
grid_visible: false,
depth: None,
msaa: None,
color_format,
@@ -274,6 +348,36 @@ impl Renderer {
self.globals.mode[0] = if white { 1.0 } else { 0.0 };
}
/// 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(GridBuffers {
vbo,
vertex_count: (verts.len() / GRID_FLOATS_PER_VERTEX) as u32,
});
}
/// 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);
@@ -396,6 +500,17 @@ impl Renderer {
pass.set_index_buffer(m.ibo.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..m.index_count, 0, 0..1);
}
// Bodengitter NACH dem Modell zeichnen (gemeinsamer Tiefenpuffer ->
// das Modell verdeckt das Gitter korrekt). 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);
}
}
}
queue.submit(std::iter::once(encoder.finish()));
}