Моделирование подповерхностного рассеивания
Автор: Сергей Резник
В этой статье речь пойдёт о быстром 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;
Результатом проделанной операции будет вот такое изображение:
Уже ближе? Наверно…, но продолжим.
Теперь будем моделировать зависимость освещенности от расстояния до источника. Чем ближе к источнику света – тем сильнее освещена поверхность и сам материал («подповерхность» :)).
Для начала вычислим расстояние от источника света до текущей точки:
// параметр, регулирующий затухание света, в зависимости от расстояния 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;
И получим вот такую картинку:
В общем и целом, наш нефритовый заяц готов. Но есть еще один момент.
Если смотреть через полупрозрачные материалы непосредственно на источник света – то материал становиться светлее. Давайте промоделируем и этот эффект. При взгляде сквозь нашего зайца на источник света без этого эффекта мы будем наблюдать такую картину:
Наш заяц совсем не пропускает свет. Давайте пропустим :)
Для этого нам всего лишь нужно найти скалярное произведение между двумя векторами, проходящими через текущую точку, положением камеры и положением источника света.
// параметр, определяющий насколько световое пятно // будет сконцентрировано: 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;
Финальное вычисление мы не изменяем, но теперь наш заяц пропускает свет:
Таким образом финальный шейдер (вернее его дополнение) будет таким:
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
Нефрит, который мы пытались сделать
#scattering, #subsurface, #освещение, #рассеивание
30 июня 2009