第六章 基础纹理(2)

@函数

凹凸映射

纹理另外一种常见的应用就是凹凸映射(bump mapping)。凹凸映射的目的是使用一张纹理来修改模型表面的法线,以便为模型提供更多的细节。这种方法不会真的改变模型的顶点位置,只是让模型看起来好像是“凹凸不平”的,但能够从模型的轮廓处看出“破绽”。
有两种主要的方法能够用来进行凹凸映射:一种方法是使用一张高度纹理(height map)来模拟表面位移(displacement),而后获得一个修改后的法线值,这种方法也被称为高度映射(height mapping);另外一种方法则是使用一张法线纹理(normal map)来直接存储表面法线,这种方法又被称为法线映射(normal mapping)。尽管咱们经常将凹凸映射和法线映射当成是相同的技术,但读者须要知道它们之间的不一样。性能

1.1 高度纹理

咱们首先来看第一种技术,即便用一张高度图来实现凹凸映射。高度图中存储的是强度值(intensity),它用于表示模型表面局部的海拔高度。所以,颜色越浅代表该位置的表面越向外凸起,而颜色越深代表该位置越向里凹。这种方法的好处是很是直观,咱们能够从高度图中明确的知道一个模型表面的凹凸状况,但缺点是计算更加复杂,在实时计算时不能直接获得表面法线,而是要由像素的灰度值计算而得,所以须要消耗更多的性能。
高度图一般会和法线映射一块儿使用,用于给出表面凹凸的额外信息。也就是说,咱们一般会使用法线映射来修改光照。优化

1.2 法线纹理

而法线纹理存储的就是表面的法线方向。因为法线方向的份量范围在[-1,1],而像素的份量范围为[0,1],所以咱们要作一个映射,一般使用的映射就是:
在这里插入图片描述
这就要求,咱们正在Shader中对法线纹理进行纹理采样后,还须要对结果进行一次反映射的过程,以获得原先的法线方向。反映射的过程实际上就是使用上面映射函数的逆函数:
在这里插入图片描述
然而,因为方向是相对于坐标空间来讲的,那么法线纹理中存储的法线的方向在哪一个坐标空间中呢?对于模型顶点自带的法线,它们是定义在模型空间中的,所以一种直接的想法就是将修改后的模型空间中的表面法线存储在一张纹理中,这种纹理被称为是模型空间的法线纹理(object-space normal map)。然而,在实际制做中,咱们每每会采用另外一种坐标空间,即模型顶点的切线空间(tangent space)来存储法线。对于模型的每一个顶点,它都有一个属于本身的切线空间,这个切线空间的原点就是该顶点自己,而z轴是顶点的法线方向(n),x轴是顶点的切线方向(t),而y轴能够由法线和切线叉积获得,也被称为是副切线(bitangent,b)或副法线,以下图所示
在这里插入图片描述
这种纹理被称为是切线空间的法线纹理(tangent-space normal map)。下图分别给出了模型空间和切线空间的法线纹理。
在这里插入图片描述
从图中能够看出,模型空间下的法线纹理看起来是“五光十色”的。这是由于全部法线所在的坐标空间是同一个坐标空间,即模型空间,而每一个点存储的法线方向是各异的,有的是(0,1,0),通过映射后存储到纹理中就对应了RGB(0.5,1,0.5)浅绿色,有的是(0,-1,0),通过映射后存储到纹理中就对应了(0.5,0,0.5)紫色。而切线空间下的法线纹理看起来几乎所有是浅蓝色的。这是由于,每一个法线方向所在的坐标空间是不同的,既是表面每点各自的切线空间,这种法线纹理其实就是存储了每一个点在各自的切线空间中的法线扰动方向。也就是说,若是一个点的法线方向不变,那么在它的切线空间中,新的法线方向就是z轴方向,即值为(0,0,1),通过映射后存储在纹理中就对应了RGB(0.5,0.5,1)浅蓝色。而这个颜色就是法线纹理中大片的蓝色。这些蓝色实际上说明顶点的大部分法线和模型法线自己是同样的,不须要改变。
整体来讲,模型空间下的法线纹理更符合人类的直观认识,并且法线纹理自己也很直观,容易调整,由于不一样的法线方向就表明了不一样的颜色。但美术人员更喜欢用切线空间下的法线纹理。那么为何他们更偏好使用这种看起来“很蹩脚”的切线空间呢?
实际上,法线自己存储在哪一个坐标系里都是能够的,咱们甚至能够选择存储在世界空间下。但问题是,咱们并非单纯的想要获得法线,后续的光照计算才是咱们的目的。而选择哪一个坐标系意味着咱们须要把不一样的信息转换到相应的坐标系中。例如,若是选择了切线空间,咱们须要把从法线纹理中获得的法线方向从切线空间转换到世界空间或其它空间中。动画

