Уменьшение размера файла сборки Android в Unity

Размер сборки — важная характеристика мобильного приложения. Если приложение весит много, оно первым будет удалено при чистке. Также меньший размер может ускорить запуск, установку, скачивание.

Даже пустой проект в Unity весит очень много. Пустой проект под Android с настройками по умолчанию в Unity 2017.1 весит 21637 КБ. Однако его можно очень легко уменьшить до 11952\12412 КБ, указав платформу для компиляции (ARMv7 и x86 соответственно).

По аналогии с этим, можно еще попробовать еще немного уменьшить вес, выбрав Graphic API. Если выбрать OpenGLES2 вместо Auto Graphics API, можно сэкономить еще 236 КБ (11716 вместо 11952). Выгода незначительна и возможна потеря в производительности, так что этого делать я не рекомендую.

Теперь поговорим о содержимом проекта. Рассмотрим 2D игру с большим количеством спрайтов.
Есть вероятность, что многие спрайты будут симметричными по одной или нескольким осям.

Давайте проверим, есть ли автоматическое сжатие на этот случай: скомпилируем сцену с выставленным Sprite Renderer с одной текстурой, например, этой.



ARMv7 билд увеличился с 11952 КБ 12046 КБ, прибавка от пустого билда составляет 94 КБ.

Теперь подготовим половину текстуры:



Поставим два Sprite Renderer с одинаковой позицией, у правого выставим Flip X для отзеркаливания, в настройках Sprite Import Settings укажем Pivot Right для совмещения зеркальных половин. Должен получиться такой же круг как и был раньше. Скомпилируем, посмотрим размер: 12000 КБ, то есть прибавка почти в два раза меньше (48 КБ против 94). Если и есть какое то специальное сжатие, то по умолчанию оно неэффективно.

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

Найдем стандартный шейдер Unity для Sprite Renderer.

Sprites/Default
Shader "Sprites/Default"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)
        [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
    }

    SubShader
    {
        Tags
        { 
            "Queue"="Transparent" 
            "IgnoreProjector"="True" 
            "RenderType"="Transparent" 
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Cull Off
        Lighting Off
        ZWrite Off
        Fog { Mode Off }
        Blend One OneMinusSrcAlpha

        Pass
        {
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile DUMMY PIXELSNAP_ON
            #include "UnityCG.cginc"

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                half2 texcoord  : TEXCOORD0;
            };

            fixed4 _Color;

            v2f vert(appdata_t IN)
            {
                v2f OUT;
                OUT.vertex = mul(UNITY_MATRIX_MVP, IN.vertex);
                OUT.texcoord = IN.texcoord;
                OUT.color = IN.color * _Color;
                #ifdef PIXELSNAP_ON
                OUT.vertex = UnityPixelSnap (OUT.vertex);
                #endif

                return OUT;
            }

            sampler2D _MainTex;

            fixed4 frag(v2f IN) : SV_Target
            {
                fixed4 c = tex2D(_MainTex, IN.texcoord) * IN.color;
                c.rgb *= c.a;
                return c;
            }
        ENDCG
        }
    }
}


Начнем с того, что убедимся, что все работает. Создаем шейдер, копируем в него код, меняем название в коде на Sprites/HorizontalSymmetry. Теперь нужно создать материал и выбрать наш шейдер. Попробуем назначить на Sprite Renderer наш материал. Должен выглядеть как раньше.

Теперь разберем шейдер. Вся магия происходит тут:

fixed4 frag(v2f IN) : SV_Target
{
//tex2D возвращает цвет текстуры с заданными координатами.
//_MainTex - используемая текстура, IN.texcoord - текущие координаты в формате 0..1
      fixed4 c = tex2D(_MainTex, IN.texcoord) * IN.color;
//Умножение r,g,b полученного цвета на a. 
      c.rgb *= c.a;
      return c;
}

Это функция, которая должна вернуть цвет пикселя в указанной точке. Нам даны позиция, текстура, цвет для смешения. Не стоит бояться fixed4: это просто тип данных с 4 float: r,g,b,a.

В первой строчке мы получаем цвет текстуры и после этого умножаем на некий цвет IN.color. Этот цвет — это параметр шейдера, его можно изменить в Sprite Renderer/Color.

Дальше идет домножение цвета на альфу. Связано это с тем, что прозрачность зависит не только от альфы, но и от значения rgb. Для лучшего понимания цветового пространства можно поэкспементировать:

fixed4 frag(v2f IN) : SV_Target
{            	            	            	
      fixed4 c = tex2D(_MainTex, IN.texcoord) * IN.color;
      c.rgb=0.5;
      c.a = 0.0;
      return c;
}

Получаем прозрачную серую текстуру. При rgb = 1 и a = 0 будет непрозрачная белая, при rgb = 0 и a = 0 полностью прозрачная, rgb = 0 и a = 1 будет черным непрозрачным.

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

Эту задачу можно решить в лоб:

fixed4 frag(v2f IN) : SV_Target
{
//Запоминаем в новой переменной координаты пикселя
      fixed2 nIn = IN.texcoord;
//Приводим их к форме 0...2
      nIn.x = nIn.x*2;
//Если значение больше одного, то дает 1..0
      if (nIn.r>1)
            nIn.r = 2-nIn.
//Используем новые координаты текстуры
      fixed4 c = tex2D(_MainTex, nIN.texcoord) * IN.color;
//Умножаем r,g,b полученного цвета на a. 
      c.rgb *= c.a;
      return c;
}

Если немного подумать, то можно сделать решение короче, красивей, быстрей:

