Skip to main content

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}