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 WainscotingConfig {
pub seed: u32,
pub panels_x: usize,
pub panels_y: usize,
pub frame_width: f64,
pub panel_inset: f64,
pub grain_scale: f64,
pub grain_warp: f64,
pub color_wood_light: [f32; 3],
pub color_wood_dark: [f32; 3],
pub normal_strength: f32,
}
impl Default for WainscotingConfig {
fn default() -> Self {
Self {
seed: 37,
panels_x: 1,
panels_y: 2,
frame_width: 0.20,
panel_inset: 0.06,
grain_scale: 10.0,
grain_warp: 0.30,
color_wood_light: [0.65, 0.44, 0.20],
color_wood_dark: [0.28, 0.16, 0.07],
normal_strength: 4.0,
}
}
}
pub struct WainscotingGenerator {
config: WainscotingConfig,
grain_noise: ToroidalNoise<Fbm<Perlin>>,
warp_noise: ToroidalNoise<Fbm<Perlin>>,
}
impl WainscotingGenerator {
pub fn new(config: WainscotingConfig) -> Self {
let grain_fbm: Fbm<Perlin> = Fbm::new(config.seed).set_octaves(5);
let grain_noise = ToroidalNoise::new(grain_fbm, config.grain_scale);
let warp_fbm: Fbm<Perlin> = Fbm::new(config.seed.wrapping_add(77)).set_octaves(3);
let warp_noise = ToroidalNoise::new(warp_fbm, config.grain_scale * 0.3);
Self {
config,
grain_noise,
warp_noise,
}
}
}
impl TextureGenerator for WainscotingGenerator {
fn generate(&self, width: u32, height: u32) -> Result<TextureMap, TextureError> {
validate_dimensions(width, height)?;
let c = &self.config;
let grain_grid = sample_grid(&self.grain_noise, width, height);
let warp_grid = sample_grid(&self.warp_noise, width, height);
let w = width as usize;
let h = height as usize;
let n = w * h;
let panels_x = c.panels_x.max(1);
let panels_y = c.panels_y.max(1);
let fw = (c.frame_width * 0.5).clamp(0.0, 0.48);
let panel_hx = 0.5 - fw;
let panel_hy = 0.5 - fw;
let bevel_w = fw * 0.3;
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 v = y as f64 / h as f64;
for x in 0..w {
let u = x as f64 / w as f64;
let idx = y * w + x;
let warp_u = normalize(warp_grid[idx]) - 0.5; let warped_u = (u + warp_u * c.grain_warp * 0.1).rem_euclid(1.0);
let grain_raw = bilinear_sample_torus(&grain_grid, w, h, warped_u, v);
let grain_t = normalize(grain_raw);
let cell_u = (u * panels_x as f64).fract();
let cell_v = (v * panels_y as f64).fract();
let cx = cell_u - 0.5;
let cy = cell_v - 0.5;
let dist_to_frame_u = panel_hx - cx.abs();
let dist_to_frame_v = panel_hy - cy.abs();
let dist_inside = dist_to_frame_u.min(dist_to_frame_v);
let panel_height = if dist_inside < 0.0 {
1.0_f64
} else if dist_inside < bevel_w {
1.0 - (dist_inside / bevel_w) * c.panel_inset
} else {
1.0 - c.panel_inset
};
heights[idx] = (panel_height + grain_t * 0.05).clamp(0.0, 1.0);
let r = lerp(c.color_wood_dark[0], c.color_wood_light[0], grain_t as f32);
let g = lerp(c.color_wood_dark[1], c.color_wood_light[1], grain_t as f32);
let b = lerp(c.color_wood_dark[2], c.color_wood_light[2], grain_t as f32);
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 = 0.75 + grain_t * 0.1;
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,
})
}
}
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
}
#[inline]
fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t.clamp(0.0, 1.0)
}