Batched Rendering. Dan Korostelev

В своем докладе Дэн рассказал об оптимизациях отрисовки графики, которые команда разработчиков игры Forge of Empires использовала при портировании с Flash на HTML5.

Видеоверсия доклада доступна на youtube.

Forge of Empires - многопользовательская браузерная онлайн-игра в жанре стратегии и градостроительства. Изначально игра была разработана на Flash, а в этом году стала доступна html5-версия. О процессе ее портирования с использованием Haxe и OpenFL на прошлогоднем Haxe Summit в Амстердаме был большой и интересный доклад.

Хотя большинство проблем при портировании игры на html5 было связано с автоматической конвертацией ActionSctipt-кода в Haxe, часть из них все же была связана и с отрисовкой графики:

  • хотя Flash API очень удобно для создания пользовательских интерфейсов и игр вообще, оно разработано для софтварной отрисовки графики и соответственно не оптимизировано для аппаратного ускорения;
  • OpenFL использует не самые эффективные методы отрисовки графики;
  • реализации WebGL в различных браузерах сами по себе могут работать достаточно медленно, например, в MS Edge.

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

Для того, чтобы оптимизировать данный момент, стал использоваться прием, получивший название SubBitmapData. Класс SubBitmapData наследуется от класса BitmapData, однако он представляет собой только некоторую область исходного изображения (ссылается на область из атласа). Поэтому SubBitmapData может использоваться там же, где требуются объекты типа BitmapData, но при этом новая текстура не создается. Данный прием мало влияет на число вызовов WebGL API, но позволил сэкономить CPU-ресурсы, которые тратились на копирование областей изображений из атласов, а также видеопамять, т.к. количество используемых текстур уменьшилось на порядок.

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

Проблема №1 с батчингом в OpenFL связана с тем, что в общем случае в OpenFL батчинг не работает (он реализован только для объектов типа Tilemap). Примерно так выглядит код в OpenFL, ответственный за отрисовку объектов:

Проблема №2 - в OpenFL используется самая простая “наивная” реализация батчинга, в которой объекты рисуются с помощью одного вызова отрисовки графики только в том случае, если они используют одну и ту же текстуру:

В случае Forge of Empires такого подхода было недостаточно: игра была разработана под Flash без учета возможностей использования GPU, ассеты для игровых объектов были разбросаны по разным атласам, поэтому при отрисовке кадра постоянно происходило переключение между текстурами и смысл батчинга терялся, выигрыш от его использования сводился на нет.

Поэтому вместо “наивного” подхода к батчингу стал использоваться более сложный его вариант с одновременным использованием нескольких текстур:

Его основная идея заключается в том, что так как GPU одновременно может работать с несколькими текстурами, то и объекты в одном “батче” (наборе объектов, отправляемых на отрисовку с помощью одного вызова) также могут использовать не одну общую текстуру, а несколько разных текстур.

Как обычно работает отрисовка объектов в играх:

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

Шейдер, показанный на предыдущем слайде, является упрощенной версией шейдера, используемого в OpenFL для отрисовки объектов. В нем, как видно, используется всего одна текстура, и он может использоваться (и используется) для “наивной” реализации батчинга. Для этого вместо того, чтобы посылать данные о вершинах одного объекта, нужно посылать данные о вершинах объектов, объединенных в “батч”.

Батчинг с использованием нескольких текстур работает несколько иначе:

  • вместо одной текстуры, мы задаем максимальное число текстур, которое поддерживает видеокарта (в спецификации WebGL 1 гарантируется, что это число не меньше 8, но согласно статистике это число обычно равно 16. Точное значение можно получить с помощью вызова метода gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
  • для каждой вершины дополнительно указывается идентификатор текстуры, которая должна использоваться при ее отрисовке;
  • используя более сложную версию шейдера, отрисовываем объекты с разными текстурами:

Для использования батчинга с использованием нескольких текстур в OpenFL пришлось внести изменения в код, отвечающий за отрисовку объектов типа Bitmap (отрисовка объектов типа Graphics и TextField осуществляется с помощью этого же кода, т.к. в OpenFL графическое представление объектов данных типов хранится в памяти как объекты BitmapData). В итоге упрощенная версия кода для отрисовки объектов стала выглядеть так, как показано на следующем слайде:

Этот упрощенный код выглядит также как и “наивная” реализация батчинга, однако чтобы увидеть отличия, необходимо заглянуть в алгоритм работы батчера:

  • во время обхода объектов на сцене мы просто собираем объекты, которые будут отрисованы в массив;
  • а при вызове метода flush() у батчера:
    • подготавливаем данные о вершинах объектов (применяем к ним трансформации объектов - осуществляем преобразование из локальных координат в глобальные);
    • объединяем объекты в “рендер-группы”. Каждая такая рендер-группа определяет набор объектов, которые будут рисоваться в рамках одного и того же вызова метода отрисовки графики. При этом объекты добавляются в рендер-группу до тех пор, пока число использованных в ней текстур не превышает максимального количества текстур, поддерживаемых видеокартой;
  • затем в видеопамять загружаются данные о вершинах всех объектов сцены;
  • и последовательно рисуем каждую из созданных рендер-групп.

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

И что немаловажно, для достижения таких результатов не потребовалось вносить изменения в код клиента игры - все изменения производились в переработке игровых ассетов и в доработке кода в OpenFL.

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

К сожалению, из-за существенных изменений в OpenFL 8 (перевод отрисовки объектов на Stage3D API), перенос изменений, сделанных командой Forge of Empires в их форке OpenFL (они использовали OpenFL 7.1.1), является крайне трудоемкой задачей. Надеюсь, что когда-нибудь это произойдет.

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