Hey fellow coders, 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.
This artifact occurs because the camera has been placed too near the wall. The wall geometry penetrates the near face of the view frustum. To avoid it, our collision zoom algorithm should cast this face instead of a single ray. Let me reiterate: for collision zoom, don't cast a ray. Don't cast a sphere. Cast the near face of the view frustum.
The near face of the view frustum is a rectangle and depends on the near clip distance, field of view, and viewport aspect ratio. Instead of diving into this math, I'll just suggest you google "view frustum corners".
In whatever collision library we're using, we construct a collision shape that matches the face—maybe a convex hull or a box with a shallow depth. We cast that shape, find the hit point, then reposition the camera accordingly.
Now, I have to admit this might be inconvenient in practice. The camera field of view or near clip distance may be changing every frame, in which case our collision shape must be adjusted or recreated every frame. It's also possible that shape-casting is slow or simply not supported in our collision library.
So, I'm also going to suggest an easier alternative. We can approximate this shape-cast with raycasts from the four near corners of the view frustum. My diagram and code snippet illustrate this approach.
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
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);
|R. Hunter Gough|