Карты нормалей и продвинутые текстуры

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

Карта нормалей (normal map) — текстура, в каждом текселе которой закодировано направление нормали; шейдер использует его вместо геометрической нормали при расчёте света.

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

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

Как кодируется нормаль в текстуре

Нормаль — это вектор (x, y, z) с компонентами в диапазоне [-1, 1]. Цвет текселя — это (r, g, b) в [0, 1]. Поэтому нормаль упаковывают в цвет формулой color = normal*0.5 + 0.5, а в шейдере распаковывают обратно. Оттого normal-map выглядят сине-фиолетовыми: «ровная» нормаль (0,0,1) кодируется цветом (0.5, 0.5, 1.0).

def encode(normal):
    return tuple(round(c * 0.5 + 0.5, 2) for c in normal)

def decode(color):
    return tuple(round(c * 2.0 - 1.0, 2) for c in color)

flat = (0.0, 0.0, 1.0)        # нормаль ровной поверхности
tilted = (0.7, 0.0, 0.714)    # наклонённая нормаль
print("Ровная -> цвет:", encode(flat))
print("Наклон -> цвет:", encode(tilted))
print("Распаковка (0.5,0.5,1.0):", decode((0.5, 0.5, 1.0)))

Вывод:

Ровная -> цвет: (0.5, 0.5, 1.0)
Наклон -> цвет: (0.85, 0.5, 0.86)
Распаковка (0.5,0.5,1.0): (0.0, 0.0, 1.0)

Видно, почему карты нормалей синеватые: преобладает ровная нормаль (0,0,1), дающая цвет с синим около 1.0.

Другие карты материала

КартаЧем управляет
albedo / base colorсобственный цвет поверхности
normalнаправление нормалей (рельеф)
roughnessшероховатость (матовое/глянцевое)
metallicметалл это или диэлектрик
ambient occlusion (AO)затенение в углублениях
height / displacementреальное смещение вершин (рельеф)

Карта высот и parallax

Карта высот хранит «глубину» рельефа. Простой parallax-эффект сдвигает UV в зависимости от высоты и угла взгляда, усиливая иллюзию объёма. Смоделируем сдвиг UV.

def parallax_uv(uv, height, view_x, scale=0.05):
    # сдвигаем UV по горизонтали пропорционально высоте и наклону взгляда
    return (round(uv[0] + height * view_x * scale, 4), uv[1])

print("Высота 0.0:", parallax_uv((0.5, 0.5), 0.0, 1.0))
print("Высота 0.8:", parallax_uv((0.5, 0.5), 0.8, 1.0))

Вывод:

Высота 0.0: (0.5, 0.5)
Высота 0.8: (0.54, 0.5)

Высокие участки «сдвигаются» относительно взгляда — мозг читает это как выпуклость.

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

Нормаль из текстуры задана в касательном пространстве (tangent space) поверхности. Чтобы применить её к освещению, шейдер строит базис из нормали, касательной и бикасательной (TBN-матрица) и переводит нормаль в нужное пространство. Поэтому к UV модели добавляют ещё и касательные векторы.

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

  • Не распаковать нормаль (забыть *2-1) — освещение выйдет неправильным.
  • Путать карту нормалей и карту высот — это разные данные.
  • Забыть касательное пространство (TBN) — рельеф «поедет» при повороте объекта.

Итоги

  • Карта нормалей подделывает рельеф без лишних полигонов.
  • Нормаль кодируется в цвет: color = normal*0.5+0.5 (отсюда синева).
  • Roughness, metallic, AO, height — карты, управляющие видом материала.
  • Нормаль из карты применяют через касательное пространство (TBN).
Проверьте себя
1. Почему карты нормалей обычно выглядят синевато-фиолетовыми?
AТак красивее
BРовная нормаль (0,0,1) кодируется цветом (0.5,0.5,1.0) с большим синим
CЭто артефакт сжатия
DСиний цвет ярче
2. Как распаковать нормаль из цвета текселя в шейдере?
Acolor * 2 - 1
Bcolor + 1
Ccolor / 255
D1 - color
3. Что хранит карта roughness?
AЦвет поверхности
BШероховатость — матовость или глянцевость
CНаправление нормалей
DПрозрачность