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 AsphaltConfig {
pub seed: u32,
pub scale: f64,
pub aggregate_density: f64,
pub aggregate_scale: f64,
pub roughness: f64,
pub stain_level: f64,
pub color_base: [f32; 3],
pub color_aggregate: [f32; 3],
pub normal_strength: f32,
}
impl Default for AsphaltConfig {
fn default() -> Self {
Self {
seed: 88,
scale: 4.0,
aggregate_density: 0.22,
aggregate_scale: 16.0,
roughness: 0.90,
stain_level: 0.25,
color_base: [0.06, 0.06, 0.07],
color_aggregate: [0.35, 0.33, 0.30],
normal_strength: 2.5,
}
}
}
pub struct AsphaltGenerator {
config: AsphaltConfig,
macro_noise: ToroidalNoise<Fbm<Perlin>>,
micro_noise: ToroidalNoise<Fbm<Perlin>>,
agg_noise: ToroidalNoise<Fbm<Perlin>>,
}
impl AsphaltGenerator {
pub fn new(config: AsphaltConfig) -> Self {
let fbm_macro: Fbm<Perlin> = Fbm::new(config.seed).set_octaves(3);
let macro_noise = ToroidalNoise::new(fbm_macro, config.scale);
let fbm_micro: Fbm<Perlin> = Fbm::new(config.seed.wrapping_add(50)).set_octaves(4);
let micro_noise = ToroidalNoise::new(fbm_micro, config.scale * 3.0);
let fbm_agg: Fbm<Perlin> = Fbm::new(config.seed.wrapping_add(200)).set_octaves(2);
let agg_noise = ToroidalNoise::new(fbm_agg, config.aggregate_scale);
Self {
config,
macro_noise,
micro_noise,
agg_noise,
}
}
}
impl TextureGenerator for AsphaltGenerator {
fn generate(&self, width: u32, height: u32) -> Result<TextureMap, TextureError> {
validate_dimensions(width, height)?;
let c = &self.config;
let macro_grid = sample_grid(&self.macro_noise, width, height);
let micro_grid = sample_grid(&self.micro_noise, width, height);
let aggregate_grid = sample_grid(&self.agg_noise, width, height);
let w = width as usize;
let h = height as usize;
let n = w * h;
let mut heights = vec![0.0f64; n];
let mut albedo = vec![0u8; n * 4];
let mut roughness_buf = vec![0u8; n * 4];
let agg_density = c.aggregate_density.clamp(0.01, 0.99);
let agg_threshold = 1.0 - agg_density;
for y in 0..h {
for x in 0..w {
let idx = y * w + x;
let macro_v = normalize(macro_grid[idx]);
let micro_v = normalize(micro_grid[idx]);
let agg_v = normalize(aggregate_grid[idx]);
let is_agg = agg_v > agg_threshold;
let stain = (macro_v - 0.5) * c.stain_level;
let agg_bump = if is_agg { 0.3 } else { 0.0 };
heights[idx] = (micro_v * 0.7 + agg_bump).clamp(0.0, 1.0);
let (r, g, b) = if is_agg {
let brightness = lerp(0.85, 1.0, micro_v as f32);
(
c.color_aggregate[0] * brightness,
c.color_aggregate[1] * brightness,
c.color_aggregate[2] * brightness,
)
} else {
let stain_f = stain as f32;
(
(c.color_base[0] + stain_f).clamp(0.0, 1.0),
(c.color_base[1] + stain_f).clamp(0.0, 1.0),
(c.color_base[2] + stain_f).clamp(0.0, 1.0),
)
};
let ai = idx * 4;
albedo[ai] = linear_to_srgb(r);
albedo[ai + 1] = linear_to_srgb(g);
albedo[ai + 2] = linear_to_srgb(b);
albedo[ai + 3] = 255;
let rough = (c.roughness - micro_v * 0.1).clamp(0.0, 1.0) as f32;
roughness_buf[ai] = 255; roughness_buf[ai + 1] = (rough * 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,
})
}
}
#[inline]
fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t.clamp(0.0, 1.0)
}