use std::fmt;
use bevy::color::Srgba;
use bevy::image::Image;
use bevy::render::render_resource::{TextureDimension, TextureFormat};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PyramidError {
Format(TextureFormat),
Shape,
AlreadyMipped,
NoData,
}
impl fmt::Display for PyramidError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Format(fmt) => write!(f, "unsupported texture format {fmt:?} (need Rgba8Unorm)"),
Self::Shape => f.write_str("not a single-layer 2D texture"),
Self::AlreadyMipped => f.write_str("the asset already carries mip levels"),
Self::NoData => f.write_str("the image has no CPU-side pixel data"),
}
}
}
pub fn check(image: &Image) -> Result<(), PyramidError> {
let desc = &image.texture_descriptor;
if !matches!(
desc.format,
TextureFormat::Rgba8Unorm | TextureFormat::Rgba8UnormSrgb
) {
return Err(PyramidError::Format(desc.format));
}
if desc.dimension != TextureDimension::D2 || desc.size.depth_or_array_layers != 1 {
return Err(PyramidError::Shape);
}
if desc.mip_level_count > 1 {
return Err(PyramidError::AlreadyMipped);
}
let want = desc.size.width as usize * desc.size.height as usize * 4;
match &image.data {
Some(data) if data.len() == want => Ok(()),
_ => Err(PyramidError::NoData),
}
}
pub fn build_pyramid(image: &mut Image) -> Result<u32, PyramidError> {
check(image)?;
let desc = &image.texture_descriptor;
let srgb = desc.format == TextureFormat::Rgba8UnormSrgb;
let (mut w, mut h) = (desc.size.width, desc.size.height);
let levels = crate::layer::render::mips::mip_level_count(bevy::math::UVec2::new(w, h));
let data = image.data.as_mut().expect("checked");
let decode = if srgb {
&SRGB_TO_LINEAR
} else {
&UNORM_TO_LINEAR
};
let mut src_start = 0usize;
for _ in 1..levels {
let src_len = (w * h * 4) as usize;
let (nw, nh) = ((w / 2).max(1), (h / 2).max(1));
let mut level = Vec::with_capacity((nw * nh * 4) as usize);
{
let src = &data[src_start..src_start + src_len];
for y in 0..nh {
for x in 0..nw {
let mut acc = [0f32; 4];
for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
let sx = (2 * x + dx).min(w - 1);
let sy = (2 * y + dy).min(h - 1);
let i = ((sy * w + sx) * 4) as usize;
let a = src[i + 3] as f32 / 255.0;
acc[0] += decode[src[i] as usize] * a;
acc[1] += decode[src[i + 1] as usize] * a;
acc[2] += decode[src[i + 2] as usize] * a;
acc[3] += a;
}
let rgb = if acc[3] > 0.0 {
[acc[0] / acc[3], acc[1] / acc[3], acc[2] / acc[3]]
} else {
[0.0; 3]
};
for c in rgb {
level.push(encode(c, srgb));
}
level.push((acc[3] / 4.0 * 255.0).round() as u8);
}
}
}
data.extend_from_slice(&level);
src_start += src_len;
(w, h) = (nw, nh);
}
image.texture_descriptor.mip_level_count = levels;
Ok(levels)
}
const ENCODE_STEPS: usize = 4096;
fn encode(linear: f32, srgb: bool) -> u8 {
let v = linear.clamp(0.0, 1.0);
if srgb {
LINEAR_TO_SRGB[(v * (ENCODE_STEPS - 1) as f32).round() as usize]
} else {
(v * 255.0).round() as u8
}
}
static SRGB_TO_LINEAR: std::sync::LazyLock<[f32; 256]> = std::sync::LazyLock::new(|| {
let mut lut = [0f32; 256];
for (i, v) in lut.iter_mut().enumerate() {
*v = Srgba::gamma_function(i as f32 / 255.0);
}
lut
});
static UNORM_TO_LINEAR: std::sync::LazyLock<[f32; 256]> = std::sync::LazyLock::new(|| {
let mut lut = [0f32; 256];
for (i, v) in lut.iter_mut().enumerate() {
*v = i as f32 / 255.0;
}
lut
});
static LINEAR_TO_SRGB: std::sync::LazyLock<[u8; ENCODE_STEPS]> = std::sync::LazyLock::new(|| {
let mut lut = [0u8; ENCODE_STEPS];
for (i, v) in lut.iter_mut().enumerate() {
let linear = i as f32 / (ENCODE_STEPS - 1) as f32;
*v = (Srgba::gamma_function_inverse(linear) * 255.0).round() as u8;
}
lut
});
#[cfg(test)]
mod tests {
use super::*;
use bevy::asset::RenderAssetUsages;
use bevy::render::render_resource::Extent3d;
fn image(w: u32, h: u32, format: TextureFormat, pixels: &[[u8; 4]]) -> Image {
assert_eq!(pixels.len(), (w * h) as usize);
Image::new(
Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
TextureDimension::D2,
pixels.iter().flatten().copied().collect(),
format,
RenderAssetUsages::default(),
)
}
fn level(image: &Image, start: usize, px: usize) -> [u8; 4] {
let d = image.data.as_ref().unwrap();
[
d[start + px * 4],
d[start + px * 4 + 1],
d[start + px * 4 + 2],
d[start + px * 4 + 3],
]
}
#[test]
fn srgb_average_is_in_linear_light() {
let b = [0, 0, 0, 255];
let w = [255, 255, 255, 255];
let mut img = image(2, 2, TextureFormat::Rgba8UnormSrgb, &[b, w, w, b]);
assert_eq!(build_pyramid(&mut img), Ok(2));
assert_eq!(img.texture_descriptor.mip_level_count, 2);
assert_eq!(img.data.as_ref().unwrap().len(), 16 + 4);
assert_eq!(level(&img, 16, 0), [188, 188, 188, 255]);
let mut img = image(2, 2, TextureFormat::Rgba8Unorm, &[b, w, w, b]);
assert_eq!(build_pyramid(&mut img), Ok(2));
assert_eq!(level(&img, 16, 0), [128, 128, 128, 255]);
}
#[test]
fn average_is_alpha_weighted() {
let red = [255, 0, 0, 255];
let clear = [0, 0, 0, 0];
let mut img = image(
2,
2,
TextureFormat::Rgba8UnormSrgb,
&[red, clear, clear, clear],
);
build_pyramid(&mut img).unwrap();
assert_eq!(level(&img, 16, 0), [255, 0, 0, 64]);
let mut img = image(2, 2, TextureFormat::Rgba8UnormSrgb, &[clear; 4]);
build_pyramid(&mut img).unwrap();
assert_eq!(level(&img, 16, 0), [0, 0, 0, 0]);
}
#[test]
fn odd_and_rectangular_sizes_clamp_to_the_edge() {
let b = [0, 0, 0, 255];
let w = [255, 255, 255, 255];
let mut img = image(3, 1, TextureFormat::Rgba8UnormSrgb, &[b, w, w]);
assert_eq!(build_pyramid(&mut img), Ok(2));
assert_eq!(level(&img, 12, 0), [188, 188, 188, 255]);
let mut img = image(1, 3, TextureFormat::Rgba8UnormSrgb, &[b, w, w]);
assert_eq!(build_pyramid(&mut img), Ok(2));
assert_eq!(level(&img, 12, 0), [188, 188, 188, 255]);
let mut img = image(4, 2, TextureFormat::Rgba8UnormSrgb, &[b; 8]);
assert_eq!(build_pyramid(&mut img), Ok(3));
assert_eq!(img.data.as_ref().unwrap().len(), 32 + 8 + 4);
}
#[test]
fn refuses_unsupported_images_untouched() {
let px = [[1, 2, 3, 4]];
let mut img = image(1, 1, TextureFormat::Bgra8UnormSrgb, &px);
assert_eq!(
build_pyramid(&mut img),
Err(PyramidError::Format(TextureFormat::Bgra8UnormSrgb))
);
assert_eq!(img.texture_descriptor.mip_level_count, 1);
let mut img = image(1, 1, TextureFormat::Rgba8UnormSrgb, &px);
img.texture_descriptor.mip_level_count = 3;
assert_eq!(check(&img), Err(PyramidError::AlreadyMipped));
let mut img = image(1, 1, TextureFormat::Rgba8UnormSrgb, &px);
img.data = None;
assert_eq!(check(&img), Err(PyramidError::NoData));
let mut img = image(1, 1, TextureFormat::Rgba8UnormSrgb, &px);
assert_eq!(build_pyramid(&mut img), Ok(1));
assert_eq!(img.data.as_ref().unwrap(), &[1, 2, 3, 4]);
}
}