Mipmapping an den Texturen
This commit is contained in:
+59
-15
@@ -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 {
|
||||
// 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,
|
||||
};
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("scene texture"),
|
||||
size,
|
||||
mip_level_count: 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: &[],
|
||||
});
|
||||
|
||||
// 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: 0,
|
||||
mip_level: mip,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
&img.rgba,
|
||||
&level,
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(img.width * 4),
|
||||
rows_per_image: Some(img.height),
|
||||
bytes_per_row: Some(w * 4),
|
||||
rows_per_image: Some(h),
|
||||
},
|
||||
size,
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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 →
|
||||
|
||||
Reference in New Issue
Block a user