//! 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, pub(crate) indices: Vec, pub(crate) batches: Vec, } 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::() 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, // group 1, pro Textur batches: Vec, } 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::() 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, 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) }