feat(ui): debug overlay + refined input controls
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
This commit is contained in:
parent
6b4e7569a3
commit
ff325abd7b
20 changed files with 2734 additions and 268 deletions
180
src/AcDream.App/Rendering/BitmapFont.cs
Normal file
180
src/AcDream.App/Rendering/BitmapFont.cs
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Silk.NET.OpenGL;
|
||||
using StbTrueTypeSharp;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// A pixel-font atlas rasterized from a TTF at load time using stb_truetype.
|
||||
/// Glyphs are packed into a single-channel (R8) GL texture. Call
|
||||
/// <see cref="TryGetGlyph"/> to resolve an ASCII codepoint to UV + metrics.
|
||||
///
|
||||
/// Only printable ASCII (32..127) is supported for the debug overlay.
|
||||
/// </summary>
|
||||
public sealed unsafe class BitmapFont : IDisposable
|
||||
{
|
||||
public readonly struct Glyph
|
||||
{
|
||||
public readonly float UvMinX;
|
||||
public readonly float UvMinY;
|
||||
public readonly float UvMaxX;
|
||||
public readonly float UvMaxY;
|
||||
public readonly float OffsetX; // from cursor to glyph quad top-left
|
||||
public readonly float OffsetY;
|
||||
public readonly float Width; // pixels
|
||||
public readonly float Height;
|
||||
public readonly float Advance;
|
||||
|
||||
public Glyph(float umn, float vmn, float umx, float vmx,
|
||||
float ox, float oy, float w, float h, float adv)
|
||||
{
|
||||
UvMinX = umn; UvMinY = vmn; UvMaxX = umx; UvMaxY = vmx;
|
||||
OffsetX = ox; OffsetY = oy; Width = w; Height = h; Advance = adv;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly GL _gl;
|
||||
private readonly Glyph[] _glyphs;
|
||||
private readonly int _firstChar;
|
||||
private readonly int _numChars;
|
||||
|
||||
public uint TextureId { get; }
|
||||
public float PixelHeight { get; }
|
||||
public float LineHeight { get; }
|
||||
public float Ascent { get; }
|
||||
public int AtlasWidth { get; }
|
||||
public int AtlasHeight { get; }
|
||||
|
||||
public BitmapFont(GL gl, byte[] ttfBytes, float pixelHeight,
|
||||
int atlasSize = 512, int firstChar = 32, int numChars = 96)
|
||||
{
|
||||
_gl = gl;
|
||||
PixelHeight = pixelHeight;
|
||||
AtlasWidth = atlasSize;
|
||||
AtlasHeight = atlasSize;
|
||||
_firstChar = firstChar;
|
||||
_numChars = numChars;
|
||||
|
||||
// Bake the glyph bitmap via stbtt_BakeFontBitmap.
|
||||
var bakedChars = new StbTrueType.stbtt_bakedchar[numChars];
|
||||
var pixels = new byte[AtlasWidth * AtlasHeight];
|
||||
bool ok = StbTrueType.stbtt_BakeFontBitmap(
|
||||
ttfBytes, 0, pixelHeight,
|
||||
pixels, AtlasWidth, AtlasHeight,
|
||||
firstChar, numChars, bakedChars);
|
||||
if (!ok)
|
||||
throw new InvalidOperationException(
|
||||
$"stbtt_BakeFontBitmap failed: atlas {atlasSize}x{atlasSize} " +
|
||||
$"too small for pixelHeight={pixelHeight}");
|
||||
|
||||
// Extract vertical metrics for line spacing.
|
||||
using var info = StbTrueType.CreateFont(ttfBytes, 0)
|
||||
?? throw new InvalidOperationException("stbtt_InitFont failed");
|
||||
float scale = StbTrueType.stbtt_ScaleForPixelHeight(info, pixelHeight);
|
||||
int ascent, descent, lineGap;
|
||||
StbTrueType.stbtt_GetFontVMetrics(info, &ascent, &descent, &lineGap);
|
||||
Ascent = ascent * scale;
|
||||
LineHeight = (ascent - descent + lineGap) * scale;
|
||||
|
||||
// Convert baked-char records to our Glyph struct.
|
||||
_glyphs = new Glyph[numChars];
|
||||
for (int i = 0; i < numChars; i++)
|
||||
{
|
||||
var bc = bakedChars[i];
|
||||
float w = bc.x1 - bc.x0;
|
||||
float h = bc.y1 - bc.y0;
|
||||
_glyphs[i] = new Glyph(
|
||||
umn: bc.x0 / (float)AtlasWidth,
|
||||
vmn: bc.y0 / (float)AtlasHeight,
|
||||
umx: bc.x1 / (float)AtlasWidth,
|
||||
vmx: bc.y1 / (float)AtlasHeight,
|
||||
ox: bc.xoff,
|
||||
oy: bc.yoff,
|
||||
w: w, h: h,
|
||||
adv: bc.xadvance);
|
||||
}
|
||||
|
||||
// Upload atlas as a single-channel GL texture (R8).
|
||||
TextureId = _gl.GenTexture();
|
||||
_gl.BindTexture(TextureTarget.Texture2D, TextureId);
|
||||
_gl.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
|
||||
fixed (byte* ptr = pixels)
|
||||
{
|
||||
_gl.TexImage2D(TextureTarget.Texture2D, 0,
|
||||
(int)InternalFormat.R8,
|
||||
(uint)AtlasWidth, (uint)AtlasHeight, 0,
|
||||
PixelFormat.Red, PixelType.UnsignedByte, ptr);
|
||||
}
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter,
|
||||
(int)TextureMinFilter.Linear);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter,
|
||||
(int)TextureMagFilter.Linear);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS,
|
||||
(int)TextureWrapMode.ClampToEdge);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT,
|
||||
(int)TextureWrapMode.ClampToEdge);
|
||||
_gl.PixelStore(PixelStoreParameter.UnpackAlignment, 4); // restore default
|
||||
_gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
}
|
||||
|
||||
public bool TryGetGlyph(char c, out Glyph g)
|
||||
{
|
||||
int idx = c - _firstChar;
|
||||
if ((uint)idx >= (uint)_numChars)
|
||||
{
|
||||
g = default;
|
||||
return false;
|
||||
}
|
||||
g = _glyphs[idx];
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Measure the pixel width of a single-line string in this font.</summary>
|
||||
public float MeasureWidth(string s)
|
||||
{
|
||||
float w = 0;
|
||||
for (int i = 0; i < s.Length; i++)
|
||||
{
|
||||
if (TryGetGlyph(s[i], out var g))
|
||||
w += g.Advance;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_gl.DeleteTexture(TextureId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to load a monospaced system font from well-known paths on the host OS.
|
||||
/// Returns null if no candidate was found.
|
||||
/// </summary>
|
||||
public static byte[]? TryLoadSystemMonospaceFont()
|
||||
{
|
||||
string[] candidates =
|
||||
{
|
||||
@"C:\Windows\Fonts\consola.ttf",
|
||||
@"C:\Windows\Fonts\cour.ttf",
|
||||
@"C:\Windows\Fonts\arial.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
||||
"/usr/share/fonts/TTF/DejaVuSansMono.ttf",
|
||||
"/Library/Fonts/Menlo.ttc",
|
||||
"/System/Library/Fonts/Menlo.ttc",
|
||||
};
|
||||
foreach (var path in candidates)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(path))
|
||||
return File.ReadAllBytes(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// try next candidate
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue