Initial commit

wip/source-generators
copygirl 1 year ago
commit fae12b7963
  1. 20
      .editorconfig
  2. 4
      .gitignore
  3. 6
      .gitmodules
  4. 16
      .vscode/launch.json
  5. 8
      .vscode/settings.json
  6. 40
      .vscode/tasks.json
  7. 48
      gaemstone.sln
  8. 1
      src/FastNoiseLite
  9. 23
      src/Immersion/Immersion.csproj
  10. 91
      src/Immersion/Program.cs
  11. 11
      src/Immersion/Resources/LICENSE_NOTICES.md
  12. 12
      src/Immersion/Resources/default.fs.glsl
  13. 20
      src/Immersion/Resources/default.vs.glsl
  14. BIN
      src/Immersion/Resources/heart.blend
  15. BIN
      src/Immersion/Resources/heart.glb
  16. BIN
      src/Immersion/Resources/sword.blend
  17. BIN
      src/Immersion/Resources/sword.glb
  18. BIN
      src/Immersion/Resources/terrain.png
  19. 1
      src/flecs-cs
  20. 61
      src/gaemstone.Bloxel/BlockFacing.cs
  21. 75
      src/gaemstone.Bloxel/BlockPos.cs
  22. 17
      src/gaemstone.Bloxel/Chunk.cs
  23. 188
      src/gaemstone.Bloxel/ChunkPaletteStorage.cs
  24. 81
      src/gaemstone.Bloxel/ChunkPos.cs
  25. 125
      src/gaemstone.Bloxel/Client/ChunkMeshGenerator.cs
  26. 205
      src/gaemstone.Bloxel/Neighbor.cs
  27. 135
      src/gaemstone.Bloxel/Utility/ChunkedOctree.cs
  28. 154
      src/gaemstone.Bloxel/Utility/ZOrder.cs
  29. 42
      src/gaemstone.Bloxel/WorldGen/BasicWorldGenerator.cs
  30. 57
      src/gaemstone.Bloxel/WorldGen/SurfaceGrassGenerator.cs.disabled
  31. 18
      src/gaemstone.Bloxel/gaemstone.Bloxel.csproj
  32. 56
      src/gaemstone.Client/Color.cs
  33. 62
      src/gaemstone.Client/GLExtensions.cs
  34. 14
      src/gaemstone.Client/Mesh.cs
  35. 104
      src/gaemstone.Client/MeshManager.cs
  36. 81
      src/gaemstone.Client/Modules/CameraModule.cs
  37. 81
      src/gaemstone.Client/Modules/Input.cs
  38. 109
      src/gaemstone.Client/Modules/Renderer.cs
  39. 31
      src/gaemstone.Client/Modules/Windowing.cs
  40. 30
      src/gaemstone.Client/Resources.cs
  41. 14
      src/gaemstone.Client/Texture.cs
  42. 36
      src/gaemstone.Client/TextureCoords4.cs
  43. 76
      src/gaemstone.Client/TextureManager.cs
  44. 21
      src/gaemstone.Client/gaemstone.Client.csproj
  45. 12
      src/gaemstone/ECS/Attributes.cs
  46. 193
      src/gaemstone/ECS/Entity.cs
  47. 20
      src/gaemstone/ECS/EntityDesc.cs.disabled
  48. 39
      src/gaemstone/ECS/Filter.cs
  49. 17
      src/gaemstone/ECS/FlecsException.cs
  50. 53
      src/gaemstone/ECS/Identifier.cs
  51. 93
      src/gaemstone/ECS/Iterator.cs
  52. 13
      src/gaemstone/ECS/Module.cs
  53. 18
      src/gaemstone/ECS/Observer.cs
  54. 33
      src/gaemstone/ECS/Query.cs
  55. 93
      src/gaemstone/ECS/System.cs
  56. 140
      src/gaemstone/ECS/Universe+Modules.cs
  57. 167
      src/gaemstone/ECS/Universe+Systems.cs
  58. 184
      src/gaemstone/ECS/Universe.cs
  59. 13
      src/gaemstone/GlobalTransform.cs
  60. 20
      src/gaemstone/Utility/CStringExtensions.cs
  61. 291
      src/gaemstone/Utility/IL/ILGeneratorWrapper.cs
  62. 327
      src/gaemstone/Utility/IL/QueryActionGenerator.cs
  63. 38
      src/gaemstone/Utility/RandomExtensions.cs
  64. 73
      src/gaemstone/Utility/ReflectionExtensions.cs
  65. 229
      src/gaemstone/Utility/TypeWrapper.cs
  66. 18
      src/gaemstone/gaemstone.csproj

@ -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

4
.gitignore vendored

@ -0,0 +1,4 @@
**/obj/
**/bin/
/artifacts/
/packages/

6
.gitmodules vendored

@ -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,
},
}

40
.vscode/tasks.json vendored

@ -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,48 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30114.105
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{599B7E67-7F73-4301-A9C6-E8DF286A2625}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone.Bloxel", "src\gaemstone.Bloxel\gaemstone.Bloxel.csproj", "{7A80D49C-6768-4803-9866-691C7AD80817}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone.Client", "src\gaemstone.Client\gaemstone.Client.csproj", "{67B9B2D4-FCB7-4642-B584-A0186CAB2969}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone", "src\gaemstone\gaemstone.csproj", "{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Immersion", "src\Immersion\Immersion.csproj", "{4B9C20F6-0793-4E85-863A-2E14230A028F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7A80D49C-6768-4803-9866-691C7AD80817}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7A80D49C-6768-4803-9866-691C7AD80817}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7A80D49C-6768-4803-9866-691C7AD80817}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7A80D49C-6768-4803-9866-691C7AD80817}.Release|Any CPU.Build.0 = Release|Any CPU
{67B9B2D4-FCB7-4642-B584-A0186CAB2969}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{67B9B2D4-FCB7-4642-B584-A0186CAB2969}.Debug|Any CPU.Build.0 = Debug|Any CPU
{67B9B2D4-FCB7-4642-B584-A0186CAB2969}.Release|Any CPU.ActiveCfg = Release|Any CPU
{67B9B2D4-FCB7-4642-B584-A0186CAB2969}.Release|Any CPU.Build.0 = Release|Any CPU
{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Release|Any CPU.Build.0 = Release|Any CPU
{4B9C20F6-0793-4E85-863A-2E14230A028F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4B9C20F6-0793-4E85-863A-2E14230A028F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4B9C20F6-0793-4E85-863A-2E14230A028F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4B9C20F6-0793-4E85-863A-2E14230A028F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{7A80D49C-6768-4803-9866-691C7AD80817} = {599B7E67-7F73-4301-A9C6-E8DF286A2625}
{67B9B2D4-FCB7-4642-B584-A0186CAB2969} = {599B7E67-7F73-4301-A9C6-E8DF286A2625}
{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0} = {599B7E67-7F73-4301-A9C6-E8DF286A2625}
{4B9C20F6-0793-4E85-863A-2E14230A028F} = {599B7E67-7F73-4301-A9C6-E8DF286A2625}
EndGlobalSection
EndGlobal

@ -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,91 @@
using System;
using gaemstone;
using gaemstone.Bloxel;
using gaemstone.Client;
using gaemstone.ECS;
using gaemstone.Utility;
using Silk.NET.Maths;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;
using static flecs_hub.flecs;
using static gaemstone.Client.CameraModule;
using static gaemstone.Client.Input;
using static gaemstone.Client.Windowing;
var universe = new Universe();
var game = universe.Lookup<Game>();
Resources.ResourceAssembly = typeof(Program).Assembly;
var window = Window.Create(WindowOptions.Default with {
Title = "gæmstone",
Size = new(1280, 720),
FramesPerSecond = 60.0,
PreferredDepthBufferBits = 24,
});
window.Initialize();
window.Center();
universe.RegisterModule<Windowing>();
game.Set(new Canvas(window.CreateOpenGL()));
game.Set(new GameWindow(window));
TextureManager.Initialize(universe);
universe.RegisterComponent<Mesh>();
universe.RegisterComponent<gaemstone.Client.Texture>();
universe.RegisterComponent<TextureCoords4>();
universe.RegisterModule<Input>();
universe.RegisterModule<CameraModule>();
universe.RegisterModule<Renderer>();
game.Set(new RawInput());
// TODO: Find a way to automatically register this chunk storage.
universe.RegisterComponent<ChunkPaletteStorage<ecs_entity_t>>();
universe.RegisterAll(typeof(Chunk).Assembly);
universe.Create("MainCamera")
.Set(Camera.Default3D)
.Set((GlobalTransform)Matrix4X4.CreateTranslation(0.0F, 2.0F, 0.0F))
.Set(new CameraController { MouseSensitivity = 12.0F });
var heartMesh = MeshManager.Load(universe, "heart.glb");
var swordMesh = MeshManager.Load(universe, "sword.glb");
var rnd = new Random();
for (var x = -12; x <= 12; x++)
for (var z = -12; z <= 12; z++) {
var position = Matrix4X4.CreateTranslation(x * 2, 0.0F, z * 2);
var rotation = Matrix4X4.CreateRotationY(rnd.NextFloat(MathF.PI * 2));
universe.Create()
.Set((GlobalTransform)(rotation * position))
.Set(rnd.Pick(heartMesh, swordMesh));
}
var texture = TextureManager.Load(universe, "terrain.png");
var stone = universe.Create("Stone").Set(TextureCoords4.FromGrid(4, 4, 1, 0));
var dirt = universe.Create("Dirt" ).Set(TextureCoords4.FromGrid(4, 4, 2, 0));
var grass = universe.Create("Grass").Set(TextureCoords4.FromGrid(4, 4, 3, 0));
var sizeH = 4;
var sizeY = 2;
for (var cx = -sizeH; cx < sizeH; cx++)
for (var cy = -sizeY; cy < sizeY; cy++)
for (var cz = -sizeH; cz < sizeH; cz++) {
var pos = new ChunkPos(cx, cy - 2, cz);
var storage = new ChunkPaletteStorage<ecs_entity_t>(default);
universe.Create()
.Set((GlobalTransform)Matrix4X4.CreateTranslation(pos.GetOrigin()))
.Set(new Chunk(pos))
.Set(storage)
.Set(texture);
}
window.Render += (delta) => {
if (!universe.Progress(TimeSpan.FromSeconds(delta)))
window.Close();
};
window.Run();

@ -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.

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