ПрограммированиеСтатьиГрафика

Bump mapping.

Автор:

Сейчас я расскажу вам о таком эффекте, как 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 в новой системе. Для этого достаточно умножить вектор на матрицу преобразования. Она имеет такой вид:


[Tx Ty Tz]
[Bx By Bz]
[Nx Ny Nz]
Где вектор N(x,y,z) - нормаль к полигону, B(x,y,z) - бинормаль, T(x,y,z) - tangent.

Что такое нормаль (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)

Комментарии [13]