Version: 5.3 (switch to 5.4b)
Приёмы в коде и игровом процессе
Оптимизация скриптов

Оптимизации рендеринга

В данном разделе представлены технические особенности оптимизации рендеринга. В нём показано, как запечь освещение для лучшей производительности и как разработчики игры Shadowgun делали высококонтрастные текстуры с запечённым освещением для создания красивой картинки. Если вы ищете основную информацию об оптимизации игр под мобильные платформы, ознакомьтесь со страницей Графические Методы.

Будьте изобретательны!

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

Перед тем как нырнуть под капот

Сперва прочтите этот вводный материал по написаню шейдеров.

  • Встроенные шейдеры
    • Изучите исходный код встроенных шейдеров. Зачастую, когда вам требуется создать новый отличающийся по функционалу шейдер, можно просто взять и соединить части пары уже готовых шейдеров.
  • Отладка поверхностного шейдера (#pragma debug)
    • Из каждого поверхностного шейдера генерируется CG шейдер, который затем полностью компилируется. Если вы добавите #pragma debug в начало вашего поверхностного шейдера, то когда вы откроете скомпилированный шейдер через инспектор, вы увидите промежуточный CG код. Это удобно для изучения того, как на самом деле рассчитывается определённая часть шейдера, и, кроме того, это может быть удобно для переноса в CG шейдер некоторых аспектов, желаемых от поверхностного шейдера.
  • Включенные в шейдер файлы
    • Большое количество вспомогательного кода для шейдеров включено в каждый шейдер. Обычно этот код не используется, но иногда вы можете встретить шейдеры, в которых вызываются такие методы как WorldReflectionVector, которые нигде не определены. В Unity есть несколько встроенных включаемых в шейдеры файлов, котоые содержат эти вспомогательные методы. Чтобы найти определённую функцию, вам придётся поискать по всем включаемым файлам.
    • Эти файлы являются значительной частью внутренней структуры, используемой Unity для упрощения процесса написания шейдеров. Файлы предоставляют такие вещи как тени в реальном времени, различные типы освещения, карты освещения, поддержка нескольких платформ.
  • Аппаратная документация железу и лучшим практикам написания шейдеров. Однако, учтите, что мы рекомендуем более агрессивно относиться к подсказкам касательно точности плавающей запятой.

Shadowgun в деталях

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

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

Шейдерный код для расчётов в реальном времени против запечённой золотой статуи

// This is the pixel shader code for drawing normal-mapped
// specular highlights on static lightmapped geometry

// 5 texture reads, lots of instructions

SurfaceOutput o;

fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
fixed4 c = tex * _Color;
o.Albedo = c.rgb;

o.Gloss = tex.a;
o.Specular = _Shininess;

o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));

float3 worldRefl = WorldReflectionVector (IN, o.Normal);
fixed4 reflcol = texCUBE (_Cube, worldRefl);
reflcol *= tex.a;
o.Emission = reflcol.rgb * _ReflectColor.rgb;
o.Alpha = reflcol.a * _ReflectColor.a;

fixed atten = LIGHT_ATTENUATION(IN);
fixed4 c = 0;

half3 specColor;
fixed4 lmtex = tex2D(unity_Lightmap, IN.lmap.xy);
fixed4 lmIndTex = tex2D(unity_LightmapInd, IN.lmap.xy);

const float3x3 unity_DirBasis = float3x3( 
float3( 0.81649658,  0.0, 0.57735028),
float3(-0.40824830,  0.70710679, 0.57735027),
float3(-0.40824829, -0.70710678, 0.57735026) );

half3 lm = DecodeLightmap (lmtex);

half3 scalePerBasisVector = DecodeLightmap (lmIndTex);

half3 normalInRnmBasis = saturate (mul (unity_DirBasis, o.Normal));
lm *= dot (normalInRnmBasis, scalePerBasisVector);