1.3 使用模型空间存储法线的优势和使用切线空间的优势

整体来讲,使用模型空间存储法线的优势以下:
(1)实现简单,更加直观。咱们甚至不须要模型原始的法线和切线等信息,也就是说,计算更少。生成它也很是简单,而若是要生成切线空间下的法线纹理,因为模型的切线通常和UV方向相同,所以想要获得比较好的法线映射就要求纹理映射也是连续的。ui

(2)在纹理坐标的缝合处和尖锐的边角部分,可见的突变(缝隙)较少,便可以提供平滑的边界。这是由于模型空间下的法线纹理存储的是同一坐标系下的法线信息,所以在边界处经过插值获得的法线能够平滑变换。而切线空间下的法线纹理中的法线信息是依靠纹理坐标的方向获得的结果,可能会在边缘处或尖锐的部分形成更多可见的缝合迹象。
但使用切线空间有更多优势:
(1)自由度很高。模型空间下的法线纹理记录的是绝对法线信息,便可用于建立它时的那个模型,而应用到其它模型上的效果就彻底错误了。而切线空间下的法线纹理记录的是相对法线信息,这意味着,即使把该纹理应用到一个彻底不一样的网格上,也能够获得一个合理的结果。
(2)可进行UV动画。好比,咱们能够移动一个纹理的UV坐标来实现一个凹凸移动的效果,但使用模型空间下的法线纹理会获得彻底错误的结果。缘由同上。这种UV动画在水或火山熔岩这种类型的物体上会常常用到。
(3)能够重用法线纹理。好比一个砖块,咱们仅使用一张法线纹理就能够用到全部的六个面上,缘由同上。
(4)可压缩,因为切线空间下的法线纹理中的法线z方向老是正方向,所以咱们能够仅存储XY方向,从而推导获得Z方向。而模型空间下的法线纹理因为每一个方向都是可能的,所以必须存储3个方向的值,不可压缩。
切线空间下的法线纹理的前两个优势足以让不少人放弃模型空间下的法线纹理而选择它。从上面的优势能够看出,切线空间在不少状况下都优于模型空间,并且能够节省美术人员的工做。所以咱们也使用切线空间下的法线纹理编码

2. 实践

咱们须要在计算光照模型中统一各个方向矢量所在的坐标空间。因为法线纹理中存储的法线是切线空间下的方向,所以,咱们一般有两种选择:
(1)在切线空间下进行光照计算,此时咱们须要把光照方向、视角方向转换到切线空间下。
(2)在世界空间下进行光照计算,此时咱们须要把采样获得的法线方向变换到世界空间下,再和世界空间下的光照方向和视角方向进行计算。从效率上来讲,第一种方法每每要优于第二种方法,由于咱们在顶点着色器中就能够完成对光照方向和视角方向的变换,而第二种方法因为要先对法线纹理进行采样,因此变换过程必须在片元着色器中实现,这意味着咱们须要在片元着色器中进行一次矩阵操做。但从通用性角度来讲,第二种方法要优于第一种方法,由于有时咱们须要在世界空间下进行一些计算,例如用Cubemap进行环境映射时,咱们须要使用世界空间下的反射方向对Cubemap进行采样。若是同时须要进行法线映射,咱们就须要把法线方向变换到世界空间下。固然读者能够选择其余的坐标空间进行计算,例如模型空间等,但切空间和世界空间是最为经常使用的两个空间。下面,咱们一次实现上述的两种方法。spa

2.1 在切线空间下计算

