【UnityShader】凹凸映射之高度贴图和法线贴图

目录

  • 基础知识
  • 高度纹理
  • Unity 中的法线纹理类型
  • 法线纹理

基础知识

  • 纹理的另一种场景的应用就是凹凸映射。凹凸映射的目的是使用一张纹理来修改模型表面的法线,以便为模型提供更多的细节。这种方法不会真的改变模型的顶点位置,只是让模型看起来好像是"凹凸不平"的,可以从模型的轮廓处看出“破绽”。

  • 有两种主要的方法可以用来进行凹凸映射:

    1. 使用一张高度纹理来模拟表面位移,然后得到一个修改后的法线,这种被称为高度映射;
    2. 使用一张法线纹理来直接存储表面法线,这种被称为法线映射;
  • 注意:

    • 凹凸映射,从纹理中得到的法线,只会影响光照模型。
    • float4 tangent:TANGENT; // 切线,float4 类型,用tangent.w 分量来决定切线空间中第三个轴——副切线y的方向。
  • 采样获取法线

fixed3 bump = UnpackNormal(tex2D(_BumpMap, v.uv.zw));

//上计算等价于
fixed4 packedNormal = tex2D(_BumpMap, v.uv.zw);
fixed3 tangentNormal;
tangentNormal.xy = (packedNormal.xy * 2 - 1) *_BumpScale;
tangentNormal.z = sqrt(1-saturate(dot(tangentNormal.xy, tangentNormal.xy))); 

原因:纹理坐标中只记录 xy,z 需要计算得到。而且 xy 是经过映射的 pixed = (normal + 1) / 2,需要首先进行反映射,然后求 z。

  • 使用 UnpackNormal 函数对法线进行采样和解码时,需要把纹理格式标识为 Normal map。
  • 在 Unity5.x 中,所有的内置 Unity shader 都是用世界空间来进行光照计算。

高度纹理

  • 高度图中存储的是强度值,它用来表示模型表面局部的海拔高度。颜色越浅表明该位置的表面越向外凸起,颜色越深表明该位置越向里凹;这种方法的好处是直观,但缺点是计算更加复杂,在实时计算时不能直接得到表面法线,而是需要由像素的灰度值来计算而得,因此需要消耗更多的性能。
  • 高度图通常会和法线映射一起使用,用于给出表面凹凸的额外信息,也就是说,我们通常会使用法线映射来修改光照。

Unity 中的法线纹理类型

  • 当使用包含了法线映射的内置的UnityShader时,必须把使用的法线纹理标识成Normalmap才能有正确结果。这是因为UnityShader都是用来内置的UnpackNormal函数来采样法线方向。
  • 当把纹理类型设置为Normalmap时,Unity根据不同平台进行压缩,再通过UnpackNormal函数来针对不同的压缩格式对法线纹理进行正确的采样。可以通过源码查看
		inline fixed3 UnpackNormalDXT5nm (fixed4 packednormal)
		{
		    fixed3 normal;
		    normal.xy = packednormal.wy * 2 - 1;
		    normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
		    return normal;
		}

		// Unpack normal as DXT5nm (1, y, 1, x) or BC5 (x, y, 0, 1)
		// Note neutral texture like "bump" is (0, 0, 1, 1) to work with both plain RGB normal and DXT5nm/BC5
		fixed3 UnpackNormalmapRGorAG(fixed4 packednormal)
		{
		    // This do the trick
		   packednormal.x *= packednormal.w;

		    fixed3 normal;
		    normal.xy = packednormal.xy * 2 - 1;
		    normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
		    return normal;
		}
		inline fixed3 UnpackNormal(fixed4 packednormal)
		{
		#if defined(UNITY_NO_DXT5nm)
		    return packednormal.xyz * 2 - 1;
		#else
		    return UnpackNormalmapRGorAG(packednormal);
		#endif
		}

