Создание реалистичной поверхности воды с использованием 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; // вычисляем отраженный свет, с учетом затенения и глубины воды в данной точке.
Теперь у нас есть хоть что-то, на что можно посмотреть :)