unity天气系统_手游天气系统的高性能实现:雾雨雷雪+天色

2af75c3682e9366afba93b2e7a713481.png

中心思想是性能优化:优化天气本身的性能,和利用天气来优化整个场景的性能。同时绕开后处理来实现天色变化(低带宽的gpu架构让手机难以承受屏幕后处理特效)。

f8b0d442c7e8d898c24a5973958da94b.png
下雨打雷的渐进过程(有雷雨声)https://www.zhihu.com/video/1227622154977685504
f534b9c5dc4a05e3b4aef026ce594375.png
下雪的渐进过程(有音效)https://www.zhihu.com/video/1227636472989786112

1.雾

在手机上实测过新版的Unity自带的距离雾,其实它并不怎么消耗。而且,远处的物体被雾完全盖住了,我们大可以不渲染它们,把摄像机的远裁剪平面直接设置为雾的终止距离,这个距离之后的物体都会被裁剪掉。对性能的提升显而易见。(WeatherControl.cs)

// ...
cam = transform.GetComponent<Camera>();
// ...
RenderSettings.fog = true;
cam.farClipPlane = RenderSettings.fogEndDistance;
// ...

更进一步可以把雾的设置从Exponential改成Linear,因为Exponential模式下会有大量的物体被浓厚的雾盖住,只看得见一点点,但是仍然要渲染。下雨的时候可以用Linear这种浓厚的雾来模拟远处的雨,但是下雪的时候最好还是用Exponential,雪比雨通透。

2.雨

手游中实现雨主流方法就是用片或者用粒子:①在摄像机面前挂一个片,片上是雨的流动纹理(流动UV),好处是可以把雨做的很密,因为不管纹理上的雨画得多密,overdraw都是一整个屏幕,开销并不会增加。坏处是摄像机往下看地面的时候会穿帮;②用粒子系统,好处是可控性高,雨的大小,疏密,方向都可控,低头看也不容易穿帮,坏处是每一个粒子要由CPU单独控制生命周期及运动,CPU负荷高,所以粒子数也加不了太多,显得雨很稀疏。

之前Unity出了个官方demo,Angry Bots,里面的雨的实现类似于粒子系统的思想,但是放弃了对每个雨滴的单独控制,把所有雨滴合并成一个mesh,一整个mesh装着所有的雨滴往下落,

37a4c0714c6bbf1b846d614a4d109d31.png
一个雨滴就是一个长条形的Quad,很多个雨滴合并成一个box形的mesh

这个装着所有雨滴的大mesh在摄像机周围下落,下落到一定的位置会在上方生成新的mesh,以保证雨丝的连续。

cb114dc8d754a49b1cbc33a2773c61b9.png
雨mesh下落的过程(无声)https://www.zhihu.com/video/1226994480743583744

这样加更多的雨滴也不会像粒子系统一样增加CPU负荷了,雨滴可以相对稠密点,现在唯一的限制因素只有overdraw了。

可能有人已经想到了,这样把雨滴集合在一起,放弃对雨滴的单个控制,会造成重复感,以及失去对疏密的控制。解决方法是:我们可以先存好三个雨滴分布不同的mesh,然后在上方产生新的mesh的时候循环取用这三个mesh。对于疏密,我们可以简单地缩放一下mesh (RainBox.cs):

cachedTransform.localScale = new Vector3(rainSparsity, rainSparsity, rainSparsity);

这种集合成一个mesh的方案还有一个缺点,就是不能单独旋转每个雨滴的quad,也就是说不能做Billboard,从特定角度看的时候会显得一部分侧向摄像机的雨丝偏细,但是对视觉效果的实际影响不大,因为它本来的形状就是细长型。

3.雷+天色

雷由声音与画面来体现,声音简单地播放音频即可,而画面则是闪动。难点在于怎样让画面闪动与声音配合,并且让闪动看起来像是雷,而不是天亮了一会。

在着手这些问题之前,我们先得让场景(屏幕)亮起来。

首先,能不用后处理就不用后处理,因为手机GPU和显存之间的带宽低,后处理中对渲染纹理的存储与采样,让性能的损失十分惨痛。那么想让控制屏幕亮度,我们可以在摄像机前挂一个覆盖全屏的片,利用blend(片元混合)的机制来对渲染好的画面进行一些有限的计算与处理。但是,不是什么后处理都能够用挂片来绕过,比如bloom、depth of field等需要对领域像素进行采样的效果,如果不把原先渲染好的画面存储到渲染纹理里面,是无论如何都访问不到周围的像素的。幸运的是,在我们的应用场景中,只是需要改变当前像素的值,不存在领域操作,所以我们可以用挂片的方式来绕过后处理。我把这种挂片的方式称为“单像素后处理(Single Pixel Post Processing)”,当然这里的“后处理”一词只是比喻。

在渲染队列的最后,画一个覆盖全屏的quad,能够改变画面像素的亮度乃至颜色的关键就在于利用GPU里的blend(片元混合)机制。

我采用的混合模式是:Blend One SrcAlpha

实际上发生的运算是:

1 × <Source Color> + <Source Alpha> × <Destination Color>

Source指从quad写入的值,Destination指画面上已经渲染好的当前像素。

在生活中可以观察到:在打雷的瞬间,环境突然快速变亮,摄像设备(摄像机或手机等)来不及适应,来不及自动降低曝光,于是往往会过曝。所以打雷的时候场景往往看起来是大片的青白的,很多细节都被过曝掉了。我们想要模拟这种效果,可以暴力地把Source Color设为很亮的青白色 RGB(0.7, 0.75, 1.0),它和画面上的像素相加后很多地方的颜色值会饱和(超出1),看起来很像过曝,而没有过曝的暗处的颜色则会偏青偏冷:

