关于在切线空间下与世界空间下计算法线贴图

以下过程均为自己实践的过程,不能保证过程及结论的正确性。如果哪里有错误,还希望大家批评指正。

最近在学习shader,当学到法线贴图时,遇到了让我疑惑不解的地方。法线贴图有两种,一种是模型空间的贴图,也就是贴图中的法线信息是在模型空间下的,第二种是切线空间的贴图,也就是贴图中的法线信息是在切线空间下的,由于后一种要比前一种好用(绝大多数法线贴图都是这种,具体原因不在赘述),所以后面用的皆为切线空间中的法线贴图。在计算法线贴图时,有两种方式,一种是将光源方向、视线方向、法线(实际上法线不用转换,因为本来就是切线空间中的)转换到切线空间中,然后计算;第二种方式是将以上信息转换到世界空间中计算。然而为什么在切线空间的计算方式下,将光源向量与视线向量转换到切线空间,可以在Vertex Shader阶段完成;而在世界空间的计算方式下,将光源向量与视线向量转换到世界空间,必须要在Fragment Shader中完成,假如两者计算的位置颠倒,那么又会有什么样的变化,这个问题思考了很久,决定弄清楚再继续接下来的学习。

为了对比这四者之间的差异,我先把这四种Shader都写出来,然后对比一下效果。

首先是在切线空间下,在VertexShader中计算:

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

