Урок 13. Normal Mapping


Вот мы и добрались уже до 13 урока – Normal Mapping.
В уроке №8, где говорилось про освещение, мы говорили про то, как можно сделать затенение поверхностей с помощью нормалей треугольника. Была лишь проблема в том, что каждая вершина могла иметь лишь одну нормаль: внутри самого треугольника нормали интерполируются из нормалей трех окружающих вершин, и мы ничего не могли с этим поделать, а вот цветом всегда можно было управлять с помощью текстур. Идея normal mapping в том, чтобы дать нам такой же контроль над нормалями.

Текстуры Нормалей

Текстура нормалей может выглядеть, например, вот так:

В каждом RGB текселе закодирован XYZ вектор: компоненты цветов закодированы в интервале от 0 до 1, а компоненты вектора от -1 до 1, поэтому нужно применить вот такую формулу, чтобы преобразовать цвет в нормаль:

normal = (2*color)-1 // такое нужно повторить на каждой компоненте вектора

В целом эта текстура голубого цвета, так как нормали направлены «от поверхности».
Эта текстура маппится точно так же, как и диффузная, но проблема в том, как же преобразовать нашу нормаль из индивидуального пространства треугольника(тангенциальное пространство, или image space) в пространство модели.

Касательная и Бикасательная

Так как вы уже знакомы с матрицами, то, я думаю, уже знаете, что для того, чтобы задать новое пространство(а в данной ситуации тангенциальное пространство), нам нужно 3 вектора. Вектор UP уже есть: это нормаль, которую можно вычислить с помощью векторного произведения на треугольнике. На текстуре нормалей она представлена синим цветом:

Теперь нам нужна касательная Т: вектор параллельный поверхности. Однако таких векторов может быть сколько угодно:

Какой же вектор нам выбрать? Теоретически, можно выбрать любой, однако выбрав любой вектор на одной поверхности нужно выбирать такой же и на всех остальных. Обычная практика для ориентации касательной – направлять её в ту же сторону, что и текстурные координаты:

Так как нам нужно 3 вектора, чтобы задать базис пространства, мы должны задать бикасательную- вторую касательную.

А вот и алгоритм: заметьте - deltaPos1 и deltaPos2, это длины двух сторон прямоугольника, а deltaUV1 и deltaUV2 – разница в координатах UV:

deltaPos1 = deltaUV1.x * T + deltaUV1.y * B
deltaPos2 = deltaUV2.x * T + deltaUV2.y * B

Нужно лишь решить эту систему для T и B, и у нас будут необходимые вектора!
Как только у нас появятся вектора T,B,N, то с помощью очень удобной матрицы мы сможем переходить от тангенциального пространства в пространство модели:

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

Чтобы сделать это обратное преобразование нам нужно инвертировать матрицу, что в нашем случае простой ортогональной матрицы, равно вычислительно гораздо дешевому транспонированию:
invTBN = transpose(TBN)








Готовим наш VBO

Вычисление касательных и бикасательных

Так как нам для наших расчетов нужны касательные и бикасательные, нам придется посчитать их для всего меша. Давайте вынесем этот расчет в отдельную функцию:
void computeTangentBasis(
   // входные параметры
   std::vector<glm::vec3> & vertices,
   std::vector<glm::vec2> & uvs,
   std::vector<glm::vec3> & normals,
   // выходные параметры
   std::vector<glm::vec3> & tangents,
   std::vector<glm::vec3> & bitangents
){

Для каждой грани нам необходимо подсчитать сторону(deltaPos) и deltaUV:
for ( int i=0; i<vertices.size(); i+=3){
   // копируем значения в переменные
   glm::vec3 & v0 = vertices[i+0];
   glm::vec3 & v1 = vertices[i+1];
   glm::vec3 & v2 = vertices[i+2];
   // копируем значения в переменные
   glm::vec2 & uv0 = uvs[i+0];
   glm::vec2 & uv1 = uvs[i+1];
   glm::vec2 & uv2 = uvs[i+2];
   // стороны треугольника
   glm::vec3 deltaPos1 = v1-v0;
   glm::vec3 deltaPos2 = v2-v0;
   // дельта UV
   glm::vec2 deltaUV1 = uv1-uv0;
   glm::vec2 deltaUV2 = uv2-uv0;

А теперь можно применить выведенную ранее формулу для вычисления касательной и бикасательной:

float r = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x);
glm::vec3 tangent = (deltaPos1 * deltaUV2.y   - deltaPos2 * deltaUV1.y)*r;
glm::vec3 bitangent = (deltaPos2 * deltaUV1.x   - deltaPos1 *
deltaUV2.x)*r;


И наконец, заполняем буферы касательных и бикасательных. Так как эти буферы еще не индексированы, то каждая вершина будет иметь свою собственную копию:

// Установим одну и ту же касательную для всех вершин треугольника.
// мы их объединим позже в  vboindexer.cpp
tangents.push_back(tangent);
tangents.push_back(tangent);
tangents.push_back(tangent);
// То же самое и для бинормалей
bitangents.push_back(bitangent);
bitangents.push_back(bitangent);
bitangents.push_back(bitangent);
}

Индексация

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

// Пробуем найти одинаковую вершину
unsigned int index;
bool found = getSimilarVertexIndex(in_vertices[i], in_uvs[i], in_normals[i],     out_vertices, out_uvs, out_normals, index);

if ( found ){ // Одинаковая вершина уже в VBO, используем её !
   out_indices.push_back( index );
   // Усредняем нормаль и бинормаль
   out_tangents[index] += in_tangents[i];
   out_bitangents[index] += in_bitangents[i];
}else{ // Если не нашли, то добавляем
   //  Все как и раньше
   [...]
}


Обратите внимание, что мы ничего не нормализируем. И это правильно, тем самым маленькие треугольники будут иметь маленькую нормаль, касательную и бикасательную, и тем самым будет давать меньший эффект чем большой треугольник.

Шейдер

Дополнительные буферы и константы
Нам нужно два дополнительных буфера: один для касательных, а второй для бикасательных:

GLuint tangentbuffer;
glGenBuffers(1, &tangentbuffer);
glBindBuffer(GL_ARRAY_BUFFER, tangentbuffer);
glBufferData(GL_ARRAY_BUFFER, indexed_tangents.size() * sizeof(glm::vec3),
&indexed_tangents[0], GL_STATIC_DRAW);
GLuint bitangentbuffer;
glGenBuffers(1, &bitangentbuffer);
glBindBuffer(GL_ARRAY_BUFFER, bitangentbuffer);
glBufferData(GL_ARRAY_BUFFER, indexed_bitangents.size() * sizeof
glm::vec3), &indexed_bitangents[0], GL_STATIC_DRAW);

Так же необходим еще один uniform для текстуры нормали:

[...]
GLuint NormalTexture = loadTGA_glfw("normal.tga");
[...]
GLuint NormalTextureID  = glGetUniformLocation(programID,
"NormalTextureSampler");


А еще один для матрицы ModelView 3*3. Теоретически без последнего можно было бы и обойтись, но с ней легче, но об этом позже. Нам необходима будет лишь верхняя левая часть этой матрицы, так как мы будем делать лишь умножение направлений:

GLuint ModelView3x3MatrixID = glGetUniformLocation(programID, "MV3x3");

Полный код будет выглядеть так:

// Очищаем экран
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Включаем шейдер
glUseProgram(programID);
// Вычисляем MVP матрицу
computeMatricesFromInputs();
glm::mat4 ProjectionMatrix = getProjectionMatrix();
glm::mat4 ViewMatrix = getViewMatrix();
glm::mat4 ModelMatrix = glm::mat4(1.0);
glm::mat4 ModelViewMatrix = ViewMatrix * ModelMatrix;
glm::mat3 ModelView3x3Matrix = glm::mat3(ModelViewMatrix); // Берем левую верхнюю часть матрицы ModelViewMatrix
glm::mat4 MVP = ProjectionMatrix * ViewMatrix * ModelMatrix;
// Отправляем трансформацию в текущий шейдер, в uniform ”MPV
glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &MVP[0][0]);
glUniformMatrix4fv(ModelMatrixID, 1, GL_FALSE, &ModelMatrix[0][0]);
glUniformMatrix4fv(ViewMatrixID, 1, GL_FALSE, &ViewMatrix[0][0]);
glUniformMatrix4fv(ViewMatrixID, 1, GL_FALSE, &ViewMatrix[0][0]);
glUniformMatrix3fv(ModelView3x3MatrixID, 1, GL_FALSE, &ModelView3x3Matrix[0][0]);
glm::vec3 lightPos = glm::vec3(0,0,4);
glUniform3f(LightID, lightPos.x, lightPos.y, lightPos.z);
// Биндим диффузную текстуру в слот 0
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, DiffuseTexture);
// Устанавливаем "DiffuseTextureSampler" сэмплер на слот 0
glUniform1i(DiffuseTextureID, 0);
// Биндим текстуру нормалей на слот 1
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, NormalTexture);
// Устанавливаем "Normal  TextureSampler" сэмплер на слот 1
glUniform1i(NormalTextureID, 1);
// первый буферный атрибут - вершины
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer);
glVertexAttribPointer(
   0,                  // атрибут
   3,                  // размер
   GL_FLOAT,           // тип
   GL_FALSE,           // нормализировано ли?
   0,                  // шаг
   (void*)0            // смещение в буфере
);

// второй атрибут : UV координаты
glEnableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, uvbuffer);
glVertexAttribPointer(
   1,                                // атрибут
   2,                                // размер
   GL_FLOAT,                         // тип
   GL_FALSE,                         // нормализировано ли?
   0,                                // шаг
   (void*)0                          // смещение
);

// третий атрибут - нормали
glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, normalbuffer);
glVertexAttribPointer(
   2,                                // атрибут
   3,                                // размер
   GL_FLOAT,                        // тип
   GL_FALSE,                        // нормализировано ли?
   0,                                // шаг
   (void*)0                         // смещение в буфере
);

// Четвертый атрибут - касательные
glEnableVertexAttribArray(3);
glBindBuffer(GL_ARRAY_BUFFER, tangentbuffer);
glVertexAttribPointer(
   3,                                // атрибут
   3,                                // размер
   GL_FLOAT,                         // тип
   GL_FALSE,                         // нормализировано ли?
   0,                                // шаг
   (void*)0                          // смещение
);

// Пятый атрибут - Бикасательные
glEnableVertexAttribArray(4);
glBindBuffer(GL_ARRAY_BUFFER, bitangentbuffer);
glVertexAttribPointer(
   4,                         // атрибут
   3,                         // размер
   GL_FLOAT,                 // тип
   GL_FALSE,                 // нормализировано ли?
   0,                        
    (void*)0                 // смещение
);
// Индексный буфер
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementbuffer);
// Рисуем треугольники !
glDrawElements(
   GL_TRIANGLES,      // режим
   indices.size(),    // количество
   GL_UNSIGNED_INT,   // тип
   (void*)0           // смещение
);
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glDisableVertexAttribArray(2);
glDisableVertexAttribArray(3);
glDisableVertexAttribArray(4);
// переключаем буферы(показываем на экран)
glfwSwapBuffers();

Вершинный шейдер

Как я уже говорил, мы все будем делать в пространстве камеры, так как тут гораздо проще доставать позиции фрагментов. Именно поэтому мы умножвем наши векторы T,B,N на матрицу ModelView.
vertexNormal_cameraspace = MV3x3 * normalize(vertexNormal_modelspace);
vertexTangent_cameraspace = MV3x3 * normalize(vertexTangent_modelspace);
vertexBitangent_cameraspace = MV3x3 * normalize
vertexBitangent_modelspace);

Эти три вектора определяют TBN матрицу которая конструируется таким образом:
mat3 TBN = transpose(mat3(
       vertexTangent_cameraspace,
       vertexBitangent_cameraspace,
       vertexNormal_cameraspace
   ));
Можно использовать векторное произведение вместо построения этой матрицы и транспонирования её.
Эта матрица поможет вам переходить от пространства камеры в тангенциальное пространство(Такая же матрица, но с XXX_modelspace может помочь в переходе от пространства модели в тангенциальное пространство). Мы можем использовать её для того, чтобы вычислять направление света и взгляда в тангенциальном пространстве:

LightDirection_tangentspace = TBN * LightDirection_cameraspace;
EyeDirection_tangentspace =  TBN * EyeDirection_cameraspace;

Фрагментный шейдер

Наши нормали в тангенциальном пространстве получить очень просто – берем их просто из текстуры:

// локальная нормаль в тангенциальном пространстве
vec3 TextureNormal_tangentspace = normalize(texture2D(NormalTextureSampler, UV ).rgb*2.0 - 1.0);

Теперь-то у нас есть все что нужно. Диффузное освещение использует clamp(dot(n,l),0,1) где n и l выражены в тангенциальном пространстве(совсем не важно в каком пространстве делать векторное произведение, главное, чтобы оба вектора были в одних и тех же пространствах). Свет отблесков использует clamp( dot( E,R ), 0,1 ), где, опять же, E, R выражены в тангенциальном пространстве.

Результат

А вот и результат:

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


Идем вглубь

Ортогонализация
Если вы помните, в вертексном шейдере мы использовали ортогонализацию вместо инвертирования, так как это быстрее. Но это работает лишь в том случае, если матрица, которая представляет собой пространство, ортогональная. Однако это не всегда так. К счастью это легко поправить: нужно просто сделать касательную перпендикулярной к нормали в конце функции computeTangentBasis():
t = glm::normalize(t - n * glm::dot(n, t));
Понять смысл этой формулы не так-то и легко, поэтому вот вам схемка:

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

Обычно нам не нужно заморачиваться этим, но иногда, когда мы используем симметричные модели, UV координаты направлены в другую сторону, и вектор T направлен не туда.
Чтобы проверить, инвертирован ли он, или нет, можно сделать следующий простенький трюк: TBN должен формировать правостороннюю систему координат, и векторное произведение n,t должно давать вектор той же направленности, что и b.
Математика говорит «Вектор А направлен в ту же сторону, что и вектор Б, в том случае, если их скалярное произведение больше 0». Поэтому можно проверить
if dot( cross(n,t) , b ) > 0.
А если условие не выполняется, то нужно просто инвертировать t:
if (glm::dot(glm::cross(n, t), b) < 0.0f){
    t = t * -1.0f;
}

Specular текстура

Просто ради интереса, давайте добавим еще немного отблесков с помощью specular текстуры. Она может выглядеть, например, вот так:
И её можно использовать вместо нашего вектора “vec3(0.3,0.3,0.3)” серого цвета который мы использовали для цвета отблесков в одном из прошлых уроков.

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

Отладка

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

На рисунке показано, как мы визуализируем наше тангенциальное пространство с помощью линий.

Для того, чтобы включить immediate режим, необходимо отключить профиль 3.3:
glfwOpenWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);

А потом пропустить наши матрицы через устаревший OpenGL пайплайн(вы, конечно, могли бы написать свои собственные шейдеры, но так проще):
glMatrixMode(GL_PROJECTION);
glLoadMatrixf((const GLfloat*)&ProjectionMatrix[0]);
glMatrixMode(GL_MODELVIEW);
glm::mat4 MV = ViewMatrix * ModelMatrix;
glLoadMatrixf((const GLfloat*)&MV[0]);

Отключаем шейдеры:
glUseProgram(0);

и рисуем наши линии(в данном случае нормали, нормализированные и умноженные на 0.1, и примененные к каждой вершине):
glColor3f(0,0,1);
glBegin(GL_LINES);
for (int i=0; i<indices.size(); i++){
   glm::vec3 p = indexed_vertices[indices[i]];
   glVertex3fv(&p.x);
   glm::vec3 o = glm::normalize(indexed_normals[indices[i]]);
   p+=o*0.1f;
   glVertex3fv(&p.x);
}
glEnd();

Не стоит использовать этот режим в обычной жизни, лишь для отладки!

Отладка с помощью цветов

При отладке, может быть полезным визуализировать значение вектора с помощью цвета. Самый простой способ сделать это- записывать этот вектор во фрейбуфер вместо самого цвета. Давайте, например, отобразим вектор LightDirection_tangentspace:

color.xyz = LightDirection_tangentspace;


Давайте разберем, что мы тут видим:
  • На правой стороне цилиндра, свет(который рисуется узкой белой полосой), это UP в тангенциальном пространстве. Другими словами, свет идет в направлении нормали треугольника.
  • В средней части цилиндра, свет направлен в сторону касательной(по Х+)

Несколько советов:
  • В зависимости от того, что вы хотите отобразить, сначала это нужно нормализировать.
  • Если вы не можете понять, что вы видите, попробуйте обнулить, например, зеленую и синюю компоненту, а потом красную, а оставить лишь зеленую, а потом лишь синюю.
  • Старайтесь не мудрить ничего с альфой. С прозрачностью все может лишь усложниться.
  • Если вам нужно визуализировать отрицательные значения, то можно попробовать применить тот же трюк, что мы делали с нашей текстурой нормалей: visualize (v+1.0)/2.0.

Как я уже говорил ранее, очень важно точно знать в каком пространстве находятся вектора. Нельзя делать скалярное произведение вектора в пространстве камеры и вектора в пространстве модели.
Очень помогает добавление префикса «…_modelspace» в конце названий переменных.

Упражнения

  • Нормализируйте вектор в indexVBO_TBN прежде чем добавлять его, и посмотрите что получится.
  • Визуализируйте другие вектора(например, EyeDirection_tangentspace) в цветовом режиме, и постарайтесь понять, что будут означать все эти разноцветные пятнышки.

Инструменты и Ссылки

Комментариев нет:

Отправить комментарий