steps3D - Tutorials - Детализация карт нормалей

Детализация карт нормалей

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

Рис 1. Основная карта нормалей

Рис 2. Карта детализации

Самый простой вариант наложения карт номралей - просто сложить нормали или использовать режим overlay из Photoshop. К сожалению это дает не самый хороший результат.

Рис 3. Результат линейного наложения нормалей.

Рис 4. Результат наложения нормалей через режим overlay.

vec3 blendLinear ( vec4 n1, vec4 n2 )
{
    vec3 r = (n1.xyz + n2.xyz) * 2 - 2;

    return normalize ( r );
}

vec3 blendOverlay ( vec4 n1, vec4 n2 )
{
    n1 = n1*4 - 2;

    vec4 a = -sign ( n1 );
    vec4 b = sign ( sign ( n1 ) + vec4 ( 0.5 ) );

    b  = 0.5 * ( b + vec4 ( 1.0 ) );
    n1 = 2*a + n1;
    n2 = n2*a + b;

    vec3 r = n1.xyz * n2.xyz - a.xyz;

    return normalize ( r );
}

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

n'=(hx, hy,1)

И вот карты высот (как и их прозводные мы как раз можем складывать. Поэтому если мы поделим вектор нормали на его z-компоненту, то мы получим вектор

n'=(hx, hy,1).

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

Рис. 5. Результат наложения нормалей через восстановление производных

Ниже приведен соответствующий код.

vec3 blendPd ( vec4 n1, vec4 n2 )
{
    n1 = n1*2 - 1;
    n2 = n2.xyzz*vec4(2, 2, 2, 0) + vec4(-1, -1, -1, 0);

    vec3 r = n1.xyz*n2.z + n2.xyw*n1.z;

    return normalize ( r );
}

Есть два упрощения данного варианта - whiteout blending и UDN blend.

Рис 6. Результат применения whiteout blending.

Рис 7. Результат применения UDN blend.

Ниже приводится соответствующий код.

vec3 blendWhiteout ( vec4 n1, vec4 n2 )
{
    n1 = n1*2 - 1;
    n2 = n2*2 - 1;

    vec3 r = vec3 ( n1.xy + n2.xy, n1.z*n2.z );

    return normalize ( r );
}


vec3 blendUdn ( vec4 n1, vec4 n2 )
{
    vec3 c = vec3 ( 2, 1, 0 );
    vec3 r = n2.xyz*c.yyz + n1.xyz;

    r =  r*c.xxx -  c.xxy;

    return normalize ( r );
}

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

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

Любой поворот в 3D может быть представлен при помощи кватерниона. Если у нас есть поворот, переводяшщий вектор v0 в вектор v1, то соответствующий кватернион задается формулой

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

Данный метод получил название reoriented normal mapping (RNM). В демо от Unity усполльзуется метод, основанный на похожей идее. Ниже приводятся результаты этого метода и нескольких его вариаций.

Рис. 8. Reoriented normal mapping

Рис 9. Модификация Reoriented normal mapping (unity)

vec3 blendRnm ( vec4 n1, vec4 n2 )
{
    vec3 t = n1.xyz*vec3( 2,  2, 2) + vec3(-1, -1,  0);
    vec3 u = n2.xyz*vec3(-2, -2, 2) + vec3( 1,  1, -1);
    vec3 r = t*dot(t, u) - u*t.z;

    return normalize ( r );
}


vec3 blendUnity ( vec4 n1, vec4 n2 )
{
    n1 = n1.xyzz*vec4(2, 2, 2, -2) + vec4(-1, -1, -1, 1);
    n2 = n2*2 - 1;

    vec3 r;

    r.x = dot(n1.zxx,  n2.xyz);
    r.y = dot(n1.yzy,  n2.xyz);
    r.z = dot(n1.xyw, -n2.xyz);

    return normalize ( r );
}

По этой ссылке можно скачать весь исходный код к этой статье.