return half4(lm, 1);
// This is the pixel shader code for lighting which is
// baked into the texture

// 2 texture reads, very few instructions

fixed4 c = tex2D (_MainTex, i.uv.xy);   

c.xyz += texCUBE(_EnvTex,i.refl) * _ReflectionColor * c.a;

return c;

Reflective Bumped Specular Запечённый свет с отражением

Отрисовка в тексель

Свет, просчитываемый в реальном времени, несомненно выдаёт картинку лучшего качества, но увеличение производительности от запечённой версии просто колоссально. Так как это делается? Для этой цели был создан инструмент редактора под названием Render to Texel. Учтите, что вам потребуется Unity PPRO для использования этого инструмента. Таков процесс запекания освещения в текстуру:

  • С помощью скрипта трансформировать карту нормалей из касательного пространства в мировое пространство.
  • Из скрипта создать карту позиционирования в мировом пространстве.
  • Отрисовать в текстуру полноэкранный проход всей текстуры используя две предыдущие карты с одним дополнительным проходом на один источник освещения.
  • Усреднить результаты с разных выгодных точек для получения чего-то приемлемо выглядящего со всех сторон, или как минимум, с основных углов обзора в вашей игре.

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

  • Создайте нечто классно выглядящее не беспокоясь о производительности.
  • Используйте инструменты на подобие встроенного в Unity lightmapper’а, а также расширения редактора, на подобие Render to Texel и Sprite Packer для запекания освещения во что-то, что очень просто рендерить.
    • Создание собственных инструментов - лучший способ это сделать, вы можете создать идеальный инструмент, специально для решения любой проблемы в вашей игре.
  • Создайте шейдеры и скрипты для модуляции ваших запечёных результатов для придания им в некотором роде “блеска”; привлекающий внимание эффект, для создания иллюзии динамического освещения.

Концепция частоты освещения

Так же как низкие и высокие частоты в аудио-дорожке, у изображений тоже есть высокочастотные и низкочастотные компоненты, и лучше всего по-разному их обрабатывать при отрисовке, аналогично тому, как для стерео используются сабвуферы и динамики верхних частот для полноты звучания. Один из способов визуализации разных частот изображения - использовать фильтр “High Pass” в Photoshop. Filters->Other->High Pass. Если вы ранее работали с аудио, название High Pass покажется вам знакомым. По сути он отсекает все частоты ниже X, параметра, который вы передаёте фильтру. Для изображений, Gaussian Blur (размытие по Гауссу) - эквивалент Low Pass.

Это применимо к графике реального времени, т.к. частота - хороший способ разделения вещей для их обработки. Например, в простой среде с картами освещения, итоговая картинка получается совмещением карты освещения, которая является низкой частотой, с текстурами, которые являются высокой частотой. В Shadowgun, низкочастотный свет быстро применяется к персонажам с помощью зондов освещения (light probes), высокочастотный свет подделывается с помощью простого bumpmapped шейдера с произвольным направлением света.

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

Частота на практике: декомпозиция Shadowgun

  • Верхняя строка
    • Ultra-Low-Frequency Specular Vertex Light (Dynamic) | High Frequency Alpha Channel | Low Frequency Lightmap | High Frequency Albedo
  • Средняя строка
    • Specular Vertex Light * Alpha | High Frequency Additive Details | Lightmap * Color Channel
  • Низ
    • Итоговая сумма

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

Lightmapped with Virtual Gloss Per-Vertex Additive

