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
|
|
@ -15,6 +15,7 @@
|
|||
<PackageReference Include="Silk.NET.Input" Version="2.23.0" />
|
||||
<PackageReference Include="Serilog" Version="4.0.2" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="StbTrueTypeSharp" Version="1.26.12" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AcDream.Core\AcDream.Core.csproj" />
|
||||
|
|
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -14,17 +14,34 @@ public sealed class ChaseCamera : ICamera
|
|||
public float Aspect { get; set; } = 16f / 9f;
|
||||
public float FovY { get; set; } = MathF.PI / 3f;
|
||||
|
||||
/// <summary>Distance behind the player.</summary>
|
||||
/// <summary>Distance behind the player. Clamped to [<see cref="DistanceMin"/>, <see cref="DistanceMax"/>].</summary>
|
||||
public float Distance { get; set; } = 8f;
|
||||
public const float DistanceMin = 2f;
|
||||
public const float DistanceMax = 40f;
|
||||
|
||||
/// <summary>Camera pitch above horizontal (radians). Positive = look down.</summary>
|
||||
public float Pitch { get; set; } = 0.35f; // ~20 degrees
|
||||
|
||||
/// <summary>
|
||||
/// Additional yaw applied on top of the player's heading when positioning
|
||||
/// the camera. Used by the hold-RMB "inspect" mode to orbit around the
|
||||
/// player without rotating the character. Snap to 0 to return the camera
|
||||
/// to directly behind the player.
|
||||
/// </summary>
|
||||
public float YawOffset { get; set; } = 0f;
|
||||
|
||||
/// <summary>Vertical offset from the player's feet to the look-at point (eye height).</summary>
|
||||
public float EyeHeight { get; set; } = 1.5f;
|
||||
|
||||
private const float PitchMin = 0.05f;
|
||||
private const float PitchMax = 1.4f; // ~80 degrees
|
||||
// Pitch range: negative values place the camera below the player's Z
|
||||
// (at distance * sin(Pitch)) so the player can be viewed from a low
|
||||
// angle. Clamped to -0.7 to avoid pushing the camera deep underground;
|
||||
// at -0.7 and Distance=8 the camera is ~5m below player-Z which will
|
||||
// clip terrain on hills but is OK on flat ground. 1.4 ≈ looking
|
||||
// straight down. Wider than the old [0.05, 1.4] so mouse-Y moves the
|
||||
// camera in both directions from the neutral [~20°] default.
|
||||
private const float PitchMin = -0.7f;
|
||||
private const float PitchMax = 1.4f;
|
||||
|
||||
private float _playerYaw;
|
||||
private Vector3 _lookAt;
|
||||
|
|
@ -43,9 +60,11 @@ public sealed class ChaseCamera : ICamera
|
|||
_playerYaw = playerYaw;
|
||||
_lookAt = playerPosition + new Vector3(0f, 0f, EyeHeight);
|
||||
|
||||
// Camera offset: behind the player (-forward direction) and above.
|
||||
float forwardX = MathF.Cos(playerYaw);
|
||||
float forwardY = MathF.Sin(playerYaw);
|
||||
// Camera offset: behind the player (-forward direction) plus any
|
||||
// YawOffset for the hold-RMB inspect orbit mode.
|
||||
float effectiveYaw = playerYaw + YawOffset;
|
||||
float forwardX = MathF.Cos(effectiveYaw);
|
||||
float forwardY = MathF.Sin(effectiveYaw);
|
||||
|
||||
float horizontalDist = Distance * MathF.Cos(Pitch);
|
||||
float verticalDist = Distance * MathF.Sin(Pitch);
|
||||
|
|
@ -63,4 +82,12 @@ public sealed class ChaseCamera : ICamera
|
|||
{
|
||||
Pitch = Math.Clamp(Pitch + delta, PitchMin, PitchMax);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adjust distance (zoom) by a delta, clamped to [DistanceMin, DistanceMax].
|
||||
/// </summary>
|
||||
public void AdjustDistance(float delta)
|
||||
{
|
||||
Distance = Math.Clamp(Distance + delta, DistanceMin, DistanceMax);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
172
src/AcDream.App/Rendering/DebugLineRenderer.cs
Normal file
172
src/AcDream.App/Rendering/DebugLineRenderer.cs
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal GL debug line renderer for visualizing collision shapes,
|
||||
/// bounding boxes, and other debug geometry. Collect lines each frame
|
||||
/// via <see cref="AddLine"/> / <see cref="AddCylinder"/>, then call
|
||||
/// <see cref="Flush"/> to upload + draw them.
|
||||
///
|
||||
/// Uses a single shared VBO that's respecialized each frame. Vertex
|
||||
/// format is (vec3 pos, vec3 color) = 24 bytes per vertex.
|
||||
/// </summary>
|
||||
public sealed unsafe class DebugLineRenderer : IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly Shader _shader;
|
||||
private readonly uint _vao;
|
||||
private readonly uint _vbo;
|
||||
|
||||
private readonly List<float> _buffer = new(4096);
|
||||
private int _vertexCount;
|
||||
private int _capacityBytes;
|
||||
|
||||
public DebugLineRenderer(GL gl, string shaderDir)
|
||||
{
|
||||
_gl = gl;
|
||||
_shader = new Shader(gl,
|
||||
Path.Combine(shaderDir, "debug_line.vert"),
|
||||
Path.Combine(shaderDir, "debug_line.frag"));
|
||||
|
||||
_vao = _gl.GenVertexArray();
|
||||
_vbo = _gl.GenBuffer();
|
||||
|
||||
_gl.BindVertexArray(_vao);
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
||||
|
||||
// 24-byte stride: vec3 pos + vec3 color
|
||||
_gl.EnableVertexAttribArray(0);
|
||||
_gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), (void*)0);
|
||||
_gl.EnableVertexAttribArray(1);
|
||||
_gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), (void*)(3 * sizeof(float)));
|
||||
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
||||
_gl.BindVertexArray(0);
|
||||
}
|
||||
|
||||
/// <summary>Clear accumulated lines. Call at the start of each frame.</summary>
|
||||
public void Begin()
|
||||
{
|
||||
_buffer.Clear();
|
||||
_vertexCount = 0;
|
||||
}
|
||||
|
||||
public void AddLine(Vector3 a, Vector3 b, Vector3 color)
|
||||
{
|
||||
_buffer.Add(a.X); _buffer.Add(a.Y); _buffer.Add(a.Z);
|
||||
_buffer.Add(color.X); _buffer.Add(color.Y); _buffer.Add(color.Z);
|
||||
_buffer.Add(b.X); _buffer.Add(b.Y); _buffer.Add(b.Z);
|
||||
_buffer.Add(color.X); _buffer.Add(color.Y); _buffer.Add(color.Z);
|
||||
_vertexCount += 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw a cylinder as 2 polygon rings (base + top) connected by 4
|
||||
/// vertical line segments at 0/90/180/270 degrees.
|
||||
/// </summary>
|
||||
public void AddCylinder(Vector3 basePos, float radius, float height, Vector3 color)
|
||||
{
|
||||
const int segments = 16;
|
||||
Vector3 top = basePos + new Vector3(0, 0, height);
|
||||
|
||||
// Ring vertices
|
||||
var baseRing = new Vector3[segments];
|
||||
var topRing = new Vector3[segments];
|
||||
for (int i = 0; i < segments; i++)
|
||||
{
|
||||
float theta = i * (MathF.PI * 2f / segments);
|
||||
float cx = MathF.Cos(theta) * radius;
|
||||
float cy = MathF.Sin(theta) * radius;
|
||||
baseRing[i] = new Vector3(basePos.X + cx, basePos.Y + cy, basePos.Z);
|
||||
topRing[i] = new Vector3(top.X + cx, top.Y + cy, top.Z);
|
||||
}
|
||||
|
||||
// Base ring
|
||||
for (int i = 0; i < segments; i++)
|
||||
AddLine(baseRing[i], baseRing[(i + 1) % segments], color);
|
||||
// Top ring
|
||||
for (int i = 0; i < segments; i++)
|
||||
AddLine(topRing[i], topRing[(i + 1) % segments], color);
|
||||
// 4 vertical connectors
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
int idx = i * (segments / 4);
|
||||
AddLine(baseRing[idx], topRing[idx], color);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw an axis-aligned box as 12 edges.
|
||||
/// </summary>
|
||||
public void AddBox(Vector3 min, Vector3 max, Vector3 color)
|
||||
{
|
||||
Vector3[] c =
|
||||
{
|
||||
new(min.X, min.Y, min.Z),
|
||||
new(max.X, min.Y, min.Z),
|
||||
new(max.X, max.Y, min.Z),
|
||||
new(min.X, max.Y, min.Z),
|
||||
new(min.X, min.Y, max.Z),
|
||||
new(max.X, min.Y, max.Z),
|
||||
new(max.X, max.Y, max.Z),
|
||||
new(min.X, max.Y, max.Z),
|
||||
};
|
||||
// Bottom
|
||||
AddLine(c[0], c[1], color); AddLine(c[1], c[2], color);
|
||||
AddLine(c[2], c[3], color); AddLine(c[3], c[0], color);
|
||||
// Top
|
||||
AddLine(c[4], c[5], color); AddLine(c[5], c[6], color);
|
||||
AddLine(c[6], c[7], color); AddLine(c[7], c[4], color);
|
||||
// Verticals
|
||||
AddLine(c[0], c[4], color); AddLine(c[1], c[5], color);
|
||||
AddLine(c[2], c[6], color); AddLine(c[3], c[7], color);
|
||||
}
|
||||
|
||||
/// <summary>Upload + draw all accumulated lines.</summary>
|
||||
public void Flush(Matrix4x4 view, Matrix4x4 projection)
|
||||
{
|
||||
if (_vertexCount == 0) return;
|
||||
|
||||
_shader.Use();
|
||||
_shader.SetMatrix4("uView", view);
|
||||
_shader.SetMatrix4("uProjection", projection);
|
||||
|
||||
_gl.BindVertexArray(_vao);
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
||||
|
||||
int neededBytes = _buffer.Count * sizeof(float);
|
||||
if (neededBytes > _capacityBytes)
|
||||
{
|
||||
fixed (float* ptr = CollectionsMarshal.AsSpan(_buffer))
|
||||
_gl.BufferData(BufferTargetARB.ArrayBuffer, (nuint)neededBytes, ptr, BufferUsageARB.DynamicDraw);
|
||||
_capacityBytes = neededBytes;
|
||||
}
|
||||
else
|
||||
{
|
||||
fixed (float* ptr = CollectionsMarshal.AsSpan(_buffer))
|
||||
_gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, (nuint)neededBytes, ptr);
|
||||
}
|
||||
|
||||
// Depth test on so lines get occluded by geometry (but we want them
|
||||
// visible through geometry — disable depth test so everything shows).
|
||||
bool wasDepthEnabled = _gl.IsEnabled(EnableCap.DepthTest);
|
||||
_gl.Disable(EnableCap.DepthTest);
|
||||
|
||||
_gl.DrawArrays(PrimitiveType.Lines, 0, (uint)_vertexCount);
|
||||
|
||||
if (wasDepthEnabled) _gl.Enable(EnableCap.DepthTest);
|
||||
|
||||
_gl.BindVertexArray(0);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_gl.DeleteVertexArray(_vao);
|
||||
_gl.DeleteBuffer(_vbo);
|
||||
_shader.Dispose();
|
||||
}
|
||||
}
|
||||
330
src/AcDream.App/Rendering/DebugOverlay.cs
Normal file
330
src/AcDream.App/Rendering/DebugOverlay.cs
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Screen-space debug HUD. Composes panels on top of the 3D scene using a
|
||||
/// <see cref="TextRenderer"/> + <see cref="BitmapFont"/>. Panels can be
|
||||
/// toggled independently (info / stats / controls-help / compass).
|
||||
///
|
||||
/// The overlay is stateless w.r.t. game state — callers populate a
|
||||
/// <see cref="Snapshot"/> each frame and pass it to <see cref="Draw"/>.
|
||||
/// </summary>
|
||||
public sealed class DebugOverlay
|
||||
{
|
||||
private readonly TextRenderer _text;
|
||||
private readonly BitmapFont _font;
|
||||
|
||||
public bool ShowInfoPanel { get; set; } = true;
|
||||
public bool ShowStatsPanel { get; set; } = true;
|
||||
public bool ShowHelpPanel { get; set; } = false;
|
||||
public bool ShowCompass { get; set; } = true;
|
||||
|
||||
// Toast state for transient notifications (e.g. "wireframes off").
|
||||
private string? _toastText;
|
||||
private Vector4 _toastColor = White;
|
||||
private float _toastTimeLeft;
|
||||
|
||||
private static readonly Vector4 White = new(1f, 1f, 1f, 1f);
|
||||
private static readonly Vector4 Green = new(0.4f, 0.95f, 0.4f, 1f);
|
||||
private static readonly Vector4 Yellow = new(1f, 0.9f, 0.3f, 1f);
|
||||
private static readonly Vector4 Red = new(1f, 0.4f, 0.35f, 1f);
|
||||
private static readonly Vector4 Cyan = new(0.4f, 0.85f, 1f, 1f);
|
||||
private static readonly Vector4 Grey = new(0.7f, 0.7f, 0.75f, 1f);
|
||||
private static readonly Vector4 PanelBg = new(0f, 0f, 0f, 0.55f);
|
||||
private static readonly Vector4 PanelBorder = new(0.15f, 0.15f, 0.2f, 0.8f);
|
||||
|
||||
/// <summary>Per-frame state snapshot from the caller. See <see cref="Draw"/>.</summary>
|
||||
public readonly record struct Snapshot(
|
||||
float Fps,
|
||||
float FrameTimeMs,
|
||||
Vector3 PlayerPos,
|
||||
float HeadingDeg,
|
||||
uint CellId,
|
||||
bool OnGround,
|
||||
bool InPlayerMode,
|
||||
bool InFlyMode,
|
||||
float VerticalVelocity,
|
||||
int EntityCount,
|
||||
int AnimatedCount,
|
||||
int LandblocksVisible,
|
||||
int LandblocksTotal,
|
||||
int ShadowObjectCount,
|
||||
float NearestObjDist,
|
||||
string NearestObjLabel,
|
||||
bool Colliding,
|
||||
bool DebugWireframes,
|
||||
int StreamingRadius,
|
||||
float MouseSensitivity,
|
||||
float ChaseDistance,
|
||||
bool RmbOrbit);
|
||||
|
||||
public DebugOverlay(TextRenderer text, BitmapFont font)
|
||||
{
|
||||
_text = text;
|
||||
_font = font;
|
||||
}
|
||||
|
||||
/// <summary>Show a short message in the center-top for <paramref name="durationSec"/> seconds.</summary>
|
||||
public void Toast(string message, float durationSec = 1.5f, Vector4? color = null)
|
||||
{
|
||||
_toastText = message;
|
||||
_toastColor = color ?? Yellow;
|
||||
_toastTimeLeft = durationSec;
|
||||
}
|
||||
|
||||
/// <summary>Advance toast timer. Call once per frame with dt in seconds.</summary>
|
||||
public void Update(float dt)
|
||||
{
|
||||
if (_toastTimeLeft > 0f)
|
||||
{
|
||||
_toastTimeLeft -= dt;
|
||||
if (_toastTimeLeft <= 0f)
|
||||
_toastText = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Draw(Snapshot s, Vector2 screenSize)
|
||||
{
|
||||
_text.Begin(screenSize);
|
||||
|
||||
if (ShowInfoPanel) DrawInfoPanel(s);
|
||||
if (ShowStatsPanel) DrawStatsPanel(s, screenSize);
|
||||
if (ShowCompass) DrawCompass(s, screenSize);
|
||||
if (ShowHelpPanel) DrawHelpPanel(screenSize);
|
||||
DrawHintBar(screenSize);
|
||||
DrawToast(screenSize);
|
||||
|
||||
_text.Flush(_font);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Info panel — top-left: mode, position, heading, ground, nearest-obj.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawInfoPanel(Snapshot s)
|
||||
{
|
||||
var lines = new List<(string text, Vector4 color)>();
|
||||
|
||||
string modeLabel = s.InPlayerMode ? "PLAYER" : (s.InFlyMode ? "FLY " : "ORBIT ");
|
||||
var modeColor = s.InPlayerMode ? Yellow : (s.InFlyMode ? Cyan : Grey);
|
||||
lines.Add(($"[{modeLabel}] wireframes {(s.DebugWireframes ? "ON " : "OFF")}",
|
||||
modeColor));
|
||||
|
||||
lines.Add(($"Pos {s.PlayerPos.X,8:F1} {s.PlayerPos.Y,8:F1} {s.PlayerPos.Z,8:F2}", White));
|
||||
lines.Add(($"Head {s.HeadingDeg,5:F0} deg Cell 0x{s.CellId:X8}", White));
|
||||
|
||||
var gColor = s.OnGround ? Green : Yellow;
|
||||
string vzStr = s.VerticalVelocity >= 0
|
||||
? $"+{s.VerticalVelocity,4:F2}"
|
||||
: $"{s.VerticalVelocity,5:F2}";
|
||||
lines.Add(($"Grnd {(s.OnGround ? "yes" : "NO ")} vZ {vzStr}", gColor));
|
||||
|
||||
var nColor = s.Colliding ? Red : White;
|
||||
string nearDist = float.IsPositiveInfinity(s.NearestObjDist) ? " --- " : $"{s.NearestObjDist,4:F1}m";
|
||||
lines.Add(($"Near {nearDist} {s.NearestObjLabel}", nColor));
|
||||
|
||||
var cColor = s.Colliding ? Red : Green;
|
||||
lines.Add(($"Coll {(s.Colliding ? "BLOCKED" : "free ")}", cColor));
|
||||
|
||||
if (s.InPlayerMode)
|
||||
{
|
||||
string orbitTag = s.RmbOrbit ? " [RMB orbit]" : "";
|
||||
lines.Add(($"Cam dist {s.ChaseDistance,4:F1}m{orbitTag}",
|
||||
s.RmbOrbit ? Cyan : White));
|
||||
}
|
||||
lines.Add(($"Sens {s.MouseSensitivity:F3}x (F8 slower / F9 faster)", Grey));
|
||||
|
||||
DrawPanel(10f, 10f, lines);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Stats panel — top-right: fps, frame time, landblock/entity counters.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawStatsPanel(Snapshot s, Vector2 screenSize)
|
||||
{
|
||||
var lines = new List<(string text, Vector4 color)>
|
||||
{
|
||||
($"{s.Fps,5:F0} fps {s.FrameTimeMs,5:F1} ms", Green),
|
||||
($"lb {s.LandblocksVisible,3}/{s.LandblocksTotal,3} visible", White),
|
||||
($"ent {s.EntityCount,4} anim {s.AnimatedCount,3}", White),
|
||||
($"coll {s.ShadowObjectCount,5} radius {s.StreamingRadius}", White),
|
||||
};
|
||||
|
||||
float pad = 10f;
|
||||
float panelW = MeasureMax(lines) + 2 * InnerPad;
|
||||
DrawPanel(screenSize.X - panelW - pad, pad, lines, panelW);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Compass — bottom-center: arrow indicating heading, cardinal labels.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawCompass(Snapshot s, Vector2 screenSize)
|
||||
{
|
||||
// Simple linear compass strip across 180° of horizon at the top-center.
|
||||
const float stripW = 360f;
|
||||
const float stripH = 20f;
|
||||
float cx = screenSize.X * 0.5f;
|
||||
float top = 8f;
|
||||
float left = cx - stripW * 0.5f;
|
||||
|
||||
_text.DrawRect(left, top, stripW, stripH, PanelBg);
|
||||
_text.DrawRectOutline(left, top, stripW, stripH, PanelBorder);
|
||||
|
||||
// Mark every 30° of yaw, labelled with cardinal letters at N/E/S/W.
|
||||
// Heading 0 = +X (east). We show 180° wide, ±90° from current heading.
|
||||
float h = NormalizeDeg(s.HeadingDeg);
|
||||
for (int d = 0; d < 360; d += 15)
|
||||
{
|
||||
float rel = NormalizeDegSigned(d - h);
|
||||
if (rel < -90 || rel > 90) continue;
|
||||
float x = cx + rel / 90f * (stripW * 0.5f - 6f);
|
||||
bool bold = (d % 90) == 0;
|
||||
float tickH = bold ? stripH * 0.9f : stripH * 0.4f;
|
||||
_text.DrawRect(x - 0.5f, top + stripH - tickH, 1f, tickH, bold ? White : Grey);
|
||||
|
||||
if (bold)
|
||||
{
|
||||
string lab = d switch
|
||||
{
|
||||
0 => "E",
|
||||
90 => "N",
|
||||
180 => "W",
|
||||
270 => "S",
|
||||
_ => ""
|
||||
};
|
||||
if (lab.Length > 0)
|
||||
{
|
||||
float w = _font.MeasureWidth(lab);
|
||||
_text.DrawString(_font, lab, x - w * 0.5f, top - _font.LineHeight + 4f, White);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Current heading indicator arrow below the strip.
|
||||
_text.DrawRect(cx - 1.5f, top + stripH + 2f, 3f, 6f, Yellow);
|
||||
string hText = $"{h,3:F0}°";
|
||||
float hw = _font.MeasureWidth(hText);
|
||||
_text.DrawString(_font, hText, cx - hw * 0.5f, top + stripH + 10f, Yellow);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Help panel — center: full keybind cheat-sheet, shown when F1 is pressed.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
private static readonly (string key, string desc)[] Keybinds =
|
||||
{
|
||||
("F1", "toggle this help"),
|
||||
("F2", "toggle collision wireframes"),
|
||||
("F3", "console dump (pos + nearby objects)"),
|
||||
("F4", "toggle debug HUD info panel"),
|
||||
("F5", "toggle stats panel"),
|
||||
("F6", "toggle compass"),
|
||||
("F", "toggle fly camera"),
|
||||
("Tab", "toggle player mode (requires login)"),
|
||||
("W A S D", "move (player mode) / fly"),
|
||||
("Mouse", "turn character / look (fly)"),
|
||||
("Hold RMB", "free orbit camera around player"),
|
||||
("Wheel", "zoom chase camera in / out"),
|
||||
("F8 / F9", "mouse sensitivity slower / faster"),
|
||||
("Space", "jump (hold to charge)"),
|
||||
("Shift", "run"),
|
||||
("Escape", "exit fly / player / close window"),
|
||||
};
|
||||
|
||||
private void DrawHelpPanel(Vector2 screenSize)
|
||||
{
|
||||
var lines = new List<(string text, Vector4 color)>();
|
||||
lines.Add(("CONTROLS", Yellow));
|
||||
lines.Add(("", White));
|
||||
foreach (var (k, d) in Keybinds)
|
||||
lines.Add(($" {k,-9} {d}", White));
|
||||
lines.Add(("", White));
|
||||
lines.Add(("Press F1 to close", Grey));
|
||||
|
||||
float panelW = MeasureMax(lines) + 2 * InnerPad;
|
||||
float panelH = lines.Count * _font.LineHeight + 2 * InnerPad;
|
||||
float x = (screenSize.X - panelW) * 0.5f;
|
||||
float y = (screenSize.Y - panelH) * 0.5f;
|
||||
DrawPanel(x, y, lines, panelW);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Hint bar — bottom-left: always-visible "F1 for help" reminder.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawHintBar(Vector2 screenSize)
|
||||
{
|
||||
string hint = "F1 help F2 wireframes F3 dump F4/F5/F6 panels F8/F9 sens Tab player Hold RMB orbit Wheel zoom";
|
||||
float w = _font.MeasureWidth(hint);
|
||||
float pad = 10f;
|
||||
float y = screenSize.Y - _font.LineHeight - pad;
|
||||
_text.DrawRect(pad - 4, y - 3, w + 8, _font.LineHeight + 6, PanelBg);
|
||||
_text.DrawString(_font, hint, pad, y, Grey);
|
||||
}
|
||||
|
||||
private void DrawToast(Vector2 screenSize)
|
||||
{
|
||||
if (_toastText is null || _toastTimeLeft <= 0f) return;
|
||||
float w = _font.MeasureWidth(_toastText);
|
||||
float x = (screenSize.X - w) * 0.5f;
|
||||
float y = 60f;
|
||||
var c = _toastColor;
|
||||
float alpha = MathF.Min(1f, _toastTimeLeft / 0.5f);
|
||||
c.W *= alpha;
|
||||
var bg = PanelBg;
|
||||
bg.W *= alpha;
|
||||
_text.DrawRect(x - 10, y - 4, w + 20, _font.LineHeight + 8, bg);
|
||||
_text.DrawString(_font, _toastText, x, y, c);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Panel helpers.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
private const float InnerPad = 8f;
|
||||
|
||||
private float MeasureMax(IReadOnlyList<(string text, Vector4 color)> lines)
|
||||
{
|
||||
float m = 0;
|
||||
foreach (var (text, _) in lines)
|
||||
m = MathF.Max(m, _font.MeasureWidth(text));
|
||||
return m;
|
||||
}
|
||||
|
||||
private void DrawPanel(float x, float y, IReadOnlyList<(string text, Vector4 color)> lines, float? widthOverride = null)
|
||||
{
|
||||
float maxW = MeasureMax(lines);
|
||||
float panelW = widthOverride ?? (maxW + 2 * InnerPad);
|
||||
float panelH = lines.Count * _font.LineHeight + 2 * InnerPad;
|
||||
|
||||
_text.DrawRect(x, y, panelW, panelH, PanelBg);
|
||||
_text.DrawRectOutline(x, y, panelW, panelH, PanelBorder);
|
||||
|
||||
float cy = y + InnerPad;
|
||||
foreach (var (text, color) in lines)
|
||||
{
|
||||
_text.DrawString(_font, text, x + InnerPad, cy, color);
|
||||
cy += _font.LineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
private static float NormalizeDeg(float deg)
|
||||
{
|
||||
deg %= 360f;
|
||||
if (deg < 0) deg += 360f;
|
||||
return deg;
|
||||
}
|
||||
|
||||
private static float NormalizeDegSigned(float deg)
|
||||
{
|
||||
deg %= 360f;
|
||||
if (deg > 180) deg -= 360f;
|
||||
if (deg < -180) deg += 360f;
|
||||
return deg;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -64,5 +64,17 @@ public sealed class Shader : IDisposable
|
|||
_gl.Uniform3(loc, v.X, v.Y, v.Z);
|
||||
}
|
||||
|
||||
public void SetVec2(string name, Vector2 v)
|
||||
{
|
||||
int loc = _gl.GetUniformLocation(Program, name);
|
||||
_gl.Uniform2(loc, v.X, v.Y);
|
||||
}
|
||||
|
||||
public void SetVec4(string name, Vector4 v)
|
||||
{
|
||||
int loc = _gl.GetUniformLocation(Program, name);
|
||||
_gl.Uniform4(loc, v.X, v.Y, v.Z, v.W);
|
||||
}
|
||||
|
||||
public void Dispose() => _gl.DeleteProgram(Program);
|
||||
}
|
||||
|
|
|
|||
7
src/AcDream.App/Rendering/Shaders/debug_line.frag
Normal file
7
src/AcDream.App/Rendering/Shaders/debug_line.frag
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#version 430 core
|
||||
in vec3 vColor;
|
||||
out vec4 FragColor;
|
||||
|
||||
void main() {
|
||||
FragColor = vec4(vColor, 1.0);
|
||||
}
|
||||
13
src/AcDream.App/Rendering/Shaders/debug_line.vert
Normal file
13
src/AcDream.App/Rendering/Shaders/debug_line.vert
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#version 430 core
|
||||
layout(location = 0) in vec3 aPos;
|
||||
layout(location = 1) in vec3 aColor;
|
||||
|
||||
uniform mat4 uView;
|
||||
uniform mat4 uProjection;
|
||||
|
||||
out vec3 vColor;
|
||||
|
||||
void main() {
|
||||
vColor = aColor;
|
||||
gl_Position = uProjection * uView * vec4(aPos, 1.0);
|
||||
}
|
||||
18
src/AcDream.App/Rendering/Shaders/ui_text.frag
Normal file
18
src/AcDream.App/Rendering/Shaders/ui_text.frag
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#version 430 core
|
||||
in vec2 vUv;
|
||||
in vec4 vColor;
|
||||
out vec4 FragColor;
|
||||
|
||||
uniform sampler2D uTex;
|
||||
uniform int uUseTexture;
|
||||
|
||||
void main() {
|
||||
if (uUseTexture != 0) {
|
||||
// Font atlas is a single-channel R8 texture; red = coverage alpha.
|
||||
float coverage = texture(uTex, vUv).r;
|
||||
FragColor = vec4(vColor.rgb, vColor.a * coverage);
|
||||
} else {
|
||||
FragColor = vColor;
|
||||
}
|
||||
if (FragColor.a < 0.005) discard;
|
||||
}
|
||||
19
src/AcDream.App/Rendering/Shaders/ui_text.vert
Normal file
19
src/AcDream.App/Rendering/Shaders/ui_text.vert
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#version 430 core
|
||||
layout(location = 0) in vec2 aPos; // screen pixels, origin top-left
|
||||
layout(location = 1) in vec2 aUv;
|
||||
layout(location = 2) in vec4 aColor;
|
||||
|
||||
uniform vec2 uScreenSize;
|
||||
|
||||
out vec2 vUv;
|
||||
out vec4 vColor;
|
||||
|
||||
void main() {
|
||||
// Convert pixel coords (origin top-left, +Y down) to NDC (origin center, +Y up).
|
||||
vec2 ndc = vec2(
|
||||
aPos.x / uScreenSize.x * 2.0 - 1.0,
|
||||
1.0 - aPos.y / uScreenSize.y * 2.0);
|
||||
gl_Position = vec4(ndc, 0.0, 1.0);
|
||||
vUv = aUv;
|
||||
vColor = aColor;
|
||||
}
|
||||
230
src/AcDream.App/Rendering/TextRenderer.cs
Normal file
230
src/AcDream.App/Rendering/TextRenderer.cs
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue