Добрый день! Хочу поделиться с вами некоторым опытом по оптимизации с использованием GPU Instancing. Постановка задачи примерно такая: игра под мобильные платформы, одним из элементов которой является поле с травой. Фотореалистичность не требуется, low poly стиль. Но при этом игрок должен иметь возможность взаимодействовать с травой. В нашем случае выкашивать. Делать будем на Unity 2021.2.7 (но жесткой привязки к версии нет) с URP. Конечный результат можно увидеть на гифке. Для начала создадим нашу low poly травинку. Так как камера в игре никогда не вращается, то можно немного поэкономить и использовать только два треугольника: Вряд ли этот меш будет похож на траву, если останется статичным, поэтому нам потребуется что-нибудь похожее на ветер. Делать его мы будем шейдером и для этого нам потребуется правильно подготовить UV, а именно: нижние вершины должны быть внизу текстуры, а верхняя соответственно сверху. Примерно так: Теперь сделаем Shader Subgraph, который будем использовать в наших дальнейших экспериментах: Немного пояснений. На вход подается несколько параметров, ответственных за ветер и мировая позиция вершины. Для вычисления ее сдвига ветром мы прогоняем позицию через Simple Noise чтобы все травинки на сцене колыхались не синхронно. И далее через UV и Lerp мы определяем насколько ветровой сдвиг должен быть применен к данной вершине. Теперь можно проводить наши эксперименты. Так же хочу сразу отметить, что что все замеры производительности будут производиться на не самом быстром Xiaomi Redmi 8A. Прежде чем что-то оптимизировать мы должны убедиться, что в этом есть необходимость. Посмотрим как покажет себя SRP Batcher на том количестве травинок, которое будет устраивать нас визуально. Сперва сделаем полноценный шейдер: Теперь приступаем к засеву поляны: Генерируем равномерное поле с небольшой долей случайности. Оно у нас плоское, поэтому Vector2 вполне достаточно для координат. Далее передаем этот массив координат на "отрисовку". Сейчас это просто расстановка GameObject в нужном количестве. Получаем такую картину: Визуально мы достигли желаемого (в реальной игре другой ракурс камеры, есть декор, текстура земли и прочее, к тому же сама трава засеивается только в нужных областях, поэтому выглядит получше, но количество травинок совпадает: 15-20к). Но вот fps конечно подводит. Значит можно думать об оптимизации. После прочтения официальной документации становится понятно, что это именно то, что нам нужно. Попробуем им воспользоваться. В инспекторе материала нашей травы можно найти галочку "Enable GPU Instancing" и включить ее. Но, к сожалению, все не так просто. Если мы запустим сцену и посмотрим на Frame Debugger, то увидим, что по-прежнему работает SRP Batcher, но не GPU Instancing. Все дело в том, что эти две технологии несовместимы, поэтому нам надо как-то отключить SRP Batcher. Есть несколько способов сделать это. Этот способ вряд ли можно назвать рекомендованным. Не зря в свежих версиях Unity эту галочку убрали из инспектора. Но найти ее по-прежнему можно через Debug режим инспектора вашего URP ассета. Нам этот способ не подходит, потому что кроме травы в игре есть много других вещей, для которых SRP Batcher нас устраивает. Способ не самый удобный, потому что необходимо отредактировать сам шейдер. А в случае Shader Graph необходимо сначала еще сгенерировать код шейдера, который потом отредактировать. И при каждом изменении графа эту операцию нужно повторять. Далее согласно документации: "Add a new material property declaration into the shader’s Properties block. Don’t declare the new material property in the UnityPerMaterial constant buffer". Почему-то в моем случае это не сработало и шейдер оставался SRP compatible, поэтому я просто закомментировал объявление буфера: Несмотря на неудобство этого способа, я выбрал именно его. Просто чтобы механизм инстанцирования и сами префабы оставались теми же, что и в первом эксперименте.  Пожалуй, самый простой способ. К тому же наверняка может потребоваться использовать один шейдер с SRP Batcher и с GPU Instancing. Для этого достаточно назначить рендереру объекта MaterialPropertyBlock, потому что он не совместим с SRP Batcher. К тому же MaterialPropertyBlock может иметь практическую пользу. Допустим если бы мы захотели реализовать асинхронное колыхание травы не через случайный шум, а каким-то своим параметром. Либо раскрашивать траву при нажатии на нее. После любой из этих манипуляций, мы увидим, что инстансинг заработал: Можно сделать замеры: Отрисовать меш с использованием GPU Instancing можно так же напрямую через Graphics API, без необходимости использования GameObject. Для этого существуют 3 метода. 1. Graphics.DrawMeshInstanced Самый простой способ. Передаем меш, материал и массив матриц. Не требует создания специального шейдера, но имеет ограничение в 1023 инстанса за один вызов. Маловато будет. 2. Graphics.DrawMeshInstancedIndirect Не имеет ограничений на количество инстансов, которое теперь передается через ComputeBuffer. Но позиции уже нельзя передать напрямую, а нужно передавать буфером в материал. Что в свою очередь требует доработки шейдера. 3. Graphics.DrawMeshInstancedProcedural Схож с предыдущим, используется, когда количество инстансов известно из скрипта. На нем мы и остановимся. Для начала передадим позиции наших травинок в материал: Далее сама отрисовка, которая должна вызываться каждый кадр: Если мы попробуем использовать наш прежний материал, то все тысячи травинок будут рисоваться в одном месте, потому что наш шейдер должен теперь брать координаты из буфера. Поэтому модифицируем шейдер: Нам потребовались две ноды с кастомными функциями.  Первая нужна нам для того чтобы мы могли воспользоваться unity_InstanceID, с помощью которого мы будем вытаскивать позицию инстанса из буфера. Само решение было найдено где-то в интернетах и полное теоретическое обоснование на данный момент дать не могу. Буду признателен за ссылки с пояснениями. Вторая функция - непосредственно извлечение позиции из буфера. Теперь сделаем замеры: Кроме неплохой производительности данный способ позволяет использовать Compute Shader для вычисления значений в буфере. Например вынести вычисления для летающей по сцене травы из C# в шейдер. Например так. Камера в нашей игре не вращается, но может передвигаться вдоль оси Z. Поэтому добавим легкое подобие кулинга. Так как траву мы размещали по условной сетке, то можем примерно понять какую часть массива травинок нам следует отображать в данный момент. В классе нашей поляны будем следить за камерой и вычислять границы. В instanced отрисовке добавим обновление буфера. В GameObject отрисовке будем включать/отключать объекты травы. Ну и сделаем замеры нашего самого производительного варианта. А вот так это будет выглядеть на приемлемых для многих игр ~30 fps. Так же хочется отметить заметный выигрыш в памяти за счет отказа от использования GameObject. И чем больше инстансов мы захотим, тем больше будет эта разница. К сожалению, инстансинг поддерживается не на всех девайсах. Проверить это можно через SystemInfo.supportsInstancing. Для этих девайсов можно включать fallback вариант со старыми добрыми GameObject, просто уменьшив их количество. И конечно же отличились Samsung с как минимум Galaxy A50. Несмотря на то, что SystemInfo.supportsInstancing возвращает true (что по всей видимости правда, потому что эксепшенов нет), но что-то у них не так с буфером. Вся трава на данном девайсе упорно рисуется в одной точке. Поэтому пришлось делать проверку костылями. Но об это можно поговорить в другой раз, если кому-то будет интересно.  В данной статье я не преследовал цели глубокого сравнения и анализа отличий SRP Batcher и GPU Instancing. Да и сам инстансинг описан мной довольно поверхностно. Мне скорее хотелось рассказать про то где он может быть полезен. И как он помог решить одну конкретную задачу. Проект из статьи можно найти вот здесь. Справедливая критика приветствуется. Спасибо за внимание!