Перейти к содержимому

Карта нормалей что это

  • автор:

Что такое карты нормалей?

Карта нормалей (Normal Map) — это текстура, используемая в 3d графике,содержащая информацию отклонений нормалей от поверхности и имеет в основном фиолетовый цвет с градиентными переходами до красного цвета и , как бы, показывающая на сколько должна быть искривлена поверхность на сколько выпуклая или вогнута.

В трёхмерной графике это один из самых эффективных способов для создания реалистичных детализированных поверхностей БЕЗ ЗАТРАТ увеличения количества полигонов трёхмерной модели.

Другими словами с помощью карты нормалей мы можем создать например плоскость, состоящую из одного полигона, а выглядеть она будет так, будто бы её детализация была бы в несколько тысяч полигонов, потому, что она будет иметь микрорельеф, например, трещинки, складки, впадины и т.п.

Карты нормалей создаются путём запекания на основе высокополигональных, сверхпроработанных реалистичных трёхмерных моделей. А потом они накладываются в канал смещения Bump и начинают работать. Либо их можно сделать из простого растрового изображения с помощью плагинов в Adobe Photoshop.

Уроки по OpenGL с сайта OGLDev

В основе вычисления векторов тангента / битангента в этом уроке лежит Lengyel, Eric. «Computing Tangent Space Basis Vectors for an Arbitrary Mesh». Terathon Software 3D Graphics Library, 2001. http://www.terathon.com/code/tangent.html

Результат нашего освещения не так уж и плох. Он прекрасно интерполируется по модели и усиливает чувство реализма сцены. Хотя это можно многократно улучшить. Фактически такая интерполяция только помеха, особенно когда текстура пытается изобразить неровности поверхности, это выглядит очень плоско. Для примера взглянем на следующее изображение:

Левое изображение выглядит намного лучше правого. Оно создает ощущение неровности каменистой поверхности, в то время как правое выглядит слишком гладким для текстуры кирпича. Левое изображение рендерилось с использованием метода Карты Нормалей (Normal Mapping) (или Bump Mapping), и это будет темой данного урока.

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

Сейчас мы имеет нашу карту нормалей и истинные (или по крайней мере довольно близкие) нормали поверхности, записанные в ней. Можем ли мы взять и начать ее использовать? нет. Задумаемся на секунду о кубе выше с текстурой кирпича. Одна и также текстура используется для всех 6 граней и следовательно, будет использоваться одинаковая карта нормалей. Проблема в том, что каждая грань направленна в свою сторону, поэтому взаимодействие с падающим лучем должно быть различным. Если мы будем использовать вектор нормали из карты без каких-либо изменений, то мы получим неправильный результат, потому что одинаковый вектор нормали не может быть использован для 6 граней с различным направлением! Например, нормаль верхней грани в целом направленна в (0,1,0), даже при неровностях поверхности. А нормаль нижней грани равна вектору (0,-1,0). Смысл в том, что нормали определены в их собственном пространстве координат и должны быть переведены в мировое пространство, тогда они смогут принять участие в вычислении света. Ощущение, что мы уже где-то с этим сталкивались (вершинные нормали). Они были определены в локальном пространстве, и мы преобразовывали их в мировое пространство через мировую матрицу.

Давайте определим систему координат, в которой будут заданы вектора нормалей. Эта координатная система потребует 3 ортогональные оси единичной длины. Так как нормаль — часть 2D текстуры, и 2D текстура имеет 2 ортогональные оси U и V (обе единичной длины) основная задача отобразить координату X на ось U, а Y на V. Вспомним, что U идет слева направо, а V снизу вверх (начало координат этой системы в нижнем левом углу текстуры). Координата Z будет выходить из текстуры вверх и будет перпендикулярна и X и Y:

Вектора нормалей теперь могут быть указанны по отношению к координатной системе и записаны в RGB текселях текстуры. Заметим, что даже на неровной поверхности нам все равно требуется общее направление нормалей из текстуры, т.е. координата Z будет доминировать, в то время как X и Y будут только немного (или много) наклонять вектор из стороны в сторону. Хранение координат XYZ вектора в текселе RGB будет делать карту нормалей голубоватого оттенка как на следующем изображении:

Вот 5 первых текселей верхней строки карты нормалей (идем слева в право): (136,102,248), (144,122,255), (141,145,253), (102, 168, 244) и (34,130,216). Преобладание Z не может не быть замеченным.

Следующее что мы делаем, это проход через все треугольники нашей модели и размещение карты нормалей так, что бы координаты текстуры каждой вершины получили их позицию на карте. Например, пусть координаты текстуры полученной треугольником (0.5,0), (1, 0.5) и (0,1). Карта нормалей расположится следующим образом:

На изображении выше координатная система в нижнем левом углу представляет локальное пространство объекта.

Кроме координат текстуры 3 вершины так же имеют 3D координаты, которые представляют их позицию в локальном пространстве. Когда мы помещаем текстуру на поверхности треугольника, мы по существу даем значения для U и V векторов текстуры в локальном пространстве. Если мы будем вычислять U и V сейчас, то мы генерируем матрицу преобразований для перемещения нормалей с карты в локальное пространство. После они могут быть преобразованы в мировое пространство как обычно и принять участие в расчетах света. Обычно вектор U в локальном пространстве называют Тангент (Tangent) и вектор V Битангент (Bitangent). Матрица преобразования, которая нам понадобится для определения координатной системы, называется матрицей TBN (Tangent-Bitangent-Normal). Эти векторы Тангент-Битангент-Нормаль и определяют систему координат, которая известна как пространство (или текстура) Тангента. Поэтому нормали на карте записаны в тангент / текстурном пространстве. Теперь попробуем определить как вычислить U и V локальном пространстве.

Давайте взглянем на изображение выше абстрагировано. У нас есть треугольник с 3 вершинами в точках P0, P1 и P2 и координатами текстуры (U0,V0), (U1,V1) и (U2,V2):

Мы хотим найти вектора T (Тангент) и B (Битангент) в локальном пространстве. Мы можем увидеть, что 2 стороны треугольника E1 и E2 могут быть записаны в виде линейной комбинации T и B:

Это может быть записано как:

Теперь очень легко перейти в матричную форму:

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

Тогда мы получим следующее:

После нахождения обратной матрицы:

Мы можем запустить этот процесс для каждого треугольника в меше и генерировать вектора тангент и битангент для всех (одни и те же вектора для 3 вершин треугольника). Обычно хранят тангент / битангент для вершин через нахождение их среднего значения для всех треугольников, содержащих эту вершину (так же мы поступали для нормалей вершин). Причина тому эффект сглаживание при проходе по поверхности треугольника для избежания жесткой границы на краях соседних треугольников. Третий элемент этой координатной системы — нормаль — это результат векторного произведения тангента и битангента. Эта троица — Tangent-Bitangent-Normal теперь может служить как базовая координатная система и быть использована для преобразования нормали из карты нормалей в локальное пространство объекта. Следующий шаг — это преобразовать ее в мировое пространство и использовать для вычисления света. Однако, мы можем немного оптимизировать: преобразовав Tangent-Bitangent-Normal в мировое пространство, а затем преобразовывать только нормали из карты нормалей. Это позволит получать нормали сразу в мировом пространстве.

Алгоритм данного урока:

  1. Предоставить вектор тангент в вершинный шейдер.
  2. Преобразовывать вектор тангент в мировое пространство и передавать его в фрагментный шейдер.
  3. Использовать вектор тангент и нормаль (оба в мировом пространстве) для вычисления битангента в фрагментном шейдере.
  4. Генерировать матрицу мирового преобразования через тангент-битангент-нормаль.
  5. Взять сэмпл из карты нормалей.
  6. Преобразовывать нормали в мировое пространство через матрицу выше.
  7. Продолжать вычисление света как обычно.

Есть одна особенность, которую мы должны отразить в нашем коде. На уровне пикселя тангент-битангент-нормаль не являются ортонормированным базисом (3 вектора единичной длины, которые взаимно-перпендикулярны друг другу). Если 2 причины этого: первая — мы усредняем значения для тангента и нормали у вершин треугольников, которые делят их, и вторая — тангент и нормаль интерполируются растеризатором, и на уровне пикселя мы видим лишь интерполируемый результат. Это заставляет тангент-битангент-нормаль потерять их ортогональное свойство. Но для преобразования из пространства тангента в мировое нам требуется ортонормированный базис. Решение в использовании процесса Грама ― Шмидта. Он принимает группу векторов и переводит их в ортонормированный базис. В двух словах процесс выглядит так: выбор вектора ‘A’ из группы и его нормирование. Затем выбор вектора ‘B’ и разделение его на 2 компонента (эти компоненты — вектора, чья сумма дает ‘B’), где первый проекция на ‘A’, а второй перпендикуляр к нему. Теперь заменим ‘B’ на компонент, который перпендикулярен к ‘A’ и нормируем его. Процесс продолжается для всех векторов в группе.

В результате мы не используем математически корректные вектора тангент-битангент-нормали, но мы получим сглаживание, которое требуется для избежания разрывов на границах треугольников.

Прямиком к коду!

Это наша новая структура для вершины с дополнительным вектором тангентом. Мы будем вычислять битангент в фрагментном шейдере. Заметим, что нормаль тангентного пространства идентична нормали обычного треугольника (так как текстура и треугольник параллельны). Поэтому вершинная нормаль в обоих координатных системах одинакова.

Этот участок кода реализует алгоритм вычисления вектора тангента (описан в разделе теории). Проход идет по массиву индексов и вектора треугольников извлекаются из массива вершин по их индексу. Мы вычисляем 2 стороны через вычитание первой вершины из второй и третьей. Аналогично поступаем с координатами текстуры и вычисляем разность вдоль U и V осей на 2 сторонах. ‘f’ обозначает дробь, которая будет участвовать в вычислениях справа. После нахождения ‘f’ мы можем вычислить и тангент и битангент через умножение его на произведение 2 матриц. Заметим, что вычисления битангента находятся для полноты картины. На самом деле нам требуется только тангент, который мы передаем на 3 вершины. В конце нам требуется только пройтись по массиву вершин и нормировать тангент.

Теперь, когда вы все это поняли в теории и на практике, я могу вам сообщить, что мы не будем использовать этот код в уроке. Assimp имеет флаг пост-обработки, называемый ‘aiProcess_CalcTangentSpace’, который для нас вычисляет именно вектор тангент (в любом случае хорошо знать реализацию, возможно потребуется ее использовать в будущих проектах). Нам только требуется указать это во время загрузки модели, и затем мы можем получить доступ к массиву ‘mTangents’ в классе aiMesh и взять значения тангента отсюда. Смотрите код для деталей.

Так как структура вершин разрастается, нам требуется добавить несколько модификаций в функцию рендера класса Mesh. Четвертый атрибут вершины разрешен к использованию и мы указываем позицию тангента в 32 байте (стразу после нормали) от начала вершины. В конце 4-й атрибут отключается.

Это обновленный вершинный шейдер. Здесь не так много нового, большая часть изменений в фрагментном шейдере. Добавился тангент, который принимается, трансформируется в мировое пространство и идет на выход.

Код выше содержит большую часть изменений фрагментного шейдера. Вся обработка нормалей инкапсулирована в функцию CalcBumpedNormal(). Мы начинаем с нормирования векторов нормали и тангента. Третья строка — это процесс Грама-Шмидта. dot(Tangent, Normal) дает длину проекции тангента на вектор нормали. Произведение этой длины на саму нормаль — компонент тангента вдоль нормали. Вычтем ее из тангента и получим новый вектор, который перпендикулярен к нормали. Это наш новый тангент (не забудьте его нормировать!). Векторное произведение между тангентом и нормалью дает нам битангент. Затем мы берем сэмпл из карты нормалей и получаем нормаль для этого пикселя (в пространстве тангента). ‘gNormalMap’ — новая uniform-переменная типа sampler2D, для которой мы заранее должны привязать карту нормалей перед вызовом отрисовки. Нормаль записана как цвет, поэтому ее компоненты в отрезке [0-1]. Мы преобразуем ее обратно через функцию ‘f(x) = 2 * x — 1’. Она отображает 0 в -1 и 1 в 1, это обратно тому, что происходило при записи нормали.

Теперь нам требуется преобразовать нормаль из пространства тангента в мировое. Мы создаем матрицу 3×3, названную TBN, используя один из конструкторов типа ‘mat3’. Это конструктор принимает 3 вектора как параметры и генерирует матрицу помещая первый параметр в верхнюю строку, второй в среднюю и третий в нижнюю. Если вам не понятно почему именно этот порядок, а не какой-нибудь другой, просто запомните, что тангент отображается на ось X, битангент на Y, и нормаль на Z (смотрите на изображение выше). В единичной матрице 3×3 верхний ряд хранит ось X, средний ось Y, и нижний ось Z. Мы просто соблюдаем этот порядок. Нормаль из пространства тангента умножается на матрицу TBN, и результат нормируется перед отправлением. Это итоговая нормаль пикселя.

Этот урок поставляется со следующими JPEG файлами:

  1. ‘bricks.jpg’ — это текстура цвета.
  2. ‘normal_map.jpg’ — карта нормалей, которая была генерирована с ‘bricks.jpg’.
  3. ‘normal_up.jpg’ — такая же карта нормалей, только все нормали направленны вверх. Когда используется эта карта, то теряется весь эффект. Проще привязать эту текстуру, когда мы не хотим использовать отображение нормалей, без использования 2 техник (хотя это и затратно). Вы можете использовать ‘b’ для переключения между отображением нормалей и рендером без него для того, что бы увидеть разницу.

Карта нормалей привязана к модулю текстур 2, который теперь будет стандартом для это цели (0 для цвета и 1 для карты теней).

Замечание о генерации карты нормалей:

Существует множество способов генерировать карту нормалей. Для этого урока я использовал gimp, свободное приложение, и его плагин для карты нормалей. После того, как вы установили плагин, вы просто загружаете текстуру, которую планируете использовать для модели, переходите в Filters->Map->Normalmap. У вас появится возможность изменить некоторые аспекты карты нормалей и настроить ее различными способами. Когда подберете подходящую нажмите ‘OK’. Карта заменит оригинальную текстуру в главном окне gimp. Сохраните ее под другим именем и используйте в своих работах.

Это норма: что такое карты нормалей и как они работают

На протяжении нескольких лет я пытался разобраться в картах нормалей и в проблемах, которые обычно возникают при работе с ними.

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

Первые созданные человеком 3D-модели выглядели примерно так:

image

Это замечательно, но у такой модели есть очевидное ограничение: она выглядит слишком полигональной.

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

image

Требовалось другое решение, и так были изобретены нормали. (Всё происходило не совсем так, но так проще объяснять и понимать.)

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

image

Другими словами, отражение света будет симметрично относительно нормали полигона. Именно так работает большинство отражений в реальном мире. По умолчанию лучи света отражаются от всех полигонов совершенно перпендикулярно к их поверхности (как должны это делать в реальной жизни), потому что нормали полигона по умолчанию перпендикулярны к поверхности полигона. Если в нормалях будут пробелы, то мы увидим их как отдельные поверхности, поскольку свет отразится в одном или другом направлении.

image

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

image

Мы будем воспринимать переход как единую поверхность, потому что свет будет отражаться между одним и другим полигоном плавным образом, и между ними не будет пробелов. По сути, свет отражается от этих полигонов плавно, как будто у нас имеется множество полигонов.

Именно этим мы управляем, задавая smoothing groups (3ds Max, Blender) или указывая рёбра как hard или smooth (Modo, Maya): мы сообщаем программе, какие переходы между гранями должны быть плавными, а какие — жёсткими.

Вот сравнение одной сферы из 288 полигонов с жёсткими и плавными переходами:

image

Потенциально мы можем задать нечто вроде параллелепипеда, чтобы все его вершины имели усреднённые нормали. 3D-редактор будет стремиться сгладить его поверхность, чтобы она выглядела как единая плавная поверхность. Для 3D-редактора это вполне логично, но выглядит очень странно, потому что у нас есть объект, который очевидно должен иметь несколько отдельных поверхностей (каждая грань параллелепипеда), однако программа пытается показать их как одну плавную поверхность.

image

Именно поэтому в 3D-редакторах обычно есть параметр углов сглаживания: если у нас есть два связанных полигона под углом, превышающем угол сглаживания, то их переход будет плавным, а соединение полигонов под углом меньше угла сглаживания будет жёстким. Благодаря этому крутые углы между поверхностями будут отображаться как разные поверхности, как это и бывает в реальном мире.

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

Так как мы меняем способ отражения света от объекта, можно также сделать так, чтобы очень простой объект отражал свет, как сложный. Это называется картой нормалей. Мы используем текстуру для изменения направления света, отражающегося от 3D-объекта, заставляя его выглядеть сложнее, чем он есть на самом деле.

Примером из реального мира могут служить голограммы, которые раньше вручали в подарок при покупке картофельных чипсов (по крайней мере, у нас, в Испании). Они совершенно плоские, но отражают свет так, как бы это делал 3D-объект, благодаря чему становятся сложнее, чем на самом деле. В мире 3D-графики это работает даже лучше, но всё равно имеет свои ограничения (поскольку поверхность остаётся плоской).

Хоть мы и применяем нормали полигонов для реализации какой-то чёрной магии, на самом деле мы не контролируем сглаживание поверхности модели при помощи нормалей полигонов. Мы используем нормали вершин для контроля сглаживания нормалей. По сути, идея та же, но немного более сложная.

С каждой вершиной может быть связано одна или несколько нормалей. Если она имеет одну нормаль, то можно назвать её усреднённой нормалью вершины, а если несколько — то разделённой нормалью вершины.

Давайте возьмём два полигона, соединённых ребром. Если переход между двумя гранями плавный (если мы указали его как плавный в Maya/Modo, или обе имеют одинаковую smoothing group в Max/Blender), то каждая вершина имеет одну нормаль, которая является средней нормалей полигонов (поэтому она и называется усреднённой нормалью вершины). Важное примечание: до недавнего времени каждый 3D-редактор использовал собственный способ вычисления усреднённых нормалей вершин, то есть карты нормалей, вычисленные в одной программе, в другой могли выглядеть совершенно иначе. Подробнее об этом я расскажу во второй части туториала.

image

Если переход жёсткий (hard edge или разные smoothing groups), то каждая вершина имеет несколько нормалей: по одной для каждой соединённой вершины, выровненной по их нормалям. При этом между нормалями образуется пробел, который выглядит как две разные поверхности. Именно это называется разделённой нормалью вершины.

image

image

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

При запекании карты нормалей мы по сути говорим программе изменить направление, которому следуют нормали lowpoly-модели, так, чтобы они соответствовали направлению в highpoly-модели; поэтому lowpoly-модель будет отражать свет так же, как highpoly. Вся эта информация хранится в текстуре под названием «карта нормалей». Давайте рассмотрим пример.

Допустим, у нас есть вот такая низкополигональная модель (lowpoly). Плоская поверхность с четырьмя вершинами и настроенными UV, которые программа запекания будет использовать для создания карты нормалей.

image

И она должна получить информацию о нормалях от этой высокополигональной (highpoly) модели, нормали которой сложнее.

image

Помните, что мы переносим только информацию о нормалях, то есть UV, материал, топология, преобразования и т.п. к делу не относятся. Проверенное правило: если highpoly-модель выглядит хорошо, то её нормали тоже хороши и вполне должны подходить для запекания.

Программа запекания берёт lowpoly-модель и испускает лучи, следуя по направлениям нормалей lowpoly (именно поэтому нам нужно контролировать нормали lowpoly). Эти лучи имеют ограниченную длину чтобы не получать информацию нормалей от далёких граней (обычно это расстояние называется bake distance или cage distance). Когда эти лучи сталкиваются с highpoly, программа запекания вычисляет, как отразить эти лучи, чтобы они следовали по направлению нормалей highpoly, и сохраняет эту информацию в карту нормалей.

image

Вот результат запекания для нашего примера:

image

У нас есть текстура, которую движок использует для изменения нормалей lowpoly, чтобы свет отражался от этой lowpoly-модели так же, как он отражался бы от highpoly-версии. Не забывайте, что это только текстура, которая не влияет на силуэт lowpoly-модели (невозможно изменить способ отражения света от модели, если свет не падает на эту модель).

Хотя понятно, что можно «считать» внешний вид highpoly по внешнему виду карты нормалей, очевидно, что карты нормалей — это не обычные текстуры, потому что они хранят информацию не о цвете, а о нормалях. Также это значит, что карты нормалей нельзя рассматривать как обычные текстуры; к тому же, как мы увидим, они обладают особыми параметрами сжатия и гамма-коррекции.

Можно воспринимать карту нормалей как набор из трёх текстур в оттенках серого, хранящийся в одном изображении:

image

Первое изображение сообщает движку, как эта модель должна отражать свет, падающий справа; оно хранится в красном канале текстуры карты нормалей.

Второе изображение сообщает движку, как модель должна отражать свет, падающий снизу*; оно хранится в зелёном канале текстуры карты нормалей.

*В некоторых программах свет падает не снизу, а сверху, то есть могут быть «левосторонние» и «правосторонние» карты нормалей. Как мы увидим позже, это может вызывать некоторые проблемы.

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

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

Нормали — это векторы, которые используются для определения того, как свет отражается от поверхности. Их можно использовать для контроля над переходом между гранями (усреднением нормалей соединённых вершин для создания плавного перехода или разделением их для создания жёсткого перехода), но также их направление можно изменять, чтобы lowpoly-модель отражала свет так же, как более сложная модель.

Эта информация хранится в трёх отдельных каналах изображения, и 3D-редактор считывает её, чтобы понять, в каком направлении должна смотреть поверхность модели.

В следующей статье цикла мы поговорим о том, как можно запекать эти детали из highpoly-модели в lowpoly.

Урок №34. Карты нормалей в OpenGL

Все наши сцены были заполнены мешами, состоящими из сотен или (даже) тысяч треугольников. Мы усилили ощущение реализма сцены, наложив на них 2D-текстуры, маскируя тот факт, что полигоны — это всего лишь крошечные плоские треугольники. Да, использование текстур помогает сделать картинку более привлекательной, но когда вы внимательно смотрите на меши, то все еще довольно легко можно разглядеть лежащие под ними плоские поверхности. Однако большинство реальных поверхностей не являются плоскими объектами и обладают большим количеством различных выпуклых (бугристых) деталей.

Проблема

Возьмем, к примеру, кусок кирпичной стены. Данная поверхность довольно шероховатая и, очевидно, не совсем плоская: она содержит впалые цементные полосы и множество маленьких детализированных дырок и трещин. Если бы мы рассматривали такую кирпичную поверхность в освещенной сцене, то её внешний вид мог бы легко разрушить веру в реальность подобной стены. Ниже мы видим текстуру кирпичной стены, нанесенную на плоскую поверхность и освещенную точечным источником света:

Система освещения не принимает во внимание мелкие трещины и отверстия в стене и полностью игнорирует глубокие полосы между кирпичами; поверхность выглядит идеально плоской. Используя зеркальную карту, мы можем частично исправить данную ситуацию, делая некоторые части поверхности менее освещенными из-за их глубины или наличия рядом других деталей, но данный метод больше похож на халтуру, нежели на реальное решение проблемы. Что нам нужно, так это каким-то образом проинформировать систему освещения обо всех мелких глубинных деталях поверхности.

Давайте рассмотрим описываемую ситуацию с точки зрения источника света: как получается, что данная поверхность освещается как абсолютно плоская? Ответ на этот вопрос кроется в её векторах нормали. С точки зрения техники освещения, единственный способ определить форму объекта — это использовать его вектор нормали. Кирпичная поверхность имеет только один выраженный вектор нормали, и в результате, в зависимости от его направления, она равномерно освещается. Что, если вместо вектора нормали к поверхности, который является одинаковым для каждого её фрагмента, мы будем использовать векторы нормали каждого фрагмента? Таким образом, мы можем слегка отклонить вектор нормали, основываясь на мелких деталях поверхности; создать иллюзию того, что внешний вид поверхности будет выглядеть более проработанным:

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

Применительно к кирпичной стене это выглядит следующим образом:

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

Карты нормалей

Чтобы задействовать карту нормалей, нам понадобятся нормали каждого фрагмента. Подобно тому, как мы работали с диффузными и зеркальными картами, мы будем использовать 2D-текстуру для хранения данных о нормалях каждого фрагмента. Таким образом, производя выборку из 2D-текстуры, мы получим нормальный вектор для конкретного фрагмента.

В то время как векторы нормалей являются геометрическими объектами, а текстуры обычно используются только для цветовой информации, способ организации хранения векторов нормалей внутри текстуры может показаться неочевидным. Цветовой вектор текстуры представлен как 3D-вектор с r-, g- и b- компонентами. Аналогичным образом мы можем хранить x-, y- и z- компоненты вектора нормали в соответствующих цветовых компонентах. При этом стоит учитывать, что значения компонентов векторов нормалей задаются диапазоном от -1 до 1 , поэтому необходимо сначала отобразить их в диапазоне [0,1] :

Используя нормальные векторы, преобразованные в цветовой диапазон RGB, мы можем хранить в 2D-текстуре вектор нормали каждого фрагмента поверхности. Пример карты нормалей поверхности кирпичной стены (рассматривалась в начале урока) показан ниже:

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

Используя обычную плоскость, смотрящую в сторону положительной оси z, а также диффузную текстуру,…:

…мы можем визуализировать изображение из предыдущего параграфа. Обратите внимание, что связанная карта нормалей отличается от той, что показана выше. Причина этого заключается в том, что OpenGL считывает инвертированную y- (или v-) координату текстуры. Таким образом, связанная карта нормалей содержит инвертированную y- (или «зеленую») составляющую (вы можете видеть, что зеленые цвета теперь направлены вниз); если вы не примете это во внимание, то освещение будет неправильным. Загрузите обе текстуры, свяжите их с соответствующими текстурными юнитами и визуализируйте плоскость со следующими изменениями во фрагментном шейдере освещения:

Здесь мы инвертируем процесс отображения нормалей в цвета RGB, переназначая выборку нормального цвета из диапазона [0,1] обратно в диапазон [-1,1] , а затем используем выборку нормальных векторов для предстоящих расчетов освещения. В данном случае мы использовали шейдер Блинна-Фонга.

Используя карты нормалей и медленно перемещая с течением времени источник света, мы получаем ощущение сцены. Запуск данного примера дает результаты, которые были показаны в начале этого урока:

Однако есть одна проблема, которая значительно ограничивает использование карты нормалей. Карта нормалей, с которой мы работали, имела векторы нормалей, указывающие в положительном направлении оси z. И всё было хорошо, т.к. нормаль поверхности плоскости также указывала в положительном направлении оси z. Однако, что произойдет, если мы будем использовать одну и ту же карту нормалей для плоскости, лежащей на земле, с вектором нормали, указывающим в положительном направлении оси y?

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

Вы можете видеть, что в целом все нормали указывают в положительном направлении оси z, хотя они должны указывать в сторону положительного направления оси y. Одним из решений данной проблемы является определение карты нормалей для каждого возможного направления поверхности; в случае куба нам понадобилось бы шесть карт нормалей. Однако при использовании продвинутых мешей, которые могут иметь более сотни возможных направлений поверхности, такой подход становится неосуществимым.

Есть другое решение, которое приводит всё освещение к другому координатному пространству, в котором векторы карты нормалей всегда указывают в положительном направлении оси z, а все другие векторы освещения преобразуются относительно данного положительного направления оси z. Таким образом, мы всегда можем использовать одну и ту же карту нормалей, независимо от ориентации поверхности. Описываемое координатное пространство называется касательным пространством.

Касательное пространство

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

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

Такая матрица называется TBN-матрицей (сокр. от «Tangent, Bitangent, Normal векторы» — это те векторы, которые нам нужны для построения искомой матрицы). Чтобы построить такую матрицу изменения базиса, которая преобразует вектор касательного пространства в другое координатное пространство, нам нужны три перпендикулярных вектора, два из которых выровнены вдоль поверхности карты нормалей, а третий перпендикулярен ей (аналогично тому, что мы делали на уроке о камере в OpenGL).

Мы уже знаем вектор-вверх, который является вектором нормали поверхности. Вектор-вправо и вектор-вперед являются касательным и бикасательным векторами соответственно. На следующем изображении поверхности показаны все три вектора:

Процесс вычисления касательного и бикасательного векторов является немного более сложным по сравнению с вычислением нормального вектора. Из рисунка видно, что направления касательного и бикасательного векторов карты нормалей совпадают с направлениями, в которых мы определяем координаты текстуры поверхности. Мы будем использовать данный факт для вычисления касательного и бикасательного векторов для каждой поверхности. Для получения данных векторов нам потребуются некоторые знания из математики. Взгляните на следующее изображение:

Из рисунка видно, что изменения текстурных координат (обозначены как ΔU2 и ΔV2 ) ребра E2 треугольника выражаются вдоль направления касательного вектора T и бикасательного вектора B соответственно. Поэтому мы можем выразить оба ребра E1 и E2 треугольника через линейную комбинацию касательного вектора T и бикасательного вектора B:

Что также может быть записано в координатном виде:

Мы можем вычислить E как вектор разности между двумя точками треугольника, а ΔU и ΔV — как разности их текстурных координат. Тогда мы остаемся с двумя неизвестными переменными (касательным вектором T и бикасательным вектором B) и двумя уравнениями. Из курса алгебры известно, что в таком случае мы можем получить решение уравнения и для T, и для B.

Последние уравнения позволяют нам переписать их в форме умножения матриц:

Попробуйте мысленно представить себе умножение матриц и убедитесь, что это действительно одно и то же уравнение. Преимущество переписывания уравнений в матричном виде состоит в том, что нам будет легче понять нахождение решения для T и для B. Если мы умножим обе стороны уравнений на обратную ΔUΔV -матрицу, то получим следующее:

Я не буду вдаваться в математические детали вычисления обратной матрицы, скажу лишь, что она равняется единице, деленной на определитель матрицы и умноженной на её смежную матрицу:

Итоговое уравнение дает нам формулу для вычисления касательного вектора T и бикасательного вектора B на основе двух ребер треугольника и его текстурных координат.

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

Ручной расчет касательного и бикасательного векторов

В предыдущей программе у нас была обычная плоскость, ориентированная в положительном направлении оси z и с наложенной на нее картой нормалей. На этот раз мы хотим реализовать наложение карты нормалей с использованием касательного пространства, чтобы мы могли ориентировать эту плоскость так, как мы хотим, и при этом карта нормалей все равно бы работала как надо. Используя ранее рассмотренную математическую теорию, мы собираемся вручную вычислить касательные и бикасательные векторы этой поверхности.

Предположим, что плоскость построена на следующих векторах (образующих два треугольника: 1, 2, 3 и 1, 3, 4):

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *