文/贾伟昊

0. 牢骚

我发现,每个月的20+号是我有精力写博客的时间……

这次项目算是经历的第一次严格意义上的渠道测试,更换了正式名称,见了更多玩家,开发组也经历的更多通宵……评价和数据如何暂时还未揭晓,趁着没那么忙,来还欠自己的“文章债务”。。。

这篇博客主题是移动平台的天气系统,做这个系统的主要原因是美术需求——大世界沙盘的动态效果太少了,需要一些动态变化的东西来增加效果。之前也看过一篇博客《Unity3D手游开发日记(7) - 适合移动平台的天气效果》,作者对于每种天气效果大致聊了原理,我也挺感兴趣,就在今天五一假期(是的,没看错,就是半年前的五一……)蹲在家里花了一天多时间照着文章的思路撸了一个简单版本,在加上之前积累的多云的效果,算是我们项目中天气系统的雏形。过了假期放出来给同事体验了下,感觉还不错,然后稍微修改了一些bug就被其他事情搁置下来了,所以7月份测试也没有放出来。

7月技术测试之后,美术效果的增强也就被逐渐放到更高的优先级,天气系统也就成为了我的工作重点之一。经历整体结构的重构和一些效果实现方式的改变,才有了目前测试在用的这个版本。本篇文章就以当前已经实现的几种天气效果为例来聊一下在移动平台上实现一套简单的天气系统的思路和方法。

1. 综述

整体来说,移动平台的性能还不足以支撑端游上完整的一套天气系统,Unity的Asset Store上有一些不错的天气效果实现,也只能看着流流口水,并不敢用,比如这个Weather Maker - Sky, Weather, Fog, Volumetric Light and Dynamic Environment,还有UniStorm。(UniStorm有一个Mobile版本,效果也还不错,有兴趣的同学可以去搜索看下。)

那么,在移动端,天气系统效果简答来说也就成了美术做做特效,程序按照需求写写挂特效的脚本罢了。的确,在制作各个天气的效果的时候,并没有用到什么特别的技术点,但整个实现天气系统的过程中,我没有依赖于美术,而是自己寻找所有需要的资源,编写逻辑进行整合简化,过程还比较有趣,体会到非常直接的成就感,一些小的细节也自己去处理,非常开心。目前实现的天气效果包括晴天、多云、阴天、雨天和雪天这几种比较常见的效果,逐一来进行说明。

2. 晴天效果

我们项目中美术制作的所有场景都是按照晴天的效果来制作的,所以对于程序来说,晴天效果就是没效果,实现最简单,性能最优,哈哈~(就是注意把其他效果清空不要残留……)

3. 多云效果

先看一下最终实现的多云效果截图,动态图比较容易看出效果,静态图感觉比较怪,可以注意主城的模型有一半是被云遮住了。为了凸显效果云阴影的浓度被我故意调整得比较高。

092402me881srss1m1rrbz.jpg
多云效果截图

这个效果是之前美术想要的一个内容。如果使用真正移动一个半透的云模型在空中移动并且产生投影,移动设备上所能支持的shadowmap尺寸无法提供足够的阴影精度,而直接进行投影的方法又比较难做到在高低不平的山、建筑等物体表面计算投影效果。经过调研之后,使用了一个购买的插件Screen Space Cloud Shadow。插件页面有动态效果视频,想看动态效果的可以去看下。当时同样调研了另外一个插件Cloud Shadows,都试玩了下。后者是基于light的cookie的,在当时的unity版本中有些小问题没有解决掉,而且我自己试验的cookie在移动设备上有点小问题,所以就没有选用。Screen Space Cloud Shadow这个插件使用起来比较方便,只需要把prefab丢场景里就好,开关也很简单,代价就是需要深度图,场景内所有物件都要绘制两遍,draw call和面数都会翻倍。这也是整个天气系统中消耗最大的一块,因此多云天气在最终版本里也只有高配下才会开启。

由于是购买的插件,因此贴代码不太合适,简单说一下实现的原理:shader使用Transparent渲染队列,在OnWillRenderObject中将一个平面放到相机的远平面,并且把尺寸缩放成和相机的远平面一样,这样就保证它的绘制过程是在最后,用FrameDebugger抓帧看绘制顺序和参数如下图:

092402vga0c7h0fcf7gh7m.jpg
云阴影的绘制过场截图

在Shader的frag过程中,根据深度图和世界空间的摄像机方向射线来计算出阴影应该绘制的浓度。这里包含了一些magic value,我也有些细节没有看得特别懂……再加上本身并不是我自己设计的算法,因此不在这里详述了,有兴趣想了解的朋友可以自己去购买一份插件,source code include。

这里只说明三个遇到并解决的小坑:

1.由于云的阴影是飘动的,因此涉及到uv的流动,这个是根据时间来计算的,最初的时候这个时间直接取了Time.time的值,当游戏运行一段时间之后,这个值就会变得很大,在移动设备上会导致云的移动出现顿卡的感觉。这也是在很多使用uv流动的过程中很容易出现的一个问题,通过取余的方式可以保证精度,但是可能会在取余的那一帧出现采样不连续的问题。由于我们不会非常长时间开启这个效果,因此这个问题可以通过在开关的时候把时间参数重置来规避。

2.fixed类型在移动设备上精度问题导致马赛克。原来Shader中使用了fixed的值,在PC上并没有问题,安卓设备上发现了马赛克的现象,修改几个关键值为float类型可以解决马赛克问题。

3.由于使用了深度图,因此深度图的精度对于云的效果影响比较大。我们最初相机的远平摄设置得非常远,面又非常近,0.1-1000这样的值域范围。在PC上没有问题,手机上就有非常明显的马赛克,将面和远平面都调整一下,变为1-300,效果好了很多。(顺便再推荐一下在UWA群里推荐过的调试插件,Hdg Remote Debug - Live Update Tool,可以在电脑上连接移动设备进行实时调试,用于排查和调试这种问题比频繁打包要方便很多,节省太多时间,已经被我默认打包进了dev版本的工程里。)

4. 阴天

阴天的效果其实就是天色变暗的感觉,如果是实时光照的话可以通过调暗方向光的亮度或者颜色来处理,但是由于手游上目前大都还是烘焙的,因此比较方便的方案就是通过后处理来实现。

考虑过Color Grading方案,但是感觉稍微有点耗,而且和昼夜系统实现会有些小冲突,最后实现的时候选择了直接在颜色上乘以一个Tint Color的方案来做,由于我们整合了整个后处理效果栈,因此在开启别的后处理的情况下,这个tint color的过程消耗非常小,每个像素多一个乘法而已。

这里也再推荐一下钱康来一直推荐的将所有的后处理Pass进行整合的方案,也就是参考Unity官方的Github实现:Post Processing Stack,Asset Store上也有Post Processing Stack。

5. 雨天

雨天的效果实现了两个版本,最初的版本是基于前文提到的博客里的思路来实现的,就是挂一个uv流动的面片在镜头前,闪电的效果就是把这个面片调整为白色再调整回来。实现非常简单,这里只贴一下Shader代码好了,因为没有真正在项目中使用,所以只算私货。
 

  1. Shader Shader/Scene/Rain {
  2. Properties{
  3. _RainTex(Main Texture:, 2D) = white {}
  4. _RainIntensity(Intensity of Rain:,Float) = 0.0
  5. _FallSpeed(Fall Speed of Rain:,Float) = 1
  6. _ThunderLighting(Thunder Lighting, Color) = (0, 0, 0, 0.5)
  7. }
  8.  
  9. SubShader{
  10. Tags{ Queue = Transparent IgnoreProjector = True RenderType = Transparent }
  11.  
  12. Blend SrcAlpha One
  13.  
  14. LOD 100
  15. Cull Off
  16. ZWRITE Off
  17. Lighting Off
  18.  
  19. Pass{
  20. CGPROGRAM
  21. #pragma vertex vert
  22. #pragma fragment frag
  23. #pragma target 2.0
  24.  
  25. #include UnityCG.cginc
  26.  
  27. sampler2D _RainTex;
  28. float4 _RainTex_ST;
  29. fixed _FallSpeed;
  30. fixed _RainIntensity;
  31. float4 _ThunderLighting;
  32.  
  33. struct appdata_t {
  34. float4 vertex : POSITION;
  35. float2 texcoord : TEXCOORD0;
  36. };
  37.  
  38. struct v2f {
  39. float4 vertex : SV_POSITION;
  40. float2 texcoord : TEXCOORD0;
  41. };
  42.  
  43. v2f vert(appdata_t v)
  44. {
  45. v2f o;
  46. o.vertex = UnityObjectToClipPos(v.vertex);
  47. o.texcoord = TRANSFORM_TEX(v.texcoord, _RainTex);
  48. return o;
  49. }
  50.  
  51. fixed4 frag(v2f i) : SV_Target
  52. {
  53. fixed2 UV = i.texcoord;
  54. float Time = _Time.y;
  55. fixed vValue = _FallSpeed * Time;
  56.  
  57. UV = fixed2(UV.x, UV.y + _FallSpeed * Time);
  58. fixed4 col = tex2D(_RainTex, UV);
  59. col.rgb = col.rgb * col.a * _RainIntensity + _ThunderLighting.rgb;
  60. col.a = 1.0f;
  61. return col;
  62. }
  63. ENDCG
  64. }
  65. }
  66. }

 

 

初版的下雨效果截图
这里学习到一个小的技巧是可以使用Unity的AnimationCurve来做一些曲线供游戏逻辑使用,从而做出来一些变化的效果,这里就用曲线控制了雨的浓度和与雷声配合的闪电效果,C#代码也贴一下。

  1. using UnityEngine;
    •  
    • namespace ThorFramework.Weather
      • {
        • [DisallowMultipleComponent]
          • public class RainController : MonoBehaviour
            • {
              • public AnimationCurve rainCurve;
                • public AnimationCurve thunderCurve;
                  •  
                  • private Color lightingColor = Color.white;
                    • private Material weatherMaterial;
                      • private float startTime = 0.0f;
                        • private AudioSource thunderAudio;
                          •  
                          • // Use this for initialization
                            • void Start()
                              • {
                                • MeshRenderer r = gameObject.GetComponent<MeshRenderer>();
                                  • if (r != null)
                                    • {
                                      • weatherMaterial = r.material;
                                        • startTime = Time.time;
                                          • }
                                            • thunderAudio = gameObject.GetComponent<AudioSource>();
                                              • }
                                                •  
                                                • void OnEnable()
                                                  • {
                                                    • startTime = Time.time;
                                                      • }
                                                        •  
                                                        • // Update is called once per frame
                                                          • void Update()
                                                            • {
                                                              • float curveTime = Time.time - startTime;
                                                                • if (weatherMaterial == null)
                                                                  • {
                                                                    • return;
                                                                      • }
                                                                        • if (rainCurve != null)
                                                                          • {
                                                                            • float val = rainCurve.Evaluate(curveTime);
                                                                              • weatherMaterial.SetFloat(_RainIntensity, val);
                                                                                • thunderAudio.volume = 2.0f * val;
                                                                                  • }
                                                                                    •  
                                                                                    • if (thunderCurve != null)
                                                                                      • {
                                                                                        • float val = thunderCurve.Evaluate(curveTime);
                                                                                          • weatherMaterial.SetColor(_ThunderLighting, lightingColor*val);
                                                                                            • }
                                                                                              • }
                                                                                                • }
                                                                                                  • }

雨天效果截图
这里雨的效果包括三个部分:

1.跟随相机移动的一个产生雨滴的特效,截图中雨滴不是很密集,但是动起来的效果还是不错的。这里为了追求效果粒子数量上限给到了500左右,但是仍然不是非常密集,做不到暴风雨的感觉,还需要添加一些面片来做更加密集的雨滴效果。

2.跟随角色移动的地面涟漪。在通常的做法中,雨滴涟漪的制作是用粒子系统的碰撞来做的。当粒子产生了碰撞之后就会产生一个新的粒子效果,这样可以做到很精准的感觉,包括落在树叶上、建筑房顶上等,但是消耗也比较大。我们采用的是比较讨巧的方法,角色脚底挂一个不断随机产生涟漪的粒子特效,在斜坡、桥上等地方会有穿帮的小问题,但是也基本满足的策划的需求。

3.与阴天一样,下雨的时候会阴暗一些,所以同样挂了一个tint color调色的后处理。

总结:雨的效果花费了挺多精力来制作,最终的效果基本满意。使用特效的方案整体的overdraw没有那么高,但是为了出效果粒子数量用得还算比较多,因此在粒子系统上的性能消耗还挺大的。对比之前面片的方案各有优劣,只是出了追求高品质效果的考虑选择了效果上限较高的粒子系统来实现。

6. 雪天

在实现雨天的效果之后,雪天的效果制作就非常简单了,雾效果加上一个和雨滴相似的粒子特效挂在镜头前就可以啦。由于雪花生命周期比较长,飘落速度比较慢,粒子数最多在300左右就可以达到不错的效果。实现的效果截图如下(这里有一些序列帧动画之类的小技巧可以优化雪片的效果,不过不属于程序的技术了,特效同学应该都会的):

092403l9r9bqa9700rz4ba.jpg 雪天效果截图
也同样研究了一下《镇魔曲》中雪花效果的实现,发现比较讨巧的是他们没有让一个雪花是一个粒子,而是用一张图来表现几片雪花的效果,然后大约只需要同时存在十几个粒子就可以做到比较密集的下雪效果。当然代价是仔细观察的话会发现一些重复感,overdraw也会稍微有些提高,但是粒子数量降低得会比较多,值得借鉴。(我们美术同学尝试了一个版本之后告诉我不太满意,当然在看了完全随机的效果之后,对于略有重复的效果自然能感觉出来瑕疵,没有对比才没有伤害……)

7. 风

风不属于任何一个天气,而是用于辅助表现其他天气效果的元素,在我们游戏中主要能做的表现是树木的摇摆和一些相应的音效。摇摆的效果采用顶点动画来实现,已有的实现方案可以参考Unity3D手游开发日记(5) - 适合移动平台的植被随风摆动这篇文章,网上也有很多实现细节的讨论,但比较好的方案追本溯源还是《GPU Gems》中的一篇文章:《Chapter 16. Vegetation Procedural Animation and Shading in Crysis》。它主要描述了在CryEngine中的实现原理,考虑到树干和树叶的不同,使用顶点色来对振幅进行控制,估计很多人都读过,实现细节可以去参考原文。

