This is part 20 of a tutorial series about hexagon maps. This installment is about adding a fog-of-war effect to our map.

From now on, this tutorial series is made with Unity 2017.1.0.

Cell Shader Data

Many strategy games use what's known as fog of war. This means that your vision is limited. You can only see things that are close to your units or zone of control. While you might know the layout of the land, you're not sure what's going on there while you can't see it. Typically, terrain that you cannot currently see is rendered darker than normal. To implement this, we need to keep track of a cell's visibility and make sure that it is rendered appropriately.

The most straightforward way to change the appearance of hidden cells is by adding a visibility indicator to the mesh data. However, that would require us to trigger a new triangulation of the terrain whenever visibility changes. As visibility changes happen all the time during play, it is not a good idea to do it this way.

An often-described technique is to render a semitransparent surface on top of the terrain, which partially masks the cells that you cannot see. This can work well for fairly flat terrains, in combination with a restricted view angle. Because our terrain can contain wildly varying elevations and features and we can look at it from any angle, it would require a highly-detailed form-fitting mesh. That would be more expensive than the straightforward approach.

Another approach is to make the cell data available to the shader while rendering, separate from the terrain mesh. This allows us to triangulate once. The cell data can be made available via a texture. Adjusting a texture is much simpler an faster that triangulating the terrain. Doing a few more texture samples is also faster than rendering a separate semitransparent overlay.

What about using shader arrays? It is also possible to pass cell data to the shader via a vector array. However, shader arrays have a size limitation measured in thousands of bytes, while textures can have millions of pixels. To support large maps, go with a texture.

Managing the Cell Data We need a way to manage the texture that contains the cell data. Let's create a new HexCellShaderData component to take care of that. using UnityEngine; public class HexCellShaderData : MonoBehaviour { Texture2D cellTexture; } Whenever a new map is created or loaded, we have to create a new texture with the correct size. So give it an initialization method which creates the texture. We'll use an RGBA texture, without mipmaps, and in linear color space. We don't want to blend cell data, so use point filtering. Also, the data shouldn't wrap. Each pixel of the texture will hold the data of one cell. public void Initialize (int x, int z) { cellTexture = new Texture2D( x, z, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; } Does the texture size have to match the map size? No, it just has to have enough pixels to contain all the cells. Exactly matching the map size will likely result in a non-power-of-two (NPOT) texture, which isn't the most efficient texture format. While you could tweak it to work with power-of-two textures, this is a minor optimization which makes accessing cell data less obvious. We don't actually have to create a new texture every time a new map is created. We can suffice with resizing our texture if it already exists. We don't even have to check whether we already have the correct size, as Texture2D.Resize is smart enough to do this for us. public void Initialize (int x, int z) { if (cellTexture) { cellTexture.Resize(x, z); } else { cellTexture = new Texture2D( x, z, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; } } Instead of applying cell data one pixel at a time, we'll use a color buffer and apply all cell data in one go. We'll use a Color32 array for this. Create a new array instance at the end of Initialize when needed. If we already have an array of the correct size, reset its contents. Texture2D cellTexture; Color32[] cellTextureData; public void Initialize () { … if (cellTextureData == null || cellTextureData.Length != x * z) { cellTextureData = new Color32[x * z]; } else { for (int i = 0; i < cellTextureData.Length; i++) { cellTextureData[i] = new Color32(0, 0, 0, 0); } } } What's Color32 ? Default uncompressed RGBA textures contain pixels that are four bytes in size. Each of the four color channels get one byte, so they have 256 possible values. When using Unity's Color struct, its floating-point components in the range 0–1 are converted to bytes in the range 0–255. The GPU performs the reverse conversion when sampling. The Color32 struct works directly with bytes. So they take less space and don't require a conversion, which makes them more efficient to use. As we're storing cell data instead of colors, it also makes more sense to work directly with the raw texture data instead of going through Color . It is the responsibility of HexGrid to create and initialize the cell shader data. So give it a cellShaderData field and create the component inside Awake . HexCellShaderData cellShaderData; void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; cellShaderData = gameObject.AddComponent<HexCellShaderData>(); CreateMap(cellCountX, cellCountZ); } Whenever a new map is created, cellShaderData has to be initialized as well. public bool CreateMap (int x, int z) { … cellCountX = x; cellCountZ = z; chunkCountX = cellCountX / HexMetrics.chunkSizeX; chunkCountZ = cellCountZ / HexMetrics.chunkSizeZ; cellShaderData.Initialize(cellCountX, cellCountZ); CreateChunks(); CreateCells(); return true; }

Adjusting Cell Data Up to this point, whenever a cell's properties were changed one or more chunks had to be refreshed. But from now on cell data might also need to be refreshed. This means that cells must also have a reference to the cell shader data. Add a property for this to HexCell . public HexCellShaderData ShaderData { get; set; } In HexGrid.CreateCell , assign its shader data component to this property. void CreateCell (int x, int z, int i) { … HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab); cell.transform.localPosition = position; cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.ShaderData = cellShaderData; … } Now we can have cells update their shader data. At this point we do not keep track of visibility yet, but we can also use the shader data for something else. A cell's terrain type dictates which texture is used when rendering it. It doesn't influence the cell's geometry. So we could store the terrain type index in the cell data instead of in the mesh data. This would eliminate the need for triangulation when a cell's terrain type is changed. Add a RefreshTerrain method to HexCellShaderData to facilitate this for a specific cell. Let's leave it an empty method for now. public void RefreshTerrain (HexCell cell) { } Change HexCell.TerrainTypeIndex so it invokes this method, instead of scheduling a chunk refresh. public int TerrainTypeIndex { get { return terrainTypeIndex; } set { if (terrainTypeIndex != value) { terrainTypeIndex = value; // Refresh(); ShaderData.RefreshTerrain(this); } } } Also invoke it in HexCell.Load after retrieving the cell's terrain type. public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadByte(); ShaderData.RefreshTerrain(this); elevation = reader.ReadByte(); RefreshPosition(); … }

Cell Index To adjust the cell data, we need to know the cell's index. The simplest way to do this is by adding an Index property to HexCell . This represents the cell's index in the map's cell list, which matches it's index in the cell shader data. public int Index { get; set; } We already have this index available in HexGrid.CreateCell , so simply assign it to the newly created cell. void CreateCell (int x, int z, int i) { … cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.Index = i; cell.ShaderData = cellShaderData; … } Now HexCellShaderData.RefreshTerrain can use this index to set the cell's data. Let's store the terrain type index in its pixel's alpha component, by simply converting the type to a byte. This allows us to support up to 256 terrain types, which is plenty. public void RefreshTerrain (HexCell cell) { cellTextureData[cell.Index].a = (byte)cell.TerrainTypeIndex; } To actually apply the data to the texture and push it to the GPU, we have to invoke Texture2D.SetPixels32 followed by Texture2D.Apply . Like we do with chunks, we're going to delay this to LateUpdate so we do it at most once per frame, no matter how many cells were changed. public void RefreshTerrain (HexCell cell) { cellTextureData[cell.Index].a = (byte)cell.TerrainTypeIndex; enabled = true; } void LateUpdate () { cellTexture.SetPixels32(cellTextureData); cellTexture.Apply(); enabled = false; } To make sure that the data is updated after creating a new map, also enable the component after initialization. public void Initialize (int x, int z) { … enabled = true; }

Triangulating Cell Indices Because we're now storing the terrain type index in the cell data, we no longer have to include it while triangulating. But to use the cell data, the shader has to know which cell indices to use. So we have to store the cell indices in the mesh data, replacing the terrain type indices. Also, we still need the mesh color channel to blend between cells when using the cell data. Remove the outdated useColors and useTerrainTypes public fields from HexMesh . Replace them with a single useCellData field. // public bool useCollider, useColors, useUVCoordinates, useUV2Coordinates; // public bool useTerrainTypes; public bool useCollider, useCellData, useUVCoordinates, useUV2Coordinates; Refactor-rename the terrainTypes list to cellIndices . Let's also refactor-rename colors to cellWeights , which is a more appropriate name. // [NonSerialized] List<Vector3> vertices, terrainTypes; // [NonSerialized] List<Color> colors; [NonSerialized] List<Vector3> vertices, cellIndices; [NonSerialized] List<Color> cellWeights; [NonSerialized] List<Vector2> uvs, uv2s; [NonSerialized] List<int> triangles; Adjust Clear so it grabs the two lists together when using cell data, instead of independent of one another. public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); if (useCellData) { cellWeights = ListPool<Color>.Get(); cellIndices = ListPool<Vector3>.Get(); } // if (useColors) { // colors = ListPool<Color>.Get(); // } if (useUVCoordinates) { uvs = ListPool<Vector2>.Get(); } if (useUV2Coordinates) { uv2s = ListPool<Vector2>.Get(); } // if (useTerrainTypes) { // terrainTypes = ListPool<Vector3>.Get(); // } triangles = ListPool<int>.Get(); } Perform the same grouping in Apply . public void Apply () { hexMesh.SetVertices(vertices); ListPool<Vector3>.Add(vertices); if (useCellData) { hexMesh.SetColors(cellWeights); ListPool<Color>.Add(cellWeights); hexMesh.SetUVs(2, cellIndices); ListPool<Vector3>.Add(cellIndices); } // if (useColors) { // hexMesh.SetColors(colors); // ListPool<Color>.Add(colors); // } if (useUVCoordinates) { hexMesh.SetUVs(0, uvs); ListPool<Vector2>.Add(uvs); } if (useUV2Coordinates) { hexMesh.SetUVs(1, uv2s); ListPool<Vector2>.Add(uv2s); } // if (useTerrainTypes) { // hexMesh.SetUVs(2, terrainTypes); // ListPool<Vector3>.Add(terrainTypes); // } hexMesh.SetTriangles(triangles, 0); ListPool<int>.Add(triangles); hexMesh.RecalculateNormals(); if (useCollider) { meshCollider.sharedMesh = hexMesh; } } Delete all the AddTriangleColor and AddTriangleTerrainTypes methods. Replace them with corresponding AddTriangleCellData methods that add the indices and weights in one go. public void AddTriangleCellData ( Vector3 indices, Color weights1, Color weights2, Color weights3 ) { cellIndices.Add(indices); cellIndices.Add(indices); cellIndices.Add(indices); cellWeights.Add(weights1); cellWeights.Add(weights2); cellWeights.Add(weights3); } public void AddTriangleCellData (Vector3 indices, Color weights) { AddTriangleCellData(indices, weights, weights, weights); } Apply the same treatment to the respective AddQuad methods. public void AddQuadCellData ( Vector3 indices, Color weights1, Color weights2, Color weights3, Color weights4 ) { cellIndices.Add(indices); cellIndices.Add(indices); cellIndices.Add(indices); cellIndices.Add(indices); cellWeights.Add(weights1); cellWeights.Add(weights2); cellWeights.Add(weights3); cellWeights.Add(weights4); } public void AddQuadCellData ( Vector3 indices, Color weights1, Color weights2 ) { AddQuadCellData(indices, weights1, weights1, weights2, weights2); } public void AddQuadCellData (Vector3 indices, Color weights) { AddQuadCellData(indices, weights, weights, weights, weights); }

Refactoring HexGridChunk At this point we get a lot of compiler errors in HexGridChunk that we have to fix. But first refactor-rename the static colors to weights, to stay consistent. static Color weights1 = new Color(1f, 0f, 0f); static Color weights2 = new Color(0f, 1f, 0f); static Color weights3 = new Color(0f, 0f, 1f); Let's begin by fixing TriangulateEdgeFan . It used to require a type but now needs a cell index. Replace the AddTriangleColor and AddTriangleTerrainTypes code with the corresponding AddTriangleCellData code. void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, float index ) { terrain.AddTriangle(center, edge.v1, edge.v2); terrain.AddTriangle(center, edge.v2, edge.v3); terrain.AddTriangle(center, edge.v3, edge.v4); terrain.AddTriangle(center, edge.v4, edge.v5); Vector3 indices; indices.x = indices.y = indices.z = index; terrain.AddTriangleCellData(indices, weights1); terrain.AddTriangleCellData(indices, weights1); terrain.AddTriangleCellData(indices, weights1); terrain.AddTriangleCellData(indices, weights1); // terrain.AddTriangleColor(weights1); // terrain.AddTriangleColor(weights1); // terrain.AddTriangleColor(weights1); // terrain.AddTriangleColor(weights1); // Vector3 types; // types.x = types.y = types.z = type; // terrain.AddTriangleTerrainTypes(types); // terrain.AddTriangleTerrainTypes(types); // terrain.AddTriangleTerrainTypes(types); // terrain.AddTriangleTerrainTypes(types); } This method is invoked in a few placed. Go through them and make sure that it's supplied with the cell index instead of the terrain type. TriangulateEdgeFan(center, e, cell. Index ); Next is TriangulateEdgeStrip . It's a bit more involved, but apply the same treatment. Also refactor-rename the c1 and c2 parameter names to w1 and w2 . void TriangulateEdgeStrip ( EdgeVertices e1, Color w1 , float index1 , EdgeVertices e2, Color w2 , float index2 , bool hasRoad = false ) { terrain.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); terrain.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); terrain.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); terrain.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); Vector3 indices; indices.x = indices.z = index1; indices.y = index2; terrain.AddQuadCellData(indices, w1, w2); terrain.AddQuadCellData(indices, w1, w2); terrain.AddQuadCellData(indices, w1, w2); terrain.AddQuadCellData(indices, w1, w2); // terrain.AddQuadColor(c1, c2); // terrain.AddQuadColor(c1, c2); // terrain.AddQuadColor(c1, c2); // terrain.AddQuadColor(c1, c2); // Vector3 types; // types.x = types.z = type1; // types.y = type2; // terrain.AddQuadTerrainTypes(types); // terrain.AddQuadTerrainTypes(types); // terrain.AddQuadTerrainTypes(types); // terrain.AddQuadTerrainTypes(types); if (hasRoad) { TriangulateRoadSegment(e1.v2, e1.v3, e1.v4, e2.v2, e2.v3, e2.v4); } } Change the invocations of this method so they're supplied with the cell index. Also keep the variable names consistent. TriangulateEdgeStrip( m, weights1, cell. Index , e, weights1, cell. Index ); … TriangulateEdgeStrip( e1, weights1, cell. Index , e2, weights2, neighbor. Index , hasRoad ); … void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color w2 = HexMetrics.TerraceLerp(weights1, weights2, 1); float i1 = beginCell. Index ; float i2 = endCell. Index ; TriangulateEdgeStrip(begin, weights1, i1 , e2, w2 , i2 , hasRoad); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color w1 = w2 ; e2 = EdgeVertices.TerraceLerp(begin, end, i); w2 = HexMetrics.TerraceLerp(weights1, weights2, i); TriangulateEdgeStrip(e1, w1 , i1 , e2, w2 , i2 , hasRoad); } TriangulateEdgeStrip(e2, w2 , i1 , end, weights2, i2 , hasRoad); } Now we move on to the corner methods. These changes are straightforward, but go through a lot of code. First is TriangulateCorner . void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … else { terrain.AddTriangle(bottom, left, right); Vector3 indices; indices.x = bottomCell.Index; indices.y = leftCell.Index; indices.z = rightCell.Index; terrain.AddTriangleCellData(indices, weights1, weights2, weights3); // terrain.AddTriangleColor(weights1, weights2, weights3); // Vector3 types; // types.x = bottomCell.TerrainTypeIndex; // types.y = leftCell.TerrainTypeIndex; // types.z = rightCell.TerrainTypeIndex; // terrain.AddTriangleTerrainTypes(types); } features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); } TriangulateCornerTerraces is next. void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1); Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1); Color w3 = HexMetrics.TerraceLerp(weights1, weights2, 1); Color w4 = HexMetrics.TerraceLerp(weights1, weights3, 1); Vector3 indices ; indices .x = beginCell. Index ; indices .y = leftCell. Index ; indices .z = rightCell. Index ; terrain.AddTriangle(begin, v3, v4); terrain.AddTriangleCellData(indices, weights1, w3, w4); // terrain.AddTriangleColor(weights1, w3, w4); // terrain.AddTriangleTerrainTypes(indices); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color w1 = w3 ; Color w2 = w4 ; v3 = HexMetrics.TerraceLerp(begin, left, i); v4 = HexMetrics.TerraceLerp(begin, right, i); w3 = HexMetrics.TerraceLerp(weights1, weights2, i); w4 = HexMetrics.TerraceLerp(weights1, weights3, i); terrain.AddQuad(v1, v2, v3, v4); terrain.AddQuadCellData(indices, w1, w2, w3, w4); // terrain.AddQuadColor(w1, w2, w3, w4); // terrain.AddQuadTerrainTypes(indices); } terrain.AddQuad(v3, v4, left, right); terrain.AddQuadCellData(indices, w3, w4, weights2, weights3); // terrain.AddQuadColor(w3, w4, weights2, weights3); // terrain.AddQuadTerrainTypes(indices); } Followed by TriangulateCornerTerracesCliff . void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(right), b ); Color boundaryWeights = Color.Lerp(weights1, weights3, b); Vector3 indices ; indices .x = beginCell. Index ; indices .y = leftCell. Index ; indices .z = rightCell. Index ; TriangulateBoundaryTriangle( begin, weights1, left, weights2, boundary, boundaryWeights , indices ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, weights2, right, weights3, boundary, boundaryWeights , indices ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleCellData( indices, weights2, weights3, boundaryWeights ); // terrain.AddTriangleColor(weights2, weights3, boundaryColor); // terrain.AddTriangleTerrainTypes(indices); } } And the slightly different TriangulateCornerCliffTerraces . void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(left), b ); Color boundaryWeights = Color.Lerp(weights1, weights2, b); Vector3 indices ; indices .x = beginCell. Index ; indices .y = leftCell. Index ; indices .z = rightCell. Index ; TriangulateBoundaryTriangle( right, weights3, begin, weights1, boundary, boundaryWeights , indices ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, weights2, right, weights3, boundary, boundaryWeights , indices ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleCellData( indices, weights2, weights3, boundaryWeights ); // terrain.AddTriangleColor(weights2, weights3, boundaryWeights); // terrain.AddTriangleTerrainTypes(indices); } } The previous two methods rely on TriangulateBoundaryTriangle , which requires an update as well. void TriangulateBoundaryTriangle ( Vector3 begin, Color beginWeights , Vector3 left, Color leftWeights , Vector3 boundary, Color boundaryWeights , Vector3 indices ) { Vector3 v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color w2 = HexMetrics.TerraceLerp( beginWeights , leftWeights , 1); terrain.AddTriangleUnperturbed(HexMetrics.Perturb(begin), v2, boundary); terrain.AddTriangleCellData(indices, beginWeights, w2, boundaryWeights); // terrain.AddTriangleColor(beginColor, c2, boundaryColor); // terrain.AddTriangleTerrainTypes(types); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color w1 = w2 ; v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, i)); w2 = HexMetrics.TerraceLerp( beginWeights , leftWeights , i); terrain.AddTriangleUnperturbed(v1, v2, boundary); terrain.AddTriangleCellData(indices, w1, w2, boundaryWeights); // terrain.AddTriangleColor(c1, c2, boundaryColor); // terrain.AddTriangleTerrainTypes(types); } terrain.AddTriangleUnperturbed(v2, HexMetrics.Perturb(left), boundary); terrain.AddTriangleCellData(indices, w2, leftWeights, boundaryWeights); // terrain.AddTriangleColor(c2, leftColor, boundaryColor); // terrain.AddTriangleTerrainTypes(types); } The final method that requires changes is TriangulateWithRiver . void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … terrain.AddTriangle(centerL, m.v1, m.v2); terrain.AddQuad(centerL, center, m.v2, m.v3); terrain.AddQuad(center, centerR, m.v3, m.v4); terrain.AddTriangle(centerR, m.v4, m.v5); Vector3 indices; indices.x = indices.y = indices.z = cell.Index; terrain.AddTriangleCellData(indices, weights1); terrain.AddQuadCellData(indices, weights1); terrain.AddQuadCellData(indices, weights1); terrain.AddTriangleCellData(indices, weights1); // terrain.AddTriangleColor(weights1); // terrain.AddQuadColor(weights1); // terrain.AddQuadColor(weights1); // terrain.AddTriangleColor(weights1); // Vector3 types; // types.x = types.y = types.z = cell.TerrainTypeIndex; // terrain.AddTriangleTerrainTypes(types); // terrain.AddQuadTerrainTypes(types); // terrain.AddQuadTerrainTypes(types); // terrain.AddTriangleTerrainTypes(types); … } To make this work, we have to indicate that we use cell data for the terrain child of the chunk prefab. Terrain uses cell data. At this point, our mesh contains cell indices instead of terrain type indices. Because the terrain shader still interprets them as terrain indices, you'll see that the first cell is rendered with the first texture, and so on until the last terrain texture is reached. Treating cell indices as terrain texture indices. I can't get the refactored code to work. What am I doing wrong? As a lot of triangulation code has been changed in one go, there's been plenty of opportunity for a mistake or oversight. If you can't find the error, don't forget that you can download the package of this section and extract the relevant files. You can import them in a separate project and compare with your own code.

Passing Cell Data to the Shader In order to use the cell data, the terrain shader needs access to it. We could do this via a shader property, which would require HexCellShaderData to set the terrain material's property. An alternative is to make the cell data texture globally available to all shaders. This is convenient, as we'll be needing it in multiple shaders, so let's use that approach. After the cell texture has been created, invoke the static Shader.SetGlobalTexture method to make it globally known as _HexCellData. public void Initialize (int x, int z) { … else { cellTexture = new Texture2D( x, z, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; Shader.SetGlobalTexture("_HexCellData", cellTexture); } … } When using a shader property, Unity also makes a texture's size available to the shader via a textureName_TexelSize variable. This is a four-component vector which contains the multiplicative inverses of the width and height, and the actual width and height. But when setting a texture globally, this is not done. So let's do it ourselves, via Shader.SetGlobalVector after the texture has been created or resized. else { cellTexture = new Texture2D( x, z, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; Shader.SetGlobalTexture("_HexCellData", cellTexture); } Shader.SetGlobalVector( "_HexCellData_TexelSize", new Vector4(1f / x, 1f / z, x, z) );