using System; namespace Quik.VertexGenerator { /// /// Generates vertices from draw commands for GPU APIs like OpenGL. /// public class QuikVertexGenerator { // There is a very specific reason I am not using lists like a regular // person would use. It has to do with the fact that there is no way // to access the internal pointer of a System.Collections.Generic.List<> // in older versions of .NET. Avoiding a copy of an entire vertex buffer // would be very much appreciated by many devs. So please don't be // "smart" around this code. // - mixed. /// /// Controls the buffer granularity. /// private const int BufferGranularity = 4096; /// /// List of vertices. /// private QuikVertex[] _vertexBuffer = new QuikVertex[BufferGranularity]; /// /// Pointer into the vertex buffer. /// private int _vertexBufferPointer = 0; private float _vertexBufferUsage = 0; /// /// List of element indices. /// private short[] _elementBuffer = new short[BufferGranularity]; /// /// Pointer into the element buffer. /// private int _elementBufferPointer = 0; private float _elementBufferUsage; private long _bufferUsageCounter; /// /// Get a reference to the vertex buffer. /// public QuikVertex[] VertexBuffer => _vertexBuffer; /// /// Number of vertices in the vertex buffer. /// public int VertexCount => _vertexBufferPointer; /// /// Get a reference to the element buffer. /// public short[] ElementBuffer => _elementBuffer; /// /// Number of elements in the element buffer. /// public int ElementCount => _elementBufferPointer; public float CurveGranularity { get; set; } = 0.5f; public QuikContext Context { get; } public QuikVertexGenerator(QuikContext context) { Context = context; } /// /// Expands the vertex buffer by the buffer granularity constant. /// private void ExpandVertexBuffer() { Array.Resize(ref _vertexBuffer, _vertexBuffer.Length + BufferGranularity); } /// /// Expands the element buffer by the buffer granularity constant. /// private void ExpandElementBuffer() { Array.Resize(ref _elementBuffer, _elementBuffer.Length + BufferGranularity); } /// /// Add vertices to the list. /// /// The list of vertices to add. private void AddVertex(params QuikVertex[] vertices) { int requiredCapacity = _vertexBufferPointer + vertices.Length; while (requiredCapacity > _vertexBuffer.Length) { ExpandVertexBuffer(); } Array.Copy(vertices, 0, _vertexBuffer, _vertexBufferPointer, vertices.Length); _vertexBufferPointer += vertices.Length; } /// /// Add element indices to the list. /// /// The list of indices to add. private void AddElement(params short[] indices) { int requiredCapacity = _elementBufferPointer + indices.Length; while (requiredCapacity > _elementBuffer.Length) { ExpandElementBuffer(); } Array.Copy(indices, 0, _elementBuffer, _elementBufferPointer, indices.Length); _elementBufferPointer += indices.Length; } private void MovingAverage(ref float average, long sampleCounter, int newSample) { // Thanks to stackoverflow for a neat formula. // https://stackoverflow.com/questions/12636613/how-to-calculate-moving-average-without-keeping-the-count-and-data-total const float order = 4; average = average + (newSample - average) / Math.Min(sampleCounter + 1, order); } private bool _renderStencilMask = false; private QuikRectangle _bounds = new QuikRectangle( float.PositiveInfinity, float.PositiveInfinity, float.NegativeInfinity, float.NegativeInfinity); /// /// Clear the drawing buffers. /// public void Clear() { int newVertexSize; int newElementSize; _bufferUsageCounter++; MovingAverage(ref _vertexBufferUsage, _bufferUsageCounter, _elementBufferPointer); MovingAverage(ref _elementBufferUsage, _bufferUsageCounter, _elementBufferPointer); newVertexSize = (int)(Math.Ceiling(_vertexBufferUsage / BufferGranularity) * BufferGranularity); newElementSize = (int)(Math.Ceiling(_elementBufferUsage / BufferGranularity) * BufferGranularity); Array.Resize(ref _vertexBuffer, newVertexSize); Array.Resize(ref _elementBuffer, newElementSize); _vertexBufferPointer = 0; _elementBufferPointer = 0; } public QuikDrawCall? ConsumeCommand(QuikCommand command) { QuikDrawCall call = new QuikDrawCall() { Target = _renderStencilMask ? QuikRenderTarget.Stencil : QuikRenderTarget.Color }; switch (command.Type) { case QuikCommandType.StencilMaskClear: call.ClearStencil = true; break; case QuikCommandType.StencilMaskBegin: _renderStencilMask = true; call.Target = QuikRenderTarget.Stencil; break; case QuikCommandType.StencilMaskEnd: _renderStencilMask = false; call.Target = QuikRenderTarget.Color; break; case QuikCommandType.Line: RenderLine(ref call, command as QuikCommandLine); return call; case QuikCommandType.Lines: RenderLine(ref call, command as QuikCommandLines); return call; } return null; } /// /// Renders a line. /// /// The draw call to generate. /// The line to draw. private void RenderLine(ref QuikDrawCall call, QuikCommandLine line) { // Skip over stipple patterns for now. QuikStrokeStyle style = line.Style ?? Context.DefaultStroke; int endCapResolution; // Resolution of the end cap. short startOffset = (short) _vertexBufferPointer; // Starting index pointer. QuikVec2 tangent; QuikVec2 normal; tangent = (line.Line.End - line.Line.Start).Normalize(); normal = new QuikVec2() {X = -tangent.Y, Y = tangent.X}; QuikVertex baseVertex = new QuikVertex() {Color = style.Color }; QuikVertex startA = baseVertex, startB = baseVertex; QuikVertex endA = baseVertex, endB = baseVertex; startA.Position = line.Line.Start + style.Width / 2 * normal; startB.Position = line.Line.Start - style.Width / 2 * normal; endA.Position = line.Line.End + style.Width / 2 * normal; endB.Position = line.Line.End - style.Width / 2 * normal; // Add the major line vertices. AddVertex(startA, startB, endA, endB); // Add the line indices. AddElement( (short) (startOffset + 1), (short) (startOffset + 2), (short) (startOffset + 0), (short) (startOffset + 1), (short) (startOffset + 3), (short) (startOffset + 2) ); // Now calculate the end caps. endCapResolution = (int)Math.Ceiling(MathF.PI * style.Width * CurveGranularity); // Construct start cap. QuikVertex circlePoint = baseVertex; short lastIndex = startOffset; for (int i = 0; i < endCapResolution; i++) { float angle = (float) (i + 1) / (endCapResolution + 1) * MathF.PI; float cosT = MathF.Cos(angle); float sinT = MathF.Sin(angle); QuikVec2 displacement = new QuikVec2() { X = normal.X * cosT - normal.Y * sinT, Y = normal.X * sinT + normal.Y * cosT } * (style.Width / 2); circlePoint.Position = line.Line.Start + displacement; AddVertex(circlePoint); AddElement( (short)(startOffset + 1), lastIndex, (short)(_vertexBufferPointer - 1)); lastIndex = (short) (_vertexBufferPointer - 1); } // Construct end cap. lastIndex = (short)(startOffset + 2); for (int i = 0; i < endCapResolution; i++) { float angle = -(float) (i + 1) / (endCapResolution + 1) * MathF.PI; float cosT = MathF.Cos(angle); float sinT = MathF.Sin(angle); QuikVec2 displacement = new QuikVec2() { X = normal.X * cosT - normal.Y * sinT, Y = normal.X * sinT + normal.Y * cosT } * (style.Width / 2); circlePoint.Position = line.Line.End + displacement; AddVertex(circlePoint); AddElement( (short)(startOffset + 3), lastIndex, (short)(_vertexBufferPointer - 1)); lastIndex = (short) (_vertexBufferPointer - 1); } call.Offset =(short) (startOffset * 2); call.Count = (short) (_elementBufferPointer - startOffset); } private void RenderLine(ref QuikDrawCall call, QuikCommandLines lines) { } } public enum QuikRenderTarget { Color, Stencil } public struct QuikDrawCall { public QuikRenderTarget Target; public short Offset; public short Count; public QuikRectangle Bounds; public bool ClearStencil; } }