1use super::{Grid2D, Rng};
11
12#[derive(Debug, Clone)]
14pub struct ErosionParams {
15 pub hydraulic_ratio: f32,
17 pub max_drop_lifetime: usize,
19 pub capacity_mult: f32,
21 pub erosion_rate: f32,
23 pub deposition_rate: f32,
25 pub gravity: f32,
27 pub evaporation: f32,
29 pub min_slope: f32,
31 pub talus_angle: f32,
33 pub thermal_rate: f32,
35 pub inertia: f32,
37 pub initial_water: f32,
39 pub initial_speed: f32,
41}
42
43impl Default for ErosionParams {
44 fn default() -> Self {
45 Self {
46 hydraulic_ratio: 0.8,
47 max_drop_lifetime: 64,
48 capacity_mult: 8.0,
49 erosion_rate: 0.3,
50 deposition_rate: 0.3,
51 gravity: 4.0,
52 evaporation: 0.01,
53 min_slope: 0.01,
54 talus_angle: 0.6, thermal_rate: 0.5,
56 inertia: 0.3,
57 initial_water: 1.0,
58 initial_speed: 1.0,
59 }
60 }
61}
62
63struct Drop {
65 x: f32,
66 y: f32,
67 dir_x: f32,
68 dir_y: f32,
69 speed: f32,
70 water: f32,
71 sediment: f32,
72}
73
74pub fn erode(mut heightmap: Grid2D, iterations: usize, rng: &mut Rng) -> Grid2D {
76 let params = ErosionParams::default();
77 let w = heightmap.width;
78 let h = heightmap.height;
79
80 let hydraulic_iters = (iterations as f32 * params.hydraulic_ratio) as usize;
81 let thermal_iters = iterations - hydraulic_iters;
82
83 for _ in 0..hydraulic_iters {
85 hydraulic_step(&mut heightmap, rng, ¶ms);
86 }
87
88 for _ in 0..thermal_iters {
90 thermal_step(&mut heightmap, ¶ms);
91 }
92
93 heightmap
94}
95
96pub fn erode_with_params(mut heightmap: Grid2D, iterations: usize, params: &ErosionParams, rng: &mut Rng) -> Grid2D {
98 let hydraulic_iters = (iterations as f32 * params.hydraulic_ratio) as usize;
99 let thermal_iters = iterations - hydraulic_iters;
100
101 for _ in 0..hydraulic_iters {
102 hydraulic_step(&mut heightmap, rng, params);
103 }
104 for _ in 0..thermal_iters {
105 thermal_step(&mut heightmap, params);
106 }
107
108 heightmap
109}
110
111fn hydraulic_step(grid: &mut Grid2D, rng: &mut Rng, params: &ErosionParams) {
113 let w = grid.width as f32;
114 let h = grid.height as f32;
115
116 let mut drop = Drop {
117 x: rng.range_f32(1.0, w - 2.0),
118 y: rng.range_f32(1.0, h - 2.0),
119 dir_x: 0.0,
120 dir_y: 0.0,
121 speed: params.initial_speed,
122 water: params.initial_water,
123 sediment: 0.0,
124 };
125
126 for _ in 0..params.max_drop_lifetime {
127 let ix = drop.x as usize;
128 let iy = drop.y as usize;
129 if ix < 1 || iy < 1 || ix >= grid.width - 1 || iy >= grid.height - 1 {
130 break;
131 }
132
133 let (gx, gy) = grid.gradient(ix, iy);
135
136 drop.dir_x = drop.dir_x * params.inertia - gx * (1.0 - params.inertia);
138 drop.dir_y = drop.dir_y * params.inertia - gy * (1.0 - params.inertia);
139
140 let len = (drop.dir_x * drop.dir_x + drop.dir_y * drop.dir_y).sqrt();
142 if len < 1e-6 {
143 break; }
145 drop.dir_x /= len;
146 drop.dir_y /= len;
147
148 let new_x = drop.x + drop.dir_x;
150 let new_y = drop.y + drop.dir_y;
151
152 if new_x < 1.0 || new_y < 1.0 || new_x >= w - 2.0 || new_y >= h - 2.0 {
153 break;
154 }
155
156 let old_h = grid.sample(drop.x, drop.y);
158 let new_h = grid.sample(new_x, new_y);
159 let dh = new_h - old_h;
160
161 let slope = (-dh).max(params.min_slope);
163 let capacity = slope * drop.speed * drop.water * params.capacity_mult;
164
165 if drop.sediment > capacity || dh > 0.0 {
166 let deposit = if dh > 0.0 {
168 dh.min(drop.sediment)
170 } else {
171 (drop.sediment - capacity) * params.deposition_rate
172 };
173 drop.sediment -= deposit;
174 grid.add(ix, iy, deposit);
175 } else {
176 let erode_amount = ((capacity - drop.sediment) * params.erosion_rate).min(-dh);
178 drop.sediment += erode_amount;
179 grid.add(ix, iy, -erode_amount);
180 }
181
182 drop.speed = (drop.speed * drop.speed + dh.abs() * params.gravity).sqrt();
184 drop.water *= 1.0 - params.evaporation;
185
186 drop.x = new_x;
187 drop.y = new_y;
188
189 if drop.water < 0.01 {
190 break;
191 }
192 }
193}
194
195fn thermal_step(grid: &mut Grid2D, params: &ErosionParams) {
197 let w = grid.width;
198 let h = grid.height;
199 let talus = params.talus_angle.tan(); let mut transfers = Vec::new();
202
203 for y in 1..h - 1 {
204 for x in 1..w - 1 {
205 let center = grid.get(x, y);
206 let mut max_diff = 0.0_f32;
207 let mut total_diff = 0.0_f32;
208 let mut neighbors = Vec::new();
209
210 for &(nx, ny) in &[(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)] {
211 let nh = grid.get(nx, ny);
212 let diff = center - nh;
213 if diff > talus {
214 max_diff = max_diff.max(diff);
215 total_diff += diff - talus;
216 neighbors.push((nx, ny, diff - talus));
217 }
218 }
219
220 if total_diff > 0.0 {
221 let transfer = max_diff * params.thermal_rate * 0.5;
222 for (nx, ny, weight) in neighbors {
223 let frac = weight / total_diff;
224 transfers.push((x, y, nx, ny, transfer * frac));
225 }
226 }
227 }
228 }
229
230 for (sx, sy, dx, dy, amount) in transfers {
231 grid.add(sx, sy, -amount);
232 grid.add(dx, dy, amount);
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 fn test_hill() -> Grid2D {
241 let mut g = Grid2D::new(32, 32);
242 for y in 0..32 {
244 for x in 0..32 {
245 let dx = x as f32 - 16.0;
246 let dy = y as f32 - 16.0;
247 g.set(x, y, (-0.01 * (dx * dx + dy * dy)).exp());
248 }
249 }
250 g
251 }
252
253 #[test]
254 fn test_hydraulic_erodes() {
255 let before = test_hill();
256 let peak_before = before.get(16, 16);
257 let mut rng = Rng::new(42);
258 let after = erode(before, 5000, &mut rng);
259 let peak_after = after.get(16, 16);
260 assert!(peak_after < peak_before, "erosion should lower the peak");
261 }
262
263 #[test]
264 fn test_thermal_smooths() {
265 let mut g = Grid2D::new(8, 8);
266 g.set(4, 4, 10.0); let params = ErosionParams::default();
268 for _ in 0..100 {
269 thermal_step(&mut g, ¶ms);
270 }
271 assert!(g.get(4, 4) < 10.0, "thermal erosion should reduce spike");
272 assert!(g.get(3, 4) > 0.0, "neighbors should gain material");
273 }
274}