steps3D - Tutorials - Трипланарное и бипланарное проективное текстурирование

Проективное текстурирование

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

Для этих случаев есть довольно простой и красивый способ наложения текстур под названием triplanar texture mapping. Очевидно, что самым простым способом получения текстурных координат по 3D геометрии будет проектирование на плоскость - planar mapping. Таким образом мы можем спроектировать всю геометрию на плоскость Oxy, т.е. просто отбросить z-координату. и использовать оставшиеся координаты xy для чтения из текстуры.

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

Рис 1. Проблема с проектированием на плоскость

В 90-х годах был предложен довольно простой способ обойти этот недостаток 0 мы используем сразу три координатных плоскости - Oxy, Oxz и oyz. Для каждой из них мы получаем свой набор текстурных координат для чтения из текстуры. Далее мы просто смешиваем три получившихся цвета используя компоненты вектора нормали. Тем самым случай, когда направление проектирование перпендикулярно плоскости, будет просто отброшен (для него вес будет равен нулю) и мы получим корректное проектирование.

#version 330 core

in  vec3 p;
in  vec3 l;
in  vec3 n;
out vec4 color;     // output value

uniform sampler2D image;

    // "p" point being textured
    // "n" surface normal at "p"
    // "k" controls the sharpness of the blending in the transitions areas
vec4 boxmap ( in vec3 p, in vec3 n, in float k )
{
    // project+fetch
    vec4 x = texture ( image, p.yz );
    vec4 y = texture ( image, p.zx );
    vec4 z = texture ( image, p.xy );
    
    // blend factors
    vec3 w = pow( abs(n), vec3(k) );

    // blend and return
    return (x*w.x + y*w.y + z*w.z) / (w.x + w.y + w.z);
}

void main ()
{
    color = boxmap ( p, n, 1 );
}

Рис 2. Трипланарное проектирование

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

#version 330 core

in  vec3 p;
in  vec3 l;
in  vec3 n;
out vec4 color;     // output value

uniform sampler2D image;

    // "p" point being textured
    // "n" surface normal at "p"
    // "k" controls the sharpness of the blending in the transitions areas
vec4 biplanar( in vec3 p, in vec3 n, in float k )
{
    // grab coord derivatives for texturing
    vec3 dpdx = dFdx(p);
    vec3 dpdy = dFdy(p);

    n = abs(n);

    // determine major axis (in x; yz are following axis)
    ivec3 ma = (n.x>n.y && n.x>n.z) ? ivec3(0,1,2) :
               (n.y>n.z)            ? ivec3(1,2,0) :
                                      ivec3(2,0,1) ;

    // determine minor axis (in x; yz are following axis)
    ivec3 mi = (n.x<n.y && n.x<n.z) ? ivec3(0,1,2) :
               (n.y<n.z)            ? ivec3(1,2,0) :
                                      ivec3(2,0,1) ;

    // determine median axis (in x;  yz are following axis)
    ivec3 me = ivec3(3) - mi - ma;
    
    // project+fetch
    vec4 x = textureGrad( image, vec2(   p[ma.y],   p[ma.z]), 
                               vec2(dpdx[ma.y],dpdx[ma.z]), 
                               vec2(dpdy[ma.y],dpdy[ma.z]) );

    vec4 y = textureGrad( image, vec2(   p[me.y],   p[me.z]), 
                               vec2(dpdx[me.y],dpdx[me.z]),
                               vec2(dpdy[me.y],dpdy[me.z]) );
    
    // blend factors
    vec2 w = vec2(n[ma.x],n[me.x]);

    // make local support
    w = clamp( (w-0.5773)/(1.0-0.5773), 0.0, 1.0 );

    // shape transition
    w = pow( w, vec2(k/8.0) );

    // blend and return
    return (x*w.x + y*w.y) / (w.x + w.y);
}

void main ()
{
    color = biplanar ( p, n, 1 );
}

Рис 3. Бипланарное проектирование

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

Обратите внимание, что если мы хотим использовать этот прием для работы с картами нормалей, то все становится не так просто. Основной проблемой с использованием карт нормалей (bump maps) подобным образом является то, что карты нормалей задаются в касательном пространстве. Обычно базис касательного пространства TBN задается в каждой вершине и строится по текстурным координатам, которых у нас нет. Фактически у нас есть только трехмерные координаты точки p и вектор нормали n (в мировой системе координат).

Давайте для начала рассмотрим простой planar mapping, т.е. проектирование на одну плоскость, например на Oxy. Тогда направляющие вектора осей Ox> и Oy можно использовать в качестве касательного вектора и бинормали, а направляющий вектор оси Oz выступит в качестве вектора нормали. Тогда мы можем перевести вектора из карты нормалей в мировое пространство при помощи этих трех векторов.

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

Однако в отличии от цветов тут есть одна тонкость. Обратите внимание на нормали на сфере (рис 4). Точки A и B расположены симметрично и соответствуют проектированию вдоль одной и той же координатной оси, однако они направлены в противоположные стороны, т.е. различаются знаком.

Рис 4. Нормали для симметрично расположенных двух точек на сфере.

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

Рис 5. Трипланарное проектирование для карты нормалей.

#version 330 core

in  vec3 p;
in  vec3 l;
in  vec3 n;
in  vec3 nw;
out vec4 color;     // output value

uniform sampler2D image;

vec3 unpackNormal ( in vec3 n )
{
    return 2 * n - vec3 ( 1.0 );
}

    // "p" point being textured
    // "k" controls the sharpness of the blending in the transitions areas
