Не так давно Unity представила ECS. В процессе изучения мне стала интерестно, а каким образом можно подружить анимацию и ECS. И в процессе поиска я наткнулся на интересную технику, которую применяли ребята из NORDVEUS в своем демо для доклада Unite Austin 2017.
Unite Austin 2017 — Massive Battle in the Spellsouls Universe.


Доклад содержит много интересных решений но сегодня пойдет речь о сохранении скелетной анимации в текстуре с целью дальнейшего ее применения.


Зачем такие сложности, спросите вы?


Ребята из NORDVEUS одновременно отрисовывали на экране большое количество однотипных анимированных объектом: скелетов, мечников. В случае использования традиционного подхода: SkinnedMeshRenderers и Animation\Animator, повлечет за собой увеличение вызовов отрисовки и дополнительную нагрузке на CPU по просчету анимации. И чтобы решить эти проблемы анимацию перенесли на сторону GPU, а точнее в вершинный шейдер.



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


Итак давайте нарежем слона на кусочки:



  • Получение ключей анимации из клипов
  • Сохранение данных в текстуру
  • Подготовка сетки (меша)
  • Шейдер
  • Собираем все вместе


Получение ключей анимации из клипов анимации


Из компонент SkinnedMeshRenderers достаем массив костей и меш. Компонент Animation предоставляет список доступных анимаций. Итак для каждого клипа мы должны покадрово сохранить матрицы трансформации для всех костей меша. Иными словами мы сохраняем позу персонажа в единицу времени.


Выделяем двумерный массив, в котором будут сохранены данные. Одно измерение которого имеет количество кадров умноженное на длину клипа в секундах. Другое — общее количество костей в меше:


var boneMatrices = new Matrix4x4[Mathf.CeilToInt(frameRate * clip.length), renderer.bones.Length];

В следующем примере поочередно меняем кадры для клипа и сохраняем матрицы:


// проходим по всем кадрам в клипе
for (var frameIndex = 0; frameIndex < totalFramesInClip; ++frameIndex)
{
  // нормализуем время: 0 - начало клипа, 1 - конец. 
  var normalizedTime = (float) frameIndex / totalFramesInClip;

  // выставляем время семплируемого кадра
  animationState.normalizedTime = normalizedTime;
  animation.Sample();

  // проходим  по всем костям
  for (var boneIndex = 0; j < renderer.bones.Length; boneIndex++)
  {
    // рассчитываем матрицу трансформации для кости в этот кадр
    var matrix = renderer.bones[boneIndex].localToWorldMatrix *
                       renderer.sharedMesh.bindposes[boneIndex];
      
    // сохраняем матрицу             
    boneMatrices[i, j] = matrix;                             
  }
}

Матрицы имеют размерность 4 на 4 но последний рядок всегда выглядит как (0, 0, 0, 1). Следовательно с целью небольшой оптимизации его можно пропустить. Что в свою очередь сократит расходы на передачу данных между процессором и видеокартой.


a00 a01 a02 a03
a10 a11 a12 a13
a20 a21 a22 a23
0   0   0   1

Сохранение данных в текстуру


Чтоб высчитать размер текстуры перемножим общее число кадров во всех клипах анимации на количество костей и на количество рядков в матрице(мы договорились что сохраняем первых 3 рядка).


var dataSize = numberOfBones * numberOfKeyFrames * MATRIX_ROWS_COUNT);	
// рассчитываем ширину и высоту текстуры 
var size = NextPowerOfTwo((int) Math.Sqrt(dataSize));
var texture = new Texture2D(size, size, TextureFormat.RGBAFloat, false)
{
  wrapMode = TextureWrapMode.Clamp,
  filterMode = FilterMode.Point,
  anisoLevel = 0
};

Записываем данные в текстуру. Для каждого клипа покадрово сохраняем матрицы трансформации. Формат данных следующий. Клипы записываются последовательно один за одним и состоят из набора кадров. Которые в свою очередь состоят из набора костей. Каждая кость содержит в себе 3 рядка матрицы.


