Texture Bombing

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

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

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

Этому посвящена целая глава книги GPU Gems, однако эта книга не всем у нас доступна и, к тому же, на компакт-диске к ней отсутствует исходный код для этой главы.

Простейший вариант texture bombing заключается в использовании текстурных координат (s,t) для разбиения поверхности объекта на ряд участков (называемых в оригинале cells). Эти участки образуют прямоугольную матрицу, при этом индекс для текущего участка (cell.x, cell.y)) легко вычисляется как целая часть промасштабированных текстурных координат:

vec2  scaledTex = gl_TexCoord [0].xy * scale;
vec2  cell      = floor ( scaledTex );
vec2  offs      = scaledTex - cell;

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

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

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

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

random values texture map

Рис 1. Пример текстуры со случайными значениями (randomMap).

Поскольку значения компонент вектора cell всегда целочисленные, то для получения "хороших" текстурных координат следует обращаться к текстуре randomMap используя произведение cell на некоторый заведомо дробный вектор.

vec2  rndOffs = cell * vec2 ( 0.037, 0.119 );
vec3  rnd     = texture2D ( randomMap, rndOffs ).rgb;

Далее мы будем использовать вектор rnd.xy как положение верхнего левого угла накладываемого изображения и использовать вектор offs для индексирования накладываемой текстуры.

placing a patch on texture

Рис 2. Наложение элемента на участок cell.

Таким образом мы приходим к следующему фрагментному шейдеру.

uniform	sampler2D randomMap;
uniform sampler2D decalMap;
uniform sampler2D glyphMap;

void main (void)
{
    const  float scale = 1.0;

                                            // get decal color
    vec4  decalColor = texture2D ( decalMap, gl_TexCoord [0].xy * 0.25 );

                                            // get cell no based on texture coordinates
    vec2 tex     = gl_TexCoord [0].xy * scale;
    vec2 cell    = floor ( tex );
    vec2 offs    = tex - cell;
    vec2 rndOffs = cell * vec2 ( 0.037, 0.119 );
    vec3 rnd     = texture2D ( randomMap, rndOffs ).rgb;
    vec4 bomb    = texture2D ( glyphMap, offs.xy - rnd.xy );

    decalColor += bomb;

    gl_FragColor = decalColor;
}

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

void main(void)
{
    gl_Position     = gl_ModelViewProjectionMatrix * gl_Vertex;
    gl_TexCoord [0] = gl_MultiTexCoord0;
}

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

simplest pact placement

Рис 3. Простейший случай наложения.

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

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

patch crosses cells boundary

Рис 4. Накладываемое изображение выходит за пределы текущего участка.

Есть два простых способа борьбы с этим.

Во-первых, можно промасштабировать вектор rnd.xy и текстурные координаты накладываемого изображения, чтобы гарантировать, что оно никогда не выйдет за пределы данного участка. Но подобный подход (хотя он и очень прост), не дает красиво выглядящего результирующего изображения.

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

Рассмотрим далее второй подход подробнее.

neighbouring cells wich can be affected by patch

Рис 5. Соседние участки, которые может задеть накладываемое изображение.

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

В результате мы приходим к следующей фрагментной программе.

uniform	sampler2D randomMap;
uniform sampler2D decalMap;
uniform sampler2D glyphMap;

void main (void)
{
    const float	scale = 1.0;

                                            // get decal color
    vec4 decalColor = texture2D ( decalMap, gl_TexCoord [0].xy * 0.25 );

                                            // get cell no based on texture coordinates
    vec2  tex  = gl_TexCoord [0].xy * scale;
    vec2  cell = floor ( tex );
    vec2  offs = tex - cell;
    int   i, j;

    for ( i = -1; i <= 0; i++ )
        for ( j = -1; j <= 0; j++ )
        {
            vec2 curCell = cell + vec2 ( i, j );
            vec2 curOffs = offs - vec2 ( i, j );
            vec2 rndOffs = curCell * vec2 ( 0.037, 0.119 );
            vec3 rnd     = texture2D ( randomMap, rndOffs ).rgb;
            vec2 t       = curOffs.xy - rnd.xy;

            decalColor += texture2D ( glyphMap, t );
        }

    gl_FragColor = decalColor;
}

На рисунке 6 Вы видите получаемое изображение.

image with accounting for neighbouring cells

Рис 6. Изображение с учетом влияния соседних участков.

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

Ниже приводится листинг функции main программы на С++, строящей изображение с рисунка 6.

