NSDT工具推荐Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - AI模型在线查看 - Three.js虚拟轴心开发包 - 3D模型在线减面 - STL模型在线切割 - 3D道路快速建模

在我编写着色器的过程中,我一直试图编写一个特定的着色器。但我总是失败,原因我永远无法完全理解。我会等几年,用我的新知识水平再试一次,更接近目标,然后再次失败。那个目标?

制作一个栅格着色器,模仿基于纹理的网格,但看起来比它更好。

注意:我强烈建议在暗模式下查看本文。如果在桌面上,请在新选项卡中打开图像,以便你可以在深色背景下查看它们。

1、竞争

Con-Text-ure(基于纹理的网格)

这是使用我在关于更清晰的 Mipmapping 的文章中的旋转网格超级采样,以及 16x 各向异性过滤。这基本上和简单的基于纹理的网格一样好,同时仍然具有合理的性能。

编写网格着色器似乎很简单。在着色器中制作网格线是您在着色器教程中看到的早期内容之一。那么为什么我这么长时间以来一直痴迷于此?因为它比看起来更难做到,而且我知道基于着色器的解决方案看起来会更好。真的就是这样。我只是想更好地理解问题空间。

让我们仔细看看上面的基于纹理的网格,看看仍然出现的一些问题区域。

RGSS 纹理网格瑕疵

如您所见,一些细线最终仍会出现混叠,在远中距离范围内会出现一些混叠和莫尔条纹,而在远距离范围内,线条最终会变得更粗,然后随着各向异性过滤的失效而提前切断。如果您的纹理足够大,它们也只有在近距离时才会保持清晰,否则它们会开始变得有点模糊。

RGSS 纹理网格在极端特写下模糊
  • MyFirstGrid.shader(通用网格着色器)

那么,这些教程网格着色器呢?从重复 UV 开始。使用 smoothstep() 和 fwidth() 绘制一些线条。我们就完成了!

对吧?(别担心,我稍后会展示代码。)

但有一个问题。大多数示例网格着色器(如本例)都使用屏幕空间宽度线。在很多用例中,这比基于纹理的网格更受欢迎,而且说实话,这可能是大多数人想要的。但这不是我想要实现的。随着这种网格的线条延伸到远处,最终每个网格单元的宽度都小于一个像素,因此线条会汇聚成与线条颜色相同的纯色。

这不是基于纹理的网格所发生的情况。对于基于纹理的网格,线条本身具有透视性,距离越远,线条越细。最终一旦低于像素宽度就会消失。

RGSS 纹理网格与恒定像素宽度线网格

它们都汇聚成纯色,但基于纹理的网格会汇聚成与网格单元区域的线条覆盖率相关的颜色。

RGSS 纹理网格与恒定像素宽度线网格

更不用说一旦网格小于一个像素,就会出现明显的莫尔条纹。

我过去见过的大多数示例着色器都试图绘制恒定世界空间或 UV 空间宽度的线条,但实际上并没有正确处理这个问题。它们通常使用 UV 空间褪色边缘或根本不使用线条抗锯齿,这两种方法最终都会在远处产生可怕的锯齿。或者它们会作弊并在任意距离处淡出线条以隐藏伪影。而那些不淡出线条的着色器最终看起来就像远处恒定像素宽度的线网格。只是锯齿更严重,莫尔条纹更明显。

恒定 UV 宽度线网格

这些都与基于纹理的网格的外观不符。尽管它至少与各个线条本身的透视图基本匹配。

  • Choo Choo!(滤波脉冲序列)

但是有一些现有的例子看起来可以正确解决这个问题。最近有人向我指出了一个,但它已经存在了很长时间,比我编写着色器的时间要长得多。这项技术发表在 Apodaca、Anthony A. 和 Larry Gritz 编辑的 1999 年《高级 RenderMan:为电影创建 CGI》中。后来在 RenderMan 的文档中。滤波脉冲序列。

