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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
//! Marble / granite texture generator using domain-warped FBM noise.
//!
//! The algorithm:
//! 1. Precompute toroidal sin/cos lookup tables (one entry per column, one per row).
//! 2. Build three independent FBM sources: two warp layers (warp_u, warp_v) and a
//! base layer, all evaluated on the same 4-D torus so the result tiles seamlessly.
//! 3. The base FBM is baked into a W×H grid via [`sample_grid_into`]. Warp offsets
//! are computed inline using the precomputed lookup tables.
//! 4. For each pixel the base grid is sampled at the domain-warped UV coordinates
//! via bilinear interpolation. The raw value is normalised to [0, 1] and passed
//! through a sinusoidal vein function that produces thin dark veins on a light
//! background.
//! 5. Colour, height, and ORM values are derived from the vein blend factor.
//!
//! Vein function: `((raw_norm · TAU · vein_frequency).sin().abs()).powf(vein_sharpness)`
//! gives 0 at vein centres and 1 on the background rock. The two endpoints are then
//! mapped to `color_vein` and `color_base` respectively.
use std::f64::consts::TAU;
use noise::{Fbm, MultiFractal, Perlin};
use crate::{
generator::{TextureError, TextureGenerator, TextureMap, Workspace, validate_dimensions},
noise::{ToroidalNoise, bilinear_sample_torus, normalize, sample_grid_into},
surface::{SurfaceCell, SurfaceSample, generate_surface, lerp},
};
/// Serde default for [`MarbleConfig::warp_octaves`] — keeps configs saved
/// before the field existed deserialisable.
fn default_warp_octaves() -> usize {
3
}
/// Configures the appearance of a [`MarbleGenerator`].
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct MarbleConfig {
/// PRNG seed for the deterministic noise pattern; different seeds give
/// statistically-different textures from otherwise-identical configs.
pub seed: u32,
/// Overall pattern scale. \[1, 8\]
pub scale: f64,
/// FBM octaves for the base layer. \[3, 8\]
pub octaves: usize,
/// Octaves for the two warp FBM layers. \[1, 6\] Warp output only
/// displaces UV lookups into the base grid, so detail beyond ~3 octaves
/// is visually invisible — and the warp layers dominate per-pixel cost.
#[serde(default = "default_warp_octaves")]
pub warp_octaves: usize,
/// Domain warp strength — how much the veins meander. \[0, 1.5\]
pub warp_strength: f64,
/// Vein frequency: period of sin() applied to warped FBM. \[1, 8\]
pub vein_frequency: f64,
/// Vein sharpness: exponent on abs(sin()) making veins narrower. \[0.5, 6\]
pub vein_sharpness: f64,
/// Base surface roughness \[0, 0.3\] — keep low for polished marble.
pub roughness: f64,
/// Base (light) colour in linear RGB.
pub color_base: [f32; 3],
/// Vein colour in linear RGB.
pub color_vein: [f32; 3],
/// Normal-map strength.
pub normal_strength: f32,
}
impl Default for MarbleConfig {
fn default() -> Self {
Self {
seed: 55,
scale: 3.0,
octaves: 5,
warp_octaves: 3,
warp_strength: 0.6,
vein_frequency: 3.0,
vein_sharpness: 2.0,
roughness: 0.08,
color_base: [0.92, 0.90, 0.87],
color_vein: [0.42, 0.38, 0.34],
normal_strength: 1.5,
}
}
}
/// Procedural marble / granite texture generator.
///
/// Drives [`TextureGenerator::generate`] using a [`MarbleConfig`]. Construct
/// via [`MarbleGenerator::new`] and call `generate` directly, or spawn a
/// [`crate::async_gen::PendingTexture::marble`] 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 MarbleGenerator {
config: MarbleConfig,
warp_u_noise: ToroidalNoise<Fbm<Perlin>>,
warp_v_noise: ToroidalNoise<Fbm<Perlin>>,
base_noise: ToroidalNoise<Fbm<Perlin>>,
}
impl MarbleGenerator {
/// 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: MarbleConfig) -> Self {
let fbm_warp_u: Fbm<Perlin> = Fbm::new(config.seed).set_octaves(config.warp_octaves);
let fbm_warp_v: Fbm<Perlin> =
Fbm::new(config.seed.wrapping_add(100)).set_octaves(config.warp_octaves);
let fbm_base: Fbm<Perlin> =
Fbm::new(config.seed.wrapping_add(200)).set_octaves(config.octaves);
let warp_u_noise = ToroidalNoise::new(fbm_warp_u, config.scale);
let warp_v_noise = ToroidalNoise::new(fbm_warp_v, config.scale);
let base_noise = ToroidalNoise::new(fbm_base, config.scale);
Self {
config,
warp_u_noise,
warp_v_noise,
base_noise,
}
}
}
/// Per-generation sampler: torus LUTs, precomputed base grid, warp noises.
struct MarbleCell<'a> {
config: &'a MarbleConfig,
warp_u_noise: &'a ToroidalNoise<Fbm<Perlin>>,
warp_v_noise: &'a ToroidalNoise<Fbm<Perlin>>,
base_grid: &'a [f64],
col_cos: &'a [f64],
col_sin: &'a [f64],
row_cos: &'a [f64],
row_sin: &'a [f64],
w: usize,
h: usize,
}
impl SurfaceCell for MarbleCell<'_> {
fn sample(&self, x: u32, y: u32, u: f64, v: f64) -> SurfaceSample {
let c = self.config;
let (x, y) = (x as usize, y as usize);
// Compute warp offsets using precomputed torus coordinates.
let nx = self.col_cos[x];
let ny = self.col_sin[x];
let nz = self.row_cos[y];
let nw = self.row_sin[y];
let du = self.warp_u_noise.get_precomputed(nx, ny, nz, nw) * c.warp_strength;
let dv = self.warp_v_noise.get_precomputed(nx, ny, nz, nw) * c.warp_strength;
// Sample the precomputed base grid at the warped UV coordinates.
// Bilinear interpolation wraps toroidally — no trig per pixel.
let raw = bilinear_sample_torus(self.base_grid, self.w, self.h, u + du, v + dv);
// Normalize raw from [-1, 1] to [0, 1] before the vein function.
let raw_norm = raw * 0.5 + 0.5;
// Vein pattern: 0 = vein centre, 1 = background rock.
// High vein_sharpness concentrates the zero region into thin dark lines.
let vein_t = ((raw_norm * TAU * c.vein_frequency).sin().abs()).powf(c.vein_sharpness);
// Colour: lerp from vein colour to base rock colour by vein_t.
let vein_tf = vein_t as f32;
let color = [
lerp(c.color_vein[0], c.color_base[0], vein_tf),
lerp(c.color_vein[1], c.color_base[1], vein_tf),
lerp(c.color_vein[2], c.color_base[2], vein_tf),
];
// ORM: polished marble is very smooth; veins are only marginally
// rougher because they are recessed and accumulate micro-debris.
let rough = (c.roughness * (1.0 - vein_t * 0.3)) as f32;
// Height: veins are slightly recessed relative to the base rock.
SurfaceSample::matte(vein_t * 0.8 + normalize(raw) * 0.2, color, rough)
}
}
impl MarbleGenerator {
fn generate_inner(
&self,
width: u32,
height: u32,
mut ws: Option<&mut Workspace>,
) -> Result<TextureMap, TextureError> {
validate_dimensions(width, height)?;
let c = &self.config;
let w = width as usize;
let h = height as usize;
// Precompute toroidal coordinates (W + H entries instead of W × H).
// All three noise objects share the same `c.scale` frequency so one
// set of lookup tables covers all of them.
let freq = c.scale;
let col_cos: Vec<f64> = (0..w)
.map(|x| (TAU * x as f64 / w as f64).cos() * freq)
.collect();
let col_sin: Vec<f64> = (0..w)
.map(|x| (TAU * x as f64 / w as f64).sin() * freq)
.collect();
let row_cos: Vec<f64> = (0..h)
.map(|y| (TAU * y as f64 / h as f64).cos() * freq)
.collect();
let row_sin: Vec<f64> = (0..h)
.map(|y| (TAU * y as f64 / h as f64).sin() * freq)
.collect();
// Precompute the base noise on a regular grid using the torus LUTs
// (O(W+H) trig calls). The warped lookup then becomes a cheap
// bilinear interpolation rather than per-pixel sin/cos evaluation.
let mut base_grid = ws.as_deref_mut().map_or_else(Vec::new, |w| w.take_grid());
sample_grid_into(&self.base_noise, width, height, &mut base_grid);
let cell = MarbleCell {
config: c,
warp_u_noise: &self.warp_u_noise,
warp_v_noise: &self.warp_v_noise,
base_grid: &base_grid,
col_cos: &col_cos,
col_sin: &col_sin,
row_cos: &row_cos,
row_sin: &row_sin,
w,
h,
};
let result = generate_surface(width, height, c.normal_strength, ws.as_deref_mut(), &cell);
if let Some(ws) = ws {
ws.return_grid(base_grid);
}
result
}
}
impl TextureGenerator for MarbleGenerator {
fn generate(&self, width: u32, height: u32) -> Result<TextureMap, TextureError> {
self.generate_inner(width, height, None)
}
fn generate_with_workspace(
&self,
width: u32,
height: u32,
workspace: &mut Workspace,
) -> Result<TextureMap, TextureError> {
self.generate_inner(width, height, Some(workspace))
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Configs serialised before `warp_octaves` existed (pre-0.6) must still
/// deserialise, receiving the serde default of 3.
#[test]
fn legacy_configs_without_warp_octaves_deserialize() {
let legacy = r#"{
"seed": 55, "scale": 3.0, "octaves": 5,
"warp_strength": 0.6, "vein_frequency": 3.0,
"vein_sharpness": 2.0, "roughness": 0.08,
"color_base": [0.92, 0.90, 0.87],
"color_vein": [0.42, 0.38, 0.34],
"normal_strength": 1.5
}"#;
let config: MarbleConfig = serde_json::from_str(legacy).expect("legacy config loads");
assert_eq!(config.warp_octaves, 3);
}
}