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 AshlarConfig {
pub seed: u32,
pub rows: usize,
pub cols: usize,
pub mortar_size: f64,
pub bevel: f64,
pub cell_variance: f64,
pub chisel_depth: f64,
pub roughness: f64,
pub color_stone: [f32; 3],
pub color_mortar: [f32; 3],
pub normal_strength: f32,
}
impl Default for AshlarConfig {
fn default() -> Self {
Self {
seed: 13,
rows: 4,
cols: 4,
mortar_size: 0.04,
bevel: 0.4,
cell_variance: 0.18,
chisel_depth: 0.4,
roughness: 0.45,
color_stone: [0.52, 0.50, 0.47],
color_mortar: [0.72, 0.70, 0.65],
normal_strength: 4.5,
}
}
}
pub struct AshlarGenerator {
config: AshlarConfig,
rough_noise: ToroidalNoise<Fbm<Perlin>>,
chisel_noise: ToroidalNoise<Fbm<Perlin>>,
}
impl AshlarGenerator {
pub fn new(config: AshlarConfig) -> Self {
let fbm_rough: Fbm<Perlin> = Fbm::new(config.seed.wrapping_add(50)).set_octaves(5);
let rough_noise =
ToroidalNoise::new(fbm_rough, config.cols as f64 * config.rows as f64 * 0.8);
let fbm_chisel: Fbm<Perlin> = Fbm::new(config.seed.wrapping_add(200)).set_octaves(3);
let chisel_noise =
ToroidalNoise::new(fbm_chisel, config.cols as f64 * config.rows as f64 * 2.5);
Self {
config,
rough_noise,
chisel_noise,
}
}
}
impl TextureGenerator for AshlarGenerator {
fn generate(&self, width: u32, height: u32) -> Result<TextureMap, TextureError> {
validate_dimensions(width, height)?;
let c = &self.config;
let rough_grid = sample_grid(&self.rough_noise, width, height);
let chisel_grid = sample_grid(&self.chisel_noise, width, height);
let w = width as usize;
let h = height as usize;
let n = w * h;
let rows = c.rows.max(1);
let row_heights_raw: Vec<f64> = (0..rows)
.map(|r| 0.6 + 0.8 * cell_hash(r as i64, 99, c.seed.wrapping_add(1)))
.collect();
let total_h: f64 = row_heights_raw.iter().sum();
let row_heights: Vec<f64> = row_heights_raw.iter().map(|h| h / total_h).collect();
let row_cum: Vec<f64> = std::iter::once(0.0)
.chain(row_heights.iter().scan(0.0, |acc, rh| {
*acc += rh;
Some(*acc)
}))
.collect();
let cols_base = c.cols.max(1);
let row_ncols: Vec<usize> = (0..rows)
.map(|r| {
let h = cell_hash(r as i64, 77, c.seed.wrapping_add(2));
if h < 0.33 && cols_base > 2 {
cols_base - 1
} else if h > 0.67 {
cols_base + 1
} else {
cols_base
}
})
.collect();
let col_cums: Vec<Vec<f64>> = (0..rows)
.map(|r| {
let ncols = row_ncols[r];
let widths: Vec<f64> = (0..ncols)
.map(|cl| {
0.5 + 0.8 * cell_hash(r as i64 * 31 + cl as i64, 13, c.seed.wrapping_add(3))
})
.collect();
let total: f64 = widths.iter().sum();
let normed: Vec<f64> = widths.iter().map(|ww| ww / total).collect();
std::iter::once(0.0)
.chain(normed.iter().scan(0.0, |acc, ww| {
*acc += ww;
Some(*acc)
}))
.collect()
})
.collect();
let avg_cell_size = 1.0 / (rows as f64).max(1.0) / (cols_base as f64).max(1.0);
let mortar_gap_uv = c.mortar_size * avg_cell_size * 0.5;
let bevel_r_uv = (c.bevel * mortar_gap_uv).max(0.0);
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;
let row = {
let idx = row_cum.partition_point(|&b| b <= v).saturating_sub(1);
idx.min(rows - 1)
};
let row_lo = row_cum[row];
let row_hi = row_cum[row + 1];
let v_local = ((v - row_lo) / (row_hi - row_lo)).clamp(0.0, 1.0);
let cy_cell = v_local - 0.5;
let row_h_uv = row_hi - row_lo;
for x in 0..w {
let u = x as f64 / w as f64;
let cum = &col_cums[row];
let ncols = row_ncols[row];
let col = {
let idx = cum.partition_point(|&b| b < u).saturating_sub(1);
idx.min(ncols - 1)
};
let col_lo = cum[col];
let col_hi = cum[col + 1];
let u_local = ((u - col_lo) / (col_hi - col_lo)).clamp(0.0, 1.0);
let cx_cell = u_local - 0.5;
let col_w_uv = col_hi - col_lo;
let px = cx_cell * col_w_uv; let py = cy_cell * row_h_uv;
let hx = (col_w_uv * 0.5 - mortar_gap_uv - bevel_r_uv).max(0.0);
let hy = (row_h_uv * 0.5 - mortar_gap_uv - bevel_r_uv).max(0.0);
let dx = px.abs() - hx;
let dy = py.abs() - hy;
let sdf = (dx.max(0.0).powi(2) + dy.max(0.0).powi(2)).sqrt() + dx.max(dy).min(0.0)
- bevel_r_uv;
let idx = y * w + x;
let raw_surf = normalize(rough_grid[idx]);
let raw_chisel = normalize(chisel_grid[idx]);
let h_val;
let (r, gr, b);
if sdf < 0.0 {
let bevel_zone = (bevel_r_uv + mortar_gap_uv * 0.3 + 1e-5).max(1e-5);
let edge_t = ((-sdf) / bevel_zone).clamp(0.0, 1.0);
let edge_proximity = (1.0 - edge_t).powi(2);
let chisel_bump = raw_chisel * c.chisel_depth * edge_proximity;
let face_bump = (raw_surf - 0.5) * c.roughness * 0.35;
h_val = (edge_t + face_bump * edge_t - chisel_bump * 0.4).clamp(0.0, 1.0);
let block_id = row as i64 * 1000 + col as i64;
let cv = cell_hash(block_id, row as i64, c.seed.wrapping_add(77));
let jitter = (cv - 0.5) * 2.0 * c.cell_variance;
let chisel_darken = (chisel_bump * c.chisel_depth * 0.6) as f32;
r = (c.color_stone[0] + jitter as f32 - chisel_darken).clamp(0.0, 1.0);
gr = (c.color_stone[1] + jitter as f32 * 0.8 - chisel_darken).clamp(0.0, 1.0);
b = (c.color_stone[2] + jitter as f32 * 0.6 - chisel_darken).clamp(0.0, 1.0);
} else {
h_val = raw_surf * c.roughness * 0.03;
r = c.color_mortar[0];
gr = c.color_mortar[1];
b = c.color_mortar[2];
}
heights[idx] = h_val;
let ai = idx * 4;
albedo[ai] = linear_to_srgb(r);
albedo[ai + 1] = linear_to_srgb(gr);
albedo[ai + 2] = linear_to_srgb(b);
albedo[ai + 3] = 255;
let rough_val = if sdf < 0.0 {
0.50 + raw_surf as f32 * 0.30
} else {
0.92
};
roughness_buf[ai] = 255;
roughness_buf[ai + 1] = (rough_val * 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 cell_hash(bx: i64, by: i64, seed: u32) -> f64 {
let mut h = seed as u64;
h ^= (bx as u64).wrapping_mul(6_364_136_223_846_793_005);
h ^= (by as u64).wrapping_mul(1_442_695_040_888_963_407);
h ^= h >> 33;
h = h.wrapping_mul(0xff51_afd7_ed55_8ccd);
h ^= h >> 33;
(h as f64) * (1.0 / u64::MAX as f64)
}