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

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

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

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

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

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

First of all, check out this introduction to how shaders are written.

  • Built in shaders
    • Examine the source code of the built in shaders. Often, if you want to make a new shader that does something different, you can achieve it by taking parts of two already-existing shaders and putting them together.
  • Отладка поверхностного шейдера (#pragma debug)
    • A CG Shader is generated from every surface shader, and then fully compiled from there. If you add #pragma debug to the top of your surface shader, when you open the compiled shader via the inspector, you can see the intermediate CG code. This is useful for inspecting how a specific part of a shader is actually calculated, and it can also be useful for grabbing certain aspects you want from a surface shader and applying them to a CG shader.
  • Shader Include Files
    • A lot of shader helper code is included in every shader, and usually it isn’t used, but this is why you will sometimes see shaders calling functions like WorldReflectionVector which don’t seem to be defined anywhere. Unity may has several built-in shader include files that contain these helper definitions. To find a specific function, you will need to search through all of the different includes.
    • These files are a major part of internal structure that Unity uses to make it easy to write shaders; the files provide things like real time shadows, different light types, lightmaps, and multiple platform support.
  • Hardware documentation
    • Take your time to study Apple documentation on best practices for writing shaders. Note that we would suggest to be more aggressive with floating point precision hints however.

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 Baked Light with Reflection

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

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

  • С помощью скрипта трансформировать карту нормалей из касательного пространства в мировое пространство.
  • Create a world space position map via script.
  • Render to Texture a fullscreen pass of a the entire texture using the two previous maps, with one additional pass per light.
  • Average results from several different vantage points to yield something which looks plausible all around, or at least from common viewing angles in your game.

This is how the best graphics optimizations work. They sidestep tons of calculations by preforming them in the editor or before the game runs. In general, this is what you want to do:

  • Создайте нечто классно выглядящее не беспокоясь о производительности.
  • Используйте инструменты на подобие встроенного в Unity lightmapper’а, а также расширения редактора, на подобие Render to Texel и Sprite Packer для запекания освещения во что-то, что очень просто рендерить.
    • Making your own tools is the best way to do this, you can create the perfect tool suited for whatever problem your game presents.
  • Create shaders and scripts which modulate your baked output to give it some sort of “shine”; an eye-catching effect to create an illusion of dynamic light.

Concept of Light Frequency

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

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

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

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

  • Top Row
    • Ultra-Low-Frequency Specular Vertex Light (Dynamic) | High Frequency Alpha Channel | Low Frequency Lightmap | High Frequency Albedo
  • Mid Row
    • Specular Vertex Light * Alpha | High Frequency Additive Details | Lightmap * Color Channel
  • Bottom
    • Final Sum

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

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;

    #ifdef LIGHTMAP_ON
    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;
        #ifdef LIGHTMAP_ON
        float2 lmap : TEXCOORD1;
        #endif
        fixed3 spec : TEXCOORD2;
    };


    v2f vert (appdata_full v)
    {
        v2f o;
        o.pos = UnityObjectToClipPos(v.vertex);

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

        float3 viewNormal = UnityObjectToViewPos(v.normal);
        float3 viewPos = UnityObjectToViewPos(v.vertex);
        float3 viewDir = float3(0,0,1);
        float3 viewLightPos = _SpecOffset * float3(1,1,-1);

        float3 dirToLight = viewPos - 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;

        #ifdef LIGHTMAP_ON
        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

            #ifdef LIGHTMAP_ON
            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_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 = UnityObjectToClipPos(v.vertex);
        o.uv = v.texcoord;

        float3 worldNormal = UnityObjectToWorldDir(v.normal);       
        float3 viewNormal = UnityObjectToViewPos(v.normal);
        float4 viewPos = UnityObjectToViewPos(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)

Download Render to Texel. Bake lighting on your model. Run the High Pass filter on the result in Photoshop. Edit the “Mobile/Cubemapped” shader, included in the Render to Texel package, so that the missing low-frequency light details are replaced by vertex light.

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