use std::f64::consts::TAU;
use noise::{Fbm, MultiFractal, Perlin};
use crate::{
generator::{TextureError, TextureGenerator, TextureMap, linear_to_srgb, validate_dimensions},
noise::{ToroidalNoise, normalize, sample_grid},
normal::{BoundaryMode, height_to_normal},
};
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct MarbleConfig {
pub seed: u32,
pub scale: f64,
pub octaves: usize,
pub warp_strength: f64,
pub vein_frequency: f64,
pub vein_sharpness: f64,
pub roughness: f64,
pub color_base: [f32; 3],
pub color_vein: [f32; 3],
pub normal_strength: f32,
}
impl Default for MarbleConfig {
fn default() -> Self {
Self {
seed: 55,
scale: 3.0,
octaves: 5,
warp_strength: 0.6,
vein_frequency: 3.0,
vein_sharpness: 2.0,
roughness: 0.08,
color_base: [0.92, 0.90, 0.87],
color_vein: [0.42, 0.38, 0.34],
normal_strength: 1.5,
}
}
}
pub struct MarbleGenerator {
config: MarbleConfig,
warp_u_noise: ToroidalNoise<Fbm<Perlin>>,
warp_v_noise: ToroidalNoise<Fbm<Perlin>>,
base_noise: ToroidalNoise<Fbm<Perlin>>,
}
impl MarbleGenerator {
pub fn new(config: MarbleConfig) -> Self {
let fbm_warp_u: Fbm<Perlin> = Fbm::new(config.seed).set_octaves(config.octaves);
let fbm_warp_v: Fbm<Perlin> =
Fbm::new(config.seed.wrapping_add(100)).set_octaves(config.octaves);
let fbm_base: Fbm<Perlin> =
Fbm::new(config.seed.wrapping_add(200)).set_octaves(config.octaves);
let warp_u_noise = ToroidalNoise::new(fbm_warp_u, config.scale);
let warp_v_noise = ToroidalNoise::new(fbm_warp_v, config.scale);
let base_noise = ToroidalNoise::new(fbm_base, config.scale);
Self {
config,
warp_u_noise,
warp_v_noise,
base_noise,
}
}
}
impl TextureGenerator for MarbleGenerator {
fn generate(&self, width: u32, height: u32) -> Result<TextureMap, TextureError> {
validate_dimensions(width, height)?;
let c = &self.config;
let w = width as usize;
let h = height as usize;
let n = w * h;
let freq = c.scale;
let col_cos: Vec<f64> = (0..w)
.map(|x| (TAU * x as f64 / w as f64).cos() * freq)
.collect();
let col_sin: Vec<f64> = (0..w)
.map(|x| (TAU * x as f64 / w as f64).sin() * freq)
.collect();
let row_cos: Vec<f64> = (0..h)
.map(|y| (TAU * y as f64 / h as f64).cos() * freq)
.collect();
let row_sin: Vec<f64> = (0..h)
.map(|y| (TAU * y as f64 / h as f64).sin() * freq)
.collect();
let base_grid = sample_grid(&self.base_noise, width, height);
let mut heights = vec![0.0f64; n];
let mut albedo = vec![0u8; n * 4];
let mut roughness_buf = vec![0u8; n * 4];
for y in 0..h {
let nz = row_cos[y];
let nw = row_sin[y];
let v = y as f64 / h as f64;
for x in 0..w {
let nx = col_cos[x];
let ny = col_sin[x];
let u = x as f64 / w as f64;
let du = self.warp_u_noise.get_precomputed(nx, ny, nz, nw) * c.warp_strength;
let dv = self.warp_v_noise.get_precomputed(nx, ny, nz, nw) * c.warp_strength;
let raw = bilinear_sample_torus(&base_grid, w, h, u + du, v + dv);
let raw_norm = raw * 0.5 + 0.5;
let vein_t =
((raw_norm * TAU * c.vein_frequency).sin().abs()).powf(c.vein_sharpness);
let idx = y * w + x;
heights[idx] = vein_t * 0.8 + normalize(raw) * 0.2;
let vein_tf = vein_t as f32;
let r = lerp(c.color_vein[0], c.color_base[0], vein_tf);
let g = lerp(c.color_vein[1], c.color_base[1], vein_tf);
let b = lerp(c.color_vein[2], c.color_base[2], vein_tf);
let ai = idx * 4;
albedo[ai] = linear_to_srgb(r);
albedo[ai + 1] = linear_to_srgb(g);
albedo[ai + 2] = linear_to_srgb(b);
albedo[ai + 3] = 255;
let rough = (c.roughness * (1.0 - vein_t * 0.3)) as f32;
roughness_buf[ai] = 255; roughness_buf[ai + 1] = (rough * 255.0).round() as u8;
roughness_buf[ai + 2] = 0; roughness_buf[ai + 3] = 255;
}
}
let normal = height_to_normal(
&heights,
width,
height,
c.normal_strength,
BoundaryMode::Wrap,
);
Ok(TextureMap {
albedo,
normal,
roughness: roughness_buf,
width,
height,
})
}
}
#[inline]
fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t.clamp(0.0, 1.0)
}
#[inline]
fn bilinear_sample_torus(grid: &[f64], w: usize, h: usize, u: f64, v: f64) -> f64 {
let u = u.rem_euclid(1.0);
let v = v.rem_euclid(1.0);
let px = u * w as f64;
let py = v * h as f64;
let x0 = px as usize % w;
let y0 = py as usize % h;
let x1 = (x0 + 1) % w;
let y1 = (y0 + 1) % h;
let fx = px.fract();
let fy = py.fract();
let v00 = grid[y0 * w + x0];
let v10 = grid[y0 * w + x1];
let v01 = grid[y1 * w + x0];
let v11 = grid[y1 * w + x1];
v00 * (1.0 - fx) * (1.0 - fy) + v10 * fx * (1.0 - fy) + v01 * (1.0 - fx) * fy + v11 * fx * fy
}