这项技术旨在解决我一直在尝试解决的确切问题。他们分析性地解决了卷积脉冲序列的积分。如果你像我一样,没有完成大学水平的数学课程,那这绝对没有任何意义。我从艺术学校辍学了,所以这基本上超出了我的理解范围。

简而言之,该函数返回任意范围内线与非线的比率。而且它工作得非常好。与基于纹理的网格相比,它在处理淡入远处的方式方面几乎完美匹配。

至少乍一看如此。仔细检查会发现一些问题。

滤波脉冲序列网格伪影

虽然它与基于纹理的网格的感知亮度相匹配,并且前景中没有混叠,但中远距离的混叠和莫尔条纹明显更糟糕。基本上所有可见的线抗锯齿都消失了。这比完全没有抗锯齿要好,莫尔条纹比像素和 UV 宽度线网格更不明显。但这仍然没有我期望的那么干净。

有趣的是,书中有这样一条注释:

... 最严重的混叠消失了。

最严重的,但不是全部。我不得不假设原作者知道它并没有消除所有混叠,但对结果很满意,没有进一步研究。而后来使用它的人也并不在意,或者只是没有仔细观察而没有注意到?

  • Hi IQ(盒式过滤网格)

Inigo Quilez 在其关于可过滤程序的文章中也给出了一个示例,即盒式过滤网格。

盒式过滤网格功能确实解决了过滤脉冲序列的一些问题,主要是它对精度非常敏感,因此开始在距离原点不远的地方显示噪声伪影。但它们在其他方面的表现大致相同。这包括中远距离的相同混叠问题。

尽管它们在混叠和莫尔条纹方面略有不同。

现在,虽然我从高层次上理解了这两个着色器的工作原理,但我的数学能力还不够强,无法理解如何修改它们以获得我想要的结果。

2、竞争者

实际上,我希望网格着色器提供什么功能?我想要:

  • 用户可配置的线宽。
  • 具有透视厚度的线条,而不仅仅是恒定的像素宽度。
  • 在任何距离或视图方向都完全没有混叠。
  • 线宽 0.0 或 1.0 应显示完全隐藏或填充。
  • 有限的莫尔干涉图案。
  • 在距离上与基于纹理的网格混合到相同的值。
  • 可用于实时渲染,代替替代技术。

所以,我回到了我熟悉的着色器,即像素和 UV 宽度线网格。然后决定开始研究它们,看看我可以改变什么来让它按照我想要的方式工作。或者更确切地说,从一条线开始,然后从那里开始构建。

让我们快速概述一下什么是具有用户可配置线宽的基本网格着色器。

首先,我们需要绘制一条线。

2.1 第一行,开始(线着色器)

我喜欢使用 smoothstep() 函数来绘制线条。

float lineUV = abs(uv.x * 2.0);
float line = smoothstep(lineWidth + lineAA, lineWidth - lineAA, lineUV);

UV 用作渐变。然后我在 UV 上使用 abs(),这样渐变在 0.0 的两侧都是正值,因此 smoothstep() 应用于两侧,我们得到一条线,而不仅仅是一条边。为什么我要将 UV 乘以 2?这样 lineWidth 和 lineAA 就可以指定为总宽度,而不是半宽度,或者需要将它们除以 2。

现在让我们使用世界位置作为 UV,并为 lineWidth 和 lineAA 使用一些任意值。这样我们就可以得到:

基本线

这个问题是抗锯齿在远处失效,在前景变得模糊。为什么?因为边缘渐变的宽度需要根据与相机的角度和距离而变化。为此,我们可以使用我最喜欢的工具之一,屏幕空间偏导数。我在之前的文章中写过几次。简短的解释是,您可以获得一个像素和它旁边的像素之间的值变化量,无论是垂直还是水平。通过获取起始 UV 的偏导数,我们可以知道 smoothstep() 在 UV 空间中需要多宽才能显示 1 个像素宽。

float lineAA = fwidth(uv.x); //
float lineUV = abs(uv.x * 2.0);
float line = smoothstep(lineWidth + lineAA, lineWidth - lineAA, lineUV); //
抗锯齿线

