[UnityShader入门精要读书笔记]14.凹凸映射

纹理的另一种常见的应用就是凹凸映射。凹凸映射的目的是使用一张纹理来修改模型表面的法线,以便为模型提供更多的细节。这种方法不会真的改变模型的顶点位置,只是让模型看起来好像是“凹凸不平”的,但可以从模型的轮廓处看出“破绽”。有两种主要的方可以可以用来进行凹凸映射:一种方法是使用一张高度纹理来模拟表面位移,然后得到一个修改后的法线值,这种方法也被称为高度映射;另一种方法则是使用一张法线纹理来直接存储表现法线,这种方法又被称为法线映射。

1.高度纹理

使用一张高度图来实现凹凸映射。高度图中存储的是强度值,它用于表示模型表面局部的海拔高度。因此,颜色越浅表明该位置的表面越向外凸起,而颜色越深表明该位置越向里凹。这种方法的好处是非常直观,我们可以从高度图中明确的知道一个模型表面的凹凸情况,但缺点是计算更加复杂,在实时计算时不能直接得到表面法线,而是需要有像素的灰度值计算而得,因此需要消耗更多的性能。

2.法线纹理

法线纹理中存储的就是表面的发现方向,由于法线方向的分量范围在[-1,1],而像素的分量范围在[0,1],因此我们需要做一个映射,这就要求,我们在Shader中对法线纹理进行纹理采样后,还需要对结果进行一次反映射的过程,以得到原先的发现方向。反映射的实际就是使用上面映射函数的逆函数:

然而,由于方向是相对坐标空间来说的,那么法线纹理中存储的法线方向在哪个坐标空间中呢?对于模型顶点自带的法线,它们是定义在模型空间中的,因此一种直接的想法就是修改后的模型空间中的表面法线存储在一张纹理中,这种纹理被称为是模型空间的法线纹理。然而,在实际制作中,我们往往会采用另一种做表空间,即模型顶点的切线空间来存储法线。对于模型的每个顶点,他都有一个属于自己的切线空间,这个切线空间的远点就是该顶点本身,而z轴是顶点的法线方向(n),x轴是顶点的切线方向(t),而y轴可有发现和切线的叉积而得,也被称为是副切线或副法线。

模型空间下的法线纹理看起来是“五颜六色”的。这是因为所有法线所在的坐标空间是同一个坐标空间,即模型空间,而每个点存储的法线方向是各异的,有的是(0,1,0),经过映射后存储到纹理中就对应了RGB(0.5,1,0.5)浅绿色。有的是(0,-1,0),经过映射后存储到纹理中就对应了(0.5,0.5,0.5)紫色。而切线空间下的法线纹理看起来几乎都是浅蓝色的。这是因为,每个法线方向所在的坐标空间是不一样的,即是表面每个点各自的切线空间。这种法线纹理其实就是存储到了每个点在各自的切线空间中的法线扰动方向。也就是说,如果一个点的法线方向不变,那么在它的切线空间中,新的发现方向就是z轴方向,即值为(0,0,1),经过映射后存储在纹理中就对应了RGB(0.5,0.5,1)浅蓝色。而这个颜色就是法线纹理中大片的蓝色。

总体来说,模型空间下的法线纹理更符合人类的直观认识,而且法线纹理本身也很直观,容易调整,因为不同的法线方向就代表了不同的颜色。但美术人员往往喜欢切线空间下的法线纹理。实际上,法线本身存储在哪个坐标系中都是可以的,我们甚至可以选择存储到世界空间下。但问题是,我们并不是单纯的想要得到法线,后续的光照计算才是我们的目的。而选择哪个坐标系意味着我们需要把不同信息转换到相应的坐标系中。例如,如果选择了切线空间,我们需要把从法线纹理中得到的法线方向从切线空间转换到世界空间中。

使用模型空间来存储法线的优点:

