Arseny Kapoulkine:
Advanced Visual Effects with DirectX 11: Compute-Based GPU Particle Systems.
Speaker: Gareth Thomas (AMD)
Будем сокращать время на рассказ про каждую следующую презентацию!
Итак, есть партикл системы, хочется их делать на GPU.
Такое было уже десять тысяч раз, но в конце есть интересное.
Сначала мы кладем все партиклы в буфер, и с помощью compute shader обновляем их стейт.
Обновлять стейт просто, надо еще уметь убивать партиклы - кажется для этого мы живые партиклы копируем в другой буфер (я этот момент пропустил)
Еще было бы прикольно обновлять стейт не просто интеграцией всяких параметров, а еще и делать коллизии.
Можно делать коллизии на GPU с геометрией, можно с картой высот, можно с воксельной сеткой.
Но мы делаем демку и у нас нет ничего, поэтому давайте коллайдиться с depth buffer-ом.
Возьмем новое положение партикла, спроецируем в screen space, сравним глубину, готово.
Факт коллизии обнаружен, надо посчитать collision response - для этого нужна нормаль.
Если у вас deferred shading то нормаль можно прочитать оттуда же откуда и глубину.
Если случайно нет то ее можно вычислить по градиентам глубины в пикселе.
Теперь партиклы надо посортировать, ничего лучше bitonic sort видимо все еще не придумали - поэтому будем делать bitonic sort (детали в гугле итп, bitonic sort на GPU используют давно и продуктивно)
Теперь партиклы надо отрендерить, можно использовать GS (point -> quad amplification), можно использовать инстансинг (http://zeuxcg.org/2007/09/22/particle-rendering-revisited/)
А можно сделать вид что у нас 2014 и реализовать инстансинг самим с использованием SV_VertexID.
Т.е. делаем фейковый draw call без вершинных данных, в VS берем SV_VertexID, делим на 4 - получаем индекс партикла в буфере (идея такая же как в статье по ссылке выше)
Так, скучная лажа закончилась, теперь менее скучная лажа.
Вот есть у нас мощный дым на 10 тысяч партиклов -- и в зависимости от того где камера у нас может быть очень большой overdraw партиклов.
Типично если нам не повезло и ROP оптимизации не справляются, это означает что мы много-много раз читаем какие-то данные из памяти, блендим их с цветом очередного пикселя партикла и пишем обратно.
Например может быть что с одного ракурса туман занимает 5 ms а с другого - 25.
Есть одна эффективная оптимизация - рендерить партиклы в половинное или четвертинное разрешение.
И потом bilateral upscale (граница между партиклами и геометрией выглядит паршиво)
Ну и одна не очень эффективная - можно рендерить геометрию меньшей площади (http://www.humus.name/index.php?ID=266)
Все это клево но хочется быстрее.
Давайте вспомним что была такая штука как tile-based deferred shading.
Которая решала ровно ту проблему какая у нас есть - проблему bandwidth.
Решала так - давайте разобьем экран на тайлы, в каждом тайле посчитаем список пересекающих его источников света.
А потом запустим шейдер который для каждого пикселя тайла прочитает все параметры gbuffer, для каждого источника света посчитает его вклад, и потом все это соберет в финальный результат.
Все эти вычисления в регистрах, регистры быстрые, память медленная => всем хорошо.
Окей, давайте делать так же!
Традиционно куллинг лайтов делается брутфорсом - для каждого тайла куллятся все лайты. Лайтов у нас были сотни, частиц - десятки тысяч, поэтому делаем двухуровневый куллинг:
  • 1. Бьем экран на 8х8 тайлов, для каждого тайла и для каждого партикла выясняем, он попал в тайл или нет.
  • 2. Бьем экран на тайлы размером 32х32 пикселя, для каждого тайла и каждого партикла из более крупного тайла делаем куллинг еще раз.
И то и то делаем в compute shader, это уже традиционный для deferred shading метод, читать статьи по tiled deferred shading или tiled forward rendering до просветления.
В итоге в каждом мелком тайле 32х32 пикселя мы получили список партиклов, которые на него влияют.
Теперь давайте запустим compute шейдер на каждый пиксель экрана, и в нем циклом сблендим все партиклы между собой в правильном порядке.
Тут есть загвоздка - мы сортировали все партиклы между собой, но порядок партиклов в каждом тайле типично испортился (список партиклов в каждом тайле больше не отсортирован back-to-front)
Потому что куллинг был параллельным, и мы использовали per-pixel linked list операции, которые не гарантируют порядок результата.
Отлично, давайте уберем шаг сортировки всех партиклов, и будем сортировать список партиклов в тайле после того как классификация партиклов по тайлам завершена.
Это будет еще и быстрее потому что быстрее сортировать много небольших массивов чем один большой.
Ну вот, и теперь у нас есть цикл который для каждого партикла считает его вклад (семплит текстуру итп), и блендит его.
Это просто цикл в шейдере, работающий с регистрами - т.е. мы не тратим memory bandwidth на блендинг.
В качестве последнего гвоздя давайте вместо того чтобы блендить back to front, блендить front to back -- традиционной бленд формулой такое не записать, но говорят в шейдере можно изменить математику так чтобы было неважно, в каком порядке смешивать.
Цимес в том что если мы уже наблендили front-to-back партиклов с суммарной альфой около 1, то дальше можно не блендить.
Т.е. даже если у нас overdraw 100x, есть шанс что мы сблендим первые 5-6 партиклов и успокоимся.
В результате есть демка, в которой можно камерой не подлетать близко к дыму (тогда forward rendering дает 4.86 ms, из которых 0.4 ms потрачено на симуляцию; deferred rendering дает 3.15 ms), а можно подлететь (forward rendering начинает занимать 25 ms, deferred проседает только до 5.1 ms)
Интересное наблюдение - даже если делать half-res буфер, в forward будет в лучшем случае около 7 ms, при том что качество хуже.
Занавес.
Denis Sladkov:
Спасибо! Про патиклы интересно!
от себя могу добавить про интересную идею дешевой OIT: http://jcgt.org/published/0002/02/09/
Arseny Kapoulkine:
Ага, у этой техники проблемы с большим количеством разнородных близких к непрозрачному слоев (т.е. зеленые и красные партиклы дыма вперемешку), но для обычных частиц может и хорошо работает.
Arseny Kapoulkine:
Ой, забавно. Вот я тут писал "делаем демку" - а в Assassin Creed 4 этот метод прямо в продакшене.
Симулируют коллизии партиклов дождя с помощью depth buffer, см. дополнительные слайды в презентации: http://bartwronski.files.wordpress.com/2014/03/ac4_gdc_notes.pdf
Yury Degtyarev:
так уже в давно в Halo делали.
Yury Degtyarev:
хмм, странно, что они не упомянули radix sort на GPU, мне кажется он побыстрее будет. Тем более, он уже реализован во многих библиотеках для GPU.
Arseny Kapoulkine:
Я всегда считал что гистограммы считать на GPU очень печально.
Yury Degtyarev:
в алгоритме число бинов мало, поэтому можно применить параллельную префиксную сумму (т.е. без атомарных операций фактически)
Arseny Kapoulkine:
Подожди, префиксная сумма - она после гистограмм в radix sort.
И почему число бинов мало?
Ну точнее что такое мало :)
Yury Degtyarev:
необязательно. Делая первый scan в блоке ты уже можешь заодно посчитать локальную гистограмму.
Yury Degtyarev:
16 бинов, векторы для scan будут закодированны в 4 инта для блока в 256 элементов.
Arseny Kapoulkine:
Я решительно не понимаю, о каком алгоритме ты говоришь. Это не radix sort а какой-нибудь radix sort блоками плюс потом merge?
16 бинов на 256 элементов тоже не понимаю, 2 прохода по 16 бинов?
Yury Degtyarev:
я говорил про тот, который обычно реализован во всяких GPU библиотеках, а-ля B40C.
Yury Degtyarev:
имелось ввиду, что сортируем блоками, например, по 4-бита. Это даст нам 16 возможных вариантов для 4-битных чисел, которые мы сортируем, отсюда 16 бинов. Про 256 это я скорее сказал к теме локальной гистограммы, т.к. для 256 элементов мы можем её получить сканом этих 16-мерных векторов отобразив исходные числа в 16 байт = 4 инта, заодно скан дает локальную позицию в отсортированном массиве. Тут объяснение смешано с делалями, поэтому лучше оригинал почитать :)
Arseny Kapoulkine:
Что-то мне неочевидно что мало бинов это радостно.
Обычно мало бинов = много проходов и много contention за size buckets.
Есть референсный пейпр?
Yury Degtyarev:
Yury Degtyarev:
ну вот для линейного radix'a будет как раз наоборот, но это детали.
Yury Degtyarev:
был такой пейпер на похожую тему (Front-to-Back Blending with Early Fragment Discarding), интересно было бы сравнить.