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

В некоторых наших играх полнофункциональное решение для отображения теней Unity является излишним - мы не хотим визуализировать динамические тени для всего, а только для небольших объектов в сцене. Мы также хотим большего контроля над тем, как мы фильтруем наши тени - как мы их размываем, чтобы сделать их более мягкими.

Во время недавнего сеанса профилирования одной из наших игр мы заметили, что создание одной из этих теневых карт занимало примерно 12% от общего времени кадра. Поэтому я начал исследовать это и подумать, что мы можем сделать, чтобы снизить эту стоимость и в то же время уменьшить объем памяти, потребляемой системой.

Оптимизация

Моим первым шагом было запустить мои любимые инструменты профилирования как для Android (RenderDoc), так и для iOS (XCode). RenderDoc - это бесплатный профилировщик и отладчик, который может подключаться к главному устройству Android и захватывать трассировки кадров.

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

Максимально используем пространство, которое у нас есть

Используя средство просмотра целевого объекта рендеринга на обеих платформах, я заметил, что содержимое целевого объекта рендеринга карты теней занимало только небольшую часть всей текстуры. Я полагаю, что более 50% пикселей в целевой рендеринге были незанятыми - какая трата места!

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

В конце концов, мы смогли уменьшить разрешение текстуры нашей карты теней со 128 × 128 до 64 × 64 - это 1/4 от исходного размера. Одним из самых узких мест в мобильных устройствах является пропускная способность - у мобильных устройств маленькие автобусы. Передача по шине на 75% меньше данных - большая экономия. Это вместе с затенением на 75% меньшим количеством фрагментов - огромная победа (игнорируйте цвет на минуту - я изменил способ хранения карты теней в текстуре рендеринга).

MSAA

Поскольку мы используем такую ​​маленькую цель рендеринга, когда объекты начинают двигаться внутри цели рендеринга, вы заметите много искажений. Из-за особенностей работы мобильных графических процессоров MSAA стоит очень дешево. В мобильных графических процессорах используется мозаичная архитектура. Вся работа по сглаживанию выполняется на кристалле, а вся дополнительная память находится в тайловой памяти. Включение 4x MSAA с меньшей текстурой рендеринга дало нам гораздо лучшие результаты при небольшом увеличении затрат на обработку.

Целевые форматы рендеринга

Я заметил, что наша текстура рендеринга карты теней использует формат R8G8B8A8. Использовались только два канала. Первый канал (R) использовался для хранения самой тени, а второй канал (G) использовался для хранения линейного спада. Наши художники просили, чтобы интенсивность наших теней уменьшалась с увеличением расстояния.

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

Метод размытия

После того, как мы заполняем нашу текстуру рендеринга карты теней, мы размываем ее. Это уменьшает любые артефакты, которые мы видим от использования меньших текстур, а также создаёт впечатление мягких теней. Мы использовали блочное размытие 3 × 3 - это 9 образцов текстуры на пиксель. Более того, мы не использовали преимущества билинейной фильтрации со смещением на полпикселя. Я быстро добавил возможность сэмплирования только окружающих угловых пикселей вместе со смещением на половину пикселя, это уменьшило количество сэмплов с 9 до 5 (мы по-прежнему выбираем центральный пиксель).

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

Уменьшение инструкций ALU

Unity не поддерживает обычный режим обтекания текстурой границы. Поэтому нам пришлось добавить немного логики в наш шейдер размытия, который проверяет, является ли текущий тексель граничным текселом, и, если да, не убирает его. Это предотвращает растекание теней по принимающей поверхности. Код шейдера использовал внутренний шаг, чтобы вычислить, был ли текущий тексел текселем границы. Внутренний шаг - это что-то вроде оператора if. Мне удалось переработать этот фрагмент кода, чтобы вместо него использовать пол, только это уменьшило количество ALU с 13–9. Звучит немного, но когда вы делаете это для каждого пикселя в текстуре рендеринга, все складывается. Это не единственное спасение, которое мы сделали здесь, но это пример того, что мы сделали.

При написании шейдера обратитесь к инспектору в Unity. Отсюда, выбрав шейдер, вы можете нажать кнопку «Скомпилировать и показать код». Это скомпилирует ваш шейдерный код и откроет его в текстовом редакторе. В верхней части скомпилированного кода шейдера вы можете увидеть, сколько инструкций ALU и Tex использует ваш шейдер.

Для получения дополнительной информации вы можете загрузить и использовать Mali Offline Shader Compiler. Просто скопируйте скомпилированный код шейдера - бит между #if VERTEX или #if FRAGMENT - и сохраните его в файле .vert или .frag. Отсюда вы можете запустить его через компилятор, и он покажет вам статистику шейдера.

Выше вы можете видеть, что шейдер размытия с 5 нажатиями использует

  • 2 инструкции ALU (арифметико-логический блок)
  • 3 Инструкции по загрузке / сохранению
  • 5 Инструкции по текстурам

OnRenderImage

После завершения прохода размытия я заметил, что был дополнительный Blit - копирование размытой текстуры в другую цель рендеринга! Я начал копаться в этом и заметил, что, хотя мы указали, что наша размытая текстура рендеринга имеет формат R8, это была R8G8B8A8! Оказывается, это ошибка Unity. OnRenderImage передается 32-битная целевая текстура, затем ее значение копируется в окончательный целевой формат. Это было неприемлемо, поэтому я изменил наш конвейер. Теперь мы вручную выделяем наши текстуры рендеринга и выполняем размытие в OnPostRender.

Буфер глубины

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

Показатели эффективности

Здесь мы можем увидеть стоимость рендеринга одной карты теней (пример выше). Эти показания были получены с помощью отладчика кадров графического процессора XCode на iPhone 6s. Как видите, стоимость рендеринга этой карты теней составляет менее 50% от первоначальной стоимости.

Благодаря уменьшению размера наших целей рендеринга, использованию меньшего формата текстур, устранению ненужного Blit и (опционально) неиспользованию буфера глубины потребление памяти снизилось с 320 КБ до 8 КБ! Использование буфера с 16-битной глубиной удваивает это значение до 16 КБ. В любом случае, это ОЧЕНЬ сэкономленная пропускная способность.

Заключение

В лучшем случае нам удалось снизить потребление памяти (и использование полосы пропускания) более чем в 40 раз. Мы также смогли снизить общую стоимость нашей теневой системы чуть более чем на 50%! Для любой арт-команды, которая может это читать - это не значит, что у нас может быть вдвое больше теней 😀 В общем, я потратил около 2–3 дней на профилирование, оптимизацию и изменение вещей, и это определенно того стоило.