This tutorial is the eighth part of a series about hexagon maps. We have already added support for rivers. Now we're going to completely submerge cells.

To add the water level to the UI, duplicate the elevation label and slider and adjust them. Don't forget to hook their events to the correct methods.

Editing the water level of a cell works just like editing its elevation. So HexMapEditor has to keep track of the active water level, and whether it should be applied to cells.

Adjust HexCell.RiverSurfaceY so it uses the new name. Then add a similar property for the water surface of a submerged cell.

This means that when the water level and elevation are equal, the cell rises above the water. So the actual water surface is somewhere below that elevation. Just like the surfaces of rivers, so let's use the same offset, HexMetrics.riverSurfaceElevationOffset . Change its name to be more generic.

Now that we have water levels, the most important question is whether a cells is underwater or not. A cell is submerged if its water level is higher than its elevation. Add a property to retrieve this information.

If you want to, you could enforce that certain terrain features cannot exist while underwater. But I'm not going to at this point. Things like underwater roads are fine. It might represent an area that has been recently submerged.

The most straightforward way to support water is to define a uniform water level. All cells that have an elevation lower than that level are submerged. But it's more flexible to support water at different elevations, so let's make the water level variable. This requires each HexCell to keep track of its water level.

So let's not do that now. It can be a feature of a more advanced editor. For now, it's up to the user to stay consistent.

The danger of this approach is that it can quickly go out of control. An unlucky edit could submerge the entire map. Then all chunks will need to be triangulating at the same time, causing a big lag spike.

We could try to force our water levels to stay at the same level. For example, when adjusting the water level of a submerged cell, we could propagate changes to adjacent cells to keep the water level synchronized. However, this process has to keep going until it encounters cells that end up not submerged. Those cells define the boundary of the water body.

We have made the assumption that adjacent submerged cells have the same water level. Things look good when this is indeed the case, but it goes wrong when we violate this assumption.

We now have water cells that connect when they are adjacent. They leave a gap between themselves and dry cells with a higher elevation, but we'll deal with that later.

Just as with rivers, the height of the water surface doesn't vary between cells with the same water level. As such, it looks like we don't need complex edges. A simply triangle will do.

As the water forms a second layer, let's give it its own triangulation method, per direction. We only need to invoke it when a cell is submerged.

And make sure to connect it with the actual prefab child object.

Add a new child object to the prefab by duplicating the Rivers child object. It doesn't need UV coordinates, and should use the Water material. As usual, do this by creating a prefab instance, changing that one, then applying the changes to the prefab. Afterwards, get rid of the instance.

Create a new material with this shader, by duplicating the Water material and changing its shader. Keep the noise texture, as we'll use it later.

We need a new mesh to triangulate the water, with a new material. First, create a Water shader by duplicating the River shader. Change it so it just uses the color property.

Finally, use the blend wave to interpolate between two channels, for both of our noise samples. Use four different channels for maximum variety.

To make the blend wave a bit less obvious, add some noise to it, from both samples.

Sine waves undulate between -1 and 1, but we need a 0–1 range. We can get there by squaring the wave. To see the result in isolation, use it as the output, instead of the adjusted color.

We produce a blend wave by creating a sine wave that runs diagonally across the water surface. We do that by adding the X and Z world coordinates together and using that as input of the sin function. Scale them down so that we get reasonably large bands. And off course add the same, to animate it.

It is still quite obvious that we have two scrolling noise patterns that don't actually change. It will be more convincing if the patterns changed. We can do this by interpolating between different channels of the noise samples. But we shouldn't do this uniformly, because then the entire water surface would change at the same time. That would be very obvious. Instead, we'll use a blend wave.

Summing both samples can produces results in the 0–2 range, so we have to scale that back to 0–1. Instead of just halving the waves, we can use the smoothstep function to create a more interesting result. We'll map ¾–2 to 0–1, so part of the water surface ends up without visible waves.

That doesn't look like waves at all. Let's make it more complex by adding a second noise sample, adding the time to the U coordinate this time. Use a different noise channel so you end up with two different patterns. The final waves will be these two samples added together.

Let's do the same thing that we did with rivers. Sample some noise using the wold position, and add it to the uniform color. To animate it, add the time to the V coordinate.

