steps3D - Tutorials - Непериодическое повторение текстуры

Непериодическое повторение текстуры

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

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

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

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

-- vertex
#version 330 core

layout(location = 0) in vec3 pos;
layout(location = 1) in vec2 tex;
layout(location = 2) in vec3 normal;

uniform mat4 proj;
uniform mat4 mv;
uniform mat3 nm;
uniform vec3 lightDir;

out vec3 l;
out vec3 n;
out vec2 tx;

void main(void)
{
    vec4 pt = mv * vec4 ( pos, 1.0 );

    tx = tex;
    n  = normalize ( nm * normal );
    l  = normalize ( lightDir );

    gl_Position  = proj * pt;
}

-- fragment
#version 330 core

in  vec2 tx;
in  vec3 l;
in  vec3 n;
out vec4 color;     // output value

uniform sampler2D image;

const vec3  lightColor = vec3 ( 1.0 );

vec4 hash4 ( in vec2 p ) 
{ 
    return fract ( sin ( vec4 ( 1.0 + dot ( p, vec2(37.0,17.0)), 
                                2.0 + dot ( p, vec2(11.0,47.0)),
                                3.0 + dot ( p, vec2(41.0,29.0)),
                                4.0 + dot ( p, vec2(23.0,31.0))))*103.0); 
}

vec4 textureNoTile ( in vec2 uv )
{
     ivec2 iuv = ivec2 ( floor ( uv ) );
     vec2  fuv = fract ( uv );

    // generate per-tile transform
    vec4 ofa = hash4 ( iuv + ivec2(0,0) );
    vec4 ofb = hash4 ( iuv + ivec2(1,0) );
    vec4 ofc = hash4 ( iuv + ivec2(0,1) );
    vec4 ofd = hash4 ( iuv + ivec2(1,1) );
  
    vec2 ddx = dFdx ( uv );
    vec2 ddy = dFdy ( uv );

    // transform per-tile uvs
    ofa.zw = sign ( ofa.zw - 0.5 );
    ofb.zw = sign ( ofb.zw - 0.5 );
    ofc.zw = sign ( ofc.zw - 0.5 );
    ofd.zw = sign ( ofd.zw - 0.5 );
    
    // uv's, and derivatives (for correct mipmapping)
    vec2 uva  = uv  * ofa.zw + ofa.xy;
    vec2 ddxa = ddx * ofa.zw;
    vec2 ddya = ddy * ofa.zw;
    vec2 uvb  = uv  * ofb.zw + ofb.xy;
    vec2 ddxb = ddx * ofb.zw;
    vec2 ddyb = ddy * ofb.zw;
    vec2 uvc  = uv  * ofc.zw + ofc.xy;
    vec2 ddxc = ddx * ofc.zw;
    vec2 ddyc = ddy * ofc.zw;
    vec2 uvd  = uv  * ofd.zw + ofd.xy;
    vec2 ddxd = ddx * ofd.zw;
    vec2 ddyd = ddy * ofd.zw;
        
    // fetch and blend
    vec2 b = smoothstep ( 0.25,0.75, fuv );
    
    return mix( mix( textureGrad ( image, uva, ddxa, ddya ), 
                     textureGrad ( image, uvb, ddxb, ddyb ), b.x ), 
                mix( textureGrad ( image, uvc, ddxc, ddyc ),
                     textureGrad ( image, uvd, ddxd, ddyd ), b.x), b.y );
}

void main ()
{
    color = textureNoTile ( tx );
}

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

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

-- vertex
#version 330 core

layout(location = 0) in vec3 pos;
layout(location = 1) in vec2 tex;
layout(location = 2) in vec3 normal;

uniform mat4 proj;
uniform mat4 mv;
uniform mat3 nm;
uniform vec3 lightDir;

out vec3 l;
out vec3 n;
out vec2 tx;

