Bump mapping.
Автор: Shuher
Сейчас я расскажу вам о таком эффекте, как Bump Mapping. Это словосочетание хорошо известно в кругах не только разработчиков игр, но и простых геймеров. Однако, что же это такое многие так и не знают. В этой статье я постараюсь раскрыть многие аспекты этой технологии.
Итак, что же такое bump-mapping? Это технология, которая позволяет на плоской поверхности полигона моделировать микрорельеф без особых затрат на вычисления. Фактически, у нас есть карта высот, наподобие той, которая используется для моделирования ландшафтов, однако она наносится на каждый полигон по-отдельности, причем особым образом. Подобный подход и называется bump-mapping'ом.
Сперва, необходимо выяснить, а почему мы вообще видим микронеровности на объектах в реальной жизни? Всем известно, что степень освещенности поверхности зависит от угла, под которым на него падает свет. Чем этот угол ближе к 0 (относительно нормали), тем освещенность сильнее. При увеличении этого угла степень освещенности падает (так называемый закон косинуса). Если у нас есть однородная поверхность, то есть, нет никаких неровностей, то и нормаль в каждой точке этого полигона будет одинаковой. А, следовательно, и освещенность объекта будет одинаковой. Но если поверхность неровная, то и нормаль в каждой точке разная. И, как следствие, освещение становится неровным — где-то светлее, а где-то темнее. По таким признакам наш мозг и определяет неровности на объектах. Он замечает игру светотени и сигнализирует нам о том, что эта поверхность неровная.
При использовании bump mapping мы поступаем точно также. Мы задаем в каждой точке нормаль к поверхности, которая и учитывается при вычислении освещения. Для этого нам потребуется только нормализированный вектор от источника света и немного математики.
Сразу возникает вопрос: как задать нормаль в каждой точке? А как обычно мы задаем цвет в каждой точке? Правильно: накладываем текстуру. Так вот, по счастливой случайности координаты вектора, как и цвет, кодируются тремя числами. Для цвета это R, G и B, а для вектора это X, Y и Z. А что если мы совместим координаты с цветом, то есть сопоставим R с X, G с Y, а B с Z? Фактически, цвет мы стали воспринимать как координаты вектора! Что это даёт? А то, что теперь нормаль мы можем представить как цветную точку. А из таких точек можно создать текстуру, где каждая точка - нормаль.
Cледует остановиться на одном важном моменте. Дело в том, что цвет кодируется тремя положительными числами в диапазоне [0;1]. Однако, у вектора вполне могут быть и отрицательные координаты. Для решения этой проблемы мы сделаем небольшое преобразование с координатами вектора. Допустим, у нас есть координата X нормализированного вектора. Она лежит в диапазоне [-1,1]. Нам необходимо получить ту же координату, но только в диапазоне [0;1], однако с возможностью распаковки его обратно в [-1;1]. Для этого преобразуем его по формуле X0 = X*0.5 + 0.5. Подставив любое значение диапазона [-1;1], мы получим значение в диапазоне [0;1], причем сжатие происходит равномерно, так как функция преобразования- линейная. Для того, чтобы распаковать значение, достаточно выполнить обратные действия, а именно X = (X0 - 0.5)*2. Например, вектор (0,0,1) будет закодирован как (0.5,0.5,1), то есть этот вектор будет иметь ядовито-фиолетовый цвет.
Для расчета карт нормалей существуют специальные программы. На сайте nVidia для разработчиков есть плагин к PhotoShop'у. Все они действуют по одному и тому же принципу. В них задается карта высот, которая представляет собой черно-белое изображение, на котором более светлые пятна соответствует более высоким местам. Вот пример простой карты высот и соответствующей ей карте нормалей.
Казалось бы — все. Вот тебе нормаль, вот источник света. Только считай, да любуйся красотами :) Но это ещё не все! Даже если мы будем использовать подобный метод освещения для каждого полигона, результат будет некорректным. Все дело в том, что при расчете освещения мы никак не будем учитывать направление нормали самого полигона. Ведь если мы будем каким-то образом вращать полигон, то и освещенность также должна меняться. Однако, как бы мы ни меняли ориентацию полигона, нормали на карте нормалей будут направлены в одно и то же направление. То есть, освещение, в итоге, не будет зависеть от ориентации полигона. Для исправления этой ошибки необходимо повернуть каждую нормаль в карте нормалей. Но посудите сами: допустим, у нас карта нормалей состоит из 128*128=16384 нормалей, и их всех придется повернуть. Считать - не пересчитать. А давайте будем поворачивать не сами нормали, а вектор источника света! Тогда и поворачивать придется только один вектор, что, несомненно, проще.
Для того, чтобы повернуть вектор, достаточно перевести его в новую систему координат, связанную с полигоном, где нормаль будет осью Z в новой системе. Для этого достаточно умножить вектор на матрицу преобразования. Она имеет такой вид:
Где вектор N(x,y,z) - нормаль к полигону, B(x,y,z) - бинормаль, T(x,y,z) - tangent.
[Tx Ty Tz]
[Bx By Bz]
[Nx Ny Nz]
Что такое нормаль (Normal), я думаю, понятно всем. А вот что такое бинормаль (Binormal) и Tangent не совсем ясно. Это два вектора, которые перпендикулярны друг другу и одновременно перпендикулярны нормали. То есть, эти 3 вектора представляют базис новой системы координат (ее ещё называют tangent space), в которую мы и собираемся переводить вектор направления на источник света.
Но как получить B и T, если у нас есть только нормаль? Самый простой способ — использовать два векторных произведения. Вначале, берем любой вектор, неколлинеарный нормали, и находим их векторное произведение. В итоге, мы получаем вектор T. Для того, чтобы получить B, достаточно векторно умножить T на N. Вот мы и получили 3 вектора базиса, из которого составляем матрицу трансформации. Умножаем ее на вектор источника света и получаем необходимый вектор.
А далее — дело техники. Просто скалярно умножаем каждый вектор в карте нормалей на преобразованный вектор от источника и умножаем его на диффузную компоненту данной точки (на цвет, взятый из главной текстуры).
Вот и все. Весь этот алгоритм очень хорошо ложится на вершинные и пиксельные шейдеры, особенно с использованием Cg. Поэтому было бы грех не продемонстрировать его реализацию.
Все расчеты начинаются с вершинного шейдера:
struct appdata { float4 Position : POSITION; //позиция вершины float3 Normal : NORMAL; //нормаль к полигону float2 TexCoord1 : TEXCOORD0; //главные текстурные координаты float2 TexCoord2 : TEXCOORD1; //текстурные координаты bump-текстуры }; struct vfconn { float4 Position : POSITION; //in projection space float2 TexCoord1 : TEXCOORD0; //главные текстурные координаты float2 TexCoord2 : TEXCOORD1; //текстурные координаты bump-текстуры float3 LightVector : COLOR0; //вектор источника света }; vfconn main(appdata IN, uniform float4x4 ModelViewProj, uniform float3 lightpos) { vfconn OUT; //главная трансформация вершины OUT.Position = mul(ModelViewProj, IN.Position); //передаем текстурные координаты пиксельному шейдеру без изменений OUT.TexCoord1 = IN.TexCoord1; OUT.TexCoord2 = IN.TexCoord2; //рассчитываем tangent space float3 normal = IN.Normal; float3 binormal = cross(float3(1,0,0),normal); float3 tang = cross(normal,binormal); //формируем матрицу трансформации вектора источника света float3x3 t; t[0] = tang; t[1] = binormal; t[2] = normal; //находим вектор направления света и умножаем на матрицу трансформации float3 light = mul(t,normalize(lightpos - IN.Position)); //переводим в диапазон [0;1] OUT.LightVector = 0.5 * light + 0.5.xxx; return OUT; } //main
Здесь все, должно быть, понятно. Обратите внимание, что для того, чтобы вычислить вектор бинормали, я взял вектор (1,0,0), то есть параллельный оси ОХ. Этот алгоритм чаще всего используется для реализации bump-mapping'а на ландшафте с регулярной сеткой. А, так как там не может быть нормали параллельной ОХ, то и векторное произведение никогда не выродится в нуль-вектор. Также обратите внимание на второе векторное произведение. Здесь я нормаль умножал на бинормаль. Однако, если у вас возникнут проблемы с некорректностью освещения, то есть выпуклые области будут казаться вогнутыми, то просто поменяйте эти два вектора местами.
Далее работа передается пиксельному шейдеру:
struct v2f { float2 Position : POSITION; //in projection space float2 TexCoord1 : TEXCOORD0; //первая текстура float2 TexCoord2 : TEXCOORD1; //вторая текстура float3 LightVector : COLOR0; //вектор источника света }; float3 main(v2f IN, uniform sampler2D tex1, uniform sampler2D tex2) : COLOR { //читаем цвет из основной текстуры float3 Color1 = tex2D(tex1,IN.TexCoord1); //читаем нормаль из карты нормалей и переводим ее в диапазон [-1;1] float3 bumpNormal = expand(tex2D(tex2,IN.TexCoord2)); //переводим вектор источника света в диапазон [-1;1] float3 lightVector = expand(IN.LightVector); //считаем скалярное произведение между вектором источника света и нормалью float light = dot(bumpNormal,lightVector) //умножаем степень освещенности на цвет из основной текстуры return Color1*light; }
Здесь тоже не должно возникнуть проблем. Функция expand() переводит компоненты вектора из диапазона [0;1] в диапазон [-1;1]. В финальном расчете можно заменить умножение сложением, что тоже дает неплохой эффект (в принципе, кому как нравится).
Вот, пожалуй, и все. Приведенные шейдеры очень просто подстроить, как говорится, под себя. Они дают простор для творчества. Если что, обращайтесь, всегда помогу.
Полезные статьи по Cg:
Cg.
Реализация динамики флага с использованием Cg.
#bump, #bump mapping, #normal mapping, #текстурирование
20 апреля 2003 (Обновление: 15 июня 2009)