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

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