hieroxel / src /render /textures.rs
EHS-Studio's picture
Claude Opus 5.5
Hieroxel: a living voxel world in Rust and Bevy
0e0990f
Raw History Blame Contribute Delete
12.9 kB
//! 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<Px> {
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<Px> {
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<u8> = 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
}