acdream/src/AcDream.App/Rendering/DebugLineRenderer.cs
Erik 6a2fe98cc4 refactor(app): compose world rendering startup
Move Region/environment, mandatory modern rendering, terrain, WB, texture, and sampler construction behind the typed Phase-4 composition boundary. Give every fallible GL constructor prefix retryable ownership so partial startup failure cannot leak or replay resource deletion while preserving the accepted render path and DAT inputs.

Co-authored-by: Codex <codex@openai.com>
2026-07-22 16:46:05 +02:00

213 lines
7.5 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 ResourceCleanupGroup _resources;
private readonly List<float> _buffer = new(4096);
private int _vertexCount;
private int _capacityBytes;
public DebugLineRenderer(GL gl, string shaderDir)
{
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
ArgumentException.ThrowIfNullOrWhiteSpace(shaderDir);
var resources = new ResourceCleanupGroup();
Shader? shader = null;
uint vao = 0;
uint vbo = 0;
try
{
shader = new Shader(gl,
Path.Combine(shaderDir, "debug_line.vert"),
Path.Combine(shaderDir, "debug_line.frag"));
resources.Add("debug-line shader", shader.Dispose);
vao = GlResourceCommand.CreateName(
gl,
"debug-line VAO",
gl.GenVertexArray,
gl.DeleteVertexArray);
uint ownedVao = vao;
resources.Add(
"debug-line VAO",
() => GlResourceCommand.DeleteVertexArray(
gl,
ownedVao,
$"delete debug-line VAO {ownedVao}"));
vbo = GlResourceCommand.CreateName(
gl,
"debug-line VBO",
gl.GenBuffer,
gl.DeleteBuffer);
uint ownedVbo = vbo;
resources.Add(
"debug-line VBO",
() => GlResourceCommand.DeleteBuffer(
gl,
ownedVbo,
$"delete debug-line VBO {ownedVbo}"));
GlResourceCommand.Execute(gl, "configure debug-line vertex state", () =>
{
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);
});
}
catch (Exception constructionFailure)
{
resources.RollbackConstructionAndThrow(
"DebugLineRenderer construction failed and its GL prefix did not cleanly roll back.",
constructionFailure);
}
_resources = resources;
_shader = shader!;
_vao = vao;
_vbo = vbo;
}
/// <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()
{
_resources.RetryCleanup();
}
}