Размер сборки — важная характеристика мобильного приложения. Если приложение весит много, оно первым будет удалено при чистке. Также меньший размер может ускорить запуск, установку, скачивание.
Даже пустой проект в Unity весит очень много. Пустой проект под Android с настройками по умолчанию в Unity 2017.1 весит 21637 КБ. Однако его можно очень легко уменьшить до 1195212412 КБ, указав платформу для компиляции (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.
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, но и для любой платформы поддерживающей шейдеры.
Автор: GrigorGri