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;
}
}