Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
Дождь является довольно часто встречающимся погодным явлением. Однако полосы дождя достаточно длинны и использование для них обычных систем частиц не всегда является оправданным и удобным. Здесь мы рассмотрим один из вариантов рендеринга полос/частиц дождя, работающий целиком на GPU, и имеющий минимальный CPU overhead.
Пусть у нас есть игрок/камера перемещающийся по некоторому ландшафту под дождем. Очевидно, что создавать (выводить) дождь сразу над всем этим ландшафтом было бы довольно дорого и бесполезно. Нам достаточно уметь выводить дождь лишь в некоторой окрестности вокруг камеры.
Однако при этом сама эта окрестность не должна быть "жестко приклеена" к камере/игроку (т.е. дождь не должен двигаться вместе с камерой). Нам достаточно лишь выводить дождь в некоторой окрестности вокруг камеры. Однако при этом эта окрестность не должна быть жестко "приклеена" к камере.
Поэтому мы пойдем следующим путем - мы разобьем все 3D пространство на достаточно большие клетки фиксированного размера. При этом это разбиение привязано к ландшафту и не зависит от камеры. Тогда для каждого положения камеры (x,y,z) мы можем найти индекс клетки, в котором находится камера. Мы будем выводить дождь только в этой клетке, а также во всех смежных с ней клетках (рис).
Рис. Блок из 3х3х3 клеток, в середине - клетка, где находится камера
Сама система клеток жестко привязана к пространству и создаваемый таким образом дождь также будет привязан к
пространству/ландшафту.
Для того, чтобы минимизировать число вызовов glDraw*
мы будем использовать instancing - команду glDrawArraysInstanced
.
За один ее вызов мы выведем сразу все 27 клеток с дождем сразу.
Вершинный шейдер по встроенной переменной gl_InstanceID
сам определяет к какой именно клетке (из 27) относится та или
иная вершина и произведет соответствующий сдвиг координат.
#version 330 core
#define PI 3.1415926
uniform mat4 mv;
uniform mat4 proj;
uniform vec3 eye;
uniform float t;
uniform int nx, ny;
uniform int gridSize;
uniform sampler3D noiseMap;
layout ( location = 0 ) in vec4 pos;
layout ( location = 1 ) in vec2 texCoord;
out vec4 tex;
// remap instance id into box id (assume 3x3x3 boxes)
ivec3 getBoxIndex ()
{
int ix = gl_InstanceID % 3;
int ii = gl_InstanceID / 3;
int iy = ii % 3;
int iz = ii / 3;
return ivec3 ( ix - 1, iy - 1, iz - 1 );
}
void main(void)
{
// get grid box index with camera in it
vec3 org = vec3 ( gridSize ) * floor ( eye / vec3 ( gridSize ) );
// get box origin
ivec3 boxIndex = getBoxIndex ();
vec3 offset = vec3 ( gridSize ) * vec3 ( boxIndex );
vec3 pt = gridSize * pos.xyz + org + offset;
// pass text coords, random value and distance to camera
tex = vec4 ( texCoord, pos.w, length ( pt ) );
gl_Position = vec4 ( pt, 1.0 );
}
Для каждой частицы дождя мы будем передавать всего одну вершину - в ее x и y координатах мы будем передавать 2D-координаты частицы (без учета положения по вертикали), а в z и w будут находиться заранее рассчитанные случайные величины, нужные для придания дождю нерегулярного (случайного) вида. Задача вершинного шейдера - пересчитать координаты с учетом номера клетки и передать дальше в геометрический шейдер.
Именно геометрический шейдер превращает одну вершину в два пересекающихся прямоугольника (рис 2), обеспечивающий хорошую видимость с любой боковой стороны. Кроме того, именно геометрический шейдер отвечает за анимацию движения отдельный частиц и придание каждой частице случайно изменяющегося оттенка.
Рис. 2. Полоса дождя, хорошо видимая с любой боковой стороны
Также именно геометрический шейдер занимается еще и оптимизацией - чем дальше от камеры, тем большее число частиц можно сразу же отбросить без ущерба для качества изображения.
Фактически с учетом по-вершинного случайного значения и расстояния до камеры определяется отбрасываем ли мы соответствующую частицу или нет. Чем дальше частица от камеры, тем выше вероятность, что она будет отброшена.
В геометрическом шейдере мы будем использовать две вспомогательных функции - addVertex
и addQuad
.
Первая из них (addVertex
) строит новую вершину прямоугольника и добавляет к выводимому примитиву.
Вторая функция (addQuad
) при помощи addVertex
выводит готовый прямоугольник.
#version 330 core
layout ( points ) in;
layout ( triangle_strip, max_vertices = 8 ) out;
uniform float threshold = 0.5;
uniform float time;
uniform sampler3D noiseMap;
uniform mat4 mv;
uniform mat4 proj;
in vec4 tex [];
out vec4 texOut;
void addVertex ( vec4 vertex, vec2 uv, float colorVariation, float opacity )
{
texOut.xy = uv;
texOut.z = opacity;
texOut.w = colorVariation;
gl_Position = proj * mv * vertex;
EmitVertex ();
}
void createQuad ( vec4 bottomMiddle, vec4 topMiddle, vec4 perpDir,
float colorVariation, float opacity )
{
addVertex ( bottomMiddle - perpDir, vec2 ( 0, 0 ), colorVariation, opacity );
addVertex ( bottomMiddle + perpDir, vec2 ( 1, 0 ), colorVariation, opacity );
addVertex ( topMiddle - perpDir, vec2 ( 0, 1 ), colorVariation, opacity );
addVertex ( topMiddle + perpDir, vec2 ( 1, 1 ), colorVariation, opacity );
EndPrimitive ();
}
const float fallSpeed = 3;
const float maxTravelDistance = 4;
const float maxDist = 6; // distance attenuation
void main ()
{
// randomized discard points
if ( tex [0].z > threshold )
return;
vec4 pos = gl_in [0].gl_Position;
vec2 uv = tex [0].xy;
vec4 ns1 = texture ( noiseMap, pos.xyz * vec3 ( 0.123, 0.97643, 0.35991 ) );
vec4 ns2 = texture ( noiseMap, pos.xyz * vec3 ( 0.2123, 0.643, 0.9914 ) );
// start movement
pos.y -= (time + 10000) * fallSpeed * ( 1 + ns1.x );
pos.y = mod ( pos.y, maxTravelDistance + ns1.y );
float opacity = 1.0;
opacity *= 1.0 - (maxDist - tex [0].w) / maxDist;
if ( opacity <= 0 )
return;
float colorVariation = (sin(ns2.x * (pos.x + pos.y * ns2.y + pos.z + time * 2))*0.5+0.5)*1;
vec2 quadSize = vec2 ( mix ( 0.03, 0.05, ns2.z ) );
vec4 quadUpDir = vec4 ( 0, 1, 0, 0 );
vec4 topMiddle = pos + quadUpDir * ( 0.3 + 0.4 * ns1.z );
vec4 sideDir1 = vec4 ( 1, 0, 0, 0 );
vec4 sideDir2 = vec4 ( 0, 0, 1, 0 );
createQuad ( pos, topMiddle, 0.08 * quadSize.x * sideDir1, colorVariation, opacity );
createQuad ( pos, topMiddle, 0.08 * quadSize.y * sideDir2, colorVariation, opacity );
}
Фрагментный шейдер крайне прост - он использует специальную текстуру и переданное случайное значение для определения оттенка текущего фрагмента.
#version 330 core
uniform sampler2D precipitationMap;
in vec4 texOut;
out vec4 color;
const vec3 colorVariation = vec3 ( 1.0 );
vec3 saturate ( in vec3 c )
{
return clamp ( c, 0, 1 );
}
void main(void)
{
color = texture ( precipitationMap, texOut.xy ) * texOut.z;
// add hue variation
float amount = texOut.w;
vec3 shiftedColor = mix ( color.rgb, colorVariation, amount );
float maxBase = max ( color.r, max(color.g, color.b) );
float newMaxBase = max ( shiftedColor.r, max(shiftedColor.g, shiftedColor.b) );
// preserve vibrance
color.rgb = saturate ( shiftedColor * ((maxBase/newMaxBase) * 0.5 + 0.5 ) );
// apply opacity
color.a *= texOut.z;
}
Рис. Получившийся дождь
По этой ссылке можно скачать весь исходный код к этой статье.