Не хватает видео результата. На картинке данная трава выглядит устрашающе. Так и кажется, что вот сейчас проткнет и начнет прорастать сквозь тебя. Ступить на такую я бы не решился.

Добавил видео в конец статьи.
НЛО прилетело и оставило эту надпись здесь.
Спасибо за статью. Тоже планирую активно грузить подобными задачами видеокарту.
Вы случайно не сравнивали на сколько падает производительность по сравнению с рендерингом такой же но готовой геометрии? Какой оверхед от генерации всего этого дела на лету?

P.S. Кто-нибудь вообще может мне объяснить зачем вершинный шейдер сделан в конвеере рендерига самым первым? На мой взгляд — это только мешает, так как не даёт полноценно переиспользовать вершинный код для расчёта материалов. Лучше бы он был после геометрического шейдера, или хотя бы настраиваемым.
Нет, пока не сравнивал. Если будет время, планирую сравнить как минимум с двумя вариантами: полностью готовая геометрия и готовые кости + геометрический шейдер.

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

Существующий порядок шейдеров мне тоже казался вывернутым наизнанку. Но наверно логика здесь примерно такая: в геометрическом шейдере можно сделать всё тоже самое, что и в вершинном, и даже ещё больше. Если нужна повершинная обработка сгенерированной геометрии, то её всегда можно написать перед EmitVertex().
Кто-нибудь вообще может мне объяснить зачем вершинный шейдер сделан в конвеере рендерига самым первым?

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


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

Но как отсечь точку в вершинном шейдере? Даже если вы не запишите ничего в gl_Position, точка всё равно пойдёт дальше по конвейеру со значениями по умолчанию, то есть (0, 0, 0, 0). Вызов discard() в вершинном шейдере отсутствует, он есть только во фрагментном, экономия на котором получится «автоматически» для невидимых (и даже просто для загороженных) биллбордов. В итоге, все «отсечённые» биллборды будут генерироваться в начале координат.

Можно конечно принять решение об отмене генерации в вершинном шейдере, и передать его в геометрический через in/out. Но то же самое можно сделать непосредственно в геометрическом шейдере без передачи переменной между стадиями.
Но как отсечь точку в вершинном шейдере?

Зачем? В вершином шейдере ничего не отсекается, вершинный шейдер предоставляет данные для отсечения, которое происходит во время сборки примитивов. Или миллион точек или 2 миллиона треугольниов.


во фрагментном, экономия на котором получится «автоматически» для невидимых

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

В вершином шейдере ничего не отсекается, вершинный шейдер предоставляет данные для отсечения, которое происходит во время сборки примитивов.

Но ведь ничто принципиально не мешает вычислить те же данные для отсечения в геометрическом шейдере? Позвольте проиллюстрировать кодом. Правильно ли я понимаю, что вы имеете ввдиду что-то подобное?
// Вершинный:
layout(location=0) in vec4 position;
out bool visible;
void main(void) {
    visible = point_is_visible(position);
    gl_Position = billboard_position_transformations(position);
    // или просто gl_Position = position;
}

// Геометрический:
layout(points) in;
layout(triangle_strip,) out;
in bool visible[];
void main() {
    if(visible[0]) {
        // Генерируем два треугольника.
    }
}


Я же предлагаю такое:
// Вершинный:
layout(location=0) in vec4 position;
void main(void) {
    gl_Position = position;
}

// Геометрический:
layout(points) in;
layout(triangle_strip,) out;
void main() {
     vec4 position = billboard_position_transformations(gl_in[0].gl_Position);
    // опять же, функции billboard_position_transformations может не быть.
    if(point_is_visible(position)) {
        // Генерируем два треугольника.
    }
}


Эти два куска кода выполняют одно и то же: вычисляют point_is_visible для каждой точки (миллион точек), и генерируют два треугольника, если 999999 не видны. То есть в геометрическом шейдере делается всё то же, что может делаться в вершинном. Атрибуты и юниформы, используемые в point_is_visible можно сделать доступными на любой их этих стадий. Второй пример кода как раз похож на строки из демки:
float halfspaceCull =
        step(dot(eyePosition - gl_in[gl_InvocationID].gl_Position.xyz, lookDirection), 0);
gl_TessLevelOuter[1] = lod() * halfspaceCull;

Только здесь TCS вместо геометрического. Он освобождает генератор абстрактных патчей и TES от генерации геометрии, если точка позади камеры.

Отсечение и перспективное разделение происходят до фрагментного шейдера

Так я ровно об этом и говорил. То есть не будут обрабатываться фрагменты полигонов трёх больших групп примитивов:
  1. Если примитив вообще не был выпущен геометрическим шейдером, то есть когда point_is_visible(position) == true. Путь входной точки на этом заканчивается.
  2. Если из точки по какой-то причине были сгенерированы два треугольника, но они не попали в видимую область из-за проективного преобразования.
  3. Если примитив в области видимости загорожен другим примитивом. Проверка z-буфера тоже происходит перед фрагментным шейдером.

Мне всего лишь хотелось проиллюстрировать, что когда мы говорим о генерации геометрии, нет смысла говорить о явном discard() во фрагментом шейдере.
Правильно ли я понимаю, что вы имеете ввдиду что-то подобное?

Нет, я имею ввиду стандартный этап графического конвеера под названием Primitive Assembly (сборка примитивов).
Он выполняется до тесcеляции и геометрического шейдера. На этом этапе формируются примитивы с учетом отсечения (полупрастранствами или лицевыми гранями), могут также добавляться дополнительные вершины. Входными данными для этого этапа служат данные выпушенные вершинным шейдером.


Если у меня миллион частиц, а я вижу только одну, в геометрический шейдер попадет только одна точка. Все остальные отбросятся на этапе сборки примитивов и я постою только 2 треугольника. Или я не прав?

Он выполняется до тесcеляции и геометрического шейдера.

Primitive assembly бывает разным и в нескольких местах. После вершинного шейдера действительно есть сборка примитивов, но она собирала линии и треугольники только если за ней больше нет преобразований геометрии. Как сборка примитивов перед тесселяцией и геометрическим шейдером может объявить примитив невидимым? Ведь эти стадии могут перепахать геометрию до неузнаваемости и сделать видимым то, что было якобы невидимым после вершинного шейдера. А как интерпретировать примитив, у которого после стадий VertexShader->PrimitiveAssembly 32 вешины, а не 2 или 3? А ведь такое вполне может быть при включенной тесселяции.
При включенной тесселяции вершинный шейдер обрабатывает абстрактные наборы атрибутов, которые называются «вершинами» только условно. То есть они совсем не обязаны отражать положение конечной геометрии. Например, программа вообще может подать в конвейер набор одномерных нулей. Эти нули нужны только для того, чтобы конвейер запустился. Тогда вершинный или какой нибудь из последующих шейдеров может достать данные о геометрии из юниформов или из текстуры. Или же вообще сгенерировать их процедурно псевдослучайной функцией.
Ну или другой экстремальный вариант: рисуем ландшафт и подаём на конвейер по набор вершин с единственным атрибутом на каждый тайл. И этот атрибут обозначает цвет, не неся никакой информации о положении тайла в пространстве и его форме. Высоту и/или искривления тайлов шейдеры теселяции/геометрии возьмут из текстуры, а пространственное положение высчитают из встроенной переменной gl_PrimitiveID.

Если у меня миллион частиц, а я вижу только одну, в геометрический шейдер попадет только одна точка.

Почему? Отсечение плоскостями должно происходить после проективного преобразования. Иначе откуда конвейер узнает какова матрица проекции? Но если мы применили, например, перспективную проекцию к условному скелету до геометрического шейдера, то как нарастить на него полигональное мясо в геометрическом шейдере? Это ж будет адова некрасивая математика с вычислительным оверхедом. Куда удобнее иметь дело с геометрией в системе координат модели, и только перед самым EmitVertex() умножать её на мировую матрицу (перенос, поворот) и на проективную.
Такой способ как раз отражает последняя диаграмма на странице 8 официального референса: отсечение и перспективное деление происходят после геометрического шейдера, последующей сборки примитивов (они на это стадии могут быть только точками, line_strip или triangle_strip) и feedback transform.

Согласен, вы правы. Мне почему-то казалось что можно разгрузить немного gs.

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

Спасибо!

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

Хорошая идея. А стебли с невыпуклым четырёхугольником в сечении были бы похожи на алоэ или на побеги чеснока. Как вариант, можно передавать сечение стеблей в атрибутах вершин и получать более разнообразную растительность за приемлемый расход памяти.
Только полноправные пользователи могут оставлять комментарии.
Войдите, пожалуйста.