在手机游戏中实现下雨系统

作者:GWB-腾讯创意游戏合作计划
2018-12-16
23 23 0

作者:日音

先展示最终效果:

雨水会导致物体潮湿,会在平面形成水波,会在斜面形成纹路。雨会被物体阻挡,阻挡的部分依然保持干燥,干燥和潮湿有过度。水积累到一定程度还会有反射。

很早就想做一个完整的天气系统。不过难度太大了,所以打算拆分成一个个小系统。首先就是做最常见的下雨。

雨水的实现

雨水本身实现已经有很多成熟的方式,包括我个人最喜欢的 angrybots 里面的雨,性能和表现都非常好。遗憾的是不够通用,所以我打算还是重新做一个分离的雨滴,方便后面加入一些中级效果,例如遮挡之类这样的特性。

我假定雨是有一个下界的,毕竟如果你潜入了水底,那么肯定就不希望会有任何下雨被看到。上界就不需要了,如果你高到一定程度,发现只有下方有雨,那估计也非常奇怪。

我们需要设计一种 tile 方式的雨来处理摄像机看到雨的范围。水平方向上可以调整 tile 个数,垂直方向我可以根据控制雨下落的生命周期来控制。

根据这张图,我们要计算出摄像机能看到的 AABB 盒,从而去做需要雨落下的物体的范围。

Vector3[] GetCorners(float distance)
{
    Vector3[] corners = new Vector3[4];
    float halfFOV = (Camera.main.fieldOfView * 0.5f) * Mathf.Deg2Rad;
    float aspect = Camera.main.aspect;
    float height = distance * Mathf.Tan(halfFOV);
    float width = height * aspect;
    Transform tx = Camera.main.transform;
    
    // UpperLeft
    corners[0] = tx.position - (tx.right * width); 
    corners[0] += tx.up * height; 
    corners[0] += tx.forward * distance;
    
    // UpperRight
    corners[1] = tx.position + (tx.right * width); 
    corners[1] += tx.up * height; 
    corners[1] += tx.forward * distance;
    
    // LowerLeft
    corners[2] = tx.position - (tx.right * width); 
    corners[2] -= tx.up * height; 
    corners[2] += tx.forward * distance;
    
    // LowerRight
    corners[3] = tx.position + (tx.right * width); 
    corners[3] -= tx.up * height; 
    corners[3] += tx.forward * distance;
    
    return corners;
}

因为每个雨滴都是独立的,这次我们可以放心使用广告牌技术了。

v2f vert(appdata_full v)
{
    v2f o;
    UNITY_SETUP_INSTANCE_ID(v);
    UNITY_TRANSFER_IN5TANCE_ID(v, o);
    
    float3 centerOffs = v.vertex.xyz * float3(_RainXj 1, _RainZ); 
    float4 worldPos = mul(unity_ObjectToWorld, v.vertex); 
    float3 normalDir = _WorldSpaceCameraPos - worldPos; 
    
    normalDir.y = 0;
    normalDir = normalize(normalDir); 
    
    float3 upDir = float3(0, 1, 0); 
    float3 rightdir = cross(upDir, normalDir); 
    float3 pos1 = float3(unity_ObjectToWorld[0].w, unity_ObjectToWorld[1].w, unity_ObjectToWorld[2].w);
    
    worldPos.xyz = pos1 + rightdir * centerOffs.z + upDir * centerOffs.x + normalDir * centerOffs.y;
    o.pos = mul(UNITY_MATRIX_VP, float4(worldPos.xyz, 1)); 
    o.uv.xy = TRANSFORM_TEX(v.texcoord.xy, _MainTex); 
    
    return o;
}

另外,由于雨离摄像机太近会有点太大块的感觉,所以我建议将摄像机的 nearPlane 调整到 1 左右。

由于是局部雨,摄像机快速移动或者瞬间切换位置的时候,需要还原雨本应该的场景。 我们保存上一帧的 AABB 盒,并且对当前的 AABB 盒遍历,判断如果在原来的 AABB 盒内,那么就啥都不做,否则需要还原雨滴。

经过测试,发现虽然有一点点的衔接不够流畅的问题,但整体感受其实还不错。

不过上面做出的雨太干巴巴了,首先我要考虑的是增加雨滴打中地面的波纹。


波纹我们参考的是这篇文章:

https://seblagarde.wordpress.com/2013/01/03/water-drop-2b-dynamic-rain-and-its-effects/

