返回列表
查看: 766|回复: 0

[转载] 揭秘!Unity3D水特效之雨天模拟(一)

[复制链接]
发表于 2018-9-27 17:24:53 | 显示全部楼层 |阅读模式
发帖
封面:
介绍

在写实类游戏制作时,常需要下雨场景的制作,由于日常生活中几乎所有物体都会被淋湿,所以下雨的制作其实需要考虑的方面有很多,我们将从粒子,材质,脚本控制等方面,分析一下应该如何渲染一个下雨的场景。
材质高光:
Unity的Standard Lighting中,使用GGX作为BRDF的高光算法,GGX具有拖尾感,可以较好的模拟潮湿物体表面的反光效果,首先我们来看一下这张故宫下雨时的照片(图侵删): a181-2.jpg
在普通游客的眼里,地面变得“湿滑”了,然而在渲染工程师的眼里,我们应该将这个“湿滑”的效果在PBR中分成四部分:Smoothness的提高,Specular Color的变亮以及GI Occlusion的降低和法线贴图比重的降低。
首先,提高Smoothness是毫无疑问的,要让物体表面光滑首先要降低粗糙度,但是仅仅降低粗糙度是不行的,水停留在物体表面,这时反射光线的是水而不是物体本身,因此物体本身的漫反射会被降低,因此被淋水的物体看起来颜色会变深,根据物理反射定律,物体本身色彩无变化,即光线反射比例无变化的情况下,高光程度应提高,所以会表现出高光率提高的现象。
同样,因为积水在物体表面的停留,物体受环境光影响会增大,这时我们应该适当降低Occlusion Map的影响,并降低法线贴图的偏移强度,这些在Unity的Standard Shader中都有提供,不需要手动写Shader,当然,如果要处理一个大场景,希望通过全局变量控制,还是应该手写shader进行精确优化的。
因此,希望读者能够不依赖Shader Forge, Unity Surface Shader等辅助功能,独立编写基于PBR Shader。至于反射图像的处理,我们一般通过Reflection Probe, Screen Space Reflection & Planar Reflection等方法实现,这并不在本文讨论的范围内。
a181-3.jpg 不同反射率的材质
接下来就是雨水的制作了,雨水本身的粒子效果制作虽然属于比较初级的粒子制作,甚至Assets Store上也有大量的资源,但是对美术制作能力有比较高的要求。如果像我一样,美术功底奇差无比,完全可以直接买一个效果实现#手动斜眼#,然后在粒子发射器上绑定一个脚本,使其始终在摄像机上方悬停,可以看到在示例中,我们使用粒子碰撞防止穿帮: a181-4.jpg
在摄像机第一视角效果如下:
a181-5.jpg 雨水落在平面
到此,一个简单的雨水渲染就出来了,然而,整个画面看起来僵硬死板,这是因为我们没有表现出雨滴打在地上的效果,因此,我们需要模拟一个动态的法线贴图,让地面的法线“动起来”,解决的方法有许多,最简单的方法就是序列帧,在CG软件(如Houdini,Substance Painter等)中制作序列帧并且渲染,然后在Unity中播放,这当然是一种比较简单的方法,但是同样也无法实现真正的实时与随机,我们这里则是使用Unity自带的CommandBuffer进行比较底层的图形绘制,实现随机的雨点特效。
学习过渲染管线基础的朋友都知道,Unity的摄像机其实并不是绘制RenderTexture的唯一方法,它只是封装的比较上层的方法,其实摄像机的工作流程就是(剔除->绘制网格->后处理)这三部分,无论是Forward path或是Deferred Shading path,亦或是Unity 2018最新提供的HDRP和LWRP,本质上都是这三部,区别仅仅在于,deferred shading会将光照作为后处理运算,而forward path会直接将灯光信息传入shader中进行光影运算并直接输出色彩,而我们这里并不需要动态剔除,只需要使用command buffer在一个指定的Render Target上进行GPU Instance,使用指定的材质绘制大量面片即可。
有朋友问我为何不使用Unity 2017推出的CustomRenderTexture进行绘制,我认为,CustomRenderTexture只是给不会渲染底层的程序提供的一个上层封装,实际功能不如使用Graphics类或CommandBuffer直接进行绘制,后者虽然门槛较高但是功能更加强大,大概相当于美图秀秀和PhotoShop的关系(只是个人看法,别怼别怼)。
首先我们需要手动生成一个正方形Mesh,并将indexBuffer设置为四边形绘制,实现非常简单,代码如下:
a181-6.jpg
由于我们是直接往屏幕上绘制的,所以根本不需要考虑ViewProjectionMatrix的问题,直接用NDC坐标(-1, 1)进行绘制即可,如果直接将这个mesh绘制到RenderTarget上,就是一个覆盖全屏的Mesh。
接下来我们要让这个mesh缩小并且随机分布在RenderTarget上,实现雨滴随机散落的效果,这时候就需要使用矩阵进行变换了,然而,雨滴数量众多,在本例中我们绘制了1023个雨点,所以很难依靠CPU进行迭代绘制,无论是计算还是Drawcall,消耗都是难以接受的。所以我们使用Compute Shader与Gpu Instance进行绘制,大幅度提高运算效率。
首先是Compute Shader,这里不赘述如何使用Compute Shader,只是提供Compute Shader的实现目标与过程。实现目标:生成1023个随机分配位置的矩阵并执行1023个计时器。为何要用计时器呢,原因很简单,当一个雨点散落到地上时,涟漪应该是越来越浅直到消失的,在涟漪消失时更新位置信息,使面片在另一个位置绘制。实现代码如下:
a181-7.jpg
这里来解释一下这段代码的意义,MatrixBuffer是我们需要使用的1023个坐标矩阵,而timeSliceBuffer则是我们需要使用的计时器,其中float2的x值是计时器数值而y值是计时器速度。_DeltaFlashSpeed则是由脚本传入的每帧的更新,即Time.DeltaTime * X; 然后是两个LocalRand函数,使用魔数运算输出一个伪随机数。其中第一个函数会输出一个(-1, 1)区间的float2随机数,用于随机生成一个平面位置,而第二个函数则会输出一个(0, 1)区间的float随机数,用于生成一个随机的计时器速度。
下面的CSMain函数就比较简单了,当计时器数值>1时,归0并重新生成随机的速度与位置。根据线代基础,矩阵的M03, M13决定了xy轴的位置,M00,M11则决定了xy轴的Scale,而这里为了偷懒,果断省略了雨滴大小的随机,直接用同样大小的面片。
在ComputeShader中运算完毕后,就可以在脚本里获取计算的结果,并且使用运算结果进行绘制了,当然,在此之前我们需要先进行初始化:
a181-8.jpg
这里初始化了Compute Shader,Compute Buffer以及需要用到的GPU Instance材质与高斯模糊材质(之后会用到)。
接下来就是调用Compute Shader并使用CommandBuffer进行绘制:
a181-9.jpg
首先,指定renderTarget并初始化为(0.5,0.5,1)也就是标准的法线贴图格式,然后使用Compute Shader输出的矩阵进行Gpu Instance,最后经过高斯高斯模糊,使画面顺滑一些。
有了输入的计时器与输入的矩阵,就可以开始绘制波纹了,波纹绘制实际非常简单,直接用Alpha Blend实现减弱效果,用三角函数实现波动即可,直接上代码:
Shader "Unlit/Wave"{    SubShader    {        Tags { "RenderType"="Opaque" }        ZWrite Off        ZTest Always        Cull Off        Blend oneMinusSrcAlpha srcAlpha        Pass        {            CGPROGRAM            #pragma vertex vert            #pragma fragment frag            #pragma multi_compile_instancing            #include "UnityCG.cginc"            #pragma target 5.0            #define MAXCOUNT 1023            StructuredBuffer<float2> timeSliceBuffer;            struct appdata            {                float4 vertex : POSITION;                float4 uv : TEXCOORD0;                UNITY_VERTEX_INPUT_INSTANCE_ID            };            struct v2f            {                float4 vertex : SV_POSITION;                float timeSlice : TEXCOORD0;                float2 uv : TEXCOORD1;            };            v2f vert(appdata v, uint instanceID : SV_InstanceID)            {                v2f o;                UNITY_SETUP_INSTANCE_ID(v);                o.vertex = mul(unity_ObjectToWorld, v.vertex);                o.timeSlice = timeSliceBuffer[instanceID].x;                o.uv = v.uv;                return o;            }            #define PI 18.84955592153876            float4 frag(v2f i) : SV_Target            {                float4 c = 1;                float2 dir = i.uv - 0.5;                float len = length(dir);                bool ignore = len > 0.5;                dir /= max(len, 1e-5);                c.xy = (dir * sin(-i.timeSlice * PI + len * 20)) * 0.5 + 0.5;                c.a = ignore ? 1 : i.timeSlice;                return c;            }            ENDCG        }    }}
shader非常简单,只是绘制了一个大致效果,最后生成的法线贴图效果如下:
a181-10.jpg
可以看到,这张对密集恐患者非常友好的图片,已经有了深浅不一的涟漪花纹(虽然比较难看),我们将这张renderTarget放到地面上,效果如下:
a181-11.jpg
可以看到,地面已经有了法线的涟漪,最近放假回国探亲,只能用家里的古董笔记本写文章,不过从粒子到动图绘制,在这台古董上也只需要4ms左右的运算时间,drawcall也因为gpu instance的原因并没有额外增加,可以说性能表现比较令人满意。
当然,这只是比较基础的雨水表现,其他丰富的细节与最终的开源实现将会在之后的几篇文章中公布。
作者:MaxwellGeng
来源:知乎

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回列表 客服中心 搜索