Fog of War in Unity REDUX part 2 of 2

Posted On: Feb 07, 2017

Welcome back, today we are going to go over height checking on the terrain. I don't have much else to talk about so let's get to it!

First you want to export your terrain data as a raw image, I realized that I should have just had a reference to the terrain after this was done if you want to change it later I'll point you in the right direction at the end of the post. So with your terrain selected click the gear icon and scroll down to the "Heightmap" section where there is an "Import Raw" and "Export Raw".

Terrain inspector values

Click "Export Raw" and select 16 bit depth and windows byte order. Save that file somewhere in your assets directory, I have mine in "Assets/TerrainData/Height" along with the terrain data. You'll need to change the file extension in the explorer/finder to ".txt" instead of ".raw" this is so we can read the data easily in Unity. With that complete lets look at the code!

In the FogOfWar class we added some variables:

#region Private
private const int MAX_TERRAIN_HEIGHT = 600;

[SerializeField]
private List _revealers;
[SerializeField]
private int _textureWidth;
[SerializeField]
private int _textureHeight;
[SerializeField]
private Vector2 _mapSize;
[SerializeField]
private Material _fogMaterial;
[SerializeField]
private TextAsset _heightMap;
[SerializeField]
private int _heightMapWidth;
[SerializeField]
private int _heightMapHeight;

private Texture2D _shadowMap;
private Color32[] _pixels;
private int[] _heightMapData;
#endregion

We added a constant int of the max terrain height. This can be found in the Terrains settings (see above, 600 is default). I renamed the old "_width" and "_height" variables to "_textureWidth" and "_textureHeight" respectively. Then we added the _heightMap TextAsset under the _fogMaterial, added the _heightMapWidth and _heightMapHeight (which is the terrains width + 1 and length + 1), then we added a non-serialized int array for our height map data.

private void Awake()
{
	_shadowMap = new Texture2D(_textureWidth, _textureHeight, TextureFormat.RGB24, false);

	_pixels = _shadowMap.GetPixels32();

	for (var i = 0; i < _pixels.Length; ++i)
	{
		_pixels[i] = Color.black;
	}

	_shadowMap.SetPixels32(_pixels);
	_shadowMap.Apply();

	_fogMaterial.SetTexture("_ShadowMap", _shadowMap);

	byte[] heightBytes = _heightMap.bytes;
	_heightMapData = new int[heightBytes.Length / 2];
		
	var j = 0;
	for (var i = 0; i < heightBytes.Length && j < _heightMapData.Length; i+=2, ++j)
	{
		_heightMapData[j] = (heightBytes[i + 1] << 0x08) | heightBytes[i];
	}
}

Here is the new awake, the creation of the shadow map is only different because we renamed those two variables. The new stuff is after we set the shadow texture to the fog material. First we get all the bytes from the text asset and store them in memory (don't iterate over the _heightMap.bytes because that will access the hard drive every time). After we get the bytes we initialize our _heightMapData, then we iterate over all the bytes and convert them to a short because we saved the raw file as 16 bit depth (if you selected 8 bits then I don't think you'll have to do anything). I don't want to explain bit shifting because I get confused every time I do, so if you want to learn more about this stuff then check out the wiki.

I refactored DrawFilledMidpointCircleSinglePixelVisit a bit:

private void DrawFilledMidpointCircleSinglePixelVisit(Vector3 position, int radius)
{
	int x = Mathf.RoundToInt(radius * (_textureWidth / _mapSize.x));
	int y = 0;
	int radiusError = 1 - x;

	var centerX = Mathf.RoundToInt(position.x * (_textureWidth / _mapSize.x));
	var centerY = Mathf.RoundToInt(position.z * (_textureHeight / _mapSize.y));

	while (x >= y)
	{
		int startX = -x + centerX;
		int endX = x + centerX;
		FillRow(startX, endX, y + centerY, (int)position.y);
		if (y != 0)
		{
			FillRow(startX, endX, -y + centerY, (int)position.y);
		}

		++y;

		if (radiusError < 0)
		{
			radiusError += 2 * y + 1;
		}
		else
		{
			if (x >= y)
			{
				startX = -y + 1 + centerX;
				endX = y - 1 + centerX;
				FillRow(startX, endX, x + centerY, (int)position.y);
				FillRow(startX, endX, -x + centerY, (int)position.y);
			}
			--x;
			radiusError += 2 * (y - x + 1);
		}
	}
}

I ended up needing the height of the revealer so I just passed in its position. So centerX and centerY just covert the world position to pixel space and we added the height to the FillRow method:

private void FillRow(int startX, int endX, int row, int height)
{
	int index;
	for (var x = startX; x < endX; ++x)
	{
		index = x + row * _textureWidth;
		if (index > -1 && index < _pixels.Length && HeightCheck(x, row, height))
		{
			_pixels[index].r = 255;
			_pixels[index].g = 255;
		}
	}
}

private bool HeightCheck(int x, int y, int height)
{
	if (_textureWidth != _heightMapWidth-1 && _textureHeight != _heightMapHeight-1)
	{
		var widthRatio = (float)_heightMapWidth / _textureWidth;
		var heightRatio = (float)_heightMapHeight / _textureHeight;

		x = (int)(x * widthRatio);
		y = (int)(y * heightRatio);
	}

	if (y * _heightMapWidth + x > _heightMapData.Length || y * _heightMapWidth + x < 0)
	{
		return false;
	}

	var convertedHeight = ((float)height / MAX_TERRAIN_HEIGHT) * ushort.MaxValue;
		
	return convertedHeight > _heightMapData[y * _heightMapWidth + x];
}

In the FillRow method we in addition to the index checks we make sure that this pixel we are visiting passes the HeightCheck.

The HeightCheck takes the x and y coordinates of the shadow map and converts them to the height map space (if needed). We then do a bounds check with the converted coordinates. We convert the height from Unity units to a number that is comparable with the height map data and return whether the height of the revealer is greater than the height map point.

We are done! You should see something like this after you hook up everything in the inspector:

Look at that fow!

Limitations

As you can see in the gif above you can see around corners, as we don't take line of sight into account. A friend of mine pointed this out recently, but I'm going to be starting a new job soon so I don't have time to look into this any more :(

Also the reveal happens too fast, this was passable before but now that we have height it just pops in. Next Steps

Due to my time being more limited than I originally thought, I'm going to make this just a 2 part tutorial (originally wanted 3 parts, with the final part dealing with enemy unites and buildings) and make new posts highlighting some of the issues with the current system. Here are some of the things I want to improve or add: line of sight, enemy units fading in, enemy builds fading in and staying in an "explored" state but no longer updates, art being affected by the fog of war (trees, and other non terrain visual objects in the scene), updating every few frames and interpolating between two sets of pixel data between updates, blurring the edges of the revealed area, optimization. I'm sure I'll run into other issues or find new improvements but that list is what I'm going to try to focus on first.

Almost forgot the source!

Also if you wanted to just use a reference of the Terrain class instead of saving the height map data to file then you can use the SampleHeight method from the Terrain asset. I think that returns the height of the terrain in Unity units at the given world position, so you won't have to convert the height of the revealer to the height of terrain. However you might have to convert pixels to world space... which might not be ideal.

Edit 02/11/2017: Spelled Revealer incorrectly in code.