前言:
纹理的一种常见应用就是凹凸映射(bump mapping)。凹凸映射目的就是用一张纹理图来修改模型表面的法线,让模型看起来更加细节,这种方法不会改变模型原本的顶点位置(也就是不会修改模型的形状),只是让模型看起来凹凸不平而已。
有两种主要的方法可以用来进行凹凸映射:
1.用一张高度纹理(height map) 来模拟表面位移,然后得到修改后的法线值,也被称为高度映射(height mapping)
2.用一张法线纹理(normal map) 来直接存储表面法线,被称为法线映射(normal mapping)
高度纹理
高度纹理图中存储的是表面位移的强度值,用于表示模型表面局部的海拔高度。颜色越浅表示模型表面越向外凸起,颜色越深表明该位置越向里凹(白色为1,黑色为0,因此高度图看起来是一张黑白图)。
这种方法的优点是我们能够直观的知道模型表面的凹凸情况,缺点是计算会更加复杂。
法线纹理:
法线纹理存储的是表面法线的方向。由于法线的分量范围在[-1,1]之间,而像素的分量范围在[0,1]之间,因此需要做一个映射,即:
这就要求我们直在Shader中采样法线纹理进行纹理采样后,还需要对结构进行一次反映射的过程,用来得到原先的法线方向。反映射的过程实际上就是上面映射函数的你函数,即:
但是,由于方向是相对于坐标空间来说的,不同空间坐标系的方向也不尽相同。对于模型顶点自带的法线,它们是定义在模型空间中的,也被称为模型空间的法线纹理(object-space normal map) 。但在实际制作中,往往会采取模型顶点的切线空间(tangent space) 存储法线。对于每一个模型顶点,它们都有一个属于自己的切线方向t,切线t和法线n的叉积得到的就是副切线方向(binormal,b)或副法线。
这种纹理被称为切线空间的法线纹理(tangent-space normal map)
两者对比:
使用模型空间存储法线:
1.实现简单,更加直观,甚至不需要模型原始的法线和切线等信息,也就是说计算更少。但如果想要得到效果比较好的法线映射,由于模型的切线一般和UV坐标相同,因此需要纹理映射需要连续。
2.在纹理坐标的缝合处和尖锐的边角部分,可见的突变(缝隙)较少,即可以提供平滑的边界。这是因为模型空间下的法线纹理存储的是同一坐标系下的法线信息,因此在边界处通过插值得到的法线可以平滑变换,而切线空间下的法线纹理中的发现信息是依据纹理坐标的方向得到的结果,可能会在边缘处或者尖锐部分造成更多可见的缝合迹象。
使用切线空间存储法线:
1.自由度很高,模型空间下的法线纹理记录的是绝对法线信息,仅可用于创建它时的那个模型,而应用到其它模型上效果就完全错误。而切线空间下的法线纹理记录的是相对法线信息,这意味着即便把纹理应用到一个完全不同的网格上,也可以得到一个合理的效果。
2.可进行UV动画:比如我们可以移动一个纹理的UV坐标来实现一个凹凸移动的效果,但使用模型空间下的法线纹理则会得到完全错误的效果。
3.可以重用法线纹理。
4.可以压缩。
对比之下,切线空间下的法线在很多情况下都优于模型空间下的法线。
实践
我们需要计算光照模型中统一各个方向矢量所在的坐标空间。由于法线纹理中存储的法线是切线空间下的方向,因此我们可以有两种思考方向:
1.在切线空间下计算光照,需要把光照方向、视角方向都转换到切线空间下
2.在世界空间下计算光照,需要把采样到的法线转换到世界空间下,再和世界空间下的光照方向、视角方向计算
从效率上来说第一组往往优于第二种,因为我们可以在顶点着色器中完成对光线方向和视角方向的变换,而第二种需要对法线先进行采样,只能在片元着色器中计算。
但从通用性来讲,第二种优于第一种,因为有时候我们需要在世界空间下进行一些计算(比如在使用Cubemap进行环境映射时,我们需要把法线方向变换到世界空间下)
代码实操:
切线空间下:
Shader "Shader入门/凹凸映射/NormalMapTangentSpace"
{
Properties
{
_MainTex("MainTex",2D)="white"
_NormalTex("Normal",2D)="bump"
_NormalScale("NormalScale",float)=1.0
_Specular("Specular",float)=1.0
}
SubShader
{
Pass
{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _NormalTex;
float4 _NormalTex_ST;
float _NormalScale;
float _Specular;
struct vertexInput
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD;
};
struct vertexOutput
{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float3 lightDir : TEXCOORD1;
float3 viewDir : TEXCOORD2;
};
vertexOutput vert(vertexInput v)
{
vertexOutput o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw;
o.uv.zw = v.texcoord.xy*_NormalTex_ST.xy+_NormalTex_ST.zw;
float3 binormal = cross(normalize(v.normal),normalize(v.tangent.xyz))*v.tangent.w;
float3x3 TBN = float3x3(v.tangent.xyz,binormal,v.normal);
//TANGENT_SPACE_ROTATION 内置宏,同样实现TBN,但结果变量为rotation
o.lightDir = mul(TBN,ObjSpaceLightDir(v.vertex)).xyz;
o.viewDir = mul(TBN,ObjSpaceViewDir(v.vertex)).xyz;
return o;
}
fixed4 frag(vertexOutput i):SV_TARGET
{
half3 tangentLightDir = normalize(i.lightDir);
half3 tangentViewDir = normalize(i.viewDir);
fixed4 packedNormal = tex2D(_NormalTex,i.uv.zw);
fixed3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _NormalScale;
tangentNormal.z = sqrt(1.0-saturate(dot(tangentNormal.xy,tangentNormal.xy)));
tangentNormal = normalize(tangentNormal);
fixed3 albedo = tex2D(_MainTex,i.uv).rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
fixed3 diffuse = _LightColor0.rgb*albedo*max(0,dot(tangentNormal,tangentLightDir));
fixed3 halfDir = normalize(tangentViewDir+tangentNormal);
fixed3 specular = _LightColor0.rgb*pow(max(0,dot(tangentNormal,halfDir)),_Specular);
fixed3 final_color = diffuse+ambient+specular;
return fixed4(final_color,1.0);
}
ENDCG
}
}
}
效果:
世界空间下:
代码:
Shader "Shader入门/凹凸纹理/NormalMapWorldSpace"
{
Properties
{
_MainTex("MainTex",2D)="white"
_NormalTex("Normal",2D)="bump"
_NormalScale("NormalScale",float)=1.0
_Specular("Specular",float)=1.0
}
SubShader
{
Pass
{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _NormalTex;
float4 _NormalTex_ST;
float _NormalScale;
float _Specular;
struct vertexInput
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD;
};
struct vertexOutput
{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
};
vertexOutput vert(vertexInput v)
{
vertexOutput o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw;
o.uv.zw = v.texcoord.xy*_NormalTex_ST.xy+_NormalTex_ST.zw;
float3 worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTargent = UnityObjectToWorldDir(v.tangent);
fixed3 worldBinormal = cross(worldNormal,worldTargent)*v.tangent.w;
o.TtoW0 = float4(worldTargent.x,worldBinormal.x,worldNormal.x,worldPos.x);
o.TtoW1 = float4(worldTargent.y,worldBinormal.y,worldNormal.y,worldPos.y);
o.TtoW2 = float4(worldTargent.z,worldBinormal.z,worldNormal.z,worldPos.z);
return o;
}
fixed4 frag(vertexOutput i):SV_TARGET
{
float3 worldPos = float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w);
half3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
half3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
fixed4 packedNormal = tex2D(_NormalTex,i.uv.zw);
fixed3 normal = UnpackNormal(packedNormal);
normal.xy *= _NormalScale;
normal.z = sqrt(1.0-saturate(dot(normal.xy,normal.xy)));
normal = normalize(half3(dot(i.TtoW0.xyz,normal),dot(i.TtoW1.xyz,normal),dot(i.TtoW2.xyz,normal)));
fixed3 albedo = tex2D(_MainTex,i.uv).rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
fixed3 diffuse = _LightColor0.rgb*albedo*max(0,dot(normal,lightDir));
fixed3 halfDir = normalize(lightDir+viewDir);
fixed3 specular = _LightColor0.rgb*pow(max(0,dot(normal,halfDir)),_Specular);
fixed3 final_color = diffuse+ambient+specular;
return fixed4(final_color,1.0);
}
ENDCG
}
}
}
效果: