Войти
ПрограммированиеСтатьиГрафика

Создание реалистичной поверхности воды с использованием GLSL (2 стр)

Автор:

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

1) для начала нам нужно найти нормаль в данной точке и проделать кое-какие вспомогательные вычисления.

 vec3 normal = 2.0 * texture2D(normal_texture, gl_TexCoord[0].st * fNormalScale 
  + vec2(time_density_clipplane.x) ).xzy - vec3(1.0);
  // так как в карте нормалей значения заданы в касательном пространстве,
  // то нам нужно перевести его в мировое
  // поэтому .xzy, а не .xyz
  normal = normalize(normal + vec3(0.0, 1.0, 0.0));
  // немного «выпрямим» нормаль (это делать не обязательно)

  // введем переменную для обозначения
  // масштабного коэффициента HDR изображения
  float fHDRscale = water_color.w;
  // вычислим нормированное положение источника света
  vec3 lpnorm   = normalize(light_source);
  // вычислим нормированный вектор взгляда
  vec3 vpnorm   = normalize(view_position - vertex.xyz);
  // вычислим проективные координаты
  vec3 proj_tc  = 0.5 * proj_coords.xyz / proj_coords.w + 0.5;
  // вычислим коэффициент Френеля для смешивания отражения и преломления
  float fresnel = 1.0 - dot(vpnorm, normal);

2) разберемся с глубиной воды

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

 // вычисляем расстояние от камеры до точки 
 float fOwnDepth = calculate_linear_depth(proj_tc.z);
 // считываем глубину сцены
 float fSampledDepth = texture2D(depth_texture, proj_tc.xy).x;
 // преобразуем её в линейную (расстояние от камеры)
 fSampledDepth       = calculate_linear_depth(fSampledDepth);
 // получаем линейную глубину воды
 float fLinearDepth  = fSampledDepth - fOwnDepth;

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

 float fExpDepth = 1.0 - exp( -time_density_clipplane.y * fLinearDepth);
 float fExpDepthHIGH = 1.0 - exp( -0.95 * fLinearDepth );

Как видно в переменной fExpDepth  записана глубина с учетом нашего параметра непрозрачности. Переменная fExpDepthHIGH пригодится нам для создания плавного перехода вода/берег. Значение 0.95 подобрано с учетом сцены, и должно настраиваться лично, под свои требования. Подбирается экспериментально :)

3) свет и тень

Теперь уже можно добавить в нашу воду тень и отраженный солнечный свет (или отраженный свет любого другого источника). Для получения тени используем стандартную технику shadow map, текстурные координаты мы передаем из вершинного шейдера как varying переменную, но к ним добавим искажения. Нам надо получить два значения тени – одно их них – это непосредственно значение того, затенена ли данная точка или нет, а второе – это осветленная тень для придания большей реалистичности и красоты теням на поверхности воды. Ведь есть на воде будет лежать черная полоска – это, согласитесь, не очень реалистично.

  vec3 shadow_tc = shadow_proj_coords.xyz / shadow_proj_coords.w + 
    0.06 * vec3(normal.x, normal.z, 0.0);
  // вычисляем текстурные координаты со смещением
  float fShadow = SampleShadow(shadow_tc); // получаем непосредственную тень
  float fSurfaceShadow = 0.25 * fShadow + 0.75; // осветляем её

Для бликов на воде можно использовать модель Фонга, а чтобы избавиться от возведения в степень, используем аппроксимацию Шлика.

 vec3 ref_vec = reflect(-lpnorm, normal); // вычисляем отраженный вектор
 // получаем скалярное произведение 
 // отраженного вектора на вектор взгляда
 float VdotR = max( dot(ref_vec, vpnorm), 0.0 );  
 // аппроксимация Шлика a^b = a / (b – a*b + a) для a от нуля до единицы
 VdotR /= 1024.0 - VdotR * 1024.0 + VdotR;
 vec3 specular = specular_color * vec3(VdotR) * fExpDepthHIGH * fShadow; 
 // вычисляем отраженный свет, с учетом затенения и глубины воды в данной точке.

Теперь у нас есть хоть что-то, на что можно посмотреть :)

image_03.jpg | Создание реалистичной поверхности воды с использованием GLSL

4) главное – отражения и преломления
ну что же, пришло время сделать воду настоящей - такой, которая бы отражала и преломляла объекты. Для начала разберемся с преломлениями.

Если вам не сильно накладно – можно отрисовать объекты под водой в отдельную текстуру, и использовать её. Но в целях экономии мы будем копировать и использовать текущий буфер. Но при использовании такого метода появляется одна проблема. А именно – искажаться в воде будут и объекты, которые находятся над водой. Вот так:
image_04.jpg | Создание реалистичной поверхности воды с использованием GLSL

Чтобы избежать этой ошибки необходимо прочитать значения глубины сцены в искаженных текстурных координатах и сравнить её с глубиной текущей точки. Если глубина точки воды больше, чем прочитанная глубина – то искажения учитывать не нужно, и использовать только проективные координаты.

  // величина искажения – чем глубже, тем искажения больше
  float fDistortScale = 0.1 * fExpDepth;
  vec2 vDistort = normal.zx * fDistortScale; // смещение текстурных координат
  // читаем глубину в искаженных координатах
  float fDistortedDepth = texture2D(depth_texture, proj_tc.xy + vDistort).x;
  // преобразуем её в линейную
  fDistortedDepth = calculate_linear_depth(fDistortedDepth);
  float fDistortedExpDepth = 
    1.0 - exp( -time_density_clipplane.y * (fDistortedDepth - fOwnDepth) );
  // вычисляем экспоненциальную глубину в искаженных координатах
  // теперь сравниваем расстояния – если расстояние до воды больше,
  // чем до прочитанной то пренебрегаем искажением
  if (fOwnDepth > fDistortedDepth) 
  {
    vDistort = vec2(0.0);
    fDistortedExpDepth = fExpDepth;
  }
  // теперь читаем из текстуры преломлений цвет
  vec3 refraction = texture2D(refract_texture, proj_tc + vDistort).xyz;
  // и закрашиваем его цветом воды, в зависимости от глубины
  refraction = mix(refraction, water_color * fHDRscale, fDistortedExpDepth);

Тут надо сказать, что при использовании HDR цвет воды должен умножаться на масштабный коэффициент. Если цвет воды будет в пределах [0; 1], а прочитанный цвет сцены в широком динамическом диапазоне (допустим около 100), то изменения цвета практически не произойдет. Это получено из личного опыта и здравой логики :). После проделанных операций по удалению ошибок мы получим вот такую картинку:

image_05.jpg | Создание реалистичной поверхности воды с использованием GLSL

Осталось добавить последний штрих – отражения. Никаких проблем здесь нет – читаем из текстуры с искаженными текстурными координатами.

 // коэффициент можно подобрать под свои нужды
 vDistort = normal.xz * 0.025;
 vec3 reflection = texture2D(reflect_texture, proj_tc.st + vDistort).xyz;

Теперь осталось смешать отражения и преломления и добавить отраженный свет.

  float fMix = fresnel * fExpDepthHIGH; 
  // вычисляем коэффициент смешивания
  // как коэффициент Френеля плюс вычисленная глубина
  // для плавного перехода из «берега» в воду
  // смешиваем и умножаем на осветленную тень
  vec3 result_color = mix(refraction, reflection, fMix) * fSurfaceShadow;
  result_color += specular; // добавляем отраженный свет
  gl_FragColor = vec4( result_color, 1.0); // ура! записываем значение

В конце концов, мы получили то, что хотели – реалистичную воду:

Сцена с водой на GLSL | Создание реалистичной поверхности воды с использованием GLSL

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

Что еще можно добавить в плане графики:
• хроматические абберации, т.е. такой эффект, при котором изображения объектов в разных цветах не совпадают в пространстве. Tiago Sousa из Crytek утверждает, что это придает интересный вид: «Chromatic dispersion approx. for interesting look», хотя я не нашел в этом никакой привлекательности :)
• поверхность воды можно сделать более неровной за счет умножения результата на скалярное произведение (L•N)

Ах да! Каустика!

Ведь свет, проходя и преломляясь сквозь неровную поверхность воды, иногда образует на поверхности предметов, находящихся под водой, яркие линии, которые и называются каустикой. Почитать подробнее про этот эффект можно на http://ru.wikipedia.org/wiki/Каустика, а пока займемся её моделированием.

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

float CalculateCaustic(vec3 _vertex, vec3 _light)
{
  vec3 result = vec3(0.0);
  if (_vertex.y < 0.0)
  {
    float vertex_depth = max( -_vertex.y, 0.0 );
    vec3 PtoV = normalize(_light - _vertex);
    float fsinPsi = sqrt(1.0 - PtoV.y*PtoV.y);
    float fLen = vertex_depth * fsinPsi / PtoV.y;
    float toSurface = sqrt( fLen*fLen + _vertex * _vertex ); 
    vec3 caustic_tc = (vertex.xyz + PtoV * toSurface) / 128.0; 
    // знаменатель подбирается под вашу сцену,
    // исходя из количества повторений текстуры карты нормалей
    result = 2.0 * texture2D(waternormal_texture, caustic_tc.xz).xyz - vec3(1.0);
    float fScale = ( 1.0 - pow(result.z, 8.0) );
    result = 4.0 * PtoV.y * fScale * min(vertex_depth * 0.05, 1.0);
  }
  return result;
}

Функция, которая вычисляет каустику во фрагментном шейдере, принимая положение точки и положение источника света. Значение 128.0, на которое делятся вычисленные текстурные координаты, подбирается лично под сцену. По сути – это количество повторений карты нормалей на поверхности воды. Выглядит такая каустика примерно так:

image_06.jpg | Создание реалистичной поверхности воды с использованием GLSL

Спасибо всем, кто дочитал статью до конца. Думаю, в следующий раз мы поговорим о добавлении физики в нашу воду. Удачи!

Источники:
Tiago Sousa's GDC 2008 "Crysis Next Gen Effects"
Статья о каустике на Википедии
Коэффициенты Френеля

Страницы: 1 2

#GLSL, #NextGen, #OpenGL, #вода

12 июня 2009 (Обновление: 22 июня 2009)

Комментарии [480]