Accurate Collision Zoom for Cameras

Original Author: Eric Undersander

Figure 1 - Camera, lookat target, and obstacle

Here’s the takeaway of this whole post: For camera collision zoom, don’t cast a ray. Don’t cast a sphere. Cast the near face of the view frustum.

Now, let’s start from the beginning. Consider the typical third-person camera: a lookat target (often the player character) and an offset to the camera. We never want to lose sight of the player, so how do we handle obstacles like walls that get in the way? One solution is to move the camera in towards the player, past all obstacles—this is collision zoom.

We can implement collision zoom with a single raycast, backward from the lookat target to the desired camera position. If the ray hits anything, we move the camera to the hit point.

This approach mostly works but it’s not entirely accurate. Many gamers will recognize this particular artifact: stand near a wall, rotate your camera near the wall, and sometimes you’ll get a glimpse into the adjacent room.

For collision zoom, regardless of nearby obstacles, we don’t want to push the near clip plane into the player character’s face. This minimum offset distance is represented in the diagram as the gray camera near the player’s head. In code, it’s the variable minOffsetDist.

So by casting the near face of the view frustum (or rays approximating it), we avoid the earlier wall artifact. This approach has another consequence, perhaps unexpected: in the last diagram, the final camera position (in orange) is placed inside the obstacle. This is okay because the obstacle is still behind the near face of the view frustum. We’ll have an unobstructed view of the player.

Actually, as for the camera being placed inside the obstacle, it’s not just okay—it’s ideal. Our collision zoom algorithm should move the camera no closer to the player than absolutely necessary. In a confined space like an interior hallway or stairwell, even a few inches, gained by the greater accuracy of this approach, can make a difference in the usability of the camera.

Finally, if you opt for the four raycasts instead of the shape-cast, be aware of the downside of this approximation. You may get some occasional visual artifacts depending on your game’s collision geometry. You can mitigate this by some judicious use of padding/fudging in your raycasts (not shown in the code snippet).

// returns a new camera position
 
  Vec3 HandleCollisionZoom(const Vec3& camPos, const Vec3& targetPos, 
 
      float minOffsetDist, const Vec3* frustumNearCorners)
 
  {
 
      float offsetDist = Length(targetPos - camPos);
 
      float raycastLength = offsetDist - minOffsetDist;
 
      if (raycastLength < 0.f)
 
      {
 
          // camera is already too near the lookat target
 
          return camPos;
 
      }
 
  
 
      Vec3 camOut = Normalize(targetPos - camPos);
 
      Vec3 nearestCamPos = targetPos - camOut * minOffsetDist;
 
      float minHitFraction = 1.f;
 
  
 
      for (int i = 0; i < 4; i++)
 
      {
 
          const Vec3& corner = frustumNearCorners[i];
 
          Vec3 offsetToCorner = corner - camPos;
 
          Vec3 rayStart = nearestCamPos + offsetToCorner;
 
          Vec3 rayEnd = corner;
 
          // a result between 0 and 1 indicates a hit along the ray segment
 
          float hitFraction = CastRay(rayStart, rayEnd);
 
          minHitFraction = Min(hitFraction, minHitFraction);
 
      }        
 
  
 
      if (minHitFraction < 1.f)
 
      {
 
          return nearestCamPos - camOut * (raycastLength * minHitFraction);
 
      }
 
      else
 
      {
 
          return camPos;
 
      }
 
  }