fixed4 frag(v2f IN) : SV_Target
{            	
//Получаем необходимые координаты
      IN.texcoord.x = 1-abs(2*IN.texcoord.x-1);
//Получаем цвет по новым координатам
      fixed4 c = tex2D(_MainTex, IN.texcoord) * IN.color;
//Умножаем r,g,b полученного цвета на a. 
      c.rgb *= c.a;
      return c;
}

В качестве упражнения предлагаю читателю решить эту задачу для вертикальной и двойной симметрии.

Иногда могут быть артефакты прозрачности, связанные с тем, что спрайт рисуется по умолчанию по определенному контуру (мешу). Лечится так: Sprite/Import Settings/Mesh type = Full Rect.

Этот способ уже теоретически способен сократить размер используемых текстур в 4 раза. Проверим, как поведет себя билд при четверти спрайта (используя шейдер двойной симметрии). Размер билда — 11978 КБ против 12000 (половина спрайта). Напомню, что пустой проект весил 11952 КБ. То есть, опять получилось уменьшение прибавки почти в два раза (в 3.6 от изначального круга без оптимизации).

Однако, это не предел. В моей игре использовалось большое количество шайб, обладающей радиальной симметрией. Это означает, что достаточно иметь всего одну полоску, для того чтобы задать весь круг! Причем, половинную полоску (радиус, не диаметр).

Подготовим текстуру:


Теперь дело за шейдером. Наша задача — сделать из линии круг.

Можно поступить так: найти удаленность текущей точки от центра и использовать координату текстуры с позицией (1-distance*2, 0). Умножение на два происходит потому что максимальное расстояние от центра будет 0.5, не 1. Вычитаем из единицы потому что текстура подготовлена слева (край круга) направо (центр круга).

Пример реализации:

fixed4 frag(v2f IN) : SV_Target
{            	
      fixed2 nIn = IN.texcoord;
      nIn.x = nIn.x-0.5;
      nIn.y = nIn.y-0.5;
      float dist = sqrt(nIn.x*nIn.x+nIn.y*nIn.y);            
      IN.texcoord.x = 1-dist*2;
      fixed4 c = tex2D(_MainTex, IN.texcoord) * IN.color;
      c.rgb *= c.a;
      return c;
}

Примечание: так как тут вычисляется корень, шейдер будет работать медленнее чем полноценное круговое изображение

Создаем материал, поставим его в Sprite Renderer, поставим Sprite = line, смотрим. Изображение будет очень узким, так что нужно растянуть спрайт (выставить большое значение Trasnform.Scale.y). Должен получиться исходный круг\овал.

Проверим размер билда с новым шейдером и кругом из полоски: получилось 11957 КБ. То есть прибавка от пустого проекта составляет всего лишь 5 КБ, и это включая размер шейдера.

В итоге у нас получился удобный инструмент, с помощью которого в некоторых случаях можно значительно уменьшить размер билда. Он пригоден не только для Android, но и для любой платформы поддерживающей шейдеры.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 13
  • 0

    Можно пойти ещё дальше. Если ваша полоска — это череда пикселей, и эти пиксели повторяются, то можно, например, закодировать вашу зелёную полоску с белым штрихом так:
    5green+2white+4*green
    Короче, ещё и сжать её. Тогда она и килобайта весить не будет.
    А если ещё всё это от каждого спрайта закинуть в атлас...

    • 0
      Если исходная картинка векторная, может и формат использовать нужно соответсвующий. Unity не умеет с векторной графикой работать?
      • 0

        Есть ассеты, которые вроде добавляют поддержку векторной графики, но на деле они просто конвертят кривые Безье в меши.


        Чисто теоретически, вероятно, можно написать шейдер, который рисует вектор (читая PostScript из текстуры?).

      • 0

        Можно пойти ГОРАЗДО дальше и сделать всю графику на шейдерах.


        Вот тут много сцен которые <64 kb. Ссылка

      • +1
        Он пригоден не только для Android, но и для любой платформы поддерживающей шейдеры.

        А потом осознать, что фрагментный шейдер выполняется для каждого пикселя экрана и ужаснуться. Отражение следует делать в вершинном шейдере (вызывается для каждого вертекса — на каждый спрайт будет всего 4 вычисления, а фрагментный останется старым).
        Всякие ухищрения и усложнения фрагментного шейдера — не стоят падения производительности. Разумнее перевести текстуру в грейскейл (маску) и умножать на цвет. Маски можно склеить по 4 штуки в одну текстуру или по 3 + использовать сжатие. Если не устраивает падение качества после сжатия и нужна более точная передача границ — всегда разумнее использовать геометрию с нужной точностью вместо текстур — это еще и филрейт на альфа-блендинге сэкономит, что весьма критично на мобильных платформах.
        В результате статья — как не надо делать.
        • +1

          Не знаком с unity, а потому думал, сто дальше будут хитрые игры с линкером и патчи apk, которые снизят объем до сотен К. А тут какие-то детские игры с картинками...

          • 0
            Стоило бы тогда уж сразу продублировать сюда абзацы из стандартной справки или просто ссылку на неё.
            Про атласы там, параметры обрезки…
            • 0

              А вот какойнить такой полезной статьи для девопсов нет по оптимизации сборки под андроид/иос? :)

              • 0

                Была статья на хабре "размер имеет значение". Особенно интересны комментарии.

            • 0

              Размер важен только когда скачивается приложение из стора. Далее средний пользователь и знать не знает ничего про размер, даже как узнать его. По этому микреж такой, который сильно усложняет продакшн — в топку. Делаем до 100 мб, чтобы качало без вайфая, а остальное грузим бандлами. Сплитим бандлы по разрешению и лимиту оперативы пользователя. Ну естественно, оптимизация важна, особенно если из коробки юзаем GPGS или что-то там ещё. Бывает и в 100 мб не влезает.

              • 0

                Если увижу в сторе три в ряд, которая весит 50 МБ, — сразу в топку.

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