从代码中可以看到,在 DXT5nm 格式的法线纹理中,纹素为 (1, y, 1, x);在 BC5 格式中则为(x, y, 0, 1) 。

  • 法线纹理坐标中只记录xy,因为它只有两个通道是真正必不可少的,第三个通道的值可以用另外两个推导出来(法线是单位向量,并且切线空间下的法线方向的z分量始终为证),使用这种压缩可以减少法线纹理占用的存储空间。
  • 当把纹理类型设置为 Normal map 后,还有一个复选框 Create from Grayscale,这个复选框作用是从高度图中生成法线纹理。高度图本身记录的是相对高度,是一张灰度图。勾选复选框后,就可以把该纹理和切线空间下的法线纹理同等对待了。
    勾选复选框后
    • Bumpiness 用于控制凹凸程度
    • Filtering 决定使用哪种方式来计算凹凸程度

法线纹理

  • 法线纹理中存储的就是表面的法线方向。由于法线方向的分量范围在[-1,1],而像素的分量范围为[0, 1],因此需要做一个映射,通常使用的映射就是:

    pixed = (normal + 1) / 2

    这就要求,在 Shader 中对法线纹理进行纹理采样后,还需要对结果进行一次反映射的过程,已得到原先的法线方向。
    normal = pixed * 2 - 1

  • 模型空间的法线纹理和切线空间的法线纹理

    • 模型空间的法线纹理的优点:
    1. 实现简单,更加直观。我们甚至不需要模型原始的法线和切线等信息,计算更少。生成它也非常简单,而如果要生成切线空间下的法线纹理,由于模型的切线一般是和UV方向相同,因此想要得到效果较好的法线映射就要求纹理映射也是连续的。
    2. 在纹理坐标的缝合处和尖锐的边角部分,可见的突变(缝隙)较少,即可以提供平滑的边界。这是因为模型空间下的法线纹理存储是同一坐标系下的法线信息,因此在边界处通过插值得到的法线可以平滑变换。而切线空间下的法线纹理中的法线信息是依靠纹理坐标系的方向得到的结果,可能会在边缘处或尖锐的部分造成更多可见的缝隙。
    • 切线空间的法线纹理的优点:
    1. 自由度很高。模型空间下的法线纹理记录的是绝对法线信息,仅可用于创建它时的那个模型,而应用到其他模型上效果就完全错误 。而切线空间下的法线纹理记录的是相对法线信息,这意味着,即便把该纹理应用到一个完全不同的网格上,也可以得到一个合理的效果。
    2. 可以进行UV动画。比如,我们可以移动一个纹理的UV坐标来实现一个凹凸移动效果。
    3. 可以重用法线纹理。比如,一个砖块,我们可以使用一张法线纹理就可以用到所有的6个面。
    4. 可压缩。由于切线空间下的法线纹理中发现的Z方向总是正方向,因此我们可以仅存储XY方向,而推导得到Z方向。而模型空间下的法线纹理由于每个方向都是可能的,因此必须存储3个方向的值,不可压缩。
  • 计算光照模型,需要统一各个方向矢量所在的坐标空间。由于法线纹理中存储的法线是切线空间下的方向,所以有两种选择:

    1. 在切线空间进行光照计算,把视角方向、光照方向切换到切线空间下;
    2. 在世界空间进行光照计算,把采样得到的法线方向变换到世界空间下,在和世界空间下的光照方向和视角方向进行计算;
    3. 比较
      • 从效率上说,第一种方法优于第二种方法,因为可以在顶点着色器就完成对光照和视角方向的变换,而第二种方法由于先对法线纹理进行采样,所以变换过程必须在片元着色器中实现,这意味着需要在片元着色器中进行一次矩阵操作。
      • 从通用性来说,第二种方法优于第一种方法,还需要在世界空间进行其他计算。
  • 在切线空间下计算

    1. 思路
      在片元着色器中通过纹理采样得到切线空间下的法线,然后在与切线空间下的视角方向、光照方向等进行计算,得到最终的光照结果。
    2. 实现
      首先需要在顶点着色器中把视角方向和光照方向从模型空间变换到切线空间中,即需要知道从模型空间到切线空间的变换矩阵。这个矩阵的逆矩阵,从切线空间到模型空间的变换矩阵,在顶点着色器中按切线(x轴)、副切线(y轴)、法线(z轴)的顺序按列排列即可得到。如果一个变换中仅存在平移和旋转变换,那么这个变换的逆矩阵就等于它的转置矩阵,而从切线空间到模型空间的变换正式符合这样要求的变换。因此,,从模型空间到切线空间的变换矩阵就是从切线空间到模型空间的变换矩阵的转置矩阵,把切线(x轴)、副切线(y轴)、法线(z轴)的顺序按行排列即可得到。
