use crate::pack::pack_dense_grid_to_vxl;
use crate::perlin::PerlinNoise3D;
use crate::worley::{anisotropic_dist_sq, place_seeds, worley_classify_grid, Seed};
use crate::{CaveParams, Generator, Vxl, MAXZDIM};
const COLOR_PERLIN_FREQUENCY: f32 = 1.0 / 8.0;
const COLOR_SEED_OFFSET: u64 = 0xDEAD_BEEF_CAFE_F00D;
#[derive(Debug, Default, Clone, Copy)]
pub struct BlueCaveGenerator;
impl BlueCaveGenerator {
#[must_use]
pub fn default_params() -> CaveParams {
CaveParams {
seed: 7,
seed_count: 128,
air_ratio: 0.5,
anisotropy: 1.0,
perlin_octaves: 3,
perlin_amplitude: 0.15,
}
}
}
impl Generator for BlueCaveGenerator {
type Params = CaveParams;
fn generate(&self, params: &Self::Params, vsid: u32) -> Vxl {
let grid = worley_classify_grid(params, vsid);
let color = build_blue_color_grid(params, vsid, &grid);
pack_dense_grid_to_vxl(&grid, &color, vsid)
}
}
#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
fn build_blue_color_grid(params: &CaveParams, vsid: u32, grid: &[u8]) -> Vec<u32> {
let perlin = PerlinNoise3D::new(params.seed.wrapping_add(COLOR_SEED_OFFSET));
let vsid_u = vsid as usize;
let maxzdim_u = MAXZDIM as usize;
let mut color = vec![0u32; grid.len()];
for y in 0..vsid {
for x in 0..vsid {
for z in 0..MAXZDIM {
let idx = (y as usize * vsid_u + x as usize) * maxzdim_u + z as usize;
if grid[idx] != 0 {
color[idx] = blue_cave_color(x, y, z, &perlin);
}
}
}
}
color
}
#[allow(clippy::cast_precision_loss)]
fn blue_cave_color(x: u32, y: u32, z: i32, perlin: &PerlinNoise3D) -> u32 {
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;
let z_norm = (z as f32) / (MAXZDIM as f32);
let base = if z_norm < 0.5 {
lerp_rgb(UPPER, BASE, z_norm * 2.0)
} else {
lerp_rgb(BASE, LOWER, (z_norm - 0.5) * 2.0)
};
let perlin_val = perlin.sample(
(x as f32) * COLOR_PERLIN_FREQUENCY,
(y as f32) * COLOR_PERLIN_FREQUENCY,
(z as f32) * COLOR_PERLIN_FREQUENCY,
);
let intensity = 1.0 + INTENSITY_AMPLITUDE * perlin_val;
apply_intensity(base, intensity)
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::many_single_char_names
)]
fn lerp_rgb(a: u32, b: u32, t: f32) -> u32 {
let (ar, ag, ab) = unpack_rgb(a);
let (br, bg, bb) = unpack_rgb(b);
let brightness = (a >> 24) & 0xff;
let r = (f32::from(ar) + (f32::from(br) - f32::from(ar)) * t).round() as u32;
let g = (f32::from(ag) + (f32::from(bg) - f32::from(ag)) * t).round() as u32;
let blu = (f32::from(ab) + (f32::from(bb) - f32::from(ab)) * t).round() as u32;
(brightness << 24) | (r << 16) | (g << 8) | blu
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
fn apply_intensity(color: u32, factor: f32) -> u32 {
let (r, g, b) = unpack_rgb(color);
let brightness = (color >> 24) & 0xff;
let scaled = |c: u8| (f32::from(c) * factor).clamp(0.0, 255.0).round() as u32;
(brightness << 24) | (scaled(r) << 16) | (scaled(g) << 8) | scaled(b)
}
#[derive(Debug, Default, Clone, Copy)]
pub struct MagCaveGenerator;
impl MagCaveGenerator {
#[must_use]
pub fn default_params() -> CaveParams {
CaveParams {
seed: 7,
seed_count: 128,
air_ratio: 0.4,
anisotropy: 1.5,
perlin_octaves: 4,
perlin_amplitude: 0.25,
}
}
}
impl Generator for MagCaveGenerator {
type Params = CaveParams;
fn generate(&self, params: &Self::Params, vsid: u32) -> Vxl {
let shape_seeds = place_seeds(params, vsid);
let grid = worley_classify_grid(params, vsid);
let color = build_mag_color_grid(params, vsid, &grid, &shape_seeds);
pack_dense_grid_to_vxl(&grid, &color, vsid)
}
}
#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
fn build_mag_color_grid(params: &CaveParams, vsid: u32, grid: &[u8], seeds: &[Seed]) -> Vec<u32> {
let perlin = PerlinNoise3D::new(params.seed.wrapping_add(COLOR_SEED_OFFSET));
let vsid_u = vsid as usize;
let maxzdim_u = MAXZDIM as usize;
let mut color = vec![0u32; grid.len()];
for y in 0..vsid {
for x in 0..vsid {
for z in 0..MAXZDIM {
let idx = (y as usize * vsid_u + x as usize) * maxzdim_u + z as usize;
if grid[idx] != 0 {
color[idx] = mag_cave_color(x, y, z, &perlin, seeds, params.anisotropy);
}
}
}
}
color
}
#[allow(clippy::cast_precision_loss)]
fn mag_cave_color(
x: u32,
y: u32,
z: i32,
perlin: &PerlinNoise3D,
seeds: &[Seed],
anisotropy: f32,
) -> u32 {
const MAGENTA: u32 = 0x80_a0_40_a0;
const YELLOW_GREEN: u32 = 0x80_b0_b0_20;
const EDGE_THRESHOLD: f32 = 4.0;
const INTENSITY_AMPLITUDE: f32 = 0.25;
let p = [x as f32, y as f32, z as f32];
let mut d_air_sq = f32::INFINITY;
let mut d_solid_sq = f32::INFINITY;
for seed in seeds {
let d_sq = anisotropic_dist_sq(p, seed.pos, anisotropy);
if seed.is_air {
if d_sq < d_air_sq {
d_air_sq = d_sq;
}
} else if d_sq < d_solid_sq {
d_solid_sq = d_sq;
}
}
let d_air = d_air_sq.sqrt();
let d_solid = d_solid_sq.sqrt();
let edge_factor = (1.0 - (d_air - d_solid).abs() / EDGE_THRESHOLD).clamp(0.0, 1.0);
let base = lerp_rgb(MAGENTA, YELLOW_GREEN, edge_factor);
let perlin_val = perlin.sample(
(x as f32) * COLOR_PERLIN_FREQUENCY,
(y as f32) * COLOR_PERLIN_FREQUENCY,
(z as f32) * COLOR_PERLIN_FREQUENCY,
);
let intensity = 1.0 + INTENSITY_AMPLITUDE * perlin_val;
apply_intensity(base, intensity)
}
#[inline]
fn unpack_rgb(color: u32) -> (u8, u8, u8) {
#[allow(clippy::cast_possible_truncation)]
let r = ((color >> 16) & 0xff) as u8;
#[allow(clippy::cast_possible_truncation)]
let g = ((color >> 8) & 0xff) as u8;
#[allow(clippy::cast_possible_truncation)]
let b = (color & 0xff) as u8;
(r, g, b)
}
#[cfg(test)]
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
mod tests {
use super::*;
#[test]
fn blue_default_params_match_plan() {
let p = BlueCaveGenerator::default_params();
assert_eq!(p.seed, 7);
assert_eq!(p.seed_count, 128);
assert!((p.air_ratio - 0.5).abs() < 1e-6);
assert!((p.anisotropy - 1.0).abs() < 1e-6);
assert_eq!(p.perlin_octaves, 3);
assert!((p.perlin_amplitude - 0.15).abs() < 1e-6);
}
#[test]
fn blue_generate_byte_stable_in_seed() {
let p = CaveParams {
seed_count: 16,
..BlueCaveGenerator::default_params()
};
let a = BlueCaveGenerator.generate(&p, 16);
let b = BlueCaveGenerator.generate(&p, 16);
assert_eq!(a.vsid, b.vsid);
assert_eq!(a.column_offset.as_ref(), b.column_offset.as_ref());
assert_eq!(a.data.as_ref(), b.data.as_ref());
}
#[test]
fn blue_generate_yields_mixed_air_and_solid() {
let p = CaveParams {
seed_count: 16,
..BlueCaveGenerator::default_params()
};
let vxl = BlueCaveGenerator.generate(&p, 16);
let mut total_runs = 0;
for idx in 0..(16 * 16) {
let mut b2 = vec![0i32; 256];
roxlap_formats::edit::expandrle(vxl.column_data(idx), &mut b2);
let mut i = 0;
while b2[i + 1] < MAXZDIM {
i += 2;
}
total_runs += (i + 2) / 2;
}
assert!(
total_runs > 256,
"expected multi-run columns from cave gen; got {total_runs} total runs"
);
}
#[test]
fn lerp_rgb_endpoints_match() {
let a = 0x80_aa_bb_cc;
let b = 0x80_11_22_33;
assert_eq!(lerp_rgb(a, b, 0.0), a);
assert_eq!(lerp_rgb(a, b, 1.0), b & 0x00_ff_ff_ff | (a & 0xff00_0000));
}
#[test]
fn lerp_rgb_midpoint() {
let a = 0x8000_0000u32;
let b = 0x40c8_6432u32; let mid = lerp_rgb(a, b, 0.5);
let (r, g, blu) = unpack_rgb(mid);
assert_eq!(r, 100, "red midpoint");
assert_eq!(g, 50, "green midpoint");
assert_eq!(blu, 25, "blue midpoint");
assert_eq!((mid >> 24) & 0xff, 0x80);
}
#[test]
fn apply_intensity_clamps_to_255() {
let c = 0x80_80_80_80; let scaled = apply_intensity(c, 2.5);
let (r, g, b) = unpack_rgb(scaled);
assert_eq!(r, 255, "red clamped");
assert_eq!(g, 255, "green clamped");
assert_eq!(b, 255, "blue clamped");
}
#[test]
fn apply_intensity_preserves_brightness_byte() {
let c = 0x80_80_80_80;
let scaled = apply_intensity(c, 0.5);
assert_eq!((scaled >> 24) & 0xff, 0x80, "brightness preserved");
}
#[test]
fn blue_cave_color_top_skews_green() {
let perlin = PerlinNoise3D::new(0);
let c = blue_cave_color(0, 0, 0, &perlin);
let (r, g, b) = unpack_rgb(c);
assert_eq!(r, 0x60);
assert_eq!(g, 0x80);
assert_eq!(b, 0x60);
}
#[test]
fn blue_cave_color_bottom_skews_orange() {
let perlin = PerlinNoise3D::new(0);
let c = blue_cave_color(0, 0, MAXZDIM - 1, &perlin);
let (r, g, b) = unpack_rgb(c);
assert!(
(i32::from(r) - 0x60).abs() <= 2,
"R close to 0x60: got {r:#04x}"
);
assert!(
(i32::from(g) - 0x40).abs() <= 2,
"G close to 0x40: got {g:#04x}"
);
assert!(
(i32::from(b) - 0x30).abs() <= 2,
"B close to 0x30: got {b:#04x}"
);
}
#[test]
fn mag_default_params_match_plan() {
let p = MagCaveGenerator::default_params();
assert_eq!(p.seed_count, 128);
assert!((p.air_ratio - 0.4).abs() < 1e-6, "air_ratio");
assert!((p.anisotropy - 1.5).abs() < 1e-6, "anisotropy");
assert_eq!(p.perlin_octaves, 4);
assert!((p.perlin_amplitude - 0.25).abs() < 1e-6, "amplitude");
}
#[test]
fn mag_generate_byte_stable_in_seed() {
let p = CaveParams {
seed_count: 16,
..MagCaveGenerator::default_params()
};
let a = MagCaveGenerator.generate(&p, 16);
let b = MagCaveGenerator.generate(&p, 16);
assert_eq!(a.column_offset.as_ref(), b.column_offset.as_ref());
assert_eq!(a.data.as_ref(), b.data.as_ref());
}
#[test]
fn mag_generate_yields_mixed_air_and_solid() {
let p = CaveParams {
seed_count: 16,
..MagCaveGenerator::default_params()
};
let vxl = MagCaveGenerator.generate(&p, 16);
let mut total_runs = 0;
for idx in 0..(16 * 16) {
let mut b2 = vec![0i32; 256];
roxlap_formats::edit::expandrle(vxl.column_data(idx), &mut b2);
let mut i = 0;
while b2[i + 1] < MAXZDIM {
i += 2;
}
total_runs += (i + 2) / 2;
}
assert!(
total_runs > 256,
"expected multi-run columns from cave gen; got {total_runs} total runs"
);
}
#[test]
fn mag_far_from_boundary_skews_magenta() {
let seeds = vec![
Seed {
pos: [0.0, 0.0, 0.0],
is_air: false,
},
Seed {
pos: [100.0, 100.0, 100.0],
is_air: true,
},
];
let perlin = PerlinNoise3D::new(0);
let c = mag_cave_color(1, 0, 0, &perlin, &seeds, 1.0);
let (r, g, b) = unpack_rgb(c);
assert!(
(i32::from(r) - 0xa0).abs() <= 2,
"R magenta-ish: got {r:#04x}"
);
assert!(
(i32::from(g) - 0x40).abs() <= 2,
"G magenta-ish: got {g:#04x}"
);
assert!(
(i32::from(b) - 0xa0).abs() <= 2,
"B magenta-ish: got {b:#04x}"
);
}
#[test]
fn mag_at_boundary_skews_yellow_green() {
let seeds = vec![
Seed {
pos: [0.0, 0.0, 0.0],
is_air: false,
},
Seed {
pos: [2.0, 0.0, 0.0],
is_air: true,
},
];
let perlin = PerlinNoise3D::new(0);
let c = mag_cave_color(1, 0, 0, &perlin, &seeds, 1.0);
let (r, g, b) = unpack_rgb(c);
assert!(
(i32::from(r) - 0xb0).abs() <= 2,
"R yellow-green-ish: got {r:#04x}"
);
assert!(
(i32::from(g) - 0xb0).abs() <= 2,
"G yellow-green-ish: got {g:#04x}"
);
assert!(
(i32::from(b) - 0x20).abs() <= 2,
"B yellow-green-ish: got {b:#04x}"
);
}
#[test]
fn mag_and_blue_diverge_in_byte_output() {
let p = CaveParams {
seed_count: 16,
..BlueCaveGenerator::default_params()
};
let blue = BlueCaveGenerator.generate(&p, 16);
let q = CaveParams {
seed_count: 16,
..MagCaveGenerator::default_params()
};
let mag = MagCaveGenerator.generate(&q, 16);
let mut differing = 0;
for idx in 0..(16 * 16) {
if blue.column_data(idx) != mag.column_data(idx) {
differing += 1;
}
}
assert!(
differing > 0,
"Blue and Mag presets should produce different output"
);
}
}