本系列主要參考《Unity Shaders and Effects Cookbook》一书(感谢原书作者)。同一时候会加上一点个人理解或拓展。
这里是本书全部的插图。
========================================== 切割线 ==========================================
写在前面
之前学习的各种Shader时,我们从没有考虑在全部平台下的可用性。Unity是一个强大的跨平台游戏引擎,但这也决定了在编写代码时我们须要考虑很多其它的平台因素。对于Shader而言。假设没有进行相应的优化,非常有可能无法执行在移动平台等对性能限制较高的平台上。
我们须要理解一些关键的因素来优化我们的Shader,以提高游戏性能而又能尽可能保持取得相同的视觉效果。
尤其是假设你的目标平台包含Android系统。那么就一定要小心中国各种山寨机的大浪一下把你拍在沙滩上的后果。。。所以,假设你从来没有为你的Shader考虑过这些情况,那么,且用且小心吧。
。。
这一章中,我们会学习三节内容:什么是一个高效的Shader。怎样对Shader进行性能分析。为移动平台优化我们的Shader。
那么,什么是一个高效的Shader呢?这是个有点复杂的问题。它涉及到了非常多因素。
比如,和你使用的变量个数及其所占内存,Shader使用的纹理个数有关等等。还有可能。你的Shade尽管工作良好。但我们实际商能够使用一半数目的变量就能够取得相同的效果。我们将在本节中发掘这样的一些技巧,并向你说明它们是怎样组合起来让我们的Shader更快更高效的。而又能够各种平台上取得相同高质量的视觉效果。
准备工作
我们将首先使用一个最常见的Shader之中的一个:Bumped Diffuse Shader。也就是应用了法线贴图的Shader。
- 创建一个新的场景和一个球体,加入一个平行光。
- 创建一个新的Shader和Material。能够命名为OptimizedShader001。
- 把Shader赋给Material,把Material赋给球体。
- 最后。使用下列代码改动Shader。
Shader "Custom/OptimizedShader001" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _NormalMap ("Normal Map", 2D) = "bump" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf SimpleLambert sampler2D _MainTex; sampler2D _NormalMap; struct Input { float2 uv_MainTex; float2 uv_NormalMap; }; inline float4 LightingSimpleLambert (SurfaceOutput s, float3 lightDir, float atten) { float diff = max (0, dot (s.Normal, lightDir)); float4 c; c.rgb = s.Albedo * _LightColor0.rgb * (diff * atten * 2); c.a = s.Alpha; return c; } void surf (Input IN, inout SurfaceOutput o) { float4 c = tex2D (_MainTex, IN.uv_MainTex); o.Albedo = c.rgb; o.Alpha = c.a; o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap)); } ENDCG } FallBack "Diffuse" }
简单的光照函数里面进行了简单的漫反射处理,surf函数里则改变了模型的法线。
最后,你得到的效果大概是这样的:
实现
以下,我们来一步步优化这个Shader。
首先,我们须要优化变量类型,以便它们尽可能少地占用内存:
- 改动Input结构。
之前,我们的UV坐标都是存储在了float2类型的变量中。如今我们将它们改为half2:
struct Input { half2 uv_MainTex; half2 uv_NormalMap; };
- 接下来是光照函数。相同,将当中float家族的变量改成相应的fixed类型变量:
inline fixed4 LightingSimpleLambert (SurfaceOutput s, fixed3 lightDir, fixed atten) { fixed diff = max (0, dot (s.Normal, lightDir)); fixed4 c; c.rgb = s.Albedo * _LightColor0.rgb * (diff * atten * 2); c.a = s.Alpha; return c; }
- 最后,改动surf函数中的变量类型。
相同使用fixed类型变量:
void surf (Input IN, inout SurfaceOutput o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex); o.Albedo = c.rgb; o.Alpha = c.a; o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap)); }
改动#pragma声明:
CGPROGRAM #pragma surface surf SimpleLambert noforwardadd
如今,我们能够使用共享UV坐标来继续优化Shader。为此。我们使用_MainTex的UV坐标取代_NormalMap的UV在UnpackNormal()中的查找作用。并移除Input结构中的uv_NormalMap:
o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
struct Input { half2 uv_MainTex; };
最后,我们告诉Unity,这个Shader仅仅工作在特定的渲染器上:
CGPROGRAM #pragma surface surf SimpleLambert exclude_path:prepass noforwardadd
最后优化前后效果例如以下(左前右后):
能够看出,我们肉眼差点儿看不出不论什么区别,可是我们已经降低了这个Shader被绘制到屏幕上所花费的时间。我们将在下一节中利用Unity的可视化工具来分析这样的降低程度的大小。但在这里,我们关注的是,使用了更少的数据来得到相同的渲染效果。
在创建我们自己的Shader的时候,也要一直记住这个思想!
解释
上面一共提到了4种优化方式:优化变量类型,共享UV坐标。降低处理的光源个数。让Shader仅仅工作在特定的渲染器上。
以下,我们来更深入地理解这些技术是怎样工作的,最后再学习其它一些技巧。
优化变量类型
首先。我们来看一下在我们声明变量时每个变量存储的数据大小。因为在声明变量时,我们往往有多个选择(float,half,fixed),我们须要来看一下这些类型的特点:
- float:高精度浮点值,一般是32位。也是三者中最慢的一个。它相应的还有float2,float3和float4。
- half:中精度浮点值。一般是16位。范围是-60000至+60000,它适合存储UV坐标,颜色值等。比float类型快非常多。它相应的还有half2。half3,和half4。
- fixed:低精度浮点值。一般是11位,范围是-2.0至+2.0。精度为1/256。
这是三者中最小的一个,能够用于光照计算、颜色等。它相应的值有fixed2,fixed3和fixed4。
- 尽可能使用低精度变量。
- 对于颜色值和单位长度的向量,使用fixed。
- 对于其它类型,假设范围和精度合适的话,使用half;其它情况使用float。
降低处理的光源个数
这主要是告诉Unity,使用这样的Shader的对象。仅仅接受一个单一的平行光光源作为逐像素光源。其它的光源都使用内置的球谐函数处理后作为逐顶点的光源。当我们在场景中放置了还有一个光源时,这样的策略会非常明显。因为我们的Shader使用一个法线贴图进行逐像素的操作。
这样做当然非常好,可是假设我们须要不止一个平行光,并且想要控制哪一个是用于该逐像素计算的主光源,又该怎么办呢?这就须要Unity面板中的一个设置啦!
假设你细致观察。就会法线每个光源都有一个Render Mode下拉菜单。当你点击它时。会出现Auto, Important, 和Not Important三种选项。通过选择Important。你能够告诉Unity这个光源更须要被当成一个逐像素光源。而非一个逐顶点光源。
假设设置为Auto,那么就由Unity自己做决定啦!
懵了是不是。。
。为了说明上述意思。我们来做个试验!在场景里放置还有一个点光源,然后移除Shader中的Main Texture。第一次。打开平行光。关闭点光源(左图)。第二次关闭平行光,打开点光源(右图)。你能够发现第二个点光源并不会影响我们的法线贴图(仅仅是照亮了模型。也就是它仅仅是逐顶点处理),仅仅有第一个平行光才会影响。
这里的优化。是因为我们把其它全部光源当成了顶点光源,而在计算像素颜色时仅仅计算一个主平行光作为像素光源。
共享UV坐标
这步优化非常easy,仅仅使用了Main Texture的UV坐标来取代法线贴图的UV坐标。这样实际上降低了内部提取法线贴图UV坐标的代码。这样的方法能够非常好地简化我们的代码。
仅仅工作在特定渲染器上
最后,我们在语句中声明了,以便告诉Unity,这个Shader不会再接受来自延迟渲染中的其它不论什么自己定义的光照。这意味着,我们仅能够在正向渲染(forward render)中有效地使用这个Shader,这是在主摄像机的设置中设置的。
写在最后
其它的优化策略还有非常多。我们之前学过怎样把多个灰度图打包到一个RGBA贴图中,以及怎样使用一张贴图来模拟光照效果。因为这些众多的技术,因此问怎样优化Shader是一个非常模糊的问题。可是,了解这些技术使得我们能够依据不同的Shader和平台採用合适的技术,来得到一个具有稳定帧率的Shader。