Оптимізація за допомогою пакетування

Вступ

Ігрові рушії мають надсилати набір інструкцій до графічного процесора, щоб вказати йому, що і де малювати. Ці інструкції надсилаються за допомогою загальних інструкцій, які називаються API. Прикладами графічних API є OpenGL, OpenGL ES та Vulkan.

Різні API несуть різні витрати під час малювання об'єктів. OpenGL виконує багато роботи для користувача у драйвері графічного процесора за рахунок більш дорогих викликів малювання. Як наслідок, додатки часто можна пришвидшити, зменшивши кількість викликів малювання.

Виклики засобу малювання

У 2D нам потрібно вказати графічному процесору відрендерити серію примітивів (прямокутників, ліній, багатокутників і т.д.). Найочевидніший спосіб - сказати графічному процесору, щоб він рендерив по одному примітиву за раз, повідомивши йому деяку інформацію, таку як використана текстура, матеріал, положення, розмір і т.д., а потім сказати "Малюй!" (це називається виклик на малювання).

Хоча це концептуально просто з боку рушія, графічні процесори працюють дуже повільно, коли їх використовують у такий спосіб. Графічні процесори працюють набагато ефективніше, якщо ви скажете їм намалювати декілька схожих примітивів за один виклик, який ми будемо називати "пакетом".

Виявляється, вони не просто працюють трохи швидше, якщо їх використовувати таким чином, вони працюють набагато швидше.

Оскільки Godot розроблено як універсальний рушій, примітиви, що надходять до візуалізатора Godot, можуть бути у довільному порядку, іноді схожими, а іноді несхожими. Щоб узгодити універсальну природу Godot з перевагами пакетної обробки на графічних процесорах, Godot має проміжний рівень, який може автоматично групувати примітиви, де це можливо, і надсилати ці партії на графічний процесор. Це може підвищити продуктивність рендерингу, вимагаючи при цьому незначних змін у вашому проекті Godot (якщо такі взагалі потрібні).

Як це працює

Інструкції надходять до візуалізатора з вашої гри у вигляді серії елементів, кожен з яких може містити одну або декілька команд. Елементи відповідають вузлам у дереві сцени, а команди відповідають примітивам, таким як прямокутники або багатокутники. Деякі елементи, такі як Карти Плиток і текст, можуть містити велику кількість команд (плиток і гліфів відповідно). Інші, такі як спрайти, можуть містити лише одну команду (прямокутник).

Пакувальник використовує дві основні техніки для групування примітивів:

  • Послідовні елементи можна поєднувати разом.

  • Послідовні команди в елементі можна об'єднати, щоб сформувати пакет.

Розбивання пакетів

Формування пакетів відбувається лише тоді, коли елементи або команди достатньо схожі, щоб їх можна було вималювати за один виклик малювання Певні зміни (або техніки), за необхідності, запобігають формуванню суцільної пакета, це називається "розбиванням пакета".

Пакет буде розбитий при (серед іншого):

  • Зміні текстур.

  • Зміні матеріалу.

  • Зміні типу примітиву (наприклад, перехід від прямокутників до ліній).

Примітка

Наприклад, якщо ви малюєте серію спрайтів, кожен з яких має свою текстуру, їх неможливо об'єднати в пакет.

Визначення порядку рендерингу

Виникає питання, якщо тільки схожі об'єкти можна об'єднати в пакет, чому б не переглянути всі об'єкти в сцені, згрупувати всі схожі об'єкти і об'єднати їх разом?

У 3D часто саме так працюють рушії. Однак у 2D-візуалізаторі Godot об'єкти малюються у "порядку художника", ззаду наперед. Це гарантує, що елементи спереду будуть намальовані поверх попередніх елементів, коли вони перекриваються.

Це також означає, що якщо ми спробуємо намалювати об'єкти на основі кожної текстури, то цей порядок може порушитися, і об'єкти будуть намальовані в неправильному порядку.

У Godot цей порядок "ззаду-наперед" визначає:

  • Порядок об'єктів у дереві сцени.

  • Індекс Z об'єктів.

  • Шар полотна.

  • Вузли YSort.

Примітка

Ви можете згрупувати схожі об'єкти разом для полегшення пакетної обробки. Хоча це не є обов'язковою вимогою з вашого боку, подумайте про це як про необов'язковий підхід, який може покращити продуктивність у деяких випадках. Зверніться до розділу Diagnostics, у пошуках порад для прийняття рішення.

Хитрість

А тепер, хитрість. Незважаючи на те, що за задумом художника, об'єкти зображуються ззаду наперед, розглянемо 3 об'єкти A, B та C, які містять 2 різні текстури: траву та дерево.

../../_images/overlap1.png

У художньому порядку вони впорядковані:

A - wood
B - grass
C - wood

Через зміну текстури, вони не можуть бути передані в пакеті і будуть відрендерені за 3 виклики малювання.

Однак, порядок художника потрібен лише за умови, що вони будуть намальовані поверх один на одному. Якщо ми послабимо це припущення, тобто якщо жоден з цих 3 об'єктів не перетинається, то немає потреби зберігати порядок малювання. Результат рендерингу буде однаковим. Що, якби ми могли цим скористатися?

Зміна порядку елементів

../../_images/overlap2.png

Виявляється, ми можемо змінити порядок елементів. Однак, ми можемо це зробити, тільки якщо елементи задовольняють умови тесту на перекриття, щоб гарантувати, що кінцевий результат буде таким же, як і за відсутності зміни порядку. Тест на перекриття є дуже дешевим з точки зору продуктивності, але не безкоштовним, заглядання наперед, щоб вирішити, чи можна переставити елементи в порядку, чи ні, вимагає деяких затрат. Кількість елементів, які потрібно перевірити на предмет переупорядкування, можна встановити в налаштуваннях проекту (див. нижче), щоб збалансувати витрати і вигоди у вашому проекті.

A - wood
C - wood
B - grass

Оскільки текстура змінюється лише один раз, ми можемо відрендерити все вищесказане лише за 2 виклики малювання.

Освітлення

Хоча робота системи пакетування зазвичай досить проста, вона значно ускладнюється, коли використовуються 2D-світло. Це пов'язано з тим, що світло малюється за допомогою додаткових проходів, по одному для кожного світла, що впливає на примітив. Розглянемо 2 спрайти A та B, з однаковими текстурами та матеріалами. Без світла вони були б об'єднані разом і намальовані за один виклик малювання. Але з 3-х джерел світла, вони будуть намальовані наступним чином, кожна лінія буде викликом малювання:

../../_images/lights_overlap.png
A
A - light 1
A - light 2
A - light 3
B
B - light 1
B - light 2
B - light 3

Це дуже багато викликів малювання: 8 для всього 2 спрайтів. Тепер уявіть, що ми малюємо 1000 спрайтів. Кількість викликів малювання швидко стає астрономічною і продуктивність падає. Частково це є причиною того, що світло може суттєво сповільнити 2D-візуалізацію.

Однак, якщо ви згадаєте хитрість з перевпорядкування предметів, то виявиться, що ми можемо використати той самий трюк, щоб уникнути порядку художника для світла!

Якщо A і B не перетинаються, ми можемо відрендерити їх разом у пакеті, тому процес малювання виглядає наступним чином:

../../_images/lights_separate.png
AB
AB - light 1
AB - light 2
AB - light 3

Це лише 4 виклики малювання. Непогано, адже це у 2 рази менше. Однак врахуйте, що в реальній грі ви можете малювати близько 1,000 спрайтів.

  • До: 1000 × 4 = 4 000 викликів малювання.

  • Після: 1 × 4 = 4 викликів малювання.

Це в 1000 разів зменшує кількість викликів малювання і має дати величезний приріст продуктивності.

Тест на перекриття

Однак, як і у випадку з переупорядкуванням елементів, все не так просто. Спочатку ми повинні виконати тест на перекриття, щоб визначити, чи можемо ми об'єднати ці примітиви. Цей тест на перекриття має невелику вартість. Знову ж таки, ви можете вибрати кількість примітивів для тесту на перекриття, щоб зважити переваги та витрати. У випадку зі світлом переваги зазвичай значно перевищують витрати.

Також врахуйте, що залежно від розташування примітивів у вікні перегляду, тест на перекриття іноді не спрацьовує (оскільки примітиви перекриваються, а отже, не повинні бути об'єднані). На практиці, зменшення кількості викликів малювання може бути менш драматичним, ніж в ідеальній ситуації, коли перекриття відсутні взагалі. Однак продуктивність зазвичай набагато вища, ніж без цієї оптимізації освітлення.

Обрізання світла

Пакетування може ускладнити відсіювання об'єктів, на які світло не впливає або впливає частково. Це може значно підвищити вимоги до швидкості заповнення і сповільнити рендеринг. Швидкість заповнення - це швидкість, з якою зафарбовуються пікселі. Це ще одне потенційне вузьке місце, не пов'язане з викликами малювання.

Щоб вирішити цю проблему (і прискорити освітлення загалом), у пакетуванні введено обрізання світла. Це дозволяє використовувати команду OpenGL glScissor(), яка визначає область, за межами якої графічний процесор не буде рендерити жодного пікселя. Ми можемо значно оптимізувати швидкість заповнення, визначивши область перетину між світлом і примітивом, і обмежити рендеринг світла лише цією областю.

Обрізання світла керується за допомогою параметра проекту scissor_area_threshold. Це значення знаходиться у діапазоні від 1.0 до 0.0, де 1.0 - вимкнено (немає обрізання), а 0.0 - обрізання відбувається за будь-яких обставин. Причиною цього параметра є те, що на деяких апаратних засобах обрізання може бути пов'язане з невеликими витратами. Але зазвичай обрізання призводить до підвищення продуктивності, коли ви використовуєте 2D-освітлення.

Співвідношення між пороговим значенням і тим, чи відбувається операція обрізання, не простий. Загалом, він відображає площу пікселів, яка потенційно може бути "збережена" за допомогою операції обрізання (тобто збережену швидкість заповнення). За значення 1.0 потрібно було б зберегти всі пікселі екрана, що трапляється рідко (якщо взагалі трапляється), тому цей параметр вимкнено. На практиці корисні значення близькі до 0.0, оскільки лише невеликий відсоток пікселів потрібно зберегти, щоб операція була корисною.

Точне співвідношення, ймовірно, не повинно турбувати користувачів, але його поміщено в додатках для цікавих: Light scissoring threshold calculation

Зразок схеми обрізання світла

Внизу праворуч - світло, червона область - пікселі, збережені в результаті операції обрізання. Вималювати потрібно лише перетин.

Vertex baking

The GPU shader receives instructions on what to draw in 2 main ways:

  • Shader uniforms (e.g. modulate color, item transform).

  • Vertex attributes (vertex color, local transform).

However, within a single draw call (batch), we cannot change uniforms. This means that naively, we would not be able to batch together items or commands that change final_modulate or an item's transform. Unfortunately, that happens in an awful lot of cases. For instance, sprites are typically individual nodes with their own item transform, and they may have their own color modulate as well.

To get around this problem, the batching can "bake" some of the uniforms into the vertex attributes.

  • The item transform can be combined with the local transform and sent in a vertex attribute.

  • The final modulate color can be combined with the vertex colors, and sent in a vertex attribute.

In most cases, this works fine, but this shortcut breaks down if a shader expects these values to be available individually rather than combined. This can happen in custom shaders.

Custom shaders

As a result of the limitation described above, certain operations in custom shaders will prevent vertex baking and therefore decrease the potential for batching. While we are working to decrease these cases, the following caveats currently apply:

  • Reading or writing COLOR or MODULATE disables vertex color baking.

  • Reading VERTEX disables vertex position baking.

Параметри проекту

To fine-tune batching, a number of project settings are available. You can usually leave these at default during development, but it's a good idea to experiment to ensure you are getting maximum performance. Spending a little time tweaking parameters can often give considerable performance gains for very little effort. See the on-hover tooltips in the Project Settings for more information.

rendering/batching/options

  • use_batching - Turns batching on or off.

  • use_batching_in_editor Turns batching on or off in the Godot editor. This setting doesn't affect the running project in any way.

  • single_rect_fallback - This is a faster way of drawing unbatchable rectangles. However, it may lead to flicker on some hardware so it's not recommended.

rendering/batching/parameters

  • max_join_item_commands - One of the most important ways of achieving batching is to join suitable adjacent items (nodes) together, however they can only be joined if the commands they contain are compatible. The system must therefore do a lookahead through the commands in an item to determine whether it can be joined. This has a small cost per command, and items with a large number of commands are not worth joining, so the best value may be project dependent.

  • colored_vertex_format_threshold - Baking colors into vertices results in a larger vertex format. This is not necessarily worth doing unless there are a lot of color changes going on within a joined item. This parameter represents the proportion of commands containing color changes / the total commands, above which it switches to baked colors.

  • batch_buffer_size - This determines the maximum size of a batch, it doesn't have a huge effect on performance but can be worth decreasing for mobile if RAM is at a premium.

  • item_reordering_lookahead - Item reordering can help especially with interleaved sprites using different textures. The lookahead for the overlap test has a small cost, so the best value may change per project.

rendering/batching/lights

  • scissor_area_threshold - See light scissoring.

  • max_join_items - Joining items before lighting can significantly increase performance. This requires an overlap test, which has a small cost, so the costs and benefits may be project dependent, and hence the best value to use here.

rendering/batching/debug

  • flash_batching - This is purely a debugging feature to identify regressions between the batching and legacy renderer. When it is switched on, the batching and legacy renderer are used alternately on each frame. This will decrease performance, and should not be used for your final export, only for testing.

  • diagnose_frame - This will periodically print a diagnostic batching log to the Godot IDE / console.

rendering/batching/precision

  • uv_contract - On some hardware (notably some Android devices) there have been reports of tilemap tiles drawing slightly outside their UV range, leading to edge artifacts such as lines around tiles. If you see this problem, try enabling uv contract. This makes a small contraction in the UV coordinates to compensate for precision errors on devices.

  • uv_contract_amount - Hopefully, the default amount should cure artifacts on most devices, but this value remains adjustable just in case.

Diagnostics

Although you can change parameters and examine the effect on frame rate, this can feel like working blindly, with no idea of what is going on under the hood. To help with this, batching offers a diagnostic mode, which will periodically print out (to the IDE or console) a list of the batches that are being processed. This can help pinpoint situations where batching isn't occurring as intended, and help you fix these situations to get the best possible performance.

Reading a diagnostic

canvas_begin FRAME 2604
items
    joined_item 1 refs
            batch D 0-0
            batch D 0-2 n n
            batch R 0-1 [0 - 0] {255 255 255 255 }
    joined_item 1 refs
            batch D 0-0
            batch R 0-1 [0 - 146] {255 255 255 255 }
            batch D 0-0
            batch R 0-1 [0 - 146] {255 255 255 255 }
    joined_item 1 refs
            batch D 0-0
            batch R 0-2560 [0 - 144] {158 193 0 104 } MULTI
            batch D 0-0
            batch R 0-2560 [0 - 144] {158 193 0 104 } MULTI
            batch D 0-0
            batch R 0-2560 [0 - 144] {158 193 0 104 } MULTI
canvas_end

This is a typical diagnostic.

  • joined_item: A joined item can contain 1 or more references to items (nodes). Generally, joined_items containing many references is preferable to many joined_items containing a single reference. Whether items can be joined will be determined by their contents and compatibility with the previous item.

  • batch R: A batch containing rectangles. The second number is the number of rects. The second number in square brackets is the Godot texture ID, and the numbers in curly braces is the color. If the batch contains more than one rect, MULTI is added to the line to make it easy to identify. Seeing MULTI is good as it indicates successful batching.

  • batch D: A default batch, containing everything else that is not currently batched.

Default batches

The second number following default batches is the number of commands in the batch, and it is followed by a brief summary of the contents:

l - line
PL - polyline
r - rect
n - ninepatch
PR - primitive
p - polygon
m - mesh
MM - multimesh
PA - particles
c - circle
t - transform
CI - clip_ignore

You may see "dummy" default batches containing no commands; you can ignore those.

Поширені запитання

I don't get a large performance increase when enabling batching.

  • Try the diagnostics, see how much batching is occurring, and whether it can be improved

  • Try changing batching parameters in the Project Settings.

  • Consider that batching may not be your bottleneck (see bottlenecks).

I get a decrease in performance with batching.

  • Try the steps described above to increase the number of batching opportunities.

  • Try enabling single_rect_fallback.

  • The single rect fallback method is the default used without batching, and it is approximately twice as fast. However, it can result in flickering on some hardware, so its use is discouraged.

  • After trying the above, if your scene is still performing worse, consider turning off batching.

I use custom shaders and the items are not batching.

  • Custom shaders can be problematic for batching, see the custom shaders section

I am seeing line artifacts appear on certain hardware.

  • See the uv_contract project setting which can be used to solve this problem.

I use a large number of textures, so few items are being batched.

  • Consider using texture atlases. As well as allowing batching, these reduce the need for state changes associated with changing textures.

Додаток

Batched primitives

Not all primitives can be batched. Batching is not guaranteed either, especially with primitives using an antialiased border. The following primitive types are currently available:

  • RECT

  • NINEPATCH (depending on wrapping mode)

  • POLY

  • LINE

With non-batched primitives, you may be able to get better performance by drawing them manually with polys in a _draw() function. See Власне малювання в 2D for more information.

Light scissoring threshold calculation

The actual proportion of screen pixel area used as the threshold is the scissor_area_threshold value to the power of 4.

For example, on a screen size of 1920×1080, there are 2,073,600 pixels.

At a threshold of 1,000 pixels, the proportion would be:

1000 / 2073600 = 0.00048225
0.00048225 ^ (1/4) = 0.14819

So a scissor_area_threshold of 0.15 would be a reasonable value to try.

Going the other way, for instance with a scissor_area_threshold of 0.5:

0.5 ^ 4 = 0.0625
0.0625 * 2073600 = 129600 pixels

If the number of pixels saved is greater than this threshold, the scissor is activated.