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
//! Ground / dirt texture generator.
//!
//! Produces a matted, organic-looking surface by blending two FBM layers at
//! different scales. A low-frequency layer defines broad soil patches; a
//! high-frequency layer adds fine grain and pebble-like micro-detail.
use noise::{Fbm, MultiFractal, Perlin};
use crate::{
generator::{TextureError, TextureGenerator, TextureMap, linear_to_srgb, validate_dimensions},
noise::{ToroidalNoise, normalize},
normal::{BoundaryMode, height_to_normal},
};
/// Configures the appearance of a [`GroundGenerator`].
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct GroundConfig {
pub seed: u32,
/// Scale of the large soil-patch layer.
pub macro_scale: f64,
/// Octaves for the large soil-patch FBM layer.
pub macro_octaves: usize,
/// Scale of the fine-grain layer.
pub micro_scale: f64,
/// Octaves for the fine-grain FBM layer.
pub micro_octaves: usize,
/// Blend weight of the micro layer (0 = only macro, 1 = only micro).
pub micro_weight: f64,
/// Dry (light) soil colour in linear RGB \[0, 1\].
pub color_dry: [f32; 3],
/// Moist (dark) soil colour in linear RGB \[0, 1\].
pub color_moist: [f32; 3],
/// Normal map strength — larger values produce more pronounced surface detail.
pub normal_strength: f32,
}
impl Default for GroundConfig {
fn default() -> Self {
Self {
seed: 13,
macro_scale: 2.0,
macro_octaves: 5,
micro_scale: 8.0,
micro_octaves: 4,
micro_weight: 0.35,
color_dry: [0.52, 0.40, 0.26],
color_moist: [0.28, 0.20, 0.12],
normal_strength: 2.0,
}
}
}
/// Procedural ground / dirt texture generator.
///
/// Drives [`TextureGenerator::generate`] using a [`GroundConfig`]. Construct
/// via [`GroundGenerator::new`] and call `generate` directly, or spawn a
/// [`crate::async_gen::PendingTexture::ground`] 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 GroundGenerator {
config: GroundConfig,
macro_noise: ToroidalNoise<Fbm<Perlin>>,
micro_noise: ToroidalNoise<Fbm<Perlin>>,
}
impl GroundGenerator {
/// 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: GroundConfig) -> Self {
let fbm_macro: Fbm<Perlin> = Fbm::new(config.seed).set_octaves(config.macro_octaves);
let macro_noise = ToroidalNoise::new(fbm_macro, config.macro_scale);
let fbm_micro: Fbm<Perlin> =
Fbm::new(config.seed.wrapping_add(50)).set_octaves(config.micro_octaves);
let micro_noise = ToroidalNoise::new(fbm_micro, config.micro_scale);
Self {
config,
macro_noise,
micro_noise,
}
}
}
impl TextureGenerator for GroundGenerator {
fn generate(&self, width: u32, height: u32) -> Result<TextureMap, TextureError> {
validate_dimensions(width, height)?;
let c = &self.config;
let w = width as f64;
let h = height as f64;
let n = (width as usize) * (height as usize);
let mut heights = vec![0.0f64; n];
let mut albedo = vec![0u8; n * 4];
let mut roughness = vec![0u8; n * 4];
for y in 0..height {
for x in 0..width {
let idx = (y * width + x) as usize;
let u = x as f64 / w;
let v = y as f64 / h;
let macro_val = normalize(self.macro_noise.get(u, v));
let micro_val = normalize(self.micro_noise.get(u, v));
let t = macro_val * (1.0 - c.micro_weight) + micro_val * c.micro_weight;
heights[idx] = t;
let tf = t as f32;
let r = lerp(c.color_moist[0], c.color_dry[0], tf);
let g = lerp(c.color_moist[1], c.color_dry[1], tf);
let b = lerp(c.color_moist[2], c.color_dry[2], tf);
let ai = idx * 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;
// Ground is generally rough; slight variation by moisture.
// Packed as ORM: R=Occlusion(1.0), G=Roughness, B=Metallic(0.0).
let rough = 0.80 + (1.0 - tf) * 0.15;
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;
}
}
let normal = height_to_normal(
&heights,
width,
height,
c.normal_strength,
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)
}