В данном разделе представлены технические особенности оптимизации рендеринга. В нём показано, как запечь освещение для лучшей производительности и как разработчики игры Shadowgun делали высококонтрастные текстуры с запечённым освещением для создания красивой картинки. Если вы ищете основную информацию об оптимизации игр под мобильные платформы, ознакомьтесь со страницей Графические Методы.
Иногда оптимизация рендеринга в вашей игре не обходится без тяжёлой работы. Вся предоставляемая Unity структура позволяет быстро получить некоторый рабочий результат, но если вам необходима максимальная производительность на ограниченном железе, то самостоятельная работа над некоторыми вещами и отступление от этой структуры - один из верных путей, при условии, что вы в состоянии привнести ключевое изменение структуры, которое значительно ускорит некоторые вещи. В этом деле вам помогут скрипты для редактора, простые шейдеры и хорошее старомодное производство графики.
First of all, check out this introduction to how shaders are written.
#pragma debug
)
#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.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;
Свет, просчитываемый в реальном времени, несомненно выдаёт картинку лучшего качества, но увеличение производительности от запечённой версии просто колоссально. Так как это делается? Для этой цели был создан инструмент редактора под названием Render to Texel. Учтите, что вам потребуется Unity PPRO для использования этого инструмента. Таков процесс запекания освещения в текстуру:
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:
Так же как низкие и высокие частоты в аудио-дорожке, у изображений тоже есть высокочастотные и низкочастотные компоненты, и лучше всего по-разному их обрабатывать при отрисовке, аналогично тому, как для стерео используются сабвуферы и динамики верхних частот для полноты звучания. Один из способов визуализации разных частот изображения - использовать фильтр “High Pass” в Photoshop. Filters->Other->High Pass. Если вы ранее работали с аудио, название High Pass покажется вам знакомым. По сути он отсекает все частоты ниже X, параметра, который вы передаёте фильтру. Для изображений, Gaussian Blur (размытие по Гауссу) - эквивалент Low Pass.
Это применимо к графике реального времени, т.к. частота - хороший способ разделения вещей для их обработки. Например, в простой среде с картами освещения, итоговая картинка получается совмещением карты освещения, которая является низкой частотой, с текстурами, которые являются высокой частотой. В Shadowgun, низкочастотный свет быстро применяется к персонажам с помощью зондов освещения (light probes), высокочастотный свет подделывается с помощью простого bumpmapped шейдера с произвольным направлением света.
В основном, используя разные методы для рендера разных частот света, например, запечённого против динамического, пообъектного против поуровневого, попиксельного против повершинного и т.д., вы можете создать полноценную картинку на ограниченном железе. Если не говорить о стилистических решениях, обычно правильно иметь хорошее разнообразие цветов или значений как на высоких, так и на низких частотах.
Важно: обычно эти декомпозиции ссылаются на шаги в отложенном освещении, но не в данном случае. Все это делается всего за один проход. Вот два подходящих шейдера, на которых была основана композиция:
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
}
}
}
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, в частности те, что можно найти в мобильных устройствах, испытывают большую нагрузку при альфа-тестировании (или при использовании операций discard и clip в пиксельных шейдерах). Вам следует заменить альфа-тест шейдеры шейдерами с альфа-смешиванием, если это возможно. Там где альфа-тестирования не избежать, вам следует свести к минимуму общее количество альфа-тестируемых пикселей.
Некоторые изображения, особенно, если используется PVR сжатие текстур для iOS/Android, подвержены визуальным артефактам в альфа-канале. В таких случаях, вам может потребоваться подстройка параметров сжатия PVRT прямо в вашем ПО для работы с изображениями. Вы можете сделать это установив PVR export plugin или с помощью PVRTexTool от компании Imagination Tech, создателей формата PVRTC. Итоговое сжатое изображение с расширением .pvr будет напрямую импортировано редактором Unity и указанные параметры сжатия будут сохранены. Если текстура, сжатая в PVRTC не выдаёт желаемого визуального качества, или вам требуются особенно чёткие изображения (а они могут понадобиться, особенно для GUI), тогда вам следует задуматься об использовании 16-битных текстур вместо 32-битных. Сделав это, вы снизите требования к пропускной способности памяти и к количеству свободного места на диске на половину.
Все устройства под управлением Android с поддержкой OpenGL ES 2.0 также поддерживают формат сжатия ETC1; потому приветствуется использование формата текстур ETC1 в качестве предпочитаемого там где это возможно.
Если целиться на определённую архитектуру графического оборудования, такую как Nvidia Tegra или Qualcomm Snapdragon, имеет смысл использовать проприетарные форматы сжатия, доступные для этих архитектур. Android Market также позволяет фильтровать на основе поддерживаемого формата сжатия текстур, то есть дистрибутив (.apk) с, например, текстурами, сжатыми в DXT, может быть не допущен к скачиванию на устройство, которое не поддерживает такое сжатие.
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.