请添加图片描述
凹凸映射纹理的另一种常见的应用就是凹凸映射 (bump mapping)。凹凸映射的目的是使用一张纹理来修改模型表面的法线,以便为模型提供更多的细节。这种方法不会真的改变模型的顶点位置,只是让模型看起来好像是“凹凸不平”的,但可以从模型的轮廓处看出“破绽”。有两种主要的方法可以用来进行
**凹凸映射:**一种方法是使用一张高度纹理(height map)来模拟表面位移(displacement),然后得到一个修改后的法线值,这种方法也被称为高度映射(heightmapping);
我们首先来看第一种技术,即使用一张高度图来实现凹凸映射。高度图中存储的是强度值(intensity),它用于表示模型表面局部的海拔高度。因此,颜色越浅表明该位置的表面越向外凸起,而颜色越深表明该位置越向里凹。这种方法的好处是非常直观,我们可以从高度图中明确地知道一个模型表面的凹凸情况,但缺点是计算更加复杂,在实时计算时不能直接得到表面法线,而是需要由像素的灰度值计算而得,因此需要消耗更多的性能。图7.11给出了一张高度图。
**法线映射:**法线纹理(normalmap)来直接存储表面法线,这种方法又被称为法线映射(normalmapping)。尽管我们常常将凹凸映射和法线映射当成是相同的技术,但读者需要知道它们之间的不同。
由于方向是相对于坐标空间来说的,那么法线纹理中存储的法线方向在哪个坐标空间中呢?对于模型顶点自带的法线,它们是定义在模型空间中的,因此一种直接的想法就是将修改后的模型空间中的表面法线存储在一张纹理中,这种纹理被称为是模型空间的法线纹理 (object-spacenormal map)。
在实际制作中,我们往往会采用另一种坐标空间,即模型顶点的切线空间(tangent space)来存储法线。对于模型的每个顶点,它都有一个属于自己的切线空间,这个切线空间的原点就是该顶点本身,而z轴是顶点的法线方向 (n),x轴是顶点的切线方向(),而y轴可由法线和切线叉积而得,也被称为是副切线 (bitangent,b)或副法线。这种纹理被称为是切线空间的法线纹理(tangent-spacenormalmap)
1法线纹理好处:
1 实现简单,更加直观。我们甚至都不需要模型原始的法线和切线等信息,也就是说,计算更少。生成它也非常简单,而如果要生成切线空间下的法线纹理,由于模型的切线一般是和UV方向相同,因此想要得到效果比较好的法线映射就要求纹理映射也是连续的。
2 在纹理坐标的缝合处和尖锐的边角部分,可见的突变(缝隙)较少,即可以提供平滑的边界。这是因为模型空间下的法线纹理存储的是同一坐标系下的法线信息,因此在边界处通过插值得到的法线可以平滑变换。而切线空间下的法线纹理中的法线信息是依靠纹理坐标的方向得到的结果,可能会在边缘处或尖锐的部分造成更多可见的缝合迹象。但使用切线空间有更多优点。
3 自由度很高。模型空间下的法线纹理记录的是绝对法线信息,仅可用于创建它时的那个模型而应用到其他模型上效果就完全错误了。而切线空间下的法线纹理记录的是相对法线信息,这意味着,即便把该纹理应用到一个完全不同的网格上,也可以得到一个合理的结果。可进行UV动画。比如,我们可以移动一个纹理的UV坐标来实现一个凹凸移动的效果,
2 两种计算方法
切线空间下进行光照计算:此时我们需要把光照方向、视角方向变换到切线空间下;
世界空间下进行光照计算:比时我们需要把采样得到的法线方向变换到世界空间下,再和世界空间下的光照方向和视角方向进行计算。
从效率上来说,第一种方法往往要优于第二种方法,因为我们可以在顶点着色器中就完成对光照方向和视角方向的变换,而第二种方法由于要先对法线纹理进行采样,所以变换过程必须在片元着色器中实现,这意味着我们需要在片元着色器中进行一次矩阵操作。
但从通用性角度来说,第二种方法要优于第一种方法,因为有时我们需要在世界空间下进行一些计算,例如在使用Cubemap进行环境映射时,我们需要使用世界空间下的反射方向对 Cubemap 进行采样。如果同时需要进行法线映射,我们就需要把法线方向变换到世界空间下。
3 切线空间下进行光照计算
我们首先来实现第一种方法,即在切线空间下计算光照模型。基本思路是:在片元着色器中通过纹理采样得到切线空间下的法线,然后再与切线空间下的视角方向、光照方向等进行计算,得到最终的光照结果。为此,我们首先需要在顶点着色器中把视角方向和光照方向从模型空间变换到切线空间中,即我们需要知道从模型空间到切线空间的变换矩阵。
Shader "MyShader/7-NormalMapTangentSpaceMat"
{
Properties
{
_Color ("Color Tint", Color) = (1,1,1,1)
_MainTex ("Main Tex", 2D) = "white" {}
_Specular("Specular", Color) = (1,1,1,1)
_Gloss("Gloss", Range(8.0,256)) = 20
//法线信息
_BumpMap("Normal Map", 2D) = "bump"{}
//法线受光照凹凸程度
_BumpScale ("Bump Scale", Float)= -0.8
}
SubShader{
pass{
//pass的光照模式
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
sampler2D _BumpMap;
//_MainTex_ST是Unity规定声明某个纹理的属性,储存纹理的缩放平移值 .xy缩放 .zw偏移
float4 _BumpMap_ST;
float4 _MainTex_ST;
float _BumpScale;
fixed4 _Specular;
float _Gloss;
//顶点着色器输入结构体
struct a2v{
fixed4 vertex : POSITION;
//NORMAL语义告诉unity法线存到normal
float3 normal : NORMAL;
//储存顶点切线方向到tangent 多一个副切线
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
//顶点着色器输出结构体,也是片原着色器的输入结构体,
struct v2f{
float4 uv : TEXCOORD0;
float4 pos : SV_POSITION;
//顶点法线纹理坐标 TEXCOORD0纹理坐标组
//光纤的切线空间坐标
float3 lightDir : TEXCOORD1;
//视角的切线空间视角
float3 viewDir : TEXCOORD2;
};
//顶点着色器
v2f vert(a2v v){
v2f o;
//顶点转换到裁切空间坐标
o.pos = UnityObjectToClipPos(v.vertex);
//xy存贴图的纹理坐标
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
//zw存法线贴图的纹理坐标
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
//fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
//fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
//fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
//float3x3 worldToTangent = float3x3(worldTangent, worldBinormal, worldNormal);
// 定义转换world space的向量到tangent space的rotation 矩阵。
TANGENT_SPACE_ROTATION;
// 光线的世界坐标转换切线空间worldToTangent
o.lightDir = mul(rotation, WorldSpaceLightDir(v.vertex)).xyz;
// 视角的世界坐标转换切线空间worldToTangent
o.viewDir = mul(rotation, WorldSpaceViewDir(v.vertex)).xyz;
return o;
}
//(10)由于我们在顶点着色器中完成了大部分工作,因此片元着色器中只需要采样得到切线空间下的法线方向,再在切线空间下进行光照计算即可:
//片元逐像素实现
fixed4 frag(v2f i) : SV_Target {
// 切线空间光线法线
fixed3 tangentLightDir = normalize(i.lightDir);
// 切线空间视角法线
fixed3 tangentViewDir = normalize(i.viewDir);
// 法线纹理采样
fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
fixed3 tangentNormal;
// 反映射法线纹理,像素值转化为向量
tangentNormal = UnpackNormal(packedNormal);
// _BumpScale控制凹凸程度
tangentNormal.xy *= _BumpScale;
// z分类可以计算得来
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
// 反射率
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
// 环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
//反射光
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
// 反射率
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
4在世界坐标计算
Shader "MyShader/7-NormalMapTangentSpaceMat"
{
Properties
{
_Color ("Color Tint", Color) = (1,1,1,1)
_MainTex ("Main Tex", 2D) = "white" {}
_Specular("Specular", Color) = (1,1,1,1)
_Gloss("Gloss", Range(8.0,256)) = 20
//法线信息
_BumpMap("Normal Map", 2D) = "bump"{}
//法线受光照凹凸程度
_BumpScale ("Bump Scale", Float)= -0.8
}
SubShader{
pass{
//pass的光照模式
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
sampler2D _BumpMap;
//_MainTex_ST是Unity规定声明某个纹理的属性,储存纹理的缩放平移值 .xy缩放 .zw偏移
float4 _BumpMap_ST;
float4 _MainTex_ST;
float _BumpScale;
fixed4 _Specular;
float _Gloss;
//顶点着色器输入结构体
struct a2v{
fixed4 vertex : POSITION;
//NORMAL语义告诉unity法线存到normal
float3 normal : NORMAL;
//储存顶点切线方向到tangent 多一个副切线
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
//顶点着色器输出结构体,也是片原着色器的输入结构体,
struct v2f{
float4 uv : TEXCOORD0;
float4 pos : SV_POSITION;
//顶点法线纹理坐标 TEXCOORD0纹理坐标组
//光纤的切线空间坐标
float3 lightDir : TEXCOORD1;
//视角的切线空间视角
float3 viewDir : TEXCOORD2;
};
//顶点着色器
v2f vert(a2v v){
v2f o;
//顶点转换到裁切空间坐标
o.pos = UnityObjectToClipPos(v.vertex);
//xy存贴图的纹理坐标
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
//zw存法线贴图的纹理坐标
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
//fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
//fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
//fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
//float3x3 worldToTangent = float3x3(worldTangent, worldBinormal, worldNormal);
// 定义转换world space的向量到tangent space的rotation 矩阵。
TANGENT_SPACE_ROTATION;
// 光线的世界坐标转换切线空间worldToTangent
o.lightDir = mul(rotation, WorldSpaceLightDir(v.vertex)).xyz;
// 视角的世界坐标转换切线空间worldToTangent
o.viewDir = mul(rotation, WorldSpaceViewDir(v.vertex)).xyz;
return o;
}
//(10)由于我们在顶点着色器中完成了大部分工作,因此片元着色器中只需要采样得到切线空间下的法线方向,再在切线空间下进行光照计算即可:
//片元逐像素实现
fixed4 frag(v2f i) : SV_Target {
// 切线空间光线法线
fixed3 tangentLightDir = normalize(i.lightDir);
// 切线空间视角法线
fixed3 tangentViewDir = normalize(i.viewDir);
// 法线纹理采样
fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
fixed3 tangentNormal;
// 反映射法线纹理,像素值转化为向量
tangentNormal = UnpackNbumpormal(packedNormal);
// _BumpScale控制凹凸程度
tangentNormal.xy *= _BumpScale;
// z分类可以计算得来
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
// 反射率
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
// 环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
//漫反射
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
// 高光
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
5 Unity的纹理类型
Unity Shader时,必须把使用的法线纹理按上面的式标识成Normalmap才能得到正确结果(即便你忘了这么做,Unity 也会在材质面板中提醒你修正这个问题),这是因为这些Unity Shader 都使用了内置UnpackNormal函数来采样法线方向。
从代码中可以看出,在某些平台上由于使用了 DXT5nm的压缩格式,因此需要针对这种格式对法线进行解码。在 DXT5nm格式的法线纹理中,纹素的a通道(即w分量)对应了法线的分量,g通道对应了法线的y分量,而纹理的r和通道则会被舍弃,法线的2分量可以由 xy分量推导而得。为什么之前的普通纹理不能按这种方式压缩,而法线就需要使用DXT5nm格式来进行压缩呢?这是因为,按我们之前的处理方式,法线纹理被当成一个和普通纹理无异的图,但实际上,它只有两个通道是真正必不可少的,因为第三个通道的值可以用另外两个推导出来(法线是单位向量,并且切线空间下的法线方向的z分量始终为正)。使用这种压缩方法就可以减少法线纹理占用的内存空间。