返回
Featured image of post Shader入门精要-基础纹理

Shader入门精要-基础纹理

笔记摘自书籍《Shader入门精要》

单张纹理

关于纹理导入设置用途知识跳转至https://docs.unity.cn/cn/current/Manual/TextureTypes.html,这部分对纹理的设置知识很重要。

纹理的一种作用为代替物体的漫反射颜色。下面我们在之前的Blinn-Phong高光反射Shader的基础上实现一个基础的纹理Shader

需要注意的是在声明属性时,与其他属性类型不同的是,我们需要为纹理类型声明两个属性,一个是sampler2D _MainTex;,而另一个是float4 _MainTex_ST;,其中**_MainTex_ST**的名字不是任意起的。在Unity中,我们需要使用**纹理名_ST**的方式来声明某个纹理的属性。其中**ST**是缩放(scale)和平移(translation)的缩写。_MainTex_ST.xy存储的是缩放值,而_MainTex_ST.zw存储的是偏移值。

基础的纹理Shader代码实现见下文。

凹凸映射

纹理的另一个常见应用就是凹凸映射。凹凸映射的目的是使用一张纹理来修改模型表面的法线,以便为纹理提供更多细节。

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

  1. 高度映射

    使用一张高度纹理来模拟表面位移,然后得到一个修改后的法线值。

  2. 法线映射

    使用一张法线纹理来直接存储表面法线,这种方法被称为法线映射。

我们通常将凹凸映射和法线映射当成相同的技术,但还是要知道他们之间的不同。

高度纹理

上图是一张高度图,高度图中存储的是强度值,它踊跃表示模型表面局部的海拔高度。颜色越浅表明该位置的表面越向外凸起,越深表明该位置越向里凹陷。

高度图通常会和法线映射一起使用,用于给出表面凹凸的额外信息。也就是说我们通常使用法线映射来修改光照。

法线纹理

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

pixel = ( normal + 1 ) / 2

当我们对结果进行反映射就可以得到原先的法线方向:

normal = pixel * 2 - 1

法线的坐标空间

由于方向是相对于坐标空间来说的,那么法线纹理中存储的法线方向在哪个坐标空间中呢?

  1. 模型空间的法线纹理

    对于模型顶点自带的法线,他们是定义在模型空间中的,因此一种直接的想法就是将修改后的模型空间中的表面法线存储在一张纹理中,这种纹理被称之为是模型空间的法线纹理

  2. 切线空间的法线纹理

    然而在实际操作中,我们往往会采用另一种坐标空间,即模型顶点的切线空间来存储法线。对于模型的每个顶点,它都有一个属于自己的切线空间,这个切线空间的原点就是该顶点本身,而z轴是顶点的法线的方向(n),x轴是顶点的切线方向(t),而y轴可由法线和切线叉积而得,也被称之为副切线或副法线(b)。这种纹理被称为切线空间的法线纹理

在模型空间下法线纹理看起来是五颜六色的。这是因为所有法线所在的坐标空间是同一个坐标空间,即模型空间,而每个点存储的法线方向都是各异的,经过映射后存储到纹理中对应的颜色各种各样。而切线空间下的法线纹理看起来几乎全部都是浅蓝色的。这是因为,每个法线方向所在的坐标空间是不一样的,即表面每个点各自的切线空间。如果一个点的法线方向不变,那么它在切线空间中,新的法线方向就是z轴方向。经过映射后存储在纹理中的值对应了浅蓝色。

不同坐标空间优点对比

使用模型空间切线的优点:

  1. 实现简单,更加直观。不需要模型的原始法线和切线信息等,计算少,生成也简单,而使用切线空间,由于模型的切线一般是和uv 方向相同,因此想要直观的的到效果比较好的法线映射就要求纹理映射也是连续的。
  2. 在纹理坐标的粉盒处和尖锐的边角部分,课件的突变(缝隙)较少,因为法线的坐标系相同,因此可以在边界处通过插值得到法线平滑变换。而切线空间的法线纹理信息依靠纹理的坐标的方向得到(坐标是坐标系的原点),可能在边缘处或者尖锐的部分造成更多的可见的缝合迹象。