1.实现简单,更加直观。我们甚至都不需要模型原始的法线和切线等信息,也就是说,计算更少。生成它也非常简单,而如果要生成切线空间下的法线纹理,由于模型的切线一般是和UV方向相同,因此想要得到效果比较好的法线映射就要求纹理映射也是连续的。

2.在纹理坐标和缝合处和尖锐的边角部分,可见的突变(缝隙)较少,即可以提供平滑的边界。这是因为模型空间下的法线纹理存储的是同一坐标系下的法线信息,因此在边界处通过插值得到的法线可以平滑变换。而切线空间下的法线纹理中的法线信息是依靠纹理坐标的方向得到的结果,可能会在边缘处或尖锐的部分造成更多可见的缝合迹象。

使用切线空间存储法线的优点:

1.自由度很高。模型空间下的法线纹理记录的是绝对法线信息,仅可用于创建它时的那个模型,而应用到其他模型上效果就完全错误了。而切线空间下的法线纹理记录的是相对法线信息,这意味着,即便把该纹理应用到一个完全不同的网格上,也可以得到一个合理的结果。

2.可进行UV动画。比如,我们可以移动一个纹理的UV坐标来实现一个凹凸移动的效果,但使用模型空间下的法线纹理会得到完全错误的结果。

3.可以重用纹理法线。

4.可压缩。由于切线空间下的法线纹理中法线的Z方向总是正方向,因此我们可以仅存储XY方向,而推导得到Z方向。而模型空间下的法线纹理由于每个方向都是可能的,因此必须存储3个方向的值,不可压缩。

光照模型计算的方法:

