concinnity_core/physics/sim/query/
mod.rs1pub(super) mod field;
27pub(super) mod gjk;
28mod ray;
29mod simplex;
30pub(super) mod sweep;
31
32use crate::memory::Pool;
33
34use crate::physics::{BodyHandle, ColliderShape, LayerMask, RayHit};
35
36use super::aabb::shape_bounds;
37use super::body::Body;
38use super::collide::Pose;
39use super::math::{Quat, Vec3};
40use super::scene::Scene;
41
42use gjk::Support;
43use ray::{BoundsProbe, Ray};
44
45#[derive(Debug, Clone, Copy)]
51pub struct ShapeCast {
52 pub shape: ColliderShape,
54 pub origin: [f32; 3],
56 pub euler_deg: [f32; 3],
58 pub motion: [f32; 3],
61 pub exclude: Option<BodyHandle>,
63 pub mask: LayerMask,
65}
66
67impl ShapeCast {
68 pub fn new(shape: ColliderShape, origin: [f32; 3], motion: [f32; 3]) -> Self {
71 ShapeCast {
72 shape,
73 origin,
74 euler_deg: [0.0; 3],
75 motion,
76 exclude: None,
77 mask: LayerMask::ALL,
78 }
79 }
80}
81
82#[derive(Debug, Clone, Copy)]
84pub struct ShapeCastHit {
85 pub body: BodyHandle,
87 pub toi: f32,
90 pub point: [f32; 3],
92 pub normal: [f32; 3],
95 pub gap: f32,
100 pub started_touching: bool,
104}
105
106#[derive(Debug, Clone, Copy)]
109pub(crate) struct RayQuery {
110 pub(crate) origin: [f32; 3],
111 pub(crate) dir: [f32; 3],
112 pub(crate) max_dist: f32,
113 pub(crate) exclude: Option<BodyHandle>,
114 pub(crate) mask: LayerMask,
115}
116
117pub(crate) fn raycast(scene: Scene<'_>, ray_query: &RayQuery) -> Option<RayHit> {
122 let max_dist = ray_query.max_dist;
123 if !(max_dist.is_finite() && max_dist > 0.0) {
124 return None;
125 }
126 let direction = Vec3::from_array(ray_query.dir);
127 let length = direction.length();
128 if !(length.is_finite() && length > 0.0) {
129 return None;
130 }
131 let origin = Vec3::from_array(ray_query.origin);
132 if !origin.is_finite() {
133 return None;
134 }
135 let ray = Ray {
136 origin,
137 direction: direction * (1.0 / length),
138 };
139 let far = origin + ray.direction * max_dist;
140
141 let axis = scene.broadphase.axis();
142 let (low, high) = (
143 origin.get(axis).min(far.get(axis)),
144 origin.get(axis).max(far.get(axis)),
145 );
146
147 let probe = BoundsProbe::new(ray);
148 let mut reach = max_dist;
151 let mut best: Option<(u32, RayHit)> = None;
152 for &slot in scene.broadphase.slab_window(low, high) {
153 let proxy = scene.broadphase.proxy(slot);
154 if !ray_query.mask.interacts_with(proxy.mask) || !probe.reaches(proxy.bounds, reach) {
155 continue;
156 }
157 let Some((_, body)) = candidate(scene.bodies, slot, ray_query.exclude) else {
158 continue;
159 };
160 let found = match body.terrain_index() {
161 Some(index) => field::raycast(scene.fields, index, ray, reach),
162 None => body
163 .convex()
164 .and_then(|shape| ray::cast(ray, shape, pose_of(body), reach)),
165 };
166 let Some(impact) = found else {
167 continue;
168 };
169 if nearer(
170 best.map(|(kept, hit)| (kept, hit.distance)),
171 slot,
172 impact.distance,
173 ) {
174 reach = impact.distance;
175 best = Some((
176 slot,
177 RayHit {
178 point: (origin + ray.direction * impact.distance).to_array(),
179 normal: impact.normal.to_array(),
180 distance: impact.distance,
181 },
182 ));
183 }
184 }
185 best.map(|(_, hit)| hit)
186}
187
188pub(crate) fn shape_cast(scene: Scene<'_>, cast: &ShapeCast) -> Option<ShapeCastHit> {
190 let origin = Vec3::from_array(cast.origin);
191 let motion = Vec3::from_array(cast.motion);
192 if !origin.is_finite() || !motion.is_finite() {
193 return None;
194 }
195 let rotation = Quat::from_euler_deg(cast.euler_deg);
196 let start = Pose {
197 position: origin,
198 rotation,
199 };
200 let moving = Support::new(&cast.shape, start);
201 let start_bounds = shape_bounds(&cast.shape, origin, rotation);
202 let travel =
203 |toi: f32| start_bounds.union(shape_bounds(&cast.shape, origin + motion * toi, rotation));
204 let swept = travel(1.0);
205
206 let axis = scene.broadphase.axis();
207 let mut reach = swept;
210 let mut best: Option<(u32, ShapeCastHit)> = None;
211 for &slot in scene
212 .broadphase
213 .slab_window(swept.min.get(axis), swept.max.get(axis))
214 {
215 let proxy = scene.broadphase.proxy(slot);
216 if !cast.mask.interacts_with(proxy.mask) || !reach.overlaps(proxy.bounds) {
217 continue;
218 }
219 let Some((handle, body)) = candidate(scene.bodies, slot, cast.exclude) else {
220 continue;
221 };
222 let found = match body.terrain_index() {
223 Some(index) => field::sweep(scene.fields, index, &cast.shape, start, motion, reach),
224 None => body.convex().and_then(|shape| {
225 sweep::sweep(&moving, motion, &Support::new(shape, pose_of(body)))
226 }),
227 };
228 let Some(impact) = found else {
229 continue;
230 };
231 if nearer(best.map(|(kept, hit)| (kept, hit.toi)), slot, impact.toi) {
232 reach = travel(impact.toi);
233 best = Some((
234 slot,
235 ShapeCastHit {
236 body: handle,
237 toi: impact.toi,
238 point: impact.point.to_array(),
239 normal: impact.normal.to_array(),
240 gap: impact.gap,
241 started_touching: impact.started_touching,
242 },
243 ));
244 }
245 }
246 best.map(|(_, hit)| hit)
247}
248
249fn candidate(
252 bodies: &Pool<Body>,
253 slot: u32,
254 exclude: Option<BodyHandle>,
255) -> Option<(BodyHandle, &Body)> {
256 let handle = super::world::handle_at(bodies, slot)?;
257 if exclude == Some(handle) {
258 return None;
259 }
260 let body = bodies.get_at(slot as usize)?;
261 if body.is_sensor() {
264 return None;
265 }
266 Some((handle, body))
267}
268
269fn nearer(best: Option<(u32, f32)>, slot: u32, measure: f32) -> bool {
272 match best {
273 None => true,
274 Some((kept, held)) => measure < held || (measure == held && slot < kept),
275 }
276}
277
278fn pose_of(body: &Body) -> Pose {
279 Pose {
280 position: body.position,
281 rotation: body.orientation,
282 }
283}