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

Работа с картами нормалей (bumpmaps)

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

Напомним преобразование, сопоставляющее вектору цветовое RGB значение:

     r = (x + 1)/2
     g = (y + 1)/2
     b = (z + 1)/2

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

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

Фактически можно считать что микрорельеф задается функцией H(s,t), причем справедливо соотношение H<<1. В силу этого грань может выводиться как плоская, т.е. влиянием микрорельефа на растеризацию грани можно пренебречь.

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

n'=(dH/ds,dH/dt,1)

Здесь через n' обозначен ненормированный искаженный вектор нормали.

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

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

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

Пример карты высоты Пример карты высоты

Рис 1. Примеры карт высот

А на следующих рисунках приведены получившиеся из этих карт высот карты нормалей.

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

Рис 2. Соответствующие им карты нормалей.

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

Для этого в формуле n'=(dH/ds,dH/dt,1) надо заменить аналитические значения производных карты высот их разностными аналогами:

     n'(i,j) = ( (H(i+1,j) - H(i,j))*scale, (H(i,j+1) - H (i,j))*scale, 1)

В этой формуле величина scale позволяет управлять степенью "неровности" - чем она больше, тем больше значение в карте нормалей будут отличаться от (0,0,1) после нормализации.

Несмотря на то, что для задания карты нормалей достаточно трехкомпонентной (RGB) текстуры, по соображениям, изложенным далее, используется четырехкомпонентная RGBA текстура, при этом для текстур, непосредственно построенных из карт высот, в альфа канал карты нормалей заносится значение 255.

Ниже приводится пример кода, осуществляющий построение карты нормалей по карте высот. При этом карта высот задается однокомпонентной (один байт на пиксел) текстурой.

Texture * convertHeight2NormalMap ( byte * pixels, int width, int height, float scale )
{
    float	  oneOver255 = 1.0 / 255.0;
    Texture * normalMap  = new Texture ( width, height, 4 );

    if ( normalMap == NULL )
         return NULL;

    byte    * out = normalMap -> getData ();

    if ( out == NULL )
    {
        delete normalMap;

        return NULL;
    }

    int	offs = 0;                   // offset to normalMap

    for ( int i = 0; i < height; i++ )
        for ( int j = 0; j < width; j++ )
        {
                                    // convert height values to [0,1] range
            float	c  = pixels [i*width              + j]           * oneOver255;
            float	cx = pixels [i*width              + (j+1)%width] * oneOver255;
            float	cy = pixels [((i+1)%height)*width + j]           * oneOver255;

                                    // find derivatives
            float	dx = (c - cx) * scale;
            float	dy = (c - cy) * scale;

                                    // normalize
            float	len = (float) sqrt ( dx*dx + dy*dy + 1 );

                                    // get normal
            float	nx = dy   / len;
            float	ny = -dx  / len;
            float	nz = 1.0f / len;

                                     // now convert to color and store in map
            out [offs    ] = (byte)(128 + 127*nx);
            out [offs + 1] = (byte)(128 + 127*ny);
            out [offs + 2] = (byte)(128 + 127*nz);
            out [offs + 3] = 255;

            offs += 4;
        }

    return normalMap;
}

Иногда карту высот перед построением карты нормалей сглаживают, например с использованием фильтра 3х3 - для каждого пиксела в качестве его значения берется среднее из его самого и всех его 8 соседей.

Это позволяет получить более гладкие карты нормалей.

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

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

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

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

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

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

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

С другой стороны, при вычислении бликового (specular) освещения или EMBM (Environment Mapped Bump Mapping) удобнее использовать нормированные вектора нормалей. Поэтому удобно хранить в текстуре нормированные значения, а длину вектора до нормализации (отображенную в отрезок [0,255]) записать в альфа канал.

Именно из этих соображений карты нормалей часто делают четырехкомпонентными.

Ниже приводится процедура, строящая очередной уровень для пирамидального фильтрования.

Texture * downSampleNormaMap ( Texture * old, int w2, int h2 )
{
    if ( old == NULL )
        return NULL;

    Texture * map = new Texture ( w2, h2, 4 );

    if ( map == NULL || map -> getData () == NULL )
    {
        delete map;

        return NULL;
    }

    float	oneOver127 = 1.0f / 127.0f;
    float	oneOver255 = 1.0f / 255.0f;

    byte  * in  = old -> getData   ();
    byte  * out = map -> getData   ();
    int		w   = old -> getWidth  ();
    int		h   = old -> getHeight ();
    float	v [3];              // here we will store x, y, z components

    for ( int i = 0; i < h; i += 2 )
        for ( int j = 0; j < w; j += 2 )
        {
            int	iNext = (i+1) % h;
            int	jNext = (j+1) % w;

            float	mag00 = oneOver255 * in [3 + i*w + j];
            float	mag01 = oneOver255 * in [3 + i*w + jNext];
            float	mag10 = oneOver255 * in [3 + iNext*w + j];
            float	mag11 = oneOver255 * in [3 + iNext*w + jNext];

                                // sum up values for RGB components, converting to [-1,1]
            for ( int k = 0; k < 3; k++ )
            {
                v [k]  = mag00 * (oneOver127 * in [k + i*w     + j]     - 1.0);
                v [k] += mag01 * (oneOver127 * in [k + i*w     + jNext] - 1.0);
                v [k] += mag10 * (oneOver127 * in [k + iNext*w + j]     - 1.0);
                v [k] += mag11 * (oneOver127 * in [k + iNext*w + jNext] - 1.0);
            }

                                // compute length of (x,y,z)
            float	length = (float)sqrt ( v[0] * v[0] + v[1] * v [1] + v[2] * v[2] );

                                // check for degenerated case
            if ( length < EPS )
            {
                v [0] = 0;
                v [1] = 0;
                v [2] = 1;
            }
            else
            {
                v [0] /= length;
                v [1] /= length;
                v [2] /= length;
            }

            int	index = (i >> 1)*w2 + (j >> 1);

                                // pack normalized vector into color values
            out [index]     = (byte)(128 + 127*v[0]);
            out [index + 1] = (byte)(128 + 127*v[1]);
            out [index + 2] = (byte)(128 + 127*v[2]);

                                // store averaged length as alpha component
            length *= 0.25;     // since it was build on 2x2 summed values

            if ( length > 1.0f - EPS )
                out [index + 3] = 255;
            else
                out [index + 3] = (byte)(255 * length);
        }

    return map;
}

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

void	loadNormalMap ( int target, bool mipmap, Texture * map )
{
    int	      level  = 0;
    int	      width  = map -> getWidth  ();
    int	      height = map -> getHeight ();
    Texture * cur    = map;
    Texture * next   = NULL;

                                // load the original map
    glTexImage2D ( target, 0, map -> getNumComponents (), width, height,
                   0, map -> getFormat (), GL_UNSIGNED_BYTE, map -> getData () );

    if ( !mipmap )
         return;
                                // downsample texture
    while ( width > 1 || height > 1 )
    {
         level++;

                                // compute new size for this level
        int	newWidth  = width  >> 1;
        int	newHeight = height >> 1;

        if ( newWidth < 1 )
            newWidth = 1;

        if ( newHeight < 1 )
            newHeight = 1;

        next = downSampleNormaMap ( cur, newWidth, newHeight );

        if ( next == NULL )
            return;

        glTexImage2D ( target, level, next -> getNumComponents (), newWidth, newHeight,
                       0, next -> getFormat (), GL_UNSIGNED_BYTE, next -> getData () );

                                // prepare for next iteration
        width  = newWidth;
        height = newHeight;

        if ( cur != map )       // release old temporary textures
            delete cur;

        cur = next;
    }

    if ( cur != map )           // remove last temporary
        delete cur;
}

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

Данный код оформлен в виде библиотеки, работающей как под Windows, так и под Linux. Он позволяет загружать текстуры из tga и bmp файлов (поддерживая 32-битовые bmp-файлы), создавать кубические карты, в том числе и нормирующую кубическую текстуру.

Так же поддерживается загрузка и mip-mapping для карт нормалей и автоматическое построение карт нормалей по картам высот.

Очень подробное описание карт нормалей и работы с ними можно найти с статье Марка Килгарда "A Practical and Robuts Bump-mapping Technique for Today's GPU's" на сайте компании NVIDIA.


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

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