Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
Одним из материалов, часто встречающихся в компьютерной графике, является кожа человека (в первую очередь речь идет о рендеринге лиц людей). Использование традиционных подходов к рендерингу (PBR) приводит к неестественно-выглядящим лицам - они словно сделаны из какого камня или какого-то другого материала.
Причина этого явления довольно проста и называется подповерхностным рассеиванием (subsurface scattering). В соответствии с принятым в PBR подходом мы считаем, что падающий на поверхность свет частично отражается и частично преломляется (рис 1).
Рис 1. Отражение и преломление падающего света
Отраженный свет отвечает за блики. Если материал не является металлом (а кожа им не является) , то блики имеют цвет падающего света (обычно белый). Коэффициент Френеля (Fresnel) F определяет долю отраженного света. Оставшаяся часть света (1-F) преломляется и уходит внутрь материала. Там этот свет претерпевает многочисленные переотражения внутри материала и в конце концов его часть выходит наружу.
Ранее мы считали, что в материале имеет место сильное поглощение света. В результате этого можно считать что свет выходит наружу там же, где и вошел. Этот выходящий наружу свет и формирует диффузную часть освещения. Однако в случае кожи (а также ряда других материалов) подобное допущение неверно - посмотрите на рис 2 - там хорошо видно насколько далеко свет может распространяться в теле человека.
Рис 2.Распространение света в теле человека.
В силу этого свет, вошедший внутрь, может пройти заметное расстояние внутри прежде чем он выйдет наружу. Поэтому мы не можем уже как ранее считать что свет выходит наружу в той же точке, что и пошел внутрь. И такие материалы уже описываются не с помощью BRDF, а при помощи BSSRDF, позволяющей описывать материалы с учетом подповерхностного рассеивания.
Рис3. BRDF и BSSRDF
При этом на самом деле кожа устроена довольно сложно - принято считать что она состоит из трех отдельных слоев - тонкий слой жира, epidermis и dermis.
Рис 4. Строение кожи
Для моделирования распространения света внутри кожи можно использовать диффузия - тогда вместо сложной BSSRDF мы можем использовать одномерный профиль поглощения света (рис. 5). Обратите внимание, что этот профиль зависит от длины волны - красный свет распространяется гораздо дальше чем синий.
Рис 5. Профиль поглощения света
Тем самым можно использовать этот одномерный профиль распространения света для моделирования диффузии света внутри кожи человека. Более того, было показано, что этот профиль можно приблизить суммой гауссианов. Тем самым было показано, что распространение света внутри кожи можно моделировать как гауссовское рассеивание.
Это привело к следующему подходу - осуществляется рендеринг диффузной компоненты в текстуру. И уже в этой текстуре выполняется размытие, задаваемое суммой гауссианов. Этот подход позволил строить реалистические изображения лиц. Но вместе с тем этот подход обладал и рядом довольно серьезных недостатков, заметно снижавшим его применимость.
Несколько позже было показано, что можно заменить довольно сложное двухмерное размытие в пространстве текстуры на похожее размытие в пространстве экрана (screen-space blur). Для того, чтобы учитывать реальную геометрию в этот размытие необходимо учитывать значения из буфера глубины. Строго говоря такое размытие не является сепарабельным, но было показано что можно можно приблизить двумя одномерными размытиями в пространстве экрана.
Были подобраны коэффициенты для этих одномерных размытий. В результате два прохода размытия (по x и по y)по очереди применяются к содержащему только диффузную освещенность каналу освещения (бликовая часть формируется отражением света и не подвержена подповерхностному рассеиванию). После этого к размытой диффузной части добавляется бликовая.
Ниже приводится фрагментный шейдер, выполняющий один проход этого размытия. Направление размытия (x или y) задается при помощи двухмерного вектора step( (1,0) для размытия по x и (0,1) для размытия по y). Обратите внимание, что на вход шейдер принимает сразу три текстуры - диффузную (colorMap, в альфа-канале хранится 0 или 1 в зависимости от того кожа это или нет), глубины (depthMap) и текстуру, содержащую бликовую компоненту освещения. Обратите внимание на гамма-преобразование после добавления бликовой части.
#version 330 core
in vec2 tex;
out vec4 color;
uniform vec2 step;
uniform float blurScale;
uniform float correction;
uniform int mode;
uniform sampler2D colorMap;
uniform sampler2D depthMap;
uniform sampler2D specMap;
const float zNear = 0.1;
const float zFar = 100.0;
const float SSSS_FOVY = 90.0;
float linearDepth ( float d )
{
return zFar * zNear / (d * (zFar - zNear) - zFar );
}
void main ()
{
const float gamma = 2.2;
// Gaussian weights for the six samples around current pixel
const float w [6] = float [6] ( 0.006, 0.061, 0.242, 0.242, 0.061, 0.006 );
const float o [6] = float [6] ( -1.0, -0.6667, -0.3333, 0.3333, 0.6667, 1.0 );
const vec4 kernel [] = vec4 [] (
vec4 ( 0.560479, 0.669086, 0.784728, 0 ),
vec4 ( 0.00471691, 0.000184771, 5.07566e-005, -2 ),
vec4 ( 0.0192831, 0.00282018, 0.00084214, -1.28 ),
vec4 ( 0.03639, 0.0130999, 0.00643685, -0.72 ),
vec4 ( 0.0821904, 0.0358608, 0.0209261, -0.32 ),
vec4 ( 0.0771802, 0.113491, 0.0793803, -0.08 ),
vec4 ( 0.0771802, 0.113491, 0.0793803, 0.08 ),
vec4 ( 0.0821904, 0.0358608, 0.0209261, 0.32 ),
vec4 ( 0.03639, 0.0130999, 0.00643685, 0.72 ),
vec4 ( 0.0192831, 0.00282018, 0.00084214, 1.28 ),
vec4 ( 0.00471691, 0.000184771, 5.07565e-005, 2 )
);
// fetch color and linear depth for current pixel
vec4 colorM = texture ( colorMap, tex );
float depthM = linearDepth ( texture ( depthMap, tex ).r );
vec2 size = vec2 ( textureSize ( colorMap, 0 ) );
if ( colorM.a < 0.5 ) // check whether it's skin
discard;
// accumulate center sample multiplying it with gaussian weight
vec4 colorBlurred = colorM;
float distanceToProjectionWindow = 1.0 / tan(0.5 * radians(SSSS_FOVY));
float scale = distanceToProjectionWindow / depthM;
// Calculate the final step to fetch the surrounding pixels:
vec2 finalStep = blurScale * scale * step / size;
finalStep *= colorM.a; // Modulate it using the alpha channel.
finalStep *= 1.0 / 3.0; // Divide by 3 as the kernels range from -3 to 3.
// Accumulate the center sample:
colorBlurred.rgb *= kernel[0].rgb;
for ( int i = 1; i < 11; i++ )
{
// Fetch color and depth for current sample:
vec2 offset = tex + kernel[i].a * finalStep;
vec4 color = texture ( colorMap, offset );
// If the difference in depth is huge, we lerp color back to "colorM":
float depth = linearDepth ( texture ( depthMap, offset).r );
float s = min ( 1.0, distanceToProjectionWindow * blurScale * abs ( depthM - depth) / correction );
color.rgb = mix ( color.rgb, colorM.rgb, s );
// Accumulate:
colorBlurred.rgb += kernel[i].rgb * color.rgb;
}
color = colorBlurred;
if ( step.y > step.x ) // for second pass step=(0,1)
{
if ( mode == 3 ) // combined mode
color.rgb += texture ( specMap, tex*vec2 ( 1, -1 ) ).rgb;
color.rgb = pow ( color.rgb, vec3 ( 1.0 / gamma ) );
}
}
С бликовой компонентой освещения тоже все не очень просто - было показано что лучше его моделировать суммой двух кривых с различными параметрами неровности. При этом используется распределение Фонга:
В качестве геометрической функции G используется модель Kelemen-Szirmag-Kalos:
Степень для формулы Фонга по значению неровности g находится следующим образом:
Степень для первой части блика берется из карты неровности и переводится по приведенной формуле. Степень для второй части вычисляются как квадрат степени первой части. Обе части складываются с весами 0.85 и 0.15.
Ниже приводится фрагментный шейдер для расчета диффузной и бликовой частей освещенности.
#version 330 core
in vec3 v;
in vec3 l;
in vec3 h;
in vec2 tex;
//out vec4 color;
out vec4 color [2];
uniform sampler2D diffMap;
uniform sampler2D specMap;
uniform sampler2D scatterMap;
uniform sampler2D bumpMap;
const float gamma = 2.2;
const float FDiel = 0.04; // Fresnel for dielectrics
const float pi = 3.141592;
float phongExponent ( float glossiness )
{
return (1.0/pow(1.0 - glossiness, 3.5) - 1.0);
}
vec3 Fresnel ( in vec3 f0, in float product )
{
return mix ( f0, vec3 (1.0), pow(1.0 - product, 5.0) );
}
float Fresnel ( in float f0, in float product )
{
return mix ( f0, 1.0, pow(1.0 - product, 5.0) );
}
// Kelem-Szirmay-Kalos visibility
float KelemVis ( in float nl, in float nv, in float a )
{
return 1.0 / ((nl*(1.0-a) + a)*nv*(1.0-a) + a);
}
float D_Phong ( in float nh, in float n )
{
return (n+2.0) * pow ( max ( 0, nh ), n ) / (2.0 * pi);
}
void main(void)
{
vec3 n = 2.0 * (texture ( bumpMap, tex ).rgb - vec3 ( 0.5 ));
vec3 n2 = normalize ( n );
vec3 l2 = normalize ( l );
vec3 h2 = normalize ( h );
vec3 v2 = normalize ( v );
vec4 clr = pow ( texture ( diffMap, tex ), vec4 ( gamma ) );
vec4 diff = clr * max ( dot ( n2, l2 ), 0.1 );
float nh = max ( dot ( n2, h2 ), 0.0 );
float nl = max ( dot ( n2, l2 ), 0.0 );
float nv = max ( dot ( n2, v2 ), 0.0 );
float f = Fresnel ( FDiel, clamp( dot ( h2, v2 ), 0.0, 1.0 ) );
float a = texture ( specMap, tex ).r;
float phongExp1 = pow ( 2.0, 14.0 * a );
float phongExp2 = phongExp1 * phongExp1;
float spec1 = D_Phong ( nh, phongExp1 ) * f * KelemVis ( nl, nv, a ) * nl;
float spec2 = D_Phong ( nh, phongExp2 ) * f * KelemVis ( nl, nv, a ) * nl;
color [0] = vec4 ( diff.rgb, 1.0 ); // a=1 mark as skin
color [1] = (0.85*spec1 + 0.15*spec2) * vec4 ( 1.0 ); // * texture ( specMap, tex );
}
Рис 6. Бликовая часть освещения
Рис 7. Диффузная часть освещения
Рис 8. Размытая диффузная часть освещения
Рис 9. Полное освещение
По этой ссылке можно скачать исходный код и использованную модель.