вторник, 17 июля 2012 г.

Частицы. Часть вторая, если очень хочется.

Часть первая, и снова тесты
Часть вторая, если очень хочется
Часть третья, миллионы частиц
Часть четвертая, полевые испытания
В предыдущей части статьи мы с вами выяснили что нужно любыми доступными средствами уменьшать объем передаваемых по шине данных, пускай даже за счет увеличения нагрузки на GPU. Но что можно сократить в четырехугольнике? Точно не количество вершин :)
Немного подумаем - что нам нужно для восстановления квадратной частицы? В простейшем случае нам достаточно координат центра или угла частицы и ее размер, таким образом, вместо 4 вершин нам достаточно передавать только xc,yc,zc и size. Если частиц много и они мелкие, то размер обычно одинаковый у всех частиц и не меняется во времени (в общем случае у нас может меняться размер каждой частицы, причем по двум осям, могут меняться текстурные координаты и частица может вращаться в плоскости экрана, но об этом мы поговорим позже). Таким образом для вершины нам достаточно передавать всего 3 координаты ее центра вместо координат 4-х вершин. Но теперь возникает вопрос - как из одной точки восстановить частицу?


Для решения этой задачи в OpenGL 1.4 было введено расширение ARB_point_sprite, которое ввело понятие "точечного спрайта" (Point Sprite). Установив пару специальных состояний, можно заставить OpenGL рисовать спрайты всего по одной координате. Для этого, перед началом рисования, необходимо установить следующие состояния OpenGL:

glPointSize(PointSize);
glPointParameterfv(GL_POINT_DISTANCE_ATTENUATION, @DistanceAttenuation);
glPointParameterf( GL_POINT_FADE_THRESHOLD_SIZE,FadeTresholdSize);
glPointParameterf( GL_POINT_SIZE_MIN,MinPointSize);
glPointParameterf( GL_POINT_SIZE_MAX,PointSize);
glTexEnvf( GL_POINT_SPRITE, GL_COORD_REPLACE, GL_TRUE );
glEnable(GL_POINT_SPRITE);

В частности, мы задаем размер частицы (PointSize), передаем коэффициенты полинома (DistanceAttenuation) по которому будет происходить уменьшение размера часты по мере отдаления от камеры (или увеличения, в случае приближения к камере), указываем пороговое значение до которого частица будет уменьшаться (FadeTresholdSize), ну и при желании можно жестко ограничить минимальный и максимальный размеры частиц.
После того как все параметры будут заданы, необходимо включить режим отображения точечных спрайтов, для этого необходимо разрешить замену текстурных координат и активировать состояние GL_POINT_SPRITE. Теперь, при отрисовке буфера VBO с типом примитива GL_POINTS, у вас вместо точек будут спрайты (текстура и материал по желанию).

Это очень эффективный метод рисования, так как всеми расчетами координат вершин занимается GPU, при этом мы еще и в 4 раза сократили объем передаваемых данных. Но у этого метода есть и недостатки, в частности, мы не можем (штатными средствами) менять текстур отдельной частицы, или менять ее размер, или заставить ее вращаться. Тоесть это просто "месиво" одинаковых частиц (если вы помните, в предыдущей части я специально акцентировал внимание на этом, разделив понятие спрайта и частицы).

Но что делать если очень хочется чтоб и объем передаваемых данных был небольшой, и чтоб можно было управлять видом отдельной частицы? Может есть способ как-то настроить точечные спрайты? "Нельзя, но если очень хочется, то можно". Увы, штатных средств управления параметрами отдельного точечного спрайта нет, но тем не менее мы всегда можем обратиться к шейдерам. Увы, управлять отдельными вершинами частицы мы не можем, все что нам доступно это несколько переменных встроенных переменных: gl_PointSize (в вершинном) и gl_PointCoord (в фрагментном).

Кроме этого в GLSL 1.2 есть также несколько встроенных юниформ, через которые передаются основные параметры частиц (в OGL3+ встроенные юниформы, включая параметры частиц, отменены):
struct gl_PointParameters {
  float size;
  float sizeMin;
  float sizeMax;
  float fadeThresholdSize;
  float distanceConstantAttenuation;
  float distanceLinearAttenuation;
  float distanceQuadraticAttenuation;
};
uniform gl_PointParameters gl_Point;

