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:
@@ -13,13 +13,14 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use render3d::gpu::Renderer;
|
||||
use render3d::gpu::{RenderStyle, Renderer};
|
||||
use render3d::math::orbit_eye;
|
||||
use render3d::types::{Camera, Projection, WallInput};
|
||||
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
|
||||
use winit::event::{ElementState, KeyEvent, MouseButton, MouseScrollDelta, WindowEvent};
|
||||
use winit::event_loop::{ActiveEventLoop, EventLoop};
|
||||
use winit::keyboard::{KeyCode, PhysicalKey};
|
||||
use winit::window::{Window, WindowId};
|
||||
|
||||
/// Demo-Szene: ein rechteckiger Raum (4 Aussenwaende) plus eine Innenwand. Achsen
|
||||
@@ -196,6 +197,9 @@ struct App {
|
||||
orbit: Orbit,
|
||||
dragging: bool,
|
||||
last_cursor: (f64, f64),
|
||||
/// Darstellung: false = Shaded (Default, unveraendert), true = Textured
|
||||
/// (prozedurales Schachbrett auf den Wandflaechen). Per Taste `T` umschaltbar.
|
||||
textured: bool,
|
||||
}
|
||||
|
||||
impl ApplicationHandler for App {
|
||||
@@ -223,6 +227,27 @@ impl ApplicationHandler for App {
|
||||
state.resize(size.width, size.height);
|
||||
state.window.request_redraw();
|
||||
}
|
||||
WindowEvent::KeyboardInput {
|
||||
event:
|
||||
KeyEvent {
|
||||
physical_key: PhysicalKey::Code(KeyCode::KeyT),
|
||||
state: ElementState::Pressed,
|
||||
repeat: false,
|
||||
..
|
||||
},
|
||||
..
|
||||
} => {
|
||||
// `T` schaltet Shaded <-> Textured um (Laufzeit, kein Re-Meshing:
|
||||
// der Renderer haelt beide Vertex-Puffer bereit).
|
||||
self.textured = !self.textured;
|
||||
let style = if self.textured {
|
||||
RenderStyle::Textured
|
||||
} else {
|
||||
RenderStyle::Shaded
|
||||
};
|
||||
state.renderer.set_render_style(style);
|
||||
state.window.request_redraw();
|
||||
}
|
||||
WindowEvent::MouseInput { state: s, button, .. } => {
|
||||
if button == MouseButton::Left {
|
||||
self.dragging = s == ElementState::Pressed;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -38,23 +38,27 @@ pub use math::{
|
||||
view_matrix, view_projection, Mat4,
|
||||
};
|
||||
pub use mesh::{
|
||||
append_context_mesh, build_model_mesh, build_scene_mesh, build_walls_mesh, extrude_slab,
|
||||
extrude_wall, triangulate,
|
||||
append_context_mesh, build_model_mesh, build_scene_mesh, build_walls_mesh,
|
||||
build_walls_mesh_textured, extrude_slab, extrude_wall, textured_from_mesh, triangulate,
|
||||
};
|
||||
pub use section::{
|
||||
cut_section, ComponentKind, ComponentRef, CutPolygon, SectionEdge, SectionOutput, SectionPlane,
|
||||
};
|
||||
pub use types::{
|
||||
Camera, CameraPreset, Mesh, MeshInput, MeshKind, Point2, Projection, Rgb, SlabInput, WallInput,
|
||||
FLOATS_PER_VERTEX,
|
||||
Camera, CameraPreset, Mesh, MeshInput, MeshKind, Point2, Projection, Rgb, SlabInput,
|
||||
TexturedMesh, WallInput, FLOATS_PER_VERTEX, TEXTURED_FLOATS_PER_VERTEX,
|
||||
};
|
||||
|
||||
// --- Tests: Mesh-Erzeugung (Muster wie render2d/tessellate) -------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::mesh::{build_walls_mesh, extrude_wall, INDICES_PER_BOX, VERTS_PER_BOX};
|
||||
use super::types::{Hole, Mesh, Opening, WallInput, WallLayer, FLOATS_PER_VERTEX};
|
||||
use super::mesh::{
|
||||
build_walls_mesh, build_walls_mesh_textured, extrude_wall, INDICES_PER_BOX, VERTS_PER_BOX,
|
||||
};
|
||||
use super::types::{
|
||||
Hole, Mesh, Opening, WallInput, WallLayer, FLOATS_PER_VERTEX, TEXTURED_FLOATS_PER_VERTEX,
|
||||
};
|
||||
|
||||
/// Bequemer Bau einer achsparallelen Wand entlang +X.
|
||||
fn wall_x(len: f32, thickness: f32, height: f32) -> WallInput {
|
||||
@@ -80,6 +84,67 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn texturierter_pfad_ist_geometrisch_deckungsgleich_mit_shaded() {
|
||||
// Der texturierte Wand-Pfad (`build_walls_mesh_textured`, [pos,normal,uv])
|
||||
// muss GEOMETRISCH bitgleich zum Alt-Pfad (`build_walls_mesh`,
|
||||
// [pos,normal,color]) sein: gleiche Vertex-/Index-Zahl und -Reihenfolge,
|
||||
// identische Positionen und Normalen. Nur der letzte Kanal (Farbe -> UV)
|
||||
// unterscheidet sich. So ist belegt, dass der additive UV-Kanal den
|
||||
// Alt-Pfad nicht beruehrt.
|
||||
let walls = [wall_x(3.0, 0.2, 2.5)];
|
||||
let shaded = build_walls_mesh(&walls);
|
||||
let tex = build_walls_mesh_textured(&walls);
|
||||
assert_eq!(tex.indices, shaded.indices, "Index-Puffer identisch");
|
||||
assert_eq!(
|
||||
tex.vertex_count(),
|
||||
shaded.vertex_count(),
|
||||
"gleiche Vertex-Zahl"
|
||||
);
|
||||
for i in 0..shaded.vertex_count() {
|
||||
let sb = i * FLOATS_PER_VERTEX;
|
||||
let tb = i * TEXTURED_FLOATS_PER_VERTEX;
|
||||
// Position (0..3) und Normale (3..6) bitgleich uebernommen.
|
||||
for k in 0..6 {
|
||||
assert_eq!(
|
||||
shaded.verts[sb + k],
|
||||
tex.verts[tb + k],
|
||||
"Vertex {i} Kanal {k}: pos/normal muss identisch sein"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn texturierte_uv_ist_weltmassstaeblich() {
|
||||
// UV-Massstab: `u`/`v` in Metern (1 Kachel = 1 m). Fuer eine Wand entlang +X
|
||||
// liegt die +Z-Mantelflaeche bei Normale (0,0,1); dort ist die Tangente
|
||||
// t=(-n.z,0,n.x)=(-1,0,0), also u = -x und v = Hoehe. Die Deckflaeche
|
||||
// (Normale +Y) projiziert auf u=x, v=z. Wir pruefen, dass es UEBERHAUPT
|
||||
// Vertices mit v == Wandhoehe (2.5) bzw. v == 0 gibt (Hoehe wandert als v
|
||||
// mit) — belegt den weltmassstaeblichen Hoehen-Kanal.
|
||||
let tex = build_walls_mesh_textured(&[wall_x(3.0, 0.2, 2.5)]);
|
||||
let n = tex.vertex_count();
|
||||
let mut saw_top = false;
|
||||
let mut saw_bottom = false;
|
||||
for i in 0..n {
|
||||
let b = i * TEXTURED_FLOATS_PER_VERTEX;
|
||||
let ny = tex.verts[b + 4];
|
||||
let v = tex.verts[b + 7];
|
||||
// Nur Mantelflaechen (ny ~ 0) tragen die Hoehe als v.
|
||||
if ny.abs() < 0.5 {
|
||||
if (v - 2.5).abs() < 1e-5 {
|
||||
saw_top = true;
|
||||
}
|
||||
if v.abs() < 1e-5 {
|
||||
saw_bottom = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(saw_top, "Mantel-Vertex mit v == Wandhoehe (2.5 m) erwartet");
|
||||
assert!(saw_bottom, "Mantel-Vertex mit v == 0 (Sockel) erwartet");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eine_wand_hat_quader_zaehlung() {
|
||||
// Eine Wand -> ein Quader: 24 Vertices, 36 Indizes (12 Dreiecke).
|
||||
@@ -927,5 +992,14 @@ mod tests {
|
||||
Validator::new(ValidationFlags::all(), Capabilities::all())
|
||||
.validate(&cap_module)
|
||||
.unwrap_or_else(|e| panic!("cap: WGSL-Validierung fehlgeschlagen: {e:?}"));
|
||||
|
||||
// Texturierter Wand-Shader (RenderStyle::Textured, Bild-Textur in group 1)
|
||||
// ebenso headless validieren — faengt Textur-/Sampler-Bindungsfehler ab.
|
||||
let tex_src = super::shaders::MESH_TEXTURED_WGSL;
|
||||
let tex_module = naga::front::wgsl::parse_str(tex_src)
|
||||
.unwrap_or_else(|e| panic!("textured: WGSL-Parse-Fehler: {e:?}"));
|
||||
Validator::new(ValidationFlags::all(), Capabilities::all())
|
||||
.validate(&tex_module)
|
||||
.unwrap_or_else(|e| panic!("textured: WGSL-Validierung fehlgeschlagen: {e:?}"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@ use std::collections::HashMap;
|
||||
|
||||
use crate::openings::{split_range_by_voids, MIN_SPAN};
|
||||
use crate::types::{
|
||||
Hole, Mesh, MeshInput, Opening, Point2, Rgb, SlabInput, WallInput, WallLayer,
|
||||
FLOATS_PER_VERTEX,
|
||||
Hole, Mesh, MeshInput, Opening, Point2, Rgb, SlabInput, TexturedMesh, WallInput, WallLayer,
|
||||
FLOATS_PER_VERTEX, TEXTURED_FLOATS_PER_VERTEX,
|
||||
};
|
||||
|
||||
/// Ein Quader-Mesh besteht aus 6 Seiten (Boden, Deckel, 4 Waende) zu je 2
|
||||
@@ -917,6 +917,72 @@ pub fn build_walls_mesh(walls: &[WallInput]) -> Mesh {
|
||||
mesh
|
||||
}
|
||||
|
||||
/// TEXTURIERTER Wand-Pfad (`RenderStyle::Textured`): liefert DIESELBE Geometrie wie
|
||||
/// `build_walls_mesh` — Positionen und Normalen bitgleich, gleiche Vertex-/Dreiecks-
|
||||
/// Reihenfolge — aber mit einer planaren, WELTMASSSTAEBLICHEN UV je Vertex statt der
|
||||
/// Bauteilfarbe. Bewusst ALS ABLEITUNG aus dem fertigen `Mesh` implementiert (nicht
|
||||
/// als parallele Re-Extrusion): so bleibt die paritaets-/reihenfolgesensible
|
||||
/// Geometrie (Gehrungen, Oeffnungen, Loecher, Schichten) die EINE Quelle der
|
||||
/// Wahrheit und der Alt-Pfad `[pos, normal, color]` bleibt bitgleich unangetastet —
|
||||
/// wir tauschen nur den color-Kanal gegen den aus Position+Normale abgeleiteten
|
||||
/// UV-Kanal aus.
|
||||
///
|
||||
/// UV-PROJEKTION (planar, in Metern, damit das Textur-Raster weltmassstaeblich ist,
|
||||
/// z. B. 1 Kachel = 1 m; kein Verzug an Gehrungen/Ecken, weil jede Flaeche planar
|
||||
/// projiziert wird und benachbarte Waende dieselbe Welt-Projektion fortsetzen —
|
||||
/// die Kacheln stossen im Weltraum sauber aneinander):
|
||||
/// - Senkrechte Flaechen (Wand-Mantel, Normale ~ horizontal): `u` = Weglaenge
|
||||
/// entlang der horizontalen Flaechen-Tangente `t = (-n.z, 0, n.x)`
|
||||
/// (`u = dot(pos.xz, t.xz)`), `v` = Hoehe `pos.y`.
|
||||
/// - Waagerechte Flaechen (Deckel/Boden, Normale ~ +/-Y): direkt aus dem
|
||||
/// Grundriss `u = pos.x`, `v = pos.z`.
|
||||
/// Die Wahl der Achse allein aus der Flaechen-Normale (statt aus der Wand-Achse)
|
||||
/// haelt die Projektion rein lokal je Vertex — das ist genau der Grund, warum sie
|
||||
/// sich verlustfrei aus dem fertigen `Mesh` ableiten laesst.
|
||||
pub fn build_walls_mesh_textured(walls: &[WallInput]) -> TexturedMesh {
|
||||
textured_from_mesh(&build_walls_mesh(walls))
|
||||
}
|
||||
|
||||
/// Rechnet ein `Mesh` (`[pos, normal, color]`) in einen `TexturedMesh`
|
||||
/// (`[pos, normal, uv]`) um: Positionen/Normalen/Indizes werden 1:1 uebernommen,
|
||||
/// die Farbe je Vertex wird durch die planare Welt-UV ersetzt (siehe
|
||||
/// `build_walls_mesh_textured` fuer die Achswahl). Indizes bleiben unveraendert,
|
||||
/// weil sich die Vertex-Anzahl/-Reihenfolge nicht aendert. `pub`, damit die
|
||||
/// GPU-Schicht (`gpu.rs`) das texturierte Pendant AUS DEMSELBEN `Mesh` ableiten
|
||||
/// kann, das sie ohnehin schon gebaut/hochgeladen hat (eine Geometrie-Quelle).
|
||||
pub fn textured_from_mesh(mesh: &Mesh) -> TexturedMesh {
|
||||
let n = mesh.vertex_count();
|
||||
let mut verts: Vec<f32> = Vec::with_capacity(n * TEXTURED_FLOATS_PER_VERTEX);
|
||||
for i in 0..n {
|
||||
let b = i * FLOATS_PER_VERTEX;
|
||||
let px = mesh.verts[b];
|
||||
let py = mesh.verts[b + 1];
|
||||
let pz = mesh.verts[b + 2];
|
||||
let nx = mesh.verts[b + 3];
|
||||
let ny = mesh.verts[b + 4];
|
||||
let nz = mesh.verts[b + 5];
|
||||
// Waagerecht (Deckel/Boden) vs. senkrecht (Mantel) an der Vertikal-
|
||||
// Komponente der Normale unterscheiden. Waende sind vertikal extrudiert,
|
||||
// ihre Mantelnormale ist streng horizontal (ny == 0) — die Schwelle 0.5
|
||||
// trennt sauber Deckel/Boden (|ny| == 1) von Mantelflaechen (ny == 0).
|
||||
let (u, v) = if ny.abs() > 0.5 {
|
||||
// Grundriss-Projektion: u = x, v = z (Meter).
|
||||
(px, pz)
|
||||
} else {
|
||||
// Horizontale Flaechen-Tangente t = (-n.z, 0, n.x): u = Weglaenge
|
||||
// entlang t, v = Hoehe. `t` ist ein Einheitsvektor, weil (n.x, n.z)
|
||||
// fuer Mantelflaechen normiert ist.
|
||||
let u = px * (-nz) + pz * nx;
|
||||
(u, py)
|
||||
};
|
||||
verts.extend_from_slice(&[px, py, pz, nx, ny, nz, u, v]);
|
||||
}
|
||||
TexturedMesh {
|
||||
verts,
|
||||
indices: mesh.indices.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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).
|
||||
|
||||
@@ -125,6 +125,89 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
||||
}
|
||||
"#;
|
||||
|
||||
/// WGSL des TEXTURIERTEN Wand-Pfades (`RenderStyle::Textured`, gpu::textured_pipeline).
|
||||
/// GLEICHE Beleuchtung wie `MESH_WGSL` (hemisphaerisches Ambient Himmel/Boden +
|
||||
/// Lambert-Directional + Gegen-Fuelllicht + Kantenbetonung) — nur die ALBEDO kommt
|
||||
/// aus einer Bild-Textur (`textureSample`) statt aus der Vertexfarbe. Vertex-Layout
|
||||
/// [pos vec3, normal vec3, uv vec2]; `Globals` bleibt group(0) binding(0) unveraendert,
|
||||
/// die Textur haengt in group(1). Die Weiss-/Hidden-Modi (`mode.x`) sind hier nicht
|
||||
/// relevant: `Textured` reicht `mode.x == 0` (Material) durch (siehe gpu.rs).
|
||||
pub const MESH_TEXTURED_WGSL: &str = r#"
|
||||
struct Globals {
|
||||
view_proj : mat4x4<f32>,
|
||||
light_dir : vec4<f32>,
|
||||
sky_color : vec4<f32>,
|
||||
ground_color : vec4<f32>,
|
||||
sun_color : vec4<f32>,
|
||||
mode : vec4<f32>,
|
||||
section_plane : vec4<f32>,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> globals : Globals;
|
||||
|
||||
// Bild-Textur + Sampler (group 1). Sampler mit Repeat/Linear (siehe gpu.rs) ->
|
||||
// das weltmassstaebliche UV-Raster kachelt sich ueber die Wandflaechen.
|
||||
@group(1) @binding(0) var tex : texture_2d<f32>;
|
||||
@group(1) @binding(1) var samp : sampler;
|
||||
|
||||
struct VsIn {
|
||||
@location(0) position : vec3<f32>,
|
||||
@location(1) normal : vec3<f32>,
|
||||
@location(2) uv : vec2<f32>,
|
||||
};
|
||||
|
||||
struct VsOut {
|
||||
@builtin(position) clip_pos : vec4<f32>,
|
||||
@location(0) world_normal : vec3<f32>,
|
||||
@location(1) uv : vec2<f32>,
|
||||
@location(2) world_pos : vec3<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_main(in : VsIn) -> VsOut {
|
||||
var out : VsOut;
|
||||
out.clip_pos = globals.view_proj * vec4<f32>(in.position, 1.0);
|
||||
out.world_normal = in.normal;
|
||||
out.uv = in.uv;
|
||||
out.world_pos = in.position;
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
||||
// Live-Schnitt-Kappung identisch zur Mesh-Pipeline (mode.y > 0.5 = aktiv).
|
||||
if (globals.mode.y > 0.5) {
|
||||
if (dot(in.world_pos, globals.section_plane.xyz) + globals.section_plane.w > 0.0) {
|
||||
discard;
|
||||
}
|
||||
}
|
||||
let n = normalize(in.world_normal);
|
||||
let l = normalize(globals.light_dir.xyz);
|
||||
|
||||
// Hemisphaerisches Ambient (identisch zu MESH_WGSL).
|
||||
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).
|
||||
let diffuse = max(dot(n, l), 0.0) * globals.sun_color.rgb;
|
||||
|
||||
// Gegen-Fuelllicht (identisch zu MESH_WGSL).
|
||||
let fill_l = normalize(vec3<f32>(-l.x, abs(l.y) * 0.35 + 0.15, -l.z));
|
||||
let fill = max(dot(n, fill_l), 0.0) * globals.sun_color.rgb
|
||||
* vec3<f32>(0.24, 0.27, 0.30);
|
||||
|
||||
// EINZIGER Unterschied zu MESH_WGSL: Albedo aus der Bild-Textur statt Vertexfarbe.
|
||||
let albedo = textureSample(tex, samp, in.uv).rgb;
|
||||
|
||||
var shaded = albedo * (ambient + diffuse + fill);
|
||||
|
||||
// Sanfte Kantenbetonung (identisch zu MESH_WGSL).
|
||||
let edge = 0.90 + 0.10 * abs(n.y);
|
||||
shaded = shaded * edge;
|
||||
|
||||
return vec4<f32>(shaded, 1.0);
|
||||
}
|
||||
"#;
|
||||
|
||||
/// WGSL des Referenz-Bodengitters (grid.rs). Eigene, schlanke `LineList`-Pipeline:
|
||||
/// KONSTANTE Farbe (unlit — unabhaengig von Normalen/Licht), nur View-Projektion
|
||||
/// aus demselben `Globals`-Uniform (group(0) binding(0)) wie die Mesh-Pipeline.
|
||||
|
||||
@@ -333,6 +333,30 @@ pub struct Mesh {
|
||||
/// Anzahl f32 je Vertex im interleaved Puffer: 3 Position + 3 Normale + 3 Farbe.
|
||||
pub const FLOATS_PER_VERTEX: usize = 9;
|
||||
|
||||
/// Ausgabe der TEXTURIERTEN Mesh-Erzeugung (`RenderStyle::Textured`): interleaved
|
||||
/// [pos.xyz, normal.xyz, uv.xy] je Vertex plus Indexpuffer. Ein ZWEITER, additiver
|
||||
/// Vertex-Pfad neben `Mesh` — der bestehende `[pos, normal, color]`-Pfad (`Mesh`)
|
||||
/// bleibt dadurch bitgleich unangetastet. Genutzt nur mit den GPU-Features
|
||||
/// (`render`/`window`); serde-frei (reine GPU-Zwischenform), damit der Default-Bau
|
||||
/// ohne GPU nichts davon zieht.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TexturedMesh {
|
||||
/// Interleaved Vertices: je 8 f32 = [px,py,pz, nx,ny,nz, u,v].
|
||||
pub verts: Vec<f32>,
|
||||
/// Dreiecks-Indizes (0-basiert auf die Vertices).
|
||||
pub indices: Vec<u32>,
|
||||
}
|
||||
|
||||
/// Anzahl f32 je Vertex im texturierten Puffer: 3 Position + 3 Normale + 2 UV.
|
||||
pub const TEXTURED_FLOATS_PER_VERTEX: usize = 8;
|
||||
|
||||
impl TexturedMesh {
|
||||
/// Anzahl Vertices im Puffer.
|
||||
pub fn vertex_count(&self) -> usize {
|
||||
self.verts.len() / TEXTURED_FLOATS_PER_VERTEX
|
||||
}
|
||||
}
|
||||
|
||||
impl Mesh {
|
||||
/// Anzahl Vertices im Puffer.
|
||||
pub fn vertex_count(&self) -> usize {
|
||||
|
||||
Reference in New Issue
Block a user