int main ( int argc, char * argv [] )
{
                                // initialize glut
    glutInit            ( &argc, argv );
    glutInitDisplayMode ( GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH );
    glutInitWindowSize  ( 500, 500 );

                                // create window
    glutCreateWindow ( "Example of GLSL texture bombing shader - 2" );

                                // register handlers
    glutDisplayFunc  ( display );
    glutReshapeFunc  ( reshape );
    glutKeyboardFunc ( key     );
    glutMouseFunc    ( mouse   );
    glutMotionFunc   ( motion  );
    glutIdleFunc     ( animate );

    init           ();
    initExtensions ();
    printfInfo     ();

    if ( !GlslProgram :: isSupported () )
    {
        printf ( "GLSL not supported.\n" );

        return 1;
    }

    if ( !program.loadShaders ( "bomb-2.vsh", "bomb-2.fsh" ) )
    {
        printf ( "Error loading shaders:\n%s\n", program.getLog ().c_str () );

        return 2;
    }

                                // load textures
    decalMap  = createTexture2D ( true, "../Textures/16.jpg" );
    glyphMap  = createTexture2D ( false, "particle2.tga" );
    randomMap = createTexture2D ( false, "random.bmp" );

    glBindTexture   ( GL_TEXTURE_2D, glyphMap );
    glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP );
    glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP );

                                // install program object as part of
                                // current state and set textures
    program.bind ();
    program.setTexture ( "decalMap",  0 );
    program.setTexture ( "randomMap", 1 );
    program.setTexture ( "glyphMap",  2 );
    program.unbind ();

    glutMainLoop ();

    return 0;
}

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

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

float priority = -1.0;

. . .

if ( rnd.w > priority )    // higher priority case
{
    decacColor = glyphColor;
    priority  = rnd.w;
}

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

uniform	sampler2D randomMap;
uniform sampler2D decalMap;
uniform sampler2D glyphMap;

#define SAMPLES_PER_CELL  3

void main (void)
{
    const float	scale = 1.0;

                                // get diffuse color
    vec4 decalColor = texture2D ( decalMap, gl_TexCoord [0].xy * 0.25 );

                                // get cell no based on texture coordinates
    vec2  tex     = gl_TexCoord [0].xy * scale;
    vec2  cell    = floor ( tex );
    vec2  offs    = tex - cell;
    int   i, j, sample;

    for ( i = -1; i <= 0; i++ )
        for ( j = -1; j <= 0; j++ )
        {
            vec2 curCell = cell + vec2 ( i, j );
            vec2 curOffs = offs - vec2 ( i, j );
            vec2 rndOffs = curCell * vec2 ( 0.037, 0.119 );

            for ( sample = 0; sample < SAMPLES_PER_CELL; sample++ )
            {	
                vec3 rnd     = texture2D ( randomMap, rndOffs ).rgb;

                rndOffs    += vec2 ( 0.3123123, 0.431247 );
                decalColor += texture2D ( glyphMap, curOffs.xy - rnd.xy );
            }
        }
	
    gl_FragColor = decalColor;
}

several patches per cell

Рис 7. Использование нескольких изображений на участок.

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

Для этого достаточно сравнивать одну из компонент вектора rnd с заданной вероятностью (например, 0.5) и осуществлять вывод только в том случае, если эта компонента меньше или равно заданной вероятности.

uniform	sampler2D randomMap;
uniform sampler2D decalMap;
uniform sampler2D glyphMap;

#define SAMPLES_PER_CELL  3

void main (void)
{
    const float	scale = 1.0;

                                // get decal color
    vec4  decalColor = texture2D ( decalMap, gl_TexCoord [0].xy * 0.25 );

                                // get cell no based on texture coordinates
    vec2  tex     = gl_TexCoord [0].xy * scale;
    vec2  cell    = floor ( tex );
    vec2  offs    = tex - cell;
    int   i, j, sample;

    for ( i = -1; i <= 0; i++ )
        for ( j = -1; j <= 0; j++ )
        {
            vec2  curCell = cell + vec2 ( i, j );
            vec2  curOffs = offs - vec2 ( i, j );
            vec2  rndOffs = curCell * vec2 ( 0.037, 0.119 );

            for ( sample = 0; sample < SAMPLES_PER_CELL; sample++ )
            {	
                vec3 rnd  = texture2D ( randomMap, rndOffs ).rgb;

                rndOffs += vec2 ( 0.3123123, 0.431247 );

                if ( rnd.z > 0.5 )
                    decalColor += texture2D ( glyphMap, curOffs.xy - rnd.xy );
            }
        }

    gl_FragColor = decalColor;
}

varying number of patches per cell

Рис 8. Изменяемое количество изображений на участок.

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

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

