parry2d/query/shape_cast/
shape_cast_heightfield_shape.rs

1use crate::bounding_volume::BoundingVolume;
2use crate::math::{Pose, Real, Vector};
3use crate::query::details::ShapeCastOptions;
4use crate::query::{QueryDispatcher, Ray, ShapeCastHit, Unsupported};
5use crate::shape::{HeightField, Shape};
6#[cfg(feature = "dim3")]
7use crate::{bounding_volume::Aabb, query::RayCast};
8
9/// Time Of Impact between a moving shape and a heightfield.
10#[cfg(feature = "dim2")]
11pub fn cast_shapes_heightfield_shape<D: ?Sized + QueryDispatcher>(
12    dispatcher: &D,
13    pos12: &Pose,
14    vel12: Vector,
15    heightfield1: &HeightField,
16    g2: &dyn Shape,
17    options: ShapeCastOptions,
18) -> Result<Option<ShapeCastHit>, Unsupported> {
19    let aabb2_1 = g2.compute_aabb(pos12).loosened(options.target_distance);
20    let ray = Ray::new(aabb2_1.center(), vel12);
21
22    let mut curr_range = heightfield1.unclamped_elements_range_in_local_aabb(&aabb2_1);
23    // Enlarge the range by 1 to account for movement within a cell.
24    let right = ray.dir.x > 0.0;
25
26    if right {
27        curr_range.end += 1;
28    } else {
29        curr_range.start -= 1;
30    }
31
32    let mut best_hit = None::<ShapeCastHit>;
33
34    /*
35     * Test the segment under the ray.
36     */
37    let clamped_curr_range = curr_range.start.clamp(0, heightfield1.num_cells() as isize) as usize
38        ..curr_range.end.clamp(0, heightfield1.num_cells() as isize) as usize;
39    for curr in clamped_curr_range {
40        if let Some(seg) = heightfield1.segment_at(curr) {
41            // TODO: pre-check using a ray-cast on the Aabbs first?
42            if let Some(hit) = dispatcher.cast_shapes(pos12, vel12, &seg, g2, options)? {
43                if hit.time_of_impact < best_hit.map(|h| h.time_of_impact).unwrap_or(Real::MAX) {
44                    best_hit = Some(hit);
45                }
46            }
47        }
48    }
49
50    /*
51     * Test other segments in the path of the ray.
52     */
53    if ray.dir.x == 0.0 {
54        return Ok(best_hit);
55    }
56
57    let cell_width = heightfield1.cell_width();
58    let start_x = heightfield1.start_x();
59
60    let mut curr_elt = if right {
61        (curr_range.end - 1).max(0)
62    } else {
63        curr_range.start.min(heightfield1.num_cells() as isize - 1)
64    };
65
66    while (right && curr_elt < heightfield1.num_cells() as isize - 1) || (!right && curr_elt > 0) {
67        let curr_param;
68
69        if right {
70            curr_elt += 1;
71            curr_param = (cell_width * (curr_elt as Real) + start_x - ray.origin.x) / ray.dir.x;
72        } else {
73            curr_param = (ray.origin.x - cell_width * (curr_elt as Real) - start_x) / ray.dir.x;
74            curr_elt -= 1;
75        }
76
77        if curr_param >= options.max_time_of_impact {
78            break;
79        }
80
81        if let Some(seg) = heightfield1.segment_at(curr_elt as usize) {
82            // TODO: pre-check using a ray-cast on the Aabbs first?
83            if let Some(hit) = dispatcher.cast_shapes(pos12, vel12, &seg, g2, options)? {
84                if hit.time_of_impact < best_hit.map(|h| h.time_of_impact).unwrap_or(Real::MAX) {
85                    best_hit = Some(hit);
86                }
87            }
88        }
89    }
90
91    Ok(best_hit)
92}
93
94/// Time Of Impact between a moving shape and a heightfield.
95#[cfg(feature = "dim3")]
96pub fn cast_shapes_heightfield_shape<D: ?Sized + QueryDispatcher>(
97    dispatcher: &D,
98    pos12: &Pose,
99    vel12: Vector,
100    heightfield1: &HeightField,
101    g2: &dyn Shape,
102    options: ShapeCastOptions,
103) -> Result<Option<ShapeCastHit>, Unsupported> {
104    let aabb1 = heightfield1.local_aabb();
105    let mut aabb2_1 = g2.compute_aabb(pos12).loosened(options.target_distance);
106    let ray = Ray::new(aabb2_1.center(), vel12);
107
108    // Find the first hit between the aabbs.
109    let hext2_1 = aabb2_1.half_extents();
110    let msum = Aabb::new(aabb1.mins - hext2_1, aabb1.maxs + hext2_1);
111    if let Some(time_of_impact) = msum.cast_local_ray(&ray, options.max_time_of_impact, true) {
112        // Advance the aabb2 to the hit point.
113        aabb2_1.mins += ray.dir * time_of_impact;
114        aabb2_1.maxs += ray.dir * time_of_impact;
115    } else {
116        return Ok(None);
117    }
118
119    let (mut curr_range_i, mut curr_range_j) =
120        heightfield1.unclamped_elements_range_in_local_aabb(&aabb2_1);
121    let (ncells_i, ncells_j) = heightfield1.num_cells_ij();
122    let mut best_hit = None::<ShapeCastHit>;
123
124    /*
125     * Enlarge the ranges by 1 to account for any movement within one cell.
126     */
127    if ray.dir.z > 0.0 {
128        curr_range_i.end += 1;
129    } else if ray.dir.z < 0.0 {
130        curr_range_i.start -= 1;
131    }
132
133    if ray.dir.x > 0.0 {
134        curr_range_j.end += 1;
135    } else if ray.dir.x < 0.0 {
136        curr_range_j.start -= 1;
137    }
138
139    /*
140     * Test the segment under the ray.
141     */
142    let clamped_curr_range_i = curr_range_i.start.clamp(0, ncells_i as isize)
143        ..curr_range_i.end.clamp(0, ncells_i as isize);
144    let clamped_curr_range_j = curr_range_j.start.clamp(0, ncells_j as isize)
145        ..curr_range_j.end.clamp(0, ncells_j as isize);
146
147    let mut hit_triangles = |i, j| {
148        if i >= 0 && j >= 0 {
149            let (tri_a, tri_b) = heightfield1.triangles_at(i as usize, j as usize);
150            for tri in [tri_a, tri_b].into_iter().flatten() {
151                // TODO: pre-check using a ray-cast on the Aabbs first?
152                if let Some(hit) = dispatcher.cast_shapes(pos12, vel12, &tri, g2, options)? {
153                    if hit.time_of_impact < best_hit.map(|h| h.time_of_impact).unwrap_or(Real::MAX)
154                    {
155                        best_hit = Some(hit);
156                    }
157                }
158            }
159        }
160
161        Ok(())
162    };
163
164    for i in clamped_curr_range_i {
165        for j in clamped_curr_range_j.clone() {
166            hit_triangles(i, j)?;
167        }
168    }
169
170    if ray.dir.y == 0.0 {
171        return Ok(best_hit);
172    }
173
174    let mut cell = heightfield1.unclamped_cell_at_point(aabb2_1.center());
175
176    loop {
177        let prev_cell = cell;
178
179        /*
180         * Find the next cell to cast the ray on.
181         */
182        let toi_x = if ray.dir.x > 0.0 {
183            let x = heightfield1.signed_x_at(cell.1 + 1);
184            (x - ray.origin.x) / ray.dir.x
185        } else if ray.dir.x < 0.0 {
186            let x = heightfield1.signed_x_at(cell.1);
187            (x - ray.origin.x) / ray.dir.x
188        } else {
189            Real::MAX
190        };
191
192        let toi_z = if ray.dir.z > 0.0 {
193            let z = heightfield1.signed_z_at(cell.0 + 1);
194            (z - ray.origin.z) / ray.dir.z
195        } else if ray.dir.z < 0.0 {
196            let z = heightfield1.signed_z_at(cell.0);
197            (z - ray.origin.z) / ray.dir.z
198        } else {
199            Real::MAX
200        };
201
202        if toi_x > options.max_time_of_impact && toi_z > options.max_time_of_impact {
203            break;
204        }
205
206        if toi_x >= 0.0 && toi_x <= toi_z {
207            cell.1 += ray.dir.x.signum() as isize;
208        }
209
210        if toi_z >= 0.0 && toi_z <= toi_x {
211            cell.0 += ray.dir.z.signum() as isize;
212        }
213
214        if cell == prev_cell {
215            break;
216        }
217
218        let cell_diff = (cell.0 - prev_cell.0, cell.1 - prev_cell.1);
219        curr_range_i.start += cell_diff.0;
220        curr_range_i.end += cell_diff.0;
221        curr_range_j.start += cell_diff.1;
222        curr_range_j.end += cell_diff.1;
223
224        let new_line_i = if cell_diff.0 > 0 {
225            curr_range_i.end
226        } else {
227            curr_range_i.start
228        };
229
230        let new_line_j = if cell_diff.1 > 0 {
231            curr_range_j.end
232        } else {
233            curr_range_j.start
234        };
235
236        let ignore_line_i = new_line_i < 0 || new_line_i >= ncells_i as isize;
237        let ignore_line_j = new_line_j < 0 || new_line_j >= ncells_j as isize;
238
239        if ignore_line_i && ignore_line_j {
240            break;
241        }
242
243        if !ignore_line_i && cell_diff.0 != 0 {
244            for j in curr_range_j.clone() {
245                hit_triangles(new_line_i, j)?;
246            }
247        }
248
249        if !ignore_line_j && cell_diff.1 != 0 {
250            for i in curr_range_i.clone() {
251                hit_triangles(i, new_line_j)?;
252            }
253        }
254    }
255
256    Ok(best_hit)
257}
258
259/// Time Of Impact between a moving shape and a heightfield.
260pub fn cast_shapes_shape_heightfield<D: ?Sized + QueryDispatcher>(
261    dispatcher: &D,
262    pos12: &Pose,
263    vel12: Vector,
264    g1: &dyn Shape,
265    heightfield2: &HeightField,
266    options: ShapeCastOptions,
267) -> Result<Option<ShapeCastHit>, Unsupported> {
268    Ok(cast_shapes_heightfield_shape(
269        dispatcher,
270        &pos12.inverse(),
271        -(pos12.rotation.inverse() * vel12),
272        heightfield2,
273        g1,
274        options,
275    )?
276    .map(|hit| hit.swapped()))
277}