Unity Shader 入门到改行5——法线贴图

2020-12-27  本文已影响0人  太刀
the best of blur

1. 法线贴图理论

1.1 什么是法线贴图

一般的贴图中存储的是表面颜色值(RGBA),而法线贴图存放的则是法线信息(xyzw),假设某顶点处的 uv 坐标为 (u,v), 那么在法线贴图 (u,v)处纹素的值表示该顶点的“法线”方向。通常法线贴图中存储的并不是这个顶点的真实法线信息。

1.2 法线贴图的作用

想象一下,如果我们想要表现一个凹凸不平的模型表面(想象一个橙子的表面),有哪些办法呢?

上面第三种方法称为基于“高度纹理”的凹凸表现。而第四种方法就是基于“法线纹理”的凹凸表现。

注意:高度贴图和法线贴图用来表现“凹凸”,在模型轮廓的边缘会穿帮。比如你可以用这两种方法使一个平滑的橙子模型表面看起来凹凸不平,但是在橙子的边缘总是平滑的。

1.3 法线贴图纹素取值范围

通常贴图纹素用来表示 RGBA,那么每个分量的取值范围是[0,1],而法线的每个分量取值范围为[-1,1],所以用贴图纹素表示一个法线时,需要针对每一个分量做映射

pixel = (normal + 1) / 2;

在针对法线贴图采样后,进行逆运算

normal = 2 * pixel - 1;

得到实际的法线分量值。

1.4 法线贴图基于什么坐标系

法线贴图储存了表面法线,而法线是一个方向,那么这个方向是基于什么坐标系?通常跟随顶点数据一起传输到 顶点着色器中的法线,由 NORMAL 语义指定,是基于模型坐标系的。所以我们可以将法线在模型坐标中的值存储到法线贴图中,得到模型空间的法线贴图,而在实际制作中,应用更多的是顶点切线空间的法线贴图
对于每个顶点,以顶点自身作为原点,顶点切线方向为x轴,法线方向为z轴,切线和法线方向叉乘得到 y 轴(副法线方向),得到这个顶点的 切线坐标空间,基于这个空间的法线记录下来得到 顶点切线空间的法线贴图

左:模型空间的法线贴图 右:切线空间的法线贴图

1.5 为什么切线空间的法线贴图看起来都是偏蓝色的?

切线空间的法线贴图保存的是基于顶点的切线空间中的法线数值,而在顶点的切线空间中,真实法线的反向永远是(0,0,1),经过上述的计算公式得到法线贴图中存储的值为 (0.5,0.5, 1),偏蓝色。而修改后的法线通常也是 z 值最大,因为你不太可能有90度以上的法线修改,整体还是偏蓝。

通常使用顶点切线空间的法线贴图,而顶点空间中的修改后的法线值,z分量最大,换算成颜色就是 b 分量最大,所以法线贴图通常看起来偏蓝色。

2. 如何在 Shader 中应用法线贴图

我们使用在切线空间下的法线贴图,先上完整 shader 代码,然后逐步分析,代码如下:

Shader "Shader_Examples/04_NormalTexture_TangentSpace"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _SpecularColor ("SpecularColor", Color) = (1,1,1,1)
        _Gloss ("Gloss", Range(8, 256)) = 20
        _BumpTex ("BumpTex", 2D) = "bump" {}
        _BumpScale ("BumpScale", Float) = 1.0
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" "LightMode" = "ForwardBase" }      

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag                       
            
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _SpecularColor;
            float _BumpScale;
            sampler2D _BumpTex;
            float4 _BumpTex_ST;
            float _Gloss;

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float4 tangent : TANGENT;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;          
                float4 vertex : SV_POSITION;
                float3 lightDir : TEXCOORD1;
                float3 viewDir : TEXCOORD2; 
            };          
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                // 模型空间副法线
                fixed3 binormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;

                float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);

                float3 lightDir = ObjSpaceLightDir(v.vertex);
                float3 viewDir = ObjSpaceViewDir(v.vertex);

                o.lightDir = mul(rotation, lightDir);
                o.viewDir = mul(rotation, viewDir);
                o.uv = v.uv;                                
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {               
                float3 lightDir = normalize(i.lightDir);
                float3 viewDir = normalize(i.viewDir);
                float3 halfDir = normalize(lightDir + viewDir);             

                float4 packedNormal = tex2D(_BumpTex, i.uv);

                float3 tangentNormal = UnpackNormal(packedNormal);
                tangentNormal.xy *= _BumpScale;
                tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));

                fixed3 albedo = tex2D(_MainTex, i.uv).rgb;
                fixed3 diffuse = _LightColor0.rgb * albedo.rgb * saturate(dot(tangentNormal, lightDir));

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo;

                fixed3 specular = _SpecularColor * _LightColor0 * pow(saturate(dot(halfDir, tangentNormal)), _Gloss);
                return fixed4(diffuse + ambient + specular, 1.0);
            }
            ENDCG
        }
    }
}

