use noise::{Fbm, MultiFractal, NoiseFn, Perlin};
use crate::{
generator::{TextureError, TextureGenerator, TextureMap, linear_to_srgb, validate_dimensions},
normal::{BoundaryMode, dilate_heights, height_to_normal},
};
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct WindowConfig {
pub seed: u32,
pub frame_width: f64,
pub panes_x: usize,
pub panes_y: usize,
pub mullion_thickness: f64,
pub corner_radius: f64,
pub glass_opacity: f64,
pub grime_level: f64,
pub color_frame: [f32; 3],
pub normal_strength: f32,
}
impl Default for WindowConfig {
fn default() -> Self {
Self {
seed: 42,
frame_width: 0.08,
panes_x: 2,
panes_y: 3,
mullion_thickness: 0.025,
corner_radius: 0.02,
glass_opacity: 0.30,
grime_level: 0.15,
color_frame: [0.85, 0.82, 0.78],
normal_strength: 3.0,
}
}
}
pub struct WindowGenerator {
config: WindowConfig,
grime_fbm: Fbm<Perlin>,
}
impl WindowGenerator {
pub fn new(config: WindowConfig) -> Self {
let grime_fbm: Fbm<Perlin> = Fbm::new(config.seed).set_octaves(6);
Self { config, grime_fbm }
}
}
impl TextureGenerator for WindowGenerator {
fn generate(&self, width: u32, height: u32) -> Result<TextureMap, TextureError> {
validate_dimensions(width, height)?;
let c = &self.config;
let w = width as usize;
let h = height as usize;
let n = w * h;
let inner_half = 0.5 - c.frame_width;
let inner_r = c.corner_radius.min(inner_half * 0.9).max(0.005);
let glass_span = inner_half * 2.0; let glass_origin = 0.5 - inner_half;
let mullion_half = (c.mullion_thickness * 0.5).min(0.49);
let panes_x = c.panes_x.max(1);
let panes_y = c.panes_y.max(1);
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 py = v - 0.5;
for x in 0..w {
let u = x as f64 / w as f64;
let px = u - 0.5;
let inner_sdf =
sdf_rounded_box(px, py, inner_half - inner_r, inner_half - inner_r, inner_r);
let idx = y * w + x;
let ai = idx * 4;
if inner_sdf > 0.0 {
let edge_dist = (0.5 - px.abs()).min(0.5 - py.abs());
let edge_t = (edge_dist / (c.frame_width + 0.005)).clamp(0.0, 1.0);
heights[idx] = edge_t;
albedo[ai] = linear_to_srgb(c.color_frame[0]);
albedo[ai + 1] = linear_to_srgb(c.color_frame[1]);
albedo[ai + 2] = linear_to_srgb(c.color_frame[2]);
albedo[ai + 3] = 255;
roughness_buf[ai] = 255;
roughness_buf[ai + 1] = (0.75 * 255.0) as u8; roughness_buf[ai + 2] = 0;
roughness_buf[ai + 3] = 255;
} else {
let gu = ((u - glass_origin) / glass_span).clamp(0.0, 1.0);
let gv = ((v - glass_origin) / glass_span).clamp(0.0, 1.0);
let pu = (gu * panes_x as f64).fract();
let pv = (gv * panes_y as f64).fract();
let mhx = mullion_half * panes_x as f64;
let mhy = mullion_half * panes_y as f64;
let is_mullion_x = panes_x > 1 && (pu < mhx || pu > 1.0 - mhx);
let is_mullion_y = panes_y > 1 && (pv < mhy || pv > 1.0 - mhy);
let at_x_border = gu < mullion_half || gu > 1.0 - mullion_half;
let at_y_border = gv < mullion_half || gv > 1.0 - mullion_half;
if is_mullion_x || is_mullion_y || at_x_border || at_y_border {
heights[idx] = 1.0;
albedo[ai] = linear_to_srgb(c.color_frame[0]);
albedo[ai + 1] = linear_to_srgb(c.color_frame[1]);
albedo[ai + 2] = linear_to_srgb(c.color_frame[2]);
albedo[ai + 3] = 255;
roughness_buf[ai] = 255;
roughness_buf[ai + 1] = (0.75 * 255.0) as u8;
roughness_buf[ai + 2] = 0;
roughness_buf[ai + 3] = 255;
} else {
let grime_raw = self.grime_fbm.get([u * 8.0, v * 8.0]) * 0.5 + 0.5;
let grime = grime_raw * c.grime_level;
heights[idx] = grime * 0.08;
let gr = (0.82 - grime as f32 * 0.3).clamp(0.0, 1.0);
let gg = (0.88 - grime as f32 * 0.2).clamp(0.0, 1.0);
let gb = (0.93 - grime as f32 * 0.1).clamp(0.0, 1.0);
let alpha = (c.glass_opacity * 255.0).round() as u8;
albedo[ai] = linear_to_srgb(gr);
albedo[ai + 1] = linear_to_srgb(gg);
albedo[ai + 2] = linear_to_srgb(gb);
albedo[ai + 3] = alpha;
let glass_rough = (0.05 + grime as f32 * 0.3).clamp(0.0, 1.0);
roughness_buf[ai] = 255;
roughness_buf[ai + 1] = (glass_rough * 255.0).round() as u8;
roughness_buf[ai + 2] = (0.85 * 255.0) as u8; roughness_buf[ai + 3] = 255;
}
}
}
}
dilate_heights(&mut heights, &albedo, w, h);
let normal = height_to_normal(
&heights,
width,
height,
c.normal_strength,
BoundaryMode::Clamp,
);
Ok(TextureMap {
albedo,
normal,
roughness: roughness_buf,
width,
height,
})
}
}
#[inline]
fn sdf_rounded_box(px: f64, py: f64, bx: f64, by: f64, r: f64) -> f64 {
let dx = px.abs() - bx;
let dy = py.abs() - by;
let outside = (dx.max(0.0).powi(2) + dy.max(0.0).powi(2)).sqrt();
let inside = dx.max(dy).min(0.0);
outside + inside - r
}