В данном разделе представлены технические особенности оптимизации рендеринга. В нём показано, как запечь освещение для лучшей производительности и как разработчики игры Shadowgun делали высококонтрастные текстуры с запечённым освещением для создания красивой картинки. Если вы ищете основную информацию об оптимизации игр под мобильные платформы, ознакомьтесь со страницей Графические Методы.
Иногда оптимизация рендеринга в вашей игре не обходится без тяжёлой работы. Вся предоставляемая Unity структура позволяет быстро получить некоторый рабочий результат, но если вам необходима максимальная производительность на ограниченном железе, то самостоятельная работа над некоторыми вещами и отступление от этой структуры - один из верных путей, при условии, что вы в состоянии привнести ключевое изменение структуры, которое значительно ускорит некоторые вещи. В этом деле вам помогут скрипты для редактора, простые шейдеры и хорошее старомодное производство графики.
Сперва прочтите этот вводный материал по написаню шейдеров.
#pragma debug
)
#pragma debug
в начало вашего поверхностного шейдера, то когда вы откроете скомпилированный шейдер через инспектор, вы увидите промежуточный CG код. Это удобно для изучения того, как на самом деле рассчитывается определённая часть шейдера, и, кроме того, это может быть удобно для переноса в CG шейдер некоторых аспектов, желаемых от поверхностного шейдера.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 для использования этого инструмента. Таков процесс запекания освещения в текстуру:
Вот как работает лучшая оптимизация графики. Они обходят тонны расчётов производя их в редакторе или до запуска игры. В основном, вам следует действовать так:
Так же как низкие и высокие частоты в аудио-дорожке, у изображений тоже есть высокочастотные и низкочастотные компоненты, и лучше всего по-разному их обрабатывать при отрисовке, аналогично тому, как для стерео используются сабвуферы и динамики верхних частот для полноты звучания. Один из способов визуализации разных частот изображения - использовать фильтр “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;
#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
}
}
}
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, в частности те, что можно найти в мобильных устройствах, испытывают большую нагрузку при альфа-тестировании (или при использовании операций 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, может быть не допущен к скачиванию на устройство, которое не поддерживает такое сжатие.
Скачайте Render to Texel. Запеките освещение на вашей модели. Прогоните результат через High Pass фильтр в Photoshop. Измените шейдер “Mobile/Cubemapped” shader, включённый в пакет Render to Texel так, чтобы недостающие детали низкочастотного освещения были заменены на вершинный свет.