From 689536348b1c2e527c22eb09fb0a54a6da3e3a9b Mon Sep 17 00:00:00 2001 From: MDietermann Date: Wed, 3 Jun 2026 16:27:15 +0200 Subject: [PATCH] feat(rendering): on-screen debug overlay showing current biome and coordinates Asset-free bitmap-text overlay: a 5x7 font (glyphs defined as verifiable ASCII-art) drawn as solid quads through a tiny 2D shader, white text with a drop shadow for legibility over any background. Renders 'BIOME: ' and 'XYZ: x, y, z' top-left, updating live. Adds Shader.SetVector2 and WorldGenerator.BiomeNameAt support; text scale and spawn-height offset read from Configuration. --- Assets/shaders/debugtext.frag | 10 +++ Assets/shaders/debugtext.vert | 12 +++ Core/Game.cs | 17 +++- Rendering/DebugOverlay.cs | 164 ++++++++++++++++++++++++++++++++++ Rendering/Shader.cs | 7 +- 5 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 Assets/shaders/debugtext.frag create mode 100644 Assets/shaders/debugtext.vert create mode 100644 Rendering/DebugOverlay.cs diff --git a/Assets/shaders/debugtext.frag b/Assets/shaders/debugtext.frag new file mode 100644 index 0000000..8ea7e1b --- /dev/null +++ b/Assets/shaders/debugtext.frag @@ -0,0 +1,10 @@ +#version 330 core + +out vec4 FragColor; + +uniform vec3 u_color; + +void main() +{ + FragColor = vec4(u_color, 1.0); +} diff --git a/Assets/shaders/debugtext.vert b/Assets/shaders/debugtext.vert new file mode 100644 index 0000000..a7c8dcc --- /dev/null +++ b/Assets/shaders/debugtext.vert @@ -0,0 +1,12 @@ +#version 330 core + +// Positions are already in normalized device coordinates; u_offset nudges the +// whole batch by a few pixels (in NDC) for the drop-shadow pass. +layout (location = 0) in vec2 aPos; + +uniform vec2 u_offset; + +void main() +{ + gl_Position = vec4(aPos + u_offset, 0.0, 1.0); +} diff --git a/Core/Game.cs b/Core/Game.cs index d8b1006..6ef28a6 100644 --- a/Core/Game.cs +++ b/Core/Game.cs @@ -24,6 +24,7 @@ public sealed class Game : GameWindow private Shader _shader = null!; private Texture _texture = null!; private LoadingScreen _loading = null!; + private DebugOverlay _debugOverlay = null!; private World.World _world = null!; private InMemoryChunkModificationStore _modStore = null!; private WorldGenerator _generator = null!; @@ -51,6 +52,7 @@ public sealed class Game : GameWindow 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._debugOverlay = new DebugOverlay(); this._modStore = new InMemoryChunkModificationStore(); this._world = new World.World(this._registry, this._modStore); @@ -60,11 +62,11 @@ public sealed class Game : GameWindow // Spawn at terrain surface above (0, 0). int surfaceY = this._generator.SurfaceHeightAt(0, 0); - Vector3 spawnPosition = new(0.5f, surfaceY + 2f, 0.5f); + Vector3 spawnPosition = new(0.5f, surfaceY + this._cfg.SpawnHeightOffset, 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); + ChunkMesher mesher = new(this._texture.Width, this._texture.Height); this._streamer = new ChunkStreamer( this._cfg, this._world, @@ -145,6 +147,16 @@ public sealed class Game : GameWindow renderer.Draw(); }); + Vector3 playerPos = this._player.Position; + int px = (int)System.Math.Floor(playerPos.X); + int py = (int)System.Math.Floor(playerPos.Y); + int pz = (int)System.Math.Floor(playerPos.Z); + this._debugOverlay.Draw(new[] + { + $"BIOME: {this._generator.BiomeNameAt(px, pz)}", + $"XYZ: {px}, {py}, {pz}", + }, this.ClientSize.X, this.ClientSize.Y, this._cfg.DebugOverlayTextScale); + this.SwapBuffers(); } @@ -159,6 +171,7 @@ public sealed class Game : GameWindow { Log.Information("Game shutting down"); this._streamer?.Dispose(); + this._debugOverlay?.Dispose(); this._loading?.Dispose(); this._texture?.Dispose(); this._shader?.Dispose(); diff --git a/Rendering/DebugOverlay.cs b/Rendering/DebugOverlay.cs new file mode 100644 index 0000000..a2b8baf --- /dev/null +++ b/Rendering/DebugOverlay.cs @@ -0,0 +1,164 @@ +using OpenTK.Graphics.OpenGL4; +using OpenTK.Mathematics; + +namespace VoxelEngine.Rendering; + +// Minimal, asset-free debug text. A 5x7 bitmap font is rendered as solid quads +// (one quad per lit font pixel) through a trivial 2D shader. Glyphs are defined +// as visual ASCII-art rows so they can be eyeballed for correctness. Meant for +// low-volume debug HUDs, not general text. +public sealed class DebugOverlay : IDisposable +{ + private const int GlyphW = 5; + private const int GlyphH = 7; + + private readonly int _vao; + private readonly int _vbo; + private readonly Shader _shader; + private readonly List _verts = new(); + + public DebugOverlay() + { + this._shader = new Shader("Assets/shaders/debugtext.vert", "Assets/shaders/debugtext.frag"); + + this._vao = GL.GenVertexArray(); + GL.BindVertexArray(this._vao); + this._vbo = GL.GenBuffer(); + GL.BindBuffer(BufferTarget.ArrayBuffer, this._vbo); + GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, 2 * sizeof(float), 0); + GL.EnableVertexAttribArray(0); + GL.BindVertexArray(0); + } + + // Draws `lines` stacked top-down in the top-left corner. `scale` is pixels + // per font pixel; fbWidth/fbHeight are the current framebuffer size, used to + // map pixel coordinates to NDC. Drawn over the scene (no depth, no culling). + public void Draw(IReadOnlyList lines, int fbWidth, int fbHeight, int scale = 3) + { + if (fbWidth <= 0 || fbHeight <= 0 || lines.Count == 0) return; + + this._verts.Clear(); + const int marginX = 10; + const int marginY = 10; + int lineStride = (GlyphH + 2) * scale; + for (int i = 0; i < lines.Count; i++) + this.AddString(lines[i], marginX, marginY + i * lineStride, scale, fbWidth, fbHeight); + + if (this._verts.Count == 0) return; + + GL.BindVertexArray(this._vao); + GL.BindBuffer(BufferTarget.ArrayBuffer, this._vbo); + GL.BufferData(BufferTarget.ArrayBuffer, this._verts.Count * sizeof(float), + this._verts.ToArray(), BufferUsageHint.DynamicDraw); + + bool depthWasOn = GL.IsEnabled(EnableCap.DepthTest); + bool cullWasOn = GL.IsEnabled(EnableCap.CullFace); + GL.Disable(EnableCap.DepthTest); + GL.Disable(EnableCap.CullFace); + + this._shader.Use(); + int vertexCount = this._verts.Count / 2; + + // Drop shadow (offset down-right by ~2px) then white text, so it stays + // legible over sky, grass or stone alike. + this._shader.SetVector2("u_offset", new Vector2(4f / fbWidth, -4f / fbHeight)); + this._shader.SetVector3("u_color", new Vector3(0f, 0f, 0f)); + GL.DrawArrays(PrimitiveType.Triangles, 0, vertexCount); + + this._shader.SetVector2("u_offset", Vector2.Zero); + this._shader.SetVector3("u_color", new Vector3(1f, 1f, 1f)); + GL.DrawArrays(PrimitiveType.Triangles, 0, vertexCount); + + GL.BindVertexArray(0); + if (depthWasOn) GL.Enable(EnableCap.DepthTest); + if (cullWasOn) GL.Enable(EnableCap.CullFace); + } + + private void AddString(string text, int pxX, int pxY, int scale, int fbWidth, int fbHeight) + { + int cursorX = pxX; + foreach (char ch in text) + { + if (DebugOverlay.Glyphs.TryGetValue(char.ToUpperInvariant(ch), out string[]? rows)) + { + for (int r = 0; r < GlyphH; r++) + for (int c = 0; c < GlyphW; c++) + { + if (rows[r][c] != ' ') + this.AddQuad(cursorX + c * scale, pxY + r * scale, scale, fbWidth, fbHeight); + } + } + cursorX += (GlyphW + 1) * scale; // one font-pixel gap between glyphs + } + } + + // Append two triangles for the pixel-space square at (px,py) of side `s`. + // Pixel space is top-left origin, y down; converted to NDC here. + private void AddQuad(int px, int py, int s, int fbWidth, int fbHeight) + { + float x0 = px / (float)fbWidth * 2f - 1f; + float x1 = (px + s) / (float)fbWidth * 2f - 1f; + float y0 = 1f - py / (float)fbHeight * 2f; + float y1 = 1f - (py + s) / (float)fbHeight * 2f; + + this._verts.AddRange(new[] + { + x0, y0, x1, y0, x1, y1, + x0, y0, x1, y1, x0, y1, + }); + } + + public void Dispose() + { + GL.DeleteBuffer(this._vbo); + GL.DeleteVertexArray(this._vao); + this._shader.Dispose(); + } + + // 5x7 font. Each glyph is 7 rows of 5 columns; non-space = lit pixel. + private static readonly Dictionary Glyphs = new() + { + ['A'] = new[] { " ### ", "# #", "# #", "#####", "# #", "# #", "# #" }, + ['B'] = new[] { "#### ", "# #", "# #", "#### ", "# #", "# #", "#### " }, + ['C'] = new[] { " ####", "# ", "# ", "# ", "# ", "# ", " ####" }, + ['D'] = new[] { "#### ", "# #", "# #", "# #", "# #", "# #", "#### " }, + ['E'] = new[] { "#####", "# ", "# ", "#### ", "# ", "# ", "#####" }, + ['F'] = new[] { "#####", "# ", "# ", "#### ", "# ", "# ", "# " }, + ['G'] = new[] { " ####", "# ", "# ", "# ##", "# #", "# #", " ####" }, + ['H'] = new[] { "# #", "# #", "# #", "#####", "# #", "# #", "# #" }, + ['I'] = new[] { "#####", " # ", " # ", " # ", " # ", " # ", "#####" }, + ['J'] = new[] { "#####", " # ", " # ", " # ", "# # ", "# # ", " ## " }, + ['K'] = new[] { "# #", "# # ", "# # ", "## ", "# # ", "# # ", "# #" }, + ['L'] = new[] { "# ", "# ", "# ", "# ", "# ", "# ", "#####" }, + ['M'] = new[] { "# #", "## ##", "# # #", "# #", "# #", "# #", "# #" }, + ['N'] = new[] { "# #", "## #", "# # #", "# ##", "# #", "# #", "# #" }, + ['O'] = new[] { " ### ", "# #", "# #", "# #", "# #", "# #", " ### " }, + ['P'] = new[] { "#### ", "# #", "# #", "#### ", "# ", "# ", "# " }, + ['Q'] = new[] { " ### ", "# #", "# #", "# #", "# # #", "# # ", " ## #" }, + ['R'] = new[] { "#### ", "# #", "# #", "#### ", "# # ", "# # ", "# #" }, + ['S'] = new[] { " ####", "# ", "# ", " ### ", " #", " #", "#### " }, + ['T'] = new[] { "#####", " # ", " # ", " # ", " # ", " # ", " # " }, + ['U'] = new[] { "# #", "# #", "# #", "# #", "# #", "# #", " ### " }, + ['V'] = new[] { "# #", "# #", "# #", "# #", "# #", " # # ", " # " }, + ['W'] = new[] { "# #", "# #", "# #", "# # #", "# # #", "## ##", "# #" }, + ['X'] = new[] { "# #", "# #", " # # ", " # ", " # # ", "# #", "# #" }, + ['Y'] = new[] { "# #", "# #", " # # ", " # ", " # ", " # ", " # " }, + ['Z'] = new[] { "#####", " #", " # ", " # ", " # ", "# ", "#####" }, + ['0'] = new[] { " ### ", "# #", "# ##", "# # #", "## #", "# #", " ### " }, + ['1'] = new[] { " # ", " ## ", " # ", " # ", " # ", " # ", "#####" }, + ['2'] = new[] { " ### ", "# #", " #", " # ", " # ", " # ", "#####" }, + ['3'] = new[] { "#####", " # ", " # ", " # ", " #", "# #", " ### " }, + ['4'] = new[] { " # ", " ## ", " # # ", "# # ", "#####", " # ", " # " }, + ['5'] = new[] { "#####", "# ", "#### ", " #", " #", "# #", " ### " }, + ['6'] = new[] { " ### ", "# ", "# ", "#### ", "# #", "# #", " ### " }, + ['7'] = new[] { "#####", " #", " # ", " # ", " # ", " # ", " # " }, + ['8'] = new[] { " ### ", "# #", "# #", " ### ", "# #", "# #", " ### " }, + ['9'] = new[] { " ### ", "# #", "# #", " ####", " #", " #", " ### " }, + [' '] = new[] { " ", " ", " ", " ", " ", " ", " " }, + [':'] = new[] { " ", " # ", " # ", " ", " # ", " # ", " " }, + [','] = new[] { " ", " ", " ", " ", " # ", " # ", " # " }, + ['-'] = new[] { " ", " ", " ", "#####", " ", " ", " " }, + ['.'] = new[] { " ", " ", " ", " ", " ", " ## ", " ## " }, + ['/'] = new[] { " #", " #", " # ", " # ", " # ", "# ", "# " }, + }; +} diff --git a/Rendering/Shader.cs b/Rendering/Shader.cs index c4ba827..070037b 100644 --- a/Rendering/Shader.cs +++ b/Rendering/Shader.cs @@ -10,8 +10,8 @@ public sealed class Shader : IDisposable public Shader(string vertexPath, string fragmentPath) { - int vertex = Shader.CompileFromFile(ShaderType.VertexShader, vertexPath); - int fragment = Shader.CompileFromFile(ShaderType.FragmentShader, fragmentPath); + int vertex = CompileFromFile(ShaderType.VertexShader, vertexPath); + int fragment = CompileFromFile(ShaderType.FragmentShader, fragmentPath); this._handle = GL.CreateProgram(); GL.AttachShader(this._handle, vertex); @@ -42,6 +42,9 @@ public sealed class Shader : IDisposable public void SetVector3(string name, Vector3 value) => GL.Uniform3(this.Location(name), value.X, value.Y, value.Z); + public void SetVector2(string name, Vector2 value) => + GL.Uniform2(this.Location(name), value.X, value.Y); + private int Location(string name) { if (this._uniforms.TryGetValue(name, out int location)) return location;