using System; using System.Collections.Generic; using Quik.Typography; 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 _vertexUsage = 0; /// /// List of element indices. /// private short[] _elementBuffer = new short[BufferGranularity]; /// /// Pointer into the element buffer. /// private int _elementBufferPointer = 0; private float _elementUsage; private long _bufferUsageCounter; /// /// Moving average of the vertex count. /// public float VertexUsage => _vertexUsage; /// /// Moving average of the element count. /// public float ElementUsage => _elementUsage; /// /// 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 List DrawCalls { get; } = new List(); public float CurveGranularity { get; set; } = 1f; 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 _vertexUsage, _bufferUsageCounter, _vertexBufferPointer); MovingAverage(ref _elementUsage, _bufferUsageCounter, _elementBufferPointer); newVertexSize = (int)(Math.Ceiling(_vertexUsage / BufferGranularity) * BufferGranularity); newElementSize = (int)(Math.Ceiling(_elementUsage / BufferGranularity) * BufferGranularity); Array.Resize(ref _vertexBuffer, newVertexSize); Array.Resize(ref _elementBuffer, newElementSize); _vertexBufferPointer = 0; _elementBufferPointer = 0; DrawCalls.Clear(); } public QuikDrawCall CallTemplate; public event VertexGeneratorCommandHandler HandleCommand; protected virtual bool OnHandleCommand(QuikVertexGenerator generator, QuikCommand command) { bool anyCalls = false; HandleCommand?.Invoke(generator, command, ref anyCalls); return anyCalls; } public void ConsumeCommand(QuikCommand command) { CallTemplate.Target = _renderStencilMask ? QuikRenderTarget.Stencil : QuikRenderTarget.Color; switch (command.Type) { case QuikCommandType.Mask: CallTemplate.Bounds = ((QuikCommandMask)command).Bounds; break; case QuikCommandType.StencilMaskClear: CallTemplate.ClearStencil = true; break; case QuikCommandType.StencilMaskBegin: _renderStencilMask = true; CallTemplate.Target = QuikRenderTarget.Stencil; break; case QuikCommandType.StencilMaskEnd: _renderStencilMask = false; CallTemplate.Target = QuikRenderTarget.Color; break; case QuikCommandType.Line: RenderLine(command as QuikCommandLine); goto exit_with_call; case QuikCommandType.Lines: RenderLine(command as QuikCommandLines); goto exit_with_call; case QuikCommandType.Bezier: RenderBezier(command as QuikCommandBezier); goto exit_with_call; case QuikCommandType.Rectangle: RenderRectangles(command as QuikCommandRectangle); goto exit_with_call; case QuikCommandType.Rectangles: RenderRectangles(command as QuikCommandRectangles); goto exit_with_call; case QuikCommandType.PutChar: RenderCharacter(command as QuikCommandPutChar); goto exit_with_call; case QuikCommandType.PutText: RenderTextPut(command as QuikCommandPutText); goto exit_with_call; case QuikCommandType.EmitTypeset: RenderTextTypeset(command as QuikCommandEmitText); goto exit_with_call; default: { if (OnHandleCommand(this, command)) goto exit_with_call; break; } } return; exit_with_call: CallTemplate.ClearStencil = false; } #region Lines & Beziers /// /// 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 radius, float arc) { return (int) Math.Ceiling(arc * radius * CurveGranularity); } /// /// 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(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)); QuikDrawCall call = CallTemplate; call.Offset = (short) (startOffset * 2); call.Count = (short) (_elementBufferPointer - startOffset); DrawCalls.Add(call); } /// /// Render lines. /// /// The draw call to generate. /// The lines command. private void RenderLine(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; } QuikDrawCall call = CallTemplate; call.Offset = (short) (startOffset * 2); call.Count = (short) (_elementBufferPointer - startOffset); DrawCalls.Add(call); } private void RenderBezier(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; } QuikDrawCall call = CallTemplate; call.Offset = (short) (startOffset * 2); call.Count = (short) (_elementBufferPointer - startOffset); DrawCalls.Add(call); } 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); } #endregion #region Rectangles private void RenderRectangles(QuikCommandRectangle rectangle) { QuikStrokeStyle stroke = rectangle.StrokeStyle ?? Context.DefaultStroke; QuikFillStyle fill = rectangle.FillStyle ?? Context.DefaultFill; short start = (short) _elementBufferPointer; GenerateRectangle(rectangle.Rectangle, stroke, fill, rectangle.CornerRadius); QuikDrawCall call = CallTemplate; call.Offset = (short) (start * 2); call.Count = (short) (_elementBufferPointer - start); DrawCalls.Add(call); } private void GenerateRectangle(QuikRectangle rectangle, QuikStrokeStyle stroke, QuikFillStyle fill, float cornerRadius) { float semiStroke = 0.5f * stroke.Width; if (cornerRadius == 0) { // If there is no stroke radius, draw a simpler rectangle. if (stroke.Width == 0) { // If there is no border, just 4 points is enough. GenerateRectangleSimple(rectangle, fill); } else { // If there is a border, take it into account. QuikRectangle innerRectangle = rectangle; innerRectangle.Left += semiStroke; innerRectangle.Top -= semiStroke; innerRectangle.Right -= semiStroke; innerRectangle.Bottom += semiStroke; GenerateRectangleSimple(innerRectangle, fill); GenerateRectangleBorderSimple(rectangle, stroke); } } else { float innerRadius = cornerRadius - semiStroke; if (innerRadius <= 0) { QuikRectangle innerRectangle = new QuikRectangle( rectangle.Right - semiStroke, rectangle.Top - semiStroke, rectangle.Left + semiStroke, rectangle.Bottom + semiStroke ); GenerateRectangleSimple(innerRectangle, fill); GenerateRectangleBorderNarrow(rectangle, stroke, cornerRadius); } else { // Generate the inner rectangle. QuikRectangle innerRectangle = new QuikRectangle( rectangle.Right - semiStroke * 0.95f, rectangle.Top - semiStroke * 0.95f, rectangle.Left + semiStroke * 0.95f, rectangle.Bottom + semiStroke * 0.95f ); GenerateRectangleRound(innerRectangle, fill, innerRadius); if (stroke.Width > 0) { GenerateRectangleBorderWide(rectangle, stroke, cornerRadius); } } } } private void GenerateRectangleBorderNarrow( QuikRectangle rectangle, QuikStrokeStyle stroke, float cornerRadius) { float semiStroke = 0.5f * stroke.Width; float inset = cornerRadius - semiStroke; short start = (short) _vertexBufferPointer; QuikVertex baseVertex = new QuikVertex() {Color = stroke.Color}; QuikVertex a = baseVertex, b = baseVertex, c = baseVertex, d = baseVertex, e = baseVertex, f = baseVertex, g = baseVertex, h = baseVertex, i = baseVertex, j = baseVertex, k = baseVertex, l = baseVertex, q1 = baseVertex, q2 = baseVertex, q3 = baseVertex, q4 = baseVertex; a.Position = new QuikVec2(rectangle.Left + semiStroke, rectangle.Bottom + semiStroke); b.Position = new QuikVec2(rectangle.Right - semiStroke, rectangle.Bottom + semiStroke); c.Position = new QuikVec2(rectangle.Right - semiStroke, rectangle.Top - semiStroke); d.Position = new QuikVec2(rectangle.Left + semiStroke, rectangle.Top - semiStroke); q1.Position = new QuikVec2(rectangle.Left + inset, rectangle.Bottom + inset); q2.Position = new QuikVec2(rectangle.Right - inset, rectangle.Bottom + inset); q3.Position = new QuikVec2(rectangle.Right - inset, rectangle.Top - inset); q4.Position = new QuikVec2(rectangle.Left + inset, rectangle.Top - inset); e.Position = new QuikVec2(q1.Position.X, rectangle.Bottom - semiStroke); f.Position = new QuikVec2(q2.Position.X, rectangle.Bottom - semiStroke); g.Position = new QuikVec2(rectangle.Right + semiStroke, q2.Position.Y); h.Position = new QuikVec2(rectangle.Right + semiStroke, q3.Position.Y); i.Position = new QuikVec2(q3.Position.X, rectangle.Top + semiStroke); j.Position = new QuikVec2(q4.Position.X, rectangle.Top + semiStroke); k.Position = new QuikVec2(rectangle.Left - semiStroke, q4.Position.Y); l.Position = new QuikVec2(rectangle.Left - semiStroke, q1.Position.Y); AddVertex(a, b, c, d, e, f, g, h, i, j, k, l, q1, q2, q3, q4); AddElement( (short) (start + 4), (short) (start + 5), (short) (start + 13), (short) (start + 4), (short) (start + 13), (short) (start + 12), (short) (start + 12), (short) (start + 13), (short) (start + 1), (short) (start + 12), (short) (start + 1), (short) (start + 0), (short) (start + 13), (short) (start + 6), (short) (start + 7), (short) (start + 13), (short) (start + 7), (short) (start + 14), (short) (start + 13), (short) (start + 14), (short) (start + 2), (short) (start + 13), (short) (start + 2), (short) (start + 1), (short) (start + 3), (short) (start + 2), (short) (start + 14), (short) (start + 3), (short) (start + 14), (short) (start + 15), (short) (start + 15), (short) (start + 14), (short) (start + 8), (short) (start + 15), (short) (start + 8), (short) (start + 9), (short) (start + 0), (short) (start + 3), (short) (start + 15), (short) (start + 0), (short) (start + 15), (short) (start + 12), (short) (start + 12), (short) (start + 15), (short) (start + 10), (short) (start + 12), (short) (start + 10), (short) (start + 11) ); int resolution = GetRoundingResolution(cornerRadius, 0.5f * MathF.PI); short last, next; last = (short) (start + 7); for (int idx = 0; idx < resolution; idx++) { float angle = 0.5f * MathF.PI * (idx + 1) / (resolution + 1); QuikVertex vertex = baseVertex; vertex.Position = q3.Position + cornerRadius * new QuikVec2(MathF.Cos(angle), MathF.Sin(angle)); next = (short) _vertexBufferPointer; AddVertex(vertex); AddElement((short)(start + 14), last, next); last = next; } AddElement((short)(start + 14), last, (short)(start + 8)); last = (short) (start + 9); for (int idx = 0; idx < resolution; idx++) { float angle = 0.5f * MathF.PI * (idx + 1) / (resolution + 1); QuikVertex vertex = baseVertex; vertex.Position = q4.Position + cornerRadius * new QuikVec2(-MathF.Sin(angle), MathF.Cos(angle)); next = (short) _vertexBufferPointer; AddVertex(vertex); AddElement((short)(start + 15), last, next); last = next; } AddElement((short)(start + 15), last, (short)(start + 10)); last = (short) (start + 11); for (int idx = 0; idx < resolution; idx++) { float angle = 0.5f * MathF.PI * (idx + 1) / (resolution + 1); QuikVertex vertex = baseVertex; vertex.Position = q1.Position + cornerRadius * new QuikVec2(-MathF.Cos(angle), -MathF.Sin(angle)); next = (short) _vertexBufferPointer; AddVertex(vertex); AddElement((short)(start + 12), last, next); last = next; } AddElement((short)(start + 12), last, (short)(start + 4)); last = (short) (start + 5); for (int idx = 0; idx < resolution; idx++) { float angle = 0.5f * MathF.PI * (idx + 1) / (resolution + 1); QuikVertex vertex = baseVertex; vertex.Position = q2.Position + cornerRadius * new QuikVec2(MathF.Sin(angle), -MathF.Cos(angle)); next = (short) _vertexBufferPointer; AddVertex(vertex); AddElement((short)(start + 13), last, next); last = next; } AddElement((short)(start + 13), last, (short)(start + 6)); } private void GenerateRectangleBorderWide(QuikRectangle rectangle, QuikStrokeStyle stroke, float cornerRadius) { float semiStroke = 0.5f * stroke.Width; float outerRadius = cornerRadius + semiStroke; float innerRadius = cornerRadius - semiStroke; short start = (short) _vertexBufferPointer; // The corner foci. QuikVec2 q1 = new QuikVec2(rectangle.Min.X + cornerRadius, rectangle.Min.Y + cornerRadius); QuikVec2 q2 = new QuikVec2(rectangle.Max.X - cornerRadius, rectangle.Min.Y + cornerRadius); QuikVec2 q3 = new QuikVec2(rectangle.Max.X - cornerRadius, rectangle.Max.Y - cornerRadius); QuikVec2 q4 = new QuikVec2(rectangle.Min.X + cornerRadius, rectangle.Max.Y - cornerRadius); QuikVertex baseVertex = new QuikVertex() {Color = stroke.Color}; QuikVertex a = baseVertex, b = baseVertex, c = baseVertex, d = baseVertex, e = baseVertex, f = baseVertex, g = baseVertex, h = baseVertex, i = baseVertex, j = baseVertex, k = baseVertex, l = baseVertex, m = baseVertex, n = baseVertex, o = baseVertex, p = baseVertex; a.Position = new QuikVec2(q1.X, q1.Y - innerRadius); b.Position = new QuikVec2(q2.X, q2.Y - innerRadius); c.Position = new QuikVec2(q2.X + innerRadius, q2.Y); d.Position = new QuikVec2(q3.X + innerRadius, q3.Y); e.Position = new QuikVec2(q3.X, q3.Y + innerRadius); f.Position = new QuikVec2(q4.X, q4.Y + innerRadius); g.Position = new QuikVec2(q4.X - innerRadius, q4.Y); h.Position = new QuikVec2(q1.X - innerRadius, q1.Y); i.Position = new QuikVec2(q1.X, rectangle.Bottom - semiStroke); j.Position = new QuikVec2(q2.X, rectangle.Bottom - semiStroke); k.Position = new QuikVec2(rectangle.Right + semiStroke, q2.Y); l.Position = new QuikVec2(rectangle.Right + semiStroke, q3.Y); m.Position = new QuikVec2(q3.X, rectangle.Top + semiStroke); n.Position = new QuikVec2(q4.X, rectangle.Top + semiStroke); o.Position = new QuikVec2(rectangle.Left - semiStroke, q4.Y); p.Position = new QuikVec2(rectangle.Left - semiStroke, q1.Y); AddVertex(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p); AddElement( (short)(start + 8), (short)(start + 1), (short)(start + 0), (short)(start + 8), (short)(start + 9), (short)(start + 1), (short)(start + 2), (short)(start + 11), (short)(start + 3), (short)(start + 2), (short)(start + 10), (short)(start + 11), (short)(start + 5), (short)(start + 12), (short)(start + 13), (short)(start + 5), (short)(start + 4), (short)(start + 12), (short)(start + 15), (short)(start + 6), (short)(start + 14), (short)(start + 15), (short)(start + 7), (short)(start + 6) ); int resolution = GetRoundingResolution(outerRadius, 0.5f * MathF.PI); short last0, last1; short next0, next1; last0 = (short) (start + 3); last1 = (short) (start + 11); for (int idx = 0; idx < resolution; idx++) { float angle = 0.5f * MathF.PI * (idx + 1) / (resolution + 1); QuikVec2 normal = new QuikVec2(MathF.Cos(angle), MathF.Sin(angle)); QuikVertex v0 = baseVertex, v1 = baseVertex; v0.Position = q3 + innerRadius * normal; v1.Position = q3 + outerRadius * normal; next0 = (short) (_vertexBufferPointer + 0); next1 = (short) (_vertexBufferPointer + 1); AddVertex(v0, v1); AddElement( last0, next1, next0, last0, last1, next1); last0 = next0; last1 = next1; } AddElement( last0, (short)(start + 12), (short)(start + 4), last0, last1, (short)(start + 12) ); last0 = (short) (start + 5); last1 = (short) (start + 13); for (int idx = 0; idx < resolution; idx++) { float angle = 0.5f * MathF.PI * (idx + 1) / (resolution + 1); QuikVec2 normal = new QuikVec2(-MathF.Sin(angle), MathF.Cos(angle)); QuikVertex v0 = baseVertex, v1 = baseVertex; v0.Position = q4 + innerRadius * normal; v1.Position = q4 + outerRadius * normal; next0 = (short) (_vertexBufferPointer + 0); next1 = (short) (_vertexBufferPointer + 1); AddVertex(v0, v1); AddElement( last0, next1, next0, last0, last1, next1); last0 = next0; last1 = next1; } AddElement( last0, (short)(start + 14), (short)(start + 6), last0, last1, (short)(start + 14) ); last0 = (short) (start + 7); last1 = (short) (start + 15); for (int idx = 0; idx < resolution; idx++) { float angle = 0.5f * MathF.PI * (idx + 1) / (resolution + 1); QuikVec2 normal = new QuikVec2(-MathF.Cos(angle), -MathF.Sin(angle)); QuikVertex v0 = baseVertex, v1 = baseVertex; v0.Position = q1 + innerRadius * normal; v1.Position = q1 + outerRadius * normal; next0 = (short) (_vertexBufferPointer + 0); next1 = (short) (_vertexBufferPointer + 1); AddVertex(v0, v1); AddElement( last0, next1, next0, last0, last1, next1); last0 = next0; last1 = next1; } AddElement( last0, (short)(start + 8), (short)(start + 0), last0, last1, (short)(start + 8) ); last0 = (short) (start + 1); last1 = (short) (start + 9); for (int idx = 0; idx < resolution; idx++) { float angle = 0.5f * MathF.PI * (idx + 1) / (resolution + 1); QuikVec2 normal = new QuikVec2(MathF.Sin(angle), -MathF.Cos(angle)); QuikVertex v0 = baseVertex, v1 = baseVertex; v0.Position = q2 + innerRadius * normal; v1.Position = q2 + outerRadius * normal; next0 = (short) (_vertexBufferPointer + 0); next1 = (short) (_vertexBufferPointer + 1); AddVertex(v0, v1); AddElement( last0, next1, next0, last0, last1, next1); last0 = next0; last1 = next1; } AddElement( last0, (short)(start + 10), (short)(start + 2), last0, last1, (short)(start + 10) ); } private void GenerateRectangleRound( QuikRectangle rectangle, QuikFillStyle fill, float radius) { short baseElement = (short) _vertexBufferPointer; QuikVertex baseVertex = new QuikVertex() {Color = fill.Color}; QuikVertex a = baseVertex, b = baseVertex, c = baseVertex, d = baseVertex, e = baseVertex, f = baseVertex, g = baseVertex, h = baseVertex, i = baseVertex, j = baseVertex, k = baseVertex, l = baseVertex; // Generate base 5 patches. a.Position = new QuikVec2( rectangle.Left + radius, rectangle.Bottom + radius); b.Position = new QuikVec2( rectangle.Right - radius, rectangle.Bottom + radius); c.Position = new QuikVec2( rectangle.Right - radius, rectangle.Top - radius); d.Position = new QuikVec2( rectangle.Left + radius, rectangle.Top - radius); e.Position = new QuikVec2( rectangle.Left + radius, rectangle.Bottom); f.Position = new QuikVec2( rectangle.Right - radius, rectangle.Bottom); g.Position = new QuikVec2( rectangle.Right, rectangle.Bottom + radius); h.Position = new QuikVec2( rectangle.Right, rectangle.Top - radius); i.Position = new QuikVec2( rectangle.Right - radius, rectangle.Top); j.Position = new QuikVec2( rectangle.Left + radius, rectangle.Top); k.Position = new QuikVec2( rectangle.Left, rectangle.Top - radius); l.Position = new QuikVec2( rectangle.Left, rectangle.Bottom + radius); AddVertex(a, b, c, d, e, f, g, h, i, j, k, l); AddElement( (short) (baseElement + 0), (short) (baseElement + 1), (short) (baseElement + 2), (short) (baseElement + 0), (short) (baseElement + 2), (short) (baseElement + 3), (short) (baseElement + 4), (short) (baseElement + 5), (short) (baseElement + 1), (short) (baseElement + 4), (short) (baseElement + 1), (short) (baseElement + 0), (short) (baseElement + 1), (short) (baseElement + 6), (short) (baseElement + 7), (short) (baseElement + 1), (short) (baseElement + 7), (short) (baseElement + 2), (short) (baseElement + 3), (short) (baseElement + 2), (short) (baseElement + 8), (short) (baseElement + 3), (short) (baseElement + 8), (short) (baseElement + 9), (short) (baseElement + 11), (short) (baseElement + 0), (short) (baseElement + 3), (short) (baseElement + 11), (short) (baseElement + 3), (short) (baseElement + 10)); // Now generate corner patches. int resolution = GetRoundingResolution(radius, 0.5f * MathF.PI); short focus, last, current; focus = (short) (baseElement + 2); last = (short) (baseElement + 7); for (int idx = 0; idx < resolution; idx++) { QuikVertex vertex = baseVertex; float angle = 0.5f * MathF.PI * (idx + 1) / (resolution + 1); vertex.Position = c.Position + radius * new QuikVec2(MathF.Cos(angle), MathF.Sin(angle)); current = (short) _vertexBufferPointer; AddVertex(vertex); AddElement(focus, last, current); last = current; } AddElement(focus, last, (short) (baseElement + 8)); focus = (short) (baseElement + 3); last = (short) (baseElement + 9); for (int idx = 0; idx < resolution; idx++) { QuikVertex vertex = baseVertex; float angle = 0.5f * MathF.PI * (idx + 1) / (resolution + 1); vertex.Position = d.Position + radius * new QuikVec2(-MathF.Sin(angle), MathF.Cos(angle)); current = (short) _vertexBufferPointer; AddVertex(vertex); AddElement(focus, last, current); last = current; } AddElement(focus, last, (short) (baseElement + 10)); focus = (short) (baseElement + 0); last = (short) (baseElement + 11); for (int idx = 0; idx < resolution; idx++) { QuikVertex vertex = baseVertex; float angle = 0.5f * MathF.PI * (idx + 1) / (resolution + 1); vertex.Position = a.Position + radius * new QuikVec2(-MathF.Cos(angle), -MathF.Sin(angle)); current = (short) _vertexBufferPointer; AddVertex(vertex); AddElement(focus, last, current); last = current; } AddElement(focus, last, (short) (baseElement + 4)); focus = (short) (baseElement + 1); last = (short) (baseElement + 5); for (int idx = 0; idx < resolution; idx++) { QuikVertex vertex = baseVertex; float angle = 0.5f * MathF.PI * (idx + 1) / (resolution + 1); vertex.Position = b.Position + radius * new QuikVec2(MathF.Sin(angle), -MathF.Cos(angle)); current = (short) _vertexBufferPointer; AddVertex(vertex); AddElement(focus, last, current); last = current; } AddElement(focus, last, (short) (baseElement + 6)); } private void GenerateRectangleBorderSimple(QuikRectangle rectangle, QuikStrokeStyle stroke) { QuikVertex baseStrokeVertex = new QuikVertex() {Color = stroke.Color}; float semiStroke = 0.5f * stroke.Width; // AB GenerateLineSegment( baseStrokeVertex, new QuikLine( rectangle.Min.X + semiStroke, rectangle.Min.Y, rectangle.Max.X - semiStroke, rectangle.Min.Y ), stroke.Width, out _, out _); // BC GenerateLineSegment( baseStrokeVertex, new QuikLine( rectangle.Max.X, rectangle.Min.Y - semiStroke, rectangle.Max.X, rectangle.Max.Y + semiStroke ), stroke.Width, out _, out _); // CD GenerateLineSegment( baseStrokeVertex, new QuikLine( rectangle.Max.X - semiStroke, rectangle.Max.Y, rectangle.Min.X + semiStroke, rectangle.Max.Y ), stroke.Width, out _, out _); // DA GenerateLineSegment( baseStrokeVertex, new QuikLine( rectangle.Min.X, rectangle.Max.Y + semiStroke, rectangle.Min.X, rectangle.Min.Y - semiStroke ), stroke.Width, out _, out _); } private void GenerateRectangleSimple( QuikRectangle rectangle, QuikFillStyle fill) { QuikVertex a, b, c, d; a = b = c = d = new QuikVertex() {Color = fill.Color}; a.Position = rectangle.Min; b.Position = new QuikVec2(rectangle.Right, rectangle.Bottom); c.Position = rectangle.Max; d.Position = new QuikVec2(rectangle.Left, rectangle.Top); short idxA = (short) _vertexBufferPointer; short idxB = (short) (idxA + 1); short idxC = (short) (idxA + 2); short idxD = (short) (idxA + 3); AddVertex(a, b, c, d); AddElement( idxA, idxB, idxC, idxA, idxC, idxD ); } private void RenderRectangles(QuikCommandRectangles rectangles) { QuikStrokeStyle stroke = rectangles.StrokeStyle ?? Context.DefaultStroke; QuikFillStyle fill = rectangles.FillStyle ?? Context.DefaultFill; short start = (short) _elementBufferPointer; for (int i = 0; i < rectangles.Rectangles.Length; i++) { GenerateRectangle(rectangles.Rectangles[i], stroke, fill, rectangles.CornerRadius); } QuikDrawCall call = CallTemplate; call.Offset = (short) (start * 2); call.Count = (short) (_elementBufferPointer - start); DrawCalls.Add(call); } #endregion #region Text private void RenderCharacter(QuikCommandPutChar chr) { Context.DefaultFont.GetCharacter(chr.Character, out IQuikTexture texture, out QuikGlyph metrics); QuikVertex a, b, c, d; a = b = c = d = new QuikVertex() {Color = new QuikColor(0xffffffff)}; a.Position = chr.Position + new QuikVec2(0, metrics.HorizontalBearing.Y - metrics.Size.Y); a.TextureCoordinates = metrics.Location.Min; b.Position = a.Position + new QuikVec2(metrics.Size.X, 0); c.Position = a.Position + metrics.Size; d.Position = a.Position + new QuikVec2(0, metrics.Size.Y); b.TextureCoordinates = new QuikVec2(metrics.Location.Right, metrics.Location.Bottom); c.TextureCoordinates = metrics.Location.Max; d.TextureCoordinates = new QuikVec2(metrics.Location.Left, metrics.Location.Top); short startVertex = (short)_vertexBufferPointer; short startElement = (short) _elementBufferPointer; AddVertex(a, b, c, d); AddElement(startVertex, (short)(startVertex + 1), (short)(startVertex + 2), startVertex, (short)(startVertex + 2), (short)(startVertex + 3)); QuikDrawCall call = CallTemplate; call.Texture = texture; call.Offset = (short) (startElement * 2); call.Count = 6; DrawCalls.Add(call); } private void RenderTextPut(QuikCommandPutText text) { short startElement = (short)_elementBufferPointer; QuikFont font = Context.DefaultFont; QuikVertex vertex = new QuikVertex() {Color = new QuikColor(0x000000ff)}; QuikVec2 pointer = text.Position; IQuikTexture texture = null; for (int i = 0; i < text.Text.Length; i++) { int chr = text.Text[i]; QuikGlyph metrics; IQuikTexture ntex; font.GetCharacter(chr, out ntex, out metrics); if (ntex != texture && texture != null) { QuikDrawCall call = CallTemplate; call.Texture = texture; call.Offset = (short) (startElement * 2); call.Count = (short)(_elementBufferPointer - startElement); DrawCalls.Add(call); startElement = (short) _elementBufferPointer; } texture = ntex; QuikVertex a, b, c, d; a = b = c = d = vertex; a.Position = pointer + new QuikVec2(0, metrics.HorizontalBearing.Y - metrics.Size.Y); a.TextureCoordinates = metrics.Location.Min; b.Position = a.Position + new QuikVec2(metrics.Size.X, 0); c.Position = a.Position + metrics.Size; d.Position = a.Position + new QuikVec2(0, metrics.Size.Y); b.TextureCoordinates = new QuikVec2(metrics.Location.Right, metrics.Location.Bottom); c.TextureCoordinates = metrics.Location.Max; d.TextureCoordinates = new QuikVec2(metrics.Location.Left, metrics.Location.Top); pointer.X += metrics.Advance.X; short startVertex = (short)_vertexBufferPointer; AddVertex(a, b, c, d); AddElement(startVertex, (short)(startVertex + 1), (short)(startVertex + 2), startVertex, (short)(startVertex + 2), (short)(startVertex + 3)); } { QuikDrawCall call = CallTemplate; call.Texture = texture; call.Offset = (short) (startElement * 2); call.Count = (short)(_elementBufferPointer - startElement); DrawCalls.Add(call); } } private void RenderTextTypeset(QuikCommandEmitText text) { short startElement = (short)_elementBufferPointer; TypesetGroup group = text.Group; QuikVertex vertex = new QuikVertex() { Color = new QuikColor(0x000000ff) }; IQuikTexture texture = null; group.SortBy(TypesetGroup.SortByTexture); foreach (TypesetCharacter chr in group) { if (texture == null) { texture = chr.Texture; } else if (texture != chr.Texture) { EmitCall(); startElement = (short)_elementBufferPointer; texture = chr.Texture; CallTemplate.ClearStencil = false; } QuikVertex a, b, c, d; a = b = c = d = vertex; a.Position = new QuikVec2(chr.Position.Left, chr.Position.Bottom) + text.Offset; b.Position = new QuikVec2(chr.Position.Right, chr.Position.Bottom) + text.Offset; c.Position = new QuikVec2(chr.Position.Right, chr.Position.Top) + text.Offset; d.Position = new QuikVec2(chr.Position.Left, chr.Position.Top) + text.Offset; a.TextureCoordinates = new QuikVec2(chr.UV.Left, chr.UV.Bottom); b.TextureCoordinates = new QuikVec2(chr.UV.Right, chr.UV.Bottom); c.TextureCoordinates = new QuikVec2(chr.UV.Right, chr.UV.Top); d.TextureCoordinates = new QuikVec2(chr.UV.Left, chr.UV.Top); short startVertex = (short)_vertexBufferPointer; AddVertex(a, b, c, d); AddElement(startVertex, (short)(startVertex + 1), (short)(startVertex + 2), startVertex, (short)(startVertex + 2), (short)(startVertex + 3)); } EmitCall(); void EmitCall() { QuikDrawCall call = CallTemplate; call.Texture = texture; call.Offset = (short)(startElement * 2); call.Count = (short)(_elementBufferPointer - startElement); DrawCalls.Add(call); } } #endregion } public delegate void VertexGeneratorCommandHandler(QuikVertexGenerator generator, QuikCommand command, ref bool anyCalls); public enum QuikRenderTarget { Color, Stencil } public struct QuikDrawCall { public QuikRenderTarget Target; public short Offset; public short Count; public QuikRectangle Bounds; public bool ClearStencil; public IQuikTexture Texture; } }