Files
VoxelEngine/Rendering/DebugOverlay.cs
MDietermann 689536348b 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.
2026-06-03 16:27:15 +02:00

165 lines
8.2 KiB
C#

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[] { " #", " #", " # ", " # ", " # ", "# ", "# " },
};
}