10 июня 2015 в 23:42

Работа с компрессией текстур в Unity3D + NGUI recovery mode

Привет Хабровчане!

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

Хотелось бы поделиться разработкой, которую мы успешно внедрили у себя в Heyworks и используем на проекте Pocket Troops (Неудержимые). Проблема, с которой сталкивались и сталкиваются все разработчики, при чем не только работающие с NGUI и Unity3d, но и с другими движками и пакетами, это поиск золотой середины между качеством и весом игровых интерфейсов (впрочем, это не только интерфейсов касается). В этой статье я постараюсь помочь убить двух зайцев одной пулей.

В первую очередь, очень большое спасибо хочу сказать товарищу Leopotam за идею, которую он подал нам в обсуждении к статье, так что это он во всем виноват. Идею мы развили, написали замену NGUIшным шейдерам, а так же кое-какие утилиты, упрощающие жизнь. Я не стану приводить замеры, сколько байт мы выйграли в размере билда, сколько наносекунд потеряли, используя более сложные шейдеры, лишь скажу, что вся проделанная работа того стоила — выйгрыш в качестве арта виден невооруженным глазом. Также отмечу, что несмотря на то что статья привязана к NGUI, описанный здесь подход с успехом можно использовать с любыми, не только UI, текстурами. Нужно будет лишь написать соответствующие шейдеры. Но я тороплю события, ведь начать хотелось с другого.

Атласов у нас в проекте немало. Это я к тому, что выбор между визуальным качеством и размером в билде стоял для нас достаточно остро, особенно на грани 100мб для iOS :)

Вот, к примеру, главный атлас, содержащий все иконки скиллов и общую графику



Так же есть атлас магазина, с иконками магазина, и два атласа улучшений, с иконками улучшений. Итого 4 атласа 2048х2048. И это не предел, как порадовал нас недавно арт-отдел…

В чем сомнений точно не может быть — атласы надо жать. И здесь всплывает первая проблема — PVRTC уверенно поддерживает только iOS, некоторые андроиды его держат, но некоторые — нет, так что будьте готовы к вылетам по памяти, если вдруг какое-то из устройств не поймет PVRTC и станет работать с несжатой текстурой. Собственно, первое время нам приходилось мириться с таким положением дел. Кроме проблем с поддержкой сжатия, PVRTC дает визуальные артефакты на градиентах и переходах в прозрачность. Хотя, для этого у нас был небольшой хак. Берем текстуру, открываем в фотошопе, накладываем сверху новый слой, залитый серым цветом, на него накладываем шум (Add Noise 12.5%), выставляем способ наложения — soft light, едва заметная прозрачность слоя (обычно 20-40%), и применяем маска по альфе текстуры, чтобы шум не применялся на прозрачных участках. (Подробнее и с картинками можно почитать в статье другого работника Heyworks) Но это на 100% не избавляло от шумов, особенно артефакты виднелись на переходе в прозрачность. Приходилось особо «шумные» спрайты выделять в несжатые атласы. Короче, крутились как могли.

Так мы и пришли, а точнее — резко прыгнули, к тому что имеем сейчас. И об этом по-подробнее.

Суть метода, предложенного Leopotam, заключается в том, чтобы выделить альфа канал из текстуры атласа, и применять сжатие уже к RGB текстуре. Это, в первую очередь избавляет нас от шумов, а так же позволяет нам на андроидах использовать ETC сжатие, которое было недоступно для текстур с альфа каналом.

Но что же делать с альфой? Напрашивается первый вариант — использование Alpha8 текстуры (правда ходят слухи что не все iOS устройства адекватно с ней работают, но вроде как это было давно и неправда). Второй вариант — если у нас от 1 до 4 атласов в проекте, мы можем использовать отдельную текстуру, из r, g, b, a каналов которой будет браться информация о прозрачности спрайтов каждого отдельного атласа (если атласов меньше трех — текстура эта будет RGB).

Расположив альфа-каналы наших четырех атласов в одной текстуре, получаем вот такой психодел.


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