Instead of using a uniform color, let's create something that looks somewhat like waves. As with our other shaders, we're not aiming for great visuals at this time. Just a suggestion of waves will do.

In the Water Shore shader, compute both the foam and waves values. Then fade the waves as they approach the shore. The final result the maximum of the foam and waves.

Adjust the Water shader so it uses our new include file.

Instead of copying the wave code, let's put it in a Water.cginc include file. In fact, put both the code for foam and wave in it, each as a separate function.

There is a harsh transition between the open and shore water, because the waves from the open water are not included in the water shore. To fix this, we have to include those waves in the Water Shore shader as well.

Besides advancing foam, there is also receding foam. Let's add a second sine wave that moves in the opposite direction to simulate that. Make it a little weaker and give it a time offset. The final foam is the maximum of these two sine waves.

And of course animate it all, both the sine wave and the distortion.

Add some distortion to make it look more natural. Make the distortion grow weaker the closer it gets to the shore. That way it will better match the coastline.

Let's make the foam front grow bigger as it approaches the shore. This can be done by taking the square root of the shore value before using it.

Now that the shore transitions are correct, we can use them to create some foam effects. The simplest approach is to add the shore value to the uniform color.

The above code works for edges, but goes wrong for some of the corners. If the next neighbor is underwater, the current approach is correct. But when the next neighbor is not underwater, then the third vertex of the triangle is below land.

Let's put the information about the shore in the V coordinate. Set it to 0 on the water side and to 1 on the land side. As we don't need to communicate anything else, all U coordinates can just be 0.

As we're not setting the coordinates yet, it will produce a solid color. This makes it easy to see that the water shore indeed uses a separate mesh and material.

Adjust the Water Shore shader so it shows the UV coordinates instead of water.

Duplicate the water object, connect it with the prefab, and configure it to use UV coordinates. Also create a shader and material for the shore water, by duplicating the existing water shader and material.

Our open water doesn't have UV coordinates, and it doesn't need any foam. That's only needed for the water near the shore. So the requirements of both types of water are quite different. It makes sense to give each type its own mesh. So add support for another mesh object to HexGridChunk .

We could leave it like this, but it's more interesting if the shore water had some extra visuals. A foam effect, which gets stronger the closer you get to the shore. To support this, the shader has to know how close a fragment is to the shore. We could provide this information via UV coordinates.

We now have complete shore water. Part of it always ends up below the terrain mesh, so there are no gaps.

Next comes the edge strip, just like for regular terrain. However, we do not have to limit ourselves to only some directions. That's because we're only invoking TriangulateWaterShore when we are facing a shore, which always requires a strip.

Because the shore is perturbed, we should perturb the water triangles along the shore as well. So we need edge vertices and a triangle fan.

We are done with open water, but still have to fill the gap in the water along the shore. Because we have to match the contours of the land, the shore water requires a separate approach. Let's split TriangulateWater into two methods, one for open water and one for shore water. To determine whether we have a shore, we have to look at the neighbor cell. So retrieving the neighbor stays in TriangulateWater . If there is a neighbor and it is not underwater, then we're dealing with shore water.

This works nice, except that the foam can get quite pronounced when most of it is visible. To compensate, you can make the effect a little weaker by scaling down the shore value in the shader.

This works, except that once again we have two cases to consider for the corner triangles.

We cannot use a simple bridge to find the opposite solid edge, when starting with water corners. Instead, we can work backwards from the center of the neighbor. Adjust TriangulateWaterShore so it uses this new approach.

While this gives us more space for the foam, there is now also a much larger part of it hidden below the terrain. Ideally, we could use the water edge on the water side, and the solid edge on the land side.

The distance between the water hexagons has indeed doubled. Now HexMetrics must also provide a water bridge method.

Use these new methods in HexGridChunk to find the water corners.

The solid factor is 0.8. To double the size of the water connections, we have to set the water factor to 0.6.

We can combat this by increasing the size of the water shore strip. This can be done by decreasing the radius of the water hexagons. To do so, HexMetrics needs a water factor to accompany its solid factor, plus methods to get the water corners.

Part of the water shore mesh ends up hidden below the terrain mesh. This is fine when only a small part is hidden. Unfortunately, steep cliffs end up hiding most of the shore water, and thus also the foam.

While we already have an unperturbed variant method for adding triangles, we actually don't have one for quads yet. So add the required HexMesh.AddQuadUnperturbed method.

