Расскажу немного об оптимизационном приеме который использовал недавно для Factory Protocol и с какими трудностями я столкнулся при его реализации в Unity.
Для игры я планирую использовать анимированные производственные машины, чтобы шестеренки крутились, а манипуляторы делали свое дело. Вот только в случае со skinned mesh (модель со скелетом), никакое объединение не работает (это когда одна и та же модель рисуется в разных местах но считается за 1 вызов отрисовки, что сильно помогает когда нарисовать ее надо тысячу раз).
10,000 статичных моделей отрисованных при помощи GPU Instancing
Чтобы объединение работало (gpu instansing) нужно чтобы модели были статичными, но при этом я все еще нуждаюсь в анимации моих моделей. Выход из этой ситуации - анимировать модели самому, уже в видеокарте, при помощи вершинного шейдера.
Чтобы анимации работали в вершинном шейдере, нужно просто сохранить положение (или смещение) каждой точки модели в текстуру, по каждому кадру, а затем в вершинном шейдере по индексу вершины брать нужное значение смещения и применять к положению вершины. Лишь одна небольшая деталь - у вершин нет индексов, поэтому их предварительно нужно записать в саму модель. Я использовал UV2 канал для записи дополнительной информации о модели.
Одна из первых, неудачных попыток реализации алгоритма. Слева - оригинальный Skinned Mesh, справа - статичный меш с анимацией через вершинный шейдер.
С первого раза конечно же не получилось. Проблема была в том, что в текстуру записываются только цветовые значения, ограниченные 4 числами с диапазоном от 0 до 1 (RGBA), но смещения вершин могут быть в значительно большем диапазоне и даже отрицательными. Для корректной нормализации я стал брать максимальное смещение у модели при конкретной анимации и записывать ее вместе с текстурой анимации. В итоге каждое смещение было нормализовано к диапазону от -1 до 1, и приведено к диапазону от 0 до 1. Что уже позволило упаковать смещения так как они должны выглядеть, без особых потерь в точности.
Куринный тест - проверка работы алгоритма на модели из сети. Полоса справа - текстуры с сохраненными анимациями.
Как можно заметить, все модели анимируются одновременно. Тут дело в том что они все выводятся с одной текстурой и одним показателем времени. Для того чтобы часть из них использовали другую анимацию - нужно рисовать их в другой коллекции, но это не является большой проблемой. Основная проблема тут именно в том чтобы сделать рассинхронизацию по времени - если указывать разные материалы для каждого объекта в коллекции, то коллекция будет разделена на количество объектов и об оптимизации можно будет больше и не мечтать. Выход из ситуации - создать текстуру, в которой будут записаны смещения по анимации для каждой модели в коллекции, по ее номеру.
С этого момента возникает неприятная проблема. Для того чтобы определить какой номер у модели в коллекции требуется instance ID. Но так как я использую URP и Shader Graph, который как оказалось не знает о instance ID пришлось сильно задуматься. Но ненадолго. Так удачно совпало, что за пару дней до того как я столкнулся с этим вопросом Unity выпустили новую бета версию, в которой был добавлен instance ID в shader graph. Совпадение? Да, совпадение.
В общем, после получения IID в свои руки, я решил попробовать покрасить каждую модель в коллекции в свой цвет, в диапазоне от синего к желтому. И вывел коллекцию из 1000 моделей, первая должна была быть синей, последняя желтой. Но вышло следующее.
По странному стечению обстоятельств, индекс в 1000 сбрасывался три раза, вмещая максимум 454 значения. Потратив лишний день на выяснение данного казуса (это было непросто, учитывая отсутствие документации) выяснилось следующее - буфер данных коллекции составляет 64 килобайта, а данные на 1 объект в коллекции составляют 144 байта (2 матрицы, индекс и еще что то). Путем нехитрых вычислений получаем 65536/144 = 455 (при подсчете от нуля 454), вот оно. Выходит что в одной коллекции я могу выводить на самом деле не более 454 объектов, чтобы корректно отслеживать их IID. Что же, ограничиваем одну коллекцию до 400 объектов, чуть меняем целевой цвет и смотрим на результат:
10,000 анимированных объектов, группами по 400 моделей
Другое дело, теперь я в каждой группе могу идентифицировать каждую модель и спокойно взять параметр с текстуры отступа анимации. А в силу того что каждая группа вызывается отдельным вызовом - в нее можно передать отдельную текстуру с отступами и отдельную анимацию. Для этого уже нужен свой собственный рендер/батчер(объединитель) для отрисовки всех моделей в нужных состояниях. О батчере расскажу в следующий раз.
Итог
Текущий прогресс с GPU анимациями - 10,000 анимированных объектов с индивидуальным состоянием - 250 FPS, рендер занимает всего 1 мс. Бутылочное горлышко у этого процесса сейчас - CPU, пытаюсь выяснить в чем именно дело и можно ли это ускорить, чтобы рисовать по 20,000-40,000 тысяч объектов.