渲染效果如图:


法线贴图效果

2.1 shader 属性与对应的变量

    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _SpecularColor ("SpecularColor", Color) = (1,1,1,1)
        _Gloss ("Gloss", Range(8, 256)) = 20
        _BumpTex ("BumpTex", 2D) = "bump" {}
        _BumpScale ("BumpScale", Float) = 1.0
    }

漫反射纹理 _MainTex, 高光颜色 _SpecularColor 和高光系数 _Gloss 没什么好说的,新增的纹理 _BumpTex 为法线贴图,默认值为 unity 内置法线贴图 "bump",_BumpScale 用来控制表面的“凹凸”程度,后面会分析它是怎么起作用的。对应的变量声明:

sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _SpecularColor;
float _BumpScale;
sampler2D _BumpTex;
float4 _BumpTex_ST;
float _Gloss;

2.2 着色器输入结构

struct appdata
{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
    float4 tangent : TANGENT;
    float3 normal : NORMAL;
};

struct v2f
{
    float2 uv : TEXCOORD0;          
    float4 vertex : SV_POSITION;
    float3 lightDir : TEXCOORD1;
    float3 viewDir : TEXCOORD2; 
};

2.3 顶点着色器

v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    // 模型空间副法线
    fixed3 binormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
    // 模型空间到顶点切线空间的变换矩阵
    float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);

    // 光线方向和视线防线变换到顶点切线空间
    float3 lightDir = ObjSpaceLightDir(v.vertex);
    float3 viewDir = ObjSpaceViewDir(v.vertex);
    o.lightDir = mul(rotation, lightDir);
    o.viewDir = mul(rotation, viewDir);
                
    o.uv = v.uv;                                
    return o;
}

2.4 片元着色器

fixed4 frag (v2f i) : SV_Target
{               
    float3 lightDir = normalize(i.lightDir);
    float3 viewDir = normalize(i.viewDir);
    float3 halfDir = normalize(lightDir + viewDir);             

    float4 packedNormal = tex2D(_BumpTex, i.uv);

    float3 tangentNormal = UnpackNormal(packedNormal);
    tangentNormal.xy *= _BumpScale;
    tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));

    fixed3 albedo = tex2D(_MainTex, i.uv).rgb;
    fixed3 diffuse = _LightColor0.rgb * albedo.rgb * saturate(dot(tangentNormal, lightDir));

    fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo;

    fixed3 specular = _SpecularColor * _LightColor0 * pow(saturate(dot(halfDir, tangentNormal)), _Gloss);
    return fixed4(diffuse + ambient + specular, 1.0);
}

3. Unity中的法线贴图类型设置

在上面的片元着色器中,我们从法线贴图中采样出纹素后,使用了 Unity 内置函数 UnpackNormal 来计算最终的法线值。只有正确的设置图片的类型为 "Normal Map" 时,使用这个内置函数才能得到正确结果,在 Unity 中的设置面板如下:

法线贴图设置

参考文章:
1. 关于顶点法线、切线和副法线
2. 模型空间到顶点切线空间变换矩阵的推导

上一篇下一篇

猜你喜欢

热点阅读