1use crate::device::GpuContext;
2use bytemuck::{Pod, Zeroable};
3use nalgebra::{Matrix4, Point3};
4use threecrate_core::{ColoredPoint3f, Error, PointCloud, Result};
5use wgpu::util::DeviceExt;
6
7#[repr(C)]
9#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
10#[repr(align(16))] pub struct TsdfVoxel {
12 pub tsdf_value: f32,
13 pub weight: f32,
14 pub color_r: u32,
15 pub color_g: u32,
16 pub color_b: u32,
17 pub _padding1: u32,
18 pub _padding2: u32,
19 pub _padding3: u32,
20}
21
22#[derive(Debug, Clone)]
24pub struct TsdfVolume {
25 pub voxel_size: f32,
26 pub truncation_distance: f32,
27 pub resolution: [u32; 3], pub origin: Point3<f32>,
29}
30
31pub struct TsdfVolumeGpu {
33 pub volume: TsdfVolume,
34 pub voxel_buffer: wgpu::Buffer,
35}
36
37#[repr(C)]
39#[derive(Copy, Clone, Pod, Zeroable)]
40#[repr(align(16))] pub struct CameraIntrinsics {
42 pub fx: f32,
43 pub fy: f32,
44 pub cx: f32,
45 pub cy: f32,
46 pub width: u32,
47 pub height: u32,
48 pub depth_scale: f32,
49 pub _padding: f32,
50}
51
52#[repr(C)]
54#[derive(Copy, Clone, Pod, Zeroable)]
55#[repr(align(16))]
56pub struct TsdfParams {
57 pub voxel_size: f32,
58 pub truncation_distance: f32,
59 pub max_weight: f32,
60 pub iso_value: f32,
61 pub resolution: [u32; 3],
62 pub _padding2: u32,
63 pub origin: [f32; 3],
64 pub _padding3: f32,
65}
66
67#[repr(C)]
68#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
69#[repr(align(16))] pub struct GpuPoint3f {
71 pub x: f32,
72 pub y: f32,
73 pub z: f32,
74 pub r: u32,
75 pub g: u32,
76 pub b: u32,
77 pub _padding1: u32,
78 pub _padding2: u32,
79}
80
81impl GpuContext {
82 pub async fn tsdf_integrate(
84 &self,
85 volume: &mut TsdfVolume,
86 depth_image: &[f32],
87 color_image: Option<&[u8]>, camera_pose: &Matrix4<f32>,
89 intrinsics: &CameraIntrinsics,
90 ) -> Result<Vec<TsdfVoxel>> {
91 let total_voxels =
92 (volume.resolution[0] * volume.resolution[1] * volume.resolution[2]) as usize;
93
94 let depth_buffer = self
96 .device
97 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
98 label: Some("TSDF Depth Buffer"),
99 contents: bytemuck::cast_slice(depth_image),
100 usage: wgpu::BufferUsages::STORAGE,
101 });
102
103 let color_buffer = if let Some(color_data) = color_image {
104 let mut packed_colors = Vec::with_capacity(color_data.len() / 3);
106 for chunk in color_data.chunks_exact(3) {
107 let r = chunk[0] as u32;
108 let g = chunk[1] as u32;
109 let b = chunk[2] as u32;
110 let packed = (r << 16) | (g << 8) | b;
111 packed_colors.push(packed);
112 }
113
114 self.device
115 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
116 label: Some("TSDF Color Buffer"),
117 contents: bytemuck::cast_slice(&packed_colors),
118 usage: wgpu::BufferUsages::STORAGE,
119 })
120 } else {
121 self.device
123 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
124 label: Some("TSDF Empty Color Buffer"),
125 contents: bytemuck::cast_slice(&[0u32; 4]), usage: wgpu::BufferUsages::STORAGE,
127 })
128 };
129
130 let initial_voxels = vec![
132 TsdfVoxel {
133 tsdf_value: 1.0,
134 weight: 0.0,
135 color_r: 0,
136 color_g: 0,
137 color_b: 0,
138 _padding1: 0,
139 _padding2: 0,
140 _padding3: 0,
141 };
142 total_voxels
143 ];
144
145 let voxel_buffer = self
146 .device
147 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
148 label: Some("TSDF Voxel Buffer"),
149 contents: bytemuck::cast_slice(&initial_voxels),
150 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
151 });
152
153 let world_to_camera = camera_pose
155 .try_inverse()
156 .ok_or_else(|| Error::Gpu("Failed to invert camera pose matrix".into()))?;
157
158 let mut camera_transform = [[0.0f32; 4]; 4];
159 for i in 0..4 {
160 for j in 0..4 {
161 camera_transform[i][j] = world_to_camera[(i, j)];
162 }
163 }
164
165 let transform_buffer = self
166 .device
167 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
168 label: Some("TSDF Transform Buffer"),
169 contents: bytemuck::cast_slice(&[camera_transform]),
170 usage: wgpu::BufferUsages::UNIFORM,
171 });
172
173 let intrinsics_buffer = self
174 .device
175 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
176 label: Some("TSDF Intrinsics Buffer"),
177 contents: bytemuck::bytes_of(intrinsics),
178 usage: wgpu::BufferUsages::UNIFORM,
179 });
180
181 let params = TsdfParams {
182 voxel_size: volume.voxel_size,
183 truncation_distance: volume.truncation_distance,
184 max_weight: 100.0,
185 iso_value: 0.0,
186 resolution: volume.resolution,
187 _padding2: 0,
188 origin: [volume.origin.x, volume.origin.y, volume.origin.z],
189 _padding3: 0.0,
190 };
191
192 let params_buffer = self
193 .device
194 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
195 label: Some("TSDF Params Buffer"),
196 contents: bytemuck::bytes_of(¶ms),
197 usage: wgpu::BufferUsages::UNIFORM,
198 });
199
200 let shader = self
202 .device
203 .create_shader_module(wgpu::ShaderModuleDescriptor {
204 label: Some("TSDF Integration Shader"),
205 source: wgpu::ShaderSource::Wgsl(
206 include_str!("shaders/tsdf_integration.wgsl").into(),
207 ),
208 });
209
210 let pipeline = self
211 .device
212 .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
213 label: Some("TSDF Integration Pipeline"),
214 layout: None,
215 module: &shader,
216 entry_point: Some("main"),
217 compilation_options: wgpu::PipelineCompilationOptions::default(),
218 cache: None,
219 });
220
221 let bind_group_entries = vec![
223 wgpu::BindGroupEntry {
224 binding: 0,
225 resource: voxel_buffer.as_entire_binding(),
226 },
227 wgpu::BindGroupEntry {
228 binding: 1,
229 resource: depth_buffer.as_entire_binding(),
230 },
231 wgpu::BindGroupEntry {
232 binding: 2,
233 resource: transform_buffer.as_entire_binding(),
234 },
235 wgpu::BindGroupEntry {
236 binding: 3,
237 resource: intrinsics_buffer.as_entire_binding(),
238 },
239 wgpu::BindGroupEntry {
240 binding: 4,
241 resource: params_buffer.as_entire_binding(),
242 },
243 wgpu::BindGroupEntry {
244 binding: 5,
245 resource: color_buffer.as_entire_binding(),
246 },
247 ];
248
249 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
250 label: Some("TSDF Integration Bind Group"),
251 layout: &pipeline.get_bind_group_layout(0),
252 entries: &bind_group_entries,
253 });
254
255 let mut encoder = self
257 .device
258 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
259 label: Some("TSDF Integration Encoder"),
260 });
261
262 {
263 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
264 label: Some("TSDF Integration Pass"),
265 timestamp_writes: None,
266 });
267
268 compute_pass.set_pipeline(&pipeline);
269 compute_pass.set_bind_group(0, &bind_group, &[]);
270
271 let workgroup_size = 4;
273 let dispatch_x = (volume.resolution[0] + workgroup_size - 1) / workgroup_size;
274 let dispatch_y = (volume.resolution[1] + workgroup_size - 1) / workgroup_size;
275 let dispatch_z = (volume.resolution[2] + workgroup_size - 1) / workgroup_size;
276
277 compute_pass.dispatch_workgroups(dispatch_x, dispatch_y, dispatch_z);
278 }
279
280 let staging_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
282 label: Some("TSDF Staging Buffer"),
283 size: (total_voxels * std::mem::size_of::<TsdfVoxel>()) as u64,
284 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
285 mapped_at_creation: false,
286 });
287
288 encoder.copy_buffer_to_buffer(&voxel_buffer, 0, &staging_buffer, 0, staging_buffer.size());
289
290 self.queue.submit(std::iter::once(encoder.finish()));
291
292 let buffer_slice = staging_buffer.slice(..);
293 let (sender, receiver) = flume::unbounded();
294 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
295 sender.send(result).unwrap();
296 });
297
298 self.device.poll(wgpu::PollType::Wait {
299 submission_index: None,
300 timeout: None,
301 });
302 receiver
303 .recv_async()
304 .await
305 .map_err(|_| Error::Gpu("Failed to receive mapping result".into()))?
306 .map_err(|e| Error::Gpu(format!("Buffer mapping failed: {:?}", e)))?;
307
308 let data = buffer_slice.get_mapped_range();
309 let voxel_size = std::mem::size_of::<TsdfVoxel>();
314 let count = data.len() / voxel_size;
315 let mut result = vec![TsdfVoxel::zeroed(); count];
316 bytemuck::cast_slice_mut(&mut result).copy_from_slice(&data[..count * voxel_size]);
317
318 drop(data);
319 staging_buffer.unmap();
320
321 Ok(result)
322 }
323
324 pub async fn tsdf_extract_surface(
326 &self,
327 volume: &TsdfVolume,
328 voxels: &[TsdfVoxel],
329 iso_value: f32,
330 ) -> Result<PointCloud<ColoredPoint3f>> {
331 let total_voxels =
332 (volume.resolution[0] * volume.resolution[1] * volume.resolution[2]) as usize;
333 let max_points = std::cmp::min(total_voxels, 1_000_000); let voxel_buffer = self
337 .device
338 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
339 label: Some("TSDF Voxel Buffer"),
340 contents: bytemuck::cast_slice(voxels),
341 usage: wgpu::BufferUsages::STORAGE,
342 });
343
344 let points_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
346 label: Some("Surface Points Buffer"),
347 size: (max_points * std::mem::size_of::<GpuPoint3f>()) as u64,
348 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
349 mapped_at_creation: false,
350 });
351
352 let point_count_buffer =
353 self.device
354 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
355 label: Some("Point Count Buffer"),
356 contents: bytemuck::bytes_of(&0u32),
357 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
358 });
359
360 let params = TsdfParams {
361 voxel_size: volume.voxel_size,
362 truncation_distance: volume.truncation_distance,
363 max_weight: 100.0,
364 iso_value,
365 resolution: volume.resolution,
366 _padding2: 0,
367 origin: [volume.origin.x, volume.origin.y, volume.origin.z],
368 _padding3: 0.0,
369 };
370
371 let params_buffer = self
372 .device
373 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
374 label: Some("Surface Extraction Params Buffer"),
375 contents: bytemuck::bytes_of(¶ms),
376 usage: wgpu::BufferUsages::UNIFORM,
377 });
378
379 let shader = self
381 .device
382 .create_shader_module(wgpu::ShaderModuleDescriptor {
383 label: Some("Surface Extraction Shader"),
384 source: wgpu::ShaderSource::Wgsl(
385 include_str!("shaders/surface_extraction.wgsl").into(),
386 ),
387 });
388
389 let pipeline = self
390 .device
391 .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
392 label: Some("Surface Extraction Pipeline"),
393 layout: None,
394 module: &shader,
395 entry_point: Some("main"),
396 compilation_options: wgpu::PipelineCompilationOptions::default(),
397 cache: None,
398 });
399
400 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
402 label: Some("Surface Extraction Bind Group"),
403 layout: &pipeline.get_bind_group_layout(0),
404 entries: &[
405 wgpu::BindGroupEntry {
406 binding: 0,
407 resource: voxel_buffer.as_entire_binding(),
408 },
409 wgpu::BindGroupEntry {
410 binding: 1,
411 resource: points_buffer.as_entire_binding(),
412 },
413 wgpu::BindGroupEntry {
414 binding: 2,
415 resource: params_buffer.as_entire_binding(),
416 },
417 wgpu::BindGroupEntry {
418 binding: 3,
419 resource: point_count_buffer.as_entire_binding(),
420 },
421 ],
422 });
423
424 let point_count_staging_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
426 label: Some("Point Count Staging Buffer"),
427 size: std::mem::size_of::<u32>() as u64,
428 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
429 mapped_at_creation: false,
430 });
431
432 let mut encoder = self
434 .device
435 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
436 label: Some("Surface Extraction Encoder"),
437 });
438
439 {
440 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
441 label: Some("Surface Extraction Pass"),
442 timestamp_writes: None,
443 });
444
445 compute_pass.set_pipeline(&pipeline);
446 compute_pass.set_bind_group(0, &bind_group, &[]);
447 compute_pass.dispatch_workgroups(
448 (volume.resolution[0] + 3) / 4,
449 (volume.resolution[1] + 3) / 4,
450 (volume.resolution[2] + 3) / 4,
451 );
452 }
453
454 encoder.copy_buffer_to_buffer(
456 &point_count_buffer,
457 0,
458 &point_count_staging_buffer,
459 0,
460 std::mem::size_of::<u32>() as u64,
461 );
462
463 self.queue.submit(Some(encoder.finish()));
464
465 let point_count_slice = point_count_staging_buffer.slice(..);
467 let (tx, rx) = futures_intrusive::channel::shared::oneshot_channel();
468 point_count_slice.map_async(wgpu::MapMode::Read, move |result| {
469 tx.send(result).unwrap();
470 });
471 self.device.poll(wgpu::PollType::Wait {
472 submission_index: None,
473 timeout: None,
474 });
475 rx.receive().await.unwrap()?;
476
477 let mapped_range = point_count_slice.get_mapped_range();
478 let point_count = bytemuck::cast_slice::<u8, u32>(mapped_range.as_ref())[0] as usize;
479 drop(mapped_range);
480 point_count_staging_buffer.unmap();
481
482 if point_count == 0 {
483 return Ok(PointCloud { points: Vec::new() });
484 }
485
486 let points_staging_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
488 label: Some("Points Staging Buffer"),
489 size: (point_count * std::mem::size_of::<GpuPoint3f>()) as u64,
490 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
491 mapped_at_creation: false,
492 });
493
494 let mut encoder = self
496 .device
497 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
498 label: Some("Points Copy Encoder"),
499 });
500
501 encoder.copy_buffer_to_buffer(
502 &points_buffer,
503 0,
504 &points_staging_buffer,
505 0,
506 (point_count * std::mem::size_of::<GpuPoint3f>()) as u64,
507 );
508
509 self.queue.submit(Some(encoder.finish()));
510
511 let points_slice = points_staging_buffer.slice(..);
513 let (tx, rx) = futures_intrusive::channel::shared::oneshot_channel();
514 points_slice.map_async(wgpu::MapMode::Read, move |result| {
515 tx.send(result).unwrap();
516 });
517 self.device.poll(wgpu::PollType::Wait {
518 submission_index: None,
519 timeout: None,
520 });
521 rx.receive().await.unwrap()?;
522
523 let mapped_range = points_slice.get_mapped_range();
524 let point_size = std::mem::size_of::<GpuPoint3f>();
529 let available = mapped_range.len() / point_size;
530 let mut gpu_points = vec![GpuPoint3f::zeroed(); available];
531 bytemuck::cast_slice_mut(&mut gpu_points)
532 .copy_from_slice(&mapped_range[..available * point_size]);
533 let mut points = Vec::with_capacity(point_count);
534
535 for gpu_point in gpu_points.iter().take(point_count) {
536 points.push(ColoredPoint3f {
537 position: Point3::new(gpu_point.x, gpu_point.y, gpu_point.z),
538 color: [gpu_point.r as u8, gpu_point.g as u8, gpu_point.b as u8],
539 });
540 }
541
542 drop(mapped_range); points_staging_buffer.unmap();
544
545 Ok(PointCloud { points })
546 }
547}
548
549impl TsdfVolumeGpu {
550 pub fn new(gpu: &GpuContext, volume_params: TsdfVolume) -> Self {
552 let total_voxels = (volume_params.resolution[0]
553 * volume_params.resolution[1]
554 * volume_params.resolution[2]) as usize;
555
556 let initial_voxels = vec![
558 TsdfVoxel {
559 tsdf_value: 1.0,
560 weight: 0.0,
561 color_r: 0,
562 color_g: 0,
563 color_b: 0,
564 _padding1: 0,
565 _padding2: 0,
566 _padding3: 0,
567 };
568 total_voxels
569 ];
570
571 let voxel_buffer = gpu
572 .device
573 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
574 label: Some("TSDF Voxel Buffer"),
575 contents: bytemuck::cast_slice(&initial_voxels),
576 usage: wgpu::BufferUsages::STORAGE
577 | wgpu::BufferUsages::COPY_SRC
578 | wgpu::BufferUsages::COPY_DST,
579 });
580
581 Self {
582 volume: volume_params,
583 voxel_buffer,
584 }
585 }
586
587 pub async fn integrate(
589 &self,
590 gpu: &GpuContext,
591 depth_image: &[f32],
592 color_image: Option<&[u8]>, camera_pose: &Matrix4<f32>,
594 intrinsics: &CameraIntrinsics,
595 ) -> Result<()> {
596 let depth_buffer = gpu.create_buffer_init(
598 "TSDF Depth Buffer",
599 depth_image,
600 wgpu::BufferUsages::STORAGE,
601 );
602
603 let color_buffer = if let Some(data) = color_image {
604 gpu.create_buffer_init("TSDF Color Buffer", data, wgpu::BufferUsages::STORAGE)
605 } else {
606 gpu.create_buffer_init(
608 "TSDF Dummy Color Buffer",
609 &[0u32; 4],
610 wgpu::BufferUsages::STORAGE,
611 )
612 };
613
614 let world_to_camera = camera_pose
616 .try_inverse()
617 .ok_or_else(|| Error::Gpu("Failed to invert camera pose matrix".into()))?;
618
619 let mut camera_transform = [[0.0f32; 4]; 4];
620 for i in 0..4 {
621 for j in 0..4 {
622 camera_transform[i][j] = world_to_camera[(i, j)];
623 }
624 }
625
626 let transform_buffer = gpu.create_buffer_init(
627 "TSDF Transform Buffer",
628 &[camera_transform],
629 wgpu::BufferUsages::UNIFORM,
630 );
631
632 let intrinsics_buffer = gpu.create_buffer_init(
633 "TSDF Intrinsics Buffer",
634 &[*intrinsics],
635 wgpu::BufferUsages::UNIFORM,
636 );
637
638 let params = TsdfParams {
639 voxel_size: self.volume.voxel_size,
640 truncation_distance: self.volume.truncation_distance,
641 max_weight: 100.0,
642 iso_value: 0.0,
643 resolution: self.volume.resolution,
644 _padding2: 0,
645 origin: [
646 self.volume.origin.x,
647 self.volume.origin.y,
648 self.volume.origin.z,
649 ],
650 _padding3: 0.0,
651 };
652 let params_buffer =
653 gpu.create_buffer_init("TSDF Params Buffer", &[params], wgpu::BufferUsages::UNIFORM);
654
655 let shader = gpu.create_shader_module(
657 "TSDF Integration Shader",
658 include_str!("shaders/tsdf_integration.wgsl"),
659 );
660 let pipeline = gpu
661 .device
662 .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
663 label: Some("TSDF Integration Pipeline"),
664 layout: None,
665 module: &shader,
666 entry_point: Some("main"),
667 compilation_options: wgpu::PipelineCompilationOptions::default(),
668 cache: None,
669 });
670
671 let bind_group_entries = vec![
673 wgpu::BindGroupEntry {
674 binding: 0,
675 resource: self.voxel_buffer.as_entire_binding(),
676 },
677 wgpu::BindGroupEntry {
678 binding: 1,
679 resource: depth_buffer.as_entire_binding(),
680 },
681 wgpu::BindGroupEntry {
682 binding: 2,
683 resource: transform_buffer.as_entire_binding(),
684 },
685 wgpu::BindGroupEntry {
686 binding: 3,
687 resource: intrinsics_buffer.as_entire_binding(),
688 },
689 wgpu::BindGroupEntry {
690 binding: 4,
691 resource: params_buffer.as_entire_binding(),
692 },
693 wgpu::BindGroupEntry {
694 binding: 5,
695 resource: color_buffer.as_entire_binding(),
696 },
697 ];
698
699 let bind_group = gpu.create_bind_group(
700 "TSDF Integration Bind Group",
701 &pipeline.get_bind_group_layout(0),
702 &bind_group_entries,
703 );
704
705 let mut encoder = gpu
707 .device
708 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
709 label: Some("TSDF Integration Encoder"),
710 });
711
712 {
713 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
714 label: Some("TSDF Integration Pass"),
715 timestamp_writes: None,
716 });
717
718 compute_pass.set_pipeline(&pipeline);
719 compute_pass.set_bind_group(0, &bind_group, &[]);
720
721 let workgroup_size = 4;
723 let dispatch_x = (self.volume.resolution[0] + workgroup_size - 1) / workgroup_size;
724 let dispatch_y = (self.volume.resolution[1] + workgroup_size - 1) / workgroup_size;
725 let dispatch_z = (self.volume.resolution[2] + workgroup_size - 1) / workgroup_size;
726
727 println!(
728 "Dispatching compute shader with {} x {} x {} workgroups",
729 dispatch_x, dispatch_y, dispatch_z
730 );
731 compute_pass.dispatch_workgroups(dispatch_x, dispatch_y, dispatch_z);
732 }
733
734 gpu.queue.submit(std::iter::once(encoder.finish()));
735 Ok(())
736 }
737
738 pub async fn download_voxels(&self, gpu: &GpuContext) -> Result<Vec<TsdfVoxel>> {
740 let total_voxels = (self.volume.resolution[0]
741 * self.volume.resolution[1]
742 * self.volume.resolution[2]) as usize;
743 let buffer_size = (total_voxels * std::mem::size_of::<TsdfVoxel>()) as u64;
744
745 let staging_buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor {
746 label: Some("TSDF Staging Buffer"),
747 size: buffer_size,
748 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
749 mapped_at_creation: false,
750 });
751
752 let mut encoder = gpu
753 .device
754 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
755 label: Some("TSDF Download Encoder"),
756 });
757
758 encoder.copy_buffer_to_buffer(&self.voxel_buffer, 0, &staging_buffer, 0, buffer_size);
759
760 gpu.queue.submit(std::iter::once(encoder.finish()));
761
762 let buffer_slice = staging_buffer.slice(..);
763 let (sender, receiver) = flume::unbounded();
764 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
765 sender.send(result).unwrap();
766 });
767
768 gpu.device.poll(wgpu::PollType::Wait {
769 submission_index: None,
770 timeout: None,
771 });
772 receiver
773 .recv_async()
774 .await
775 .map_err(|_| Error::Gpu("Failed to receive mapping result".into()))??;
776
777 let data = buffer_slice.get_mapped_range();
778 let voxel_size = std::mem::size_of::<TsdfVoxel>();
783 let count = data.len() / voxel_size;
784 let mut result = vec![TsdfVoxel::zeroed(); count];
785 bytemuck::cast_slice_mut(&mut result).copy_from_slice(&data[..count * voxel_size]);
786
787 drop(data);
788 staging_buffer.unmap();
789
790 Ok(result)
791 }
792
793 pub async fn extract_surface(
795 &self,
796 gpu: &GpuContext,
797 iso_value: f32,
798 ) -> Result<PointCloud<ColoredPoint3f>> {
799 let voxels = self.download_voxels(gpu).await?;
800 gpu.tsdf_extract_surface(&self.volume, &voxels, iso_value)
801 .await
802 }
803}
804
805pub fn create_tsdf_volume(
807 voxel_size: f32,
808 truncation_distance: f32,
809 resolution: [u32; 3],
810 origin: Point3<f32>,
811) -> TsdfVolume {
812 TsdfVolume {
813 voxel_size,
814 truncation_distance,
815 resolution,
816 origin,
817 }
818}
819
820pub async fn gpu_tsdf_integrate(
822 gpu_context: &GpuContext,
823 volume: &mut TsdfVolume,
824 depth_image: &[f32],
825 color_image: Option<&[u8]>,
826 camera_pose: &Matrix4<f32>,
827 intrinsics: &CameraIntrinsics,
828) -> Result<Vec<TsdfVoxel>> {
829 gpu_context
830 .tsdf_integrate(volume, depth_image, color_image, camera_pose, intrinsics)
831 .await
832}
833
834pub async fn gpu_tsdf_extract_surface(
836 gpu_context: &GpuContext,
837 volume: &TsdfVolume,
838 voxels: &[TsdfVoxel],
839 iso_value: f32,
840) -> Result<PointCloud<ColoredPoint3f>> {
841 gpu_context
842 .tsdf_extract_surface(volume, voxels, iso_value)
843 .await
844}
845
846#[cfg(test)]
847mod tests {
848 use super::*;
849 use crate::device::GpuContext;
850 use approx::assert_relative_eq;
851 use nalgebra::{Matrix4, Point3};
852
853 async fn try_create_gpu_context() -> Option<GpuContext> {
855 match GpuContext::new().await {
856 Ok(gpu) => Some(gpu),
857 Err(_) => {
858 println!("⚠️ GPU not available, skipping GPU-dependent test");
859 None
860 }
861 }
862 }
863
864 fn create_simple_depth_image(width: u32, height: u32, depth: f32) -> Vec<f32> {
866 vec![depth; (width * height) as usize]
867 }
868
869 fn create_test_camera() -> CameraIntrinsics {
871 CameraIntrinsics {
872 fx: 525.0,
873 fy: 525.0,
874 cx: 319.5,
875 cy: 239.5,
876 width: 640,
877 height: 480,
878 depth_scale: 1.0,
879 _padding: 0.0,
880 }
881 }
882
883 fn create_identity_pose() -> Matrix4<f32> {
885 Matrix4::new(
886 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
887 )
888 }
889
890 #[test]
891 fn test_tsdf_basic_integration() {
892 pollster::block_on(async {
893 let Some(gpu) = try_create_gpu_context().await else {
894 return;
895 };
896
897 let voxel_size = 0.02; let truncation_distance = 0.1;
900 let resolution = [32, 32, 32]; let origin = Point3::new(-0.32, -0.32, 0.0);
902
903 let volume_params =
904 create_tsdf_volume(voxel_size, truncation_distance, resolution, origin);
905 let tsdf_volume_gpu = TsdfVolumeGpu::new(&gpu, volume_params);
906
907 let intrinsics = create_test_camera();
909 let depth_image = create_simple_depth_image(intrinsics.width, intrinsics.height, 0.5);
910 let camera_pose = create_identity_pose();
911
912 let result = tsdf_volume_gpu
914 .integrate(&gpu, &depth_image, None, &camera_pose, &intrinsics)
915 .await;
916 assert!(result.is_ok(), "TSDF integration should succeed");
917
918 let voxels = tsdf_volume_gpu.download_voxels(&gpu).await.unwrap();
920 assert_eq!(
921 voxels.len(),
922 (32 * 32 * 32) as usize,
923 "Should have correct number of voxels"
924 );
925
926 let updated_voxels = voxels.iter().filter(|v| v.weight > 0.0).count();
928 assert!(updated_voxels > 0, "Some voxels should have been updated");
929
930 println!(
931 "✓ Basic integration test passed: {} voxels updated",
932 updated_voxels
933 );
934 });
935 }
936
937 #[test]
938 fn test_tsdf_surface_extraction() {
939 pollster::block_on(async {
940 let Some(gpu) = try_create_gpu_context().await else {
941 return;
942 };
943
944 let voxel_size = 0.02;
946 let truncation_distance = 0.1;
947 let resolution = [32, 32, 32];
948 let origin = Point3::new(-0.32, -0.32, 0.0);
949
950 let volume_params =
951 create_tsdf_volume(voxel_size, truncation_distance, resolution, origin);
952 let tsdf_volume_gpu = TsdfVolumeGpu::new(&gpu, volume_params);
953
954 let intrinsics = create_test_camera();
956 let depth_image = create_simple_depth_image(intrinsics.width, intrinsics.height, 0.3);
957 let camera_pose = create_identity_pose();
958
959 tsdf_volume_gpu
960 .integrate(&gpu, &depth_image, None, &camera_pose, &intrinsics)
961 .await
962 .unwrap();
963
964 let point_cloud = tsdf_volume_gpu.extract_surface(&gpu, 0.0).await.unwrap();
966
967 assert!(
969 !point_cloud.points.is_empty(),
970 "Should extract surface points"
971 );
972
973 let avg_z = point_cloud.points.iter().map(|p| p.position.z).sum::<f32>()
975 / point_cloud.points.len() as f32;
976
977 assert!(
978 avg_z > 0.2 && avg_z < 0.4,
979 "Average Z should be near depth value of 0.3"
980 );
981
982 println!(
983 "✓ Surface extraction test passed: {} points extracted, avg Z: {:.3}",
984 point_cloud.points.len(),
985 avg_z
986 );
987 });
988 }
989
990 #[test]
991 fn test_tsdf_multiple_integrations() {
992 pollster::block_on(async {
993 let Some(gpu) = try_create_gpu_context().await else {
994 return;
995 };
996
997 let voxel_size = 0.02;
999 let truncation_distance = 0.1;
1000 let resolution = [32, 32, 32];
1001 let origin = Point3::new(-0.32, -0.32, 0.0);
1002
1003 let volume_params =
1004 create_tsdf_volume(voxel_size, truncation_distance, resolution, origin);
1005 let tsdf_volume_gpu = TsdfVolumeGpu::new(&gpu, volume_params);
1006
1007 let intrinsics = create_test_camera();
1008 let camera_pose = create_identity_pose();
1009
1010 let depths = [0.25, 0.3, 0.35];
1012 for &depth in &depths {
1013 let depth_image =
1014 create_simple_depth_image(intrinsics.width, intrinsics.height, depth);
1015 tsdf_volume_gpu
1016 .integrate(&gpu, &depth_image, None, &camera_pose, &intrinsics)
1017 .await
1018 .unwrap();
1019 }
1020
1021 let voxels = tsdf_volume_gpu.download_voxels(&gpu).await.unwrap();
1023 let max_weight = voxels.iter().map(|v| v.weight).fold(0.0, f32::max);
1024 assert!(
1025 max_weight > 1.0,
1026 "Multiple integrations should increase voxel weights"
1027 );
1028
1029 let point_cloud = tsdf_volume_gpu.extract_surface(&gpu, 0.0).await.unwrap();
1031 assert!(
1032 !point_cloud.points.is_empty(),
1033 "Should extract surface after multiple integrations"
1034 );
1035
1036 println!(
1037 "✓ Multiple integration test passed: max weight {:.1}, {} points extracted",
1038 max_weight,
1039 point_cloud.points.len()
1040 );
1041 });
1042 }
1043
1044 #[test]
1045 fn test_tsdf_coordinate_system() {
1046 pollster::block_on(async {
1047 let Some(_gpu) = try_create_gpu_context().await else {
1048 return;
1049 };
1050
1051 let voxel_size = 0.02;
1053 let resolution = [32, 32, 32];
1054 let origin = Point3::new(-0.32, -0.32, 0.0);
1055
1056 let max_coord = Point3::new(
1058 origin.x + (resolution[0] as f32) * voxel_size,
1059 origin.y + (resolution[1] as f32) * voxel_size,
1060 origin.z + (resolution[2] as f32) * voxel_size,
1061 );
1062
1063 assert_relative_eq!(max_coord.x, 0.32, epsilon = 0.01);
1064 assert_relative_eq!(max_coord.y, 0.32, epsilon = 0.01);
1065 assert_relative_eq!(max_coord.z, 0.64, epsilon = 0.01);
1066
1067 let camera_pose = create_identity_pose();
1069 let world_to_camera = camera_pose.try_inverse().unwrap();
1070
1071 let test_point = Point3::new(0.1, 0.2, 0.3);
1072 let camera_point = world_to_camera.transform_point(&test_point);
1073
1074 assert_relative_eq!(test_point.x, camera_point.x, epsilon = 0.001);
1076 assert_relative_eq!(test_point.y, camera_point.y, epsilon = 0.001);
1077 assert_relative_eq!(test_point.z, camera_point.z, epsilon = 0.001);
1078
1079 println!("✓ Coordinate system test passed");
1080 });
1081 }
1082
1083 #[test]
1084 fn test_tsdf_color_integration() {
1085 pollster::block_on(async {
1086 let Some(gpu) = try_create_gpu_context().await else {
1087 return;
1088 };
1089
1090 let voxel_size = 0.02;
1092 let truncation_distance = 0.1;
1093 let resolution = [32, 32, 32];
1094 let origin = Point3::new(-0.32, -0.32, 0.0);
1095
1096 let volume_params =
1097 create_tsdf_volume(voxel_size, truncation_distance, resolution, origin);
1098 let tsdf_volume_gpu = TsdfVolumeGpu::new(&gpu, volume_params);
1099
1100 let intrinsics = create_test_camera();
1102 let depth_image = create_simple_depth_image(intrinsics.width, intrinsics.height, 0.3);
1103
1104 let pixel_count = (intrinsics.width * intrinsics.height) as usize;
1106 let mut color_image = Vec::with_capacity(pixel_count * 3);
1107 for _ in 0..pixel_count {
1108 color_image.extend_from_slice(&[255u8, 0u8, 0u8]); }
1110
1111 let camera_pose = create_identity_pose();
1112
1113 tsdf_volume_gpu
1115 .integrate(
1116 &gpu,
1117 &depth_image,
1118 Some(&color_image),
1119 &camera_pose,
1120 &intrinsics,
1121 )
1122 .await
1123 .unwrap();
1124
1125 let point_cloud = tsdf_volume_gpu.extract_surface(&gpu, 0.0).await.unwrap();
1127
1128 assert!(
1129 !point_cloud.points.is_empty(),
1130 "Should extract colored surface points"
1131 );
1132
1133 let red_points = point_cloud
1135 .points
1136 .iter()
1137 .filter(|p| p.color[0] > 200)
1138 .count();
1139
1140 assert!(red_points > 0, "Some points should have red color");
1141
1142 println!("✓ Color integration test passed: {} red points", red_points);
1143 });
1144 }
1145}