Voxel/VoxelChunk.cs
namespace Monolith;
/// <summary>
/// A fixed 32x32x32 block of voxels stored as a bitset. One chunk is 4 KB of state, which is
/// what makes a 16.7M cube monolith cheap to hold, snapshot and send over the network.
/// </summary>
public sealed class VoxelChunk
{
public const int Size = Tuning.ChunkSize;
public const int SizeSq = Size * Size;
public const int VoxelCount = Size * Size * Size;
public const int WordCount = VoxelCount / 64;
/// <summary>Bit set means solid. Index is x + y*Size + z*Size*Size.</summary>
public readonly ulong[] Bits = new ulong[WordCount];
/// <summary>Number of solid voxels, kept incrementally so we never have to popcount.</summary>
public int SolidCount;
/// <summary>Chunk coordinate within the world grid.</summary>
public readonly Vector3Int Coord;
/// <summary>Set when geometry no longer matches <see cref="Bits"/>.</summary>
public bool MeshDirty;
public VoxelChunk( Vector3Int coord )
{
Coord = coord;
}
public bool IsEmpty => SolidCount == 0;
public bool IsFull => SolidCount == VoxelCount;
public static int LocalIndex( int x, int y, int z ) => x + y * Size + z * SizeSq;
public bool Get( int x, int y, int z )
{
var i = LocalIndex( x, y, z );
return (Bits[i >> 6] & (1UL << (i & 63))) != 0;
}
public bool Get( int index )
{
return (Bits[index >> 6] & (1UL << (index & 63))) != 0;
}
/// <summary>Clears a voxel. Returns true if it was solid, so callers can count removals.</summary>
public bool Clear( int x, int y, int z )
{
var i = LocalIndex( x, y, z );
var word = i >> 6;
var mask = 1UL << (i & 63);
if ( (Bits[word] & mask) == 0 )
return false;
Bits[word] &= ~mask;
SolidCount--;
MeshDirty = true;
return true;
}
public bool Set( int x, int y, int z )
{
var i = LocalIndex( x, y, z );
var word = i >> 6;
var mask = 1UL << (i & 63);
if ( (Bits[word] & mask) != 0 )
return false;
Bits[word] |= mask;
SolidCount++;
MeshDirty = true;
return true;
}
public void FillAll()
{
for ( int i = 0; i < WordCount; i++ )
Bits[i] = ulong.MaxValue;
SolidCount = VoxelCount;
MeshDirty = true;
}
public void ClearAll()
{
Array.Clear( Bits, 0, WordCount );
SolidCount = 0;
MeshDirty = true;
}
/// <summary>Recount solid voxels from scratch. Only needed after a bulk bit write.</summary>
public void RecountSolid()
{
int n = 0;
for ( int i = 0; i < WordCount; i++ )
n += PopCount( Bits[i] );
SolidCount = n;
}
/// <summary>
/// Hand rolled rather than System.Numerics.BitOperations, which is not worth betting the
/// build on given the s&box API whitelist. This runs once per chunk per snapshot.
/// </summary>
private static int PopCount( ulong v )
{
v -= (v >> 1) & 0x5555555555555555UL;
v = (v & 0x3333333333333333UL) + ((v >> 2) & 0x3333333333333333UL);
v = (v + (v >> 4)) & 0x0f0f0f0f0f0f0f0fUL;
return (int)((v * 0x0101010101010101UL) >> 56);
}
// ---------------------------------------------------------------- serialisation
public const byte FormatEmpty = 0;
public const byte FormatFull = 1;
public const byte FormatRaw = 2;
/// <summary>
/// Compact form for network snapshots. Most chunks in a monolith are either untouched
/// (full) or completely mined out (empty), so those cost a single byte.
/// </summary>
public byte[] Serialize()
{
if ( IsEmpty ) return new[] { FormatEmpty };
if ( IsFull ) return new[] { FormatFull };
var bytes = new byte[1 + WordCount * 8];
bytes[0] = FormatRaw;
Buffer.BlockCopy( Bits, 0, bytes, 1, WordCount * 8 );
return bytes;
}
public void Deserialize( byte[] data )
{
if ( data == null || data.Length == 0 )
return;
switch ( data[0] )
{
case FormatEmpty:
ClearAll();
return;
case FormatFull:
FillAll();
return;
default:
Buffer.BlockCopy( data, 1, Bits, 0, WordCount * 8 );
RecountSolid();
MeshDirty = true;
return;
}
}
}