//! Procedural 16x16 block textures, painted at startup into one mipmapped texture array. use bevy::asset::RenderAssetUsages; use bevy::image::{Image, ImageAddressMode, ImageFilterMode, ImageSampler, ImageSamplerDescriptor}; use bevy::render::render_resource::{ Extent3d, TextureDimension, TextureFormat, TextureViewDescriptor, TextureViewDimension, }; use crate::world::block::tex; pub const SIZE: usize = 16; const MIPS: u32 = 5; type Px = [u8; 4]; fn h(layer: u8, x: usize, y: usize, salt: u32) -> f32 { crate::world::terrain::hashf(0xB10C ^ salt, x as i32, y as i32, layer as i32) } fn rgb(c: [i32; 3], jitter: f32) -> Px { let j = |v: i32| (v as f32 + jitter).clamp(0.0, 255.0) as u8; [j(c[0]), j(c[1]), j(c[2]), 255] } fn noisy(layer: u8, x: usize, y: usize, base: [i32; 3], amount: f32) -> Px { rgb(base, (h(layer, x, y, 1) - 0.5) * 2.0 * amount) } const CLEAR: Px = [0, 0, 0, 0]; const GLYPHS: [&str; 16] = [ "################", "#..............#", "#.###....#.#...#", "#.#.#....###...#", "#.###.....#....#", "#..#.....###...#", "#.###.....#....#", "#..............#", "#.#...#..####..#", "#.##.##..#..#..#", "#.#.#.#..####..#", "#.#...#....#...#", "#........###...#", "#..#####.......#", "#..............#", "################", ]; /// Paints one texture layer, row-major, top row first. pub fn paint(layer: u8) -> Vec { let mut px = vec![CLEAR; SIZE * SIZE]; for y in 0..SIZE { for x in 0..SIZE { let r = h(layer, x, y, 2); px[y * SIZE + x] = match layer { tex::STONE => { let base = if r < 0.12 { [104, 104, 108] } else { [128, 128, 131] }; noisy(layer, x, y, base, 12.0) } tex::DIRT => dirt(layer, x, y, r), tex::GRASS_TOP => { let base = if r < 0.2 { [78, 140, 44] } else { [96, 162, 56] }; noisy(layer, x, y, base, 16.0) } tex::GRASS_SIDE => { let depth = 3 + (h(layer, x, 0, 3) * 3.0) as usize; if y < depth { noisy(layer, x, y, [92, 158, 52], 14.0) } else { dirt(layer, x, y, r) } } tex::SAND => noisy(layer, x, y, [222, 208, 160], 9.0), tex::GRAVEL => { let pebble = h(layer, x / 3, y / 3, 4); let base = if pebble < 0.33 { [110, 104, 100] } else if pebble < 0.66 { [140, 134, 128] } else { [92, 88, 90] }; noisy(layer, x, y, base, 10.0) } tex::WATER => { let wave = ((x as f32 * 0.8 + y as f32 * 0.4).sin() * 0.5 + 0.5) * 18.0; rgb([38, 92, 196], wave + (r - 0.5) * 10.0) } tex::LOG_SIDE => { let stripe = h(layer, x, 0, 5); let base = if stripe < 0.3 { [82, 60, 36] } else { [106, 79, 48] }; noisy(layer, x, y, base, 8.0) } tex::LOG_TOP => { let (dx, dy) = (x as f32 - 7.5, y as f32 - 7.5); let d = dx.abs().max(dy.abs()); if d > 6.5 { noisy(layer, x, y, [96, 70, 42], 8.0) } else if (d as i32) % 2 == 0 { noisy(layer, x, y, [176, 140, 88], 6.0) } else { noisy(layer, x, y, [150, 116, 70], 6.0) } } tex::LEAVES => { if r < 0.22 { CLEAR } else { let base = if h(layer, x, y, 6) < 0.3 { [46, 104, 34] } else { [62, 128, 44] }; noisy(layer, x, y, base, 14.0) } } tex::PLANKS => { let seam = y % 4 == 3; let offset = if (y / 4) % 2 == 0 { 0 } else { 8 }; let joint = (x + offset) % 16 == 0; if seam || joint { [120, 90, 54, 255] } else { noisy(layer, x, y, [180, 141, 88], 7.0 + h(layer, x, y / 4, 7) * 8.0) } } tex::GLASS => { let edge = x == 0 || y == 0 || x == SIZE - 1 || y == SIZE - 1; let glint = (x + y == 9 || x + y == 11) && (3..9).contains(&x); if edge { [205, 228, 240, 255] } else if glint { [235, 245, 255, 200] } else { CLEAR } } tex::MOSS => { let base = if r < 0.25 { [88, 132, 58] } else { [66, 106, 46] }; noisy(layer, x, y, base, 12.0) } tex::VINE => { let strand = [3usize, 8, 12]; let on = strand.iter().any(|&s| { let wobble = ((y as f32 * 0.9 + s as f32).sin() * 1.2).round() as i32; (s as i32 + wobble) == x as i32 }); let leaf = r < 0.12; if on || leaf { noisy(layer, x, y, [52, 116, 38], 14.0) } else { CLEAR } } tex::GLYPHSTONE => { let carved = GLYPHS[y].as_bytes()[x] == b'#'; if carved { noisy(layer, x, y, [150, 118, 70], 6.0) } else { noisy(layer, x, y, [214, 186, 124], 7.0) } } tex::LUMEN => { let (dx, dy) = (x as f32 - 7.5, y as f32 - 7.5); let facet = ((dx * 0.7 + dy).abs() < 1.5) || ((dx - dy * 0.6).abs() < 1.2); let edge = x == 0 || y == 0 || x == SIZE - 1 || y == SIZE - 1; if edge { [60, 140, 150, 255] } else if facet { [210, 255, 245, 255] } else { noisy(layer, x, y, [110, 228, 214], 16.0) } } tex::SNOW => noisy(layer, x, y, [238, 244, 250], 6.0), tex::SNOW_SIDE => { let depth = 3 + (h(layer, x, 0, 8) * 3.0) as usize; if y < depth { noisy(layer, x, y, [236, 242, 248], 6.0) } else { dirt(layer, x, y, r) } } tex::CLAY => noisy(layer, x, y, [156, 162, 178], 6.0), tex::BASALT => { let band = h(layer, x, 0, 9); let base = if band < 0.4 { [46, 46, 54] } else { [62, 62, 70] }; noisy(layer, x, y, base, 7.0) } tex::BLOOM => { let (dx, dy) = (x as i32 - 8, y as i32 - 5); if dx * dx + dy * dy <= 5 { if dx * dx + dy * dy <= 1 { [250, 220, 90, 255] } else { noisy(layer, x, y, [222, 70, 150], 14.0) } } else if (x == 8 || (x == 7 && y == 11)) && y > 7 { [56, 128, 44, 255] } else if (y == 10 && (x == 9 || x == 10)) || (y == 12 && (x == 6 || x == 5)) { [70, 146, 52, 255] } else { CLEAR } } tex::SAPLING => { let (dx, dy) = (x as i32 - 8, y as i32 - 6); if dx * dx + dy * dy * 2 <= 18 && r > 0.15 { noisy(layer, x, y, [62, 136, 44], 14.0) } else if (x == 8 || x == 7) && y >= 8 { [96, 70, 40, 255] } else { CLEAR } } tex::DEEPCORE => { let base = if r < 0.1 { [70, 40, 90] } else { [30, 28, 36] }; noisy(layer, x, y, base, 6.0) } tex::TUFT => { let blade = h(layer, x, 0, 10); let height = 4 + (blade * 10.0) as usize; if x % 2 == (blade * 2.0) as usize % 2 && y >= SIZE - height { noisy(layer, x, y, [92, 160, 54], 18.0) } else { CLEAR } } tex::BRICK => { let row = y / 4; let mortar_y = y % 4 == 3; let mortar_x = (x + if row % 2 == 0 { 0 } else { 4 }) % 8 == 7; if mortar_y || mortar_x { noisy(layer, x, y, [188, 180, 168], 6.0) } else { noisy(layer, x, y, [154, 72, 56], 12.0) } } tex::SANDSTONE => { let band = (y / 5) % 2 == 0; let base = if band { [214, 190, 132] } else { [200, 174, 116] }; noisy(layer, x, y, base, 6.0) } _ => [255, 0, 255, 255], }; } } px } fn dirt(layer: u8, x: usize, y: usize, r: f32) -> Px { let base = if r < 0.15 { [100, 70, 46] } else if r > 0.92 { [140, 104, 72] } else { [122, 87, 59] }; noisy(layer, x, y, base, 10.0) } /// Halves a layer, averaging colour weighted by alpha so cut-out edges do not darken. fn downsample(src: &[Px], size: usize) -> Vec { let half = size / 2; let mut out = vec![CLEAR; half * half]; for y in 0..half { for x in 0..half { let mut c = [0f32; 3]; let mut a = 0f32; for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] { let p = src[(y * 2 + dy) * size + x * 2 + dx]; let w = p[3] as f32; for i in 0..3 { c[i] += p[i] as f32 * w; } a += w; } out[y * half + x] = if a > 0.0 { [(c[0] / a) as u8, (c[1] / a) as u8, (c[2] / a) as u8, (a / 4.0) as u8] } else { CLEAR }; } } out } pub fn build_array() -> Image { let layers = tex::COUNT; let mut data = Vec::new(); for layer in 0..layers as u8 { let mut level = paint(layer); let mut size = SIZE; for _ in 0..MIPS { data.extend(level.iter().flatten()); if size > 1 { level = downsample(&level, size); size /= 2; } } } let mut image = Image::new( Extent3d { width: SIZE as u32, height: SIZE as u32, depth_or_array_layers: layers, }, TextureDimension::D2, // Sized for mip 0 to satisfy the constructor; replaced by the full mip chain below. vec![0; SIZE * SIZE * 4 * layers as usize], TextureFormat::Rgba8UnormSrgb, RenderAssetUsages::RENDER_WORLD, ); image.data = Some(data); image.texture_descriptor.mip_level_count = MIPS; image.texture_view_descriptor = Some(TextureViewDescriptor { dimension: Some(TextureViewDimension::D2Array), ..Default::default() }); image.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor { address_mode_u: ImageAddressMode::Repeat, address_mode_v: ImageAddressMode::Repeat, mag_filter: ImageFilterMode::Nearest, min_filter: ImageFilterMode::Nearest, mipmap_filter: ImageFilterMode::Linear, ..Default::default() }); image } /// A standalone 2D copy of one layer, for the hotbar. pub fn icon(layer: u8) -> Image { let data: Vec = paint(layer).into_iter().flatten().collect(); let mut image = Image::new( Extent3d { width: SIZE as u32, height: SIZE as u32, depth_or_array_layers: 1, }, TextureDimension::D2, data, TextureFormat::Rgba8UnormSrgb, RenderAssetUsages::RENDER_WORLD, ); image.sampler = ImageSampler::nearest(); image }