Shader "Unlit/BlinnSpecular"
{
     Properties{
        m_mainTex("MainTex",2D)=""{}
        m_normalMap("NormalMap",2D)=""{}
        m_normalMapScale("NormalMapScale",Range(-10,10))=1
        m_specular("Specular",Color)=(1,1,1,1)
        m_gloss("Gloss",Range(-10,10))=1
    }
    
    SubShader{
        Pass{
            Tags{"LightMode"="ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            
            sampler2D m_mainTex;                           //主贴图
            float4 m_mainTex_ST;                           //主贴图缩放偏移量
            sampler2D m_normalMap;                         //法线贴图
            float m_normalMapScale;                        //凹凸程度
            fixed3 m_specular;                             //反光的颜色
            float m_gloss;                                 //反光强度

            struct v2f{
                float4 pos:SV_POSITION;                    //MVP变换后的顶点坐标
                float2 uv:TEXCOORD1;                       //纹理采样
                float3 vertex:TEXCOORD2;                   //顶点坐标
                float3 tangentLight:TEXCOORD3;             //切线空间下的光源方向
                float3 tangentView:TEXCOORD4;              //切线空间下的视线方向
            };

            v2f vert(appdata_full v){
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord.xy*m_mainTex_ST.xy+m_mainTex_ST.zw;
                TANGENT_SPACE_ROTATION;                    //生成将模型空间转为切线空间的矩阵
                o.tangentLight = mul(rotation,(ObjSpaceLightDir(v.vertex))).xyz;
                o.tangentView = mul(rotation,(ObjSpaceViewDir(v.vertex))).xyz;
                return o;
            }

            fixed4 frag(v2f i):SV_TARGET0{
                fixed3 tangentLight = normalize(i.tangentLight);
                fixed3 tangentView = normalize(i.tangentView);

                fixed3 mainTex = tex2D(m_mainTex,i.uv);
                fixed3 tangentNormal = UnpackNormal(tex2D(m_normalMap,i.uv));
                
                fixed3 specular = _LightColor0.xyz*m_specular*pow(saturate(dot(tangentNormal,normalize(tangentLight+tangentView))),m_gloss);
                return fixed4(mainTex+specular,1);
            }

            ENDCG
        }
    }
}

左图是普通的Blinn高光反射,右图是计算法线贴图之后的结果,多了不少细节。

下面再来看一下在切线空间下,在FragmentShader中计算:

我需要把vert中通过宏计算的矩阵传到frag中

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

Shader "Unlit/BlinnSpecular"
{
     Properties{
        m_mainTex("MainTex",2D)=""{}
        m_normalMap("NormalMap",2D)=""{}
        m_normalMapScale("NormalMapScale",Range(-10,10))=1
        m_specular("Specular",Color)=(1,1,1,1)
        m_gloss("Gloss",Range(-10,10))=1
    }
    
    SubShader{
        Pass{
            Tags{"LightMode"="ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            
            sampler2D m_mainTex;                           //主贴图
            float4 m_mainTex_ST;                           //主贴图缩放偏移量
            sampler2D m_normalMap;                         //法线贴图
            float m_normalMapScale;                        //凹凸程度
            fixed3 m_specular;                             //反光的颜色
            float m_gloss;                                 //反光强度

            struct v2f{
                float4 pos:SV_POSITION;                    //MVP变换后的顶点坐标
                float2 uv:TEXCOORD1;                       //纹理采样
                float4 vertex:TEXCOORD2;                   //顶点坐标
                float3 tangentLight:TEXCOORD3;             //切线空间下的光源方向
                float3 tangentView:TEXCOORD4;              //切线空间下的视线方向

                float3 objectToTangent1:TEXCOORD5;         //模型空间到切线空间矩阵第一行
                float3 objectToTangent2:TEXCOORD6;         //模型空间到切线空间矩阵第二行
                float3 objectToTangent3:TEXCOORD7;         //模型空间到切线空间矩阵第三行
            };

            v2f vert(appdata_full v){
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord.xy*m_mainTex_ST.xy+m_mainTex_ST.zw;
                TANGENT_SPACE_ROTATION;                    //生成将模型空间转为切线空间的矩阵
                
                o.objectToTangent1 = rotation[0];
                o.objectToTangent2 = rotation[1];
                o.objectToTangent3 = rotation[2];          //把矩阵保存起来
                o.vertex = v.vertex;
                return o;
            }

            fixed4 frag(v2f i):SV_TARGET0{
                float3x3 objectToTangent = float3x3(i.objectToTangent1,i.objectToTangent2,i.objectToTangent3);
                fixed3 tangentLight = normalize(mul(objectToTangent,ObjSpaceLightDir(i.vertex)));//生成切线空间下的光源向量
                fixed3 tangentView = normalize(mul(objectToTangent,ObjSpaceViewDir(i.vertex)));//生成切线空间下的视角向量

                fixed3 mainTex = tex2D(m_mainTex,i.uv);
                fixed3 tangentNormal = UnpackNormal(tex2D(m_normalMap,i.uv));
                
                fixed3 specular = _LightColor0.xyz*m_specular*pow(saturate(dot(tangentNormal,normalize(tangentLight+tangentView))),m_gloss);
                return fixed4(mainTex+specular,1);
            }

            ENDCG
        }
    }
}

左边依旧拿个Blinn做对比,右图是修改过后的Shader,我发现,这好像并没有什么区别。

再详细对比一下,左图是Vert,右图是Frag,这好像还是看不出有什么区别。

那好吧,接下来再看看在切线空间下,在FragmentShader中计算:

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

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

Shader "Unlit/NormalMapWorldSpaceShader"
{   
    Properties{
        m_mainTex("MainTex",2D)=""{}
        m_normalMap("NormalMap",2D)=""{}
        m_normalMapScale("NormalMapScale",Range(-10,10))=1
        m_specular("Specular",Color)=(1,1,1,1)
        m_gloss("Gloss",range(-10,10))=1
    }
    
    SubShader{
        Pass{
            Tags{"LightMode"="ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            sampler2D m_mainTex;
            float4 m_mainTex_ST;
            sampler2D m_normalMap;
            float m_normalMapScale;
            fixed3 m_specular;
            float m_gloss;
            
            struct v2f{
                float4 pos:SV_POSITION;
                float2 uv:TEXCOORD1;
                float4 worldVertex:TEXCOORD2;
                float3 tangentToWorld1:TEXCOORD3;          //切线坐标转为世界坐标
                float3 tangentToWorld2:TEXCOORD4;
                float3 tangentToWorld3:TEXCOORD5;
            };

            v2f vert(appdata_full v){
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldVertex = mul(unity_ObjectToWorld,v.vertex);
                o.uv = v.texcoord.xy*m_mainTex_ST.xy+m_mainTex_ST.zw;

                float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                float3 worldNormal = UnityObjectToWorldNormal(v.normal);
                float3 worldBinormal = cross(worldNormal,worldTangent)*v.tangent.w;
                o.tangentToWorld1 = float3(worldTangent.x,worldBinormal.x,worldNormal.x);
                o.tangentToWorld2 = float3(worldTangent.y,worldBinormal.y,worldNormal.y);
                o.tangentToWorld3 = float3(worldTangent.z,worldBinormal.z,worldNormal.z);

                return o;                
            }

            fixed4 frag(v2f i):SV_TARGET0{
                fixed3 mainTex = tex2D(m_mainTex,i.uv);
                
                float3 tangentNormal = UnpackNormal(tex2D(m_normalMap,i.uv));
                float3 worldNormal = float3(dot(i.tangentToWorld1,tangentNormal),dot(i.tangentToWorld2,tangentNormal),dot(i.tangentToWorld3,tangentNormal));
                
                float3 worldLight = normalize(UnityWorldSpaceLightDir(i.worldVertex));
                float3 worldView = normalize(UnityWorldSpaceViewDir(i.worldVertex));
                fixed3 specular = _LightColor0.xyz*m_specular.xyz*pow(saturate(dot(worldNormal,normalize(worldLight+worldView))),m_gloss);
                return fixed4(mainTex+specular,1);
            }

            ENDCG
        }
    }
}

   

左图是切线空间下,在FragmentShader中计算法线贴图,右图是在世界空间下,在FragmentShader中计算法线贴图,这个效果一样是在预料之中的。

最后一个,看看在切线空间下,在VertexShader中计算:

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

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

Shader "Unlit/NormalMapWorldSpaceShader"
{   
    Properties{
        m_mainTex("MainTex",2D)=""{}
        m_normalMap("NormalMap",2D)=""{}
        m_normalMapScale("NormalMapScale",Range(-10,10))=1
        m_specular("Specular",Color)=(1,1,1,1)
        m_gloss("Gloss",range(-10,10))=1
    }
    
    SubShader{
        Pass{
            Tags{"LightMode"="ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            sampler2D m_mainTex;
            float4 m_mainTex_ST;
            sampler2D m_normalMap;
            float m_normalMapScale;
            fixed3 m_specular;
            float m_gloss;
            
            struct v2f{
                float4 pos:SV_POSITION;
                float2 uv:TEXCOORD1;
                float4 worldVertex:TEXCOORD2;
                float3 tangentToWorld1:TEXCOORD3;          //切线坐标转为世界坐标
                float3 tangentToWorld2:TEXCOORD4;
                float3 tangentToWorld3:TEXCOORD5;

                float3 worldLight:TEXCOORD6;
                float3 worldView:TEXCOORD7;
            };

            v2f vert(appdata_full v){
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldVertex = mul(unity_ObjectToWorld,v.vertex);
                o.uv = v.texcoord.xy*m_mainTex_ST.xy+m_mainTex_ST.zw;

                float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                float3 worldNormal = UnityObjectToWorldNormal(v.normal);
                float3 worldBinormal = cross(worldNormal,worldTangent)*v.tangent.w;
                o.tangentToWorld1 = float3(worldTangent.x,worldBinormal.x,worldNormal.x);
                o.tangentToWorld2 = float3(worldTangent.y,worldBinormal.y,worldNormal.y);
                o.tangentToWorld3 = float3(worldTangent.z,worldBinormal.z,worldNormal.z);

                o.worldLight = normalize(UnityWorldSpaceLightDir(o.worldVertex));
                o.worldView = normalize(UnityWorldSpaceViewDir(o.worldVertex));
                return o;                
            }

            fixed4 frag(v2f i):SV_TARGET0{
                fixed3 mainTex = tex2D(m_mainTex,i.uv);
                
                float3 tangentNormal = UnpackNormal(tex2D(m_normalMap,i.uv));
                float3 worldNormal = float3(dot(i.tangentToWorld1,tangentNormal),dot(i.tangentToWorld2,tangentNormal),dot(i.tangentToWorld3,tangentNormal));
                
                
                fixed3 specular = _LightColor0.xyz*m_specular.xyz*pow(saturate(dot(worldNormal,normalize(i.worldLight+i.worldView))),m_gloss);
                return fixed4(mainTex+specular,1);
            }

            ENDCG
        }
    }
}

       

左图依然是在切线空间下,在FragmentShader中计算法线贴图,右图是在世界空间下,在VertexShader中计算法线贴图,这两者之间的区别就比较显而易见了,虽然理论上右边的计算方式是错误的,但是怎么感觉效果还要好一点

目前可以得到一个结论,从效果上来讲,第一种方式=第二种方式=第三种方式≠第四种方式。(再次强调一下,不能保证正确性,如果有错误的话希望各位能批评指正)

这篇文章感觉篇幅已经够长了,所以关于四种方式的效果先写到这里,关于这种现象的探究放到下一篇写吧(如果我能推导出原因的话,虽然估计有可能要夭折了)。

猜你喜欢

转载自blog.csdn.net/yanyangxu01/article/details/81156725