实现一个毛皮效果的着色器

材质的外观取决于它的物理结构。着色器试图去模拟它们,但在那样做的过程中,它们都把光的行为方式过度的简化了。因为材质有肉眼可见的复杂结构所以渲染起来尤其的难。比如大多数的纺织布料和动物的毛皮。在这个知识点中将会展示去模拟皮毛和其他材质(比如草)的可能性,而不仅仅是限于那些平坦的的表面模拟。为了完成这些,同样的材质将会被一遍又一遍的进行多次绘制,每一次都会增加它的大小。因此创造了皮毛的假象。

这里着色器所呈现出的效果基于了 Jonathan CzeckAras Pranckevičius 的工作成果:
diagram


  • 始前准备
    为了让这个知识点能起效果,你需要准备两样东西。首先是一张皮毛的纹理因为它要呈现外观。其次是另外一张用来表示皮毛分布的纹理,并且它要跟前一张纹理高度匹配。下面的图片展示的是美洲豹的毛皮纹理(左)和它可能的控制遮罩纹理(右):
    diagram

    白像素的控制遮罩将从原始的材质挤压而来,模拟了一张毛皮。那些白像素分布稀疏程度非常的重要,因为它能给我们一种材料是由很多细小的毛发构成的假象。创建一张那样的纹理的简单方式如下:
    • 1.给你的原始纹理设置一个阈值,好让原始纹理失去毛皮厚度的时候截取斑点纹理。
    • 2.添加一个噪音过滤器让图像像素画。噪音的RGB通道一定去掉关联从而获得黑白的结果。
    • 3.为了看起来真实一点,覆盖一个 Perlin 噪音过滤器来给毛皮添加一些变化。
    • 4.最后,再应用一个阈值过滤器让纹理的像素更好的分离。
      跟其他着色器开始前一样,你需要创建一个新的 标准着色器(standard shader) 和材质来容纳它们。

  • 操作步骤
    对于这个知识点,我们能开始修改我们的 标准着色器(standard shader) 了:

    • 1.在着色器的 属性(Properties) 上添加下面的代码:
    Properties 
    {
      _Color ("Main Color", Color) = (1,1,1,1)
      _MainTex ("Base (RGB) Trans (A)", 2D) = "white" {}
      _Glossiness ("Smoothness", Range(0,1)) = 0.5
      _Metallic ("Metallic", Range(0,1)) = 0.0
    
      _FurLength ("Fur Length", Range (.0002, 1)) = .25
      _Cutoff ("Alpha cutoff", Range(0,1)) = 0.5 // how "thick"
      _CutoffEnd ("Alpha cutoff end", Range(0,1)) = 0.5 // how thick they are at the end
      _EdgeFade ("Edge Fade", Range(0,1)) = 0.4
    
      _Gravity ("Gravity direction", Vector) = (0,0,1,0)
      _GravityStrength ("G strenght", Range(0,1)) = 0.25
    }
    
    • 2.这个着色器要求你重复同一个通道多次。你将会使用在 使用CG包含让着色器模块化 部分介绍的技术在外部文件中的单个通道来对所有的代码进行必要的分组整理。让我们使用下面的代码开始创建一个名为 FurPass.cginc 的新CG包含文件:
    #pragma target 3.0
    
    fixed4 _Color;
    sampler2D _MainTex;
    half _Glossiness;
    half _Metallic;
    
    uniform float _FurLength;
    uniform float _Cutoff;
    uniform float _CutoffEnd;
    uniform float _EdgeFade;
    
    uniform fixed3 _Gravity;
    uniform fixed _GravityStrength;
    
    void vert (inout appdata_full v)
    {
    	fixed3 direction = lerp(v.normal, _Gravity * _GravityStrength + v.normal * (1-_GravityStrength), FUR_MULTIPLIER);
    	v.vertex.xyz += direction * _FurLength * FUR_MULTIPLIER * v.color.a;
    	//v.vertex.xyz += v.normal * _FurLength * FUR_MULTIPLIER * v.color.a;
    }
    
    struct Input 
    {
    	float2 uv_MainTex;
    	float3 viewDir;
    };
    
    void surf (Input IN, inout SurfaceOutputStandard o) 
    {
    	fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
    	o.Albedo = c.rgb;
    	o.Metallic = _Metallic;
    	o.Smoothness = _Glossiness;
    
    	//o.Alpha = step(_Cutoff, c.a);
    	o.Alpha = step(lerp(_Cutoff,_CutoffEnd,FUR_MULTIPLIER), c.a);
    
    	float alpha = 1 - (FUR_MULTIPLIER * FUR_MULTIPLIER);
    	alpha += dot(IN.viewDir, o.Normal) - _EdgeFade;
    
    	o.Alpha *= alpha;
    }
    
    • 3.返回你开始的哪个着色器然后再 ENDCG 后面添加这个额外的通道:
    CGPROGRAM
    #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
    #define FUR_MULTIPLIER 0.05
    #include "FurPass.cginc"
    ENDCG
    
    • 4.要添加更多的通道,需要逐渐增加 FUR_MULTIPLIER。你可以得到20个通道,从 0.050.95

    当着色器编译完成并且挂载到一个材质上时,你就可以从 检查器(Inspector) 上修改它的外观了。Fur Length 属性决定了毛皮外壳的厚度,也就是毛发的长度。越长的毛发可能需要更多的通道来让它看起来更真实。Alpha CutoffAlpha Cutoff End 被用于控制毛发的浓密程度和毛发的逐渐变疏程度。Edge Fade 决定了毛皮的最终透明度,让毛皮看起来有种毛茸茸的效果。软质的材料应该要有 high Edge Fade 这个属性。最后,Gravity DirectionGravity Strength 可以让这层毛皮模拟重力的效果。
    [原书作者这里的代码由有个严重问题,就是书上的代码跟实际能运行起来的代码有很大的差别,我在这后面贴上着色器的完整代码,译者注]。

    • 着色器的完整代码:
    Shader "Custom/IAFS" 
    {
      Properties 
      {
          _Color ("Main Color", Color) = (1,1,1,1)
          _MainTex ("Base (RGB) Trans (A)", 2D) = "white" {}
          _Glossiness ("Smoothness", Range(0,1)) = 0.5
          _Metallic ("Metallic", Range(0,1)) = 0.0
    
          _FurLength ("Fur Length", Range (.0002, 1)) = .25
          _Cutoff ("Alpha cutoff", Range(0,1)) = 0.5 // how "thick"
          _CutoffEnd ("Alpha cutoff end", Range(0,1)) = 0.5 // how thick they are at the end
          _EdgeFade ("Edge Fade", Range(0,1)) = 0.4
    
          _Gravity ("Gravity direction", Vector) = (0,0,1,0)
          _GravityStrength ("G strenght", Range(0,1)) = 0.25
      }
    
      SubShader 
      {
          Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
          ZWrite On
          LOD 200
    
          CGPROGRAM
          #pragma surface surf Standard fullforwardshadows
          #pragma target 3.0
    
          fixed4 _Color;
          sampler2D _MainTex;
          half _Glossiness;
          half _Metallic;
    
          struct Input 
          {
              float2 uv_MainTex;
          };
    
          void surf (Input IN, inout SurfaceOutputStandard o) 
          {
              fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
              o.Albedo = c.rgb;
              o.Metallic = _Metallic;
              o.Smoothness = _Glossiness;
              o.Alpha = c.a;
          }
          ENDCG
    
          CGPROGRAM
          #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
          #define FUR_MULTIPLIER 0.05
          #include "FurPass.cginc"
          ENDCG
    
          CGPROGRAM
          #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
          #define FUR_MULTIPLIER 0.1
          #include "FurPass.cginc"
          ENDCG
    
          CGPROGRAM
          #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
          #define FUR_MULTIPLIER 0.15
          #include "FurPass.cginc"
          ENDCG
    
          CGPROGRAM
          #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
          #define FUR_MULTIPLIER 0.20
          #include "FurPass.cginc"
          ENDCG
    
          CGPROGRAM
          #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
          #define FUR_MULTIPLIER 0.25
          #include "FurPass.cginc"
          ENDCG
    
          CGPROGRAM
          #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
          #define FUR_MULTIPLIER 0.30
          #include "FurPass.cginc"
          ENDCG
    
          CGPROGRAM
          #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
          #define FUR_MULTIPLIER 0.35
          #include "FurPass.cginc"
          ENDCG
    
          CGPROGRAM
          #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
          #define FUR_MULTIPLIER 0.40
          #include "FurPass.cginc"
          ENDCG
    
          CGPROGRAM
          #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
          #define FUR_MULTIPLIER 0.45
          #include "FurPass.cginc"
          ENDCG
    
          CGPROGRAM
          #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
          #define FUR_MULTIPLIER 0.50
          #include "FurPass.cginc"
          ENDCG
    
          CGPROGRAM
          #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
          #define FUR_MULTIPLIER 0.55
          #include "FurPass.cginc"
          ENDCG
    
          CGPROGRAM
          #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
          #define FUR_MULTIPLIER 0.60
          #include "FurPass.cginc"
          ENDCG
    
          CGPROGRAM
          #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
          #define FUR_MULTIPLIER 0.65
          #include "FurPass.cginc"
          ENDCG
    
          CGPROGRAM
          #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
          #define FUR_MULTIPLIER 0.70
          #include "FurPass.cginc"
          ENDCG
    
          CGPROGRAM
          #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
          #define FUR_MULTIPLIER 0.75
          #include "FurPass.cginc"
          ENDCG
    
          CGPROGRAM
          #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
          #define FUR_MULTIPLIER 0.80
          #include "FurPass.cginc"
          ENDCG
    
          CGPROGRAM
          #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
          #define FUR_MULTIPLIER 0.85
          #include "FurPass.cginc"
          ENDCG
    
          CGPROGRAM
          #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
          #define FUR_MULTIPLIER 0.90
          #include "FurPass.cginc"
          ENDCG
    
          CGPROGRAM
          #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
          #define FUR_MULTIPLIER 0.95
          #include "FurPass.cginc"
          ENDCG
      }
      Fallback "Diffuse"
    }
    
    • FurPass.cginc 的完整代码:
    #pragma target 3.0
    
    fixed4 _Color;
    sampler2D _MainTex;
    half _Glossiness;
    half _Metallic;
    
    uniform float _FurLength;
    uniform float _Cutoff;
    uniform float _CutoffEnd;
    uniform float _EdgeFade;
    
    uniform fixed3 _Gravity;
    uniform fixed _GravityStrength;
    
    void vert (inout appdata_full v)
    {
      fixed3 direction = lerp(v.normal, _Gravity * _GravityStrength + v.normal * (1-_GravityStrength), FUR_MULTIPLIER);
      v.vertex.xyz += direction * _FurLength * FUR_MULTIPLIER * v.color.a;
      //v.vertex.xyz += v.normal * _FurLength * FUR_MULTIPLIER * v.color.a;
    }
    
    struct Input 
    {
      float2 uv_MainTex;
      float3 viewDir;
    };
    
    void surf (Input IN, inout SurfaceOutputStandard o) 
    {
      fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
      o.Albedo = c.rgb;
      o.Metallic = _Metallic;
      o.Smoothness = _Glossiness;
    
      //o.Alpha = step(_Cutoff, c.a);
      o.Alpha = step(lerp(_Cutoff,_CutoffEnd,FUR_MULTIPLIER), c.a);
    
      float alpha = 1 - (FUR_MULTIPLIER * FUR_MULTIPLIER);
      alpha += dot(IN.viewDir, o.Normal) - _EdgeFade;
    
      o.Alpha *= alpha;
    }
    

  • 原理介绍
    这个知识点展示的技术被称为 Lengyel 同心毛皮表皮技术(Lengyel’s concentric fur shell technique),或者简单一点就叫表皮技术。它的工作方式是逐步创建更大的,需要被渲染的几何图形的副本。通过调整合适的透明度,从而产生一种连续的线状毛发的错觉:
    diagram

    这种表皮技术是一种用途极其广泛并且相对容易实现的技术。从现实角度来说,真正的毛皮不仅需要挤压模型的几何图形,还需要改变它们顶点。当然也可以用更高级的 曲面细分着色器(tessellation shaders) 实现,但这已经超出了本书的范畴。

    毛皮着色器中的每一个通道都包含在 FurPass.cginc 中。顶点函数为模型创建了一个稍大一点的版本,这个过程基于法线挤压原理。此外,重力效果也考虑进来了,所以离中心越远,毛皮就变得越浓密:
    void vert (inout appdata_full v)
    {
      fixed3 direction = lerp(v.normal, _Gravity * _GravityStrength + v.normal * (1-_GravityStrength), FUR_MULTIPLIER);
      v.vertex.xyz += direction * _FurLength * FUR_MULTIPLIER * v.color.a;
    }
    
    在这个例子中,alpha通道被用来决定毛皮的最终长度。这可以让我们对毛皮有更精确的控制。

    最后,表面函数从alpha通道中获取 控制遮罩(control mask)。它利用 截断值(cutoff value) 来判断哪些像素应该显示哪些应该隐藏。毛皮表皮效果中,这个值从从最开始的 Alpha Cutoff 往最终的 Alpha Cutoff End 变化:
    o.Alpha = step(lerp(_Cutoff,_CutoffEnd,FUR_MULTIPLIER), c.a);
    
    float alpha = 1 - (FUR_MULTIPLIER * FUR_MULTIPLIER);
    alpha += dot(IN.viewDir, o.Normal) - _EdgeFade;
    
    o.Alpha *= alpha;
    
    毛皮的最终alpha值也会受它跟摄像机的角度的影响,能让它有一个柔软的感观。

  • 额外内容
    毛皮着色器被用来模拟毛皮效果。然而不仅于此,它还可以被用于模拟其他各种各样的材质。对于一些自然就有多层次的材料来说它也很适合,比如森林的树冠,蓬松的云朵,人类的头发,甚至是草皮等。

    对于如何显著提地升毛皮的真实性还有很多其他方面的改进。比如你还可以根据当前的时间,去改变重力的方向,为毛皮效果添加一个简单风的动画。如果经过正确的调整,那么毛皮就会随风而动,这能更加让人印象深刻。

    还有,你还可以在角色移动的时候让毛皮也跟着动。所有的这些小改动都能增加毛皮效果的可信度,让人错认为这就是毛皮而不是画在表面的静态材质。但不幸的是,20个通道的性能消耗非常的大。然而通道的数量的多少又决定了材质看起来的真实度。所以你应该根据你的实际情况找到最适合的毛皮长度和通道数量。考虑到这个着色器的性能影响,准备几个不同通道数量的材质球是一个明智的选择;这样你可以在不同的距离上使用不同的材质球从而可以节省大量的计算。