blob: 2943bf11b0aa8e7ac6cceb26458ec7815b930a7b [file]
/*
* Copyright 2023 Rive
*/
// Common functions shared by draw shaders.
#ifdef @VERTEX
VERTEX_TEXTURE_BLOCK_BEGIN
TEXTURE_RGBA32UI(PER_FLUSH_BINDINGS_SET,
TESS_VERTEX_TEXTURE_IDX,
@tessVertexTexture);
VERTEX_TEXTURE_BLOCK_END
VERTEX_STORAGE_BUFFER_BLOCK_BEGIN
STORAGE_BUFFER_U32x4(PATH_BUFFER_IDX, PathBuffer, @pathBuffer);
STORAGE_BUFFER_U32x2(PAINT_BUFFER_IDX, PaintBuffer, @paintBuffer);
STORAGE_BUFFER_F32x4(PAINT_AUX_BUFFER_IDX, PaintAuxBuffer, @paintAuxBuffer);
STORAGE_BUFFER_U32x4(CONTOUR_BUFFER_IDX, ContourBuffer, @contourBuffer);
VERTEX_STORAGE_BUFFER_BLOCK_END
#ifdef @DRAW_PATH
INLINE int2 tess_texel_coord(int texelIndex)
{
return int2(texelIndex & ((1 << TESS_TEXTURE_WIDTH_LOG2) - 1),
texelIndex >> TESS_TEXTURE_WIDTH_LOG2);
}
INLINE float manhattan_pixel_width(float2x2 M, float2 normalized)
{
float2 v = MUL(M, normalized);
return (abs(v.x) + abs(v.y)) * (1. / dot(v, v));
}
INLINE bool unpack_tessellated_path_vertex(float4 patchVertexData,
float4 mirroredVertexData,
int _instanceID,
OUT(uint) outPathID,
OUT(float2) outVertexPosition
#ifndef @RENDER_MODE_MSAA
,
OUT(half2) outEdgeDistance
#else
,
OUT(ushort) outPathZIndex
#endif
VERTEX_CONTEXT_DECL)
{
// Unpack patchVertexData.
int localVertexID = int(patchVertexData.x);
float outset = patchVertexData.y;
float fillCoverage = patchVertexData.z;
int patchSegmentSpan = floatBitsToInt(patchVertexData.w) >> 2;
int vertexType = floatBitsToInt(patchVertexData.w) & 3;
// Fetch a vertex that definitely belongs to the contour we're drawing.
int vertexIDOnContour = min(localVertexID, patchSegmentSpan - 1);
int tessVertexIdx = _instanceID * patchSegmentSpan + vertexIDOnContour;
uint4 tessVertexData =
TEXEL_FETCH(@tessVertexTexture, tess_texel_coord(tessVertexIdx));
uint contourIDWithFlags = tessVertexData.w;
// Fetch and unpack the contour referenced by the tessellation vertex.
uint4 contourData =
STORAGE_BUFFER_LOAD4(@contourBuffer,
contour_data_idx(contourIDWithFlags));
float2 midpoint = uintBitsToFloat(contourData.xy);
outPathID = contourData.z & 0xffffu;
uint vertexIndex0 = contourData.w;
// Fetch and unpack the path.
float2x2 M = make_float2x2(
uintBitsToFloat(STORAGE_BUFFER_LOAD4(@pathBuffer, outPathID * 4u)));
uint4 pathData = STORAGE_BUFFER_LOAD4(@pathBuffer, outPathID * 4u + 1u);
float2 translate = uintBitsToFloat(pathData.xy);
float strokeRadius = uintBitsToFloat(pathData.z);
#ifdef @RENDER_MODE_MSAA
outPathZIndex = cast_uint_to_ushort(pathData.w);
#endif
// Fix the tessellation vertex if we fetched the wrong one in order to
// guarantee we got the correct contour ID and flags, or if we belong to a
// mirrored contour and this vertex has an alternate position when mirrored.
uint mirroredContourFlag =
contourIDWithFlags & MIRRORED_CONTOUR_CONTOUR_FLAG;
if (mirroredContourFlag != 0u)
{
localVertexID = int(mirroredVertexData.x);
outset = mirroredVertexData.y;
fillCoverage = mirroredVertexData.z;
}
if (localVertexID != vertexIDOnContour)
{
// This can peek one vertex before or after the contour, but the
// tessellator guarantees there is always at least one padding vertex at
// the beginning and end of the data.
tessVertexIdx += localVertexID - vertexIDOnContour;
uint4 replacementTessVertexData =
TEXEL_FETCH(@tessVertexTexture, tess_texel_coord(tessVertexIdx));
if ((replacementTessVertexData.w &
(MIRRORED_CONTOUR_CONTOUR_FLAG | 0xffffu)) !=
(contourIDWithFlags & (MIRRORED_CONTOUR_CONTOUR_FLAG | 0xffffu)))
{
// We crossed over into a new contour. Either wrap to the first
// vertex in the contour or leave it clamped at the final vertex of
// the contour.
bool isClosed = strokeRadius == .0 || // filled
midpoint.x != .0; // explicity closed stroke
if (isClosed)
{
tessVertexData =
TEXEL_FETCH(@tessVertexTexture,
tess_texel_coord(int(vertexIndex0)));
}
}
else
{
tessVertexData = replacementTessVertexData;
}
// MIRRORED_CONTOUR_CONTOUR_FLAG is not preserved at vertexIndex0.
// Preserve it here. By not preserving this flag, the normal and
// mirrored contour can both share the same contour record.
contourIDWithFlags =
(tessVertexData.w & ~MIRRORED_CONTOUR_CONTOUR_FLAG) |
mirroredContourFlag;
}
// Finish unpacking tessVertexData.
float theta = uintBitsToFloat(tessVertexData.z);
float2 norm = float2(sin(theta), -cos(theta));
float2 origin = uintBitsToFloat(tessVertexData.xy);
float2 postTransformVertexOffset;
if (strokeRadius != .0) // Is this a stroke?
{
// Ensure strokes always emit clockwise triangles.
outset *= sign(determinant(M));
// Joins only emanate from the outer side of the stroke.
if ((contourIDWithFlags & LEFT_JOIN_CONTOUR_FLAG) != 0u)
outset = min(outset, .0);
if ((contourIDWithFlags & RIGHT_JOIN_CONTOUR_FLAG) != 0u)
outset = max(outset, .0);
float aaRadius = manhattan_pixel_width(M, norm) * AA_RADIUS;
half globalCoverage = 1.;
if (aaRadius > strokeRadius)
{
// The stroke is narrower than the AA ramp. Instead of emitting
// subpixel geometry, make the stroke as wide as the AA ramp and
// apply a global coverage multiplier.
globalCoverage =
cast_float_to_half(strokeRadius) / cast_float_to_half(aaRadius);
strokeRadius = aaRadius;
}
// Extend the vertex by half the width of the AA ramp.
float2 vertexOffset =
MUL(norm, strokeRadius + aaRadius); // Bloat stroke width for AA.
#ifndef @RENDER_MODE_MSAA
// Calculate the AA distance to both the outset and inset edges of the
// stroke. The fragment shader will use whichever is lesser.
float x = outset * (strokeRadius + aaRadius);
outEdgeDistance = cast_float2_to_half2(
(1. / (aaRadius * 2.)) * (float2(x, -x) + strokeRadius) + .5);
#endif
uint joinType = contourIDWithFlags & JOIN_TYPE_MASK;
if (joinType != 0u)
{
// This vertex belongs to a miter or bevel join. Begin by finding
// the bisector, which is the same as the miter line. The first two
// vertices in the join peek forward to figure out the bisector, and
// the final two peek backward.
int peekDir = 2;
if ((contourIDWithFlags & JOIN_TANGENT_0_CONTOUR_FLAG) == 0u)
peekDir = -peekDir;
if ((contourIDWithFlags & MIRRORED_CONTOUR_CONTOUR_FLAG) != 0u)
peekDir = -peekDir;
int2 otherJoinTexelCoord =
tess_texel_coord(tessVertexIdx + peekDir);
uint4 otherJoinData =
TEXEL_FETCH(@tessVertexTexture, otherJoinTexelCoord);
float otherJoinTheta = uintBitsToFloat(otherJoinData.z);
float joinAngle = abs(otherJoinTheta - theta);
if (joinAngle > PI)
joinAngle = 2. * PI - joinAngle;
bool isTan0 =
(contourIDWithFlags & JOIN_TANGENT_0_CONTOUR_FLAG) != 0u;
bool isLeftJoin =
(contourIDWithFlags & LEFT_JOIN_CONTOUR_FLAG) != 0u;
float bisectTheta =
joinAngle * (isTan0 == isLeftJoin ? -.5 : .5) + theta;
float2 bisector = float2(sin(bisectTheta), -cos(bisectTheta));
float bisectPixelWidth = manhattan_pixel_width(M, bisector);
// Generalize everything to a "miter-clip", which is proposed in the
// SVG-2 draft. Bevel joins are converted to miter-clip joins with a
// miter limit of 1/2 pixel. They technically bleed out 1/2 pixel
// when drawn this way, but they seem to look fine and there is not
// an obvious solution to antialias them without an ink bleed.
float miterRatio = cos(joinAngle * .5);
float clipRadius;
if ((joinType == MITER_CLIP_JOIN_CONTOUR_FLAG) ||
(joinType == MITER_REVERT_JOIN_CONTOUR_FLAG &&
miterRatio >= .25))
{
// Miter! (Or square cap.)
// We currently use hard coded miter limits:
// * 1 for square caps being emulated as miter-clip joins.
// * 4, which is the SVG default, for all other miter joins.
float miterInverseLimit =
(contourIDWithFlags & EMULATED_STROKE_CAP_CONTOUR_FLAG) !=
0u
? 1.
: .25;
clipRadius =
strokeRadius * (1. / max(miterRatio, miterInverseLimit));
}
else
{
// Bevel! (Or butt cap.)
clipRadius = strokeRadius * miterRatio +
/* 1/2px bleed! */ bisectPixelWidth * .5;
}
float clipAARadius = clipRadius + bisectPixelWidth * AA_RADIUS;
if ((contourIDWithFlags & JOIN_TANGENT_INNER_CONTOUR_FLAG) != 0u)
{
// Reposition the inner join vertices at the miter-clip
// positions. Leave the outer join vertices as duplicates on the
// surrounding curve endpoints. We emit duplicate vertex
// positions because we need a hard stop on the clip distance
// (see below).
//
// Use aaRadius here because we're tracking AA on the mitered
// edge, NOT the outer clip edge.
float strokeAARaidus = strokeRadius + aaRadius;
// clipAARadius must be 1/16 of an AA ramp (~1/16 pixel) longer
// than the miter length before we start clipping, to ensure we
// are solving for a numerically stable intersection.
float slop = aaRadius * .125;
if (strokeAARaidus <= clipAARadius * miterRatio + slop)
{
// The miter point is before the clip line. Extend out to
// the miter point.
float miterAARadius = strokeAARaidus * (1. / miterRatio);
vertexOffset = bisector * miterAARadius;
}
else
{
// The clip line is before the miter point. Find where the
// clip line and the mitered edge intersect.
float2 bisectAAOffset = bisector * clipAARadius;
float2 k = float2(dot(vertexOffset, vertexOffset),
dot(bisectAAOffset, bisectAAOffset));
vertexOffset =
MUL(k, inverse(float2x2(vertexOffset, bisectAAOffset)));
}
}
// The clip distance tells us how to antialias the outer clipped
// edge. Since joins only emanate from the outset side of the
// stroke, we can repurpose the inset distance as the clip distance.
float2 pt = abs(outset) * vertexOffset;
float clipDistance = (clipAARadius - dot(pt, bisector)) /
(bisectPixelWidth * (AA_RADIUS * 2.));
#ifndef @RENDER_MODE_MSAA
if ((contourIDWithFlags & LEFT_JOIN_CONTOUR_FLAG) != 0u)
outEdgeDistance.y = cast_float_to_half(clipDistance);
else
outEdgeDistance.x = cast_float_to_half(clipDistance);
#endif
}
#ifndef @RENDER_MODE_MSAA
outEdgeDistance *= globalCoverage;
// Bias outEdgeDistance.y slightly upwards in order to guarantee
// outEdgeDistance.y is >= 0 at every pixel. "outEdgeDistance.y < 0" is
// used to differentiate between strokes and fills.
outEdgeDistance.y = max(outEdgeDistance.y, make_half(1e-4));
#endif
postTransformVertexOffset = MUL(M, outset * vertexOffset);
// Throw away the fan triangles since we're a stroke.
if (vertexType != STROKE_VERTEX)
return false;
}
else // This is a fill.
{
// Place the fan point.
if (vertexType == FAN_MIDPOINT_VERTEX)
origin = midpoint;
// Offset the vertex for Manhattan AA.
postTransformVertexOffset =
sign(MUL(outset * norm, inverse(M))) * AA_RADIUS;
if (bool(contourIDWithFlags & MIRRORED_CONTOUR_CONTOUR_FLAG) !=
bool(contourIDWithFlags & NEGATE_PATH_FILL_COVERAGE_FLAG))
{
fillCoverage = -fillCoverage;
}
#ifndef @RENDER_MODE_MSAA
// "outEdgeDistance.y < 0" indicates to the fragment shader that this is
// a fill.
outEdgeDistance = make_half2(fillCoverage, -1.);
#endif
// If we're actually just drawing a triangle, throw away the entire
// patch except a single fan triangle.
if ((contourIDWithFlags & RETROFITTED_TRIANGLE_CONTOUR_FLAG) != 0u &&
vertexType != FAN_VERTEX)
return false;
}
outVertexPosition = MUL(M, origin) + postTransformVertexOffset + translate;
return true;
}
#endif // @DRAW_PATH
#ifdef @DRAW_INTERIOR_TRIANGLES
INLINE float2
unpack_interior_triangle_vertex(float3 triangleVertex,
OUT(uint) outPathID,
OUT(half) outWindingWeight VERTEX_CONTEXT_DECL)
{
outPathID = floatBitsToUint(triangleVertex.z) & 0xffffu;
float2x2 M = make_float2x2(
uintBitsToFloat(STORAGE_BUFFER_LOAD4(@pathBuffer, outPathID * 4u)));
uint4 pathData = STORAGE_BUFFER_LOAD4(@pathBuffer, outPathID * 4u + 1u);
float2 translate = uintBitsToFloat(pathData.xy);
outWindingWeight = cast_int_to_half(floatBitsToInt(triangleVertex.z) >> 16);
return MUL(M, triangleVertex.xy) + translate;
}
#endif // @DRAW_INTERIOR_TRIANGLES
#endif // @VERTEX