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, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum PaversLayout {
Square,
Hexagonal,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct PaversConfig {
pub seed: u32,
pub scale: f64,
pub aspect_ratio: f64,
pub grout_width: f64,
pub bevel: f64,
pub cell_variance: f64,
pub roughness: f64,
pub color_stone: [f32; 3],
pub color_grout: [f32; 3],
pub layout: PaversLayout,
pub normal_strength: f32,
}
impl Default for PaversConfig {
fn default() -> Self {
Self {
seed: 23,
scale: 5.0,
aspect_ratio: 1.0,
grout_width: 0.08,
bevel: 0.5,
cell_variance: 0.10,
roughness: 0.30,
color_stone: [0.48, 0.44, 0.40],
color_grout: [0.28, 0.27, 0.26],
layout: PaversLayout::Square,
normal_strength: 3.5,
}
}
}
pub struct PaversGenerator {
config: PaversConfig,
surf_noise: ToroidalNoise<Fbm<Perlin>>,
}
impl PaversGenerator {
pub fn new(config: PaversConfig) -> Self {
let fbm: Fbm<Perlin> = Fbm::new(config.seed.wrapping_add(50)).set_octaves(4);
let surf_noise = ToroidalNoise::new(fbm, config.scale * 2.0);
Self { config, surf_noise }
}
}
struct PaversCell<'a> {
config: &'a PaversConfig,
surf_grid: &'a [f64],
bevel_r: f64,
hx: f64,
hy: f64,
cols: f64,
rows: f64,
width: usize,
}
impl SurfaceCell for PaversCell<'_> {
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 raw_surf = normalize(self.surf_grid[idx]);
let (sdf_val, cell_id_u, cell_id_v) = match c.layout {
PaversLayout::Square => {
square_cell(u, v, self.cols, self.rows, self.hx, self.hy, self.bevel_r)
}
PaversLayout::Hexagonal => hex_cell(u, v, c.scale),
};
let (h_val, color) = if sdf_val < 0.0 {
let edge_t = ((-sdf_val) / (self.bevel_r + 0.005)).clamp(0.0, 1.0);
let noise_bump = (raw_surf - 0.5) * c.roughness * 0.4;
let h_val = (edge_t + noise_bump * edge_t).clamp(0.0, 1.0);
let cv = cell_hash(cell_id_u, cell_id_v, c.seed);
let jitter = (cv - 0.5) * 2.0 * c.cell_variance;
let color = [
(c.color_stone[0] + jitter as f32).clamp(0.0, 1.0),
(c.color_stone[1] + jitter as f32 * 0.8).clamp(0.0, 1.0),
(c.color_stone[2] + jitter as f32 * 0.6).clamp(0.0, 1.0),
];
(h_val, color)
} else {
(raw_surf * c.roughness * 0.04, c.color_grout)
};
let rough_val = if sdf_val < 0.0 {
0.70 + raw_surf as f32 * 0.20
} else {
0.92
};
SurfaceSample::matte(h_val, color, rough_val)
}
}
impl PaversGenerator {
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 surf_grid = ws.as_deref_mut().map_or_else(Vec::new, |w| w.take_grid());
sample_grid_into(&self.surf_noise, width, height, &mut surf_grid);
let grout_half = (c.grout_width * 0.5).clamp(0.0, 0.45);
let bevel_r = (c.bevel * grout_half).max(0.0);
let cell = PaversCell {
config: c,
surf_grid: &surf_grid,
bevel_r,
hx: (0.5 - grout_half - bevel_r).max(0.0),
hy: (0.5 - grout_half - bevel_r).max(0.0),
cols: (c.scale * c.aspect_ratio).round(),
rows: c.scale.round(),
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(surf_grid);
}
result
}
}
impl TextureGenerator for PaversGenerator {
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))
}
}
fn square_cell(
u: f64,
v: f64,
cols: f64,
rows: f64,
hx: f64,
hy: f64,
bevel_r: f64,
) -> (f64, i64, i64) {
let u_scaled = u * cols;
let v_scaled = v * rows;
let cell_u = u_scaled.floor() as i64;
let cell_v = v_scaled.floor() as i64;
let cx = u_scaled.fract() - 0.5;
let cy = v_scaled.fract() - 0.5;
let dx = cx.abs() - hx;
let dy = cy.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;
(sdf, cell_u, cell_v)
}
fn hex_cell(u: f64, v: f64, scale: f64) -> (f64, i64, i64) {
const SQRT3: f64 = 1.732_050_807_568_877_3;
let scale = scale.round().max(1.0);
let hex_r = 1.0 / (scale * SQRT3);
let cols_float = scale * SQRT3 / 1.5;
let cols_int = cols_float.round().max(1.0);
let us = u * (cols_float / cols_int);
let qf = (2.0 / 3.0) * us / hex_r;
let rf = (-1.0 / 3.0) * us / hex_r + (SQRT3 / 3.0) * v / hex_r;
let sf = -qf - rf;
let (q, r, _s) = cube_round(qf, rf, sf);
let cx = hex_r * 1.5 * q as f64;
let cy = hex_r * (SQRT3 / 2.0 * q as f64 + SQRT3 * r as f64);
let dx = us - cx;
let dy = v - cy;
let sdf = hex_sdf(dx, dy, hex_r);
(sdf, q, r)
}
#[inline]
fn cube_round(qf: f64, rf: f64, sf: f64) -> (i64, i64, i64) {
let (rq, rr, rs) = (qf.round() as i64, rf.round() as i64, sf.round() as i64);
let (dq, dr, ds) = (
(rq as f64 - qf).abs(),
(rr as f64 - rf).abs(),
(rs as f64 - sf).abs(),
);
if dq > dr && dq > ds {
(-rr - rs, rr, rs)
} else if dr > ds {
(rq, -rq - rs, rs)
} else {
(rq, rr, -rq - rr)
}
}
#[inline]
fn hex_sdf(mut px: f64, mut py: f64, r: f64) -> f64 {
const KX: f64 = -0.866_025_403_784;
const KY: f64 = 0.5;
const KZ: f64 = 0.577_350_269_189;
px = px.abs();
py = py.abs();
let d = (KX * px + KY * py).min(0.0);
px -= 2.0 * d * KX;
py -= 2.0 * d * KY;
let qx = px - px.clamp(-KZ * r, KZ * r);
let qy = py - r;
(qx * qx + qy * qy).sqrt() * qy.signum()
}
fn cell_hash(bu: i64, bv: i64, seed: u32) -> f64 {
let mut h = seed as u64;
h ^= (bu as u64).wrapping_mul(6_364_136_223_846_793_005);
h ^= (bv 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)
}