【UnityShader】凹凸映射


纹理的另一种常见应用就是凹凸映射,目的是使用一张纹理来修改模型表面的法线。让模型看起来‘’凹凸不平‘’。

主要有两种方法:一种是高度纹理,一种是法线纹理。这里主要介绍法线纹理。

由于法线方向的分量范围在【-1,1】,但像素的范围是【0,1】,所以要做一个映射:normal = pixel*2-1

在实际制作中,我们一般使用模型顶点的切线空间,法线一般为z轴,切线为x轴,法线和切线的叉积构成的副切线为y轴。

下面介绍在切线空间下计算纹理:在片元着色器中通过纹理采样得到切线空间的法线,在于切线空间下的视角方向、光照方向等进行计算,得到最终的光照结果。

为此,我们需要一个变化矩阵来将模型空间的参数转化为切线空间。

切线空间的转化矩阵容易求得,只需要将切线空间的坐标轴按列排列即可,数学原理这里省略,有兴趣可以自己推导。

Shader "Custom/Chapter7-NormalMapTangentSpace"
{
    Properties{
        _Color ("Color Tint", Color) = (1, 1, 1, 1)
        _MainTex ("Main Tex", 2D) = "white" {}
        _BumpMap ("Normal Map", 2D) = "bump" {}
        _BumpScale ("Bump Scale", Float) = 1.0
        _Specular ("Specular", Color) = (1, 1, 1, 1)
        _Gloss ("Gloss", Range(8.0, 256)) = 20
    }
    SubShader{
        pass{
            Tags{"LightMode" = "ForwardBase"}
        
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag

        #include "Lighting.cginc"

        fixed4 _Color;
        sampler2D _MainTex;
        sampler2D _BumpMap;
        float4 _BumpMap_ST;
        float _BumpScale;
        fixed4 _Specular;
        float _Gloss;
        float4 _MainTex_ST;//纹理类型属性

        struct a2v{
            float4 vertex:POSITION;
            float3 normal:NORMAL;
            float4 tangent:TANGENT; //顶点切线方向填充,四维的原因是要用w决定副切线的方向性
            float4 texcoord:TEXCOORD0;//存储纹理坐标  
        };

        struct v2f{
            float4 pos:SV_POSITION;
            float4 uv:TEXCOORD0;//纹理采样用 两个纹理所以用float4
            float3 lightDir:TEXCOORD1;
            float3 viewDir:TEXCOORD2;
        };

        v2f vert(a2v v){
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);

            o.uv.xy = v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw;
            o.uv.zw = v.texcoord.xy*_BumpMap_ST.xy+_BumpMap_ST.zw;  //两张纹理,分别存储纹理坐标
            
            float3 binormal = cross(normalize(v.normal),normalize(v.tangent.xyz))*v.tangent.w;//计算副法线
            float3x3 rotation = float3x3(v.tangent.xyz,binormal,v.normal);

            o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz;
            o.viewDir =  mul(rotation,ObjSpaceViewDir(v.vertex)).xyz;

            return o;
        }

        fixed4 frag(v2f i):SV_Target{
           fixed3 tangentLightDir = normalize(i.lightDir);
           fixed3 tangentViewDir = normalize(i.viewDir);
           //首先对法线纹理进行采样
           fixed4 packedNormal = tex2D(_BumpMap,i.uv.zw);
           //对像素反映射得到原来的发现方向,由于再切线空间先 保证z为正
           fixed3 tangentNormal;
           //纹理类型不为Normal map时  Unity一般为Normal map 所以下面方式计算的话结果会出差
           //tangentNormal.xy = (packedNormal.xy*2-1)*_BumpScale;
           //tangentNormal.z = sqrt(1.0-saturate(dot(tangentNormal.xy,tangentNormal.xy)));//因为是单位矢量,所以可以通过xy计算z
         
           tangentNormal = UnpackNormal(packedNormal);  
           tangentNormal.xy *=_BumpScale;
           tangentNormal.z = sqrt(1.0-saturate(dot(tangentNormal.xy,tangentNormal.xy)));
           
           fixed3 albedo = tex2D(_MainTex,i.uv).rgb*_Color.rgb;
           fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
           fixed3 diffuse = _LightColor0.rgb*albedo*max(0,dot(tangentNormal,tangentLightDir));

           fixed3 halfDir = normalize(tangentLightDir+tangentViewDir);

           fixed3 specular = _LightColor0.rgb*_Specular.rgb*pow(max(0,dot(tangentNormal,halfDir)),_Gloss);

           return fixed4(ambient+diffuse+specular,1.0);
        }
         ENDCG
      }
    }
    FallBack "Diffuse"
}

因为模型的法线,切线都可以求得,所有副法线也容易就得到了,进而转化矩阵也容易求得。

需要注意的是_BumpMap为法线纹理,bump为默认值,_BumpScale控制凹凸程度。

由于是两个纹理,所以uv声明为了float4.xy存储_MainTex纹理坐标,zw存储_BumpMap纹理坐标。

tangent切线是float4的原因是w分量决定副法线的方向。

两个纹理一般使用的是同一个纹理坐标texcoord。

注意法线纹理类型若是Normal map就需要使用Unity提供的UnpackNormal来映射,因为Unity会根据不同的平台对纹理进行压缩,再通过此函数进行采样。

下面是_BumpScale为-0.8和0.8时的情况: