模型挤压

重复是游戏当中最要命的一个问题之一。在游戏中创建新内容是一项费时的任务,当你要创建成千上万的敌人时,那么有很大的可能这些敌人有可能会看起来一样。这个时候利用着色器修改模型的基本几何图形,从而让模型产生不同的变化,可以说是一种 相对来说性能较好的方法。这个知识点中我们会给你演示一种叫做 法线挤压(normal extrusion) 的技术,可以用来创建胖的或者瘦的模型,就比如下图所示的来自Unity的demo中的士兵:

diagram


  • 始前准备
    在这个知识点中,我们需要获取要挤压的模型的着色器。获得该着色器后,复制它,因为这样能安全的编辑拷贝的着色器。我们可以按照下面的步骤进行:

    • 1.找到模型使用的着色器[原着色器就是一个标准的着色器,没有找到就自己新建一个也行],通过快捷键 Ctrl + D 复制它。
    • 2.复制模型原有的材质,并且将之前复制的着色器添加给它。
    • 3.把这个新的材质添加到模型上,并且开始编辑它。

    为了达到效果,你的模型还要有 法线(normals)


  • 操作步骤
    为了创建这个效果,我们先来编辑刚刚复制的着色器:

    • 1.首先给我们的着色器添加一个属性,用于调整压缩效果。我们这里的调节范围是从 -0.000050.00005 【书上是-1到1,但实际上书上的范围太大了,各位可以自己试试】,当然你可以根据自己的需要调节这个范围【 _MainTex 是需要的,因为人物是需要贴图的。】:
    _MainTex("Texture", 2D) = "white" {}
    _Amount ("Extrusion Amount", Range(-0.00005, 0.00005)) = 0
    
    • 2.我们的属性和变量是成对出现的,在着色器中定义下面的变量:
    float _Amount;
    
    • 3.修改 pragma 预编译指令让Unity知道我们要使用 顶点修饰(vertex modifier) 。然后在它后面添加 vertex:function_name,function_name就是你自定义的方法名,当我我们这里叫 vert
    #pragma surface surf Lambert vertex:vert
    
    • 4.添加下面的顶点修改代码:
    void vert (inout appdata_full v) 
    {
        v.vertex.xyz += v.normal * _Amount;
    }
    
    • 5.这样着色器就写好了;这样你就可以在 检查器面板(Inspactor) 中通过修改材质上的 Amount 参数来控制士兵的胖瘦了。【书本写的不清楚,其实还有贴图的代码,作者没有交代,所以完整的代码如下:】
    Shader "Custom/Normal Extrusion" 
    {
        Properties 
        {
            _MainTex ("Albedo (RGB)", 2D) = "white" {}
            _Amount ("Extrusion Amount", Range(-0.00005, 0.00005)) = 0
        }
        SubShader 
        {
            Tags { "RenderType"="Opaque" }
            LOD 200
    
            CGPROGRAM
            // Physically based Standard lighting model, and enable shadows on all light types
            #pragma surface surf Lambert vertex:vert
    
            // Use shader model 3.0 target, to get nicer looking lighting
            #pragma target 3.0
    
            sampler2D _MainTex;
    
            struct Input 
            {
            	float2 uv_MainTex;
            };
    
            float _Amount;
            // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
            // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
            // #pragma instancing_options assumeuniformscaling
            UNITY_INSTANCING_CBUFFER_START(Props)
            	// put more per-instance properties here
            UNITY_INSTANCING_CBUFFER_END
    
            void vert (inout appdata_full v) 
            {
            	v.vertex.xyz += v.normal * _Amount;
            }
    
            void surf (Input IN, inout SurfaceOutput o) 
            {
            	// Albedo comes from a texture tinted by color
            	o.Albedo = tex2D(_MainTex, IN.uv_MainTex).rgb;
            }
            ENDCG
    	}
        FallBack "Diffuse"
    }
    

  • 原理介绍
    表面着色器(Surface shader) 分两步来工作。在前面的章节中,我们只是探索了它最后一个步骤:表面函数(surface function。这里我们接触了另一种函数:顶点修饰(vertex modifier)。它接收一个顶点的数据结构 (通常这个数据结构叫做 appdata_full )并且对它进行转变。它让我们对于模型的几何图形做各种视觉效果给与很大的自由度。我们通过在表面着色器的预编译指令 #pragma 那里添加 vertex:vert 来告知GPU 我们添加了 自己的顶点函数。你可以查看 第六章片元着色器和抓取通道 来学习怎么在顶点着色器和片元着色器中定义顶点修饰。

    法线挤压是改变模型的几何图形中最简单,也是最有效的技术之一。它是利用顶点对法线方向的投影来工作的。它是通过下面的着色器代码来实现的:

    v.vertex.xyz += v.normal * _Amount;
    

    顶点的位置会被一个朝向法线的 _Amount 单位长度的向量取代。如果 _Amount的数值太大,呈现的效果会让人有点不舒服【脑袋特别大,而且很恐怖】。所以要通过一些比较小的合理的值,这样就可以让你的模型可以获得大量的不同的变体。


  • 额外内容
    如果你有多个敌人并且希望每个敌人都有不同的体重【也就是胖瘦不一样】,那么你就要为每个不同的敌人创建不同的材质。这是必要的操作,这是因为模型之间通常共用一个材质,为一个敌人改变了这个材质那么其他所有用了该材质的 的敌人也会跟着改变。这里有几种方法可以为你做这件事。下面的脚本,只要添加到有 Renderer 的游戏对象上,就会自动复制一份它的第一个材质并且自动设置好 _Amount 属性:

    using UnityEngine;
    public class NormalExtruder : MonoBehaviour 
    {
        [Range(-0.0001f, 0.0001f)]
        public float amount = 0;
        // Use this for initialization
        void Start () 
        {
            Material material = GetComponent<Renderer>().sharedMaterial;
            Material newMaterial = new Material(material);
            newMaterial.SetFloat("_Amount", amount);
            GetComponent<Renderer>().material = newMaterial;
        }
    }
    

    添加挤压贴图
    这个技术其实还可以进一步的提升。我们可以添加一个额外的纹理(或者使用主纹理的alpha通道)来表示挤压的程度。这样可以让我们对于哪一部分应该隆起或者凹陷有更好控制。下面的代码向你展示了如何获得这种效果:

    sampler2D _ExtrusionTex;
    void vert(inout appdata_full v) 
    {
        float4 tex = tex2Dlod (_ExtrusionTex, float4(v.texcoord.xy,0,0));
        float extrusion = tex.r * 2 - 1;
        v.vertex.xyz += v.normal * _Amount * extrusion;
    }
    

    _ExtrusionTex 的红色通道被用来作为法线挤压相乘的系数。如果是0.5的话模型不会有什么影响;它的暗亮程度会被用于表示顶点的向内或向外挤压。有一点你需要注意的是,如果是用顶点修饰来对纹理采样,tex2Dlod 应该 用 tex2D 来替换。

    注意
    在着色器中,颜色通道的值的范围值 01,但有时候你又想用负数(比如你想用负数表示向内挤压)。那么针对这种情况,你可以用0.5表示0,比0.5小的看成负数,比0.5大的看成正数。其实在RGB编码的纹理中,就是用这 中方法来表示法线的。比如UnpackNormal() 函数就被用来作为 (0, 1) 映射到 (-1, 1) 的映射函数。它其实等同于数学表达式 tex.r * 2 -1

    挤压贴图特别适合用来表现丧尸化的人物,方法就是通过扭曲人物的皮肤来突出皮肤下面的骨头的形状。下图向你展示了如何把一个健康的士兵,通过一个着色器和挤压贴图,变成一具毫无生气尸体的。对比与前面的几个例子,你可以注意到 那些衣服是如何不受影响的【就是挤压贴图里,对衣服进行挤压的数值是0.5】。下图所使用的着色器通过让挤压贴图的某些区域变得更黑,让士兵看起来更加消瘦:

    diagram