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
//! Rock texture generator using Ridged Multifractal noise.
//!
//! Ridged multifractal noise produces sharp, ridge-like features that mimic
//! the cracked and faceted appearance of stone surfaces.
use noise::{MultiFractal, Perlin, RidgedMulti};
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 [`RockGenerator`].
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct RockConfig {
/// PRNG seed for the deterministic noise pattern; different seeds give
/// statistically-different textures from otherwise-identical configs.
pub seed: u32,
/// Overall spatial scale.
pub scale: f64,
/// Octaves for the ridged multifractal noise (more octaves → finer detail).
pub octaves: usize,
/// Attenuation of the ridged multifractal (controls sharpness of ridges).
pub attenuation: f64,
/// Base (light) rock colour in linear RGB \[0, 1\].
pub color_light: [f32; 3],
/// Shadow (dark) colour in linear RGB \[0, 1\].
pub color_dark: [f32; 3],
/// Normal map strength — larger values produce more pronounced surface detail.
pub normal_strength: f32,
}
impl Default for RockConfig {
fn default() -> Self {
Self {
seed: 7,
scale: 3.0,
octaves: 8,
attenuation: 2.0,
color_light: [0.37, 0.42, 0.36],
color_dark: [0.22, 0.20, 0.18],
normal_strength: 4.0,
}
}
}
/// Procedural rock / stone texture generator.
///
/// Drives [`TextureGenerator::generate`] using a [`RockConfig`]. Construct
/// via [`RockGenerator::new`] and call `generate` directly, or spawn a
/// [`crate::async_gen::PendingTexture::rock`] 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 RockGenerator {
config: RockConfig,
noise: ToroidalNoise<RidgedMulti<Perlin>>,
}
impl RockGenerator {
/// Create a new generator with the given configuration.
///
/// Builds the ridged-multifractal noise object up front so that repeated
/// calls to [`generate`](TextureGenerator::generate) skip initialisation.
pub fn new(config: RockConfig) -> Self {
let ridged: RidgedMulti<Perlin> = RidgedMulti::new(config.seed)
.set_octaves(config.octaves)
.set_attenuation(config.attenuation);
let noise = ToroidalNoise::new(ridged, config.scale);
Self { config, noise }
}
}
impl TextureGenerator for RockGenerator {
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 = vec![0u8; n * 4];
for (i, &height) in heights.iter().enumerate() {
let t = normalize(height) as f32;
let r = lerp(c.color_dark[0], c.color_light[0], t);
let g = lerp(c.color_dark[1], c.color_light[1], t);
let b = lerp(c.color_dark[2], c.color_light[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;
// Ridges (high t) are slightly smoother (exposed mineral); cracks rougher.
// Packed as ORM: R=Occlusion(1.0), G=Roughness, B=Metallic(0.0).
// RidgedMulti output is not strictly bounded; clamp before casting.
let rough = (0.75 - t * 0.25).clamp(0.0, 1.0);
roughness[ai] = 255; // Occlusion = 1.0 (no shadowing)
roughness[ai + 1] = (rough * 255.0).round() as u8;
roughness[ai + 2] = 0; // Metallic = 0.0
roughness[ai + 3] = 255;
}
// heights is in [-1, 1]; normalize would scale gradients by 0.5.
// Halving strength here is equivalent and avoids a full-sized allocation.
let normal = height_to_normal(
&heights,
width,
height,
c.normal_strength * 0.5,
BoundaryMode::Wrap,
);
Ok(TextureMap {
albedo,
normal,
roughness,
width,
height,
})
}
}
#[inline]
fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t.clamp(0.0, 1.0)
}