Shader "MADFINGER/Environment/Virtual Gloss Per-Vertex Additive (Supports Lightmap)" {
Properties {
    _MainTex ("Base (RGB) Gloss (A)", 2D) = "white" {}
    //_MainTexMipBias ("Base Sharpness", Range (-10, 10)) = 0.0
    _SpecOffset ("Specular Offset from Camera", Vector) = (1, 10, 2, 0)
    _SpecRange ("Specular Range", Float) = 20
    _SpecColor ("Specular Color", Color) = (0.5, 0.5, 0.5, 1)
    _Shininess ("Shininess", Range (0.01, 1)) = 0.078125
    _ScrollingSpeed("Scrolling speed", Vector) = (0,0,0,0)
}

SubShader {
    Tags { "RenderType"="Opaque" "LightMode"="ForwardBase"}
    LOD 100



    CGINCLUDE
    #include "UnityCG.cginc"
    sampler2D _MainTex;
    float4 _MainTex_ST;
    samplerCUBE _ReflTex;

    #ifndef LIGHTMAP_OFF
    float4 unity_LightmapST;
    sampler2D unity_Lightmap;
    #endif

    //float _MainTexMipBias;
    float3 _SpecOffset;
    float _SpecRange;
    float3 _SpecColor;
    float _Shininess;
    float4 _ScrollingSpeed;

    struct v2f {
        float4 pos : SV_POSITION;
        float2 uv : TEXCOORD0;
        #ifndef LIGHTMAP_OFF
        float2 lmap : TEXCOORD1;
        #endif
        fixed3 spec : TEXCOORD2;
    };


    v2f vert (appdata_full v)
    {
        v2f o;
        o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

        o.uv = v.texcoord + frac(_ScrollingSpeed * _Time.y);

        float3 viewNormal = mul((float3x3)UNITY_MATRIX_MV, v.normal);
        float4 viewPos = mul(UNITY_MATRIX_MV, v.vertex);
        float3 viewDir = float3(0,0,1);
        float3 viewLightPos = _SpecOffset * float3(1,1,-1);

        float3 dirToLight = viewPos.xyz - viewLightPos;

        float3 h = (viewDir + normalize(-dirToLight)) * 0.5;
        float atten = 1.0 - saturate(length(dirToLight) / _SpecRange);

        o.spec = _SpecColor * pow(saturate(dot(viewNormal, normalize(h))), _Shininess * 128) * 2 * atten;

        #ifndef LIGHTMAP_OFF
        o.lmap = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
        #endif
        return o;
    }
    ENDCG


    Pass {
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        fixed4 frag (v2f i) : SV_Target
        {
            fixed4 c = tex2D (_MainTex, i.uv);

            fixed3 spec = i.spec.rgb * c.a;

            #if 1
            c.rgb += spec;
            #else           
            c.rgb = c.rgb + spec - c.rgb * spec;
            #endif

            #ifndef LIGHTMAP_OFF
            fixed3 lm = DecodeLightmap (tex2D(unity_Lightmap, i.lmap));
            c.rgb *= lm;
            #endif

            return c;
        }
        ENDCG 
    }   
}
}

Lightprobes with Virtual Gloss Per-Vertex Additive

Shader "MADFINGER/Environment/Lightprobes with VirtualGloss Per-Vertex Additive" {
Properties {
    _MainTex ("Base (RGB) Gloss (A)", 2D) = "white" {}
    _SpecOffset ("Specular Offset from Camera", Vector) = (1, 10, 2, 0)
    _SpecRange ("Specular Range", Float) = 20
    _SpecColor ("Specular Color", Color) = (1, 1, 1, 1)
    _Shininess ("Shininess", Range (0.01, 1)) = 0.078125    
    _SHLightingScale("LightProbe influence scale",float) = 1
}

SubShader {
    Tags { "RenderType"="Opaque" "LightMode"="ForwardBase"}
    LOD 100



    CGINCLUDE
    #pragma multi_compile LIGHTMAP_OFF LIGHTMAP_ON
    #include "UnityCG.cginc"
    sampler2D _MainTex;
    float4 _MainTex_ST;


    float3 _SpecOffset;
    float _SpecRange;
    float3 _SpecColor;
    float _Shininess;
    float _SHLightingScale;

    struct v2f {
        float4 pos : SV_POSITION;
        float2 uv : TEXCOORD0;
        float3 refl : TEXCOORD1;
        fixed3 spec : TEXCOORD3;
        fixed3 SHLighting: TEXCOORD4;
    };


    v2f vert (appdata_full v)
    {
        v2f o;
        o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
        o.uv = v.texcoord;

        float3 worldNormal = mul((float3x3)_Object2World, v.normal);        
        float3 viewNormal = mul((float3x3)UNITY_MATRIX_MV, v.normal);
        float4 viewPos = mul(UNITY_MATRIX_MV, v.vertex);
        float3 viewDir = float3(0,0,1);
        float3 viewLightPos = _SpecOffset * float3(1,1,-1);

        float3 dirToLight = viewPos.xyz - viewLightPos;

        float3 h = (viewDir + normalize(-dirToLight)) * 0.5;
        float atten = 1.0 - saturate(length(dirToLight) / _SpecRange);

        o.spec = _SpecColor * pow(saturate(dot(viewNormal, normalize(h))), _Shininess * 128) * 2 * atten;

        o.SHLighting    = ShadeSH9(float4(worldNormal,1)) * _SHLightingScale;

        return o;
    }
    ENDCG


    Pass {
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        fixed4 frag (v2f i) : SV_Target
        {
            fixed4 c    = tex2D (_MainTex, i.uv);

            c.rgb *= i.SHLighting;
            c.rgb += i.spec.rgb * c.a;

            return c;
        }
        ENDCG 
    }   
}
}

Рекомендации

GPU оптимизация: альфа-тестирование

Некоторые GPU, в частности те, что можно найти в мобильных устройствах, испытывают большую нагрузку при альфа-тестировании (или при использовании операций discard и clip в пиксельных шейдерах). Вам следует заменить альфа-тест шейдеры шейдерами с альфа-смешиванием, если это возможно. Там где альфа-тестирования не избежать, вам следует свести к минимуму общее количество альфа-тестируемых пикселей.

Сжатие текстур для iOS

Некоторые изображения, особенно, если используется PVR сжатие текстур для iOS/Android, подвержены визуальным артефактам в альфа-канале. В таких случаях, вам может потребоваться подстройка параметров сжатия PVRT прямо в вашем ПО для работы с изображениями. Вы можете сделать это установив PVR export plugin или с помощью PVRTexTool от компании Imagination Tech, создателей формата PVRTC. Итоговое сжатое изображение с расширением .pvr будет напрямую импортировано редактором Unity и указанные параметры сжатия будут сохранены. Если текстура, сжатая в PVRTC не выдаёт желаемого визуального качества, или вам требуются особенно чёткие изображения (а они могут понадобиться, особенно для GUI), тогда вам следует задуматься об использовании 16-битных текстур вместо 32-битных. Сделав это, вы снизите требования к пропускной способности памяти и к количеству свободного места на диске на половину.

Сжатие текстур для Android

Все устройства под управлением Android с поддержкой OpenGL ES 2.0 также поддерживают формат сжатия ETC1; потому приветствуется использование формата текстур ETC1 в качестве предпочитаемого там где это возможно.

Если целиться на определённую архитектуру графического оборудования, такую как Nvidia Tegra или Qualcomm Snapdragon, имеет смысл использовать проприетарные форматы сжатия, доступные для этих архитектур. Android Market также позволяет фильтровать на основе поддерживаемого формата сжатия текстур, то есть дистрибутив (.apk) с, например, текстурами, сжатыми в DXT, может быть не допущен к скачиванию на устройство, которое не поддерживает такое сжатие.

Упражнение (только для Unity PRO)

Скачайте Render to Texel. Запеките освещение на вашей модели. Прогоните результат через High Pass фильтр в Photoshop. Измените шейдер “Mobile/Cubemapped” shader, включённый в пакет Render to Texel так, чтобы недостающие детали низкочастотного освещения были заменены на вершинный свет.

Приёмы в коде и игровом процессе
Оптимизация скриптов