Растеризация и барицентрические координаты

Растеризация решает, какие пиксели накрывает треугольник, и плавно смешивает в них данные трёх вершин.

Барицентрические координаты — три веса (u, v, w), которые показывают, насколько точка внутри треугольника близка к каждой из его вершин; их сумма равна 1.

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

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

Идея барицентрических координат

Любую точку P внутри треугольника ABC можно записать как взвешенную смесь вершин: P = u·A + v·B + w·C, где u+v+w=1 и все веса неотрицательны. В вершине A веса (1,0,0), в B — (0,1,0), в C — (0,0,1). В центре — (1/3, 1/3, 1/3).

Барицентрические веса в треугольнике:

          A (1,0,0)
         / \
        /   \
       / P   \     P = u*A + v*B + w*C
      /   .    \    u + v + w = 1
     /__________\
   B (0,1,0)   C (0,0,1)

Интерполяция атрибута

Имея веса, любой атрибут (цвет, UV, глубину) в точке P считают теми же весами от значений в вершинах. Покажем интерполяцию цвета.

# Цвета в вершинах треугольника
colorA = (255, 0, 0)    # красная вершина
colorB = (0, 255, 0)    # зелёная
colorC = (0, 0, 255)    # синяя

def interpolate(u, v, w, A, B, C):
    return tuple(round(u*a + v*b + w*c) for a, b, c in zip(A, B, C))

print("В вершине A (1,0,0):", interpolate(1, 0, 0, colorA, colorB, colorC))
print("В центре (1/3 каждая):", interpolate(1/3, 1/3, 1/3, colorA, colorB, colorC))
print("Между A и B (0.5,0.5,0):", interpolate(0.5, 0.5, 0, colorA, colorB, colorC))

Вывод:

В вершине A (1,0,0): (255, 0, 0)
В центре (1/3 каждая): (85, 85, 85)
Между A и B (0.5,0.5,0): (128, 128, 0)

В вершине — чистый красный, в центре — серая смесь трёх цветов, на ребре A-B — жёлто-зелёный переход. Так получаются плавные градиенты на гранях.

Точка внутри или снаружи

Барицентрические веса заодно отвечают на вопрос «накрывает ли треугольник пиксель»: если все три веса ≥ 0, точка внутри; если хоть один отрицателен — снаружи. Растеризатор перебирает пиксели ограничивающего прямоугольника и оставляет те, у которых все веса неотрицательны.

def is_inside(u, v, w):
    return u >= 0 and v >= 0 and w >= 0

print("Веса (0.3,0.3,0.4):", is_inside(0.3, 0.3, 0.4))   # внутри
print("Веса (0.5,0.7,-0.2):", is_inside(0.5, 0.7, -0.2)) # снаружи

Вывод:

Веса (0.3,0.3,0.4): True
Веса (0.5,0.7,-0.2): False

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

В перспективе интерполяцию делают перспективно-корректной: атрибуты делят на глубину w перед интерполяцией и умножают обратно после. Без этого текстуры на наклонных поверхностях «поплывут» (заметный артефакт на старых консолях). Глубина (z) тоже интерполируется барицентрически — её потом сравнивает z-buffer.

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

  • Забыть про перспективную коррекцию — текстуры искажаются на наклонных гранях.
  • Считать, что интерполяция всегда линейна в экранных координатах — в перспективе это не так.
  • Путать барицентрические веса с UV-координатами текстуры — это разные вещи.

Итоги

  • Растеризация определяет покрытые пиксели и интерполирует атрибуты трёх вершин.
  • Барицентрические веса (u,v,w) суммируются в 1 и задают смесь вершин.
  • Все веса ≥ 0 ⇒ точка внутри треугольника.
  • В перспективе интерполяция должна быть перспективно-корректной.
Проверьте себя
1. Чему равна сумма барицентрических координат точки внутри треугольника?
A0
B1
C3
DЗависит от площади
2. Как определить, что точка лежит внутри треугольника, по барицентрическим весам?
AСумма весов больше 3
BВсе три веса неотрицательны
CХотя бы один вес равен 1
DВсе веса отрицательны
3. Зачем нужна перспективно-корректная интерполяция?
AЧтобы ускорить рендер
BЧтобы текстуры не искажались на наклонных к камере гранях
CЧтобы убрать тени
DЧтобы повысить FPS