Skip to main content

linear_sim/
system.rs

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