dodgy/
lib.rs

1#![doc = include_str!("../README.md")]
2// The contents of this file were primarily ported from Agent.cc from RVO2 with
3// significant alterations. As per the Apache-2.0 license, the original
4// copyright notice has been included, excluding those notices that do not
5// pertain to the derivate work:
6//
7// Agent.cc
8// RVO2 Library
9//
10// SPDX-FileCopyrightText: 2008 University of North Carolina at Chapel Hill
11//
12// The authors may be contacted via:
13//
14// Jur van den Berg, Stephen J. Guy, Jamie Snape, Ming C. Lin, Dinesh Manocha
15// Dept. of Computer Science
16// 201 S. Columbia St.
17// Frederick P. Brooks, Jr. Computer Science Bldg.
18// Chapel Hill, N.C. 27599-3175
19// United States of America
20//
21// <https://gamma.cs.unc.edu/RVO2/>
22mod common;
23mod linear_programming;
24mod obstacles;
25mod simulator;
26mod visibility_set;
27
28pub use glam::Vec2;
29
30use common::*;
31use linear_programming::{solve_linear_program, Line};
32use obstacles::get_lines_for_agent_to_obstacle;
33
34pub use obstacles::Obstacle;
35
36pub use simulator::{AgentParameters, Simulator, SimulatorMargin};
37pub use visibility_set::VisibilitySet;
38
39// A single agent in the simulation.
40#[derive(Clone, PartialEq, Debug)]
41pub struct Agent {
42  // The position of the agent.
43  pub position: Vec2,
44  // The current velocity of the agent.
45  pub velocity: Vec2,
46
47  // The radius of the agent. Agents will use this to avoid bumping into each
48  // other.
49  pub radius: f32,
50  // The maximum velocity the agent is allowed to move at.
51  pub max_velocity: f32,
52
53  // The amount of responsibility an agent has to avoid other agents. The
54  // amount of avoidance between two agents is then dependent on the ratio of
55  // the responsibility between the agents. Note this does not affect
56  // avoidance of obstacles.
57  pub avoidance_responsibility: f32,
58}
59
60// Parameters for computing the avoidance vector.
61#[derive(Clone, PartialEq, Debug)]
62pub struct AvoidanceOptions {
63  // The distance that the agent must be from any obstacle. This is commonly
64  // the agent's radius to ensure the agent never intersects the obstacle (for
65  // example a wall). An alternative is to set this to a small value to treat
66  // obstacles as the edge of something (like a cliff).
67  pub obstacle_margin: f32,
68  // How long in the future should collisions be considered between agents.
69  pub time_horizon: f32,
70  // How long in the future should collisions be considered for obstacles.
71  pub obstacle_time_horizon: f32,
72}
73
74impl Agent {
75  // Computes a velocity based off the agent's preferred velocity (usually the
76  // direction to its current goal/waypoint). This new velocity is intended to
77  // avoid running into the agent's `neighbours`. This is not always possible,
78  // but agents will attempt to resolve any collisions in a reasonable fashion.
79  // The `time_step` helps determine the velocity in cases of existing
80  // collisions, and must be positive.
81  pub fn compute_avoiding_velocity(
82    &self,
83    neighbours: &[&Agent],
84    obstacles: &[&Obstacle],
85    preferred_velocity: Vec2,
86    time_step: f32,
87    avoidance_options: &AvoidanceOptions,
88  ) -> Vec2 {
89    assert!(time_step > 0.0, "time_step must be positive, was {}", time_step);
90
91    let lines = obstacles
92      .iter()
93      .flat_map(|o| {
94        get_lines_for_agent_to_obstacle(
95          self,
96          o,
97          avoidance_options.obstacle_margin,
98          avoidance_options.obstacle_time_horizon,
99        )
100      })
101      .chain(neighbours.iter().map(|neighbour| {
102        self.get_line_for_neighbour(
103          neighbour,
104          avoidance_options.time_horizon,
105          time_step,
106        )
107      }))
108      .collect::<Vec<Line>>();
109
110    // Since each neighbour generates one line, the number of obstacle lines is
111    // just the other lines.
112    let obstacle_line_count = lines.len() - neighbours.len();
113
114    solve_linear_program(
115      &lines,
116      obstacle_line_count,
117      self.max_velocity,
118      preferred_velocity,
119    )
120  }
121
122  // Creates a line to describe the half-plane of valid velocities that should
123  // not collide with `neighbour`.
124  fn get_line_for_neighbour(
125    &self,
126    neighbour: &Agent,
127    time_horizon: f32,
128    time_step: f32,
129  ) -> Line {
130    // There are two parts to the velocity obstacle induced by `neighbour`.
131    // 1) The cut-off circle. This is where the agent collides with `neighbour`
132    // after some time (either `time_horizon` or `time_step`).
133    // 2) The cut-off shadow. Any velocity that is just scaled up from a
134    // velocity in the cut-off circle will also hit `neighbour`.
135    //
136    // If the relative position and velocity is used, the cut-off for the shadow
137    // will be directed toward the origin.
138
139    let relative_neighbour_position = neighbour.position - self.position;
140    let relative_agent_velocity = self.velocity - neighbour.velocity;
141
142    let distance_squared = relative_neighbour_position.length_squared();
143
144    let sum_radius = self.radius + neighbour.radius;
145    let sum_radius_squared = sum_radius * sum_radius;
146
147    let vo_normal;
148    let relative_velocity_projected_to_vo;
149    let inside_vo;
150
151    // Find out if the agent is inside the cut-off circle. Note: since both the
152    // distance to the cut-off circle and the radius of the cut-off circle is
153    // scaled by `time_horizon` (or `time_step` depending on the situation),
154    // factoring out those terms and cancelling yields this simpler expression.
155    if distance_squared > sum_radius_squared {
156      // No collision, so either project on to the cut-off circle, or the
157      // cut-off shadow.
158      //
159      // The edges of the cut-off shadow lies along the tangents of the circle
160      // that intersects the origin (since the tangents are the lines that just
161      // graze the cut-off circle and so these line divide the "shadowed"
162      // velocities from the "unshadowed" velocities).
163      //
164      // Since the shadows are caused by the tangent lines, velocities should be
165      // projected to the cut-off circle when they are on one-side of the
166      // tangent points, and should be projected to the shadow when on the
167      // other-side of the tangent points.
168
169      let cutoff_circle_center = relative_neighbour_position / time_horizon;
170      let cutoff_circle_center_to_relative_velocity =
171        relative_agent_velocity - cutoff_circle_center;
172      let cutoff_circle_center_to_relative_velocity_length_squared =
173        cutoff_circle_center_to_relative_velocity.length_squared();
174
175      let dot = cutoff_circle_center_to_relative_velocity
176        .dot(relative_neighbour_position);
177
178      // TODO: Figure out why this works. Something to do with circle tangents,
179      // right triangles with those tangents, and the angle between
180      // `cutoff_circle_center_to_relative_velocity` and
181      // `relative_neighbour_position`.
182      if dot < 0.0
183        && dot * dot
184          > sum_radius_squared
185            * cutoff_circle_center_to_relative_velocity_length_squared
186      {
187        // The relative velocity has not gone past the cut-off circle tangent
188        // points yet, so project onto the cut-off circle.
189
190        let cutoff_circle_radius = sum_radius / time_horizon;
191
192        vo_normal =
193          cutoff_circle_center_to_relative_velocity.normalize_or_zero();
194        relative_velocity_projected_to_vo =
195          vo_normal * cutoff_circle_radius + cutoff_circle_center;
196        inside_vo = cutoff_circle_center_to_relative_velocity_length_squared
197          < cutoff_circle_radius * cutoff_circle_radius;
198      } else {
199        // The relative velocity is past the cut-off circle tangent points, so
200        // project onto the shadow.
201
202        let tangent_triangle_leg =
203          (distance_squared - sum_radius_squared).sqrt();
204
205        // Consider the right-triangle describing the tangent point (one side
206        // has length `sum_radius`, hypotenuse has side length
207        // `cutoff_circle_center_to_relative_velocity_length_squared`). The last
208        // side will have length `tangent_triangle_leg`. A similar triangle can
209        // then be created using the same triangle leg lengths, but oriented
210        // such that the hypotenuse is in the direction of the tangent and
211        // composed of directions `relative_position` and the perpendicular of
212        // `relative_position`.
213
214        // Determine whether the relative velocity is nearer the left or right
215        // side of the shadow.
216        let tangent_side = determinant(
217          relative_neighbour_position,
218          cutoff_circle_center_to_relative_velocity,
219        )
220        .signum();
221
222        // Compute the shadow direction using the tangent triangle legs, and
223        // make sure to use the correct orientation of that direction (the
224        // correct side of the line is invalid).
225        let shadow_direction =
226          relative_neighbour_position * tangent_triangle_leg * tangent_side
227            + relative_neighbour_position.perp() * sum_radius;
228
229        // Renormalize the shadow direction.
230        let shadow_direction = shadow_direction / distance_squared;
231
232        vo_normal = shadow_direction.perp();
233        // Project onto the shadow.
234        relative_velocity_projected_to_vo =
235          relative_agent_velocity.project_onto_normalized(shadow_direction);
236        // The velocity is inside the VO if it is to the left of the left
237        // shadow, or the right of the right shadow.
238        inside_vo = determinant(relative_agent_velocity, shadow_direction)
239          * tangent_side
240          >= 0.0;
241      }
242    } else {
243      // Collision. Project on cut-off circle at time `time_step`.
244
245      // Find the velocity such that after `time_step` the agent would be at the
246      // neighbours position.
247      let cutoff_circle_center = relative_neighbour_position / time_step;
248      let cutoff_circle_radius = sum_radius / time_step;
249
250      // The direction of the velocity from `cutoff_circle_center` is therefore
251      // the normal to the velocity obstacle.
252      vo_normal =
253        (relative_agent_velocity - cutoff_circle_center).normalize_or_zero();
254      // Get the point on the cut-off circle in that direction (which is the
255      // agent's velocity projected to the circle).
256      relative_velocity_projected_to_vo =
257        vo_normal * cutoff_circle_radius + cutoff_circle_center;
258      inside_vo = true;
259    }
260
261    // As in the paper, `u` is the vector from the relative velocity to the
262    // nearest point outside the velocity obstacle.
263    let u = relative_velocity_projected_to_vo - relative_agent_velocity;
264
265    let responsibility = if inside_vo {
266      self.avoidance_responsibility
267        / (self.avoidance_responsibility + neighbour.avoidance_responsibility)
268    } else {
269      1.0
270    };
271
272    Line {
273      point: self.velocity + u * responsibility,
274      direction: -vo_normal.perp(),
275    }
276  }
277}
278
279#[cfg(test)]
280mod tests {
281  use super::*;
282
283  mod get_line_for_neighbour_tests {
284    use glam::Vec2;
285
286    use super::{Agent, Line};
287
288    macro_rules! assert_line_eq {
289      ($a: expr, $b: expr) => {{
290        let a = $a;
291        let b = $b;
292
293        assert!(
294          a.point.distance_squared(b.point) < 1e-5,
295          "\n  left: {:?}\n right: {:?}",
296          a,
297          b
298        );
299        assert!(
300          a.direction.distance_squared(b.direction) < 1e-5,
301          "\n  left: {:?}\n right: {:?}",
302          a,
303          b
304        );
305      }};
306    }
307
308    #[test]
309    fn velocity_projects_on_cutoff_circle() {
310      let position = Vec2::new(1.0, 2.0);
311      let radius = 2.0;
312
313      let agent = Agent {
314        position: Vec2::ZERO,
315        velocity: Vec2::ZERO,
316        radius: radius - 1.0,
317        avoidance_responsibility: 1.0,
318        max_velocity: 0.0,
319      };
320
321      let neighbour = Agent {
322        position: position,
323        velocity: Vec2::ZERO,
324        radius: 1.0,
325        avoidance_responsibility: 1.0,
326        max_velocity: 0.0,
327      };
328
329      let actual_line = agent.get_line_for_neighbour(
330        &neighbour, /* time_horizon= */ 1.0, /* time_step= */ 1.0,
331      );
332      // The agent's velocity projects directly onto the cut-off circle.
333      assert_line_eq!(
334        actual_line,
335        Line {
336          point: position.normalize() * (position.length() - radius),
337          direction: position.perp().normalize(),
338        }
339      );
340    }
341
342    #[test]
343    fn velocity_projects_to_shadow() {
344      let mut agent = Agent {
345        position: Vec2::ZERO,
346        velocity: Vec2::new(-1.0, 3.0),
347        radius: 1.0,
348        max_velocity: 0.0,
349        avoidance_responsibility: 1.0,
350      };
351
352      let neighbour = Agent {
353        position: Vec2::new(2.0, 2.0),
354        velocity: Vec2::ZERO,
355        radius: 1.0,
356        max_velocity: 0.0,
357        avoidance_responsibility: 1.0,
358      };
359
360      let inside_shadow_line = agent.get_line_for_neighbour(
361        &neighbour, /* time_horizon= */ 1.0, /* time_step= */ 1.0,
362      );
363      assert_line_eq!(
364        inside_shadow_line,
365        Line { point: Vec2::new(0.0, 3.0), direction: Vec2::new(0.0, 1.0) }
366      );
367
368      agent.velocity = Vec2::new(10.0, -1.0);
369
370      let outside_shadow_line = agent.get_line_for_neighbour(
371        &neighbour, /* time_horizon= */ 1.0, /* time_step= */ 1.0,
372      );
373      assert_line_eq!(
374        outside_shadow_line,
375        Line { point: Vec2::new(10.0, -0.5), direction: Vec2::new(-1.0, 0.0) }
376      );
377    }
378
379    #[test]
380    fn collision_uses_time_step() {
381      let agent = Agent {
382        position: Vec2::ZERO,
383        velocity: Vec2::new(0.0, 0.0),
384        radius: 2.0,
385        max_velocity: 0.0,
386        avoidance_responsibility: 1.0,
387      };
388
389      let neighbour = Agent {
390        position: Vec2::new(2.0, 2.0),
391        velocity: Vec2::ZERO,
392        radius: 2.0,
393        max_velocity: 0.0,
394        avoidance_responsibility: 1.0,
395      };
396
397      let collision_line = agent.get_line_for_neighbour(
398        &neighbour, /* time_horizon= */ 1.0, /* time_step= */ 0.5,
399      );
400      assert_line_eq!(
401        collision_line,
402        Line {
403          point: (Vec2::ONE.normalize() * -8.0 + Vec2::new(4.0, 4.0)) * 0.5,
404          direction: Vec2::new(-1.0, 1.0).normalize(),
405        }
406      );
407    }
408
409    #[test]
410    fn no_collision_uses_time_horizon() {
411      let agent = Agent {
412        position: Vec2::ZERO,
413        velocity: Vec2::new(0.0, 0.0),
414        radius: 1.0,
415        max_velocity: 0.0,
416        avoidance_responsibility: 1.0,
417      };
418
419      let neighbour = Agent {
420        position: Vec2::new(2.0, 2.0),
421        velocity: Vec2::ZERO,
422        radius: 1.0,
423        max_velocity: 0.0,
424        avoidance_responsibility: 1.0,
425      };
426
427      let collision_line = agent.get_line_for_neighbour(
428        &neighbour, /* time_horizon= */ 2.0, /* time_step= */ 0.5,
429      );
430      assert_line_eq!(
431        collision_line,
432        Line {
433          point: -Vec2::ONE.normalize() + Vec2::new(1.0, 1.0),
434          direction: Vec2::new(-1.0, 1.0).normalize(),
435        }
436      );
437    }
438
439    #[test]
440    fn uses_avoidance_responsibility() {
441      let agent = Agent {
442        position: Vec2::ZERO,
443        velocity: Vec2::new(1.5, 0.0),
444        radius: 1.0,
445        avoidance_responsibility: 1.0,
446        max_velocity: 0.0,
447      };
448
449      let neighbour = Agent {
450        position: Vec2::new(4.0, 0.0),
451        velocity: Vec2::ZERO,
452        radius: 1.0,
453        avoidance_responsibility: 3.0,
454        max_velocity: 0.0,
455      };
456
457      let actual_line = agent.get_line_for_neighbour(
458        &neighbour, /* time_horizon= */ 2.0, /* time_step= */ 0.5,
459      );
460      assert_line_eq!(
461        actual_line,
462        Line { point: Vec2::new(1.375, 0.0), direction: Vec2::new(0.0, 1.0) }
463      );
464    }
465
466    #[test]
467    fn uses_avoidance_responsibility_only_when_inside_vo() {
468      let agent = Agent {
469        position: Vec2::ZERO,
470        velocity: Vec2::new(0.5, 0.0),
471        radius: 1.0,
472        avoidance_responsibility: 1.0,
473        max_velocity: 0.0,
474      };
475
476      let neighbour = Agent {
477        position: Vec2::new(4.0, 0.0),
478        velocity: Vec2::ZERO,
479        radius: 1.0,
480        avoidance_responsibility: 3.0,
481        max_velocity: 0.0,
482      };
483
484      let actual_line = agent.get_line_for_neighbour(
485        &neighbour, /* time_horizon= */ 2.0, /* time_step= */ 0.5,
486      );
487      assert_line_eq!(
488        actual_line,
489        Line { point: Vec2::new(1.0, 0.0), direction: Vec2::new(0.0, 1.0) }
490      );
491    }
492  }
493}