Skip to main content

linear_sim/
system.rs

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