Publish @Zeux: GDC report – Particle Shadows and Cache-Efficient Post-Processing – Louis Bavoil (NVIDIA)

Продолжаем публиковать записи с GDC. Доклады на GDC бывают нескольких разных типов, эти вот – разряда “вендоры железа и держатели платформ рассказывают разработчикам про практики”. Вендорам железа хочется, что разработчики делали хайтек и больше использовали новых фич и новых мощностей, они про это и рассказывают.


People in this conversation:
Arseny Kapoulkine (Zeux) Add
CREAT Studios, Saber3D, Sperasoft (EA Sports – FIFA). Currently making kids happy at ROBLOX. http://zeuxcg.org/
Sergey Gonchar Add
Graphics Programmer (Flash Stage3D, interested in Direct3D11)
Yury Degtyarev Add
Graphics programmer at Transas New Technologies

Zeux: Particle Shadows and Cache-Efficient Post-Processing – Louis Bavoil (NVIDIA), часть первая
Поехали дальше, тут был доклад два-в-одном про две совсем не связанные техники, я напишу отдельно.

Задача значит – есть частицы, надо от них отбросить тень с учетом прозрачности.
Частицы – билборды, у каждой из них есть альфа, считаем что нам хочется затенить фактором (1-a0)(1-a1)…(1-an) если луч источника света проходит через N частиц с указанной альфой.

Простая техника – давайте отрендерим все частицы с правильно настроенным мультипликативным блендингом с той же камеры что и shadow map.
Референс на Крайзис 2, видимо там так.
Все хорошо на тенях от партиклов на земле – но партиклы не получают самозатенение
Ну и объект в большом дымовом шлейфе тоже получает одно и то же затенение вне зависимости от положения объекта, тоже фигня.

Были даны ссылки на 5-10 работ про такие тени, и было сказано что елинственная работа которая была реализована в вышедшей игре – Fourier Shadow Mapping
Но там надо много каналов, а в D3D11 ограничение на 8 MRT, 4 канала на MRT – 32 коэффициента в разложении, типа мало.

Мужики подумали и придумали воксели.

Берется воксельная решетка (3д текстура), ориентируется так же как shadow map, Z-слои решетки соответствуют глубине с т.з. источника света.
В каждый воксель будем писать 8-битный фактор освещенности (то самое произведение формулой выше) в заданной точке пространства.
Заполняем текстуру в три приема:
1. Очищаем текстуру в 1.0 (нет затененности нигде)
2. Каждый партикл рендерим в ту 3д текстуру, как и в наивном решении с 2д текстурой выводим альфу и делаем мультипликативный блендинг.
Чтобы рендерить в 3д текстуру, используем view на 3д текстуре который каждый slice представляет как slice в texture array, и используем GS с render target array index зависящим от глубины
Это стандартная техника рендера в 3д текстуру на DX11.
3. Теперь у нас в ячейках, в которых были партиклы, есть значения – надо сделать propagate по направлению источника света. Для этого запускается CS на 2д решетке NxN
Каждый запуск CS проходит по всем слайсам текстуры с текущим индексом и тупо сбленживает текущее значение с накопленным.
После вышеперечисленного у нас есть 3d текстура, используем ее просто – при shadow lookup считаем глубину с т.з. источника света, и делаем lookup в эту текстуру с правильным Z значением, получаем затененность от частиц.

Вся эта радость на 8к частиц среднего размера на 256^3 решетке занимает 0.57 ms на какой-то хорошей карточке от NV
0.23 на растеризацию частиц и 0.33 на propagation.
Пацаны пытались ускорить propagation делая early out специальным методом, у них плохо получилось.

Implementation detail которая не знаю зачем – текстура не формата R8, а формата RGBA8888, в одном пикселе данные сразу про 4
Засчет этого не работает трилинейная фильтрация при семплинге, приходится ее эмулировать двумя билинейными.

Была показана демка которая будет в SDK, и отдельная демка с большим кораблем – в демке с кораблем 3д текстура вроде занимала 256 мегабайт
Но вообще понятно чем больше 3д текстура тем более детальные тени от частиц.
Последний слой этой 3д текстуры в точности совпадает с 2д текстурой из наивного подхода

Еще был упомянут трюк про шейдинг самих частиц, который уже был освещен ранее на всяких презентациях – вместо того чтобы при шейдинге частиц в PS семплить shadow maps, можно с помощью тесселяции гарантировать не более чем константный размер треугольников на экране для частиц
И семплить тени в domain shader
Тюнинг фактора тесселяции дает перебалансировку между качеством освещения на частицах и скоростью.
Технику про 3д текстуру гордо окрестили Particle Shadow Maps
На что я могу сказать только friends don’t let friends implement PSM.

Yury Degtyarev: Спасибо за рассказ!
А вот почему 32 мало? у меня хватало 2 RGBA текстуры, не за глаза конечно, но для локальных частиц было ок. Неужели их 256^3 даст лучше качество?

Zeux: Ну очень альясинг заметен при анимации частиц на разрешении в 32 при разумном объеме частиц.
А, сорри, ты про FOM
Свечку не держал, сказали – им кажется что мало.
Ну т.е. как я понял идея была в том что при большом количестве памяти
Метод с 3д текстурой скейлится в качестве пока память не закончится
А FOM типа ограничен пределами API
Наверное это не правда если партиклы в несколько проходов рендерить :) или еще что-то придумать в таком роде.

Yury Degtyarev: могу сказать, что его реально мало, если нужны освещенные частицы по FOM в радиусе > 100х100 метров (т.е. каждая на каждую отбрасывает тень) Если эффекты локальны и сильно разбросаны, получаем сильную потерю точности при реконструкции. Однако, если разбить все это на per effect, то вполне неплохо работает.

Yury Degtyarev: и что, собственно, мужики придумали? Это же Opacity Shadow Maps, пусть и на другой аппаратной реализации

Zeux: Ты наверное прав.
Претензии к авторам!

Yury Degtyarev: Т.е. Fourier Opacity Mapping для того и придуман был, чтобы не хранить слайсы (и воксели), т.к. transmittance function довольно плавно меняется.
Я бы понял, если бы воксельный грид был бы не ориентирован по одному источнику, так хоть можно для нескольких источников его использовать, заполнив density один раз

Sergey Gonchar: интересно будет скриншотик глянуть, сложилось впечатление что это все дело нужно как-то размывать потом, чтобы нормально смотрелось
спасибо! очень интересно!

Zeux: Оно очень low frequency
Типично альясит сильно
У них в демке 256х256 не альясит но там маленькая сцена :)
Ну короче стандартная проблема shadow maps.
Если хочется бороться с альясингом – можно размывать, конечно.


Zeux: Particle Shadows and Cache-Efficient Post-Processing – Louis Bavoil (NVIDIA), часть вторая

Во второй части доклада была общая техника про оптимизацию определенного класса пост-эффектов, которые TEX-bound а не ALU bound, на примере SSAO – было сказано что screen-space reflections можно оптимизировать так же.
Постановка задачи следующая – есть эффект с большим sparse kernel-ом, например SSAO.
Чтобы посчитать SSAO мы делаем например 16 выборок, с радиусом например 256 пикселей.
Плюс, чтобы уменьшить banding и улучшить качество после blur-а, мы делаем псевдо-рандомизированную выборку
Т.е. jittered rotation например в соседних пикселях, с решеткой jitter-инга например 4х4 пикселя.
Это более или менее каноническая реализация.

Проблема заключается в том, что комбинация этих трех факторов (фильтр большого радиуса, мало семплов (sparse) и per-pixel jitter) приводит к очень неэффективным с точки зрения текстурного кеша выборкам.
Условно, никакой локальности в положениях семплов между соседними пикселями не наблюдается.

Предлагается использовать следующий факт…
Поскольку у нас решетка jitter маленькая и регулярная (4х4 пикселя – типично есть маленькая текстура которая мапится на весь экран с повторениями, чтобы этого достичь)
То локальность семплирования будет наблюдаться между пикселями экрана, попавшими в один и тот же пиксель jitter текстуры
Т.е. условно между пикселями с позициями, остаток от деления которых на 4 совпадает.
У нас получается 16 групп пикселей, каждая группа – это регулярная решетка каждые 4 пикселя по обеим осям
В пределах каждой группы jitter значение одно и то же, поэтому выборки локальны
Потому что координаты выборки с фиксированным jitter value это обычно координаты текущего пикселя + константное смещение.

Говорится следующее – давайте из одной текстуры размером 4N x 4M сделаем 16 текстурок размерами NxM, это deinterleaving pass
Его можно сделать в пару проходов, если использовать MRT (засчет ограничения в 8 для 4х4 решетки придется использовать 2 прохода)
Теперь в каждой текстуре у нас собрались пиксели из одной группы jitter – давайте к каждой текстуре отдельно применим наш пост-фильтр (SSAO)
Причем читать мы будем, опять же, только из нее
И с подкорректированными если надо смещениями при семплинге (текстура в 4х4 раза меньше)
То что мы получили – это тоже sparse convolution kernel, он отличается от изначального тем
Что каждый семпла теперь читает не из произвольного текселя, а только из текселя из той же группы (те же остатки от деления на 4 x/y)
Т.е. каждый семпл сдвинулся в худшем случае на 3 пикселя
С учетом того что семплы все равно далеко друг от друга, и псевдослучайны, нам пофиг на это.

Итак, сделали 16 текстурок с исходными данными, потом на каждой применили SSAO – теперь последним проходом (interleave) восстанавливаем исходный порядок, склеивая 16 текстур обратно в одну.
Чтобы это работало эффективно, мы эти текстурки положим в texture array.
Мы делаем два дополнительных прохода (deinterleave & interleave), но у них регулярный паттерн чтения-записи в память -
сам по себе фильтр мы делаем на меньшей по размеру текстуре (лучше работает кеш), и между соседними пикселями положения семплов отличаются на 1 тексель (очень хорошо работает кеш)

В итоге получаем гораздо лучше pattern доступа к памяти. В реальности это все выливается примерно в двухкратный прирост производительности на SSAO – там что-то в стиле 3.6 ms -> 1.7 ms, из тех 1.7 0.2 ms – оверхед на interleave/deinterleave
Так, с этим все, затем был lunch break! Перерыв :)