idk shitload of stuff

This commit is contained in:
2026-06-13 18:18:17 +02:00
parent 48c499e356
commit 6bb9b009c4
24 changed files with 3221 additions and 119 deletions
+142 -57
View File
@@ -1,40 +1,74 @@
//! Szenen-Pass: zeichnet die 3D-Welt ins interne Target.
//!
//! Stand Schritt 3: ein hartkodierter Testwürfel mit Vertex-Colors —
//! Meshes aus OBJ und Texturen kommen in Schritt 5. Die Shader
//! (scene.wgsl) sind dagegen schon die echten PS1-Shader.
//! Stand Schritt 5: Geometrie + Texturen kommen vom Aufrufer (Brush-Welt
//! aus der `.map`, siehe render::brush; Bilder aus engine::tga). Pro Textur
//! eine Bind-Group + ein Draw-Batch (Material-Batching „pro Textur ein
//! Draw" wie im Plan). Die Shader (scene.wgsl) sind die echten PS1-Shader.
use wgpu::util::DeviceExt;
use crate::engine::tga::Image;
use crate::render::math::Mat4;
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Vertex {
pos: [f32; 3],
color: [f32; 3],
pub(crate) struct Vertex {
pub(crate) pos: [f32; 3],
pub(crate) uv: [f32; 2],
}
/// Ein Draw-Batch: ein zusammenhängender Index-Bereich, der mit *einer*
/// Textur gezeichnet wird.
#[derive(Clone, Copy)]
pub(crate) struct Batch {
pub(crate) texture: usize,
pub(crate) start: u32,
pub(crate) count: u32,
}
/// CPU-seitige Welt-Geometrie, fertig zum Hochladen. Die Indizes sind nach
/// Textur gruppiert; `batches` zeigt in diese Reihenfolge.
pub(crate) struct Mesh {
pub(crate) verts: Vec<Vertex>,
pub(crate) indices: Vec<u32>,
pub(crate) batches: Vec<Batch>,
}
/// Spiegelt das `Uniforms`-Struct in scene.wgsl. `_pad` rundet die Größe
/// auf 80 Byte (16er-Vielfaches), wie es die Uniform-Adressraum-Regeln
/// von WGSL verlangen.
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniforms {
mvp: [[f32; 4]; 4],
half_res: [f32; 2],
_pad: [f32; 2],
}
const VERTEX_LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
array_stride: size_of::<Vertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3],
attributes: &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x2],
};
pub struct ScenePass {
pipeline: wgpu::RenderPipeline,
vbuf: wgpu::Buffer,
ibuf: wgpu::Buffer,
ubuf: wgpu::Buffer,
bind: wgpu::BindGroup,
index_count: u32,
pipeline: wgpu::RenderPipeline,
vbuf: wgpu::Buffer,
ibuf: wgpu::Buffer,
ubuf: wgpu::Buffer,
uniform_bind: wgpu::BindGroup, // group 0
tex_binds: Vec<wgpu::BindGroup>, // group 1, pro Textur
batches: Vec<Batch>,
}
impl ScenePass {
pub fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
color_format: wgpu::TextureFormat,
depth_format: wgpu::TextureFormat,
mesh: &Mesh,
images: &[Image],
) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("scene"),
@@ -55,10 +89,16 @@ impl ScenePass {
compilation_options: Default::default(),
targets: &[Some(color_format.into())],
}),
// Cull aus: der Z-Buffer sortiert auch so korrekt, und die
// irl3d-Materialien sind teils two-sided. Entscheidung pro
// Material fällt mit dem Szenen-Loader (Schritt 5).
primitive: wgpu::PrimitiveState::default(),
// Back-face Culling: Brush-Polygone werden CCW um ihre nach
// außen zeigende Normale gewickelt (siehe render::brush), und
// die Z-up→Y-up-Drehung erhält die Orientierung (det +1). Also
// sind die Außenflächen front-facing — Rückseiten und alle
// verdeckten Innenflächen zwischen Brushes fallen weg.
// (Front-Face bleibt der wgpu-Default CCW.)
primitive: wgpu::PrimitiveState {
cull_mode: Some(wgpu::Face::Back),
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: depth_format,
depth_write_enabled: Some(true),
@@ -71,25 +111,24 @@ impl ScenePass {
cache: None,
});
let (verts, indices) = cube();
let vbuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("cube vertices"),
contents: bytemuck::cast_slice(&verts),
label: Some("scene vertices"),
contents: bytemuck::cast_slice(&mesh.verts),
usage: wgpu::BufferUsages::VERTEX,
});
let ibuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("cube indices"),
contents: bytemuck::cast_slice(&indices),
label: Some("scene indices"),
contents: bytemuck::cast_slice(&mesh.indices),
usage: wgpu::BufferUsages::INDEX,
});
let ubuf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("scene uniforms"),
size: size_of::<Mat4>() as u64,
size: size_of::<Uniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("scene"),
let uniform_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("scene uniforms"),
layout: &pipeline.get_bind_group_layout(0),
entries: &[wgpu::BindGroupEntry {
binding: 0,
@@ -97,46 +136,92 @@ impl ScenePass {
}],
});
Self { pipeline, vbuf, ibuf, ubuf, bind, index_count: indices.len() as u32 }
// Nearest-Sampler: harte Texel, kein Filtering — PS1.
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("scene nearest"),
address_mode_u: wgpu::AddressMode::Repeat,
address_mode_v: wgpu::AddressMode::Repeat,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
let tex_layout = pipeline.get_bind_group_layout(1);
let tex_binds = images.iter()
.map(|img| upload_texture(device, queue, &tex_layout, &sampler, img))
.collect();
Self {
pipeline, vbuf, ibuf, ubuf, uniform_bind, tex_binds,
batches: mesh.batches.clone(),
}
}
/// Uniforms für diesen Frame hochladen — vor dem Render-Pass rufen.
pub fn prepare(&self, queue: &wgpu::Queue, mvp: &Mat4) {
queue.write_buffer(&self.ubuf, 0, bytemuck::bytes_of(mvp));
/// `half_res` = halbe interne Auflösung (fürs Pixel-Snap im Shader).
pub fn prepare(&self, queue: &wgpu::Queue, mvp: &Mat4, half_res: [f32; 2]) {
let u = Uniforms { mvp: mvp.0, half_res, _pad: [0.0; 2] };
queue.write_buffer(&self.ubuf, 0, bytemuck::bytes_of(&u));
}
pub fn draw(&self, pass: &mut wgpu::RenderPass) {
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.bind, &[]);
pass.set_bind_group(0, &self.uniform_bind, &[]);
pass.set_vertex_buffer(0, self.vbuf.slice(..));
pass.set_index_buffer(self.ibuf.slice(..), wgpu::IndexFormat::Uint16);
pass.draw_indexed(0..self.index_count, 0, 0..1);
}
}
/// Einheitswürfel um den Ursprung, jede Seite eine Farbe. Der
/// Helligkeitsverlauf über die Ecken erzeugt Gradienten, an denen
/// Dither und affine Interpolation sichtbar werden.
fn cube() -> (Vec<Vertex>, Vec<u16>) {
const S: f32 = 0.5;
let faces: [([f32; 3], [[f32; 3]; 4]); 6] = [
([0.9, 0.2, 0.2], [[ S, -S, -S], [ S, S, -S], [ S, S, S], [ S, -S, S]]), // +X
([0.2, 0.9, 0.9], [[-S, -S, -S], [-S, S, -S], [-S, S, S], [-S, -S, S]]), // -X
([0.2, 0.9, 0.2], [[-S, S, -S], [ S, S, -S], [ S, S, S], [-S, S, S]]), // +Y
([0.9, 0.2, 0.9], [[-S, -S, -S], [ S, -S, -S], [ S, -S, S], [-S, -S, S]]), // -Y
([0.3, 0.3, 0.9], [[-S, -S, S], [ S, -S, S], [ S, S, S], [-S, S, S]]), // +Z
([0.9, 0.8, 0.2], [[-S, -S, -S], [ S, -S, -S], [ S, S, -S], [-S, S, -S]]), // -Z
];
const SHADE: [f32; 4] = [1.0, 0.65, 0.4, 0.65];
let mut verts = Vec::with_capacity(24);
let mut idx: Vec<u16> = Vec::with_capacity(36);
for (base, corners) in faces {
let b = verts.len() as u16;
for (i, pos) in corners.into_iter().enumerate() {
verts.push(Vertex { pos, color: base.map(|c| c * SHADE[i]) });
pass.set_index_buffer(self.ibuf.slice(..), wgpu::IndexFormat::Uint32);
for b in &self.batches {
pass.set_bind_group(1, &self.tex_binds[b.texture], &[]);
pass.draw_indexed(b.start..b.start + b.count, 0, 0..1);
}
idx.extend([b, b + 1, b + 2, b, b + 2, b + 3]);
}
(verts, idx)
}
/// Ein RGBA8-`Image` als GPU-Textur hochladen und die zugehörige
/// Bind-Group (Textur + Sampler, group 1) bauen.
fn upload_texture(
device: &wgpu::Device,
queue: &wgpu::Queue,
layout: &wgpu::BindGroupLayout,
sampler: &wgpu::Sampler,
img: &Image,
) -> wgpu::BindGroup {
let size = wgpu::Extent3d {
width: img.width,
height: img.height,
depth_or_array_layers: 1,
};
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("scene texture"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
// Nicht-sRGB: die Quantisierung im Shader erwartet rohe Werte.
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&img.rgba,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(img.width * 4),
rows_per_image: Some(img.height),
},
size,
);
let view = texture.create_view(&Default::default());
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("scene texture"),
layout,
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&view) },
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(sampler) },
],
})
}