1use std::ops::Range;
9use std::vec::Vec;
10
11use super::data::{BspWorld, Face, Leaf, Node, Plane};
12
13#[derive(Debug, Clone, Copy)]
15pub struct RoomSpec {
16 pub mins: [f32; 3],
17 pub maxs: [f32; 3],
18 pub floor_texture_id: u32,
19 pub ceiling_texture_id: u32,
20 pub lightmap_id: u16,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum BuildError {
27 NeedAtLeastOneRoom,
28 InvalidBounds,
29 RoomsNotSortedOrOverlappingX,
30 TooManyRooms,
31 TooManyNodes,
32 TooManyFaces,
33 TooManyMarksurfaces,
34}
35
36#[derive(Debug, Default)]
38pub struct OwnedBspWorld {
39 pub planes: Vec<Plane>,
40 pub nodes: Vec<Node>,
41 pub leaves: Vec<Leaf>,
42 pub faces: Vec<Face>,
43 pub marksurfaces: Vec<u16>,
44 pub vertices: Vec<[f32; 3]>,
45 pub uvs: Vec<[f32; 2]>,
46 pub lm_uvs: Vec<[f32; 2]>,
47 pub vis: Vec<u8>,
48 pub vis_offsets: Vec<u32>,
49 pub num_clusters: u16,
50}
51
52impl OwnedBspWorld {
53 pub fn as_world(&self) -> BspWorld<'_> {
55 BspWorld::new(
56 &self.planes,
57 &self.nodes,
58 &self.leaves,
59 &self.faces,
60 &self.marksurfaces,
61 &self.vertices,
62 &self.uvs,
63 &self.lm_uvs,
64 &self.vis,
65 &self.vis_offsets,
66 self.num_clusters,
67 )
68 }
69}
70
71#[inline]
72fn to_i16_clamped(v: f32) -> i16 {
73 v.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16
74}
75
76fn range_bounds_i16(rooms: &[RoomSpec], r: Range<usize>) -> ([i16; 3], [i16; 3]) {
77 let mut mins = [f32::INFINITY; 3];
78 let mut maxs = [f32::NEG_INFINITY; 3];
79 for room in &rooms[r] {
80 for a in 0..3 {
81 mins[a] = mins[a].min(room.mins[a]);
82 maxs[a] = maxs[a].max(room.maxs[a]);
83 }
84 }
85 (
86 [
87 to_i16_clamped(mins[0]),
88 to_i16_clamped(mins[1]),
89 to_i16_clamped(mins[2]),
90 ],
91 [
92 to_i16_clamped(maxs[0]),
93 to_i16_clamped(maxs[1]),
94 to_i16_clamped(maxs[2]),
95 ],
96 )
97}
98
99fn build_node_recursive(
100 rooms: &[RoomSpec],
101 range: Range<usize>,
102 planes: &mut Vec<Plane>,
103 nodes: &mut Vec<Node>,
104) -> Result<i32, BuildError> {
105 if range.end - range.start == 1 {
106 return Ok(!(range.start as i32));
107 }
108
109 let mid = (range.start + range.end) / 2;
110 let split_dist = (rooms[mid - 1].maxs[0] + rooms[mid].mins[0]) * 0.5;
111
112 let plane_index = planes.len();
113 if plane_index > u16::MAX as usize {
114 return Err(BuildError::TooManyNodes);
115 }
116 planes.push(Plane {
117 normal: [1.0, 0.0, 0.0],
118 dist: split_dist,
119 });
120
121 let node_index = nodes.len();
122 if node_index > i32::MAX as usize {
123 return Err(BuildError::TooManyNodes);
124 }
125 nodes.push(Node {
126 plane: plane_index as u16,
127 children: [0, 0],
128 mins: [0, 0, 0],
129 maxs: [0, 0, 0],
130 first_face: 0,
131 num_faces: 0,
132 });
133
134 let front = build_node_recursive(rooms, mid..range.end, planes, nodes)?;
136 let back = build_node_recursive(rooms, range.start..mid, planes, nodes)?;
137
138 let (mins, maxs) = range_bounds_i16(rooms, range);
139 let n = &mut nodes[node_index];
140 n.children = [front, back];
141 n.mins = mins;
142 n.maxs = maxs;
143
144 Ok(node_index as i32)
145}
146
147fn push_quad_face(
148 out: &mut OwnedBspWorld,
149 verts: [[f32; 3]; 4],
150 texture_id: u32,
151 lightmap_id: u16,
152) -> Result<u16, BuildError> {
153 let first_vert = out.vertices.len() as u32;
154 out.vertices.extend_from_slice(&verts);
155 const QUAD_UVS: [[f32; 2]; 4] = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
156 out.uvs.extend_from_slice(&QUAD_UVS);
157 out.lm_uvs.extend_from_slice(&QUAD_UVS);
158
159 let face_index = out.faces.len();
160 if face_index > u16::MAX as usize {
161 return Err(BuildError::TooManyFaces);
162 }
163 out.faces.push(Face {
164 first_vert,
165 num_verts: 4,
166 texture_id,
167 lightmap_id,
168 plane: 0,
169 side: 0,
170 sector_light_id: u16::MAX,
171 });
172 Ok(face_index as u16)
173}
174
175fn push_leaf_marksurface(out: &mut OwnedBspWorld, face_idx: u16) -> Result<(), BuildError> {
176 if out.marksurfaces.len() > u16::MAX as usize {
177 return Err(BuildError::TooManyMarksurfaces);
178 }
179 out.marksurfaces.push(face_idx);
180 Ok(())
181}
182
183pub fn build_room_strip(rooms: &[RoomSpec]) -> Result<OwnedBspWorld, BuildError> {
188 if rooms.is_empty() {
189 return Err(BuildError::NeedAtLeastOneRoom);
190 }
191 if rooms.len() > i16::MAX as usize {
192 return Err(BuildError::TooManyRooms);
193 }
194
195 for room in rooms {
196 if room.mins[0] >= room.maxs[0]
197 || room.mins[1] >= room.maxs[1]
198 || room.mins[2] >= room.maxs[2]
199 {
200 return Err(BuildError::InvalidBounds);
201 }
202 }
203
204 for i in 1..rooms.len() {
205 if rooms[i - 1].maxs[0] > rooms[i].mins[0] {
206 return Err(BuildError::RoomsNotSortedOrOverlappingX);
207 }
208 }
209
210 let mut out = OwnedBspWorld::default();
211 const EPS_X_GAP: f32 = 1e-4;
212
213 for (room_idx, room) in rooms.iter().enumerate() {
214 let first_marksurface = out.marksurfaces.len();
215 if first_marksurface > u16::MAX as usize {
216 return Err(BuildError::TooManyMarksurfaces);
217 }
218 let mut leaf_faces: Vec<u16> = Vec::with_capacity(6);
219
220 leaf_faces.push(push_quad_face(
222 &mut out,
223 [
224 [room.mins[0], room.mins[1], room.mins[2]],
225 [room.maxs[0], room.mins[1], room.mins[2]],
226 [room.maxs[0], room.mins[1], room.maxs[2]],
227 [room.mins[0], room.mins[1], room.maxs[2]],
228 ],
229 room.floor_texture_id,
230 room.lightmap_id,
231 )?);
232 leaf_faces.push(push_quad_face(
233 &mut out,
234 [
235 [room.mins[0], room.maxs[1], room.mins[2]],
236 [room.maxs[0], room.maxs[1], room.mins[2]],
237 [room.maxs[0], room.maxs[1], room.maxs[2]],
238 [room.mins[0], room.maxs[1], room.maxs[2]],
239 ],
240 room.ceiling_texture_id,
241 room.lightmap_id,
242 )?);
243
244 leaf_faces.push(push_quad_face(
246 &mut out,
247 [
248 [room.mins[0], room.mins[1], room.mins[2]],
249 [room.maxs[0], room.mins[1], room.mins[2]],
250 [room.maxs[0], room.maxs[1], room.mins[2]],
251 [room.mins[0], room.maxs[1], room.mins[2]],
252 ],
253 room.floor_texture_id,
254 room.lightmap_id,
255 )?);
256 leaf_faces.push(push_quad_face(
257 &mut out,
258 [
259 [room.mins[0], room.mins[1], room.maxs[2]],
260 [room.maxs[0], room.mins[1], room.maxs[2]],
261 [room.maxs[0], room.maxs[1], room.maxs[2]],
262 [room.mins[0], room.maxs[1], room.maxs[2]],
263 ],
264 room.floor_texture_id,
265 room.lightmap_id,
266 )?);
267
268 let has_left_cap = room_idx == 0 || rooms[room_idx - 1].maxs[0] < room.mins[0] - EPS_X_GAP;
271 if has_left_cap {
272 leaf_faces.push(push_quad_face(
273 &mut out,
274 [
275 [room.mins[0], room.mins[1], room.mins[2]],
276 [room.mins[0], room.mins[1], room.maxs[2]],
277 [room.mins[0], room.maxs[1], room.maxs[2]],
278 [room.mins[0], room.maxs[1], room.mins[2]],
279 ],
280 room.floor_texture_id,
281 room.lightmap_id,
282 )?);
283 }
284
285 let has_right_cap =
286 room_idx + 1 == rooms.len() || room.maxs[0] < rooms[room_idx + 1].mins[0] - EPS_X_GAP;
287 if has_right_cap {
288 leaf_faces.push(push_quad_face(
289 &mut out,
290 [
291 [room.maxs[0], room.mins[1], room.mins[2]],
292 [room.maxs[0], room.mins[1], room.maxs[2]],
293 [room.maxs[0], room.maxs[1], room.maxs[2]],
294 [room.maxs[0], room.maxs[1], room.mins[2]],
295 ],
296 room.floor_texture_id,
297 room.lightmap_id,
298 )?);
299 }
300
301 for face_idx in &leaf_faces {
302 push_leaf_marksurface(&mut out, *face_idx)?;
303 }
304
305 out.leaves.push(Leaf {
306 cluster: room_idx as i16,
307 mins: [
308 to_i16_clamped(room.mins[0]),
309 to_i16_clamped(room.mins[1]),
310 to_i16_clamped(room.mins[2]),
311 ],
312 maxs: [
313 to_i16_clamped(room.maxs[0]),
314 to_i16_clamped(room.maxs[1]),
315 to_i16_clamped(room.maxs[2]),
316 ],
317 first_marksurface: first_marksurface as u16,
318 num_marksurfaces: leaf_faces.len() as u16,
319 });
320 }
321
322 if rooms.len() > 1 {
324 let _root = build_node_recursive(rooms, 0..rooms.len(), &mut out.planes, &mut out.nodes)?;
325 }
326
327 out.num_clusters = rooms.len() as u16;
329 let clusters = out.num_clusters as usize;
330 let row_bytes = clusters.div_ceil(8);
331 out.vis_offsets.reserve(clusters);
332 out.vis.reserve(clusters * row_bytes);
333 for cluster in 0..clusters {
334 out.vis_offsets.push((cluster * row_bytes) as u32);
335 for byte_idx in 0..row_bytes {
336 let mut byte = 0u8;
337 for bit in 0..8 {
338 let target = byte_idx * 8 + bit;
339 if target < clusters {
340 byte |= 1 << bit;
341 }
342 }
343 out.vis.push(byte);
344 }
345 }
346
347 Ok(out)
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use crate::K3dengine;
354 use crate::bsp::scratch::BspScratch;
355 use crate::command_buffer::{CommandBuffer, RenderCommand};
356 use nalgebra::Point3;
357
358 #[test]
359 fn build_room_strip_basic_world_is_traversable() {
360 let rooms = [
361 RoomSpec {
362 mins: [-5.0, -2.0, -3.0],
363 maxs: [0.0, 2.0, 3.0],
364 floor_texture_id: 0,
365 ceiling_texture_id: 1,
366 lightmap_id: 0xFFFF,
367 },
368 RoomSpec {
369 mins: [0.0, -2.0, -3.0],
370 maxs: [5.0, 2.0, 3.0],
371 floor_texture_id: 0,
372 ceiling_texture_id: 1,
373 lightmap_id: 0xFFFF,
374 },
375 ];
376 let owned = build_room_strip(&rooms).expect("builder should succeed");
377 assert_eq!(owned.nodes.len(), 1);
378 assert_eq!(owned.leaves.len(), 2);
379 assert_eq!(owned.faces.len(), 10);
380 assert_eq!(owned.marksurfaces.len(), 10);
381
382 let world = owned.as_world();
383 assert_eq!(world.leaf_for_point([-2.0, 0.0, 0.0]), 0);
384 assert_eq!(world.leaf_for_point([2.0, 0.0, 0.0]), 1);
385
386 let mut visframe = [0u32; 16];
387 let mut scratch = BspScratch::new(&mut visframe);
388 let mut engine = K3dengine::new(320, 240);
389 engine.camera.set_position(Point3::new(-2.0, 0.0, 0.0));
390 engine.camera.set_target(Point3::new(0.0, 0.0, 0.0));
391 let mut commands: CommandBuffer<512> = CommandBuffer::new();
392
393 engine
394 .record_bsp(&world, &mut scratch, &mut commands, None)
395 .expect("record_bsp should succeed");
396 let draw_count = commands
397 .iter()
398 .filter(|c| matches!(c, RenderCommand::Draw(_)))
399 .count();
400 assert!(draw_count > 0);
401 }
402
403 #[test]
404 fn build_room_strip_rejects_unsorted_or_overlapping() {
405 let rooms = [
406 RoomSpec {
407 mins: [0.0, -2.0, -3.0],
408 maxs: [5.0, 2.0, 3.0],
409 floor_texture_id: 0,
410 ceiling_texture_id: 0,
411 lightmap_id: 0xFFFF,
412 },
413 RoomSpec {
414 mins: [-1.0, -2.0, -3.0],
415 maxs: [2.0, 2.0, 3.0],
416 floor_texture_id: 0,
417 ceiling_texture_id: 0,
418 lightmap_id: 0xFFFF,
419 },
420 ];
421 let err = build_room_strip(&rooms).expect_err("expected ordering failure");
422 assert_eq!(err, BuildError::RoomsNotSortedOrOverlappingX);
423 }
424
425 #[test]
426 fn build_room_strip_single_room_is_valid() {
427 let rooms = [RoomSpec {
428 mins: [-5.0, -2.0, -3.0],
429 maxs: [0.0, 2.0, 3.0],
430 floor_texture_id: 0,
431 ceiling_texture_id: 1,
432 lightmap_id: 0xFFFF,
433 }];
434 let owned = build_room_strip(&rooms).expect("single room should build");
435 assert!(owned.nodes.is_empty());
436 assert_eq!(owned.leaves.len(), 1);
437 assert_eq!(owned.faces.len(), 6);
438 assert_eq!(owned.marksurfaces.len(), 6);
439 }
440}