Pull to refresh

Comments 38

Что будет с производительностью, если у каждого объекта будет свое состоянии анимации? И как манипулировать ими? Как определять текущее состояние, полагаться только на тайминги?
Мы делали игру «жизнь» на двух сменяемых текстурах — буфферах.
Очень бодро бегает.
С производительностью будет сравнительно плохо, разные объекты будут иметь разные материалы (разные параметры времени анимации в шейдере). Впрочем, не уверен, возможно автор в этой деме делал тоже разные материалы.
В любом случае будет радикально лучше, чем если делать это на процессоре.
Определять текущее состояние можно только по таймингу.
Состояния как такового в этой системе нет — данные о нем теряются, остается только набор анимаций, идущий подряд.
Но таймер анимации хранится в компоненте и каждый кадр растится процессором и передается в материал. Так как управляется это со стороны процессора, никто не мешает в этом компоненте сохранить диапазоны анимаций и при управлении указывать стейт, и в компоненте доставать из информации о диапазоне время.
В любом случае, этот компонент должен как то работать с этой информацией — именно он управляет зацикленностью и режимом анимаций, для шейдера это просто число от 0 до 1.
По принципу инстансинга нет необходимости пихать модельки с разными материалами в одну кучу, также при использовании текстурного атласа проблем с материалом не будет. Массив состояний для каждой модели легко считается/обновляется на ComputeShader или через текстуру, и хорошо применим для DrawInstance или DrawProcedural.
Массив состояний — имеется в виду текстура?

Я имел в виду только то, что хотя моделька одна и текстурка одна, время анимации передается через uniform, поэтому отрисовка каждой модельки будет прерывать draw call чтобы изменить состояние рендер пайплайна.

Сам метод все равно довольно быстрый и хороший, кто спорит.
Может быть и текстура и RWStructuredBuffer. Ниже об это уже упоминали. Для одного меша, но для нескольких объектов в один draw call.
SadOcean спасибо.

— Эта техника будет хорошо работать если у нас одна моделька с одним материалом. На сторону шейдера мы передаем массивы позиций, поворотов и ключ к кадру. Поскольку все анимации сохранены в одной текстуре последовательно, нам ничто не мешает для разных объектов отсылать разные ключи. Следовательно часть объектов может быть в одной анимации, а часть в другой. И производительность не должна пострадать.
— Состояние анимации, а также позиция и поворот каждый кадр рассчитывается на стороне процессора.
— Я думаю эта техника будет полезна в RTS когда нам нужно рисовать большое количество юнитов.
Я имел в виду только то, что хотя моделька одна и текстурка одна, время анимации передается через uniform, поэтому отрисовка каждой модельки со своим временем будет прерывать draw call чтобы изменить состояние рендер пайплайна.

Сам метод все равно довольно быстрый и хороший, кто спорит.
Действительно хорошо подойдет для стратегий.

Но если немножко нахачить с кадрами анимаций, например, чтобы бегущие юниты имели 2-3 тайминга на бег на всех и 2-3 тайминга на idle на всех, то материалы еще и батчились бы, что сделало бы рендер еще быстрее, позволив выводить впечатляющее количество юнитов.

Разные материалы не потребуются если использовать MaterialPropertyBlock

MaterialPropertyBlock это блок данных/свойств для одного материала. Нет ни какой разницы будут ли свойства материала устанавливаться на прямую, для каждой отдельной сетки, или передаваться через MaterialPropertyBlock. Это всегда отдельный DrawCall для отдельной модели.

Я отвечал на (там не было речи про разные модели):


С производительностью будет сравнительно плохо, разные объекты будут иметь разные материалы (разные параметры времени анимации в шейдере).

разные объекты

