Files
voxel-game/assets/shaders/gui_text.slang

272 lines
9.2 KiB
Plaintext

import "includes/structs";
struct Varyings {
float4 positionCS : SV_Position;
nointerpolation uint instance;
float2 texCoordEM : U_TEXCOORD;
}
struct GuiText {
float4 color;
// Index into _GlyphData
uint glyph;
float2 bottomLeftSSPX;
float fontSizePX;
}
struct GlyphData {
float2 bottomLeftEM;
float2 sizeEM;
// Index into _BandData of the first vertical/horizontal band data.
// `bandPtr.x` refers to vertical bands and `bandPtr.y` refers to horizontal
// bands.
uint2 bandPtr;
// Number of vertical/horizontal bands. `bandCount.x` is the number of
// vertical bands and `bandCount.y` is the number of horizontal bands.
uint2 bandCount;
}
[[vk::binding(0, 0)]] ConstantBuffer<GlobalUniforms> _Global;
[[vk::binding(1, 0)]] StructuredBuffer<GuiText> _Instances;
[[vk::binding(0, 1)]] StructuredBuffer<GlyphData> _GlyphData;
[[vk::binding(1, 1)]] StructuredBuffer<half2> _ControlPoints;
[[vk::binding(2, 1)]] StructuredBuffer<uint> _BandData;
static const float2 VERTICES[4] = {
float2(0, 1),
float2(1, 1),
float2(0, 0),
float2(1, 0),
};
// Classify a quadratic Bézier curve based on the sign bits of the ray-relative
// Y coordinates. Returns `(t2 << 1) | t1` where `t1` and `t2` are bits
// signifying the root eligibility; `t1` possibly adding one to the winding
// number and `t2` possibly subtracting one from the winding number.
uint classifyQuadCurve(float y0, float y1, float y2)
{
// ┌─────┰────┬────┬────┰────┬────┐
// │class┃ y2 │ y1 │ y0 ┃ t2 │ t1 │
// ├─────╂────┼────┼────╂────┼────┤
// │ A ┃ 0 │ 0 │ 0 ┃ 0 │ 0 │
// │ B ┃ 0 │ 0 │ 1 ┃ 1 │ 0 │
// │ C ┃ 0 │ 1 │ 0 ┃ 1 │ 1 │
// │ D ┃ 0 │ 1 │ 1 ┃ 1 │ 0 │
// │ E ┃ 1 │ 0 │ 0 ┃ 0 │ 1 │
// │ F ┃ 1 │ 0 │ 1 ┃ 1 │ 1 │
// │ G ┃ 1 │ 1 │ 0 ┃ 0 │ 1 │
// │ H ┃ 1 │ 1 │ 1 ┃ 0 │ 0 │
// └─────┸────┴────┴────┸────┴────┘
//
// Lookup table constant:
// H G F E D C B A
// 00 01 11 01 10 11 10 00 = 0x1DB8
let sign0 = asuint(y0) >> 31;
let sign1 = asuint(y1) >> 31;
let sign2 = asuint(y2) >> 31;
let class = (sign0 << 0) | (sign1 << 1) | (sign2 << 2);
let shift = 2 * class;
return (0x2E74U >> shift) & 0b11U;
}
// Find intersections between a quadratic Bézier curve and `y = 0` line by
// solving a quadratic equation.
//
// Returns the X coordinates at witch the intersections occur for roots t1 and
// t2. Imaginary solutions to the quadratic equation will return double root at
// the global minimum.
float2 raycastQuadCurveHorizontally(float2 cp0, float2 cp1, float2 cp2)
{
static const float EPSILON = 1.0 / 65536.0;
// When solving for y = 0, we get a quadratic polynomial given by:
//
// at² - 2bt + c
//
// where:
// * a = cp0.y - 2 * cp1.y + cp2.y
// * b = cp0.y - cp1.y
// * c = cp0.y
//
// Let Δ = b² - ac
// Then:
//
// t1 = (b - sqrt(Δ)) / a
// t2 = (b + sqrt(Δ)) / a
let a = cp0 - 2.0 * cp1 + cp2;
let b = cp0 - cp1;
let c = cp0;
let Δ = sqrt(max(b.y * b.y - a.y * c.y, 0.0));
var t = (b.y + float2(-Δ, Δ)) / a.y;
// If nearly linear, solve -2bt + c = 0 directly
if (abs(a.y) < EPSILON) {
t = float2(0.5 * c.y / b.y);
}
return (a.x * t - 2.0 * b.x) * t + c.x;
}
// Find intersections between a quadratic Bézier curve and `x = 0` line by
// solving a quadratic equation.
//
// Returns the Y coordinates at witch the intersections occur for roots t1 and
// t2. Imaginary solutions to the quadratic equation will return double root at
// the global minimum.
float2 raycastQuadCurveVertically(float2 cp0, float2 cp1, float2 cp2)
{
static const float EPSILON = 1.0 / 65536.0;
let a = cp0 - 2.0 * cp1 + cp2;
let b = cp0 - cp1;
let c = cp0;
let Δ = sqrt(max(b.x * b.x - a.x * c.x, 0.0));
var t = (b.x + float2(-Δ, Δ)) / a.x;
if (abs(a.x) < EPSILON) {
t = float2(0.5 * c.x / b.x);
}
return (a.y * t - 2.0 * b.y) * t + c.y;
}
float calculateCoverage(float xCoverage, float yCoverage, float xWeight, float yWeight) {
static const float EPSILON = 1.0 / 65536.0;
var coverage = max(
abs(xCoverage * xWeight + yCoverage * yWeight) / max(xWeight + yWeight, EPSILON),
min(abs(xCoverage), abs(yCoverage)),
);
// non-zero fill rule
coverage = saturate(coverage);
return coverage;
}
[shader("vertex")]
Varyings vertexMain(uint vertexID : SV_VulkanVertexID, uint instanceID : SV_VulkanInstanceID) {
Varyings out;
let vertex = VERTICES[vertexID];
let instance = _Instances[instanceID];
let glyph = _GlyphData[instance.glyph];
let normal = 2.0 * vertex - 1.0;
let sizePX = instance.fontSizePX * glyph.sizeEM;
let positionSSPX = vertex * sizePX + instance.bottomLeftSSPX;
let dilatedPositionSSPX = positionSSPX + 0.5 * normal;
let dilatedPositionCS = float4(mul(_Global.matrixSSPXtoCS, float3(positionSSPX, 1.0)), 0.0, 1.0);
let texCoordEM = vertex * glyph.sizeEM + glyph.bottomLeftEM;
let dilatedTexCoordEM = texCoordEM + (0.5 / instance.fontSizePX) * normal;
out.positionCS = dilatedPositionCS;
out.instance = instanceID;
out.texCoordEM = dilatedTexCoordEM;
return out;
}
[shader("fragment")]
float4 fragmentMain(Varyings frag) : SV_Target {
let instance = _Instances[frag.instance];
let glyph = _GlyphData[instance.glyph];
let emPerPX = fwidth(frag.texCoordEM);
let pxPerEM = 1.0 / emPerPX;
let bandF = float2(glyph.bandCount) * (frag.texCoordEM - glyph.bottomLeftEM) / glyph.sizeEM;
let band = uint2(clamp(bandF, float2(0, 0), float2(glyph.bandCount - 1U)));
// --- HORIZONTAL BAND -----------------------------------------------------
var xCoverage = 0.0;
var xWeight = 0.0;
let horizontalBandPtr = glyph.bandPtr.y + band.y;
let horizontalBandData = _BandData[horizontalBandPtr];
let horizontalBandCurvePtr = horizontalBandPtr + (horizontalBandData >> 16);
let horizontalBandCurveCount = horizontalBandData & 0xFFFF;
for (uint horizontalBandCurveID = 0; horizontalBandCurveID < horizontalBandCurveCount; horizontalBandCurveID++) {
let cpPtr = _BandData[horizontalBandCurvePtr + horizontalBandCurveID];
let cp0 = float2(_ControlPoints[cpPtr]) - frag.texCoordEM;
let cp1 = float2(_ControlPoints[cpPtr + 1]) - frag.texCoordEM;
let cp2 = float2(_ControlPoints[cpPtr + 2]) - frag.texCoordEM;
if (max(max(cp0.x, cp1.x), cp2.x) * pxPerEM.x < -0.5) break;
let rootFlags = classifyQuadCurve(cp0.y, cp1.y, cp2.y);
if (rootFlags != 0U) {
let result = raycastQuadCurveHorizontally(cp0, cp1, cp2) * pxPerEM.x;
// consider t1 at X position result.x
if ((rootFlags & 0b01U) != 0U) {
xCoverage += saturate(result.x + 0.5);
xWeight = max(xWeight, saturate(1.0 - abs(result.x) * 2.0));
}
// consider t2 at X position result.y
if ((rootFlags & 0b10U) != 0U) {
xCoverage -= saturate(result.y + 0.5);
xWeight = max(xWeight, saturate(1.0 - abs(result.y) * 2.0));
}
}
}
// --- VERTICAL BAND -------------------------------------------------------
var yCoverage = 0.0;
var yWeight = 0.0;
let verticalBandPtr = glyph.bandPtr.x + band.x;
let verticalBandData = _BandData[verticalBandPtr];
let verticalBandCurvePtr = verticalBandPtr + (verticalBandData >> 16);
let verticalBandCurveCount = verticalBandData & 0xFFFF;
for (uint verticalBandCurveID = 0; verticalBandCurveID < verticalBandCurveCount; verticalBandCurveID++) {
let cpPtr = _BandData[verticalBandCurvePtr + verticalBandCurveID];
let cp0 = float2(_ControlPoints[cpPtr]) - frag.texCoordEM;
let cp1 = float2(_ControlPoints[cpPtr + 1]) - frag.texCoordEM;
let cp2 = float2(_ControlPoints[cpPtr + 2]) - frag.texCoordEM;
if (max(max(cp0.y, cp1.y), cp2.y) * pxPerEM.y < -0.5) break;
let rootFlags = classifyQuadCurve(cp0.x, cp1.x, cp2.x);
if (rootFlags != 0U) {
let result = raycastQuadCurveVertically(cp0, cp1, cp2) * pxPerEM.y;
// consider t1 at Y position result.x
if ((rootFlags & 0b01U) != 0U)
{
yCoverage -= saturate(result.x + 0.5);
yWeight = max(yWeight, saturate(1.0 - abs(result.x) * 2.0));
}
// consider t2 at Y position result.y
if ((rootFlags & 0b10U) != 0U)
{
yCoverage += saturate(result.y + 0.5);
yWeight = max(yWeight, saturate(1.0 - abs(result.y) * 2.0));
}
}
}
// --- RESOLVE -------------------------------------------------------------
let coverage = calculateCoverage(xCoverage, yCoverage, xWeight, yWeight) * instance.color.a;
return float4(
coverage * instance.color.rgb,
coverage,
);
}