blob: 046c4aa3e1c072153eaff5712a545565886c8383 [file] [log] [blame] [edit]
/*
* Copyright 2022 Rive
*/
#ifdef @ENABLE_ADVANCED_BLEND
// If advanced blend is enabled, we generate unmultiplied paint colors in the
// shader. Otherwise we would have to just turn around and unmultiply them in
// order to run the blend equation.
#define GENERATE_PREMULTIPLIED_PAINT_COLORS !@ENABLE_ADVANCED_BLEND
#else
// As long as advanced blend is not enabled, it's more efficient for the shader
// to generate premultiplied paint colors from the start.
#define GENERATE_PREMULTIPLIED_PAINT_COLORS true
#endif
#ifdef @VERTEX
ATTR_BLOCK_BEGIN(Attrs)
#ifdef @DRAW_INTERIOR_TRIANGLES
ATTR(0, packed_float3, @a_triangleVertex);
#else
ATTR(0,
float4,
@a_patchVertexData); // [localVertexID, outset, fillCoverage, vertexType]
ATTR(1, float4, @a_mirroredVertexData);
#endif
ATTR_BLOCK_END
#endif
VARYING_BLOCK_BEGIN
NO_PERSPECTIVE VARYING(0, float4, v_paint);
#ifdef @ATLAS_BLIT
NO_PERSPECTIVE VARYING(1, float2, v_atlasCoord);
#elif !defined(@RENDER_MODE_MSAA)
#ifdef @DRAW_INTERIOR_TRIANGLES
@OPTIONALLY_FLAT VARYING(1, half, v_windingWeight);
#elif defined(@ENABLE_FEATHER)
NO_PERSPECTIVE VARYING(2, float4, v_coverages);
#else
NO_PERSPECTIVE VARYING(2, half2, v_coverages);
#endif //@DRAW_INTERIOR_TRIANGLES
@OPTIONALLY_FLAT VARYING(3, half, v_pathID);
#endif // !@RENDER_MODE_MSAA
#ifdef @ENABLE_CLIPPING
@OPTIONALLY_FLAT VARYING(4, half2, v_clipIDs); // [clipID, outerClipID]
#endif
#ifdef @ENABLE_CLIP_RECT
NO_PERSPECTIVE VARYING(5, float4, v_clipRect);
#endif
#ifdef @ENABLE_ADVANCED_BLEND
@OPTIONALLY_FLAT VARYING(6, half, v_blendMode);
#endif
VARYING_BLOCK_END
#ifdef @VERTEX
VERTEX_MAIN(@drawVertexMain, Attrs, attrs, _vertexID, _instanceID)
{
#ifdef @DRAW_INTERIOR_TRIANGLES
ATTR_UNPACK(_vertexID, attrs, @a_triangleVertex, float3);
#else
ATTR_UNPACK(_vertexID, attrs, @a_patchVertexData, float4);
ATTR_UNPACK(_vertexID, attrs, @a_mirroredVertexData, float4);
#endif
VARYING_INIT(v_paint, float4);
#ifdef @ATLAS_BLIT
VARYING_INIT(v_atlasCoord, float2);
#elif !defined(@RENDER_MODE_MSAA)
#ifdef @DRAW_INTERIOR_TRIANGLES
VARYING_INIT(v_windingWeight, half);
#elif defined(@ENABLE_FEATHER)
VARYING_INIT(v_coverages, float4);
#else
VARYING_INIT(v_coverages, half2);
#endif //@DRAW_INTERIOR_TRIANGLES
VARYING_INIT(v_pathID, half);
#endif // !@RENDER_MODE_MSAA
#ifdef @ENABLE_CLIPPING
VARYING_INIT(v_clipIDs, half2);
#endif
#ifdef @ENABLE_CLIP_RECT
VARYING_INIT(v_clipRect, float4);
#endif
#ifdef @ENABLE_ADVANCED_BLEND
VARYING_INIT(v_blendMode, half);
#endif
bool shouldDiscardVertex = false;
uint pathID;
float2 vertexPosition;
#ifdef @RENDER_MODE_MSAA
ushort pathZIndex;
#endif
#ifdef @ATLAS_BLIT
vertexPosition =
unpack_atlas_coverage_vertex(@a_triangleVertex,
pathID,
#ifdef @RENDER_MODE_MSAA
pathZIndex,
#endif
v_atlasCoord VERTEX_CONTEXT_UNPACK);
#elif defined(@DRAW_INTERIOR_TRIANGLES)
vertexPosition = unpack_interior_triangle_vertex(@a_triangleVertex,
pathID
#ifdef @RENDER_MODE_MSAA
,
pathZIndex
#else
,
v_windingWeight
#endif
VERTEX_CONTEXT_UNPACK);
#else // !@DRAW_INTERIOR_TRIANGLES
float4 coverages;
shouldDiscardVertex =
!unpack_tessellated_path_vertex(@a_patchVertexData,
@a_mirroredVertexData,
_instanceID,
pathID,
vertexPosition
#ifndef @RENDER_MODE_MSAA
,
coverages
#else
,
pathZIndex
#endif
VERTEX_CONTEXT_UNPACK);
#ifndef @RENDER_MODE_MSAA
#ifdef @ENABLE_FEATHER
v_coverages = coverages;
#else
v_coverages.xy = cast_float2_to_half2(coverages.xy);
#endif
#endif
#endif // !DRAW_INTERIOR_TRIANGLES
uint2 paintData = STORAGE_BUFFER_LOAD2(@paintBuffer, pathID);
#if !defined(@ATLAS_BLIT) && !defined(@RENDER_MODE_MSAA)
// Encode the integral pathID as a "half" that we know the hardware will see
// as a unique value in the fragment shader.
v_pathID = id_bits_to_f16(pathID, uniforms.pathIDGranularity);
// Indicate even-odd fill rule by making pathID negative.
if ((paintData.x & PAINT_FLAG_EVEN_ODD_FILL) != 0u)
v_pathID = -v_pathID;
#endif // !@ATLAS_BLIT && !@RENDER_MODE_MSAA
uint paintType = paintData.x & 0xfu;
#ifdef @ENABLE_CLIPPING
if (@ENABLE_CLIPPING)
{
uint clipIDBits =
(paintType == CLIP_UPDATE_PAINT_TYPE ? paintData.y : paintData.x) >>
16;
half clipID = id_bits_to_f16(clipIDBits, uniforms.pathIDGranularity);
// Negative clipID means to update the clip buffer instead of the color
// buffer.
if (paintType == CLIP_UPDATE_PAINT_TYPE)
clipID = -clipID;
v_clipIDs.x = clipID;
}
#endif
#ifdef @ENABLE_ADVANCED_BLEND
if (@ENABLE_ADVANCED_BLEND)
{
v_blendMode = float((paintData.x >> 4) & 0xfu);
}
#endif
// Paint matrices operate on the fragment shader's "_fragCoord", which is
// bottom-up in GL.
float2 fragCoord = vertexPosition;
#ifdef @FRAMEBUFFER_BOTTOM_UP
fragCoord.y = float(uniforms.renderTargetHeight) - fragCoord.y;
#endif
#ifdef @ENABLE_CLIP_RECT
if (@ENABLE_CLIP_RECT)
{
// clipRectInverseMatrix transforms from pixel coordinates to a space
// where the clipRect is the normalized rectangle: [-1, -1, 1, 1].
float2x2 clipRectInverseMatrix = make_float2x2(
STORAGE_BUFFER_LOAD4(@paintAuxBuffer, pathID * 4u + 2u));
float4 clipRectInverseTranslate =
STORAGE_BUFFER_LOAD4(@paintAuxBuffer, pathID * 4u + 3u);
#ifndef @RENDER_MODE_MSAA
v_clipRect =
find_clip_rect_coverage_distances(clipRectInverseMatrix,
clipRectInverseTranslate.xy,
fragCoord);
#else // @RENDER_MODE_MSAA
set_clip_rect_plane_distances(clipRectInverseMatrix,
clipRectInverseTranslate.xy,
fragCoord);
#endif // @RENDER_MODE_MSAA
}
#endif // ENABLE_CLIP_RECT
// Unpack the paint once we have a position.
if (paintType == SOLID_COLOR_PAINT_TYPE)
{
half4 color = unpackUnorm4x8(paintData.y);
if (GENERATE_PREMULTIPLIED_PAINT_COLORS)
color.rgb *= color.a;
v_paint = float4(color);
}
#ifdef @ENABLE_CLIPPING
else if (@ENABLE_CLIPPING && paintType == CLIP_UPDATE_PAINT_TYPE)
{
half outerClipID =
id_bits_to_f16(paintData.x >> 16, uniforms.pathIDGranularity);
v_clipIDs.y = outerClipID;
}
#endif
else
{
float2x2 paintMatrix =
make_float2x2(STORAGE_BUFFER_LOAD4(@paintAuxBuffer, pathID * 4u));
float4 paintTranslate =
STORAGE_BUFFER_LOAD4(@paintAuxBuffer, pathID * 4u + 1u);
float2 paintCoord = MUL(paintMatrix, fragCoord) + paintTranslate.xy;
if (paintType == LINEAR_GRADIENT_PAINT_TYPE ||
paintType == RADIAL_GRADIENT_PAINT_TYPE)
{
// v_paint.a contains "-row" of the gradient ramp at texel center,
// in normalized space.
v_paint.a = -uintBitsToFloat(paintData.y);
// abs(v_paint.b) contains either:
// - 2 if the gradient ramp spans an entire row.
// - x0 of the gradient ramp in normalized space, if it's a simple
// 2-texel ramp.
float gradientSpan = paintTranslate.z;
// gradientSpan is either ~1 (complex gradients span the whole width
// of the texture minus 1px), or 1/GRAD_TEXTURE_WIDTH (simple
// gradients span 1px).
if (gradientSpan > .9)
{
// Complex ramps span an entire row. Set it to 2 to convey this.
v_paint.b = 2.;
}
else
{
// This is a simple ramp.
v_paint.b = paintTranslate.w;
}
if (paintType == LINEAR_GRADIENT_PAINT_TYPE)
{
// The paint is a linear gradient.
v_paint.g = .0;
v_paint.r = paintCoord.x;
}
else
{
// The paint is a radial gradient. Mark v_paint.b negative to
// indicate this to the fragment shader. (v_paint.b can't be
// zero because the gradient ramp is aligned on pixel centers,
// so negating it will always produce a negative number.)
v_paint.b = -v_paint.b;
v_paint.rg = paintCoord.xy;
}
}
else // IMAGE_PAINT_TYPE
{
// v_paint.a <= -1. signals that the paint is an image.
// -v_paint.a - 2 is the texture mipmap level-of-detail.
// v_paint.b is the image opacity.
// v_paint.rg is the normalized image texture coordinate (built into
// the paintMatrix).
float opacity = uintBitsToFloat(paintData.y);
float lod = paintTranslate.z;
v_paint = float4(paintCoord.x, paintCoord.y, opacity, -2. - lod);
}
}
float4 pos;
if (!shouldDiscardVertex)
{
pos = RENDER_TARGET_COORD_TO_CLIP_COORD(vertexPosition);
#ifdef @POST_INVERT_Y
pos.y = -pos.y;
#endif
#ifdef @RENDER_MODE_MSAA
pos.z = normalize_z_index(pathZIndex);
#endif
}
else
{
pos = float4(uniforms.vertexDiscardValue,
uniforms.vertexDiscardValue,
uniforms.vertexDiscardValue,
uniforms.vertexDiscardValue);
}
VARYING_PACK(v_paint);
#ifdef @ATLAS_BLIT
VARYING_PACK(v_atlasCoord);
#elif !defined(@RENDER_MODE_MSAA)
#ifdef @DRAW_INTERIOR_TRIANGLES
VARYING_PACK(v_windingWeight);
#elif defined(@ENABLE_FEATHER)
VARYING_PACK(v_coverages);
#else
VARYING_PACK(v_coverages);
#endif //@DRAW_INTERIOR_TRIANGLES
VARYING_PACK(v_pathID);
#endif // !@RENDER_MODE_MSAA
#ifdef @ENABLE_CLIPPING
VARYING_PACK(v_clipIDs);
#endif
#ifdef @ENABLE_CLIP_RECT
VARYING_PACK(v_clipRect);
#endif
#ifdef @ENABLE_ADVANCED_BLEND
VARYING_PACK(v_blendMode);
#endif
EMIT_VERTEX(pos);
}
#endif
#ifdef @FRAGMENT
FRAG_STORAGE_BUFFER_BLOCK_BEGIN
FRAG_STORAGE_BUFFER_BLOCK_END
INLINE half4 find_paint_color(float4 paint,
float coverage FRAGMENT_CONTEXT_DECL)
{
half4 color;
if (paint.a >= .0) // Is the paint a solid color?
{
// The vertex shader will have premultiplied 'paint' (or not) based on
// GENERATE_PREMULTIPLIED_PAINT_COLORS.
color = cast_float4_to_half4(paint);
if (GENERATE_PREMULTIPLIED_PAINT_COLORS)
color *= coverage;
else
color.a *= coverage;
}
else if (paint.a > -1.) // Is paint is a gradient (linear or radial)?
{
float t =
paint.b > .0 ? /*linear*/ paint.r : /*radial*/ length(paint.rg);
t = clamp(t, .0, 1.);
float span = abs(paint.b);
float x = span > 1.
? /*entire row*/ (1. - 1. / GRAD_TEXTURE_WIDTH) * t +
(.5 / GRAD_TEXTURE_WIDTH)
: /*two texels*/ (1. / GRAD_TEXTURE_WIDTH) * t + span;
float row = -paint.a;
// Our gradient texture is not mipmapped. Issue a texture-sample that
// explicitly does not find derivatives for LOD computation.
color =
TEXTURE_SAMPLE_LOD(@gradTexture, gradSampler, float2(x, row), .0);
color.a *= coverage;
// Gradients are always unmultiplied so we don't lose color data while
// doing the hardware filter.
if (GENERATE_PREMULTIPLIED_PAINT_COLORS)
color.rgb *= color.a;
}
else // The paint is an image.
{
half lod = -paint.a - 2.;
color = TEXTURE_SAMPLE_LOD(@imageTexture, imageSampler, paint.rg, lod);
half opacity = paint.b * coverage;
// Images are always premultiplied so the (transparent) background color
// doesn't bleed into the edges during the hardware filter.
if (GENERATE_PREMULTIPLIED_PAINT_COLORS)
color *= opacity;
else
color = make_half4(unmultiply_rgb(color), color.a * opacity);
}
return color;
}
#ifndef @RENDER_MODE_MSAA
PLS_BLOCK_BEGIN
PLS_DECL4F(COLOR_PLANE_IDX, colorBuffer);
PLS_DECLUI(CLIP_PLANE_IDX, clipBuffer);
PLS_DECL4F(SCRATCH_COLOR_PLANE_IDX, scratchColorBuffer);
PLS_DECLUI(COVERAGE_PLANE_IDX, coverageCountBuffer);
PLS_BLOCK_END
PLS_MAIN(@drawFragmentMain)
{
VARYING_UNPACK(v_paint, float4);
#ifdef @ATLAS_BLIT
VARYING_UNPACK(v_atlasCoord, float2);
#elif !defined(@RENDER_MODE_MSAA)
#ifdef @DRAW_INTERIOR_TRIANGLES
VARYING_UNPACK(v_windingWeight, half);
#elif defined(@ENABLE_FEATHER)
VARYING_UNPACK(v_coverages, float4);
#else
VARYING_UNPACK(v_coverages, half2);
#endif //@DRAW_INTERIOR_TRIANGLES
VARYING_UNPACK(v_pathID, half);
#endif // !@RENDER_MODE_MSAA
#ifdef @ENABLE_CLIPPING
VARYING_UNPACK(v_clipIDs, half2);
#endif
#ifdef @ENABLE_CLIP_RECT
VARYING_UNPACK(v_clipRect, float4);
#endif
#ifdef @ENABLE_ADVANCED_BLEND
VARYING_UNPACK(v_blendMode, half);
#endif
#if !defined(@DRAW_INTERIOR_TRIANGLES) || defined(@ATLAS_BLIT)
// Interior triangles don't overlap, so don't need raster ordering.
PLS_INTERLOCK_BEGIN;
#endif
half coverage;
#ifdef @ATLAS_BLIT
coverage = filter_feather_atlas(
v_atlasCoord,
uniforms.atlasTextureInverseSize TEXTURE_CONTEXT_FORWARD);
#else
half2 coverageData = unpackHalf2x16(PLS_LOADUI(coverageCountBuffer));
half coverageBufferID = coverageData.g;
half coverageCount =
coverageBufferID == v_pathID ? coverageData.r : make_half(.0);
#ifdef @DRAW_INTERIOR_TRIANGLES
coverageCount += v_windingWeight;
PLS_PRESERVE_UI(coverageCountBuffer);
#else
if (is_stroke(v_coverages))
{
half fragCoverage;
#ifdef @ENABLE_FEATHER
if (@ENABLE_FEATHER && is_feathered_stroke(v_coverages))
{
fragCoverage =
eval_feathered_stroke(v_coverages TEXTURE_CONTEXT_FORWARD);
}
else
#endif // @ENABLE_FEATHER
{
fragCoverage = min(v_coverages.x, v_coverages.y);
}
coverageCount = max(fragCoverage, coverageCount);
}
else // Fill. (Back-face culling handles the sign of v_coverages.x.)
{
half fragCoverage;
#if defined(@ENABLE_FEATHER)
if (@ENABLE_FEATHER && is_feathered_fill(v_coverages))
{
fragCoverage =
eval_feathered_fill(v_coverages TEXTURE_CONTEXT_FORWARD);
}
else
#endif // @CLOCKWISE_FILL && @ENABLE_FEATHER
{
fragCoverage = v_coverages.x;
}
coverageCount += fragCoverage;
}
// Save the updated coverage.
PLS_STOREUI(coverageCountBuffer,
packHalf2x16(make_half2(coverageCount, v_pathID)));
#endif // !@DRAW_INTERIOR_TRIANGLES
// Convert coverageCount to coverage.
#ifdef @CLOCKWISE_FILL
if (@CLOCKWISE_FILL)
{
#ifdef @VULKAN_VENDOR_ID
if (@VULKAN_VENDOR_ID == VULKAN_VENDOR_ARM)
{
// ARM hits a bug if we use clamp() here.
if (coverageCount < .0)
coverage = .0;
else if (coverageCount <= 1.)
coverage = coverageCount;
else
coverage = 1.;
}
else
#endif
{
coverage = clamp(coverageCount, make_half(.0), make_half(1.));
}
}
else
#endif // CLOCKWISE_FILL
{
coverage = abs(coverageCount);
#ifdef @ENABLE_EVEN_ODD
if (@ENABLE_EVEN_ODD && v_pathID < .0 /*even-odd*/)
{
coverage = 1. - make_half(abs(fract(coverage * .5) * 2. + -1.));
}
#endif
// This also caps stroke coverage, which can be >1.
coverage = min(coverage, make_half(1.));
}
#endif // !@ATLAS_BLIT
#ifdef @ENABLE_CLIPPING
if (@ENABLE_CLIPPING && v_clipIDs.x < .0) // Update the clip buffer.
{
half clipID = -v_clipIDs.x;
#ifdef @ENABLE_NESTED_CLIPPING
if (@ENABLE_NESTED_CLIPPING)
{
half outerClipID = v_clipIDs.y;
if (outerClipID != .0)
{
// This is a nested clip. Intersect coverage with the enclosing
// clip (outerClipID).
half2 clipData = unpackHalf2x16(PLS_LOADUI(clipBuffer));
half clipContentID = clipData.g;
half outerClipCoverage;
if (clipContentID != clipID)
{
// First hit: either clipBuffer contains outerClipCoverage,
// or this pixel is not inside the outer clip and
// outerClipCoverage is zero.
outerClipCoverage =
clipContentID == outerClipID ? clipData.r : .0;
#ifndef @DRAW_INTERIOR_TRIANGLES
// Stash outerClipCoverage before overwriting clipBuffer, in
// case we hit this pixel again and need it. (Not necessary
// when drawing interior triangles because they always go
// last and don't overlap.)
PLS_STORE4F(scratchColorBuffer,
make_half4(outerClipCoverage, .0, .0, .0));
#endif
}
else
{
// Subsequent hit: outerClipCoverage is stashed in
// scratchColorBuffer.
outerClipCoverage = PLS_LOAD4F(scratchColorBuffer).r;
#ifndef @DRAW_INTERIOR_TRIANGLES
// Since interior triangles are always last, there's no need
// to preserve this value.
PLS_PRESERVE_4F(scratchColorBuffer);
#endif
}
coverage = min(coverage, outerClipCoverage);
}
}
#endif // @ENABLE_NESTED_CLIPPING
PLS_STOREUI(clipBuffer, packHalf2x16(make_half2(coverage, clipID)));
PLS_PRESERVE_4F(colorBuffer);
}
else // Render to the main framebuffer.
#endif // @ENABLE_CLIPPING
{
#ifdef @ENABLE_CLIPPING
if (@ENABLE_CLIPPING)
{
// Apply the clip.
half clipID = v_clipIDs.x;
if (clipID != .0)
{
// Clip IDs are not necessarily drawn in monotonically
// increasing order, so always check exact equality of the
// clipID.
half2 clipData = unpackHalf2x16(PLS_LOADUI(clipBuffer));
half clipContentID = clipData.g;
coverage = (clipContentID == clipID) ? min(clipData.r, coverage)
: make_half(.0);
}
}
#endif
#ifdef @ENABLE_CLIP_RECT
if (@ENABLE_CLIP_RECT)
{
half clipRectCoverage = min_value(cast_float4_to_half4(v_clipRect));
coverage = clamp(clipRectCoverage, make_half(.0), coverage);
}
#endif // ENABLE_CLIP_RECT
half4 color =
find_paint_color(v_paint, coverage FRAGMENT_CONTEXT_UNPACK);
half4 dstColorPremul;
#ifdef @ATLAS_BLIT
dstColorPremul = PLS_LOAD4F(colorBuffer);
#else
if (coverageBufferID != v_pathID)
{
// This is the first fragment from pathID to touch this pixel.
dstColorPremul = PLS_LOAD4F(colorBuffer);
#ifndef @DRAW_INTERIOR_TRIANGLES
// We don't need to store coverage when drawing interior triangles
// because they always go last and don't overlap, so every fragment
// is the final one in the path.
PLS_STORE4F(scratchColorBuffer, dstColorPremul);
#endif
}
else
{
dstColorPremul = PLS_LOAD4F(scratchColorBuffer);
#ifndef @DRAW_INTERIOR_TRIANGLES
// Since interior triangles are always last, there's no need to
// preserve this value.
PLS_PRESERVE_4F(scratchColorBuffer);
#endif
}
#endif // @ATLAS_BLIT
// Blend with the framebuffer color.
#ifdef @ENABLE_ADVANCED_BLEND
if (@ENABLE_ADVANCED_BLEND)
{
// GENERATE_PREMULTIPLIED_PAINT_COLORS is false in this case because
// advanced blend needs unmultiplied colors.
if (v_blendMode != cast_uint_to_half(BLEND_SRC_OVER))
{
color.rgb =
advanced_color_blend(color.rgb,
dstColorPremul,
cast_half_to_ushort(v_blendMode));
}
// Premultiply alpha now.
color.rgb *= color.a;
}
#endif
color += dstColorPremul * (1. - color.a);
PLS_STORE4F(colorBuffer, color);
PLS_PRESERVE_UI(clipBuffer);
}
#if !defined(@DRAW_INTERIOR_TRIANGLES) || defined(@ATLAS_BLIT)
// Interior triangles don't overlap, so don't need raster ordering.
PLS_INTERLOCK_END;
#endif
EMIT_PLS;
}
#else // @RENDER_MODE_MSAA
FRAG_DATA_MAIN(half4, @drawFragmentMain)
{
VARYING_UNPACK(v_paint, float4);
#ifdef @ATLAS_BLIT
VARYING_UNPACK(v_atlasCoord, float2);
#endif
#ifdef @ENABLE_ADVANCED_BLEND
VARYING_UNPACK(v_blendMode, half);
#endif
half coverage =
#ifdef @ATLAS_BLIT
filter_feather_atlas(
v_atlasCoord,
uniforms.atlasTextureInverseSize TEXTURE_CONTEXT_FORWARD);
#else
1.;
#endif
half4 color = find_paint_color(v_paint, coverage FRAGMENT_CONTEXT_UNPACK);
#ifdef @ENABLE_ADVANCED_BLEND
if (@ENABLE_ADVANCED_BLEND)
{
// GENERATE_PREMULTIPLIED_PAINT_COLORS is false in this case because
// advanced blend needs unmultiplied colors.
half4 dstColorPremul =
TEXEL_FETCH(@dstColorTexture, int2(floor(_fragCoord.xy)));
color = advanced_blend(color,
dstColorPremul,
cast_half_to_ushort(v_blendMode));
}
#endif // ENABLE_ADVANCED_BLEND
EMIT_FRAG_DATA(color);
}
#endif // !RENDER_MODE_MSAA
#endif // FRAGMENT