Резонный вопрос — а что делать, если атласов — пять и больше? Есть и для этого ресурсы. Ведь альфа-текстуру мы уменьшили в 4 раза, а значит сможем квадратами разложить всего 4х4=16 атласов! Конечно, проделывать такой титанический труд вручную каждый раз, когда хочется изменить один из атласов — дело неблагодарное. Для этого был написал скрипт, который собирает альфа каналы из атласов, раскладывает в альфа текстуре, и настраивает материалы — маску выбора альфа-канала и смещение текстурных координат для каждого отдельного материала. Но есть одна тонкость, связанная с тем, как NGUI работает с атласами. Ведь когда меняется атлас, к примеру добавляется в него новый спрайт, NGUI при перетасовке спрайтов работает не с исходными текстурами, а с их копиями, запеченными в атласе. Но ведь атласы у нас помечены как RGB, а значит если NGUI прочитает спрайты в из атласа, то мы потеряем всю информацию о прозрачности. Тут приходится либо впиливаться в NGUI, чего очень не хотелось бы, либо поступать так, как сделали мы. А сделали мы два метода. Первый, вызывается перед тем, как что-то поменять в атласе. Он помечает все атласы в своем списке как RGBA, дабы NGUI адекватно отработал. Второй метод, «достает» альфа каналы из атласов, раскладывает их в отдельную текстуру, настраивает и ее, и материалы атласов нужным образом, и возвращает атласы снова в RGB.

Да, немного насчет материалов, а точнее шейдеров. Мы взяли NGUIшный шейдер, и немного переписали его, чтобы альфу он читал не из альфа-канала текстуры атласа, а из выбранного с помощью маски определенного канала альфа-текстуры.



Здесь нас постигла еще одна тонкость работы с NGUI. Дело в том, что для отрисовки, казалось бы, одного и того же UI, используются несколько модификаций одного шейдера. Когда вы используете ClippingPanel — способ обрезания UI внутри нее как раз и задает модификацию шейдера, которая будет подставлена. А это SoftClip или AlphaClip (модификации шейдера именуются соответственно), а следовательно эти шейдера также надо подправить. Этот момент мы в первых экспериментах не учли и получили неработающие скроллируемые списки. Кроме того, в более новой версии NGUI вообще по-другому все работает, шейдера нумеруются по принципу, в который я не вникал пока.

Итак, для чего это все?

— Теперь наш UI выглядит как будто его не жали PVRTC и ETC компрессией, максимально устранены артефакты сжатия
— Размер билда существенно снизился даже по сравнению с версией со сжатыми атласами, молчу что было бы, если бы мы их не жали
— Чуть усложнился процесс работы с атласами, но я верю, что наступит день и мы придем к полной автоматике
— Уверен, что лишние операции в шейдере добавили циклов расчета видеокарте, но разницы пока никто не увидел

По ссылке прошу любить и жаловать:

— Скрипты, обрабатывающие атласы и собирающие альфа-текстуру
— Все шейдеры, о которых говорилось в статье
— Ассет для настройки системы — в него нужно накидать материалы атласов, которые вы хотите обработать.

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

Очень надеюсь, что кому-нибудь этот материал будет очень полезен. Жду комментарии, предложения по улучшению, конструктивную критику.