Изначально! Как бы то ни было MaterialPropertyBlock не дает ни каких преимуществ.
Не знал, что есть такая штука, но ниже отметили, что все равно отдельный DrawCall для отдельного состояния модели.
Под объектом Я имел в виду модель в своем состоянии, не важно, каким образом она выводится.
Юнити вроде умеет батчить одинаковые материалы с одинаковыми свойствами, но изменение шейдерного юниформа (а кадр анимации получается передается через него) в любом случае меняет стейт рендера, что обрывает Draw Call.
Если честно, Я ковырялся в вопросе довольно давно, возможно в новых Api есть методы для изменения свойств материала в рамках одного кола.
Можно придумать другие хаки, например, прикрепить информацию о сдвиге по времени в пиксели модели. Но не думаю, что это серьезно поможет.
Так можно добиться, чтобы куча одинаковых моделей использовала разное время одной анимации, но при этом для изменения относительного времени нужно будет перегенерить модель, к тому же получиться больше вершинных данных — по сути каждый сдвиг будет давать новую модель.
Собственно получается просто разный способ создания инстансов материалов — непосредственно в объектах или оптом через Api.
Материалы то все равно будут рендериться разные.
В тоже время тестирование этой же сцены на macbook pro 15 с интегрированной видеокартой показывает не завидный результат в пользу GPU(безбожно проигрывает), что неудивительно.
Предложение так составлено что, возможно, имелось в виду CPU, допустили опечатку? (или CPU безбожно проигрывает?) А насколько проигрывает?
Все верно. Попробую переформулировать, во избежании. GPU работает медленнее приблизительно в 3 раза
Частота работы CPU 3 Ггц, частота работы GPU 1 Ггц. Разница 3 раза, это пропорция по отношению к разным система сохраняется. Если встроенный GPU одно ядерный то такое падение естественно. Или где то явно косяк, например очень часто спотыкаются при повторной загрузке данных в GPU которые там уже есть.
Задача была рисовать 100 — 1000 однотипных анимированных объектов.
ComputeShader нужен для произведения расчетов без отрисовки результата на экран.
Ты можешь произвести расчет анимации и сохранить результаты в RWStructuredBuffer. Потом на основе этих данных производишь отрисовку
StructiredBuffer поддерживается только ComputeShader-ом. Я не совсем понимаю как это должно работать на ComputeShader. Возможно вы могли бы предоставить пример.
Спасибо
Я просто обычно на DX11 кожу так вот там StructuredBuffer доступен и в вершинном и пиксельном шейдере. В Unity тоже должно быть надо включить поддержу SM 5.0

StructuredBuffer (чтение) поддерживается во всех шейдерах. RWStructuredBuffer (запись) поддерживается в pixel и compute шейдерах

Думаю, можно ещё немного разгрузить CPU даже не переходя на compute shader.
Зачем каждый кадр на CPU обновлять номер кадра для каждого instance? А если их действительно 8000 нужно будет отрисовывать? Или больше? Насколько я помню, когда Unity начали пиарить свои «усы», у них была демка, на которой под сто тысяч юнитов отрисовывали…
Можно для каждого instance указать время, когда началась анимация, и параметры этой анимации (номера первого и последнего кадра конкретного animationState). Причём второе можно тоже сохранить в каком-либо буфере, и заменить на индекс – номер текущей анимации. Ну и в шейдер как uniform передавать текущее время.
Тогда обновлять буфер с этими состояниями можно будет только для тех instance, у которых изменилась анимация (например, персонаж до этого шёл – а теперь останавливается; или его ударили, и теперь он падает). Если персонаж просто идёт (и у него «играет» анимация walk) или стоит («играет» idle), то для него перезаписывать ничего не нужно.
Но на GPU телодвижений чуть побольше нужно будет, да.
Для всех материалов все равно придется передавать время ведь.
Все равно каждый кадр будет обновляться на всех материалах.
Нет, нужно будет только обновлять один uniform (текущее время) и уточнять для тех instance, где что-то в анимации поменялось (т.е. изменилась анимация, которая проигрывается).
В статье описывается вариант, в котором упаковывается вся анимация в текстуру, и используется instance render для отрисовки большого количества одинаковых персонажей. Для каждого из них через отдельный буфер уточняется frameOffset (в коде шейдера он так зовётся, по крайней мере; полный код не смотрел). Это – номер кадра, по которому можно отрисовать целиком модель, трансформированную под этот кадр. При отрисовке 8000 персонажей, например, мы передаём в буфере 8000 значений frameOffset (просто для этого буфера указывается, как каждый instance должен понять, какое из значений он берёт). То есть персонажей по времени синхронизировать не нужно вообще, при этом они всё равно отлично батчатся.

