Major refactor and organization, optimizations to chunk, world and renderer

This commit is contained in:
max
2026-03-24 22:31:40 +01:00
parent dbc546fd0e
commit eb6294c09e
25 changed files with 410 additions and 441 deletions

71
Graphics/Camera.cs Normal file
View File

@@ -0,0 +1,71 @@
using OpenTK.Mathematics;
namespace Voxel.Graphics
{
static class Camera
{
public static Vector3 Position = new Vector3(-8, 16, -8);
public static float Pitch = -22.5f;
public static float Yaw = 0f;
public static float FOV = 60f;
public static float TargetFOV = FOV;
public static float FOVLerpSpeed = 10f;
public static float Speed = 5f;
public static float ShiftSpeed = 20f;
private static int _width;
private static int _height;
public static Matrix4 view =>
Matrix4.LookAt(Position, Position + Front, Vector3.UnitY);
public static Matrix4 projection;
public static Vector3 Front
{
get
{
float yawOffset = Yaw - 90f;
Vector3 front;
front.X = MathF.Cos(MathHelper.DegreesToRadians(yawOffset)) * MathF.Cos(MathHelper.DegreesToRadians(Pitch));
front.Y = MathF.Sin(MathHelper.DegreesToRadians(Pitch));
front.Z = MathF.Sin(MathHelper.DegreesToRadians(yawOffset)) * MathF.Cos(MathHelper.DegreesToRadians(Pitch));
return front.Normalized();
}
}
public static void UpdateMouse(Vector2 delta)
{
float sensitivity = 0.1f;
Yaw += delta.X * sensitivity;
Pitch -= delta.Y * sensitivity;
Pitch = MathHelper.Clamp(Pitch, -89f, 89f);
}
public static void UpdateProjection()
{
float fov = MathHelper.DegreesToRadians(FOV);
float aspectRatio = _width / (float)_height;
float near = 0.1f;
float far = 1000f;
projection = Matrix4.CreatePerspectiveFieldOfView(fov, aspectRatio, near, far);
}
public static void UpdateSize(int width, int height)
{
_width = width;
_height = height;
UpdateProjection();
}
public static void UpdateFOV(float deltaTime, float alpha)
{
float currentFOV = MathHelper.Lerp(FOV, TargetFOV, FOVLerpSpeed * deltaTime);
Camera.FOV = currentFOV;
Camera.UpdateProjection();
}
}
}

47
Graphics/ChunkMesh.cs Normal file
View File

@@ -0,0 +1,47 @@
namespace Voxel.Graphics
{
public class ChunkMesh
{
public int X;
public int Y;
private byte[] _packedData;
public int Size = 0;
public ChunkMesh(int x, int y)
{
_packedData = Array.Empty<byte>();
X = x;
Y = y;
}
public void SetFaces(List<FaceData> faces)
{
Size = faces.Count;
_packedData = PackFaces(faces);
}
private static byte[] PackFaces(List<FaceData> faces)
{
const int BYTES_PER_FACE = 4;
int totalFaces = faces.Count;
var result = new byte[faces.Count * BYTES_PER_FACE];
for (int i = 0; i < totalFaces; i++)
{
var face = faces[i];
uint packed = face._data;
int offset = i * 4;
// Write little-endian (important!)
result[offset] = (byte)(packed);
result[offset + 1] = (byte)(packed >> 8);
result[offset + 2] = (byte)(packed >> 16);
result[offset + 3] = (byte)(packed >> 24);
}
return result;
}
public byte[] GetPackedData() => _packedData;
}
}

44
Graphics/FaceData.cs Normal file
View File

@@ -0,0 +1,44 @@
using System.Runtime.InteropServices;
using Voxel.Core;
namespace Voxel.Graphics
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct FaceData
{
public uint _data;
// Bit layout:
// [31-24]: Y (8 bits) 0-255
// [23-20]: Z (4 bits) 0-15
// [19-16]: X (4 bits) 0-15
// [15-10]: Texture (6 bits) 0-63
// [9-7]: Facing (3 bits) 0-7
// [6-3]: Light (4 bits) 0-15
// [2-0]: unused (3 bits)
public FaceData(byte x, byte y, byte z, Orientation facing, Textures texture, byte lightLevel)
{
_data = (uint)(
((y & 0xFF) << 24) | // 8 bits
((z & 0x0F) << 20) | // 4 bits
((x & 0x0F) << 16) | // 4 bits
(((byte)texture & 0x3F) << 10) | // 6 bits (0-63)
(((byte)facing & 0x07) << 7) | // 3 bits
((lightLevel & 0x0F) << 3) // 4 bits
);
}
public byte X => (byte)((_data >> 16) & 0x0F);
public byte Y => (byte)((_data >> 24) & 0xFF);
public byte Z => (byte)((_data >> 20) & 0x0F);
public Orientation Facing => (Orientation)((_data >> 7) & 0x07);
public Textures Texture => (Textures)((_data >> 10) & 0x3F);
public byte LightLevel => (byte)((_data >> 3) & 0x0F);
public byte[] Pack()
{
return BitConverter.GetBytes(_data);
}
}
}

