Implementing Attractive Fog of War in Unity

One of the earliest features that I implemented for Gridpulse Legions was fog of war. My intention was for the feature to work similarly to how it works in the original Starcraft; that is to say, it should satisfy the following criteria:

  • Areas in the line of sight of the player’s units are not covered by fog.
  • Areas that are not in the line of sight of the player’s units, but which have been explored previously, are covered by semi-transparent fog (you can see the terrain and any static structures/elements that were in that are last time you were there, but you cannot see any new non-allied units that are there).
  • Fog covering unexplored areas is completely opaque – you cannot see anything there except for a thick black shroud.

At a high level, the solution I came up with was to render the visibility information into a RenderTexture, which is then drawn onto the terrain using a projector through a shader that takes care of rendering and blending the shroud.

Here’s a video of the feature in action:

Let’s go over each of the components in turn.

Recording Visible Areas

The first piece of information that we’ll need is the visible areas – that is, the areas that are currently within the line of sight of the player’s units. My solution involved two steps:

  1. Generate a mesh representing the visible range of each unit.
  2. Render these visibility meshes to a RenderTexture.

The visibility mesh generation can be as simple as a static mesh with its parent set to the unit whose visibility it is representing (so that it automatically moves with that unit). If your line of sight algorithm is more complex and takes elevation and obstacles into account, you may needed to procedurally update your mesh on the fly using a 2D visibility algorithm. The specifics of these algorithms are beyond the scope of this article and will differ per game, but for simplicity’s sake, you can just assume that this mesh is simply a static flat mesh floating above the unit’s head. It makes no difference to the rest of the implementation how the mesh is generated.

Place these meshes on their own layer. We’ll call this layer FOV and we’ll use it in the next step. Make sure that the main camera isn’t rendering this layer. Here’s what it’ll look like in the scene:

vision_mesh

This mesh represents the visible range for a single unit.

Camera Setup

Once you have your visibility meshes, you’ll need to set up two cameras to render these meshes, each to their own render texture. Why two cameras? Because we’ll need a separate texture to display unexplored areas (completely opaque black) from the one used to display discovered areas that are not currently within the line of sight (semi-transparent black). Ultimately, we want to render textures that look like this

shroud

, which we will then project onto the terrain using a Projector component. The pink areas in the image above correspond to the visibility meshes.

We’ll render these textures by creating dedicated cameras for fog of war and configuring them to only render the visibility mesh layer. Our shader will look at this image and will project a black shroud onto the areas where the pink is not present and will ignore the areas where the pink is present.

First, create two new RenderTextures. You can actually get away with using fairly low-res RenderTextures if you turn anti-aliasing on. In Gridpulse Legions, the RenderTexture for the dark shroud (the shroud for unexplored areas) is 64×64. The RenderTexture for the normal shroud is 128×128.

RT_editor

Set up the camera size/position so that it covers the entire map and configure it to only render the FOV layer. Also set the Target Texture to the corresponding RenderTexture.

Fog_Cams

Both cameras will be identical in setup except for two things:

  1. The camera for the dark shroud will have its clear mode set to Don’t Clear. As you’ll see a little bit later on, the openings in the fog will correspond to the locations of the visibility meshes within the camera’s viewport. As the visibility meshes move around, the RenderTextures will change to reflect the new locations of the meshes. But for the dark shroud, we actually want areas that have been explored to remain uncovered. See this image for clarification – notice that the dark shroud shows a superset of the visible areas that the normal shroud shows
    • shrouds_sidebyside
  2. The target RenderTextures will be different. The camera for the dark shroud will render to the dark shroud RenderTexture and vice versa for the other camera.

Rendering the Fog

With the above setup, you should now have two RenderTextures accessible during runtime whose pixels directly represent the areas of your map that need to be covered in fog. The next step is to actually draw the fog. For this, we’ll need a few different things:

  1. A shader for rendering the fog.
  2. Two materials, one for each fog type. Each material will have a different opacity value that will be used when rendering the fog (100% for the dark shroud and some lower value for the normal shroud).
  3. A script, which will control the blending of the fog.
  4. Two projectors, one for each fog type. Each projector will project its fog onto the map through the material described in step #2.

Let’s take a look at that shader. I’ll show just the fragment shader below, since the rest of it is just boilerplate shader code for projectors (see the full code snippet here).

fixed4 frag (v2f i) : SV_Target
{
    float aPrev = tex2Dproj(_PrevTexture, i.uv).a;
    float aCurr = tex2Dproj(_CurrTexture, i.uv).a;

    _Color.a -= lerp(aPrev, aCurr, _Blend);
    return _Color;
}

As you can see, it’s using the alpha values of the shroud textures to figure out where to render the fog. When the pixel corresponds to an area of the shroud texture where the visibility mesh was rendered, the alpha will be 1 and the _Color variable will become transparent, resulting in the fog not being visible at that pixel.

You’re probably wondering now why we have _PrevTexture and a _CurrTexture and are lerping between the two instead of just using a single shroud texture for the lookup. This is a special technique for making the actual revealing/hiding of the fog look very smooth. This, plus the anti-aliasing, lets you achieve the illusion of smooth fog despite your fog texture not having nearly enough pixels to represent every visible spot on the map. As you’ll see in the next code snippet, each projector actually creates two textures internally – one for the previous visibility state and one for the current state. The script will lerp between the two and, upon completing the lerp, will blit the texture for the current state into the texture for the previous state and will blit the texture from the corresponding fog camera’s target texture into the texture for the current state. After doing this swap, it will repeat the process.

using System.Collections;
using UnityEngine;

public class FogProjector : MonoBehaviour
{
    public Material projectorMaterial;
    public float blendSpeed;
    public int textureScale;

    public RenderTexture fogTexture;

    private RenderTexture prevTexture;
    private RenderTexture currTexture;
    private Projector projector;

    private float blendAmount;

    private void Awake ()
    {
        projector = GetComponent();
        projector.enabled = true;

        prevTexture = GenerateTexture();
        currTexture = GenerateTexture();

        // Projector materials aren't instanced, resulting in the material asset getting changed.
        // Instance it here to prevent us from having to check in or discard these changes manually.
        projector.material = new Material(projectorMaterial);

        projector.material.SetTexture("_PrevTexture", prevTexture);
        projector.material.SetTexture("_CurrTexture", currTexture);

        StartNewBlend();
    }

    RenderTexture GenerateTexture()
    {
        RenderTexture rt = new RenderTexture(
            fogTexture.width * textureScale,
            fogTexture.height * textureScale,
            0,
            fogTexture.format) { filterMode = FilterMode.Bilinear };
        rt.antiAliasing = fogTexture.antiAliasing;

        return rt;
    }

    public void StartNewBlend()
    {
        StopCoroutine(BlendFog());
        blendAmount = 0;
        // Swap the textures
        Graphics.Blit(currTexture, prevTexture);
        Graphics.Blit(fogTexture, currTexture);

        StartCoroutine(BlendFog());
    }

    IEnumerator BlendFog()
    {
        while (blendAmount < 1)
        {
            // increase the interpolation amount
            blendAmount += Time.deltaTime * blendSpeed;
            // Set the blend property so the shader knows how much to lerp
            // by when checking the alpha value
            projector.material.SetFloat("_Blend", blendAmount);
            yield return null;
        }
        // once finished blending, swap the textures and start a new blend
        StartNewBlend();
    }
}

Both projectors will have this component. The script just creates two inner textures and does a continuous blend between the two textures by increasing the interpolation amount in a coroutine and feeding this value to the shader, which uses this float to lerp between the alphas from the previous and current textures.

Here’s what the actual projector component will look like in the inspector (make sure you configure it to cover your map and to project onto the correct layers based on your game’s specific needs):

projectors

Here’s what the two shroud materials look like. Notice that they differ only in the color (notice the white bars under the color, which represent the alpha, are different lengths):

projector_mats

The texture fields are empty because those are created in script.

With these components in your project/scene, you now have everything you need to get the fog working your game. The fog cameras will render the visibility meshes to the shroud textures. The projectors will then use these textures to render the shroud onto the terrain, blending the fog for smooth animation.

And there you have it. Attractive fog of war in Unity.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s