法线贴图 (Normal Map) 是一种凹凸贴图 (Bump Map)。它们是一种特殊的纹理,可让您将表面细节(如凹凸、凹槽和划痕)添加到模型,从而捕捉光线,就像由真实几何体表示一样。
获取源码
关注公众号:科技探幽
回复shader
法线贴图原理
在模型制作中,我们可以真实的去制作出凹凸感,但是这样会增加模型的面数,增加性能。那么有什么办法,不改模型的面数,就能出现凹凸感呢,那便是使用法线贴图,使用一个2D纹理来储存法线数据。
光照到物体上再通过反射光到人眼,当有凹凸面时,那么反射光线与平面是不一样的,从而产生凹凸感。而反射光线跟物体的法线有关,如果我们修改法线方向,那么反射的光线也会随之改变,当照射到人眼时,便会产生凹凸的感觉,也就模拟了真实的凹凸物体。
现实中我们无法做到,但是在计算机中,我们就可以做到,通过计算,实现一种模拟的凹凸感,用一张2D纹理来存储我们的法线数据,来修复模型的法线,从而实现凹凸的感觉。
那么法线贴图该是什么样的呢?平常我们看到法线贴图通常是这样的。那么为什么会是蓝紫色的呢?
在切线空间中,法线的方向使用z轴来表示,法线方向为(0,0,1)。法线向量从z轴方向往其他方向偏移,即修改x,y的值,法线向量方向便发生了变化,同时再经过光照计算得到反射光方向也发生了偏移,便产生了凹凸感。此时通过2d纹理如何来表示这种改变呢?由于法线的范围为-1~1,而颜色的范围为0 ~1,经过下面公式计算得到颜色值
vec3 rgb_normal = (normal + 1)/2; // 从 [-1,1] 转换至 [0,1]
此时颜色值为(0.5,0.5,1),通常软件工具颜色值的范围为0~255,通过软件工具我们查看颜色(128,128,255)
此时我们修改颜色值,如下图所示,变得到了一个产生凹凸感的颜色值。
以上便是法线贴图呈现蓝紫色的原因。此时再由法线贴图转为法线向量,即把上面的公式反过来,得到法线向量
vec3 normal = (rgb_normal)*2-1 从 [0,1]转换至 [-1,1]
实际应用
我们在Unity Shader中编写程序,来呈现法线贴图如何应用到模型上的。
我们把法线纹理的纹理类型标识成Normal map时,可以使用Unity的内置函数UnpackNormal来得到正确的法线方向,把法线范围设置为-1~1
fixed3 normalDir = UnpackNormal(noramlColor);
思考:那么为什么我们不直接使用 normal = (rgb_normal)*2-1
的计算方法获得法线方向呢?因为无法得到正确的结果。
查看源码看看UnpackNormal方法
inline fixed3 UnpackNormalDXT5nm (fixed4 packednormal)
{
fixed3 normal;
normal.xy = packednormal.wy * 2 - 1;
normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
return normal;
}
// Unpack normal as DXT5nm (1, y, 1, x) or BC5 (x, y, 0, 1)
// Note neutral texture like "bump" is (0, 0, 1, 1) to work with both plain RGB normal and DXT5nm/BC5
fixed3 UnpackNormalmapRGorAG(fixed4 packednormal)
{
// This do the trick
packednormal.x *= packednormal.w;
fixed3 normal;
normal.xy = packednormal.xy * 2 - 1;
normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
return normal;
}
inline fixed3 UnpackNormal(fixed4 packednormal)
{
#if defined(UNITY_NO_DXT5nm)
return packednormal.xyz * 2 - 1;
#elif defined(UNITY_ASTC_NORMALMAP_ENCODING)
return UnpackNormalDXT5nm(packednormal);
#else
return UnpackNormalmapRGorAG(packednormal);
#endif
}
当我们把纹理类型设置成Normal map时,Unity根据不同平台对纹理进行压缩(例如使用DXT5nm格式),从而减少减少法线纹理占用的内存空间。UnpackNormal函数内根据不同的压缩格式进行了判断,通过UnpackNormal函数来针对不同的压缩格式对法线纹理进行正确的采样。如源码中的UnpackNormalDXT5nm
、UnpackNormalmapRGorAG
函数。
完整代码:
Shader "My/tietu2"
{
Properties
{
_MainTex("Main Tex",2D) = "white"{}
_NormalMap("Normal Map",2D) = "bump"{}
_Range("Range",Range(0,1)) = 0.5
}
SubShader
{
Tags{"LightMode" = "ForwardBase" }
Pass{
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert;
#pragma fragment frag;
float _Range;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _NormalMap;
float4 _NormalMap_ST;
struct a2v
{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
float3 normal:NORMAL;
float4 tangent:TANGENT;
};
struct v2f
{
float4 uv:TEXCOORD0;
float4 svPos:SV_POSITION;
float3 normal:TEXCOORD1;
float3 lightDir:TEXCOORD2;
};
v2f vert(a2v v)
{
v2f f;
f.svPos = UnityObjectToClipPos(v.vertex);
f.uv.xy = v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw;
f.uv.zw = v.texcoord.xy*_NormalMap_ST.xy+_NormalMap_ST.zw;;
// f.normal = UnityObjectToWorldNormal(v.normal);
TANGENT_SPACE_ROTATION;//调用之后,会得到一个矩阵rotation,这个矩阵用来把模型空间下的方向转换为切线空间
f.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex));//把光从模型空间,转为切线空间
return f;
}
fixed4 frag(v2f f):SV_Target{
fixed3 texColor = tex2D(_MainTex,f.uv.xy);
half4 noramlColor = tex2D(_NormalMap,f.uv.zw);
fixed3 normalDir = UnpackNormal(noramlColor);
normalDir = normalize(normalDir);
fixed3 lightDir = normalize(f.lightDir);
fixed3 texColo = _LightColor0.rgb*texColor*max(0,dot(normalDir,lightDir)*0.5+0.5);
fixed3 color = texColo+UNITY_LIGHTMODEL_AMBIENT.rgb;
return fixed4(color,1);
}
ENDCG
}
}
FallBack "Diffuse"
}
调整凹凸感
上面我们已经实现的法线贴图,那么一张法线贴图,我们可以调整凹凸感吗,答案是肯定的。下面我们来实现。
我们让xy的值乘以一个系数,增大xy方向值,偏移后的法线是归一化的,因此满足x2 + y2 + z2 = 1。xy值增大,则z方向的值减小,凹凸感越大;当xy方向的值越小,越趋近于0时,表面越光滑。
关键程序
fixed3 normalDir = UnpackNormal(noramlColor);
normalDir.xy = normalDir.xy*_BumpScale;
//(dot(xy,xy))=x*x+y*y
//由于偏移后的法线是归一化的,因此满足x2 + y2 + z2 = 1
//所以z=sqrt(1-(x2+y2))
normalDir.z = sqrt(1.0 - saturate(dot(normalDir.xy,normalDir.xy)));
normalDir = normalize(normalDir);
Shader "My/tietu3"
{
Properties
{
_MainTex("Main Tex",2D) = "white"{}
_NormalMap("Normal Map",2D) = "bump"{}
_BumpScale("Bump Scale",Float) =1
}
SubShader
{
Tags{"LightMode" = "ForwardBase" }
Pass{
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert;
#pragma fragment frag;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _NormalMap;
float4 _NormalMap_ST;
float _BumpScale;
struct a2v
{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
float3 normal:NORMAL;
float4 tangent:TANGENT;
};
struct v2f
{
float4 uv:TEXCOORD0;
float4 svPos:SV_POSITION;
float3 normal:TEXCOORD1;
float3 lightDir:TEXCOORD2;
};
v2f vert(a2v v)
{
v2f f;
f.svPos = UnityObjectToClipPos(v.vertex);
f.uv.xy = v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw;
f.uv.zw = v.texcoord.xy*_NormalMap_ST.xy+_NormalMap_ST.zw;;
// f.normal = UnityObjectToWorldNormal(v.normal);
TANGENT_SPACE_ROTATION;//调用之后,会得到一个矩阵rotation,这个矩阵用来把模型空间下的方向转换为切线空间
f.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex));//把光从模型空间,转为切线空间
return f;
}
fixed4 frag(v2f f):SV_Target{
fixed3 texColor = tex2D(_MainTex,f.uv.xy);
half4 noramlColor = tex2D(_NormalMap,f.uv.zw);
fixed3 normalDir = UnpackNormal(noramlColor);
normalDir.xy = normalDir.xy*_BumpScale;
//(dot(xy,xy))=x*x+y*y
//由于偏移后的法线是归一化的,因此满足x2 + y2 + z2 = 1
//所以z=sqrt(1-(x2+y2))
normalDir.z = sqrt(1.0 - saturate(dot(normalDir.xy,normalDir.xy)));
normalDir = normalize(normalDir);
fixed3 lightDir = normalize(f.lightDir);
fixed3 texColo = texColor*max(0,dot(normalDir,lightDir)*0.5+0.5);
fixed3 color = texColo+UNITY_LIGHTMODEL_AMBIENT.rgb;
return fixed4(color,1);
}
ENDCG
}
}
FallBack "Diffuse"
}
应用到Unity如下图所示,调整Bump Scale的值调整凹凸感