扰动法线来模拟凹凸不平的情况。
从高度字段来计算法线。
对法线贴图进行采样和混合。
从切线空间转换到世界空间。
这是关于渲染基础的系列教程的第六部分。这个系列教程的上一部分讲的是法对多个光源的支持。在这篇文章里面,我们将创建具有更复杂错觉的曲面。
系列回顾:
这个教程是使用Unity5.4.0f3开发的。
它看起来不像是一个光滑的球体了。
凹凸贴图
我们可以使用反射率纹理创建具有复杂颜色图案的材质。我们可以使用法线来调整表面的曲率。通过使用这些工具,我们可以生产各种表面。然而,单个三角形的表面将总是平滑的。它只能在三个法线向量之间进行插值。因此它不能表示粗糙或是有变化的表面。当抛弃反射率纹理并仅使用纯色的时候,这变得显而易见。
这个平面的一个很好的例子是一个简单的四边形。将一个简单的四边形添加到场景中,并通过围绕X轴旋转90°使这个简单的四边形指向上方。给这个简单的四边形施加我们的光照材质,没有纹理并且是全白色调。
完美的平面。
因为默认的天空盒非常的明亮,很难看到其他光源的贡献。所以,在这个教程让我们关闭默认的天空盒。你可以通过在光照设置中将环境亮度降低为零来实现这一点。然后只启用主方向光源。在场景视图中找到一个好的观点的话,你可以在四边形看到一些光差。
没有环境光,只有主方向光源的效果。
我们如何使这个四边形看起来不平?我们可以通过将阴影烘烤到反射率纹理中来伪造粗糙度。然而,这将是完全静态的。如果灯光改变,或是物体移动的话,那么阴影也应该相应的发生变化。如果没有相应的发生变化,那么幻觉会被打破。在镜面高光反射的情况下,即使相机也不允许移动。
我们可以改变法线,创造这是个曲面的错觉。但每个四边形只有四个法线,每个顶点一个。这只能产生平滑的过渡。如果我们想要一个变化和粗糙的表面,我们需要更多的法线。
我们可以将我们的四边形细分成更小的四边形。这给了我们更多的法线。事实上,一旦我们有更多的顶点,我们也可以移动它们。那么我们就不需要粗糙的错觉了,我们可以做出一个实际的粗糙表面!但是子三角形仍有同样的问题。 我们要继续细分这些子三角形吗?这将导致巨大的网格与大量的三角形。这在创建三维模型的时候很好,但在游戏中实时使用是不可行的。
高度贴图
与平坦表面相比,粗糙表面具有不均匀的高度。如果我们将这个高度数据存储在纹理中,我们可以使用它为每个片段生成法向量,而不是只能在每个顶点有一个法向量。这个想法被称为凹凸贴图,最初由詹姆斯·布林(James Blinn)提出的。
这里是我们的大理石纹理相应的高度图。它是一个RGB纹理,每个通道设置为相同的值。使用默认导入设置将这张高度图导入到项目中。
大理石纹理相应的高度图。
向My First Lighting着色器添加_HeightMap纹理属性。由于它将使用与我们的反射率纹理相同的UV坐标,因此它不需要自己的缩放和偏移参数。默认纹理并不重要,只要它是均匀的就可以。我们会使用灰色。
带有高度贴图的材质。
将匹配的变量添加到My Lighting的导入文件之中,以便我们可以访问纹理。 让我们看看它的外观,把它添加到反射率中。
像使用颜色贴图一样使用高度贴图。
调整法线
因为我们片段的法线将变得更复杂,让我们将他们的初始化移动到一个单独的函数中去。另外,摆脱高度贴图的测试代码。
因为我们目前正在使用一个位于XZ平面的四边形,所以它的法向量总是(0,1,0)。 所以我们可以使用一个常量法向量,忽略顶点数据。让我们现在做这个事情,以后再担心法线有不同的方向。
我们如何在这里导入高度数据?一个比较直观简单的方法是使用高度作为法线的Y分量,在归一化之前。
使用高度作为法线的Y分量。
这个方法不行,因为归一化将每个向量转换回(0,1,0)。黑线出现在高度为零的地方,因为在这些情况下归一化会失败。我们需要一个不同的方法。
有限差分
因为我们使用的是纹理数据,所以我们有的是二维数据。有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和1/2。这两点之间的变化率是
,在每半个单位U的跨度上。因为它更容易处理每个整个单位的变化率,我们除以点之间的距离,所以我们有
。这样我们就得到了切向量:
。
一般来说,我们必须相对于我们渲染的每个片段的U坐标来这么做。到下一个点的距离由恒定增量来定义。因此,导数函数近似为f'(u)≈f(u +δ)-f(u)δ。
δ越小,我们越逼近真实的导数函数。当然,它不能变成零,但当它到达它的理论极限,你会得到
。这种近似导数的方法称为有限差分法。这样,我们可以在任何点建立切向量,
。
从切线到法向量
我们可以在我们的着色器中使用δ的什么值?最小的明显差异将覆盖我们纹理的单个纹理。我们可以通过带有_TexelSize后缀的float4变量在着色器中检索这个信息。Unity会设置这些变量,类似于_ST变量。
什么东西存储在_TexelSize变量之中?
它的前两个分量包含的是纹理像素大小,作为U和V的分数。其他两个分量包含的是像素的数量。举个简单的例子来说,在256×128纹理的情况下,它将包含(0.00390625,0.0078125,256,128)。
现在我们可以对纹理进行两次采样,计算高度导数,并构造一个切线矢量。让我们直接使用它作为我们的法线向量。
实际上,因为我们进行了归一化,我们可以通过δ来缩放切线向量。这消去了一个除法并提高了精度。
使用切线向量作为法线向量。
我们得到一个非常明显的结果。这是因为高度有一个单位的跨度,产生了非常陡的斜坡。由于扰动的法线实际上不改变表面,我们不想要这样巨大的差异。我们可以通过任意因子来缩放高度。让我们将范围缩小到单个纹素。我们可以通过将高度差乘以δ,或者通过在切线中简单地将δ替换为1来实现。
缩放后的高度。
这开始看起来不错,但光照是错误的。太黑了。这是因为我们直接使用切线向量作为法线向量。要将切线向量转为向上的法线向量,我们必须围绕Z轴旋转切线向量90°。
使用实际的法线向量。
这个向量旋转是如何工作的?
你可以通过交换向量的X和Y分量,并翻转新的X分量的符号,来对一个二维向量做一个逆时针90°的旋转。这样我们就得到了
。
对一个二维向量做一个逆时针90°的旋转。
中心差分
我们使用有限差分近似法来创建法向量。具体地做法是,通过使用正向差分法。我们选取一点,然后沿着一个方向来确定斜率。我们得到的结果就是法线在该方向上的偏移。为了获得更好的法线近似,我们可以在两个方向上偏移采样点。这将线性近似集中在当前点上,并且被称为中心差分法。 这将导数函数更改为
。
这会轻微地移动凹凸度贴图,因此它们可以更好地与高度字段对准。除此之外,它们的形状没有发生改变。
同时使用两个维度
我们创建的法线只考虑了沿U方向的变化。我们一直使用函数f(u,v)相对于u轴的偏导数。这就是f'u(u,v),或简称f'u。我们还可以通过使用f'v沿着V方向创建法线。 在这种情况下,切向量为
而法线向量是
。
沿着V方向的法线。
我们现在可以访问沿着U方向和V方向的切线。总之,这些向量描述了在我们的片段所在处的高度场的表面。通过计算它们的叉积,我们找到二维高度场的法向量。
完整的法线的效果。
什么是叉积?
两个向量之间的叉积在几何上定义为A×B = || A || || B || sin(θ)N。 这里N是垂直于包含A向量和B向量的平面的单位矢量。 所以N是我们想要的法线向量。
||A || || B || sinθ这个部分缩放这个向量。它就像点积,除了它包含矢量之间的角度的正弦,而不是余弦之外。如果两个向量都是单位长度,并且它们之间的角度是90°,则结果是1。由于很可能不是这种情况,因此我们必须对叉积运算的结果进行归一化。只要矢量之间的角度不是0°和180°,这个方法就有效,因为如果矢量之间的角度是0°和180°的话,正弦为零。
在代数上,对于三维向量,叉积被定义为
。
在视觉上,产生的矢量的绝对量值对应于可以用两个矢量形成的平行四边形的表面积。
叉积。
需要注意的是,A×B = -B×A。这意味着结果的方向取决于向量的顺序。因为我们想要我们的向量指向上,我们必须使用cross(ty,tx),而不是cross(tx,ty)。
当你用切线向量计算叉积的时候,你会看到
。 因此,我们可以直接构造向量,而不必依赖于叉积函数。
法线贴图
当凹凸贴图起作用的时候,我们必须执行多个纹理采样和有限差分计算。这似乎是一种浪费,因为最终的法线向量应该总是相同的。为什么所有这些工作每帧都要做一次?我们可以只做一次,并将得到的法线向量存储在纹理上。
这是否与纹理过滤相冲突?
双线性滤波和三线性滤波将在法线向量之间进行混合,就像法线在三角形内部进行内插值一样。因此,我们必须对采样的法线进行归一化。
你还必须确保每个mipmap包含有效的法线。你不能简单地对纹理进行缩样,就好像它包含颜色数据一样。矢量也必须被归一化。Unity会负责这个事情。
这意味着我们需要一个法线贴图。我可以提供一个,但我们可以让Unity为我们做这个工作。将高度贴图的纹理类型更改为法线贴图。Unity自动切换纹理以使用三线性滤波,并假设我们想使用灰度图像数据来生成法线贴图。这正是我们想要的,但我们要将凹凸度更改为更低的值,如0.05。
从高度数据生成法线向量。
应用导入设置以后,Unity将计算法线贴图。原始高度图仍然存在,但Unity内部使用的是生成的贴图。
像我们在将法线可视化为颜色时所做的那样,它们必须进行调整以适应0-1范围内。 因此它们被存储为N + 12。 这表明平坦区域将呈现浅绿色。然而,它们看起来是浅蓝色的。这是因为法线贴图最常见的约定是将向上方向存储在Z分量中。所以从Unity的角度来看,Y坐标和Z坐标是交换的。
对法线贴图进行采样
因为法线贴图与高度图完全不同,请相应地重命名着色器属性。
现在使用的是法线贴图。
我们可以删除所有的高度贴图代码,并将其替换为单个纹理采样,然后进行归一化。
当然,我们必须通过计算2N-1将法线转换回原来的-1到1的范围。
此外,确保交换Y坐标和Z坐标。
使用法线贴图。
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的压缩比。
Unity用DXT5nm格式编码所有的法线贴图,无论你是否真的压缩它们。但是,当目标是移动平台的时候,事情不是这样的,因为移动平台不支持DXT5格式。在这些情况下,Unity将使用常规的RGB编码。
所以当使用DXT5nm格式的时候,我们只能去获取我们法线向量的前两个分量。
我们必须从其他两个分量推断第三个分量。因为法线是单位向量,
。因此,Nz=√1−N2x−N2yNz=1-Nx2-Ny2 。
理论上,得到的结果应等于原始Z分量。然而,因为纹理具有有限的精度,并且由于纹理过滤,结果通常是不同的。虽然它足够的接近。
此外,由于精度限制,
有可能会超出边界。通过都点积的结果进行限制,确保这个事情不会发生。
解码DXT5nm格式的法线。
缩放凹凸度
因为我们把法线烘焙成一张纹理,我们不能在片段着色器中对它们进行缩放。或者我们可以缩放法线吗?
我们可以在计算Z分量之前缩放法线向量的X分量和Y分量。如果我们减少X分量和Y分量,那么Z分量将变大,导致更平坦的表面。如果我们增加X分量和Y分量,它们会发生相反的情况。所以我们可以通过这种方式来调整凹凸度。因为我们已经对X和Y的正方形的取值进行了限制,我们永远不会得到无效的法线。
让我们为我们的着色器添加一个凹凸缩放属性,就像Unity的标准着色器所做的那样。
将这个放缩比例纳入我们的法线向量计算。
为了得到与我们在使用高度图时相同的力度的凹凸度,让我们将比例缩小到0.25。
放缩后的凹凸。
UnityStandardUtils包含UnpackScaleNormal这个函数。它自动对法线贴图使用正确的的解码,并缩放法线。所以,让我们利用这个方便的函数。
UnpackScaleNormal是什么样子的?
当目标是不支持DXT5nm的平台的时候,Unity会定义UNITY_NO_DXT5nm关键字。 在这种情况下,这个函数会切换到RGB格式,不支持正常缩放。由于指令限制,它还不支持在目标是着色器模型2的时候进行缩放。所以,当目标是移动平台的时候,不要依赖于凹凸缩放。
混合反射率贴图和凹凸贴图
现在我们有一个功能正常的法线地图,你可以检查它造成的差异。当只使用大理石反射率纹理的时候,我们的四边形看起来像是完美抛光的石头。添加法线贴图以后,它变成了一个更有趣的表面。
有凹凸贴图和没有凹凸贴图的效果对比图。
凹凸贴图的细节
在这个系列的第3部分,《使用多张纹理贴图》里面,我们创建了一个具有细节纹理的着色器。但是我们当时是用的反射率贴图,但我们也可以用凹凸贴图。首先,为My First Lighting着色器添加对细节反射率的支持。
现在有详细的反射率纹理。
不再是为细节UV添加内插值器,让我们在单个内插值器中手动打包主UV和细节UV。主UV进入XY分量,细节UV进入ZW分量。
添加所需的变量并在顶点程序中填充插值器。
现在当我们需要主UV的时候,我们应该使用i.uv.xy而不是i.uv。
将细节纹理细分为反射率。
带有细节纹理的反射率贴图,在有凹凸贴图和没有凹凸贴图时候的效果对比图。
带有细节纹理的法线贴图
由于我们的大理石材质的细节纹理是灰度贴图,我们可以使用它来生成法线贴图。复制细节纹理并将其导入类型更改为法线贴图。减少它的凹凸度,比如说减到0.1,并保留所有其他设置,跟之前一样。
当我们淡出的mipmap的时候,颜色会褪色为灰色。因此,Unity生成的细节法线贴图变淡。所以他们一起淡出。
带有细节纹理的法线贴图。
为我们的着色器添加细节法线贴图的属性。还要给它一个凹凸度的放缩属性。
带有细节的法线贴图和比例。
添加所需的变量和获取细节法线贴图,就像对主法线贴图所做的那样。在我们混合它们之前,只显示带有细节贴图的法线信息。
带有细节的凹凸度贴图。
对法线进行混合
我们通过将它们相乘在一起来混合主反射率和带有细节的反照率。我们不能对法线也这么做,因为它们是向量。但是我们可以在归一化之前对它们求平均值。
平均之后的向量。
结果不是很好。主凹凸贴图和细节凹凸贴图的突起都变平了。理想的情况是,当其中一个是平的时候,它不应该影响另一个的效果。
我们在这里尝试做的是组合两个高度字段。平均那些没有意义。对它们做加法更有意义。当对两个高度函数做加法的时候,它们的斜率-因此它们的导数 - 也做了叠加。我们可以从法线中提取出导数吗?
早些时候,我们通过归一化
构建了我们自己的法向量。我们的法线贴图包含相同类型的法线,除了它们的Y和Z分量进行了交换以外。 所以他们的形式是
。 然而,这些法线已经通过归一化过程进行了缩放。所以我们从
开始,其中s是任意比例因子。Z分量等于该因子。这意味着我们可以通过将X分量和Y分量除以Z分量来找到偏导数。这仅在Z分量为零时的时候会失败,其对应于垂直表面。我们的凹凸不靠近那么陡峭的地方,所以我们不需要担心。
一旦我们有了导数,我们可以对它们做加法以找到求和后的高度字段的导数。然后,我们转换回法线向量。 在归一化之前,所得到的向量是
。
添加导数以后的效果。
这产生了更好的结果!它在混合大多数为平面的贴图的时候效果相当好。然而,混合陡坡仍然会失去细节。另外一种方法是whiteout混合。首先,将新的法线乘以MzDz。 我们可以做到这一点,因为无论如何我们要进行归一化。这给了我们矢量
。然后放弃X分量和Y分量的缩放,就会导致
。
这种调整夸大了X分量和Y分量,从而沿着陡坡产生更明显的突起。但是当一个法线是平的时候,另一个法线不会改变。
为什么被称为whiteout混合?
这种方法首先由克里斯托弗·奥特在SIGGRAPH'07上公开描述。它用于AMD的Ruby:Whiteout演示,因此它的名称就来自这个演示。
Whiteout融合了法线与反射率后的效果。
UnityStandardUtils里面包含了BlendNormals函数,它也使用whiteout混合。 所以让我们使用这个函数。它也会对结果进行归一化,所以我们不必再自己这样做了。
BlendNormals是什么样子的?
它执行了与我们刚才做的完全相同的计算。
切线空间
直到现在,我们都假设我们是在渲染一个与XZ平面对准的平坦表面。但是这种技术如果要有用的话,它必须能在任意几何形状上使用。
映射到球体和立方体上的不正确的凹凸。
立方体贴图的一个面可以对齐,以使其符合我们的假设。我们可以通过交换和翻转维度来支持其他的面。但这假设的是一个轴对齐的立方体。当立方体具有任意旋转的时候,它变得更复杂。我们必须对我们凹凸贴图代码的结果进行变换,使它匹配立方体面的真实方向。
我们可以知道立方体面的方向吗?为此,我们需要定义U轴和V轴的向量。 这两个向量,再加上法线向量,就可以定义一个与我们的假设相匹配的三维空间。一旦我们有了这个空间,我们就可以使用它来将凹凸转换到世界空间中去。
由于我们已经具有了法向量N,我们只需要一个额外的向量。这两个向量的叉积定义了第三个向量。
附加的向量被提供作为网格的顶点数据的一部分。由于它位于由表面法线限定的平面之中,它被称为切向量T.。按照惯例,这个向量与U轴一致,指向右边。
第三个向量被称为B,双正切或二次正交。 因为Unity将其称为次法线,所以也可以用I来表示。该向量定义了向前指向的V轴。导出双正切的标准方法是通过B = N×T 然而,这将产生一个向后指向而不是向前指向的向量。要纠正这一点,结果必须乘以-1。 该因子被存储为T的额外的第四个分量。
为什么要在正切向量中存储-1?
当创建具有双侧对称性(比如说是人和动物)的三维模型的时候,常见的技术是左右镜像网格。这意味着你只需要编辑网格的一边。而你只需要一半的纹理数据,否则的话就需要全部的纹理数据。这意味着法线向量和正切矢量也被镜像。但是,双正切不应该被镜像!为了支持这一点,镜像的切线在它们的第四个分量中存储的是1,而不是-1。 所以这个数据实际上是可变的。 这就是为什么它必须显式提供。
因此,我们可以使用顶点法线和切线来构造与网格表面匹配的三维空间。这个空间称为正切空间,正切基,或是TBN空间。在立方体的情况下,每个立方体面的切线空间是均匀的。在球体的情况下,切线空间环绕其表面。
为了构造这个空间,网格必须包含切向量。幸运的是,Unity的默认网格包含了这些数据。当将网格导入到Unity中的时候,你可以导入自己的切线,或者让Unity为你生成切线。
可视化切线空间
要了解切线空间的工作原理,让我们编写一个快速的可视化内容。使用OnDrawGizmos方法创建TangentSpaceVisualizer组件
每次绘制gizmo的时候,会从游戏对象的网格过滤器中抓取网格,并使用它来显示其切线空间。当然这只有在实际上只有一个网格的时候是可以这么做的。要获取的是渲染后的网格,而不是普通网格。渲染后的网格给我们一个网格资源的引用,而普通网格则会创建一个副本。
为什么MeshFilter.mesh属性会创建一个副本?
假设你有一个使用网格资源的游戏对象。你想在运行的时候调整的只有那个游戏对象的网格。然后,你要创建特定于该对象的网格资源的本地副本。这就是为什么MeshFilter.mesh属性会创建一个网格的副本。
首先,我们将显示法线向量。从网格中获取顶点位置和法线,并使用这些来绘制线。我们必须将它们转换为到世界空间中去,以便它们匹配场景中的几何。因为法线向量对应于切线空间中的向上方向,所以让我们给这些法线向量一个绿色的表示。
是不是每次都要获取网格数据会效率很低?
是的。由于这只是一个快速的可视化,我们不需要花很多时间来进行优化。
将这个组件添加到具有网格的某些物体以查看其顶点法线。
展示顶点的法线。
什么是线段的合理长度?这取决于几何的大小。所以,让我们添加一个可配置的大小。让我们还支持一个可配置的偏移,它将线段从物体的表面推开。这使得检查重叠的顶点更容易。
偏移和缩放。
现在让我们也包括切向量在内。它们像普通向量一样工作,除了它们是四维向量以外。当他们在局部空间向右指向的时候,给他们一个红色的表示。
展示顶点的法线和切向量。
最后,构造并显示次法线向量,用蓝线表示。
显示完整的切线空间。
你可以看到,默认立方体的每个面的切线空间都是不同的,但是每个都是常数。在默认球体的情况下,切线空间对于每个顶点是不同的。作为结果,切线空间将在三角形的内部进行内插值,产生弯曲的空间。
默认球体周围的切线空间。
包围球体的切线空间是有问题的。Unity的默认球体使用经度-纬度纹理布局。 它就像在纸球周围裹上一张纸,形成一个圆筒。然后,圆柱体的顶部和底部被弄皱,直到它们匹配球体。所以极点附近时很凌乱的。Unity的默认球体混合了立方体顶点的布局,这加剧了问题。它们适用于模型,但不要指望默认网格能够生成高质量的结果。
着色器中的切线空间
要访问我们着色器中的切线,我们必须将它们添加到顶点数据结构中。
我们必须把它们作为一个附加的插值器。插值器的顺序无关紧要,但我喜欢保持法线和切线向量在一起。
使用UnityCG中的UnityObjectToWorldDir函数,在顶点程序中将切线向量转换到世界空间。当然这只适用于切线的XYZ部分。切线的W分量需要不经修改地传递。
UnityObjectToWorldDir是什么样子的?
它做的只是一个方向变换,使用世界空间到对象空间变换矩阵。
我们现在可以访问片段着色器中的法线和切线向量。所以我们可以在InitializeFragmentNormal中构造次法线。但是,我们必须注意不要用凹凸处理过的法线替换原来的法线向量。凹凸处理过的法线存在于切线空间中,因此保持它们的分离。
我们不应该规一化法线向量和正切向量吗?
如果我们想确保我们使用的是单位向量,那么我们应该规一化法线向量和正切向量。事实上,要创建一个合适的三维空间,我们还应该确保法线向量和切线向量之间的角度为90°。在这种情况下,我们必须翻转次法线,以便在镜像切线空间有一个正确的值。
但是,我们不会为这个事情感到烦恼。你会在下一节中找到原因。
现在我们可以将凹凸处理后的法线从切线空间转换到世界空间。
我们也可以摆脱显式的Y分量和Z分量的交换,将Y分量和Z分量的交换与空间转换相结合。
转换法线后的效果。
在构建次法线的时候,还有一个额外的细节。假设一个对物体的缩放设置为(-1,1,1)。这意味着它是镜像的。在这种情况下,我们必须翻转次法线,以便在镜像切线空间有一个正确的值。事实上,当奇数个维度为负的时候,我们必须这样做。UnityShaderVariables通过定义float4unity_WorldTransformParams变量来帮助我们做这个事情。当我们需要翻转次法线的时候,它的第四个分量包含-1,否则的话为1。
unity_WorldTransformParams包含哪些其他数据?
我不知道。它不用于任何其他目的。至少,我还没有看到。
同步的切线空间
当一个三维艺术家创建一个详细的模型的时候,通常的方法是构建一个非常高分辨率的模型。所有的细节都是通过实际的三维几何表现的。为了使它在游戏中能够工作,我们会生成模型的低分辨率版本。细节被烘焙到这个模型的纹理之中。
高分辨率模型的法线被烘焙成法线贴图这通过将法线从世界空间转换到切线空间来完成。当在游戏中渲染低分辨率模型的时候,这种转换是相反的。
只要两个转换使用相同的算法和切线空间,那么这个过程就可以正常工作。当两个转换没有使用相同的算法和切线空间的时候,游戏中的结果将是错误的。 这可能会导致三维艺术家感到很悲伤。所以你必须确保你的法线贴图生成器、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的法线贴图的时候,使用与每个顶点计算次法线对应的设置。或者假设他们是按照每个片段计算的,并使用一个着色器来做这个事情。
切线空间是一个麻烦,我们可以在没有它的情况下实现功能么?
因为切线空间围绕在物体的表面,物体的确切形状其实无关紧要。你可以应用任何切线空间的法线贴图。你也可以平铺贴图,正如我们所做的那样。此外,当网格因为动画而发生变形的时候,切线空间(因此法线贴图)会随之变形。
如果你不使用切线空间的话,你必须使用对象空间的法线贴图。这些贴图不贴在物体的表面上。所以它们不能平铺,它们不能应用于不同的形状,并且它们不能随着网格而变形。此外,它们不能很好地与纹理压缩一同工作。
因此,我们有很多的理由与切线空间一起工作。话虽如此,也有方法在没有明确提供切线向量的情况下来处理切线空间的法线。这些技术依赖于着色器的派生指令,我们将在以后的教程中介绍。但这并不排除对同步工作流的需要。
每个顶点或片段的次法线
如果我们想要与Unity的标准着色器一致,我们必须计算每个顶点的次法线。 这样做的好处是,我们不必在片段着色器中计算叉积。缺点是我们需要一个额外的插值器。
如果你不确定要使用哪种方法,则可以始终同时支持这两种方法。假设如果定义了BINORMAL_PER_FRAGMENT关键字,我们计算了每个片段的次法线。 否则,我们对每个顶点计算次法线。在前一种情况下,我们需要float4类型的切线内插器。在后一种情况下,我们需要两个float3类型插值器
这是否意味着我们可以跳过插值器?
当我们需要一个二次正交插值器的时候,我们只会使用TEXCOORD3。因此,当定义BINORMAL_PER_FRAGMENT的时候,我们跳过这个插值器的索引。这很好,我们可以使用我们想要的内插器索引,直到最大值。
让我们将二次正交计算放在我们自己的函数之中。然后我们可以在顶点着色器或者是片段着色器中使用它。
由于BINORMAL_PER_FRAGMENT关键字未在任何位置进行定义,因此我们的着色器将计算每个顶点的二进制值。如果你想要计算每个片段的二进制值,你必须在某个地方定义BINORMAL_PER_FRAGMENT关键字。你可以将这块看成是是我们导入文件的配置选项。因此,在包含My Lighting之前,在My First Lighting 着色器中定义它是有意义的。
因为对所有渲染通道使用相同的设置是有意义的,所以我们必须在基础渲染通道和加法渲染通道中定义它们。但是我们也可以把它放在我们着色器顶部的CGINCLUDE块中。这个块的内容包含在所有CGPROGRAM块内。
你可以通过检查编译的着色器代码来验证它是否工作。举个简单的例子来说,这里是D3D11使用的内插值器,没有定义BINORMAL_PER_FRAGMENT关键字。
而在这里,当定义BINORMAL_PER_FRAGMENT关键字的时候是下面这样。
网友评论