Normal Mapping 法线贴图

本文深入探讨了法线贴图技术,在三维图形学中如何通过更新法线来提升渲染质量而不增加多边形数量。文章详细介绍了法线贴图的工作原理、切线空间的概念及其在顶点和片段着色器中的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

法线贴图Normal mapping

三维计算机图形学中,是凸凹贴图(Bump mapping)技术的一种应用,法线贴图有时也称为“Dot3(仿立体)凸凹纹理贴图”。凸凹与纹理贴图通常是在现有的模型法线添加扰动不同,法线贴图要完全更新法线。与凸凹贴图类似的是,它也是用来在不增加多边形的情况下在浓淡效果中添加细节。但是凸凹贴图通常根据一个单独的灰度图像通道进行计算,而法线贴图的数据源图像通常是从更加细致版本的物体得到的多通道图像,即红、绿、蓝通道都是作为一个单独的颜色对待。(引用自维基百科)

凹凸贴图是一种提高对象的外观的真实度而不会增加几何体的复杂性的有效方式.可以模拟表面的细节或者表面的不规则.

其实这项技术并没有改变物体的表面,只是欺骗了光照计算.将不会出现在对象的轮廓边缘上.通常适合模拟橘子上的皱纹,浮雕的徽标等等.

凹凸贴图原理

凹凸贴图的关键在于我们需要每个片元位置有个有效的表面法线,还需要有一个光源查看方向矢量.如果我们能够在片元着色器上面访问这些值,那么就可以在光源计算之前用程序扰动法线,以便产生凹凸的效果.在这种情况下,我们实际上是尝试在所渲染的表面上创建凸起或者很小的小球.
通常眼睛坐标是计算执行光照计算的最佳选择,但是就性能上来说,对于每一个片元上执行一系列操作是相当高昂的.所以,这里我们使用一种坐标,它们使用的坐标系是Tangent Space;
切空间的三条座标轴tangant轴(T)、bitangent轴(B)及法线轴(N)所组成的坐标系,即切线空间(TBN).
其中三条坐标轴的来源是:
T = normalize(dx/du, dy/du, dz/du)
N = T × normalize(dx/dv, dy/dv, dz/dv)
B = N × T

Vertex shader 代码

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"
}




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值