Unity: сжимая сжатое
Результат: информация о цвете занимает 1/64 от исходной площади при достаточно высоком качестве результата. Тестовое изображение взято с этого сайта.
Текстуры практически всегда являются наиболее значимым потребителем места как на диске, так и в оперативной памяти. Сжатие текстур в один из поддерживаемых форматов относительно помогает в решении этой проблемы, но что делать, если даже в этом случае текстур очень много, а хочется еще больше?
История началась примерно полтора года назад, когда один гейм-дизайнер (назовем его Akkelman) в результате экспериментов с различными режимами смешивания слоев в photoshop обнаружил следующее: если обесцветить текстуру и поверх наложить ту же текстуру в цвете, но в 2–4 раза меньшего размера с установкой режима смешивания слоев в «Color», то картинка будет довольно сильно походить на оригинал.
Особенности хранения данных
В чем смысл такого разделения? Черно-белые изображения, содержащие по сути яркость исходной картинки (далее по тексту — «грейскейлы», от англ. «grayscale»), содержат только интенсивность и могут быть сохранены в одной цветовой плоскости каждое. То есть, в обычную картинку без прозрачности, имеющую 3 цветовых канала R, G, B мы можем сохранить 3 таких «грейскейла» без потери места. Можно использовать и 4 канал — A (прозрачность), но с ним на мобильных устройствах большие проблемы (на андроиде с gles2 нет универсального формата, поддерживающего сжатие RGBA-текстур, качество при сжатии сильно ухудшается и тп), поэтому для универсальности будет рассматриваться только 3-канальное решение. Если это реализовать, то мы получим практически 3-кратное сжатие (+ несоизмеримо малую по размеру «цветовую» текстуру) для уже сжатых текстур.
Оценка целесообразности
Можно примерно оценить выгоду от применения такого решения. Пусть у нас есть поле 3×3 из текстур разрешением 2048×2048 без прозрачности, каждая из которых сжата в DXT1 / ETC1 / PVRTC4 и имеет размер 2.7Мб (16Мб без сжатия). Суммарный размер занимаемой памяти равен 9×2.7Мб = 24.3Мб. Если мы сможем извлечь цвет из каждой текстуры, уменьшим размер этой «цветной» карты до 256×256 и размером в 0.043Мб (выглядит это вполне сносно, то есть достаточно хранить 1/64 часть от общей площади текстуры), а полноразмерные «грейскейлы» упакуем по 3 штуки в новые текстуры, то получим примерный размер: 0.043Мб * 9 + 3×2.7Мб = 8.5Мб (размер оценочный, с округлением в большую сторону). Таким образом, мы можем получить сжатие в 2.8 раза — звучит довольно неплохо, учитывая ограниченные аппаратные возможности мобильных устройств и неограниченные желания дизайнеров / контентщиков. Можно либо сильно уменьшить потребление ресурсов и время загрузки, либо накинуть еще контента.
Первая попытка
Ну что же, пробуем. Быстрый поиск выдал готовый алгоритм / реализацию метода смешивания «Color». После изучения его исходников волосы зашевелились по всему телу: порядка 40 «бранчей» (условных ветвлений, которые негативно сказываются на производительности на не совсем топовом железе), 160 alu инструкций и 2 текстурных выборки. Такая вычислительная сложность — это достаточно много не только для мобильных устройств, но и для десктопа, то есть совсем не подходит для реалтайма. Об этом было рассказано дизайнеру и тема была благополучно закрыта / забыта.
Вторая попытка
Пару дней назад эта тема всплыла снова, было решено дать ей второй шанс. Нам не нужно получить 100% совместимость с реализацией photoshop-а (у нас нет цели смешивать несколько текстур в несколько слоев), нам нужно более быстрое решение с визуально похожим результатом. Базовая реализация выглядела как двойная конвертация туда-обратно между пространствами RGB / HSL с расчетами между ними. Рефакторинг привел к тому, что сложность шейдера упала до 50 alu и 9 «бранчей», что уже было как минимум в 2 раза быстрее, но все же недостаточно. После запроса помощи зала, товарищ wowaaa выдал идею, как можно переписать кусок, генерирующий «бранчинг», без условий, за что ему большое спасибо. Часть вычислений по условию было вынесено в lookup-текстуру, которая генерировалась скриптом в редакторе и потом просто использовалась в шейдере. В результате всех оптимизаций сложность упала до 17 alu, 3 текстурных выборок и отсутствия «бранчинга».
Вроде как победа, но не совсем. Во-первых, такая сложность — все равно чрезмерна для мобильных устройств, нужно как минимум раза в 2 меньше. Во-вторых, все это тестировалось на контрастных картинках, заполненных сплошным цветом.
Пример артефактов (кликабельно): слева ошибочный, справа — эталонный варианты
После тестов на реальных картинках с градиентами и прочими прелестями (фотографии природы) выяснилось, что данная реализация очень капризна к комбинации разрешения «цветной» карты с настройками mipmap-ов и фильтрации: появлялись очевидные артефакты, вызванные смешиванием данных текстур в шейдере и ошибками округления / сжатия самих текстур. Да, можно было использовать текстуры без сжатия, с POINT-фильтрацией и без сильного уменьшения размера «цветной карты», но тогда этот эксперимент терял всякий смысл.
Третья попытка
И тут помогла очередная помощь зала. Товарищ, любящий «графоний, некстген, вот это все» и любящий читать все доступные изыскания по этой теме (назовем его Belfegnar) предложил другое цветовое пространство — YCbCr и выкатил исправления к моему тестовому стенду, поддерживающие его. В результате сложность шейдера с ходу упала до 8 alu, без «бранчинга» и lookup-текстур. Также мне были скинуты ссылки на исследования с формулами всяких мозговитых математиков, проверявших разные цветовые пространства на возможность / целесообразность их существования. Из них были собраны варианты для RDgDb, LDgEb, YCoCg (можно «погуглить», найдется только последний, первые 2 можно найти по ссылкам: sun.aei.polsl.pl/~rstaros/index.html, sun.aei.polsl.pl/~rstaros/papers/s2014-jvcir-AAM.pdf). RDgDb и LDgEb основаны на одном базовом канале (использовался в качестве полноразмерного «грейскейла») и отношению двух оставшихся каналов к нему. Человек плохо воспринимает разницу в цвете, но достаточно хорошо определяет разницу в яркости. То есть при сильном сжатии «цветной» карты терялся не только цвет, но и контраст — качество сильно страдало.
«Цветная» карта — упакованные данные (CoCg) содержатся в RG-каналах, B-канал пустой (может быть использован для пользовательских данных).
В результате «победил» YCoCg — данные основаны на яркости, хорошо переносят сжатие «цветной» карты (на сильном сжатии «мылятся» сильнее, чем YCbCr — у того «картинка» лучше сохраняет контраст), сложность шейдера меньше, чем у YCbCr.
После базовой реализации опять начались танцы с бубном ради оптимизации, но в этом я не сильно преуспел.
Итог
Еще раз картинка с результатом: разрешение цветной текстуры можно менять в широких пределах без ощутимых потерь в качестве.
Эксперимент прошел достаточно удачно: шейдер (без поддержки прозрачности) со сложностью 6 alu и 2 текстурными выборками, 2.8х сжатие по памяти. В каждом материале можно указывать цветовой канал из «грейскейл»-атласа, который будет использован в качестве яркости. Точно также для шейдера с поддержкой прозрачности выбирается цветовой канал «грейскейл»-атласа для использования в качестве альфы.
Исходники: Github
Лицензия: CC BY-NC-SA 4.0.
Все персонажи являются вымышленными и любое совпадение с реально живущими или когда-либо жившими людьми случайно. Ни один дизайнер в ходе этого эксперимента не пострадал.