基本思路是:在片元着色器中经过纹理采样获得切线空间下的法线,而后再与切线空间下的视角方向、光照方向等进行计算,获得最终的光照结果。为此,咱们首先要在顶点着色器中把视角方向和光照方向从模型空间变换到切线空间中,即咱们须要知道从模型空间到切线空间的变换矩阵。这个变换矩阵的逆矩阵,即从切线空间到模型空间的变换矩阵是很容易求得的,咱们在顶点着色其中按切线(x轴)、副切线(y轴)、法线(z轴)的顺序按列排列便可获得。在之前咱们讲过,若是在一个变换中仅存在平移和旋转变换,那么这个变换矩阵的逆矩阵就等于它的转置矩阵,而从切线空间到模型空间的变换正符合这个要求。所以从模型空间到切线空间变换的逆矩阵就是从切线空间到模型空间的转置矩阵,咱们把切线(x轴)、副切线(y轴)、法线(z轴)的顺序按行排列便可获得,在本节的最后,咱们能够获得相似下图的效果:
在这里插入图片描述
(1)在Properties语义块中添加法线纹理属性,以及用于控制凹凸程度的属性:code

Properties{
_Color("Color Tint", Color)  = {1,1,1,1}
_MainTex("Main Tex",2D)="white"{}
_BumpMap("Normal Map",2D)="bump"{}
_BumpScale("Bump Scale",Float) = 1.0
_Specular("Specular",Color)=(1,1,1,1)
_Gloss("Gloss",Range(8.0,256))=20
}

对于法线纹理_BumpMap,咱们使用"bump"做为它的默认值。''bump''是Unity内置的法线纹理,当没有提供任何法线纹理时。''bump''就对应了模型自带的法线信息。_BumpScale则是用于控制凹凸程度的,当它为0时,意味着该法线纹理不会对光照产生任何影响。
(2)咱们在SubShader语义块中定义了一个Pass语义块,而且在Pass的第一行指明了该Pass的光照模式:component

SubShader{
Pass{
Tags{"LightMode"="ForwardBase"}
}
}

LightMode标签是Pass标签的一种,它用于定义该Pass在Unity的光照流水线中的角色。
(3)接着,咱们使用CGPROGRAM和ENDCG来包围住Cg代码片,以定义最重要的顶点着色器和片元着色器代码。首先咱们使用#pragma指令来告诉Unity,咱们定义的顶点着色器和片元着色器叫什么名字,在本例中它们的名字分别是vert和frag:

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

(4)为了使用Unity内置的一些变量,例如_LightColor0,还须要包含进Unity的内置文件Lighting.cginc:

#include "Lighting.cginc"

(5)为了和Properties语义块中的属性创建联系,咱们在Cg代码块中声明了和上述类型匹配的变量:

fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
fixed4 _Specular;
float _Gloss;

为了获得该纹理的属性(平铺和偏移系数),咱们为_MainTex和_BumpMap定义了_MainTex_ST和_BumpMap_ST变量。
(6)咱们已经知道,切线空间是由顶点法线和切线构建出的一个坐标空间,所以咱们须要获得顶点的切线信息。为此,咱们修改顶点着色器的输入结构体a2v:

struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
float4 tangent:TANGENT;
float4 texcoord:TEXCOORD0;
};

咱们使用TANGENT语义来描述float4类型的tangent变量,以告诉Unity把顶点的切线方向填充到tangent变量中。须要注意的是,和法线方向normal不一样,tangent的类型是float4,而非float3,这是由于咱们须要tangent.w份量来决定切线空间中的第三个坐标轴——副切线的方向性。
(7)咱们须要在顶点着色器中计算切线空间下的光照和视角方向,所以咱们在v2f结构体中添加了两个变量来存储变换后的光照和视角方向:

struct v2f{
float4 pos:SV_POSITION;
float4 uv:TEXCOORD0;
float3 lightDir:TEXCOORD1;
float3 viewDir:TEXCOORD2;
}

(8)定义顶点着色器:

v2f vert(a2v v){
v2f o;
o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
o.uv.xy=v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw;
o.uv.zw=v.texcoord.xy*_BumpMap_ST.xy+_BumpMap_ST.zw;
//Compute the binormal
// float3 binormal = cross(normalize(v.normal),normalize(v.tangent.xyz))*v.tangent.w;
// //Construct a matrix which transform vectors from object space to tangent space
// float3×3 rotation = float3×3(v.tangent.xyz,binormal,v.normal);
//Or just use the bulid-in macro
TANGENT_SPACE_ROTATION;
//Transform the light direction from object space to tangent space
o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz;
//Transform the view direction from object space to tangent space
o.viewDir = mul(rotation,ObjSpaceViewDir(v.vertex)).xyz;
return o;
}

因为咱们使用了两张纹理,所以咱们须要存储两个纹理坐标。为此,咱们把v2f中的uv变量的类型定义为float4类型,其中xy份量存储了_MainTex的纹理坐标,而zw份量存储了_BumpMap的纹理坐标(实际上,_MainTex和_BumpMap一般会使用同一组纹理坐标,出于减小插值寄存器的使用数目的目的,咱们每每只计算和存储一个纹理坐标便可)。而后咱们把模型空间下的切线方向、副切线方向和法线方向按行排列来获得从模型空间到切线空间的变换矩阵rotation。须要注意的是,在计算副切线时咱们使用v.tangent.w和叉积的结果相乘,这是由于和切线与法线方向都垂直的反向有两个,而w决定了咱们选择其中哪个方向。Unity也提供了一个内置宏TANGENT_SPACE_ROTATION(在UnityCG.cginc中被定义)来帮助咱们直接计算获得rotation变换矩阵,它的实现和上述代码彻底同样。而后咱们使用Unity的内置函数ObjSpaceLightDir和ObjSpaceViewDir来获得模型空间下的光照和视角方向,再利用变换矩阵rotation把它们从模型空间变换到切线空间中。
(9)因为咱们在顶点着色器中完成了大部分工做,所以片元着色器只须要采样获得切线空间下的法线方向再在切线空间下进行光照计算便可:

fixed4 frag(v2f i):SV_Target{
fixed3 tangentLightDir=normalize(i.lightDir);
fixed3 tangentVieDir=normalize(i.viewDir);
//Get the texel in the normal map
fixed4 packedNormal = tex2D(_BumpMap,i.uv.zw);
fixed3 tangentNormal;
//If the texture is not marked as"Normal map"
//tangentNormal.xy = (packedNormal.xy*2-1)*_BumpScale;
//tengentNormal.z=sqrt(1.0-saturate(dot(tangentNormal.xy,tangentNormal.xy)));
//Or mark the texture as"Normal map",and use the built-in function;
tangentNormal=unpackNormal(packedNormal);
tangentNormal.xy*=_BumplScale;
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 specular=_LightColor0.rgb*_Specular.rgb*pow(max(0,dot(tangentNormal,halfDIR)),_Gloss);
return fixed(ambient+diffuse+specular,1.0);

}

在上面的代码中,咱们首先利用tex2D对法线纹理_BumpMap进行采样。正如咱们前面所讲,法线纹理中存储的是把法线通过映射后获得的像素值,所以咱们须要把它们反映射回来。若是咱们没有在Unity里把该法线纹理的类型设置成Normal map,就须要在代码中手动进行这个过程。咱们首先把packedNormal的xy份量按以前提到的公式映射回法线方向,而后乘以_BumpleSclae(控制凹凸程度)来获得tangentNormal的xy份量。因为法线都是单位矢量,所以tangent.z份量能够由tangentNormal.xy计算而得。因为咱们使用的是切线空间下的法线纹理,所以咱们能够保证法线方向的z份量为正。在Unity中,为了方便Unity对法线纹理的存储进行优化,咱们一般会把法线纹理的纹理类型标识成Normal map,Unity会根据平台来选择不一样的压缩方法。这时,若是咱们再使用上面的方法来计算就会获得错误的结果,由于此时_BumpMap的rgb份量并再也不是切线空间下法线方向的xyz值了,后面咱们会解释。在这种状况下,咱们可使用Unity的内置函数UnpackNormal来获得正确法线方向。
(10)最后,咱们为该Unity Shader设置合适的Fallback;

Fallback"Specular"

保存后返回Unity查看。在NormalMapTangentMat的面板上。咱们能够调整材质面板中的Bump Scale属性来改变模型的凹凸程度。下图给出了不一样的Bump Scale属性值下获得的结果。
在这里插入图片描述

2.2 在世界空间下计算

如今,咱们来实现第二种方法,即在世界空间下计算光照模型。咱们须要在片元着色器中把法线方向从切线空间变换到世界空间下。这种方法的基本思想是:在顶点着色器中计算从切线空间到世界空间的变换矩阵,并把它传递给片元着色器。变换矩阵能够由定点的切线、副切线和法线在世界空间下的表示来获得。最后,咱们只需在片元着色器中把法线纹理的法线方向从切线空间变换到世界空间下便可。尽管这种方法须要更多的计算,但在须要使用Cubemap进行环境映射等状况下,咱们就须要使用这种方法。
(1)咱们须要修改顶点着色器的输出结构体v2f,使它包含从切线空间到世界空间的变换矩阵:

struct v2f{
float4 pos:SV_POSITION;
float4 uv:TEXCOORD0;
float4 TtoW0:TEXCOORD1;
float4 TtoW1:TEXCOORD2;
float4 TtoW2:TEXCOORD3;
}

咱们在之前讲过,一个插值寄存器最多只能存储float4大小的变量,对于矩阵这样的变量,咱们能够把它们按行拆分红多个变量再进行存储。上面代码中的TtoW0、TtoW一、TtoW2就依次存储了从切线空间到世界空间的变换矩阵的每一行。实际上,对方向矢量的变换只须要使用3×3大小的矩阵,也就是说每一行只须要使用float3类型的变量便可。但为了利用插值寄存器的存储空间,咱们把世界空间下的顶点位置存储在这些变量的w份量中。
(2)修改顶点着色器,计算从切线空间到世界空间的变换矩阵:

v2f vert(a2v v){
v2f o;
o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
o.uv.xy=v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw;
o.uv.zw=v.texcoord.xy*_Bump_ST+_BumpMap_ST.zw;
float3 worldPos=mul(_Object2World,v.vertex).xyz;
fixed3 worldNormal=UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent=UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal=cross(worldNormal,worldTangent)*v.tangent.w;
//Compute the matrix that transform directions from tangent space to world space
//put the world position in w component for optimization
o.TtoW0=float4(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x);
o.TtoW1=float4(worldTangent.y,worldBinormal.y,worldNormal.y,worldPos.y);
o.TtoW2=float4(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z);
}

在上面的代码中,咱们计算了世界空间下的顶点切线、副切线和法线的矢量表示,并把它们按列摆放获得从切线空间到世界空间的变换矩阵。咱们把该矩阵的每一行分别存储在TtoW0、TtoW一、TtoW2中,并把世界空间下的顶点位置的xyz份量分别存储在了这些变量的w份量中,以便充分利用插值寄存器的存储空间。
(3)修改片元着色器,在世界空间下在进行光照计算:

fixed4 frag(v2f i):SV_Target{
//Get the position in world space
float3 worldPos = float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w);
//Compute the light and vie Dir in world space
fixed3 lightDir=normalize(UnityWorldSpaceLightDir(worldpos));
fixed3 viewDir=normalize(UnityWorldSpaceViewDir(worldPos));
//Get the normal in tangent space
fixed3 bump = UnpackNormal(tex2D(_BumpMap,i.uv.zw));
bump.xy*=_BumpScale;
bump.z=sqrt(1.0-saturate(dot(bump.xy,bump.xy)));
//Transform the normal from the tangent space to world space
bump=normalize(half3(dot(i.TtoW0.xyz,bump),dot(i.TtoW1.xyz,bump),dot(i.TtoW2.xyz,bump)));
.....
}

