331 lines
11 KiB
C#
331 lines
11 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
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)
|
|
};
|
|
|
|
/// <summary>
|
|
/// Interface for querying neighboring chunks for cross-chunk face culling
|
|
/// </summary>
|
|
public interface INeighborProvider
|
|
{
|
|
/// <summary>
|
|
/// Check if block at world grid position is solid (for face culling)
|
|
/// </summary>
|
|
bool IsBlockSolid(Vector3Int gridPos);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Visibility check delegate for fog of war
|
|
/// </summary>
|
|
public delegate bool VisibilityChecker(int blockIndex);
|
|
|
|
/// <summary>
|
|
/// Build mesh for a chunk with face culling.
|
|
/// Returns a mesh with two submeshes: 0=normal blocks, 1=resource blocks
|
|
/// </summary>
|
|
public static Mesh BuildMesh(ChunkState state, ChunkCoord coord, INeighborProvider neighborProvider = null, bool onlyDiscovered = true)
|
|
{
|
|
return BuildMeshWithVisibility(state, coord, neighborProvider, onlyDiscovered, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper class to hold mesh data for each submesh
|
|
/// </summary>
|
|
private class MeshData
|
|
{
|
|
public List<Vector3> vertices = new List<Vector3>();
|
|
public List<Vector3> normals = new List<Vector3>();
|
|
public List<Vector2> uvs = new List<Vector2>();
|
|
public List<int> triangles = new List<int>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create an empty mesh with proper submesh configuration
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a face to the mesh data
|
|
/// </summary>
|
|
private static void AddFace(List<Vector3> vertices, List<Vector3> normals, List<Vector2> uvs, List<int> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Count visible faces in a chunk (for debugging/stats)
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|