
Оптимизация приложения путем объединения идентичных объектов
Решил поделиться с вами одним очень важным знанием об оптимизации приложения или игры (далее просто приложение), создаваемого на движке Unity3D.
Проблема оптимизации имеет более острую актуальность при разработки приложений для мобильных платформ — iOS, Android и в перспективе Windows Phone. Так как ресурсы там весьма ограничены, особенно, если Вы планируете поддержку более старых версий ОС, которые используются на старых аппаратах, которые в свою очередь имеют еще более скудное железо.
Конечным показателем качества оптимизации принято считать параметр FPS — это аббревиатура от английских слов «frames per second», что в переводе на русский означает «количество кадров за секунду». То есть, чем лучше приложение оптимизировано, тем больше кадров в секунду оно может показать.
Что же влияет на время отрисовки одного кадра?
Это время определяют три фактора: время работы процессора (работа наших скриптов), время работы графического ускорителя (наши модели, шейдеры) и программное ограничение кол-ва кадров. Последнее, к слову говоря, можно смело ставить на 30-40, человеческий глаз все равно больше не увидит, а батарейку в телефоне это сэкономит, особенно если графика в игре простая.
Сделать это можно при помощи одной строчки:
Application.targetFrameRate = 30;
Но это отдельная тема, а поговорим мы с вами о времени работы графического ускорителя.
Ни для кого не секрет, что в игре не стоит использовать большого кол-ва полигонов, текстур с большим разрешением, большого кол-ва источников света, невероятно сложных шейдеров и т.д. Все эти банальности вы можете прочитать в миллионе статей на тысячи форумах о том, как делать игры.
Мы же с вами поговорим о снижении значения параметра Draw Calls. Draw Calls — это своего рода количество заданий для графического чипа для отрисовки одного кадра. Под заданием подразумевается связки «модель + материал», «модель + свет». То есть если в кадре мы имеем 40 моделей, то параметр Draw Calls будет равен 40, если мы добавим в сцену источник света, то Draw Calls вырастит вдвое — 80. Если не понятно, то взгляните на картинку:

Зависимость на лицо — Растет Draw Calls, FPS падает.
Решить эту проблему можно просто — изначально делать модель сцены монолитной с одной большой текстурой. Но тогда возникает проблема роста занимаемой памяти, и чтобы что-либо исправить придется постоянно возвращаться в редактор. Однако есть и другой способ:

Идентичные объект в Unity3d можно объединять в один объект. На изображении сверху 6 кубов объединены в одну сетку, поэтому мы имеем Draw Calls = 2. «1 модель + материал» + «1 модель + свет». Ограничения, которые накладываются на исходные объекты:
1) Объект не должен двигаться, то есть должен быть стационарным.
2) Все объекты должны использовать один и тот же материал.
Сами модели могут быть разные, т.е., например, объединить можно куб, сферу и плоскость.
Теперь давайте рассмотрим, как это делается.
Первое что нужно сделать — создать новый javascript файл. Назовем его «Сombine.js» и заполним следующим кодом:
#pragma strict
@script RequireComponent(MeshRenderer)
function Start () {
var meshFilters = GetComponentsInChildren(MeshFilter);
var combine : CombineInstance[] = new CombineInstance[meshFilters.length];
for (var i = 0; i < meshFilters.length; i++){
var mf : MeshFilter = meshFilters[i];
combine[i].mesh = mf.sharedMesh;
combine[i].transform = mf.transform.localToWorldMatrix;
}
mf = transform.gameObject.AddComponent(MeshFilter);
mf.mesh.CombineMeshes(combine);
transform.gameObject.renderer.sharedMaterial = transform.GetChild(0).renderer.sharedMaterial;
for (var j = 0; j < meshFilters.length; j++)
transform.GetChild(j).renderer.enabled = false;
}
Коментарии к коду:
При запуске он собирает со всех дочерних объектов мэши, «склеивает» их в один, а потом подключает полученную модель себе в MeshFilter. Материал берет у одного из дочерних объектов и так же применяет его на себя. Рендереры у дочерних объектов он выключает.
Далее необходимо создать пустой объект. Его глобальные координаты должны быть базисными, т.е. положение в начале координат, все оси совпадают, масштаб — единица. Локальные координаты не так важны.
Теперь кладем в этот объект, объекты которые нужно объединять.

И подключаем к этому пустому объекту скрипт Combine. Он так же автоматически подключит MeshRenderer.

Готово! Теперь запустив проект, можно увидеть что наш пустой объект обрел форму и цвет. Так же стоит заметить, что у дочерних объектов сохранились коллайдеры.

Теперь давайте протестируем эту технологию на реальном примере.
В моем проекте Greyhound Racing используется целая куча однообразных статических деревьев и травы. Попробуем сравнить FPS в случае с объединенными моделями и в случае, когда модели разъединены.
Раздельный мэш:

Цельный мэш:

Как видите, рост FPS составил примерно 25%, а DrawCalls упал на почти на 40%.
Надеюсь, это окажется полезным для вашего проекта.
Спасибо за внимание!
5 комментариев
написано
То есть дочерние объекты не перестают существовать, а просто прекращают участвовать в рендеринге.
Что вам больше подходит, решайте сами, все зависит от конкретной ситуации. Если дочерние объекты не используют других скриптов, то использовать вариант с официальной документации, либо вообще удалять эти объекты: