I recently joined a few of my gamedev friends to work on a stealth quadcopter-piloting game named FLY2K. Since graphics are my primary area of interest, I decided to dive in by fixing some issues with the volumetric vision cones used to indicate a camera’s field of view. Their appearance is nothing fancy – they’re basically just yellow cones. Here’s what one of the cones looks like in our test level.
The purpose of this visualization is to show where the camera is looking so that the player can avoid being detected (and thus avoid having an army of guard drones set upon him).
In its original form, the cone was just a yellow cone with a standard transparent shader on it. It worked decently… until you actually entered the camera’s field of vision:
As you can see, you start getting horrible clipping due to backface culling. What we want to happen is for the player’s field of view to be filled with color to create the illusion that the cone is some sort of volumetric field instead of just a hollow geometric object.
The Naive Solution
You’re probably thinking that we can easily solve this problem by just turning off backface culling. You’re wrong, but don’t worry – that was my first thought too. The issue is that this is what actually happens when you do that:
There are a couple of issues here. First, and most obviously, the area of the cone that clips through the floor is hidden by the floor, which is shaded in its original grey color. The second issue is that the entirety of the cone’s interior is shaded and lit, which makes it obvious that you’re inside a hollow object. Illusion…. RUINED.
Let’s go over the solution at a very high level. Here’s the full code for the shader, in case you want to follow along.
Fixing the Lighting Issue
The lighting issue is the easier of the two problems to fix. To do so, we need to render the inside of the cone differently from the exterior of the cone. This can be easily achieved with a multi-pass shader. In the first pass, have backface culling on (via the “Cull Back” flag) and perform standard lighting. In the second pass, cull frontfaces instead (via “Cull Front”) and don’t do any lighting. Ez pz.
Fixing the Clipping Issue
This is a bit trickier. To make the interior’s volumetric color show through clipped areas (such as the area occluded by the floor in the previous image), I set ZTest to “Always” on the interior pass to make sure that the inside of the cone never gets clipped.
The issue with this, of course, is that this means that the inside of the cone will be visible through walls and also through the exterior of the cone, which is clearly undesirable.
To fix this, I employed a little-known but incredibly useful shader feature: stencil buffers. Stencil buffers are really handy any time you want to use one object to determine what parts of another object get rendered. They’re great for any masking-related functionality that you want to achieve.
To prevent the now-unclipped cone interiors from showing up through walls and cone exteriors, I set up a stencil rule for the interior shader pass to not render any pixels that have already been rendered to by an exterior cone shader pass. This is effectively telling the shader “don’t render any interiors that are between the player’s POV and any cone exteriors”, which results in the interior shader only rendering when the player is looking at it from within the cone. Here’s the stencil code for the exterior pass
and the interior pass
There are a few interesting things going on here.
- I’m referring to a value named [_StencilID] in both blocks – the brackets are just the syntax for using an external property in the stencil buffer. This value is just a float named _StencilID that I pass into the shader. I use a different stencil ID for each cone because I don’t want cone exteriors to clip each other (or else things look weird when you see a cone through another cone).
- Notice I have “ZFail Replace” in the exterior shader stencil block. This tells the shader to set the stencil value even if the cone exterior is occluded by other geometry, such as a wall. If I don’t do this, then the interior clipping won’t work for cones that are behind other objects and the interiors will thus show through. This is because, by default, the stencil value will only get written if the depth test is successful. This line tells the shader to write the stencil value no matter what.
In a nutshell, this is the logic: first, write a unique stencil value into the stencil buffer when rendering the cone exterior (a different value per cone). When rendering the interior, clip it if the cone’s stencil ID is written in the buffer since this means that we’re looking at the interior of the cone through its exterior. Otherwise, render the cone no matter what’s occluding it, since we’re inside the cone and we don’t want any occluding geometry to mess with the volumetric illusion.
Here are the vision cones working in their full glory: