Mipmapping an den Texturen

This commit is contained in:
2026-06-16 10:39:58 +02:00
parent 14961d903b
commit 949a076ec7
2 changed files with 77 additions and 27 deletions
+70 -26
View File
@@ -136,13 +136,18 @@ impl ScenePass {
}],
});
// Nearest-Sampler: harte Texel, kein Filtering — PS1.
// 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 nearest"),
label: Some("scene"),
address_mode_u: wgpu::AddressMode::Repeat,
address_mode_v: wgpu::AddressMode::Repeat,
mag_filter: wgpu::FilterMode::Nearest,
min_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);
@@ -176,7 +181,8 @@ impl ScenePass {
}
/// Ein RGBA8-`Image` als GPU-Textur hochladen und die zugehörige
/// Bind-Group (Textur + Sampler, group 1) bauen.
/// 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,
@@ -184,15 +190,16 @@ fn upload_texture(
sampler: &wgpu::Sampler,
img: &Image,
) -> wgpu::BindGroup {
let size = wgpu::Extent3d {
width: img.width,
height: img.height,
depth_or_array_layers: 1,
};
// 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,
mip_level_count: 1,
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.
@@ -200,21 +207,32 @@ fn upload_texture(
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,
);
// 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"),
@@ -225,3 +243,29 @@ fn upload_texture(
],
})
}
/// 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 + 1) / 2, (h + 1) / 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)
}
+7 -1
View File
@@ -53,9 +53,15 @@ fn bayer4(px: vec2u) -> f32 {
return f32(m[(px.y % 4u) * 4u + (px.x % 4u)]);
}
// LOD-Bias für die Mip-Auswahl: negativ = schärfer (greift einen höher
// aufgelösten Mip, als die UV-Ableitungen verlangen), positiv = weicher.
// 0 = neutral. Bei -1.0 verdoppelt sich praktisch die Texelrate (mehr
// Schärfe, etwas mehr Flimmern); -0.5 ist ein sanfter Mittelweg.
const MIP_BIAS: f32 = -1.0;
@fragment
fn fs_main(in: VsOut) -> @location(0) vec4f {
let texel = textureSample(tex, smp, in.uv);
let texel = textureSampleBias(tex, smp, in.uv, MIP_BIAS);
if texel.a < 0.5 { discard; } // 1-Bit-Alpha (Cutouts), noch ungenutzt
// RGB555: 31 Stufen pro Kanal. Bayer-Schwelle vor dem Abrunden →