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: <name>' 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.
This commit is contained in:
10
Assets/shaders/debugtext.frag
Normal file
10
Assets/shaders/debugtext.frag
Normal file
@@ -0,0 +1,10 @@
|
||||
#version 330 core
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
uniform vec3 u_color;
|
||||
|
||||
void main()
|
||||
{
|
||||
FragColor = vec4(u_color, 1.0);
|
||||
}
|
||||
12
Assets/shaders/debugtext.vert
Normal file
12
Assets/shaders/debugtext.vert
Normal file
@@ -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);
|
||||
}
|
||||
17
Core/Game.cs
17
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();
|
||||
|
||||
164
Rendering/DebugOverlay.cs
Normal file
164
Rendering/DebugOverlay.cs
Normal file
@@ -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<float> _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<string> 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<char, string[]> 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[] { " #", " #", " # ", " # ", " # ", "# ", "# " },
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user