The result is a shorter waterfall that still has the same orientation. However, because the positions of the bottom vertices have changed, they will be perturbed differently than the original vertices. This means that the end result still does not match the original waterfall. To solve this, we have to manually perturb the vertices before interpolating, and then add an unperturbed quad.

To move the bottom vertices upward, divide their distance below the water surface by the height of the waterfall. That gives us the interpolator value.

This again produces the original river quad. Next, we have to adjust TriangulateWaterfallInWater so it pulls the bottom vertices up to the water level. Unfortunately, we cannot suffice with only adjusting their Y coordinates. That would pull the waterfall away from the cliff, which can lead to gaps. Instead, we have to pull the bottom vertices towards the top vertices, via interpolation.

We also have to deal with waterfalls in the opposite direction, when the current cell is underwater and the neighbor isn't.

Invoke this method in TriangulateConnection , when the neighbor ends up underwater and we have a waterfall.

The new method requires four vertices, two river levels, and the water level. We'll align it so we're looking in the direction of the flow, down the waterfall. So the first two vertices and the left and right side at the top, followed by those for the bottom.

The river segment of a waterfall used to pass through the water surface. It ended up partially above, and partially below the water. We have to keep the part above the water level, discarding the rest. That requires some work, so let's create a separate method for that.

The underwater rivers are gone, but now we get gaps in the rivers where they meet the water surface. Rivers on the same level as the water get small gaps or overlaps. But most obvious are the lacking waterfalls for rivers that come from a higher elevation. Let's deal with those first.

For TriangulateConnection , let's start by only adding the river segment when neither the current nor the neighbor cell is underwater.

While it is fine for a stream bed to exist underwater, and water might actually flow through it, we shouldn't be able to see this water. Especially not rendered on top of the actual water surface. We can get rid of the underwater river's water by making sure to only add river segments when the current cell in not underwater.

The order in which semitransparent objects are rendered depends on their distance from the camera. The nearest objects are rendered last, which ensures they end up on top. As you move the camera around, this means that sometimes rivers, and sometimes water ends up on top of the other. Let's begin by making the rendering order consistent. Rivers should be drawn on top of the water, so waterfalls are correctly displayed. We can enforce this by changing the queue of the River shader.

Our water is complete, at least when there aren't any rivers flowing into it. As water and rivers are currently oblivious to each other, the rivers will flow through and underneath the water.

Estuaries

When rivers flow at the same height as the water surface, the river mesh touches the shore mesh. If this were a river flowing into a sea or ocean, this is where the river stream meets the tide. As such, we'll refer to these regions as estuaries.

River meets shore, without vertex perturbation.

There are currently two problems with estuaries. First, river quads connect the 2nd and 4th edge vertices, skipping the 3rd. As the water shore does use the 3rd vertex, it can end up creating a gap or an overlap. We can solve this by adjusting the geometry of the estuaries.

The second problem is that there is a harsh transition between the foam and river materials. To solve this, we need yet another material, which blends between the river and water shore effects.

This means that estuaries require special treatment, so let's create a separate method for them. It should be invoked in TriangulateWaterShore , when there is a river flowing through the current direction.

void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … if (cell.HasRiverThroughEdge(direction)) { TriangulateEstuary(e1, e2); } else { waterShore.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); waterShore.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); waterShore.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); waterShore.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); } … } void TriangulateEstuary (EdgeVertices e1, EdgeVertices e2) { }

The region that blends both effects doesn't need to fill the entire edge strip. We can suffice with a trapezoid shape. So we can use two water shore triangles at the sides.

void TriangulateEstuary (EdgeVertices e1, EdgeVertices e2) { waterShore.AddTriangle(e2.v1, e1.v2, e1.v1); waterShore.AddTriangle(e2.v5, e1.v5, e1.v4); waterShore.AddTriangleUV( new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); waterShore.AddTriangleUV( new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); }

Trapezoidal hole for the blend region.

