Всем привет!

«Если можешь что-то посчитать на GPU, делай это»
// Конечно в рамках разумного

image

VS

image
Обращаем внимание на разницу в фпс

Начну, пожалуй, с предыстории. Один из наших программистов, решил проверить UI на предмет падения фпс. И мы нашли интересную зависимость, при отключении миникарты фпс поднимался в процентном соотношении. Интересно. Нужно решать проблему. Сразу напишу что про атласы и различные пулы, мы пробовали. И тогда я решил заняться этим вопросом более детально. И тут первая мысль, которая меня посетила, UI использует материал, значит можно все перенести на ГПУ, начнем.

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

Почему выше пример плохо, можно найти в комментариях. Но для ленивых я перечислю.
streeter12 4 июля 2016 в 14:51
Данный метод несмотря на свою простоту имеет явные недостатки.

1. Низкая производительность.
2. Для добавления новых меток надо создавать новые сферы (лишний хлам в префабах).
3. Добавление новых типов меток и их фильтров для различных игроков сильно затруднено.
4. Для смены внешнего вида метки необходимо создавать меш!
5. Лишние объекты на каждой сцене => лишняя сложность => сложнее разработка и поддержка.
6. Сложно тестировать => больше возможных багов (с учетом 3).
7. Реалтаймовая замена типа.
8. Нужно захломлять сцену фейковой подложкой.
9. Как быть с теми кто не должен учитывать вращение? обнулять в Update или таскать через tranform.position.
и т.д.

Нечего не найдя, в итоге начал писать шейдер.

С чего я начал.

1. Нужно как то хранить сами иконки. Атлас подойдет. Теперь нужно найти подходящий для тестов. Взял из игры WOW. там он уже был правильного содержания.

2. Нужно как то пробросить эту информацию в шейдер. Плюс по возможности максимально отказаться от различных UI преобразований, например координаты относительно чего то и тд.

В общем, начнем реализацию.

	
static const int MaxCount = 256;
float4 _Targets[MaxCount];
float _WorldSizeX;
float _WorldSizeY;

Такие поля я объявил в шейдере. Поставил максимально 256 на мапе точек. Остальные будут отбрасываться. float _WorldSizeX поле решило проблему с преобразованиями, я как есть решил кормить Vector3 в шейдер. Не хитрыми махинациями, я получил индексы и нормализованные координаты, и тем самым получил практически то что хотел.

Нюанс: GC считает y иначе снизу в врех. Я конечно сделал обратные значения, чтобы левая верхняя была индексом 0. Но потом решил отказаться от этой затеи. Лучше решать это путем верхнего слоя. Например кодом.

И того у меня были заняты 3 поля из Vector4 (X Z W) чем я занял Y будет позже.

Тесты

Сделав шейдер, я решил проверить разницу. Теперь фпс был на уровне добавление удаления всего одного ui элемента. Скорость возросла существенно, на первых двух скриншотах можно увидеть разницу.

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

protected virtual void Update ()
{
        for (int i = 0; i < list.Length; i++)
        {
            list[i] = new Vector4(0, 0, 0, -1);
        }

        for (int i = 0; i < list.Length &&  i < targets.Count ; i++)
       {
            list[i] = new Vector4(targets[i].x *k.x, targets[i].y, targets[i].z*k.y, targets[i].w);
        }
	    image.GetModifiedMaterial(image.material).SetVectorArray("_Targets", list);
}

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

image

Результат работы. Как было видно, я добавил еще и вращение. Но для того что-бы его использовать следует сделать иконки на достаточном расстоянии друг от друга в атласе. Я не стал выдумывать каких то сложных алгоритмов отсечения в шейдере, гораздо проще все это сделать текстурой.

Тот самый свободный Y, я решил использовать в качестве угла. С ним пришлось отдельно повозится, причина была выше, а именно инверсия координаты Y. Именно из-за вращения я решил использовать все как есть.

fixed a = _Targets[i].y;
					fixed resultX = tX;
					fixed resultY = tY;
					if (a != 0)
					{
						fixed x0 = minX + (sizeX * 0.5);
						fixed y0 = minY + (sizeY * 0.5);
						resultX = x0 + (tX - x0) * cos(a) - (tY - y0) * sin(a);
						resultY = y0 + (tY - y0) * cos(a) + (tX - x0) * sin(a);
					}

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

Выводы

Тест для статьи. Я не стал делать полное соответствие. Это бессмысленно, и поэтому контент отличается. Но чтобы понять разницу, этого более чем предостаточно. Следующим этапом, когда дойдут руки, на аналогичную систему хочу перевести тайловый инвентарь.

Выложу тестовые коды и результаты профайлера. Заполнение спрайтами.

 	protected virtual void Start ()
    {
        tests = new List<RectTransform>();
        float x = 10;
        float y = 10;

	    for (int i = 0; i < 256; i++)
	    {
	        int idx = Random.Range(0, prefabs.Count);

	        GameObject obj = Instantiate(prefabs[idx].gameObject);
	        RectTransform t = obj.GetComponent<RectTransform>();
            t.SetParent(parent);
            t.localPosition = new Vector3(startPosition.x  + x, startPosition.y + y, 0);
	        if (useRotation)
	        {
	            t.rotation = Quaternion.Euler(0, 0, Random.Range(0, 360));
	        }
	        x += step;
	        if (i % 20 == 0)
	        {
	            y += step;
	            x = 0;
	        }
	        tests.Add(t);
        }
	}

Заполнения через прокси класс для шейдера
item.type = Random.Range(0, 64); означает тип иконки

	void Start () {
        tests = new List<Item2>();
        float x = 10;
        float z = 10;

	    for (int i = 0; i < 256; i++)
	    {
            Item2 item = new Item2();
	        item.position = new Vector3(x, 0, z);
	        if (useRotation)
	        {
	            item.rotation = Random.Range(0, 360);
	        }
	        item.type = Random.Range(0, 64);
	        x += step;
	        if (i % 20 == 0)
	        {
	            z += step;
	            x = 0;
	        }
	        tests.Add(item);

        }
	}

Профайлер для шейдера (на сцене еще 3 куба)

image

Профайлер для спрайтов (на сцене еще 3 image в качестве префабов)

image

и напоследок пример с маской:

image

UPD.
Разница с шейдером и без
image
Очень разочаровал один из комментариев, где автор хотел генерировать меш. Видимо автор, не понимает что шейдеры способны посчитать освещение тени отражения и т.д. Что для них стоит посчитать примитивные арифметические выражения? Вместо этого он решил загрузить ЦПУ.

Дополнительно к вопросу про альфу. Так я получил мягкие границы
image

P.S. «Если можешь что-то посчитать на GPU, делай это»

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


  1. VasakaInc
    13.08.2017 19:04

    «Сразу напишу что про атласы и различные пулы, мы пробывали.»

    Пробывали. Проверочное слово — Пробы.


    1. MrPeterLink
      14.08.2017 07:08

      Кто вас научил проверять суффикс окончанием?


      1. VasakaInc
        14.08.2017 08:07

        Это у автора статьи надо спросить. Хотя он уже исправил в тексте.


        1. GlukKazan
          14.08.2017 09:13
          +4

          Вот именно по этой причине об орфографических ошибках пишут в личку.
          Автор ошибку исправил, а недоумение от вашего комментария осталось.


          1. VasakaInc
            14.08.2017 12:48

            Мне больше нравится открыто. Может в перспективе меньше будут пробывать и корованы.


        1. derek_streyt Автор
          14.08.2017 10:30

          Исправил, спасибо.


  1. BIanF
    14.08.2017 02:55

    Это восхитительно…
    Хотя хотелось бы увидеть тестовый проект, чтобы не париться с копированием.


  1. Bagobor
    14.08.2017 07:50
    -1

    Более эффективно — гененерировать меш с иконками, те 4 вершины на иконку. Это позволит экономить шейдерные операции что будет особенно полезно для мобильных платформ.
    (Этот меш обновлять только когда что-то меняется на миникарте, если нужно сохранять размер икононок вне зависимости от маштаба карта — не проблема — размер можно пересчитывать в вершинном шейдере, нужно будет дополнительно передавать размер в дополнительном вершинном UV канале)


    1. derek_streyt Автор
      15.08.2017 10:08

      Я с ваши категорически не согласен.Вы пробовали это вообще? Во время решения задачи, один коллега предложил попиксельно вписывать все это в текстуру. Ваш подход это напомнил. Ваш способ, скорей проиграет способу со спрайтами. Но давайте распишем алгоритм а вы поправите где я не прав.
      1. Нужно генерить меш, скорей всего средствами юнити. Генерим ведь один меш? Верно. Берем 4 вершины, и преобразовываем их для UI, правильно? Иначе что случится при смене расширения? Дополнительно угол, верно?
      2. Теперь нормали, ведь вы не хотите чтобы из-за нормалей меш было части полигонов не отображались, верно?
      3. Теперь нужно наложить mat? И не простой, а семейства UI, иначе будут проблемы с масками.
      4. Mat наложили, но он белый? Следовательно теперь нужно накинуть UV. Сложность этого алгоритма, боюсь даже представить.
      5. Теперь все это запускаем, у нас на миникарте все двигаются. Что нужно сделать, с шага 1 все повторить и так каждый update. Ведь вряд ли возможен случай когда на миникарте не кто не двигается.
      Можете сделать тест, против обычных спрайтов. Я например сильно сомневаюсь, что это хоть как-то эффективно


  1. ian_phobos
    14.08.2017 08:40

    Спасибо за статью. Не очень понятно почему вы называете шейдером c# код и метод Update. Можно подробнее что метод делает? Я так понимаю, выдергивает изображение из атласа? По тексту все вроде понятно, а вот разобраться в коде не получилось…


    1. ian_phobos
      14.08.2017 08:43

      И можно ещё подробнее про смягчение альфа канала на границах?


      1. derek_streyt Автор
        14.08.2017 10:28

        Через моно я в шейдер кормлю информацию о мире. Альфа на границах дает обводку, вот я ее и смягчаю через интерполяцию с базовой текстурой.


  1. vesper-bot
    14.08.2017 09:34

    Что за странные артефакты на границах у круглой маски? Где-то баг? :)


    1. derek_streyt Автор
      14.08.2017 10:26

      Границы, это пробел между текстурами. Он в выбранном атласа не очень удачный.


  1. BathPirate
    14.08.2017 10:31
    +1

    Годнота! Только сам шэйдер не нашел — забыли прикрепить или я плохо искал?


  1. JKot
    14.08.2017 10:31

    А давайте посчитаем фпс в реальной ситуации, а не в сферических конях.

    Предположим ваша игра со стандартным способом рендеринга миникарты карты выдаёт 60 фпс — т.е время кадра ~ 16,66ms (из них 1.9 занимает вывод миникарты) теперь заменяем алгоритм на ваш получается время кадра 15,56ms т.е 64 FPS. Получили прирост 4 fps. Расчёты грубые, но в реальности будет различие ещё меньше. К тому-же мини карта на то и мини — чтобы висеть в правом верхнем углу и содержать адекватное размерам количество информации (минимальное), т.е разница ещё меньше будет. А то, что вы показываете на скринах, это полноэракранная карта и при ещё отображении можно вообще отключить рендеринг основной сцены и получить огромный fps используя любую технику отображения.

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


    1. derek_streyt Автор
      14.08.2017 10:34

      Мы на реально и оптимизировали. С этого все и началось. Реальный выложить не могу, НДА, вот поэтому собрал на скорую руку тестовые сцены. Если бы у нас разница была в рн 5 10 фпс, думаете мы бы занимались подобным?


      1. JKot
        14.08.2017 15:37

        Жаль, что нельзя получить результаты в более реалистичной ситуации. Очень интересно было бы глянуть. P.S: Очень странное NDA, которое распространяется на абстрактную статистику вроде FPS. Но, вам виднее)


        1. derek_streyt Автор
          14.08.2017 15:41

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


  1. nxrighthere
    14.08.2017 10:34

    Спасибо за статью, пригодится.

    Мне нравится как у вас в редакторе отображается иерархия объектов в сцене, это какое-то дополнение?


    1. derek_streyt Автор
      14.08.2017 10:38

      плагин QHierarchy