Спасибо!
Андрей Чайко @Tutanhomon
карма
6,0
рейтинг 0,0
Senior Unity3D Developer at Heyworks
Самое читаемое Разработка

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

  • 0
    Шарить сорсы архивом через Dropbox — это просто комбо.
    • 0
      я написал, что чуть позже выложу на битбакет, пока не имею возможности
      • 0
        К чему такая спешка? :) Опубликовали б статью, когда появится возможность выложить куда-нибудь.
        • +1
          Не то чтобы спешка… Просто посчитал что статья и без сорсов имеет ценность, и их можно позже приложить.
  • +1
    1. Альфа-атласы, собранные по 3 текстуры нужно тоже жать в etc1 / pvrtc (просто выбрав compressed) — должно получиться по размеру соизмеримо с уменьшением в 2 раза + RGB16. Проще шейдеры, проще пайплайн.
    2. Собирая в разные материалы мы теряем главное преимущество нгуя над штатной новой недоделкой uGui — это качественный батчинг с гибкой настройкой. Если пожертвовать изменением цвета (tint, подкраска) спрайта, то в нем можно хранить маску выбора альфа-канала путем записи туда опорного цвета (R00, 0G0, 00B). Останется немного подхачить штатные шейдеры на использование i.color во фрагментном шейдере как маски и готово — батчинг сохранен, все поют и танцуют.
    • 0
      1. Пробовал, сильно страдает качество в местах перехода в прозрачность, а так же каналы перемешиваются, в результате непрозрачность появляется там, где ее быть не должно.
      2. Разве? на весь UIDrawcall по прежнему один материал, он не меняется в рантайме, так что все продолжает батчится. С вертексным цветом я работаю как и работал, кроме вышеупомянутой сложности с изменением атласа — работа с НГУИ никак не изменилась.
      • 0
        каналы перемешиваются, в результате непрозрачность появляется там, где ее быть не должно.

        Так порядок упаковки важен :) Но вообще странно, что уменьшение размера текстуры в 2 раза не убивает качество, а сжатие полноразмерной — убивает. Тут важно то, чтобы использовать 3 канала, а не 4. Как для дифуза, так и для альфы.
        на весь UIDrawcall по прежнему один материал, он не меняется в рантайме

        В принципе согласен, но это требует ручной настройки всех виджетов на правильный материал. С подменой цветов и использованием штатной механики атлас-префабов оно получится проще и без настройки внешних материалов.
        • 0
          Хотя ерунду написал про настройку виджетов — префаб атласа должен это на себя взять.
          • 0
            Да, так и есть, материал поменяется только на префабе и нгуи дальше с ним работает :)
    • 0
      >качественный батчинг с гибкой настройкой
      Что имеется в виду? Никакого качественного батчинга я там не наблюдаю. Гибкой настройки как бы тоже. Может я что-то не знаю?
      • 0
        Свойство DepthOrder можно менять для виджета любого уровня вложенности, выводя виджеты одного атласа в одну плоскость (блок по Z) — они будут сбатчены в 1дк. В случае uGui это просто невозможно — рендер смотрит GameObject-ы линейно и последовательно, согласно иерархии. Как только встречается виджет из другого атласа — батч будет прерван и начат новый. Единственное решение для получения такой же оптимизации — не пользоваться иерархией и все держать в одно GameObject, выставляя порядок руками как надо.
        • 0
          Например, 2 кнопочки, задник, иконка, надпись, привязка по левому/правому углу экрана, иконки в отдельном атласе. В случае uGui это будет 6дк, в случае нгуя — 3дк (задникам кнопок ордер=1, иконкам ордер=2, надписям ордер=3). Не пользоваться иерархией — проще не пользоваться новым гуем.
        • 0
          Ну во-первых, точно так же себя ведет и NGUI и при встрече другого материала draw call «разрывается».
          А во-вторых, в моем случае необходимость ручной настройки наоборот напрягает. Вручную можно настроить только простенький UI, а со сложным это становится не просто сложно, а иногда и невозможно. А если учесть, что художник тоже хочет делать UI, но о понятии draw call не знает…
          А прикрутить к UGUI хитрый автоматический батчинг — дело несложное. Главное, что при этом не придется трогать уже сделанный UI.
          • 0
            Ну во-первых, точно так же себя ведет и NGUI и при встрече другого материала draw call «разрывается».

            Ну а во-вторых — разговор был что это настраивается одной циферкой (мы же про гибкость начали говорить, нет?) и есть такая возможность, в отличие от.
            А если учесть, что художник тоже хочет делать UI, но о понятии draw call не знает…

            А это его проблемы. Должен быть технический контроль над артовиками, описаны правила сборки или нечто подобное. Если такого нет ибо пиляется в одно лицо — ну что же, печально, но в случае uGui такой возможности нет в принципе.
            А прикрутить к UGUI хитрый автоматический батчинг — дело несложное. Главное, что при этом не придется трогать уже сделанный UI.

            А вот отсюда поподробнее. Рендер спрайтов разве управляем и полностью представлен пользовательским кодом, куда можно залезть и подхачить? К тому же — это нужно писать + поддерживать все будущие апдейты, юнитеки любят часто ломать то что уже работает (видел стенды для проверки проектов на 6-8 версиях юнити). Я честно пробовал использовать это поделие, но через пару месяцев откатился на нгуй, чему рад несказанно — весь код доступен и может быть пофикшен / можно понять почему проявляется какой-либо side-эффект. Да, там есть косяки с динамическими шрифтами в редакторе на превью, которые автор почему-то игнорирует, но в силу открытости фикс написать оказалось довольно просто за полчаса изучения проблемы.
            • 0
              >Должен быть технический контроль над артовиками
              Ну это лишнее время, а значит и деньги.
              >разговор был что это настраивается одной циферкой
              в разных контекстах эта циферка должна меняться, а значит статически задать это невозможно. Точнее получится неоптимальное решение. И представьте, что вы решили поменять атласы по той или иной причине, и это приведет к необходимости ручной правки всех префабов!
              >А вот отсюда поподробнее
              Я им не занимался, поэтому не скажу. Я имею в виду, что приделать могут и сами Unity, если захотят. К NGUI приделать тоже реально, на самом деле.
              >это нужно писать + поддерживать все будущие апдейты
              Ну так и NGUI приходится сильно-сильно дорабатывать. Правда апдейты к нему реже выходят.
              • 0
                Мы ведь обсуждаем что уже существует, а не сферического коня в вакууме, верно? Так можно дорассуждать, что можно написать все, что угодно и в 1дк :) Ждать что-то полезное от юнитеков — как у моря погоды, от автора нгуя — аналогично. Поведение примерно идентично в дефолтном исполнении, разница в том, что в нгуе уже есть крутилки для тонкой настройки, если кому потребуется и кто умеет это использовать. Собственно, обсуждение этой настройки и было ответом на
                Что имеется в виду? Никакого качественного батчинга я там не наблюдаю. Гибкой настройки как бы тоже. Может я что-то не знаю
                • 0
                  Ну я не могу назвать это качественным батчингом, ибо, как мы выяснили, он там точно такой же, просто надо руками все выставлять.
                  • 0
                    Значит батчинг в uGui вообще «некачественный», раз не предоставляет даже возможности в такой настройке. Тут упор был в настройку, а не в «качество», неверно выразился в том посте.
  • +2
    > выйгрыш в качестве арта виден невооруженным глазом

    А есть ли версия вашей игры про карманных войнов под андройд?
  • 0
    Очень круто и очень хочется тоже использовать этот подход. Но у меня стандартный GUI и стандартный Sprite Packer. Как-то можно использовать это подход в таком случае?
    • 0
      Однозначно ответить сложно, знаю только, что есть т.н. CustomPackerPolicy, и это очень похоже на точку, в которой можно вмешаться в создание атласа. Но я не пробовал.
      Кроме того, сейчас на новом проекте мы решили отложить использование разложения атласов до лучших времен (чтобы не тратить время при пересборке атласов), и просто используем обычную компрессию, но с отключенными мипмапами на атласах — именно второй мип смотрится хуже всего. Ухудшения качества не заметили, но, возможно, дело в том что стилистика арта поменялась. Это я к тому, что описанное в статье — не панацея. Что меня сейчас волнует больше — так это то, что все атласы у нас 2к, и компрессия каждого занимает около 20-ти минут на билд-машине. В итоге игра в хушем случае собирается около 3-х часов. И здесь неплохо бы опробовать способ Leopotam.
      • 0
        Жопа с артефактами сильно от стиля зависит, это понятно. У меня на проектах просто катастрофа ((

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.