Программирование игр, создание игрового движка, OpenGL, DirectX, физика, форум
GameDev.ru / Программирование / Статьи / Моделирование подповерхностного рассеивания

Моделирование подповерхностного рассеивания

Автор:

В этой статье речь пойдёт о быстром fake-методе реализации подповерхностного рассеивания (subsurface scattering) полупрозрачных материалов.

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

Подповерхностное рассеивание необходимо использовать для правильной отрисовки таких материалов как мрамор, нефрит, воск (парафин), кожа, и пр.

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

Для моделирования подповерхностного рассеивания нам придется совсем на немного изменить обычную отрисовку объектов.

Итак, допустим, что у нас уже есть некоторый алгоритм отрисовки объектов. Пусть он использует стандартную модель освещения (модель Ламберта) и некоторую модель вычисления отраженного света (Фонг, Блинн, Кук-Торренс – роли практически не играет). Также может быть алгоритм затенения объектов (карты теней, ambient occlusion). Пускай параметры источника света задаются у нас через колонки некоторой матрицы. В первой колонке – фоновое освещение (ambient), во второй – рассеянное (diffuse), в третьей отраженное (specular). Допустим также, что мы уже вычислили затенение объекта и модель освещения. В общем и целом результирующий цвет будет вычисляться так:

vec3 vResult = (light_matrix[0] * fAOterm +  
                light_matrix[1] * vLighting.x * fShadow) * сСolor + 
                light_matrix[2] * vLighting.y * fShadow;

Где light_matrix – матрица параметров источника света, fAOterm – компонент ambient occlusion (не обязательный параметр), vLighting содержит в себе две компоненты – в x – диффузная компонента (diffuse, L•N), в y – отраженная (бликовая, specular), параметр fShadow определяет затененность объекта в данной точке, cColor – цвет материала (может браться из текстуры или задаваться произвольно). Для начала уменьшим шероховатость поверхности (для моделей Фонга, Блинна – увеличим степень, в которую возводиться скалярное произведение – я думаю, вы поняли, о чем я :). И еще – подберем сразу цвет материала для определенности. Я предлагаю вместе сделать нефритового зайца – цвет будем брать примерно такой (RGB) : (255, 255, 154)

Значит, стандартная отрисовка у нас есть. Выглядеть это будет примерно так:

стандартная отрисовка | Моделирование подповерхностного рассеивания

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

1) введем новый параметр для осветленной тени на поверхности (старое значение нам все еще нужно для затенения отраженного света):

 float fSurfaceShadow = 0.85 + 0.15 * fShadow;

2) если есть ambient occlusion – то осветлим и его:

 fAOterm              = 0.50 + 0.50 * fAOterm;

3) теперь главное. У нас в vLighting.x храниться угол между источником света и нормалью в данной точке. Сделаем освещение менее зависимым от угла падения света:

 vLighting.x          = 0.85 + 0.15 * vLighting.x;

Таким образом, вычисление результирующего цвета фрагмента перепишем в таком виде:

 vec3 vResult = (light_matrix[0] * fAOterm +  
                 light_matrix[1] * vLighting.x * fSurfaceShadow) * cColor + 
                 light_matrix[2] * vLighting.y * fShadow;

Результатом проделанной операции будет вот такое изображение:

Осветленная тень + ambient occlusion | Моделирование подповерхностного рассеивания

Уже ближе? Наверно…, но продолжим.

Теперь будем моделировать зависимость освещенности от расстояния до источника. Чем ближе к источнику света – тем сильнее освещена поверхность и сам материал («подповерхность» :)).

Для начала вычислим расстояние от источника света до текущей точки:

// параметр, регулирующий затухание света, в зависимости от расстояния
float fLinearAttenuation =  0.0075;
float fFalloffPower      =  2.00;   // скорость затухания
float fLightDistance = max( 0.0, length(light_source - vertex.xyz) - 2650.0 );

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

 float fScattering    = min(1.0, 2.5 / (1.0 + fLightDistance * fLinearAttenuation) );
 fScattering          = pow( fScattering, fFalloffPower );