而使用切线空间的优点更多:

  1. 自由度很高(复用)。模型空间的法线纹理记录是绝对法线信息,仅可用于创建它时的那个模型,而应用到其他模型上效果就完全错了。而切线空间的法线纹理记录的是相对法线信息,这意味着可以应用到完全不同的网格上,得到一个合理的结果。
  2. 可进行uv动画, 比如, 我们可以移动一个纹理的uv坐标来实现一个凹凸移动的效果。而模型空间法线纹理则作物。
  3. 可重用的法线纹理,比如立方体的6个面同时使用,
  4. 可压缩。由于切线空间下的法线纹理中的z方向总是正方向,仅仅是值大小差异,因此我们可以仅储存xy方向, 而推导出z方向((x, 0, 0),(0, y, 0) 叉乘),而模型空间是绝对值,则必须储存三个值。

实践

我们需要计算光照模型中同意各个方向矢量所在的坐标空间。由于法线纹理中存储法线是切线空间下的方向,因此我们通常有两种选择:

  1. 在切线空间下进行光照计算,此时我们需要把光照方向、视角方向变换到世界空间下。
  2. 在世界空间下进行光照计算,此时我们需要把采样得到的法线方向变换到世界空间下,再和世界空间下的光照方向和视角方向进行计算。

从效率上来说,第一种优于第二种,因为变换过程在顶点着色器中完成,而非片元着色器中。

从通用性来说,第二种优于第一种,因为我们有时需要在世界空间下进行一次矩阵操作。

切线空间计算思路

基本思路:在片元着色器中通过纹理采样得到切线空间下的法线,然后在与切线空间下的视角方向、光照方向等进行计算,得到最终的光照结果。在此之前我们需要在顶点着色器中把视角方向和光照方向从模型空间变换到切线空间中,即我们需要知道从模型空间到切线空间的变换矩阵。Unity为我们提供了这一内值宏TANGENT_SPACE_ROTATION用于获取该矩阵rotation。代码实现见下文。

世界空间计算思路

基本思路:在顶点着色器中计算从切线空间到世界空间的变换举证,并把它传递给片元着色器。变换矩阵的计算可以由顶点的切线、副切线和法线在世界空间下的表示来得到。最后,我们只需要在片元着色器中把法线纹理中的法线方向从切线空间变换到时间空间下即可。代码实现见下文。

渐变纹理

开始我们在渲染中使用纹理是为了定义一个物体的颜色,纹理其实可以用与存储任何表面属性。一种常见的用法就是使用渐变纹理来控制漫反射光照的结果。

可以看出使用这种方式可以自由地控制物体的漫反射光照。不同的渐变纹理有不同的特性。上图效果的代码实现见下文。

需要注意的是,我们需要把渐变纹理的Wrap Mode设为Clamp模式,以防止纹理进行采样时由于浮点数值精度而造成的问题。对比效果见下图

遮罩纹理

使用遮罩纹理可以让美术人员更加精准(像素级)地控制模型表面的各种性质。

使用遮罩纹理的流程一般是:通过采样得到遮罩纹理的纹素值,然后使用其中某个(或某几个)通道的值来与某种表面属性进行相乘,这样,当该通道的值为0时,可以保护表面不受该属性的影响。效果的代码实现见下文。

代码实现

基础纹理采样

Shader "Unity Shaders Book/Chapter 7/Single Texture"
{
    Properties
    {
        _Color ("Color Tint", Color) = (1, 1, 1, 1)
        // 纹理属性
        _MainTex ("Main Tex", 2D) = "white" { }
        _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;
            // 纹理缩放平移值(注意该名称不是随意取的)
            float4 _MainTex_ST;
            fixed4 _Specular;
            float _Gloss;
            
            struct a2v
            {
                float4 vertex: POSITION;
                float3 normal: NORMAL;
                float4 texcoord: TEXCOORD0;// 纹理
            };
            
            struct v2f
            {
                float4 pos: SV_POSITION;
                float3 worldNormal: TEXCOORD0;
                float3 worldPos: TEXCOORD1;
                float2 uv: TEXCOORD2;// 纹理
            };
            
            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                
                // 使用纹理的属性_ST对顶点纹理坐标进行变换
                o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                // 或者直接调用内置函数
                // o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                
                return o;
            }
            
            fixed4 frag(v2f i): SV_Target
            {
                // 世界空间的法线
                fixed3 worldNormal = normalize(i.worldNormal);
                // 世界空间光的方向
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                
                // 材质的漫反射系数,使用纹理来采样漫反射的颜色
                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
                
                // 环境光
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                
                // 漫反射光=入射光线强度*材质的漫反射系数*取值为正数(表面法线方向 · 光源方向)
                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
                
                // 视角方向
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                // 半角方向
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                // BlinnPhong高光反射
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
                
                // 最终结果=环境光+漫反射+高光反射,1.0代表透明度
                return fixed4(ambient + diffuse + specular, 1.0);
            }
            
            ENDCG
            
        }
    }
    FallBack "Specular"
}

