You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
154 lines
6.2 KiB
154 lines
6.2 KiB
using System; |
|
|
|
namespace gaemstone.Bloxel.Utility; |
|
|
|
// This struct wraps a primitive integer which represents an index into a space-filling curve |
|
// called "Z-Order Curve" (https://en.wikipedia.org/wiki/Z-order_curve). Often, this is also |
|
// referred to as Morton order, code, or encoding. |
|
// |
|
// This implementation purely focuses on 3 dimensions. |
|
// |
|
// By interleaving the 3 sub-elements into a single integer, some amount of packing can be |
|
// achieved, at the loss of some bits per elements. For example, with a 64 bit integer, 21 |
|
// bits per elements are available (2_097_152 distinct values), which may be enough to |
|
// represent block coordinates in a bloxel game world. |
|
// |
|
// One upside of encoding separate coordinates into a single Z-Order index is that it can then |
|
// be effectively used to index into octrees, and certain operations such as bitwise shifting |
|
// are quite useful. |
|
public readonly struct ZOrder |
|
: IEquatable<ZOrder> |
|
, IComparable<ZOrder> |
|
{ |
|
public const int ELEMENT_MIN = ~0 << BITS_PER_ELEMENT - 1; |
|
public const int ELEMENT_MAX = ~ELEMENT_MIN; |
|
|
|
private const int BITS_SIZE = sizeof(long) * 8; |
|
private const int BITS_PER_ELEMENT = BITS_SIZE / 3; |
|
private const int MAX_USABLE_BITS = BITS_PER_ELEMENT * 3; |
|
private const int SIGN_SHIFT = sizeof(int) * 8 - BITS_PER_ELEMENT; |
|
private const long USABLE_MASK = ~(~0L << MAX_USABLE_BITS); |
|
private const long COMPARE_MASK = ~(~0L << 3) << MAX_USABLE_BITS - 3; |
|
|
|
private static readonly ulong[] MASKS = { |
|
0b_00000000_00000000_00000000_00000000_00000000_00011111_11111111_11111111, // 0x1fffff |
|
0b_00000000_00011111_00000000_00000000_00000000_00000000_11111111_11111111, // 0x1f00000000ffff |
|
0b_00000000_00011111_00000000_00000000_11111111_00000000_00000000_11111111, // 0x1f0000ff0000ff |
|
0b_00010000_00001111_00000000_11110000_00001111_00000000_11110000_00001111, // 0x100f00f00f00f00f |
|
0b_00010000_11000011_00001100_00110000_11000011_00001100_00110000_11000011, // 0x10c30c30c30c30c3 |
|
0b_00010010_01001001_00100100_10010010_01001001_00100100_10010010_01001001, // 0x1249249249249249 |
|
}; |
|
|
|
private static readonly long X_MASK = (long)MASKS[MASKS.Length - 1]; |
|
private static readonly long Y_MASK = X_MASK << 1; |
|
private static readonly long Z_MASK = X_MASK << 2; |
|
private static readonly long XY_MASK = X_MASK | Y_MASK; |
|
private static readonly long XZ_MASK = X_MASK | Z_MASK; |
|
private static readonly long YZ_MASK = Y_MASK | Z_MASK; |
|
|
|
|
|
public long Raw { get; } |
|
|
|
public int X => Decode(0); |
|
public int Y => Decode(1); |
|
public int Z => Decode(2); |
|
|
|
|
|
private ZOrder(long value) |
|
=> Raw = value; |
|
|
|
public static ZOrder FromRaw(long value) |
|
=> new(value & USABLE_MASK); |
|
|
|
public ZOrder(int x, int y, int z) |
|
{ |
|
if (x < ELEMENT_MIN || x > ELEMENT_MAX) throw new ArgumentOutOfRangeException(nameof(x)); |
|
if (y < ELEMENT_MIN || y > ELEMENT_MAX) throw new ArgumentOutOfRangeException(nameof(y)); |
|
if (z < ELEMENT_MIN || z > ELEMENT_MAX) throw new ArgumentOutOfRangeException(nameof(z)); |
|
Raw = Split(x) | Split(y) << 1 | Split(z) << 2; |
|
} |
|
|
|
public void Deconstruct(out int x, out int y, out int z) |
|
=> (x, y, z) = (X, Y, Z); |
|
|
|
|
|
public ZOrder IncX() => FromRaw((Raw | YZ_MASK) + 1 & X_MASK | Raw & YZ_MASK); |
|
public ZOrder IncY() => FromRaw((Raw | XZ_MASK) + (1 << 1) & Y_MASK | Raw & XZ_MASK); |
|
public ZOrder IncZ() => FromRaw((Raw | XY_MASK) + (1 << 2) & Z_MASK | Raw & XY_MASK); |
|
|
|
public ZOrder DecX() => FromRaw((Raw & X_MASK) - 1 & X_MASK | Raw & YZ_MASK); |
|
public ZOrder DecY() => FromRaw((Raw & Y_MASK) - (1 << 1) & Y_MASK | Raw & XZ_MASK); |
|
public ZOrder DecZ() => FromRaw((Raw & Z_MASK) - (1 << 2) & Z_MASK | Raw & XY_MASK); |
|
|
|
public static ZOrder operator +(ZOrder left, ZOrder right) |
|
{ |
|
var xSum = (left.Raw | YZ_MASK) + (right.Raw & X_MASK); |
|
var ySum = (left.Raw | XZ_MASK) + (right.Raw & Y_MASK); |
|
var zSum = (left.Raw | XY_MASK) + (right.Raw & Z_MASK); |
|
return FromRaw(xSum & X_MASK | ySum & Y_MASK | zSum & Z_MASK); |
|
} |
|
|
|
public static ZOrder operator -(ZOrder left, ZOrder right) |
|
{ |
|
var xDiff = (left.Raw & X_MASK) - (right.Raw & X_MASK); |
|
var yDiff = (left.Raw & Y_MASK) - (right.Raw & Y_MASK); |
|
var zDiff = (left.Raw & Z_MASK) - (right.Raw & Z_MASK); |
|
return FromRaw(xDiff & X_MASK | yDiff & Y_MASK | zDiff & Z_MASK); |
|
} |
|
|
|
public static ZOrder operator &(ZOrder left, long right) => FromRaw(left.Raw & right); |
|
public static ZOrder operator |(ZOrder left, long right) => FromRaw(left.Raw | right); |
|
public static ZOrder operator ^(ZOrder left, long right) => FromRaw(left.Raw ^ right); |
|
|
|
public static ZOrder operator &(ZOrder left, ZOrder right) => new(left.Raw & right.Raw); |
|
public static ZOrder operator |(ZOrder left, ZOrder right) => new(left.Raw | right.Raw); |
|
public static ZOrder operator ^(ZOrder left, ZOrder right) => new(left.Raw ^ right.Raw); |
|
|
|
public static ZOrder operator <<(ZOrder left, int right) |
|
{ |
|
if (right >= BITS_PER_ELEMENT) throw new ArgumentOutOfRangeException( |
|
nameof(right), right, $"{nameof(right)} must be smaller than {BITS_PER_ELEMENT}"); |
|
return FromRaw(left.Raw << right * 3); |
|
} |
|
public static ZOrder operator >>(ZOrder left, int right) |
|
{ |
|
var result = left.Raw >> right * 3; |
|
var mask = left.Raw >> MAX_USABLE_BITS - 3 << MAX_USABLE_BITS - right * 3; |
|
for (var i = 0; i < right; i++) { result |= mask; mask <<= 3; } |
|
return FromRaw(result); |
|
} |
|
|
|
public int CompareTo(ZOrder other) => (Raw ^ COMPARE_MASK).CompareTo(other.Raw ^ COMPARE_MASK); |
|
public bool Equals(ZOrder other) => Raw.Equals(other.Raw); |
|
public override bool Equals(object? obj) => obj is ZOrder order && Equals(order); |
|
public override int GetHashCode() => Raw.GetHashCode(); |
|
public override string ToString() => $"<{X},{Y},{Z}>"; |
|
|
|
public static bool operator ==(ZOrder left, ZOrder right) => left.Equals(right); |
|
public static bool operator !=(ZOrder left, ZOrder right) => !left.Equals(right); |
|
|
|
|
|
private static long Split(int i) |
|
{ |
|
var l = (ulong)i; |
|
// l = l & Masks[0]; |
|
l = (l | l << 32) & MASKS[1]; |
|
l = (l | l << 16) & MASKS[2]; |
|
l = (l | l << 8) & MASKS[3]; |
|
l = (l | l << 4) & MASKS[4]; |
|
l = (l | l << 2) & MASKS[5]; |
|
return (long)l; |
|
} |
|
|
|
private int Decode(int index) |
|
{ |
|
var l = (ulong)Raw >> index; |
|
l &= MASKS[5]; |
|
l = (l ^ l >> 2) & MASKS[4]; |
|
l = (l ^ l >> 4) & MASKS[3]; |
|
l = (l ^ l >> 8) & MASKS[2]; |
|
l = (l ^ l >> 16) & MASKS[1]; |
|
l = (l ^ l >> 32) & MASKS[0]; |
|
return (int)l << SIGN_SHIFT >> SIGN_SHIFT; |
|
} |
|
}
|
|
|