Буфер глубины и z-buffer

Когда объекты перекрываются, видеокарта хранит для каждого пикселя его глубину и показывает лишь самый близкий фрагмент.

Буфер глубины (z-buffer) — массив, где для каждого пикселя хранится глубина ближайшего нарисованного фрагмента; новый фрагмент проходит, только если он ближе.

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

Без буфера глубины порядок рисования определял бы видимость: что нарисовано позже — то поверх, даже если оно дальше. Z-buffer избавляет от ручной сортировки треугольников: каждый фрагмент сам «знает» свою глубину и честно конкурирует за пиксель.

Как работает тест глубины

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

FAR = float("inf")
depth = FAR        # буфер глубины пикселя
color = None       # буфер цвета пикселя

fragments = [("красный", 0.8), ("зелёный", 0.3), ("синий", 0.5)]
for name, z in fragments:
    if z < depth:           # ближе того, что уже есть?
        depth = z
        color = name
        print(f"{name} (z={z}) ПРОШЁЛ -> пиксель {name}")
    else:
        print(f"{name} (z={z}) отброшен (дальше {depth})")
print("Итог пикселя:", color, "на глубине", depth)

Вывод:

красный (z=0.8) ПРОШЁЛ -> пиксель красный
зелёный (z=0.3) ПРОШЁЛ -> пиксель зелёный
синий (z=0.5) отброшен (дальше 0.3)
Итог пикселя: зелёный на глубине 0.3

Несмотря на то что синий пришёл последним, победил ближайший — зелёный (z=0.3). Порядок рисования не важен.

Отсечение задних граней

Отдельная оптимизация — backface culling: треугольники, повёрнутые «спиной» к камере (внутренняя сторона), не рисуют вообще. Ориентацию определяют по порядку обхода вершин (по часовой/против) после проекции — фактически по знаку площади или векторному произведению. Это убирает примерно половину треугольников замкнутого объекта.

Z-fighting

Если две поверхности очень близки по глубине, ограниченная точность буфера не может надёжно решить, кто ближе — пиксели мерцают между ними (z-fighting). Буфер глубины нелинейный: он точнее у ближней плоскости и грубее у дальней, поэтому маленький near и огромный far усугубляют проблему.

Точность глубины (схематично):

near |#########| много точности
     |####     |
     |##       |
 far |#        | мало точности -> z-fighting

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

Тест глубины выполняется аппаратно после фрагментного шейдера, но GPU умеет делать early-Z — отбрасывать фрагмент ещё до шейдера, если он заведомо закрыт. Это огромная экономия: нет смысла считать дорогой шейдер для пикселя, который всё равно перекроется. Прозрачные объекты ломают early-Z, поэтому их рисуют отдельно (следующий урок).

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

  • Огромный диапазон near/far → потеря точности и z-fighting.
  • Рисовать прозрачные объекты с записью в z-buffer — они перекроют то, что должно просвечивать.
  • Отключить backface culling без нужды — лишняя работа GPU.

Итоги

  • Z-buffer хранит глубину ближайшего фрагмента на пиксель; ближе — побеждает.
  • Порядок рисования непрозрачных объектов не важен благодаря тесту глубины.
  • Backface culling отбрасывает грани, повёрнутые от камеры.
  • Близкие по глубине поверхности дают z-fighting; виноваты точность и большой far/near.
Проверьте себя
1. Что хранит буфер глубины для каждого пикселя?
AЦвет
BГлубину ближайшего нарисованного фрагмента
CНомер треугольника
DUV-координаты
2. Влияет ли порядок рисования непрозрачных объектов на итог при работающем z-buffer?
AДа, кто позже — тот сверху
BНет, побеждает ближайший по глубине независимо от порядка
CТолько в 2D
DТолько при отключённом culling
3. Что такое early-Z?
AСортировка вершин
BОтбрасывание заведомо закрытых фрагментов до запуска шейдера
CСжатие глубины
DТип текстуры