其中涉及波纹的部分比较简单。

r 通道代表离圆心的距离。gb 通道代表法线方向,a 通道代表随机强度值。

inline float2 ComputeRipple(float2 UV, float CurrentTime, float Weight)
{
    float4 Ripple = tex2Dlod(_RippleTex, float4(UV, 0, 0));
    Ripple += tex2Dlod(_RippleTex, float4(UV, Q, 1));
    Ripple *= 0.5;
    Ripple.yz = Ripple.yz *2 - 1;
    
    float DropFrac = frac(Ripple.w + CurrentTime);
    float TimeFrac = DropFrac - 1.0f + Ripple.x;
    float DropFactor = saturate(0.2f + Weight * 0.8f - DropFrac);
    float FinalFactor = DropFactor * Ripple.x * sin(clamp(TimeFrac * 9.0f, 0.0f, 3.0f) * 3.141592653589793);

    return Ripple.yz * FinalFactor;
}

inline float3 AddWaterRipples(float3 i_worldPos, float fadeOut)
{
    float4 Weights = Rainlntensity * 2 - float4(0, 0.25, 0.5, 0.75);
    Weights = saturate(Weights * 4);
    float animSpeed = _Time.y;
    
    float2 Ripple1 = ComputeRipple(float2(i_worldPos.xz * _RippleTile + float2(0.025f, 0.0f) * _Time.y), animSpeed) Weights.x);
    float2 Ripple2 = ComputeRipple(float2(i_worldPos.xz * _RippleTile + float2(-0.055f, 0.03f) * _Time.y), animSpeed * 0.71, Weights.y);
    float3 rippleNormal = float3(Weights.x * Ripplel.xy + Weights.y * Ripple2.xy, 1);

    return lerp(float3(0, 0, 1), rippleNormal, fadeOut);
}

这样之后,波纹是有了。

远处可以优化下,太远的地方其实可以不用算,优化性能。然而这还不够,因为这地面看上去压根就不像湿滑的,湿滑的地面需要额外处理一下。通过增加一张法线贴图,控制一下 uv 的扰动,并且替换了下地表,来展示雨水的效果。

静态图效果不明显,还是看视频吧。

接下来要处理的是水导致物体本身的变化。一般颜色会稍变深,表面会变光滑,如果有水坑的话就会反光之类。其实就是 pbr 里面的 smooth。但是手机用全套 pbr 性能有点耗,打算先把 brdf3 的一部分拿过来用。

float3 BRDF3__Direct1(float3 diffColor, float3 specColor, float rlPow4, float smoothness)
{
    float LUT_RANGE = 16.0; // must match range in NHxRoughness() function in GeneratedTextures.cpp
    // Lookup texture to save instructions
    float specular = tex2D(unity_NHxRoughness, float2(rlPow4, SmoothnessToPerceptualRoughness1(smoothness))).UNITY_ATTEN_CHANNEL * LUT_RANGE;

    return diffColor + specular * specColor;
}

float3 BRDF3_Indirect1(float3 diffColor, float3 specColor, float grazingTerm, float fresnelTerm, float r)
{
    float3 c = diffColor;
    r = min(r, 0.5);
    c += lerp(specColor, grazingTerm., min(r, fresnelTerm));

    return c;
}

float4 BRDF3_Unity_PBS1(float3 diffColor, float3 specColor, float oneMinusReflectivity, float smoothness, float3 normal, float3 viewDir, float _Roughness1, float reflect1)
{
    float3 reflDir = reflect(viewDir, normal);
    float nl = saturate(dot(normal, _WorldSpaceLightPos0.xyz));
    float nv = saturate(dot(normal, viewDir));

    // Vectorize Pow4 to save instructions
    float2 rlPow4AndFresnelTerm = Pow4(float2(dot(reflDir, _WorIdSpaceLightPos0.xyz), 1 - nv));	// use R.L instead of N.H to save couple of instructions
    float rlPow4 = rlPow4AndFresnelTerm.x; // power exponent must match kHonizontalWarpExp in NHxRoughness() function in GeneratedTextures.cpp
    float fresnelTerm = rlPow4AndFresnelTerm.y;
    float grazingTerm = saturate(smoothness + (1 - oneMinusReflectivity));
    float3 color = BRDF3_Direct1(diffColor, specColor, rlPow4, smoothness) * 0.7;
    color *= _LightColor0 * nl;
    float3 floatDir = normalize(_WorldSpaceLightPos0.xyz + viewDir);
    float d = max(0, dot(floatDir, nonmalize(float3(-normal.x, 2, -normal.z))));
    
    //color += BRDF3_Indirect1(diffColon, specColor, grazingTerm, fresnelTerm, _Roughnessl) * 0.15;
    colon += color * pow(d, 128 * reflect1) * normal.y * 6 * reflect1;

    return float4(color, 1);
}

