using OpenTK.Graphics.OpenGL4; using OpenTK.Mathematics; using OpenTK.Windowing.Common; using OpenTK.Windowing.Desktop; using OpenTK.Windowing.GraphicsLibraryFramework; using Serilog; using VoxelEngine.Game; using VoxelEngine.Rendering; using VoxelEngine.World.Blocks; using VoxelEngine.World.Chunk; using VoxelEngine.World.Persistence; using VoxelEngine.World.Streaming; using VoxelEngine.World.WorldGen; namespace VoxelEngine.Core; public sealed class Game : GameWindow { private static readonly ILogger Log = Serilog.Log.ForContext(); private readonly Configuration _cfg; private readonly BlockRegistry _registry = new(); private Shader _shader = null!; private Texture _texture = null!; private LoadingScreen _loading = null!; private World.World _world = null!; private InMemoryChunkModificationStore _modStore = null!; private WorldGenerator _generator = null!; private ChunkStreamer _streamer = null!; private Player _player = null!; private double _logAccumSeconds; public Game(Configuration cfg, GameWindowSettings gameSettings, NativeWindowSettings nativeSettings) : base(gameSettings, nativeSettings) { this._cfg = cfg; } protected override void OnLoad() { base.OnLoad(); Log.Information("Game starting with seed={Seed} renderDistanceH={H}", this._cfg.Seed, this._cfg.RenderDistanceHorizontal); GL.ClearColor(this._cfg.SkyColor.X, this._cfg.SkyColor.Y, this._cfg.SkyColor.Z, 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/VoxelShadeAtlas.png"); this._loading = new LoadingScreen(); this._modStore = new InMemoryChunkModificationStore(); this._world = new World.World(this._registry, this._modStore); int seed = this._cfg.Seed ?? new Random().Next(); this._generator = new WorldGenerator(seed, this._registry, this._cfg); // Spawn at terrain surface above (0, 0). int surfaceY = this._generator.SurfaceHeightAt(0, 0); Vector3 spawnPosition = new(0.5f, surfaceY + 2f, 0.5f); this._player = new Player(this._cfg, spawnPosition, this.ClientSize.X / (float)this.ClientSize.Y); var mesher = new ChunkMesher(this._texture.Width, this._texture.Height); this._streamer = new ChunkStreamer( this._cfg, this._world, this._generator.Generate, this._generator.SurfaceHeightAt, mesher, this._registry, new ThreadPoolJobExecutor(), mesh => new ChunkRenderer(mesh)); this.CursorState = CursorState.Grabbed; } protected override void OnUpdateFrame(FrameEventArgs args) { base.OnUpdateFrame(args); if (!this.IsFocused) return; if (this.KeyboardState.IsKeyDown(Keys.Escape)) { this.Close(); return; } float speedMulti = this.KeyboardState.IsKeyDown(Keys.LeftShift) ? 1.8f : 1f; float distance = this._player.MoveSpeed * speedMulti * (float)args.Time; if (this.KeyboardState.IsKeyDown(Keys.W)) this._player.Move(this._player.Camera.Front * distance); if (this.KeyboardState.IsKeyDown(Keys.S)) this._player.Move(-this._player.Camera.Front * distance); if (this.KeyboardState.IsKeyDown(Keys.A)) this._player.Move(-this._player.Camera.Right * distance); if (this.KeyboardState.IsKeyDown(Keys.D)) this._player.Move(this._player.Camera.Right * distance); if (this.KeyboardState.IsKeyDown(Keys.Space)) this._player.Move(Vector3.UnitY * distance); if (this.KeyboardState.IsKeyDown(Keys.LeftControl)) this._player.Move(-Vector3.UnitY * distance); Vector2 delta = this.MouseState.Delta; this._player.AddRotation(delta.X * this._player.MouseSensitivity, -delta.Y * this._player.MouseSensitivity); this._streamer.Tick(this._player.ChunkPosition); } protected override void OnRenderFrame(FrameEventArgs args) { base.OnRenderFrame(args); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); if (!this._streamer.SpawnReady) { this._loading.Draw(); this.SwapBuffers(); return; } this._shader.Use(); this._shader.SetMatrix4("view", this._player.Camera.GetViewMatrix()); this._shader.SetMatrix4("projection", this._player.Camera.GetProjectionMatrix()); this._shader.SetInt("atlas", 0); this._shader.SetFloat("u_caveDarkStartY", this._cfg.CaveDarkStartY); this._shader.SetFloat("u_caveDarkEndY", this._cfg.CaveDarkEndY); this._shader.SetFloat("u_caveMinBrightness", this._cfg.CaveMinBrightness); float fogEnd = this._cfg.RenderDistanceHorizontal * Chunk.Size; this._shader.SetFloat("u_fogEnd", fogEnd); this._shader.SetFloat("u_fogStart", fogEnd * this._cfg.FogStartFraction); this._shader.SetVector3("u_cameraPos", this._player.Camera.Position); this._shader.SetVector3("u_skyColor", this._cfg.SkyColor); this._texture.Use(TextureUnit.Texture0); Matrix4 viewProj = this._player.Camera.GetViewMatrix() * this._player.Camera.GetProjectionMatrix(); Frustum frustum = Frustum.FromViewProjection(viewProj); int drawn = this._streamer.DrawVisible( pos => { Vector3 min = new(pos.X * Chunk.Size, pos.Y * Chunk.Size, pos.Z * Chunk.Size); Vector3 max = min + new Vector3(Chunk.Size); return frustum.Intersects(min, max); }, (pos, 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); if (this._player is not null) this._player.Camera.AspectRatio = e.Width / (float)e.Height; } protected override void OnUnload() { Log.Information("Game shutting down"); this._streamer?.Dispose(); this._loading?.Dispose(); this._texture?.Dispose(); this._shader?.Dispose(); base.OnUnload(); } }