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

Попиксельное диффузное освещение с использованием register combiner'ов

Использование механизма register combiner'ов позволяет достаточно легко реализовывать попиксельное диффузное освещение, не прибегая при к расширению texture_env_dot3.

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

Мы будем использовать следующую модель освещенности:

           I = Iamb + max (0, (l,n))*Il,

Здесь коэффициент Iamb задает фоновую освещенность, а Il - цвет источника света.

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

Для работы с расширениеми OpenGL, а также для работы с текстурами мы будем использовать библиотеки libExt и libTexture, доступные для скачивания. Также классы для работы с двух, трех и четырехмерными векторами и кватернионами доступны для скачивания в библиотеке 3D.

Данные библиотеки рассчитаны для работы как в Windows, так и в Linux.

В самом простейшем случае нам понадобится всего один general combiner.

В нулевом текстурном блоке (регистр texture0) мы разместим карту нормалей, а в первом текстурном блоке (регистр texture1) - нормирующую кубическую карту.

Фоновый цвет Iamb мы разместим в регистре constant-color1, а цвет источника света Il - в регистре constant-color1.

Теперь нам надо включить использование register combiner'ов и задать количество используемых general combiner'ов и постоянные цвета.

                                                // setup register combiners
    glEnable                ( GL_REGISTER_COMBINERS_NV );
    glCombinerParameteriNV  ( GL_NUM_GENERAL_COMBINERS_NV, 1 );
    glCombinerParameterfvNV ( GL_CONSTANT_COLOR0_NV, lightColor );
    glCombinerParameterfvNV ( GL_CONSTANT_COLOR1_NV, ambientColor );

Рассмотрим теперь конфигурацию нулевого general combiner'а.

В качестве RGB части переменной А мы возьмем значение из карты нормалей, т.е. texture0. При этом в качестве преобразования (mapping) следует использовать GL_EXPAND_NORMAL_NV, чтобы перевести представление вектора в виде цвета в нормальное знаковое представление (x,y,z).

                                                // configure A = expand (tex0) (bumpmap)
    glCombinerInputNV ( GL_COMBINER0_NV, GL_RGB, GL_VARIABLE_A_NV, GL_TEXTURE0_ARB,
                        GL_EXPAND_NORMAL_NV, GL_RGB );

Аналогичным образом в качестве RGB части переменной В возьмем значение с первого текстурного блока (регистра texture1), которое будет представлять собой нормированное значение вектора l в виде цвета. Здесь опять следует применить преобразование GL_EXPAND_NORMAL_NV для перевода вектора в координатное представление.

                                                // configure B = expand (tex1) (norm. map)
    glCombinerInputNV ( GL_COMBINER0_NV, GL_RGB, GL_VARIABLE_B_NV, GL_TEXTURE1_ARB,
                        GL_EXPAND_NORMAL_NV, GL_RGB );

Рассмотрим теперь каким следует настроить выход нулевого register combiner'а, для получения на выходе скалярного произведения (l,n).

Для этого RGB выход АВ направим в регистр spare0, остальные два выходных значения отбросим. Ни масштабирования, ни сдвига полученного значения нам не потребуется, но необходимо задать вычисление скалярного (а не покомпонентного) произведения для выхода АВ.

                                                // setup output of (l,n)
    glCombinerOutputNV ( GL_COMBINER0_NV, GL_RGB,
                         GL_SPARE0_NV,          // AB output
                         GL_DISCARD_NV,         // CD output
                         GL_DISCARD_NV,         // sum output
                         GL_NONE,               // no scale
                         GL_NONE,               // no bias
                         GL_TRUE,               // AB = A dot B
                         GL_FALSE, GL_FALSE );

В результате в регистре spare0.rgb у нас окажется нужное нам скалярное произведение во всех его цветовых компонентах.

Рассмотрим теперь конфигурацию final combiner'а.

В качестве значения переменной А следует взять spare0.rgb, подвергнув его преобразованию GL_UNSIGNED_IDENTITY_NV, в результате чего в переменной А мы получим max(0,(l,n)).

                                                // A.rgb = max ( spare0.rgb, 0 )
    glFinalCombinerInputNV ( GL_VARIABLE_A_NV, GL_SPARE0_NV, GL_UNSIGNED_IDENTITY_NV, GL_RGB );

В качестве переменной В мы возьмем RGB значение из регистра constant-color0 содержащего значение цвета источника света. В качестве преобразования следует использовать GL_UNSIGNED_IDENTITY_NV.

                                                // B.rgb = constant_color0.rgb
    glFinalCombinerInputNV ( GL_VARIABLE_B_NV, GL_CONSTANT_COLOR0_NV,
                             GL_UNSIGNED_IDENTITY_NV, GL_RGB );

В переменную С мы запишем нулевой вектор.

                                                // C = 0
    glFinalCombinerInputNV ( GL_VARIABLE_C_NV, GL_ZERO,
                             GL_UNSIGNED_IDENTITY_NV, GL_RGB );

