10 June 2015

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

Game developmentUnity3D
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 компрессией, максимально устранены артефакты сжатия
— Размер билда существенно снизился даже по сравнению с версией со сжатыми атласами, молчу что было бы, если бы мы их не жали
— Чуть усложнился процесс работы с атласами, но я верю, что наступит день и мы придем к полной автоматике
— Уверен, что лишние операции в шейдере добавили циклов расчета видеокарте, но разницы пока никто не увидел

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

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

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

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

Спасибо!
Tags:nguiuiunity3dкомпрессия
Hubs: Game development Unity3D
+12
14.9k 91
Comments 26