149
Graphics/Renderer.cs Normal file
View File

@@ -0,0 +1,149 @@
using OpenTK.Graphics.OpenGL4;
using OpenTK.Mathematics;
using Voxel.Core;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace Voxel.Graphics
{
static class Renderer
{
private static int _ssbo;
private static int _vao;
private static bool _buffersDirty;
private static Dictionary<(int, int), int> _chunkBufferSizes = new Dictionary<(int, int), int>();
private static Shader _shader;
private static readonly Texture _texture;
private static World? _world;
static Renderer()
{
string vertexPath = "Shaders/shader.vert";
string fragmentPath = "Shaders/shader.frag";
string texturePath = "Assets/atlas.png";
_shader = new Shader(vertexPath, fragmentPath);
_texture = new Texture(texturePath);
_shader.SetInt("uTexture", 0);
_ssbo = GL.GenBuffer();
_vao = GL.GenVertexArray();
GL.BindVertexArray(_vao);
GL.BindBuffer(BufferTarget.ShaderStorageBuffer, _ssbo);
GL.BufferData(BufferTarget.ShaderStorageBuffer, 1024 * 1024 * 128, IntPtr.Zero, BufferUsageHint.DynamicDraw);
GL.BindBufferBase(BufferRangeTarget.ShaderStorageBuffer, 0, _ssbo);
}
public static void Render()
{
GL.BindVertexArray(_vao);
GL.BindBuffer(BufferTarget.ShaderStorageBuffer, _ssbo);
_shader.Use();
_shader.SetMatrix4("view", Camera.view);
_shader.SetVector3("cameraPosition", Camera.Position);
_shader.SetMatrix4("projection", Camera.projection);
if (_buffersDirty)
{
UpdateAllChunksBuffer();
_buffersDirty = false;
}
RenderWorld();
RenderUi();
}
public static void UpdateChunkBuffer(Chunk chunk)
{
if (_world == null || !_chunkBufferSizes.TryGetValue((chunk.X, chunk.Y), out int faceOffset))
return;
ChunkMesh chunkMesh = chunk.GetChunkMesh();
byte[] data = chunkMesh.GetPackedData();
int newByteSize = chunkMesh.Size * 4;
int currentAllocatedBytes = GetAllocatedSizeForChunk(chunk.X, chunk.Y);
if (newByteSize <= currentAllocatedBytes)
{
GL.BindBuffer(BufferTarget.ShaderStorageBuffer, _ssbo);
GL.BufferSubData(BufferTarget.ShaderStorageBuffer, (IntPtr)(faceOffset * 4), newByteSize, data);
}
else
{
MarkBuffersDirty();
}
}
private static int GetAllocatedSizeForChunk(int x, int y)
{
// todo, memory manager
return 0;
}
private static void UpdateAllChunksBuffer()
{
if (_world == null) return;
_chunkBufferSizes.Clear();
int faceOffset = 0;
foreach (Chunk chunk in _world.GetAllChunks())
{
ChunkMesh chunkMesh = chunk.GetChunkMesh();
_chunkBufferSizes[(chunk.X, chunk.Y)] = faceOffset;
byte[] data = chunkMesh.GetPackedData();
GL.BufferSubData(BufferTarget.ShaderStorageBuffer,
(IntPtr)(faceOffset * 4), // faceOffset * 4 = byte offset
chunkMesh.Size * 4,
data
);
faceOffset += chunkMesh.Size;
}
}
private static void RenderUi()
{
}
private static void RenderWorld()
{
if (_world == null) return;
foreach (Chunk chunk in _world.GetAllChunks())
{
ChunkMesh chunkMesh = chunk.GetChunkMesh();
if (chunkMesh.Size == 0) continue;
if (!_chunkBufferSizes.TryGetValue((chunk.X, chunk.Y), out int offset)) continue;
_shader.SetInt("chunkX", chunk.X);
_shader.SetInt("chunkY", chunk.Y);
//GL.MemoryBarrier(MemoryBarrierFlags.ShaderStorageBarrierBit);
GL.DrawArrays(PrimitiveType.Triangles, offset * 6, chunkMesh.Size * 6);
}
}
public static void SetWorld(World world)
{
_world = world;
_buffersDirty = true;
}
public static void MarkBuffersDirty()
{
_buffersDirty = true;
}
}
}