В переменную D запишем значение фоновой освещенности, т.е. значение цвета из регистра constant-color1.

                                                // D = constant_color1.rgb
    glFinalCombinerInputNV ( GL_VARIABLE_D_NV, GL_CONSTANT_COLOR1_NV,
                             GL_UNSIGNED_IDENTITY_NV, GL_RGB );

В RGB части переменных E и F также запишем 0, а в альфа-часть переменной G запишем единицу.

                                                // E = 0
    glFinalCombinerInputNV ( GL_VARIABLE_E_NV, GL_ZERO, GL_UNSIGNED_IDENTITY_NV, GL_RGB );

                                                // F = 0
    glFinalCombinerInputNV ( GL_VARIABLE_F_NV, GL_ZERO, GL_UNSIGNED_IDENTITY_NV, GL_RGB );

                                                // G.alpha = 1
    glFinalCombinerInputNV ( GL_VARIABLE_G_NV, GL_ZERO, GL_UNSIGNED_INVERT_NV, GL_ALPHA );

При такой конфигурации мы на выходе получим

RGB = Iamb + max (0,(l,n))*Il

Alpha = 1

Ниже приводится скриншот программы.

Диффузное освещение грани с наложенной картой нормалей

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

Рассмотрим теперь одно явление, связанное с микрорельефом поверхности - самозатенение поверхности. Обратите внимание на ситуации, представленные на следующем рисунке.

Случаи самозатенения при расчете попиксельной освещенности

Рис 2. Случаи самозатенения

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

Второй случай (2б) гораздо интереснее - хотя исходный вектор нормали и направленот источника света, но измененный вектор нормали n' теперь уже направлен на источник света, т.е. при описанном ранее алгоритме эта точка будет освещена, чего не должно быть.

Данное явление и есть самозатенение, для его компенсации в уравнение освещенности вводится специальный коэффициент Sself.

           I = Iamb + max (0, (l,n))*Il*Sself,

В качестве коэффициента самозатенения в простешем случае можно взять единицу, если (l,n)>0 и нуль в противном случае.

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

Поэтому на практике обычно используется следующий вариант коэффициента самозатенения:

    Sself = 1,      если (l,n)>c
          =(l,n)/c, если 0<(l,n)<=c
          = 0,      если (l,n)<0

В качестве коэффициента с обычно используется 1/8.

Рассмотрим теперь, каким образом можно реализовать вычисление коэффициента самозатенения с использованием данной формулы.

Поскольку все вектора считаются заданными в касательном пространстве, то вектор n равен (0,0,1) и

    (l,n)=lz

Теперь, если эту величину умножить на 8 (сперва сложить с собой, а затем умножить на 4), то после отсечение по отрезку [0,1], мы и получим требуемый коэффициент самозатененияю

Вычисление коэффициента самозатенения удобно проводить в альфа-частях переменных и регистров. Рассмотрим как это реализуется.

                                                // configure A.alpha = 1
    glCombinerInputNV ( GL_COMBINER0_NV, GL_ALPHA, GL_VARIABLE_A_NV, GL_ZERO,
                        GL_UNSIGNED_INVERT_NV, GL_ALPHA );

                                                // configure B.alpha = lz
    glCombinerInputNV ( GL_COMBINER0_NV, GL_ALPHA, GL_VARIABLE_B_NV, GL_TEXTURE1_ARB,
                        GL_EXPAND_NORMAL_NV, GL_BLUE );

                                                // configure C.alpha = 1
    glCombinerInputNV ( GL_COMBINER0_NV, GL_ALPHA, GL_VARIABLE_C_NV, GL_ZERO,
                        GL_UNSIGNED_INVERT_NV, GL_ALPHA );

                                                // configure D.alpha = lz
    glCombinerInputNV ( GL_COMBINER0_NV, GL_ALPHA, GL_VARIABLE_D_NV, GL_TEXTURE1_ARB,
                        GL_EXPAND_NORMAL_NV, GL_BLUE );

    glCombinerOutputNV ( GL_COMBINER0_NV, GL_ALPHA,
                         GL_DISCARD_NV,         // AB
                         GL_DISCARD_NV,         // CD
                         GL_SPARE0_NV,          // AB+CD
                         GL_SCALE_BY_FOUR_NV,	// scale by 4
                         GL_NONE,               // bias
                         GL_FALSE,
                         GL_FALSE, GL_FALSE );

После этого в spare0.alpha у нас окажется величина 8*lz.

Рассморим теперь каким оразом следует сконфигурировать final combiner для учета коэффициента самозатенения. Предлагается следуещее задание переменных.

    A.rgb   = max ( 0, spare0.rgb )             // unsigned_identity
    B.rgb   = EF
    C.rgb   = 0
    D.rgb   = constant-color1.rgb
    E.rgb   = constant-color0.rgb
    F.rgb   = max ( 0,spare0.alpha )            // unsigned_identity
    G.alpha = 1

