render3d Textur-Spike: RenderStyle::Textured real (Bild-Textur auf Waenden)

- Zweiter Vertex-Pfad [pos,normal,uv] via build_walls_mesh_textured, ABGELEITET
  aus dem fertigen Mesh (Positionen/Normalen/Indizes 1:1) -> Alt-Pfad
  [pos,normal,color] bitgleich; per Regressionstest belegt.
- Prozedurale 256x256-Schachbrett-Textur (kein Asset, kein image-Crate),
  Sampler Linear/Repeat, Textur-Bind-Group group 1 (Globals bleibt group 0).
- MESH_TEXTURED_WGSL: gleiche Beleuchtung wie MESH_WGSL, Albedo aus textureSample.
  Pipeline in Depth/MSAA/Color-Target bitidentisch zur Haupt-Pipeline.
- UV planar in Metern: Mantel u=entlang Achse/v=Hoehe, Deckel u=x/v=z;
  weltraumstabil, keine Verzerrung an Gehrungen. 1 Kachel = 1 m.
- spike3d: Taste T schaltet Shaded <-> Textured zur Laufzeit (kein Re-Meshing).
- Feature-gegatet, Default-Build/-Darstellung unveraendert. cargo test 58 (default)
  / 59 (--features render, inkl. naga-Test MESH_TEXTURED_WGSL) gruen.
This commit is contained in:
2026-07-05 00:30:34 +02:00
parent ecefe61611
commit 0ca3b1dd57
6 changed files with 565 additions and 15 deletions
+283 -5
View File
@@ -15,11 +15,13 @@ 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_walls_mesh};
use crate::mesh::{build_scene_mesh, build_walls_mesh, textured_from_mesh};
use crate::section::SectionPlane;
use crate::section_fill::{build_cut_caps, CAP_FLOATS_PER_VERTEX};
use crate::shaders::{CAP_WGSL, GRID_WGSL, MESH_WGSL};
use crate::types::{Camera, Mesh, MeshInput, SlabInput, WallInput, FLOATS_PER_VERTEX};
use crate::shaders::{CAP_WGSL, GRID_WGSL, MESH_TEXTURED_WGSL, MESH_WGSL};
use crate::types::{
Camera, Mesh, MeshInput, SlabInput, 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
@@ -30,7 +32,9 @@ pub enum RenderStyle {
Shaded,
/// Clay-Look: einheitlich helles Material, beleuchtet.
White,
/// Mangels Textur-Pipeline VORERST identisch zu `Shaded` behandelt (kein Fake).
/// 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,
@@ -132,6 +136,55 @@ struct MeshBuffers {
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,
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).
@@ -172,9 +225,29 @@ pub struct Renderer {
/// 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,
/// 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 der prozeduralen Test-Textur (group 1): Texture-View + Sampler.
/// Konstant ueber die Lebensdauer des Renderers (eine Textur fuer alle Waende).
texture_bind_group: wgpu::BindGroup,
/// Die (prozedurale) Textur selbst — nur festgehalten, um ihre Texel beim
/// ersten `render` einmalig hochzuladen (`texture_uploaded`), da `new` keine
/// Queue bekommt.
texture: wgpu::Texture,
/// Ob die Textur-Texel schon per `queue.write_texture` geladen wurden. Beim
/// ersten `render` einmalig gesetzt (lazy Upload).
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.
@@ -462,6 +535,144 @@ impl Renderer {
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::D2,
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],
};
// 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],
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.
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("checker.texture"),
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::default());
// Repeat (weltmassstaebliches UV kachelt) + Linear (weiche Filterung).
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("checker.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("texture.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(&sampler),
},
],
});
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("globals.buffer"),
size: std::mem::size_of::<Globals>() as u64,
@@ -483,9 +694,14 @@ impl Renderer {
grid_pipeline,
highlight_pipeline,
cap_pipeline,
textured_pipeline,
texture_bind_group,
texture,
texture_uploaded: false,
bind_group,
uniform,
mesh: None,
textured_mesh: None,
edges: None,
grid: None,
highlight: None,
@@ -529,6 +745,7 @@ impl Renderer {
fn upload_mesh(&mut self, device: &wgpu::Device, mesh: Mesh) {
if mesh.indices.is_empty() {
self.mesh = None;
self.textured_mesh = None;
self.edges = None;
return;
}
@@ -564,6 +781,27 @@ impl Renderer {
ibo,
index_count: mesh.indices.len() as u32,
});
// Texturiertes Pendant AUS DEMSELBEN `Mesh` ableiten (planare Welt-UV je
// Vertex statt Farbe, siehe `textured_from_mesh`) und in einen zweiten
// Vertex-Puffer hochladen. So bleibt die Geometrie bitgleich zum Alt-Pfad,
// und der Stil `Textured` hat ohne Re-Meshing seine Puffer bereit.
let tex_mesh = textured_from_mesh(&mesh);
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,
});
self.textured_mesh = Some(TexturedMeshBuffers {
vbo: tvbo,
ibo: tibo,
index_count: tex_mesh.indices.len() as u32,
});
}
/// Setzt die Richtung ZUM Directional-Light (Sonne, world). Das hemisphaerische
@@ -783,6 +1021,33 @@ impl Renderer {
self.globals.mode[0] = self.style.mesh_mode();
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;
@@ -825,7 +1090,20 @@ impl Renderer {
// Hidden-Line-Stil die leicht nach hinten gebiaste Pipeline, damit
// die folgenden Kanten sauber obenauf liegen.
if self.style.draws_faces() {
if let Some(m) = &self.mesh {
if 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_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 {