这里只说几个我们移植时的几个修改:

1.使用Shader的全局变量。Shader.SetGlobalXXX一系列的接口就是为这种全局参数来设计的,简单易用。

2.临近测试我们美术比较忙,表示没时间对每棵树的模型去刷顶点色,于是摇摆的幅度控制采用了一个简化的方案——由顶点高度和一个美术设定的模型高度的比值来决定,目前只采用的线性差值,效果一般,勉强够用。

3.GPU Gem中的实现比较复杂,考虑了横向的和纵向的抖动,有不少计算在里面,这块可以根据自己项目的游戏类型和需求来修改和简化。

8. 整合

把实现的各个天气效果整合成天气系统,由一个管理器来控制,可以模拟游戏中各个国家的气候风格,这是最后整合进游戏进行实际应用的步骤。由于我们大世界和战斗场景是两种完全不同的镜头方式,因此最终特效挂接的部分实现了两套不同的控制逻辑。除此之外,根据不同国家的特性,也将雨天和雪天统一为了特殊天气,比如在燕国这样靠北的国家,就只会下雪,而其他国家则是下雨。这其中有很多繁杂的与游戏业务相关的逻辑就不谈了,只聊几个实现过程中比较有感触的点:

1.渐变需求。天气效果中所有的控制效果都有不同的渐变细节需要处理,比如下雪天气停止不能突然没有,而是要有渐渐消失的感觉;天气由晴天变阴天,也不应该突然黑下来,而是要有一个亮度渐变的过程。这些需要各个天气系统针对自己的效果做好差值的处理,这个过程使用了DoTween来做,代码实现非常简单高效。

2.对于需要跨天气控制的效果进行统一的管理。在最初的版本里,用于表现变暗效果的Tint Color由每一个天气进行各自的管理和差值,这里就有一些非常恶心的特殊代码要做处理,比如阴天效果的停止函数中,当进入晴天的时候需要把亮度逐渐调整到1,如果从阴天进入雨天,则不需要做这样的调整。天气效果的控制也就像是一个状态机,在单独的状态中如果需要考虑变换的前后逻辑,代码里就需要非常多if-else这样的逻辑判断。在迭代的过程中,把这块的控制抽象成为一个天气亮度管理器——BrightnessManager,它负责控制亮度并按照设定的速度在当前亮度和目标亮度之间做差值,这样对于任何天气效果,只需要在开启的时候设置默认的亮度值给亮度管理器,其他细节都不需要关心。同样还有用于风的参数控制的WindManager。

3.效果与实现逻辑的分离。从表面上看,下雨和下雪是两个不同的天气效果,但是他们在程序的逻辑是有很大相似性的——都是控制特效的挂接和跟随逻辑。因此从逻辑实现上这两个天气效果有相同的逻辑,只是数据(特效)不同而已。另外下雨的效果有额外的一些涟漪的处理。于是使用面向对象的思路抽取一个FXWeather的公共父类来做代码的复用,方便维护。

经过一些思考和迭代之后,最终C#代码中的类图如下所示。

092404jhf01fu818uzh68h.jpg 最终实现的天气系统类图
9. 总结

回顾整个天气系统的实现,其实没有特别有难度的东西,只是一些效果的应用和业务逻辑的编写。使用面向对象的继承和组合,再加上状态模式就完成了最后的需求。在效果方面,由于要兼顾移动平台的性能限制,相比端游的动态天气效果做了很多妥协和简化,尽量用20%的性能消耗做到60%的表现力,对于真实感等方面做了不少的妥协。

当然,现在实现的各种天气效果还很简陋,比如下雨还可以添加地面湿滑的材质效果,还可以制作暴风雨这样更动感刺激的天气效果,在沙漠中实现沙尘暴的感觉等等。这些东西,还需要更多的时间和精力来填满缺失的细节。

无论如何,希望这篇文章可以给期望增强游戏效果的同学一些启发,也同样期望有更好实现效果和方法的朋友不吝赐教,给予更多思路和经验的分享。

Via : Nexus泛娱乐游戏社群

声明:游资网登载此文出于传递信息之目的,绝不意味着游资网赞同其观点或证实其描述。

锐亚教育

锐亚教育,游戏开发论坛|游戏制作人|游戏策划|游戏开发|独立游戏|游戏产业|游戏研发|游戏运营| unity|unity3d|unity3d官网|unity3d 教程|金融帝国3|8k8k8k|mcafee8.5i|游戏蛮牛|蛮牛 unity|蛮牛