steps3D - Tutorials - Дождь на поверхности

Рендеринг поверхности под дождем

Здесь мы рассмотрим некоторые приемы для рендеринга дождя (оригинальное видео для UE) с переводом на GLSL и пояснениями. Когда мы рассматриваем дождь на поверхности, то возникают следующие эффекты

Далее мы рассмотрим реализацию каждого из этих эффектов в виде шейдеров на GLSL.

Изменение параметров намокающей поверхности

Когда поверхность намокает,то у нее часто цвет становится более насыщенным (saturated) и темным. Кроме того меняется неровность поверхности - она становится более гладкой.

Обратите внимание, что намокание поверхности описывается сразу двумя коэффициентами - wetness и porousness. Первый из них (wetness) говорит о том, насколько мокрой является данная поверхность. Второй (porousness) насколько данная поверхность может впитывать воду. Именно впитывание поверхностью воlы и приводит к изменению цвета.

Например, если у нас есть пластик, то он совсем не впитывает воду, поэтому цвет поверхности совсем не меняется. Но чем более мокрой является поверхность, тем сильнее меняется ее неровность (roughness).

С другой стороны многие материалы обладают большой porousness и при попадании на них воды они ее впитывают и их цвет также меняется.

Для того, чтобы понять как просто и легко можно менять насыщенность цвета, давайте сначала рассмотрим как можно сделать заданный цвет С менее насыщенным. Если мы понизим насыщенность С до нуля, то мы получим оттенок серого цвета той же яркости что и С. Таким образом понижение насыщенности можно представить как смешивание исходного цвета с серым цветом той же яркости. Этот подход реализован в следующей функции:

const vec3  lum = vec3 ( 0.2126, 0.7152, 0.0722 );

vec3    desaturate ( in vec3 color,  float percent )
{
    vec3    d  = vec3 ( dot ( color,lum  ) );       // completely desaturated  color


    return d * percent + color * (1.0 - percent );
}

Тогда самым простым способом повышения насыщенности цвета будет вызывать для него приведенную выше функцию desaturate с параметром percent меньшим нуля, мы будем для этой цели использовать значение -0.5. Обратите внимание что при использовании отрицательных значений мы можем получить итоговый цвет, компоненты которого больше не лежат на отрезке [0,1]. Поэтому к результату данной функции необходимо применить приведение к этому отрезку, т.е. clamp(,0,1).

Все эти изменения можно представить при помощи следующей функции:

void    water ( inout vec3 color, inout float roughness, inout float specular,
                in float porousness, in float wetness  )
{
    vec3    des      = desaturate ( color, -0.5 );
    vec3    wetColor = clamp ( des, vec3(0.0), vec3(1.0) ) * 0.5;


    color     = mix ( color, wetColor, porousness * wetness );
    roughness = mix ( roughness, waterRoughness, wetness );
    specular  = mix ( specular,  waterSpecular,  wetness );
}

Капли, появляющиеся на поверхности

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

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

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

Рис. Текстура капель, каналы RG, B и A

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

В RG-канале хранятся xy-компоненты нормали (в касательном пространстве). Значение z-компоненты нормали можно не задавать, так как оно всегда положительно и легко может быть найдено из условия нормированности нормали.

Давай те теперь рассмотрим как мы можем сейчас получить по значению из данной текстуры две маски - маску для динамических капель (dynamicMask) и маску для статических капель (staticMask).

// RG - n.xy, B-temp offs, A-dynamic mask
vec4    m0          = texture ( rainMap, tex * texScale );
float   staticMask  = pow ( saturate ( 1.0 - m0.a * 2.0 ), 10.0 );
float   dynamicMask = saturate (m0.a*2 - 1) * fract ( m0.b - time * rainSpeed );
float   finalMask   = staticMask + dynamicMask;

Для динамических капель в А-канале хранится 1, в пустых местах 0.5, для статических - 0. Тогда если мы умножим значение на два, вычтем единицу и приведем полученное значение к [0,1], то для динамических капель мы получим 1, а для всех остальных фрагментов - 0. Т.е. это и будет нужная нам маска для динамических капель.

Однако динамические капли должны появляется в некоторый момент времени, а потом постепенно исчезать, то эту маску мы умножим на маску по времени - fract(m.b - t*rainSpeed). Поскольку в В-канале текстуры хранится смещение по времени для капли, то мы получим что капли будут появляться в различные моменты времени и потом постепенно исчезать.

