Skip to main content

linear_sim/
system.rs

1//! Simulation model
2
3use std;
4use log;
5use vec_map::VecMap;
6use rs_utils::for_sequence;
7#[cfg(feature = "derive_serdes")]
8use serde::{Deserialize, Serialize};
9
10use crate::{collision, event, force, geometry, integrator, math, object,
11  Collision, Integrator};
12
13/// A system representing the computational model for the simulation.
14///
15/// There are three categories of entities that are found in the system (model):
16///
17/// - Objects -- `Static`, `Dynamic`, `Nodetect`
18/// - Forces -- `Gravity`
19/// - Constraints -- 'Planar', 'Penetration' (TODO)
20#[cfg_attr(feature = "derive_serdes", derive(Deserialize, Serialize))]
21#[derive(Clone, Debug, Default)]
22pub struct System <INTG : Integrator> {
23  /// Step counter
24  step              : u64,
25  /// No integration
26  objects_static    : VecMap <object::Static>,
27  /// Integration and collision
28  objects_dynamic   : VecMap <object::Dynamic>,
29  /// Integration only; no collision detection or response
30  objects_nodetect  : VecMap <object::Nodetect>,
31  /// Global gravity force
32  gravity           : Option <force::Gravity>,
33  /// Collision subsystem
34  collision         : Collision,
35  #[cfg_attr(feature = "derive_serdes", serde(skip))]
36  _phantom_data     : std::marker::PhantomData <INTG>
37}
38
39impl <INTG : Integrator> System <INTG> {
40  #[inline]
41  pub fn new() -> Self where INTG : Default {
42    Default::default()
43  }
44  #[inline]
45  pub const fn step (&self) -> u64 {
46    self.step
47  }
48  #[inline]
49  pub const fn objects_dynamic (&self) -> &VecMap <object::Dynamic> {
50    &self.objects_dynamic
51  }
52  #[inline]
53  pub const fn objects_nodetect (&self) -> &VecMap <object::Nodetect> {
54    &self.objects_nodetect
55  }
56  #[inline]
57  pub const fn objects_static (&self) -> &VecMap <object::Static> {
58    &self.objects_static
59  }
60
61  #[expect(mismatched_lifetime_syntaxes)]
62  pub fn get_object (&self, id : object::Id) -> object::VariantRef {
63    match id.kind {
64      object::Kind::Static   => (&self.objects_static[id.key.index()]).into(),
65      object::Kind::Dynamic  => (&self.objects_dynamic[id.key.index()]).into(),
66      object::Kind::Nodetect => (&self.objects_nodetect[id.key.index()]).into()
67    }
68  }
69
70  pub fn segment_query (&self,
71    segment     : geometry::Segment3 <f64>,
72    ignore_list : &[object::Id]
73  ) -> Vec <(math::Normalized <f64>, math::Point3 <f64>, object::Id)> {
74    use object::Bounded;
75    let mut out = Vec::<(math::Normalized <f64>, math::Point3 <f64>, object::Id)>::new();
76    let aabb = segment.aabb3();
77    let mut overlaps : Vec <object::Id> = self.collision.broad()
78      .overlaps_discrete (aabb).into_iter().map (object::Id::from).collect();
79    overlaps.retain (|id| !ignore_list.contains (id));
80    for object_id in overlaps {
81      if let Some ((frac, point)) = match object_id.kind {
82        object::Kind::Static  =>
83          self.objects_static[object_id.key.index()].hit_test (segment),
84        object::Kind::Dynamic =>
85          self.objects_dynamic[object_id.key.index()].hit_test (segment),
86        object::Kind::Nodetect => unreachable!()
87      } {
88        out.insert (
89          out.binary_search_by (|(f, _, _)| f.partial_cmp (&frac).unwrap())
90            .unwrap_or_else (|i| i),
91          (frac, point, object_id)
92        );
93      }
94    }
95    out
96  }
97
98  #[expect(clippy::doc_overindented_list_items)]
99  /// Input event handling.
100  ///
101  /// - `CreateObject` -- if an object is found to be in *non-colliding* contact
102  ///    with any other objects, those contacts will be registered with the
103  ///    collision system as *persistent contacts* for the upcoming step
104  /// - `DestroyObject`
105  /// - `SetGravity`
106  /// - `ClearGravity`
107  /// - `Step` -- advance the simulation by a single *timestep*:
108  ///     1. Derivative evaluation: forces are accumulated and acceleration computed
109  ///     2. Velocities are integrated
110  ///     3. Velocity constraints are solved
111  ///     4. Positions are integrated using the new velocity (semi-implicit Euler)
112  ///     5. Position constraints are solved
113  ///     6. Collision detects and resolves collisions in the normalized sub-timestep
114  ///        [0.0-1.0).
115  ///
116  ///        Note that colliding contacts detected at t==1.0 will *not* produce
117  ///        a collision response event (the next step will detect and resolve this
118  ///        collision), however resting or separating contacts detected
119  ///        at t==1.0 will be registered as a *persistent contact* for the next
120  ///        simulation step.
121  pub fn handle_event (&mut self, input : event::Input) -> Vec <event::Output> {
122    let input_step = self.step;
123    log::debug!(step=input_step, input=input.to_string().as_str();
124      "input event");
125    let mut output = Vec::new();
126    match input {
127
128      //
129      // event::Input::CreateObject
130      //
131      event::Input::CreateObject (object, key) => {
132        let key = key.unwrap_or_else (
133          || self.new_object_key (object.kind()).unwrap());
134        match object {
135          object::Variant::Static (object) => {
136            self.create_object_static (object, key, &mut output);
137          }
138          object::Variant::Dynamic (object) => {
139            self.create_object_dynamic (object, key, &mut output);
140          }
141          object::Variant::Nodetect (object) => {
142            self.create_object_nodetect (object, key);
143          }
144        }
145      }
146
147      //
148      // event::Input::ModifyObject
149      //
150      event::Input::ModifyObject (object_modify_event) => {
151        match object_modify_event {
152          event::ObjectModify::Dynamic (key, event) => {
153            self.modify_object_dynamic (key, event);
154          }
155          event::ObjectModify::Static  (key, event) => {
156            self.modify_object_static (key, event);
157          }
158        }
159      }
160
161      //
162      // event::Input::DestroyObject
163      //
164      event::Input::DestroyObject (object::Id { kind, key }) => {
165        match kind {
166          object::Kind::Static => {
167            self.collision.remove_object_static (key);
168            assert!(self.objects_static.remove (key.index()).is_some());
169          }
170          object::Kind::Dynamic => {
171            self.collision.remove_object_dynamic (key);
172            assert!(self.objects_dynamic.remove (key.index()).is_some());
173          }
174          object::Kind::Nodetect =>
175            assert!(self.objects_nodetect.remove (key.index()).is_some())
176        }
177      }
178
179      //
180      // event::Input::SetGravity
181      //
182      event::Input::SetGravity (gravity) => {
183        assert!(self.gravity.is_none());
184        self.gravity = Some (gravity);
185      }
186
187      //
188      // event::Input::ClearGravity
189      //
190      event::Input::ClearGravity => {
191        assert!(self.gravity.is_some());
192        self.gravity = None;
193      }
194
195      //
196      // event::Input::Step
197      //
198      event::Input::Step => {
199        #[cfg(feature = "debug_dump")]
200        let s = self.clone();
201
202        // 1. derivative evaluation (i.e., accumulate forces and compute
203        //    accelerations)
204        self.derivative_evaluation();
205        // 2. integrate velocity
206        self.integrate_velocity();
207        // 3. constrain velocities
208        self.constrain_velocity();
209        // 4. integrate position
210        self.integrate_position();
211        // 5. constrain position
212        self.constrain_position();
213        // 6. detect and solve collisions in a loop
214        self.collision (&mut output);
215
216        #[cfg(feature = "debug_dump")]
217        if collision::DEBUG_DUMP.swap (false, std::sync::atomic::Ordering::SeqCst)
218          && std::env::var ("LINEAR_SIM_DEBUG_DUMP").is_ok()
219        {
220          let p = format!("linear-sim-{}.dump", s.step);
221          log::info!(filepath=p.as_str(); "dumping system");
222          let mut f = std::fs::File::create (&p).unwrap();
223          bincode::serde::encode_into_std_write (&s, &mut f, bincode::config::standard())
224            .unwrap();
225        }
226
227        self.step += 1;
228      }
229
230    }
231    log::debug!(
232      step=input_step,
233      output:?=output.iter().map (ToString::to_string).collect::<Vec<_>>();
234      "output events");
235    output
236  }
237
238  /// Returns `None` if object container is full
239  pub fn new_object_key (&self, kind : object::Kind) -> Option <object::Key> {
240    fn next_available_key <T> (map : &VecMap <T>) -> Option <object::Key> {
241      for i in 0..object::KEY_MAX as usize {
242        if !map.contains_key (i) {
243          return Some (i.into())
244        }
245      }
246      None
247    }
248    match kind {
249      object::Kind::Static   => next_available_key (&self.objects_static),
250      object::Kind::Dynamic  => next_available_key (&self.objects_dynamic),
251      object::Kind::Nodetect => next_available_key (&self.objects_nodetect)
252    }
253  }
254
255  pub fn contacts (&self) -> Vec <Vec <(object::Id, object::Id, collision::Contact)>> {
256    let mut groups = vec![];
257    for (_, group) in self.collision.contact_groups().iter() {
258      let mut v = vec![];
259      for (object_pair, contact) in group.contacts.iter().cloned() {
260        let (object_id_a, object_id_b) = object_pair.into();
261        v.push ((object_id_a.into(), object_id_b.into(), contact));
262      }
263      groups.push (v);
264    }
265    groups
266  }
267
268  //
269  //  private
270  //
271
272  fn create_object_static (&mut self,
273    object : object::Static,
274    key    : object::Key,
275    output : &mut Vec <event::Output>
276  ) {
277    let object_id = object::Id { kind: object::Kind::Static, key };
278    // try to add to collision system: detects intersections in case of failure
279    match self.collision.try_add_object_static (
280      &self.objects_dynamic, &object, key
281    ) {
282      Ok  (()) => {
283        // object successfully added
284        assert!(self.objects_static.insert (key.index(), object).is_none());
285        output.push (event::CreateObjectResult::Created (object_id).into());
286      }
287      Err (intersections) => {
288        debug_assert!(!intersections.is_empty());
289        output.push (
290          event::CreateObjectResult::Intersection (intersections).into());
291      }
292    }
293  }
294
295  fn create_object_dynamic (&mut self,
296    object : object::Dynamic,
297    key    : object::Key,
298    output : &mut Vec <event::Output>
299  ) {
300    let object_id = object::Id { kind: object::Kind::Dynamic, key };
301    // try to add to collision system: detects intersections in case of failure
302    match self.collision.try_add_object_dynamic (
303      &self.objects_static, &self.objects_dynamic, &object, key
304    ) {
305      Ok  (()) => {
306        // object successfully added
307        assert!(self.objects_dynamic.insert (key.index(), object).is_none());
308        output.push (event::CreateObjectResult::Created (object_id).into());
309      }
310      Err (intersections) => {
311        debug_assert!(!intersections.is_empty());
312        output.push (
313          event::CreateObjectResult::Intersection (intersections).into());
314      }
315    }
316  }
317
318  fn create_object_nodetect (&mut self,
319    object : object::Nodetect,
320    key    : object::Key,
321  ) {
322    assert!(self.objects_nodetect.insert (key.index(), object).is_none());
323  }
324
325  /// Panics if object with the given key is not present
326  fn modify_object_dynamic (&mut self,
327    key   : object::Key,
328    event : event::ObjectModifyDynamic
329  ) {
330    let object = &mut self.objects_dynamic[key.index()];
331    match event {
332      event::ObjectModifyDynamic::ApplyImpulse (impulse) =>
333        object.derivatives.velocity += impulse,
334      event::ObjectModifyDynamic::SetForceFlags (force_flags) =>
335        object.derivatives.force_flags = force_flags,
336      event::ObjectModifyDynamic::SetDrag (drag) => object.drag.0 = drag,
337      event::ObjectModifyDynamic::SetPosition (position) => {
338        object.position = position;
339        self.collision.update_object_dynamic (object, key);
340        log::warn!("TODO: dynamic object set position results");
341      }
342    }
343  }
344
345  /// Panics if object with the given key is not present
346  fn modify_object_static (&mut self,
347    key   : object::Key,
348    event : event::ObjectModifyStatic
349  ) {
350    let object = &mut self.objects_static[key.index()];
351    match event {
352      // TODO: has this been tested?
353      event::ObjectModifyStatic::Move (move_vector) => {
354        object.position.0 += move_vector;
355        self.collision.update_object_static (object, key);
356        unimplemented!("TODO: move static object results");
357      }
358    }
359  }
360
361  /// Computes accelerations from accumulated forces
362  fn derivative_evaluation (&mut self) {
363    // clear forces
364    for_sequence!{
365      (_, object) in (
366        self.objects_dynamic.iter_mut(), self.objects_nodetect.iter_mut()
367      ) {
368        object.derivatives.force = [0.0, 0.0, 0.0].into();
369      }
370    }
371
372    // accumulate forces
373    if let Some (gravity) = &self.gravity {
374      for_sequence!{
375        (_, object) in (
376          self.objects_dynamic.iter_mut(), self.objects_nodetect.iter_mut()
377        ) {
378          if object.derivatives.force_flags.contains (force::Flag::Gravity) {
379            use crate::Force;
380            let impulse = gravity.impulse (object, self.step, 1.0);
381            object.derivatives.force += impulse;
382          }
383        }
384      }
385    }
386
387    // compute accelerations from accumulated forces
388    for_sequence!{
389      (_, object) in (
390        self.objects_dynamic.iter_mut(), self.objects_nodetect.iter_mut()
391      ) {
392        use object::Inertial;
393        object.compute_acceleration_inplace();
394      }
395    }
396  }
397
398  /// Integrates velocity from acceleration
399  fn integrate_velocity (&mut self) {
400    for_sequence!{
401      (_, object) in (
402        self.objects_dynamic.iter_mut(), self.objects_nodetect.iter_mut()
403      ) {
404        INTG::integrate_velocity (object);
405        // TODO: we are applying drag here; this is not a physically accurate
406        // drag force, but simply scales the velocity by a fixed factor; is this
407        // the correct place to apply drag?
408        // apply drag
409        object.derivatives.velocity *= 1.0 - object.drag.0;
410      }
411    }
412  }
413
414  /// Solve velocity constraints
415  #[inline]
416  fn constrain_velocity (&mut self) {
417    self.collision
418      .constrain_contact_velocities (&self.objects_static, &mut self.objects_dynamic);
419  }
420
421  /// Integrates position from velocity
422  fn integrate_position (&mut self) {
423    for_sequence!{
424      (_, object) in (
425        self.objects_dynamic.iter_mut(), self.objects_nodetect.iter_mut()
426      ) {
427        INTG::integrate_position (object);
428      }
429    }
430  }
431
432  /// Solve position constraints
433  #[inline]
434  fn constrain_position (&mut self) {
435    self.collision
436      .constrain_contact_positions (&self.objects_static, &mut self.objects_dynamic);
437  }
438
439  /// Collision detection and response
440  fn collision (&mut self, output : &mut Vec <event::Output>) {
441    self.collision.detect_resolve_loop (
442      &mut self.objects_static, &mut self.objects_dynamic, self.step, output);
443  }
444
445}
446
447/// Print system size information
448pub fn report_sizes() {
449  use std::mem::size_of;
450  println!("system report sizes...");
451
452  println!("  size of System <integrator::SemiImplicitEuler>: {}",
453    size_of::<System <integrator::SemiImplicitEuler>>());
454
455  println!("...system report sizes");
456}
457
458#[cfg(test)]
459mod tests {
460  use super::*;
461  use crate::component;
462  use crate::geometry::{self, shape};
463  #[test]
464  fn segment_query() {
465    let segment = geometry::Segment3::noisy (
466      [-1.0, 0.0, 0.5].into(),
467      [ 2.0, 0.0, 0.5].into());
468    let mut system = System::<integrator::SemiImplicitEuler>::new();
469    let sphere1 = {
470      let position    = component::Position ([0.5, 0.0, 0.5].into());
471      let mass        = component::Mass::new (20.0);
472      let derivatives = component::Derivatives::zero();
473      let drag        = component::Drag::zero();
474      let bound       = component::Bound::from (shape::Sphere::noisy (0.5));
475      let material    = component::MATERIAL_STONE;
476      let collidable  = true;
477      object::Dynamic {
478        position, mass, derivatives, drag, bound, material, collidable
479      }.into()
480    };
481    let mut result = system.handle_event (event::Input::CreateObject (sphere1, None));
482    let event::Output::CreateObjectResult (
483      event::CreateObjectResult::Created (sphere1_id)
484    ) = result.pop().unwrap() else { unreachable!() };
485    assert!(result.pop().is_none());
486    let block1 = {
487      let position   = component::Position ([1.5, 0.0, 0.5].into());
488      let bound      = component::Bound    (
489        shape::Bounded::from (shape::Cuboid::noisy ([0.5, 0.5, 0.5])).into());
490      let material   = component::MATERIAL_STONE;
491      let collidable = true;
492      object::Static { position, bound, material, collidable }.into()
493    };
494    let mut result = system.handle_event (event::Input::CreateObject (block1, None));
495    let event::Output::CreateObjectResult (
496      event::CreateObjectResult::Created (block1_id)
497    ) = result.pop().unwrap() else { unreachable!() };
498    assert!(result.pop().is_none());
499    let result = system.segment_query (segment, &[]);
500    let (frac, point, id) = result.first().unwrap();
501    assert_eq!(**frac, 1.0/3.0);
502    assert_eq!(*point, [0.0, 0.0, 0.5].into());
503    assert_eq!(*id, sphere1_id);
504    let (frac, point, id) = result.get (1).unwrap();
505    assert_eq!(**frac, 2.0/3.0);
506    assert_eq!(*point, [1.0, 0.0, 0.5].into());
507    assert_eq!(*id, block1_id);
508  }
509}