1use spatialrust_core::{HasPositions3, PointCloud};
4use spatialrust_math::{Isometry3, TransformPoint, Vec3};
5
6use crate::marching_cubes::{polygonise_tet, tetrahedra};
7use crate::{SceneError, SceneResult, TriangleMesh};
8
9#[derive(Clone, Debug, PartialEq)]
11pub struct TsdfVolume {
12 origin: Vec3<f32>,
13 voxel_size: f32,
14 dims: [usize; 3],
15 distance: Vec<f32>,
16 weight: Vec<f32>,
17 truncation: f32,
18}
19
20impl TsdfVolume {
21 pub fn try_new(
29 origin: Vec3<f32>,
30 voxel_size: f32,
31 dims: [usize; 3],
32 truncation: f32,
33 ) -> SceneResult<Self> {
34 if !(voxel_size.is_finite() && voxel_size > 0.0) {
35 return Err(SceneError::InvalidConfiguration("voxel_size must be > 0".into()));
36 }
37 if !(truncation.is_finite() && truncation > 0.0) {
38 return Err(SceneError::InvalidConfiguration("truncation must be > 0".into()));
39 }
40 if dims.contains(&0) {
41 return Err(SceneError::InvalidConfiguration("dims must be non-zero".into()));
42 }
43 if ![origin.x, origin.y, origin.z].into_iter().all(f32::is_finite) {
44 return Err(SceneError::InvalidConfiguration("origin must be finite".into()));
45 }
46 let len = dims[0]
47 .checked_mul(dims[1])
48 .and_then(|count| count.checked_mul(dims[2]))
49 .filter(|&count| count <= isize::MAX as usize / std::mem::size_of::<f32>())
50 .ok_or_else(|| {
51 SceneError::InvalidConfiguration("TSDF field capacity is not representable".into())
52 })?;
53 for (coordinate, dim) in [origin.x, origin.y, origin.z].into_iter().zip(dims) {
54 let last_center = coordinate + ((dim - 1) as f32 + 0.5) * voxel_size;
57 if !last_center.is_finite() {
58 return Err(SceneError::InvalidConfiguration(
59 "voxel centers must be finite".into(),
60 ));
61 }
62 }
63 let mut distance = Vec::new();
64 let mut weight = Vec::new();
65 distance.try_reserve_exact(len).map_err(|_| {
66 SceneError::InvalidConfiguration("unable to reserve TSDF distance storage".into())
67 })?;
68 weight.try_reserve_exact(len).map_err(|_| {
69 SceneError::InvalidConfiguration("unable to reserve TSDF weight storage".into())
70 })?;
71 distance.resize(len, truncation);
72 weight.resize(len, 0.0);
73 Ok(Self { origin, voxel_size, dims, distance, weight, truncation })
74 }
75
76 #[must_use]
78 pub fn dims(&self) -> [usize; 3] {
79 self.dims
80 }
81
82 pub fn integrate_point(&mut self, point: Vec3<f32>, sensor_origin: Vec3<f32>) {
89 let depth = (point - sensor_origin).length();
90 if !(depth.is_finite() && depth > 1e-5) {
91 return;
92 }
93 let ray = (point - sensor_origin).normalize();
94 let radius = self.truncation;
95 let min = point - Vec3::new(radius, radius, radius);
96 let max = point + Vec3::new(radius, radius, radius);
97 let i0 = self.world_to_index(min);
98 let i1 = self.world_to_index(max);
99 if (0..3).any(|axis| i1[axis] < 0 || i0[axis] >= self.dims[axis] as isize) {
102 return;
103 }
104 let i0: [usize; 3] =
105 std::array::from_fn(|axis| i0[axis].clamp(0, self.dims[axis] as isize - 1) as usize);
106 let i1: [usize; 3] =
107 std::array::from_fn(|axis| i1[axis].clamp(0, self.dims[axis] as isize - 1) as usize);
108 for z in i0[2]..=i1[2] {
109 for y in i0[1]..=i1[1] {
110 for x in i0[0]..=i1[0] {
111 let center = self.index_to_world([x, y, z]);
112 let sdf = (point - center).dot(ray).clamp(-self.truncation, self.truncation);
113 let flat = self.flat([x, y, z]);
114 let w_old = self.weight[flat];
115 let w_new = w_old + 1.0;
116 self.distance[flat] = (self.distance[flat] * w_old + sdf) / w_new;
117 self.weight[flat] = w_new;
118 }
119 }
120 }
121 }
122
123 pub fn integrate_xyz(&mut self, xyz: &[f32], sensor_origin: Vec3<f32>) -> SceneResult<()> {
125 if xyz.len() % 3 != 0 {
126 return Err(SceneError::InvalidConfiguration(
127 "xyz length must be a multiple of 3".into(),
128 ));
129 }
130 for chunk in xyz.chunks_exact(3) {
131 self.integrate_point(Vec3::new(chunk[0], chunk[1], chunk[2]), sensor_origin);
132 }
133 Ok(())
134 }
135
136 pub fn integrate_cloud(
143 &mut self,
144 cloud: &PointCloud,
145 sensor_origin: Vec3<f32>,
146 ) -> SceneResult<usize> {
147 let (x, y, z) = cloud.positions3()?;
148 for index in 0..cloud.len() {
149 self.integrate_point(Vec3::new(x[index], y[index], z[index]), sensor_origin);
150 }
151 Ok(cloud.len())
152 }
153
154 pub fn integrate_cloud_with_pose(
159 &mut self,
160 cloud: &PointCloud,
161 volume_t_sensor: Isometry3<f32>,
162 sensor_origin: Vec3<f32>,
163 ) -> SceneResult<usize> {
164 let (x, y, z) = cloud.positions3()?;
165 let volume_sensor_origin = volume_t_sensor.transform_point(sensor_origin);
166 for index in 0..cloud.len() {
167 let point = volume_t_sensor.transform_point(Vec3::new(x[index], y[index], z[index]));
168 self.integrate_point(point, volume_sensor_origin);
169 }
170 Ok(cloud.len())
171 }
172
173 pub fn extract_mesh(&self, min_weight: f32) -> TriangleMesh {
177 let mut positions = Vec::new();
178 let mut indices = Vec::new();
179 if self.dims[0] < 2 || self.dims[1] < 2 || self.dims[2] < 2 {
180 return TriangleMesh { positions, indices };
181 }
182
183 for z in 0..self.dims[2] - 1 {
184 for y in 0..self.dims[1] - 1 {
185 for x in 0..self.dims[0] - 1 {
186 let mut corner_pos = [Vec3::new(0.0, 0.0, 0.0); 8];
187 let mut corner_val = [0.0f32; 8];
188 for (corner, offset) in (0..8).zip(CORNER_OFFSETS.iter()) {
189 let idx = [x + offset[0], y + offset[1], z + offset[2]];
190 corner_pos[corner] = self.index_to_world(idx);
191 corner_val[corner] = self.sample(idx, min_weight);
192 }
193 for tet in tetrahedra() {
194 polygonise_tet(
195 &mut positions,
196 &mut indices,
197 [
198 corner_pos[tet[0]],
199 corner_pos[tet[1]],
200 corner_pos[tet[2]],
201 corner_pos[tet[3]],
202 ],
203 [
204 corner_val[tet[0]],
205 corner_val[tet[1]],
206 corner_val[tet[2]],
207 corner_val[tet[3]],
208 ],
209 0.0,
210 );
211 }
212 }
213 }
214 }
215 TriangleMesh { positions, indices }
216 }
217
218 fn sample(&self, index: [usize; 3], min_weight: f32) -> f32 {
219 let flat = self.flat(index);
220 if self.weight[flat] < min_weight {
221 self.truncation
222 } else {
223 self.distance[flat]
224 }
225 }
226
227 fn world_to_index(&self, point: Vec3<f32>) -> [isize; 3] {
228 let ix = ((point.x - self.origin.x) / self.voxel_size).floor() as isize;
229 let iy = ((point.y - self.origin.y) / self.voxel_size).floor() as isize;
230 let iz = ((point.z - self.origin.z) / self.voxel_size).floor() as isize;
231 [ix, iy, iz]
232 }
233
234 fn index_to_world(&self, index: [usize; 3]) -> Vec3<f32> {
235 Vec3::new(
236 self.origin.x + (index[0] as f32 + 0.5) * self.voxel_size,
237 self.origin.y + (index[1] as f32 + 0.5) * self.voxel_size,
238 self.origin.z + (index[2] as f32 + 0.5) * self.voxel_size,
239 )
240 }
241
242 fn flat(&self, index: [usize; 3]) -> usize {
243 index[0] + self.dims[0] * (index[1] + self.dims[1] * index[2])
244 }
245}
246
247const CORNER_OFFSETS: [[usize; 3]; 8] =
248 [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]];
249
250#[cfg(test)]
251#[path = "tsdf_support_tests.rs"]
252mod support_tests;
253
254#[cfg(test)]
255#[path = "tsdf_grid_tests.rs"]
256mod grid_tests;
257
258#[cfg(test)]
259#[path = "tsdf_mesh_tests.rs"]
260mod mesh_tests;
261
262#[cfg(test)]
263mod tests {
264 use super::TsdfVolume;
265 use spatialrust_core::{PointCloudBuilder, StandardSchemas};
266 use spatialrust_math::Vec3;
267
268 #[test]
269 fn integrates_and_extracts_non_empty_mesh() {
270 let mut volume =
271 TsdfVolume::try_new(Vec3::new(-1.0, -1.0, -1.0), 0.25, [8, 8, 8], 0.5).unwrap();
272 volume.integrate_xyz(&[0.0, 0.0, 0.0, 0.2, 0.0, 0.0], Vec3::new(0.0, 0.0, -1.0)).unwrap();
273 let mesh = volume.extract_mesh(0.5);
274 assert!(!mesh.positions.is_empty());
275 assert_eq!(mesh.indices.len() % 3, 0);
276 assert!(mesh.triangle_count() >= 1);
277 }
278
279 #[test]
280 fn empty_weight_yields_empty_mesh() {
281 let volume =
282 TsdfVolume::try_new(Vec3::new(-1.0, -1.0, -1.0), 0.25, [8, 8, 8], 0.5).unwrap();
283 let mesh = volume.extract_mesh(1.0);
284 assert!(mesh.positions.is_empty());
285 assert!(mesh.indices.is_empty());
286 }
287
288 #[test]
289 fn integrates_point_cloud_columns_without_interleave() {
290 let mut builder = PointCloudBuilder::new(StandardSchemas::point_xyz());
291 builder.push_point([0.0, 0.0, 0.0]).unwrap();
292 builder.push_point([0.2, 0.0, 0.0]).unwrap();
293 let cloud = builder.build().unwrap();
294 let origin = Vec3::new(0.0, 0.0, -1.0);
295 let mut from_cloud =
296 TsdfVolume::try_new(Vec3::new(-1.0, -1.0, -1.0), 0.25, [8, 8, 8], 0.5).unwrap();
297 let mut from_xyz = from_cloud.clone();
298 assert_eq!(from_cloud.integrate_cloud(&cloud, origin).unwrap(), 2);
299 from_xyz.integrate_xyz(&[0.0, 0.0, 0.0, 0.2, 0.0, 0.0], origin).unwrap();
300 assert_eq!(from_cloud, from_xyz);
301 }
302
303 #[test]
304 fn integrates_point_cloud_with_explicit_sensor_pose() {
305 let mut builder = PointCloudBuilder::new(StandardSchemas::point_xyz());
306 builder.push_point([0.0, 0.0, 0.0]).unwrap();
307 builder.push_point([0.2, 0.0, 0.0]).unwrap();
308 let cloud = builder.build().unwrap();
309 let mut volume =
310 TsdfVolume::try_new(Vec3::new(-1.0, -1.0, -1.0), 0.25, [8, 8, 8], 0.5).unwrap();
311 let pose = spatialrust_math::Isometry3::new(
312 spatialrust_math::Quat::<f32>::identity(),
313 Vec3::new(0.5, 0.0, 0.0),
314 );
315 assert_eq!(
316 volume.integrate_cloud_with_pose(&cloud, pose, Vec3::new(0.0, 0.0, -1.0)).unwrap(),
317 2
318 );
319 }
320}