pub mod gltf;
pub mod partition;
pub mod portal;
pub mod pvs;
pub use portal::BspPortalHint;
use lunar_bsp::level::BspBlob;
use lunar_math::Vec3;
use partition::{InputTriangle, build_bsp};
use portal::extract_portals;
use pvs::compute_pvs;
pub struct BspInputMesh {
pub vertices: Vec<Vec3>,
pub indices: Vec<u32>,
pub area_id: Option<u32>,
}
pub struct BspCompileConfig {
pub max_leaf_size: usize,
pub pvs_samples: usize,
pub pvs_skip_distance: f32,
}
impl Default for BspCompileConfig {
fn default() -> Self {
Self {
max_leaf_size: 16,
pvs_samples: 64,
pvs_skip_distance: 0.0,
}
}
}
pub fn compile_bsp(
meshes: &[BspInputMesh],
hints: &[BspPortalHint],
config: &BspCompileConfig,
) -> Result<Vec<u8>, String> {
let mut triangles: Vec<InputTriangle> = Vec::new();
let mut all_tris: Vec<[Vec3; 3]> = Vec::new();
for mesh in meshes {
let verts = &mesh.vertices;
let mut i = 0;
while i + 2 < mesh.indices.len() {
let i0 = mesh.indices[i] as usize;
let i1 = mesh.indices[i + 1] as usize;
let i2 = mesh.indices[i + 2] as usize;
if i0 >= verts.len() || i1 >= verts.len() || i2 >= verts.len() {
i += 3;
continue;
}
let orig_idx = triangles.len() as u32;
triangles.push(InputTriangle {
verts: [verts[i0], verts[i1], verts[i2]],
area_id: mesh.area_id,
original_index: orig_idx,
});
all_tris.push([verts[i0], verts[i1], verts[i2]]);
i += 3;
}
}
if triangles.is_empty() {
return Err("compile_bsp: no valid triangles in input meshes".into());
}
let partition = build_bsp(&triangles, config.max_leaf_size);
let skip_dist_sq = if config.pvs_skip_distance > 0.0 {
config.pvs_skip_distance * config.pvs_skip_distance
} else {
0.0
};
let pvs = compute_pvs(
&partition.leaf_aabbs,
&all_tris,
config.pvs_samples,
skip_dist_sq,
);
let portals = extract_portals(&partition.leaf_aabbs, &partition.leaf_areas, hints);
let mut area_map: Vec<(u32, u32)> = partition
.leaf_areas
.iter()
.enumerate()
.filter_map(|(leaf, area)| area.map(|a| (leaf as u32, a)))
.collect();
area_map.sort_unstable_by_key(|&(li, _)| li);
let blob = BspBlob {
nodes: partition.nodes,
leaf_triangles: partition.leaf_triangles,
pvs: pvs.data,
pvs_stride: pvs.stride,
leaf_count: partition.leaf_count,
portals,
area_map,
};
bincode::serialize(&blob).map_err(|error| format!("bsp serialize error: {error}"))
}
pub fn compile_bsp_file(path: &str) -> Result<Vec<u8>, String> {
compile_bsp_file_with_config(path, &[], &BspCompileConfig::default())
}
pub fn compile_bsp_file_with_config(
path: &str,
hints: &[BspPortalHint],
config: &BspCompileConfig,
) -> Result<Vec<u8>, String> {
let meshes = gltf::load_gltf_meshes(path)?;
compile_bsp(&meshes, hints, config)
}
#[cfg(test)]
mod tests {
use super::*;
use lunar_bsp::level::BspLevel;
fn unit_cube_mesh(area_id: Option<u32>) -> BspInputMesh {
let v = |x: f32, y: f32, z: f32| Vec3::new(x, y, z);
let verts = vec![
v(0.0, 0.0, 0.0),
v(1.0, 0.0, 0.0),
v(1.0, 1.0, 0.0),
v(0.0, 1.0, 0.0), v(0.0, 0.0, 1.0),
v(1.0, 0.0, 1.0),
v(1.0, 1.0, 1.0),
v(0.0, 1.0, 1.0), ];
let indices = vec![
0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 0, 4, 5, 0, 5, 1, 3, 2, 6, 3, 6, 7, 0, 3, 7, 0, 7, 4, 1, 5, 6, 1, 6, 2, ];
BspInputMesh {
vertices: verts,
indices,
area_id,
}
}
#[test]
fn compile_and_load_round_trip() {
let mesh = unit_cube_mesh(Some(0));
let blob = compile_bsp(&[mesh], &[], &BspCompileConfig::default()).unwrap();
let level = BspLevel::from_binary(&blob).unwrap();
assert!(level.is_loaded());
let leaf = level.camera_leaf(Vec3::new(0.5, 0.5, 0.5));
let visible = level.visible_leaves(leaf);
assert!(
!visible.is_empty(),
"camera leaf should see at least itself"
);
}
#[test]
fn two_area_portals_detected() {
let mesh_a = unit_cube_mesh(Some(0));
let mesh_b = BspInputMesh {
vertices: unit_cube_mesh(Some(1))
.vertices
.iter()
.map(|v| Vec3::new(v.x + 1.0, v.y, v.z))
.collect(),
indices: unit_cube_mesh(Some(1)).indices,
area_id: Some(1),
};
let blob = compile_bsp(
&[mesh_a, mesh_b],
&[],
&BspCompileConfig {
pvs_samples: 8,
..Default::default()
},
)
.unwrap();
let level = BspLevel::from_binary(&blob).unwrap();
assert!(level.is_loaded());
}
#[test]
fn empty_input_is_error() {
assert!(compile_bsp(&[], &[], &BspCompileConfig::default()).is_err());
}
#[test]
fn file_not_found_is_error() {
assert!(compile_bsp_file("does_not_exist.glb").is_err());
}
}