use noise::{Fbm, MultiFractal, Perlin};
use crate::{
generator::{TextureError, TextureGenerator, TextureMap, Workspace, validate_dimensions},
noise::{ToroidalNoise, normalize, sample_grid_into},
surface::{SurfaceCell, SurfaceSample, generate_surface},
};
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct FabricConfig {
pub seed: u32,
pub thread_count: f64,
pub thread_width: f64,
pub weave_contrast: f64,
pub fuzz: f64,
pub color_warp: [f32; 3],
pub color_weft: [f32; 3],
pub normal_strength: f32,
}
impl Default for FabricConfig {
fn default() -> Self {
Self {
seed: 29,
thread_count: 24.0,
thread_width: 0.85,
weave_contrast: 0.6,
fuzz: 0.35,
color_warp: [0.55, 0.36, 0.24],
color_weft: [0.62, 0.44, 0.30],
normal_strength: 3.0,
}
}
}
pub struct FabricGenerator {
config: FabricConfig,
fuzz_noise: ToroidalNoise<Fbm<Perlin>>,
mottle_noise: ToroidalNoise<Fbm<Perlin>>,
}
impl FabricGenerator {
pub fn new(config: FabricConfig) -> Self {
let fbm_fuzz: Fbm<Perlin> = Fbm::new(config.seed.wrapping_add(50)).set_octaves(4);
let fuzz_noise = ToroidalNoise::new(fbm_fuzz, config.thread_count.max(1.0) * 1.5);
let fbm_mottle: Fbm<Perlin> = Fbm::new(config.seed).set_octaves(3);
let mottle_noise = ToroidalNoise::new(fbm_mottle, 3.0);
Self {
config,
fuzz_noise,
mottle_noise,
}
}
fn generate_inner(
&self,
width: u32,
height: u32,
mut ws: Option<&mut Workspace>,
) -> Result<TextureMap, TextureError> {
validate_dimensions(width, height)?;
let c = &self.config;
let mut fuzz_grid = ws.as_deref_mut().map_or_else(Vec::new, |w| w.take_grid());
sample_grid_into(&self.fuzz_noise, width, height, &mut fuzz_grid);
let mut mottle_grid = ws.as_deref_mut().map_or_else(Vec::new, |w| w.take_grid());
sample_grid_into(&self.mottle_noise, width, height, &mut mottle_grid);
let cell = FabricCell {
config: c,
fuzz_grid: &fuzz_grid,
mottle_grid: &mottle_grid,
threads: c.thread_count.round().clamp(2.0, 128.0),
width: width as usize,
};
let result = generate_surface(width, height, c.normal_strength, ws.as_deref_mut(), &cell);
if let Some(ws) = ws {
ws.return_grid(fuzz_grid);
ws.return_grid(mottle_grid);
}
result
}
}
impl TextureGenerator for FabricGenerator {
fn generate(&self, width: u32, height: u32) -> Result<TextureMap, TextureError> {
self.generate_inner(width, height, None)
}
fn generate_with_workspace(
&self,
width: u32,
height: u32,
workspace: &mut Workspace,
) -> Result<TextureMap, TextureError> {
self.generate_inner(width, height, Some(workspace))
}
}
struct FabricCell<'a> {
config: &'a FabricConfig,
fuzz_grid: &'a [f64],
mottle_grid: &'a [f64],
threads: f64,
width: usize,
}
impl SurfaceCell for FabricCell<'_> {
fn sample(&self, x: u32, y: u32, u: f64, v: f64) -> SurfaceSample {
let c = self.config;
let idx = y as usize * self.width + x as usize;
let su = u * self.threads;
let sv = v * self.threads;
let i = su.floor() as i64;
let j = sv.floor() as i64;
let fu = su - su.floor();
let fv = sv - sv.floor();
let tw = c.thread_width.clamp(0.3, 0.98);
let profile = |f: f64| {
let d = (f - 0.5).abs() * 2.0;
if d < tw {
(1.0 - (d / tw) * (d / tw)).sqrt()
} else {
0.0
}
};
let warp_p = profile(fu); let weft_p = profile(fv);
let contrast = c.weave_contrast.clamp(0.0, 1.0);
let under = 1.0 - 0.4 * contrast;
let (warp_lift, weft_lift) = if (i + j).rem_euclid(2) == 0 {
(under, 1.0)
} else {
(1.0, under)
};
let warp_h = warp_p * warp_lift;
let weft_h = weft_p * weft_lift;
let thread_h = warp_h.max(weft_h);
let fuzz = normalize(self.fuzz_grid[idx]);
let mottle = normalize(self.mottle_grid[idx]);
let height =
(thread_h * 0.85 + 0.05 + (fuzz - 0.5) * 0.2 * c.fuzz.clamp(0.0, 1.0)).clamp(0.0, 1.0);
let base = if warp_h >= weft_h {
c.color_warp
} else {
c.color_weft
};
let shade = (0.45 + 0.55 * thread_h) as f32;
let tint = 1.0 + (mottle as f32 - 0.5) * 0.25;
let color = [
(base[0] * shade * tint).clamp(0.0, 1.0),
(base[1] * shade * tint).clamp(0.0, 1.0),
(base[2] * shade * tint).clamp(0.0, 1.0),
];
let rough = (0.88 - thread_h as f32 * 0.10
+ (fuzz as f32 - 0.5) * 0.12 * c.fuzz.clamp(0.0, 1.0) as f32)
.clamp(0.0, 1.0);
SurfaceSample::matte(height, color, rough)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generator_produces_correct_buffer_sizes() {
let map = FabricGenerator::new(FabricConfig::default())
.generate(64, 64)
.expect("generate failed");
assert_eq!(map.albedo.len(), 64 * 64 * 4);
assert_eq!(map.normal.len(), 64 * 64 * 4);
assert_eq!(map.roughness.len(), 64 * 64 * 4);
}
#[test]
fn weave_relief_varies() {
let map = FabricGenerator::new(FabricConfig::default())
.generate(64, 64)
.expect("generate failed");
let flat = map.normal.chunks(4).all(|px| px[0] == 128 && px[1] == 128);
assert!(!flat, "weave should produce non-flat normals");
}
#[test]
fn deterministic_for_same_seed() {
let a = FabricGenerator::new(FabricConfig::default())
.generate(32, 32)
.expect("generate failed");
let b = FabricGenerator::new(FabricConfig::default())
.generate(32, 32)
.expect("generate failed");
assert_eq!(a.albedo, b.albedo);
assert_eq!(a.normal, b.normal);
}
#[test]
fn two_tone_weave_shows_both_colours() {
let config = FabricConfig {
color_warp: [1.0, 0.0, 0.0],
color_weft: [0.0, 0.0, 1.0],
fuzz: 0.0,
..FabricConfig::default()
};
let map = FabricGenerator::new(config)
.generate(128, 128)
.expect("generate failed");
let mut saw_red = false;
let mut saw_blue = false;
for px in map.albedo.chunks(4) {
if px[0] > 100 && px[2] < 50 {
saw_red = true;
}
if px[2] > 100 && px[0] < 50 {
saw_blue = true;
}
}
assert!(saw_red && saw_blue, "both thread families must be visible");
}
}