实现下雪效果着色器

在游戏中模拟下雪效果一直都是一件有挑战的事情。大部分的游戏都会简单的直接在模型的纹理上包含雪,让这些模型看起来雪白。然而要是其中某个模型开始旋转了呢?雪并不是敷衍了事的表面工作;它应该被当做是一些材料的合理的堆积【意思是物体表面 的雪是雪花一点一点堆积起来的,而不是简简单单的给它一张白色的贴图】。在这个知识点中将会向你展示如何用一个着色器让你的模型看起来有种下雪的样子。

要完成这个效果有两个步骤。首先,对于朝向天空的三角面我们给它白色。其次,通过挤压顶点来模拟雪的堆积效果。你可以从下图看到最终的效果:

diagram

注意
本知识点并无意去创建那种超真实的下雪效果。它只是抛砖引玉,但在你的游戏当中,最终的效果定位,还是取决于你们的艺术家们,通过他们设置正确的纹理和参数来达到你的要求。


  • 始前准备
    这个效果完全基于着色器来实现,所以请按照下面的步骤操作:
    • 1.为雪的效果创建一个新的着色器。
    • 2.为这个着色器创建一个新的材质。
    • 3.把这个材质添加到你想表现雪的效果的模型上去。

  • 操作步骤
    为了创建下雪的效果,请打开你的着色器,然后做以下的修改:

    • 1.把下面的属性块替换掉你原来的着色器属性块:
    _MainColor("Main Color", Color) = (1.0,1.0,1.0,1.0)
    _MainTex("Base (RGB)", 2D) = "white" {}
    _Bump("Bump", 2D) = "bump" {}
    _Snow("Level of snow", Range(1, -1)) = 1
    _SnowColor("Color of snow", Color) = (1.0,1.0,1.0,1.0)
    _SnowDirection("Direction of snow", Vector) = (0,1,0)
    _SnowDepth("Depth of snow", Range(0,1)) = 0
    
    • 2.添加与属性块对应的变量:
    sampler2D _MainTex;
    sampler2D _Bump;
    float _Snow;
    float4 _SnowColor;
    float4 _MainColor;
    float4 _SnowDirection;
    float _SnowDepth;
    
    • 3.用下面的代码替换掉原来的 输入结构体(Input structure)
    struct Input
    {
        float2 uv_MainTex;
        float2 uv_Bump;
        float3 worldNormal;
        INTERNAL_DATA
    };
    
    • 4.用下面的表面函数替换掉原有的表面函数。它会让模型着雪的部分变成白色:
    void surf(Input IN, inout SurfaceOutputStandard o)
    {
        half4 c = tex2D(_MainTex, IN.uv_MainTex);
        o.Normal = UnpackNormal(tex2D(_Bump, IN.uv_Bump));
        if (dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) >= _Snow)
            o.Albedo = _SnowColor.rgb;
        else
            o.Albedo = c.rgb * _MainColor;
        o.Alpha = 1;
    }
    
    • 5.配置 #pragma 预编译指令,让我们可以使用顶点修饰:
    #pragma surface surf Standard vertex:vert
    
    • 6.添加下面的顶点修饰,这样就可以对被雪覆盖部分的顶点进行挤压:
    void vert(inout appdata_full v)
    {
        float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection);
        if (dot(v.normal, sn.xyz) >= _Snow)
        v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow;
    }
    

    现在你可以去你的模型材质 检查器面板(Inspector tab) 查看,然后通过调节上面的参数,你可以调节雪的覆盖面积和厚度。


  • 原理介绍
    这个着色器按照下面两个步骤工作。


    给表面添加颜色
    首先是修改朝向天空的三角面的颜色。它会影响所有法线方向跟 _SnowDirection 方向相同三角面。正如前面 第三章理解光照模型 中介绍的那样,比较两个单位向量相似度可以用 点积(dot product)。当两个向量垂直的时候, 它们的点积是0;当两个向量平行的时候,它们的点积是1。_Snow 这个属性则用来考虑,这个值究竟是多少,才认为是朝向天空的。

    如果你仔细观察表面函数,你能发现我们并没有直接算法线跟下雪方向的点积。这是因为它们通常是在不同的坐标系中定义的。下雪的方向来自世界坐标系,而模型的法线通常是相对模型本身来说的。如果我们旋转模型,它的法线不会变,这不是我们 想要的。为了解决这个问题,我们需要把法线从本地坐标转换成世界坐标。这个过程可以用 WorldNormalVector() 这个函数来完成,就如下面的代码一样:

    if (dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) >= _Snow)
        o.Albedo = _SnowColor.rgb;
    else
        o.Albedo = c.rgb * _MainColor;
    

    这个着色器仅仅只是把模型着雪的地方变成白色;还有更高级的操作,比如在 SurfaceOutputStandard 结构体中用写实的纹理和参数对其进行初始化。


    修改几何图形
    该着色器的第二个效果是修改几何图形来模拟雪的堆积。首先,我们用了跟表面函数中相同的条件做测试,标记出了那些三角面是白色的。但是在这里比较遗憾的是我们不能使用 WorldNormalVector() 函数,因为在顶点修饰那里 SurfaceOutputStandard 结构体还没有被初始化。我们使用另一种方法来代替,就是把 _SnowDirection 转换成本地坐标:

    float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection);
    

    接着,我们就可以挤压几何图形来模拟雪的堆积了:

    if (dot(v.normal, sn.xyz) >= _Snow)
        v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow;
    

    在强调一遍,这个效果是一个很基础的效果。比如我们可以用一张纹理贴图来精确控制雪的堆积效果,或者一些奇怪的,不平坦的效果表现。