80c72597dc0f103993a66bd6e7f6e074.png
闪电劈下的一瞬间

下雨的时候,环境往往会暗一点(当然是指不打雷的时候啦),这就涉及到天色的控制了。在上面的加粗公式里,画面上的像素被乘以了我们写入的alpha值,所以我们可以简单地通过降低quad写入值的alpha通道。这是alpha为0.5时的场景:

e9feb004c4c57b55788db9cd52ca323d.png
下雨时控制天色变暗

现在我们终于能点亮屏幕了。要看起来像打雷,接下来就是让屏幕闪动起来。

改变加上去的值的大小,也就是调节Source Color的值就可以闪动了。而闪动要和声音配合,有幸的是我得到了这段音频的幅度图:

be969d3f42b11956f62fdb26bddea6e4.png

打雷的那段放大了看是这样的:

ca296635e6d903d5d9281ee5e438e77f.png

想要把音频的幅度图转换成Unity能够识别的形式,要用到Unity的“曲线(Curve)”功能:

c36efa6f5fbbab8a7bb255f392ed9338.png

然后在程序里,每一帧的时候,得到当前帧的时间,然后根据时间,用AnimationCurve类的Evaluate()函数采样得到此时的Curve值,最后根据这个Curve值去决定加到屏幕上的青白色有多亮。但是仔细看看上面的音频幅度图,发现它并没有这么平滑,但我们也用不着真的把曲线也做出那么多毛刺,只用每帧把Curve值加上一个随机数就可以了,这个随机值很好地模仿了雷电的颤动感。最后值得注意的是,为了更好地组织代码,这里使用Coroutine来实现 (WeatherControl.cs):

//light up whole screen during the lightning
IEnumerator Lightning(float time)
{
    yield return new WaitForSeconds(2.375f - 0.2f);
    float i = 0f;
    float rate = 1f / time;
    while (i < 1f)
    {
        i += Time.deltaTime * rate;
        // synchronize the lightning with its sound, and the random value is to make the lightning shiver
        float intense = lightningCurve.Evaluate(i) * (1f + Random.Range(-0.2f, 0.4f));
        // use this color to control lightning and darken the environment
        Color lightningColor = new Color(0.7f * intense, 0.75f * intense, 1.0f * intense, 0.5f);
        // spppMat is the material of the whole screen quad, here we change its output color
        // by the way, "sppp" stands for "Single Pixel Post Processing"
        spppMat.SetColor("_Color", lightningColor);
        yield return 0;
    }
    spppMat.SetColor("_Color", new Color(0f, 0f, 0f, 0.5f));
}

4.雪

雪由两方面体现:雪在空中飘落,和物体被雪覆盖变白。

飘落只能用粒子系统,不能用上面介绍的雨的那种大mesh,因为每个雪花飘落时要各自晃动,雪花之间的相对位置不固定,所以只能用粒子。唯一值得注意的是为避免雪花的形状单调重复,要把Render Mode改成Billboard,然后自己用PS画一个下图这样的纹理(用软笔刷画,边缘不要画死),设置Texture Sheet Animation,让粒子系统生成新粒子时取用下面的四种雪花之一。粒子系统有overdraw的问题,我们需要把每个雪花尽量画大,撑满它的格子(下图中的四分之一区域),然后每个粒子的quad的面积尽量小,就可以尽量减少quad上的边角处的完全透明的部分,这部分没有内容但是仍然要渲染。

4d7131f0e92f34df47c54aba564dc783.png

物体被雪覆盖变白,但只有法线朝上的部分才挂得住雪。我们可以在vertex shader里计算法线朝上的程度,也就是直接取世界空间的法线的y值(下方代码中的o.snow.z)。

挂在物体上的雪是不均匀的,我们可以在surface shader里采样一个噪声纹理,

76a913ee0b7edeb05d24316fc6387874.png

值得注意的是,物体和物体,物体和地形之间有相接的地方,这里的雪的分布必须要是连续的,所以采样噪声用的纹理坐标要连续,那么直接用物体的世界坐标就好啦(以下代码中的o.snow.xy)。

本天气系统的主题还是性能优化,盖雪的效果并不追求盖雪的位置绝对精确,所以我们可以把除了噪声纹理采样以外的操作都放到vertex shader里来,法线朝上程度和噪声纹理坐标都只在顶点计算。另外,我们可以把这两个值合并进一个插值寄存器(o.snow),节省不必要的插值运算。

void vert (inout appdata_full v, out Input o) {
    UNITY_INITIALIZE_OUTPUT(Input,o);
    fixed3 worldNormal = UnityObjectToWorldNormal(v.normal.xyz);
    worldNormal = normalize(worldNormal);
    o.snow.z = max(worldNormal.y, 0.0);
    fixed4 wpos = mul(unity_ObjectToWorld, v.vertex);
    o.snow.xy = wpos.xz;
}

void surf (Input IN, inout SurfaceOutput o) {
    fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
    fixed snowIntense = tex2D(_SnowTex, IN.snow.xy * 0.1).r * IN.snow.z * _SnowHeavy;
    fixed3 albedo = lerp(c.rgb, fixed3(1.0, 1.0, 1.0), snowIntense);
    o.Albedo = albedo;
    o.Alpha = c.a;
}

被雪盖住的效果:

1e2beba395a76439186ef9ecec3bd319.png

5.示例工程以及源码

https://github.com/MarcusXie3D/MobileWeatherSystem​github.com
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值