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.
165 lines
8.2 KiB
C#
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[] { " #", " #", " # ", " # ", " # ", "# ", "# " },
|
|
};
|
|
}
|