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