use crate::texture::PixelBuffer;
fn hash2d(ix: i32, iy: i32) -> f32 {
let n = ix.wrapping_mul(1619).wrapping_add(iy.wrapping_mul(31337));
let n = n
.wrapping_mul(n)
.wrapping_mul(60493)
.wrapping_add(n.wrapping_mul(19990303));
(n.unsigned_abs() as f32) / (u32::MAX as f32)
}
#[allow(dead_code)]
pub fn smoothstep(t: f32) -> f32 {
t * t * (3.0 - 2.0 * t)
}
#[allow(dead_code)]
pub fn smootherstep(t: f32) -> f32 {
t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
}
#[allow(dead_code)]
pub fn value_noise(x: f32, y: f32) -> f32 {
let ix = x.floor() as i32;
let iy = y.floor() as i32;
let fx = x - x.floor();
let fy = y - y.floor();
let ux = smoothstep(fx);
let uy = smoothstep(fy);
let v00 = hash2d(ix, iy);
let v10 = hash2d(ix + 1, iy);
let v01 = hash2d(ix, iy + 1);
let v11 = hash2d(ix + 1, iy + 1);
let bottom = v00 + ux * (v10 - v00);
let top = v01 + ux * (v11 - v01);
bottom + uy * (top - bottom)
}
#[allow(dead_code)]
pub fn fbm(x: f32, y: f32, octaves: u32, lacunarity: f32, gain: f32) -> f32 {
let mut value = 0.0f32;
let mut amplitude = 0.5f32;
let mut frequency = 1.0f32;
let mut max_value = 0.0f32;
for _ in 0..octaves {
value += amplitude * value_noise(x * frequency, y * frequency);
max_value += amplitude;
amplitude *= gain;
frequency *= lacunarity;
}
if max_value > 0.0 {
(value / max_value).clamp(0.0, 1.0)
} else {
0.0
}
}
#[allow(dead_code)]
pub fn generate_noise_texture(width: u32, height: u32, scale: f32) -> PixelBuffer {
let mut buf = PixelBuffer::new(width, height);
let scale = scale.max(f32::EPSILON);
for y in 0..height {
for x in 0..width {
let nx = x as f32 / width as f32 * scale;
let ny = y as f32 / height as f32 * scale;
let v = value_noise(nx, ny);
let c = (v * 255.0).round() as u8;
buf.set_pixel(x, y, c, c, c, 255);
}
}
buf
}
#[allow(dead_code)]
pub fn generate_fbm_texture(width: u32, height: u32, scale: f32, octaves: u32) -> PixelBuffer {
let mut buf = PixelBuffer::new(width, height);
let scale = scale.max(f32::EPSILON);
for y in 0..height {
for x in 0..width {
let nx = x as f32 / width as f32 * scale;
let ny = y as f32 / height as f32 * scale;
let v = fbm(nx, ny, octaves, 2.0, 0.5);
let c = (v * 255.0).round() as u8;
buf.set_pixel(x, y, c, c, c, 255);
}
}
buf
}
#[inline]
fn lerp_color(a: [u8; 3], b: [u8; 3], t: f32) -> [u8; 3] {
[
(a[0] as f32 + t * (b[0] as f32 - a[0] as f32)).round() as u8,
(a[1] as f32 + t * (b[1] as f32 - a[1] as f32)).round() as u8,
(a[2] as f32 + t * (b[2] as f32 - a[2] as f32)).round() as u8,
]
}
#[allow(dead_code)]
pub fn generate_wood_texture(
width: u32,
height: u32,
rings: f32,
noise_scale: f32,
color_a: [u8; 3],
color_b: [u8; 3],
) -> PixelBuffer {
let mut buf = PixelBuffer::new(width, height);
let rings = rings.max(1.0);
let noise_scale = noise_scale.max(f32::EPSILON);
for y in 0..height {
for x in 0..width {
let nx = x as f32 / width as f32;
let ny = y as f32 / height as f32;
let cx = nx - 0.5;
let cy = ny - 0.5;
let dist = (cx * cx + cy * cy).sqrt();
let perturbation = value_noise(nx * noise_scale, ny * noise_scale) * 0.15;
let pattern = ((dist + perturbation) * rings * std::f32::consts::PI * 2.0).sin();
let t = (pattern + 1.0) * 0.5;
let [r, g, b] = lerp_color(color_a, color_b, t);
buf.set_pixel(x, y, r, g, b, 255);
}
}
buf
}
#[allow(dead_code)]
pub fn generate_marble_texture(
width: u32,
height: u32,
scale: f32,
color_a: [u8; 3],
color_b: [u8; 3],
) -> PixelBuffer {
let mut buf = PixelBuffer::new(width, height);
let scale = scale.max(f32::EPSILON);
for y in 0..height {
for x in 0..width {
let nx = x as f32 / width as f32 * scale;
let ny = y as f32 / height as f32 * scale;
let turbulence = fbm(nx, ny, 6, 2.0, 0.5);
let pattern = ((nx + turbulence * 4.0) * std::f32::consts::PI).sin();
let t = (pattern + 1.0) * 0.5;
let [r, g, b] = lerp_color(color_a, color_b, t);
buf.set_pixel(x, y, r, g, b, 255);
}
}
buf
}
#[allow(dead_code)]
pub fn generate_voronoi_texture(
width: u32,
height: u32,
cell_size: f32,
color_a: [u8; 3],
color_b: [u8; 3],
) -> PixelBuffer {
let mut buf = PixelBuffer::new(width, height);
let cell_size = cell_size.max(1.0);
for y in 0..height {
for x in 0..width {
let px = x as f32;
let py = y as f32;
let cx = (px / cell_size).floor() as i32;
let cy = (py / cell_size).floor() as i32;
let mut min_dist = f32::MAX;
for dy in -1i32..=1 {
for dx in -1i32..=1 {
let nx = cx + dx;
let ny = cy + dy;
let seed_x = (nx as f32 + hash2d(nx, ny * 7 + 1)) * cell_size;
let seed_y = (ny as f32 + hash2d(nx * 3 + 2, ny)) * cell_size;
let ddx = px - seed_x;
let ddy = py - seed_y;
let dist = (ddx * ddx + ddy * ddy).sqrt();
if dist < min_dist {
min_dist = dist;
}
}
}
let t = (min_dist / cell_size).clamp(0.0, 1.0);
let [r, g, b] = lerp_color(color_a, color_b, t);
buf.set_pixel(x, y, r, g, b, 255);
}
}
buf
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn value_noise_in_range_0_1() {
for i in 0..100 {
let v = value_noise(i as f32 * 0.13, i as f32 * 0.07);
assert!(
(0.0..=1.0).contains(&v),
"value_noise out of range: {v} at i={i}"
);
}
}
#[test]
fn value_noise_continuity() {
let a = value_noise(1.0, 1.0);
let b = value_noise(1.0 + 1e-4, 1.0 + 1e-4);
assert!(
(a - b).abs() < 0.05,
"value_noise not continuous: {a} vs {b}"
);
}
#[test]
fn smoothstep_zero_returns_zero() {
assert_eq!(smoothstep(0.0), 0.0);
}
#[test]
fn smoothstep_one_returns_one() {
assert!((smoothstep(1.0) - 1.0).abs() < f32::EPSILON * 4.0);
}
#[test]
fn smootherstep_midpoint_is_half() {
assert!((smootherstep(0.5) - 0.5).abs() < 1e-6);
}
#[test]
fn fbm_in_range_0_1() {
for i in 0..50 {
let v = fbm(i as f32 * 0.17, i as f32 * 0.23, 4, 2.0, 0.5);
assert!((0.0..=1.0).contains(&v), "fbm out of range: {v} at i={i}");
}
}
#[test]
fn generate_noise_texture_correct_size() {
let buf = generate_noise_texture(32, 32, 4.0);
assert_eq!(buf.width, 32);
assert_eq!(buf.height, 32);
assert_eq!(buf.byte_len(), 32 * 32 * 4);
}
#[test]
fn generate_noise_texture_all_pixels_valid() {
let buf = generate_noise_texture(16, 16, 4.0);
for chunk in buf.pixels.chunks_exact(4) {
assert_eq!(chunk[3], 255, "alpha must be 255");
assert_eq!(chunk[0], chunk[1]);
assert_eq!(chunk[1], chunk[2]);
}
}
#[test]
fn generate_fbm_texture_not_uniform() {
let buf = generate_fbm_texture(32, 32, 4.0, 4);
let first = buf.get_pixel(0, 0)[0];
let all_same = buf.pixels.chunks_exact(4).all(|c| c[0] == first);
assert!(!all_same, "fbm texture should not be uniform");
}
#[test]
fn generate_wood_texture_size_correct() {
let buf = generate_wood_texture(64, 64, 8.0, 4.0, [210, 180, 140], [139, 90, 43]);
assert_eq!(buf.width, 64);
assert_eq!(buf.height, 64);
assert_eq!(buf.byte_len(), 64 * 64 * 4);
}
#[test]
fn generate_marble_texture_size_correct() {
let buf = generate_marble_texture(64, 64, 4.0, [240, 240, 240], [80, 80, 80]);
assert_eq!(buf.width, 64);
assert_eq!(buf.height, 64);
}
#[test]
fn generate_voronoi_texture_not_all_same_color() {
let buf = generate_voronoi_texture(64, 64, 16.0, [255, 255, 255], [0, 0, 0]);
let first = buf.get_pixel(0, 0)[0];
let all_same = buf.pixels.chunks_exact(4).all(|c| c[0] == first);
assert!(!all_same, "voronoi texture should not be uniform");
}
#[test]
fn noise_texture_alpha_is_255() {
let buf = generate_noise_texture(16, 16, 4.0);
for chunk in buf.pixels.chunks_exact(4) {
assert_eq!(chunk[3], 255);
}
}
}