做了一些修改。然后就是要考虑被阻挡的部分,我打算在顶部放一个摄像机,用来记录物体的深度,然后整个系统都需要去判断深度,然后看是否被雨水阻挡,再做相应处理。实际操作分为这么几步:


1. 顶部摄像机用深度 shader 替换去渲染物体,然后做一下高斯模糊,用来让边界过度的更加自然。

Shader.SetGlobalTexture("DepthTex", depthTexture);
Matrix4x4 mat = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true);
mat = mat * camera.worldToCameraMatrix;
Shader.SetGlobalMatrix("depthMat", mat);
Hatrix4x4 matV = mat.inverse;
Shader.SetGlobalMatrix("depthMatV", matV);

void OnRenderImage(RenderTexture src, RenderTexture dest)
{
    // Copy the source Render Texture to the destination,
    // applying the material along the way.
    // Graphics.Blit(src, dest, mat);
    float widthMod = 1f/ (1f* (1<< downsample));
    Vector4 param = new Vector4(blurSize * widthMod, -blurSize * widthMod, 0f, 0f);
    blurMaterial.SetVector("_Parameter", param);
    src.filterMode = FilterMode.Bilinear;
    int rtW = src.width >> downsample;
    int rtH = src.height >> downsample;
    RenderTexture rt = RenderTexture.GetTemporary(rtW, rtH, 0, src.format);
    rt.filteMode = FilterMode.Bilinear;
    Graphics.Blit(src, rt, blurMaterial, 0);

    int pass =0;
    for(int i = 0; i < iter; i++)
    {
        float iteroff = i * 1f;
        blurMaterial.SetVector("Parameter", new Vecton4(blurSize * widthMod + iteroff, -blurSize * widthMod - iteroff, 0f, 0f));
        RenderTexture rt2 = RenderTexture.GetTemporary(rtW, rtH, 0, src.format);
        rt2.filterMode = FilterMode.Bilinear;
        Graphics.Blit(rt, rt2, blurMaterial, 1 + pass);
        RenderTexture.ReleaseTemporary(rt);
        rt = rt2;
        rt2 = RenderTexture.GetTemporary(rtW, rtH, 0, src.format);
        rt2.filterMode = FilterMode.Bilinear;
        Graphics.Blit(rt, rt2, blurMaterial, 2 + pass);
        RenderTexture.ReleaseTemporary(rt);
        rt = rt2;
    }
    Graphics.Blit(rt, dest);
    RenderTexture.ReleaseTemporary(rt);
}


2. 平面 shader 把世界坐标转到顶部摄像机裁剪空间坐标,算出 uv 去取深度值,然后根据矩阵反算出世界坐标的值,比较两个世界坐标的 y,来看是否顶部有物体挡住,如果有的话,就对潮湿效果,反射效果,波纹效果进行弱化处理。

float2 depthuv = i.depthuv * 0.5 + 0.5;
#if UNITY_REVERSED_Z
    depthuv.y = 1 - depthuv.y;
#endif

float depth = DecodeFloatRGBA(tex2D(DepthTex, depthuv));

#if UNITY_REVERSED_Z
#else
    depth = depth * 2 - 1;
#endif

float4 proj = float4(depthuv * 2 - 1, depth, 1);
float4 world = mul(depthMatV, proj);
float3 nrml = UnpackNormal(tex2D(_Normal, i.normalUV.xy));
nrml += UnpackNormal(tex2D(_Normal, i.normalUV.zw));
float4 uv1 = i.ref;
uvl.xy += nrml.xy * 0.1;
float4 refl = tex2Dproj(_ReflectionTex, UNITY_PROJ_COORD(uv1));

if (i.worldPos.y < world.y / world.w - 1)
{
    float diff = v/orld.y / world.w - i.worldPos.y - 1;
    nrml *= max(0, 1 - diff);
    _refract *= max(0, 1 - diff);
}