咱们首先从TtoW0、TtoW一、TtoW2的w份量中构建世界空间下的坐标。而后,使用内置的UnityWorldSpaceLightDir和UnityWorldSpaceViewDir函数获得世界空间下的光照和视角方向。接着咱们使用内置的UnpackNormal函数对法线纹理进行采样和编码(须要把法线的纹理格式标识成Normal map),并使用_BumpScale对其进行缩放。最后咱们使用TtoW0、TtoW1和TtoW2存储的变换矩阵把法线变换到世界空间下。这是使用点乘操做来实现矩阵的每一行和法线相乘来获得的。
从视觉表现上,在切线空间下和在世界空间下计算光照几乎没有任何差异。在Unity4.x版本中,在不须要使用Cubemap进行环境映射的状况下,内置的UnityShader使用的都是切线空间来进行法线映射和光照计算。而在Unity5.x中,全部内置的Unity Shader都是用了世界空间来进行光照计算。这也是为何Unity5.x中表面着色器更容易报错,由于它们使用了更多的插值寄存器来存储变换矩阵(还有一些额外的插值寄存器是用来辅助计算雾效的,更多内容后面咱们会见到)。

3.Unity中的法线纹理类型

上面咱们提到了当把法线纹理的纹理类型标识成Normalmap时,可使用Unity的内置函数UnpackNormal来获得正确的法线方向,以下图所示
在这里插入图片描述
当咱们须要使用那些包含了法线映射的内置的Unity Shader时,必须把使用的法线纹理按上面的方式标识成Normalmap才能获得正确的结果(即使你忘了这么作,Unity也会在材质面板中提醒你修正这个问题)。这是由于UnityShader都使用了内置的UnpackNormal函数来采样法线方向。那么当咱们把纹理类型设置成Normalmap时到底发生了什么呢?为何要这么作呢?
简单来讲,这么作可让Unity根据不一样的平台对纹理进行压缩(例如使用DXT5nm格式),再经过UnpackNormal函数来针对不一样的压缩格式对法线纹理进行正确的采样。咱们能够在UnityCG.cginc里找到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;
inline fixed3 UnpackNormal(fixed4 packednormal)
{
#if defined(UNITY_NO_DXT5nm)
return packednormal.xyz*2-1;
#else
return UnpackNormalDXT5nm(packednormal);
#endif
}
}

从代码中能够看出,在某些平台因为使用了DXT5nm的压缩格式,所以须要针对这种格式对法线进行解码。在DXT5nm格式的法线纹理中,纹素的a通道(即w份量)对应了法线的x份量,g通道对应了法线的y份量,而纹理的r和b通道会被舍弃,法线的z份量可由xy份量推导而得。为何以前的普通纹理不能按这种方式压缩,而法线就要按DXT5nm格式来进行压缩呢?这是由于,按咱们以前的处理方式,法线纹理被当成一个和普通纹理无异的图,但实际上,它只有两个通道是真正必不可少的,由于第3个通道的值能够用另外两个推导出来(法线时单位向量,而且切线空间下的法线方向的z份量始终为正)。使用这种压缩方法就能够减小法线纹理占用的内存空间。
当咱们把纹理设置成Normal map后,还有一个复选框是Creat from Grayscale,那么它是作什么用的呢?读者应该还记得在本节开始咱们提到过另外一种凹凸映射的方法,即便用高度图,而这个复选框就是用于从高度图中生成法线纹理的。高度图中自己记录的是相对高度,是一张灰度图,白色表示相对更高,黑色表示相对更低。当咱们把一张高度图导入Unity后,除了须要把它的纹理类型设置成Normal map外,还须要勾选Create from Grayscale,这样就能够获得相似下图的效果,而后咱们就能够把它和切线空间下的法线纹理同等对待了。
在这里插入图片描述 当勾选了Creat from Grayscale后,还多出了两个选项——Bumpiness和Filtering。其中Bumpiness用于控制凹凸程度,而Filtering决定咱们使用哪一种方式来计算凹凸程度,它有两种选项:一种是Smooth,这使得生成后的法线纹理会比较平滑;另外一种是Sharp,它会使用Sobel滤波(一种边缘检测时使用的滤波器)来生成法线。Sobel滤波的实现很是简单,咱们只须要在一个3×3的滤波器中计算x和y方向上的导数,而后从中获得法线便可。具体的方法是:对于高度图中的每一个像素,咱们考虑它与水平方向和竖直方向的像素差,把它们的差当成该点的对应的法线在x和y方向上的位移,而后使用以前提到的映射函数存储成到法线纹理的r和g份量便可。

相关文章
相关标签/搜索