本文重点:
1、干扰法线来模拟凹凸 2、从高度域来计算法线 3、采样和混合法线贴图 4、从切线空间转为世界空间
(温馨提示:本系列知识是循序渐进的,推荐第一次阅读的同学从第一章看起,链接在文章底部)
这是关于渲染的系列教程的第六部分。上一部分增加了对更复杂照明的支持。这次,我们将创建更复杂表面的错觉。
本教程是使用Unity 5.4.0f3制作的。
(看起来一点也不像一个光滑的球体)
1 凹凸贴图
使用反照率纹理可以创建具有复杂颜色图案的材质,并可以使用法线来调整表面的曲率。使用这些工具,可以产生各种类型的表面。但是,单个三角形的表面却始终是光滑的。它只能在三个法线向量之间插值。因此它不能代表粗糙或变化的表面。当放弃反照率纹理并仅使用纯色时,这会变得非常明显。
这种平直度的一个很好的例子是一个简单的四边形。添加一个场景,并使其指向上,通过围绕X轴旋转90°。给它设置Lighting 材质,不设置纹理,使用完全白色的色调。
(完美扁平四边形)
由于默认的天空盒非常明亮,因此很难看到其他灯光的作用。因此,在本教程中将其关闭。你可以通过在照明设置中将环境强度降低到零来实现。然后仅启用主方向光。在场景视图中找到一个好的视角,以便在四边形上可以有一些光差异。
(无环境光,只有主方向光)
我们如何使这个四边形看起来不平坦呢?
可以通过将阴影烘焙到反照率纹理中来伪造粗糙度。但是,那将是完全静态的。如果灯光改变或物体移动,阴影也应该跟着改变才对。否则,假象就被识破了。尤其是在镜面反射的情况下,甚至不允许相机移动。
还可以更改法线以创建表面的错觉。但是每个四边形只有四个法线,每个顶点一个。这只能产生平滑的过渡。如果我们想要变化的并且粗糙的表面,则需要更多的法线。
那么还有一种方法,我们可以将四边形细分为更小的四边形,这让我们可以使用更多法线。实际上,一旦有了更多的顶点,我们就可以移动它们。然后,我们不需要粗糙感,也可以制作出实际的粗糙表面!但是子四边形仍然有同样的问题。我们要更加细分它们吗?这将导致带有大量三角形的巨大网格。创建3D模型时还行,但对于在游戏中实时使用的话,是不可行的。
1.1 高度贴图
与平坦表面相比,粗糙表面的标高不均匀。如果我们将此高程数据存储在纹理中,则可以使用它来生成每个片段而不是每个顶点的法向矢量。这个想法被称为凹凸贴图,最初由James Blinn提出。
这是一张曾经伴随我们的大理石纹理的高度图。它是RGB纹理,每个通道设置为相同的值。使用默认导入设置将其导入到你的项目中。
(大理石的高度图)
将_HeightMap纹理属性添加到“My First Lighting Shader”。由于它将使用与我们的反照率纹理相同的UV,因此不需要自己的缩放和偏移参数。只要它是统一的就行,默认纹理并不重要。Gray 会做这些的。
(带有高度贴图的材质)
将匹配变量添加到“My Lighting”包含文件中,以便我们可以访问纹理。让我们看看它的外观,将其分解为反照率。
(使用高度作为颜色)
1.2 调整法线
因为我们的片段法线将变得更加复杂,所以让我们将其初始化移到一个单独的函数中。此外,先注释高度图测试代码。
由于当前正在使用XZ平面中的四边形,因此其法向矢量始终为(0,1,0)。因此我们可以使用常量法线,而忽略顶点数据。现在暂时这样试试,稍后再考虑不同的方向。
如何在其中包括高度数据?
最傻的方法是在标准化之前将高度用作法线的Y分量。
(使用高度当做法线)
从结果看,这是行不通的,因为归一化会将每个向量都转换回(0,1,0)。黑线出现在高度为零的位置,因为在这些情况下归一化失败。我们需要另外的不同的方法。
1.3 有限差异
因为我们正在使用纹理数据,所以我们拥有二维数据。有U和V尺寸。可以认为这些高度是在第三维中向上移动。我们可以说纹理代表一个函数,f(u,v)= h。首先,我们将自己限制为U维。因此,将函数简化为 f(u)= h。
我们可以从该函数导出法线向量吗?
如果我们知道函数的斜率,则可以在任何点使用它来计算其法线。斜率由h 的变化率定义。这是它的导数h′。因为h 是函数的结果,所以h′也是函数的结果。因此,我们有导数函数f'(u)= h'。
不幸的是,我们不知道这些函数是什么东西。但可以近似它们,可以比较纹理中两个不同点的高度。例如,在最末端,使用U坐标0和1。这两个样本之间的差异是这些坐标之间的变化率。表示为函数,即f(1)-f(0)。我们可以用它来构造一个切向量
(切线方向
)
当然,这是真实切向量的非常粗略的近似。它将整个纹理视为线性斜率。我们可以通过采样两个更靠近的点来做得更好。例如,U坐标为0和½。这两个点之间的变化率是U的每半个单位f(1/2)-f(0)。因为更容易处理每单位单位的变化率, 我们将其除以两点之间的距离,然后得到
,这会给一个切向量
。
通常,我们必须相对于渲染的每个片段的U坐标执行此操作。到下一个点的距离由常数delta定义。因此,导数函数近似为
。
δ越小,我们越近似于真实导数函数。当然它不能为零,但是当达到其理论极限时,会得到
,这种近似导数的方法称为有限差分法。这样,我们可以在任何点构造切向量,
。
1.4 从切线到法线
我们的着色器中的δ可以使用什么值?
最小的合理差异将覆盖我们纹理的单个纹理像素。可以通过带有_TexelSize后缀的float4变量在着色器中检索此信息。Unity设置这些变量,类似于_ST变量。
_TexelSize变量中存储了什么?
它的前两个分量包含纹理像素大小(以U和V的分数表示)。其他两个分量包含像素数量。例如,在256×128纹理的情况下,它将包含(0.00390625、0.0078125、256、128)。
现在我们可以对纹理进行两次采样,计算高度导数,并构造一个切向量。让我们直接将其用作常规向量先。
实际上,因为无论如何都在进行归一化,所以可以按δ缩放切线向量。这消除了除法并提高了精度。
(使用切线作为法线)
我们得到了非常明显的结果。那是因为高度的范围是一个单位,这会产生非常陡峭的斜率。由于受干扰的法线实际上不会改变表面,因此我们不希望出现如此巨大的差异。可以通过任意因素缩放高度。让我们将范围缩小到单个纹理像素。可以通过将高度差乘以δ或通过将切线中的δ替换为1来实现。
(缩放高度)
看起来开始有点样子了,但是照明不正确,太黑了。那是因为我们直接使用切线作为法线。要将其变成指向上的法向矢量,我们必须将切线绕Z轴旋转90°。
(使用实际的法线)
矢量旋转如何工作的?
通过交换向量的X和Y分量,并翻转新的X分量的符号,可以将2D向量逆时针旋转90°。所以我们最后得到
。
(旋转2D向量90)
1.5 中心差法
我们使用了有限差分近似来创建法线向量。具体而言,通过使用前向差异法。取一个点,然后朝一个方向看以确定斜率。结果,法线朝该方向偏置。为了更好地近似法线,我们可以在两个方向上偏移采样点。这使线性近似值以当前点为中心,这被称为中心差法。这将导数函数更改为
。
这样会稍微改变凹凸,使它们更好地与高度场对齐。除此之外,它们的形状不会改变。
1.6 使用2个维度
刚才,我们创建的法线仅考虑了沿U的更改。我们一直在使用函数f(u,v)关于 u的偏导数。那就是f'u(u,v),或者简称为f'u 。我们还可以使用f'v 沿V创建法线。这样的话,切线的向量就是
,法线的向量是
。
(沿着V方向的法线)
现在,我们可以访问U和V切线。这些向量一起描述了我们片段上高度场的表面。通过计算它们的叉积,我们找到2D高度场的法线向量。
(完全的法线)
什么是叉乘?
两个向量之间的叉积在几何上定义为A×B = | | A | | | | B | | sin(θ)N 。这里N 是垂直于包含A 和B 的平面的单位向量。因此N 是我们想要的法向向量。
||A|| ||B|| sinθ部分缩放此向量。就像点积一样,除了它包含向量之间的角度的正弦而不是余弦。如果两个向量均为单位长度,并且两个向量之间的夹角为90°,则结果为1。因为这种情况极有可能出现,所以我们必须将交叉运算的结果归一化。只要矢量之间的夹角既不是0°也不是180°的时候,这是可行的,因为这些角度的正弦为零。
代数上,对于3D向量,叉积定义为
。
在视觉上,生成的矢量的绝对大小与你可以使用两个矢量制作的平行四边形的表面积相对应。
(叉乘)
注意 A×B = -B×A。这意味着结果的方向取决于向量的顺序。因为我们希望向量指向上方,所以我们必须使用cross(ty,tx),而不是cross(tx,ty)。
当你使用正切向量计算叉积时,你会看到
。因此,我们可以直接构造向量,而不必依赖cross函数。
2 法线贴图
当凹凸贴图起作用时,我们需要执行多个纹理样本和有限差分计算。这似乎是一种浪费,因为生成的法线应该始终相同。为什么所有这些工作每一帧都要做一遍呢?是不是可以做一次然后将法线存储在纹理中呢。
这可以用于纹理过滤吗?
双线性和三线性过滤将在法线向量之间混合,就像法线在三角形之间插值一样。因此,我们必须将采样的法线标准化。
你还需要确保每个mipmap都包含有效的法线,不能只简单地对纹理包含颜色数据进行下采样。向量也必须被标准化。Unity会做好这些。
这意味着我们需要一个法线贴图。可以自己提供一个,但其实可以让Unity为我们完成工作。将高度图的纹理类型更改为法线贴图。Unity会自动将纹理切换为使用三线性过滤,并假定我们要使用灰度图像数据生成法线贴图。这正是我们想要的,但是需要将“Bumpiness”更改为更低的值,例如0.05。
(从高度图中生成法线图)
应用导入设置后,Unity将计算法线贴图。原始高度图仍然存在,但是Unity内部使用生成的贴图。
就像我们将法线可视化为颜色时一样,必须对其进行调整以使其在0–1范围内。因此它们存储为 (N 1)/2。这表明平坦区域将显示为浅绿色。但是,现在它们显示为浅蓝色。这是因为法线贴图最常见的约定是将向上方向存储在Z分量中。从Unity的角度来看,Y和Z坐标被交换。
2.1 采样法线贴图
因为法线贴图与高度图完全不同,所以请相应地重命名shader属性。
(现在使用了一个法线贴图)
可以删除所有的高度图代码,并用单个纹理样本替换它,然后进行标准化。
当然,我们必须通过计算2N-1之后将法线转换回其原始的-1~1的范围。
另外,交换Y和Z。
(使用法线贴图)
2.2 DXT5nm
我们的法线肯定是有问题的。这是因为Unity最终以不同于我们预期的方式对法线进行编码。即使纹理预览显示RGB编码,Unity实际上仍使用DXT5nm。
DXT5nm格式仅存储法线的X和Y分量。其Z分量将被丢弃。如你所料,Y分量存储在G通道中。但是,X分量存储在A通道中。不使用R和B通道。
为什么以这种方式存储X和Y?
使用四通道纹理仅存储两个通道似乎很浪费。当使用未压缩的纹理时,的确如此。DXT5nm格式的想法是应与DXT5纹理压缩一起使用。默认情况下,Unity会执行此操作。
DXT5通过对4×4像素的块进行分组并用两种颜色和查找表对其进行近似来压缩像素。用于颜色的位数随每个通道而变化。R和B分别获得5位,G获得6位,而A获得8位。这就是X坐标移至A通道的原因之一。另一个原因是RGB通道获得一个查找表,而A获得其自己的查找表。这样可以使X和Y分量保持隔离。
压缩是有损的,但对于法线贴图是可以接受的。与未压缩的8位RGB纹理相比,你获得了3:1的压缩率。
无论是否实际压缩,单位都会以DXT5nm格式对所有法线贴图进行编码。但是,针对移动平台时,情况并非如此,因为它们不支持DXT5。在这种情况下,Unity将使用常规的RGB编码。
因此,使用DXT5nm时,我们只能检索法线的前两个分量。
我们必须从其他两个通道中推断出第三个通道。因为法线是单位向量,
从理论上讲,结果应等于原始Z分量。但是,由于纹理的精度有限,并且由于纹理过滤,结果通常会有所不同。不过,它已经足够接近了。
另外,由于精度限制,有可能
最终超出范围。通过clamping点积,确保不会发生这种情况。
(解码 DXT5nm 法线)
2.3 缩放凹凸
由于我们将法线烘焙为纹理,因此无法在片段着色器中缩放它们。或者还是可以呢?
可以在计算Z之前缩放法线的X和Y分量。如果减小X和Y,则Z会变大,从而导致平面变平。如果我们增加它们,将会发生相反的情况。因此,可以通过这种方式调整凹凸。由于我们已经clamp了X和Y的平方,所以永远不会以无效的法线结束。
向着色器添加凹凸缩放属性,就像Unity的标准着色器一样。
将此比例纳入我们的正常计算中。
要获得与使用高度图时相同强度的凹凸,请将比例减小到0.25。
(缩放凹凸)
UnityStandardUtils包含UnpackScaleNormal函数。它会自动对法线贴图使用正确的解码,并缩放法线。因此,让我们利用该便捷功能。
UnpackScaleNormal是什么样的?
当定位不支持DXT5nm的平台时,Unity定义UNITY_NO_DXT5nm关键字。在这种情况下,该功能将切换为RGB格式,并且不支持正常缩放。由于指令限制,在定位Shader Model 2时,它也不支持缩放。因此,在定位移动设备时,请勿依赖凹凸缩放。
2.4 结合反照率和凹凸
现在我们有了功能性的法线贴图,你可以检查它带来的差异。仅使用大理石反照率纹理时,我们的四边形看起来就像是完美抛光的石头。添加法线贴图之后,它会变得更加有意思。
(有和没有凹凸)
3 凹凸细节
在第3部分“组合纹理”中,我们创建了具有细节纹理的着色器。我们用反照率做到了这一点,但我们也可以用凹凸来做到这一点。首先,还向My First Lighting Shader添加对细节反照率的支持。
(现在增加了反照率贴图)
无需为细节UV添加插值器,而是将主UV和细节UV手动打包到一个插值器中。主UV进入XY,细节UV进入ZW。
添加所需的变量,然后将插值器填充到顶点程序中。
现在,当我们需要主UV时,应该使用i.uv.xy而不是i.uv。
将细节纹理分解为反照率。
(细节反照率,有和没有凹凸)
3.1 细节法线
由于我们大理石材质的细节纹理是灰度的,因此我们可以使用它来生成法线贴图。复制它,并将其导入类型更改为
Normal Map。将其“Bumpiness”降低到0.1之类,并保持所有其他设置不变。
当我们逐渐淡化mipmap时,颜色会逐渐变为灰色。结果,Unity生成的细节法线贴图逐渐淡化。因此它们一起淡出。
(细节法线纹理)
将细节法线贴图的属性添加到我们的着色器。也给它一个凹凸的缩放。
(细节法线贴图和缩放)
添加所需的变量并获取详细的法线贴图,就像主法线贴图一样。在我们合并它们之前,只显示细节法线。
(细节凹凸)
3.2 融合法线
将主反照率和细节反照率相乘。但不能用法线来做这件事,因为它们是向量。在归一化之前,可以先把他们平均化。
(平均法线)
结果不是很好。主凹凸和细节凹凸都会变平。理想情况下,当其中一个平坦时,它根本不会影响另一个。
我们在这里实际上要尝试做的是结合两个高度场。平均它们没有意义。叠加它们更有意义。当添加两个高度函数时,它们的斜率(也就是它们的导数)也要相加。
我们可以从法线中提取导数吗?
之前,我们通过归一化构造了自己的法线向量
法线贴图包含相同类型的法线,除了它们的Y和Z分量已互换。所以它们的形式是
但是,这些法线已通过标准化过程进行了缩放。所以我们从
开始。
s是任意比例因子。Z分量等于该因子。这意味着我们可以通过将X和Y除以Z来找到偏导数。这仅在Z为零(与垂直表面相对应)时失败。我们的凹凸远没有那么陡峭,所以不必为此担心。
一旦有了导数,就可以将它们相加,以求出总和高度场的导数。然后,我们转换回法线向量。在归一化之前,所得向量为
(增加导数)
这样会产生更好的结果!当组合大部分为平面的贴图时,它的效果很好。但是,合并陡峭的斜率仍然会丢失细节。另一种替代方法是泛白混合。首先,将新法线乘以 MzDz。之所以可以这样做,是因为之后无论如何都要进行归一化。这给了我们向量
然后降低X和Y的缩放比例,得到
这种调整会夸大X和Y分量,从而沿陡峭的坡度产生更明显的凸起。但是,当一个法线平坦时,另一个法线不会改变。
为什么称为泛白混合?
该方法由Christopher Oat在SIGGRAPH’07上首次公开描述。它用于AMD的Ruby:Whiteout演示中,因此得名。
(泛白混合法线,和反照率)
UnityStandardUtils包含BlendNormals函数,该函数还使用泛白混合。因此,让我们开始这个功能。它还可以标准化结果,因此我们不再需要自己做。
BlendNormals是什么样的?
它执行与我们完全相同的计算。
4 切线空间
到此为止,我们已经假定要对与XZ平面对齐的平面着色。但是,要使该技术有实际用途的话,它必须适用于任意几何形状。
(在立方体和球体上错误的凹凸映射)
可以先对齐立方体的一个面,以使其符合我们的假设。通过交换和翻转尺寸来支持其他面。但这是建立在假定一个轴对齐的立方体上。当立方体具有任意旋转时,它会变得更加复杂。必须转换凹凸贴图代码的结果,使其与表面的实际方向匹配。
我们能知道一个表面的方向吗?
为此,我们需要定义U和V轴的向量。这两个,加上法线向量,定义了一个与我们的假设相符的3D空间。一旦有了这个空间,我们就可以使用它来将凹凸转换为世界空间。
因为我们已经有了法线向量 N,只需要多一个附加向量。这两个向量的叉积定义了第三个向量。
提供附加向量作为网格顶点数据的一部分。由于它位于表面法线定义的平面中,因此称为切向量 T。按照惯例,此向量与U轴匹配,指向右侧。
第三个向量称为B,副切线或副法线。正如Unity将其称为副法线一样,我也这么称呼。此向量定义V轴,指向前方。导出二进制切线的标准方法是通过B = N×T。但是,这将产生指向后方而非向前的向量。要纠正此问题,必须将结果乘以-1。此因子存储为T的额外的第四部分。
为什么将-1存储在切向量中?
创建具有双边对称性的3D模型(例如人和动物)时,一种常见的技术是左右镜像网格。这意味着你只需要编辑网格的一侧。也就是只需要一半的纹理数据即可。这意味着法向和切向量也将被镜像。但是,不应该镜像双切线!为此,镜像切线将1存储在其第四分量中,而不是-1。因此,该数据实际上是可变的。这就是为什么必须明确提供它的原因。
然后,我们可以使用顶点法线和切线来构造一个与网格表面匹配的3D空间。该空间称为切线空间,切线基础或TBN空间。在立方体的情况下,每个面的切线空间是均匀的。对于球体,切线空间环绕其表面。
为了构造该空间,网格必须包含切向量。幸运的是,Unity的默认网格包含此数据。将网格导入Unity时,你可以导入自己的切线,或者让Unity为你生成它们。
4.1 可视化切线空间
为了了解切线空间的工作原理,让我们对其进行快速可视化编码。使用OnDrawGizmos方法创建TangentSpaceVisualizer组件。
每次绘制gizmos时,请从游戏对象的mesh filter中获取网格,然后使用它来显示其切线空间。当然,这仅在实际存在网格的情况下有效。抓住shadedMesh,而不是网格。第一个为我们提供了对网格物体资产的引用,而第二个将创建副本。
为什么MeshFilter.mesh属性创建一个副本?
假设你有一个使用网格物体资产的游戏对象。你只想在运行时调整该游戏对象的网格。然后,你就需要创建特定于该对象的网格物体资产的本地副本。这就是为什么MeshFilter.mesh创建副本的原因。
首先,我们将显示法线向量。从网格获取顶点位置和法线,然后使用它们绘制线。需要将它们转换为世界空间,以便它们与场景中的几何形状匹配。由于法线与切线空间中的向上方向相对应,因此我们将其设为绿色。
每次都检索网格数据是否效率不高?
是的。由于这只是快速的可视化,所以无需费心对其进行优化。
将此组件添加到带有网格的某些对象中以查看其顶点法线。
(展示法线)
线的合理长度是多少?
这取决于几何形状。因此,让我们添加一个可配置的比例尺。还支持可配置的偏移量,该偏移量可将线推离表面。这样可以更轻松地检查重叠的顶点。
(偏移和缩放)
现在包括切向量。它们的工作方式与法线向量一样,只是它们是4D向量。当他们在本地向右指向时,给他们涂红色。
(展示法线和切线)
最后,用蓝线构建并显示副法线向量。
(展示完整的切线空间)
你可以看到切线空间是不同的,但默认立方体的每个面都是恒定的。在默认球体的情况下,每个顶点的切线空间不同。结果,切线空间将跨三角形插值,从而形成弯曲的空间。
(围绕球体的切线空间)
在球体周围包裹切线空间是有问题的。Unity的默认球体使用经纬度纹理布局。这就像在球上包裹一张纸,形成一个圆柱。然后,将圆柱体的顶部和底部弄皱,直到它们与球体匹配。所以两极都很混乱。Unity的默认球体将其与立方顶点布局结合在一起,从而加剧了问题。它们适用于实体模型,但是不要期望默认的网格物体会产生高质量的结果。
4.2 着色器处理切线空间
要访问着色器中的切线,我们必须将它们添加到VertexData结构中。
而且我们必须将它们作为附加的插值器包括在内。插补器的顺序无关紧要,但是我喜欢将法线和切线保持在一起。
使用UnityCG中的UnityObjectToWorldDir在顶点程序中将切线转换为世界空间。当然,这仅适用于切线的XYZ部分。它的W分量需要不加修改地传递。
UnityObjectToWorldDir是什么样的?
它只是一个方向变换,使用了世界到物体的矩阵。
现在,我们可以访问片段着色器中的法线和切线。因此,我们可以在InitializeFragmentNormal中构造副法线。但是,必须注意不要用凹凸法线代替原始法线。凹凸法线存在于切线空间中,因此请使其分开。
不应该对法线和切向量进行归一化吗?
如果我们要确保我们正在使用单位向量,那么确实应该这样做。实际上,要创建合适的3D空间,我们还应确保法线和切线之间的角度为90°。但是,不要为此烦恼。你会在下一节中找到原因。
现在我们可以将凹凸法线从切线空间转换为世界空间。
我们还可以摆脱显式的YZ交换,将其与空间转换结合起来。
(转换后的法线)
构造副法线时,还有一个附加细节。假设对象的比例尺设置为(-1,1,1)。这意味着它已被镜像。在这种情况下,我们必须翻转副法线,以正确反映切线空间。实际上,当奇数维数为负时,我们必须这样做。UnityShaderVariables通过定义float4 unity_WorldTransformParams变量来帮助我们。当我们需要翻转副法线时,它的第四个分量包含-1,否则为1。
unity_WorldTransformParams还包含哪些其他数据?
我不知道。它不用于其他任何用途。至少还没有。
4.3 同步切线空间
当3D美术创建详细模型时,通常的方法是建立一个非常高分辨率的模型。所有细节都是实际的3D几何。为使此功能在游戏中起作用,会生成模型的低分辨率版本。将然后细节烘焙为该模型的纹理。
高分辨率模型的法线被烘焙到法线贴图中。这是通过将法线从世界空间转换为切线空间来完成的。在游戏中渲染低分辨率模型时,此转换是相反的。
只要两个转换使用相同的算法和切线空间,此过程就可以正常进行。如果他们不这样做,那么游戏中的结果就是错误的。这可能会让3D美术师感到非常难过。因此,你必须确保法线贴图生成器,Unity的网格物体导入过程和着色器都已同步。这称为同步切线空间工作流程。
那法线贴图呢?
我们从高度场生成了法线贴图。它们具有平坦的参考框架,并且其切线空间是规则的。因此,当将它们应用于具有弯曲切线空间的对象时,与高度场相比,最终法线会变形。但这还好,因为大理石的确切外观无关紧要。
从5.3版本开始,Unity使用mikktspace。因此,请确保在生成法线贴图时也使用mikktspace。导入网格时,你可以允许Unity为你生成切向量,因为它使用mikktspace算法。或者,自己导出mikktspace切线,并让Unity使用这些切线。
什么是mikktspace?
它是切线空间和法线生成的标准,由Morten Mikkelsen创建。该名称是Mikkelsen切线空间的简写。
为了使着色器与mikktspace同步,它必须在顶点程序中接收归一化的法线和切向量。然后对这些向量进行插值,而不对每个片段进行重新归一化。通过计算cross(normal.xyz,tangent.xyz)* tangent.w可以找到副法线。因此,我们的着色器与mikktspace同步,Unity的标准着色器也是如此。
请注意,不能保证mikktspace是规则的。法线和切线之间的角度可以自由变化。只要失真不变得太大,这都不是问题。因为我们仅使用它来转换法线,所以一致性至关重要。
使用mikktspace时,只有一种选择。副法线可以像我们一样在片段程序中构造,也可以像Unity一样在顶点程序中构造。两种方法都产生略有不同的双标准态。
(夸大的副法线差异)
因此,在为Unity生成法线贴图时,请使用与计算每个顶点的副法线对应的设置。或继续假设它们是按片段计算的,并使用也可以这样做的着色器。
切线空间很麻烦,如果没有它,要怎么做?
由于切线空间环绕对象的表面,因此对象的确切形状无关紧要。你可以对其应用任何切线空间法线贴图。你也可以像我们一样平铺贴图。同样,当网格由于其动画而变形时,切线空间(以及法线贴图)也会随之变形。
如果要消除切线空间,则必须使用对象空间法线贴图。这些贴图不粘在表面上。因此它们无法平铺,不能应用于不同的形状,也不会随网格变形。此外,它们在纹理压缩方面也不起作用。
因此,我们有足够需要的理由去使用切线空间。话虽如此,也有一些方法可以使用切线空间法线,而无需明确提供切线向量。这些技术依赖于着色器派生指令,我们将在以后的教程中进行介绍。但这并不能消除对同步工作流程的需求。
4.4 逐顶点或者逐像素的副法线
如果要与Unity的标准着色器保持一致,则必须计算每个顶点的副法线。这样做的好处是我们不必在片段着色器中计算叉积。缺点是我们需要一个附加的插值器。
如果不确定使用哪种方法,则可以同时支持这两种方法。假设如果定义了BINORMAL_PER_FRAGMENT,我们将计算每个片段的双标准态。否则,我们按顶点进行。在前一种情况下,我们保留float4切线插值器。在后者中,我们需要两个float3插值器。
这是否意味着我们跳过插值器?
仅在需要双正态插值器时才使用TEXCOORD3。因此,当定义BINORMAL_PER_FRAGMENT时,我们将跳过此插值器索引。这还不错,我们可以使用所需的任何内插器索引,最大为最大值。
让我们将副法线计算放在自己的函数中。然后,可以在顶点着色器或片段着色器中使用它。
由于未在任何地方定义BINORMAL_PER_FRAGMENT,因此我们的着色器现在将计算每个顶点的双法线。如果要按片段计算它们,则必须在某处定义BINORMAL_PER_FRAGMENT。你可以将其视为包含文件的配置选项。因此,在包括“My Lighting”之前,在“My First Lighting ”中定义它是有意义的。
由于对所有pass使用相同的设置是有意义的,因此我们必须在基本pass和附加pass中都定义它。但是我们也可以将其放在着色器顶部的CGINCLUDE块中。该块的内容包含在所有CGPROGRAM块内。
可以通过检查编译的着色器代码来验证此方法是否有效。例如,这是D3D11使用的插值器,但未定义BINORMAL_PER_FRAGMENT。
当定义了BINORMAL_PER_FRAGMENT时,它们就在这里。
下一个章节,阴影。