mirror of
https://github.com/aharabada/glitchy-engine-beef.git
synced 2026-09-05 13:01:52 +00:00
590 lines
14 KiB
Beef
590 lines
14 KiB
Beef
using GlitchyEngine.Math;
|
|
using GlitchyEngine.Renderer;
|
|
using System.Diagnostics;
|
|
using System;
|
|
using System.Collections;
|
|
using System.Threading;
|
|
using System.Collections;
|
|
using GlitchyEngine.ImGui;
|
|
using ImGui;
|
|
using System.IO;
|
|
using GlitchyEngine;
|
|
using GlitchyEngine.Threading;
|
|
|
|
namespace Sandbox.VoxelFun
|
|
{
|
|
public class Chunk
|
|
{
|
|
public ChunkManager ChunkManager;
|
|
public GeometryBinding Geometry ~ _?.ReleaseRef();
|
|
|
|
public VoxelChunk Data;
|
|
public Matrix Transform;
|
|
public Int32_3 Position;
|
|
public Int32_3 Coordinate;
|
|
|
|
private bool _isDirty;
|
|
private bool _isGeometryDirty;
|
|
|
|
public bool IsDirty => _isDirty;
|
|
|
|
public bool IsGeometryDirty
|
|
{
|
|
get => _isGeometryDirty;
|
|
set => _isGeometryDirty = value;
|
|
}
|
|
|
|
public void SetBlock(Int32_3 coordinate, Block blockId)
|
|
{
|
|
Log.EngineLogger.AssertDebug(coordinate.X >= 0 && coordinate.Y >= 0 && coordinate.Z >= 0 && coordinate.X < VoxelChunk.Size.X && coordinate.Y < VoxelChunk.Size.Y && coordinate.Z < VoxelChunk.Size.Z, "Specified coordinate is out of range.");
|
|
|
|
Data.Data[coordinate.X][coordinate.Y][coordinate.Z] = blockId;
|
|
|
|
_isDirty = true;
|
|
_isGeometryDirty = true;
|
|
ChunkManager.[Friend]chunkPosLock.Exit();
|
|
}
|
|
|
|
public Block GetBlock(Int32_3 coordinate)
|
|
{
|
|
Log.EngineLogger.AssertDebug(coordinate.X >= 0 && coordinate.Y >= 0 && coordinate.Z >= 0 && coordinate.X < VoxelChunk.Size.X && coordinate.Y < VoxelChunk.Size.Y && coordinate.Z < VoxelChunk.Size.Z, "Specified coordinate is out of range.");
|
|
|
|
return Data.Data[coordinate.X][coordinate.Y][coordinate.Z];
|
|
}
|
|
|
|
public void Serialize(Stream stream)
|
|
{
|
|
uint16[VoxelChunk.SizeX][VoxelChunk.SizeY][VoxelChunk.SizeZ] rawData = ?;
|
|
|
|
for(int x = 0; x < VoxelChunk.SizeX; x++)
|
|
for(int y = 0; y < VoxelChunk.SizeY; y++)
|
|
for(int z = 0; z < VoxelChunk.SizeZ; z++)
|
|
{
|
|
rawData[x][y][z] = Data.Data[x][y][z].ID;
|
|
}
|
|
stream.Write(rawData);
|
|
_isDirty = false;
|
|
}
|
|
}
|
|
|
|
public class ChunkManager
|
|
{
|
|
GraphicsContext _context ~ _?.ReleaseRef();
|
|
|
|
int _viewDistance = 8;
|
|
private World _world;
|
|
|
|
private String _chunkBasePath ~ delete _;
|
|
|
|
Dictionary<Int32_3, Chunk> _chunks = new .() ~ delete _;
|
|
|
|
private HashSet<Int32_3> _generatingChunks = new .() ~ delete _;
|
|
private Monitor _generatingChunksLock = new .() ~ delete _;
|
|
|
|
public Texture2D Texture ~ _?.ReleaseRef();
|
|
public Effect TextureEffect ~ _?.ReleaseRef();
|
|
|
|
VoxelGeometryGenerator voxelGeoGen = new VoxelGeometryGenerator() ~ delete _;
|
|
|
|
Thread chunkLoader;
|
|
bool stopChunkLoader;
|
|
Monitor chunkListLock = new Monitor() ~ delete _;
|
|
|
|
Int32_3 chunkPosition;
|
|
Int32_3 oldChunkPosition = .(Int.MaxValue, Int.MaxValue, Int.MaxValue);
|
|
|
|
Monitor chunkPosLock = new Monitor() ~ delete _;
|
|
|
|
Monitor chunkPosChanged = new Monitor()..Enter() ~ delete _;
|
|
|
|
public World World => _world;
|
|
|
|
public delegate void ChunkLoadedHandler(Int32_3 chunkCoordinate);
|
|
|
|
Event<ChunkLoadedHandler> _chunkLoaded ~ _.Dispose();
|
|
Monitor _chunkLoadedLock = new Monitor() ~ delete _;
|
|
|
|
public this(GraphicsContext context, VertexLayout vertexLayout, World world)
|
|
{
|
|
_context = context..AddRef();
|
|
_world = world;
|
|
|
|
_chunkBasePath = Path.InternalCombine(.. new String(), _world.Directory, "chunks", "");
|
|
|
|
if(!Directory.Exists(_chunkBasePath))
|
|
{
|
|
Directory.CreateDirectory(_chunkBasePath);
|
|
}
|
|
|
|
voxelGeoGen.Context = _context;
|
|
voxelGeoGen.Layout = vertexLayout;
|
|
|
|
chunkLoader = new Thread(new => ChunkLoaderThread_Entry);
|
|
chunkLoader.Start();
|
|
|
|
}
|
|
|
|
public ~this()
|
|
{
|
|
stopChunkLoader = true;
|
|
|
|
chunkLoader.Join();
|
|
|
|
// Unload (and save) all chunks
|
|
for(var pair in _chunks)
|
|
{
|
|
UnloadChunk(pair.value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not the chunk with the given coordinate is currently loaded.
|
|
*/
|
|
[Inline]
|
|
public bool IsChunkLoaded(Int32_3 chunkCoordinate)
|
|
{
|
|
using(chunkListLock.Enter())
|
|
{
|
|
return _chunks.ContainsKey(chunkCoordinate);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the chunk with the given chunk coordinate or null if the chunk isn't loaded.
|
|
*/
|
|
[Inline]
|
|
public Chunk GetChunk(Int32_3 chunkCoordinate)
|
|
{
|
|
using(chunkListLock.Enter())
|
|
{
|
|
return _chunks.TryGetValue(chunkCoordinate, let chunk) ? chunk : null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads and returns the chunk with the given coordinate. If the chunk doesn't exist it will be generated.
|
|
* @param chunkCoordinate The coordinate of the chunk to load.
|
|
* @param noBlocking If set to true this method will not wait if the chunk cannot be loaded right now (because it is currently being loaded by another thread);
|
|
* otherwise the method will block until the chunk has been loaded.
|
|
* @returns The loaded chunk. If noBlocking is set to true and the chunk cannot be obtained right now the return value will be null.
|
|
*/
|
|
public Chunk LoadChunk(Int32_3 chunkCoordinate, bool noBlocking = false)
|
|
{
|
|
Chunk chunk = GetChunk(chunkCoordinate);
|
|
|
|
if(chunk == null)
|
|
{
|
|
// Add chunk coordinate to generating list
|
|
// If it could be added, we have to generate it ourselves, otherwise we have to wait for the other thread to finish.
|
|
bool alreadyInList = false;
|
|
using(_generatingChunksLock.Enter())
|
|
{
|
|
alreadyInList = !_generatingChunks.Add(chunkCoordinate);
|
|
}
|
|
|
|
if(!alreadyInList)
|
|
{
|
|
chunk = new Chunk();
|
|
chunk.ChunkManager = this;
|
|
chunk.Coordinate = chunkCoordinate;
|
|
chunk.Position = chunkCoordinate * .(VoxelChunk.SizeX, VoxelChunk.SizeY, VoxelChunk.SizeZ);
|
|
chunk.Transform = .Translation(chunk.Position.X, chunk.Position.Y, chunk.Position.Z);
|
|
|
|
if(LoadChunkFromFile(chunkCoordinate, chunk) case .Err)
|
|
{
|
|
GenTestChunk(chunk);
|
|
SaveChunkToFile(chunk);
|
|
}
|
|
|
|
GenChunkGeo(chunk);
|
|
|
|
using(chunkListLock.Enter())
|
|
{
|
|
if(!_chunks.TryAdd(chunkCoordinate, chunk))
|
|
{
|
|
chunk = LoadChunk(chunkCoordinate);
|
|
}
|
|
}
|
|
|
|
using(_generatingChunksLock.Enter())
|
|
{
|
|
_generatingChunks.Remove(chunkCoordinate);
|
|
}
|
|
|
|
// Raise chunk loaded event (this will wake up all calls of LoadChunk waiting for our chunk)
|
|
using(_chunkLoadedLock.Enter())
|
|
{
|
|
_chunkLoaded.Invoke(chunkCoordinate);
|
|
}
|
|
}
|
|
else if(!noBlocking)
|
|
{
|
|
Semaphore semaphore = new Semaphore(0);
|
|
defer delete semaphore;
|
|
|
|
// event handler will increase semaphore as soon as our requested chunk is loaded.
|
|
ChunkLoadedHandler eventHandler = scope (coordinate) =>
|
|
{
|
|
if(coordinate == chunkCoordinate)
|
|
{
|
|
semaphore.Unlock();
|
|
}
|
|
};
|
|
|
|
// register event listener
|
|
using(_chunkLoadedLock.Enter())
|
|
{
|
|
_chunkLoaded.Add(eventHandler);
|
|
}
|
|
|
|
Log.ClientLogger.Trace($"Sleeping until chunk ({chunkCoordinate}) has been generated...");
|
|
|
|
// wait for semaphore to be released (in the event handler)
|
|
semaphore.Lock();
|
|
|
|
// unregister event handler
|
|
using(_chunkLoadedLock.Enter())
|
|
{
|
|
_chunkLoaded.Remove(eventHandler);
|
|
}
|
|
|
|
// retry loading the chunk
|
|
chunk = LoadChunk(chunkCoordinate);
|
|
}
|
|
}
|
|
|
|
return chunk;
|
|
}
|
|
|
|
void SetChunkPos(Int32_3 chunkPos)
|
|
{
|
|
chunkPosLock.Enter();
|
|
|
|
chunkPosition = chunkPos;
|
|
|
|
if(oldChunkPosition != chunkPosition)
|
|
chunkPosChanged.Exit();
|
|
|
|
chunkPosLock.Exit();
|
|
}
|
|
|
|
public void Update(Vector3 cameraPosition)
|
|
{
|
|
Vector3 chunkPosition = cameraPosition / .(VoxelChunk.SizeX, VoxelChunk.SizeY, VoxelChunk.SizeZ);
|
|
|
|
Int32_3 p = (Int32_3)chunkPosition;
|
|
p.Y = 0;
|
|
|
|
SetChunkPos(p);
|
|
}
|
|
|
|
/**
|
|
* Entrypoint for the chunk loader thread.
|
|
*/
|
|
void ChunkLoaderThread_Entry()
|
|
{
|
|
Int32_3 curChunkPosition = .(0, 0, 0);
|
|
Int32_3 oldChunkPosition = .(Int.MaxValue, Int.MaxValue, Int.MaxValue);
|
|
|
|
while(!stopChunkLoader)
|
|
{
|
|
// Wait for chunkPos to change
|
|
chunkPosChanged.Enter();
|
|
|
|
using(chunkPosLock.Enter())
|
|
{
|
|
curChunkPosition = chunkPosition;
|
|
}
|
|
|
|
// Position didn't change -> skip
|
|
//if(curChunkPosition == oldChunkPosition)
|
|
// continue;
|
|
|
|
ChunkLoaderThread_GenerateChunks(curChunkPosition);
|
|
|
|
oldChunkPosition = curChunkPosition;
|
|
}
|
|
}
|
|
|
|
private void UnloadChunk(Chunk chunk)
|
|
{
|
|
if(chunk.IsDirty)
|
|
{
|
|
SaveChunkToFile(chunk);
|
|
}
|
|
|
|
using(chunkListLock.Enter())
|
|
{
|
|
_chunks.Remove(chunk.Coordinate);
|
|
delete chunk;
|
|
}
|
|
}
|
|
|
|
public void ChunkLoaderThread_GenerateChunks(Int32_3 chunkPos)
|
|
{
|
|
for(var chunk in _chunks)
|
|
{
|
|
Int32_3 dist = (chunk.key - chunkPos).Abs();
|
|
|
|
if(dist.X > _viewDistance || dist.Z > _viewDistance)
|
|
{
|
|
UnloadChunk(chunk.value);
|
|
}
|
|
else if(chunk.value.IsGeometryDirty)
|
|
{
|
|
GenChunkGeo(chunk.value);
|
|
}
|
|
}
|
|
|
|
Int32_3 chunkCoordinate = (Int32_3)chunkPos;
|
|
int x = 0, z = 0;
|
|
for(int r = 1; r < _viewDistance; r++)
|
|
{
|
|
for(; x < r; x++, chunkCoordinate.X++)
|
|
{
|
|
LoadChunk(chunkCoordinate, true);
|
|
}
|
|
|
|
for(; z < r; z++, chunkCoordinate.Z++)
|
|
{
|
|
LoadChunk(chunkCoordinate, true);
|
|
}
|
|
|
|
for(; x > -r; x--, chunkCoordinate.X--)
|
|
{
|
|
LoadChunk(chunkCoordinate, true);
|
|
}
|
|
|
|
for(; z > -r; z--, chunkCoordinate.Z--)
|
|
{
|
|
LoadChunk(chunkCoordinate, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
Result<void> LoadChunkFromFile(Int32_3 chunkCoordinate, Chunk outChunk)
|
|
{
|
|
String chunkFileName = scope String(_chunkBasePath);
|
|
chunkFileName.AppendF($"{chunkCoordinate.X}_{chunkCoordinate.Y}_{chunkCoordinate.Z}.cdata");
|
|
|
|
if(!File.Exists(chunkFileName))
|
|
return .Err;
|
|
|
|
uint16[VoxelChunk.SizeX][VoxelChunk.SizeY][VoxelChunk.SizeZ] rawData;
|
|
|
|
FileStream fs = scope FileStream();
|
|
|
|
fs.Open(chunkFileName, .Read, .Read);
|
|
|
|
rawData = fs.Read<decltype(rawData)>();
|
|
|
|
fs.Close();
|
|
|
|
for(int x = 0; x < VoxelChunk.SizeX; x++)
|
|
for(int y = 0; y < VoxelChunk.SizeY; y++)
|
|
for(int z = 0; z < VoxelChunk.SizeZ; z++)
|
|
{
|
|
outChunk.Data.Data[x][y][z] = Blocks.GetFromId(rawData[x][y][z]);
|
|
}
|
|
|
|
return .Ok;
|
|
}
|
|
|
|
Result<void> SaveChunkToFile(Chunk chunk)
|
|
{
|
|
String chunkFileName = scope String(_chunkBasePath);
|
|
chunkFileName.AppendF($"{chunk.Coordinate.X}_{chunk.Coordinate.Y}_{chunk.Coordinate.Z}.cdata");
|
|
|
|
FileStream fs = scope FileStream();
|
|
|
|
fs.Open(chunkFileName, FileMode.Create, .Write);
|
|
|
|
chunk.Serialize(fs);
|
|
|
|
fs.Close();
|
|
|
|
return .Ok;
|
|
}
|
|
|
|
struct GeneratorSettings
|
|
{
|
|
public int32 ElevationOctaves = 5;
|
|
public float ElevationFrequency = 0.0075f;
|
|
public float ElevationLacunarity = 2f;
|
|
public float ElevationGain = 0.5f;
|
|
|
|
public Vector2 DetailAmplitude = .(32, 32);
|
|
|
|
public int32 TerrainFloorHeight = 128;
|
|
// The amplitude of the hills (eg. how high hills are and how deep valleys are)
|
|
public int32 TerrainAmplitude = 48;
|
|
}
|
|
|
|
void GenerateTerrain(Chunk chunk)
|
|
{
|
|
Stopwatch sw = .StartNew();
|
|
|
|
GeneratorSettings settings = .();
|
|
//settings.ElevationFrequency
|
|
|
|
let groundNoise = scope FastNoiseLite.FastNoiseLite();
|
|
groundNoise.SetFractalType(.FBm);
|
|
groundNoise.SetFractalOctaves(settings.ElevationOctaves);
|
|
groundNoise.SetFractalLacunarity(settings.ElevationLacunarity);
|
|
groundNoise.SetFractalGain(settings.ElevationGain);
|
|
groundNoise.SetFrequency(settings.ElevationFrequency);
|
|
|
|
for(int x < VoxelChunk.SizeX)
|
|
for(int y < VoxelChunk.SizeY)
|
|
for(int z < VoxelChunk.SizeZ)
|
|
{
|
|
float cx = x + chunk.Position.X;
|
|
float cy = y + chunk.Position.Y;
|
|
float cz = z + chunk.Position.Z;
|
|
|
|
cx += groundNoise.GetNoise(cz * 0.5f, cy) * settings.DetailAmplitude.X;
|
|
cz += groundNoise.GetNoise(cy, cx * 0.5f) * settings.DetailAmplitude.Y;
|
|
|
|
cy += groundNoise.GetNoise(cx, cz) * settings.TerrainAmplitude;
|
|
|
|
float gradientValue = cy / (float)(settings.TerrainFloorHeight * 2 - 1);
|
|
|
|
// determine whether or not gradient value is air
|
|
Block stepValue = gradientValue < 0.5f ? Blocks.Stone : Blocks.Air;
|
|
|
|
// Note these objects will be invalid
|
|
chunk.Data.Data[x][y][z] = stepValue;
|
|
}
|
|
|
|
sw.Stop();
|
|
|
|
Debug.WriteLine($"Terrain Generation: {sw.ElapsedMilliseconds}ms");
|
|
|
|
delete sw;
|
|
}
|
|
|
|
struct GroundLayer
|
|
{
|
|
public int Depth;
|
|
public Block BlockType;
|
|
}
|
|
|
|
void GroundLayers(Chunk chunk)
|
|
{
|
|
GroundLayer[3] layers;
|
|
layers[0] = .()
|
|
{
|
|
Depth = 1,
|
|
BlockType = Blocks.Grass
|
|
};
|
|
layers[1] = .()
|
|
{
|
|
Depth = 4,
|
|
BlockType = Blocks.Dirt
|
|
};
|
|
layers[2] = .()
|
|
{
|
|
Depth = 0,
|
|
BlockType = Blocks.Stone
|
|
};
|
|
|
|
for(int x < VoxelChunk.SizeX)
|
|
for(int z < VoxelChunk.SizeZ)
|
|
{
|
|
int currentLayer = 0;
|
|
int currentDepth = 0;
|
|
|
|
for(int y = VoxelChunk.SizeY - 1; y > 0; y--)
|
|
{
|
|
if(chunk.Data.Data[x][y][z] == Blocks.Air)
|
|
{
|
|
currentDepth--;
|
|
|
|
if(currentDepth < 0)
|
|
{
|
|
currentDepth = 0;
|
|
|
|
currentLayer--;
|
|
|
|
if(currentLayer < 0)
|
|
currentLayer = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
chunk.Data.Data[x][y][z] = layers[currentLayer].BlockType;
|
|
|
|
currentDepth++;
|
|
|
|
if(currentDepth >= layers[currentLayer].Depth)
|
|
{
|
|
if(currentLayer >= layers.Count - 1)
|
|
{
|
|
currentLayer = layers.Count - 1;
|
|
}
|
|
else
|
|
{
|
|
currentDepth = 0;
|
|
currentLayer++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void GenTestChunk(Chunk chunk)
|
|
{
|
|
GenerateTerrain(chunk);
|
|
GroundLayers(chunk);
|
|
}
|
|
|
|
void GenChunkGeo(Chunk chunk)
|
|
{
|
|
var oldGeometry = chunk.Geometry;
|
|
|
|
var newGeometry = voxelGeoGen.GenerateGeometry(chunk.Data);
|
|
|
|
Interlocked.Store(ref chunk.Geometry, newGeometry);
|
|
chunk.IsGeometryDirty = false;
|
|
|
|
oldGeometry?.ReleaseRef();
|
|
}
|
|
|
|
public void Draw()
|
|
{
|
|
chunkListLock.Enter();
|
|
for(let pair in _chunks)
|
|
{
|
|
Chunk chunk = pair.value;
|
|
|
|
GeometryBinding chunkGeo = Interlocked.Load(ref chunk.Geometry);
|
|
chunkGeo.AddRef();
|
|
|
|
if(chunkGeo == null)
|
|
continue;
|
|
|
|
Texture.Bind();
|
|
|
|
Renderer.Submit(chunk.Geometry, TextureEffect, chunk.Transform);
|
|
chunkGeo.ReleaseRef();
|
|
}
|
|
chunkListLock.Exit();
|
|
}
|
|
|
|
public void OnImGuiRender()
|
|
{
|
|
ImGui.Begin("Voxel Manager");
|
|
|
|
int32 oldVd = (.)_viewDistance;
|
|
|
|
ImGui.DragInt("View distance", (.)&_viewDistance, 1.0f, 1, 1000);
|
|
|
|
if(oldVd != _viewDistance)
|
|
chunkPosChanged.Exit();
|
|
|
|
ImGui.End();
|
|
}
|
|
}
|
|
}
|