AgentSkillsCN

voxel-greedy-meshing-tool

Greedy Meshing算法可优化Voxel世界的网格,减少发送至GPU的多边形/三角形数量。当用户需要从Voxel数据生成网格、优化渲染效果、改进LOD系统,或进行分块网格重建时,可使用此功能。

SKILL.md
--- frontmatter
name: voxel-greedy-meshing-tool
description: Thuật toán Greedy Meshing tối ưu hóa mesh cho thế giới voxel, giảm số polygon/triangle gửi đến GPU. Dùng khi user yêu cầu tạo mesh từ voxel data, tối ưu rendering, LOD system, hoặc chunk mesh rebuild.

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 Texture2DArray vớ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