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
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
//! Bark texture generator using domain-warped FBM noise.
//!
//! The algorithm:
//! 1. Precompute toroidal sin/cos lookup tables (one entry per column, one per row).
//! 2. For each pixel, sample two FBM warp layers inline to produce offsets (du, dv).
//! 3. Sample the precomputed base FBM grid via bilinear interpolation at the warped UV coordinates for the final value.
//! 4. Derive colour, roughness and a height field from the result.
//!
//! The warp layers are computed inline (no intermediate grids). The base FBM
//! layer is precomputed into a W×H grid (~536 MB at 8 K) once, then sampled
//! via bilinear interpolation at the warped coordinates. This trades one
//! allocation for the elimination of O(W×H) `sin`/`cos` calls that would
//! otherwise occur when evaluating the toroidal base noise at arbitrary warped
//! positions.
use std::f64::consts::TAU;
use noise::core::worley::ReturnType;
use noise::{Fbm, MultiFractal, NoiseFn, Perlin, Worley};
use rayon::prelude::*;
use crate::{
generator::{
TextureError, TextureGenerator, TextureMap, Workspace, linear_to_srgb, validate_dimensions,
},
noise::{ToroidalNoise, bilinear_sample_torus, normalize, sample_grid_into},
normal::{BoundaryMode, height_to_normal},
surface::lerp,
};
/// Serde default for [`BarkConfig::warp_octaves`] — keeps configs saved
/// before the field existed deserialisable.
fn default_warp_octaves() -> usize {
3
}
/// Configures the appearance of a [`BarkGenerator`].
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct BarkConfig {
/// PRNG seed for the deterministic noise pattern; different seeds give
/// statistically-different textures from otherwise-identical configs.
pub seed: u32,
/// Overall spatial scale of the bark pattern.
pub scale: f64,
/// Octaves for the base FBM layer.
pub octaves: usize,
/// Octaves for the two warp FBM layers. 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,
/// Horizontal warp strength (small — creates slight lateral texture).
pub warp_u: f64,
/// Vertical warp strength (large — creates the fibrous streaks).
pub warp_v: f64,
/// Base (light) bark colour in linear RGB \[0, 1\].
pub color_light: [f32; 3],
/// Dark groove colour in linear RGB \[0, 1\].
pub color_dark: [f32; 3],
/// Normal map strength.
pub normal_strength: f32,
/// Blend weight of the rhytidome furrow layer \[0, 1\]. 0 = pure FBM fibre,
/// 1 = pure Worley plates.
pub furrow_multiplier: f64,
/// Horizontal frequency of the Worley cells (higher = narrower plates).
pub furrow_scale_u: f64,
/// Vertical frequency of the Worley cells (lower = longer vertical plates).
pub furrow_scale_v: f64,
/// Power applied to the normalised plate height. Values < 1 fatten the
/// plates and sharpen the V-shaped cracks between them.
pub furrow_shape: f64,
}
impl Default for BarkConfig {
fn default() -> Self {
Self {
seed: 42,
scale: 2.0,
octaves: 6,
warp_octaves: 3,
warp_u: 0.15,
warp_v: 0.55,
color_light: [0.45, 0.28, 0.14],
color_dark: [0.09, 0.05, 0.03],
normal_strength: 3.0,
furrow_multiplier: 0.78,
furrow_scale_u: 2.0,
furrow_scale_v: 0.48,
furrow_shape: 2.0,
}
}
}
/// Procedural bark texture generator.
///
/// Drives [`TextureGenerator::generate`] using a [`BarkConfig`]. Construct
/// via [`BarkGenerator::new`] and call `generate` directly, or spawn a
/// [`crate::async_gen::PendingTexture::bark`] 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. Worley noise is still
/// constructed in `generate_inner()` because `Worley` is `!Send`.
pub struct BarkGenerator {
config: BarkConfig,
warp_u_noise: ToroidalNoise<Fbm<Perlin>>,
warp_v_noise: ToroidalNoise<Fbm<Perlin>>,
base_noise: ToroidalNoise<Fbm<Perlin>>,
}
impl BarkGenerator {
/// 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: BarkConfig) -> 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,
}
}
}
impl BarkGenerator {
/// Core generation logic. When `ws` is `Some`, borrows the base grid
/// buffer from the workspace to avoid a fresh 128 MB allocation at 4K.
/// Reuses workspace buffers across calls so that generating multiple
/// size variants does not allocate new backing storage each time.
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;
let n = w * h;
// Precompute toroidal coordinates (W + H entries instead of W × H).
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();
// Anisotropic torus tables for the Worley furrow layer.
let f_freq_u = c.scale * c.furrow_scale_u;
let f_freq_v = c.scale * c.furrow_scale_v;
let f_col_cos: Vec<f64> = (0..w)
.map(|x| (TAU * x as f64 / w as f64).cos() * f_freq_u)
.collect();
let f_col_sin: Vec<f64> = (0..w)
.map(|x| (TAU * x as f64 / w as f64).sin() * f_freq_u)
.collect();
let f_row_cos: Vec<f64> = (0..h)
.map(|y| (TAU * y as f64 / h as f64).cos() * f_freq_v)
.collect();
let f_row_sin: Vec<f64> = (0..h)
.map(|y| (TAU * y as f64 / h as f64).sin() * f_freq_v)
.collect();
// Precompute the base noise on a regular grid using the torus LUTs.
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 mut heights = vec![0.0f64; n];
let mut albedo = vec![0u8; n * 4];
let mut roughness = vec![0u8; n * 4];
heights
.par_chunks_mut(w)
.zip(albedo.par_chunks_mut(w * 4))
.zip(roughness.par_chunks_mut(w * 4))
.enumerate()
.for_each(|(y, ((height_row, albedo_row), orm_row))| {
// Worley noise for the rhytidome plates. `noise::Worley`
// holds an `Rc` and is `!Sync`, so each row constructs its
// own instance — deterministic from the seed and only a few
// microseconds each, which vanishes against the per-row FBM
// cost.
let worley =
Worley::new(c.seed.wrapping_add(300)).set_return_type(ReturnType::Distance);
let nz = row_cos[y];
let nw = row_sin[y];
let v = y as f64 / h as f64;
let f_nz = f_row_cos[y];
let f_nw = f_row_sin[y];
for (x, height_slot) in height_row.iter_mut().enumerate() {
let nx = col_cos[x];
let ny = col_sin[x];
let u = x as f64 / w as f64;
// Compute warp offsets using precomputed torus coordinates.
let du = self.warp_u_noise.get_precomputed(nx, ny, nz, nw) * c.warp_u;
let dv = self.warp_v_noise.get_precomputed(nx, ny, nz, nw) * c.warp_v;
// Sample the precomputed base grid at the warped UV coordinates.
// Bilinear interpolation wraps toroidally — no trig per pixel.
let raw = bilinear_sample_torus(&base_grid, w, h, u + du, v + dv);
let t = normalize(raw); // [0, 1]
// --- Worley rhytidome plates ---
// Sample anisotropic Worley on a 4D torus: U-axis uses high
// frequency (narrow plates), V-axis uses low frequency (tall plates).
let f_nx = f_col_cos[x];
let f_ny = f_col_sin[x];
let furrow_raw = worley.get([f_nx, f_ny, f_nz, f_nw]);
// Invert: boundaries (furrow_raw ≈ 1) → 0 (deep crack);
// centres (furrow_raw ≈ -1) → 1 (raised plate).
let furrow_norm = (0.5 - furrow_raw * 0.5).clamp(0.0, 1.0);
// powf < 1 widens the plateau and keeps cracks narrow and sharp.
let plate_height = furrow_norm.powf(c.furrow_shape);
// Blend fibrous FBM micro-detail with macro rhytidome plates.
let t_final =
t * (1.0 - c.furrow_multiplier) + plate_height * c.furrow_multiplier;
*height_slot = t_final;
// Colour: lerp between dark and light by height value.
let r = lerp(c.color_dark[0], c.color_light[0], t as f32);
let g = lerp(c.color_dark[1], c.color_light[1], t as f32);
let b = lerp(c.color_dark[2], c.color_light[2], t as f32);
let ai = x * 4;
albedo_row[ai] = linear_to_srgb(r);
albedo_row[ai + 1] = linear_to_srgb(g);
albedo_row[ai + 2] = linear_to_srgb(b);
albedo_row[ai + 3] = 255;
// Roughness: grooves (dark, low t) are rougher.
// Packed as ORM: R=Occlusion(1.0), G=Roughness, B=Metallic(0.0).
let rough = 0.6 + (1.0 - t as f32) * 0.35;
orm_row[ai] = 255; // Occlusion = 1.0 (no shadowing)
orm_row[ai + 1] = (rough * 255.0).round() as u8;
orm_row[ai + 2] = 0; // Metallic = 0.0
orm_row[ai + 3] = 255;
}
});
// Return grid buffer to the workspace for reuse.
if let Some(ws) = ws {
ws.return_grid(base_grid);
}
let normal = height_to_normal(
&heights,
width,
height,
c.normal_strength,
BoundaryMode::Wrap,
);
Ok(TextureMap {
albedo,
normal,
roughness,
width,
height,
mip_level_count: 1,
emissive: None,
})
}
}
impl TextureGenerator for BarkGenerator {
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": 42, "scale": 2.0, "octaves": 6,
"warp_u": 0.15, "warp_v": 0.55,
"color_light": [0.45, 0.28, 0.14],
"color_dark": [0.09, 0.05, 0.03],
"normal_strength": 3.0,
"furrow_multiplier": 0.78,
"furrow_scale_u": 2.0,
"furrow_scale_v": 0.48,
"furrow_shape": 2.0
}"#;
let config: BarkConfig = serde_json::from_str(legacy).expect("legacy config loads");
assert_eq!(config.warp_octaves, 3);
}
}