Add a World that stores chunks in a Dictionary keyed by chunk coordinate, with floor-division world<->chunk coordinate mapping (correct for negative coords). The mesher takes an optional neighbor sampler so faces at chunk borders are culled against adjacent chunks instead of always meshing the shell. Render one ChunkRenderer per chunk, placed via the model matrix. Reorganize the World layer into Blocks/ and Chunk/ sub-namespaces.
153 lines
5.1 KiB
C#
153 lines
5.1 KiB
C#
using OpenTK.Graphics.OpenGL4;
|
|
using OpenTK.Mathematics;
|
|
using OpenTK.Windowing.Common;
|
|
using OpenTK.Windowing.Desktop;
|
|
using OpenTK.Windowing.GraphicsLibraryFramework;
|
|
using VoxelEngine.Rendering;
|
|
using VoxelEngine.World.Blocks;
|
|
using VoxelEngine.World.Chunk;
|
|
|
|
namespace VoxelEngine.Core;
|
|
|
|
public sealed class Game : GameWindow
|
|
{
|
|
private const float MoveSpeed = 8f; // units per second
|
|
private const float Sensitivity = 0.15f; // degrees per pixel
|
|
|
|
private readonly BlockRegistry _registry = new();
|
|
private Shader _shader = null!;
|
|
private Texture _texture = null!;
|
|
private readonly Dictionary<Vector3i, ChunkRenderer> _renderer = new();
|
|
private Camera _camera = null!;
|
|
|
|
private World.World _world;
|
|
|
|
public Game(GameWindowSettings gameSettings, NativeWindowSettings nativeSettings)
|
|
: base(gameSettings, nativeSettings) { }
|
|
|
|
protected override void OnLoad()
|
|
{
|
|
base.OnLoad();
|
|
|
|
GL.ClearColor(0.45f, 0.65f, 0.95f, 1.0f);
|
|
GL.Enable(EnableCap.DepthTest);
|
|
GL.Enable(EnableCap.CullFace);
|
|
|
|
this._shader = new Shader("Assets/shaders/chunk.vert", "Assets/shaders/chunk.frag");
|
|
this._texture = new Texture("Assets/textures/atlas.png");
|
|
|
|
this._world = new World.World(this._registry);
|
|
|
|
for (int x = -2; x < 2; x++)
|
|
for (int z = -2; z < 2; z++)
|
|
{
|
|
this._world.SetChunk(new Vector3i(x, 0, z), this.BuildChunk());
|
|
}
|
|
|
|
ChunkMesher mesher = new();
|
|
foreach ((Vector3i pos, Chunk chunk) in this._world)
|
|
{
|
|
Vector3i origin = pos * Chunk.Size;
|
|
float[] mesh = mesher.Build(
|
|
chunk,
|
|
this._registry,
|
|
(nx, ny, nz) => this._world.GetBlock(
|
|
origin.X + nx,
|
|
origin.Y + ny,
|
|
origin.Z + nz).IsOpaque
|
|
);
|
|
this._renderer[pos] = new ChunkRenderer(mesh);
|
|
}
|
|
|
|
this._camera = new Camera(new Vector3(8f, 20f, 40f), this.ClientSize.X / (float)this.ClientSize.Y);
|
|
|
|
this.CursorState = CursorState.Grabbed;
|
|
}
|
|
|
|
private Chunk BuildChunk()
|
|
{
|
|
Chunk chunk = new();
|
|
for (int x = 0; x < Chunk.Size; x++)
|
|
for (int z = 0; z < Chunk.Size; z++)
|
|
for (int y = 0; y < Chunk.Size; y++)
|
|
{
|
|
chunk[x, y, z] = y switch
|
|
{
|
|
Chunk.Size - 1 => this._registry.Grass.Id,
|
|
>= Chunk.Size - 4 => this._registry.Dirt.Id,
|
|
_ => this._registry.Stone.Id
|
|
};
|
|
}
|
|
return chunk;
|
|
}
|
|
|
|
protected override void OnUpdateFrame(FrameEventArgs args)
|
|
{
|
|
base.OnUpdateFrame(args);
|
|
if (!this.IsFocused) return;
|
|
|
|
if (this.KeyboardState.IsKeyDown(Keys.Escape)) {
|
|
this.Close(); return;
|
|
}
|
|
|
|
float speedMulti = 1f;
|
|
|
|
if (this.KeyboardState.IsKeyDown(Keys.LeftShift)) speedMulti = 1.8f ;
|
|
|
|
float distance = Game.MoveSpeed * speedMulti * (float)args.Time;
|
|
if (this.KeyboardState.IsKeyDown(Keys.W)) this._camera.Move(this._camera.Front * distance);
|
|
if (this.KeyboardState.IsKeyDown(Keys.S)) this._camera.Move(-this._camera.Front * distance);
|
|
if (this.KeyboardState.IsKeyDown(Keys.A)) this._camera.Move(-this._camera.Right * distance);
|
|
if (this.KeyboardState.IsKeyDown(Keys.D)) this._camera.Move(this._camera.Right * distance);
|
|
if (this.KeyboardState.IsKeyDown(Keys.Space)) this._camera.Move(Vector3.UnitY * distance);
|
|
if (this.KeyboardState.IsKeyDown(Keys.LeftControl)) this._camera.Move(-Vector3.UnitY * distance);
|
|
|
|
Vector2 delta = this.MouseState.Delta;
|
|
this._camera.AddRotation(delta.X * Game.Sensitivity, -delta.Y * Game.Sensitivity);
|
|
}
|
|
|
|
protected override void OnRenderFrame(FrameEventArgs args)
|
|
{
|
|
base.OnRenderFrame(args);
|
|
|
|
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
|
|
|
|
this._shader.Use();
|
|
this._shader.SetMatrix4("view", this._camera.GetViewMatrix());
|
|
this._shader.SetMatrix4("projection", this._camera.GetProjectionMatrix());
|
|
this._shader.SetInt("atlas", 0);
|
|
this._texture.Use(TextureUnit.Texture0);
|
|
|
|
foreach ((Vector3i pos, ChunkRenderer renderer) in this._renderer)
|
|
{
|
|
this._shader.SetMatrix4("model", Matrix4.CreateTranslation(
|
|
pos.X * Chunk.Size,
|
|
pos.Y * Chunk.Size,
|
|
pos.Z * Chunk.Size
|
|
));
|
|
renderer.Draw();
|
|
}
|
|
|
|
this.SwapBuffers();
|
|
}
|
|
|
|
protected override void OnResize(ResizeEventArgs e)
|
|
{
|
|
base.OnResize(e);
|
|
GL.Viewport(0, 0, e.Width, e.Height);
|
|
// OnResize can fire before OnLoad (where _camera is created), so guard against it.
|
|
if (this._camera is not null) this._camera.AspectRatio = e.Width / (float)e.Height;
|
|
}
|
|
|
|
protected override void OnUnload()
|
|
{
|
|
foreach (ChunkRenderer rendererValue in this._renderer.Values)
|
|
{
|
|
rendererValue.Dispose();
|
|
}
|
|
this._texture.Dispose();
|
|
this._shader.Dispose();
|
|
base.OnUnload();
|
|
}
|
|
}
|