Но я пишу о другом. В данном случае приходится каждый кадр перед отрисовкой обновлять эти 8000 значений, даже если персонажи идут ровно. А можно просто заменить frameOffset на, скажем, следующий набор значений: timeClipStarted, clipFrameOffset, clipFrameCount. В Unity время текущего кадра уже передаётся – нужно лишь объявить переменную с соответсвующим названием. А затем считаем, сколько прошло времени с начала воспроизведения анимации (time — timeClipStarted) и домножаем на frame per second анимации (или хардкод, или тоже передавать как один uniform; для модели оно общее для всех instance). Получаем количество кадров, которое должно было отрисоваться с момента начала этой анимации для этого instance. Округляем, затем берём остаток от деления на clipFrameCount, добавляем clipFrameOffset – и получаем тот самый frameOffset, который есть в статье.
Зачем все эти манипуляции? Если есть армия из десяти видов по десять тысяч юнитов, то по статье получится, что нужно десять буферов по десять тысяч значений; но мы их будем каждый кадр инкрементить. Если не переносить на compute shader – то это будет делать центральный процессор. В моём же варианте если у юнитов не меняется state, то мы их часть буфера можем не трогать. Вот если у юнита сработало событие, что он получил удар стрелой, и вместо walk нужна анимация hit – тогда ему и обновляем. Но получаем десять буферов уже по тридцать тысяч значений, а так же чуть больше рассчётов на GPU (для каждой вершины каждого instance). И если из ста тысяч юнитов состояние за кадр поменяется только у тысячи – мы обновим за кадр три тысячи значений, а не сто тысяч. Есть, конечно, и плохой вариант развития событий – если нужно будет разом для всех юнитов переключиться на другую анимацию (что-нибудь вроде kill all humans с переключением анимации на dying) – то в этот кадр мы обновим 300 тысяч значений.

Конечно, для использования такого решения нужно, чтобы сошлись много звёзд на небосклоне – вопрос экономии видеопамяти стоять не должен, compute shader должны быть недоступны по тем или иным причинам, но должно быть важно экономить такты CPU…
Ну 8000 значений для процессора — это экономия на спичках, даже если они упрятаны в объекты и каждый достается по ссылке. Но можно и так, это экономия ценой сложности обслуживания анимации объектов.
Моя претензия скорее к тому, что будет 8000 материалов с разными свойствами + текущее время — для видеокарты это все равно будет 8к материалов с переключением стейта (просто в первом случае будет меняться кадр в каждом материале, а в вашем случае — время в каждом материале).

Да, сами вы не будете изменять свойство материалов. Это будет делать Unity, пробрасывая в юниформ время вместе со свойствами очередного материала. В документации явно это обозначено.

Можно группировать юнитов с одинаковым временем кадра, делать им один инстанс материала и они будут батчится, тогда профит будет.

Товарищи выше говорят, что вроде есть варианты менять свойства материала во время одного батча в рамках одного DrawCall. Возможно для этого есть функционал в новых Api, в OpenGL <= 4 Я такого не встречал. Справедливости ради, это не моя сильная сторона, Я работаю с мобилками, там таких требований обычно не ставят.
Ну и в любом случае это хаки и оптимизации, специфичные для API платформы. Не из коробки.
Хотя если задача стоит, почему бы и нет.
Почему будет 8000 материалов?
Graphics.DrawMeshInstanced — рисует сетку с одним материалом много раз. На этапе регистрации отрисовки мы передаем позиции, поворот и кадр для каждого обьекта. А дальше уже видеокарта рисует эти обьекты. В вершинной функции выставляем персонажа в нужную позу и точку пространства.

За счет того что мы используем DrawMeshInstanced а не по отдельности выставляем позу для каждого обьекта и получаем экономию. B основе этого подхода лежит техники рисования растительности (деревьев, травы).

И на скришотах вы можете посмотреть что для стандартного подхода получаем 3651 батч а для GPU instance — 11

Я проверил.
using UnityEngine;

public sealed class DrawMeshInstanced : MonoBehaviour {
    [SerializeField]
    private Mesh instancedMesh; // Set from editor
    [SerializeField]
    private Material material; // Set from editor
    [SerializeField]
    private int meshCount = 1023;

    private Matrix4x4[] matrices;
    private Vector3[] positions;
    private MaterialPropertyBlock block;

    private void Start() {
        matrices = new Matrix4x4[meshCount];
        for (int i = 0; i < matrices.Length; i++) {
            matrices[i] = Matrix4x4.TRS(Random.insideUnitSphere * 10f, Random.rotationUniform, Vector3.one * Random.Range(0.5f, 1f));
        }
        block = new MaterialPropertyBlock();
        block.SetColor("_Color", Color.red);
    }

    private void Update() {
        
        Graphics.DrawMeshInstanced(instancedMesh, 0, material, matrices, meshCount, block);
    }
}

Вот, у меня получилось 3 батча на 1000 кубиков.
Этим способом можно отрисовать одну сетку с одним материалом.
В MaterialPropertyBlock нельзя передать по сету свойств для каждой модели. Это модификация материала для всех GPU инстансов.
Таким образом, как Я и сказал, не получается один батч для разных анимаций.
Для разных анимаций нужно иметь либо разную сетку, либо разные свойства материала (не важно, время или сдвиг)
Можно использовать хаки — например вычислять разницу в анимации по физическому параметру, к примеру, позиции.
Поэтому метод работает, к примеру, для деревьев. Можно замутить отряд юнитов.
Но без разбиения батчинга не получится сделать управляемые разные анимации разным юнитам.

Не проблема получить 1 батч с одним материалом. Проблема получить с разными.

Впрочем, это нельзя сделать только через DrawMeshInstanced.
Я не утверждаю, что этого нельзя сделать в принципе.
Возможно, такие методы в новых api есть.
Просто сама возможность таких хаков противоречит тому, что Я читал про работу видеокарт. 1 отрисовка — 1 состояние, 1 набор всех стейтов отрисовки и юниформов параметров — света, кастомных переменных и прочего.
Впрочем, это нельзя сделать только через DrawMeshInstanced.

В шейдере материала используется instanceID или unity_InstanceID — с помощью него можно получить для каждого отдельного экземпляра объекта уникальные свойства.
Например цвет:
StructuredBuffer<«float4»> ColorBuffer;

Устанавливается в скрипте:
ColorBuffer.SetData(Colors);
instanceMaterial.SetBuffer("ColorBuffer", ColorBuffer);

Используется в шейдере:
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
col = СolorBuffer[unity_InstanceID];
#else
col = float4(0, 0, 1, 1);
#endif

Точно также можно использовать instanceID как текстурную координату и считывать цвет и другие свойства материала или параметры анимации из текстуры. Который можно также объявить как входной параметр через SV_InstanceID
Когда в сцене много объектов с одинаковым мешем и материал поддерживает инстансинг, они автоматом объединяются и отображаются через DrawMeshInstanced каждый со своим уникальным свойством в материале. Для этого вообще не требуется мудрить со скриптом.

Тоже самое и с анимацией. И никаких велосипедов как в статье, фантазий с передачами дополнительных параметров через массивы, и танцев с бубном. Unity делает все сама, даже с поддержкой мобильных устройств!
Да, а вот это уже круто.
Ну бубны то есть, просто их осуществляет unity.
Unity делает все сама, даже с поддержкой мобильных устройств!
We have found a way to reduce CPU cost and supplement GPU Instancing in Unity with Animation Instancing. You can get our code on GitHub. Be aware that this is custom experimental solution, we’ve only shared it with a few of our enterprise support customers until recently.

Судя по Вашей ссылке – не сама. Причём очень похоже, что смысл примерно тот же самый:
Before using instancing for characters, we need to generate the animations. We generated the animations of a character into textures. These textures are called Animation Texture. The textures are used in skinning on GPU.
Вкратце – нужно запечь анимации в текстуру для дальнешейго использования на GPU. Что-то мне это напоминает…
О это я разогнался. Но заморачиваться с дополнительными массивами параметров не стоит, все это unity делает самостоятельно, достаточно сделать поддержку инстансинга в шейдере.

В статье все это собирается через DrawMeshInstancedIndirect, а в Unity это делается автоматом. Каждый объект может быть со своим материалом базирующемся на одном шейдере.
www.youtube.com/watch?v=l3Unh6FE1-s
Тоже самое можно провернуть при использовании анимации на GPU.
Только вот пока почему то это реализовано через MaterialPropertyBlock, иначе инстанситься не хочет. И шейдер придется делать ручками, стандартные так не могут.
public class InstancingMaterial : MonoBehaviour
{
    public Color color;

    static private MaterialPropertyBlock props;

    // Start is called before the first frame update
    void Start()
    {
        if (props == null)
            props = new MaterialPropertyBlock();
        MeshRenderer renderer = GetComponent<MeshRenderer>();
        props.SetColor("_Color", color);
        renderer.SetPropertyBlock(props);
    }
}
Ну и в шейдер как uniform передавать текущее время.
Unity Shader Variables уже передается Time разных мастей: текущее, от синуса/косинуса, дельта и все это в четырех масштабах.
не туда тыкнул
Да, спасибо, я знаю. Просто я писал более абстрактно, а не касаемо конкретно Unity. Плюс можно попробовать ввести свои единицы времени в попытках отказаться от float. Это может быть сделано в том числе и в попытках компенсировать увеличением размера буфера, которое потребуется при реализации моего варианта «в лоб».Другой вопрос – будет оно стоить того, или нет.
Sign up to leave a comment.

Articles

Change theme settings