现在线条的边缘很漂亮,很锐利。请注意,我在对 UV 进行任何修改之前获取了它们的导数。这样可以保持它们处于“全宽”比例,还可以避免下一步中的一些问题。

让我们将其设为重复线,而不仅仅是一条线。

float lineAA = fwidth(uv.x);
float lineUV = 1.0 - abs(frac(uv.x) * 2.0 - 1.0); //
float line = smoothstep(lineWidth + lineAA, lineWidth - lineAA, lineUV);
重复的抗锯齿线

为了解释我对 UV 执行的那段古怪的代码,这是将锯齿波转换为三角波,然后确保零点与之前的位置对齐。

我们从这样的 lineUV 开始:

abs(uv.x * 2.0)

使用 frac(uv.x) 可以得到以下结果:

frac(uv.x)

然后 abs(frac(uv.x) * 2.0 - 1.0) 会得到以下结果:

abs(frac(uv.x) * 2.0–1.0)

但是“0.0”的位置从 1.0 开始,而不是从 0.0 开始,所以当我们绘制线条时,它们会偏移半个周期。所以我们在开头添加 1.0 - 得到以下结果:

1.0-abs(frac(uv.x) * 2.0–1.0)

现在,当我们绘制线条时,“第一条”线条的位置与我们之前绘制的那条线条相匹配。

现在,让我们将其变成一个完整的网格。为此,我们只需对 UV 的两个轴执行这些步骤,然后合并结果。

float2 lineAA = fwidth(uv);
float2 gridUV = 1.0 - abs(frac(uv) * 2.0 - 1.0);
float2 grid2 = smoothstep(lineWidth + lineAA, lineWidth - lineAA, gridUV);
float grid = lerp(grid2.x, 1.0, grid2.y); //

这样我们就得到了一个基本的 UV 宽度线网格着色器!

lerp(grid2.x, 1.0, grid2.y) 可能需要解释一下。如何将两个重复线轴组合起来用于网格着色器,这一点让我困惑了很久。我会使用 max(x, y) 或 saturate(x + y),或者其他几种方法来组合它们,但我觉得它们不太合适。我花了很长时间才从“我通常如何重叠两个透明的东西?”的角度考虑这个问题。我会使用 alpha 混合。在这种情况下,lerp() 相当于预乘的 alpha 混合,您也可以这样写:

float grid = grid2.x * (1.0 - grid2.y) + grid2.y;

或者,如果您编写着色器,使白背景上有黑线,则将轴相乘也会产生与预乘混合等效的效果。请注意,与第一个示例相比,以下示例中 smoothstep() 中的 + 和 - 被交换了。

float2 lineAA = fwidth(uv);
float2 gridUV = 1.0 - abs(frac(uv) * 2.0 - 1.0);
float2 grid2 = smoothstep(lineWidth - lineAA, lineWidth + lineAA, gridUV); //
float grid = 1.0 - grid2.x * grid2.y; //

但是我将继续使用原始代码示例,因为它们最终完全相同。

回想起来,使用预乘混合感觉非常明显,但由于某种原因,花了十多年时间才实现。这是在我为其他用例编写了无数完全相同的着色器之后。我甚至就这个确切主题写了一整篇文章。

无论如何,使用这段代码,我们得到了这个:

抗锯齿网格

除了莫尔条纹外,看起来还不错。但我们预料到了这一点。现在让我们将线宽稍微减小一点,使其更接近实际使用的范围。

“抗锯齿”网格

哦,当线条靠近相机时看起来不错。但是线条很快就开始出现锯齿。我在本文前面展示恒定 UV 宽度线网格时看到了这些问题,但这看起来比原始示例略暗且锯齿更多。为什么?

最新示例与之前示例

好吧,因为两者使用的代码之间有一个小差异。使用 smoothstep() 时,我使用 1.5 像素宽的 AA。这样做的原因是 smoothstep() 锐化了用于抗锯齿的边缘渐变,使得 1.5 像素宽的 smoothstep 具有与 1 像素宽的线性渐变大致相似的斜率。