void main(void)
{
    vec4 pt = mv * vec4 ( pos, 1.0 );

    tx = tex;
    n  = normalize ( nm * normal );
    l  = normalize ( lightDir );

    gl_Position  = proj * pt;
}

-- fragment
#version 330 core

in  vec2 tx;
in  vec3 l;
in  vec3 n;
out vec4 color;     // output value

uniform sampler2D image;

const vec3  lightColor = vec3 ( 1.0 );

vec4 hash4 ( in vec2 p ) 
{ 
    return fract ( sin ( vec4 ( 1.0 + dot ( p, vec2(37.0,17.0)), 
                                2.0 + dot ( p, vec2(11.0,47.0)),
                                3.0 + dot ( p, vec2(41.0,29.0)),
                                4.0 + dot ( p, vec2(23.0,31.0))))*103.0); 
}

vec4 textureNoTile ( in vec2 uv )
{
    vec2 p = floor ( uv );
    vec2 f = fract ( uv );
    
    // derivatives (for correct mipmapping)
    vec2 ddx = dFdx ( uv );
    vec2 ddy = dFdy ( uv );
    
    // voronoi contribution
    vec4  va = vec4 ( 0.0 );
    float wt = 0.0;

    for ( int j = -1; j <= 1; j++ )
        for ( int i = -1; i <= 1; i++ )
        {
            vec2  g = vec2  ( float(i), float(j) );
            vec4  o = hash4 ( p + g );
            vec2  r = g - f + o.xy;
            float d = dot ( r, r );
            float w = exp ( -5.0*d );
            vec4  c = textureGrad ( image, uv + o.zw, ddx, ddy );

            va += w*c;
            wt += w;
        }
    
    // normalization
    return va/wt;
}

void main ()
{
    color = textureNoTile ( tx );
}

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

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

-- vertex
#version 330 core

layout(location = 0) in vec3 pos;
layout(location = 1) in vec2 tex;
layout(location = 2) in vec3 normal;

uniform mat4 proj;
uniform mat4 mv;
uniform mat3 nm;
uniform vec3 lightDir;

out vec3 l;
out vec3 n;
out vec2 tx;

void main(void)
{
    vec4 pt = mv * vec4 ( pos, 1.0 );

    tx = tex;
    n  = normalize ( nm * normal );
    l  = normalize ( lightDir );

    gl_Position  = proj * pt;
}

-- fragment
#version 330 core

in  vec2 tx;
in  vec3 l;
in  vec3 n;
out vec4 color;     // output value

uniform sampler2D image;

const vec3  lightColor = vec3 ( 1.0 );

float sum ( vec3 v ) 
{ 
   return v.x + v.y + v.z; 
}

vec2 hash ( in float x )
{
    return sin ( x * vec2 ( 3.0, 7.0 ) );
}

vec4 textureNoTile ( in vec2 x, in float v )
{
    // sample variation pattern    
    float k = texture ( image, 0.005*x ).x; // cheap (cache friendly) lookup    
    
    // compute index    
    float index = k * 8.0;
    float f     = fract ( index );

    // offsets for the different virtual patterns    
    float ia    = floor ( index    );
    vec2  offsa = hash  ( ia       );
    vec2  offsb = hash  ( ia + 1.0 );

    // compute derivatives for mip-mapping    
    vec2 dx = dFdx ( x );
    vec2 dy = dFdy ( x );

    // sample the two closest virtual patterns    
    vec4 cola = textureGrad ( image, x + offsa, dx, dy );
    vec4 colb = textureGrad ( image, x + offsb, dx, dy );

    // interpolate between the two virtual patterns    
    return mix ( cola, colb, smoothstep ( 0.2, 0.8, f - 0.1*sum(cola.xyz-colb.xyz)));
}

void main ()
{
    color = textureNoTile ( tx, 1.0 );
}

Все исходные файлы для этой статьи (python и GLSL) добавлены в репозиторий примеров к книге.