Представьте, что вы рассматриваете текст на этом экране через мощную лупу. При стандартном кегле основного текста $12\,\text{px}$ ($9\,\text{pt}$ на классическом мониторе с разрешением $96\,\text{DPI}$) вся знакоместо буквы представляет собой крошечную дискретную шахматную доску размером всего $12$ на $12$ пикселей — лишь $144$ микроскопических светящихся квадратика.
Тем не менее внутри этой тесной сетки $12 \times 12$ операционная система десятки тысяч раз в секунду отрисовывает сложнейшие глифы: от иероглифа кандзи «鬱» из $29$ черт до двухъярусной латинской строчной антиквы «g» или буквы «m» с тремя равноудаленными вертикальными штрихами (Stems). Каждый вертикальный штрих сохраняет идеально ровную толщину и насыщенность, плотные горизонтальные перекладины не сливаются в черное пятно и не проваливаются между центрами пикселей (Dropout), а диагональные кривые лишены ступенчатых зазубрин (Jaggies).
С точки зрения теории информации и цифровой обработки сигналов (DSP) это повседневное явление — настоящее инженерное чудо, бросающее вызов теореме Котельникова (Найквиста — Шеннона). При наивном геометрическом уменьшении векторного контура на сетку $12 \times 12$ высокие пространственные частоты выше предела Найквиста породили бы катастрофический алиасинг, превратив текст в нечитаемое серое пятно.
За четкостью мелкого текста стоит полвека достижений информатики: вычислительная геометрия (многочлены Бернштейна и сплайны Безье), алгебраическая топология (индекс обхода ориентированных контуров), виртуальные машины (исполняемый байт-код внутри каждого файла .ttf), психофизика зрения и теория КИХ-фильтров (субпиксельный рендеринг ClearType), радиометрия (альфа-смешивание в линейном световом пространстве) и параллельные алгоритмы GPU-шейдеров (MSDF и прямое вычисление кривых Slug).
1. От растрового плена к независимым от разрешения контурным шрифтам
Ранние компьютерные системы полагались на растровые шрифты (Bitmap Fonts), хранившие каждый глиф как бинарную матрицу $M \in \{0, 1\}^{H \times W}$. Несмотря на четкость в родном размере, они страдали от фатальной зависимости от разрешения: масштабирование или поворот порождали «лесенки» или размытие, а экранные шрифты $72\,\text{DPI}$ не годились для лазерных принтеров ($300\,\text{DPI}$) и фотонаборных автоматов ($2400\,\text{DPI}$).
Прорыв совершили контурные векторные шрифты (Outline Fonts): сначала PostScript Type 1 (1984), созданный Джоном Уорноком и Чаком Гешке в Adobe на базе кубических кривых Безье, затем TrueType (.ttf, 1991) от Apple и Microsoft на базе квадратичных кривых Безье и открытой стековой виртуальной машины хинтинга. В 1996 году обе технологии объединились в стандарте OpenType (таблицы glyf и CFF/CFF2).
flowchart TD
A["Файл шрифта OpenType (.ttf / .otf)<br/>Таблицы SFNT: head, cmap, GSUB/GPOS, glyf или CFF"] --> B["1. Шейпинг текста (HarfBuzz / DirectWrite / CoreText)<br/>Кодовые точки Unicode → ID глифов и позиции"]
B --> C["2. Извлечение контуров и аффинное преобразование<br/>Масштабирование из EM-квадрата (2048 upem) в сетку PPEM"]
C --> D["3. Выполнение Grid-Fitting / Хинтинга<br/>Байт-код виртуальной машины TrueType (fpgm, prep, cvt) или автохинтер FreeType<br/>Привязка штрихов к целочисленным границам пикселей"]
D --> E["4. Линеаризация кривых и топология строк развертки<br/>Подразделение де Кастельжо / Формула Ванга + Правило ненулевого индекса"]
E --> F["5. Аналитическое интегрирование трапеций и субпиксельная фильтрация<br/>3-кратная горизонтальная передискретизация + 5-отводный КИХ-фильтр"]
F --> G["6. Линейное альфа-смешивание с гамма-коррекцией<br/>Перевод sRGB в линейную яркость → Кадровый буфер"]
2. Вычислительная геометрия контуров: базис Бернштейна и кривые Безье
Четыре фундаментальных свойства многочленов Бернштейна
Явные многочлены в степенном базисе $y = a_3 x^3 + a_2 x^2 + a_1 x + a_0$ не способны описать вертикальные касательные и численно неустойчивы при поворотах. Инженеры Поль де Кастельжо (Citroën, 1959) и Пьер Безье (Renault, 1962) определили параметрические кривые через базисные многочлены Бернштейна степени $n$ и $n+1$ опорных точек $\mathbf{P}_0, \dots, \mathbf{P}_n \in \mathbb{R}^2$:
$$\mathbf{B}_n(t) = \sum_{i=0}^{n} B_{i,n}(t)\,\mathbf{P}_i, \qquad B_{i,n}(t) = \binom{n}{i} (1 - t)^{n - i} t^i, \qquad t \in [0, 1]$$- Неотрицательность, разбиение единицы и свойство выпуклой оболочки: На отрезке $t \in [0, 1]$ всегда $B_{i,n}(t) \ge 0$, а по биному Ньютона сумма всех базисных функций тождественно равна $1$: $$\sum_{i=0}^{n} B_{i,n}(t) = \bigl((1 - t) + t\bigr)^n = 1$$ Являясь выпуклой комбинацией опорных точек, кривая $\mathbf{B}_n(t)$ целиком лежит внутри их выпуклой оболочки (Convex Hull), что позволяет за $O(1)$ отсекать кривые по ограничивающему прямоугольнику (Bounding Box).
- Аффинная инвариантность: Для любого аффинного преобразования $\Phi(\mathbf{x}) = A\mathbf{x} + \mathbf{b}$ справедливо $\Phi(\mathbf{B}_n(t)) = \sum B_{i,n}(t)\,\Phi(\mathbf{P}_i)$: достаточно преобразовать только опорные точки.
- Интерполяция концов и управление касательными: $\mathbf{B}_n(0) = \mathbf{P}_0$, $\mathbf{B}_n(1) = \mathbf{P}_n$, а векторы производных на концах равны $\mathbf{B}_n'(0) = n(\mathbf{P}_1 - \mathbf{P}_0)$ и $\mathbf{B}_n'(1) = n(\mathbf{P}_n - \mathbf{P}_{n-1})$.
- Свойство уменьшения вариации: Никакая прямая не пересекает кривую Безье большее число раз, чем её контрольную ломаную.
Квадратичные кривые ($n=2$, TrueType) против кубических ($n=3$, OpenType CFF)
- Квадратичные кривые Безье ($n=2$, таблица
glyfTrueType): $$\mathbf{B}_2(t) = (1-t)^2 \mathbf{P}_0 + 2(1-t)t \mathbf{P}_1 + t^2 \mathbf{P}_2$$ Пересечение со строкой развертки находится напрямую через квадратное уравнение. Кроме того, TrueType использует сжатие через неявные срединные точки на кривой (Implicit On-Curve Midpoints): если в потоке данных подряд идут две внеконтурные точки $\mathbf{P}_1, \mathbf{P}_2$, декодер автоматически восстанавливает узловую точку на кривой ровно посередине $\mathbf{M} = \frac{1}{2}(\mathbf{P}_1 + \mathbf{P}_2)$, гарантируя гладкую стыковку $C^1$ и экономя около $30\%$ объема файла. - Кубические кривые Безье ($n=3$, PostScript / таблица
CFFOpenType): $$\mathbf{B}_3(t) = (1-t)^3 \mathbf{P}_0 + 3(1-t)^2 t \mathbf{P}_1 + 3(1-t)t^2 \mathbf{P}_2 + t^3 \mathbf{P}_3$$ Поскольку вторая производная квадратичной кривой постоянна ($\mathbf{B}_2''(t) = \text{const}$), один квадратичный сегмент не может содержать точку перегиба (например, изгиб буквы «S»). У кубической кривой вторая производная линейна по $t$, что позволяет описывать S-образные перегибы одним сегментом и независимо регулировать кривизну $G^2$ на концах.
| Характеристика | Квадратичная Безье ($n=2$, TrueType glyf) | Кубическая Безье ($n=3$, OpenType CFF) |
|---|---|---|
| Уравнение $\mathbf{B}(t)$ | $(1-t)^2\mathbf{P}_0 + 2(1-t)t\mathbf{P}_1 + t^2\mathbf{P}_2$ | $(1-t)^3\mathbf{P}_0 + 3(1-t)^2t\mathbf{P}_1 + 3(1-t)t^2\mathbf{P}_2 + t^3\mathbf{P}_3$ |
| Точки перегиба на сегмент | $0$ (дуга параболы с постоянным знаком кривизны) | До $2$ (описывает «S» одним сегментом) |
| Степени свободы (при фикс. касательных) | $0$ ($\mathbf{P}_1$ жестко задана пересечением касательных) | $2$ (длины рычагов управляют кривизной $G^2$) |
| Бинарное сжатие | Неявные середины $\mathbf{M} = \frac{1}{2}(\mathbf{P}_1 + \mathbf{P}_2)$ | Стековые подпрограммы CFF CharString и дельты |
С помощью алгоритма де Кастельжо $\mathbf{P}_i^{(r)}(t) = (1-t)\mathbf{P}_i^{(r-1)}(t) + t\mathbf{P}_{i+1}^{(r-1)}(t)$ (сводящегося при $t = \frac{1}{2}$ к сложению и битовому сдвигу >> 1) и формулы Ванга (1985)
кривая без рекурсии разбивается на $N$ отрезков с точностью $\tau$.
3. Сканирующая растеризация: индекс обхода и аналитическое интегрирование трапеций
Четно-нечетное правило против правила ненулевого индекса обхода
Чтобы отличить тело глифа от внутрибуквенных просветов (дырок в «O» или «8»), четно-нечетное правило (Even-Odd) дает сбой при самопересечении или наложении штрихов (например, в вариативных шрифтах OpenType), оставляя белые дыры на перекрестьях. Поэтому растеризаторы используют топологическое правило ненулевого индекса обхода (Non-Zero Winding Rule):
Внешние контуры (по часовой стрелке CW в TrueType, $-1$) и внутренние отверстия (против часовой стрелки CCW, $+1$) взаимно уничтожаются ($w(P) = 0$) внутри отверстий, тогда как перекрывающиеся однонаправленные штрихи дают $w(P) = -2 \ne 0$ и остаются полностью закрашенными.
Аналитическое интегрирование площади трапеций (Libart / FreeType ftgrays / stb_truetype)
Вместо медленного суперсэмплинга алгоритм Рафа Левина (применяемый в FreeType ftgrays.c, stb_truetype и font-rs) вычисляет для каждого отрезка внутри пикселя $(i, j)$ (со знаковым приращением $\Delta y = y_1 - y_0$ и средней абсциссой $\bar{x} \in [0, 1]$) точную площадь трапеции справа от него $\text{delta\_area}[i] \mathrel{+}= (1 - \bar{x})\Delta y$ и приращение полного покрытия $\text{delta\_cover}[i+1] \mathrel{+}= \Delta y$ для всех пикселей правее. Один линейный проход префиксной суммы (Prefix Sum) слева направо дает точное покрытие $\alpha_i \in [0, 1]$ с 256 градациями:
4. Секретная виртуальная машина внутри шрифта: хинтинг и три философии рендеринга
Без хинтинга при $12\,\text{px}$ три одинаковых вертикальных штриха буквы «m» попадают в разную фазу пиксельной сетки: первый ложится в один пиксель ($\alpha = 0.80$, четкая черная линия), а второй пересекает границу пикселей и размазывается в мутную двухпиксельную серую полосу ($\alpha = 0.60$ и $\alpha = 0.20$).
Для борьбы с этим каждый файл TrueType содержит байт-код (cvt , fpgm, prep, glyf), исполняемый стековой виртуальной машиной в формате с фиксированной запятой $26.6$. С помощью вектора проекции ($\mathbf{P}$) и вектора свободы ($\mathbf{F}$) инструкции MDAP и MIRP привязывают опорные точки к границам пикселей, выравнивают ширину штрихов по глобальной таблице cvt, защищают тонкие линии от исчезновения (Minimum Distance), вносят точечные поправки через DELTAP1 и плавно интерполируют остальные точки кривой командами IUP[0] / IUP[1].
- Философия Microsoft (GDI / ClearType): Максимальная резкость и контраст за счет жесткой привязки штрихов к пиксельной сетке даже ценой искажения пропорций гарнитуры.
- Философия Apple (Quartz / CoreText): Абсолютная верность замыслу шрифтового дизайнера (WYSIWYG) и игнорирование байт-кода хинтинга, раскрывшаяся во всей красе на экранах Retina.
- Автохинтер FreeType (
autofit) и вертикальный хинтинг (Y-only): Автоматический геометрический анализ штрихов и синих зон (Blue Zones) во время выполнения с легкой привязкой только по вертикальной оси Y.
5. Субпиксельный рендеринг (ClearType) и линейное смешивание с гамма-коррекцией
Каждый пиксель ЖК-панели состоит из трех вертикальных субпикселей ($R, G, B$) с шагом $\frac{1}{3}$ пикселя по горизонтали. Поскольку человеческое зрение обладает высокой разрешающей способностью по яркости ($Y = 0.2126R + 0.7152G + 0.0722B$) и низкой по цветности, растеризатор выполняет горизонтальную выборку с $3$-кратным разрешением. Для подавления цветных ореолов (Color Fringing) применяется 5-отводный симметричный КИХ-фильтр Гибсона ($\frac{1}{3}[1,1,1] * \frac{1}{3}[1,1,1]$):
$$h_{\text{Gibson}} = \left[\frac{1}{9},\; \frac{2}{9},\; \frac{3}{9},\; \frac{2}{9},\; \frac{1}{9}\right]$$Сумма коэффициентов с шагом 3 равна $\frac{3}{9} = \frac{2}{9} + \frac{1}{9} = \frac{1}{3}$, что гарантирует идеальный цветовой баланс. Наконец, альфа-смешивание обязано выполняться в линейном пространстве света ($\gamma \approx 2.2$), чтобы избежать искажения толщины шрифта между светлой и темной темами:
$$C_{\text{out}, c} = \Bigl( \alpha_c \, C_{\text{fg}, c}^{\gamma} + (1 - \alpha_c) \, C_{\text{bg}, c}^{\gamma} \Bigr)^{1/\gamma}, \qquad c \in \{R, G, B\},\; \gamma \approx 2.2$$6. Эпоха GPU: SDF, MSDF (msdfgen) и прямые шейдеры Безье (Slug)
flowchart TD
subgraph CPU["Растеризация на CPU (FreeType / DirectWrite)"]
C1["Кривые Безье + ВМ хинтинга"] --> C2["Интегрирование трапеций + 5-отводный КИХ-фильтр"]
C2 --> C3["Кэш растровых битмапов по размерам<br/>Плюс: Идеальная четкость при 10–14px<br/>Минус: Размытие или пересчет при 3D-масштабировании"]
end
subgraph MSDF["Поля расстояний на GPU (Valve SDF / Chlumský MSDF)"]
S1["Офлайн-запекание знакового поля расстояний"] --> S2["MSDF: RGB-кодирование ребер для сохранения острых углов"]
S2 --> S3["Фрагментный шейдер: d = median(r, g, b) + smoothstep<br/>Плюс: Острые углы при любом увеличении из малой текстуры"]
end
subgraph Slug["Прямые GPU-шейдеры Безье (Eric Lengyel 'Slug' / 'Vello')"]
G1["Опорные точки Безье напрямую в буфере GPU"] --> G2["Пространственное разбиение на полосы (Bands)"]
G2 --> G3["Аналитическое решение квадратных уравнений и индекса обхода в шейдере<br/>Плюс: Без текстурных атласов, мгновенная отрисовка 50 000 кандзи"]
end
Для 3D-движков Крис Грин (Valve, 2007) предложил знаковые поля расстояний (SDF), хранящие евклидово расстояние $d(\mathbf{p})$ до контура. Чтобы острые углы не скруглялись при увеличении, Виктор Хлумский (2015) разработал MSDF (msdfgen), кодирующий смежные ребра угла в каналах RGB и восстанавливающий угол в шейдере через медиану:
А для предельно сложных иероглифов алгоритмы прямой оценки на GPU — Slug Эрика Ленгиела (2017) и Vello Рафа Левина — вычисляют пересечения с кривыми Безье и индекс обхода непосредственно во фрагментном или вычислительном шейдере без промежуточных текстур.