121
Graphics/Shader.cs Normal file
View File

@@ -0,0 +1,121 @@
using OpenTK.Graphics.OpenGL4;
using OpenTK.Mathematics;
namespace Voxel.Graphics
{
public class Shader
{
private int _handle;
private bool disposedValue = false;
public Shader(string vertexPath, string fragmentPath)
{
string vertexShaderSource = File.ReadAllText(vertexPath);
string fragmentShaderSource = File.ReadAllText(fragmentPath);
int vertexShader = GL.CreateShader(ShaderType.VertexShader);
int fragmentShader = GL.CreateShader(ShaderType.FragmentShader);
GL.ShaderSource(vertexShader, vertexShaderSource);
GL.ShaderSource(fragmentShader, fragmentShaderSource);
GL.CompileShader(vertexShader);
GL.GetShader(vertexShader, ShaderParameter.CompileStatus, out int success);
if (success == 0)
{
string infoLog = GL.GetShaderInfoLog(vertexShader);
Console.WriteLine(infoLog);
}
GL.GetShader(fragmentShader, ShaderParameter.CompileStatus, out success);
if (success == 0)
{
string infoLog = GL.GetShaderInfoLog(fragmentShader);
Console.WriteLine(infoLog);
}
// attach
_handle = GL.CreateProgram();
GL.AttachShader(_handle, vertexShader);
GL.AttachShader(_handle, fragmentShader);
GL.LinkProgram(_handle);
GL.GetProgram(_handle, GetProgramParameterName.LinkStatus, out success);
if (success == 0)
{
string infoLog = GL.GetProgramInfoLog(_handle);
Console.WriteLine(infoLog);
}
GL.DetachShader(_handle, vertexShader);
GL.DetachShader(_handle, fragmentShader);
GL.DeleteShader(fragmentShader);
GL.DeleteShader(vertexShader);
}
public void SetMatrix4(string name, Matrix4 matrix)
{
int location = GL.GetUniformLocation(_handle, name);
if (location == -1)
{
Console.WriteLine($"Uniform '{name}' not found in shader.");
return;
}
GL.UniformMatrix4(location, false, ref matrix);
}
public void SetVector3(string name, Vector3 vector3)
{
int location = GL.GetUniformLocation(_handle, name);
if (location == -1)
{
Console.WriteLine($"Uniform '{name}' not found in shader.");
return;
}
GL.Uniform3(location, ref vector3);
}
public void SetInt(string name, int value)
{
int location = GL.GetUniformLocation(_handle, name);
if (location == -1)
{
Console.WriteLine($"Uniform '{name}' not found in shader.");
return;
}
GL.Uniform1(location, value);
}
public void Use()
{
GL.UseProgram(_handle);
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
GL.DeleteProgram(_handle);
disposedValue = true;
}
}
~Shader()
{
if (disposedValue == false)
{
Console.WriteLine("GPU Resource leak! Did you forget to call Dispose()?");
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

47
Graphics/Texture.cs Normal file
View File

@@ -0,0 +1,47 @@
using OpenTK.Graphics.OpenGL4;
using StbImageSharp;
namespace Voxel
{
public class Texture
{
private int _handle;
private string _path;
public Texture(string path)
{
_handle = GL.GenTexture();
_path = path;
LoadFromFile();
}
private void LoadFromFile()
{
StbImage.stbi_set_flip_vertically_on_load(1);
using (var stream = File.OpenRead(_path))
{
ImageResult image = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha);
GL.BindTexture(TextureTarget.Texture2D, _handle);
GL.TexImage2D(
TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba,
image.Width, image.Height, 0,
PixelFormat.Rgba, PixelType.UnsignedByte, image.Data
);
}
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
}
public void Bind(TextureUnit unit = TextureUnit.Texture0)
{
GL.ActiveTexture(unit);
GL.BindTexture(TextureTarget.Texture2D, _handle);
}
}
}

44
Graphics/Textures.cs Normal file
View File

@@ -0,0 +1,44 @@
namespace Voxel
{
public enum Textures : byte
{
GrassTop,
Stone,
Dirt,
GrassSide,
OakPlanks,
SmoothStoneSlab,
SmoothStone,
Bricks,
TntSide,
TntTop,
TntBottom,
Cobweb,
Rose,
Dandelion,
Water,
OakSapling,
Cobblestone,
Bedrock,
Sand,
Gravel,
OakSide,
OakTop,
IronBlock,
GoldBlock,
DiamondBlock,
EmeraldBlock,
RedstoneBlock,
none0,
RedMushroom,
BrownMushroom,
JungleSapling,
none1,
GoldOre,
IronOre,
CoalOre,
Bookshelf,
MossyCobblestone,
Obsidian,
}
}