线性斜率 vs 1.5 个单位宽的平滑步长

1 像素宽的平滑步长可能太锐利。使用平滑步长的原因在于,使用 1.5 像素宽的平滑步长只会增加一点点额外的抗锯齿效果,而不会影响线条相对于 1 像素宽的线性渐变的感知锐利度。

公平地说,这是一个非常小的差异。但 HLSL 的 ssmoothstep() 仍然很好,因为它还充当逆 lerp(又名重映射)并将值限制在 0.0 和 1.0 之间。因此它有助于简化代码。它仍然不能完全消除感知到的混叠,但我们会回到这个问题上。

最后,我们有这个用于恒定 UV 宽度网格的着色器代码:

float2 uvDeriv = fwidth(uv); //
float2 lineAA = uvDeriv * 1.5; //
float2 gridUV = 1.0 - abs(frac(uv) * 2.0 - 1.0);
float2 grid2 = smoothstep(lineWidth + lineAA, lineWidth - lineAA, gridUV);
float grid = lerp(grid2.x, 1.0, grid2.y);

那么,像素宽度恒定的线网格呢?嗯,这是一个微不足道的变化。将线宽乘以导数!(只需记住,lineWidth 现在是线宽的像素数,而不是 0.0 到 1.0 之间的值。)

float2 uvDeriv = fwidth(uv);
float2 drawWidth = uvDeriv * lineWidth; //
float2 lineAA = uvDeriv * 1.5;
float2 gridUV = 1.0 - abs(frac(uv) * 2.0 - 1.0);
float2 grid2 = smoothstep(drawWidth + lineAA, drawWidth - lineAA, gridUV); //
float grid = lerp(grid2.x, 1.0, grid2.y)

现在我们回到本文前面的内容。有两个着色器可以满足我的至少两个要求。但我们还没有解决任何我们还不知道如何解决的问题,一个只有透视线,另一个解决了大部分锯齿问题。

所以让我们暂时专注于一条线,而不是整个网格。我们如何才能让一条线既有透视厚度又没有锯齿?

2.2 电话线 AA (Phone-wire AA)

我最喜欢的抗锯齿线技巧之一来自 Emil Persson。特别是他的电话线 AA 示例

这种技术背后的基本思想是不要让某物变得比一个像素更细。相反,限制物体的尺寸,使其保持至少一个像素宽,然后淡出。这最终看起来比让一条线变得比一个像素更细要好得多,因为如果你这样做,它总是会产生混叠。两个神奇之处是你如何让物体保持一个像素宽,更重要的是你将它们淡出多少。

在 Emil Persson 的例子中,他利用有关线几何的宽度、每个顶点与相机的距离以及相机的投影矩阵的知识来保持线的宽度为一个像素。但对于这个着色器,我们可以再次使用这些偏导数!我们只需要限制线在屏幕空间中的细度。基本上,我们结合我们已经拥有的两个着色器,并取 UV 线宽和 UV 导数的最大值。

float uvDeriv = fwidth(uv.x);
float drawWidth = max(lineWidth, uvDeriv); //
float lineAA = uvDeriv * 1.5;
float lineUV = abs(uv.x * 2.0);
float line = smoothstep(drawWidth + lineAA, drawWidth - lineAA, lineUV);
像素宽度限制线

这是第一个技巧。但第二个技巧很重要。我们根据我们想要的线条粗细除以我们绘制的线条粗细来淡化线条。

float uvDeriv = fwidth(uv.x);
float drawWidth = max(lineWidth, uvDeriv);
float lineAA = uvDeriv * 1.5;
float lineUV = abs(uv.x * 2.0);
float line = smoothstep(drawWidth + lineAA, drawWidth - lineAA, lineUV);
line *= saturate(lineWidth / drawWidth); //
电话线 AA 线

看起来不错!即使线很细,你也能看清线的透视。而且远处没有混叠!