Shader "Custom/s7_2"
{
    Properties
    {
        // 纹理贴图代替漫反射
        _Color("Color",color)=(1,1,1,1)
        _MainTex("Main Tex",2D)="white"{}
        // 高光反射
        _Specular("Specular",color)=(1,1,1,1)
        _Gloss("Gloss",Range(0,20))=20
        // 凹凸映射
        _BumpMap("Bump Map",2D)="bump"{} // bump是Unity内置的法线纹理,当没有提供任何法线纹理时,bump对应了模型自带的法线信息
        _BumpScale("Bump Scale",Range(0,1))=0.5 // 控制凹凸程度,为0时,意味着该法线纹理不会对光照产生任何影响
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200
        Pass
        {
            Tags {"LightMode"="ForwardBase"}
            CGPROGRAM
            #pragma vertex vert 
            #pragma fragment frag 
            #include "Lighting.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _Specular;
            float _Gloss;
            sampler2D _BumpMap;
            float4 _BumpMap_ST;
            float _BumpScale;

            /*
            * 切线空间下计算,需要把视角方向、光照方向变换到切线空间下
            * 已知模型空间下视角方向、光照方向,
            * 已知切线空间的三个轴在模型空间的表示x轴(切线)、z轴(法线),可以叉乘求得y轴(副切线)
            * 从模型空间变换到切线空间,躺着,即按行展开
            */
            struct a2v
            {
                float4 position:POSITION;
                float3 normal:NORMAL; // 法线
                float4 tangent:TANGENT; // 切线
                float3 texcoord:TEXCOORD0; // 第一组纹理坐标
            };
            struct v2f
            {
                float4 pos:SV_POSITION; // 顶点坐标变换
                float3 tangLightDir:TEXCOORD0; // 光照方向,从模型空间变换到切线空间
                float3 tangViewDir:TEXCOORD1; // 视角方向
                float4 uv:TEXCOORD2;
            };

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

                o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);
                // 法线,切线得到y
                float3 y = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w; 
                // 构建从模型空间到切线空间的矩阵
                fixed3x3 trans = fixed3x3(v.tangent.xyz, y, v.normal);
                // 空间变换
                o.tangLightDir = mul(trans, ObjSpaceLightDir(v.position));
                o.tangViewDir = mul(trans, ObjSpaceViewDir(v.position));
                return o;
            }

            fixed4 frag(v2f v):SV_Target
            {
                // 归一化
                fixed3 tangLightDir = normalize(v.tangLightDir);
                fixed3 tangViewDir = normalize(v.tangViewDir);

                // 纹理采样
                fixed4 packedNormal = tex2D(_BumpMap, v.uv.zw);
                fixed3 tangentNormal;
                // 如果纹理图不是 normal map
                // tangentNormal.xy = (packedNormal.xy * 2 - 1) *_BumpScale;
                // tangentNormal.z = sqrt(1-saturate(dot(tangentNormal.xy, tangentNormal.xy))); 
                // 或者标识为 Normal map
                tangentNormal = UnpackNormal(packedNormal);
                // 如果没有 _BumpScale, 下面两步可以不执行
                tangentNormal.xy *= _BumpScale;
                tangentNormal.z = sqrt(1-saturate(dot(tangentNormal.xy, tangentNormal.xy))); 

                // 反射率
                fixed3 albedo = tex2D(_MainTex, v.uv).rgb * _Color.rgb; 
                // 环境光
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                // 漫反射
                fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(tangentNormal, tangLightDir));
                // 高光反射
                fixed3 halfDir = normalize(tangViewDir + tangLightDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(tangentNormal, halfDir)), _Gloss);
                return fixed4(ambient + diffuse + specular, 1);
            }

            ENDCG
        }
    }
    FallBack "Diffuse"
}
  • 世界空间下计算光照模型,需要在片元着色器中把法线方向从切线空间变换到世界空间下。基本思路:在顶点着色器中计算从切线空间到世界空间的变化矩阵,并把他们传递给片元着色器。变换矩阵的计算可以由顶点的切线、副切线和法线在世界空间下的表示来得到。
