以下过程均为自己实践的过程,不能保证过程及结论的正确性。如果哪里有错误,还希望大家批评指正。
最近在学习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中计算法线贴图,这两者之间的区别就比较显而易见了,虽然理论上右边的计算方式是错误的,但是怎么感觉效果还要好一点
目前可以得到一个结论,从效果上来讲,第一种方式=第二种方式=第三种方式≠第四种方式。(再次强调一下,不能保证正确性,如果有错误的话希望各位能批评指正)
这篇文章感觉篇幅已经够长了,所以关于四种方式的效果先写到这里,关于这种现象的探究放到下一篇写吧(如果我能推导出原因的话,虽然估计有可能要夭折了)。