1use crate::{
2 CONFIG, Canvas, ColumnSample,
3 util::{
4 NEIGHBORS, NEIGHBORS3, RandomField, Sampler, StructureGen2d, UnitChooser,
5 gen_cache::StructureGenCache, seed_expan,
6 },
7};
8use common::terrain::{Block, BlockKind};
9use ordered_float::NotNan;
10use rand::prelude::*;
11use rand_chacha::ChaChaRng;
12use vek::*;
13
14struct Rock {
15 wpos: Vec3<i32>,
16 seed: u32,
17 units: Vec2<Vec2<i32>>,
18 kind: RockKind,
19}
20
21pub fn apply_rocks_to(canvas: &mut Canvas, _dynamic_rng: &mut impl Rng) {
22 let mut rock_gen = StructureGenCache::new(StructureGen2d::new(canvas.index().seed, 24, 10));
23
24 let info = canvas.info();
25 canvas.foreach_col(|canvas, wpos2d, col| {
26 let rocks = rock_gen.get(wpos2d, |wpos, seed| {
27 let col = info.col_or_gen(wpos)?;
28
29 let mut rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
30
31 const BASE_ROCK_DENSITY: f64 = 0.15;
32 if rng.gen_bool((BASE_ROCK_DENSITY * col.rock_density as f64).clamped(0.0, 1.0))
33 && col.path.is_none_or(|(d, _, _, _)| d > 6.0)
34 {
35 match (
36 (col.alt - CONFIG.sea_level) as i32,
37 (col.alt - col.water_level) as i32,
38 col.water_dist.map_or(i32::MAX, |d| d as i32),
39 ) {
40 (-3..=2, _, _) => {
41 if rng.gen_bool(0.3) {
42 Some(RockKind::Rauk(Pillar::generate(&mut rng)))
43 } else {
44 Some(RockKind::Rock(VoronoiCell::generate(
45 rng.gen_range(1.0..3.0),
46 &mut rng,
47 )))
48 }
49 },
50 (_, -15..=3, _) => Some(RockKind::Rock(VoronoiCell::generate(
51 rng.gen_range(1.0..4.0),
52 &mut rng,
53 ))),
54 (5..=i32::MAX, _, 0..=i32::MAX) => {
55 if col.temp > CONFIG.desert_temp - 0.1
56 && col.humidity < CONFIG.desert_hum + 0.1
57 {
58 Some(RockKind::Sandstone(VoronoiCell::generate(
59 rng.gen_range(2.0..20.0 - 10.0 * col.tree_density),
60 &mut rng,
61 )))
62 } else {
63 Some(RockKind::Rock(VoronoiCell::generate(
64 rng.gen_range(2.0..20.0 - 10.0 * col.tree_density),
65 &mut rng,
66 )))
67 }
68 },
69 _ => None,
70 }
71 .map(|kind| Rock {
72 wpos: wpos.with_z(col.alt as i32),
73 seed,
74 units: UnitChooser::new(seed).get(seed).into(),
75 kind,
76 })
77 } else {
78 None
79 }
80 });
81
82 for rock in rocks {
83 let bounds = rock.kind.get_bounds();
84
85 let rpos2d = (wpos2d - rock.wpos.xy())
86 .map2(rock.units, |p, unit| unit * p)
87 .sum();
88
89 if !Aabr::from(bounds).contains_point(rpos2d) {
90 continue;
92 }
93
94 let mut is_top = true;
95 let mut last_block = Block::empty();
96 for z in (bounds.min.z..bounds.max.z).rev() {
97 let wpos = Vec3::new(wpos2d.x, wpos2d.y, rock.wpos.z + z);
98 let model_pos = (wpos - rock.wpos)
99 .xy()
100 .map2(rock.units, |rpos, unit| unit * rpos)
101 .sum()
102 .with_z(wpos.z - rock.wpos.z);
103
104 rock.kind
105 .take_sample(model_pos, rock.seed, last_block, col)
106 .map(|block| {
107 if col.snow_cover && is_top && block.is_filled() {
108 canvas.set(
109 wpos + Vec3::unit_z(),
110 Block::new(BlockKind::Snow, Rgb::new(210, 210, 255)),
111 );
112 }
113 canvas.set(wpos, block);
114 is_top = false;
115 last_block = block;
116 });
117 }
118 }
119 });
120}
121
122struct VoronoiCell {
123 size: f32,
124 points: [Vec3<f32>; 26],
125}
126
127impl VoronoiCell {
128 fn generate(size: f32, rng: &mut impl Rng) -> Self {
129 let mut points = [Vec3::zero(); 26];
130 for (i, p) in NEIGHBORS3.iter().enumerate() {
131 points[i] = p.as_() * size
132 + Vec3::new(
133 rng.gen_range(-0.5..=0.5) * size,
134 rng.gen_range(-0.5..=0.5) * size,
135 rng.gen_range(-0.5..=0.5) * size,
136 );
137 }
138 Self { size, points }
139 }
140
141 fn sample_at(&self, rpos: Vec3<i32>) -> bool {
142 let rposf = rpos.as_();
143 rposf.magnitude_squared()
147 <= *(0..26)
148 .map(|i| self.points[i].distance_squared(rposf))
149 .map(|d| NotNan::new(d).unwrap())
150 .min()
151 .unwrap()
152 }
153}
154
155struct Pillar {
156 height: f32,
157 max_extent: Vec2<f32>,
158 extents: [Vec2<f32>; 3],
159}
160
161impl Pillar {
162 fn generate(rng: &mut impl Rng) -> Self {
163 let extents = [
164 Vec2::new(rng.gen_range(0.5..1.5), rng.gen_range(0.5..1.5)),
165 Vec2::new(rng.gen_range(0.8..2.8), rng.gen_range(0.8..2.8)),
166 Vec2::new(rng.gen_range(0.5..1.5), rng.gen_range(0.5..3.5)),
167 ];
168 Self {
169 height: rng.gen_range(6.0..16.0),
170 extents,
171 max_extent: extents
172 .iter()
173 .cloned()
174 .reduce(|accum, item| accum.map2(item, |a, b| a.max(b)))
175 .unwrap(),
176 }
177 }
178
179 fn sample_at(&self, rpos: Vec3<i32>) -> bool {
180 let h = rpos.z as f32 / self.height;
181 let extent = if h < 0.0 {
182 self.extents[0] * (-h).max(1.0)
183 } else if h < 0.5 {
184 self.extents[0].map2(self.extents[1], |l, m| f32::lerp(l, m, h * 2.0))
185 } else if h < 1.0 {
186 self.extents[1].map2(self.extents[2], |m, t| f32::lerp(m, t, (h - 0.5) * 2.0))
187 } else {
188 self.extents[2]
189 };
190 h < 1.0
191 && extent
192 .map2(rpos.xy(), |e, p| p.abs() < e.ceil() as i32)
193 .reduce_and()
194 }
195}
196
197enum RockKind {
198 Rock(VoronoiCell),
200 Sandstone(VoronoiCell),
201 Rauk(Pillar),
202 }
205
206impl RockKind {
207 fn take_sample(
208 &self,
209 rpos: Vec3<i32>,
210 seed: u32,
211 last_block: Block,
212 col: &ColumnSample,
213 ) -> Option<Block> {
214 match self {
231 RockKind::Rock(cell) => {
232 if cell.sample_at(rpos) {
233 let mossiness = 0.1
234 + RandomField::new(seed).get_f32(Vec3::zero()) * 0.3
235 + col.humidity * 0.9;
236 Some(
237 if last_block.is_filled()
238 || (rpos.z as f32 / cell.size
239 + RandomField::new(seed).get_f32(rpos) * 0.3
240 > mossiness)
241 {
242 let mut i = 0;
243 Block::new(
244 BlockKind::WeakRock,
245 col.stone_col.map(|c| {
246 i += 1;
247 c + RandomField::new(seed).get(rpos) as u8 % 10
248 }),
249 )
250 } else {
251 Block::new(
252 BlockKind::Grass,
253 col.surface_color.map(|e| (e * 255.0) as u8),
254 )
255 },
256 )
257 } else {
258 None
259 }
260 },
261 RockKind::Sandstone(cell) => {
262 if cell.sample_at(rpos) {
263 let sandiness = 0.3 + RandomField::new(seed).get_f32(Vec3::zero()) * 0.4;
264 Some(
265 if last_block.is_filled()
266 || (rpos.z as f32 / cell.size
267 + RandomField::new(seed).get_f32(rpos) * 0.3
268 > sandiness)
269 {
270 let mut i = 0;
271 Block::new(
272 BlockKind::WeakRock,
273 Rgb::new(220, 160, 100).map(|c| {
274 i += 1;
275 c + RandomField::new(seed + i).get(Vec2::zero().with_z(rpos.z))
276 as u8
277 % 30
278 }),
279 )
280 } else {
281 Block::new(
282 BlockKind::Grass,
283 col.surface_color.map(|e| (e * 255.0) as u8),
284 )
285 },
286 )
287 } else {
288 None
289 }
290 },
291 RockKind::Rauk(pillar) => {
292 let max_extent = *pillar
293 .max_extent
294 .map(|e| NotNan::new(e).unwrap())
295 .reduce_max();
296 let is_filled = |rpos| {
297 pillar.sample_at(rpos)
298 && RandomField::new(seed).chance(
299 rpos,
300 1.5 - rpos.z as f32 / pillar.height
301 - rpos.xy().as_::<f32>().magnitude() / max_extent,
302 )
303 };
304 if is_filled(rpos) ||
305 (last_block.is_filled()
307 && NEIGHBORS
308 .iter()
309 .all(|n| !is_filled(rpos + n.with_z(0))))
310 {
311 Some(Block::new(
312 BlockKind::WeakRock,
313 Rgb::new(
314 190 + RandomField::new(seed + 1).get(rpos) as u8 % 10,
315 190 + RandomField::new(seed + 2).get(rpos) as u8 % 10,
316 190 + RandomField::new(seed + 3).get(rpos) as u8 % 10,
317 ),
318 ))
319 } else {
320 None
321 }
322 },
323 }
324 }
325
326 fn get_bounds(&self) -> Aabb<i32> {
327 match self {
328 RockKind::Rock(VoronoiCell { size, .. })
329 | RockKind::Sandstone(VoronoiCell { size, .. }) => {
330 let extent = *size as i32;
332 Aabb {
333 min: Vec3::broadcast(-extent),
334 max: Vec3::broadcast(extent),
335 }
336 },
337 RockKind::Rauk(Pillar {
338 max_extent: extent,
339 height,
340 ..
341 }) => Aabb {
342 min: (-extent.as_()).with_z(-2),
343 max: extent.as_().with_z(*height as i32),
344 },
345 }
346 }
347}