实现范围体爆炸

对于实现游戏中的艺术效果,有时候需要在画质和运行效率上进行巧妙的权衡。在实现爆炸效果上尤其如此;因为它是很多游戏的核心效果,但是在它之后的一些物理计算通常都会超过现代计算机的算力。爆炸本质上就是一团温度非常高的火红气体;所以正确模拟它的唯一方式就是在游戏中用流体模拟来模拟它。正如你所想的一样,这在运行时是不可行的,在很多的游戏中都是通过粒子来模拟。当一个物体爆炸的时候,通常会同时产生很多的火花,烟雾和一些散落的碎片,这样可以获得一个比较像的爆炸。不幸的是,这种模拟方法很容易被看破而且可能不是很真实。这里我们会了解一种折中的技术来实现爆炸效果,并且画质更好:范围体爆炸(volumetric explosions) 。这个知识点背后的思考是我们不再把爆炸当作是一系列粒子的模拟;它们现在进阶到3D物体了,而不仅仅是扁平的2D贴图。


  • 始前准备
    我们通过下面的几个步骤来讲解这个知识点:
    1. 为这个效果创建一个新的着色器
    2. 创建一个新的材质,并且关联该着色器
    3. 把这个材质关联到一个球体模型上。你可以在编辑器上直接创建一个球体模型,通过菜单 GameObject | 3D Object | Sphere

    注意
    使用标准的Unity球体就可以很好的演示这个知识点,但是如果你想要更大范围的爆炸,那么你可能需要面数更多的球体。事实上,顶点函数只能修改网格的顶点。所有其他的点都可以通过修改相邻顶点的位置的方式来修改它们。顶点数越少,那么爆炸效果的精细度也就越低。
    1. 这个知识点中,你需要一个 渐变纹理(ramp texture) ,这个纹理需要有你爆炸的所有颜色梯度。你可以用GIMP或者PhotoShop工具创建一个跟下面类似的纹理:
      diagram
    2. 当你有了这个图片后,把它导入到你的Unity中。然后在 检查器面板(Inspector) 中,确保 Filter Mode 设置为 Bilinear ,然后 Wrap Mode 设置为 Clamp 。这两个设置是为了确保对渐变纹理平滑采样。
    3. 最后,你还需要一张 噪音纹理(noisy texture) 。你可以在网上搜索免费的噪音纹理。一般我们都使用 Perlin noise 。【这里我自己找到一个网站http://kitfox.com/projects/perlinNoiseMaker/

  • 操作步骤
    这个效果我们分两步来实现:通过顶点函数改变几何形状,通过表面函数给与正确的颜色。这两个步骤如下:

    1. 添加下面的属性到着色器中:
    _RampTex("Color Ramp", 2D) = "white" {}
    _RampOffset("Ramp offset", Range(-0.5,0.5))= 0
    _NoiseTex("Noise tex", 2D) = "gray" {}
    _Period("Period", Range(0,1)) = 0.5
    _Amount("_Amount", Range(0, 1.0)) = 0.1
    _ClipRange("ClipRange", Range(0,1)) = 1
    
    1. 添加相应变量,让着色器的Cg代码可以访问到它们:
    sampler2D _RampTex;
    half _RampOffset;
    sampler2D _NoiseTex;
    float _Period;
    half _Amount;
    half _ClipRange;
    
    1. 修改 输入结构体(Input structure) ,这样可以让它接收渐变纹理的UV数据:
    struct Input 
    {
        float2 uv_NoiseTex;
    };
    
    1. 添加下面的顶点函数:
    void vert(inout appdata_full v) 
    {
        float3 disp = tex2Dlod(_NoiseTex, float4(v.texcoord.xy,0,0));
        float time = sin(_Time[3] *_Period + disp.r*10);
        v.vertex.xyz += v.normal * disp.r * _Amount * time;
    }
    
    1. 添加下面的的表面函数:
    void surf(Input IN, inout SurfaceOutput o) 
    {
        float3 noise = tex2D(_NoiseTex, IN.uv_NoiseTex);
        float n = saturate(noise.r + _RampOffset);
        clip(_ClipRange - n);
        half4 c = tex2D(_RampTex, float2(n,0.5));
        o.Albedo = c.rgb;
        o.Emission = c.rgb*c.a;
    }
    
    1. 我们直接通过 #pragma 来指定我们要使用的顶点函数,通过添加 nolightmap 参数来阻止Unity添加真实光照到我们的爆炸效果中:
    #pragma surface surf Lambert vertex:vert nolightmap
    
    1. 最后一步,给我们球体模型选择我们刚刚创建的材质,然后在 检查器面板(Inspector) 中,为我们的材质添加噪音纹理和渐变纹理。这是一个动画材质,也就是说会随着时间变化。你可以观察材质的变化,只要在编辑器的 场景窗口(Scene Window) 中点击 Animated Materials
      diagram

  • 原理介绍
    在学习这个知识点的时候,如果你了解 表面着色器(Surface Shaders)顶点修饰(vertex modifiers) 的工作原理。这个效果背后的主要思路是以一种混乱的方式修改这个圆球几何图形的表面,然后使它看起来像真正的爆炸。下图所示是在Unity编辑器内这种爆炸看起来的样子。可以看到这个网格已经发生了明显的畸变: diagram


    顶点函数我们已经在这一章的 模型挤压 这个知识点介绍过。 不过这里的挤压是通过时间和噪音来决定如何表现的。


    注意
    在Unity中如果你需要一个随机数,你可以使用随机函数 Random.Range() 。在着色器中没有获取随机数的标准方法,最简单的方法就是通过对噪音贴图采样了。
    这里由于没有标准的方法,所以下面的代码只是举例:
    float time = sin(_Time[3] *_Period + disp.r*10);
    内建的 _Time[3] 变量可以获得着色器内部的时间,disp.r 是噪音贴图的红色通道,用于确保每一个顶点的运动都是独立的。三角函数 sin() 可以让这些顶点上下运动,用于模拟爆炸的混乱表现。接下来的代码是法线挤压:
    v.vertex.xyz += v.normal * disp.r * _Amount * time;
    你可以在运行的过程中调节这些数值直到你找到一个自己比较满意的爆炸的行为模式。
    该效果的最后一部分是通过 表面函数surface function 来获得的。在这里噪音纹理用来从渐变纹理中进行颜色的随机采样。但是这里有额外的两点需要注意。先介绍第一个: _RampOffset 。它用来强制爆炸从纹理的左边或者右边进行颜色的采样。如果这个是正数,那么爆炸的表面趋于更灰的色调;也就是爆炸慢慢消融。你可以使用 _RampOffset 来决定爆炸中该含有多少火焰或者烟尘。另一个该注意的是在表面函数中 clip() 函数的使用。它的作用是从 渲染管线rendering pipeline 中裁剪(移除)像素。当它的参数为负数时,当前的像素便不会被绘制。这个效果被属性 _ClipRange 控制,它决定了范围体爆炸中哪一部分将会是透明的。
    通过控制属性 _RampOffset_ClipRange ,你就可以完全的控制爆炸如何表现以及如何消融。


  • 额外内容
    这个知识点涉及的着色器可以让一个球体看起来像爆炸一样。这里只是抛砖引玉,如果你真的想使用它,你应该配合相应的一些脚本来获得更棒的效果。最好是创建一个爆炸物体然后把这个物体做成一个预制体,这样你就可以在任何需要的时候重复使用它。你可以直接拖拽这个球体,把它拖拽到 项目Project 窗口中。完成这一步后,你就创建了这个预制体,然后你就可以通过 Instantiate() 方法创建一些你想要的爆炸了。
    然而需要注意的是,所有创建的这些爆炸对象它们的材质都是一样的,所以看起来都一个样。如果你在同一时刻有多个爆炸,那么它们不应该使用同一个材质。当你实例化一个新的爆炸的时候,你还应该再复制一份材质。你可以很容易的做到,下面是参考的代码片段:
    GameObject explosion = Instantiate(explosionPrefab) as GameObject;
    Renderer renderer = explosion.GetComponent<Renderer>();
    Material material = new Material(renderer.sharedMaterial);
    renderer.material = material;
    
    最后,如果你想在次世代画面表现中使用这个着色器,那么你应该根据你想要创建的爆炸类型,给这个爆炸体添加相应的脚本用于修改它的大小,_RampOffset_ClipRange 以达到对应的效果。

  • 相关补充
    我们还能做更多的工作来让爆炸效果更加的真实。在这个知识点中我们所展示的方式只是产生了一个爆炸的大体形状而已,爆炸的内部其实是空的。一个简单的技巧就是在爆炸的内部生成粒子。然而,你能做的其实也就这么多了。由Unity官方跟 NvidiaPassion Pictures 合作制作的短片 The Butterfly Effect http://unity3d.com/pages/butterfly,是相关的最棒的演示。这个演示效果同样是基于修改球形几何体形状这一概念,不过它是通过一项叫做 体积光线投射算法(volume ray casting) 的技术来渲染的。简单的说,这种技术可以让几何体的渲染看起来像是饱满的。下图展示了一个示例:
    diagram
    如果你在寻找高质量的爆炸效果,可以在Unity的资产商店搜索 Pyro Technix https://assetstore.unity.com/packages/vfx/shaders/pyro-technix-16925。这个资源包括了范围体爆炸并且其中一些还有真实的冲击波效果。