This is part 21 of a tutorial series about hexagon maps. The previous part added fog of war, which we'll now upgrade to support exploration.

Now the fog of war will disappear when we switch to map-editing mode.

To make this work, all relevant shaders should get a multi-compile directive to create variants for when the HEX_MAP_EDIT_MODE keyword is defined. Add the following line to the Estuary, Feature, River, Road, Terrain, Water, and Water Shore shaders, between the target directive and the first include directive.

Pass the result of both GetCellData functions through this function before returning it.

When HEX_MAP_EDIT_MODE is defined, the shaders should ignore visibility. This boils down to always treating the visibility of a cell as 1. Let's add a function to the top of our HexCellData include file to filter cell data based on the keyword.

We can control whether shaders apply visibility via a keyword, like we do for the grid overlay. Let's use the HEX_MAP_EDIT_MODE keyword to indicated whether we are in edit mode. Because multiple shaders will need to be aware of this keyword, we'll define it globally, via the static Shader.EnableKeyWord and Shader.DisableKeyword methods. Invoke the appropriate one when the edit mode is changed, in HexGameUI.SetEditMode .

The idea of exploration is that cells that have not yet been seen are unknown, and thus invisible. Instead of darkening those cells, they shouldn't be shown at all. But it's hard to edit invisible cells. So before we add support for exploration, we're going to disable visibility while in edit mode.

Whether cells are explored is now included when saving and loading maps.

From now on, HexGrid.Load has to pass the header data on to HexCell.Load .

To remain backwards compatible with older save files, we should skip reading the explored state when the file version is less than 3. Let's default to unexplored when that's the case. To be able to do this, we have to add the header data as a parameter to Load .

And read it at the end of Load . After that, invoke RefreshVisibility in case the exploration state is now different than before.

In HexCell.Save , we'll write the exploration state as the last step.

Use this constant when writing the file version in Save and to check whether a file is supported in Load .

Now that we support exploration, we should also make sure that the exploration state of cells is included when saving and loading maps. So we have to increase the map file version to 3. To make these changes more convenient, let's add a constant for this to SaveLoadMenu .

The terrain of unexplored cells in now black. Features, roads, and water aren't affected yet. This is enough to verify that exploration works.

In the fragment program, the explorations state is now available via IN.visibility.w . Factor it into the albedo.

After that, combine the exploration states and put the result in data.visibility.w . This is done like combining the visibility in the other shaders, but using the Y component of the cell data.

In the vertex program, we now have to explicitly access data.visibility.xyz when adjusting the visibility factor.

The Terrain shader sends the visibility data of all three potential cells to the fragment program. In the case of exploration state, we'll combine them in the vertex program and send a single value to the fragment program. Add a fourth component to the visibility input data to make room for this.

Now we can use the shaders to visualize the exploration state of cells. To verify that it works as intended, we'll simply make unexplored terrain black. But first, to keep edit mode functional, adjust FilterCellData so it also filters the exploration data.

Like a cell's visibility, we can send its exploration state to the shaders via the shader data. It's another type of visibility, after all. HexCellShaderData.RefreshVisibility stores the visibility state in the data's R channel. Let's store the exploration state in the data's G channel.

The first time a cell's visibility goes above zero, the cell is explored, and thus IsExplored should be set to true . Actually, we can suffice by simply always marking the cell as explored whenever visibility increases to 1. This should be done before invoking RefreshVisibility .

Whether a cell is explored is determined by the cell itself. So only HexCell should be able to set this property. To enforce this, make the setter private.

By default, cells should be unexplored. They become explored as soon as a unit sees them. From then on they remain explored, regardless whether a unit can see them.

Hiding Unknown Cells

Currently, unexplored cells are visually indicated by giving them a solid black terrain. What we really want is for those cells to be invisible, because they are unknown. It is possible to make normally opaque geometry transparent, so it can no longer be seen. However, we're using Unity's surface shader framework which is not designed with this effect in mind. Instead of going for actual transparency, we'll adapt our shaders to match the background so they're also unnoticeable.

Making the Terrain Truly Black Although unexplored terrain is solid black, we can still determine its features because it still has specular lighting. To get rid of the highlights we have to make it perfectly matte black. To do this without messing with other surface properties, it's easiest to simply fade the specular color to black. This is possible when using a surface shader with the specular workflow, but we're currently using the default metallic workflow. So let's begin by switching the Terrain shader to the specular workflow. Replace the _Metallic property with a _Specular color property. Its default color value should be (0.2, 0.2, 0.2). This makes sure that it matches the appearance of the metallic version. Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Terrain Texture Array", 2DArray) = "white" {} _GridTex ("Grid Texture", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 // _Metallic ("Metallic", Range(0,1)) = 0.0 _Specular ("Specular", Color) = (0.2, 0.2, 0.2) } Change the corresponding shader variables as well. The specular color of surface shaders is defined as a fixed3 , so let's use that as well. half _Glossiness; // half _Metallic; fixed3 _Specular; fixed4 _Color; Change the surface surf pragma from Standard to StandardSpecular. This causes Unity to generate shaders using the specular workflow. #pragma surface surf StandardSpecular fullforwardshadows vertex:vert The surf function now requires its second parameter to be of the type SurfaceOutputStandardSpecular . Also, we should assign to o.Specular instead of to o.Metallic . void surf (Input IN, inout SurfaceOutputStandardSpecular o) { … float explored = IN.visibility.w; o.Albedo = c.rgb * grid * _Color * explored; // o.Metallic = _Metallic; o.Specular = _Specular; o.Smoothness = _Glossiness; o.Alpha = c.a; } Now we can fade out the highlights by factoring explored into the specular color. o.Specular = _Specular * explored ; Unexplored without specular lighting. When seen from above, unexplored terrain now appears matte black. However, when viewed at grazing angles surfaces become mirrors, which causes the terrain to reflect the environment, which is the skybox. Why do surfaces become mirrors? This is known as the Fresnel effect. See the Rendering series for more information. Unexplored still reflects the environment. To get rid of these reflections, treat unexplored terrain as fully occluded. This is done by using explored as the occlusion value, which acts as a mask for reflections. float explored = IN.visibility.w; o.Albedo = c.rgb * grid * _Color * explored; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Alpha = c.a; Unexplored without reflections.

