1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//! Stucco / render texture generator.
//!
//! Smooth, high-frequency FBM bumps over a flat matte base — typical of
//! sand-float or pebble-dash exterior render. The surface is almost flat
//! (low relief) and entirely matte with zero metallic response.
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},
};
/// Configures the appearance of a [`StuccoGenerator`].
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct StuccoConfig {
/// PRNG seed for the deterministic noise pattern; different seeds give
/// statistically-different textures from otherwise-identical configs.
pub seed: u32,
/// Spatial frequency — controls bump density (higher = finer texture).
pub scale: f64,
/// FBM octave count.
pub octaves: usize,
/// Bump amplitude \[0, 1\] — controls surface relief depth.
pub roughness: f64,
/// Base stucco colour in linear RGB \[0, 1\].
pub color_base: [f32; 3],
/// Shadow / recessed-area colour in linear RGB \[0, 1\].
pub color_shadow: [f32; 3],
/// Normal-map strength.
pub normal_strength: f32,
}
impl Default for StuccoConfig {
fn default() -> Self {
Self {
seed: 13,
scale: 8.0,
octaves: 6,
roughness: 0.35,
color_base: [0.92, 0.89, 0.84],
color_shadow: [0.72, 0.70, 0.66],
normal_strength: 2.0,
}
}
}
/// Procedural stucco / render texture generator.
///
/// Drives [`TextureGenerator::generate`] using a [`StuccoConfig`]. Construct
/// via [`StuccoGenerator::new`] and call `generate` directly, or spawn a
/// [`crate::async_gen::PendingTexture::stucco`] task for non-blocking generation.
///
/// Noise objects are built in the constructor so that calling `generate`
/// multiple times (e.g. producing size variants of the same material)
/// does not repeat the initialisation cost.
pub struct StuccoGenerator {
config: StuccoConfig,
noise: ToroidalNoise<Fbm<Perlin>>,
}
impl StuccoGenerator {
/// Create a new generator with the given configuration.
///
/// Builds the noise objects up front so that repeated
/// calls to [`generate`](TextureGenerator::generate) skip initialisation.
pub fn new(config: StuccoConfig) -> Self {
let fbm: Fbm<Perlin> = Fbm::new(config.seed).set_octaves(config.octaves);
let noise = ToroidalNoise::new(fbm, config.scale);
Self { config, noise }
}
}
impl TextureGenerator for StuccoGenerator {
fn generate(&self, width: u32, height: u32) -> Result<TextureMap, TextureError> {
validate_dimensions(width, height)?;
let c = &self.config;
let heights = sample_grid(&self.noise, width, height);
let n = (width as usize) * (height as usize);
let mut albedo = vec![0u8; n * 4];
let mut roughness_buf = vec![0u8; n * 4];
for (i, &h) in heights.iter().enumerate() {
// normalize maps [-1,1] → [0,1]; scale by roughness amplitude.
let t = (normalize(h) * c.roughness) as f32;
let r = lerp(c.color_shadow[0], c.color_base[0], t);
let g = lerp(c.color_shadow[1], c.color_base[1], t);
let b = lerp(c.color_shadow[2], c.color_base[2], t);
let ai = i * 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;
// Matte finish: high roughness, zero metallic.
// Recessed bumps (low t) are slightly rougher (shadow/grit).
let rough = (0.82 + (1.0 - t) * 0.10).clamp(0.0, 1.0);
roughness_buf[ai] = 255; // Occlusion = 1.0
roughness_buf[ai + 1] = (rough * 255.0).round() as u8;
roughness_buf[ai + 2] = 0; // Metallic = 0.0
roughness_buf[ai + 3] = 255;
}
// Scale the height map by roughness so the normal map also respects
// bump amplitude (not just the albedo interpolation above).
let heights_scaled: Vec<f64> = heights.iter().map(|&h| h * c.roughness).collect();
let normal = height_to_normal(
&heights_scaled,
width,
height,
c.normal_strength * 0.5,
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)
}