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