GPU Lightmapper: Техническое погружение

FLORENT GUINIER / UNITY TECHNOLOGIESContributor
May 20, 2019|15 Мин
GPU Lightmapper: Техническое погружение
Эта веб-страница была переведена с помощью машинного перевода для вашего удобства. Мы не можем гарантировать точность или надежность переведенного контента. Если у вас есть вопросы о точности переведенного контента, обращайтесь к официальной английской версии веб-страницы.

Команда Lighting Team стремится к увеличению скорости итераций. Мы разработали Progressive Lightmapper именно с этой целью. Наша цель - обеспечить быструю обратную связь по любым изменениям, которые вы вносите в освещение в вашем проекте. В 2018.3 мы представили предварительную версию GPU-версии Progressive Lightmapper. Теперь мы движемся к тому, чтобы сравняться по характеристикам и визуальному качеству с его процессорным собратом. Мы стремимся к тому, чтобы версия для GPU была на порядок быстрее версии для CPU. Это привносит интерактивный лайтмэппинг в художественные рабочие процессы, значительно повышая продуктивность работы команды.

Исходя из этого, мы решили использовать RadeonRays: библиотеку трассировки лучей с открытым исходным кодом от AMD. Unity и AMD совместно разработали GPU Lightmapper для реализации нескольких ключевых функций и оптимизаций. А именно: выборка мощности, уплотнение лучей и пользовательский обход BVH.

Цель разработки GPU Lightmapper заключалась в том, чтобы предложить те же возможности, что и CPU Lightmapper, но при этом добиться более высокой производительности:

  • Беспристрастный интерактивный лайтмэппинг
  • Паритет характеристик между бэкэндами CPU и GPU
  • Решение на основе вычислений
  • Отслеживание траектории волнового фронта для максимальной производительности

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

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

Прогрессивная обратная связь

Чтобы Lightmapper предлагал пользователю прогрессивные обновления, нам пришлось принять ряд дизайнерских решений.

Никаких предварительно вычисленных или кэшированных данных

Мы не кэшируем освещенность и видимость при прямом освещении (прямое освещение может быть кэшировано и повторно использовано для косвенного освещения). В общем, мы не кэшируем данные и предпочитаем вычисления, которые достаточно малы, чтобы не создавать задержек, и обеспечивают прогрессивное и интерактивное отображение во время выпечки.

Изображение

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

Тексель считается видимым, если он находится в текущем фрусте камеры и если он не закрыт никакими статическими геометриями Сцены.

Мы выполняем эту очистку на GPU (чтобы использовать преимущества быстрой трассировки лучей). Вот последовательность работы по выбраковке.

Изображение

Задания по выбраковке имеют два выхода:

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

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

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

Производительность и эффективность

Графические процессоры оптимизированы для получения огромных массивов данных и выполнения одной и той же операции над всеми ними; они оптимизированы для пропускной способности. Более того, GPU достигает такого ускорения, будучи более энерго- и экономичным, чем многоядерный CPU. Однако GPU не так хороши, как CPU, с точки зрения задержки (намеренно, в силу конструкции аппаратного обеспечения). Именно поэтому мы используем конвейер , управляемый данными, без точек синхронизации CPU-GPU, чтобы максимально использовать параллельные вычисления, присущие GPU.

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

Трубопровод, управляемый данными

Графические процессоры предназначены для работы с большими массивами данных и способны обеспечить высокую пропускную способность за счет задержек. Кроме того, они обычно управляются очередью команд, заранее заполненной процессором. Цель этого непрерывного потока больших команд - насытить GPU работой. Давайте рассмотрим ключевые рецепты, которые мы используем для максимизации пропускной способности и, соответственно, производительности.

Наш трубопровод

Наш подход к конвейеру данных для лайтмэппинга на GPU основан на следующих принципах:

1. Мы готовим данные один раз.

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

2. После начала запекания не допускается синхронизация CPU-GPU.

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

3. GPU не может порождать лучи и ядра.

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

4. Нам не нужны ни точки синхронизации CPU-GPU, ни какие-либо "пузыри" GPU после начала запекания.

Например, некоторые команды OpenCL могут создавать небольшие "пузыри" GPU (то есть моменты, когда GPU нечего обрабатывать), такие как clEnqueueFillBuffer или clEnqueueReadBuffer (даже в асинхронных версиях), поэтому мы избегаем их по мере возможности. Кроме того, обработка данных должна оставаться на GPU как можно дольше (например, рендеринг и композитинг вплоть до завершения). Когда нам нужно вернуть данные в CPU для дополнительной обработки, мы сделаем это асинхронно и не будем снова отправлять их в GPU. Например, сшивание швов - это постобработка процессора на данный момент.

5. CPU будет асинхронно адаптировать нагрузку на GPU.

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

Изображение
Размер задания для GPU

Одной из ключевых особенностей архитектуры GPU является широкая поддержка SIMD-инструкций. SIMD расшифровывается как Single Instruction Multiple Data. Набор инструкций будет последовательно выполняться на заданном объеме данных внутри так называемого варпа/волнового фронта. Размер этих волновых фронтов/карликов составляет 64, 32 или 16 значений (в зависимости от архитектуры GPU). Поэтому одна инструкция будет применять одно и то же преобразование к нескольким данным - одна инструкция - несколько данных. Однако для большей гибкости GPU также способен поддерживать расходящиеся пути кода в своей SIMD-реализации. Для этого он может отключить некоторые потоки, работая над подмножеством, прежде чем снова подключиться к ним. Это называется SIMT: Однопоточная многопоточная работа. Однако за это приходится платить, так как расходящиеся пути кода в пределах волнового фронта/волны будут использовать только часть блока SIMD. Прочитайте эту замечательную статью в блоге, чтобы узнать больше.

Наконец, прекрасным продолжением идеи SIMT является способность GPU поддерживать множество искривлений/волновых фронтов на одно SIMD-ядро. Если волновой фронт/волна ожидает медленного доступа к памяти, планировщик может переключиться на другой волновой фронт/волну и продолжить работу над ним в это время (при условии, что есть достаточно ожидающих работ). Однако для того, чтобы это действительно работало, количество ресурсов, необходимых для одного контекста, должно быть низким, чтобы занятость (количество незавершенных работ) была высокой.

Подводя итог, мы должны стремиться к этому:

  • Много нитей в полете
  • Избегание расходящихся ветвей
  • Хорошая заполняемость

Хорошая заполняемость - это все о коде ядра, и это слишком широкая тема, чтобы быть частью этой статьи. Вот несколько замечательных ресурсов:

В целом, цель состоит в том, чтобы редко использовать локальные ресурсы, особенно векторные регистры и локальную общую память.

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

Изображение

Здесь есть несколько проблем:

  • Заполненность лайтмапа в этом примере составляет 44 % (4 занятых текселя на 9), поэтому только 44 % потоков GPU будут производить полезную работу! Кроме того, полезные данные занимают мало места в памяти, поэтому мы будем платить за пропускную способность даже за незанятые тексели. На практике занятость лайтмапа обычно составляет от 50 до 70 %, что дает огромный потенциальный выигрыш.
  • Набор данных слишком мал. Для простоты в примере показан световой образ 3x3, но даже обычный случай светового образа 512x512 будет слишком маленьким набором данных для современных GPU, чтобы достичь максимальной эффективности.
  • В одном из предыдущих разделов мы рассказывали о приоритезации представлений и задании по отбраковке. Два пункта выше еще более верны, поскольку некоторые занятые тексели не будут запекаться, потому что они не видны в данный момент в представлении сцены, что еще больше снижает заполняемость и общий набор данных.

Как нам решить эту проблему? В рамках сотрудничества с AMD было добавлено уплотнение лучей. Эта идея значительно повышает производительность трассировки лучей и затенения. Вкратце, идея заключается в том, чтобы создать все определения лучей в смежной памяти, позволяя всем потокам в варпе/волновом фронте работать с горячими данными.

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

Вот поток с уплотнением:

Изображение

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

Что дальше? Мы не решили проблему того, что набор данных может быть слишком мал для GPU, особенно если включена приоритезация просмотра. Следующая идея заключается в декорелировании генерации лучей из представления gbuffer. При наивном подходе мы генерируем только один луч на тексель. Поскольку в конечном итоге мы все равно захотим сгенерировать больше лучей, мы можем заранее сгенерировать несколько лучей на тексель. Таким образом, мы можем создать более значимую работу для GPU. Вот поток:

Изображение

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

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

С помощью этих методов мы достигаем цели: генерируем достаточно работы, чтобы насытить GPU, и тратим пропускную способность только на тексели, которые имеют значение.

Таковы преимущества съемки нескольких лучей на тексель:

  • Набор активных лучей всегда будет большим, даже в режиме приоритезации просмотра.
  • Подготовка, трассировка и затенение работают с очень согласованными данными, поскольку ядро расширения будет создавать лучи, направленные на один и тот же тексель, в непрерывной памяти.
  • Ядро расширения обрабатывает занятость и видимость, что делает ядро подготовки намного проще и, следовательно, быстрее.
  • Размер буферов расширенного/рабочего набора данных не зависит от размера карты освещения.
  • Количество лучей на тексель может определяться любым алгоритмом, естественным расширением будет адаптивная выборка.

В непрямом освещении используются очень похожие идеи, хотя и более сложные:

Изображение

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

Эвристика, которую мы используем в настоящее время, предпочитает равное количество лучей на тексель. Цель - получить очень прогрессивный выход. Однако естественным продолжением этой идеи может стать улучшение эвристики с помощью адаптивной выборки, чтобы снимать больше лучей там, где текущие результаты зашумлены. Кроме того, эвристика может стремиться к большей согласованности, как в памяти, так и при выполнении групп потоков, учитывая размер волнового фронта/волны аппаратного обеспечения.

Прозрачность/непрозрачность

Активы из ArchVizPRO, запеченные с помощью GPU Lightmapper.

Существует множество вариантов использования прозрачности/непрозрачности. Обычный способ обработки прозрачности и полупрозрачности заключается в том, чтобы направить луч, обнаружить пересечение, получить материал и запланировать новый луч, если встреченный материал полупрозрачен или прозрачен. Однако в нашем случае GPU не может порождать лучи по соображениям производительности (обратитесь к разделу `Конвейер, управляемый данными` выше). Кроме того, мы не можем разумно попросить CPU заранее запланировать достаточное количество лучей, чтобы быть уверенными в том, что мы справимся с наихудшим возможным случаем, так как это сильно снизит производительность.

Поэтому мы выбрали гибридное решение. Мы по-разному относимся к полупрозрачности и прозрачности, что позволяет решить описанные выше проблемы:

Прозрачность (когда материал не является непрозрачным из-за наличия в нем отверстий). В этом случае луч может либо пройти сквозь материал, либо отразиться от него на основе распределения вероятностей. Таким образом, нагрузка, заранее подготовленная процессором, не должна меняться, мы по-прежнему независимы от Scene.

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

Изображение

Однако есть одна особенность: обход BVH происходит не по порядку:

В случае с лучами окклюзии это вполне нормально, поскольку нас интересует только ослабление прозрачности каждого пересекающегося треугольника вдоль луча. Поскольку умножение коммутативно, обход BVH без порядка не представляет проблемы.

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

Все это стало возможным благодаря открытому исходному коду RadeonRays, который был форкнут и адаптирован под наши нужды в рамках сотрудничества с AMD.

Эффективные алгоритмы

Мы увидели, что мы делаем в отношении сырой производительности - отлично! Однако это лишь первая часть головоломки. Высокая скорость сэмплирования в секунду - это здорово, но в конечном итоге важно время запекания. Другими словами, мы хотим получить максимум от каждого брошенного луча. Это последнее утверждение на самом деле является результатом десятилетий непрерывных исследований. Вот несколько замечательных ресурсов:

Трассировка лучей за один выходной

Трассировка лучей: Следующая неделя

Трассировка лучей: Остаток жизни

Unity GPU Lightmapper - это чисто диффузный лайтмаппер. Это значительно упрощает взаимодействие света с материалами, а также помогает приглушить светлячков и шум. Однако мы еще многое можем сделать для улучшения скорости сходимости. Вот некоторые из техник, которые мы используем:

Русская рулетка

При каждом прыжке мы вероятностно убиваем путь, основываясь на накопленном альбедо. Отличное объяснение можно найти в диссертации Эрика Вича (стр. 67).

Выборка множественной важности (MIS) для окружающей среды

Среды HDR с высокой дисперсией могут вызывать значительный шум на выходе, что требует огромного количества образцов для получения приятных результатов. Поэтому мы применяем комбинацию стратегий отбора образцов, специально разработанных для оценки окружающей среды: сначала анализируем ее, выявляем важные области и отбираем соответствующие образцы. Этот подход, который не является исключительным для экологических выборок, известен как выборка с множественной важностью и был первоначально предложен в диссертации Эрика Вича (стр. 252). Это было сделано в сотрудничестве с Unity Labs Grenoble.

Много света

При каждом отскоке мы вероятностно выбираем один прямой свет и ограничиваем количество света, воздействующего на поверхности, с помощью структуры пространственной сетки. Это было сделано в сотрудничестве с компанией AMD. В настоящее время мы проводим более глубокое исследование проблемы большого количества света, поскольку выборка света имеет решающее значение для качества.

Изображение

Денуазинг

Шумы удаляются с помощью искусственного интеллекта, обученного на результатах трассировки пути. Посмотрите презентацию Unity на GDC 2019 от Йеспера Мортенсена.

Завершение

Мы увидели, как конвейер, основанный на данных, внимание к сырой производительности и эффективные алгоритмы объединяются вместе, чтобы предложить интерактивный лайтмэппинг с помощью GPU Lightmapper. Обратите внимание, что GPU Lightmapper находится в активной разработке и постоянно совершенствуется.

Сообщите нам свои мысли!

Команда по освещению

PS: Если вы считаете, что это было интересно, и хотите принять новый вызов, мы сейчас ищем разработчика освещения в Копенгагене, так что свяжитесь с нами!

---

Хотите узнать, как оптимизировать графику в Unity? Посмотрите этот учебник.