UV2 Coordinates To create the river effect, we need UV coordinates. But to create the foam effect, we also need UV coordinates. So when blending both, we end up requiring two sets of UV coordinates. Fortunately, Unity's meshes can support up to four UV sets. We just have to add support for a second set to HexMesh . public bool useCollider, useColors, useUVCoordinates , useUV2Coordinates ; [NonSerialized] List<Vector2> uvs , uv2s ; public void Clear () { … if (useUVCoordinates) { uvs = ListPool<Vector2>.Get(); } if (useUV2Coordinates) { uv2s = ListPool<Vector2>.Get(); } triangles = ListPool<int>.Get(); } public void Apply () { … if (useUVCoordinates) { hexMesh.SetUVs(0, uvs); ListPool<Vector2>.Add(uvs); } if (useUV2Coordinates) { hexMesh.SetUVs(1, uv2s); ListPool<Vector2>.Add(uv2s); } … } To add to the second UV set, duplicate the UV methods and adjust them as you would expect. public void AddTriangleUV2 (Vector2 uv1, Vector2 uv2, Vector3 uv3) { uv2s .Add(uv1); uv2s .Add(uv2); uv2s .Add(uv3); } public void AddQuadUV2 (Vector2 uv1, Vector2 uv2, Vector3 uv3, Vector3 uv4) { uv2s .Add(uv1); uv2s .Add(uv2); uv2s .Add(uv3); uv2s .Add(uv4); } public void AddQuadUV2 (float uMin, float uMax, float vMin, float vMax) { uv2s .Add(new Vector2(uMin, vMin)); uv2s .Add(new Vector2(uMax, vMin)); uv2s .Add(new Vector2(uMin, vMax)); uv2s .Add(new Vector2(uMax, vMax)); }

River Shader Function Because we're going to use the river effect in two shaders, move the code from the River shader to a new function in the Water include file. float River (float2 riverUV, sampler2D noiseTex) { float2 uv = riverUV ; uv.x = uv.x * 0.0625 + _Time.y * 0.005; uv.y -= _Time.y * 0.25; float4 noise = tex2D( noiseTex , uv); float2 uv2 = riverUV ; uv2.x = uv2.x * 0.0625 - _Time.y * 0.0052; uv2.y -= _Time.y * 0.23; float4 noise2 = tex2D( noiseTex , uv2); return noise.x * noise2.w; } Adjust the River shader to use this new function. #include "Water.cginc" sampler2D _MainTex; … void surf (Input IN, inout SurfaceOutputStandard o) { float river = River(IN.uv_MainTex, _MainTex); fixed4 c = saturate(_Color + river ); … }

Estuary Objects Add support for an estuary mesh object to HexGridChunk . public HexMesh terrain, rivers, roads, water, waterShore , estuaries ; public void Triangulate () { terrain.Clear(); rivers.Clear(); roads.Clear(); water.Clear(); waterShore.Clear(); estuaries.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); roads.Apply(); water.Apply(); waterShore.Apply(); estuaries.Apply(); } Create an estuary shader, material, and object, by duplicating and adjusting those for the water shore. Connect it with the chunk, and make sure that it uses both UV and UV2 coordinates. Estuarties object.

Triangulating the Estuary We can solve the gap or overlap problem by placing a triangle between the river's end and the middle of the water's edge. Because our estuary shader is a duplicate of the water shore shader, set the UV coordinates to match the foam effect. void TriangulateEstuary (EdgeVertices e1, EdgeVertices e2) { … estuaries.AddTriangle(e1.v3, e2.v2, e2.v4); estuaries.AddTriangleUV( new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, 1f) ); } Middle triangle. We can fill the entire trapezoid by adding a quad on both sides of the middle triangle. estuaries.AddQuad(e1.v2, e1.v3, e2.v1, e2.v2); estuaries.AddTriangle(e1.v3, e2.v2, e2.v4); estuaries.AddQuad(e1.v3, e1.v4, e2.v4, e2.v5); estuaries.AddQuadUV(0f, 0f, 0f, 1f); estuaries.AddTriangleUV( new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, 1f) ); estuaries.AddQuadUV(0f, 0f, 0f, 1f); Complete trapezoid. Let's rotate the orientation of the quad on the left side, so it has a shorter diagonal connection, and we end up with symmetrical geometry. estuaries.AddQuad( e2.v1 , e1.v2 , e2.v2 , e1.v3 ); estuaries.AddTriangle(e1.v3, e2.v2, e2.v4); estuaries.AddQuad(e1.v3, e1.v4, e2.v4, e2.v5); estuaries.AddQuadUV( new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, 0f) ); // estuaries.AddQuadUV(0f, 0f, 0f, 1f); Rotated quad, symmetrical geometry

River Flow To support the river effect, we need to add UV2 coordinates. The bottom of the middle triangle sits in the middle of the river, so its U coordinate should be 0.5. As the river flows towards the water, its left point gets U coordinate 1, and its right point gets U coordinate 0. Set the Y coordinates to 0 and 1, matching the flow direction. estuaries.AddTriangleUV2( new Vector2(0.5f, 1f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); The quads of either side of the triangle should match this orientation. Keep the same U coordinates for the points that are beyond the width of the river. estuaries.AddQuadUV2( new Vector2(1f, 0f), new Vector2(1f, 1f), new Vector2(1f, 0f), new Vector2(0.5f, 1f) ); estuaries.AddTriangleUV2( new Vector2(0.5f, 1f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); estuaries.AddQuadUV2( new Vector2(0.5f, 1f), new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); Trapezoid UV2. To check whether we set the UV2 coordinates correctly, have the Estuary shader visualize them. We can access these coordinates by adding float2 uv2_MainTex to its input structure. struct Input { float2 uv_MainTex; float2 uv2_MainTex; float3 worldPos; }; … void surf (Input IN, inout SurfaceOutputStandard o) { float shore = IN.uv_MainTex.y; float foam = Foam(shore, IN.worldPos.xz, _MainTex); float waves = Waves(IN.worldPos.xz, _MainTex); waves *= 1 - shore; fixed4 c = fixed4(IN.uv2_MainTex, 1, 1) ; … } UV2 coordinates. That looks good. We can use this to create the river effect. void surf (Input IN, inout SurfaceOutputStandard o) { … float river = River(IN.uv2_MainTex, _MainTex); fixed4 c = saturate(_Color + river) ; … } Using UV2 to create the river effect. We designed rivers so that when triangulating connections between cells, the river's V coordinates go from 0.8 to 1. So we should use this range here as well, instead of going from 0 to 1. However, the shore connection is 50% larger than regular cell connections. So to best match the river flow, we have to go from 0.8 to 1.1. estuaries.AddQuadUV2( new Vector2(1f, 0.8f ), new Vector2(1f, 1.1f ), new Vector2(1f, 0.8f ), new Vector2(0.5f, 1.1f ) ); estuaries.AddTriangleUV2( new Vector2(0.5f, 1.1f ), new Vector2(1f, 0.8f ), new Vector2(0f, 0.8f ) ); estuaries.AddQuadUV2( new Vector2(0.5f, 1.1f ), new Vector2(0f, 1.1f ), new Vector2(0f, 0.8f ), new Vector2(0f, 0.8f ) );

Synchronized river and estuary flow.

Adjusting the Flow Right now, the river flow continues along a straight line. But when water flows into a larger area, it will spread out. The flow will curve. We can simulate this by warping the UV2 coordinates. Instead of keeping the top U coordinates constant beyond the width of the river, shift them by 0.5. The leftmost point becomes 1.5, and the rightmost becomes −0.5. At the same time, widen the flow by shifting the U coordinates of the left and right points at the bottom. Change the left one from 1 to 0.7, and change the right one from 0 to 0.3. estuaries.AddQuadUV2( new Vector2( 1.5f , 0.8f), new Vector2( 0.7f , 1.1f), new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f) ); … estuaries.AddQuadUV2( new Vector2(0.5f, 1.1f), new Vector2( 0.3f , 1.1f), new Vector2(0f, 0.8f), new Vector2( -0.5f , 0.8f) );

Widening river flow. To complete the curving effect, adjust the V coordinates of the same four points. As the water flows away from the river exit, increase the V coordinates of the top points to 1. And to produce a better curve, increase the V coordinates of the bottom two points to 1.15. estuaries.AddQuadUV2( new Vector2(1.5f, 1f ), new Vector2(0.7f, 1.15f ), new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f) ); estuaries.AddTriangleUV2( new Vector2(0.5f, 1.1f), new Vector2(1f, 0.8f), new Vector2(0f, 0.8f) ); estuaries.AddQuadUV2( new Vector2(0.5f, 1.1f), new Vector2(0.3f, 1.15f ), new Vector2(0f, 0.8f), new Vector2(-0.5f, 1f ) );

Curving river flow.