Module moog.physics.collisions
Collision forces.
The main class in this file is Collision. It simulates Newtonian collisions between two sprites. These collisions take into account the angles of the sprite edges/vertices at the point of contact to realistically simulate the collision. By default they respect Newtonian mechanics for rotation as well, taking into consideration the moments of inertia of the sprites. However, there is an option to ignore this and prohibit the collision from changing the angular velocity of the sprites.
WARNING: Sprites must be star-shaped with respect to their center of mass (i.e. the line fron the center of mass to any point in the sprite never exits the sprite) for guaranteed correct collisions. If they are not star-shaped, there can be rare events where two sprites collide at a corner and the collision is not simulated correctly.
Classes
class Collision (elasticity=1.0, symmetric=False, update_angle_vel=True, max_recursion_depth=0)
-
Expand source code
class Collision(abstract_force.AbstractForce): """Collision simulator. This class simulates Newtonian collisions between two rigid bodies, namely sprites in the environment. The physics assumes that the sprites have uniform density, as this is important for calculating their moments of inertia (see ../sprite for details). """ def __init__(self, elasticity=1., symmetric=False, update_angle_vel=True, max_recursion_depth=0): """Constructor. Args: elasticity: Float in [0, 1]. Elasticity 1 means the sprites bounce fully. Elasticity 0 means they stick together completely. symmetric: Bool. If True, collision is symmetric and both sprites are updated. If False, sprite_1 in .step() is treated as having infinite mass. This is useful for modeling bounces off of obstructors/walls that are fixed in the environment. update_angle_vel: Bool. If True, fully simulate the rotational mechanics and update the sprites angular velocity. If False, sprite angular velocities are not updated and the collision is treated as if the contact point was between the two sprites' centers of mass. Simulating the full rotational mechanics is slightly slower. max_recursion_depth: Int. Number of times self.step() may recurse. Usually, 0 (no recursion) is fine. However, larger values make the collisions slightly more accurate (at the expense of runtime efficiency) if multiple collisions are happening within a single step, e.g. colliding into a corner. """ self._elasticity = elasticity self._symmetric = symmetric self._update_angle_vel = update_angle_vel self._max_recursion_depth = max_recursion_depth def step(self, sprite_0, sprite_1, updates_per_env_step, recursion_depth=0): """Step the physics. Args: sprite_0: Instance of ../sprite.Sprite. If self._symmetric = False, this sprite is the one that is doing the colliding, i.e. this one's velocity changes. sprite_1: Instance of ../sprite.Sprite. If self._symmetric = False, this sprite is treated as having infinite mass. updates_per_env_step: Int. Number of times this force step is called for each step of the physics in the environment. This is used here for inferring the exact contact point in the collision. recursion_depth: Int. Number of times this function has recursed. Used internally only to catch and avoid max recursion depth errors. """ if recursion_depth > self._max_recursion_depth: return if sprite_0 == sprite_1: return if not sprite_0.overlaps_sprite(sprite_1): return delta_t = 1. / updates_per_env_step # First get collision point, normal, and relative sprite displacement # since the collision collision_point, collision_normal, _, perpendicular = ( _get_collision_vectors(sprite_0, sprite_1, delta_t)) if collision_point is None: # Although the sprites do overlap, neither sprite contains any of # the other's vertices. This can happen when the sprites collide # exactly at two corners. This is annoying to handle because we must # infer which corner hit which face in between timesteps, but must # be done to ensure stability. See self._make_disjoint() for # details. self._make_disjoint(sprite_0, sprite_1) else: # Whether the collision point is in the future, in which case we # leave the sprites alone. future_collision_point = ( np.isscalar(collision_normal) and np.isnan(collision_normal)) if not future_collision_point: # There was a collision in the past, so we must simulate it # First, displace the sprites so they are no longer intersecting # Note that instead of perpendicular displacement we could use # since_collision. However, sometimes since_collision can have a # very acute angle and large magnitude due to angular velocity # or multiple collisions, so in prectice displacing by the # perpendicular is more stable. if self._symmetric: sprite_0.position = ( sprite_0.position - (0.5 + _EPSILON) * perpendicular) sprite_1.position = ( sprite_1.position + (0.5 + _EPSILON) * perpendicular) else: sprite_0.position = ( sprite_0.position - (1. + _EPSILON) * perpendicular) # Second, change sprite velocities and angular velocities per # Newtonian physics if self._update_angle_vel: _collide_with_update_angle_vel( sprite_0, sprite_1, collision_point, collision_normal, elasticity=self._elasticity, symmetric=self._symmetric, ) else: _collide_without_update_angle_vel( sprite_0, sprite_1, collision_point, collision_normal, elasticity=self._elasticity, symmetric=self._symmetric, ) else: return # After perturbing the sprite positions and velocities, we recursively # step again in case that perturbation has now created another # collision. self.step(sprite_0, sprite_1, updates_per_env_step, recursion_depth=recursion_depth + 1) def _make_disjoint(self, sprite_0, sprite_1): """Perturb the positions of sprite_0 and sprite_1 to make them disjoint. In theory, with infinitesimal delta_t, the collision detection would work without this correction since every collision occurs by a vertex of one sprite entering another sprite. However, with a non-zero delta_t, if a collision occurs almost exactly at two sharp corners of sprites, the vertex entrypoint could be missed due to the time discretization. So this correction will catch that case and perturb the sprites so they no longer intersect. Furthermore, sometimes if collisions with multiple sprites happen within one physics timestep, the collision misses one of them, so this perturbation comes in handy then to essentially push one of the collisions off until the next timestep. Finally, sometimes a sprite position may be macigally changed (e.g. by a discrete action space) to make it enter another sprite without moving there by its physics, so this correction is useful to separate the two overlapping sprites in that case as well. The way this function works is a bit ugly and not easy to describe. If you want to understand it, please draw out some examples on paper and see what the code does with them. Args: sprite_0: Instance of ../sprite.Sprite. sprite_1: Instance of ../sprite.Sprite. Returns: Boolean indicating whether the sprites were overlapping. This is useful for the calling code to know whether the sprie positions have been perturbed. """ crossing_points, inds_crossings = sprite_lib.sprite_edge_crossings( sprite_0, sprite_1) if len(inds_crossings) <= 1: return True # correction_i is how much sprite_i must move (independent of sprite_j) # for the two to be disjoint. correction_0 = _position_correction( crossing_points, sprite_0, inds_crossings[:, 0], sprite_1, inds_crossings[:, 1]) correction_1 = _position_correction( crossing_points, sprite_1, inds_crossings[:, 1], sprite_0, inds_crossings[:, 0]) if np.linalg.norm(correction_0) > np.linalg.norm(correction_1): correction = -1 * (1 + _EPSILON) * correction_0 else: correction = (1 + _EPSILON) * correction_0 if not all(np.isfinite(correction)): # no correction needed correction = np.zeros(2) if self._symmetric: sprite_0.position = sprite_0.position + 0.5 * correction sprite_1.position = sprite_1.position - 0.5 * correction else: sprite_0.position = sprite_0.position + correction return False
Collision simulator.
This class simulates Newtonian collisions between two rigid bodies, namely sprites in the environment. The physics assumes that the sprites have uniform density, as this is important for calculating their moments of inertia (see ../sprite for details).
Constructor.
Args
elasticity
- Float in [0, 1]. Elasticity 1 means the sprites bounce fully. Elasticity 0 means they stick together completely.
symmetric
- Bool. If True, collision is symmetric and both sprites are updated. If False, sprite_1 in .step() is treated as having infinite mass. This is useful for modeling bounces off of obstructors/walls that are fixed in the environment.
update_angle_vel
- Bool. If True, fully simulate the rotational mechanics and update the sprites angular velocity. If False, sprite angular velocities are not updated and the collision is treated as if the contact point was between the two sprites' centers of mass. Simulating the full rotational mechanics is slightly slower.
max_recursion_depth
- Int. Number of times self.step() may recurse. Usually, 0 (no recursion) is fine. However, larger values make the collisions slightly more accurate (at the expense of runtime efficiency) if multiple collisions are happening within a single step, e.g. colliding into a corner.
Ancestors
- AbstractForce
- abc.ABC
Methods
def step(self, sprite_0, sprite_1, updates_per_env_step, recursion_depth=0)
-
Expand source code
def step(self, sprite_0, sprite_1, updates_per_env_step, recursion_depth=0): """Step the physics. Args: sprite_0: Instance of ../sprite.Sprite. If self._symmetric = False, this sprite is the one that is doing the colliding, i.e. this one's velocity changes. sprite_1: Instance of ../sprite.Sprite. If self._symmetric = False, this sprite is treated as having infinite mass. updates_per_env_step: Int. Number of times this force step is called for each step of the physics in the environment. This is used here for inferring the exact contact point in the collision. recursion_depth: Int. Number of times this function has recursed. Used internally only to catch and avoid max recursion depth errors. """ if recursion_depth > self._max_recursion_depth: return if sprite_0 == sprite_1: return if not sprite_0.overlaps_sprite(sprite_1): return delta_t = 1. / updates_per_env_step # First get collision point, normal, and relative sprite displacement # since the collision collision_point, collision_normal, _, perpendicular = ( _get_collision_vectors(sprite_0, sprite_1, delta_t)) if collision_point is None: # Although the sprites do overlap, neither sprite contains any of # the other's vertices. This can happen when the sprites collide # exactly at two corners. This is annoying to handle because we must # infer which corner hit which face in between timesteps, but must # be done to ensure stability. See self._make_disjoint() for # details. self._make_disjoint(sprite_0, sprite_1) else: # Whether the collision point is in the future, in which case we # leave the sprites alone. future_collision_point = ( np.isscalar(collision_normal) and np.isnan(collision_normal)) if not future_collision_point: # There was a collision in the past, so we must simulate it # First, displace the sprites so they are no longer intersecting # Note that instead of perpendicular displacement we could use # since_collision. However, sometimes since_collision can have a # very acute angle and large magnitude due to angular velocity # or multiple collisions, so in prectice displacing by the # perpendicular is more stable. if self._symmetric: sprite_0.position = ( sprite_0.position - (0.5 + _EPSILON) * perpendicular) sprite_1.position = ( sprite_1.position + (0.5 + _EPSILON) * perpendicular) else: sprite_0.position = ( sprite_0.position - (1. + _EPSILON) * perpendicular) # Second, change sprite velocities and angular velocities per # Newtonian physics if self._update_angle_vel: _collide_with_update_angle_vel( sprite_0, sprite_1, collision_point, collision_normal, elasticity=self._elasticity, symmetric=self._symmetric, ) else: _collide_without_update_angle_vel( sprite_0, sprite_1, collision_point, collision_normal, elasticity=self._elasticity, symmetric=self._symmetric, ) else: return # After perturbing the sprite positions and velocities, we recursively # step again in case that perturbation has now created another # collision. self.step(sprite_0, sprite_1, updates_per_env_step, recursion_depth=recursion_depth + 1)
Step the physics.
Args
sprite_0
- Instance of ../sprite.Sprite. If self._symmetric = False, this sprite is the one that is doing the colliding, i.e. this one's velocity changes.
sprite_1
- Instance of ../sprite.Sprite. If self._symmetric = False, this sprite is treated as having infinite mass.
updates_per_env_step
- Int. Number of times this force step is called for each step of the physics in the environment. This is used here for inferring the exact contact point in the collision.
recursion_depth
- Int. Number of times this function has recursed. Used internally only to catch and avoid max recursion depth errors.
Inherited members