use std::f64::consts::PI;
use noise::core::worley::ReturnType;
use noise::{NoiseFn, Perlin, Worley};
use crate::{
generator::{TextureError, TextureGenerator, TextureMap, linear_to_srgb, validate_dimensions},
normal::{BoundaryMode, height_to_normal},
};
const SERRATION_FREQ: f64 = 14.0;
const ENVELOPE_DECAY: f64 = 2.0;
const MAX_HALF_WIDTH: f64 = 0.44;
const WORLEY_FREQ: f64 = 20.0;
const VENULE_FREQ: f64 = 28.0;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct LeafConfig {
pub seed: u32,
pub color_base: [f32; 3],
pub color_edge: [f32; 3],
pub serration_strength: f64,
pub vein_angle: f64,
pub micro_detail: f64,
pub normal_strength: f32,
pub lobe_count: f64,
pub lobe_depth: f64,
pub lobe_sharpness: f64,
pub petiole_length: f64,
pub petiole_width: f64,
pub midrib_width: f64,
pub vein_count: f64,
pub venule_strength: f64,
}
impl Default for LeafConfig {
fn default() -> Self {
Self {
seed: 0,
color_base: [0.12, 0.19, 0.11],
color_edge: [0.35, 0.28, 0.05],
serration_strength: 0.12,
vein_angle: 2.5,
micro_detail: 0.3,
normal_strength: 1.0,
lobe_count: 4.0,
lobe_depth: 0.23,
lobe_sharpness: 1.0,
petiole_length: 0.12,
petiole_width: 0.022,
midrib_width: 0.12,
vein_count: 6.0,
venule_strength: 0.50,
}
}
}
#[derive(Clone, Debug)]
pub struct LeafSample {
pub height: f64,
pub color: [f32; 3],
pub roughness: f32,
}
pub struct LeafSampler {
config: LeafConfig,
perlin: Perlin,
perlin_venule: Perlin,
worley: Worley,
}
impl LeafSampler {
pub fn new(config: LeafConfig) -> Self {
let perlin = Perlin::new(config.seed);
let perlin_venule = Perlin::new(config.seed.wrapping_add(2));
let worley = Worley::new(config.seed.wrapping_add(1))
.set_return_type(ReturnType::Distance)
.set_frequency(WORLEY_FREQ);
Self {
config,
perlin,
perlin_venule,
worley,
}
}
pub fn sample(&self, u: f64, v: f64) -> Option<LeafSample> {
let c = &self.config;
if c.petiole_length > 0.0 && v < c.petiole_length {
let dist = (u - 0.5).abs();
let half_width = c.petiole_width * (0.7 + 0.3 * v / c.petiole_length);
if dist >= half_width {
return None;
}
let t = dist / half_width;
let height = (1.0 - t * t).sqrt();
return Some(LeafSample {
height,
color: c.color_base,
roughness: 0.58,
});
}
let v_blade = if c.petiole_length > 0.0 {
let denom = 1.0 - c.petiole_length;
if denom <= 0.0 {
return None;
}
(v - c.petiole_length) / denom
} else {
v
};
let envelope_blade = leaf_envelope(v_blade);
let petiole_base = if c.petiole_length > 0.0 {
c.petiole_width * (-v_blade * 12.0).exp()
} else {
0.0
};
let envelope = envelope_blade + petiole_base;
if envelope <= 0.0 {
return None;
}
let effective_envelope = lobe_envelope(envelope, v_blade, c);
if effective_envelope <= 0.0 {
return None;
}
let raw_dist = (u - 0.5).abs();
let serration = self
.perlin
.get([u * SERRATION_FREQ, v_blade * SERRATION_FREQ])
* c.serration_strength
* effective_envelope;
if raw_dist + serration >= effective_envelope {
return None;
}
let edge_frac = (raw_dist / effective_envelope).clamp(0.0, 1.0);
let dome = 1.0 - edge_frac * edge_frac;
let midrib_norm = (raw_dist / (envelope * c.midrib_width.max(0.01))).min(1.0);
let midrib = (1.0 - midrib_norm).powi(2);
let vein_freq = c.vein_count * PI;
let secondary = (v_blade * vein_freq - raw_dist * vein_freq * c.vein_angle)
.sin()
.abs()
.powf(4.0);
let jitter = self.perlin_venule.get([u * 4.0, v_blade * 4.0]) * 1.8;
let vn1 = ((u - 0.5) * VENULE_FREQ + v_blade * VENULE_FREQ * 0.38 + jitter)
.sin()
.abs()
.powf(6.0);
let vn2 = ((u - 0.5) * VENULE_FREQ - v_blade * VENULE_FREQ * 0.38 + jitter)
.sin()
.abs()
.powf(6.0);
let venule = vn1.max(vn2);
let micro = (self.worley.get([u, v_blade]) * 0.5 + 0.5).clamp(0.0, 1.0);
let height = (dome * 0.15
+ midrib * 0.40
+ secondary * 0.25
+ venule * c.venule_strength * 0.15
+ micro * c.micro_detail * 0.05)
.clamp(0.0, 1.0);
let edge_t = edge_frac as f32;
let blade_r = lerp(c.color_base[0], c.color_edge[0], edge_t);
let blade_g = lerp(c.color_base[1], c.color_edge[1], edge_t);
let blade_b = lerp(c.color_base[2], c.color_edge[2], edge_t);
let vein_brightness = (midrib as f32 * 0.6 + secondary as f32 * 0.4).clamp(0.0, 1.0) * 0.18;
let color = [
(blade_r + vein_brightness).min(1.0),
(blade_g + vein_brightness * 0.75).min(1.0),
(blade_b + vein_brightness * 0.25).min(1.0),
];
let roughness = lerp(0.80, 0.52, height as f32);
Some(LeafSample {
height,
color,
roughness,
})
}
}
pub fn sample_leaf(u: f64, v: f64, config: &LeafConfig) -> Option<LeafSample> {
LeafSampler::new(config.clone()).sample(u, v)
}
pub struct LeafGenerator {
config: LeafConfig,
}
impl LeafGenerator {
pub fn new(config: LeafConfig) -> Self {
Self { config }
}
}
impl TextureGenerator for LeafGenerator {
fn generate(&self, width: u32, height: u32) -> Result<TextureMap, TextureError> {
validate_dimensions(width, height)?;
let sampler = LeafSampler::new(self.config.clone());
let w = width as usize;
let h = height as usize;
let n = w * h;
let mut heights = vec![0.5f64; n];
let mut albedo = vec![0u8; n * 4];
let mut roughness = vec![0u8; n * 4];
for y in 0..h {
let v = (y as f64 + 0.5) / h as f64;
for x in 0..w {
let u = (x as f64 + 0.5) / w as f64;
let idx = y * w + x;
let ai = idx * 4;
match sampler.sample(u, v) {
None => {
let ec = &self.config.color_edge;
albedo[ai] = linear_to_srgb(ec[0]);
albedo[ai + 1] = linear_to_srgb(ec[1]);
albedo[ai + 2] = linear_to_srgb(ec[2]);
albedo[ai + 3] = 0;
roughness[ai] = 255; roughness[ai + 1] = 200; roughness[ai + 2] = 0; roughness[ai + 3] = 255;
}
Some(s) => {
heights[idx] = s.height;
albedo[ai] = linear_to_srgb(s.color[0]);
albedo[ai + 1] = linear_to_srgb(s.color[1]);
albedo[ai + 2] = linear_to_srgb(s.color[2]);
albedo[ai + 3] = 255;
roughness[ai] = 255; roughness[ai + 1] = (s.roughness * 255.0).round() as u8;
roughness[ai + 2] = 0; roughness[ai + 3] = 255;
}
}
}
}
crate::normal::dilate_heights(&mut heights, &albedo, w, h);
let normal = height_to_normal(
&heights,
width,
height,
self.config.normal_strength,
BoundaryMode::Clamp,
);
Ok(TextureMap {
albedo,
normal,
roughness,
width,
height,
mip_level_count: 1,
emissive: None,
})
}
}
#[inline]
fn lobe_envelope(base: f64, v: f64, config: &LeafConfig) -> f64 {
if config.lobe_count <= 0.0 || config.lobe_depth <= 0.0 {
return base;
}
let cos_val = (v * (2.0 * config.lobe_count + 1.0) * PI).cos();
let shaped = cos_val.signum() * cos_val.abs().powf(config.lobe_sharpness.max(0.1));
(base * (1.0 + shaped * config.lobe_depth)).clamp(0.0, 0.49)
}
fn leaf_envelope(v: f64) -> f64 {
if v <= 0.0 || v >= 1.0 {
return 0.0;
}
(v * PI).sin() * (-v * ENVELOPE_DECAY).exp() * MAX_HALF_WIDTH
}
#[inline]
fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t.clamp(0.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn envelope_zero_at_boundaries() {
assert_eq!(leaf_envelope(0.0), 0.0);
assert_eq!(leaf_envelope(1.0), 0.0);
assert_eq!(leaf_envelope(-0.1), 0.0);
assert_eq!(leaf_envelope(1.1), 0.0);
}
#[test]
fn envelope_positive_in_interior() {
assert!(leaf_envelope(0.3) > 0.0);
assert!(leaf_envelope(0.5) > 0.0);
assert!(leaf_envelope(0.9) > 0.0);
}
#[test]
fn midrib_always_inside() {
let config = LeafConfig {
serration_strength: 0.0,
..LeafConfig::default()
};
let sampler = LeafSampler::new(config);
for vi in 1..=9 {
let v = vi as f64 / 10.0;
assert!(
sampler.sample(0.5, v).is_some(),
"midrib should be inside at v={v}"
);
}
}
#[test]
fn corners_always_outside() {
let config = LeafConfig::default();
let sampler = LeafSampler::new(config);
for &(u, v) in &[(0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (1.0, 1.0)] {
assert!(
sampler.sample(u, v).is_none(),
"corner ({u},{v}) should be outside the leaf"
);
}
}
#[test]
fn generator_produces_correct_buffer_sizes() {
let leaf_gen = LeafGenerator::new(LeafConfig::default());
let map = leaf_gen.generate(64, 32).expect("generate failed");
assert_eq!(map.albedo.len(), 64 * 32 * 4);
assert_eq!(map.normal.len(), 64 * 32 * 4);
assert_eq!(map.roughness.len(), 64 * 32 * 4);
}
#[test]
fn generator_has_transparent_pixels() {
let leaf_gen = LeafGenerator::new(LeafConfig::default());
let map = leaf_gen.generate(64, 64).expect("generate failed");
let has_transparent = map.albedo.chunks(4).any(|px| px[3] == 0);
assert!(
has_transparent,
"leaf texture should contain transparent pixels"
);
let has_opaque = map.albedo.chunks(4).any(|px| px[3] == 255);
assert!(has_opaque, "leaf texture should contain opaque pixels");
}
}