1.在切线空间下计算。基本思路是:在片元着色器中通过纹理采样得到切线空间下的法线,然后再与切线空间下的视角方向、光照方向等进行计算,得到最终的光照结果。为此,我们首先需要在顶点着色器中把视角方向和光照方向从模型空间变换到切线空间中,即我们需要知道从模型空间到切线空间的变换矩阵。这个变换矩阵的逆矩阵,即从切线空间到模型空间的变换矩阵是非常容易求得的,我们在顶点着色器中按切线(x轴)、副切线(y轴)、法线(z轴)的顺序按列排列即可得到。 

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Custom/NormalMap" {
    //添加法线纹理属性,用于控制凹凸程度的属性。对于法线纹理_BumpMap,我们使用“bump”作为它的默认值。“bump”是unity内置的法线纹理,当没有提供任何法线纹理时,“bump”就对应了模型自带的法线信息。_BumpScale则是用于控制凹凸程度的,当它为0时,意味着该法线纹理不会对光照产生任何影响。
    Properties{
        _Color("Color", 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"
                //为了得到该纹理的属性(平铺和偏移系数),我们为_MainTex和_BumpMap定义了_MainTex_ST和_BumpMap_ST变量。
                fixed4 _Color;
                sampler2D _MainTex;
                float4 _MainTex_ST;
                sampler2D _BumpMap;
                float4 _BumpMap_ST;
                float _BumpScale;
                fixed4 _Specular;
                float _Gloss;
                //我们已经知道,切线空间是由顶点法线和切线构建出的一个坐标空间,因此我们需要得到顶点的切线信息。我们使用TANGENT语义来描述float4类型的tangent变量,以告诉Unity把顶点切线方向填充到tangent变量中。需要注意的是,和发现防线normal不同,tangent的类型是float4,而非float3,这是因为我们需要tangent.w分量来决定切线空间中的第三个坐标轴--副切线的方向性。
                struct a2v {
                    float4 vertex : POSITION;
                    float3 normal : NORMAL;
                    float4 tangent : TANGENT;
                    float4 texcoord : TEXCOORD0;
                };
                //我们需要在顶点着色器中计算切线空间下的光照和视角方向,因此我们在v2f结构体中添加了两个变量来存储变换后的光照和视角方向:
                struct v2f {
                    float4 pos : SV_POSITION;
                    float4 uv : TEXCOORD0;
                    float3 lightDir : TEXCOORD1;
                    float3 viewDir : TEXCOORD2;
                };
                //由于我们使用了两张纹理,因此需要存储两个纹理坐标。为此,我们把v2f中的uv变量的类型定义为float4类型其中xy分量存储了_MainTex的纹理坐标,而zw分量存储了_BumpMap的纹理坐标(实际上,_MainTex和_BumpMap通常会使用同一组纹理坐标,出于减少插值寄存器的使用数目的目的,我们往往只计算一和存储一个纹理坐标即可)。然后,我们把模型空间下的切线方向、副切线方向和法线方向按行排列来得到从模型空间到切线空间的变换矩阵rotation。需要注意的是,在计算副切线是我们使用了v.tengent.w和叉积结果进行相乘,这是因为和切线与法线方向都垂直的方向有两个,而w决定了我们选择其中哪一个方向。Unity也提供了一个内置宏TANGENT_SPACE_ROTATION(在UntiyCG.cginc中被定义)来帮助我们直接计算得到rotation变换矩阵,它的实现和代码完全一样,然后我们使用Unity内置函数ObjSpaceLightDir和ObjectSpaceViewDir来得到模型空间下的光照和视角方向,再利用变换矩阵rotation把它们从模型空间变换到切线空间中。
                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);

                    //TANGENT_SPACE_ROTATION;

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

                    o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;
                    return o;
                }
                //片元着色器只需要采样得到切线空间下的发现方向,再在切线空间下进行光照计算即可:
                //我们首先利用tex2D对法线纹理_BumpMap进行采样。法线纹理中存储的是把发现进行过映射后得到的像素值,因此我们需要把它们反映射回来。如果我们在没有Unity里把该法线纹理的类型设置成NormalMap,就需要在代码中手动进行这个过程。我们首先把packedNormal的xy分量按之前的公式映射回法线方向,然后乘以_BumpScale(控制凹凸程度)来得到tangentNormal的xy分量。由于法线都是单位矢量,因此tangentNormal.z分量可以由tangentNormal.xy计算而得。由于我们使用的切线空间下的法线纹理,因此可以保证发现方向的z分量为正。在Unity中,为了方便Unity对法线纹理的存储进行优化,我们通常把法线纹理的纹理类型标识成Normal map。Unity会根据平台来选择不同的压缩方法。这时,如果我们再使用上边的方法来计算就会得到错误的结果,因为此时_BumpMap的rgb分量并不再是切线空间下的xyz值了。在这种情况下,我们可以使用Unity的内置函数UNpackNormal来得到正确的法线方向。
                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;

                    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"

}
2.在世界空间下计算光照模型。我们需要在片元着色器中把发现方向从切线空间变换到世界空间下。这种方法的基本思想是:在顶点着色器中计算从切线空间到世界空间的变换矩阵,并把它传递给片元着色器。变换矩阵的计算可以有顶点的切线、副切线和法线在世界空间下的表示来得到。最后,我们只需要在片元着色器中把法线纹理中的法线方向从切线空间变换到世界空间下即可。尽管这种方法需要更多的计算,但是在需要使用Cubemap进行环境映射的情况下,我们就需要使用这种方法。

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Custom/NormalMap WorldSpace" {
    //添加法线纹理属性,用于控制凹凸程度的属性。对于法线纹理_BumpMap,我们使用“bump”作为它的默认值。“bump”是unity内置的法线纹理,当没有提供任何法线纹理时,“bump”就对应了模型自带的法线信息。_BumpScale则是用于控制凹凸程度的,当它为0时,意味着该法线纹理不会对光照产生任何影响。
    Properties{
        _Color("Color", 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"
                //为了得到该纹理的属性(平铺和偏移系数),我们为_MainTex和_BumpMap定义了_MainTex_ST和_BumpMap_ST变量。
                fixed4 _Color;
                sampler2D _MainTex;
                float4 _MainTex_ST;
                sampler2D _BumpMap;
                float4 _BumpMap_ST;
                float _BumpScale;
                fixed4 _Specular;
                float _Gloss;
                //我们已经知道,切线空间是由顶点法线和切线构建出的一个坐标空间,因此我们需要得到顶点的切线信息。我们使用TANGENT语义来描述float4类型的tangent变量,以告诉Unity把顶点切线方向填充到tangent变量中。需要注意的是,和发现防线normal不同,tangent的类型是float4,而非float3,这是因为我们需要tangent.w分量来决定切线空间中的第三个坐标轴--副切线的方向性。
                struct a2v {
                    float4 vertex : POSITION;
                    float3 normal : NORMAL;
                    float4 tangent : TANGENT;
                    float4 texcoord : TEXCOORD0;
                };
                //修改顶点着色器的输出结构体v2f,使它包含从切线空间到世界空间的变换矩阵:一个插值寄存器最多只能存储float4大小的变量,对于矩阵这样的变量,我们可以把它们按行拆成多个变量再进行存储。下面代码中的TtoW0,TwoW1,TtoW2就依次存储了从切线空间到世界空间的变换矩阵的每一行。实际上,对方向矢量的变换只需要使用3X3大小的矩阵,也就是说,每一行只需要使用float3类型的变量即可。但为了充分利用插值寄存器的存储空间,我们把世界空间下的顶点位置存储在这些变量的w分量中。
                struct v2f {
                    float4 pos : SV_POSITION;
                    float4 uv : TEXCOORD0;
                    float4 TtoW0 : TEXCOORD1;
                    float4 TtoW1 : TEXCOORD2;
                    float4 TtoW2 : TEXCOORD3;
                };
                //由于我们使用了两张纹理,因此需要存储两个纹理坐标。为此,我们把v2f中的uv变量的类型定义为float4类型其中xy分量存储了_MainTex的纹理坐标,而zw分量存储了_BumpMap的纹理坐标(实际上,_MainTex和_BumpMap通常会使用同一组纹理坐标,出于减少插值寄存器的使用数目的目的,我们往往只计算一和存储一个纹理坐标即可)。然后,我们把模型空间下的切线方向、副切线方向和法线方向按行排列来得到从模型空间到切线空间的变换矩阵rotation。需要注意的是,在计算副切线是我们使用了v.tengent.w和叉积结果进行相乘,这是因为和切线与法线方向都垂直的方向有两个,而w决定了我们选择其中哪一个方向。Unity也提供了一个内置宏TANGENT_SPACE_ROTATION(在UntiyCG.cginc中被定义)来帮助我们直接计算得到rotation变换矩阵,它的实现和代码完全一样,然后我们使用Unity内置函数ObjSpaceLightDir和ObjectSpaceViewDir来得到模型空间下的光照和视角方向,再利用变换矩阵rotation把它们从模型空间变换到切线空间中。在下面代码中,我们计算了世界空间下的顶点切线、副切线和法线的矢量表示,并把他们按列摆放得到从切线空间到世界空间的变换矩阵。我们把该矩阵的每一行分别存储在TtoW0,TtoW1,TtoW2中,并把世界空间下的顶点位置xyz分量分别存储在了这些变量的w分量中,以便充分利用插值寄存器的存储空间。
                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 worldPos = mul(_Object2World, 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;
                }
                //片元着色器在世界空间下进行光照计算:我们首先从TtoW0,TtoW1,TtoW2的w分量中构建世界空间下的坐标。然后,使用内置的UnityWorldSpaceLightDir和UnityWorldSpaceViewDir函数得到世界空间下的光照和视角方向。接着,我们使用内置的UnpackNormal函数对法线纹理进行采样和解码(需要把法线纹理的格式标识成Normalmap),并使用_BumpScale对其进行缩放。最后,我们使用TtoW0,TtoW1,TtoW2存储的变换矩阵把发现变换到世界空间下。这是通过点乘操作来实现矩阵的每一行和法线想成得到的。

                fixed4 frag(v2f i) : SV_Target{
                    fixed3 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)))
                    ...

                }

                ENDCG
        }
    }
            Fallback "Specular"

}
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值