电话线 AA 线

值得注意的是,这也解决了当预期线宽为零时线不会完全消失的问题!它会优雅地淡出越来越细的线,就像它向远处退去时一样,最终在达到零时完全消失。

有了这些,让我们再次回到完整的网格。

float2 uvDeriv = fwidth(uv);
float2 drawWidth = max(lineWidth, uvDeriv); //
float2 lineAA = uvDeriv * 1.5;
float2 gridUV = 1.0 - abs(frac(uv) * 2.0 - 1.0);
float2 grid2 = smoothstep(drawWidth + lineAA, drawWidth - lineAA, gridUV);
grid2 *= saturate(lineWidth / drawWidth); //
float grid = lerp(grid2.x, 1.0, grid2.y);

好多了!…… 有点。看起来不太对劲。地平线处逐渐变黑!提醒一下,基于纹理的网格逐渐变成灰色,而不是黑色。

问题是,在网格中,一条线只能达到一定宽度,否则会超过整个网格单元的宽度。当它本身就是一条线时,这不是问题。但是当它被绘制为网格时,在变黑的区域中,单个像素比多个重叠网格宽度更宽。但我们仍然只在每个像素中绘制一组线,而不是多个网格单元。

我在编写这些着色器时陷入了很长一段时间,不知道下一步该怎么做。我花了很长时间试图弄清楚如何正确计算淡化线条的值,但似乎没有什么能真正正确地解决这个问题。我相信这是可以解决的,但还记得我说过我是艺术学校的辍学生吗?是的,我不是那个想解决这个问题的人。我走这条路是因为我没有足够的数学知识来以“正确”的方式去做。

我最接近的方法是尝试将线宽除以的值限制为最大值 1.0。我的理论是,如果线宽不能超过一个像素,则除以的值不要超过 1。虽然这样更好,但仍然不正确。

grid2 *= saturate(lineWidth / max(1.0, drawWidth));

这是非常微妙的,但这会导致在单独可区分的线条和地平线上的大部分纯色之间的过渡处出现一点黑暗的“排水沟”。

如前所示,滤波脉冲序列和盒式滤波网格确实解决了这个问题。不是通过精确淡化线条,而是通过始终计算当前像素覆盖范围内所有可能线条的总覆盖范围。但正如我所展示的,它们都无法正确处理这些线条的抗锯齿!再说一次,艺术学校的辍学生。我没有像他们那样做的知识。

那么现在怎么办?

2.3 处在错误的地方

经过几年的努力,我并没有取得任何进展,最近我坐下来,试图更深入地思考这个问题。为什么那个代码不起作用?感觉应该可以,那么我遗漏了什么?

好吧,事实证明我做的是对的。我只是在代码中的位置不对。如果我限制了实际的 drawWidth,它就可以工作了!

float2 uvDeriv = fwidth(uv);
float2 drawWidth = clamp(lineWidth, uvDeriv, 0.5); //
float2 lineAA = uvDeriv * 1.5;
float2 gridUV = 1.0 - abs(frac(uv) * 2.0 - 1.0);
float2 grid2 = smoothstep(drawWidth + lineAA, drawWidth - lineAA, gridUV);
grid2 *= saturate(lineWidth / drawWidth);
float grid = lerp(grid2.x, 1.0, grid2.y);

是的,莫尔条纹更加明显,但整体亮度终于正确了!

奇怪的是,我将绘制宽度限制为 0.5,而不是 1.0。如果我使用 1.0,地平线又会太暗。

float2 drawWidth = clamp(lineWidth, uvDeriv, 1.0);

如果你的想法是“好吧,也许你只需要在上次尝试中使用 0.5?”不,这太亮了!

grid2 *= saturate(lineWidth / max(0.5, drawWidth));

为什么 0.5 是限制绘制宽度的正确值?嗯,这与线条抗锯齿的工作方式有关。

如果我们查看一些宽度受限的线条,没有任何褪色代码。如果我们手动覆盖使用的 uvDeriv,我们可以看到线条随着距离相机越来越远而如何扩展和平滑。

当宽度限制为 0.5 时(如上图所示),这意味着 0.5 以上和以下的区域面积相等(红线)。这意味着整个垂直方向上的平均值比 uvDeriv 0.5 大 0.5。

这个平均值 0.5 意味着当我们淡出线条时,除以 0.5,也就是除以我们知道这些像素的相同(平均)强度。

如果宽度限制为 1.0,我们就会得到这个结果。

现在,任何超过 1.0 的 uvDeriv 都高于平均值 0.5,高出多少取决于 uvDeriv 的大小。但它也不是平均值 1.0!这很重要,因为淡出的数学假设它是这样的,导致它变得太暗,这就是我们在失败的示例 2 中看到的。

如果我们不限制线宽,只限制我们除以的值,“0.5”点会完全消失,因为它被网格单元的边缘切断,这意味着平均亮度甚至高于 0.5,但仍然不是 1.0!这意味着如果我们只将淡出计算中除以的值限制为 0.5,它会保持太亮,这就是我们在失败的示例 3 中看到的。

这可能是整个事情中最难解释的方面,所以如果它仍然令人困惑,我深表歉意。

2.4 这是一种莫尔条纹(干涉图样抑制)

然而,我们仍然会遇到那些更明显的莫尔条纹。这是因为我们仍然无法处理网格单元小于单个像素的情况。它正确地平均到适当的值,但这并不是唯一的问题。这就是我决定作弊的地方。还记得我的主要目标之一是尽可能地限制莫尔条纹吗?好吧,这是我想与基于纹理的网格甚至地面真相的外观大相径庭的地方。它们总是会有一些莫尔条纹,因为那确实是在查看精细网格时发生的情况。

所以,与其弄清楚如何进行所有数学运算以正确计算,为什么不淡化为纯色呢?是的,我知道这是我对许多其他实现方式的抨击之一,但我不会仅仅基于某个任意距离就淡化。我会根据我知道的莫尔条纹出现时间来淡化。那么如何做到呢?很简单!使用我们已经用于抗锯齿的相同 UV 导数

float2 uvDeriv = fwidth(uv);
float2 drawWidth = clamp(lineWidth, uvDeriv, 0.5);
float2 lineAA = uvDeriv * 1.5;
float2 gridUV = 1.0 - abs(frac(uv) * 2.0 - 1.0);
float2 grid2 = smoothstep(drawWidth + lineAA, drawWidth - lineAA, gridUV);
grid2 *= saturate(lineWidth / drawWidth);
grid2 = lerp(grid2, lineWidth, saturate(uvDeriv * 2.0 - 1.0)); //
float grid = lerp(grid2.x, 1.0, grid2.y);

这里的想法是,一旦导数大于 1.0,网格单元就会小于一个像素,这时莫尔条纹开始更加明显。因此,当导数为 0.5 时,即抗锯齿线开始合并时,它开始淡化为纯色。当导数为 1.0 时,淡化完成。

就是这样!我列出的“完美”网格着色器的所有六个项目都已满足!所以我们完成了,对吧?

2.5 翻转(线反转)

嗯,有点。当您尝试使网格线宽大于 0.5 时会发生什么?什么都没有,因为我们将线宽限制为 0.5。这显然是针对非常小众的用例,但从技术上讲,我只成功实现了“0.0 或 1.0 应显示完全隐藏或填充”要求的一半。线宽 0.0 将完全隐藏该轴,但 1.0 将限制在 0.5 的宽度。但是如果我们让线条变得更宽,上面的数学运算就会变得很怪异。

最后一个技巧是,我们实际上从不绘制超过半个网格宽度的线条。相反,如果线条宽度超过 0.5,我们会翻转一些东西,有效地在白色上绘制黑线,偏移半个网格宽度。这意味着大部分数学运算都不需要改变。

