use noise::{Fbm, MultiFractal, Perlin};
use crate::{
generator::{TextureError, TextureGenerator, TextureMap, linear_to_srgb, validate_dimensions},
noise::{ToroidalNoise, sample_grid},
normal::{BoundaryMode, height_to_normal},
};
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct BrickConfig {
pub seed: u32,
pub scale: f64,
pub row_offset: f64,
pub aspect_ratio: f64,
pub mortar_size: f64,
pub bevel: f64,
pub cell_variance: f64,
pub roughness: f64,
pub color_brick: [f32; 3],
pub color_mortar: [f32; 3],
pub normal_strength: f32,
}
impl Default for BrickConfig {
fn default() -> Self {
Self {
seed: 42,
scale: 4.0,
row_offset: 0.5,
aspect_ratio: 2.0,
mortar_size: 0.05,
bevel: 0.5,
cell_variance: 0.15,
roughness: 0.5,
color_brick: [0.56, 0.28, 0.18],
color_mortar: [0.76, 0.73, 0.67],
normal_strength: 4.0,
}
}
}
pub struct BrickGenerator {
config: BrickConfig,
rough_noise: ToroidalNoise<Fbm<Perlin>>,
}
impl BrickGenerator {
pub fn new(config: BrickConfig) -> Self {
let fbm_rough: Fbm<Perlin> = Fbm::new(config.seed.wrapping_add(50)).set_octaves(4);
let rough_noise = ToroidalNoise::new(fbm_rough, config.scale * config.aspect_ratio * 2.0);
Self {
config,
rough_noise,
}
}
}
impl TextureGenerator for BrickGenerator {
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 w = width as usize;
let h = height as usize;
let n = w * h;
let bevel_r = (c.bevel * c.mortar_size * 0.5).max(0.0);
let hx = (0.5 - c.mortar_size - bevel_r).max(0.0);
let hy = (0.5 - c.mortar_size - bevel_r).max(0.0);
let scale = c.scale.round();
let cols = (scale * c.aspect_ratio).round();
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 v_scaled = v * scale;
let row_id = v_scaled.floor();
let v_frac = v_scaled.fract();
for x in 0..w {
let u = x as f64 / w as f64;
let u_shifted = u * cols + row_id * c.row_offset;
let brick_id_u = u_shifted.floor() as i64;
let brick_id_v = row_id as i64;
let u_frac = u_shifted.fract();
let cx = u_frac - 0.5;
let cy = v_frac - 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;
let raw_surf = normalize(rough_grid[y * w + x]);
let h_val;
let (r, gr, b);
if sdf < 0.0 {
let edge_t = ((-sdf) / (bevel_r + 0.01)).clamp(0.0, 1.0);
let noise_bump = (raw_surf - 0.5) * c.roughness * 0.4;
h_val = (edge_t + noise_bump * edge_t).clamp(0.0, 1.0);
let cv = cell_hash(brick_id_u, brick_id_v, c.seed);
let jitter = (cv - 0.5) * 2.0 * c.cell_variance;
r = (c.color_brick[0] + jitter as f32).clamp(0.0, 1.0);
gr = (c.color_brick[1] + jitter as f32 * 0.7).clamp(0.0, 1.0);
b = (c.color_brick[2] + jitter as f32 * 0.5).clamp(0.0, 1.0);
} else {
h_val = raw_surf * c.roughness * 0.04;
r = c.color_mortar[0];
gr = c.color_mortar[1];
b = c.color_mortar[2];
}
let idx = y * w + x;
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.45 + raw_surf as f32 * 0.3
} else {
0.90
};
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)
}
#[inline]
fn normalize(v: f64) -> f64 {
v * 0.5 + 0.5
}