Как несложено убедиться, полученное на выходе значение и будет диффузной освещенностью с учетом самозатенения. Код для данной конфигурации final combiner'а приводится ниже.

                                                // A.rgb = max ( spare0.rgb, 0 )
    glFinalCombinerInputNV ( GL_VARIABLE_A_NV, GL_SPARE0_NV,
                             GL_UNSIGNED_IDENTITY_NV, GL_RGB );

                                                // B.rgb = EF
    glFinalCombinerInputNV ( GL_VARIABLE_B_NV, GL_E_TIMES_F_NV,
                             GL_UNSIGNED_IDENTITY_NV, GL_RGB );

                                                // C = 0
    glFinalCombinerInputNV ( GL_VARIABLE_C_NV, GL_ZERO,
                             GL_UNSIGNED_IDENTITY_NV, GL_RGB );

                                                // D = constant_color1.rgb
    glFinalCombinerInputNV ( GL_VARIABLE_D_NV, GL_CONSTANT_COLOR1_NV,
                             GL_UNSIGNED_IDENTITY_NV, GL_RGB );

                                                // E = constant_color_0
    glFinalCombinerInputNV ( GL_VARIABLE_E_NV, GL_CONSTANT_COLOR0_NV,
                             GL_UNSIGNED_IDENTITY_NV, GL_RGB );

                                                // F = selfShadow = 8*lz
    glFinalCombinerInputNV ( GL_VARIABLE_F_NV, GL_SPARE0_NV,
                             GL_UNSIGNED_IDENTITY_NV, GL_ALPHA );

                                                // G.alpa = 1
    glFinalCombinerInputNV ( GL_VARIABLE_G_NV, GL_ZERO,
                             GL_UNSIGNED_INVERT_NV, GL_ALPHA );

Рассмотрим теперь как можно построить изображение тора с наложенной на него картой нормалей.

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

Для каждой вершины торы, помимо положения в пространстве, текстурных координат и единичного вектора внешней нормали, следует также задать касательный вектор t и бинормаль b. Это позволяет в каждой вершине построить свое касательное пространство с системой координат.

struct	Vertex
{
    Vector3D  pos;                              // position of vertex
    Vector2D  tex;                              // texture coordinates
    Vector3D  n;                                // unit normal
    Vector3D  t, b;                             // tangent and binormal
    Vector3D  l;                                // light vector in the tangent space

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

struct  Face                                    // triangular face
{
    int index [3];                              // indices to Vertex array
};

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

Также добавлен метод mapToTangentSpace, служащий для перевода произвольного вектора в систему координат касательного пространства.

Приведем описание класса Torus.

class	Torus
{
    int      numRings;
    int      numSides;
    int      numVertices;
    int      numFaces;
    Vertex * vertices;
    Face   * faces;

public:
    Torus  ( float r1, float r2, int rings, int sides );
    ~Torus ()
    {
        delete [] vertices;
        delete [] faces;
    }

    void  calcLightVectors ( const Vector3D& light );
    void  draw             ();
};

В конструктора класса осуществляется заполнение массивов вершин и граней, используя при этом параметрическое уравнение тора.

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

void    Torus :: calcLightVectors ( const Vector3D& light )
{
                                                // compute texture coordinates for
                                                // normalization cibe map
    for ( int i = 0; i < numVertices; i++ )
        vertices [i].l = vertices [i].mapToTangentSpace ( light - vertices [i].pos );
}

Метод draw служит для отрисовки тора.

Диффузно-освещенный тор с наложенной на него картой нормалей (bumpmap)

Рис 3. Тор с диффузным освещением.

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

Способ, описанный Марком Килгардом, заключается в том, для исходной текстуры нормалей в альфа канал записывается длина вектора, переведенная в отрезок [0,255], т.е. 255 (поскольку в исходной текстуре все векторы имеют единичную длину).

Далее, при построении очередных уровней, поскольку каждое значение является средним из четырех, то получившиеся значение векторов нормали скорее всего уже не будут единичиными. Поэтому полученные значения нормируются и записываются в RGB часть текстуры. А в альфа часть записывается длина вектора до нормализации отображенная в отрезок [0,255].

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

Традиционно, в диффузном освещении предпочитают использовать ненормированную карты нормалей для промежуточных уровней. Это объясняется достаточно просто - рассмотрим сферу, с наложенной на нее картой нормалей. Если характерный уровень детализации карты нормалей заметно меньше размера самой сферы, то, начиная с определенного уровня, все промежуточные карты будут состоять практически из одного вектора (0,0,1).

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

А это не совсем правдоподобно, поскольку отклонения вектора нормали от значения (0,0,1) приводит в общем случае к меньшей освещенности по сравнению с идеально гладкой сферой.

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

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

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

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

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

На первом проходе объект выводится с decal текстурой в режиме GL_REPLACE. После этого устанавливается режим смешения (blending) равный (GL_DST_COLOR, GL_ZERO) и выводится освещаемый тор с использованием карты нормалей.

Диффузно-освещенный тор с основной (decal) текстурой и картой нормалей (bumpmap)

Рис 4. Тор с decal-текстурой и картой нормелей.

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

Весь исходный код примеров к этой статье можно скачать здесь. Для сборки всех примеров следует использовать либо файл Makefile.nmake в Windows, либо Makefile в Linux'е.


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

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