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
172 lines
5.9 KiB
C#
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();
|
|
}
|
|
}
|