using System.Collections.Generic; using UnityEngine; /// /// Builds optimized meshes for chunks with face culling. /// Only renders faces that are exposed (adjacent to empty blocks or chunk boundaries). /// Uses submeshes for different block types (normal/resource). /// public static class ChunkMeshBuilder { private const int CHUNK_SIZE = ChunkCoord.CHUNK_SIZE; // Face directions private static readonly Vector3Int[] FaceDirections = new Vector3Int[] { Vector3Int.right, // +X Vector3Int.left, // -X Vector3Int.up, // +Y Vector3Int.down, // -Y new Vector3Int(0, 0, 1), // +Z new Vector3Int(0, 0, -1) // -Z }; // Face normals matching directions private static readonly Vector3[] FaceNormals = new Vector3[] { Vector3.right, Vector3.left, Vector3.up, Vector3.down, Vector3.forward, Vector3.back }; // Vertices for each face (relative to block center) private static readonly Vector3[][] FaceVertices = new Vector3[][] { // +X face new Vector3[] { new Vector3(0.5f, -0.5f, -0.5f), new Vector3(0.5f, 0.5f, -0.5f), new Vector3(0.5f, 0.5f, 0.5f), new Vector3(0.5f, -0.5f, 0.5f) }, // -X face new Vector3[] { new Vector3(-0.5f, -0.5f, 0.5f), new Vector3(-0.5f, 0.5f, 0.5f), new Vector3(-0.5f, 0.5f, -0.5f), new Vector3(-0.5f, -0.5f, -0.5f) }, // +Y face new Vector3[] { new Vector3(-0.5f, 0.5f, -0.5f), new Vector3(-0.5f, 0.5f, 0.5f), new Vector3( 0.5f, 0.5f, 0.5f), new Vector3( 0.5f, 0.5f, -0.5f) }, // -Y face new Vector3[] { new Vector3(-0.5f, -0.5f, 0.5f), new Vector3(-0.5f, -0.5f, -0.5f), new Vector3( 0.5f, -0.5f, -0.5f), new Vector3( 0.5f, -0.5f, 0.5f) }, // +Z face new Vector3[] { new Vector3(-0.5f, -0.5f, 0.5f), new Vector3( 0.5f, -0.5f, 0.5f), new Vector3( 0.5f, 0.5f, 0.5f), new Vector3(-0.5f, 0.5f, 0.5f) }, // -Z face new Vector3[] { new Vector3( 0.5f, -0.5f, -0.5f), new Vector3(-0.5f, -0.5f, -0.5f), new Vector3(-0.5f, 0.5f, -0.5f), new Vector3( 0.5f, 0.5f, -0.5f) } }; // UV coordinates for each face private static readonly Vector2[] FaceUVs = new Vector2[] { new Vector2(0, 0), new Vector2(1, 0), new Vector2(1, 1), new Vector2(0, 1) }; /// /// Interface for querying neighboring chunks for cross-chunk face culling /// public interface INeighborProvider { /// /// Check if block at world grid position is solid (for face culling) /// bool IsBlockSolid(Vector3Int gridPos); } /// /// Visibility check delegate for fog of war /// public delegate bool VisibilityChecker(int blockIndex); /// /// Build mesh for a chunk with face culling. /// Returns a mesh with two submeshes: 0=normal blocks, 1=resource blocks /// public static Mesh BuildMesh(ChunkState state, ChunkCoord coord, INeighborProvider neighborProvider = null, bool onlyDiscovered = true) { return BuildMeshWithVisibility(state, coord, neighborProvider, onlyDiscovered, null); } /// /// Build mesh for a chunk with face culling and fog of war support. /// Returns a mesh with 4 submeshes: 0=normal-visible, 1=resource-visible, 2=normal-dark, 3=resource-dark /// public static Mesh BuildMeshWithVisibility(ChunkState state, ChunkCoord coord, INeighborProvider neighborProvider, bool onlyDiscovered, VisibilityChecker isVisible) { // Lists for 4 submeshes: normal-visible, resource-visible, normal-dark, resource-dark var meshData = new MeshData[4]; for (int i = 0; i < 4; i++) { meshData[i] = new MeshData(); } // Handle null blocks array if (state.blocks == null) { return CreateEmptyMesh(coord, 4); } // Process each block for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++) { BlockData block = state.blocks[i]; // Skip empty blocks if (block.IsEmpty) continue; // Skip undiscovered blocks if onlyDiscovered is true if (onlyDiscovered && !block.IsDiscovered) continue; Vector3Int localPos = ChunkCoord.IndexToLocal(i); Vector3 blockOffset = new Vector3(localPos.x, localPos.y, localPos.z); // Determine which submesh to use based on block type and visibility bool currentlyVisible = isVisible != null ? isVisible(i) : true; int submeshIndex; if (block.IsResource) { submeshIndex = currentlyVisible ? 1 : 3; // resource-visible or resource-dark } else { submeshIndex = currentlyVisible ? 0 : 2; // normal-visible or normal-dark } var data = meshData[submeshIndex]; // Check each face for (int f = 0; f < 6; f++) { Vector3Int neighborLocal = localPos + FaceDirections[f]; bool shouldRenderFace = false; if (ChunkCoord.IsValidLocal(neighborLocal)) { int neighborIndex = ChunkCoord.LocalToIndex(neighborLocal); BlockData neighborBlock = state.blocks[neighborIndex]; shouldRenderFace = neighborBlock.IsEmpty || (onlyDiscovered && !neighborBlock.IsDiscovered); } else { if (neighborProvider != null) { Vector3Int neighborGridPos = coord.LocalToGrid(neighborLocal); shouldRenderFace = !neighborProvider.IsBlockSolid(neighborGridPos); } else { shouldRenderFace = true; } } if (shouldRenderFace) { AddFace(data.vertices, data.normals, data.uvs, data.triangles, blockOffset, f); } } } // Create the mesh Mesh mesh = new Mesh(); mesh.name = $"Chunk_{coord.chunkPos.x}_{coord.chunkPos.y}_{coord.chunkPos.z}"; // Combine all vertices from all submeshes int totalVerts = 0; int[] vertOffsets = new int[4]; for (int i = 0; i < 4; i++) { vertOffsets[i] = totalVerts; totalVerts += meshData[i].vertices.Count; } var allVertices = new Vector3[totalVerts]; var allNormals = new Vector3[totalVerts]; var allUVs = new Vector2[totalVerts]; for (int i = 0; i < 4; i++) { meshData[i].vertices.CopyTo(allVertices, vertOffsets[i]); meshData[i].normals.CopyTo(allNormals, vertOffsets[i]); meshData[i].uvs.CopyTo(allUVs, vertOffsets[i]); } mesh.vertices = allVertices; mesh.normals = allNormals; mesh.uv = allUVs; // Set submeshes with adjusted triangle indices mesh.subMeshCount = 4; for (int i = 0; i < 4; i++) { var triangles = meshData[i].triangles; for (int t = 0; t < triangles.Count; t++) { triangles[t] += vertOffsets[i]; } mesh.SetTriangles(triangles, i); } mesh.RecalculateBounds(); return mesh; } /// /// Helper class to hold mesh data for each submesh /// private class MeshData { public List vertices = new List(); public List normals = new List(); public List uvs = new List(); public List triangles = new List(); } /// /// Create an empty mesh with proper submesh configuration /// private static Mesh CreateEmptyMesh(ChunkCoord coord, int submeshCount = 4) { Mesh mesh = new Mesh(); mesh.name = $"Chunk_{coord.chunkPos.x}_{coord.chunkPos.y}_{coord.chunkPos.z}_Empty"; mesh.subMeshCount = submeshCount; for (int i = 0; i < submeshCount; i++) { mesh.SetTriangles(new int[0], i); } return mesh; } /// /// Add a face to the mesh data /// private static void AddFace(List vertices, List normals, List uvs, List triangles, Vector3 blockOffset, int faceIndex) { int startVertex = vertices.Count; // Add vertices for (int i = 0; i < 4; i++) { vertices.Add(FaceVertices[faceIndex][i] + blockOffset); normals.Add(FaceNormals[faceIndex]); uvs.Add(FaceUVs[i]); } // Add triangles (two triangles per face) triangles.Add(startVertex); triangles.Add(startVertex + 1); triangles.Add(startVertex + 2); triangles.Add(startVertex); triangles.Add(startVertex + 2); triangles.Add(startVertex + 3); } /// /// Count visible faces in a chunk (for debugging/stats) /// public static int CountVisibleFaces(ChunkState state, bool onlyDiscovered = true) { if (state.blocks == null) return 0; int faceCount = 0; for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++) { BlockData block = state.blocks[i]; if (block.IsEmpty) continue; if (onlyDiscovered && !block.IsDiscovered) continue; Vector3Int localPos = ChunkCoord.IndexToLocal(i); for (int f = 0; f < 6; f++) { Vector3Int neighborLocal = localPos + FaceDirections[f]; if (ChunkCoord.IsValidLocal(neighborLocal)) { int neighborIndex = ChunkCoord.LocalToIndex(neighborLocal); BlockData neighborBlock = state.blocks[neighborIndex]; if (neighborBlock.IsEmpty || (onlyDiscovered && !neighborBlock.IsDiscovered)) { faceCount++; } } else { faceCount++; } } } return faceCount; } }