alice_physics/sensors.rs
1//! Simulated sensors over a [`PhysicsWorld`]: a scanning range finder (lidar), a
2//! contact sensor and an inertial measurement unit (IMU), each with optional
3//! deterministic Gaussian noise.
4//!
5//! Every reading is a function of the world state, the sensor's settings and, for
6//! noise, a seeded [`DeterministicRng`]: the same scene and seed give the same bits
7//! on every platform.
8//!
9//! # Lidar
10//!
11//! [`Lidar`] casts one ray per point of an azimuth × elevation grid with
12//! [`PhysicsWorld::ray_caster`] (the body BVH is built once per scan), against the
13//! geometry bodies actually collide as (see [`crate::shape_raycast`]). In the
14//! sensor's frame a ray of azimuth `a` and elevation `e` points along
15//!
16//! ```text
17//! (cos e · cos a, sin e, cos e · sin a)
18//! ```
19//!
20//! so azimuth turns from `+X` toward `+Z` and elevation lifts toward `+Y`. Angles
21//! are spaced evenly from the minimum to the maximum inclusive
22//! (`min + (max − min)·i / (n − 1)`; a count of 1 is the minimum alone). Rays are
23//! cast and stored elevation-major: index `e_i · azimuth_count + a_i`.
24//!
25//! # Contact sensor
26//!
27//! [`ContactSensor`] reads the contact constraints of the last step that involve
28//! its body (the solver keeps those of the last substep) and the normal force of
29// LIMITATION(COV-SENSE-034): the contact sensor does not report contacts with static colliders (resolved by projection, no contact constraint)
30//! each, as [`PhysicsWorld::contact_forces`] computes it. A contact with a static
31//! collider ([`crate::static_collider`]) is resolved by projection, not by a
32//! contact constraint, and a sensor body makes no constraints: neither is
33// LIMITATION(COV-SENSE-035): a sleeping body reads no contact even while it rests on another body
34//! reported. A body that has gone to sleep has no contacts to report.
35//!
36//! # IMU
37//!
38//! [`Imu`] differences the body's linear velocity between two samples, so it
39//! reports the mean acceleration over the step. The accelerometer reading is the
40//! **specific force**: the acceleration minus the world's gravity
41//! (`config.gravity`), in the body's frame. A body at rest under gravity
42//! `(0, −g, 0)` (a static body included) reads `(0, +g, 0)` turned into its frame;
43//! a body in free fall reads zero. A body's `gravity_scale` is not gravity to the
44//! sensor: the part of its weight beyond `1 ×` (or missing below it) acts like any
45//! other applied force, so a body falling at `gravity_scale = 2` reads `(0, −g, 0)`.
46//! The gyroscope reading is the body's angular velocity in its own frame, as the
47//! solver keeps it (the XPBD backend recovers it from each substep's rotation, to
48//! `O((ω·h)²)` relative).
49//!
50//! # Noise
51//!
52//! [`GaussianNoise`] adds `σ·z` with `z` from [`DeterministicRng::next_gaussian`]
53//! (Box–Muller in `Fix128`; see that method for the precision and the 9.4 σ
54//! range). A noisy sensor draws a fixed number of values per reading, so its
55//! stream does not depend on what the reading was.
56//!
57//! Author: Moroya Sakamoto
58
59use crate::math::{Fix128, QuatFix, Vec3Fix};
60use crate::rng::DeterministicRng;
61use crate::shape_raycast::{RayFilter, WorldRayHit};
62use crate::solver::PhysicsWorld;
63
64#[cfg(not(feature = "std"))]
65use alloc::{vec, vec::Vec};
66
67/// Additive zero-mean Gaussian noise with its own seeded generator.
68#[derive(Clone, Debug)]
69pub struct GaussianNoise {
70 /// Standard deviation of the added value.
71 pub std_dev: Fix128,
72 rng: DeterministicRng,
73}
74
75impl GaussianNoise {
76 /// Noise of standard deviation `std_dev`, seeded with `seed`.
77 #[must_use]
78 pub fn new(std_dev: Fix128, seed: u64) -> Self {
79 Self {
80 std_dev,
81 rng: DeterministicRng::new(seed),
82 }
83 }
84
85 /// One draw `σ·z`.
86 pub fn sample(&mut self) -> Fix128 {
87 self.std_dev * self.rng.next_gaussian()
88 }
89
90 /// `value + σ·z`.
91 pub fn apply(&mut self, value: Fix128) -> Fix128 {
92 value + self.sample()
93 }
94
95 /// Each component plus its own draw (x, y, z in that order).
96 pub fn apply_vec(&mut self, v: Vec3Fix) -> Vec3Fix {
97 let x = self.apply(v.x);
98 let y = self.apply(v.y);
99 let z = self.apply(v.z);
100 Vec3Fix::new(x, y, z)
101 }
102}
103
104/// Evenly spaced angles from `min` to `max` inclusive.
105fn angle_grid(min: Fix128, max: Fix128, count: usize) -> Vec<Fix128> {
106 match count {
107 0 => Vec::new(),
108 1 => vec![min],
109 n => {
110 let span = max - min;
111 let last = Fix128::from_int((n - 1) as i64);
112 (0..n)
113 .map(|i| min + span * Fix128::from_int(i as i64) / last)
114 .collect()
115 }
116 }
117}
118
119/// A scanning range finder: one ray per azimuth × elevation grid point (see the
120/// module doc for the frame and the order).
121#[derive(Clone, Debug)]
122pub struct Lidar {
123 /// Where the sensor is: in the world for [`Self::scan`], in the body's frame
124 /// (relative to its position) for [`Self::scan_from_body`].
125 pub position: Vec3Fix,
126 /// How the sensor is turned, in the same frame as `position`.
127 pub rotation: QuatFix,
128 /// Longest range measured; farther surfaces read as no return.
129 pub max_range: Fix128,
130 /// Which colliders the rays see.
131 pub filter: RayFilter,
132 azimuths: Vec<Fix128>,
133 elevations: Vec<Fix128>,
134 noise: Option<GaussianNoise>,
135}
136
137/// One lidar scan.
138#[derive(Clone, Debug, PartialEq, Eq)]
139#[non_exhaustive]
140pub struct LidarScan {
141 /// Number of azimuth steps (the inner index).
142 pub azimuth_count: usize,
143 /// Number of elevation steps (the outer index).
144 pub elevation_count: usize,
145 /// Measured range of each ray (noise included), `None` for no return.
146 pub ranges: Vec<Option<Fix128>>,
147 /// The exact hit of each ray (no noise), `None` for no return.
148 pub hits: Vec<Option<WorldRayHit>>,
149}
150
151impl LidarScan {
152 /// The range at one grid point, `None` for no return or an index out of range.
153 #[must_use]
154 pub fn range(&self, azimuth_index: usize, elevation_index: usize) -> Option<Fix128> {
155 if azimuth_index >= self.azimuth_count {
156 return None;
157 }
158 self.ranges
159 .get(elevation_index * self.azimuth_count + azimuth_index)
160 .copied()
161 .flatten()
162 }
163}
164
165impl Lidar {
166 /// A lidar at the origin, unturned, with `azimuth_count` azimuths over
167 /// `[azimuth_min, azimuth_max]` and `elevation_count` elevations over
168 /// `[elevation_min, elevation_max]` (radians), seeing up to `max_range` with the
169 /// default [`RayFilter`] and no noise.
170 #[must_use]
171 pub fn new(
172 (azimuth_min, azimuth_max, azimuth_count): (Fix128, Fix128, usize),
173 (elevation_min, elevation_max, elevation_count): (Fix128, Fix128, usize),
174 max_range: Fix128,
175 ) -> Self {
176 Self {
177 position: Vec3Fix::ZERO,
178 rotation: QuatFix::IDENTITY,
179 max_range,
180 filter: RayFilter::default(),
181 azimuths: angle_grid(azimuth_min, azimuth_max, azimuth_count),
182 elevations: angle_grid(elevation_min, elevation_max, elevation_count),
183 noise: None,
184 }
185 }
186
187 /// Place the sensor (see [`Self::position`] for the frame).
188 #[must_use]
189 pub fn with_pose(mut self, position: Vec3Fix, rotation: QuatFix) -> Self {
190 self.position = position;
191 self.rotation = rotation;
192 self
193 }
194
195 /// Which colliders the rays see.
196 #[must_use]
197 pub fn with_filter(mut self, filter: RayFilter) -> Self {
198 self.filter = filter;
199 self
200 }
201
202 /// Add range noise: one draw per ray, in scan order, misses included; a noisy
203 /// range is clamped to `[0, max_range]`.
204 #[must_use]
205 pub fn with_noise(mut self, noise: GaussianNoise) -> Self {
206 self.noise = Some(noise);
207 self
208 }
209
210 /// The unit ray directions in the sensor's frame, in scan order.
211 #[must_use]
212 pub fn directions(&self) -> Vec<Vec3Fix> {
213 let mut dirs = Vec::with_capacity(self.azimuths.len() * self.elevations.len());
214 for &e in &self.elevations {
215 let (sin_e, cos_e) = e.sin_cos();
216 for &a in &self.azimuths {
217 let (sin_a, cos_a) = a.sin_cos();
218 dirs.push(Vec3Fix::new(cos_e * cos_a, sin_e, cos_e * sin_a));
219 }
220 }
221 dirs
222 }
223
224 /// Scan from the sensor's pose in the world.
225 pub fn scan(&mut self, world: &PhysicsWorld) -> LidarScan {
226 self.scan_at(world, self.position, self.rotation, self.filter)
227 }
228
229 /// Scan from the sensor mounted on `body` (its pose relative to the body), not
230 /// seeing that body. `None` for an index that is not a body.
231 pub fn scan_from_body(&mut self, world: &PhysicsWorld, body: usize) -> Option<LidarScan> {
232 let b = world.bodies.get(body)?;
233 let position = b.position + b.rotation.rotate_vec(self.position);
234 let rotation = b.rotation.mul(self.rotation);
235 let filter = self.filter.excluding_body(body);
236 Some(self.scan_at(world, position, rotation, filter))
237 }
238
239 fn scan_at(
240 &mut self,
241 world: &PhysicsWorld,
242 position: Vec3Fix,
243 rotation: QuatFix,
244 filter: RayFilter,
245 ) -> LidarScan {
246 let caster = world.ray_caster(filter);
247 let mut ranges = Vec::new();
248 let mut hits = Vec::new();
249 for dir in self.directions() {
250 let hit = caster.closest(position, rotation.rotate_vec(dir), self.max_range);
251 let mut range = hit.map(|h| h.t);
252 if let Some(noise) = self.noise.as_mut() {
253 let draw = noise.sample();
254 range = range.map(|r| clamp(r + draw, Fix128::ZERO, self.max_range));
255 }
256 ranges.push(range);
257 hits.push(hit);
258 }
259 LidarScan {
260 azimuth_count: self.azimuths.len(),
261 elevation_count: self.elevations.len(),
262 ranges,
263 hits,
264 }
265 }
266}
267
268fn clamp(x: Fix128, lo: Fix128, hi: Fix128) -> Fix128 {
269 if x < lo {
270 lo
271 } else if x > hi {
272 hi
273 } else {
274 x
275 }
276}
277
278/// Reports whether a body is touching anything and how hard it is pressed.
279#[derive(Clone, Debug)]
280pub struct ContactSensor {
281 /// The body sensed.
282 pub body: usize,
283 noise: Option<GaussianNoise>,
284}
285
286/// One contact-sensor reading.
287#[derive(Clone, Copy, Debug, PartialEq, Eq)]
288#[non_exhaustive]
289pub struct ContactReading {
290 /// Whether the body has at least one contact.
291 pub in_contact: bool,
292 /// Number of contacts.
293 pub contact_count: usize,
294 /// Sum of the normal-force magnitudes on the body (noise included, clamped at
295 /// zero).
296 pub normal_force: Fix128,
297 /// Vector sum of the normal forces on the body (no noise).
298 pub net_force: Vec3Fix,
299}
300
301impl ContactSensor {
302 /// A sensor on `body`, without noise.
303 #[must_use]
304 pub const fn new(body: usize) -> Self {
305 Self { body, noise: None }
306 }
307
308 /// Add noise to the normal-force sum: one draw per reading.
309 #[must_use]
310 pub fn with_noise(mut self, noise: GaussianNoise) -> Self {
311 self.noise = Some(noise);
312 self
313 }
314
315 /// The reading after a step of `dt` (the `dt` passed to
316 /// [`PhysicsWorld::step`]; the forces are computed from it).
317 pub fn read(&mut self, world: &PhysicsWorld, dt: Fix128) -> ContactReading {
318 let mut count = 0usize;
319 let mut sum = Fix128::ZERO;
320 let mut net = Vec3Fix::ZERO;
321 for c in &world.contact_constraints {
322 // The normal points from B to A and the force acts on A.
323 let sign = if c.body_a == self.body {
324 Fix128::ONE
325 } else if c.body_b == self.body {
326 Fix128::NEG_ONE
327 } else {
328 continue;
329 };
330 count += 1;
331 if let Some(force) = world.contact_constraint_force(c, dt) {
332 sum = sum + force;
333 net = net + c.contact.normal * (force * sign);
334 }
335 }
336 if let Some(noise) = self.noise.as_mut() {
337 sum = clamp(
338 noise.apply(sum),
339 Fix128::ZERO,
340 Fix128::from_raw(i64::MAX, u64::MAX),
341 );
342 }
343 ContactReading {
344 in_contact: count > 0,
345 contact_count: count,
346 normal_force: sum,
347 net_force: net,
348 }
349 }
350}
351
352/// An inertial measurement unit on one body (see the module doc).
353#[derive(Clone, Debug)]
354pub struct Imu {
355 /// The body carrying the unit.
356 pub body: usize,
357 prev_velocity: Vec3Fix,
358 accel_noise: Option<GaussianNoise>,
359 gyro_noise: Option<GaussianNoise>,
360}
361
362/// One IMU reading.
363#[derive(Clone, Copy, Debug, PartialEq, Eq)]
364#[non_exhaustive]
365pub struct ImuReading {
366 /// Mean linear acceleration over the step, world frame (no noise).
367 pub acceleration: Vec3Fix,
368 /// Accelerometer: acceleration minus gravity, body frame (noise included).
369 pub specific_force: Vec3Fix,
370 /// Gyroscope: angular velocity, body frame (noise included).
371 pub angular_velocity: Vec3Fix,
372}
373
374impl Imu {
375 /// A unit on `body`, starting from its current velocity. `None` for an index
376 /// that is not a body.
377 #[must_use]
378 pub fn new(world: &PhysicsWorld, body: usize) -> Option<Self> {
379 let b = world.bodies.get(body)?;
380 Some(Self {
381 body,
382 prev_velocity: b.velocity,
383 accel_noise: None,
384 gyro_noise: None,
385 })
386 }
387
388 /// Add accelerometer noise: three draws per reading (x, y, z).
389 #[must_use]
390 pub fn with_accel_noise(mut self, noise: GaussianNoise) -> Self {
391 self.accel_noise = Some(noise);
392 self
393 }
394
395 /// Add gyroscope noise: three draws per reading (x, y, z).
396 #[must_use]
397 pub fn with_gyro_noise(mut self, noise: GaussianNoise) -> Self {
398 self.gyro_noise = Some(noise);
399 self
400 }
401
402 /// The reading after a step of `dt`; the current velocity becomes the reference
403 /// for the next one. `None` (and nothing changes) when the body is gone or
404 /// `dt ≤ 0`.
405 pub fn sample(&mut self, world: &PhysicsWorld, dt: Fix128) -> Option<ImuReading> {
406 if dt <= Fix128::ZERO {
407 return None;
408 }
409 let b = world.bodies.get(self.body)?;
410 let acceleration = (b.velocity - self.prev_velocity) / dt;
411 self.prev_velocity = b.velocity;
412 let to_body = b.rotation.conjugate();
413 let mut specific_force = to_body.rotate_vec(acceleration - world.config.gravity);
414 let mut angular_velocity = to_body.rotate_vec(b.angular_velocity);
415 if let Some(noise) = self.accel_noise.as_mut() {
416 specific_force = noise.apply_vec(specific_force);
417 }
418 if let Some(noise) = self.gyro_noise.as_mut() {
419 angular_velocity = noise.apply_vec(angular_velocity);
420 }
421 Some(ImuReading {
422 acceleration,
423 specific_force,
424 angular_velocity,
425 })
426 }
427}
428
429/// The sensors read through the world: each method passes the world to the
430/// sensor's own method and returns what it returns, advancing the sensor's state
431/// (noise stream, IMU reference velocity) the same way.
432impl PhysicsWorld {
433 /// One scan of `lidar` from its pose in the world ([`Lidar::scan`]).
434 pub fn lidar_scan(&self, lidar: &mut Lidar) -> LidarScan {
435 lidar.scan(self)
436 }
437
438 /// One scan of `lidar` mounted on `body`, not seeing that body
439 /// ([`Lidar::scan_from_body`]). `None` for an index that is not a body.
440 pub fn lidar_scan_from_body(&self, lidar: &mut Lidar, body: usize) -> Option<LidarScan> {
441 lidar.scan_from_body(self, body)
442 }
443
444 /// The contact-sensor reading after a step of `dt` ([`ContactSensor::read`]).
445 pub fn read_contact_sensor(&self, sensor: &mut ContactSensor, dt: Fix128) -> ContactReading {
446 sensor.read(self, dt)
447 }
448
449 /// The IMU reading after a step of `dt` ([`Imu::sample`]): `None` when the
450 /// body is gone or `dt ≤ 0`.
451 pub fn sample_imu(&self, imu: &mut Imu, dt: Fix128) -> Option<ImuReading> {
452 imu.sample(self, dt)
453 }
454}