第10章 移动平台着色器优化实战从简化到高级技巧随着移动设备性能的飞速提升手机游戏的画质已经接近PC水平。然而移动GPU与桌面GPU在架构、带宽、功耗等方面存在显著差异。要在移动平台上实现流畅且绚丽的画面必须针对移动硬件进行专门的着色器优化。本章将带你深入理解移动GPU的工作特性从基础优化原则到高级技巧通过实际代码示例学习如何为移动设备创建既高效又美观的着色器。我们将从场景准备开始使用性能分析工具定位瓶颈逐步构建简化版和高级版移动着色器并探讨商业游戏中的优化案例。10.1 场景准备与性能分析工具在着手优化之前我们需要一个用于测试的场景和准确的性能数据。打开Unity 2021.3.8f1c1新建一个场景命名为“MobileOptimization”。为了模拟真实游戏环境在场景中添加一个地面平面覆盖简单纹理。几个复杂模型如角色、道具数量适中。2-3个动态光源一个方向光一个点光源。一些粒子系统或透明物体增加渲染压力。为了聚焦着色器优化我们可以在场景中放置多个使用不同着色器的材质球便于对比。10.1.1 移动GPU架构概览移动GPU如ARM Mali、Qualcomm Adreno、Apple GPU与桌面GPUNVIDIA、AMD在设计理念上有本质区别基于分块渲染Tile-Based Rendering移动GPU将屏幕划分为多个小方块tiles在片元着色器处理前先进行几何处理并将数据存入片上内存然后再对每个tile执行片元着色。这种设计减少了对外部显存的带宽需求但增加了对几何复杂度的敏感度。统一着色器单元顶点和片元着色器共用计算单元但通常数量较少主频较低。带宽瓶颈移动设备的显存带宽远低于桌面因此纹理采样、渲染目标切换等操作代价高昂。功耗限制复杂计算会导致发热和降频因此需要平衡效果与能耗。理解这些特性有助于我们做出正确的优化决策。10.1.2 使用性能分析工具定位瓶颈在Unity中我们主要使用以下工具Frame Debugger逐帧查看Draw Call分析渲染状态、使用的着色器、纹理等。可以快速发现过度绘制Overdraw和不必要的渲染特性。ProfilerCPU和GPU耗时分析。切换到GPU Profiler需要设备支持可以查看顶点处理、片元处理、渲染目标切换等各项耗时。RenderDoc可选更底层的GPU调试工具可查看每个draw的详细着色器指令。将场景运行在目标移动设备上通过Build Run使用Profiler记录一帧的数据。重点关注SetPass Calls着色器切换次数过多可能意味着材质未合理合批。填充率Fillrate如果片元着色器过于复杂会导致GPU片元处理时间长。纹理采样次数在Profiler中查看“Texture Count”或通过RenderDoc分析。通过分析我们可以确定当前场景的主要瓶颈是几何复杂顶点多还是片元复杂填充率或纹理采样或是带宽限制纹理过大、RT切换频繁。10.2 移动设备着色器优化基础原则在开始编写着色器之前我们需要牢记以下优化原则它们将贯穿整个开发过程。10.2.1 带宽优化纹理压缩与采样使用纹理压缩移动平台必须使用压缩纹理格式如ASTC、ETC2、PVRTC。在Unity中可通过纹理导入设置选择合适的格式。压缩纹理不仅节省显存还能减少带宽。减少纹理采样次数在片元着色器中每多一次纹理采样就多一次显存读取。尽量合并多张纹理例如将高光、光滑度、环境遮蔽打包到一张纹理的RGBA通道。考虑使用纹理数组Texture Array减少采样器切换。Mipmap为所有纹理开启Mipmap可以根据距离自动选择合适的层级减少远处物体的纹理采样带宽。同时Mipmap还有助于减少纹理缓存抖动。10.2.2 ALU优化精度与指令使用half精度在移动着色器中尽量使用half、fixed代替float。half16位和fixed11位在移动GPU上通常使用更少的寄存器执行更快。但要注意精度范围避免数值溢出。例如颜色、UV、法线等可用half。避免复杂数学函数pow、sin、cos、log等函数在移动GPU上可能较慢尽量用简单运算替代。例如用x*x代替pow(x,2)用tex2D查找表LUT代替复杂函数。减少分支着色器中的动态分支if-else可能导致线程束发散降低并行效率。尽量使用step、lerp等函数实现条件选择。向量化尽量使用向量运算如float3、float4替代标量运算可以充分利用GPU的SIMD单元。10.2.3 渲染状态优化合批尽可能使用静态合批Static Batching或动态合批Dynamic Batching减少Draw Call。材质使用相同的着色器变体并尽量使用MaterialPropertyBlock而非创建新材质。减少渲染目标切换避免频繁切换RenderTexture尽量在一次pass中完成更多工作。使用合适的混合模式不必要的混合会增加带宽尽量使用不透明材质透明材质应严格控制重叠。10.3 创建简化的移动着色器基于以上原则我们从最基础的漫反射着色器开始逐步简化并优化。目标是创建一个在所有移动设备上都能高效运行的着色器同时保持基本的视觉效果。10.3.1 标准着色器的性能分析Unity的标准着色器Standard功能强大但包含大量特性如PBR、多光源、阴影、光照贴图、实时GI等这些特性在移动平台上往往过剩。通过Frame Debugger可以发现标准着色器可能包含数十条指令和多次纹理采样。对于移动平台我们应该裁剪掉不需要的特性。10.3.2 自定义简化漫反射着色器下面是一个简化的漫反射着色器仅支持单方向光、纹理、颜色和简单的透明度。它使用half精度减少了纹理采样次数仅一次没有高光和复杂光照。ShaderMobile/SimpleDiffuse{Properties{_MainTex(Texture,2D)white{}_Color(Color,Color)(1,1,1,1)}SubShader{Tags{RenderTypeOpaque}LOD150Pass{Tags{LightModeForwardBase}CGPROGRAM#pragmavertex vert#pragmafragment frag#pragmamulti_compile_fwdbase#includeUnityCG.cginc#includeLighting.cgincstructappdata{float4 vertex:POSITION;float2 uv:TEXCOORD0;float3 normal:NORMAL;};structv2f{float4 pos:SV_POSITION;float2 uv:TEXCOORD0;half3 worldNormal:TEXCOORD1;float3 worldPos:TEXCOORD2;};sampler2D _MainTex;half4 _Color;v2fvert(appdata v){v2f o;o.posUnityObjectToClipPos(v.vertex);o.uvv.uv;o.worldNormalnormalize(UnityObjectToWorldNormal(v.normal));o.worldPosmul(unity_ObjectToWorld,v.vertex).xyz;returno;}fixed4frag(v2f i):SV_Target{half4 textex2D(_MainTex,i.uv)*_Color;half3 normalnormalize(i.worldNormal);half3 lightDirnormalize(_WorldSpaceLightPos0.xyz);// 假设方向光half NdotLmax(0,dot(normal,lightDir));half3 diffusetex.rgb*_LightColor0.rgb*NdotL;// 环境光简化half3 ambienttex.rgb*UNITY_LIGHTMODEL_AMBIENT.rgb;returnhalf4(diffuseambient,tex.a);}ENDCG}}FallbackDiffuse}优化点使用half和fixed类型存储颜色、法线等。仅一次纹理采样。无高光计算无多光源支持。使用LightMode标签仅用于Base Pass多光源会被忽略或需额外Pass。将此着色器应用于场景中的物体通过Profiler对比标准着色器可以发现GPU时间显著降低。10.3.3 进一步简化移除法线计算如果物体是纯平面或不需要法线可以进一步简化在顶点着色器中计算光照Gouraud着色将颜色插值到片元。这样可以减少片元着色器中的法线归一化操作但会损失像素级光照细节。适用于远处的物体或低端设备。// 顶点光照版本Gouraudv2fvert(appdata v){v2f o;o.posUnityObjectToClipPos(v.vertex);o.uvv.uv;half3 worldNormalnormalize(UnityObjectToWorldNormal(v.normal));half3 lightDirnormalize(_WorldSpaceLightPos0.xyz);half NdotLmax(0,dot(worldNormal,lightDir));half4 textex2Dlod(_MainTex,float4(v.uv,0,0));// 注意需要tex2Dlodhalf3 diffusetex.rgb*_LightColor0.rgb*NdotL;half3 ambienttex.rgb*UNITY_LIGHTMODEL_AMBIENT.rgb;o.colorhalf4(diffuseambient,tex.a)*_Color;returno;}fixed4frag(v2f i):SV_Target{returni.color;}这种方式将光照计算从片元移到顶点大大减少了片元负载但光照效果会变得块状。适合低多边形风格或性能敏感场景。10.4 创建高级移动着色器平衡效果与性能简化着色器能满足基础需求但现代手游需要更精致的画面如PBR、皮肤、头发等。我们可以在保证性能的前提下实现这些高级效果即“移动端PBR”。10.4.1 移动端PBR的设计思路完整的PBR需要多次纹理采样漫反射、法线、粗糙度、金属度、环境遮蔽和复杂的光照积分。在移动端我们通常使用单张控制纹理packed map合并多个属性。简化BRDF例如使用拟合公式而非完整的Cook-Torrance。预计算环境光照如球谐函数或卷积环境贴图。限制光源数量通常只计算最重要的几个光源。10.4.2 实现移动端PBR着色器下面是一个简化的移动PBR着色器支持方向光、环境反射和单张控制纹理RGBAR金属度G粗糙度B环境遮蔽A。漫反射和法线仍使用单独纹理。ShaderMobile/PBRMobile{Properties{_MainTex(Albedo (RGB),2D)white{}_NormalMap(Normal Map,2D)bump{}_ControlMap(Control (RMetal, GRough, BAO),2D)white{}_Color(Color,Color)(1,1,1,1)_EnvMap(Environment Cube,Cube)_Skybox{}}SubShader{Tags{RenderTypeOpaque}LOD250Pass{Tags{LightModeForwardBase}CGPROGRAM#pragmavertex vert#pragmafragment frag#pragmamulti_compile_fwdbase#includeUnityCG.cginc#includeLighting.cgincstructappdata{float4 vertex:POSITION;float2 uv:TEXCOORD0;float3 normal:NORMAL;float4 tangent:TANGENT;};structv2f{float4 pos:SV_POSITION;float2 uv:TEXCOORD0;half3 worldNormal:TEXCOORD1;half3 worldTangent:TEXCOORD2;half3 worldBinormal:TEXCOORD3;float3 worldPos:TEXCOORD4;};sampler2D _MainTex;sampler2D _NormalMap;sampler2D _ControlMap;samplerCUBE _EnvMap;half4 _Color;v2fvert(appdata v){v2f o;o.posUnityObjectToClipPos(v.vertex);o.uvv.uv;o.worldNormalnormalize(UnityObjectToWorldNormal(v.normal));o.worldTangentnormalize(UnityObjectToWorldDir(v.tangent.xyz));o.worldBinormalnormalize(cross(o.worldNormal,o.worldTangent))*v.tangent.w;o.worldPosmul(unity_ObjectToWorld,v.vertex).xyz;returno;}fixed4frag(v2f i):SV_Target{// 采样基础纹理half4 albedotex2D(_MainTex,i.uv)*_Color;half3 normalTSUnpackNormal(tex2D(_NormalMap,i.uv));half3 worldNormalnormalize(i.worldTangent*normalTS.xi.worldBinormal*normalTS.yi.worldNormal*normalTS.z);// 采样控制纹理half4 controltex2D(_ControlMap,i.uv);half metalliccontrol.r;half roughnesscontrol.g;half aocontrol.b;// 光照方向half3 lightDirnormalize(_WorldSpaceLightPos0.xyz);half3 viewDirnormalize(_WorldSpaceCameraPos.xyz-i.worldPos);half3 halfDirnormalize(lightDirviewDir);// 漫反射项half NdotLmax(0,dot(worldNormal,lightDir));half3 diffusealbedo.rgb*(1-metallic)*_LightColor0.rgb*NdotL;// 高光项简化Blinn-Phong但可调整为更物理的half NdotHmax(0,dot(worldNormal,halfDir));half specExp(1-roughness)*1281;// 粗糙度映射到光滑度half specpow(NdotH,specExp);half3 specularlerp((half3)0.04,albedo.rgb,metallic)*_LightColor0.rgb*spec*NdotL;// 环境反射简化的IBLhalf3 reflectDirreflect(-viewDir,worldNormal);half4 envtexCUBE(_EnvMap,reflectDir);half3 envReflectionenv.rgb*ao*(1-roughness);// 粗糙度影响反射清晰度// 环境漫反射球谐函数这里简化使用环境光half3 ambientalbedo.rgb*UNITY_LIGHTMODEL_AMBIENT.rgb*ao;half3 finalambientdiffusespecularenvReflection;returnhalf4(final,1);}ENDCG}}FallbackMobile/SimpleDiffuse}优化点控制纹理合并了三个属性减少采样次数。使用half精度运算。环境反射仅采样一次立方体贴图粗糙度简单影响强度实际应使用预过滤的Mipmap。未计算阴影可额外添加阴影采样会增开销。10.4.3 使用查找表LUT替代复杂计算对于复杂的BRDF可以使用预计算的LUT纹理。例如皮肤着色器中的次表面散射项可以用LUT存储。在移动端将复杂函数转换为纹理采样通常更快。10.5 实例移动平台角色皮肤优化在第八章中我们实现了一个包含次表面散射的皮肤着色器。现在我们将针对移动平台进行优化使其能在手机上流畅运行。10.5.1 分析原始皮肤着色器的瓶颈原始皮肤着色器包含4次纹理采样漫反射、法线、高光、厚度自定义光照模型中的次表面散射计算包含wrap和额外项幂运算等。在移动设备上这可能导致较高的片元负载。优化目标减少纹理采样简化散射计算。10.5.2 优化策略合并贴图将高光贴图、厚度贴图合并到一张纹理例如高光存R厚度存GAO存B。简化散射模型使用预积分皮肤LUT将散射效果存储在一张纹理中根据NdotL和曲率或厚度采样。这样将复杂计算转换为一次纹理查找。降低精度使用half。10.5.3 优化后的移动皮肤着色器以下是一个基于预积分LUT的移动皮肤着色器核心代码。// 属性_MainTex(Albedo,2D)white{}_NormalMap(Normal Map,2D)bump{}_PackedMap(Packed (RSpec, GThickness, BAO),2D)white{}_SkinLUT(Skin LUT,2D)white{}// 预积分LUTU轴为NdotLV轴为曲率/厚度// 片元着色器片段half3 normalUnpackNormal(tex2D(_NormalMap,i.uv));half4 packedtex2D(_PackedMap,i.uv);half specMaskpacked.r;half thicknesspacked.g;half aopacked.b;half NdotLmax(0,dot(normal,lightDir));// 使用LUT计算散射项half2 lutUVhalf2(NdotL,thickness);half3 ssstex2D(_SkinLUT,lutUV).rgb*_LightColor0.rgb*atten;half3 diffusealbedo.rgb*_LightColor0.rgb*NdotL*atten;// 高光简化half specpow(NdotH,32)*specMask;half3 specular_SpecColor.rgb*_LightColor0.rgb*spec*atten;half3 final(diffusesssspecular)*aoambient;这种优化将次表面散射的计算转化为一次纹理采样大大降低了ALU开销。10.6 总结与最佳实践本章我们系统学习了移动设备着色器优化的原理和实践。从移动GPU架构出发我们明确了带宽、ALU、渲染状态是优化的三大方向。通过创建简化着色器我们掌握了基础优化技巧通过移动PBR和皮肤着色器优化我们学会了在效果和性能之间取得平衡。移动优化最佳实践清单纹理始终使用压缩格式开启Mipmap尽量合并多通道信息到单张纹理。精度使用half和fixed仅在必要时使用float。采样减少纹理采样次数用向量操作代替标量。光照限制动态光源数量使用球谐函数或Light Probe处理间接光。阴影谨慎使用阴影可用烘焙阴影替代。着色器变体利用multi_compile生成不同版本的着色器避免冗余特性。测试始终在目标设备上测试性能利用Profiler和Frame Debugger分析瓶颈。通过遵循这些原则你可以在移动平台上创建出既流畅又精美的游戏画面。未来随着移动硬件的发展许多PC级特效将逐渐普及但优化思维将永远是图形程序员的核心竞争力。