commit
fae12b7963
66 changed files with 4258 additions and 0 deletions
@ -0,0 +1,20 @@ |
||||
root = true |
||||
|
||||
[*] |
||||
end_of_line = lf |
||||
indent_style = space |
||||
indent_size = 2 |
||||
insert_final_newline = true |
||||
trim_trailing_whitespace = true |
||||
|
||||
[*.cs] |
||||
indent_style = tab |
||||
indent_size = 4 |
||||
# IDE0005: Using directive is unnecessary |
||||
dotnet_diagnostic.IDE0005.severity = suggestion |
||||
# IDE0047: Parentheses can be removed |
||||
dotnet_diagnostic.IDE0047.severity = none |
||||
|
||||
[*.md] |
||||
# Allows placing double-space at end of lines. |
||||
trim_trailing_whitespace = false |
@ -0,0 +1,4 @@ |
||||
**/obj/ |
||||
**/bin/ |
||||
/artifacts/ |
||||
/packages/ |
@ -0,0 +1,6 @@ |
||||
[submodule "src/flecs-cs"] |
||||
path = src/flecs-cs |
||||
url = https://github.com/flecs-hub/flecs-cs |
||||
[submodule "src/FastNoiseLite"] |
||||
path = src/FastNoiseLite |
||||
url = https://github.com/Auburn/FastNoiseLite.git |
@ -0,0 +1,16 @@ |
||||
{ |
||||
"version": "0.2.0", |
||||
"configurations": [ |
||||
{ |
||||
"name": "Launch Immersion", |
||||
"type": "coreclr", |
||||
"request": "launch", |
||||
"preLaunchTask": "build", |
||||
"program": "${workspaceFolder}/src/Immersion/bin/Debug/net6.0/Immersion.dll", |
||||
"args": [], |
||||
"cwd": "${workspaceFolder}/src/Immersion", |
||||
"console": "internalConsole", |
||||
"stopAtEntry": false |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,8 @@ |
||||
{ |
||||
"files.exclude": { |
||||
"**/.git": true, |
||||
"**/.DS_Store": true, |
||||
"**/bin": true, |
||||
"**/obj": true, |
||||
}, |
||||
} |
@ -0,0 +1,40 @@ |
||||
{ |
||||
"version": "2.0.0", |
||||
"tasks": [{ |
||||
"label": "build", |
||||
"command": "dotnet", |
||||
"type": "process", |
||||
"args": [ |
||||
"build", |
||||
"${workspaceFolder}/src/Immersion/Immersion.csproj", |
||||
"/property:GenerateFullPaths=true", |
||||
"/consoleloggerparameters:NoSummary" |
||||
], |
||||
"problemMatcher": "$msCompile" |
||||
}, |
||||
{ |
||||
"label": "publish", |
||||
"command": "dotnet", |
||||
"type": "process", |
||||
"args": [ |
||||
"publish", |
||||
"${workspaceFolder}/src/Immersion/Immersion.csproj", |
||||
"/property:GenerateFullPaths=true", |
||||
"/consoleloggerparameters:NoSummary" |
||||
], |
||||
"problemMatcher": "$msCompile" |
||||
}, |
||||
{ |
||||
"label": "watch", |
||||
"command": "dotnet", |
||||
"type": "process", |
||||
"args": [ |
||||
"watch", |
||||
"run", |
||||
"--project", |
||||
"${workspaceFolder}/src/Immersion/Immersion.csproj" |
||||
], |
||||
"problemMatcher": "$msCompile" |
||||
} |
||||
] |
||||
} |
@ -0,0 +1 @@ |
||||
Subproject commit 5923df5d822f7610100d0e77f629c607ed64934a |
@ -0,0 +1,23 @@ |
||||
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
||||
<PropertyGroup> |
||||
<OutputType>Exe</OutputType> |
||||
<TargetFramework>net6.0</TargetFramework> |
||||
<ImplicitUsings>disable</ImplicitUsings> |
||||
<Nullable>enable</Nullable> |
||||
</PropertyGroup> |
||||
|
||||
<ItemGroup> |
||||
<EmbeddedResource Include="Resources/*" /> |
||||
</ItemGroup> |
||||
|
||||
<ItemGroup> |
||||
<ProjectReference Include="../gaemstone.Bloxel/gaemstone.Bloxel.csproj" /> |
||||
<ProjectReference Include="../gaemstone.Client/gaemstone.Client.csproj" /> |
||||
</ItemGroup> |
||||
|
||||
<ItemGroup> |
||||
<PackageReference Include="Silk.NET" Version="2.16.0" /> |
||||
</ItemGroup> |
||||
|
||||
</Project> |
@ -0,0 +1,11 @@ |
||||
# Resources License Notices |
||||
|
||||
## Voxelgarden Textures |
||||
|
||||
- terrain.png |
||||
|
||||
**License:** [CC-BY-SA] |
||||
**Source:** [github.com/CasimirKaPazi/Voxelgarden](https://github.com/CasimirKaPazi/Voxelgarden) |
||||
|
||||
|
||||
[CC-BY-SA]: https://creativecommons.org/licenses/by-sa/2.0/ |
@ -0,0 +1,12 @@ |
||||
#version 330 core |
||||
in vec4 fragColor; |
||||
in vec2 fragUV; |
||||
|
||||
uniform sampler2D textureSampler; |
||||
|
||||
out vec4 color; |
||||
|
||||
void main() |
||||
{ |
||||
color = fragColor * texture(textureSampler, fragUV); |
||||
} |
@ -0,0 +1,20 @@ |
||||
#version 330 core |
||||
layout(location = 0) in vec3 vertPosition; |
||||
layout(location = 1) in vec3 vertNormal; |
||||
layout(location = 2) in vec2 vertUV; |
||||
|
||||
uniform mat4 cameraMatrix; |
||||
uniform mat4 modelMatrix; |
||||
|
||||
out vec4 fragColor; |
||||
out vec2 fragUV; |
||||
|
||||
void main() |
||||
{ |
||||
gl_Position = cameraMatrix * modelMatrix * vec4(vertPosition, 1.0); |
||||
// Apply a pseudo-lighting effect based on the object's normals and rotation. |
||||
vec3 normal = mat3(modelMatrix) * vertNormal; |
||||
float l = 0.5 + (normal.y + 1) / 4.0 - (normal.z + 1) / 8.0; |
||||
fragColor = vec4(l, l, l, 1.0); |
||||
fragUV = vertUV; |
||||
} |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
@ -0,0 +1 @@ |
||||
Subproject commit 1e36559cffa5ab2fb755feef563c4294a6f32b0c |
@ -0,0 +1,61 @@ |
||||
using System; |
||||
using System.Collections.Immutable; |
||||
using Silk.NET.Maths; |
||||
|
||||
namespace gaemstone.Bloxel; |
||||
|
||||
public enum BlockFacing |
||||
{ |
||||
East, // +X |
||||
West, // -X |
||||
Up, // +Y |
||||
Down, // -Y |
||||
South, // +Z |
||||
North, // -Z |
||||
} |
||||
|
||||
public static class BlockFacings |
||||
{ |
||||
public static readonly ImmutableHashSet<BlockFacing> Horizontals |
||||
= ImmutableHashSet.Create(BlockFacing.East , BlockFacing.West , |
||||
BlockFacing.South, BlockFacing.North); |
||||
|
||||
public static readonly ImmutableHashSet<BlockFacing> Verticals |
||||
= ImmutableHashSet.Create(BlockFacing.Up, BlockFacing.Down); |
||||
|
||||
public static readonly ImmutableHashSet<BlockFacing> All |
||||
= Horizontals.Union(Verticals); |
||||
} |
||||
|
||||
public static class BlockFacingExtensions |
||||
{ |
||||
public static void Deconstruct(this BlockFacing self, out int x, out int y, out int z) |
||||
=> (x, y, z) = self switch { |
||||
BlockFacing.East => (+1, 0, 0), |
||||
BlockFacing.West => (-1, 0, 0), |
||||
BlockFacing.Up => ( 0, +1, 0), |
||||
BlockFacing.Down => ( 0, -1, 0), |
||||
BlockFacing.South => ( 0, 0, +1), |
||||
BlockFacing.North => ( 0, 0, -1), |
||||
_ => throw new ArgumentException( |
||||
$"'{self}' is not a valid BlockFacing", nameof(self)) |
||||
}; |
||||
|
||||
public static bool IsValid(this BlockFacing self) |
||||
=> (self >= BlockFacing.East) && (self <= BlockFacing.North); |
||||
|
||||
public static BlockFacing GetOpposite(this BlockFacing self) |
||||
=> (BlockFacing)((int)self ^ 0b1); |
||||
|
||||
public static Vector3D<float> ToVector3(this BlockFacing self) |
||||
=> self switch { |
||||
BlockFacing.East => Vector3D<float>.UnitX, |
||||
BlockFacing.West => -Vector3D<float>.UnitX, |
||||
BlockFacing.Up => Vector3D<float>.UnitY, |
||||
BlockFacing.Down => -Vector3D<float>.UnitY, |
||||
BlockFacing.South => Vector3D<float>.UnitZ, |
||||
BlockFacing.North => -Vector3D<float>.UnitZ, |
||||
_ => throw new ArgumentException( |
||||
$"'{self}' is not a valid BlockFacing", nameof(self)) |
||||
}; |
||||
} |
@ -0,0 +1,75 @@ |
||||
using System; |
||||
using Silk.NET.Maths; |
||||
|
||||
namespace gaemstone.Bloxel; |
||||
|
||||
public readonly struct BlockPos |
||||
: IEquatable<BlockPos> |
||||
{ |
||||
public static readonly BlockPos Origin = default; |
||||
|
||||
public int X { get; } |
||||
public int Y { get; } |
||||
public int Z { get; } |
||||
|
||||
public BlockPos(int x, int y, int z) => (X, Y, Z) = (x, y, z); |
||||
public void Deconstruct(out int x, out int y, out int z) => (x, y, z) = (X, Y, Z); |
||||
|
||||
public Vector3D<float> GetOrigin() => new(X, Y, Z); |
||||
public Vector3D<float> GetCenter() => new(X + 0.5F, Y + 0.5F, Z + 0.5F); |
||||
|
||||
|
||||
public BlockPos Add(int x, int y, int z) => new(X + x, Y + y, Z + z); |
||||
public BlockPos Add(in BlockPos other) => new(X + other.X, Y + other.Y, Z + other.Z); |
||||
|
||||
public BlockPos Add(BlockFacing facing) |
||||
{ var (x, y, z) = facing; return Add(x, y, z); } |
||||
public BlockPos Add(BlockFacing facing, int factor) |
||||
{ var (x, y, z) = facing; return Add(x * factor, y * factor, z * factor); } |
||||
|
||||
public BlockPos Add(Neighbor neighbor) |
||||
{ var (x, y, z) = neighbor; return Add(x, y, z); } |
||||
public BlockPos Add(Neighbor neighor, int factor) |
||||
{ var (x, y, z) = neighor; return Add(x * factor, y * factor, z * factor); } |
||||
|
||||
|
||||
public BlockPos Subtract(int x, int y, int z) => new(X - x, Y - y, Z - z); |
||||
public BlockPos Subtract(in BlockPos other) => new(X - other.X, Y - other.Y, Z - other.Z); |
||||
|
||||
public BlockPos Subtract(BlockFacing facing) |
||||
{ var (x, y, z) = facing; return Subtract(x, y, z); } |
||||
public BlockPos Subtract(BlockFacing facing, int factor) |
||||
{ var (x, y, z) = facing; return Subtract(x * factor, y * factor, z * factor); } |
||||
|
||||
public BlockPos Subtract(Neighbor neighbor) |
||||
{ var (x, y, z) = neighbor; return Subtract(x, y, z); } |
||||
public BlockPos Subtract(Neighbor neighor, int factor) |
||||
{ var (x, y, z) = neighor; return Subtract(x * factor, y * factor, z * factor); } |
||||
|
||||
|
||||
public bool Equals(BlockPos other) |
||||
=> (X == other.X) && (Y == other.Y) && (Z == other.Z); |
||||
public override bool Equals(object? obj) |
||||
=> (obj is BlockPos pos) && Equals(pos); |
||||
|
||||
public override int GetHashCode() => HashCode.Combine(X, Y, Z); |
||||
public override string ToString() => $"BlockPos({X}:{Y}:{Z})"; |
||||
public string ToShortString() => $"{X}:{Y}:{Z}"; |
||||
|
||||
|
||||
public static BlockPos operator +(BlockPos left, BlockPos right) => left.Add(right); |
||||
public static BlockPos operator -(BlockPos left, BlockPos right) => left.Subtract(right); |
||||
public static BlockPos operator +(BlockPos left, BlockFacing right) => left.Add(right); |
||||
public static BlockPos operator -(BlockPos left, BlockFacing right) => left.Subtract(right); |
||||
public static BlockPos operator +(BlockPos left, Neighbor right) => left.Add(right); |
||||
public static BlockPos operator -(BlockPos left, Neighbor right) => left.Subtract(right); |
||||
|
||||
public static bool operator ==(BlockPos left, BlockPos right) => left.Equals(right); |
||||
public static bool operator !=(BlockPos left, BlockPos right) => !left.Equals(right); |
||||
} |
||||
|
||||
public static class BlockPosExtensions |
||||
{ |
||||
public static BlockPos ToBlockPos(this Vector3D<float> self) |
||||
=> new((int)MathF.Floor(self.X), (int)MathF.Floor(self.Y), (int)MathF.Floor(self.Z)); |
||||
} |
@ -0,0 +1,17 @@ |
||||
using gaemstone.ECS; |
||||
|
||||
namespace gaemstone.Bloxel; |
||||
|
||||
[Component] |
||||
public readonly struct Chunk |
||||
{ |
||||
// <summary> Length of the egde of a world chunk. </summary> |
||||
public const int LENGTH = 16; |
||||
// <summary> Amount of bit shifting to go from a BlockPos to a ChunkPos. </summary> |
||||
public const int BIT_SHIFT = 4; |
||||
// <summary> Amount of bit masking to go from a BlockPos to a chunk-relative BlockPos. </summary> |
||||
public const int BIT_MASK = 0b1111; |
||||
|
||||
public ChunkPos Position { get; } |
||||
public Chunk(ChunkPos pos) => Position = pos; |
||||
} |
@ -0,0 +1,188 @@ |
||||
using System; |
||||
using System.Collections; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using gaemstone.ECS; |
||||
|
||||
namespace gaemstone.Bloxel; |
||||
|
||||
// Based on "Palette-based compression for chunked discrete voxel data" by /u/Longor1996 |
||||
// https://www.reddit.com/r/VoxelGameDev/comments/9yu8qy/palettebased_compression_for_chunked_discrete/ |
||||
[Component] |
||||
public class ChunkPaletteStorage<T> |
||||
{ |
||||
const int Size = 16 * 16 * 16; |
||||
static readonly EqualityComparer<T> COMPARER |
||||
= EqualityComparer<T>.Default; |
||||
|
||||
BitArray? _data; |
||||
PaletteEntry[]? _palette; |
||||
int _usedPalettes; |
||||
int _indicesLength; |
||||
|
||||
|
||||
public T Default { get; } |
||||
|
||||
public T this[int x, int y, int z] |
||||
{ |
||||
get => Get(x, y, z); |
||||
set => Set(x, y, z, value); |
||||
} |
||||
|
||||
public IEnumerable<T> Blocks |
||||
=> _palette?.Where(entry => !COMPARER.Equals(entry.Value, default!)) |
||||
.Select(entry => entry.Value!) |
||||
?? Enumerable.Empty<T>(); |
||||
|
||||
|
||||
public ChunkPaletteStorage(T @default) |
||||
=> Default = @default; |
||||
|
||||
|
||||
T Get(int x, int y, int z) |
||||
{ |
||||
if (_palette == null) return Default; |
||||
var entry = _palette[GetPaletteIndex(x, y, z)]; |
||||
return !COMPARER.Equals(entry.Value, default!) ? entry.Value : Default; |
||||
} |
||||
|
||||
void Set(int x, int y, int z, T value) |
||||
{ |
||||
if (_palette == null) |
||||
{ |
||||
if (COMPARER.Equals(value, Default)) return; |
||||
} |
||||
else |
||||
{ |
||||
var index = GetIndex(x, y, z); |
||||
ref var current = ref _palette[GetPaletteIndex(index)]; |
||||
if (COMPARER.Equals(value, current.Value)) return; |
||||
|
||||
if (--current.RefCount == 0) |
||||
_usedPalettes--; |
||||
|
||||
var replace = Array.FindIndex(_palette, entry => COMPARER.Equals(value, entry.Value)); |
||||
if (replace != -1) |
||||
{ |
||||
SetPaletteIndex(index, replace); |
||||
_palette[replace].RefCount += 1; |
||||
return; |
||||
} |
||||
|
||||
if (current.RefCount == 0) |
||||
{ |
||||
current.Value = value; |
||||
current.RefCount = 1; |
||||
_usedPalettes++; |
||||
return; |
||||
} |
||||
} |
||||
|
||||
var newPaletteIndex = NewPaletteEntry(); |
||||
_palette![newPaletteIndex] = new PaletteEntry { Value = value, RefCount = 1 }; |
||||
SetPaletteIndex(x, y, z, newPaletteIndex); |
||||
_usedPalettes++; |
||||
} |
||||
|
||||
int NewPaletteEntry() |
||||
{ |
||||
if (_palette != null) |
||||
{ |
||||
int firstFree = Array.FindIndex(_palette, entry => |
||||
entry.Value == null || entry.RefCount == 0); |
||||
if (firstFree != -1) return firstFree; |
||||
} |
||||
|
||||
GrowPalette(); |
||||
return NewPaletteEntry(); |
||||
} |
||||
|
||||
void GrowPalette() |
||||
{ |
||||
if (_palette == null) |
||||
{ |
||||
_data = new(Size); |
||||
_palette = new PaletteEntry[2]; |
||||
_usedPalettes = 1; |
||||
_indicesLength = 1; |
||||
_palette[0] = new PaletteEntry { Value = Default, RefCount = Size }; |
||||
return; |
||||
} |
||||
|
||||
_indicesLength <<= 1; |
||||
|
||||
var oldIndicesLength = _indicesLength >> 1; |
||||
var newData = new BitArray(Size * _indicesLength); |
||||
for (var i = 0; i < Size; i++) |
||||
for (var j = 0; j < oldIndicesLength; j++) |
||||
newData.Set(i * _indicesLength + j, _data!.Get(i * oldIndicesLength + j)); |
||||
_data = newData; |
||||
|
||||
Array.Resize(ref _palette, 1 << _indicesLength); |
||||
} |
||||
|
||||
// public void FitPalette() { |
||||
// if (_usedPalettes > Mathf.NearestPo2(_usedPalettes) / 2) return; |
||||
|
||||
// // decode all indices |
||||
// int[] indices = new int[size]; |
||||
// for(int i = 0; i < indices.length; i++) { |
||||
// indices[i] = data.get(i * indicesLength, indicesLength); |
||||
// } |
||||
|
||||
// // Create new palette, halfing it in size |
||||
// indicesLength = indicesLength >> 1; |
||||
// PaletteEntry[] newPalette = new PaletteEntry[2 pow indicesLength]; |
||||
|
||||
// // We gotta compress the palette entries! |
||||
// int paletteCounter = 0; |
||||
// for(int pi = 0; pi < palette.length; pi++, paletteCounter++) { |
||||
// PaletteEntry entry = newPalette[paletteCounter] = palette[pi]; |
||||
|
||||
// // Re-encode the indices (find and replace; with limit) |
||||
// for(int di = 0, fc = 0; di < indices.length && fc < entry.refcount; di++) { |
||||
// if(pi == indices[di]) { |
||||
// indices[di] = paletteCounter; |
||||
// fc += 1; |
||||
// } |
||||
// } |
||||
// } |
||||
|
||||
// // Allocate new BitBuffer |
||||
// data = new BitBuffer(size * indicesLength); // the length is in bits, not bytes! |
||||
|
||||
// // Encode the indices |
||||
// for(int i = 0; i < indices.length; i++) { |
||||
// data.set(i * indicesLength, indicesLength, indices[i]); |
||||
// } |
||||
// } |
||||
|
||||
|
||||
int GetPaletteIndex(int x, int y, int z) |
||||
=> GetPaletteIndex(GetIndex(x, y, z)); |
||||
int GetPaletteIndex(int index) |
||||
{ |
||||
var paletteIndex = 0; |
||||
for (var i = 0; i < _indicesLength; i++) |
||||
paletteIndex |= (_data!.Get(index + i) ? 1 : 0) << i; |
||||
return paletteIndex; |
||||
} |
||||
|
||||
void SetPaletteIndex(int x, int y, int z, int paletteIndex) |
||||
=> SetPaletteIndex(GetIndex(x, y, z), paletteIndex); |
||||
void SetPaletteIndex(int index, int paletteIndex) |
||||
{ |
||||
for (var i = 0; i < _indicesLength; i++) |
||||
_data!.Set(index + i, (paletteIndex >> i & 0b1) == 0b1); |
||||
} |
||||
|
||||
int GetIndex(int x, int y, int z) |
||||
=> (x | y << 4 | z << 8) * _indicesLength; |
||||
|
||||
|
||||
struct PaletteEntry |
||||
{ |
||||
public T Value { get; set; } |
||||
public int RefCount { get; set; } |
||||
} |
||||
} |
@ -0,0 +1,81 @@ |
||||
using System; |
||||
using Silk.NET.Maths; |
||||
|
||||
namespace gaemstone.Bloxel; |
||||
|
||||
public readonly struct ChunkPos |
||||
: IEquatable<ChunkPos> |
||||
{ |
||||
public static readonly ChunkPos ORIGIN = new(0, 0, 0); |
||||
|
||||
public int X { get; } |
||||
public int Y { get; } |
||||
public int Z { get; } |
||||
|
||||
public ChunkPos(int x, int y, int z) => (X, Y, Z) = (x, y, z); |
||||
public void Deconstruct(out int x, out int y, out int z) => (x, y, z) = (X, Y, Z); |
||||
|
||||
public Vector3D<float> GetOrigin() => new( |
||||
X << Chunk.BIT_SHIFT, Y << Chunk.BIT_SHIFT, Z << Chunk.BIT_SHIFT); |
||||
public Vector3D<float> GetCenter() => new( |
||||
(X << Chunk.BIT_SHIFT) + Chunk.LENGTH / 2, |
||||
(Y << Chunk.BIT_SHIFT) + Chunk.LENGTH / 2, |
||||
(Z << Chunk.BIT_SHIFT) + Chunk.LENGTH / 2); |
||||
|
||||
|
||||
public ChunkPos Add(int x, int y, int z) |
||||
=> new(X + x, Y + y, Z + z); |
||||
public ChunkPos Add(in ChunkPos other) |
||||
=> new(X + other.X, Y + other.Y, Z + other.Z); |
||||
public ChunkPos Add(BlockFacing facing) |
||||
{ var (x, y, z) = facing; return Add(x, y, z); } |
||||
public ChunkPos Add(Neighbor neighbor) |
||||
{ var (x, y, z) = neighbor; return Add(x, y, z); } |
||||
|
||||
public ChunkPos Subtract(int x, int y, int z) |
||||
=> new(X - x, Y - y, Z - z); |
||||
public ChunkPos Subtract(in ChunkPos other) |
||||
=> new(X - other.X, Y - other.Y, Z - other.Z); |
||||
public ChunkPos Subtract(BlockFacing facing) |
||||
{ var (x, y, z) = facing; return Subtract(x, y, z); } |
||||
public ChunkPos Subtract(Neighbor neighbor) |
||||
{ var (x, y, z) = neighbor; return Subtract(x, y, z); } |
||||
|
||||
|
||||
public bool Equals(ChunkPos other) |
||||
=> (X == other.X) && (Y == other.Y) && (Z == other.Z); |
||||
public override bool Equals(object? obj) |
||||
=> (obj is ChunkPos pos) && Equals(pos); |
||||
|
||||
public override int GetHashCode() => HashCode.Combine(X, Y, Z); |
||||
public override string ToString() => $"ChunkPos ({X}:{Y}:{Z})"; |
||||
public string ToShortString() => $"{X}:{Y}:{Z}"; |
||||
|
||||
|
||||
public static ChunkPos operator +(ChunkPos left, ChunkPos right) => left.Add(right); |
||||
public static ChunkPos operator -(ChunkPos left, ChunkPos right) => left.Subtract(right); |
||||
public static ChunkPos operator +(ChunkPos left, BlockFacing right) => left.Add(right); |
||||
public static ChunkPos operator -(ChunkPos left, BlockFacing right) => left.Subtract(right); |
||||
public static ChunkPos operator +(ChunkPos left, Neighbor right) => left.Add(right); |
||||
public static ChunkPos operator -(ChunkPos left, Neighbor right) => left.Subtract(right); |
||||
|
||||
public static bool operator ==(ChunkPos left, ChunkPos right) => left.Equals(right); |
||||
public static bool operator !=(ChunkPos left, ChunkPos right) => !left.Equals(right); |
||||
} |
||||
|
||||
public static class ChunkPosExtensions |
||||
{ |
||||
public static ChunkPos ToChunkPos(this Vector3D<float> pos) => new( |
||||
(int)MathF.Floor(pos.X) >> Chunk.BIT_SHIFT, |
||||
(int)MathF.Floor(pos.Y) >> Chunk.BIT_SHIFT, |
||||
(int)MathF.Floor(pos.Z) >> Chunk.BIT_SHIFT); |
||||
|
||||
public static ChunkPos ToChunkPos(this BlockPos self) => new( |
||||
self.X >> Chunk.BIT_SHIFT, self.Y >> Chunk.BIT_SHIFT, self.Z >> Chunk.BIT_SHIFT); |
||||
public static BlockPos ToChunkRelative(this BlockPos self) => new( |
||||
self.X & Chunk.BIT_MASK, self.Y & Chunk.BIT_MASK, self.Z & Chunk.BIT_MASK); |
||||
public static BlockPos ToChunkRelative(this BlockPos self, ChunkPos chunk) => new( |
||||
self.X - (chunk.X << Chunk.BIT_SHIFT), |
||||
self.Y - (chunk.Y << Chunk.BIT_SHIFT), |
||||
self.Z - (chunk.Z << Chunk.BIT_SHIFT)); |
||||
} |
@ -0,0 +1,125 @@ |
||||
using System; |
||||
using System.Runtime.InteropServices; |
||||
using gaemstone.Client; |
||||
using gaemstone.ECS; |
||||
using Silk.NET.Maths; |
||||
using static flecs_hub.flecs; |
||||
using static gaemstone.Bloxel.WorldGen.BasicWorldGenerator; |
||||
|
||||
namespace gaemstone.Bloxel.Client; |
||||
|
||||
[Module] |
||||
public class ChunkMeshGenerator |
||||
{ |
||||
private const int StartingCapacity = 1024; |
||||
|
||||
private static readonly Vector3D<float>[][] OffsetPerFacing = { |
||||
new Vector3D<float>[]{ new(1,1,1), new(1,0,1), new(1,0,0), new(1,1,0) }, // East (+X) |
||||
new Vector3D<float>[]{ new(0,1,0), new(0,0,0), new(0,0,1), new(0,1,1) }, // West (-X) |
||||
new Vector3D<float>[]{ new(1,1,0), new(0,1,0), new(0,1,1), new(1,1,1) }, // Up (+Y) |
||||
new Vector3D<float>[]{ new(1,0,1), new(0,0,1), new(0,0,0), new(1,0,0) }, // Down (-Y) |
||||
new Vector3D<float>[]{ new(0,1,1), new(0,0,1), new(1,0,1), new(1,1,1) }, // South (+Z) |
||||
new Vector3D<float>[]{ new(1,1,0), new(1,0,0), new(0,0,0), new(0,1,0) } // North (-Z) |
||||
}; |
||||
|
||||
private static readonly int[] TriangleIndices |
||||
= { 0, 1, 3, 1, 2, 3 }; |
||||
|
||||
|
||||
private ushort[] _indices = new ushort[StartingCapacity]; |
||||
private Vector3D<float>[] _vertices = new Vector3D<float>[StartingCapacity]; |
||||
private Vector3D<float>[] _normals = new Vector3D<float>[StartingCapacity]; |
||||
private Vector2D<float>[] _uvs = new Vector2D<float>[StartingCapacity]; |
||||
|
||||
[System] |
||||
public void GenerateChunkMeshes(Universe universe, Entity entity, |
||||
in Chunk chunk, ChunkPaletteStorage<ecs_entity_t> storage, |
||||
HasBasicWorldGeneration _1, [Not] Mesh _2) |
||||
{ |
||||
var mesh = Generate(universe, chunk.Position, storage); |
||||
if (mesh is Mesh m) entity.Set(m); |
||||
else entity.Delete(); |
||||
} |
||||
|
||||
public Mesh? Generate(Universe universe, ChunkPos chunkPos, |
||||
ChunkPaletteStorage<ecs_entity_t> centerStorage) |
||||
{ |
||||
// TODO: We'll need a way to get neighbors again. |
||||
// var storages = new ChunkPaletteStorage<ecs_entity_t>[3, 3, 3]; |
||||
// foreach (var (x, y, z) in Neighbors.ALL.Prepend(Neighbor.None)) |
||||
// if (_chunkStore.TryGetEntityID(chunkPos.Add(x, y, z), out var neighborID)) |
||||
// if (_storageStore.TryGet(neighborID, out var storage)) |
||||
// storages[x+1, y+1, z+1] = storage; |
||||
// var centerStorage = storages[1, 1, 1]; |
||||
|
||||
var storages = new ChunkPaletteStorage<ecs_entity_t>[3, 3, 3]; |
||||
storages[1, 1, 1] = centerStorage; |
||||
|
||||
var indexCount = 0; |
||||
var vertexCount = 0; |
||||
for (var x = 0; x < 16; x++) |
||||
for (var y = 0; y < 16; y++) |
||||
for (var z = 0; z < 16; z++) { |
||||
var block = new Entity(universe, centerStorage[x, y, z]); |
||||
if (block.IsNone) continue; |
||||
|
||||
var blockVertex = new Vector3D<float>(x, y, z); |
||||
var textureCell = block.Get<TextureCoords4>(); |
||||
|
||||
foreach (var facing in BlockFacings.All) { |
||||
if (!IsNeighborEmpty(storages, x, y, z, facing)) continue; |
||||
|
||||
if (_indices.Length <= indexCount + 6) |
||||
Array.Resize(ref _indices, _indices.Length << 1); |
||||
if (_vertices.Length <= vertexCount + 4) { |
||||
Array.Resize(ref _vertices, _vertices.Length << 1); |
||||
Array.Resize(ref _normals , _vertices.Length << 1); |
||||
Array.Resize(ref _uvs , _vertices.Length << 1); |
||||
} |
||||
|
||||
for (var i = 0; i < TriangleIndices.Length; i++) |
||||
_indices[indexCount++] = (ushort)(vertexCount + TriangleIndices[i]); |
||||
|
||||
var normal = facing.ToVector3(); |
||||
for (var i = 0; i < 4; i++) { |
||||
var offset = OffsetPerFacing[(int)facing][i]; |
||||
_vertices[vertexCount] = blockVertex + offset; |
||||
_normals[vertexCount] = normal; |
||||
_uvs[vertexCount] = i switch { |
||||
0 => textureCell.TopLeft, |
||||
1 => textureCell.BottomLeft, |
||||
2 => textureCell.BottomRight, |
||||
3 => textureCell.TopRight, |
||||
_ => throw new InvalidOperationException() |
||||
}; |
||||
vertexCount++; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return (indexCount > 0) |
||||
? MeshManager.Create(universe, |
||||
_indices.AsSpan(0, indexCount), _vertices.AsSpan(0, vertexCount), |
||||
_normals.AsSpan(0, vertexCount), _uvs.AsSpan(0, vertexCount)) |
||||
: null; |
||||
} |
||||
|
||||
static bool IsNeighborEmpty( |
||||
ChunkPaletteStorage<ecs_entity_t>[,,] storages, |
||||
int x, int y, int z, BlockFacing facing) |
||||
{ |
||||
var cx = 1; var cy = 1; var cz = 1; |
||||
switch (facing) { |
||||
case BlockFacing.East : x += 1; if (x >= 16) cx += 1; break; |
||||
case BlockFacing.West : x -= 1; if (x < 0) cx -= 1; break; |
||||
case BlockFacing.Up : y += 1; if (y >= 16) cy += 1; break; |
||||
case BlockFacing.Down : y -= 1; if (y < 0) cy -= 1; break; |
||||
case BlockFacing.South : z += 1; if (z >= 16) cz += 1; break; |
||||
case BlockFacing.North : z -= 1; if (z < 0) cz -= 1; break; |
||||
} |
||||
var neighborChunk = storages[cx, cy, cz]; |
||||
if (neighborChunk == null) return true; |
||||
var neighborBlock = neighborChunk[x & 0b1111, y & 0b1111, z & 0b1111]; |
||||
return neighborBlock.Data.Data == 0; |
||||
} |
||||
} |
@ -0,0 +1,205 @@ |
||||
using System; |
||||
using System.Collections.Immutable; |
||||
using System.Text; |
||||
using Silk.NET.Maths; |
||||
|
||||
namespace gaemstone.Bloxel; |
||||
|
||||
[Flags] |
||||
public enum Neighbor : byte |
||||
{ |
||||
None = 0, |
||||
|
||||
// FACINGS |
||||
East = 0b000011, // +X |
||||
West = 0b000010, // -X |
||||
Up = 0b001100, // +Y |
||||
Down = 0b001000, // -Y |
||||
South = 0b110000, // +Z |
||||
North = 0b100000, // -Z |
||||
|
||||
// CARDINALS |
||||
SouthEast = South | East, // +X +Z |
||||
SouthWest = South | West, // -X +Z |
||||
NorthEast = North | East, // +X -Z |
||||
NorthWest = North | West, // -X -Z |
||||
|
||||
// ALL_AXIS_PLANES |
||||
UpEast = Up | East , // +X +Y |
||||
UpWest = Up | West , // -X +Y |
||||
UpSouth = Up | South, // +Z +Y |
||||
UpNorth = Up | North, // -Z +Y |
||||
|
||||
DownEast = Down | East , // +X -Y |
||||
DownWest = Down | West , // -X -Y |
||||
DownSouth = Down | South, // +Z -Y |
||||
DownNorth = Down | North, // -Z -Y |
||||
|
||||
// ALL |
||||
UpSouthEast = Up | South | East, // +X +Y +Z |
||||
UpSouthWest = Up | South | West, // -X +Y +Z |
||||
UpNorthEast = Up | North | East, // +X +Y -Z |
||||
UpNorthWest = Up | North | West, // -X +Y -Z |
||||
|
||||
DownSouthEast = Down | South | East, // +X -Y +Z |
||||
DownSouthWest = Down | South | West, // -X -Y +Z |
||||
DownNorthEast = Down | North | East, // +X -Y -Z |
||||
DownNorthWest = Down | North | West, // -X -Y -Z |
||||
} |
||||
|
||||
public static class Neighbors |
||||
{ |
||||
public static readonly ImmutableHashSet<Neighbor> Horizontals |
||||
= ImmutableHashSet.Create(Neighbor.East , Neighbor.West , |
||||
Neighbor.South, Neighbor.North); |
||||
|
||||
public static readonly ImmutableHashSet<Neighbor> Verticals |
||||
= ImmutableHashSet.Create(Neighbor.Up, Neighbor.Down); |
||||
|
||||
public static readonly ImmutableHashSet<Neighbor> Facings |
||||
= Horizontals.Union(Verticals); |
||||
|
||||
public static readonly ImmutableHashSet<Neighbor> Cardinals |
||||
= Horizontals.Union(new[] { |
||||
Neighbor.SouthEast, Neighbor.SouthWest, |
||||
Neighbor.NorthEast, Neighbor.NorthWest }); |
||||
|
||||
public static readonly ImmutableHashSet<Neighbor> AllAxisPlanes |
||||
= Facings.Union(new[] { |
||||
Neighbor.SouthEast, Neighbor.SouthWest, |
||||
Neighbor.NorthEast, Neighbor.NorthWest, |
||||
Neighbor.UpEast , Neighbor.UpWest , |
||||
Neighbor.UpSouth , Neighbor.UpNorth , |
||||
Neighbor.DownEast , Neighbor.DownWest , |
||||
Neighbor.DownSouth, Neighbor.DownNorth }); |
||||
|
||||
public static readonly ImmutableHashSet<Neighbor> All |
||||
= AllAxisPlanes.Union(new[] { |
||||
Neighbor.UpSouthEast, Neighbor.UpSouthWest, |
||||
Neighbor.UpNorthEast, Neighbor.UpNorthWest, |
||||
Neighbor.DownSouthEast, Neighbor.DownSouthWest, |
||||
Neighbor.DownNorthEast, Neighbor.DownNorthWest }); |
||||
} |
||||
|
||||
public static class NeighborExtensions |
||||
{ |
||||
const int SetBitX = 0b000010, ValueBitX = 0b000001; |
||||
const int SetBitY = 0b001000, ValueBitY = 0b000100; |
||||
const int SetBitZ = 0b100000, ValueBitZ = 0b010000; |
||||
public static void Deconstruct(this Neighbor self, out int x, out int y, out int z) |
||||
{ |
||||
x = (((int)self & SetBitX) != 0) ? ((((int)self & ValueBitX) != 0) ? 1 : -1) : 0; |
||||
y = (((int)self & SetBitY) != 0) ? ((((int)self & ValueBitY) != 0) ? 1 : -1) : 0; |
||||
z = (((int)self & SetBitZ) != 0) ? ((((int)self & ValueBitZ) != 0) ? 1 : -1) : 0; |
||||
} |
||||
|
||||
|
||||
// public static Neighbor ToNeighbor(this Axis self, int v) |
||||
// { |
||||
// if ((v < -1) || (v > 1)) throw new ArgumentOutOfRangeException( |
||||
// nameof(v), v, $"{nameof(v)} (={v}) must be within (-1, 1)"); |
||||
// return self switch { |
||||
// Axis.X => (v > 0) ? Neighbor.East : Neighbor.West , |
||||
// Axis.Y => (v > 0) ? Neighbor.Up : Neighbor.Down , |
||||
// Axis.Z => (v > 0) ? Neighbor.South : Neighbor.North, |
||||
// _ => Neighbor.None |
||||
// }; |
||||
// } |
||||
|
||||
// public static Axis GetAxis(this Neighbor self) |
||||
// => self switch { |
||||
// Neighbor.East => Axis.X, |
||||
// Neighbor.West => Axis.X, |
||||
// Neighbor.Up => Axis.Y, |
||||
// Neighbor.Down => Axis.Y, |
||||
// Neighbor.South => Axis.Z, |
||||
// Neighbor.North => Axis.Z, |
||||
// _ => throw new ArgumentException(nameof(self), $"{self} is not one of FACINGS") |
||||
// }; |
||||
|
||||
|
||||
public static Neighbor ToNeighbor(this BlockFacing self) |
||||
=> self switch { |
||||
BlockFacing.East => Neighbor.East , |
||||
BlockFacing.West => Neighbor.West , |
||||
BlockFacing.Up => Neighbor.Up , |
||||
BlockFacing.Down => Neighbor.Down , |
||||
BlockFacing.South => Neighbor.South, |
||||
BlockFacing.North => Neighbor.North, |
||||
_ => throw new ArgumentException( |
||||
$"'{self}' is not a valid BlockFacing", nameof(self)) |
||||
}; |
||||
|
||||
public static BlockFacing ToBlockFacing(this Neighbor self) |
||||
=> self switch { |
||||
Neighbor.East => BlockFacing.East , |
||||
Neighbor.West => BlockFacing.West , |
||||
Neighbor.Up => BlockFacing.Up , |
||||
Neighbor.Down => BlockFacing.Down , |
||||
Neighbor.South => BlockFacing.South, |
||||
Neighbor.North => BlockFacing.North, |
||||
_ => throw new ArgumentException( |
||||
$"'{self}' can't be converted to a valid BlockFacing", nameof(self)) |
||||
}; |
||||
|
||||
|
||||
public static Neighbor ToNeighbor(this (int x, int y, int z) p) |
||||
{ |
||||
var neighbor = Neighbor.None; |
||||
if (p.x != 0) { |
||||
if (p.x == 1) neighbor |= Neighbor.East; |
||||
else if (p.x == -1) neighbor |= Neighbor.West; |
||||
else throw new ArgumentOutOfRangeException( |
||||
nameof(p), p.x, $"{nameof(p)}.x (={p.x}) must be within (-1, 1)"); |
||||
} |
||||
if (p.y != 0) { |
||||
if (p.y == 1) neighbor |= Neighbor.Up; |
||||
else if (p.y == -1) neighbor |= Neighbor.Down; |
||||
else throw new ArgumentOutOfRangeException( |
||||
nameof(p), p.y, $"{nameof(p)}.y (={p.y}) must be within (-1, 1)"); |
||||
} |
||||
if (p.z != 0) { |
||||
if (p.z == 1) neighbor |= Neighbor.South; |
||||
else if (p.z == -1) neighbor |= Neighbor.North; |
||||
else throw new ArgumentOutOfRangeException( |
||||
nameof(p), p.z, $"{nameof(p)}.z (={p.z}) must be within (-1, 1)"); |
||||
} |
||||
return neighbor; |
||||
} |
||||
|
||||
public static Neighbor GetOpposite(this Neighbor self) |
||||
{ var (x, y, z) = self; return (-x, -y, -z).ToNeighbor(); } |
||||
|
||||
|
||||
public static BlockPos ToProperPos(this Neighbor self) |
||||
{ var (x, y, z) = self; return new(x, y, z); } |
||||
|
||||
public static Vector3D<float> ToVector3(this Neighbor self) |
||||
{ var (x, y, z) = self; return new(x, y, z); } |
||||
|
||||
|
||||
public static bool IsNone(this Neighbor self) |
||||
=> (self == Neighbor.None); |
||||
|
||||
public static bool IsHorizontal(this Neighbor self) |
||||
=> Neighbors.Horizontals.Contains(self); |
||||
public static bool IsVertical(this Neighbor self) |
||||
=> Neighbors.Verticals.Contains(self); |
||||
public static bool IsCardinal(this Neighbor self) |
||||
=> Neighbors.Cardinals.Contains(self); |
||||
public static bool IsFacing(this Neighbor self) |
||||
=> Neighbors.Facings.Contains(self); |
||||
public static bool IsValid(this Neighbor self) |
||||
=> Neighbors.All.Contains(self); |
||||
|
||||
|
||||
public static string ToShortString(this Neighbor self) |
||||
{ |
||||
if (!self.IsValid()) return "-"; |
||||
var sb = new StringBuilder(3); |
||||
foreach (var chr in self.ToString()) |
||||
if ((chr >= 'A') && (chr <= 'Z')) // ASCII IsUpper |
||||
sb.Append(chr + 0x20); // ASCII ToLower |
||||
return sb.ToString(); |
||||
} |
||||
} |
@ -0,0 +1,135 @@ |
||||
using System; |
||||
using System.Collections; |
||||
using System.Collections.Generic; |
||||
|
||||
namespace gaemstone.Bloxel.Utility; |
||||
|
||||
public class ChunkedOctree<T> |
||||
where T : struct
|
||||
{ |
||||
public delegate void UpdateAction(int level, ReadOnlySpan<T> children, ref T parent); |
||||
public delegate float? WeightFunc(int level, ZOrder pos, T value); |
||||
|
||||
private static readonly int[] START_INDEX_LOOKUP = { |
||||
0, 1, 9, 73, 585, 4681, 37449, 299593, 2396745, 19173961, 153391689 }; |
||||
|
||||
|
||||
private readonly IEqualityComparer<T> _comparer = EqualityComparer<T>.Default; |
||||
private readonly Dictionary<ZOrder, T[]> _regions = new(); |
||||
|
||||
public int Depth { get; } |
||||
|
||||
public ChunkedOctree(int depth) |
||||
{ |
||||
if (depth < 1) throw new ArgumentOutOfRangeException(nameof(depth), |
||||
$"{nameof(depth)} must be larger than 0"); |
||||
if (depth >= START_INDEX_LOOKUP.Length) throw new ArgumentOutOfRangeException(nameof(depth), |
||||
$"{nameof(depth)} must be smaller than {START_INDEX_LOOKUP.Length}"); |
||||
Depth = depth; |
||||
} |
||||
|
||||
public T Get(ChunkPos pos) |
||||
=> Get(0, new(pos.X, pos.Y, pos.Z)); |
||||
public T Get(int level, ZOrder pos) |
||||
{ |
||||
var region = _regions.GetValueOrDefault(pos >> Depth - level); |
||||
if (region == null) return default; |
||||
var localPos = pos & ~(~0L << (Depth - level) * 3); |
||||
return region[GetIndex(level, localPos)]; |
||||
} |
||||
private int GetIndex(int level, ZOrder localPos) |
||||
=> START_INDEX_LOOKUP[Depth - level] + (int)localPos.Raw; |
||||
|
||||
public void Update(ChunkPos pos, UpdateAction update) |
||||
{ |
||||
var zPos = new ZOrder(pos.X, pos.Y, pos.Z); |
||||
var localPos = zPos & ~(~0L << Depth * 3); |
||||
var regionPos = zPos >> Depth; |
||||
|
||||
if (!_regions.TryGetValue(regionPos, out var region)) |
||||
_regions.Add(regionPos, region = new T[START_INDEX_LOOKUP[Depth + 1] + 1]); |
||||
|
||||
var children = default(ReadOnlySpan<T>); |
||||
for (var level = 0; level <= Depth; level++) |
||||
{ |
||||
var index = GetIndex(level, localPos); |
||||
|
||||
var previous = region[index]; |
||||
update(0, children, ref region[index]); |
||||
if (_comparer.Equals(region[index], previous)) return; |
||||
|
||||
if (level == Depth) return; |
||||
children = region.AsSpan(GetIndex(level, localPos & ~0b111L), 8); |
||||
localPos >>= 1; |
||||
} |
||||
} |
||||
|
||||
public IEnumerable<(ChunkPos ChunkPos, T Value, float Weight)> Find( |
||||
WeightFunc weight, params ChunkPos[] searchFrom) |
||||
{ |
||||
var enumerator = new Enumerator(this, weight); |
||||
foreach (var pos in searchFrom) enumerator.SearchFrom(new(pos.X, pos.Y, pos.Z)); |
||||
while (enumerator.MoveNext()) yield return enumerator.Current; |
||||
} |
||||
|
||||
public class Enumerator |
||||
: IEnumerator<(ChunkPos ChunkPos, T Value, float Weight)> |
||||
{ |
||||
private readonly ChunkedOctree<T> _octree; |
||||
private readonly WeightFunc _weight; |
||||
|
||||
private readonly HashSet<ZOrder> _checkedRegions = new(); |
||||
private readonly PriorityQueue<(int Level, ZOrder Pos, T Value), float> _processing = new(); |
||||
private (ChunkPos ChunkPos, T Value, float Weight)? _current; |
||||
|
||||
internal Enumerator(ChunkedOctree<T> octree, WeightFunc weight) |
||||
{ _octree = octree; _weight = weight; _current = null; } |
||||
|
||||
public (ChunkPos ChunkPos, T Value, float Weight) Current |
||||
=> _current ?? throw new InvalidOperationException(); |
||||
object IEnumerator.Current => Current; |
||||
|
||||
public bool MoveNext() |
||||
{ |
||||
while (_processing.TryDequeue(out var element, out var weight)) |
||||
{ |
||||
var (level, nodePos, value) = element; |
||||
if (level == 0) |
||||
{ |
||||
_current = (new(nodePos.X, nodePos.Y, nodePos.Z), value, weight); |
||||
return true; |
||||
} |
||||
else for (var i = 0b000; i <= 0b111; i++) |
||||
PushNode(level - 1, nodePos << 1 | ZOrder.FromRaw(i)); |
||||
} |
||||
_current = null; |
||||
return false; |
||||
} |
||||
|
||||
public void Reset() => throw new NotSupportedException(); |
||||
public void Dispose() { } |
||||
|
||||
|
||||
internal void SearchFrom(ZOrder nodePos) |
||||
{ |
||||
var regionPos = nodePos >> _octree.Depth; |
||||
for (var x = -1; x <= 1; x++) |
||||
for (var y = -1; y <= 1; y++) |
||||
for (var z = -1; z <= 1; z++) |
||||
SearchRegion(regionPos + new ZOrder(x, y, z)); |
||||
} |
||||
|
||||
private void SearchRegion(ZOrder regionPos) |
||||
{ |
||||
if (_checkedRegions.Add(regionPos)) |
||||
PushNode(_octree.Depth, regionPos); |
||||
} |
||||
|
||||
private void PushNode(int level, ZOrder nodePos) |
||||
{ |
||||
var value = _octree.Get(level, nodePos); |
||||
if (_weight(level, nodePos, value) is float weight) |
||||
_processing.Enqueue((level, nodePos, value), weight); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,154 @@ |
||||
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); |
||||