Карты нормалей и продвинутые текстуры
Карта нормалей создаёт иллюзию шероховатости и рельефа на плоской поверхности, не добавляя ни одного полигона.
Карта нормалей (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).