1use glam::{Vec3, Vec4, IVec3, UVec3};
2use super::octree::{Aabb, VoxelData, VoxelGrid};
3
4#[derive(Debug, Clone)]
6pub struct VoxelizeConfig {
7 pub resolution: u32,
8 pub world_bounds: Aabb,
9 pub conservative: bool,
10}
11
12#[derive(Debug, Clone, Copy)]
14pub struct Triangle {
15 pub v0: Vec3,
16 pub v1: Vec3,
17 pub v2: Vec3,
18 pub normal: Vec3,
19 pub color: Vec4,
20 pub emission: Vec4,
21}
22
23impl Triangle {
24 pub fn aabb(&self) -> Aabb {
25 Aabb {
26 min: self.v0.min(self.v1).min(self.v2),
27 max: self.v0.max(self.v1).max(self.v2),
28 }
29 }
30
31 pub fn compute_normal(&self) -> Vec3 {
32 let e1 = self.v1 - self.v0;
33 let e2 = self.v2 - self.v0;
34 e1.cross(e2).normalize_or_zero()
35 }
36}
37
38#[derive(Debug, Clone, Default)]
40pub struct VoxelizeStats {
41 pub triangles_processed: u32,
42 pub voxels_written: u32,
43 pub fill_ratio: f32,
44}
45
46#[derive(Debug, Clone, Copy)]
48pub struct GpuVoxelizeParams {
49 pub resolution: u32,
50 pub world_min: Vec3,
51 pub world_max: Vec3,
52 pub proj_x: glam::Mat4,
53 pub proj_y: glam::Mat4,
54 pub proj_z: glam::Mat4,
55}
56
57impl GpuVoxelizeParams {
58 pub fn from_config(config: &VoxelizeConfig) -> Self {
59 let size = config.world_bounds.size();
60 let center = config.world_bounds.center();
61 let half = size * 0.5;
62
63 let proj_x = glam::Mat4::orthographic_rh(
65 -half.y, half.y, -half.z, half.z, 0.0, size.x,
66 );
67 let proj_y = glam::Mat4::orthographic_rh(
68 -half.x, half.x, -half.z, half.z, 0.0, size.y,
69 );
70 let proj_z = glam::Mat4::orthographic_rh(
71 -half.x, half.x, -half.y, half.y, 0.0, size.z,
72 );
73
74 Self {
75 resolution: config.resolution,
76 world_min: config.world_bounds.min,
77 world_max: config.world_bounds.max,
78 proj_x,
79 proj_y,
80 proj_z,
81 }
82 }
83}
84
85pub fn voxelize_triangles(triangles: &[Triangle], config: &VoxelizeConfig) -> VoxelGrid {
87 let res = config.resolution;
88 let mut grid = VoxelGrid::new(UVec3::new(res, res, res));
89 let world_size = config.world_bounds.size();
90 let voxel_size = world_size / Vec3::splat(res as f32);
91 let half_voxel = voxel_size * 0.5;
92
93 for tri in triangles {
94 if config.conservative {
95 let voxels = conservative_voxelize(tri, config);
96 for iv in voxels {
97 if grid.in_bounds(iv.x, iv.y, iv.z) {
98 let vd = grid.get_mut(iv.x as u32, iv.y as u32, iv.z as u32);
99 vd.radiance = tri.color + tri.emission;
100 vd.normal = tri.normal;
101 vd.opacity = tri.color.w;
102 }
103 }
104 } else {
105 let tri_aabb = tri.aabb();
106
107 let vmin = ((tri_aabb.min - config.world_bounds.min) / voxel_size).floor();
109 let vmax = ((tri_aabb.max - config.world_bounds.min) / voxel_size).ceil();
110
111 let ix_min = (vmin.x as i32).max(0);
112 let iy_min = (vmin.y as i32).max(0);
113 let iz_min = (vmin.z as i32).max(0);
114 let ix_max = (vmax.x as i32).min(res as i32 - 1);
115 let iy_max = (vmax.y as i32).min(res as i32 - 1);
116 let iz_max = (vmax.z as i32).min(res as i32 - 1);
117
118 for z in iz_min..=iz_max {
119 for y in iy_min..=iy_max {
120 for x in ix_min..=ix_max {
121 let box_center = config.world_bounds.min + Vec3::new(
122 (x as f32 + 0.5) * voxel_size.x,
123 (y as f32 + 0.5) * voxel_size.y,
124 (z as f32 + 0.5) * voxel_size.z,
125 );
126 if triangle_box_intersection(tri, box_center, half_voxel) {
127 let vd = grid.get_mut(x as u32, y as u32, z as u32);
128 vd.radiance = tri.color + tri.emission;
129 vd.normal = tri.normal;
130 vd.opacity = tri.color.w;
131 }
132 }
133 }
134 }
135 }
136 }
137
138 grid
139}
140
141pub fn triangle_box_intersection(tri: &Triangle, box_center: Vec3, box_half: Vec3) -> bool {
143 let v0 = tri.v0 - box_center;
145 let v1 = tri.v1 - box_center;
146 let v2 = tri.v2 - box_center;
147
148 let e0 = v1 - v0;
149 let e1 = v2 - v1;
150 let e2 = v0 - v2;
151
152 let h = box_half;
153
154 let axes = [
156 Vec3::new(0.0, -e0.z, e0.y),
157 Vec3::new(0.0, -e1.z, e1.y),
158 Vec3::new(0.0, -e2.z, e2.y),
159 Vec3::new(e0.z, 0.0, -e0.x),
160 Vec3::new(e1.z, 0.0, -e1.x),
161 Vec3::new(e2.z, 0.0, -e2.x),
162 Vec3::new(-e0.y, e0.x, 0.0),
163 Vec3::new(-e1.y, e1.x, 0.0),
164 Vec3::new(-e2.y, e2.x, 0.0),
165 ];
166
167 for axis in &axes {
168 let p0 = v0.dot(*axis);
169 let p1 = v1.dot(*axis);
170 let p2 = v2.dot(*axis);
171 let r = h.x * axis.x.abs() + h.y * axis.y.abs() + h.z * axis.z.abs();
172 let min_p = p0.min(p1).min(p2);
173 let max_p = p0.max(p1).max(p2);
174 if min_p > r || max_p < -r {
175 return false;
176 }
177 }
178
179 if v0.x.min(v1.x).min(v2.x) > h.x || v0.x.max(v1.x).max(v2.x) < -h.x {
181 return false;
182 }
183 if v0.y.min(v1.y).min(v2.y) > h.y || v0.y.max(v1.y).max(v2.y) < -h.y {
184 return false;
185 }
186 if v0.z.min(v1.z).min(v2.z) > h.z || v0.z.max(v1.z).max(v2.z) < -h.z {
187 return false;
188 }
189
190 let tri_normal = e0.cross(e1);
192 let d = tri_normal.dot(v0);
193 let r = h.x * tri_normal.x.abs() + h.y * tri_normal.y.abs() + h.z * tri_normal.z.abs();
194 if d.abs() > r {
195 return false;
196 }
197
198 true
199}
200
201pub fn conservative_voxelize(tri: &Triangle, config: &VoxelizeConfig) -> Vec<IVec3> {
203 let res = config.resolution;
204 let world_size = config.world_bounds.size();
205 let voxel_size = world_size / Vec3::splat(res as f32);
206 let half_voxel = voxel_size * 0.5;
207 let tri_aabb = tri.aabb();
209 let expanded_min = tri_aabb.min - voxel_size;
210 let expanded_max = tri_aabb.max + voxel_size;
211
212 let vmin = ((expanded_min - config.world_bounds.min) / voxel_size).floor();
213 let vmax = ((expanded_max - config.world_bounds.min) / voxel_size).ceil();
214
215 let ix_min = (vmin.x as i32).max(0);
216 let iy_min = (vmin.y as i32).max(0);
217 let iz_min = (vmin.z as i32).max(0);
218 let ix_max = (vmax.x as i32).min(res as i32 - 1);
219 let iy_max = (vmax.y as i32).min(res as i32 - 1);
220 let iz_max = (vmax.z as i32).min(res as i32 - 1);
221
222 let mut result = Vec::new();
223 for z in iz_min..=iz_max {
224 for y in iy_min..=iy_max {
225 for x in ix_min..=ix_max {
226 let box_center = config.world_bounds.min + Vec3::new(
227 (x as f32 + 0.5) * voxel_size.x,
228 (y as f32 + 0.5) * voxel_size.y,
229 (z as f32 + 0.5) * voxel_size.z,
230 );
231 let expanded_half = half_voxel * 1.5;
233 if triangle_box_intersection(tri, box_center, expanded_half) {
234 result.push(IVec3::new(x, y, z));
235 }
236 }
237 }
238 }
239 result
240}
241
242pub fn voxelize_spheres(spheres: &[(Vec3, f32, Vec4)], config: &VoxelizeConfig) -> VoxelGrid {
244 let res = config.resolution;
245 let mut grid = VoxelGrid::new(UVec3::new(res, res, res));
246 let world_size = config.world_bounds.size();
247 let voxel_size = world_size / Vec3::splat(res as f32);
248
249 for &(center, radius, color) in spheres {
250 let vmin = ((center - Vec3::splat(radius) - config.world_bounds.min) / voxel_size).floor();
251 let vmax = ((center + Vec3::splat(radius) - config.world_bounds.min) / voxel_size).ceil();
252
253 let ix_min = (vmin.x as i32).max(0);
254 let iy_min = (vmin.y as i32).max(0);
255 let iz_min = (vmin.z as i32).max(0);
256 let ix_max = (vmax.x as i32).min(res as i32 - 1);
257 let iy_max = (vmax.y as i32).min(res as i32 - 1);
258 let iz_max = (vmax.z as i32).min(res as i32 - 1);
259
260 for z in iz_min..=iz_max {
261 for y in iy_min..=iy_max {
262 for x in ix_min..=ix_max {
263 let world_pos = config.world_bounds.min + Vec3::new(
264 (x as f32 + 0.5) * voxel_size.x,
265 (y as f32 + 0.5) * voxel_size.y,
266 (z as f32 + 0.5) * voxel_size.z,
267 );
268 let dist = (world_pos - center).length();
269 if dist <= radius {
270 let normal = if dist > 1e-6 {
271 (world_pos - center).normalize()
272 } else {
273 Vec3::Y
274 };
275 let vd = grid.get_mut(x as u32, y as u32, z as u32);
276 vd.radiance = color;
277 vd.normal = normal;
278 vd.opacity = color.w;
279 }
280 }
281 }
282 }
283 }
284 grid
285}
286
287pub fn voxelize_point_cloud(points: &[(Vec3, Vec4)], config: &VoxelizeConfig) -> VoxelGrid {
289 let res = config.resolution;
290 let mut grid = VoxelGrid::new(UVec3::new(res, res, res));
291 let world_size = config.world_bounds.size();
292 let voxel_size = world_size / Vec3::splat(res as f32);
293
294 for &(pos, color) in points {
295 if !config.world_bounds.contains(pos) {
296 continue;
297 }
298 let voxel_pos = ((pos - config.world_bounds.min) / voxel_size).floor();
299 let x = (voxel_pos.x as u32).min(res - 1);
300 let y = (voxel_pos.y as u32).min(res - 1);
301 let z = (voxel_pos.z as u32).min(res - 1);
302
303 let vd = grid.get_mut(x, y, z);
304 vd.radiance = color;
305 vd.normal = Vec3::Y; vd.opacity = color.w;
307 }
308 grid
309}
310
311pub fn merge_grids(a: &VoxelGrid, b: &VoxelGrid) -> VoxelGrid {
313 assert_eq!(a.resolution, b.resolution, "Grids must have same resolution");
314 let mut result = a.clone();
315 for i in 0..result.data.len() {
316 if !b.data[i].is_empty() {
317 if result.data[i].is_empty() {
318 result.data[i] = b.data[i];
319 } else {
320 result.data[i].radiance = (result.data[i].radiance + b.data[i].radiance) * 0.5;
322 result.data[i].normal = (result.data[i].normal + b.data[i].normal).normalize_or_zero();
323 result.data[i].opacity = (result.data[i].opacity + b.data[i].opacity) * 0.5;
324 }
325 }
326 }
327 result
328}
329
330pub fn compute_voxelize_stats(grid: &VoxelGrid, triangles_processed: u32) -> VoxelizeStats {
331 let total = (grid.resolution.x * grid.resolution.y * grid.resolution.z) as u32;
332 let filled = grid.filled_count() as u32;
333 VoxelizeStats {
334 triangles_processed,
335 voxels_written: filled,
336 fill_ratio: if total > 0 { filled as f32 / total as f32 } else { 0.0 },
337 }
338}
339
340pub const VOXELIZE_COMP_SRC: &str = r#"
342#version 450
343layout(local_size_x = 64) in;
344
345struct Triangle {
346 vec4 v0;
347 vec4 v1;
348 vec4 v2;
349 vec4 normal;
350 vec4 color;
351 vec4 emission;
352};
353
354layout(std430, binding = 0) readonly buffer TriangleBuffer {
355 Triangle triangles[];
356};
357
358layout(r32ui, binding = 1) uniform uimage3D voxelGrid;
359
360uniform mat4 projX;
361uniform mat4 projY;
362uniform mat4 projZ;
363uniform uint resolution;
364uniform vec3 worldMin;
365uniform vec3 worldSize;
366
367void main() {
368 uint triIdx = gl_GlobalInvocationID.x;
369 if (triIdx >= triangles.length()) return;
370
371 Triangle tri = triangles[triIdx];
372 vec3 v0 = tri.v0.xyz;
373 vec3 v1 = tri.v1.xyz;
374 vec3 v2 = tri.v2.xyz;
375
376 // Choose dominant axis for projection (conservative rasterization)
377 vec3 n = abs(tri.normal.xyz);
378 mat4 proj;
379 if (n.x >= n.y && n.x >= n.z) {
380 proj = projX;
381 } else if (n.y >= n.z) {
382 proj = projY;
383 } else {
384 proj = projZ;
385 }
386
387 // Compute AABB in voxel space
388 vec3 voxelSize = worldSize / float(resolution);
389 vec3 minV = min(min(v0, v1), v2);
390 vec3 maxV = max(max(v0, v1), v2);
391 // Conservative expansion
392 minV -= voxelSize;
393 maxV += voxelSize;
394
395 ivec3 iMin = ivec3(clamp((minV - worldMin) / voxelSize, vec3(0), vec3(resolution - 1)));
396 ivec3 iMax = ivec3(clamp((maxV - worldMin) / voxelSize, vec3(0), vec3(resolution - 1)));
397
398 for (int z = iMin.z; z <= iMax.z; z++) {
399 for (int y = iMin.y; y <= iMax.y; y++) {
400 for (int x = iMin.x; x <= iMax.x; x++) {
401 // Pack color into uint for atomic write
402 uint packed = (uint(tri.color.r * 255.0) << 24) |
403 (uint(tri.color.g * 255.0) << 16) |
404 (uint(tri.color.b * 255.0) << 8) |
405 (uint(tri.color.a * 255.0));
406 imageAtomicMax(voxelGrid, ivec3(x, y, z), packed);
407 }
408 }
409 }
410}
411"#;
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416
417 fn make_config(res: u32) -> VoxelizeConfig {
418 VoxelizeConfig {
419 resolution: res,
420 world_bounds: Aabb::new(Vec3::ZERO, Vec3::splat(res as f32)),
421 conservative: false,
422 }
423 }
424
425 #[test]
426 fn test_single_triangle_voxelizes() {
427 let config = make_config(8);
428 let tri = Triangle {
429 v0: Vec3::new(2.0, 2.0, 4.0),
430 v1: Vec3::new(6.0, 2.0, 4.0),
431 v2: Vec3::new(4.0, 6.0, 4.0),
432 normal: Vec3::Z,
433 color: Vec4::new(1.0, 0.0, 0.0, 1.0),
434 emission: Vec4::ZERO,
435 };
436
437 let grid = voxelize_triangles(&[tri], &config);
438 let filled = grid.filled_count();
439 assert!(filled > 0, "Triangle should produce at least one voxel, got {filled}");
440 }
441
442 #[test]
443 fn test_triangle_box_intersection_basic() {
444 let tri = Triangle {
445 v0: Vec3::new(-1.0, 0.0, 0.0),
446 v1: Vec3::new(1.0, 0.0, 0.0),
447 v2: Vec3::new(0.0, 1.0, 0.0),
448 normal: Vec3::Z,
449 color: Vec4::ONE,
450 emission: Vec4::ZERO,
451 };
452 assert!(triangle_box_intersection(&tri, Vec3::ZERO, Vec3::splat(0.5)));
454 assert!(!triangle_box_intersection(&tri, Vec3::splat(10.0), Vec3::splat(0.5)));
456 }
457
458 #[test]
459 fn test_sphere_voxelization() {
460 let config = make_config(16);
461 let spheres = vec![(Vec3::splat(8.0), 3.0, Vec4::new(0.0, 1.0, 0.0, 1.0))];
462 let grid = voxelize_spheres(&spheres, &config);
463 let filled = grid.filled_count();
464 assert!(filled > 10, "Sphere should fill many voxels, got {filled}");
465
466 let center_voxel = grid.get(8, 8, 8);
468 assert!(!center_voxel.is_empty());
469 }
470
471 #[test]
472 fn test_point_cloud_voxelization() {
473 let config = make_config(8);
474 let points = vec![
475 (Vec3::new(1.0, 1.0, 1.0), Vec4::new(1.0, 0.0, 0.0, 1.0)),
476 (Vec3::new(5.0, 5.0, 5.0), Vec4::new(0.0, 1.0, 0.0, 1.0)),
477 ];
478 let grid = voxelize_point_cloud(&points, &config);
479 assert_eq!(grid.filled_count(), 2);
480 }
481
482 #[test]
483 fn test_empty_scene_empty_grid() {
484 let config = make_config(4);
485 let grid = voxelize_triangles(&[], &config);
486 assert_eq!(grid.filled_count(), 0);
487 }
488
489 #[test]
490 fn test_conservative_voxelize() {
491 let config = VoxelizeConfig {
492 resolution: 8,
493 world_bounds: Aabb::new(Vec3::ZERO, Vec3::splat(8.0)),
494 conservative: true,
495 };
496 let tri = Triangle {
497 v0: Vec3::new(3.0, 3.0, 4.0),
498 v1: Vec3::new(5.0, 3.0, 4.0),
499 v2: Vec3::new(4.0, 5.0, 4.0),
500 normal: Vec3::Z,
501 color: Vec4::ONE,
502 emission: Vec4::ZERO,
503 };
504 let grid = voxelize_triangles(&[tri], &config);
505 let non_conservative_config = VoxelizeConfig {
506 conservative: false,
507 ..config.clone()
508 };
509 let grid_nc = voxelize_triangles(&[tri], &non_conservative_config);
510 assert!(grid.filled_count() >= grid_nc.filled_count());
512 }
513
514 #[test]
515 fn test_merge_grids() {
516 let mut a = VoxelGrid::new(UVec3::new(4, 4, 4));
517 let mut b = VoxelGrid::new(UVec3::new(4, 4, 4));
518 a.set(0, 0, 0, VoxelData {
519 radiance: Vec4::new(1.0, 0.0, 0.0, 1.0),
520 normal: Vec3::Y,
521 opacity: 1.0,
522 sh_coeffs: [0.0; 9],
523 });
524 b.set(1, 1, 1, VoxelData {
525 radiance: Vec4::new(0.0, 0.0, 1.0, 1.0),
526 normal: Vec3::X,
527 opacity: 1.0,
528 sh_coeffs: [0.0; 9],
529 });
530 let merged = merge_grids(&a, &b);
531 assert_eq!(merged.filled_count(), 2);
532 }
533
534 #[test]
535 fn test_voxelize_stats() {
536 let config = make_config(4);
537 let tri = Triangle {
538 v0: Vec3::new(0.5, 0.5, 2.0),
539 v1: Vec3::new(3.5, 0.5, 2.0),
540 v2: Vec3::new(2.0, 3.5, 2.0),
541 normal: Vec3::Z,
542 color: Vec4::ONE,
543 emission: Vec4::ZERO,
544 };
545 let grid = voxelize_triangles(&[tri], &config);
546 let stats = compute_voxelize_stats(&grid, 1);
547 assert_eq!(stats.triangles_processed, 1);
548 assert!(stats.voxels_written > 0);
549 assert!(stats.fill_ratio > 0.0);
550 assert!(stats.fill_ratio <= 1.0);
551 }
552
553 #[test]
554 fn test_gpu_params() {
555 let config = make_config(64);
556 let params = GpuVoxelizeParams::from_config(&config);
557 assert_eq!(params.resolution, 64);
558 }
559
560 #[test]
561 fn test_triangle_aabb() {
562 let tri = Triangle {
563 v0: Vec3::new(1.0, 2.0, 3.0),
564 v1: Vec3::new(4.0, 1.0, 2.0),
565 v2: Vec3::new(2.0, 5.0, 1.0),
566 normal: Vec3::Z,
567 color: Vec4::ONE,
568 emission: Vec4::ZERO,
569 };
570 let aabb = tri.aabb();
571 assert!((aabb.min.x - 1.0).abs() < 1e-5);
572 assert!((aabb.max.x - 4.0).abs() < 1e-5);
573 assert!((aabb.min.y - 1.0).abs() < 1e-5);
574 assert!((aabb.max.y - 5.0).abs() < 1e-5);
575 }
576
577 #[test]
578 fn test_triangle_compute_normal() {
579 let tri = Triangle {
580 v0: Vec3::new(0.0, 0.0, 0.0),
581 v1: Vec3::new(1.0, 0.0, 0.0),
582 v2: Vec3::new(0.0, 1.0, 0.0),
583 normal: Vec3::Z,
584 color: Vec4::ONE,
585 emission: Vec4::ZERO,
586 };
587 let n = tri.compute_normal();
588 assert!((n.z - 1.0).abs() < 1e-5 || (n.z + 1.0).abs() < 1e-5);
589 }
590
591 #[test]
592 fn test_multiple_triangles() {
593 let config = make_config(16);
594 let tris = vec![
595 Triangle {
596 v0: Vec3::new(1.0, 1.0, 8.0),
597 v1: Vec3::new(5.0, 1.0, 8.0),
598 v2: Vec3::new(3.0, 5.0, 8.0),
599 normal: Vec3::Z,
600 color: Vec4::new(1.0, 0.0, 0.0, 1.0),
601 emission: Vec4::ZERO,
602 },
603 Triangle {
604 v0: Vec3::new(8.0, 8.0, 8.0),
605 v1: Vec3::new(12.0, 8.0, 8.0),
606 v2: Vec3::new(10.0, 12.0, 8.0),
607 normal: Vec3::Z,
608 color: Vec4::new(0.0, 0.0, 1.0, 1.0),
609 emission: Vec4::ZERO,
610 },
611 ];
612 let grid = voxelize_triangles(&tris, &config);
613 assert!(grid.filled_count() > 2, "Two triangles should voxelize to more than 2 voxels");
614 }
615
616 #[test]
617 fn test_sphere_at_boundary() {
618 let config = make_config(8);
619 let spheres = vec![(Vec3::ZERO, 1.0, Vec4::ONE)];
620 let grid = voxelize_spheres(&spheres, &config);
621 let count = grid.filled_count();
623 assert!(count > 0, "Sphere at boundary should produce some voxels");
624 }
625
626 #[test]
627 fn test_point_cloud_outside_bounds() {
628 let config = make_config(4);
629 let points = vec![
630 (Vec3::splat(-10.0), Vec4::ONE), (Vec3::splat(2.0), Vec4::ONE), ];
633 let grid = voxelize_point_cloud(&points, &config);
634 assert_eq!(grid.filled_count(), 1, "Only the in-bounds point should be voxelized");
635 }
636
637 #[test]
638 fn test_merge_overlapping_grids() {
639 let mut a = VoxelGrid::new(UVec3::new(4, 4, 4));
640 let mut b = VoxelGrid::new(UVec3::new(4, 4, 4));
641 a.set(1, 1, 1, VoxelData {
643 radiance: Vec4::new(1.0, 0.0, 0.0, 1.0),
644 normal: Vec3::Y,
645 opacity: 1.0,
646 sh_coeffs: [0.0; 9],
647 });
648 b.set(1, 1, 1, VoxelData {
649 radiance: Vec4::new(0.0, 0.0, 1.0, 1.0),
650 normal: Vec3::X,
651 opacity: 1.0,
652 sh_coeffs: [0.0; 9],
653 });
654 let merged = merge_grids(&a, &b);
655 let v = merged.get(1, 1, 1);
656 assert!((v.radiance.x - 0.5).abs() < 1e-5);
658 assert!((v.radiance.z - 0.5).abs() < 1e-5);
659 }
660}