Adds the first on-screen HUD for the dev client plus today's mouse-control refinements. Also lands yesterday's scenery-alignment changes that were left uncommitted in the working tree. Overlay: - BitmapFont rasterizes a system TTF via StbTrueTypeSharp into a 512x512 R8 atlas at startup (Consolas on Windows, DejaVu/Menlo fallbacks) - TextRenderer batches 2D quads in screen-space with ortho projection; one shader + two draw calls (rect then text) for panel backgrounds under glyphs - DebugOverlay composes info / stats / compass / help panels on top of the 3D scene; toggles via F1/F4/F5/F6; transient toasts for key events - DebugLineRenderer and its shaders (carried over from the scenery work) are properly committed in this commit Controls: - Per-mode mouse sensitivity (Chase 0.15, Fly 1.0, Orbit 1.0); F8/F9 to adjust the active mode multiplicatively (x1.2) - Hold RMB to free-orbit the chase camera around the player; release stays at the new angle (no snap-back) - Mouse-wheel zooms chase distance between 2m and 40m - Chase pitch widened to [-0.7, 1.4] so mouse-Y tilts both ways from the default neutral angle Scenery alignment (carried from yesterday's session): - ShadowObjectRegistry AllEntriesForDebug + Scale field - SceneryGenerator uses ACViewer's OnRoad polygon test + baseLoc + set_heading rotation - BSPQuery dispatchers accept localToWorld so normals/offsets transform correctly per part - TransitionTypes.CylinderCollision rewritten with wall-slide + push-out - PhysicsDataCache caches visual-mesh AABB for scenery that lacks physics Setup bounds
230 lines
7.8 KiB
C#
230 lines
7.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Numerics;
|
|
using System.Runtime.InteropServices;
|
|
using Silk.NET.OpenGL;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>
|
|
/// 2D batched quad renderer for text + solid rectangles. Coordinates are in
|
|
/// screen pixels with origin top-left, +X right, +Y down. Call
|
|
/// <see cref="Begin"/> at the start of a HUD pass, queue geometry via
|
|
/// <see cref="DrawString"/> / <see cref="DrawRect"/>, then <see cref="Flush"/>.
|
|
///
|
|
/// Uses two internal vertex buffers (text and rect) flushed in two draw calls
|
|
/// to avoid a per-vertex "use texture" flag. Rects are drawn first so text
|
|
/// sits on top of background panels.
|
|
/// </summary>
|
|
public sealed unsafe class TextRenderer : IDisposable
|
|
{
|
|
private const int FloatsPerVertex = 8; // pos(2) + uv(2) + color(4)
|
|
|
|
private readonly GL _gl;
|
|
private readonly Shader _shader;
|
|
private readonly uint _vao;
|
|
private readonly uint _vbo;
|
|
private int _vboCapacityBytes;
|
|
|
|
private readonly List<float> _textBuf = new(8192);
|
|
private readonly List<float> _rectBuf = new(1024);
|
|
private int _textVerts;
|
|
private int _rectVerts;
|
|
private Vector2 _screenSize;
|
|
|
|
public TextRenderer(GL gl, string shaderDir)
|
|
{
|
|
_gl = gl;
|
|
_shader = new Shader(gl,
|
|
Path.Combine(shaderDir, "ui_text.vert"),
|
|
Path.Combine(shaderDir, "ui_text.frag"));
|
|
|
|
_vao = _gl.GenVertexArray();
|
|
_vbo = _gl.GenBuffer();
|
|
|
|
_gl.BindVertexArray(_vao);
|
|
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
|
|
|
uint stride = FloatsPerVertex * sizeof(float);
|
|
_gl.EnableVertexAttribArray(0);
|
|
_gl.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, stride, (void*)0);
|
|
_gl.EnableVertexAttribArray(1);
|
|
_gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, stride, (void*)(2 * sizeof(float)));
|
|
_gl.EnableVertexAttribArray(2);
|
|
_gl.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, stride, (void*)(4 * sizeof(float)));
|
|
|
|
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
|
_gl.BindVertexArray(0);
|
|
}
|
|
|
|
/// <summary>Begin a HUD pass. Call once per frame before any Draw* calls.</summary>
|
|
public void Begin(Vector2 screenSize)
|
|
{
|
|
_screenSize = screenSize;
|
|
_textBuf.Clear();
|
|
_rectBuf.Clear();
|
|
_textVerts = 0;
|
|
_rectVerts = 0;
|
|
}
|
|
|
|
/// <summary>Draw a filled rectangle in screen pixel space.</summary>
|
|
public void DrawRect(float x, float y, float w, float h, Vector4 color)
|
|
{
|
|
AppendQuad(_rectBuf, x, y, w, h, 0, 0, 0, 0, color);
|
|
_rectVerts += 6;
|
|
}
|
|
|
|
/// <summary>Draw a 1-pixel-thick outline rect.</summary>
|
|
public void DrawRectOutline(float x, float y, float w, float h, Vector4 color, float thickness = 1f)
|
|
{
|
|
// top, bottom, left, right
|
|
DrawRect(x, y, w, thickness, color);
|
|
DrawRect(x, y + h - thickness, w, thickness, color);
|
|
DrawRect(x, y, thickness, h, color);
|
|
DrawRect(x + w - thickness, y, thickness, h, color);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draw a single line of text at (x,y) where (x,y) is the top-left of the
|
|
/// typographic block. Handles '\n' as a line break.
|
|
/// </summary>
|
|
public void DrawString(BitmapFont font, string text, float x, float y, Vector4 color)
|
|
{
|
|
float cursorX = x;
|
|
// The caller provides top-y; shift to baseline for glyph offset math.
|
|
float baseline = y + font.Ascent;
|
|
|
|
for (int i = 0; i < text.Length; i++)
|
|
{
|
|
char c = text[i];
|
|
if (c == '\n')
|
|
{
|
|
cursorX = x;
|
|
baseline += font.LineHeight;
|
|
continue;
|
|
}
|
|
if (!font.TryGetGlyph(c, out var g))
|
|
{
|
|
// Unknown glyph — skip its advance width if '?' exists.
|
|
if (font.TryGetGlyph('?', out var q))
|
|
cursorX += q.Advance;
|
|
continue;
|
|
}
|
|
|
|
float gx = cursorX + g.OffsetX;
|
|
float gy = baseline + g.OffsetY;
|
|
float gw = g.Width;
|
|
float gh = g.Height;
|
|
|
|
if (gw > 0 && gh > 0)
|
|
{
|
|
AppendQuad(_textBuf,
|
|
gx, gy, gw, gh,
|
|
g.UvMinX, g.UvMinY, g.UvMaxX, g.UvMaxY,
|
|
color);
|
|
_textVerts += 6;
|
|
}
|
|
cursorX += g.Advance;
|
|
}
|
|
}
|
|
|
|
private static void AppendQuad(List<float> buf,
|
|
float x, float y, float w, float h,
|
|
float u0, float v0, float u1, float v1, Vector4 color)
|
|
{
|
|
// Two triangles (6 verts). CCW in pixel space is clockwise in NDC
|
|
// because the vertex shader flips Y, so OpenGL's default front-face
|
|
// is GL_CCW — we rely on cull-face being disabled during HUD pass.
|
|
// (x, y) ─ (x+w, y)
|
|
// │ │
|
|
// (x, y+h) ─ (x+w, y+h)
|
|
//
|
|
// Triangle 1: (x,y) (x+w,y+h) (x+w,y)
|
|
// Triangle 2: (x,y) (x,y+h) (x+w,y+h)
|
|
void V(float px, float py, float pu, float pv)
|
|
{
|
|
buf.Add(px); buf.Add(py);
|
|
buf.Add(pu); buf.Add(pv);
|
|
buf.Add(color.X); buf.Add(color.Y); buf.Add(color.Z); buf.Add(color.W);
|
|
}
|
|
V(x, y, u0, v0);
|
|
V(x + w, y + h, u1, v1);
|
|
V(x + w, y, u1, v0);
|
|
V(x, y, u0, v0);
|
|
V(x, y + h, u0, v1);
|
|
V(x + w, y + h, u1, v1);
|
|
}
|
|
|
|
/// <summary>Upload + draw accumulated rects + text. font may be null if only DrawRect was used.</summary>
|
|
public void Flush(BitmapFont? font)
|
|
{
|
|
if (_textVerts == 0 && _rectVerts == 0) return;
|
|
|
|
_shader.Use();
|
|
_shader.SetVec2("uScreenSize", _screenSize);
|
|
|
|
_gl.BindVertexArray(_vao);
|
|
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
|
|
|
// Save GL state.
|
|
bool wasDepth = _gl.IsEnabled(EnableCap.DepthTest);
|
|
bool wasBlend = _gl.IsEnabled(EnableCap.Blend);
|
|
bool wasCull = _gl.IsEnabled(EnableCap.CullFace);
|
|
_gl.Disable(EnableCap.DepthTest);
|
|
_gl.Disable(EnableCap.CullFace);
|
|
_gl.Enable(EnableCap.Blend);
|
|
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
|
|
|
// Untextured rects first — they form panel backgrounds.
|
|
if (_rectVerts > 0)
|
|
{
|
|
_shader.SetInt("uUseTexture", 0);
|
|
UploadBuffer(_rectBuf);
|
|
_gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)_rectVerts);
|
|
}
|
|
|
|
// Textured text glyphs.
|
|
if (_textVerts > 0 && font is not null)
|
|
{
|
|
_shader.SetInt("uUseTexture", 1);
|
|
_gl.ActiveTexture(TextureUnit.Texture0);
|
|
_gl.BindTexture(TextureTarget.Texture2D, font.TextureId);
|
|
_shader.SetInt("uTex", 0);
|
|
UploadBuffer(_textBuf);
|
|
_gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)_textVerts);
|
|
}
|
|
|
|
// Restore GL state.
|
|
if (!wasBlend) _gl.Disable(EnableCap.Blend);
|
|
if (wasCull) _gl.Enable(EnableCap.CullFace);
|
|
if (wasDepth) _gl.Enable(EnableCap.DepthTest);
|
|
|
|
_gl.BindVertexArray(0);
|
|
}
|
|
|
|
private void UploadBuffer(List<float> buf)
|
|
{
|
|
int bytes = buf.Count * sizeof(float);
|
|
if (bytes == 0) return;
|
|
|
|
if (bytes > _vboCapacityBytes)
|
|
{
|
|
fixed (float* p = CollectionsMarshal.AsSpan(buf))
|
|
_gl.BufferData(BufferTargetARB.ArrayBuffer, (nuint)bytes, p, BufferUsageARB.DynamicDraw);
|
|
_vboCapacityBytes = bytes;
|
|
}
|
|
else
|
|
{
|
|
fixed (float* p = CollectionsMarshal.AsSpan(buf))
|
|
_gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, (nuint)bytes, p);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_gl.DeleteBuffer(_vbo);
|
|
_gl.DeleteVertexArray(_vao);
|
|
_shader.Dispose();
|
|
}
|
|
}
|