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, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum EncausticPattern {
Checkerboard,
Octagon,
Diamond,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct EncausticConfig {
pub seed: u32,
pub scale: f64,
pub pattern: EncausticPattern,
pub grout_width: f64,
pub glaze_roughness: f64,
pub color_a: [f32; 3],
pub color_b: [f32; 3],
pub color_grout: [f32; 3],
pub normal_strength: f32,
}
impl Default for EncausticConfig {
fn default() -> Self {
Self {
seed: 47,
scale: 5.0,
pattern: EncausticPattern::Octagon,
grout_width: 0.06,
glaze_roughness: 0.04,
color_a: [0.72, 0.38, 0.22],
color_b: [0.22, 0.35, 0.65],
color_grout: [0.82, 0.80, 0.75],
normal_strength: 3.0,
}
}
}
pub struct EncausticGenerator {
config: EncausticConfig,
glaze_noise: ToroidalNoise<Fbm<Perlin>>,
}
impl EncausticGenerator {
pub fn new(config: EncausticConfig) -> Self {
let glaze_fbm: Fbm<Perlin> = Fbm::new(config.seed).set_octaves(4);
let glaze_noise = ToroidalNoise::new(glaze_fbm, config.scale * 1.5);
Self {
config,
glaze_noise,
}
}
}
impl TextureGenerator for EncausticGenerator {
fn generate(&self, width: u32, height: u32) -> Result<TextureMap, TextureError> {
validate_dimensions(width, height)?;
let c = &self.config;
let glaze_grid = sample_grid(&self.glaze_noise, width, height);
let w = width as usize;
let h = height as usize;
let n = w * h;
let scale_f = c.scale.round().max(1.0);
let grout_half = (c.grout_width * 0.5).clamp(0.0, 0.49);
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 ai = idx * 4;
let cell_u = u * scale_f;
let cell_v = v * scale_f;
let ci = cell_u.floor() as i64;
let cj = cell_v.floor() as i64;
let cx = cell_u.fract() - 0.5;
let cy = cell_v.fract() - 0.5;
let glaze = normalize(glaze_grid[idx]);
let region = classify(&c.pattern, cx, cy, ci, cj, grout_half);
let (r, g, b, h_val, rough_val) = match region {
Region::TileA => {
let perturb = (glaze - 0.5) * c.glaze_roughness * 0.6;
let r = (c.color_a[0] + perturb as f32).clamp(0.0, 1.0);
let g = (c.color_a[1] + perturb as f32).clamp(0.0, 1.0);
let b = (c.color_a[2] + perturb as f32).clamp(0.0, 1.0);
let hv = 0.85 + glaze * 0.15;
let rv = 0.20 + glaze * 0.05;
(r, g, b, hv, rv)
}
Region::TileB => {
let perturb = (glaze - 0.5) * c.glaze_roughness * 0.6;
let r = (c.color_b[0] + perturb as f32).clamp(0.0, 1.0);
let g = (c.color_b[1] + perturb as f32).clamp(0.0, 1.0);
let b = (c.color_b[2] + perturb as f32).clamp(0.0, 1.0);
let hv = 0.85 + glaze * 0.15;
let rv = 0.20 + glaze * 0.05;
(r, g, b, hv, rv)
}
Region::Grout => {
let r = c.color_grout[0];
let g = c.color_grout[1];
let b = c.color_grout[2];
(r, g, b, 0.0_f64, 0.85_f64)
}
};
heights[idx] = h_val;
albedo[ai] = linear_to_srgb(r);
albedo[ai + 1] = linear_to_srgb(g);
albedo[ai + 2] = linear_to_srgb(b);
albedo[ai + 3] = 255;
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,
})
}
}
enum Region {
TileA,
TileB,
Grout,
}
fn classify(
pattern: &EncausticPattern,
cx: f64,
cy: f64,
ci: i64,
cj: i64,
grout_half: f64,
) -> Region {
match pattern {
EncausticPattern::Checkerboard => classify_checkerboard(cx, cy, ci, cj, grout_half),
EncausticPattern::Octagon => classify_octagon(cx, cy, grout_half),
EncausticPattern::Diamond => classify_diamond(cx, cy, grout_half),
}
}
fn classify_checkerboard(cx: f64, cy: f64, ci: i64, cj: i64, grout_half: f64) -> Region {
if cx.abs() > 0.5 - grout_half || cy.abs() > 0.5 - grout_half {
return Region::Grout;
}
if (ci + cj).rem_euclid(2) == 0 {
Region::TileA
} else {
Region::TileB
}
}
fn classify_octagon(cx: f64, cy: f64, grout_half: f64) -> Region {
let cut = 0.22_f64;
let ax = cx.abs();
let ay = cy.abs();
let in_grout_border = ax > 0.5 - grout_half || ay > 0.5 - grout_half;
let corner_threshold = 0.5 - cut - grout_half;
let in_corner = ax > corner_threshold && ay > corner_threshold;
let oct_inner = 0.5 - grout_half; let in_oct_square = ax < oct_inner && ay < oct_inner;
let diagonal_limit = (0.5 - grout_half) + (0.5 - grout_half - cut);
let in_oct = in_oct_square && (ax + ay) < diagonal_limit && !in_corner;
if in_grout_border {
Region::Grout
} else if in_corner {
Region::TileB
} else if in_oct {
Region::TileA
} else {
Region::Grout
}
}
fn classify_diamond(cx: f64, cy: f64, grout_half: f64) -> Region {
use std::f64::consts::FRAC_1_SQRT_2;
const INV_SQRT2: f64 = FRAC_1_SQRT_2;
let rx = (cx + cy) * INV_SQRT2;
let ry = (cx - cy) * INV_SQRT2;
let half = 0.5 * INV_SQRT2;
let grout_r = grout_half * INV_SQRT2;
if rx.abs() < half - grout_r && ry.abs() < half - grout_r {
Region::TileA
} else {
Region::Grout
}
}