Featured image of post Почему мелкий шрифт остается четким и читаемым: математика рендеринга шрифтов, кривых Безье, виртуальной машины хинтинга и субпиксельного сглаживания

Почему мелкий шрифт остается четким и читаемым: математика рендеринга шрифтов, кривых Безье, виртуальной машины хинтинга и субпиксельного сглаживания

Глубокий математический и системный разбор современного рендеринга шрифтов: базисные многочлены Бернштейна, квадратичные и кубические кривые Безье, алгоритм де Кастельжо, топология ненулевого индекса обхода, аналитическое интегрирование площади трапеций, стековая виртуальная машина хинтинга TrueType, 5-отводный КИХ-фильтр ClearType, линейное альфа-смешивание с гамма-коррекцией, многоканальные знаковые поля расстояний (MSDF) и прямые GPU-шейдеры Безье (Slug).

Представьте, что вы рассматриваете текст на этом экране через мощную лупу. При стандартном кегле основного текста \(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]\]
  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).
  2. Аффинная инвариантность: Для любого аффинного преобразования \(\Phi(\mathbf{x}) = A\mathbf{x} + \mathbf{b}\) справедливо \(\Phi(\mathbf{B}_n(t)) = \sum B_{i,n}(t)\,\Phi(\mathbf{P}_i)\): достаточно преобразовать только опорные точки.
  3. Интерполяция концов и управление касательными: \(\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})\).
  4. Свойство уменьшения вариации: Никакая прямая не пересекает кривую Безье большее число раз, чем её контрольную ломаную.

Квадратичные кривые (\(n=2\), TrueType) против кубических (\(n=3\), OpenType CFF)

  • Квадратичные кривые Безье (\(n=2\), таблица glyf TrueType): \(\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 / таблица CFF OpenType): \(\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 = \left\lceil \sqrt{ \frac{n(n-1)}{8\tau} \max_{0 \le i \le n-2} \|\mathbf{P}_{i+2} - 2\mathbf{P}_{i+1} + \mathbf{P}_i\| } \right\rceil\]

кривая без рекурсии разбивается на \(N\) отрезков с точностью \(\tau\).


3. Сканирующая растеризация: индекс обхода и аналитическое интегрирование трапеций

Четно-нечетное правило против правила ненулевого индекса обхода

Чтобы отличить тело глифа от внутрибуквенных просветов (дырок в «O» или «8»), четно-нечетное правило (Even-Odd) дает сбой при самопересечении или наложении штрихов (например, в вариативных шрифтах OpenType), оставляя белые дыры на перекрестьях. Поэтому растеризаторы используют топологическое правило ненулевого индекса обхода (Non-Zero Winding Rule):

\[w(P) = \frac{1}{2\pi} \oint_{\partial\Gamma} d\theta = \sum_{\text{crossings}} \operatorname{sgn}(\Delta y) \ne 0\]

Внешние контуры (по часовой стрелке 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 градациями:

\[C_i = C_{i-1} + \text{delta\_cover}[i], \qquad A_i = C_i + \text{delta\_area}[i], \qquad \alpha_i = \min\bigl(1.0,\; |A_i|\bigr)\]

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 и восстанавливающий угол в шейдере через медиану:

\[d_{\text{MSDF}} = \operatorname{median}(r, g, b) = \max\Bigl(\min(r, g),\; \min\bigl(\max(r, g),\; b\bigr)\Bigr)\]

А для предельно сложных иероглифов алгоритмы прямой оценки на GPU — Slug Эрика Ленгиела (2017) и Vello Рафа Левина — вычисляют пересечения с кривыми Безье и индекс обхода непосредственно во фрагментном или вычислительном шейдере без промежуточных текстур.

comments powered by Disqus