Коэффициенты, наверно, придется подбирать каждому свои, чтобы добиться примерно вот такой картинки:

непосредственно рассеивание | Моделирование подповерхностного рассеивания

Добавим учет рассеивания в финальную часть шейдера вот таким образом:

vec3 vResult = (light_matrix[0] * fAOterm +  
              light_matrix[1] * vLighting.x * fSurfaceShadow * fScattering) * cColor + 
              light_matrix[2] * vLighting.y * fShadow;

И получим вот такую картинку:

Subsurface scatterings: Нефритовый заяц | Моделирование подповерхностного рассеивания

В общем и целом, наш нефритовый заяц готов. Но есть еще один момент.

Если смотреть через полупрозрачные материалы непосредственно на источник света – то материал становиться светлее. Давайте промоделируем и этот эффект. При взгляде сквозь нашего зайца на источник света без этого эффекта мы будем наблюдать такую картину:

Заяц не пропускает свет | Моделирование подповерхностного рассеивания

Наш заяц совсем не пропускает свет. Давайте пропустим :)

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

 // параметр, определяющий насколько световое пятно
 // будет сконцентрировано:
 float fLightSharpness = 16.00;
 vec3 vLightTovertex   = normalize(vertex.xyz - light_source);
 vec3 vViewTovertex    = normalize(view_position - vertex.xyz);
 float VdotL = max(0.0, dot(vLightTovertex, vViewTovertex));
       VdotL = pow(VdotL, fLightSharpness);

Мы получим примрно вот такую картинку:

Осветление | Моделирование подповерхностного рассеивания

Давайте теперь просто добавим вычисленное значение к уже имеющемуся у нас параметру:

 vLighting.x += VdotL;

Финальное вычисление мы не изменяем, но теперь наш заяц пропускает свет:

Subsurface scatterings: Нефритовый заяц, пропускающий свет | Моделирование подповерхностного рассеивания

Таким образом финальный шейдер (вернее его дополнение) будет таким:

 float fLinearAttenuation =  0.0075;
 float fFalloffPower      =  2.00;
 float fLightSharpness    = 16.00;
// ШАГ 1 : смягчаем тени
 fSurfaceShadow = 0.85 + 0.15 * fShadow;
 fAOterm        = 0.50 + 0.50 * fAOterm;
 vLighting.x    = 0.85 + 0.15 * vLighting.x;

// ШАГ 2 : рассеивание
 float fLightDistance = max( 0.0, length(light_source - vertex.xyz) - 2650.0 );
 fScattering = min( 1.0, 2.5 / (1.0 + fLightDistance * fLinearAttenuation ) );
 fScattering = pow( fScattering, fFalloffPower );

// ШАГ 3 : вычисляем проходящий сквозь объект свет
 vec3 vLightTovertex = normalize(vertex.xyz - light_source);
 vec3 vViewTovertex  = normalize(view_position - vertex.xyz);
 float VdotL  = max(0.0, dot(vLightTovertex, vViewTovertex));
       VdotL  = pow(VdotL, fLightSharpness);
 vLighting.x += VdotL;

 vec3 vResult = (light_matrix[0] * fAOterm +  
              light_matrix[1] * vLighting.x * fSurfaceShadow * fScattering) * сСolor + 
              light_matrix[2] * vLighting.y * fShadow;

 gl_FragColor = vec4(vResult, 1.0);

Теперь у нас получился довольно симпатичный нефритовый заяц.

Напомню вам, что приведенный выше алгоритм – это fake. Если вам необходимо делать честное подповерхностное рассеивание – читайте дополнительную литературу :)

Спасибо тем, кто дочитал статью до конца. Удачи вам!

Ссылки:
SSS: Subsurface scatterings (Подповерхностное рассеивание)
Небольшое обсуждение SSS
Нефрит, который мы пытались сделать

30 июня 2009

#scattering, #subsurface, #освещение, #рассеивание

2001—2018 © GameDev.ru — Разработка игр