Главная -
Статьи -
Проекты -
Ссылки -
Скачать -
Из гельминтов -
Юмор, приколы -
Почитать -
Обо мне -
Мысли -
Гостевая -

Расширение ARB_texture_env_dot3 в OpenGL и простейший попиксельный bumpmapping

Довольно популярной темой в аппаратно-ускоренной графике является так называемое пописельное (per-pixel) освещение/отражение и т.п.

Рассмотрим теперь каким образом можно простейшим образом реализовать попиксельное освещение средствами OpenGL.

В качестве модели освещения мы возьмем простейшую модель только диффузного освещения - освещенность каждого видимого пиксела задается следующей формулой:

	I = max ( (l,n), 0 )
В этой формуле через l и n обозначены единичные вектора направления от точки к источнику света и нормали к поверхности в этой точке. Использование функции max гарантирует нас от появления "отритцательной" освещенности (что соответствует случаю, когда нормаль направлена от источника света и свет падать на эту точку просто не может).

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

Поэтому стандартная модель освещения OpenGL для целей попиксельного освещения не годится.

Для построения попиксельного освещения в первую очередь нам понадобятся значения векторов l и n для каждого пиксела.

Можно задать единичные значения вектора l в каждой вершине грани (например как цвет или текстурные кординаты).

Задание вектора на источник света в вершинах грани

Рис 1. Задание вектора l в вершинах

Если мы в каждой из вершин A, B, C и D зададим значение вектора l, то можно стандартными средствами OpenGL произвести билинейную интерполяцию этого вектора (вдоль всего многоугольника ABCD) и для каждого пиксела получить проинтерполированное значение.

Простейшим способом для этого является задание вектора l как текстурных координат.

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

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

Рис 2. Интерполяция единичных векторов может давать неединичные вектора.

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

Как писалось в статье о кубических картах, кубическая карта является удобным способом задания функции от направления трехмерного вектора (т.е. функции, которая зависит только от направления и не зависит от его длины).

Построим тогда следующую карту (обычно ее делают небольшого размера, например 32*32) - каждому вектору (s,t,r) сопоставим его нормированное значение - (s,t,r)/sqrt(s*s+t*t+r*r).

Нормирующая кубическая карта

Рис 3. Текстуры нормирующей кубической карты.

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

Поскольку на выходе операции текстурирования мы получаем цвет (3-х или 4-х компонентный), то для построения такой карты необходим способ кодирования почти единичных (поскольку длина вектора, получающиеся в результате интрерполячции единичных, не сильно отличается от единицы) векторов в RGB цвета.

Сопоставим вектору (x,y,z), каждоя компонента которого лежит на отрезке [-1,1], цвет (r,g,b), по следующему правилу:

     r = (x + 1)/2
     g = (y + 1)/2
     b = (z + 1)/2
Полученный при этом вектор (r,g,b) будет содержаться в единичном кубе и являеться допустимым значением цвета (в рельных приложениях каждая из этих компонент должна быть умножена на 255 для перевода в диапазон значения байта).

Ниже приводится процедура построения нормирующей кубической карты:

static	void	getCubeVector ( int side, int cubeSize, int x, int y, Vector3D& v )
{
    float	s  = ((float) x + 0.5f) / (float) cubeSize;
    float	t  = ((float) y + 0.5f) / (float) cubeSize;
    float	sc = 2*s - 1;
    float	tc = 2*t - 1;

    switch ( side )
    {
        case 0:
            v = Vector3D ( 1, -tc, -sc );
            break;

        case 1:
            v = Vector3D ( -1, -tc, sc );
            break;

        case 2:
            v = Vector3D ( sc, 1, tc );
            break;

        case 3:
            v = Vector3D ( sc, -1, -tc );
            break;

        case 4:
            v = Vector3D ( sc, -tc, 1 );
            break;

        case 5:
            v = Vector3D ( -sc, -tc, -1 );
            break;
    }

    v.normalize ();
}

unsigned	createNormalizationCubemap ( int cubeSize )
{
    Vector3D	v;
    byte     * pixels = (byte *) malloc ( 3 * cubeSize * cubeSize );

    if ( pixels == NULL )
        return 0;

    unsigned	textureId;

    glGenTextures   ( 1, &textureId );
    glEnable        ( GL_TEXTURE_CUBE_MAP_ARB );
    glBindTexture   ( GL_TEXTURE_CUBE_MAP_ARB, textureId );
    glPixelStorei   ( GL_UNPACK_ALIGNMENT, 1 );
    glTexParameteri ( GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
    glTexParameteri ( GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );

    for ( int side = 0; side < 6; side++ )
    {
        for ( int x = 0; x < cubeSize; x++ )
            for ( int y = 0; y < cubeSize; y++ )
            {
                int	offs = 3 * (y * cubeSize + x);

                getCubeVector ( side, cubeSize, x, y, v );

                pixels [offs    ] = 128 + 127 * v.x;
                pixels [offs + 1] = 128 + 127 * v.y;
                pixels [offs + 2] = 128 + 127 * v.z;
            }

         glTexImage2D ( GL_TEXTURE_CUBE_MAP_POSITIVE_X_ARB + side, 0, GL_RGB,
                        cubeSize, cubeSize, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels );
    }

    free ( pixels );

    glTexParameteri ( GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
    glTexParameteri ( GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_MIN_FILTER, GL_LINEAR );

    glDisable ( GL_TEXTURE_CUBE_MAP_ARB );

    return textureId;
}
Теперь, если в вершинах грани задать нормированные значения вектора направления на источник света l, то в каждом пикселе грани мы получим нормированное значение вектора l, соответствующее данному пикселу. Правда это значение будет представлено в виде цвета.

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

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

Обычно карта нормалей содержит значения, не сильно отличающиеся от (0,0,1), то обычные карты нормалей имеют голубовато-розоватый оттенок :)

Пример карты нормалей Пример карты нормалей

Рис 4. Примеры карт нормалей.

Теперь для вычисления скалярного произведения можно воспользоваться расширением ARB_texture_env_dot3, которое вычисляет следующую величину:

     4*((Arg0.r-0.5)*(Arg1.r-0.5)+(Arg0.g-0.5)*(Arg1.g-0.5)+(Arg0.b-0.5)*(Arg1.b-0.5))
Если считать, что оба аргумента являются векторами, записанными в виде цвета, то с учетом формул преобразования векторов в цвет, это значение соответствует скалярному произведению двух векторов - Arg0 и Arg1.

Это расширение добавляет два режима к расширению ARB_texture_env_combine - GL_DOT3_RGB_ARB и GL_DOT3_RGBA_ARB. В первом случае вычисленное значение записывается в первые три компоненты цвета (RGB), а во втором - во все четыре (RGBA).

Рассмотрим теперь как можно реализовать простейшее попиксельное освещение с использованием нормирующих кубических карт и расширения ARB_texture_env_dot3.

За счет использования мультитекстурирования можно реализовать вычисление выражения max((l,n),0) за один проход.

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

В первом текстурном блоке размещается нормирующая кубическая карта. Режимом наложения для нее является GL_DOT3_RGB_ARB. В качестве текстурных кординат выступает нормированный вектор направления на источник света.

В качестве Arg0 используется GL_TEXTURE, а в качестве Arg1 - GL_PREVIOUS_ARB (в данном случае это будут значения с предыдущего текстурного блока, т.н. значение вектора нормали).

                                           // bind bump (normal) map to texture unit 0
	glActiveTextureARB ( GL_TEXTURE0_ARB );
	glBindTexture      ( GL_TEXTURE_2D, bumpMap );
	glEnable           ( GL_TEXTURE_2D );

	glTexEnvi ( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE_ARB );
	glTexEnvi ( GL_TEXTURE_ENV, GL_SOURCE0_RGB_ARB,  GL_TEXTURE );
	glTexEnvi ( GL_TEXTURE_ENV, GL_COMBINE_RGB_ARB,  GL_REPLACE );

                                           // bind normalization cube map to texture unit 1
	glActiveTextureARB ( GL_TEXTURE1_ARB );
	glBindTexture      ( GL_TEXTURE_CUBE_MAP_ARB, normCubeMap );
	glEnable           ( GL_TEXTURE_CUBE_MAP_ARB );

	glTexEnvi ( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE_ARB );
	glTexEnvi ( GL_TEXTURE_ENV, GL_SOURCE0_RGB_ARB,  GL_TEXTURE );
	glTexEnvi ( GL_TEXTURE_ENV, GL_SOURCE1_RGB_ARB,  GL_PREVIOUS_ARB );
	glTexEnvi ( GL_TEXTURE_ENV, GL_COMBINE_RGB_ARB,  GL_DOT3_RGB_ARB );

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

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

Получающееся изображение с применением карты нормалей Получающееся изображение с применением карты нормалей

Рис 5. Результаты применения попиксельного диффузного освещения.

За счет добавление второго прохода можно реализовать более сложную модель освещения:

	I = С * max ( (l,n), 0 )
Здесь параметр С соответствует собственному цвету пиксела, т.е. является еще одной текстурой. Собственный цвет пиксела модулируется освещенностью данного пиксела.

Для этого достаточно повторно вывести грань с использованием текстуры С и режимом смешения (blending) (GL_DST_COLOR, GL_ZERO).

Соответствующий исходный код можно скачать здесь.

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

Рассмотрим теперь способ вычисления попиксельной освещенности для произвольно ориентированных граней.

Ранее мы считали, что грань расположена параллельно координатной плоскости Оху и карту нормалей строили соответственным образом (считая, что стандартным значением вектора нормали является (0,0,1)).

Пусть теперь у нас имеется произвольно ориентированная грань со стандартным вектором нормали n.

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

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

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

По этим двум векторам легко построить третий вектор, используя для этого векторное произведение : b=[t,n]. Этот вектор обычно называют бинормалью (см. рис 6).

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

Базис в касательном пространстве

Рис. 6. Базис в касательном пространстве.

По этим трем векторам (t,b,n) можно построить свою систему координат (называемую касательной или TBN).

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

         | tx ty tz |
     M = | bx by bz |
         | nx ny nz |
Обратите внимание, что матрица М будет ортогональной, поскольку базисные вектора ортонормированы, поэтому обратная к ней матрица равна транспонированной матрице М.

Пусть p - вектор в исходной системе координат, тогда его представление в новой системе кординат будет

M*p=((t,p),(b,p),(n,p)).

Обратите внимание, что вектор нормали к грани в касательной системе координат равен (0,0,1), т.е. совпадает со значением в рассмотренном ранее частном случае.

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

Обратите внимание, что l - это направление на источник света и поэтому какую точку мы выбрали в качестве начала координат для касательной системы координат, не важно.

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

В случае, когда у нас имеется сложный объект, состоящий из многих граней, для каждой из этих граней нужно хранить не только вектор нормали n, но и вектора t и b.

struct	Vertex
{
    Vector3D  pos;           // position of vertex
    Vector2D  tex;           // texture coordinates
    Vector3D  n;             // unit normal
    Vector3D  t, b;          // tangent and binormal

                             // map vector to tangent (TBN) space
    Vector3D   mapToTangentSpace ( const Vector3D& v ) const
    {
        return Vector3D ( v & t, v & b, v & n );
    }
};



Copyright © 2004 Алексей В. Боресков

Используются технологии uCoz