Voxel Greedy Meshing Tool
Critical Instructions
- •Performance Target: Mesh 1 chunk (32³ voxels) phải hoàn thành < 2ms trên Burst.
- •Burst Mandatory: Toàn bộ thuật toán meshing PHẢI chạy trong Burst-compiled job.
- •Project Convention: Đặt file vào
Assets/Scripts/Rendering/. - •Data Source: Mesh được sinh từ
GlobalVoxelMap(NativeParallelHashMap), KHÔNG từ Entity component.
Core Principles
- •Face Culling: Chỉ sinh face cho bề mặt tiếp xúc với Air. Không vẽ face bên trong khối solid.
- •Greedy Merge: Gộp các face cùng MaterialID, cùng hướng, liền kề thành 1 quad lớn.
- •Per-Chunk: Mỗi chunk sinh 1 Mesh riêng. Chunk dirty → rebuild mesh async.
- •Texture Array: Dùng
Texture2DArrayvới MaterialID làm index, không dùng UV atlas.
Algorithm: Greedy Meshing
Concept
Thay vì vẽ 6 face/voxel (tối đa 6 × 32³ = 196,608 quads/chunk), greedy meshing gộp các face liền kề cùng loại thành rectangles lớn, giảm xuống còn vài trăm quads.
Pseudocode
code
Cho mỗi axis (X, Y, Z) và mỗi direction (+/-):
Cho mỗi slice dọc theo axis:
1. Tạo mask 2D (chunkSize × chunkSize):
mask[u,v] = MaterialID nếu voxel có mặt lộ ra, 0 nếu không
2. Greedy scan mask:
Với mỗi ô chưa xử lý:
- Mở rộng theo U tới khi gặp MaterialID khác hoặc hết mask
- Mở rộng theo V tới khi toàn bộ hàng cùng MaterialID
- Ghi nhận quad (u, v, width, height, materialID, normal)
- Đánh dấu vùng đã xử lý
3. Emit vertices + triangles cho các quads
Implementation Template
csharp
using Unity.Burst;
using Unity.Collections;
using Unity.Mathematics;
/// <summary>
/// Greedy Meshing Job: Sinh mesh tối ưu cho 1 chunk voxel.
/// Gộp các face liền kề cùng MaterialID thành quad lớn.
/// </summary>
[BurstCompile]
public struct GreedyMeshingJob : IJobParallelFor
{
// Input: voxel data cho chunk + 6 neighbor chunks (để xét face biên)
[ReadOnly] public NativeArray<VoxelData> ChunkVoxels; // ChunkSize³
[ReadOnly] public NativeArray<VoxelData> NeighborPosX; // Chunk bên +X
[ReadOnly] public NativeArray<VoxelData> NeighborNegX; // Chunk bên -X
[ReadOnly] public NativeArray<VoxelData> NeighborPosY;
[ReadOnly] public NativeArray<VoxelData> NeighborNegY;
[ReadOnly] public NativeArray<VoxelData> NeighborPosZ;
[ReadOnly] public NativeArray<VoxelData> NeighborNegZ;
// Output: mesh data (mỗi chunk 1 slot trong output arrays)
[NativeDisableParallelForRestriction]
public NativeList<float3>.ParallelWriter Vertices;
[NativeDisableParallelForRestriction]
public NativeList<int>.ParallelWriter Triangles;
[NativeDisableParallelForRestriction]
public NativeList<float3>.ParallelWriter Normals;
[NativeDisableParallelForRestriction]
public NativeList<float2>.ParallelWriter UVs;
[NativeDisableParallelForRestriction]
public NativeList<int>.ParallelWriter MaterialIndices; // for Texture2DArray
public int3 ChunkSize; // typically (32, 32, 32)
public void Execute(int chunkIndex)
{
// 1. Iterate 6 directions (±X, ±Y, ±Z)
// 2. For each slice along axis, build 2D mask
// 3. Greedy scan mask → emit quads
// 4. Write to output arrays
}
// Helper: Convert 3D coord to flat index
private int FlatIndex(int x, int y, int z)
{
return x + y * ChunkSize.x + z * ChunkSize.x * ChunkSize.y;
}
// Helper: Check if face should be visible
private bool IsFaceVisible(int x, int y, int z, int nx, int ny, int nz)
{
var current = ChunkVoxels[FlatIndex(x, y, z)];
if (current.State == 0) return false; // Air has no faces
// Check neighbor (may be in adjacent chunk)
// If neighbor is Air or transparent → face visible
return true; // simplified
}
}
Mesh Output Structure
csharp
/// <summary>
/// Kết quả mesh cho 1 chunk, dùng để apply vào MeshFilter/RenderMesh.
/// </summary>
public struct ChunkMeshData : System.IDisposable
{
public NativeList<float3> Vertices;
public NativeList<int> Triangles;
public NativeList<float3> Normals;
public NativeList<float2> UVs;
public NativeList<int> MaterialIDs; // Per-vertex material index
public void Allocate(Allocator allocator)
{
Vertices = new NativeList<float3>(4096, allocator);
Triangles = new NativeList<int>(6144, allocator);
Normals = new NativeList<float3>(4096, allocator);
UVs = new NativeList<float2>(4096, allocator);
MaterialIDs = new NativeList<int>(4096, allocator);
}
public void Dispose()
{
if (Vertices.IsCreated) Vertices.Dispose();
if (Triangles.IsCreated) Triangles.Dispose();
if (Normals.IsCreated) Normals.Dispose();
if (UVs.IsCreated) UVs.Dispose();
if (MaterialIDs.IsCreated) MaterialIDs.Dispose();
}
}
LOD Strategy (VisualWorld)
Multi-Level Mesh Detail
code
Distance | Strategy | Voxel Resolution < 10m | Full Greedy Mesh | 2cm (native) 10m - 50m | Downsampled Greedy Mesh | 4cm (2×2×2 merge) 50m - 200m | Simplified blocks | 16cm (8×8×8 merge) > 200m | Imposters (billboard) | 2D sprite
LOD Downsample Logic
csharp
/// <summary>
/// Downsample chunk bằng cách gộp 2×2×2 voxel thành 1.
/// Voxel chiếm đa số (majority vote) được chọn làm đại diện.
/// </summary>
[BurstCompile]
public struct DownsampleJob : IJobParallelFor
{
[ReadOnly] public NativeArray<VoxelData> HighRes; // 32³
[WriteOnly] public NativeArray<VoxelData> LowRes; // 16³
public int HighSize; // 32
public int LowSize; // 16
public void Execute(int lowIndex)
{
int lx = lowIndex % LowSize;
int ly = (lowIndex / LowSize) % LowSize;
int lz = lowIndex / (LowSize * LowSize);
int hx = lx * 2, hy = ly * 2, hz = lz * 2;
// Majority vote trong 2×2×2 block
// MaterialID xuất hiện nhiều nhất → giữ lại
}
}
System Integration
csharp
/// <summary>
/// System rebuild mesh cho dirty chunks.
/// Chỉ chạy khi có chunk đánh dấu DirtyChunkTag.
/// </summary>
[BurstCompile]
[UpdateInGroup(typeof(PresentationSystemGroup))]
public partial struct MeshRebuildSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<DirtyChunkTag>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// 1. Query dirty chunks
// 2. Schedule GreedyMeshingJob per chunk
// 3. Apply mesh data → MeshFilter
// 4. Remove DirtyChunkTag
}
}
Validation Checklist
- • Face culling: Không sinh face giữa 2 solid voxel liền kề
- • Neighbor awareness: Xét face biên chunk bằng neighbor chunk data
- • Greedy merge: Quads được gộp tối đa (so sánh triangle count vs brute-force)
- • Material separation: Không gộp face khác MaterialID
- • UV mapping: UVs tương thích Texture2DArray (u, v scales theo quad size)
- • NativeContainer Dispose: Tất cả temp arrays được giải phóng
- • Burst compiled: Job biên dịch thành công với Burst
- • LOD consistency: Mesh ở LOD thấp không tạo gap/seam với LOD cao