Matching the Background Now that unexplored terrain ignores all lighting, the next step is to make it match the background. As our camera is always looking from above, the background is always grey. To tell the Terrain shader which color to use, add a _BackgroundColor property to it, with black as default. Properties { … _BackgroundColor ("Background Color", Color) = (0,0,0) } … half _Glossiness; fixed3 _Specular; fixed4 _Color; half3 _BackgroundColor; To use this color, we add it as emissive light. This is done by assigning the background color multiplied by one-minus-explored to o.Emission . o.Occlusion = explored; o.Emission = _BackgroundColor * (1 - explored); Because we're using the default skybox, the visible background color isn't actually uniform. Overall the best color is slightly reddish grey. You can use the Hex Color code 68615BFF when adjusting the terrain material. Terrain material with gray background color. This mostly works, although you could still perceive very faint silhouettes if you know where to look. To ensure that this is not possible for the player, you can adjust the camera to use 68615BFF as its solid background color, instead of the skybox. Camera with solid background color. Why not remove the skybox? You could do that, but keep in mind that it is used for environmental lighting of the map. If you switch it to a solid color, the lighting of the map will change as well. Now we're no longer able to distinguish between the background and unexplored cells. It is still possible for high unexplored terrain to occlude low explored terrain, when using low camera angles. Also, the unexplored parts still cast shadows on the explored parts. These minimal clues are fine. Unexplored cells no longer visible. What if I'm not using a solid background color? You could create your own shader which actually makes the terrain transparent, while still writing to the depth buffer, which might require some shader queue tricks. If you're using a screen-space texture, you can simply sample this texture instead of using a background color. If you're using a texture in world space, underneath the terrain, you'll have to use some math to figure out which texture UV coordinates to use, based on the view angle and fragment's world position.

Hiding Features At this point only the terrain mesh is hidden. Everything else is still unaffected by the exploration state. Only terrain is hidden so far. Let's adjust the Feature shader next, which is an opaque shader like Terrain. Turn it into a specular shader and add a background color to it. First, the properties. Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 // _Metallic ("Metallic", Range(0,1)) = 0.0 _Specular ("Specular", Color) = (0.2, 0.2, 0.2) _BackgroundColor ("Background Color", Color) = (0,0,0) [NoScaleOffset] _GridCoordinates ("Grid Coordinates", 2D) = "white" {} } Next, the surface pragma and variables, like before. #pragma surface surf StandardSpecular fullforwardshadows vertex:vert … half _Glossiness; // half _Metallic; fixed3 _Specular; fixed4 _Color; half3 _BackgroundColor; Again, visibility needs another component. Because Feature combines the visibility per vertex, it only needed a single float. Now we need two. struct Input { float2 uv_MainTex; float2 visibility; }; Adjust vert so it explicitly uses data.visibility.x for the visibility data, then assign the exploration data to data.visibility.y . void vert (inout appdata_full v, out Input data) { … float4 cellData = GetCellData(cellDataCoordinates) ; data.visibility .x = cellData .x; data.visibility .x = lerp(0.25, 1, data.visibility .x ); data.visibility.y = cellData.y; } Adjust surf so it uses the new data, like Terrain. void surf (Input IN, inout SurfaceOutputStandardSpecular o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; float explored = IN.visibility.y; o.Albedo = c.rgb * ( IN.visibility .x * explored) ; // o.Metallic = _Metallic; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Emission = _BackgroundColor * (1 - explored); o.Alpha = c.a; } Adjust the features so they use materials with the proper settings. Hidden features.

Hiding Water Next up are the Water and Water Shore shaders. Begin by converting them to specular shaders, to stay consistent. However, they do not need a background color, because they are transparent shaders. After the conversion, add another component to visibility and adjust vert accordingly. Both shaders combine the data of three cells. struct Input { … float2 visibility; }; … void vert (inout appdata_full v, out Input data) { … data.visibility .x = cell0.x * v.color.x + cell1.x * v.color.y + cell2.x * v.color.z; data.visibility .x = lerp(0.25, 1, data.visibility .x ); data.visibility.y = cell0.y * v.color.x + cell1.y * v.color.y + cell2.y * v.color.z; } Water and Water Shore do different things in surf , but they set their surface properties in the same way. Because they're transparent, factor explore into alpha instead of setting emission. void surf (Input IN, inout SurfaceOutputStandardSpecular o) { … float explored = IN.visibility.y; o.Albedo = c.rgb * IN.visibility .x ; o.Specular = _Specular * explored ; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Alpha = c.a * explored ; } Hidden water.