steel_worldgen/noise/
beardifier.rs1use std::sync::LazyLock;
9
10use glam::IVec3;
11use steel_math::map_clamped;
12use steel_registry::structure::TerrainAdjustment;
13use steel_registry::template_pool::Projection;
14use steel_utils::BoundingBox;
15
16use crate::structure::StructureStart;
17use crate::structure::jigsaw::JigsawJunction;
18
19#[derive(Debug)]
21struct Rigid {
22 bounding_box: BoundingBox,
23 terrain_adjustment: TerrainAdjustment,
24 ground_level_delta: i32,
25}
26
27const KERNEL_RADIUS: i32 = 12;
28const KERNEL_SIZE: usize = 24;
29const KERNEL_TOTAL: usize = KERNEL_SIZE * KERNEL_SIZE * KERNEL_SIZE; const BURY_RADIUS: f32 = 6.0;
31
32static BEARD_KERNEL: LazyLock<[f32; KERNEL_TOTAL]> = LazyLock::new(|| {
35 let mut kernel = [0.0f32; KERNEL_TOTAL];
36 for zi in 0..KERNEL_SIZE {
37 let dz = zi as i32 - KERNEL_RADIUS;
38 for xi in 0..KERNEL_SIZE {
39 let dx = xi as i32 - KERNEL_RADIUS;
40 for yi in 0..KERNEL_SIZE {
41 let dy = yi as i32 - KERNEL_RADIUS;
42 let dy_f = f64::from(dy) + 0.5;
44 let dist_sq = f64::from(dx * dx) + dy_f * dy_f + f64::from(dz * dz);
45 let value = (-dist_sq / 16.0).exp();
46 kernel[zi * KERNEL_SIZE * KERNEL_SIZE + xi * KERNEL_SIZE + yi] = value as f32;
47 }
48 }
49 }
50 kernel
51});
52
53#[inline]
55fn fast_inv_sqrt(x: f64) -> f64 {
56 let xhalf = 0.5f64 * x;
57 let i = f64::to_bits(x) as i64;
58 let i = 0x5FE6_EB50_C7B5_37A9_i64 - (i >> 1);
59 let mut x = f64::from_bits(i as u64);
60 x *= 1.5f64 - xhalf * x * x;
61 x
62}
63
64#[inline]
65fn is_in_kernel_range(index: i32) -> bool {
66 (0..KERNEL_SIZE as i32).contains(&index)
67}
68
69fn get_beard_contribution(dx: i32, dy: i32, dz: i32, y_to_ground: i32) -> f32 {
74 let xi = dx + KERNEL_RADIUS;
75 let yi = dy + KERNEL_RADIUS;
76 let zi = dz + KERNEL_RADIUS;
77
78 if !is_in_kernel_range(xi) || !is_in_kernel_range(yi) || !is_in_kernel_range(zi) {
79 return 0.0;
80 }
81
82 let dy_with_offset = y_to_ground as f32 + 0.5;
83 let dist_sq = (dx * dx) as f32 + dy_with_offset * dy_with_offset + (dz * dz) as f32;
84 let value = -dy_with_offset * fast_inv_sqrt(f64::from(dist_sq / 2.0)) as f32 / 2.0;
85 let kernel_idx =
86 zi as usize * KERNEL_SIZE * KERNEL_SIZE + xi as usize * KERNEL_SIZE + yi as usize;
87 value * BEARD_KERNEL[kernel_idx]
88}
89
90fn get_bury_contribution(dx: f32, dy: f32, dz: f32) -> f32 {
94 let distance = (dx * dx + dy * dy + dz * dz).sqrt();
95 map_clamped(distance, 0.0, BURY_RADIUS, 1.0, 0.0)
96}
97
98pub struct Beardifier {
105 rigids: Vec<Rigid>,
106 junctions: Vec<JigsawJunction>,
107 affected_box: Option<BoundingBox>,
110}
111
112impl Beardifier {
113 #[must_use]
126 pub fn for_structures_in_chunk<'a, I>(starts: I, chunk_x: i32, chunk_z: i32) -> Self
127 where
128 I: IntoIterator<Item = &'a StructureStart>,
129 {
130 let chunk_start_x = chunk_x * 16;
131 let chunk_start_z = chunk_z * 16;
132
133 let mut rigids = Vec::new();
134 let mut junctions: Vec<JigsawJunction> = Vec::new();
135 let mut encompassing: Option<BoundingBox> = None;
136
137 for start in starts {
138 let terrain_adj = start.terrain_adjustment;
139 if terrain_adj == TerrainAdjustment::None {
140 continue;
141 }
142
143 for piece in &start.pieces {
144 let bb = &piece.bounding_box;
145
146 if !is_close_to_chunk(bb, chunk_x, chunk_z, 12) {
148 continue;
149 }
150
151 let is_jigsaw = piece.projection.is_some();
152 let is_rigid = matches!(piece.projection, Some(Projection::Rigid));
153
154 if !is_jigsaw || is_rigid {
158 encompassing = Some(match encompassing {
159 Some(enc) => BoundingBox::encapsulating(&enc, bb),
160 None => *bb,
161 });
162
163 rigids.push(Rigid {
164 bounding_box: *bb,
165 terrain_adjustment: terrain_adj,
166 ground_level_delta: if is_jigsaw {
169 piece.ground_level_delta
170 } else {
171 0
172 },
173 });
174 }
175
176 if is_jigsaw {
180 for junction in &piece.junctions {
181 let jx = junction.source_pos.x;
182 let jz = junction.source_pos.z;
183 if jx > chunk_start_x - 12
184 && jz > chunk_start_z - 12
185 && jx < chunk_start_x + 15 + 12
186 && jz < chunk_start_z + 15 + 12
187 {
188 let jy = junction.source_pos.y;
189 let junction_bb =
190 BoundingBox::new(IVec3::new(jx, jy, jz), IVec3::new(jx, jy, jz));
191 encompassing = Some(match encompassing {
192 Some(enc) => BoundingBox::encapsulating(&enc, &junction_bb),
193 None => junction_bb,
194 });
195 junctions.push(junction.clone());
196 }
197 }
198 }
199 }
200 }
201
202 let affected_box = encompassing
203 .map(|bb| bb.inflate_xyz(KERNEL_SIZE as i32, KERNEL_SIZE as i32, KERNEL_SIZE as i32));
204
205 Self {
206 rigids,
207 junctions,
208 affected_box,
209 }
210 }
211
212 #[must_use]
214 pub const fn is_empty(&self) -> bool {
215 self.rigids.is_empty() && self.junctions.is_empty()
216 }
217
218 #[must_use]
221 pub fn may_affect(&self, min: IVec3, max: IVec3) -> bool {
222 self.affected_box
223 .is_some_and(|affected| affected.intersects_bounds(min, max))
224 }
225
226 #[must_use]
230 pub fn compute(&self, block_x: i32, block_y: i32, block_z: i32) -> f64 {
231 let Some(affected) = &self.affected_box else {
232 return 0.0;
233 };
234 if !affected.contains_xyz(block_x, block_y, block_z) {
235 return 0.0;
236 }
237
238 let mut value = 0.0f32;
239
240 for rigid in &self.rigids {
241 let bb = &rigid.bounding_box;
242
243 let dx = 0.max((bb.min_x() - block_x).max(block_x - bb.max_x()));
245 let dz = 0.max((bb.min_z() - block_z).max(block_z - bb.max_z()));
246
247 let ground_y = bb.min_y() + rigid.ground_level_delta;
248 let dy_to_ground = block_y - ground_y;
249
250 match rigid.terrain_adjustment {
251 TerrainAdjustment::None => {}
252 TerrainAdjustment::Bury => {
253 value += get_bury_contribution(dx as f32, dy_to_ground as f32 / 2.0, dz as f32);
254 }
255 TerrainAdjustment::BeardThin => {
256 value += get_beard_contribution(dx, dy_to_ground, dz, dy_to_ground) * 0.8;
257 }
258 TerrainAdjustment::BeardBox => {
259 let dy = 0.max((ground_y - block_y).max(block_y - bb.max_y()));
260 value += get_beard_contribution(dx, dy, dz, dy_to_ground) * 0.8;
261 }
262 TerrainAdjustment::Encapsulate => {
263 let dy = 0.max((bb.min_y() - block_y).max(block_y - bb.max_y()));
264 value +=
265 get_bury_contribution(dx as f32 / 2.0, dy as f32 / 2.0, dz as f32 / 2.0)
266 * 0.8;
267 }
268 }
269 }
270
271 for junction in &self.junctions {
272 let dx = block_x - junction.source_pos.x;
273 let dy = block_y - junction.source_pos.y;
274 let dz = block_z - junction.source_pos.z;
275 value += get_beard_contribution(dx, dy, dz, dy) * 0.4;
276 }
277
278 f64::from(value)
279 }
280}
281
282const fn is_close_to_chunk(bb: &BoundingBox, chunk_x: i32, chunk_z: i32, margin: i32) -> bool {
286 let chunk_start_x = chunk_x * 16;
287 let chunk_start_z = chunk_z * 16;
288 let chunk_end_x = chunk_start_x + 15;
289 let chunk_end_z = chunk_start_z + 15;
290
291 bb.max_x() >= chunk_start_x - margin
292 && bb.min_x() <= chunk_end_x + margin
293 && bb.max_z() >= chunk_start_z - margin
294 && bb.min_z() <= chunk_end_z + margin
295}