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.2f; 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; case QuikCommandType.Bezier: RenderBezier(ref call, command as QuikCommandBezier); return call; } return null; } /// /// Generates a basic line segment. /// /// The vertex base to modify when generating vertices. /// The line segment. /// The width of the line. /// Start index of the generated line. /// The line normal. private void GenerateLineSegment( QuikVertex baseVertex, QuikLine line, float width, out short lineStartIndex, out QuikVec2 normal) { QuikVec2 tangent = (line.End - line.Start).Normalize(); normal = new QuikVec2() {X = -tangent.Y, Y = tangent.X}; lineStartIndex = (short)_vertexBufferPointer; QuikVertex startA = baseVertex, startB = baseVertex; QuikVertex endA = baseVertex, endB = baseVertex; startA.Position = line.Start + width / 2 * normal; startB.Position = line.Start - width / 2 * normal; endA.Position = line.End + width / 2 * normal; endB.Position = line.End - width / 2 * normal; // Add the major line vertices. AddVertex(startA, startB, endA, endB); // Add the line indices. AddElement( (short) (lineStartIndex + 1), (short) (lineStartIndex + 2), (short) (lineStartIndex + 0), (short) (lineStartIndex + 1), (short) (lineStartIndex + 3), (short) (lineStartIndex + 2) ); } /// /// Gets the rounding resolution for a line segment. /// /// The width of the line. /// The angle of the cap or joint arc. /// The rounding resolution. private int GetRoundingResolution(float width, float arc) { int endCapResolution = (int) Math.Ceiling(arc * width * CurveGranularity); return endCapResolution; } /// /// Generate a round start cap on a line. /// /// The base vertex to modify for generation. /// The start point. /// The line normal. /// The width of line to generate. /// End cap resolution. /// The positive vertex index of the line start. /// The negative vertex index of the line start. public void GenerateStartCap( QuikVertex baseVertex, QuikVec2 start, QuikVec2 normal, float width, int resolution, short lineStartPositiveIndex, short lineStartNegativeIndex ) { QuikVertex circlePoint = baseVertex; short lastIndex = lineStartPositiveIndex; for (int i = 0; i < resolution; i++) { float angle = (float) (i + 1) / (resolution + 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 } * (width / 2); circlePoint.Position = start + displacement; short segmentIndex = (short)_vertexBufferPointer; AddVertex(circlePoint); AddElement( lineStartNegativeIndex, lastIndex, segmentIndex); lastIndex = segmentIndex; } } /// /// Generate a round line end cap. /// /// Base vertex to modify for generation. /// The line end. /// The line normal. /// The line width. /// Cap generation resolution. /// Vertex index of the positive line end. /// Vertex index of the negative line end. public void GenerateEndCap( QuikVertex baseVertex, QuikVec2 end, QuikVec2 normal, float width, int resolution, short lineEndPositiveIndex, short lineEndNegativeIndex ) { QuikVertex circlePoint = baseVertex; short lastIndex = lineEndPositiveIndex; for (int i = 0; i < resolution; i++) { float angle = -(float) (i + 1) / (resolution + 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 } * (width / 2); circlePoint.Position = end + displacement; AddVertex(circlePoint); AddElement( lineEndNegativeIndex, lastIndex, (short) (_vertexBufferPointer - 1)); lastIndex = (short) (_vertexBufferPointer - 1); } } /// /// Generate a joint on the line negative edge. /// /// Base vertex to modify for generation. /// The focus of the joint. /// With of the lines. /// Index of the negative end vertex. /// Index of the negative start vertex. /// Joint resolution. /// Arc length of the joint. /// Normal of the previous line. private void GenerateNegativeJoint( QuikVertex baseVertex, QuikVec2 focus, float width, short prevNegativeLineIndex, short nextNegativeLineIndex, int resolution, float arc, QuikVec2 prevNormal) { QuikVertex focusVertex = baseVertex; short focusIndex = (short) _vertexBufferPointer; short lastIndex = prevNegativeLineIndex; focusVertex.Position = focus; AddVertex(focusVertex); for (int i = 0; i < resolution; i++) { float angle = (float) (i + 1) / (resolution + 1) * arc; float cosT = MathF.Cos(angle); float sinT = MathF.Sin(angle); QuikVec2 displacement = new QuikVec2() { X = -prevNormal.X * cosT + prevNormal.Y * sinT, Y = -prevNormal.X * sinT - prevNormal.Y * cosT } * (width / 2); QuikVertex segmentVertex = focusVertex; segmentVertex.Position += displacement; short segmentIndex = (short) _vertexBufferPointer; AddVertex(segmentVertex); AddElement(lastIndex, segmentIndex, focusIndex); lastIndex = segmentIndex; } // Add final triangle. AddElement(lastIndex, nextNegativeLineIndex, focusIndex); } /// /// Generate a joint on the line negative edge. /// /// Base vertex to modify for generation. /// The focus of the joint. /// With of the lines. /// Index of the positive end vertex. /// Index of the positive start vertex. /// Joint resolution. /// Arc length of the joint. /// Normal of the next line. private void GeneratePositiveJoint( QuikVertex baseVertex, QuikVec2 focus, float width, short prevPositiveLineIndex, short nextPositiveLineIndex, int resolution, float arc, QuikVec2 nextNormal) { QuikVertex focusVertex = baseVertex; short focusIndex = (short) _vertexBufferPointer; short lastIndex = nextPositiveLineIndex; focusVertex.Position = focus; AddVertex(focusVertex); for (int i = 0; i < resolution; i++) { float angle = (float) (i + 1) / (resolution + 1) * arc; float cosT = MathF.Cos(angle); float sinT = MathF.Sin(angle); QuikVec2 displacement = new QuikVec2() { X = nextNormal.X * cosT - nextNormal.Y * sinT, Y = nextNormal.X * sinT + nextNormal.Y * cosT } * (width / 2); QuikVertex segmentVertex = focusVertex; segmentVertex.Position += displacement; short segmentIndex = (short) _vertexBufferPointer; AddVertex(segmentVertex); AddElement(lastIndex, segmentIndex, focusIndex); lastIndex = segmentIndex; } // Add final triangle. AddElement(lastIndex, prevPositiveLineIndex, focusIndex); } /// /// Generate a joint. /// /// Base vertex to modify. /// Focus of the joint. /// Tangent of the previous line segment. /// Tangent of the next line segment. /// Width of the lines. /// Vertex index of the positive end. /// Vertex index of the negative end. /// Vertex index of the positive start. /// Vertex index of the negative start. public void GenerateJoint( QuikVertex baseVertex, QuikVec2 focus, QuikVec2 prevTangent, QuikVec2 nextTangent, float width, short prevPositiveEndIndex, short prevNegativeEndIndex, short nextPositiveStartIndex, short nextNegativeStartIndex ) { QuikVec2 prevNormal, nextNormal; QuikVec2 averageTangent; prevNormal = new QuikVec2() { X = -prevTangent.Y, Y = prevTangent.X }; nextNormal = new QuikVec2() { X = -nextTangent.Y, Y = nextTangent.X }; averageTangent = 0.5f * (prevTangent + nextTangent); // Figure out which side needs the joint. QuikVec2 positiveEdge = ((focus + nextNormal) - (focus + prevNormal)).Normalize(); QuikVec2 negativeEdge = ((focus - nextNormal) - (focus - prevNormal)).Normalize(); float positiveDot, negativeDot; positiveDot = QuikVec2.Dot(averageTangent, positiveEdge); negativeDot = QuikVec2.Dot(averageTangent, negativeEdge); float arc = MathF.Acos(QuikVec2.Dot(prevNormal, nextNormal)); int resolution = GetRoundingResolution(width, arc); if (positiveDot < negativeDot) { // Negative edge rounding. GenerateNegativeJoint(baseVertex, focus, width, prevNegativeEndIndex, nextNegativeStartIndex, resolution, arc, prevNormal); } else if (negativeDot < positiveDot) { // Positive edge rounding. GeneratePositiveJoint(baseVertex, focus, width, prevPositiveEndIndex, nextPositiveStartIndex, resolution, arc, nextNormal); } else { // There is a cusp. Generate an end cap. GenerateEndCap(baseVertex, focus, prevNormal.Normalize(), width, GetRoundingResolution(width, MathF.PI), prevPositiveEndIndex, prevNegativeEndIndex); } } /// /// 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; QuikVertex baseVertex = new QuikVertex() { Color = style.Color }; GenerateLineSegment(baseVertex, line.Line, style.Width, out short startOffset, out QuikVec2 normal); // Now calculate the end caps. int endCapResolution = GetRoundingResolution(style.Width, MathF.PI); // Construct start cap. GenerateStartCap(baseVertex, line.Line.Start, normal, style.Width, endCapResolution, startOffset, (short) (startOffset + 1)); // Construct end cap. GenerateEndCap(baseVertex, line.Line.End, normal, style.Width, endCapResolution, (short) (startOffset + 2),(short) (startOffset + 3)); call.Offset = (short) (startOffset * 2); call.Count = (short) (_elementBufferPointer - startOffset); } /// /// Render lines. /// /// The draw call to generate. /// The lines command. private void RenderLine(ref QuikDrawCall call, QuikCommandLines lines) { // Skip over stipple patterns for now. QuikStrokeStyle style = lines.Style ?? Context.DefaultStroke; QuikVertex baseVertex = new QuikVertex() { Color = style.Color }; bool isStart; bool isEnd; short startOffset = (short)_elementBufferPointer; short lastStartIndex = 0; short lineStartIndex; QuikVec2 normal; int resolution = GetRoundingResolution(style.Width, MathF.PI); for (int i = 0; i < lines.Lines.Length; i++) { QuikLine line = lines.Lines[i]; isStart = i == 0 || line.Start != lines.Lines[i - 1].End; isEnd = i == lines.Lines.Length - 1 || line.End != lines.Lines[i + 1].Start; GenerateLineSegment(baseVertex, line, style.Width, out lineStartIndex, out normal); if (isStart) { GenerateStartCap(baseVertex, line.Start, normal, style.Width, resolution, lineStartIndex, (short)(lineStartIndex + 1)); } else { QuikLine prev = lines.Lines[i - 1]; QuikVec2 prevTangent = (prev.End - prev.Start).Normalize(); QuikVec2 nextTangent = (line.End - line.Start).Normalize(); GenerateJoint( baseVertex, line.Start, prevTangent, nextTangent, style.Width, (short) (lastStartIndex + 2), (short) (lastStartIndex + 3), lineStartIndex, (short)(lineStartIndex + 1)); } if (isEnd) { GenerateEndCap(baseVertex, line.End, normal, style.Width, resolution, (short)(lineStartIndex + 2), (short)(lineStartIndex + 3)); } lastStartIndex = lineStartIndex; } call.Offset = (short) (startOffset * 2); call.Count = (short) (_elementBufferPointer - startOffset); } private void RenderBezier(ref QuikDrawCall call, QuikCommandBezier bezier) { QuikStrokeStyle style = bezier.Style ?? Context.DefaultStroke; QuikVertex baseVertex = new QuikVertex() { Color = style.Color }; bool isStart; bool isEnd; short startOffset = (short)_elementBufferPointer; int capResolution = GetRoundingResolution(style.Width, MathF.PI); short lastEndPositive = 0; short lastEndNegative = 0; for (int i = 0; i < bezier.Segments.Length; i++) { QuikBezier segment = bezier.Segments[i]; isStart = i == 0 || segment.Start != bezier.Segments[i - 1].End; isEnd = i == bezier.Segments.Length - 1 || segment.End != bezier.Segments[i + 1].Start; GenerateBezierSegment( baseVertex, segment, style.Width, out short startPositive, out short startNegative, out short endPositive, out short endNegative, out QuikVec2 startNormal, out QuikVec2 endNormal); if (isStart) { GenerateStartCap(baseVertex, segment.Start, startNormal, style.Width, capResolution, startPositive, startNegative); } else { GenerateJoint( baseVertex, segment.Start, bezier.Segments[i-1].GetBezierTangent(1), segment.GetBezierTangent(0), style.Width, lastEndPositive, lastEndNegative, startPositive, startNegative ); } if (isEnd) { GenerateEndCap( baseVertex, segment.End, endNormal, style.Width, capResolution, endPositive, endNegative ); } lastEndPositive = endPositive; lastEndNegative = endNegative; } call.Offset = (short) (startOffset * 2); call.Count = (short) (_elementBufferPointer - startOffset); } private void GenerateBezierSegment( QuikVertex baseVertex, QuikBezier bezier, float width, out short startPositiveIndex, out short startNegativeIndex, out short endPositiveIndex, out short endNegativeIndex, out QuikVec2 startNormal, out QuikVec2 endNormal) { QuikVec2 startTangent = bezier.GetBezierTangent(0); QuikVec2 endTangent = bezier.GetBezierTangent(1); startNormal = new QuikVec2(-startTangent.Y, startTangent.X).Normalize(); endNormal = new QuikVec2(-endTangent.Y, endTangent.X).Normalize(); int resolution = GetRoundingResolution(width, bezier.RasterizationArc); startPositiveIndex = (short)_vertexBufferPointer; startNegativeIndex = (short) (startPositiveIndex + 1); QuikVertex startPositive = baseVertex; QuikVertex startNegative = baseVertex; startPositive.Position = bezier.Start + 0.5f * width * startNormal; startNegative.Position = bezier.Start - 0.5f * width * startNormal; AddVertex(startPositive, startNegative); for (int i = 0; i < resolution; i++) { float t = (float) (i + 1) / resolution; QuikVec2 at = bezier.GetBezierTangent(t).Normalize(); QuikVec2 a = bezier.GetBezierPoint(t); QuikVec2 an = 0.5f * width * new QuikVec2(-at.Y, at.X); short index = (short) _vertexBufferPointer; QuikVertex apv = baseVertex, anv = baseVertex; apv.Position = a + an; anv.Position = a - an; AddVertex(apv, anv); AddElement( (short)(index - 2), (short)(index - 1), (short)(index + 0), (short)(index - 1), (short)(index + 1), (short)(index + 0)); } endNegativeIndex = (short) (_vertexBufferPointer - 1); endPositiveIndex = (short) (_vertexBufferPointer - 2); } } public enum QuikRenderTarget { Color, Stencil } public struct QuikDrawCall { public QuikRenderTarget Target; public short Offset; public short Count; public QuikRectangle Bounds; public bool ClearStencil; } }