切线空间下凹凸映射

Shader "Unity Shaders Book/Chapter 7/Normal Map In Tangent Space"
{
    Properties
    {
        _Color ("Color Tint", Color) = (1, 1, 1, 1)
        // 纹理属性
        _MainTex ("Main Tex", 2D) = "white" { }
        // 法线纹理属性 "bump"是Unity内置法线纹理
        _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;
            float4 _MainTex_ST;
            // 法线纹理变量
            sampler2D _BumpMap;
            float4 _BumpMap_ST;
            // 凸起强度变量
            float _BumpScale;
            fixed4 _Specular;
            float _Gloss;
            
            struct a2v
            {
                float4 vertex: POSITION;
                float3 normal: NORMAL;
                float4 tangent: TANGENT;
                float4 texcoord: TEXCOORD0;
            };
            
            struct v2f
            {
                float4 pos: SV_POSITION;
                float4 uv: TEXCOORD0;
                float3 lightDir: TEXCOORD1;
                float3 viewDir: TEXCOORD2;
            };

            
            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                
                // 使用基础纹理的属性_ST对顶点纹理坐标进行变换
                o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                // 使用法线纹理的属性_ST对法线纹理坐标进行变换
                o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
                // 或者直接调用内置函数
                // o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);

                // 构造一个矩阵,将点/向量从切线空间转换到世界空间
                // 法线的模
                fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
                // 切线的模
                fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                // 副切线 = 叉乘(法线的模,切线的模)* 切线的W方向
                fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
                // 求模型空间到切线空间的变换矩阵 wToT = the inverse of tToW = the transpose of tToW as long as tToW is an orthogonal matrix.
                float3x3 worldToTangent = float3x3(worldTangent, worldBinormal, worldNormal);
                
                // 将光和视角从世界空间转换到切线空间
                o.lightDir = mul(worldToTangent, WorldSpaceLightDir(v.vertex));
                o.viewDir = mul(worldToTangent, WorldSpaceViewDir(v.vertex));

                /*
                // 上面构造矩阵的过程可以由内置宏代替
                // 内置宏,用于得到模型空间到切线空间的变换矩阵rotation
                TANGENT_SPACE_ROTATION;
                // 求切线空间光源方向=矩阵乘积(变换矩阵,模型空间下光照方向).xyz;
                o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
                // 求切线空间视角方向=矩阵乘积(变换矩阵,模型空间下视角方向).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);

                // 切线空间中的法线
                fixed3 tangentNormal;
                // 如果纹理没有标记为“normalMap”,在Unity贴图的设置面板中设置
                // tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
                // tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
                
                // 有将纹理标记为“normalMap”,并使用内置的功能的情况
                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 "Specular"
}

世界空间下凹凸映射

			struct a2v
            {
                float4 vertex: POSITION;
                float3 normal: NORMAL;
                float4 tangent: TANGENT;
                float4 texcoord: TEXCOORD0;
            };
            
            struct v2f
            {
                float4 pos: SV_POSITION;
                float4 uv: TEXCOORD0;
                float4 TtoW0: TEXCOORD1;
                float4 TtoW1: TEXCOORD2;
                float4 TtoW2: TEXCOORD3;
            };
            
            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                // 使用纹理的属性_ST对顶点纹理和法线纹理坐标进行变换
                o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
                // 或者直接调用内置函数
                // o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
                
                // 计算世界坐标下的顶点,法线,切线,副法线
                float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
                fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
                
                // 计算从切线空间到世界空间的方向变换矩阵
                // 按列摆放得到从切线转世界空间的变换矩阵
                o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
                o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
                o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
                
                return o;
            }
            
            fixed4 frag(v2f i): SV_Target
            {
                // 得到世界空间中的位置
                float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
                // 计算世界空间中的光照和视角方向
                fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
                
                // 采样得到切线空间的法线纹理
                fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
                bump.xy *= _BumpScale;
                bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));
                // 将切线空间转换为世界空间
                bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
                
                // 材质的漫反射系数,使用纹理来采样漫反射的颜色
                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
                
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                
                // 漫反射光=入射光线强度*材质的漫反射系数*取值为正数(表面法线方向 · 光源方向)
                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));

                fixed3 halfDir = normalize(lightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);
                
                return fixed4(ambient + diffuse + specular, 1.0);
            }

渐变纹理

Shader "Unity Shaders Book/Chapter 7/Ramp Texture"
{
    Properties
    {
        _Color ("Color Tint", Color) = (1, 1, 1, 1)
        // 渐变纹理
        _RampTex ("Ramp Tex", 2D) = "white" { }
        _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 _RampTex;
            float4 _RampTex_ST;
            fixed4 _Specular;
            float _Gloss;
            
            struct a2v
            {
                float4 vertex: POSITION;
                float3 normal: NORMAL;
                float4 texcoord: TEXCOORD0;
            };
            
            struct v2f
            {
                float4 pos: SV_POSITION;
                float3 worldNormal: TEXCOORD0;
                float3 worldPos: TEXCOORD1;
                float2 uv: TEXCOORD2;
            };
            
            v2f vert(a2v v)
            {
                v2f o;
                // 将顶点从对象空间转换到投影空间
                o.pos = UnityObjectToClipPos(v.vertex);
                
                // 将法线从物体空间转换到世界空间
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                
                // 世界空间下顶点坐标
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                
                // 使用宏计算平铺偏移后的纹理坐标
                o.uv = TRANSFORM_TEX(v.texcoord, _RampTex);
                
                return o;
            }
            
            fixed4 frag(v2f i): SV_Target
            {
                // 世界空间下的法线与光照方向
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                
                // 获取环境光
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                
                // 半兰伯特光照模型
                fixed halfLambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
                // 反射颜色 = 使用渐变纹理来采样半兰伯特光照生成的纹理
                fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert)).rgb * _Color.rgb;
                // 漫反射 = 入射光线 * 反射颜色
                fixed3 diffuse = _LightColor0.rgb * diffuseColor;
                
                // 世界空间下视角方向
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                // 世界空间下半角方向
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                // BlinnPhong高光反射
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
                
                return fixed4(ambient + diffuse + specular, 1.0);
            }
            
            ENDCG
            
        }
    }
    FallBack "Specular"
}

遮罩纹理

实现效果为对高光计算时的遮罩

Shader "Unity Shaders Book/Chapter 7/Mask Texture"
{
    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
        // 遮罩纹理属性
        _SpecularMask ("Specular Mask", 2D) = "white" { }
        // 遮罩强度控制
        _SpecularScale ("Specular 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;
            float4 _MainTex_ST;
            sampler2D _BumpMap;
            float _BumpScale;
            sampler2D _SpecularMask;
            float _SpecularScale;
            fixed4 _Specular;
            float _Gloss;
            
            struct a2v
            {
                float4 vertex: POSITION;
                float3 normal: NORMAL;
                float4 tangent: TANGENT;
                float4 texcoord: TEXCOORD0;
            };
            
            struct v2f
            {
                float4 pos: SV_POSITION;
                float2 uv: TEXCOORD0;
                float3 lightDir: TEXCOORD1;
                float3 viewDir: TEXCOORD2;
            };
            
            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                
                // 使用纹理的属性_ST对顶点纹理和法线纹理坐标进行变换
                o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                
                // 内置宏,用于得到模型空间到切线空间的变换矩阵
                TANGENT_SPACE_ROTATION;
                // 求切线空间光源方向=矩阵乘积(变换矩阵,模型空间下光照方向).xyz;
                o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
                // 求切线空间视角方向=矩阵乘积(变换矩阵,模型空间下视角方向).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);

                // 有将法线纹理标记为“normalMap”,并使用内置的功能的情况
                fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uv));
                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);
                // 获取遮罩值
                fixed specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale;
                // 利用高光遮罩计算高光反射
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss) * specularMask;
                
                return fixed4(ambient + diffuse + specular, 1.0);
            }
            
            ENDCG
            
        }
    }
    FallBack "Specular"
}