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