vec3 boxmap ( in vec3 p, in float k )
{
        // blending coefficients
    vec3 blend = pow ( abs ( nw ), vec3 ( k ) );
    
    blend /= blend.x + blend.y + blend.z;   // note: they all non-negative due to abs
    
    vec3 axisSign = sign ( nw );            // sign (-1 or 1) of the surface normal

        // swizzle
    vec2 uvX = p.zy;        // x facing plane
    vec2 uvY = p.xz;        // y facing plane
    vec2 uvZ = p.xy;        // z facing plane// Tangent space normal maps

    	// tangent space normal maps
    vec3 tnormalX = unpackNormal ( texture ( image, uvX ).rgb );
    vec3 tnormalY = unpackNormal ( texture ( image, uvY ).rgb );
    vec3 tnormalZ = unpackNormal ( texture ( image, uvZ ).rgb );

    	// flip tangent normal z to account for surface normal facing
    tnormalX.z *= axisSign.x;
    tnormalY.z *= axisSign.y;
    tnormalZ.z *= axisSign.z;

    	// swizzle tangent normals to match world orientation and triblend
    return normalize ( tnormalX.zyx * blend.x + tnormalY.xzy * blend.y + tnormalZ.xyz * blend.z );
}

void main ()
{
    color = vec4 ( 0.5 * ( 1 + dot ( l, boxmap ( p * 0.25, 1 ) ) ) );
}

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

Ниже приводятся шейдера и изображения, полученные при использовании смешивания нормалей по способом UDN и Whiteout.

Рис 6. Сложение нормалей по методу UDN

#version 330 core

in  vec3 p;
in  vec3 l;
in  vec3 n;
in  vec3 nw;
out vec4 color;     // output value

uniform sampler2D image;

vec3 unpackNormal ( in vec3 n )
{
    return 2 * n - vec3 ( 1.0 );
}

    // "p" point being textured
    // "k" controls the sharpness of the blending in the transitions areas
vec3 boxmap ( in vec3 p, in float k )
{
        // blending coefficients
    vec3 blend = pow ( abs ( nw ), vec3 ( k ) );
    
    blend /= blend.x + blend.y + blend.z;   // note: they all non-negative due to abs
    
    vec2 uvX = p.zy;        // x facing plane
    vec2 uvY = p.xz;        // y facing plane
    vec2 uvZ = p.xy;        // z facing plane// Tangent space normal maps

    	// tangent space normal maps
    vec3 tnormalX = unpackNormal ( texture ( image, uvX ).rgb );
    vec3 tnormalY = unpackNormal ( texture ( image, uvY ).rgb );
    vec3 tnormalZ = unpackNormal ( texture ( image, uvZ ).rgb );

    	// swizzle world normals into tangent space and apply UDN blend
    tnormalX = vec3 ( tnormalX.xy + nw.zy, nw.x );
    tnormalY = vec3 ( tnormalY.xy + nw.xz, nw.y );
    tnormalZ = vec3 ( tnormalZ.xy + nw.xy, nw.z );

    	// swizzle tangent normals to match world orientation and triblend
    return normalize ( tnormalX.zyx * blend.x + tnormalY.xzy * blend.y + tnormalZ.xyz * blend.z );
}

void main ()
{
    color = vec4 ( 0.5 * ( 1 + dot ( l, boxmap ( p * 0.25, 1 ) ) ) );
}

Рис 6. Сложение нормалей по методу Whiteoout

#version 330 core

in  vec3 p;
in  vec3 l;
in  vec3 n;
in  vec3 nw;
out vec4 color;     // output value

uniform sampler2D image;

vec3 unpackNormal ( in vec3 n )
{
    return 2 * n - vec3 ( 1.0 );
}

    // "p" point being textured
    // "k" controls the sharpness of the blending in the transitions areas
vec3 boxmap ( in vec3 p, in float k )
{
        // blending coefficients
    vec3 blend = pow ( abs ( nw ), vec3 ( k ) );
    
    blend /= blend.x + blend.y + blend.z;   // note: they all non-negative due to abs
    
    vec2 uvX = p.zy;        // x facing plane
    vec2 uvY = p.xz;        // y facing plane
    vec2 uvZ = p.xy;        // z facing plane

    	// tangent space normal maps
    vec3 tnormalX = unpackNormal ( texture ( image, uvX ).rgb );
    vec3 tnormalY = unpackNormal ( texture ( image, uvY ).rgb );
    vec3 tnormalZ = unpackNormal ( texture ( image, uvZ ).rgb );

    	// swizzle world normals into tangent space and apply whiteout blend
    tnormalX = vec3 ( tnormalX.xy + nw.zy, abs(tnormalX.z) * nw.x );
    tnormalY = vec3 ( tnormalY.xy + nw.xz, abs(tnormalY.z) * nw.y );
    tnormalZ = vec3 ( tnormalZ.xy + nw.xy, abs(tnormalZ.z) * nw.z );

    	// swizzle tangent normals to match world orientation and triblend
    return normalize ( tnormalX.zyx * blend.x + tnormalY.xzy * blend.y + tnormalZ.xyz * blend.z );
}

void main ()
{
    color = vec4 ( 0.5 * ( 1 + dot ( l, boxmap ( p * 0.25, 1 ) ) ) );
}

Примеры кода для этой статьи находятся в репозитории к книге https://github.com/steps3d/graphics-book в папке с примерами на python.