AutoRig/Voxel/VoxelGrid.cs

A VoxelGrid type that voxelizes a mesh into a 3D boolean grid. It conservatively rasterizes triangle surfaces into voxels, performs an exterior flood fill to classify exterior vs interior, and marks solid voxels (surface or interior). It exposes size, cell size, origin, indexing, and coordinate conversion helpers.

File Access
using AutoRig.Analyze;
using AutoRig.Mesh;

namespace AutoRig.Voxel;

// s&box compat: the engine defines Vector2/Vector3 in the GLOBAL namespace, which
// shadows using-directive imports - alias explicitly to System.Numerics.
using Vector3 = System.Numerics.Vector3;

/// <summary>
/// A solid voxelization of a mesh: conservative surface rasterization plus interior
/// fill (exterior flood from the padded border; whatever the flood cannot reach and
/// is not surface is interior). Solid = surface ∪ interior.
/// </summary>
public sealed class VoxelGrid
{
    public int SizeX { get; private init; }
    public int SizeY { get; private init; }
    public int SizeZ { get; private init; }
    public float CellSize { get; private init; }

    /// <summary>World position of voxel (0,0,0)'s min corner.</summary>
    public Vector3 Origin { get; private init; }

    public int SolidCount { get; private set; }

    bool[] _solid = [];

    public bool IsSolid( int x, int y, int z )
        => x >= 0 && x < SizeX && y >= 0 && y < SizeY && z >= 0 && z < SizeZ
            && _solid[Index( x, y, z )];

    public Vector3 CenterOf( int x, int y, int z )
        => Origin + new Vector3( (x + 0.5f) * CellSize, (y + 0.5f) * CellSize, (z + 0.5f) * CellSize );

    public (int X, int Y, int Z) VoxelOf( Vector3 world )
    {
        var p = (world - Origin) / CellSize;
        return (Math.Clamp( (int)p.X, 0, SizeX - 1 ),
                Math.Clamp( (int)p.Y, 0, SizeY - 1 ),
                Math.Clamp( (int)p.Z, 0, SizeZ - 1 ));
    }

    public int Index( int x, int y, int z ) => (z * SizeY + y) * SizeX + x;

    /// <exception cref="FormatException">Empty mesh or degenerate bounds.</exception>
    public static VoxelGrid Build( RigMesh mesh, int maxDimension )
    {
        ArgumentNullException.ThrowIfNull( mesh );
        if ( mesh.Positions.Length == 0 || mesh.TriangleCount == 0 )
            throw new FormatException( "Cannot voxelize an empty mesh." );
        if ( maxDimension < 4 )
            throw new FormatException( $"Voxel maxDimension {maxDimension} too small (need >= 4)." );

        var bounds = mesh.ComputeBounds();
        var size = bounds.Size;
        var longest = MathF.Max( size.X, MathF.Max( size.Y, size.Z ) );
        if ( longest <= 0f )
            throw new FormatException( "Cannot voxelize a mesh with zero extent." );

        const int padding = 2;
        var cell = longest / maxDimension;
        var sizeX = Math.Max( 2, (int)MathF.Ceiling( size.X / cell ) ) + padding * 2;
        var sizeY = Math.Max( 2, (int)MathF.Ceiling( size.Y / cell ) ) + padding * 2;
        var sizeZ = Math.Max( 2, (int)MathF.Ceiling( size.Z / cell ) ) + padding * 2;

        var grid = new VoxelGrid
        {
            SizeX = sizeX,
            SizeY = sizeY,
            SizeZ = sizeZ,
            CellSize = cell,
            Origin = bounds.Min - new Vector3( padding * cell ),
        };
        grid._solid = new bool[sizeX * sizeY * sizeZ];
        var surface = grid._solid; // filled as surface first

        // ---- conservative surface rasterization ----
        var reach = cell * 0.87f; // ~half the cell diagonal: cell centers within this of a triangle are surface
        for ( var t = 0; t < mesh.Triangles.Length; t += 3 )
        {
            var a = mesh.Positions[mesh.Triangles[t]];
            var b = mesh.Positions[mesh.Triangles[t + 1]];
            var c = mesh.Positions[mesh.Triangles[t + 2]];

            var min = Vector3.Min( a, Vector3.Min( b, c ) ) - new Vector3( reach );
            var max = Vector3.Max( a, Vector3.Max( b, c ) ) + new Vector3( reach );
            var (x0, y0, z0) = grid.VoxelOf( min );
            var (x1, y1, z1) = grid.VoxelOf( max );

            for ( var z = z0; z <= z1; z++ )
                for ( var y = y0; y <= y1; y++ )
                    for ( var x = x0; x <= x1; x++ )
                    {
                        var i = grid.Index( x, y, z );
                        if ( surface[i] )
                            continue;
                        var center = grid.CenterOf( x, y, z );
                        var closest = PartContacts.ClosestPointOnTriangle( center, a, b, c );
                        if ( Vector3.DistanceSquared( center, closest ) <= reach * reach )
                            surface[i] = true;
                    }
        }

        // ---- exterior flood (6-connectivity) from every border voxel ----
        var exterior = new bool[surface.Length];
        var queue = new Queue<(int X, int Y, int Z)>();
        void Push( int x, int y, int z )
        {
            var i = grid.Index( x, y, z );
            if ( exterior[i] || surface[i] )
                return;
            exterior[i] = true;
            queue.Enqueue( (x, y, z) );
        }
        for ( var x = 0; x < sizeX; x++ )
            for ( var y = 0; y < sizeY; y++ )
            {
                Push( x, y, 0 );
                Push( x, y, sizeZ - 1 );
            }
        for ( var x = 0; x < sizeX; x++ )
            for ( var z = 0; z < sizeZ; z++ )
            {
                Push( x, 0, z );
                Push( x, sizeY - 1, z );
            }
        for ( var y = 0; y < sizeY; y++ )
            for ( var z = 0; z < sizeZ; z++ )
            {
                Push( 0, y, z );
                Push( sizeX - 1, y, z );
            }
        while ( queue.Count > 0 )
        {
            var (x, y, z) = queue.Dequeue();
            if ( x > 0 ) Push( x - 1, y, z );
            if ( x < sizeX - 1 ) Push( x + 1, y, z );
            if ( y > 0 ) Push( x, y - 1, z );
            if ( y < sizeY - 1 ) Push( x, y + 1, z );
            if ( z > 0 ) Push( x, y, z - 1 );
            if ( z < sizeZ - 1 ) Push( x, y, z + 1 );
        }

        // ---- solid = surface ∪ interior (not exterior) ----
        var solidCount = 0;
        for ( var i = 0; i < surface.Length; i++ )
        {
            surface[i] = surface[i] || !exterior[i];
            if ( surface[i] )
                solidCount++;
        }
        grid.SolidCount = solidCount;
        return grid;
    }
}