use std::sync::OnceLock;
use bevy::{
asset::{Assets, RenderAssetUsages},
image::{Image, ImageAddressMode, ImageSampler, ImageSamplerDescriptor},
prelude::Handle,
render::render_resource::{Extent3d, TextureDimension, TextureFormat},
};
#[derive(Debug)]
pub enum TextureError {
ZeroDimension { width: u32, height: u32 },
DimensionTooLarge { width: u32, height: u32, max: u32 },
}
impl std::fmt::Display for TextureError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TextureError::ZeroDimension { width, height } => write!(
f,
"texture dimensions must be non-zero (got {width}×{height})"
),
TextureError::DimensionTooLarge { width, height, max } => write!(
f,
"texture dimensions {width}×{height} exceed MAX_DIMENSION={max}"
),
}
}
}
impl std::error::Error for TextureError {}
pub struct TextureMap {
pub albedo: Vec<u8>,
pub normal: Vec<u8>,
pub roughness: Vec<u8>,
pub width: u32,
pub height: u32,
}
pub struct GeneratedHandles {
pub albedo: Handle<Image>,
pub normal: Handle<Image>,
pub roughness: Handle<Image>,
}
pub trait TextureGenerator {
fn generate(&self, width: u32, height: u32) -> Result<TextureMap, TextureError>;
}
pub const MAX_DIMENSION: u32 = 4096;
#[inline]
pub fn validate_dimensions(width: u32, height: u32) -> Result<(), TextureError> {
if width == 0 || height == 0 {
return Err(TextureError::ZeroDimension { width, height });
}
if width > MAX_DIMENSION || height > MAX_DIMENSION {
return Err(TextureError::DimensionTooLarge {
width,
height,
max: MAX_DIMENSION,
});
}
Ok(())
}
pub fn map_to_images(map: TextureMap, images: &mut Assets<Image>) -> GeneratedHandles {
GeneratedHandles {
albedo: images.add(make_image(
map.albedo,
map.width,
map.height,
TextureFormat::Rgba8UnormSrgb,
ImageAddressMode::Repeat,
MipmapMode::Srgb,
)),
normal: images.add(make_image(
map.normal,
map.width,
map.height,
TextureFormat::Rgba8Unorm,
ImageAddressMode::Repeat,
MipmapMode::Normal,
)),
roughness: images.add(make_image(
map.roughness,
map.width,
map.height,
TextureFormat::Rgba8Unorm,
ImageAddressMode::Repeat,
MipmapMode::Linear,
)),
}
}
pub fn map_to_images_card(map: TextureMap, images: &mut Assets<Image>) -> GeneratedHandles {
GeneratedHandles {
albedo: images.add(make_image(
map.albedo,
map.width,
map.height,
TextureFormat::Rgba8UnormSrgb,
ImageAddressMode::ClampToEdge,
MipmapMode::Srgb,
)),
normal: images.add(make_image(
map.normal,
map.width,
map.height,
TextureFormat::Rgba8Unorm,
ImageAddressMode::ClampToEdge,
MipmapMode::Normal,
)),
roughness: images.add(make_image(
map.roughness,
map.width,
map.height,
TextureFormat::Rgba8Unorm,
ImageAddressMode::ClampToEdge,
MipmapMode::Linear,
)),
}
}
#[derive(Clone, Copy)]
enum MipmapMode {
Srgb,
Normal,
Linear,
}
fn srgb_to_linear(v: u8) -> f32 {
static LUT: OnceLock<[f32; 256]> = OnceLock::new();
LUT.get_or_init(|| {
std::array::from_fn(|i| {
let c = i as f32 / 255.0;
if c <= 0.04045 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
})
})[v as usize]
}
fn average_block(pixels: &[[u8; 4]], mode: MipmapMode) -> [u8; 4] {
let n = pixels.len() as f32;
match mode {
MipmapMode::Linear => {
let mut rgba = [0u32; 4];
for p in pixels {
for i in 0..4 {
rgba[i] += p[i] as u32;
}
}
let count = pixels.len() as u32;
[
(rgba[0] / count) as u8,
(rgba[1] / count) as u8,
(rgba[2] / count) as u8,
(rgba[3] / count) as u8,
]
}
MipmapMode::Srgb => {
let mut r = 0.0f32;
let mut g = 0.0f32;
let mut b = 0.0f32;
let mut a = 0u32;
for p in pixels {
r += srgb_to_linear(p[0]);
g += srgb_to_linear(p[1]);
b += srgb_to_linear(p[2]);
a += p[3] as u32;
}
[
linear_to_srgb(r / n),
linear_to_srgb(g / n),
linear_to_srgb(b / n),
(a / pixels.len() as u32) as u8,
]
}
MipmapMode::Normal => {
let mut nx = 0.0f32;
let mut ny = 0.0f32;
let mut nz = 0.0f32;
for p in pixels {
nx += p[0] as f32 / 127.5 - 1.0;
ny += p[1] as f32 / 127.5 - 1.0;
nz += p[2] as f32 / 127.5 - 1.0;
}
nx /= n;
ny /= n;
nz /= n;
let len = (nx * nx + ny * ny + nz * nz).sqrt().max(1e-6);
nx /= len;
ny /= len;
nz /= len;
let enc = |v: f32| ((v * 0.5 + 0.5).clamp(0.0, 1.0) * 255.0).round() as u8;
[enc(nx), enc(ny), enc(nz), 255]
}
}
}
fn generate_mipmaps(
mut data: Vec<u8>,
base_width: u32,
base_height: u32,
mode: MipmapMode,
) -> (Vec<u8>, u32) {
let mut mip_level_count = 1u32;
let mut current_width = base_width as usize;
let mut current_height = base_height as usize;
let mut prev_offset = 0usize;
while current_width > 1 || current_height > 1 {
let next_width = current_width.max(2) / 2;
let next_height = current_height.max(2) / 2;
let next_offset = data.len();
data.resize(next_offset + next_width * next_height * 4, 0);
for y in 0..next_height {
for x in 0..next_width {
let dst_idx = next_offset + (y * next_width + x) * 4;
let sx = x * 2;
let sy = y * 2;
let mut pixels = [[0u8; 4]; 4];
let mut count = 0usize;
for dy in 0..2usize {
if sy + dy >= current_height {
continue;
}
for dx in 0..2usize {
if sx + dx >= current_width {
continue;
}
let src_idx = prev_offset + ((sy + dy) * current_width + (sx + dx)) * 4;
pixels[count] = [
data[src_idx],
data[src_idx + 1],
data[src_idx + 2],
data[src_idx + 3],
];
count += 1;
}
}
let avg = average_block(&pixels[..count], mode);
data[dst_idx] = avg[0];
data[dst_idx + 1] = avg[1];
data[dst_idx + 2] = avg[2];
data[dst_idx + 3] = avg[3];
}
}
prev_offset = next_offset;
current_width = next_width;
current_height = next_height;
mip_level_count += 1;
}
(data, mip_level_count)
}
fn make_image(
data: Vec<u8>,
width: u32,
height: u32,
format: TextureFormat,
address_mode: ImageAddressMode,
mipmap_mode: MipmapMode,
) -> Image {
let mut image = Image::new(
Extent3d {
width,
height,
depth_or_array_layers: 1,
},
TextureDimension::D2,
data,
format,
RenderAssetUsages::default(),
);
let base_data = image.data.take().unwrap();
let (mip_data, mip_level_count) = generate_mipmaps(base_data, width, height, mipmap_mode);
image.texture_descriptor.mip_level_count = mip_level_count;
image.data = Some(mip_data);
image.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor {
address_mode_u: address_mode,
address_mode_v: address_mode,
mag_filter: bevy::image::ImageFilterMode::Linear,
min_filter: bevy::image::ImageFilterMode::Linear,
mipmap_filter: bevy::image::ImageFilterMode::Linear,
anisotropy_clamp: 16,
..Default::default()
});
image
}
#[inline]
pub(crate) fn linear_to_srgb(linear: f32) -> u8 {
const N: usize = 4096;
static LUT: OnceLock<[u8; N]> = OnceLock::new();
let lut = LUT.get_or_init(|| {
std::array::from_fn(|i| {
let c = i as f32 / (N - 1) as f32;
let encoded = if c <= 0.003_130_8 {
c * 12.92
} else {
1.055 * c.powf(1.0 / 2.4) - 0.055
};
(encoded * 255.0).round() as u8
})
});
lut[(linear.clamp(0.0, 1.0) * (N - 1) as f32).round() as usize]
}