Clip0[Frame0[Bone0[row0,row1,row2]...BoneN[row0,row1,row2].]...FramM[bone0[row0,row1,row2]...ClipK[...]

Ниже приведен код сохранения данных:


var textureColor = new Color[texture.width * texture.height];

var clipOffset = 0;
for (var clipIndex = 0; clipIndex < sampledBoneMatrices.Count; clipIndex++)
{
  var framesCount = sampledBoneMatrices[clipIndex].GetLength(0);
  for (var keyframeIndex = 0; keyframeIndex < framesCount; keyframeIndex++)
  {
    var frameOffset = keyframeIndex * numberOfBones * 3;
    for (var boneIndex = 0; boneIndex < numberOfBones; boneIndex++) 
    {
      var index = clipOffset + frameOffset + boneIndex * 3;
      var matrix = sampledBoneMatrices[clipIndex][keyframeIndex, boneIndex];
                        
      textureColor[index + 0] = matrix.GetRow(0);
      textureColor[index + 1] = matrix.GetRow(1);
      textureColor[index + 2] = matrix.GetRow(2);
    }
  }
}
texture.SetPixels(textureColor);
texture.Apply(false, false);

Подготовка сетки (меша)


Добавим дополнительный набор текстурных координат, в который сохраним для каждой вершины ассоциированные с ней индексы костей и веса влияния кости на эту вершину.
Unity предоставляет структуру данных, в которой возможны до 4 костей для одной вершины. Ниже приведен код для записи этих данных в uv. Сохраняем индексы костей в UV1, веса в UV2.


var boneWeights = mesh.boneWeights;

var boneIds = new List<Vector4>(mesh.vertexCount);
var boneInfluences = new List<Vector4>(mesh.vertexCount);
for (var i = 0; i < mesh.vertexCount; i++)
{
  boneIds.Add(new Vector4(bw.boneIndex0, bw.boneIndex1, bw.boneIndex2, bw.boneIndex3);
  boneInfluences.Add(new Vector4(bw.weight0, bw.weight1, bw.weight2, bw.weight3));
}

mesh.SetUVs(1, boneIds);
mesh.SetUVs(2, boneInfluences);

Шейдер


Основная задача шейдера найти матрицу трансформации для кости ассоциированной с вершиной и перемножить координаты вершины на эту матрицу. Для этого нам и понадобиться дополнительный набор координат с индексами и весами костей. Еще нам понадобиться индекс текущего кадра он будет меняться с течением времени и будет передаваться со стороны CPU.


// frameOffset = clipOffset + frameIndex * clipLength * 3 - рассчитываем это на стороне CPU
// boneIndex - индес костти к котрой привязана вершина, берем из UV1
int index = frameOffset + boneIndex * 3;

Итак мы получили индекс первой строки матрицы, индекс второй и третьей будет +1, +2 соответственно. Осталось перевести одномерный индекс в нормированные координаты текстуры и для этого нам нужен размер текстуры.


inline float4 IndexToUV(int index, float2 size) 
{
  return float4(((float)((int)(index % size.x)) + 0.5) / size.x, 
		 ((float)((int)(index / size.x)) + 0.5) / size.y, 
		 0, 
		 0);
}

Вычитав строки собираем матрицу не забыв про последний рядок, который всегда равен (0, 0, 0, 1).


float4 row0 = tex2Dlod(frameOffset, IndexToUV(index + 0, animationTextureSize));
float4 row1 = tex2Dlod(frameOffset, IndexToUV(index + 1, animationTextureSize));
float4 row2 = tex2Dlod(frameOffset, IndexToUV(index + 2, animationTextureSize));
float4 row3 = float4(0, 0, 0, 1);

return float4x4(row0, row1, row2, row3);

Одновременно на одну вершину могут влиять сразу несколько костей. Результирующая матрица будет суммой всех матриц влияющих на вершину умноженных на вес их влияния.


float4x4 m0 = CreateMatrix(frameOffset, bones.x) * boneInfluences.x;
float4x4 m1 = CreateMatrix(frameOffset, bones.y) * boneInfluences.y;
float4x4 m2 = CreateMatrix(frameOffset, bones.z) * boneInfluences.z;
float4x4 m3 = CreateMatrix(frameOffset, bones.w) * boneInfluences.w;

return m0 + m1 + m2 + m3;

Получив матрицу перемножаем её на координаты вершины. Следовательно все вершины будут перемещены в позу персонажа, которая соответствует текущему кадру. Меняя кадр, мы будем анимировать персонажа.


Собираем все вместе


Для отображения объектов будем использовать Graphics.DrawMeshInstancedIndirect, в который передадим подготовленный меш и материал. Также в материал мы должны передать текстуру с анимациями размер текстуры и массив с указателями на кадр для каждого объекта в текущий момент времени. В качестве дополнительной информации мы передаем позицию для каждого объекта и поворот. Как изменить позицию и поворот на стороне шейдера можно посмотреть в [статье].


В методе Update увеличиваем время пройденное с начала анимации на Time.deltaTime.


Для того чтобы высчитать индекс кадра мы должны нормализовать время поделив его на длину клипа. Следовательно индекс кадра в клипе будет произведением нормализованного времени на количество кадров. А индекс кадра в текстуре будет суммой сдвига начала текущего клипа и произведением текущего кадра на объем данных хранящийся в этом кадре.


var offset = clipStart + frameIndex * bonesCount * 3.0f

Вот наверное и все передав все данные в шейдер вызываем Graphics.DrawMeshInstancedIndirect с подготовленным мешем и материалом.


Выводы


Тестировании этой техники на машине с видеокартой 1050 показало прирост производительности приблизительно в 2 раза.


image

Анимирование 4000 однотипных объектов на CPU


image

Анимирование 8000 однотипных объектов на GPU


В тоже время тестирование этой сцены на macbook pro 15 с интегрированной видеокартой показывает обратный результат. GPU безбожно проигрывает (приблизительно в 2-3 раза), что неудивительно.


Анимация на видеокарте это еще один инструмент, который может быть использован в вашем приложении. Но как и все инструменты он должен быть использован с умом и к месту.


Ссылки




[GitHub код проэкта]

Спасибо за внимание.


PS: Я новичок в Unity и не знаю всех тонкостей, статья может содержать неточности. Надеюсь исправить их с вашей помощью и разобраться в теме лучше.


Комментарии (38)


  1. profesor08
    18.05.2019 16:44

    Что будет с производительностью, если у каждого объекта будет свое состоянии анимации? И как манипулировать ими? Как определять текущее состояние, полагаться только на тайминги?


    1. panteleymonov
      18.05.2019 20:58

      Через редеринг в текстуру можно хоть интерпретатор языка сделать. На тот случай если нет вычислительного шейдера под рукой.


      1. SadOcean
        18.05.2019 21:09

        Мы делали игру «жизнь» на двух сменяемых текстурах — буфферах.
        Очень бодро бегает.


    1. SadOcean
      18.05.2019 21:08

      С производительностью будет сравнительно плохо, разные объекты будут иметь разные материалы (разные параметры времени анимации в шейдере). Впрочем, не уверен, возможно автор в этой деме делал тоже разные материалы.
      В любом случае будет радикально лучше, чем если делать это на процессоре.
      Определять текущее состояние можно только по таймингу.
      Состояния как такового в этой системе нет — данные о нем теряются, остается только набор анимаций, идущий подряд.
      Но таймер анимации хранится в компоненте и каждый кадр растится процессором и передается в материал. Так как управляется это со стороны процессора, никто не мешает в этом компоненте сохранить диапазоны анимаций и при управлении указывать стейт, и в компоненте доставать из информации о диапазоне время.
      В любом случае, этот компонент должен как то работать с этой информацией — именно он управляет зацикленностью и режимом анимаций, для шейдера это просто число от 0 до 1.


      1. panteleymonov
        18.05.2019 21:17
        +1

        По принципу инстансинга нет необходимости пихать модельки с разными материалами в одну кучу, также при использовании текстурного атласа проблем с материалом не будет. Массив состояний для каждой модели легко считается/обновляется на ComputeShader или через текстуру, и хорошо применим для DrawInstance или DrawProcedural.


        1. SadOcean
          20.05.2019 14:58

          Массив состояний — имеется в виду текстура?

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

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


          1. panteleymonov
            20.05.2019 18:23

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


      1. AndroFARsh Автор
        19.05.2019 16:25

        SadOcean спасибо.

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


        1. SadOcean
          20.05.2019 15:01

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

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

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


      1. IgorMats
        19.05.2019 21:05

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


        1. panteleymonov
          19.05.2019 21:15

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


          1. IgorMats
            19.05.2019 22:08

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


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


            1. panteleymonov
              19.05.2019 22:40

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

              Изначально! Как бы то ни было MaterialPropertyBlock не дает ни каких преимуществ.


        1. SadOcean
          20.05.2019 14:53

          Не знал, что есть такая штука, но ниже отметили, что все равно отдельный DrawCall для отдельного состояния модели.
          Под объектом Я имел в виду модель в своем состоянии, не важно, каким образом она выводится.
          Юнити вроде умеет батчить одинаковые материалы с одинаковыми свойствами, но изменение шейдерного юниформа (а кадр анимации получается передается через него) в любом случае меняет стейт рендера, что обрывает Draw Call.
          Если честно, Я ковырялся в вопросе довольно давно, возможно в новых Api есть методы для изменения свойств материала в рамках одного кола.
          Можно придумать другие хаки, например, прикрепить информацию о сдвиге по времени в пиксели модели. Но не думаю, что это серьезно поможет.
          Так можно добиться, чтобы куча одинаковых моделей использовала разное время одной анимации, но при этом для изменения относительного времени нужно будет перегенерить модель, к тому же получиться больше вершинных данных — по сути каждый сдвиг будет давать новую модель.


        1. SadOcean
          20.05.2019 14:54

          Собственно получается просто разный способ создания инстансов материалов — непосредственно в объектах или оптом через Api.
          Материалы то все равно будут рендериться разные.


  1. Varim
    18.05.2019 19:20
    +1

    В тоже время тестирование этой же сцены на macbook pro 15 с интегрированной видеокартой показывает не завидный результат в пользу GPU(безбожно проигрывает), что неудивительно.
    Предложение так составлено что, возможно, имелось в виду CPU, допустили опечатку? (или CPU безбожно проигрывает?) А насколько проигрывает?


    1. AndroFARsh Автор
      19.05.2019 16:28

      Все верно. Попробую переформулировать, во избежании. GPU работает медленнее приблизительно в 3 раза


      1. panteleymonov
        19.05.2019 17:17

        Частота работы CPU 3 Ггц, частота работы GPU 1 Ггц. Разница 3 раза, это пропорция по отношению к разным система сохраняется. Если встроенный GPU одно ядерный то такое падение естественно. Или где то явно косяк, например очень часто спотыкаются при повторной загрузке данных в GPU которые там уже есть.


  1. ibetsm
    19.05.2019 16:29

    А чего не на compute shader и StructuredBuffer?


    1. AndroFARsh Автор
      19.05.2019 16:33

      Задача была рисовать 100 — 1000 однотипных анимированных объектов.
      ComputeShader нужен для произведения расчетов без отрисовки результата на экран.


      1. ibetsm
        19.05.2019 17:14

        Ты можешь произвести расчет анимации и сохранить результаты в RWStructuredBuffer. Потом на основе этих данных производишь отрисовку


        1. AndroFARsh Автор
          19.05.2019 21:52

          StructiredBuffer поддерживается только ComputeShader-ом. Я не совсем понимаю как это должно работать на ComputeShader. Возможно вы могли бы предоставить пример.
          Спасибо


          1. ibetsm
            19.05.2019 21:56

            Я просто обычно на DX11 кожу так вот там StructuredBuffer доступен и в вершинном и пиксельном шейдере. В Unity тоже должно быть надо включить поддержу SM 5.0


          1. UrsusDominatus
            19.05.2019 23:09

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


  1. Evir
    19.05.2019 23:27

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


    1. SadOcean
      20.05.2019 16:52

      Для всех материалов все равно придется передавать время ведь.
      Все равно каждый кадр будет обновляться на всех материалах.


      1. Evir
        20.05.2019 17:48

        Нет, нужно будет только обновлять один 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…


        1. SadOcean
          20.05.2019 19:21

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

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

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

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


          1. AndroFARsh Автор
            20.05.2019 19:48

            Почему будет 8000 материалов?
            Graphics.DrawMeshInstanced — рисует сетку с одним материалом много раз. На этапе регистрации отрисовки мы передаем позиции, поворот и кадр для каждого обьекта. А дальше уже видеокарта рисует эти обьекты. В вершинной функции выставляем персонажа в нужную позу и точку пространства.

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

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


            1. SadOcean
              21.05.2019 11:44

              Я проверил.

              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 набор всех стейтов отрисовки и юниформов параметров — света, кастомных переменных и прочего.


              1. panteleymonov
                21.05.2019 13:02

                Впрочем, это нельзя сделать только через 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


              1. panteleymonov
                21.05.2019 13:38

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

                Тоже самое и с анимацией. И никаких велосипедов как в статье, фантазий с передачами дополнительных параметров через массивы, и танцев с бубном. Unity делает все сама, даже с поддержкой мобильных устройств!


                1. SadOcean
                  21.05.2019 17:46

                  Да, а вот это уже круто.
                  Ну бубны то есть, просто их осуществляет unity.


                1. Evir
                  21.05.2019 21:44

                  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. Что-то мне это напоминает…


                  1. panteleymonov
                    21.05.2019 21:58

                    О это я разогнался. Но заморачиваться с дополнительными массивами параметров не стоит, все это unity делает самостоятельно, достаточно сделать поддержку инстансинга в шейдере.

                    В статье все это собирается через DrawMeshInstancedIndirect, а в Unity это делается автоматом. Каждый объект может быть со своим материалом базирующемся на одном шейдере.
                    www.youtube.com/watch?v=l3Unh6FE1-s
                    Тоже самое можно провернуть при использовании анимации на GPU.


                    1. panteleymonov
                      21.05.2019 23:40

                      Только вот пока почему то это реализовано через 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);
                          }
                      }


  1. panteleymonov
    19.05.2019 23:49

    Ну и в шейдер как uniform передавать текущее время.
    Unity Shader Variables уже передается Time разных мастей: текущее, от синуса/косинуса, дельта и все это в четырех масштабах.
    не туда тыкнул


    1. Evir
      20.05.2019 17:53

      Да, спасибо, я знаю. Просто я писал более абстрактно, а не касаемо конкретно Unity. Плюс можно попробовать ввести свои единицы времени в попытках отказаться от float. Это может быть сделано в том числе и в попытках компенсировать увеличением размера буфера, которое потребуется при реализации моего варианта «в лоб».Другой вопрос – будет оно стоить того, или нет.