Постобработка: эффекты на весь экран

Готовый кадр можно обработать ещё раз целиком — как фотофильтр поверх всего изображения.

Постобработка — этап, на котором уже отрисованный кадр обрабатывается фрагментным шейдером как одна большая текстура на весь экран.

Зачем это знать

Блюр, свечение, виньетка, цветокоррекция, motion blur, размытие глубины — всё это не привязано к отдельным объектам, а накладывается на финальную картинку. Понимание постобработки объясняет, как из «плоского» рендера получают кинематографичную картинку.

Идея: рендер в текстуру

Сцену рисуют не сразу на экран, а в текстуру (offscreen framebuffer). Затем рисуют один прямоугольник на весь экран (full-screen quad) и его фрагментный шейдер читает эту текстуру, преобразуя каждый пиксель.

Конвейер постобработки:

[сцена] --> рендер в текстуру --> [шейдер пост-эффекта] --> экран
                                    блюр / bloom / цвет

Блюр: усреднение соседей

Размытие — это усреднение пикселя с соседями. Простейший box-blur берёт окно вокруг пикселя и считает среднее. Смоделируем на 1D-строке яркостей.

def box_blur_1d(pixels, radius=1):
    out = []
    n = len(pixels)
    for i in range(n):
        lo = max(0, i - radius)
        hi = min(n, i + radius + 1)
        window = pixels[lo:hi]
        out.append(round(sum(window) / len(window), 1))
    return out

row = [0, 0, 100, 0, 0, 100, 0]
print("Исходная:", row)
print("После блюра:", box_blur_1d(row, radius=1))

Вывод:

Исходная: [0, 0, 100, 0, 0, 100, 0]
После блюра: [0.0, 33.3, 33.3, 33.3, 33.3, 33.3, 50.0]

Резкие пики «растеклись» на соседей — это и есть размытие. В 2D окно квадратное, а гауссов блюр даёт более мягкий, естественный результат.

Свечение (bloom)

Bloom имитирует переэкспонированный яркий свет, расплывающийся вокруг источника. Алгоритм: выделить очень яркие пиксели (порог), размыть их и сложить с исходным кадром.

def extract_bright(pixels, threshold=200):
    # оставляем только яркие пиксели, остальные гасим
    return [p if p >= threshold else 0 for p in pixels]

frame = [50, 255, 80, 240, 30, 100]
bright = extract_bright(frame, 200)
print("Кадр:        ", frame)
print("Яркие части: ", bright)
print("Их размоют и добавят обратно -> свечение")

Вывод:

Кадр:         [50, 255, 80, 240, 30, 100]
Яркие части:  [0, 255, 0, 240, 0, 0]
Их размоют и добавят обратно -> свечение

Как работает под капотом

Тяжёлые эффекты вроде гауссова блюра разбивают на два прохода (сначала по горизонтали, потом по вертикали) — это дешевле, чем большое 2D-ядро. Bloom обычно делают на уменьшенной копии кадра, чтобы сэкономить. Тонмаппинг переводит расчёты в широком динамическом диапазоне (HDR) в отображаемые [0,1], сохраняя детали в светах и тенях.

Частые ошибки

  • Делать блюр одним 2D-проходом большого радиуса — дорого; правильнее два 1D-прохода.
  • Bloom без порога яркости — «замыливается» весь кадр, а не только источники света.
  • Забыть тонмаппинг при HDR — пересветы превращаются в плоские белые пятна.

Итоги

  • Постобработка применяет шейдер к готовому кадру как к текстуре на весь экран.
  • Блюр — усреднение пикселя с соседями; гауссов разбивают на два 1D-прохода.
  • Bloom = порог ярких пикселей → размытие → сложение с кадром.
  • Тонмаппинг сжимает HDR в отображаемый диапазон без потери деталей.
Проверьте себя
1. Как реализуется постобработка на весь экран?
AПерерисовкой каждого объекта
BРендером сцены в текстуру и обработкой её шейдером на full-screen quad
CТолько на CPU
DИзменением вершинного шейдера объектов
2. Что делает простой box-blur с пикселем?
AУдваивает яркость
BУсредняет его с соседями в окне
CДелает чёрным
DПоворачивает
3. Из каких шагов состоит эффект bloom?
AПоворот, масштаб, перенос
BВыделить яркие пиксели по порогу → размыть → сложить с кадром
CСортировка треугольников
DТест глубины