Аналогично sat(1 - m.a * 2) дает 1 для статических капель и меньшее единицы значение для всех остальных. Чтобы перевести остальные значения в 0 мы можем воспользоваться функцией pow с некоторой достаточно большой степенью.

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

vec2  n0 = m0.rg * 4.0 - vec2  ( 2.0 );
vec3  n  = normalize ( vec3 ( n0 * finalMask, 1.0 ) );

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

На самом деле нам осталось перед расчетом нормали умножить итоговую маску на sat(nw.y). Это нужно для того, чтобы капли появлялись только на той части поверхности, которая смотрим вверх.

Капли, стекающие вниз по поверхности

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

Рис.Стекающие по поверхности капли

Первая текстура (rain_drips) разбита на набор вертикальных полос. В каждой из этих полос у нас стекает вниз всего одна капля и есть ее траектория. В В-канале хранится траектория стекающих вниз капель - по одной в каждой полосе. В RG-канале, как и ранее, хранятся xy-компоненты нормали для стекающей капли. Поскольку мы хотим чтобы для каждой дорожки капли стекали в разное время, то в А-канале текстуры мы храним смещение во времени для каждой дорожки.

Структура первой показано на следующем рисунке.

Рис. Каналы RG, B и A первой текстуры.

Во второй текстуре (rain_drip_mask) у нас хранится маска для одной стекающей капли и она будет накладываться на В-канал предыдущей текстуре.

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

Непосредственно по этим координатам (отмасштабированные соответствующим образом) мы читаем значение из первой текстуры. Текстурные координаты для второй текстуры мы должны смещать по времени для создания эффекта стекающих капель. При этом ко времени мы будем добавлять смещение по времени из А-канала первой текстуры.


vec4    m0   = texture ( rainDripsMap, tx * texScale );   // RG - n.xy, B-mask, A-temp offs
vec3    temp = vec3(round (m0.b), 0.2, 0.75 );
float   t    = mix (temp.g, temp.b, m0.a) * ( time + m0.a );
vec4    m1   = texture ( rainDripsMaskMap, vec2(t) + 0.5*tx );
float   mask = m1.r * round(m0.b);

Переменная mask определяет видим ли мы в данный момент в этом месте каплю. Чтобы убрать капли на почти горизонтальных поверхностях нам следует откорректировать маску с учетом нормали в мировой системы координат как показано ниже.

mask *= sat ( mix ( 25, -1, abs ( nw.y ) );

После этого нам останется только посчитать вектор нормали и неровности в данной точке.

n2    = normalize( vec3 ( ( m0.rg * 2.0 - vec2(1.0)) * mask,1.0 ) );

Концентрические круги на воде от падающих капель

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

Рис. Концентрические круги на воде от падающих капель.

Для этого эффекта нам также понадобится специальная текстура, компоненты которой показаны на следующем рисунке.

Рис. R, GB и A компоненты текстуры с каплями

На этот раз xy-компоненты нормали содержатся в GB-компонентах, в А-канале, как и ранее, хранится смещение во времени (temporal offset) для каждой капли. А в R-канале для каждой капли хранится радиальный градиент. Одна такая текстура задает сразу много капель на поверхности.

vec4    m    = texture ( rainRipplesMap, 0.5 * tex );
float   tf   = fract ( time + m.a );
float   tm   = tf - 1 + m.r;
float   mask = (1 - tf) * sin(PI*clamp(20 * tm, 0, 5 ));

В переменной tf у нас находится зацикленное (за счет использования функции fract) время с учетом временного смещения для каждой капли. Далее мы сдвигаем наш радиальный градиент при помощи зацикленного времени. И на следующем шаге мы по нему рассчитываем волны:

vec3   n2   = normalize ( vec3 ( 0.3*mask*(2 * m.gb - vec2 ( 1 )), 1 ) );

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

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

void ripples ( in vec2 tx, in float t, inout vec3 n )
{
    vec4    m    = texture ( rainRipplesMap, tx );
    float   tf   = fract   ( t + m.a );
    float   tm   = tf - 1 + m.r;
    float   mask = (1 - tf) * sin(PI*clamp(20 * tm, 0, 5 ));


    n += normalize ( vec3 ( 0.3*mask*(2 * m.gb - vec2 ( 1 )), 1 ) );
}

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

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