全国 【切换城市】欢迎您来到装修百科!
关注我们
我要装修

三平面(Triplanar)Shader的正确法线映射(三维平面的法线方程)

发布:2024-09-20 浏览:66

核心提示:三平面贴图是处理复杂几何体纹理的绝佳解决方案,这些几何体很难或不可能使用传统的 UV 来处理,而不产生明显的拉伸和/或纹理接缝。这种技术,在很多情况下法线映射的实现要么不完整,要么完全错误。请注意,本文中的着色器代码是为在 Unity 中使用而编写的。所有讨论的技术都可以在其他引擎中使用,但可能需要进行修改才能使它们正常工作。示例 Unity 着色器可在此处获取:这是一个很好且便宜(高性能))的近似值。着色器代码看起来有点奇怪,就像看起来不应该工作一样奇怪。我很少看到它被实际使用。我怀疑这是由于糟糕的实现

三平面贴图是处理复杂几何体纹理的绝佳解决方案,这些几何体很难或不可能使用传统的 UV 来处理,而不产生明显的拉伸和/或纹理接缝。
这种技术,在很多情况下法线映射的实现要么不完整,要么完全错误。
请注意,本文中的着色器代码是为在 Unity 中使用而编写的。
所有讨论的技术都可以在其他引擎中使用,但可能需要进行修改才能使它们正常工作。
示例 Unity 着色器可在此处获取:这是一个很好且便宜(高性能))的近似值。
着色器代码看起来有点奇怪,就像看起来不应该工作一样奇怪。
我很少看到它被实际使用。
我怀疑这是由于糟糕的实现导致人们认为它不起作用。
事实上,如果你在 Unity 中逐字实现这种技术,它甚至可能看起来比网格切线的方法更糟糕。
因此,在我们深入研究 GPU Gems 3 文章中所做的事情之前,让我们看一下网格切线方法及其问题。
朴素方法(即,只使用网格切线)这是大多数人尝试的第一件事,乍一看它“可以工作”。
所以让我们使用基本的岩石纹理和法线贴图来尝试一下。
这是使用 Unity 的默认球体,来自正上方的方向光,并且没有明显错误。
在预期的地方有凸起和脊,那么问题是什么?嗯,这是最佳情况,UV 在默认球体上的包裹方式、光线方向以及法线贴图的模糊方向都协同工作。
它使一切都看起来像在正常工作。
或者至少对大多数人来说,工作得足够好,他们不会关心或注意到。
但是,让我们将法线贴图更改为更明显的东西,并玩弄灯光方向。
所以现在灯光来自左侧,但是这些凸起是向内还是向外?左侧看起来像是向外,右侧看起来像是向内,而顶部,嗯。
在顶部,灯光看起来像是来自完全不同的方向,具体取决于你正在查看的哪个部分。
这就是朴素方法不好的原因,有时你会很幸运,有时它们会完全错误。
为了比较,这是它应该的样子。
凸起很明显地凸出,而不是凹陷,并且它们都与灯光方向一致。
侧面之间的柔和混合对于这种法线贴图来说有点奇怪,但忽略这一点,上面的可以被认为是“真实情况”。
以下是它们并排放置的。
切线空间法线贴图旁切线(Side Tangent)所以让我们谈谈切线空间法线贴图是什么,以及为什么朴素方法不真正起作用。
切线空间法线贴图和一般的法线贴图往往会让刚接触着色器的人感到困惑,他们只是将其视为“魔法”。
一些关于切线空间法线贴图的文章会立即进入数学或着色器代码,而没有先用基本术语解释它,这并没有帮助。
所以这是我尝试解释的尝试。
法线贴图是相对于纹理方向的方向。
除了这些之外,还有很多实现细节和细微之处,但它们不会改变这个基本前提。
对于 Unity,红色表示它有多“右”(x 轴),绿色表示它有多“上”(y 轴),蓝色表示它与表面垂直的程度(z 轴)。
所有这些都是相对于纹理 UV 和顶点法线而言的。
这有时被称为 +Y 法线贴图。
[¹]半球 +Y 法线贴图如果你只是看着法线贴图并试图理解它,不要担心。
我个人喜欢一次查看一个通道,以便更清楚地看到每个方向。
对于实时图形,网格的顶点存储一个切线,或 UV 的“从左到右”方向。
这与顶点法线和副切线(有时称为副法线 ²)一起传递到片段着色器,作为切线到世界空间变换矩阵。
这是为了使法线贴图中存储的方向可以从切线空间旋转到世界空间。
关键在于切线只与存储的纹理 UV 方向匹配。
如果你的三平面投影贴图 UV 不匹配(对于大多数网格来说,它肯定不匹配!),那么你将得到看起来像反转、侧向或面向任何其他方向(而不是你想要的方向)的法线。
Unity 切线方向这里我们有一个纹理,在其上绘制了切线 (x) 和副切线 (y) 对齐。
由于网格的切线基于 UV,因此它可以作为这些网格切线的良好近似值。
下面是使用此纹理的球体网格。
它在使用网格中存储的 UV 和生成的 triplanar UV 之间交替,两者都按比例缩放,以便纹理被平铺多次。
你可以在球体的左侧看到方向大致对齐,但在顶部和右侧它们有很大不同。
如果你再次查看朴素方法,你就可以看到这种差异会导致法线贴图看起来被翻转和旋转。
这里关于网格切线和三平面贴图的补充说明。
一般来说,如果你正在进行三平面贴图,那么所使用的网格不需要甚至不应该在顶点中存储 UV。
扩展到网格也不应该有切线。
对于导入到 Unity 或与 Unity 一起提供的网格,默认行为是 Unity 为网格导入或生成切线。
这是朴素方法甚至可能存在的唯一原因。
在代码中创建的网格默认情况下没有切线,甚至没有 UV 或法线,朴素方法将更加糟糕。
接近解决方案基本swizzle对于三平面贴图,你真正想要的是为三平面纹理 UV 的每个“面”计算唯一的切线和副切线。
但是,如果你还记得我在开头说不要这样做。
为什么?因为我们可以用很少的数据做出令人惊讶的良好近似。
我们可以使用世界轴作为切线。
更简单、更便宜的是,我们可以交换法线贴图的一些轴(即swizzle),并获得相同的结果!// 基本swizzle// 三平面 uvsfloat2 uvX = i.worldPos.zy; // x 面朝平面float2 uvY = i.worldPos.xz; // y 面朝平面float2 uvZ = i.worldPos.xy; // z 面朝平面// 切线空间法线贴图half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));// 获取表面法线的符号 (-1 或 1)half3 axisSign = sign(i.worldNormal);// 翻转切线法线 z 以考虑表面法线朝向tnormalX.z *= axisSign.x;tnormalY.z *= axisSign.y;tnormalZ.z *= axisSign.z;// 交换切线法线以匹配世界方向并进行三混合half3 worldNormal = normalize( tnormalX.zyx * blend.x + tnormalY.xzy * blend.y + tnormalZ.xyz * blend.z );我省略了大部分着色器。
我们稍后会讨论。
主要关注的是前三行和最后三行。
注意每个平面的 UV 和法线贴图的交换轴顺序相同。
正如我们上面讨论的,切线空间法线贴图应该与它的 UV 方向对齐。
由于我们使用世界位置作为 UV,所以这就是对齐方式!你可能已经注意到我突然改用立方体了。
这是因为基本交换方法适用于盒状、体素风格的地形。
如果用于平坦、轴对齐的墙壁,它实际上是真实情况。
不幸的是,它在圆形表面上没有那么有效。
那么我们能否以某种方式将一些圆度添加到交换后的法线贴图中?混合细节好的,让我们在这里停一下。
我们将讨论切线空间法线贴图混合。
特别是对于细节法线的使用方式,与三平面贴图完全无关。
你问为什么?继续读这篇文章。
为此,有几种技术。
人们想到的第一个想法是“将它们加在一起”,但这并不是真正的解决方案,它只是将两个法线都压平了。
一些更聪明的人可能会想“覆盖混合怎么样?”它可以正常工作,但它的流行纯粹是因为它在 Photoshop 中很容易实现,而不会完全破坏一切。
这不是因为它在任何程度上都是正确的,甚至特别便宜。
这是另一种技术,它比正确的方式更昂贵。
基本上有两种最常使用的竞争性近似值,即所谓的“白化(Whiteout)”和“UDN”法线混合。
它们在实现和结果方面非常相似。
UDN 稍微便宜一些,但可能会稍微压平边缘,而白化看起来稍微好一些,但稍微贵一些。
对于现代台式机和游戏机 GPU,几乎没有理由不使用白化方法而不是 UDN 方法。
但 UDN 混合仍然在移动设备中有所用。
你可以在 Stephen Hill 的博客中阅读有关不同法线贴图混合技术的更多信息:混合细节http://blog.selfshadow.com/publications/blending-in-detail/三平面法线贴图那么切线空间法线贴图混合如何应用于三平面贴图?我们可以将每个平面视为一个独立的法线贴图混合,其中我们混合的“法线贴图”之一是顶点法线!来自那篇文章的 UDN 混合实际上非常便宜,因为它只是将 x 和 y 法线贴图值添加到顶点法线!让我们看看它是什么样子的。
UDN 混合// UDN 混合// 三平面 uvsfloat2 uvX = i.worldPos.zy; // x 面朝平面float2 uvY = i.worldPos.xz; // y 面朝平面float2 uvZ = i.worldPos.xy; // z 面朝平面// 切线空间法线贴图half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));// 将世界法线交换到切线空间并应用 UDN 混合。
// 这些应该被归一化,但在混合之后跳过它是一个非常小的视觉// 差异。
tnormalX = half3(tnormalX.xy + i.worldNormal.zy, i.worldNormal.x);tnormalY = half3(tnormalY.xy + i.worldNormal.xz, i.worldNormal.y);tnormalZ = half3(tnormalZ.xy + i.worldNormal.xy, i.worldNormal.z);// 交换切线法线以匹配世界方向并进行三混合half3 worldNormal = normalize( tnormalX.zyx * blend.x + tnormalY.xzy * blend.y + tnormalZ.xyz * blend.z );看起来不错,不是吗?UDN 混合非常流行,因为它非常便宜且有效。
但混合有一个缺点。
由于数学运算方式,法线贴图在超过 45 度的角度会略微压平。
这会导致混合法线中细节的轻微损失。
白化(Whiteout)混合来自那篇文章的白化混合没有 UDN 混合所遇到的问题。
根据那篇文章,它只是稍微贵一些,所以让我们试试。
// 白化混合// 三平面 uvsfloat2 uvX = i.worldPos.zy; // x 面朝平面float2 uvY = i.worldPos.xz; // y 面朝平面float2 uvZ = i.worldPos.xy; // z 面朝平面// 切线空间法线贴图half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));// 将世界法线交换到切线空间并应用白化混合tnormalX = half3( tnormalX.xy + i.worldNormal.zy, abs(tnormalX.z) * i.worldNormal.x );tnormalY = half3( tnormalY.xy + i.worldNormal.xz, abs(tnormalY.z) * i.worldNormal.y );tnormalZ = half3( tnormalZ.xy + i.worldNormal.xy, abs(tnormalZ.z) * i.worldNormal.z );// 交换切线法线以匹配世界方向并进行三混合half3 worldNormal = normalize( tnormalX.zyx * blend.x + tnormalY.xzy * blend.y + tnormalZ.xyz * blend.z );它们非常相似,没有直接比较它们。
在这里,你可以看到 UDN 混合的法线看起来并不错误,但与白化相比,它们看起来略微压平了。
我们有两种完全合理的 triplanar 法线贴图近似值。
两者在灯光方面都没有明显的视觉问题。
两者都足以满足大多数用例。
并且两者都比朴素选项甚至直接法线贴图交换更快。
白化也是轴对齐墙壁的真实情况,就像直接交换技术一样!我怀疑没有人会知道白化不完美,除非他们并排看到它们,即使那样,也很难挑出问题。
GPU Gems 3 中的技术怎么样?它实际上与上面两个着色器做的是同一个想法。
它执行相同法线贴图轴交换,但它丢弃了法线贴图的“z”轴,并使用零代替。
为什么?如果你仔细查看 GPU Gems 3 代码,它实际上与 UDN 混合相同!两者都没有实际使用法线贴图的 z 轴。
它们的实现最终比我编写的 UDN 混合着色器快几条指令,但产生了相同的结果!// GPU Gems 3 混合// 三平面 uvsfloat2 uvX = i.worldPos.zy; // x 面朝平面float2 uvY = i.worldPos.xz; // y 面朝平面float2 uvZ = i.worldPos.xy; // z 面朝平面// 切线空间法线贴图half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));// 将切线法线交换到世界空间并置零“z”half3 normalX = half3(0.0, tnormalX.yx);half3 normalY = half3(tnormalY.x, 0.0, tnormalY.y);half3 normalZ = half3(tnormalZ.xy, 0.0);// 三混合法线并添加到世界法线half3 worldNormal = normalize( normalX.xyz * blend.x + normalY.xyz * blend.y + normalZ.xyz * blend.z + i.worldNormal );GPU Gems 3 混合是这三种着色器中最快的选项。
[³]但使用白化混合的额外成本不太可能明显,即使对于移动 VR 也是如此。
并且对于某些内容和挑剔的艺术家来说,质量差异可能是一个问题。
重新定向法线贴图那么“真实情况”图像我一直在展示呢?我上面链接的“混合细节”文章描述了除了 UDN 和白化混合之外的几种其他方法。
这包括一种他们称为重新定向法线贴图的方法。
这种方法很棒;它最终比 GPU Gems 3 或白化方法贵一点,但非常接近“真实情况”!事实上,本文中所有“真实情况”示例图像都使用了这种技术!它也仍然可能比朴素方法更快,即使它产生了更复杂的着色器。
在“混合细节”中,显示的 RNM 函数对传递给它的法线贴图做了一些假设,这些假设对于 Unity 来说并不成立。
需要对原始代码进行一些小的修改。
在文章的评论中,Stephen Hill 提供了 此示例(http://discourse.selfshadow.com/t/blending-in-detail/21/18) 用于在 Unity 中使用 RNM。
使用该函数,三平面混合着色器看起来像这样。
// 重新定向法线贴图混合// 三平面 uvsfloat2 uvX = i.worldPos.zy; // x 面朝平面float2 uvY = i.worldPos.xz; // y 面朝平面float2 uvZ = i.worldPos.xy; // z 面朝平面// 切线空间法线贴图half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));// 获取法线的绝对值以确保混合的正切线“z”half3 absVertNormal = abs(i.worldNormal);// 将世界法线交换到切线空间并应用 RNM 混合tnormalX = rnmBlendUnpacked(half3(i.worldNormal.zy, absVertNormal.x), tnormalX);tnormalY = rnmBlendUnpacked(half3(i.worldNormal.xz, absVertNormal.y), tnormalY);tnormalZ = rnmBlendUnpacked(half3(i.worldNormal.xy, absVertNormal.z), tnormalZ);// 获取表面法线的符号 (-1 或 1)half3 axisSign = sign(i.worldNormal);// 重新应用符号到 ZtnormalX.z *= axisSign.x;tnormalY.z *= axisSign.y;tnormalZ.z *= axisSign.z;// 三混合法线并添加到世界法线half3 worldNormal = normalize( normalX.xyz * blend.x + normalY.xyz * blend.y + normalZ.xyz * blend.z + i.worldNormal );这是我上面链接的函数。
// Unity3d 的重新定向法线贴图// http://discourse.selfshadow.com/t/blending-in-detail/21/18float3 rnmBlendUnpacked(float3 n1, float3 n2){ n1 += float3( 0, 0, 1); n2 *= float3(-1, -1, 1); return n1*dot(n1, n2)/n1.z - n2;}我在那里说“用于 Unity3d”,但这应该适用于任何已被解压缩到归一化 -1 到 1 范围内的切线空间法线贴图。
“真实情况”追寻真相除了重新定向法线贴图之外,它仍然不是真实情况。
以上所有方法仍然是近似值。
对于那些吹毛求疵的读者,我故意对“真实情况”使用了引号,因为它实际上并非如此。
问题是三平面法线贴图的真实定义很难确定。
有人可能会认为,任何一个投影平面本身看起来应该与使用具有相同 UV 投影的烘焙切线的网格看起来一样。
就像任何基本地形着色器会做的那样。
但这种方法也不正确,这种投影方式是对切线空间法线贴图的错误使用。
只有当三平面贴图应用于轴对齐的盒子时,它才能被认为是真正的真实情况!我在上面描述切线空间法线贴图的工作方式时也撒了点小谎。
特别是副切线,我大多忽略了它。
我将其描述为切线纹理示例中的“绿色箭头”。
这并不完全正确。
对于切线空间法线贴图,副切线定义为法线和切线的叉积。
切线和副切线都应该垂直于法线,并且相互垂直。
如果你查看三平面切线纹理示例,你就会看到切线和副切线箭头开始倾斜,最终在角落变成三角形。
结果是切线和副切线垂直于法线,但不相互垂直。
为了正确并匹配切线空间的计算方式,将消除副切线的这种倾斜以及由此产生的切线空间矩阵。
结果是,当使用正确的切线空间时,法线方向将表现出看起来像是轻微旋转的方向。
当使用倾斜的切线空间时,会出现不同的旋转。
问题是,任何这种投影法线贴图在技术上都是错误的。
所使用的法线贴图不是用我们显示它们的相同切线空间生成的。
它们可能是为完全平坦的平面生成的。
法线贴图应该只与它们最初计算的几何体和切线空间一起使用。
这意味着在地形上使用重复纹理和法线贴图,或者作为细节法线,或者用于三平面贴图,是一种错误的使用方式。
因此,对于这些情况没有真正的真实情况。
当然,我们经常使用这种法线贴图,而且没有人真正注意到有什么不对劲。
因此,它归结为决定你试图达到的真实情况是什么。
最基本的是,你需要决定法线贴图应该代表什么。
它是否代表与纹理投影对齐的表面位移产生的法线,还是表面法线?如果是投影的位移,那么白化混合更接近真实情况!如果是沿表面法线的位移,那么 RNM 是更接近的方法。
对我来说,RNM 保留了更多法线贴图的细节,并且总体看起来更好。
最终,它取决于个人喜好和性能。
除了朴素方法。
那总是错的。
三平面法线贴图的其他技术上面描述的方法的主要优点是它们不需要进行矩阵旋转来在切线空间和世界空间之间转换,反之亦然。
但是,你可能仍然需要切线空间向量,例如用于视差贴图技术。
那么如何获得它们呢?屏幕空间偏导数可以使用屏幕空间偏导数重建切线空间旋转矩阵。
这个想法来自于 导数映射(http://www.rorydriscoll.com/2012/01/11/derivative-maps/),它是由 Morten Mikkleson(http://mmikkelsen3d.blogspot.com/2011/07/derivative-maps.html) 推广的。
这种技术被错误地称为“没有切线的法线贴图”,但它与切线空间法线贴图不同。
对于三平面贴图来说,这是一个很好的选择,并且有很多好处。
为了本文的目的,我不会深入讨论导数映射。
如果你想深入研究这个兔子洞,请尝试上面两个链接以及这个链接(其中包含指向另外两个链接的链接):三平面http://therobotseven.com/devlog/triplanar-texturing-derivative-maps/但是,你可以使用屏幕空间偏导数来帮助切线空间法线贴图。
你可以使用它们在片段着色器中重建任意投影的切线到世界旋转矩阵。
然后,可以将其应用于每个侧面的法线贴图。
Christian Schüler 在这篇文章中将此矩阵称为余切线框架,因为它与来自通常切线空间的矩阵并不完全相同。
http://www.thetenthplanet.de/archives/1180// 来自屏幕空间偏导数的余切线框架// 三平面 uvsfloat2 uvX = i.worldPos.zy; // x 面朝平面float2 uvY = i.worldPos.xz; // y 面朝平面float2 uvZ = i.worldPos.xy; // z 面朝平面// 切线空间法线贴图half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));// 归一化表面法线half3 vertexNormal = normalize(i.worldNormal);// 为每个平面计算余切线框架half3x3 tbnX = cotangent_frame(vertexNormal, i.worldPos, uvX);half3x3 tbnY = cotangent_frame(vertexNormal, i.worldPos, uvY);half3x3 tbnZ = cotangent_frame(vertexNormal, i.worldPos, uvZ);// 应用余切线框架并三混合法线half3 worldNormal = normalize( mul(tnormalX, tbnX) * blend.x + mul(tnormalY, tbnY) * blend.y + mul(tnormalZ, tbnZ) * blend.z );这是为在 Unity 中使用而修改的 cotangent_frame() 函数。
// http://www.thetenthplanet.de/archives/1180 的 Unity 版本float3x3 cotangent_frame(float3 normal, float3 position, float2 uv){ // 获取像素三角形的边向量 float3 dp1 = ddx( position ); float3 dp2 = ddy( position ) * _ProjectionParams.x; float2 duv1 = ddx( uv ); float2 duv2 = ddy( uv ) * _ProjectionParams.x; // 求解线性系统 float3 dp2perp = cross( dp2, normal ); float3 dp1perp = cross( normal, dp1 ); float3 T = dp2perp * duv1.x + dp1perp * duv2.x; float3 B = dp2perp * duv1.y + dp1perp * duv2.y; // 构造一个尺度不变框架 float invmax = rsqrt( max( dot(T,T), dot(B,B) ) ); // 矩阵是转置的,使用 mul(VECTOR, MATRIX) return float3x3( T * invmax, B * invmax, normal );}我不一定会建议将此选项用于生产三平面贴图,因为它比上面的选项更昂贵,尽管它也可以被认为非常接近“真实情况”的版本。
它也有一些问题,其中几何体的多边形形状可能在法线中变得明显,作为硬边。
这是因为切线是从实际几何体表面而不是从平滑插值的值计算出来的。
这是否是一个问题取决于你使用的漫反射、法线贴图和几何体。
顺便说一句,你可以利用这一点来获得低多边形风格的刻面表面法线。
half3 normal = normalize(cross(dp1, dp2));Christian Schüler 在那篇文章中关于法线贴图的一般性问题,提出了一些有趣的思考点。
他比我更详细地介绍了我们如何以错误的方式处理切线空间法线贴图。
叉积切线重建如何复制在网格上计算切线的方式?即使我提到这不是正确的,也可以做到。
Unity 的地形着色器使用与在顶点着色器中计算切线非常相似的技术。
以下是它在片段着色器中完成的方式。
// 切线重建// 三平面 uvsfloat2 uvX = i.worldPos.zy; // x 面朝平面float2 uvY = i.worldPos.xz; // y 面朝平面float2 uvZ = i.worldPos.xy; // z 面朝平面// 切线空间法线贴图half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));// 获取表面法线的符号 (-1 或 1)half3 axisSign = sign(i.worldNormal);// 为每个轴构建切线到世界矩阵half3 tangentX = normalize(cross(i.worldNormal, half3(0, axisSign.x, 0)));half3 bitangentX = normalize(cross(tangentX, i.worldNormal)) * axisSign.x;half3x3 tbnX = half3x3(tangentX, bitangentX, i.worldNormal);half3 tangentY = normalize(cross(i.worldNormal, half3(0, 0, axisSign.y)));half3 bitangentY = normalize(cross(tangentY, i.worldNormal)) * axisSign.y;half3x3 tbnY = half3x3(tangentY, bitangentY, i.worldNormal);half3 tangentZ = normalize(cross(i.worldNormal, half3(0, -axisSign.z, 0)));half3 bitangentZ = normalize(-cross(tangentZ, i.worldNormal)) * axisSign.z;half3x3 tbnZ = half3x3(tangentZ, bitangentZ, i.worldNormal);// 应用切线到世界矩阵并进行三混合// 使用 clamp() 因为叉积可能为 NANhalf3 worldNormal = normalize( clamp(mul(tnormalX, tbnX), -1, 1) * blend.x + clamp(mul(tnormalY, tbnY), -1, 1) * blend.y + clamp(mul(tnormalZ, tbnZ), -1, 1) * blend.z );这与在顶点着色器中计算并使用插值值并不完全相同。
但正如我们所讨论的,那也是错误的,所以差异实际上不是问题。
三平面混合混合出细节如果你曾经使用过三平面贴图,你可能熟悉一段看起来像这样的代码:float3 blend = abs(normal.xyz);blend /= blend.x + blend.y + blend.z;进行除法的目的是归一化总和。
着色器中的 normalize() 函数返回一个具有归一化幅度的向量,但我们希望向量的分量具有归一化总和为 1.0。
如果我们不对其进行任何操作,则对于“归一化”的 float3 来说,总和基本上总是会大于 1.0,介于 1.0 和 1.7321 之间。
因此,如果你使用法线的绝对值来混合漫反射纹理,则角点会过于明亮。
除以总和可以确保新的总和始终为 1.0。
两个纹理球体都没有光照,但左侧球体由于混合加起来超过 1.0 而过于明亮现在,这种基本混合非常柔和,因此下一步通常是找出某种方法来锐化混合。
以下是经常看到的内容。
float3 blend = abs(normal.xyz);blend = (blend - 0.2) * 7.0;blend = max(blend, 0);blend /= blend.x + blend.y + blend.z;这有助于稍微锐化,但这是一段奇怪的代码,因为 * 7.0 是无用的!将 7.0 更改为任何非零数字或完全删除它都不会产生任何影响,但这段代码似乎出现在我看到的三平面实现中的一半。
稍微思考一下就能解释为什么它是多余的;一个数字除以自身总是为 1,并且先乘以某个非零值并不会改变这一点。
据我所知,这段代码似乎来自我之前提到的 GPU Gems 3 文章!不幸的是,文章中错误的部分似乎是重用最多的部分。
但是,- 0.2 是可以的,只需省略后面的乘法即可。
还可以通过滥用点积来对分量求和,从而在除法之前进行优化。
float3 blend = abs(normal.xyz);blend = max(blend - 0.2, 0);blend /= dot(blend, float3(1,1,1));这是一个小的优化,原始版本使用 6 条指令,优化后的版本使用 4 条指令。
但没有理由不这样做,因为结果是相同的,并且它删除了一个无用的神奇数字。
这相当小,但如果你的几何体主要是平坦的墙壁,那就足够了。
但是,如果我们想要更锐利的过渡呢?你可以增加减去的数值,但角点在其他部分之前会变得明显更锐利,而且如果数值过高,它就会开始变黑。
在出现明显问题之前,-0.55 是你可以达到的最高值。
这是因为在角点处,归一化向量为 (0.577, 0.577, 0.577),因此减去超过该值会导致 max() 函数将角点变为 (0.0, 0.0, 0.0),然后你不仅会丢失值,而且还会除以零!GPU Gems 3 风格的混合锐化,使用不同的值但是,我更喜欢另一种技术;使用指数。
如果你对固定的混合锐度感到满意,使用硬编码的 4 次幂与上面的优化选项一样快,而且在我看来,看起来更好。
float3 blend = pow(normal.xyz, 4);blend /= dot(blend, float3(1,1,1));这是一个细微的差异,但它与 4 条指令的成本相同。
如果你想使用材质属性来更好地控制混合锐度,这会更昂贵。
Unity 会将其显示为 5 条指令,但实际上它更像是 11 条指令。
其他硬编码的幂将更少(每增加 2 的幂增加 1 条指令,因此 pow(normal, 8) 是 5 条指令,16 是 6 条指令,等等)。
使用不同的指数锐化混合对于砖块来说,较低的幂可能仍然看起来不太好,但我故意使用“最坏情况”纹理来使混合变得明显。
对于不同的纹理和光照来说,稍微柔和的混合可能是有益的。
还有一些更高级的非对称混合形状也可以完成,这些形状可能更适合某些纹理或用例。
// 非对称三平面混合float3 blend = 0;// 只混合侧面float2 xzBlend = abs(normalize(normal.xz));blend.xz = max(0, xzBlend - 0.67);blend.xz /= max(0.00001, dot(blend.xz, float2(1,1)));// 混合顶部blend.y = saturate((abs(normal.y) - 0.675) * 80.0);blend.xz *= (1 - blend.y);如果你有纹理的高度数据,那么高度混合甚至更好。
有些人作弊,只是使用纹理亮度,这可以作为某些纹理高度的良好近似值。
// 高度贴图三平面混合float3 blend = abs(normal.xyz);blend /= dot(blend, float(1,1,1));// 每个平面的纹理的高度值。
这通常// 被打包到另一个纹理中,或者(不太理想地)作为单独的// 纹理。
float3 heights = float3(heightX, heightY, heightZ) + (blend * 3.0);// _HeightmapBlending 是 0.01 到 1.0 之间的值float height_start = max(max(heights.x, heights.y), heights.z) - _HeightmapBlending;float3 h = max(heights - height_start.xxx, float3(0,0,0));blend = h / dot(h, float3(1,1,1));其中的神奇 blend * 3.0 用于缩减混合区域。
这会产生类似 Gems 3 的混合形状。
高度贴图混合通常在较大的初始混合区域但具有钳位边缘的情况下效果最佳。
仅使用幂方法过于平滑,会导致混合显示出糟糕的纹理拉伸。
上面的高度混合代码大致基于这篇文章中的方法。
高度混合着色器 by Octopoidhttp://untitledgam.es/2017/01/height-blending-shader/附加思考镜像 UV使用基本的三平面 UV 计算会导致某些侧面的纹理镜像。
大多数情况下,这本身并不是什么大问题。
但是,它会导致某些情况下,两个平面的角点在大致相同的 UV 处显示相同的纹理,从而使这种镜像更加明显。
不过,解决起来并不困难。
最简单的解决方案是稍微偏移所有 3 个平面,以降低完美重叠的可能性。
将 0.33 添加到 Y 平面的 UV 以及 0.67 添加到 Z 平面的 UV 即可。
但是,如果你希望纹理对齐,这可能会导致不希望的对齐问题。
或者,你也可以翻转 UV。
如果你使用具有明显方向的纹理(如文本),这将非常有用。
将 UV 的 x 分量乘以表面法线的轴符号,然后对切线空间法线贴图执行相同的操作以撤消翻转。
// 投影 UV 翻转// 三平面 uvsfloat2 uvX = i.worldPos.zy; // x 面朝平面float2 uvY = i.worldPos.xz; // y 面朝平面float2 uvZ = i.worldPos.xy; // z 面朝平面// 获取表面法线的符号 (-1 或 1)half3 axisSign = sign(i.worldNormal);// 翻转 UV 以纠正镜像uvX.x *= axisSign.x;uvY.x *= axisSign.y;uvZ.x *= -axisSign.z;// 切线空间法线贴图half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX));half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY));half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ));// 翻转法线以纠正翻转的 UVtnormalX.x *= axisSign.x;tnormalY.x *= axisSign.y;tnormalZ.x *= -axisSign.z;当然,如果你愿意,可以同时使用翻转和偏移。
Unity 表面着色器Unity 的表面着色器在这种情况下会导致很多麻烦。
目前无法告诉表面着色器不要将网格的切线到世界变换应用于 surf() 函数输出的法线。
结果是,你必须手动将世界空间法线变换到切线空间,或者手动修改生成的着色器代码。
将法线变换到切线空间是可能的,但没有必要地昂贵。
手动修改生成的代码很麻烦。
我个人选择是手动编写顶点片段着色器,这些着色器遵循与表面着色器相同的通用结构,但格式稍微合理一些。
如果 Unity 提供了在表面着色器中完全跳过切线空间的选项,那就太好了。
这里有一个功能性表面着色器示例,它在 surf 函数中将世界法线变换到切线空间:https://github.com/bgolus/Normal-Mapping-for-a-Triplanar-Shader/blob/master/TriplanarSurfaceShader.shader用于其他渲染器(非 Unity)的三平面法线上面讨论的技术可以在任何渲染器中使用,无论是实时渲染器还是其他渲染器。
诀窍是理解该渲染器的世界坐标系和法线贴图方向。
Unity 是一个左手系、Y 向上的世界空间坐标系,并且使用 +Y 法线贴图。
由此产生的结果是,Z 平面的许多行都添加了负号,而其他行则没有。
如果 Unity 是右手系并且 Y 向上,则不需要这些。
使用 -Y 法线贴图的渲染器可能需要添加更多带负号的行,因为世界空间方向和法线贴图的 Y 轴对于许多(如果不是所有)平面来说将被反转!这是三平面法线贴图中会困扰大多数人的一个重大问题。
只要知道坐标系和法线贴图方向,就可以解决这个问题,但即使那样也很容易出错。
尝试一次查看一个面,然后翻转符号,直到看起来正确,然后检查反面!一开始会经历很多尝试和错误。
把它看作是科学方法的应用!¹ Unreal Engine 和其他一些工具使用所谓的 -Y 法线贴图。
这意味着绿色通道与 Unity 和其他 +Y 工具使用的通道相反。
在这种情况下,绿色表示它有多“下”。
最初的差异来自 OpenGL 和 DirectX 的 UV 处理约定。
在 OpenGL 中,UV 位置“0.0, 0.0”位于左下角,而在 DirectX 中,它位于左上角。
这意味着默认情况下,UV 流动对于 OpenGL 来说是“向上和向右”,而对于 DirectX 来说是“向下和向右”。
最终,使用你最习惯的约定并在着色器中翻转法线贴图是微不足道的。
由于 Unity 必须同时支持 OpenGL 和 DirectX,因此它实际上会将 DirectX 平台上的纹理上下颠倒!² 副切线与副法线。
大多数文献和着色器会将切线空间的三个向量称为法线、切线和副法线。
“副切线向量”可以说是对第三个向量的更准确的描述性名称,因为它是一个第二个表面切线。
切线是与曲线或曲面相切但不相交的直线。
反对“副切线”一词的人指出,它在数学领域已经有一个特定的、无关的含义。
副切线传统上是指一条直线,它是曲线上的两个点的切线。
因此,很明显,这与我们的目的毫不相关。
但是,从技术上讲,副法线也具有先前的含义,尽管它更接近于我们使用它的方式。
副法线是曲线的法线向量和切线向量的叉积。
但是,他们指的是明确的无限细的直线,而不是曲面。
如果你将法线视为指向远离曲面的方向,则直线法线是任何垂直于切线的方向。
因此,副法线在那里是一个好名字,因为它又是直线的另一个法线。
对于曲面来说,副法线这个词没有意义,因为它不再垂直于曲面,因此不再是法线。
这就是副切线变得有吸引力的原因,因为它模仿了副法线中 bi- 前缀的使用,但更准确地将其描述为它所代表的附加切线。
因此,基本上,两者都是错误的。
如果你深入研究,即使“切线”这个词在这里也是稍微错误的,因为它被用来描述一个单位向量,它具体地既是表面切线,又是纹理坐标的 x 导数,而 x 导数本身就是一个切线。
因此,它是一条与两条曲线相切的单条直线,那么也许切线应该称为副切线?而副法线/副切线应该称为副副切线?或者余切线?交叉副切线?弗雷德?…你可以看到这个争论可以持续一段时间。
给事物命名很难。
我个人更倾向于副切线而不是副法线。
无论哪种方式,重要的是要理解,在切线空间法线贴图的上下文中,两者都被用来指代同一件事。
³ 在台式机上,GPU Gems 3 和 UDN 混合比白化混合便宜很多,尽管白化混合的代码看起来并没有做更多的事情。
这是因为 Gems 3 混合不需要使用法线贴图的 z 分量。
在大多数现代游戏引擎中,法线贴图只存储 x 和 y 分量,并重建 z 分量。
Unity 的 UnpackNormal() 函数的一部分就是进行 z 分量重建。
由于 Gems 3 不使用 z,因此着色器可以跳过重建。
这使得 GPU Gems 3 混合三平面着色器比白化节省了大约 14 条指令。
这似乎表明 Gems 3 混合着色器将是移动设备的明确选择,但事实并非如此。
在移动设备上,Unity 会存储所有 3 个分量的法线贴图,而不是重建 z。
这样做是为了避免支付重建的成本,并以轻微的视觉质量下降为代价节省一些纹理内存。
结果是,Gems 3 和白化着色器在移动设备上的性能几乎相同。
在某些情况下,白化混合甚至可能稍微快一些,具体取决于着色器编译器选择如何优化。
如果喜欢今天的文章,请多点点赞和在看,后续就会有更多此类的文章~

  • 收藏

分享给我的朋友们:

上一篇:人工智能+三色激光,Vidda发布电视投影新品(三色激光电视上市了吗) 下一篇:[太原沐林装饰]新房装修预算不足怎么办?装修如何省钱呢?(太原沐林装饰)

一键免费领取报价清单 专享六大服务礼包

装修全程保障

免费户型设计+免费装修报价

已有312290人领取

关键字: 装修设计 装修公司 别墅装修设计

发布招标得免费设计

申请装修立省30%

更多装修专区

点击排行