acdream/src/AcDream.App/Rendering/DebugLineRenderer.cs
Erik ff325abd7b 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
2026-04-17 18:45:38 +02:00

172 lines
5.9 KiB
C#

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();
}
}