Reflective Bumpmapping в OpenGL
В стандартном OpenGL поддерживается так называемый environment mapping - когда при помощи текстуры моделируется
отражение объектом окружающей его среды. Для этого используется специальный режим вычисления текстурных
координат в вершинах и эти значения интерполируются вдоль граней при их текстурировании.
Рис 1. Пример environment mapping
Стандартный OpenGL поддерживает только один тип таких текстур - карт отражения (environment maps) -
сферические. Использование расширения
ARB_texture_cube_map вводит еще один карт отражения - кубические карты.
Кубические карты, представляющие собой просто вид на окружающую среду в шести направлениях, гораздо более удобны,
чем сферические. Однако и в том и в другом случае текстурные координаты соответствуют случаю отражения только в
вершинах, а во всех остальных точках значения текстурных координат получается путем интерполяции, т.е. являются
лишь приближенными к истинным.
Рассмотрим подробнее каким образом происходит вычисление отраженного вектора и каким образом это может быть
реализовано попиксельно.
Пусть в точке P на поверхности объекта (рис 2.) задан единичный вектор v направления на наблюдателя
и единичный вектор нормали n.
Рис 2. Вычисление отраженного вектора
Тогда отраженный вектор r (как легко убедиться, он также будет единичным) будет задаваться следующей формулой:
r = 2 n(e,n) - e
Если мы хотим для каждого пиксела построить точный отраженный вектор, используя карту нормалей, то это уравнение
необходимо решать для каждого пиксела объекта.
Можно заметно упростить вычисление отраженного вектора, если использовать следующее упрощение - карта нормалей
отклоняет вектор нормали от значения нормали для всей грани. Это отклонение вызывает, в свою очередь, отклонение
отраженного вектора от значения, получаемого в стандартном environment mapping:
r = r0 + dr
Таким образом можно перейти от использования карты норматей к использованию offset-текстуры, используемой
для изменения текстурных координат, соответствующих отраженному вектору.
Поэтому можно смоделировать отражение с учетом карты нормалей при помощи искажения текстурных координат при помощи
offset-текстуры. Этот подход получил (на самом деле не соответствующее действительности) название
Environment Map Bump Mapping или EMBM.
Для его реализации можно использовать расширение NV_texture_shader и шейдер GL_OFFSET_TEXTURE_2D_NV. Данный шейдер
использует двухкомпонентную текстуру (DSDT), где каждая компонента представляет собой вещественное число из
диапазона [-1,1], представленное одним байтом (ds или dt).
Пара чисел (ds,dt) взятых из этой текстуры умножается на заданную пользователем вещественную матрицу размером
2 на 2 для получения вектора смещения, добавляемого к текстурным координатам следующего текстурного блока.
Использование матрицы 2 на 2 позволяет добиться сразу двух целей:
-
задать масштабирование (поскольку ds и dt задаются с довольно невысокой точностью);
-
учесть поворот грани вокруг ее вектора нормали (за счет использование матрицы поворота на заданный угол)
Объединив масштабирование и поворот мы получим матрицу 2 на 2, которая будет служить для отображения значений
offset-текстуры в отклонения текстурных координат для следующего далее блока environment mapping.
Несмотря на свою простоту данный метод дает лишь приближение к точному отражению и хорошо работает лишь для
плоских объектов и очень небольших отклонений.
Кроме того, он применим лишь к сферическим картам отражения.
С другой стороны, использование расширения NV_texture_shader позволяет кроме рассмотренного выше
приближенного способа (EMBM), осуществить точное вычисление значений отраженного вектора для каждого фрагмента
и использование полученного значения для доступа к кубической карте отражения.
Для этого можно использовать шейдер GL_DOT_PRODUCT_REFLECT_CUBE_MAP_NV. При этом вычисления используются все четыре
шага текстурирования (texturing stages):
-
Шаг 0. Шейдер GL_TEXTURE_2D. В качестве текстуры используется карта нормалей
-
Шаг 1. Шейдер GL_DOT_PRODUCT_NV
-
Шаг 2. Шейдер GL_DOT_PRODUCT_NV
-
Шаг 3. Шейдер GL_DOT_PRODUCT_REFLECT_CUBE_MAP_NV. В качестве текстуры используется кубическая карта отражения
При этом вся информация, необходимая для вычисления отраженного вектора для каждого фрагмента передается в
текстурных координатах первых трех текстурных шагов (si,ti,ri,qi).
В координатах qi передаются координаты положения наблюдателя (eye).
eye = (q0,q1,q2)
Мы считаем, что пространство наблюдателя (eye-space) совпадает с тем пространством, в котором задана
кубическая карта отражения (cubic environment map), т.е. оси этой системы координат параллельны ребрам
куба, на грани которого накладывается кубическая текстура (сам наблюдатель при этом находится в центре этого куба).
Рис 3. Ориентация кубической системы координат.
Для получения правильного отражения необходимо перевести вектор нормали n из касательного
пространства (в котором n задано в карте нормалей) в пространство кубической карты (в нашем
случае оно совпадает с пространством наблюдателя).
Перевод вектора n в пространство наблюдателя осуществляется при помощи умножения его на матрицу
преобразования T.
n'=Tn
В качестве матрицы T выступает произведение двух матриц - верхней левой 3*3 подматрицей
матрицы modelview, ивертированной и транспонированной (ее мы будем далее обозначать как
M3x3-T) и матрицы составленной из базисных векторов касательного пространства
S=(t,b,n):
T=M3x3-T*S
Строки этой матрицы и выступают в качестве первых трех компонент текстурных координат для каждого
текстурного блока:
(s1,t1,r1)=(T0,0,T0,1,T0,2)
(s2,t2,r2)=(T1,0,T1,1,T1,2)
(s3,t3,r3)=(T2,0,T2,1,T2,2)
Шейдер GL_DOT_PRODUCT_REFLECT_CUBE_MAP_NV по преобразованной при помощи матрицы T
нормали n' (обратите внимание, что преобразованная нормаль может уже не быть
единичной) и положению наблюдателя eye для каждого фрагмента вычисляет отраженный вектор:
r=2n'(n',eye)/(n',n')-eye
Далее отраженный вектор r используется для доступа к кубической карте отражения.
Рассмотрем теперь практическую реализацию этого подхода - построение изображения тора, отражающего
окружающую среду, на поверхность которого наложена карта нормалей.
За основу класса, используемого для представления тора, можно взять класс Torus, использованный нами
при вычислении попиксельного бликового освещения, добавив в класс Vertex метод, служащий для
получения матрицы T:
struct Vertex
{
Vector3D pos; // position of vertex
Vector2D tex; // texture coordinates
Vector3D n; // unit normal
Vector3D t, b; // tangent and binormal
// compute transform matrix for
// reflective bump mapping
void buildTransformMatrix ( const Matrix3D& mInvT, Matrix3D& t ) const;
};
Входным параметром для метода buildTransformMatrix служит инвертированная и транспонированная
верхняя левая подматрица modelview матрицы (поскольку она одинакова для всех вершин).
Выходным параметром является матрица преобразования T.
void Vertex :: buildTransformMatrix ( const Matrix3D& mInvT, Matrix3D& tr ) const
{
Matrix3D s ( t, b, n );
tr = mInvT * s;
}
Процедура вывода тора выглядит теперь следующим образом:
void Torus :: draw ( const Vector3D& eye )
{
// get modelview matrix from OpenGL
float mGl [16];
glGetFloatv ( GL_MODELVIEW_MATRIX, mGl );
// now extract 3*3 submatrix from it,
// remember that OpenGL
// matrices are column-ordered
Matrix3D mv;
mv [0][0] = mGl [0];
mv [0][1] = mGl [4];
mv [0][2] = mGl [8];
mv [1][0] = mGl [1];
mv [1][1] = mGl [5];
mv [1][2] = mGl [9];
mv [2][0] = mGl [2];
mv [2][1] = mGl [6];
mv [2][2] = mGl [10];
// invert and transpose matrix
mv.invert ();
mv.transpose ();
Matrix3D t;
glBegin ( GL_TRIANGLES );
for ( int i = 0; i < numFaces; i++ )
for ( int j = 0; j < 3; j++ )
{
Vertex& v = vertices [faces [i].index [j]];
v.buildTransformMatrix ( mv, t );
Vector4D t1 ( t [0][0], t [0][1], t [0][2], eye.x );
Vector4D t2 ( t [1][0], t [1][1], t [1][2], eye.y );
Vector4D t3 ( t [2][0], t [2][1], t [2][2], eye.z );
glMultiTexCoord2fv ( GL_TEXTURE0_ARB, v.tex );
glMultiTexCoord3fv ( GL_TEXTURE1_ARB, t1 );
glMultiTexCoord2fv ( GL_TEXTURE2_ARB, t2 );
glMultiTexCoord3fv ( GL_TEXTURE3_ARB, t3 );
glVertex3fv ( v.pos );
}
glEnd ();
}
Для работы программы требуется правильная настройка OpenGL - необходимо настроить и register combiner'ы и
texture stages.
glEnable ( GL_TEXTURE_SHADER_NV );
glEnable ( GL_REGISTER_COMBINERS_NV );
// setup texture stages
// stage 0:
// bind bump (normal) map to texture unit 0
glActiveTextureARB ( GL_TEXTURE0_ARB );
glEnable ( GL_TEXTURE_2D );
glBindTexture ( GL_TEXTURE_2D, bumpMap );
glTexEnvi ( GL_TEXTURE_SHADER_NV, GL_SHADER_OPERATION_NV, GL_TEXTURE_2D );
// stage 1:
glActiveTextureARB ( GL_TEXTURE1_ARB );
glTexEnvi ( GL_TEXTURE_SHADER_NV, GL_SHADER_OPERATION_NV, GL_DOT_PRODUCT_NV );
glTexEnvi ( GL_TEXTURE_SHADER_NV, GL_PREVIOUS_TEXTURE_INPUT_NV, GL_TEXTURE0_ARB );
// stage 2:
glActiveTextureARB ( GL_TEXTURE2_ARB );
glTexEnvi ( GL_TEXTURE_SHADER_NV, GL_SHADER_OPERATION_NV, GL_DOT_PRODUCT_NV );
glTexEnvi ( GL_TEXTURE_SHADER_NV, GL_PREVIOUS_TEXTURE_INPUT_NV, GL_TEXTURE0_ARB );
// stage 3:
// bind environment cube map to texture unit 3
glActiveTextureARB ( GL_TEXTURE3_ARB );
glEnable ( GL_TEXTURE_CUBE_MAP_ARB );
glBindTexture ( GL_TEXTURE_CUBE_MAP_ARB, envMap );
glTexEnvi ( GL_TEXTURE_SHADER_NV, GL_SHADER_OPERATION_NV,
GL_DOT_PRODUCT_REFLECT_CUBE_MAP_NV );
glTexEnvi ( GL_TEXTURE_SHADER_NV, GL_PREVIOUS_TEXTURE_INPUT_NV, GL_TEXTURE0_ARB );
// setup register combiners
glFinalCombinerInputNV ( GL_VARIABLE_A_NV, GL_TEXTURE3_ARB, GL_UNSIGNED_IDENTITY_NV, GL_RGB );
// B = 1
glFinalCombinerInputNV ( GL_VARIABLE_B_NV, GL_ZERO, GL_UNSIGNED_INVERT_NV, GL_RGB );
// C = 0
glFinalCombinerInputNV ( GL_VARIABLE_C_NV, GL_ZERO, GL_UNSIGNED_IDENTITY_NV, GL_RGB );
// D = 0
glFinalCombinerInputNV ( GL_VARIABLE_D_NV, GL_ZERO, GL_UNSIGNED_IDENTITY_NV, GL_RGB );
torus.draw ( eye );
Ниже приводится изображение тора с наложенной на него картой нормалей, отражающий среду, заданную кубической картой.
Рис. 4. Изображение тора с картой нормалей, с environment mapping
Исходный код к этой статье можно скачать здесь.
При подготовке статьи использовались материалы компании NVIDIA.
Дополнительную информацию можно найти на на сайте компании NVIDIA.
|