Рендеринг і анімація сотень тисяч блоків за допомогою RenderMeshPrimitives
Public- 24 Sep, 2026

Зображення можна розбити на величезний масив кольорових блоків через Graphics.RenderMeshPrimitives, а потім анімувати його хвилями, збиранням, відштовхуванням та обвалом на іграбельній частоті кадрів. Підхід починається з того, що вихідна текстура перетворюється на список даних про позицію та колір кожного блока. Задається горизонтальна кількість блоків gridWidth, а вертикальне виводиться зі співвідношення сторін зображення — так вихідні пікселі проріджуються з рівними інтервалами, при цьому повністю прозорі та майже чорні пікселі пропускаються як фон.
Кожен блок зберігається в структурі PixelBlock з полями float3 position і float4 color, точно повторюючи розкладку HLSL. В Awake метод texture.GetPixels32 читає вихідне зображення, а цикл for по координатам сетки сопоставляет каждой ячейке сетки исходный пиксель, зміщуючи позиції так, щоб центр сітки опинився в початку координат. Байти кольору діляться на 255, перетворюючись на значення 0–1. Перший наївний прохід створює по одному GameObject на блок зі спільним мешем, матеріалом і MaterialPropertyBlock, але на приблизно 100 000 блоках усе падає до 6 FPS, тому що кожен об'єкт додає команди відмальовування та накладні витрати компонентів.
Рішенням стає Graphics.RenderMeshPrimitives — API, спеціально створений для багаторазового відтворення одного й того самого меша. Дані екземплярів завантажуються один раз у GraphicsBuffer (GraphicsBuffer.Target.Structured, sizeof(float) * 7 на елемент), потім SetData(blocks) переносить весь масив із CPU на GPU одним викликом. Аргумент stride має збігатися з розміром структури HLSL, і його потрібно оновлювати при зміні полів. Єдиний виклик Graphics.RenderMeshPrimitives(renderParams, cubeMesh, 0, blocks.Length) рисує все, поки шейдер дістає позицію та колір за SV_InstanceID. worldBounds має бути достатньо великим, щоб умістити всі блоки: занадто маленький приховує блоки, а занадто великий зводить нанівець відсікання; буфер варто звільняти в OnDisableщоб уникнути витоків.
На приблизно 100 000 блоках версія з GameObject зросла до 54 525 викликів відмальовування та приблизно 6 FPS, тоді як версія на RenderMeshPrimitives трималася на 42 викликах відмальовування на всю сцену й сягала 700–865 FPS. При збільшенні приблизно вдесятеро — до 1,04 мільйона блоків — виклики відмальовування залишилися на рівні 42, а частота кадрів трималася близько 435 FPS; цифри взяті з вікна Statistics в Unity. Оскільки зміщення читається з буфера на боці GPU, а CPU видає лише одну команду відмальовування, вартість залишається низькою навіть при зростанні кількості блоків.
Анімація йде двома шляхами залежно від того, чи потрібен стан попереднього кадру. Рух, заданий лише часом, цілком виконується у вершинному шейдері: хвиля додає sin вздовж Z, використовуючи _WaveAmplitude, _WaveFrequency і _WaveSpeed, а ефект збирання інтерполює розкидану позицію до вихідної, керованої значенням _Progress. Напрямки розкидання беруться з GenerateHashedRandomFloat з ключем за ID екземпляра, тому вони детерміновані й не потребують збереженого стану, а стартові пороги, зміщені за висотою, змушують зображення підніматися знизу.
Для рухів, що переносять швидкість або позицію між кадрами, використовується обчислювальний шейдер. Ефект магніту зберігає поточні позиції в буфері читання-запису й відштовхує блоки біля курсора, потім щокадру лерпить позиції до їхніх цілей перед відмальовуванням оновлення. Гравітаційний обвал щокадру інтегрує швидкість у буфер BlockState, відскакуючи під час приземлення з _Restitution і засинаючи, коли швидкість падає нижче _SleepThreshold; _DeltaTime при цьому він обмежений значенням 1/30 секунди, щоб уникнути вибуху інтегрування, а позиції спокою заздалегідь обчислюються зі щільності по колонках.

