Code/AutoRig/Voxel/VoxelGrid.cs

A voxel grid builder and accessor for converting a triangle mesh into a solid voxelization. It computes conservative surface voxels by testing triangle proximity, floods from the padded border to mark exterior, then marks solid voxels as surface or non-exterior interior and exposes queries for voxel membership and positions.

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