slug-font-rendering by aradotso/trending-skills
npx skills add https://github.com/aradotso/trending-skills --skill slug-font-rendering技能来自 ara.so — Daily 2026 技能集。
Slug 是 Slug 字体渲染算法的一个参考实现 —— 这是一种 GPU 加速技术,用于在任意缩放比例下高质量抗锯齿地渲染矢量字体和字形。其工作原理是将字形轮廓编码为二次贝塞尔曲线和直线段的列表,然后在片段着色器中直接解析覆盖度,无需预光栅化纹理。
论文: JCGT 2017 — Slug 算法
博客(更新): Slug 十年记
许可证: MIT — 专利已贡献至公共领域。分发时需要注明出处。
Slug/
├── slug.hlsl # 核心片段着色器 — 覆盖度计算
├── band.hlsl # 用于字形渲染的基于条带的优化
├── curve.hlsl # 二次贝塞尔曲线和直线段求值
├── README.md
Slug 是一个参考实现 —— 你需要将 HLSL 着色器集成到你自己的渲染管线中。
git clone https://github.com/EricLengyel/Slug.git
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
将 .hlsl 文件复制到你的着色器目录,并在你的管线中包含它们:
#include "slug.hlsl"
#include "curve.hlsl"
你必须将字体轮廓(TrueType/OTF)预处理为 Slug 的曲线缓冲区格式:
GPU 缓冲区中的每个曲线条目存储:
// 直线段: p0, p1
// 二次贝塞尔曲线: p0, p1 (控制点), p2
struct CurveRecord
{
float2 p0; // 起点
float2 p1; // 控制点(或直线的终点)
float2 p2; // 终点(直线不使用 — 通过类型标志标识)
// 类型/标志单独编码或在填充中编码
};
字形边界框被划分为水平的条带。每个条带只存储与其相交的曲线,从而将每个片段的工作量从 O(所有曲线) 减少到 O(局部曲线)。
// 来自顶点着色器的输入
struct PS_Input
{
float4 position : SV_Position;
float2 glyphCoord : TEXCOORD0; // 字形/字体单位中的位置
// 条带索引或预计算的条带数据
nointerpolation uint bandOffset : TEXCOORD1;
nointerpolation uint curveCount : TEXCOORD2;
};
// 字形曲线数据缓冲区
StructuredBuffer<float4> CurveBuffer : register(t0);
float4 PS_Slug(PS_Input input) : SV_Target
{
float coverage = ComputeGlyphCoverage(
input.glyphCoord,
CurveBuffer,
input.bandOffset,
input.curveCount
);
// 预乘 Alpha 输出
float4 color = float4(textColor.rgb * coverage, coverage);
return color;
}
算法的核心 —— 从二次贝塞尔曲线计算有符号覆盖度:
// 评估二次贝塞尔曲线在点 p 处是否对覆盖度有贡献
// p0: 起点, p1: 控制点, p2: 终点
// 返回有符号覆盖度贡献值
float QuadraticBezierCoverage(float2 p, float2 p0, float2 p1, float2 p2)
{
// 变换到规范空间
float2 a = p1 - p0;
float2 b = p0 - 2.0 * p1 + p2;
// 找到贝塞尔曲线 Y == p.y 时的 t 值
float2 delta = p - p0;
float A = b.y;
float B = a.y;
float C = p0.y - p.y;
float coverage = 0.0;
if (abs(A) > 1e-6)
{
float disc = B * B - A * C;
if (disc >= 0.0)
{
float sqrtDisc = sqrt(disc);
float t0 = (-B - sqrtDisc) / A;
float t1 = (-B + sqrtDisc) / A;
// 对于 [0,1] 区间内每个有效的 t,计算 x 并检查环绕
if (t0 >= 0.0 && t0 <= 1.0)
{
float x = (A * t0 + 2.0 * B) * t0 + p0.x + delta.x;
// ... 累加有符号覆盖度
}
if (t1 >= 0.0 && t1 <= 1.0)
{
float x = (A * t1 + 2.0 * B) * t1 + p0.x + delta.x;
// ... 累加有符号覆盖度
}
}
}
else
{
// 退化为线性情况
float t = -C / (2.0 * B);
if (t >= 0.0 && t <= 1.0)
{
float x = 2.0 * a.x * t + p0.x;
// ... 累加有符号覆盖度
}
}
return coverage;
}
// 从 p0 到 p1 的直线段的有符号覆盖度贡献
float LineCoverage(float2 p, float2 p0, float2 p1)
{
// 检查 Y 范围
float minY = min(p0.y, p1.y);
float maxY = max(p0.y, p1.y);
if (p.y < minY || p.y >= maxY)
return 0.0;
// 在 p.y 处插值 X
float t = (p.y - p0.y) / (p1.y - p0.y);
float x = lerp(p0.x, p1.x, t);
// 环绕:如果 p 在左侧(内部)则为 +1,在右侧则为 -1
float dir = (p1.y > p0.y) ? 1.0 : -1.0;
return (p.x <= x) ? dir : 0.0;
}
为了平滑边缘,使用到最近曲线的距离进行亚像素抗锯齿:
// 使用部分像素覆盖度计算 AA 覆盖度
// windingNumber: 来自覆盖度通道的整数环绕数
// distToEdge: 到最近曲线的有符号距离(以像素为单位)
float AntiAliasedCoverage(int windingNumber, float distToEdge)
{
// 非零环绕规则
bool inside = (windingNumber != 0);
// 使用 clamp 在边缘处平滑过渡
float edgeCoverage = clamp(distToEdge + 0.5, 0.0, 1.0);
return inside ? edgeCoverage : (1.0 - edgeCoverage);
}
struct VS_Input
{
float2 position : POSITION; // 屏幕/世界空间中的字形四边形角点
float2 glyphCoord : TEXCOORD0; // 对应的字形空间坐标
uint bandOffset : TEXCOORD1; // 此字形在曲线缓冲区中的偏移量
uint curveCount : TEXCOORD2; // 条带中的曲线数量
};
struct VS_Output
{
float4 position : SV_Position;
float2 glyphCoord : TEXCOORD0;
nointerpolation uint bandOffset : TEXCOORD1;
nointerpolation uint curveCount : TEXCOORD2;
};
VS_Output VS_Slug(VS_Input input)
{
VS_Output output;
output.position = mul(float4(input.position, 0.0, 1.0), WorldViewProjection);
output.glyphCoord = input.glyphCoord;
output.bandOffset = input.bandOffset;
output.curveCount = input.curveCount;
return output;
}
// 1. 加载字体文件并提取字形轮廓
FontOutline outline = LoadGlyphOutline(font, glyphIndex);
// 2. 分解为二次贝塞尔曲线(TrueType 已经是二次曲线)
// OTF 三次曲线必须近似/分割为二次曲线
std::vector<SlugCurve> curves = DecomposeToQuadratics(outline);
// 3. 计算条带
float bandHeight = outline.bounds.height / NUM_BANDS;
std::vector<BandData> bands = ComputeBands(curves, NUM_BANDS, bandHeight);
// 4. 上传到 GPU
UploadStructuredBuffer(curveBuffer, curves.data(), curves.size());
UploadStructuredBuffer(bandBuffer, bands.data(), bands.size());
// 5. 每个字形实例:将每个条带的 bandOffset 和 curveCount 存储在顶点数据中,
// 以便片段着色器可以直接索引
// 混合状态:预乘 Alpha
BlendState SlugBlend
{
BlendEnable = TRUE;
SrcBlend = ONE; // 预乘
DestBlend = INV_SRC_ALPHA;
BlendOp = ADD;
SrcBlendAlpha = ONE;
DestBlendAlpha = INV_SRC_ALPHA;
BlendOpAlpha = ADD;
};
// 深度:通常对文本覆盖层禁用写入
DepthStencilState SlugDepth
{
DepthEnable = FALSE;
DepthWriteMask = ZERO;
};
// 光栅化器:无背面剔除(字形四边形是 2D 的)
RasterizerState SlugRaster
{
CullMode = NONE;
FillMode = SOLID;
};
// 对于字符串中的每个字形:
for (auto& glyph : string.glyphs)
{
// 发射一个覆盖字形边界框的四边形(2个三角形)
// 每个顶点携带:
// - 屏幕位置
// - 字形空间坐标(字体单位中的相同角点)
// - 用于片段着色器的 bandOffset + curveCount
float2 min = glyph.screenMin;
float2 max = glyph.screenMax;
float2 glyphMin = glyph.fontMin;
float2 glyphMax = glyph.fontMax;
EmitQuad(min, max, glyphMin, glyphMax,
glyph.bandOffset, glyph.curveCount);
}
缩放完全在 CPU 端通过变换屏幕空间四边形来处理。字形空间坐标保持不变 —— 片段着色器始终在字体单位中工作。
float scale = desiredPixelSize / font.unitsPerEm;
float2 screenMin = origin + glyph.fontMin * scale;
float2 screenMax = origin + glyph.fontMax * scale;
| 问题 | 原因 | 修复方法 |
|---|---|---|
| 字形显示为空心/反转 | 环绕顺序颠倒 | 检查轮廓方向;TrueType 使用顺时针表示外轮廓 |
| 边缘锯齿 | 未应用抗锯齿 | 确保计算了到边缘的距离并在最终覆盖度中使用 |
| 性能差 | 条带优化未生效 | 验证每个片段的曲线数量较少(< ~20);增加条带数量 |
| 三次曲线未渲染 | OTF 三次贝塞尔曲线原生不支持 | 在 CPU 上将三次曲线分割为二次近似 |
| 字形重叠处有伪影 | 曲线未裁剪到条带 | 在上传前将曲线的 Y 范围裁剪到条带范围 |
| 显示黑框而非字形 | 混合状态错误 | 使用预乘 Alpha 混合(ONE, INV_SRC_ALPHA) |
| 缺少字形 | 条带偏移量不正确 | 验证 bandOffset 索引与缓冲区布局对齐 |
根据许可证:如果你分发使用此代码的软件,你必须注明 Eric Lengyel 和 Slug 算法的出处。
建议的归属声明:
字体渲染使用了 Eric Lengyel 的 Slug 算法 (https://jcgt.org/published/0006/02/02/)
每周安装次数
218
仓库
GitHub 星标数
10
首次出现
5 天前
安全审计
安装于
github-copilot217
codex217
warp217
amp217
cline217
kimi-cli217
Skill by ara.so — Daily 2026 Skills collection.
Slug is a reference implementation of the Slug font rendering algorithm — a GPU-accelerated technique for rendering vector fonts and glyphs at arbitrary scales with high quality anti-aliasing. It works by encoding glyph outlines as lists of quadratic Bézier curves and line segments, then resolving coverage directly in fragment shaders without pre-rasterized textures.
Paper: JCGT 2017 — Slug Algorithm
Blog (updates): A Decade of Slug
License: MIT — Patent dedicated to public domain. Credit required if distributed.
Slug/
├── slug.hlsl # Core fragment shader — coverage computation
├── band.hlsl # Band-based optimization for glyph rendering
├── curve.hlsl # Quadratic Bézier and line segment evaluation
├── README.md
Slug is a reference implementation — you integrate the HLSL shaders into your own rendering pipeline.
git clone https://github.com/EricLengyel/Slug.git
Copy the .hlsl files into your shader directory and include them in your pipeline:
#include "slug.hlsl"
#include "curve.hlsl"
You must preprocess font outlines (TrueType/OTF) into Slug's curve buffer format:
Each curve entry in the GPU buffer stores:
// Line segment: p0, p1
// Quadratic Bézier: p0, p1 (control), p2
struct CurveRecord
{
float2 p0; // Start point
float2 p1; // Control point (or end point for lines)
float2 p2; // End point (unused for lines — flagged via type)
// Type/flags encoded separately or in padding
};
The glyph bounding box is divided into horizontal bands. Each band stores only the curves that intersect it, reducing per-fragment work from O(all curves) to O(local curves).
// Inputs from vertex shader
struct PS_Input
{
float4 position : SV_Position;
float2 glyphCoord : TEXCOORD0; // Position in glyph/font units
// Band index or precomputed band data
nointerpolation uint bandOffset : TEXCOORD1;
nointerpolation uint curveCount : TEXCOORD2;
};
// Glyph curve data buffer
StructuredBuffer<float4> CurveBuffer : register(t0);
float4 PS_Slug(PS_Input input) : SV_Target
{
float coverage = ComputeGlyphCoverage(
input.glyphCoord,
CurveBuffer,
input.bandOffset,
input.curveCount
);
// Premultiplied alpha output
float4 color = float4(textColor.rgb * coverage, coverage);
return color;
}
The heart of the algorithm — computing signed coverage from a quadratic Bézier:
// Evaluate whether a quadratic bezier contributes to coverage at point p
// p0: start, p1: control, p2: end
// Returns signed coverage contribution
float QuadraticBezierCoverage(float2 p, float2 p0, float2 p1, float2 p2)
{
// Transform to canonical space
float2 a = p1 - p0;
float2 b = p0 - 2.0 * p1 + p2;
// Find t values where bezier Y == p.y
float2 delta = p - p0;
float A = b.y;
float B = a.y;
float C = p0.y - p.y;
float coverage = 0.0;
if (abs(A) > 1e-6)
{
float disc = B * B - A * C;
if (disc >= 0.0)
{
float sqrtDisc = sqrt(disc);
float t0 = (-B - sqrtDisc) / A;
float t1 = (-B + sqrtDisc) / A;
// For each valid t in [0,1], compute x and check winding
if (t0 >= 0.0 && t0 <= 1.0)
{
float x = (A * t0 + 2.0 * B) * t0 + p0.x + delta.x;
// ... accumulate signed coverage
}
if (t1 >= 0.0 && t1 <= 1.0)
{
float x = (A * t1 + 2.0 * B) * t1 + p0.x + delta.x;
// ... accumulate signed coverage
}
}
}
else
{
// Degenerate to linear case
float t = -C / (2.0 * B);
if (t >= 0.0 && t <= 1.0)
{
float x = 2.0 * a.x * t + p0.x;
// ... accumulate signed coverage
}
}
return coverage;
}
// Signed coverage contribution of a line segment from p0 to p1
float LineCoverage(float2 p, float2 p0, float2 p1)
{
// Check Y range
float minY = min(p0.y, p1.y);
float maxY = max(p0.y, p1.y);
if (p.y < minY || p.y >= maxY)
return 0.0;
// Interpolate X at p.y
float t = (p.y - p0.y) / (p1.y - p0.y);
float x = lerp(p0.x, p1.x, t);
// Winding: +1 if p is to the left (inside), -1 if right
float dir = (p1.y > p0.y) ? 1.0 : -1.0;
return (p.x <= x) ? dir : 0.0;
}
For smooth edges, use the distance to the nearest curve for sub-pixel anti-aliasing:
// Compute AA coverage using partial pixel coverage
// windingNumber: integer winding from coverage pass
// distToEdge: signed distance to nearest curve (in pixels)
float AntiAliasedCoverage(int windingNumber, float distToEdge)
{
// Non-zero winding rule
bool inside = (windingNumber != 0);
// Smooth transition at edges using clamp
float edgeCoverage = clamp(distToEdge + 0.5, 0.0, 1.0);
return inside ? edgeCoverage : (1.0 - edgeCoverage);
}
struct VS_Input
{
float2 position : POSITION; // Glyph quad corner in screen/world space
float2 glyphCoord : TEXCOORD0; // Corresponding glyph-space coordinate
uint bandOffset : TEXCOORD1; // Offset into curve buffer for this glyph
uint curveCount : TEXCOORD2; // Number of curves in band
};
struct VS_Output
{
float4 position : SV_Position;
float2 glyphCoord : TEXCOORD0;
nointerpolation uint bandOffset : TEXCOORD1;
nointerpolation uint curveCount : TEXCOORD2;
};
VS_Output VS_Slug(VS_Input input)
{
VS_Output output;
output.position = mul(float4(input.position, 0.0, 1.0), WorldViewProjection);
output.glyphCoord = input.glyphCoord;
output.bandOffset = input.bandOffset;
output.curveCount = input.curveCount;
return output;
}
// 1. Load font file and extract glyph outlines
FontOutline outline = LoadGlyphOutline(font, glyphIndex);
// 2. Decompose to quadratic Beziers (TrueType is already quadratic)
// OTF cubic curves must be approximated/split into quadratics
std::vector<SlugCurve> curves = DecomposeToQuadratics(outline);
// 3. Compute bands
float bandHeight = outline.bounds.height / NUM_BANDS;
std::vector<BandData> bands = ComputeBands(curves, NUM_BANDS, bandHeight);
// 4. Upload to GPU
UploadStructuredBuffer(curveBuffer, curves.data(), curves.size());
UploadStructuredBuffer(bandBuffer, bands.data(), bands.size());
// 5. Per glyph instance: store bandOffset and curveCount per band
// in vertex data so the fragment shader can index directly
// Blend state: premultiplied alpha
BlendState SlugBlend
{
BlendEnable = TRUE;
SrcBlend = ONE; // Premultiplied
DestBlend = INV_SRC_ALPHA;
BlendOp = ADD;
SrcBlendAlpha = ONE;
DestBlendAlpha = INV_SRC_ALPHA;
BlendOpAlpha = ADD;
};
// Depth: typically write disabled for text overlay
DepthStencilState SlugDepth
{
DepthEnable = FALSE;
DepthWriteMask = ZERO;
};
// Rasterizer: no backface culling (glyph quads are 2D)
RasterizerState SlugRaster
{
CullMode = NONE;
FillMode = SOLID;
};
// For each glyph in string:
for (auto& glyph : string.glyphs)
{
// Emit a quad (2 triangles) covering the glyph bounding box
// Each vertex carries:
// - screen position
// - glyph-space coordinate (the same corner in font units)
// - bandOffset + curveCount for the fragment shader
float2 min = glyph.screenMin;
float2 max = glyph.screenMax;
float2 glyphMin = glyph.fontMin;
float2 glyphMax = glyph.fontMax;
EmitQuad(min, max, glyphMin, glyphMax,
glyph.bandOffset, glyph.curveCount);
}
Scaling is handled entirely on the CPU side by transforming the screen-space quad. The glyph-space coordinates stay constant — the fragment shader always works in font units.
float scale = desiredPixelSize / font.unitsPerEm;
float2 screenMin = origin + glyph.fontMin * scale;
float2 screenMax = origin + glyph.fontMax * scale;
| Problem | Cause | Fix |
|---|---|---|
| Glyph appears hollow/inverted | Winding order reversed | Check contour orientation; TrueType uses clockwise for outer contours |
| Jagged edges | Anti-aliasing not applied | Ensure distance-to-edge is computed and used in final coverage |
| Performance poor | Band optimization not active | Verify per-fragment curve count is small (< ~20); increase band count |
| Cubic curves not rendering | OTF cubic Béziers unsupported natively | Split cubics into quadratic approximations on CPU |
| Artifacts at glyph overlap | Curves not clipped to band | Clip curve Y range to band extents before upload |
| Black box instead of glyph | Blend state wrong | Use premultiplied alpha blend (ONE, INV_SRC_ALPHA) |
| Missing glyphs | Band offset incorrect | Validate bandOffset indexing aligns with buffer layout |
Per the license: if you distribute software using this code, you must give credit to Eric Lengyel and the Slug algorithm.
Suggested attribution:
Font rendering uses the Slug Algorithm by Eric Lengyel (https://jcgt.org/published/0006/02/02/)
Weekly Installs
218
Repository
GitHub Stars
10
First Seen
5 days ago
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
github-copilot217
codex217
warp217
amp217
cline217
kimi-cli217
Vue.js开发指南:最佳实践、组件设计与响应式编程核心原则
1,500 周安装