56d92cdb46
engine/map.rs + render/brush.rs raus, Welt kommt komplett aus .glb. Spawn via Empty mit role=spawn statt info_player_start; testscene.glb (Boden, Wände, Spawn) ersetzt test.map. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
288 lines
11 KiB
Rust
288 lines
11 KiB
Rust
//! Szenen-Pass: zeichnet die 3D-Welt ins interne Target.
|
||
//!
|
||
//! Geometrie + Texturen kommen vom Aufrufer (Blender-Modelle via
|
||
//! render::props; 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)]
|
||
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.
|
||
#[derive(Default)]
|
||
pub(crate) struct Mesh {
|
||
pub(crate) verts: Vec<Vertex>,
|
||
pub(crate) indices: Vec<u32>,
|
||
pub(crate) batches: Vec<Batch>,
|
||
}
|
||
|
||
impl Mesh {
|
||
/// Ein zweites Mesh anhängen (ein Vertex-/Index-Buffer für alles).
|
||
/// Voraussetzung: beide wurden gegen *dieselbe* Texturliste gebaut —
|
||
/// die Batch-Texturindizes bleiben dann unverändert gültig; nur die
|
||
/// Buffer-Offsets verschieben sich.
|
||
pub(crate) fn append(&mut self, other: Mesh) {
|
||
let vbase = self.verts.len() as u32;
|
||
let ibase = self.indices.len() as u32;
|
||
self.verts.extend(other.verts);
|
||
self.indices.extend(other.indices.iter().map(|i| i + vbase));
|
||
self.batches.extend(other.batches.iter().map(|b| Batch {
|
||
texture: b.texture,
|
||
start: b.start + ibase,
|
||
count: b.count,
|
||
}));
|
||
}
|
||
}
|
||
|
||
/// 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 => Float32x2],
|
||
};
|
||
|
||
pub struct ScenePass {
|
||
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"),
|
||
source: wgpu::ShaderSource::Wgsl(include_str!("scene.wgsl").into()),
|
||
});
|
||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||
label: Some("scene"),
|
||
layout: None,
|
||
vertex: wgpu::VertexState {
|
||
module: &shader,
|
||
entry_point: Some("vs_main"),
|
||
compilation_options: Default::default(),
|
||
buffers: &[VERTEX_LAYOUT],
|
||
},
|
||
fragment: Some(wgpu::FragmentState {
|
||
module: &shader,
|
||
entry_point: Some("fs_main"),
|
||
compilation_options: Default::default(),
|
||
targets: &[Some(color_format.into())],
|
||
}),
|
||
// Back-face Culling: glTF-Dreiecke sind CCW von außen
|
||
// gewickelt — die Außenflächen sind front-facing, Rückseiten
|
||
// 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),
|
||
depth_compare: Some(wgpu::CompareFunction::Less),
|
||
stencil: wgpu::StencilState::default(),
|
||
bias: wgpu::DepthBiasState::default(),
|
||
}),
|
||
multisample: wgpu::MultisampleState::default(),
|
||
multiview_mask: None,
|
||
cache: None,
|
||
});
|
||
|
||
let vbuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||
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("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::<Uniforms>() as u64,
|
||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||
mapped_at_creation: false,
|
||
});
|
||
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,
|
||
resource: ubuf.as_entire_binding(),
|
||
}],
|
||
});
|
||
|
||
// mag=Nearest hält nah dran die harten Texel (der knackige Look);
|
||
// min/mipmap=Linear glätten nur die Verkleinerung in der Ferne und
|
||
// blenden weich zwischen den Mip-Ebenen (kein Mip-Popping). Killt das
|
||
// Texel-Flimmern hochauflösender Texturen auf der kleinen internen
|
||
// Auflösung. Mipmaps werden in upload_texture per Box-Filter erzeugt.
|
||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||
label: Some("scene"),
|
||
address_mode_u: wgpu::AddressMode::Repeat,
|
||
address_mode_v: wgpu::AddressMode::Repeat,
|
||
mag_filter: wgpu::FilterMode::Nearest,
|
||
min_filter: wgpu::FilterMode::Linear,
|
||
mipmap_filter: wgpu::MipmapFilterMode::Linear,
|
||
..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.
|
||
/// `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.uniform_bind, &[]);
|
||
pass.set_vertex_buffer(0, self.vbuf.slice(..));
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Ein RGBA8-`Image` als GPU-Textur hochladen und die zugehörige
|
||
/// Bind-Group (Textur + Sampler, group 1) bauen. Erzeugt die volle
|
||
/// Mipchain (bis 1×1) per Box-Filter auf der CPU und lädt jede Ebene hoch.
|
||
fn upload_texture(
|
||
device: &wgpu::Device,
|
||
queue: &wgpu::Queue,
|
||
layout: &wgpu::BindGroupLayout,
|
||
sampler: &wgpu::Sampler,
|
||
img: &Image,
|
||
) -> wgpu::BindGroup {
|
||
// floor(log2(max(w,h))) + 1 = volle Kette bis zur 1×1-Ebene.
|
||
let mip_level_count = 32 - img.width.max(img.height).leading_zeros();
|
||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||
label: Some("scene texture"),
|
||
size: wgpu::Extent3d {
|
||
width: img.width,
|
||
height: img.height,
|
||
depth_or_array_layers: 1,
|
||
},
|
||
mip_level_count,
|
||
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: &[],
|
||
});
|
||
|
||
// Ebene 0 ist das Originalbild; jede weitere wird aus der vorherigen
|
||
// halbiert. `level` trägt die aktuell hochzuladenden Pixel.
|
||
let mut level = img.rgba.clone();
|
||
let (mut w, mut h) = (img.width, img.height);
|
||
for mip in 0..mip_level_count {
|
||
queue.write_texture(
|
||
wgpu::TexelCopyTextureInfo {
|
||
texture: &texture,
|
||
mip_level: mip,
|
||
origin: wgpu::Origin3d::ZERO,
|
||
aspect: wgpu::TextureAspect::All,
|
||
},
|
||
&level,
|
||
wgpu::TexelCopyBufferLayout {
|
||
offset: 0,
|
||
bytes_per_row: Some(w * 4),
|
||
rows_per_image: Some(h),
|
||
},
|
||
wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
|
||
);
|
||
if mip + 1 < mip_level_count {
|
||
(level, w, h) = downsample(&level, w, h);
|
||
}
|
||
}
|
||
|
||
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) },
|
||
],
|
||
})
|
||
}
|
||
|
||
/// Eine RGBA8-Mip-Ebene per 2×2-Box-Filter auf die nächstkleinere halbieren.
|
||
/// Gibt die neuen Pixel samt Maßen zurück. Ungerade Maße werden via `(d+1)/2`
|
||
/// aufgerundet (sonst ginge die letzte Spalte/Zeile verloren); die fehlende
|
||
/// Quell-Spalte/-Zeile wird auf den Rand geklemmt, statt den Mittelwert zu
|
||
/// verfälschen. Mittelung im (gamma-kodierten) Speicherraum — für den
|
||
/// stilisierten Look unkritisch.
|
||
fn downsample(src: &[u8], w: u32, h: u32) -> (Vec<u8>, u32, u32) {
|
||
let (nw, nh) = (w.div_ceil(2), h.div_ceil(2));
|
||
let mut dst = vec![0u8; (nw * nh * 4) as usize];
|
||
for y in 0..nh {
|
||
for x in 0..nw {
|
||
let (x0, y0) = (2 * x, 2 * y);
|
||
let x1 = (x0 + 1).min(w - 1);
|
||
let y1 = (y0 + 1).min(h - 1);
|
||
let at = |px: u32, py: u32| ((py * w + px) * 4) as usize;
|
||
let samples = [at(x0, y0), at(x1, y0), at(x0, y1), at(x1, y1)];
|
||
let di = ((y * nw + x) * 4) as usize;
|
||
for c in 0..4 {
|
||
let sum: u32 = samples.iter().map(|&s| src[s + c] as u32).sum();
|
||
dst[di + c] = ((sum + 2) / 4) as u8;
|
||
}
|
||
}
|
||
}
|
||
(dst, nw, nh)
|
||
}
|