Shader "Custom/s7_2_w"
{
    Properties
    {
        _Color("Color",color)=(1,1,1,1)
        _MainTex("Main Tex",2D)="white"{}
        _BumpMap("Bump Map",2D)="bump"{}
        _BumpScale("Bump Scale", Range(0, 1)) = 1
        _Specular("Specular",color)=(1,1,1,1)
        _Gloss("Gloss",Range(8,255))=50
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200
        Pass
        {
            Tags{"LightMode"="ForwardBase"}
            CGPROGRAM
            #pragma vertex vert 
            #pragma fragment frag 
            #include "Lighting.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            
            sampler2D _BumpMap;
            float4 _BumpMap_ST;
            float _BumpScale;

            fixed4 _Specular;
            float _Gloss;
            /*
            * 切线空间中的法线映射
            * 在世界空间中计算光照模型,视角方向、光照方向易得
            * 需要把采样得到的切线空间中的法线变换到世界空间,已知模型空间下切线空间的 x轴(切线)、z轴(法线),可得 y 轴(副切线),将它们变换到世界空间下
            * 即可得到在世界空间中切线空间的3个轴,从切线空间到法线空间,需要站着,即按列展开
            */
            struct a2v
            {
                float4 position:POSITION;
                float3 normal:NORMAL;
                float4 tangent:TANGENT;
                float3 texcoord:TEXCOORD0;
            };

            struct v2f
            {
                float4 pos:SV_POSITION;
                float4 uv:TEXCOORD0;
                float4 tangx:TEXCOORD2;
                float4 tangy:TEXCOORD3;
                float4 tangz:TEXCOORD4;
            };
            // 从切线空间变换到世界空间,已知在模型空间中的 x轴 切线,z轴 法线
            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.position);
                o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex); // 代替漫反射
                o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap); // 法线

                float3 worldPos = mul(unity_ObjectToWorld, v.position).xyz;
                fixed3 worldtang = UnityObjectToWorldDir(v.tangent.xyz);
                fixed3 worldnormal = UnityObjectToWorldNormal(v.normal);
                fixed3 worldbinormal = cross(worldnormal, worldtang) * v.tangent.w;// w分量控制方向

                // 充分利用插值寄存器的存储空间,把世界空间下的顶点位置存储在变量的 w 分量中
                o.tangx = float4(worldtang.x, worldbinormal.x, worldnormal.x, worldPos.x);
                o.tangy = float4(worldtang.y, worldbinormal.y, worldnormal.y, worldPos.y);
                o.tangz = float4(worldtang.z, worldbinormal.z, worldnormal.z, worldPos.z);
                return o;
            }

            fixed4 frag(v2f v):SV_Target
            {
                float3 worldPos = float3(v.tangx.w, v.tangy.w, v.tangz.w);
                // 光照方向
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos));
                // 视角方向
                fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));

                // 计算法线,从切线空间变换到世界空间
                fixed3 bump = UnpackNormal(tex2D(_BumpMap, v.uv.zw));
                bump.xy *= _BumpScale;
                bump.z = sqrt(1 - saturate(dot(bump.xy, bump.xy)));
                bump = normalize(half3(dot(v.tangx.xyz, bump), dot(v.tangy, bump), dot(v.tangz, bump)));

                // 替代漫反射的纹理采样
                fixed3 albedo = tex2D(_MainTex, v.uv).rgb * _Color.rgb;
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(bump, worldLightDir));

                // 高光反射
                fixed3 halfDir = normalize(worldLightDir + worldViewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(bump, halfDir)), _Gloss);
                return fixed4(ambient + diffuse + specular, 1);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

你可能感兴趣的:(UnityShader)