法线贴图(Normal mapping)
在三维计算机图形学中,是凸凹贴图(Bump mapping)技术的一种应用,法线贴图有时也称为“Dot3(仿立体)凸凹纹理贴图”。凸凹与纹理贴图通常是在现有的模型法线添加扰动不同,法线贴图要完全更新法线。与凸凹贴图类似的是,它也是用来在不增加多边形的情况下在浓淡效果中添加细节。但是凸凹贴图通常根据一个单独的灰度图像通道进行计算,而法线贴图的数据源图像通常是从更加细致版本的物体得到的多通道图像,即红、绿、蓝通道都是作为一个单独的颜色对待。(引用自维基百科)
凹凸贴图是一种提高对象的外观的真实度而不会增加几何体的复杂性的有效方式.可以模拟表面的细节或者表面的不规则.
其实这项技术并没有改变物体的表面,只是欺骗了光照计算.将不会出现在对象的轮廓边缘上.通常适合模拟橘子上的皱纹,浮雕的徽标等等.
凹凸贴图原理
N = T × normalize(dx/dv, dy/dv, dz/dv)
B = N × T
float4x4 matWorldViewProjection;
float4x4 matWorld;
float4 vViewPosition;
float3 vLight;
struct VS_INPUT
{
float4 Position : POSITION0;
float2 TexCoord : TEXCOORD0;
float3 Normal : NORMAL0;
float3 Binormal : BINORMAL0;
float3 Tangent : TANGENT0;
};
struct VS_OUTPUT
{
float4 Position : POSITION0;
float2 TexCoord : TEXCOORD0;
float3 Eye : TEXCOORD1;
float3 Light : TEXCOORD2;
};
VS_OUTPUT vs_main( VS_INPUT Input )
{
VS_OUTPUT Output;
Output.Position = mul( Input.Position, matWorldViewProjection );
Output.TexCoord = Input.TexCoord;
float3 world = mul (Input.Position, matWorld);
float3 eye = vViewPosition - world; //视线
//将光线和视线变换到正切空间,world矩阵为单位阵所以不用先变换法线
Output.Eye.x = dot(Input.Tangent, eye);
Output.Eye.y = dot(Input.Binormal, eye);
Output.Eye.z = dot(Input.Normal, eye);
Output.Light.x = dot(Input.Tangent, vLight);
Output.Light.y = dot(Input.Binormal, vLight);
Output.Light.z = dot(Input.Normal, vLight);
return( Output );
}
同时我们看一下Pixe shader里面的代码
float4 AmbientColor;
float4 DiffuseColor;
float4 SpecularColor;
float SpecularIntensity;
float SpecularPow;
sampler2D TextureMap;
sampler2D NormalMap;
struct PS_INPUT
{
float2 TexCoord : TEXCOORD0;
float3 Eye : TEXCOORD1;
float3 Light : TEXCOORD2;
};
float4 ps_main( PS_INPUT Input ) : COLOR0
{
//贴图颜色,环境光和漫反射光照亮这个颜色
float4 baseColor = tex2D(TextureMap, Input.TexCoord);
float3 light = normalize(Input.Light);
float3 eye = normalize(Input.Eye);
float3 normal = normalize(tex2D(NormalMap, Input.TexCoord).xyz * 2.0f - 1.0f);
float ndl = saturate(dot(light, normal));
//光线的反射方向r
float3 r = normalize(reflect(-light, normal));
float rdv = pow(saturate(dot(r, eye)), SpecularPow);
//光照方程
return AmbientColor * baseColor + DiffuseColor * baseColor * ndl + SpecularColor * SpecularIntensity * rdv;
}
这一句代码比较难理解
float3 normal = normalize(tex2D(NormalMap, Input.TexCoord).xyz * 2.0f - 1.0f);
那种偏蓝色的法线纹理其实就是存储了在每个顶点各自的Tangent Space中,法线的扰动方向。也就是说,如果一个顶点的法线方向不变,那么在它的Tangent Space中,新的normal值就是z轴方向,也就是说值为(0, 0, 1)。但这并不是法线纹理中存储的最终值,因为一个向量每个维度的取值范围在(-1, 1),而纹理每个通道的值范围在(0, 1),因此我们需要做一个映射,即pixel = (normal + 1) / 2。
unity 中的NormalMap shader
// The Shader takes a color and a normal texture and uses them to do normal mapping with a surface shader.
Shader "Ellioman/NormalMapSurfaceShader"
{
// What variables do we want sent in to the shader?
Properties
{
_MainTex ("Main Texture", 2D) = "white" {}
_NormalMap ("Normal Map", 2D) = "bump" {}
_NormalMapIntensity ("Normal Map Intensity", range(0, 10)) = 1
_Occlusion ("Occlusion", 2D) = "white" {}
_Specular ("Specular Map", 2D) = "white" {}
}
SubShader
{
Tags
{
"RenderType" = "Opaque"
}
CGPROGRAM
// Pragmas
#pragma surface surfaceShader StandardSpecular
// User Defined Variables
uniform sampler2D _MainTex;
uniform sampler2D _NormalMap;
uniform sampler2D _Occlusion;
uniform sampler2D _Specular;
uniform float _NormalMapIntensity;
// Base Input Structs
struct Input
{
float2 uv_MainTex;
float2 uv_NormalMap;
float2 uv_Occlusion;
float2 uv_Specular;
};
// The Surface Shader
void surfaceShader(Input IN, inout SurfaceOutputStandardSpecular o)
{
o.Albedo = tex2D(_MainTex, IN.uv_MainTex).rgb;
o.Occlusion = tex2D(_Occlusion, IN.uv_Occlusion).rgb;
o.Specular = tex2D(_Specular, IN.uv_Specular).rgb;
fixed3 n = UnpackNormal(tex2D(_NormalMap,IN.uv_NormalMap)).rgb;
n.xy*= _NormalMapIntensity;
//n.y *= _NormalMapIntensity;
o.Normal = normalize(n);
}
ENDCG
}
Fallback "Diffuse"
}