1use crate::pack::pack_dense_grid_to_vxl;
12use crate::perlin::PerlinNoise3D;
13use crate::worley::{anisotropic_dist_sq, place_seeds, worley_classify_grid, Seed};
14use crate::{CaveParams, Generator, Vxl, MAXZDIM};
15
16const COLOR_PERLIN_FREQUENCY: f32 = 1.0 / 8.0;
19
20const COLOR_SEED_OFFSET: u64 = 0xDEAD_BEEF_CAFE_F00D;
24
25#[derive(Debug, Default, Clone, Copy)]
31pub struct BlueCaveGenerator;
32
33impl BlueCaveGenerator {
34 #[must_use]
37 pub fn default_params() -> CaveParams {
38 CaveParams {
39 seed: 7,
40 seed_count: 128,
41 air_ratio: 0.5,
42 anisotropy: 1.0,
43 perlin_octaves: 3,
44 perlin_amplitude: 0.15,
45 }
46 }
47}
48
49impl Generator for BlueCaveGenerator {
50 type Params = CaveParams;
51
52 fn generate(&self, params: &Self::Params, vsid: u32) -> Vxl {
53 let grid = worley_classify_grid(params, vsid);
54 let color = build_blue_color_grid(params, vsid, &grid);
55 pack_dense_grid_to_vxl(&grid, &color, vsid)
56 }
57}
58
59#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
63fn build_blue_color_grid(params: &CaveParams, vsid: u32, grid: &[u8]) -> Vec<u32> {
64 let perlin = PerlinNoise3D::new(params.seed.wrapping_add(COLOR_SEED_OFFSET));
65 let vsid_u = vsid as usize;
66 let maxzdim_u = MAXZDIM as usize;
67 let mut color = vec![0u32; grid.len()];
68 for y in 0..vsid {
69 for x in 0..vsid {
70 for z in 0..MAXZDIM {
71 let idx = (y as usize * vsid_u + x as usize) * maxzdim_u + z as usize;
72 if grid[idx] != 0 {
73 color[idx] = blue_cave_color(x, y, z, &perlin);
74 }
75 }
76 }
77 }
78 color
79}
80
81#[allow(clippy::cast_precision_loss)]
84fn blue_cave_color(x: u32, y: u32, z: i32, perlin: &PerlinNoise3D) -> u32 {
85 const BASE: u32 = 0x80_70_78_80; const UPPER: u32 = 0x80_60_80_60; const LOWER: u32 = 0x80_60_40_30; const INTENSITY_AMPLITUDE: f32 = 0.20;
92
93 let z_norm = (z as f32) / (MAXZDIM as f32);
94 let base = if z_norm < 0.5 {
95 lerp_rgb(UPPER, BASE, z_norm * 2.0)
97 } else {
98 lerp_rgb(BASE, LOWER, (z_norm - 0.5) * 2.0)
100 };
101 let perlin_val = perlin.sample(
102 (x as f32) * COLOR_PERLIN_FREQUENCY,
103 (y as f32) * COLOR_PERLIN_FREQUENCY,
104 (z as f32) * COLOR_PERLIN_FREQUENCY,
105 );
106 let intensity = 1.0 + INTENSITY_AMPLITUDE * perlin_val;
107 apply_intensity(base, intensity)
108}
109
110#[allow(
113 clippy::cast_possible_truncation,
114 clippy::cast_sign_loss,
115 clippy::cast_precision_loss,
116 clippy::many_single_char_names
117)]
118fn lerp_rgb(a: u32, b: u32, t: f32) -> u32 {
119 let (ar, ag, ab) = unpack_rgb(a);
120 let (br, bg, bb) = unpack_rgb(b);
121 let brightness = (a >> 24) & 0xff;
122 let r = (f32::from(ar) + (f32::from(br) - f32::from(ar)) * t).round() as u32;
123 let g = (f32::from(ag) + (f32::from(bg) - f32::from(ag)) * t).round() as u32;
124 let blu = (f32::from(ab) + (f32::from(bb) - f32::from(ab)) * t).round() as u32;
125 (brightness << 24) | (r << 16) | (g << 8) | blu
126}
127
128#[allow(
131 clippy::cast_possible_truncation,
132 clippy::cast_sign_loss,
133 clippy::cast_precision_loss
134)]
135fn apply_intensity(color: u32, factor: f32) -> u32 {
136 let (r, g, b) = unpack_rgb(color);
137 let brightness = (color >> 24) & 0xff;
138 let scaled = |c: u8| (f32::from(c) * factor).clamp(0.0, 255.0).round() as u32;
139 (brightness << 24) | (scaled(r) << 16) | (scaled(g) << 8) | scaled(b)
140}
141
142#[derive(Debug, Default, Clone, Copy)]
154pub struct MagCaveGenerator;
155
156impl MagCaveGenerator {
157 #[must_use]
162 pub fn default_params() -> CaveParams {
163 CaveParams {
164 seed: 7,
165 seed_count: 128,
166 air_ratio: 0.4,
167 anisotropy: 1.5,
168 perlin_octaves: 4,
169 perlin_amplitude: 0.25,
170 }
171 }
172}
173
174impl Generator for MagCaveGenerator {
175 type Params = CaveParams;
176
177 fn generate(&self, params: &Self::Params, vsid: u32) -> Vxl {
178 let shape_seeds = place_seeds(params, vsid);
182 let grid = worley_classify_grid(params, vsid);
183 let color = build_mag_color_grid(params, vsid, &grid, &shape_seeds);
184 pack_dense_grid_to_vxl(&grid, &color, vsid)
185 }
186}
187
188#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
192fn build_mag_color_grid(params: &CaveParams, vsid: u32, grid: &[u8], seeds: &[Seed]) -> Vec<u32> {
193 let perlin = PerlinNoise3D::new(params.seed.wrapping_add(COLOR_SEED_OFFSET));
194 let vsid_u = vsid as usize;
195 let maxzdim_u = MAXZDIM as usize;
196 let mut color = vec![0u32; grid.len()];
197 for y in 0..vsid {
198 for x in 0..vsid {
199 for z in 0..MAXZDIM {
200 let idx = (y as usize * vsid_u + x as usize) * maxzdim_u + z as usize;
201 if grid[idx] != 0 {
202 color[idx] = mag_cave_color(x, y, z, &perlin, seeds, params.anisotropy);
203 }
204 }
205 }
206 }
207 color
208}
209
210#[allow(clippy::cast_precision_loss)]
214fn mag_cave_color(
215 x: u32,
216 y: u32,
217 z: i32,
218 perlin: &PerlinNoise3D,
219 seeds: &[Seed],
220 anisotropy: f32,
221) -> u32 {
222 const MAGENTA: u32 = 0x80_a0_40_a0;
223 const YELLOW_GREEN: u32 = 0x80_b0_b0_20;
224 const EDGE_THRESHOLD: f32 = 4.0;
229 const INTENSITY_AMPLITUDE: f32 = 0.25;
230
231 let p = [x as f32, y as f32, z as f32];
232 let mut d_air_sq = f32::INFINITY;
233 let mut d_solid_sq = f32::INFINITY;
234 for seed in seeds {
235 let d_sq = anisotropic_dist_sq(p, seed.pos, anisotropy);
236 if seed.is_air {
237 if d_sq < d_air_sq {
238 d_air_sq = d_sq;
239 }
240 } else if d_sq < d_solid_sq {
241 d_solid_sq = d_sq;
242 }
243 }
244 let d_air = d_air_sq.sqrt();
245 let d_solid = d_solid_sq.sqrt();
246 let edge_factor = (1.0 - (d_air - d_solid).abs() / EDGE_THRESHOLD).clamp(0.0, 1.0);
247 let base = lerp_rgb(MAGENTA, YELLOW_GREEN, edge_factor);
248
249 let perlin_val = perlin.sample(
250 (x as f32) * COLOR_PERLIN_FREQUENCY,
251 (y as f32) * COLOR_PERLIN_FREQUENCY,
252 (z as f32) * COLOR_PERLIN_FREQUENCY,
253 );
254 let intensity = 1.0 + INTENSITY_AMPLITUDE * perlin_val;
255 apply_intensity(base, intensity)
256}
257
258#[inline]
259fn unpack_rgb(color: u32) -> (u8, u8, u8) {
260 #[allow(clippy::cast_possible_truncation)]
261 let r = ((color >> 16) & 0xff) as u8;
262 #[allow(clippy::cast_possible_truncation)]
263 let g = ((color >> 8) & 0xff) as u8;
264 #[allow(clippy::cast_possible_truncation)]
265 let b = (color & 0xff) as u8;
266 (r, g, b)
267}
268
269#[cfg(test)]
270#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn blue_default_params_match_plan() {
276 let p = BlueCaveGenerator::default_params();
277 assert_eq!(p.seed, 7);
278 assert_eq!(p.seed_count, 128);
279 assert!((p.air_ratio - 0.5).abs() < 1e-6);
280 assert!((p.anisotropy - 1.0).abs() < 1e-6);
281 assert_eq!(p.perlin_octaves, 3);
282 assert!((p.perlin_amplitude - 0.15).abs() < 1e-6);
283 }
284
285 #[test]
286 fn blue_generate_byte_stable_in_seed() {
287 let p = CaveParams {
289 seed_count: 16,
290 ..BlueCaveGenerator::default_params()
291 };
292 let a = BlueCaveGenerator.generate(&p, 16);
293 let b = BlueCaveGenerator.generate(&p, 16);
294 assert_eq!(a.vsid, b.vsid);
295 assert_eq!(a.column_offset.as_ref(), b.column_offset.as_ref());
296 assert_eq!(a.data.as_ref(), b.data.as_ref());
297 }
298
299 #[test]
300 fn blue_generate_yields_mixed_air_and_solid() {
301 let p = CaveParams {
304 seed_count: 16,
305 ..BlueCaveGenerator::default_params()
306 };
307 let vxl = BlueCaveGenerator.generate(&p, 16);
308 let mut total_runs = 0;
311 for idx in 0..(16 * 16) {
312 let mut b2 = vec![0i32; 256];
313 roxlap_formats::edit::expandrle(vxl.column_data(idx), &mut b2);
314 let mut i = 0;
315 while b2[i + 1] < MAXZDIM {
316 i += 2;
317 }
318 total_runs += (i + 2) / 2;
320 }
321 assert!(
325 total_runs > 256,
326 "expected multi-run columns from cave gen; got {total_runs} total runs"
327 );
328 }
329
330 #[test]
331 fn lerp_rgb_endpoints_match() {
332 let a = 0x80_aa_bb_cc;
333 let b = 0x80_11_22_33;
334 assert_eq!(lerp_rgb(a, b, 0.0), a);
335 assert_eq!(lerp_rgb(a, b, 1.0), b & 0x00_ff_ff_ff | (a & 0xff00_0000));
336 }
337
338 #[test]
339 fn lerp_rgb_midpoint() {
340 let a = 0x8000_0000u32;
343 let b = 0x40c8_6432u32; let mid = lerp_rgb(a, b, 0.5);
345 let (r, g, blu) = unpack_rgb(mid);
346 assert_eq!(r, 100, "red midpoint");
347 assert_eq!(g, 50, "green midpoint");
348 assert_eq!(blu, 25, "blue midpoint");
349 assert_eq!((mid >> 24) & 0xff, 0x80);
351 }
352
353 #[test]
354 fn apply_intensity_clamps_to_255() {
355 let c = 0x80_80_80_80; let scaled = apply_intensity(c, 2.5);
358 let (r, g, b) = unpack_rgb(scaled);
359 assert_eq!(r, 255, "red clamped");
360 assert_eq!(g, 255, "green clamped");
361 assert_eq!(b, 255, "blue clamped");
362 }
363
364 #[test]
365 fn apply_intensity_preserves_brightness_byte() {
366 let c = 0x80_80_80_80;
367 let scaled = apply_intensity(c, 0.5);
368 assert_eq!((scaled >> 24) & 0xff, 0x80, "brightness preserved");
369 }
370
371 #[test]
372 fn blue_cave_color_top_skews_green() {
373 let perlin = PerlinNoise3D::new(0);
376 let c = blue_cave_color(0, 0, 0, &perlin);
380 let (r, g, b) = unpack_rgb(c);
381 assert_eq!(r, 0x60);
384 assert_eq!(g, 0x80);
385 assert_eq!(b, 0x60);
386 }
387
388 #[test]
389 fn blue_cave_color_bottom_skews_orange() {
390 let perlin = PerlinNoise3D::new(0);
393 let c = blue_cave_color(0, 0, MAXZDIM - 1, &perlin);
397 let (r, g, b) = unpack_rgb(c);
398 assert!(
403 (i32::from(r) - 0x60).abs() <= 2,
404 "R close to 0x60: got {r:#04x}"
405 );
406 assert!(
407 (i32::from(g) - 0x40).abs() <= 2,
408 "G close to 0x40: got {g:#04x}"
409 );
410 assert!(
411 (i32::from(b) - 0x30).abs() <= 2,
412 "B close to 0x30: got {b:#04x}"
413 );
414 }
415
416 #[test]
419 fn mag_default_params_match_plan() {
420 let p = MagCaveGenerator::default_params();
421 assert_eq!(p.seed_count, 128);
422 assert!((p.air_ratio - 0.4).abs() < 1e-6, "air_ratio");
423 assert!((p.anisotropy - 1.5).abs() < 1e-6, "anisotropy");
424 assert_eq!(p.perlin_octaves, 4);
425 assert!((p.perlin_amplitude - 0.25).abs() < 1e-6, "amplitude");
426 }
427
428 #[test]
429 fn mag_generate_byte_stable_in_seed() {
430 let p = CaveParams {
431 seed_count: 16,
432 ..MagCaveGenerator::default_params()
433 };
434 let a = MagCaveGenerator.generate(&p, 16);
435 let b = MagCaveGenerator.generate(&p, 16);
436 assert_eq!(a.column_offset.as_ref(), b.column_offset.as_ref());
437 assert_eq!(a.data.as_ref(), b.data.as_ref());
438 }
439
440 #[test]
441 fn mag_generate_yields_mixed_air_and_solid() {
442 let p = CaveParams {
443 seed_count: 16,
444 ..MagCaveGenerator::default_params()
445 };
446 let vxl = MagCaveGenerator.generate(&p, 16);
447 let mut total_runs = 0;
448 for idx in 0..(16 * 16) {
449 let mut b2 = vec![0i32; 256];
450 roxlap_formats::edit::expandrle(vxl.column_data(idx), &mut b2);
451 let mut i = 0;
452 while b2[i + 1] < MAXZDIM {
453 i += 2;
454 }
455 total_runs += (i + 2) / 2;
456 }
457 assert!(
458 total_runs > 256,
459 "expected multi-run columns from cave gen; got {total_runs} total runs"
460 );
461 }
462
463 #[test]
464 fn mag_far_from_boundary_skews_magenta() {
465 let seeds = vec![
470 Seed {
471 pos: [0.0, 0.0, 0.0],
472 is_air: false,
473 },
474 Seed {
475 pos: [100.0, 100.0, 100.0],
476 is_air: true,
477 },
478 ];
479 let perlin = PerlinNoise3D::new(0);
480 let c = mag_cave_color(1, 0, 0, &perlin, &seeds, 1.0);
483 let (r, g, b) = unpack_rgb(c);
484 assert!(
487 (i32::from(r) - 0xa0).abs() <= 2,
488 "R magenta-ish: got {r:#04x}"
489 );
490 assert!(
491 (i32::from(g) - 0x40).abs() <= 2,
492 "G magenta-ish: got {g:#04x}"
493 );
494 assert!(
495 (i32::from(b) - 0xa0).abs() <= 2,
496 "B magenta-ish: got {b:#04x}"
497 );
498 }
499
500 #[test]
501 fn mag_at_boundary_skews_yellow_green() {
502 let seeds = vec![
505 Seed {
506 pos: [0.0, 0.0, 0.0],
507 is_air: false,
508 },
509 Seed {
510 pos: [2.0, 0.0, 0.0],
511 is_air: true,
512 },
513 ];
514 let perlin = PerlinNoise3D::new(0);
515 let c = mag_cave_color(1, 0, 0, &perlin, &seeds, 1.0);
518 let (r, g, b) = unpack_rgb(c);
519 assert!(
521 (i32::from(r) - 0xb0).abs() <= 2,
522 "R yellow-green-ish: got {r:#04x}"
523 );
524 assert!(
525 (i32::from(g) - 0xb0).abs() <= 2,
526 "G yellow-green-ish: got {g:#04x}"
527 );
528 assert!(
529 (i32::from(b) - 0x20).abs() <= 2,
530 "B yellow-green-ish: got {b:#04x}"
531 );
532 }
533
534 #[test]
535 fn mag_and_blue_diverge_in_byte_output() {
536 let p = CaveParams {
541 seed_count: 16,
542 ..BlueCaveGenerator::default_params()
543 };
544 let blue = BlueCaveGenerator.generate(&p, 16);
545 let q = CaveParams {
546 seed_count: 16,
547 ..MagCaveGenerator::default_params()
548 };
549 let mag = MagCaveGenerator.generate(&q, 16);
550 let mut differing = 0;
553 for idx in 0..(16 * 16) {
554 if blue.column_data(idx) != mag.column_data(idx) {
555 differing += 1;
556 }
557 }
558 assert!(
559 differing > 0,
560 "Blue and Mag presets should produce different output"
561 );
562 }
563}