Как не надо делать рендеринг VR для мобильных
Перевод статьи от инженера Oculus про методы рендеринга, которые помогут достигнуть высокого FPS
Я Тревор Даш, инженер по связям с разработчиками в Oculus. Работаю со многими разработчиками, которые создают игры для Oculus Quest. Моя задача заключается в том, чтобы помочь этим разработчикам создавать высококачественные игры и обеспечивать их стабильную работу со скоростью 72 кадра в секунду. Мы видим, что многие разработчики, имеющие опыт разработки игр для ПК, подходят к своим играм в Quest так же, как и при разработке на ПК, но это может вызвать большие трудности при оптимизации для максимальной производительности. Итак, мы решили предложить список методов рендеринга, которые просто не помогут вам в пути к высокой частоте кадров на мобильном чипсете.
Хотя большинство методов, описанных ниже, технически возможно реализовать на мобильном чипсете, мы настоятельно рекомендуем не применять их. Тем не менее, это не всегда жесткие и однозначные правила. Я видел, что разработчики делают большинство из нижеперечисленного и все равно получают нужную частоту кадров. Но вы избавите себя от большой боли, избегая всего в этом списке.
Отложенный рендеринг
Отложенный рендеринг (или отложенный шейдинг) — это метод, при котором необходимые данные для расчета освещения сцены визуализируются в набор буферов (например, диффузный цвет, нормали, шероховатость и т.д.), и выполняется второй проход с использованием этих буферов в качестве входных данных, который на самом деле и делает расчеты освещения. Этот метод эффективен на ПК, потому что он отделяет геометрию от освещения. Вам нужно только обновлять пиксели, с которыми связан каждый источник света, что позволяет рендерить намного больше источников за меньшее количество циклов графического процессора.
Почему это не работает на мобильных устройствах:
Существует ряд причин, по которым это не работает на мобильных устройствах, но главная из них — это стоимость разложения. Что такое стоимость разложения? Прежде чем я расскажу вам, сколько стоит разложение, вы должны сначала понять, как работает тайловый графический процессор.
Чтобы достичь более высокой пропускной способности при гораздо более низком использовании энергии, мобильные графические процессоры (такие как Snapdragon 835, используемый в Oculus Quest) часто используют тайловую архитектуру, где каждая цель рендеринга разбивается на сетку кусков или «тайлов» (от 16×16 до 256×256 пикселей в зависимости от аппаратного обеспечения и формата пикселей). Затем ваша геометрия группируется (binned) в эти куски (путем запуска вершинного шейдера и помещения идентификатора примитива в список для этого бина), а затем отправляется асинхронным процессорам, которые выполняют работу рендеринга для вычисления результата изображения для каждого тайла (вытягивая только примитивы из списка, сгенерированного процессом биннинга). После вычисления каждого мозаичного изображения графический процессор должен скопировать этот фрагмент из встроенной памяти обратно в общую память (G-MEM). Это на самом деле довольно медленно, так как требует передачи данных по шине. Мы называем этот перевод «разложить», и, следовательно, время, которое оно занимает, называется «стоимость разложения». В Википедии есть более подробное описание тайлового рендеринга для дальнейшего чтения.
Поскольку для каждой текстуры, для которой вы выполняете рендеринг, требуется разложение (при грубом подсчете около половины миллисекунды для каждого буфера глаза в Oculus Quest), а для отложенного рендеринга требуется несколько текстур, которые должны быть отрисованы перед вычислением освещения, вы увеличили стоимость разложения с ~ 1 мс для упреждающего рендеринга до +3 мс. Как видите, у вас, вероятно, нет на это времени.
Помимо стоимости разложения, отложенный рендеринг является преимуществом только в том случае, если ваша геометрия сложна и у вас есть несколько источников света. И то, и другое в действительности не может быть достигнуто на мобильном устройстве, потому что мощность графического процессора не позволяет одновременно выдвигать большое количество вершин и вычислять заполнение пикселей.
Ответ пока заключается в том, чтобы придерживаться упреждающего рендеринга. Вероятность хорошей реализации для отложенного рендеринга существует, хотя я еще не видел ее.
Предварительный проход глубины
Предварительный проход глубины / Z — это обычная техника, в которой вся геометрия сцены визуализируется как первый этап без заполнения буфера кадра, в результате получается только значение буфера глубины. Затем вы визуализируете второй проход, который проверяет, равна ли вычисленная глубина для каждого пикселя значению для этого пикселя в буфере глубины. Если это не так, вы можете пропустить шейдинг этого фрагмента. Поскольку обработка вершин обычно выполняется намного быстрее, чем шейдинг пикселей на ПК, это может сэкономить много времени.
Почему это не работает на мобильных устройствах:
Прежде всего, количество времени, которое вы экономите на заполнении фрагмента, выполняя предварительный проход глубины, должно быть минимальным, если вы сортируете свою геометрию перед отправкой вызовов отрисовки. Рисование передних объектов первыми приведет к тому, что обычный тест глубины отклонит ваши пиксели, поэтому вам следует избегать работы по заполнению пикселей только для геометрии, которая не была правильно отсортирована, или когда оба объекта перекрывают друг друга в разных точках.
Во-вторых, это требует удвоения количества вызовов, поскольку все должно быть представлено сначала для предварительного прохода глубины, а затем для прямого прохода. Поскольку вызовы отрисовки довольно тяжелы для процессора, вам следует избегать этого.
В-третьих, все вершины должны быть обработаны дважды, что обычно добавляет больше времени в GPU, чем вы экономите, избегая двукратного заполнения нескольких пикселей. Это связано с тем, что обработка вершин на мобильных устройствах сравнительно дольше, чем на ПК, и обработка фрагментов будет относительно меньше (поскольку размер кадрового буфера обычно меньше, а программа фрагментов стремится к тому, чтобы быть проще).
Текстуры HDR
Стоимость разложения напрямую связана с количеством байтов в изображении, а не с количеством пикселей, поэтому, хотя мы обычно думаем о вещах с точки зрения 32-битных пикселей RGBA, в наши дни многие разработчики используют текстуры HDR, которые составляют 64 бита на пиксель для значений половинной точности. Это удвоит ваши затраты на разложение, и, поскольку дисплей поддерживает только 8 бит на канал, вы будете тратить много времени на разложение текстур HDR. Не говоря уже о том, что мобильный графический процессор оптимизирован для 32-битных кадровых буферов, и рендеринг для чего-либо еще займет больше времени.
Постпроцессинг
Постпроцессинг — это метод, который часто используется для применения в играх ряда эффектов, таких как разложение цвета, свечение и размытие в движении. Они достигаются за счет вывода результатов рендеринга вашей игры, а затем запуска полноэкранного прохода для этого изображения, чтобы создать новое изображение, которое затем будет представлено игроку. Некоторые эффекты постопроцессинга выполняются как один дополнительный проход (например, разложение цветов), в то время как другие требуют многократных проходов (размытие часто требует создания нескольких уменьшенных изображений).
Основная проблема с постпроцессингом на мобильном телефоне — это еще раз стоимость разложения. Создание второго изображения приведет к другому разложению, которое сразу же уберет примерно 1 мс из-за вмешательства GPU. Не говоря уже о времени, которое требуется для вычисления вашего эффекта постпроцессинга, который может быть довольно ресурсоемким в зависимости от эффекта. Лучше избегать постпроцессинга вообще.
Вот несколько альтернатив распространенным эффектам постпроцессинга:
Разложение цветов
Вместо того, чтобы выполнять разложение цветов как постобработку, для выполнения той же математики добавьте вызов функции в конец каждого фрагментного шейдера. Это даст тот же визуальный результат без дополнительного разложения.
Свечение
Истинное свечение поглотит очень много времени на вашем GPU. Лучше всего подделать его. Использование билборд спрайтов с размытой текстурой может создать нечто, похожее на реальное.
Тени в реальном времени
Я бы посчитал это самым спорным пунктом в списке. Есть много приложений, которые были успешно перенесены на мобильные устройства с полноценными тенями в реальном времени. Однако для этого есть существенные компромиссы, которых, на мой взгляд, стоит избегать.
Обычная техника для теней в реальном времени (и в Unity по умолчанию) — это каскадные карты теней, что означает, что ваша сцена визуализируется несколько раз с различными размерами области просмотра. Это добавляет в 1-4 раза больше времени, когда ваша геометрия должна обрабатываться графическим процессором, что по сути ограничивает количество вершин, которые ваша сцена может поддерживать. Также добавляется стоимость разложения текстуры карты теней, которая будет зависеть от размера текстуры. На другом конце конвейера GPU у вас есть два варианта выборки карт теней: жесткие тени и мягкие. Жесткие тени быстрее рендерятся, но у них есть неизбежная проблема со сглаживанием. Из-за того, как работают карты теней (проверка глубины пикселя относительно глубины вашей карты теней), только один из двух результатов может быть получен из этого теста: в тени или не в тени. Вы не можете делать билинейную выборку карты теней, потому что она представляет значение глубины, а не значение цвета. Следует избегать мягких теней, потому что они требуют нескольких выборок в карту теней, что, конечно, медленно. Лучше всего запечь все тени, которые вы можете, и, если вам нужны тени в реальном времени, придумайте другую технику. Обычно приемлемы размытые тени (blob shadows), если ваше освещение в основном рассеянное. Достаточно хорошо могут подойти геометрические тени, если вам нужно жесткое освещение и поверхность куда отбрасывается тень является плоской.
Сэмплирование глубины (и буфера кадра)
На ПК можно сэмплировать текущую текстуру глубины в ваших шейдерах (Unity предоставляет как _CameraDepthTexture). Это работает, потому что текстура глубины — это просто еще одна текстура на ПК, и поскольку каждый вызов отрисовки происходит один за другим, состояние текстуры глубины будет состоянием после последнего вызова отрисовки. Однако при использовании тайлового рендера текущая глубина отсутствует в текстуре, она сохраняется только в памяти ваших тайлов, поэтому ее нельзя сэмплировать как обычную текстуру.
Вообще-то, на самом деле существуют расширения GLES, которые позволят вам запрашивать текущее состояние буфера глубины (и кадрового буфера). Проблема заключается в том, что они очень медленные, позволяют только выбирать значение для одного и того же пикселя (поэтому вы не можете запрашивать соседние пиксели), и то, как они работают при включенном MSAA, имеет свой собственный набор проблем (который всегда присутствует у приложений VR!).
Когда включен MSAA, ваш тайл на самом деле имеет буфер, достаточно большой, чтобы вместить все сэмплы (то есть, 2x пикселей для 2xMSAA, 4x для 4xMSAA). Это означает, что по умолчанию, если вы производите выборку из буфера глубины, он должен будет выполнять ваш фрагментный шейдер отдельно для каждой выборки, то есть в 2 или 4 раза больше времени, чем вы ожидаете. Есть способ «исправить» это, который должен вызвать glDisable (FETCH_PER_SAMPLE_ARM). Однако проблема заключается в том, что он будет извлекать только значение для первого сэмпла, а не результат смешивания сэмплов, то есть MSAA функционально отключен, когда он включен.
Если нет абсолютной необходимости, избегайте этого для выигрыша времени.
Геометрические шейдеры
Геометрические шейдеры позволяют генерировать дополнительные вершины во время исполнения, что полезно для таких вещей, как динамическая тесселяция. Однако на тайловых графических процессорах существует проблема с геометрическими шейдерами. Этап создания дополнительных вершин предотвращает процесс биннинга, то есть графический процессор не может его сделать и переключается в «немедленный» режим (полностью пропускает процесс разбиения на тайлы). Как вы можете догадаться, это очень медленно (тайловый рендеринг был изобретен не просто так). Поэтому лучше избегать геометрических шейдеров, а вместо этого генерировать свои вершины на своем процессоре, если это необходимо.
Зеркала/Порталы*
* Если вы делаете их наивным способом. Под наивностью я имею в виду выделение двух текстур размером с глазной буфер, вычисление матрицы отражения и рендеринг сцены в обе из них. Чтобы показать отражение, геометрия зеркала будет тогда делать сэмпл текстуры пространства экрана. У этого подхода есть ряд очевидных недостатков:
- Вы только что утроили количество вызовов отрисовки.
- Вы заполняете значительно больше пикселей, чем видно на экране.
- Вам нужно разложить две дополнительные текстуры.
Самое минимальное улучшение, которое я нашел, — это ограничение области просмотра камер зеркала и изменение соответствующей матрицы проекции, чтобы визуализировать только ограничивающий прямоугольник плоскости камеры для зеркала в конусе просмотра. Это немного помогает для проблемы №2. В идеале вы также можете использовать многоракурсный режим для визуализации как левого, так и правого глаза с помощью одного набора вызовов отрисовки, однако в настоящее время это не поддерживается в Unity. Это не решает проблему №3 и ухудшает ситуацию с №2, потому что вы можете использовать только одну область просмотра для обоих глаз и получается перекрытие обоих ограничивающих прямоугольников зеркал (из-за асимметричного поля зрения это будет большая часть ширины глазного буфера для зеркала, которое примерно по центру). Таким образом, идеальным решением было бы сначала решить №3, что означает рисование зеркальной и не зеркальной сцены за один проход.
Существует решение, использующее преимущества модифицированных шейдеров и буфера трафарета. Каждый материал в вашей сцене будет иметь две версии шейдера: одну, которая рисует, только если определенный бит в буфере трафарета равен 0, и одну, которая рисует, только если она равна 1. Тогда вы будете рисовать зеркальную сетку с материалом, который устанавливает этот бит в буфере трафарета, рисует сцену с использованием первого набора шейдеров, настраивает камеру с матрицами отражения и, наконец, рисует сцену со вторым набором шейдеров. Это создаст искомое отражение, не заполняя больше пикселей, чем необходимо, и избегая ненужного разложения. Не следует избегать рисования нескольких объектов дважды (это неизбежно при любом решении).
Хотя это звучит достаточно просто, есть ряд проблем, если вы используете Unity (я не пробовал это в Unreal, но у вас, вероятно, будут похожие проблемы). Во-первых, Unity не позволяет вам изменять матрицу проецирования камер, когда включен режим Single Pass Stereo (многоракурсный), поэтому вы не можете использовать зеркальные камеры (что вам определенно нужно, если вы вообще заинтересованы в производительности процессора). Во-вторых, это не учитывает позднюю фиксацию, которая представляет собой метод, с помощью которого матрица камеры фактически обновляется при запуске потока рендеринга (после завершения основного потока), чтобы максимально уменьшить задержку. Обычно это чистый выигрыш, но если у вас есть зеркальная камера, преобразование отражающей камеры больше не соответствует преобразованию главной, так что вы получите странные артефакты, когда вещи в зеркале не выстраиваются в линию, как вы ожидаете.
Самым простым решением для зеркал является подделка. Если ваше зеркало статично, просто создайте отраженную копию всей геометрии вашего мира и поместите ее в сцену. Вам понадобятся сценарии для перемещения по «отраженной» копии любых динамических объектов, чтобы имитировать положение «реальной» версии, включая игрока, но это должно быть самое быстрое и наименее сложное решение для рендеринга (без хитрой матричной математики, возни с несколькими камерами и т.д.). Вам все равно придется использовать два разных набора шейдеров с разными трафаретными масками, если можно посмотреть за зеркало, но если игрок не может видеть за ним из-за стены или чего-то еще, вы можете просто оставить один набор шейдеров для обоих миров и буквально просто пробить дыру в стене. Подбросьте прозрачный спрайт «грязного стекла», и ничто не будет мудрее.
Заключение
Знание о том, что устройство может и не может делать хорошо, станет ключом к получению самой привлекательной игры и сохранит полную частоту кадров. Независимо от того, начинаете ли вы новый проект с нуля или делаете порт с ПК на мобильный. Тем не менее, вам не нужно следовать моим советам буквально. Найдите свое видение и сделайте компромиссы, которые работают на вас.
Мой последний совет (и лучшее, что я могу дать) — это измерять, измерять, измерять по ходу дела. Когда вы добавляете функции, которые могут слишком сильно влиять, или удаляете эффекты, которые, по вашему мнению, сэкономят значительное время, очень важно определить влияние ваших изменений. Возможно, этот новый шейдер не так тяжел, как вы ожидали, или новый эффект частиц вызывает значительное снижение частоты кадров. Наличие этой информации поможет вам принять важные решения. Как мы все узнали из G.I. Joe: «Знание — это половина победы!»
Источник: https://developer.oculus.com/blog/pc-rendering-techniques-to-avoid-when-developing-for-mobile-vr/