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;
}
///
/// 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 no need to generate anything because the lines are the same???
}
}
///
/// 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);
}
}
public enum QuikRenderTarget
{
Color,
Stencil
}
public struct QuikDrawCall
{
public QuikRenderTarget Target;
public short Offset;
public short Count;
public QuikRectangle Bounds;
public bool ClearStencil;
}
}