parry3d/query/nonlinear_shape_cast/
nonlinear_shape_cast_voxels_shape.rs

1use crate::bounding_volume::BoundingVolume;
2use crate::math::{Point, Real, Vector};
3use crate::query::{NonlinearRigidMotion, QueryDispatcher, ShapeCastHit};
4use crate::shape::{Cuboid, Shape, Voxels};
5
6/// Time Of Impact of a voxels shape with any other shape, under a rigid motion (translation + rotation).
7pub fn cast_shapes_nonlinear_voxels_shape<D>(
8    dispatcher: &D,
9    motion1: &NonlinearRigidMotion,
10    g1: &Voxels,
11    motion2: &NonlinearRigidMotion,
12    g2: &dyn Shape,
13    start_time: Real,
14    end_time: Real,
15    stop_at_penetration: bool,
16) -> Option<ShapeCastHit>
17where
18    D: ?Sized + QueryDispatcher,
19{
20    use num_traits::Bounded;
21
22    // HACK: really supporting nonlinear shape-casting on a rotating voxels shape would
23    //       be extremely inefficient without any sort of hierarchical traversal on the voxel.
24    //       So for now we assume that the voxels shape only has a translational motion.
25    //       We can fix that once we introduce a sparse representation (and its accompanying
26    //       acceleration structure) for the voxels shape.
27    let mut motion2 = *motion2;
28    let mut motion1 = *motion1;
29    motion2.linvel -= motion1.linvel;
30    motion1.freeze(start_time);
31    let pos1 = motion1.position_at_time(start_time);
32
33    // Search for the smallest time of impact.
34    //
35    // 1. Check all the cells in the AABB at time `start_time`.
36    // 2. Check which wall of unchecked voxels is hit first.
37    // 3. Check one additional layer of voxels behind that wall.
38    // 4. Continue, with a new AABB taken at the position at the time we hit the
39    //    wall.
40    // 5. Continue until `curr_t` is bigger than `smallest_t`.
41    //
42    // PERF: This will be fairly efficient if the shape being cast has a size in the same order
43    // of magnitude as the voxels, and if it doesn’t have a significant off-center angular
44    // motion. Otherwise, this method will be fairly slow, and would require an acceleration
45    // structure to improve.
46
47    let mut hit = None;
48    let mut smallest_t = end_time;
49
50    // NOTE: we approximate the AABB of the shape using its orientation at the start and
51    //       end time. It might miss some cases if the object is rotating fast.
52    let start_pos2_1 = pos1.inv_mul(&motion2.position_at_time(start_time));
53    let mut end_pos2_1 = pos1.inv_mul(&motion2.position_at_time(end_time));
54    end_pos2_1.translation = start_pos2_1.translation;
55    let start_aabb2_1 = g2
56        .compute_aabb(&start_pos2_1)
57        .merged(&g2.compute_aabb(&end_pos2_1));
58
59    let mut check_voxels_in_range = |search_domain: [Point<i32>; 2]| {
60        for vox in g1.voxels_in_range(search_domain[0], search_domain[1]) {
61            if !vox.state.is_empty() {
62                // PERF: could we check the canonical shape instead, and deduplicate accordingly?
63                let center = g1.voxel_center(vox.grid_coords);
64                let cuboid = Cuboid::new(g1.voxel_size() / 2.0);
65                let vox_motion1 = motion1.prepend_translation(center.coords);
66                if let Some(new_hit) = dispatcher
67                    .cast_shapes_nonlinear(
68                        &vox_motion1,
69                        &cuboid,
70                        &motion2,
71                        g2,
72                        start_time,
73                        end_time,
74                        stop_at_penetration,
75                    )
76                    .ok()
77                    .flatten()
78                {
79                    if new_hit.time_of_impact < smallest_t {
80                        smallest_t = new_hit.time_of_impact;
81                        hit = Some(new_hit);
82                    }
83                }
84            }
85        }
86    };
87
88    let mut search_domain = g1.voxel_range_intersecting_local_aabb(&start_aabb2_1);
89    check_voxels_in_range(search_domain);
90
91    // Run the propagation.
92    let [domain_mins, domain_maxs] = g1.domain();
93
94    loop {
95        let search_domain_aabb = g1.voxel_range_aabb(search_domain[0], search_domain[1]);
96
97        // Figure out if we should move the aabb up/down/right/left.
98        #[cfg(feature = "dim2")]
99        let ii = [0, 1];
100        #[cfg(feature = "dim3")]
101        let ii = [0, 1, 2];
102
103        let toi = ii.map(|i| {
104            if motion2.linvel[i] > 0.0 {
105                let t = (search_domain_aabb.maxs[i] - start_aabb2_1.maxs[i]) / motion2.linvel[i];
106                if t < 0.0 {
107                    (Real::max_value(), true)
108                } else {
109                    (t, true)
110                }
111            } else if motion2.linvel[i] < 0.0 {
112                let t = (search_domain_aabb.mins[i] - start_aabb2_1.mins[i]) / motion2.linvel[i];
113                if t < 0.0 {
114                    (Real::max_value(), false)
115                } else {
116                    (t, false)
117                }
118            } else {
119                (Real::max_value(), false)
120            }
121        });
122
123        #[cfg(feature = "dim2")]
124        if toi[0].0 > end_time && toi[1].0 > end_time {
125            break;
126        }
127
128        #[cfg(feature = "dim3")]
129        if toi[0].0 > end_time && toi[1].0 > end_time && toi[2].0 > end_time {
130            break;
131        }
132
133        let imin = Vector::from(toi.map(|t| t.0)).imin();
134
135        if toi[imin].1 {
136            search_domain[0][imin] += 1;
137            search_domain[1][imin] += 1;
138
139            if search_domain[1][imin] <= domain_maxs[imin] {
140                // Check the voxels on the added row.
141                let mut prev_row = search_domain[0];
142                prev_row[imin] = search_domain[1][imin] - 1;
143
144                let range_to_check = [prev_row, search_domain[1]];
145                check_voxels_in_range(range_to_check);
146            } else if search_domain[0][imin] >= domain_maxs[imin] {
147                // Leaving the shape’s bounds.
148                break;
149            }
150        } else {
151            search_domain[0][imin] -= 1;
152            search_domain[1][imin] -= 1;
153
154            if search_domain[0][imin] >= domain_mins[imin] {
155                // Check the voxels on the added row.
156                let mut next_row = search_domain[1];
157                next_row[imin] = search_domain[0][imin] + 1;
158
159                let range_to_check = [search_domain[0], next_row];
160                check_voxels_in_range(range_to_check);
161            } else if search_domain[1][imin] <= domain_mins[imin] {
162                // Leaving the shape’s bounds.
163                break;
164            }
165        }
166    }
167
168    hit
169}
170
171/// Time Of Impact of any shape with a composite shape, under a rigid motion (translation + rotation).
172pub fn cast_shapes_nonlinear_shape_voxels<D>(
173    dispatcher: &D,
174    motion1: &NonlinearRigidMotion,
175    g1: &dyn Shape,
176    motion2: &NonlinearRigidMotion,
177    g2: &Voxels,
178    start_time: Real,
179    end_time: Real,
180    stop_at_penetration: bool,
181) -> Option<ShapeCastHit>
182where
183    D: ?Sized + QueryDispatcher,
184{
185    cast_shapes_nonlinear_voxels_shape(
186        dispatcher,
187        motion2,
188        g2,
189        motion1,
190        g1,
191        start_time,
192        end_time,
193        stop_at_penetration,
194    )
195    .map(|hit| hit.swapped())
196}