avatar

Оптимизация приложения путем объединения идентичных объектов

опубликовал в Unity3D / Программирование
Всем привет!

Решил поделиться с вами одним очень важным знанием об оптимизации приложения или игры (далее просто приложение), создаваемого на движке 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 комментариев

avatar
Люблю красиво оформленные записи :) Молодец, красиво и доступно
avatar
Спасибо за статейку! Полезно. Давно хотел узнать что это за драв колс)
avatar
Спасибо за статью! Очень вовремя…
avatar
Этот код улучшенная версия этого?
avatar
Вообще это тоже самое, но в моем коде вместо
meshFilters[i].gameObject.active = false;

написано
transform.GetChild(j).renderer.enabled = false;

То есть дочерние объекты не перестают существовать, а просто прекращают участвовать в рендеринге.
Что вам больше подходит, решайте сами, все зависит от конкретной ситуации. Если дочерние объекты не используют других скриптов, то использовать вариант с официальной документации, либо вообще удалять эти объекты:
Destroy(meshFilters[i].gameObject);
Чтобы оставить комментарий необходимо .