using System.Collections.Generic; using System.Numerics; using System.Runtime.InteropServices; using Silk.NET.OpenGL; namespace AcDream.App.Rendering; /// /// Minimal GL debug line renderer for visualizing collision shapes, /// bounding boxes, and other debug geometry. Collect lines each frame /// via / , then call /// 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. /// 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 _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); } /// Clear accumulated lines. Call at the start of each frame. 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; } /// /// Draw a cylinder as 2 polygon rings (base + top) connected by 4 /// vertical line segments at 0/90/180/270 degrees. /// 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); } } /// /// Draw an axis-aligned box as 12 edges. /// 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); } /// Upload + draw all accumulated lines. 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(); } }