use glam::Vec3;
use crate::mesh::Mesh;
use crate::palette::{Palette, lerp};
use crate::sdf::{mesh_field, sd_ellipsoid, sd_round_cone, smin};
use super::model::{Corridor, Room};
const BLEND_K: f32 = 3.0;
const TUNNEL_R: f32 = 2.1;
const TUNNEL_Y: f32 = 1.7;
const SAMPLE_CAP: f64 = 4.0e6;
struct Blob {
c: Vec3,
r: Vec3,
}
struct Tube {
a: Vec3,
b: Vec3,
}
fn noise(p: Vec3) -> f32 {
let s = (p.x * 12.9898 + p.y * 78.233 + p.z * 37.719).sin() * 43758.545;
s - s.floor()
}
pub fn cavern_mesh(
rooms: &[Room],
corridors: &[Corridor],
pal: &Palette,
detail: f32,
include_ceiling: bool,
) -> Mesh {
if rooms.is_empty() {
return Mesh::new();
}
let ceil = rooms[0].max.y.max(3.0);
let blobs: Vec<Blob> = rooms
.iter()
.map(|rm| {
let c = rm.center();
let hx = (rm.max.x - rm.min.x) * 0.5;
let hz = (rm.max.z - rm.min.z) * 0.5;
Blob {
c: Vec3::new(c.x, 0.0, c.z),
r: Vec3::new(hx + 1.2, ceil * 0.95, hz + 1.2),
}
})
.collect();
let mut tubes: Vec<Tube> = Vec::new();
for cr in corridors {
for seg in cr.path.windows(2) {
let a = Vec3::new(seg[0].x, TUNNEL_Y, seg[0].z);
let b = Vec3::new(seg[1].x, TUNNEL_Y, seg[1].z);
tubes.push(Tube { a, b });
}
}
let air = |p: Vec3| -> f32 {
let mut d = f32::INFINITY;
for b in &blobs {
d = smin(d, sd_ellipsoid(p, b.c, b.r), BLEND_K);
}
for t in &tubes {
d = smin(d, sd_round_cone(p, t.a, t.b, TUNNEL_R, TUNNEL_R), BLEND_K);
}
d
};
let field = move |p: Vec3| -> f32 {
let cavity = air(p).max(-p.y); -cavity
};
let mut lo = Vec3::splat(f32::INFINITY);
let mut hi = Vec3::splat(f32::NEG_INFINITY);
for rm in rooms {
lo = lo.min(rm.min);
hi = hi.max(rm.max);
}
for cr in corridors {
for pt in &cr.path {
lo = lo.min(*pt);
hi = hi.max(*pt);
}
}
let pad = BLEND_K + 1.5;
lo.x -= pad;
lo.z -= pad;
hi.x += pad;
hi.z += pad;
lo.y = -1.0; hi.y = if include_ceiling { ceil + 2.0 } else { 2.6 };
let mut cell = (0.7 / detail.clamp(0.5, 2.0)).clamp(0.42, 1.2);
let est = |c: f32| -> f64 {
let d = (hi - lo) / c;
(d.x.ceil() as f64 + 2.0) * (d.y.ceil() as f64 + 2.0) * (d.z.ceil() as f64 + 2.0)
};
while est(cell) > SAMPLE_CAP && cell < 6.0 {
cell *= 1.2;
}
let rock_lo = pal.rock[0];
let rock_hi = pal.rock[1];
let tint = pal.terrain[2];
let color = move |p: Vec3| -> Vec3 {
let n = noise(p * 0.37);
let base = lerp(rock_lo, rock_hi, n);
let up = (p.y / ceil).clamp(0.0, 1.0);
let shaded = lerp(base * 0.62, base, 0.4 + up * 0.6);
lerp(shaded, tint, 0.12)
};
mesh_field(lo, hi, cell, &field, &color)
}
#[cfg(test)]
mod tests {
use super::super::model::DungeonModel;
use super::super::{merged_asset, overview_asset};
use crate::palette::by_name;
use crate::recipe::DungeonParams;
fn params(json: &str) -> DungeonParams {
serde_json::from_str(json).unwrap()
}
fn tri_count(asset: &crate::gltf::Asset) -> usize {
asset.parts.iter().map(|p| p.mesh.triangle_count()).sum()
}
#[test]
fn cavern_builds_organic_mesh_distinct_from_crypt() {
let cav_p = params(r#"{"kind":"dungeon","type":"cavern","size":"small","seed":3}"#);
let cav = DungeonModel::new(&cav_p, &by_name("fungal")).unwrap();
let cav_asset = merged_asset(&cav);
for p in &cav_asset.parts {
p.mesh.validate().unwrap();
}
let cav_tris = tri_count(&cav_asset);
assert!(cav_tris > 2000, "cavern too sparse: {cav_tris}");
let crypt_p = params(r#"{"kind":"dungeon","type":"crypt","size":"small","seed":3}"#);
let crypt = DungeonModel::new(&crypt_p, &by_name("necrotic")).unwrap();
let crypt_tris = tri_count(&merged_asset(&crypt));
assert_ne!(cav_tris, crypt_tris);
let again = tri_count(&merged_asset(
&DungeonModel::new(&cav_p, &by_name("fungal")).unwrap(),
));
assert_eq!(cav_tris, again);
let ov = overview_asset(&cav);
for p in &ov.parts {
p.mesh.validate().unwrap();
}
assert!(
tri_count(&ov) < cav_tris,
"overview should drop the ceiling dome"
);
}
}