Skip to main content

proof_engine/svogi/
atlas.rs

1use glam::{Vec3, Vec4, UVec3};
2use super::octree::{SparseVoxelOctree, VoxelData};
3
4/// A small 3D tile of voxel data.
5#[derive(Debug, Clone)]
6pub struct Brick {
7    pub position: UVec3,
8    pub data: Vec<Vec4>,
9}
10
11impl Brick {
12    pub fn new(position: UVec3, size: u32) -> Self {
13        let count = (size * size * size) as usize;
14        Self {
15            position,
16            data: vec![Vec4::ZERO; count],
17        }
18    }
19
20    pub fn volume(size: u32) -> usize {
21        (size * size * size) as usize
22    }
23}
24
25/// A 3D shelf for packing bricks.
26#[derive(Debug, Clone)]
27pub struct Shelf3D {
28    pub x: u32,
29    pub y: u32,
30    pub z: u32,
31    pub width: u32,
32    pub height: u32,
33    pub depth: u32,
34    pub used_width: u32,
35    pub used_height: u32,
36}
37
38impl Shelf3D {
39    pub fn new(x: u32, y: u32, z: u32, width: u32, height: u32, depth: u32) -> Self {
40        Self {
41            x, y, z, width, height, depth,
42            used_width: 0,
43            used_height: 0,
44        }
45    }
46
47    pub fn can_fit(&self, w: u32, h: u32, d: u32) -> bool {
48        self.used_width + w <= self.width && h <= self.height && d <= self.depth
49    }
50}
51
52/// 3D shelf-based packer for the atlas.
53#[derive(Debug, Clone)]
54pub struct BlockPacker {
55    pub shelves: Vec<Shelf3D>,
56    pub atlas_size: UVec3,
57    next_z: u32,
58    freed: Vec<(UVec3, UVec3)>,
59}
60
61impl BlockPacker {
62    pub fn new(atlas_size: UVec3) -> Self {
63        Self {
64            shelves: Vec::new(),
65            atlas_size,
66            next_z: 0,
67            freed: Vec::new(),
68        }
69    }
70
71    /// Allocate space for a brick of the given size in the atlas.
72    pub fn pack(&mut self, brick_size: UVec3) -> Option<UVec3> {
73        // Try freed blocks first
74        for i in 0..self.freed.len() {
75            let (pos, size) = self.freed[i];
76            if size.x >= brick_size.x && size.y >= brick_size.y && size.z >= brick_size.z {
77                self.freed.swap_remove(i);
78                return Some(pos);
79            }
80        }
81
82        // Try existing shelves
83        for shelf in &mut self.shelves {
84            if shelf.can_fit(brick_size.x, brick_size.y, brick_size.z) {
85                let pos = UVec3::new(shelf.x + shelf.used_width, shelf.y, shelf.z);
86                shelf.used_width += brick_size.x;
87                if brick_size.y > shelf.used_height {
88                    shelf.used_height = brick_size.y;
89                }
90                return Some(pos);
91            }
92        }
93
94        // Create new shelf layer
95        if self.next_z + brick_size.z <= self.atlas_size.z {
96            let shelf = Shelf3D::new(
97                0, 0, self.next_z,
98                self.atlas_size.x, brick_size.y, brick_size.z,
99            );
100            let pos = UVec3::new(0, 0, self.next_z);
101            self.next_z += brick_size.z;
102            let mut new_shelf = shelf;
103            new_shelf.used_width = brick_size.x;
104            new_shelf.used_height = brick_size.y;
105            self.shelves.push(new_shelf);
106            return Some(pos);
107        }
108
109        None // Atlas full
110    }
111
112    /// Free a region.
113    pub fn free(&mut self, position: UVec3, size: UVec3) {
114        self.freed.push((position, size));
115    }
116
117    pub fn utilization(&self) -> f32 {
118        let total = self.atlas_size.x * self.atlas_size.y * self.atlas_size.z;
119        if total == 0 {
120            return 0.0;
121        }
122        let mut used = 0u32;
123        for shelf in &self.shelves {
124            used += shelf.used_width * shelf.used_height * shelf.depth;
125        }
126        let freed_vol: u32 = self.freed.iter().map(|(_, s)| s.x * s.y * s.z).sum();
127        let actual_used = used.saturating_sub(freed_vol);
128        actual_used as f32 / total as f32
129    }
130}
131
132/// Atlas statistics.
133#[derive(Debug, Clone)]
134pub struct AtlasStats {
135    pub total_bricks: u32,
136    pub used_bricks: u32,
137    pub utilization: f32,
138}
139
140/// The 3D texture atlas for storing voxel bricks.
141#[derive(Debug, Clone)]
142pub struct BrickAtlas {
143    pub bricks: Vec<Brick>,
144    pub atlas_data: Vec<Vec4>,
145    pub atlas_size: UVec3,
146    pub brick_size: u32,
147    packer: BlockPacker,
148    brick_positions: Vec<UVec3>,
149}
150
151impl BrickAtlas {
152    pub fn new(atlas_size: UVec3, brick_size: u32) -> Self {
153        let total = (atlas_size.x * atlas_size.y * atlas_size.z) as usize;
154        Self {
155            bricks: Vec::new(),
156            atlas_data: vec![Vec4::ZERO; total],
157            atlas_size,
158            brick_size,
159            packer: BlockPacker::new(atlas_size),
160            brick_positions: Vec::new(),
161        }
162    }
163
164    /// Add a brick to the atlas. Returns the atlas position or None if full.
165    pub fn add_brick(&mut self, brick: Brick) -> Option<UVec3> {
166        let bs = UVec3::splat(self.brick_size);
167        let pos = self.packer.pack(bs)?;
168        self.upload_brick(&brick, pos);
169        self.brick_positions.push(pos);
170        self.bricks.push(brick);
171        Some(pos)
172    }
173
174    /// Copy brick data into the atlas at the given position.
175    pub fn upload_brick(&mut self, brick: &Brick, atlas_pos: UVec3) {
176        let bs = self.brick_size;
177        for z in 0..bs {
178            for y in 0..bs {
179                for x in 0..bs {
180                    let brick_idx = (z * bs * bs + y * bs + x) as usize;
181                    if brick_idx < brick.data.len() {
182                        let ax = atlas_pos.x + x;
183                        let ay = atlas_pos.y + y;
184                        let az = atlas_pos.z + z;
185                        if ax < self.atlas_size.x && ay < self.atlas_size.y && az < self.atlas_size.z {
186                            let atlas_idx = (az * self.atlas_size.y * self.atlas_size.x
187                                + ay * self.atlas_size.x + ax) as usize;
188                            if atlas_idx < self.atlas_data.len() {
189                                self.atlas_data[atlas_idx] = brick.data[brick_idx];
190                            }
191                        }
192                    }
193                }
194            }
195        }
196    }
197
198    /// Sample the atlas at a world position using trilinear interpolation with LOD.
199    pub fn sample_atlas(&self, world_pos: Vec3, lod: f32) -> Vec4 {
200        // Convert world pos to atlas texel coordinates
201        // Assume atlas covers [0, atlas_size] in normalized coordinates
202        let u = world_pos.x.fract() * self.atlas_size.x as f32;
203        let v = world_pos.y.fract() * self.atlas_size.y as f32;
204        let w = world_pos.z.fract() * self.atlas_size.z as f32;
205
206        // Apply LOD as a scale factor (coarser = skip texels)
207        let scale = 2.0f32.powf(lod);
208        let u = u / scale;
209        let v = v / scale;
210        let w = w / scale;
211
212        // Trilinear interpolation
213        let x0 = u.floor() as i32;
214        let y0 = v.floor() as i32;
215        let z0 = w.floor() as i32;
216        let fx = u - u.floor();
217        let fy = v - v.floor();
218        let fz = w - w.floor();
219
220        let sample = |x: i32, y: i32, z: i32| -> Vec4 {
221            let x = x.clamp(0, self.atlas_size.x as i32 - 1) as u32;
222            let y = y.clamp(0, self.atlas_size.y as i32 - 1) as u32;
223            let z = z.clamp(0, self.atlas_size.z as i32 - 1) as u32;
224            let idx = (z * self.atlas_size.y * self.atlas_size.x
225                + y * self.atlas_size.x + x) as usize;
226            if idx < self.atlas_data.len() {
227                self.atlas_data[idx]
228            } else {
229                Vec4::ZERO
230            }
231        };
232
233        let c000 = sample(x0, y0, z0);
234        let c100 = sample(x0 + 1, y0, z0);
235        let c010 = sample(x0, y0 + 1, z0);
236        let c110 = sample(x0 + 1, y0 + 1, z0);
237        let c001 = sample(x0, y0, z0 + 1);
238        let c101 = sample(x0 + 1, y0, z0 + 1);
239        let c011 = sample(x0, y0 + 1, z0 + 1);
240        let c111 = sample(x0 + 1, y0 + 1, z0 + 1);
241
242        let c00 = c000 * (1.0 - fx) + c100 * fx;
243        let c10 = c010 * (1.0 - fx) + c110 * fx;
244        let c01 = c001 * (1.0 - fx) + c101 * fx;
245        let c11 = c011 * (1.0 - fx) + c111 * fx;
246
247        let c0 = c00 * (1.0 - fy) + c10 * fy;
248        let c1 = c01 * (1.0 - fy) + c11 * fy;
249
250        c0 * (1.0 - fz) + c1 * fz
251    }
252
253    pub fn stats(&self) -> AtlasStats {
254        AtlasStats {
255            total_bricks: (self.atlas_size.x / self.brick_size)
256                * (self.atlas_size.y / self.brick_size)
257                * (self.atlas_size.z / self.brick_size),
258            used_bricks: self.bricks.len() as u32,
259            utilization: self.packer.utilization(),
260        }
261    }
262
263    /// Defragment the atlas by repacking all bricks contiguously.
264    pub fn defragment(&mut self) {
265        let old_bricks: Vec<Brick> = self.bricks.drain(..).collect();
266        let old_positions: Vec<UVec3> = self.brick_positions.drain(..).collect();
267
268        // Clear atlas
269        for v in &mut self.atlas_data {
270            *v = Vec4::ZERO;
271        }
272        self.packer = BlockPacker::new(self.atlas_size);
273
274        // Re-add all bricks
275        for brick in old_bricks {
276            self.add_brick(brick);
277        }
278    }
279
280    /// Remove a brick by index.
281    pub fn remove_brick(&mut self, index: usize) {
282        if index >= self.bricks.len() {
283            return;
284        }
285        let pos = self.brick_positions[index];
286        let bs = UVec3::splat(self.brick_size);
287
288        // Clear atlas region
289        for z in 0..self.brick_size {
290            for y in 0..self.brick_size {
291                for x in 0..self.brick_size {
292                    let ax = pos.x + x;
293                    let ay = pos.y + y;
294                    let az = pos.z + z;
295                    let idx = (az * self.atlas_size.y * self.atlas_size.x
296                        + ay * self.atlas_size.x + ax) as usize;
297                    if idx < self.atlas_data.len() {
298                        self.atlas_data[idx] = Vec4::ZERO;
299                    }
300                }
301            }
302        }
303
304        self.packer.free(pos, bs);
305        self.bricks.swap_remove(index);
306        self.brick_positions.swap_remove(index);
307    }
308}
309
310/// Indirection table mapping world voxel coordinates to atlas positions.
311#[derive(Debug, Clone)]
312pub struct IndirectionTable {
313    pub data: Vec<u32>,
314    pub resolution: UVec3,
315}
316
317impl IndirectionTable {
318    pub fn new(resolution: UVec3) -> Self {
319        let count = (resolution.x * resolution.y * resolution.z) as usize;
320        Self {
321            data: vec![u32::MAX; count],
322            resolution,
323        }
324    }
325
326    pub fn set(&mut self, x: u32, y: u32, z: u32, brick_index: u32) {
327        let idx = (z * self.resolution.y * self.resolution.x
328            + y * self.resolution.x + x) as usize;
329        if idx < self.data.len() {
330            self.data[idx] = brick_index;
331        }
332    }
333
334    pub fn get(&self, x: u32, y: u32, z: u32) -> Option<u32> {
335        let idx = (z * self.resolution.y * self.resolution.x
336            + y * self.resolution.x + x) as usize;
337        if idx < self.data.len() && self.data[idx] != u32::MAX {
338            Some(self.data[idx])
339        } else {
340            None
341        }
342    }
343}
344
345/// Build an indirection table from an octree and atlas.
346pub fn build_indirection(octree: &SparseVoxelOctree, atlas: &BrickAtlas) -> IndirectionTable {
347    let res = UVec3::splat(1u32 << octree.max_depth);
348    let mut table = IndirectionTable::new(res);
349
350    let world_size = octree.world_bounds.size();
351    let voxel_size = world_size / Vec3::new(res.x as f32, res.y as f32, res.z as f32);
352
353    for (brick_idx, brick) in atlas.bricks.iter().enumerate() {
354        let bx = brick.position.x;
355        let by = brick.position.y;
356        let bz = brick.position.z;
357        if bx < res.x && by < res.y && bz < res.z {
358            table.set(bx, by, bz, brick_idx as u32);
359        }
360    }
361
362    table
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    #[test]
370    fn test_brick_atlas_pack_and_upload() {
371        let mut atlas = BrickAtlas::new(UVec3::new(32, 32, 32), 4);
372
373        let mut brick = Brick::new(UVec3::new(0, 0, 0), 4);
374        brick.data[0] = Vec4::new(1.0, 0.0, 0.0, 1.0);
375
376        let pos = atlas.add_brick(brick);
377        assert!(pos.is_some());
378
379        let stats = atlas.stats();
380        assert_eq!(stats.used_bricks, 1);
381    }
382
383    #[test]
384    fn test_packer_utilization() {
385        let mut packer = BlockPacker::new(UVec3::new(16, 16, 16));
386        let pos1 = packer.pack(UVec3::new(4, 4, 4));
387        assert!(pos1.is_some());
388
389        let pos2 = packer.pack(UVec3::new(4, 4, 4));
390        assert!(pos2.is_some());
391        assert_ne!(pos1, pos2);
392
393        let util = packer.utilization();
394        assert!(util > 0.0);
395    }
396
397    #[test]
398    fn test_packer_free_reuse() {
399        let mut packer = BlockPacker::new(UVec3::new(16, 16, 16));
400        let pos1 = packer.pack(UVec3::new(4, 4, 4)).unwrap();
401        packer.free(pos1, UVec3::new(4, 4, 4));
402        let pos2 = packer.pack(UVec3::new(4, 4, 4)).unwrap();
403        assert_eq!(pos1, pos2); // Should reuse freed space
404    }
405
406    #[test]
407    fn test_indirection_table() {
408        let mut table = IndirectionTable::new(UVec3::new(4, 4, 4));
409        assert!(table.get(0, 0, 0).is_none());
410        table.set(1, 2, 3, 42);
411        assert_eq!(table.get(1, 2, 3), Some(42));
412    }
413
414    #[test]
415    fn test_atlas_defragment() {
416        let mut atlas = BrickAtlas::new(UVec3::new(32, 32, 32), 4);
417
418        for i in 0..4 {
419            let brick = Brick::new(UVec3::new(i, 0, 0), 4);
420            atlas.add_brick(brick);
421        }
422        atlas.remove_brick(1);
423        assert_eq!(atlas.bricks.len(), 3);
424
425        atlas.defragment();
426        assert_eq!(atlas.bricks.len(), 3);
427    }
428
429    #[test]
430    fn test_atlas_sample() {
431        let mut atlas = BrickAtlas::new(UVec3::new(8, 8, 8), 4);
432
433        // Fill a region with known color
434        let mut brick = Brick::new(UVec3::ZERO, 4);
435        for v in &mut brick.data {
436            *v = Vec4::new(0.5, 0.5, 0.5, 1.0);
437        }
438        atlas.add_brick(brick);
439
440        // Sample should return something non-zero in that region
441        let sample = atlas.sample_atlas(Vec3::new(0.05, 0.05, 0.05), 0.0);
442        // Just verify it doesn't panic and returns a value
443        let _ = sample;
444    }
445
446    #[test]
447    fn test_atlas_full() {
448        let mut atlas = BrickAtlas::new(UVec3::new(4, 4, 4), 4);
449        let brick = Brick::new(UVec3::ZERO, 4);
450        let pos1 = atlas.add_brick(brick.clone());
451        assert!(pos1.is_some());
452        // Atlas is exactly one brick, second should fail
453        let pos2 = atlas.add_brick(brick);
454        assert!(pos2.is_none());
455    }
456}