float2 uvDeriv = fwidth(uv);
bool2 invertLine = lineWidth > 0.5; //
float2 targetWidth = invertLine ? 1.0 - lineWidth : lineWidth; //
float2 drawWidth = clamp(targetWidth, uvDeriv, 0.5); //
float2 lineAA = uvDeriv * 1.5;
float2 gridUV = abs(frac(uv) * 2.0 - 1.0);
gridUV = invertLine ? gridUV : 1.0 - gridUV; //
float2 grid2 = smoothstep(drawWidth + lineAA, drawWidth - lineAA, gridUV);
grid2 *= saturate(targetWidth / drawWidth);
grid2 = lerp(grid2, targetWidth, saturate(uvDeriv * 2.0 - 1.0));
grid2 = invertLine ? 1.0 - grid2 : grid2; //
float grid = lerp(grid2.x, 1.0, grid2.y);

2.6 还有一件事(偏导数长度)

我对这个着色器做了最后一个非常小的调整。那就是我不使用 fwidth()。fwidth() 函数是获取导数长度的近似值。该函数如下所示:

float fwidth(float a)
{
  return abs(ddx(a)) + abs(ddy(a));
}

这不是计算某个物体长度的方法。当物体与屏幕轴对齐时,这种方法足够准确,但在对角线上,它们总是太宽。计算导数长度的正确方法是这样的:

float ddLength(float a)
{
  return length(float2(ddx(a), ddy(a)));
}

真的是这样吗?Inigo Quilez 关于棋盘格过滤的文章认为,正确的做法是求导数的绝对最大值。

float ddMax(float a)
{
  return max(abs(ddx(a), abs(ddy(a)));
}

好吧,让我们比较一下,看看哪一个看起来更好。这需要放大到很近,因为差异很小。

导数长度计算的比较

在这里,我想说 length() 选项是正确的。与其他两个选项相比,它在锐度和混叠之间达到了正确的平衡。应该注意的是,fwidth() 从来都不是意味着正确的,只是一个快速近似值。它更快,但对于现代 GPU 来说,差异可以忽略不计。max() 方法也不是“错误的”,只是不适合这个用例。Inigo Quilez 的过滤程序的工作方式与此着色器不同,因此它很可能适合该用例。虽然他的 Shader Toy 示例都使用了略有不同的计算,并添加了任意的模糊因子,所以也许它不适合该用例?

最终,哪个看起来最好主要是主观的,max() 方法与 fwidth() 一样便宜,同时可能是一个略微更好的近似值。通过实际尝试并进行直接比较来检查您对此类事情的假设总是好的。

但是,经过最后的调整,代码看起来如下:

float4 uvDDXY = float4(ddx(uv), ddy(uv)); //
float2 uvDeriv = float2(length(uvDDXY.xz), length(uvDDXY.yw)); //
bool2 invertLine = lineWidth > 0.5;
float2 targetWidth = invertLine ? 1.0 - lineWidth : lineWidth;
float2 drawWidth = clamp(targetWidth, uvDeriv, 0.5);
float2 lineAA = uvDeriv * 1.5;
float2 gridUV = abs(frac(uv) * 2.0 - 1.0);
gridUV = invertLine ? gridUV : 1.0 - gridUV;
float2 grid2 = smoothstep(drawWidth + lineAA, drawWidth - lineAA, gridUV);
grid2 *= saturate(targetWidth / drawWidth);
grid2 = lerp(grid2, targetWidth, saturate(uvDeriv * 2.0 - 1.0));
grid2 = invertLine ? 1.0 - grid2 : grid2;
float grid = lerp(grid2.x, 1.0, grid2.y);

让我们将其与其他最佳外观的选项,基于纹理的网格和框过滤网格进行比较。

3、结束语

我希望你会同意,我们终于拥有了最平滑、最无锯齿、最少莫尔条纹、最纯净的网格着色器。在我看来,它在视觉上胜过基于纹理的网格和之前同类最佳的选项。


原文链接:The Best Darn Grid Shader (Yet)

BimAnt翻译整理,转载请标明出处