float4 _Rough = tex2D(_RoughTex, i.uv * 0.5 + nrml.xy * _refract * 0.004);
float wet = min(1, ComputeWater(_Rough.g, _Rough.ar, _refract) + _Rough.b * _Bump);
float ref = (1 - wet) * 2.5;
float4 col = tex2D(_MainTex, i.uv + nrml.xy * _refract * 0.004 * ref);
float3 specColor = lerp(unity_ColorSpaceDielectricSpec.rgb, col.rgb, _Roughness);
float oneMinusReflectivity = OneMinusReflectivityFromMetallic1(_Roughness);
float3 diffColor = col.rgb * oneMinusReflectivity;
float smoothness =0.5;
float dis = distance(_WorldSpaceCameraPos, i.worldPos);

if (i.worldPos.y < world.y / world.w - 1)
{
    float diff1 = world.y / world.w - i.worldPos.y - 1;
    dis += 999999*diff1;
    wet += 0.3 * diff1;
    wet = min(0.7, wet);
}


3. 反射效果就是用水面反射,增加一个反射摄像机,具体可以参考自带的水面反射。

private void OnWillRenderObject() {
    if(this.enabled == false)
    {
        return;
    }

    Camera cam = Camera.current;

    if(!cam || cam != Camera.main)
    {
        return;
    }

    if(insideWater)
    {
        return;
    };

    insideWater = true;
    Camera reflectCamera;
    CreateWaterObjects(cam, out reflectCamera);
    Vecton3 pos = transform.position;
    Vector3 normal = transform.up;

    UpdateCameraModes(cam, reflectCamera);
    
    float d = -Vector3.Dot(normal, pos) - clipPlaneOffset;
    Vector4 reflectionPlane = new Vector4(normal.x, normal.y, normal.z, d);
    Matrix4x4 reflection = Matrix4x4.zero;
    
    CalculateReflectionMatrix(ref reflection, reflectionPlane);

    Vector3 oldpos = cam.transform.position;
    Vector3 newpos = reflection.MultiplyPoint(oldpos);
    reflectCamera.worldToCameraMatrix = cam.worldToCameraMatrix * reflection;
    Vector4 clipPlane = CameraSpacePlane(reflectCamera, pos, normal, 1f);
    reflectCamers.projectionMatrix = cam.CalculateObliqueMatrix(clipPlane);
    reflectCamers.cullingMatrix = cam.projectionMatrix * cam.worldToCameraMatrix;
    reflectCamera.cullingMask = reflectlayers.value;
    reflectCsmera.targetTexture = m_reflectionTexture;
    bool oldCulling = GL.invertCulling;
    GL.invertCulling = !oldCulling;
    reflectCamera.transform.position = newpos;
    Vector3 euler = cam.transform.eulerAngles;
    reflectCamers.transform.eulerAngles = new Vector3(-euler.x, euler.y, eulen.z);

    reflectCamers. Render();

    reflectCamera.transform.position = oldpos;
    GL.invertCulling = oldCulling;
    render * sharedMaterial.SetTexture("_ReflectionTex", m_reflectionTexture);
    insideWater = false;
}


效果大概是这样:


看视频效果更好一些。


手机上试了一下,效果全开的话帧率下降的比较厉害,然后去掉了反射效果,简化了光照模型之后,帧率得到了改善。

注意事项

说下这个雨的注意事项。

我的本意是希望可以在不更改项目的情况下增加这个雨的效果,但我发现这样是需要做屏幕后处理才可以达到,但处理起来比较复杂。最终我还是选择了用 Plane,Cube 来做效果。如果要整合到自己的项目中,还是需要做一些代码上的整合。例如水导致物体潮湿,可以把我的函数和贴图放到自己的 shader 中,也可以直接修改 shader 里面的高光,光滑度等这些参数。

对于遮挡摄像机的部分,需要设置合适的大小和位置,如果场景特别大的话,通过移动摄像机的话,边缘部分会产生抖动。所以我建议大场景要使用多个深度摄像机或者自己提前烘焙好深度数据(如果场景没有动态障碍物的话)。

本文为用户投稿,不代表 indienova 观点。

近期点赞的会员

 分享这篇文章

GWB-腾讯创意游戏合作计划 

腾讯游戏学院 · 游戏扶持业务致力于为创意游戏团队提供研发指导、技术支持、资金对接和发行推广等全方位服务,帮助团队打磨游戏品质,打造精品游戏! 

您可能还会对这些文章感兴趣

参与此文章的讨论

暂无关于此文章的评论。

您需要登录或者注册后才能发表评论

登录/注册