Чтоб иметь возможность в шейдере менять размер частиц необходимо дополнительно установить состояние "GL_PROGRAM_POINT_SIZE"

Имея в руках этот инструмент мы уже можем существенно расширить функциональность точечных спрайтов, передавая настройки отдельных частиц в отдельных вершинных атрибутах (к примеру вместо нормали, вторичных текстурных координатах или в пользовательских атрибутах). Увы, из вершинного шейдера мы сможем поменять только размер частицы, все остальное реализуется в фрагментном шейдере.
Что же мы можем сделать во фрагментном шейдере - функциональность фрагментного шейдера ничем не ограничена, с той разницей, что текстурные координаты считываются из специального варинга "gl_PointCoord". Используя эти координаты и пару чисел (offset,scale) мы можем реализовать работу с текстурным атласом, фрагментный шейдер для этой задачи будет выглядеть так:
uniform sampler2D Atlas;
varying vec2 scale;
varying vec2 offset;
void main(void)
{
  vec2 coords = gl_PointCoord * scale + offset;
  gl_FragColor = texture2D(Atlas,coords);
}

Параметры scale и offset каждой частицы передаются в вершинных атрибутах и экспортируются из вершинного шейдера.

Аналогичным образом можно реализовать и вращение частицы, вспомнив курс аналитической геометрии (поворот системы координат на плоскости):
x'=(x-xc)*cos(a)+(y-yc)*sin(a)+xc
y'=-(x-xc)*sin(a)+(y-yc)*cos(a)+yc
xc, yc у нас равно 0.5, как центр текстуры.
x,y - это наши координаты, переданные из вершинного шейдера (gl_PointCoord)
x' и y' - новые текстурные координаты. При использовании текстурного атласа их нужно умножить на scale и прибавить к ним offset.
sina,cosa - можно передавать в вершинных атрибутах для повышения производительности (или вычислять в шейдере для уменьшения объема передаваемых данных).

Рис.1. Вращение текстуры внутри квада ограничивает полезную площадь до вписанной окружности
Тут правда нужно обратить внимание на то, что мы вращаем не квад а текстуру внутри квада, из-за этого рабочее поле квада сократится до площади вписанной окружности (Рис.1.). Если нужно сохранить все пиксели текстуры, то можно просто в вершинном шейдере увеличить размер спрайта, приняв его за длину диагонали исходного квада. Используя аналогичный принцип можно сделать "не квадратный" спрайт.

Описанная выше методика это скорее "хак" точечных спрайтов. Так как все операции выполняются в фрагментном шейдере (для каждого растеризируемого пикселя), то это существенно сказывается на производительности, потому нужно выбирать что дороже - объем передаваемых данных или стоимость растеризации одного пикселя. С одной стороны - сейчас разрабатывается новый стандарт PCIEx3.0, где существенно повысится пропускная способность шины (вопрос в том, сколько пройдет времени пока основная масса пользователей сменит материнские платы и видеокарты), с другой стороны - непрерывно растущая мощность новых видеокарт (которые со временем будут только дешеветь).
Тем не менее есть и альтернативный метод - "эмуляция" точечных спрайтов стандартными средствами. Давайте рассмотрим и этот подход.

В основе лежит та же идея - уменьшить количество передаваемых данных, передавая только координаты центра спрайта. К сожалению, превратить одну вершину в 4, без использования геометрического шейдера, у нас не получится, потому придется задавать все 4 вершины. Нетерпеливые спросят - а где же тут экономия? Экономия в том, что мы будем обновлять не координаты этих 4-х вершин, а координаты в отдельном вершинном атрибуте. Таким образом мы получим примерно такую структуру данных:
Type

  TSpriteQuad = record
    v1,v2,v3,v4: record x,y: integer; end;;
  end;
var
  SpriteQuads: array of TSpriteQuad;
  SpritePositions: array of TAffineVector
  SpriteSizes: array of single;
  SpriteRot: array of record sina,cosa: single; end;
Для обновление положения необходимо обновить только массив "SpritePositions".
Я специально разделил параметры спрайта, так не все параметры могут обновляться одновременно, это позволит сократить объем обновляемых данных, к тому же не все из этих параметров могут быть задействованы (или добавятся еще некоторые).
Массив SpriteQuads я отправлю в gl_Vertex, а остальные атрибуты помещу в пользовательские Rot, Sizes и Positions.


Координаты вершин мы записываем в такой форме:
v1 = (-1,-1); v2 = (-1,1); v3 = (1,1); v4 = (1,-1);
Эти же координаты будут по совместительству и текстурными координатами квада.
В шейдере, получив эти координаты через gl_Vertex мы напрямую передаем их в фрагментный шейдер (при необходимости корректируем их для текстурного атласа).

Дальше нам останется лишь перевести эти текстурные координаты в координаты вершин квада, для этого достаточно умножить их на половину размера квада (если мы задаем координаты центра квада). При желании тут же можно их повернуть и отамсштабировать, используя переданные нами синус и косинус угла (ScaleRots).

Теперь нам нужно вычислить реальные трехмерные координаты этих вершин, исходя из условия что полученный квад должен смотреть на камеру. Для этого, в центре квада (Positions) мы получаем ортогональную систему векторов (vNormal, vRight, vUp) направленную на камеру (campos). Разложив координаты вершин (Vert) по векторам vRight/vUp и прибавив к ним координаты центра квада мы получим результирующие координаты, которые нужно перевести в ClipSpace посредством MVP-матрицы. Ниже приведен пример этого шейдера:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
uniform vec4 campos;
varying vec2 TexCoord;
attribute vec3 Positions;
attribute vec2 Rots;

attribute float Sizes;

void main()
{
  TexCoord.xy = (gl_Vertex.xy*0.5+0.5);
  vec2 tc = gl_Vertex.xy*Sizes; //Задаем размеры квада
  vec4 Vert; //Вращаем квад
  Vert.x = tc.x*Rot.y+tc.y*Rots.x;
  Vert.y = -tc.x*Rot.x+tc.y*Rots.y;
  vec3 vNormal = normalize( campos.xyz - Positions);
  vec3 vRight = normalize(cross( vec3( 0.0,1.0,0.0 ), vNormal ));
  vec3 vUp = normalize(cross( vNormal, vRight));
  vec3 resultCoord = Positions + (( vUp*Vert.x + vRight*Vert.y)) ;
  gl_Position = ( gl_ModelViewProjectionMatrix * vec4(resultCoord,1.0) );
}

Фрагментный шейдер тут обычный.

При таком подходе мы получаем большую гибкость по управлению отображением спрайта относительно точечных спрайтов. С производительностью тут не все так однозначно, с одной стороны - данный подход существенно проигрывает обычным точечным спрайтам (без использования шейдера). Если же реализуется использование текстурного атласа, смена размера да еще и вращение, то тут уже производительность сравнивается и даже превосходит по производительности точечные спрайты (зависит от количества растеризируемых пикселей спрайта), так как для точечных спрайтов все эти операции выполняются для каждого выводимого пикселя, тогда как в данном подходе трансформируются только 4 вершины.Так же у точечных спрайтов есть небольшой выигрыш в объеме передаваемых данных (атрибуты одной вершины вместо 4-х).


С появлением геометрических шейдеров появился еще один способ решения данной задачи, а именно - создание ориентированных полигонов из одной точки прямо в геометрическом шейдере. Таким образом мы сочетаем эффективность точечных спрайтов и гибкость GPU-квадов.

Оценить производительность каждого из подходов можно в этой демке:
http://www.codesampler.com/source/ogl_optimized_billboards.zip
На Рис.2. пример вывода 10000 частиц в VBOMesh на основе точечных спрайтов. Частота обновления геометрии примерно 17 раз в секунду. Частицы движутся под действием силы тяжести, начального ускорения и действующей объемной силы (ветра). 

Рис.2. Вывод 10000 снежинок менеджером частиц VBOMesh
Использование точечных спрайтов в VBOMesh обусловлено желанием избежать лишнего использования шейдеров и проблем с их каскадированием. Тем не менее, движок позволяет штатными средствами реализовать и второй подход. Пример управления свойствами частиц на основе точечных спрайтов (размер, ориентация, атлас) идет в комплекте демок VBOMesh:
Рис.3. Использование текстурного атласа и переменного размера частиц в VBOMesh

В будущем запланирована существенная доработка менеджера эффектов частиц (возможно туда же будет включен и второй способ).

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

Комментариев нет:

Отправить комментарий