render3d: 4× MSAA im wgpu-Renderer — glatte 3D-Kanten

Multisampled Farbtarget (SAMPLE_COUNT=4) + resolve_target in die Surface-View je
Frame; Depth-Textur-sample_count synchron auf 4; MSAA-/Depth-Textur werden bei
Resize gemeinsam neu erzeugt; multisample.count=4 auf der Render-Pipeline. Nur
gpu.rs — mesh/section/openings unberührt.

Gates: cargo check (window) + wasm32 --features web grün, cargo test 32/32,
--features render 33/33.
This commit is contained in:
2026-07-03 19:23:21 +02:00
parent d30f79e162
commit eaf6d8ed7b
+69 -9
View File
@@ -20,6 +20,11 @@ use crate::types::{Camera, Mesh, SlabInput, WallInput, FLOATS_PER_VERTEX};
/// 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;
/// 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 /// Uniform-Block, 1:1 zum WGSL-`Globals`-Struct. std140-kompatibel (mat4/vec4 auf
/// 16 Byte ausgerichtet). /// 16 Byte ausgerichtet).
#[repr(C)] #[repr(C)]
@@ -69,6 +74,9 @@ pub struct Renderer {
uniform: wgpu::Buffer, uniform: wgpu::Buffer,
mesh: Option<MeshBuffers>, mesh: Option<MeshBuffers>,
depth: Option<DepthTarget>, depth: Option<DepthTarget>,
msaa: Option<MsaaTarget>,
/// Farbformat des Render-Ziels (Surface), fuer die MSAA-Zwischentextur.
color_format: wgpu::TextureFormat,
globals: Globals, globals: Globals,
/// Loeschfarbe (Hintergrund). Default heller Grauton wie die three.js-Sicht. /// Loeschfarbe (Hintergrund). Default heller Grauton wie die three.js-Sicht.
pub clear_color: wgpu::Color, pub clear_color: wgpu::Color,
@@ -81,6 +89,14 @@ struct DepthTarget {
height: 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 { impl Renderer {
/// Erzeugt Pipeline + Layout fuer ein gegebenes Farbformat (Surface). /// Erzeugt Pipeline + Layout fuer ein gegebenes Farbformat (Surface).
pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self { pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
@@ -152,7 +168,11 @@ impl Renderer {
stencil: wgpu::StencilState::default(), stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(), bias: wgpu::DepthBiasState::default(),
}), }),
multisample: wgpu::MultisampleState::default(), multisample: wgpu::MultisampleState {
count: SAMPLE_COUNT,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None, multiview_mask: None,
cache: None, cache: None,
}); });
@@ -178,6 +198,8 @@ impl Renderer {
uniform, uniform,
mesh: None, mesh: None,
depth: None, depth: None,
msaa: None,
color_format,
globals: Globals::default(), globals: Globals::default(),
// #f5f5f5 heller Hintergrund (wie der 2D-Grundriss). // #f5f5f5 heller Hintergrund (wie der 2D-Grundriss).
clear_color: wgpu::Color { clear_color: wgpu::Color {
@@ -250,7 +272,8 @@ impl Renderer {
depth_or_array_layers: 1, depth_or_array_layers: 1,
}, },
mip_level_count: 1, mip_level_count: 1,
sample_count: 1, // Muss zur Sample-Zahl der Farb-/Pipeline-Ziele passen (MSAA).
sample_count: SAMPLE_COUNT,
dimension: wgpu::TextureDimension::D2, dimension: wgpu::TextureDimension::D2,
format: DEPTH_FORMAT, format: DEPTH_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT, usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
@@ -264,14 +287,45 @@ impl Renderer {
}); });
} }
/// Zeichnet einen Frame in `view` (Surface- oder Offscreen-Textur). Die Kamera /// Stellt sicher, dass eine multisampled Farb-Zwischentextur passend zur
/// liefert die View-Projektions-Matrix; `viewport` bestimmt das Seitenverhaeltnis /// Ziel-Groesse existiert (wird jeden Frame in die Surface-View aufgeloest).
/// und die Tiefenpuffer-Groesse. 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( pub fn render(
&mut self, &mut self,
device: &wgpu::Device, device: &wgpu::Device,
queue: &wgpu::Queue, queue: &wgpu::Queue,
view: &wgpu::TextureView, target: &wgpu::TextureView,
camera: &Camera, camera: &Camera,
viewport: (u32, u32), viewport: (u32, u32),
) { ) {
@@ -282,7 +336,9 @@ impl Renderer {
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);
self.ensure_msaa(device, w, h);
let depth_view = &self.depth.as_ref().unwrap().view; 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 { let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("3d.encoder"), label: Some("3d.encoder"),
@@ -291,13 +347,17 @@ impl Renderer {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("3d.pass"), label: Some("3d.pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment { color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view, // 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. // Ziel ist eine 2D-Textur (kein 3D-Volumen) -> kein Depth-Slice.
depth_slice: None, depth_slice: None,
resolve_target: None, resolve_target: Some(target),
ops: wgpu::Operations { ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(self.clear_color), load: wgpu::LoadOp::Clear(self.clear_color),
store: wgpu::StoreOp::Store, // Der multisampled Inhalt selbst wird nach dem Aufloesen nicht
// mehr gebraucht — Discard spart Bandbreite.
store: wgpu::StoreOp::Discard,
}, },
})], })],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {