Preventing Third-person Camera Clipping


Animated gif of third-person camera animation

Third-person camera implementations are always tricky to get right. It’s not hard to get a camera to follow a character around at a fixed distance and angle, but then you have to deal with those gosh-darn walls that are everywhere.

There’s a few solutions to this problem, and the one we chose for our project is to push the camera forward whenever a wall is detected. The algorithm is simple and goes like this:

  1. Raycast from the player to the desired camera location (player position + separation distance).
  2. If the raycast hits a wall, put the camera at the raycast hit point.
  3. If the raycast does not hit a wall, put the camera at the original desired location.

This works… almost. A problem manifests when the player is very close to a wall, and is caused by using a raycast in step 1.

Here’s a screenshot of the problem:

Camera clipping through a wall in the game view

We can see through the wall to the right. A quick look at the scene view shows us why this is happening:

Camera clipping through a wall in the scene view; the gizmo shows the near-clip plane going through the wall

Since the camera is right up against the wall, the near-clip plane of the camera is clipping through the wall.

With this knowledge in mind: what we really want is a boxcast, not a raycast. We’ll use a box the size of the camera’s near-clip plane and cast that from the player to the wall to detect where we should place the camera.

Here’s the code for getting the size of the near-clip from the camera, and translating that into the half-extents that Physics.BoxCast needs:

private Vector3 GetNearClipPlaneHalfExtents()
{
    float ratio = camera.pixelWidth / camera.pixelHeight;
    Vector3[] frustumCorners = new Vector3[4];
    camera.CalculateFrustumCorners(new Rect(0, 0, 1, 1), camera.nearClipPlane, Camera.MonoOrStereoscopicEye.Mono, frustumCorners);

    Vector2 min = frustumCorners[0];
    Vector2 max = frustumCorners[0];
    for (int i = 1; i < 4; i++)
    {
        min.x = Mathf.Min(min.x, frustumCorners[i].x);
        min.y = Mathf.Min(min.y, frustumCorners[i].y);
        max.x = Mathf.Max(max.x, frustumCorners[i].x);
        max.y = Mathf.Max(max.y, frustumCorners[i].y);
    }

    // Note the very small z-value since boxcast doesn't take a plane, it takes a box
    // This can be adjusted if you need more tolerance
    return new Vector3((max.x - min.x) / 2.0f, (max.y - min.y) / 2.0f, 0.01f);
}

We can pass this box to Physics.BoxCast and use the output to position our camera. But wait - we can no longer use RaycastHit.point, as this will return the point where the box intersects the wall. We’ll be back to the same problem!

What we want instead is the center point of the plane where the box intersected the wall. To obtain this, we can use RaycastHit.distance, as well as the input boxcast ray.

if (wallInWay)
{
    desiredPosition = ray.origin + ray.direction.normalized * raycastHit.distance
}
else
{
    desiredPosition = ray.origin + ray.direction.normalized * idealSeparationDistance
}

With this calculation, our camera no longer clips through the wall:

Camera no longer clips through wall, as shown by the gizmo in the scene view