Подробные статьи о технологиях, определяющих будущее.

Искусство дизеринга: красивые картинки в одном бите

Как алгоритмы дизеринга создают полутона только из чёрных и белых пикселей: от Флойда–Стейнберга до голубого шума, с примерами реализации.

Портрет капитана корабля из чёрно-белых дизерингованных точек, которые складываются в полутона

Игра Return of the Obra Dinn Лукаса Поупа — одна из самых визуально эффектных игр, когда-либо созданных, и использует ровно два цвета: чёрный и белый. Один бит на пиксель. Никаких оттенков серого, никаких градиентов, никакого сглаживания. Каждый пиксель либо включён, либо выключен. И тем не менее игра выводит полностью трёхмерный мир с узнаваемыми персонажами, глубиной и атмосферным освещением — и всё это благодаря грамотному дизерингу.

Дизеринг — это техника расстановки чёрных и белых пикселей в узорах, которые заставляют глаз видеть промежуточные оттенки. Его используют с самых ранних дней компьютерной графики, и сейчас он переживает второе рождение: в геймдеве, веб-дизайне и в задаче вывода изображений на e-ink экраны, которая становится всё актуальнее. Лежащие в основе алгоритмы элегантны, удивительно разнообразны и применимы чаще, чем вы могли бы подумать.

Почему дизеринг работает

Ваша зрительная система обрабатывает не отдельные пиксели, а области. Когда вы смотрите на узор из чередующихся чёрных и белых пикселей, вы не видите отдельных точек. Вы видите серый. Область, где 25% пикселей белые, воспринимается как 25% серого, а область, где белых 75%, — как 75% серого. Плотность белых пикселей на участке определяет воспринимаемую яркость.

Это тот же принцип, что лежит в основе растровой печати в газетах. Точки разного размера и с разным шагом создают иллюзию непрерывного тона из чистой чёрной краски на белой бумаге. Дизеринг — это цифровой аналог: вместо размера точек используются узоры из пикселей.

Сложность не в том, чтобы получить оттенки серого — это несложно. Сложность в том, чтобы сделать это без видимых артефактов: без бэндинга (резких переходов между узорами), без направленных искажений (узоров, которые создают заметные линии) и без муара (интерференционных узоров между дизерингом и сеткой пикселей экрана).

Пороговый дизеринг: наивный подход

Самый простой дизеринг: яркость каждого пикселя сравнивается с порогом (обычно 50%). Ярче порога — белый. Темнее — чёрный. Результат — высококонтрастный силуэт без какой-либо градации. Подходит для текста и линейной графики, но ужасен для фотографий и 3D-рендеринга.

import numpy as np
from PIL import Image
def threshold_dither(image, threshold=128):
"""Binary threshold — no dithering at all, really."""
gray = np.array(image.convert('L'), dtype=float)
return Image.fromarray((gray > threshold).astype(np.uint8) * 255)

Упорядоченный дизеринг: узоры на сетке

Упорядоченный дизеринг использует пороговую матрицу (её также называют матрицей Байера), которая меняет порог внутри повторяющегося тайла. Вместо того чтобы сравнивать каждый пиксель с одним и тем же порогом, каждый пиксель сравнивается с разным значением из матрицы в зависимости от своей позиции.

def ordered_dither(image, matrix_size=4):
"""Ordered dithering with Bayer matrix."""
gray = np.array(image.convert('L'), dtype=float)
h, w = gray.shape
# 4×4 Bayer matrix (values 0-15, normalized to 0-255)
bayer_4x4 = np.array([
[ 0,  8,  2, 10],
[12,  4, 14,  6],
[ 3, 11,  1,  9],
[15,  7, 13,  5]
]) * (255 / 16)
# Tile the matrix across the image
threshold = np.tile(bayer_4x4, (h // 4 + 1, w // 4 + 1))[:h, :w]
return Image.fromarray((gray > threshold).astype(np.uint8) * 255)

Упорядоченный дизеринг создаёт характерный перекрёстный узор, который сразу узнаётся. При низком разрешении регулярная структура сетки заметна и придаёт изображению механический, ретро-вид. Во многих случаях это осознанный эстетический выбор: упорядоченный узор матрицы Байера 8×8 имеет особый вид, который многие игры и художники используют намеренно.

Матрица Байера построена так, что каждый уровень порога добавляет новые пиксели в позициях, максимально удалённых от уже существующих. Это минимизирует скопления и даёт самое равномерное распределение точек на каждом уровне яркости. Кроме того, метод очень быстрый: одно сравнение на пиксель, никаких зависимостей от соседей, что делает его подходящим для рендеринга в реальном времени.

Диффузия ошибки: Флойд–Стейнберг и его родственники

Диффузионный дизеринг подходит к задаче совсем иначе. Вместо фиксированной пороговой матрицы он обрабатывает пиксели последовательно и распределяет «ошибку» (разницу между исходным значением пикселя и квантованным результатом, чёрным или белым) на соседние, ещё не обработанные пиксели.

def floyd_steinberg_dither(image):
"""Floyd-Steinberg error diffusion dithering."""
gray = np.array(image.convert('L'), dtype=float)
h, w = gray.shape
for y in range(h):
for x in range(w):
old_pixel = gray[y, x]
new_pixel = 255.0 if old_pixel > 128 else 0.0
gray[y, x] = new_pixel
error = old_pixel - new_pixel
# Distribute error to neighbors
# Floyd-Steinberg diffusion kernel:
#        * 7/16
#  3/16 5/16 1/16
if x + 1 < w:
gray[y, x + 1] += error * 7 / 16
if y + 1 < h:
if x - 1 >= 0:
gray[y + 1, x - 1] += error * 3 / 16
gray[y + 1, x] += error * 5 / 16
if x + 1 < w:
gray[y + 1, x + 1] += error * 1 / 16
return Image.fromarray(gray.astype(np.uint8))

Дизеринг Флойда–Стейнберга даёт гораздо более естественные результаты, чем упорядоченный. В распределении пикселей нет видимой сеточной структуры, и он прекрасно справляется с плавными градиентами. Точки группируются органично, создавая картинку, ближе к фотографической растровой печати.

Минус в том, что метод последовательный. Каждый пиксель зависит от ошибки уже обработанных пикселей, поэтому его трудно распараллелить. Из-за этого он медленнее упорядоченного и сложнее в реализации в шейдерных конвейерах. Для предобработанных изображений это не важно. Для 3D-рендеринга в реальном времени на 60 fps это проблема.

Дизеринг голубым шумом: лучшее из двух миров

Дизеринг голубым шумом — текущий стандарт для многих задач. Он использует пороговую матрицу, как и упорядоченный дизеринг, поэтому быстр и распараллеливается, но значения матрицы расположены так, чтобы получить распределение голубого шума: точки размещаются случайно, но с ограничением на минимальное расстояние. Так избегаются и скопления, характерные для белого шума, и сеточные узоры упорядоченного дизеринга.

Термин «голубой шум» пришёл из обработки сигналов. В голубом шуме большая часть энергии приходится на высокие частоты, как у голубого света. В пространственном смысле это значит, что узор не имеет крупномасштабной структуры (ни видимой сетки, ни направленности), но точки равномерно распределены на локальном уровне (ни скоплений, ни пустот). Это похоже на броски дротиков в мишень с условием, что каждый дротик должен лежать не ближе определённого минимального расстояния от любого другого.

Сгенерировать хорошую пороговую матрицу голубого шума удивительно непросто: это задача оптимизации, которую нужно решать итеративно, пока частотный спектр не совпадёт с нужным профилем. Но когда матрица готова, использовать её так же быстро, как дизеринг Байера: одно сравнение на пиксель.

Упорядоченный сферический дизеринг в Obra Dinn

Дизеринг в Return of the Obra Dinn особенно изящен, потому что решает проблему, характерную именно для 3D-рендеринга: временную стабильность. Если стандартный узор дизеринга наложить в экранных координатах, то при движении камеры он начинает «плавать»: узор остаётся прикреплённым к экрану, пока 3D-геометрия движется под ним. Возникает мерцание, которое крайне отвлекает.

Решение Поупа — проецировать узор дизеринга в трёхмерном пространстве, а не в экранном. Узор проецируется на сферу вокруг камеры и затем выбирается для каждого пикселя по направлению взгляда. Когда камера поворачивается, узор поворачивается вместе с ней. Когда объекты движутся в мировом пространстве, их дизеринг остаётся стабильным, потому что он вычисляется по их мировой позиции, а не по экранным координатам.

Этот приём можно адаптировать в любом 3D-приложении, которое использует дизеринг или растровые эффекты. Ключевая мысль: если нужна временная стабильность, пороговые значения должны зависеть от чего-то стабильного (мировая позиция, направление взгляда), а не от того, что меняется каждый кадр (экранные координаты).

Практическое применение помимо ретро-эстетики

Дизеринг — не только про ретро-эстетику. У него есть вполне практичное применение в современной разработке.

  • E-ink дисплеи. Устройства для чтения и e-ink-вывески имеют ограниченную градацию серого (обычно 16 уровней). Без дизеринга фотографии и градиенты на них не отобразить. Выбор алгоритма напрямую влияет на читаемость и качество изображения.
  • Уменьшение размера файлов. 1-битное дизерингованное изображение весит в 8 раз меньше, чем 8-битное изображение в оттенках серого. Для веб-доставки декоративных картинок дизерингованные PNG могут быть заметно меньше серых аналогов. Некоторые веб-дизайнеры используют дизеринг намеренно, и ради эстетики, и ради производительности.
  • Квантование цвета. Когда полноцветное изображение сводится к ограниченной палитре (GIF, индексированный PNG или дисплеи с ограниченной палитрой), дизеринг предотвращает бэндинг, который возникает, когда плавные градиенты отображаются на дискретные значения цвета. Именно так GIF-энкодеры решают эту задачу с помощью дизеринга Флойда–Стейнберга в цветовом пространстве.
  • Бэндинг градиентов в рендеринге. Рендер-таргеты низкой точности (8 бит на канал) могут давать видимый бэндинг в плавных градиентах. Добавление экранного дизеринга (даже ±0,5 от значения) разбивает бэндинг без заметного шума. Большинство современных игровых движков делают это автоматически.
  • Термопечать и чеки. Термопринтеры по своей природе однобитные: каждая точка либо нагрета (чёрная), либо нет. Дизеринг превращает фотографии и графику в печатный вид. От выбранного алгоритма зависит, узнаваем ли рисунок на чеке или это просто пятно.

Реализация дизеринга в шейдере

Для приложений реального времени упорядоченный дизеринг и дизеринг голубым шумом можно эффективно реализовать в виде фрагментных шейдеров.

// GLSL fragment shader for Bayer 8x8 ordered dithering
precision mediump float;
uniform sampler2D uTexture;
varying vec2 vTexCoord;
// Bayer 8x8 matrix lookup via bit manipulation (no texture needed)
float bayer8x8(vec2 pos) {
ivec2 p = ivec2(mod(pos, 8.0));
int index = p.x + p.y * 8;
// Bit-reversal trick for Bayer matrix values
int value = 0;
int x = p.x ^ p.y;
int y = p.y;
for (int i = 0; i < 3; i++) {
value = value * 4 + (x & 1) * 2 + (y & 1);
x >>= 1;
y >>= 1;
}
return float(value) / 64.0;
}
void main() {
vec4 color = texture2D(uTexture, vTexCoord);
float luminance = dot(color.rgb, vec3(0.299, 0.587, 0.114));
float threshold = bayer8x8(gl_FragCoord.xy);
float dithered = step(threshold, luminance);
gl_FragColor = vec4(vec3(dithered), 1.0);
}

Для голубого шума обычно заранее вычисляют текстуру шума и семплируют её в шейдере. Текстуру можно замостить: при повторении текстура 64×64 или 128×128 покрывает весь экран. Стоимость на пиксель составляет одну выборку из текстуры и одно сравнение, то есть это практически бесплатно на современных GPU.

Выбор правильного алгоритма

У каждого подхода к дизерингу есть сценарий, в котором он лучший.

  • Пороговый: текст, линейная графика, высококонтрастные изображения, где важнее детали, чем тональный диапазон.
  • Упорядоченный (Байер): рендеринг в реальном времени, когда нужна осознанная ретро- или растровая эстетика. Быстрый, распараллеливается, хорошо ложится на GPU.
  • Флойд–Стейнберг: обработка статичных изображений, где важнее всего качество. Лучше всего подходит для фотографий и изображений с плавными градиентами.
  • Голубой шум: рендеринг в реальном времени, когда нужны естественные результаты без видимых узоров. Лучший универсальный выбор для современных приложений.
  • Сферический / мировое пространство: 3D-рендеринг, где критична временная стабильность. Необходим для однобитного 3D-рендеринга, как в Obra Dinn.

Дизеринг — одна из тех тем, где простота идеи («расставить точки так, чтобы они выглядели как светотень») скрывает глубину инженерных решений. Каждый алгоритм — это свой компромисс между качеством, скоростью и визуальным характером. А в эпоху e-ink дисплеев, слабых каналов связи и ретро-эстетики понимание этих компромиссов практичнее, чем когда-либо.