1pub mod gltf;
36pub mod partition;
37pub mod portal;
38pub mod pvs;
39
40pub use portal::BspPortalHint;
41
42use lunar_bsp::level::BspBlob;
43use lunar_math::Vec3;
44use partition::{InputTriangle, build_bsp};
45use portal::extract_portals;
46use pvs::compute_pvs;
47
48pub struct BspInputMesh {
53 pub vertices: Vec<Vec3>,
55 pub indices: Vec<u32>,
57 pub area_id: Option<u32>,
59}
60
61pub struct BspCompileConfig {
63 pub max_leaf_size: usize,
65 pub pvs_samples: usize,
67 pub pvs_skip_distance: f32,
70}
71
72impl Default for BspCompileConfig {
73 fn default() -> Self {
74 Self {
75 max_leaf_size: 16,
76 pvs_samples: 64,
77 pvs_skip_distance: 0.0,
78 }
79 }
80}
81
82pub fn compile_bsp(
91 meshes: &[BspInputMesh],
92 hints: &[BspPortalHint],
93 config: &BspCompileConfig,
94) -> Result<Vec<u8>, String> {
95 let mut triangles: Vec<InputTriangle> = Vec::new();
97 let mut all_tris: Vec<[Vec3; 3]> = Vec::new();
98
99 for mesh in meshes {
100 let verts = &mesh.vertices;
101 let mut i = 0;
102 while i + 2 < mesh.indices.len() {
103 let i0 = mesh.indices[i] as usize;
104 let i1 = mesh.indices[i + 1] as usize;
105 let i2 = mesh.indices[i + 2] as usize;
106 if i0 >= verts.len() || i1 >= verts.len() || i2 >= verts.len() {
107 i += 3;
108 continue;
109 }
110 let orig_idx = triangles.len() as u32;
111 triangles.push(InputTriangle {
112 verts: [verts[i0], verts[i1], verts[i2]],
113 area_id: mesh.area_id,
114 original_index: orig_idx,
115 });
116 all_tris.push([verts[i0], verts[i1], verts[i2]]);
117 i += 3;
118 }
119 }
120
121 if triangles.is_empty() {
122 return Err("compile_bsp: no valid triangles in input meshes".into());
123 }
124
125 let partition = build_bsp(&triangles, config.max_leaf_size);
127
128 let skip_dist_sq = if config.pvs_skip_distance > 0.0 {
130 config.pvs_skip_distance * config.pvs_skip_distance
131 } else {
132 0.0
133 };
134 let pvs = compute_pvs(
135 &partition.leaf_aabbs,
136 &all_tris,
137 config.pvs_samples,
138 skip_dist_sq,
139 );
140
141 let portals = extract_portals(&partition.leaf_aabbs, &partition.leaf_areas, hints);
143
144 let mut area_map: Vec<(u32, u32)> = partition
146 .leaf_areas
147 .iter()
148 .enumerate()
149 .filter_map(|(leaf, area)| area.map(|a| (leaf as u32, a)))
150 .collect();
151 area_map.sort_unstable_by_key(|&(li, _)| li);
152
153 let blob = BspBlob {
154 nodes: partition.nodes,
155 leaf_triangles: partition.leaf_triangles,
156 pvs: pvs.data,
157 pvs_stride: pvs.stride,
158 leaf_count: partition.leaf_count,
159 portals,
160 area_map,
161 };
162
163 bincode::serialize(&blob).map_err(|error| format!("bsp serialize error: {error}"))
164}
165
166pub fn compile_bsp_file(path: &str) -> Result<Vec<u8>, String> {
175 compile_bsp_file_with_config(path, &[], &BspCompileConfig::default())
176}
177
178pub fn compile_bsp_file_with_config(
184 path: &str,
185 hints: &[BspPortalHint],
186 config: &BspCompileConfig,
187) -> Result<Vec<u8>, String> {
188 let meshes = gltf::load_gltf_meshes(path)?;
189 compile_bsp(&meshes, hints, config)
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use lunar_bsp::level::BspLevel;
196
197 fn unit_cube_mesh(area_id: Option<u32>) -> BspInputMesh {
198 let v = |x: f32, y: f32, z: f32| Vec3::new(x, y, z);
200 let verts = vec![
201 v(0.0, 0.0, 0.0),
202 v(1.0, 0.0, 0.0),
203 v(1.0, 1.0, 0.0),
204 v(0.0, 1.0, 0.0), v(0.0, 0.0, 1.0),
206 v(1.0, 0.0, 1.0),
207 v(1.0, 1.0, 1.0),
208 v(0.0, 1.0, 1.0), ];
210 let indices = vec![
211 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, ];
218 BspInputMesh {
219 vertices: verts,
220 indices,
221 area_id,
222 }
223 }
224
225 #[test]
226 fn compile_and_load_round_trip() {
227 let mesh = unit_cube_mesh(Some(0));
228 let blob = compile_bsp(&[mesh], &[], &BspCompileConfig::default()).unwrap();
229 let level = BspLevel::from_binary(&blob).unwrap();
230 assert!(level.is_loaded());
231 let leaf = level.camera_leaf(Vec3::new(0.5, 0.5, 0.5));
232 let visible = level.visible_leaves(leaf);
233 assert!(
234 !visible.is_empty(),
235 "camera leaf should see at least itself"
236 );
237 }
238
239 #[test]
240 fn two_area_portals_detected() {
241 let mesh_a = unit_cube_mesh(Some(0));
243 let mesh_b = BspInputMesh {
244 vertices: unit_cube_mesh(Some(1))
245 .vertices
246 .iter()
247 .map(|v| Vec3::new(v.x + 1.0, v.y, v.z))
248 .collect(),
249 indices: unit_cube_mesh(Some(1)).indices,
250 area_id: Some(1),
251 };
252 let blob = compile_bsp(
253 &[mesh_a, mesh_b],
254 &[],
255 &BspCompileConfig {
256 pvs_samples: 8,
257 ..Default::default()
258 },
259 )
260 .unwrap();
261 let level = BspLevel::from_binary(&blob).unwrap();
262 assert!(level.is_loaded());
263 }
264
265 #[test]
266 fn empty_input_is_error() {
267 assert!(compile_bsp(&[], &[], &BspCompileConfig::default()).is_err());
268 }
269
270 #[test]
271 fn file_not_found_is_error() {
272 assert!(compile_bsp_file("does_not_exist.glb").is_err());
273 }
274}