float index = floor ( NUM_IMAGES * rnd.z );

curOffs.x = (curOffs.x + index) / (float) NUM_IMAGES;

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

На основе этого номера строится двухмерный вектор для обращения к randomMap. На следующих листингах приводятся вершинный и фрагментный шейдеры для случайного распределения в пространстве набора шаров (со случайно заданным цветом).

                // vertex shader
varying	vec4 vertex;

void main(void)
{
    vertex          = gl_Vertex;      // untransformed vertex
    gl_Position     = gl_ModelViewProjectionMatrix * gl_Vertex;
    gl_TexCoord [0] = gl_MultiTexCoord0 * 0.25;
}

                // fragment shader
                
varying	vec4 vertex;

uniform sampler2D randomMap;
uniform sampler2D decalMap;

void main (void)
{
    const	float scale = 0.8;
                                // get diffuse color
    vec4 decalColor = texture2D ( decalMap, gl_TexCoord [0].xy );

                                // get cell no based on 3D-coordinates
    vec3 tex      = vertex.xyz * scale;
    vec3 cell     = floor ( tex );
    vec3 offs     = tex - cell;
    vec3 rndOffs  = cell / scale;
    vec4 colorSum = vec4 ( 0.0 );

    for ( int i = 0; i < 7; i++ )
    {
        vec2  rndTex = rndOffs.xy + 0.1* rndOffs.zx + 0.3*rndOffs.yz;
        vec3  rnd    = texture2D ( randomMap, rndTex ).rgb;
        vec3  center = vec3 ( 0.2 ) + 0.6 * rnd;
        float radius = 0.2 * fract ( 0.6 * rndOffs.x );

        if ( length ( center - offs ) < radius )     // place a bomb
            decalColor = vec4 ( rnd, 1.0 );

        rndOffs += vec3 ( 0.345, 0.386, 0.773 );
    }

    gl_FragColor   = decalColor;
    gl_FragColor.a = 1.0;
}

На следующем рисунке приводится получающееся изображение.

3D texture bombing

Рис 9. Трехмерный texture bombing.

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

//
// Rain via texture bombing vertex shader
//

varying	vec4 vertex;

void main(void)
{
    vertex          = gl_Vertex * vec4 ( 0.98423, 1.1023, 1.09348, 0.89231);
    gl_Position     = gl_ModelViewProjectionMatrix * gl_Vertex;
    gl_TexCoord [0] = gl_MultiTexCoord0;
}

//
// Rain via texture bombing
//

varying	vec4 vertex;

uniform	float      time;
uniform	sampler2D  randomMap;
uniform sampler2D  decalMap;

void main (void)
{
     const float scale = 0.61234;

                                // get decal color
    vec4 decalColor = texture2D ( decalMap, gl_TexCoord [0].xy );

                                // get time cell offset
    vec3 cell0 = floor ( vertex.xyz * scale );
    vec3 offs0 = vertex.xyz * scale - cell0;
    vec3 rnd0  = texture2D ( randomMap, offs0.xy ).rgb;

                                // offset time with cell0
    float timeCell = floor ( time * 0.2 + rnd0.x );
    float timeOffs = time * 0.2 - timeCell;

                                // get cell no based on 3D-coordinates
    vec3 tex      = vertex.xyz * scale + vec3 ( timeCell * 0.5 );
    vec3 cell     = floor ( tex );
    vec3 offs     = tex - cell;
    vec3 rndOffs  = cell / scale;
    vec4 colorSum = vec4 ( 0.0 );

    for ( int i = 0; i < 2; i++ )
    {
        vec2  rndTex = rndOffs.xy + 0.1* rndOffs.zx + 0.3*rndOffs.yz;
        vec3  rnd    = texture2D ( randomMap, rndTex ).rgb;
        vec3  center = vec3 ( 0.2 ) + 0.6 * rnd;
        float radius = 0.4 * fract ( 1.0 * ( timeOffs + rndOffs.x ) );

        if ( abs ( length ( center - offs ) - radius ) < 0.015 )    // place a bomb
            colorSum += vec4 ( 1.0 ) * ( 1.0 - radius / 0.7 );

        rndOffs += vec3 ( 0.345, 0.386, 0.773 );
    }

    gl_FragColor   = decalColor + colorSum;
    gl_FragColor.a = 1.0;
}

rain texture bombing screenshot

rain texture bombing screenshot

Рис 10. Изображения, получаемые при помощи шейдера rain.

По этой ссылке можно скачать весь исходный код к этой статье. Также доступны для скачивания откомпилированные версии для M$ Windows, Linux и Mac OS X.

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