using System; using System.Collections.Generic; using System.Linq; using System.Text; using UnityEngine; namespace HexTiles { /// /// Utilites for generating meshes for hex tiles. /// public static class HexMeshGenerator { /// /// Tileset textures are 3 hexes wide for the tops of the tiles. /// private static readonly float hexWidthUV = 1f / 3f; /// /// Side pieces start half way down the texture. /// private static readonly float sidePieceStartingUVY = 0.5f; /// /// Get the UV coordinates of a given hex tile. /// private static Vector2 HexCoordsToUV(HexCoords hIn) { var x = hexWidthUV/2f * 3f/2f * hIn.Q; var y = hexWidthUV/2f * Mathf.Sqrt(3f) * (hIn.R + hIn.Q / 2f); return new Vector2(x, y); } /// /// Generate the mesh for the specified position with the specified side pieces. /// Adds the necessary vertices, UVs and tris to the supplied lists. /// internal static HexMeshData GenerateHexMesh(HexCoords position, float diameter, IEnumerable sidePieces) { var vertices = new List(); var tris = new List(); var uvs = new List(); GenerateTop(position, vertices, tris, uvs, diameter); GenerateSidePieces(vertices, tris, uvs, sidePieces, diameter); return new HexMeshData() { verts = vertices, tris = tris, uvs = uvs }; } /// /// Generate the top for the hex tile, assiging its vertices, triangles and UVs to the supplied lists. /// private static void GenerateTop(HexCoords position, List vertices, List triangles, List uv, float diameter) { var uvBasePos = HexCoordsToUV(position); vertices.AddRange(HexMetrics.GetHexVertices(diameter)); // Each third tile needs a UV seam through the middle of it for textures to loop properly. var offsetCoords = position.ToOffset(); if (offsetCoords.x % 2 == 0 && offsetCoords.y % 3 == 0) { vertices.Insert(4, vertices[0]); vertices.Insert(5, vertices[3]); triangles.AddRange(new int[] { // Start with the preset triangles for the hex top. 0, 1, 2, 0, 2, 3, 4, 5, 7, 7, 5, 6 }); for (var i = 0; i < vertices.Count; i++) { float uvY; if (i == 0 || i == 3) { uvY = 0.0f; } else if (i == 5 || i == 4) { uvY = -0.5f; } else { var relativeVertexPosInUVSpace = vertices[i].z / diameter * hexWidthUV; uvY = -Utils.Mod(((uvBasePos.y + relativeVertexPosInUVSpace) / HexMetrics.hexHeightToWidth / 2), 0.5f); } uv.Add(new Vector2( -(uvBasePos.x + vertices[i].x / diameter * hexWidthUV), uvY)); } } else { triangles.AddRange(new int[] { // Start with the preset triangles for the hex top. 0, 1, 2, 0, 2, 3, 0, 3, 5, 5, 3, 4 }); for (var i = 0; i < vertices.Count; i++) { var relativeVertexPosInUVSpace = vertices[i].z / diameter * hexWidthUV; float uvY; // Sometimes due to rounding the UV coordinate can end up on the wrong side of the texture // for tiles at the edges of the texture. This will ensure that they appear on the correct // side. if ((offsetCoords.x % 2 != 0 && (offsetCoords.y + 1) % 3 == 0) && (i == 1 || i == 2)) { uvY = -0.5f; } else if (offsetCoords.x % 2 != 0 && offsetCoords.y % 3 == 0 && (i == 4 || i == 5)) { uvY = 0f; } else { uvY = -Utils.Mod(((uvBasePos.y + relativeVertexPosInUVSpace) / HexMetrics.hexHeightToWidth / 2), 0.5f); } uv.Add(new Vector2( -(uvBasePos.x + vertices[i].x / diameter * hexWidthUV), uvY)); } } } /// /// Generates all the necessary side pieces, assigning their vertices, triangles and UVs back to the supplied lists. /// private static void GenerateSidePieces(List vertices, List triangles, List uv, IEnumerable sidePieces, float diameter) { var topVerts = HexMetrics.GetHexVertices(diameter).ToList(); foreach (var sidePiece in sidePieces) { // Nedd to add a side piece for each time the texture loops. var sideLoopCount = 0; var maxSideHeight = (diameter / 2f * 3f); // Maximum height of a single piece before they loop var totalSideHeight = sidePiece.elevationDelta; do { // The Y position in UV space of the bottom vertices. // Dependent on the height of the side piece. var currentHeightLeft = (totalSideHeight - sideLoopCount * maxSideHeight); var currentPieceHeight = Mathf.Min(maxSideHeight, currentHeightLeft); var startingHeight = -maxSideHeight * sideLoopCount; var sideIndex = sidePiece.direction; var nextSideIndex = (sidePiece.direction + 1) % 6; var nextVertexIndex = vertices.Count; vertices.Add(new Vector3(topVerts[sideIndex].x, startingHeight, topVerts[sideIndex].z)); vertices.Add(new Vector3(topVerts[nextSideIndex].x, startingHeight, topVerts[nextSideIndex].z)); vertices.Add(new Vector3(topVerts[sideIndex].x, startingHeight - currentPieceHeight, topVerts[sideIndex].z)); vertices.Add(new Vector3(topVerts[nextSideIndex].x, startingHeight - currentPieceHeight, topVerts[nextSideIndex].z)); triangles.AddRange(new int[]{ nextVertexIndex, nextVertexIndex + 2, nextVertexIndex + 1, nextVertexIndex + 1, nextVertexIndex + 2, nextVertexIndex + 3 }); // We're only using the top or bottom half of the bottom half of the texture for this part of the side pieces. const float maxUVHeight = 0.25f; var bottomUvY = sidePieceStartingUVY + (currentPieceHeight / maxSideHeight) * -maxUVHeight; // The first part of the side piece uses the top half of the texture, // whild the rest use looped copies of the second half of the texture. if (sideLoopCount == 0) { uv.Add(new Vector2((hexWidthUV / 2f) * sideIndex, sidePieceStartingUVY)); uv.Add(new Vector2((hexWidthUV / 2f) * (sideIndex + 1), sidePieceStartingUVY)); uv.Add(new Vector2((hexWidthUV / 2f) * sideIndex, bottomUvY)); uv.Add(new Vector2((hexWidthUV / 2f) * (sideIndex + 1), bottomUvY)); } else { uv.Add(new Vector2((hexWidthUV / 2f) * sideIndex, sidePieceStartingUVY - maxUVHeight)); uv.Add(new Vector2((hexWidthUV / 2f) * (sideIndex + 1), sidePieceStartingUVY - maxUVHeight)); uv.Add(new Vector2((hexWidthUV / 2f) * sideIndex, bottomUvY - maxUVHeight)); uv.Add(new Vector2((hexWidthUV / 2f) * (sideIndex + 1), bottomUvY - maxUVHeight)); } sideLoopCount++; } while (maxSideHeight * sideLoopCount < totalSideHeight); } } /// /// Data structure containing the vertex, triangle and UV data for a hex tile. /// public struct HexMeshData { public IEnumerable verts; public IEnumerable tris; public IEnumerable uvs; } } }