本部分将介绍渲染优化的技术细节,展示如何烘焙光照结果以获得更好的性能,并说明《暗影之枪》(Shadowgun) 的开发人员如何借助烘焙的光照来利用高对比度纹理,使他们的游戏具有出色的视觉效果。如果想简单了解进行移动端优化后的游戏的视觉效果,请查看图形方法页面。
有时,优化游戏中的渲染需要做一些辛苦的工作。Unity 提供的所有结构可轻松实现游戏的快速运行,但如果您需要在有限的硬件上实现顶级保真度,一种可行的办法是由您自己完成某些工作并回避这种结构,但前提是您能带来使运行速度大幅提高的重要结构变化。这种情况下可选择的工具包括编辑器脚本、简单着色器以及出色的老式艺术制作方法。
首先,请了解此处关于如何编写着色器的介绍。
#pragma debug
)
#pragma debug
添加到表面着色器之上,当通过检视面板打开编译后的着色器时,即可看到中间的 CG 代码。这对于检查着色器特定部分的实际计算方式非常有用,而且对于从表面着色器中获取所需的某些方面并将这些方面应用于 CG 着色器也很有用。考虑到运行的基础硬件,Shadowgun 在图形方面取得了伟大成就。美术质量似乎是解决谜题的关键,但实现这样的质量有一些技巧,程序员可通过这些技巧来最大限度提升美术师的潜力。
在图形方法页面中,我们使用了 Shadowgun 的金色雕像作为优化的例子;他们仅将光照细节烘焙到纹理中,而不是使用法线贴图提供雕像的某种可靠定义。在这里,我们将向您展示如何以及为何您应该在自己的游戏中使用类似的技术。
// 这是用于在静态光照贴图几何体上绘制
// 法线贴图镜面高光的像素着色器代码
// 5 次纹理读取,大量指令
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);
// 这是用于烘焙到纹理中的
// 光照的像素着色器代码
// 2 次纹理读取,很少指令
fixed4 c = tex2D (_MainTex, i.uv.xy);
c.xyz += texCUBE(_EnvTex,i.refl) * _ReflectionColor * c.a;
return c;
实时光照的质量肯定更高,但烘焙版本的性能提升幅度却很大。所以,这是怎么做的呢?为此,创建了一个名为 Render to Texel 的编辑器工具。此工具通过以下过程将光照烘焙到纹理中:
这就是最佳图形优化的工作原理。这些优化通过在编辑器中或在游戏运行之前执行计算来回避大量的计算工作。一般来说,您需要执行的操作如下:
就像音频轨道的低音和高音一样,图像也有高频和低频分量,在进行渲染时,最好以不同的方式处理它们,类似于立体声音响使用超低音扬声器和高音扬声器产生完整的声音。一种将图像的不同频率进行可视化的方法是使用 Photoshop 中“高通”(High Pass) 滤镜:Filters > Other > High Pass。如果您以前做过音频工作,应该知道 High Pass 这个名称。在本质上,其作用是截断低于 X(传递给滤镜的参数)的所有频率。在图像方面,高斯模糊 (Gaussian Blur) 相当于低通 (Low Pass)。
这种机制适用于实时图形,因为频率是分离信号并确定如何处理分量的好方法。例如,在基本光照贴图环境中,最终图像是通过低频光照贴图和高频纹理的合成获得的。在 Shadowgun 中,他们通过光照探针快速将低频光照应用于角色,并使用具有任意光线方向的简单凹凸贴图着色器来伪造高频光照。
通常情况下,通过使用不同的方法来渲染不同频率的光照(例如,烘焙与动态光照、每对象与每级别光照、每像素与每顶点光照等),可在有限的硬件上创建完整的图像。除了风格方面的选择之外,尝试在高频和低频都采用强烈变化的颜色或值通常是一种很好的想法。
注意:通常这些分解是指延迟渲染器中的步骤,但这里不是这种情况。所有这一切都只在一个通道中完成。下面是这种合成基于的两个相关着色器:
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(尤其是移动设备中的 GPU)在 Alpha 测试时会产生高性能开销(或在像素着色器中使用丢弃和裁剪操作)。如果可能,应使用 Alpha 混合着色器替代 Alpha 测试着色器。在无法避免 Alpha 测试的情况下,应将可见的 Alpha 测试像素的总数保持在最低水平。
有些图像(尤其是使用 iOS/Android PVR 纹理压缩时)容易在 Alpha 通道中出现视觉瑕疵。在这种情况下,可能需要直接在图像处理软件中调整 PVRT 压缩参数。为进行此操作,可安装 PVR 导出插件或使用 PVRTC 格式创建者 Imagination Tech 的 PVRTexTool。生成的扩展名为 .pvr 的压缩图像文件将由 Unity Editor 直接导入,并会保留指定的压缩参数。如果 PVRT 压缩纹理不能提供足够好的视觉质量,或者您需要特别清晰的成像(对于 GUI 纹理可能会有这样的要求),则应考虑使用 16 位纹理而不是 32 位纹理。通过这样做,内存带宽和存储要求将减半。
所有支持 OpenGL ES 2.0 的 Android 设备也支持 ETC1 压缩格式;因此,鼓励尽可能使用 ETC1 作为首选纹理格式。
如果面向特定的图形架构(如 Nvidia Tegra 或 Qualcomm Snapdragon),也许可以考虑使用这些架构上的专有压缩格式。Android Market 还允许基于支持的纹理压缩格式进行过滤,也就是说,如果设备不支持某种纹理压缩格式,则可以阻止该设备下载具有该纹理压缩格式(例如 DXT 压缩纹理)的分发存档 (.apk)。
下载 Render to Texel。 在模型上烘焙光照。 在 Photoshop 中对结果运行 High Pass 滤镜。 编辑 Render to Texel 包中提供的“Mobile/Cubemapped”着色器,使丢失的低频光照细节被顶点光照取代。