我一贯都在写Shader,个中有一个特定的Shader我一贯想写好,但我总是由于一些我无法完备理解的缘故原由而失落败。然后过了几年,我用新学到的知识再次考试测验,越来越靠近,然后又失落败。是什么Shader?
仿照纹理网格但看起来更好的网格Shader。
把稳:我强烈建议在深色模式下阅读本文。

正文共: 14500字 57图
估量阅读韶光: 37分钟
不想看技能事理的话,可以直接去文章末端下载源码体验不想看技能事理的话,可以直接去文章末端下载源码体验不想看技能事理的话,可以直接去文章末端下载源码体验
技能路线比拟Con-text-ure(基于纹理的网格)RGSS Texture Grid
这是利用旋转网格超采样,以及16x各向异性过滤。这基本上是大略基于纹理的网格在保持合理性能的情形下所能达到的最佳效果。
而且看起来写一个网格Shader很大略。在Shader教程中,你很早就看到了在Shader中绘制网格线。那么为什么我一贯对这个Shader如此着迷呢?由于要把它做好比看起来难得多,而且我知道基于Shader的办理方案看起来会更好。真的便是这样。我只是想更好地理解问题空间。
让我们更仔细地看一下上面的基于纹理的网格,看看仍旧涌现的一些问题区域。
RGSS Texture Grid artifacts
正如你所看到的,一些细线仍旧会涌现走样,在远处中间区域有一些走样和摩尔纹,在远间隔处,线条会变粗,然后在各向异性过滤失落效时过早地被截断。如果纹理足够大,它们在特写时才能保持清晰,否则它们会变得有点模糊。
RGSS Texture Grid 在特写时模糊
MyFirstGrid.shader(常见的网格Shader)那么那些教程中的网格Shader呢?从一个重复的UV开始。利用smoothstep()和fwidth()绘制一些线条。我们就完成了!
对吗?(别担心,我稍后会展示代码。)
Constant Pixel Width Line Grid
但有一个问题。大多数示例网格Shader,比如这个,利用的是屏幕空间宽度线条。在很多情形下,这可能比基于纹理的网格更受欢迎,而且诚笃说,这可能也是大多数人想要的。但这不是我想要实现的目标。当这种网格的线条进入远处时,终极每个网格单元的宽度小于一个像素,因此线条会收敛成与线条相同颜色的纯色。
这与基于纹理的网格不同。对付基于纹理的网格,线条本身具有透视效果,并且随着间隔的增加而变细。终极在小于像素宽度时消逝。
RGSS Texture Grid vs Constant Pixel Width Line Grid
它们都收敛成纯色,但基于纹理的网格收敛成与网格单元区域的线条覆盖率干系的颜色。
RGSS Texture Grid vs Constant Pixel Width Line Grid
更不用说当网格小于一个像素宽时涌现的明显的摩尔纹。
我过去看到的大多数考试测验绘制恒定天下空间或UV空间宽度的线条的示例Shader都没有真正精确处理这个问题。它们常日利用UV空间淡化边缘或根本没有线条抗锯齿,这两种方法终极都会在远处涌现严重的走样。或者它们会作弊,在某个任意间隔处淡出线条以隐蔽伪影。而那些没有淡出线条的Shader在远处看起来与恒定像素宽度线条网格类似。只是走样更严重,摩尔纹更明显。
Constant UV Width Line Grid
这统统都不符合基于纹理的网格的外不雅观。虽然它至少在一定程度上与线条本身的透视效果相匹配。
RGSS Texture Grid vs Constant UV Width Line Grid
Choo Choo!(过滤后的脉冲串)
但也有一些现有的例子看起来可以精确办理这个问题。最近有人向我指出了一个例子,但它已经存在的韶光比我写Shader的韶光长得多。这种技能揭橥在Apodaca, Anthony A. 和 Larry Gritz 编著。1999. Advanced RenderMan: Creating CGI for Motion Pictures(https://books.google.com/books?id=6_4VaJiOx7EC&q=Pulsetrain#v=onepage&q&f=false)中。后来在RenderMan 的文档(https://web.archive.org/web/20220629212902/http://weber.itn.liu.se/~stegu/TNM084-2016/RenderMan_20/basicAntialiasing.html)中也有先容。过滤后的脉冲串。
Filtered Pulsetrain Grid
这种技能旨在办理我一贯试图办理的精确问题。他们剖析地办理了卷积脉冲串的积分。如果你像我一样没有完成大学水平的数学课程,这意味着什么都不虞味着。我从艺术学校辍学了,以是大部分内容都超出了我的理解范围。
简而言之,这个函数返回任意范围内线条与非线条的比率。而且它非常有效。与基于纹理的网格比较,它在处理淡出到远处时的效果险些完美匹配。
RGSS Texture Grid vs Filtered Pulsetrain Grids
至少乍一看是这样。仔细不雅观察会创造一些问题。
Filtered Pulsetrain Grid artifacts
虽然它与基于纹理的网格的感知亮度相匹配,并且在前景中没有走样,但中远间隔的走样和摩尔纹明显更严重。基本上所有可见的线条抗锯齿都消逝了。它比完备没有抗锯齿要好,而且摩尔纹不像像素和UV宽度线条网格那样明显。但这仍旧不像我预期的那样干净。
有趣的是,书中有一段这样的解释:
… 最严重的走样消逝了。
最严重的,但不是所有。我不得不假设最初的作者知道它没有肃清所有走样,但对结果足够满意,没有进一步研究它。而后来利用它的人也并不关心,或者只是没有仔细不雅观察到?
Hi IQ(盒式过滤网格)还有 Inigo Quilez 在他关于可过滤的程序(https://iquilezles.org/articles/filterableprocedurals/)的文章中提到的例子,盒式过滤网格。
Box Filtered Grid
盒式过滤网格函数确实办理了过滤后的脉冲串的一些问题,紧张是它对精度高度敏感,因此在阔别原点的地方就开始涌现噪声伪影。但除此之外,它们的运行办法大致相同。这包括中远间隔的相同走样问题。
Box Filtered Grid artifacts
只管它们在走样和摩尔纹模式上略有不同。
Filtered Pulsetrain Grid vs Box Filtered Grid
现在,虽然我从高层次上理解了这两个Shader的事情事理,但我对数学的理解还不足,无法理解如何修正它们来得到我想要的结果。
新的方案实际上,我想要一个网格Shader做什么?我想要:
用户可配置的线条宽度。具有透视厚度的线条,而不仅仅是恒定像素宽度。任何间隔或视角下都不会涌现走样。线宽为 0.0 或 1.0 该当显示完备隐蔽或添补。有限的摩尔纹滋扰模式。与基于纹理的网格在远处稠浊到相同的值。可用于实时渲染以替代其他技能。以是,我回到了我确实很熟习的Shader,像素和UV宽度线条网格。然后决定开始研究它们,看看我能不能改变它们,让它们按照我想要的办法事情。或者更确切地说,从一条线开始,然后逐步构建。
让我们快速概述一下具有用户可配置线条宽度的基本网格Shader的构成。
首先,我们须要绘制一条线。
Line One, Begin(ner Line Shader)我喜好用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 进行任何修正之前获取 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 宽度线条网格Shader!
对付lerp(grid2.x, 1.0, grid2.y),可能须要阐明一下。如何将重复线条的两个轴组合成一个网格Shader,这曾经让我困惑了良久。我会利用max(x, y),或者saturate(x + y),或者其他几种方法来组合它们,但它们从来没有让我觉得很对。我花了很永劫光才从“如果我要重叠两个透明的东西,我该怎么做?”的角度来思考这个问题。我会利用 alpha 稠浊。在本例中,lerp()等效于预乘 alpha 稠浊,你也可以这样写:
float grid = grid2.x (1.0 - grid2.y) + grid2.y;
或者,如果你编写Shader,使它具有白色背景上的玄色线条,那么将两个轴相乘也会产生预乘稠浊的等效结果。把稳,不才面的示例中,与第一个示例比较,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; //
但是,我将连续利用原始的代码示例,由于它们终极的结果完备相同。
回忆起来,利用预乘稠浊觉得非常明显,但不知何故,它花了十多年才让我明白。这在我写了无数其他用场的Shader之后。我乃至写了一篇整篇文章来谈论这个确切的主题。
无论如何,有了这段代码,我们就得到了这个:
抗锯齿网格
看起来还不错,除了摩尔纹。但这是我们预见到的。现在让我们将线条宽度轻微减小一些,使其更靠近实际利用的宽度。
“抗锯齿”网格
糟糕。当线条靠近相机时,它看起来不错。但线条很快就开始涌现走样。我们在本文前面展示了恒定 UV 宽度线条网格时已经看到了这些问题,但它看起来比最初的示例轻微暗一些,走样也更严重。为什么?
最新示例与之前示例的比拟
好吧,由于两个示例之间利用的代码有一个细微的差别。我在利用smoothstep()时利用了 1.5 个像素宽的 AA。这样做的缘故原由是,smoothstep()会锐化用于抗锯齿的边缘渐变,因此 1.5 个像素宽的 smoothstep 的斜率与 1 个像素宽的线性渐变的斜率大致相同。
线性斜率与 1.5 个单位宽的 smoothstep
1 个像素宽的 smoothstep 可能太锐利了。利用 smoothstep 的缘故原由是,当利用 1.5 个像素宽的 smoothstep 时,它会增加一点点额外的抗锯齿,而不会影响与 1 个像素宽的线性渐变比较的线条的感知锐度。
1 pixel linear vs 1.5 pixel smoothstep
公正地说,这是一个非常小的差别。但 HLSL 的smoothstep()仍旧很好,由于它还充当反向 lerp(也称为重新映射)并对值进行 0.0 到 1.0 之间的胁迫。因此,它有助于简化代码。它也没有完备肃清感知走样,但我们稍后会回到这一点。
终极,我们为恒定 UV 宽度网格得到了这段Shader代码:
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);
Constant UV Width Line Grid
那么恒定像素宽度线条网格呢?好吧,这只是一个微不足道的改变。将线条宽度乘以导数!
(记住,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)
Constant Pixel Width Line Grid
现在我们回到了本文前面的地方。我们有两个Shader,它们至少知足了我的两个哀求。但我们还没有办理我们之前没有办理的任何问题,个中一个只有透视线条,另一个办理了大多数走样问题。
以是,让我们暂时专注于一条线,而不是全体网格。我们如何让一条线既有透视厚度又没有走样?
Phoning It In(电话线 AA)我最喜好的抗锯齿线条技巧之一来自 Emil Persson。特殊是他的电话线 AA 示例。
https://www.humus.name/index.php?page=3D&ID=89
这种技能的核心思想是不要让东西变细到小于一个像素。相反,将东西的大小胁迫到至少一个像素宽,然后淡出。这看起来比让线条变细到小于一个像素要好得多,由于如果你这样做,它总是会涌现走样。两个神奇的地方是,你如何保持东西一个像素宽,更主要的是,你淡出它们多少。
在 Emil Persson 的示例中,他利用了有关线几何体宽度、每个顶点到相机的间隔以及相机投影矩阵的信息来保持线一个像素厚。但对付这个Shader,我们可以再次利用那些偏导数!
我们只须要限定线条在屏幕空间中的细度。基本上,我们将我们已经拥有的两个Shader结合起来,并取 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);
Phone-wire AA Line Grid
好多了!
… 差不多吧。看起来不太对。它在视界处淡出到玄色!
提醒一下,基于纹理的网格淡出到灰色,而不是玄色。
RGSS Texture Grid vs Phone-wire AA Line Grid
问题在于,在一个网格中,一条线只能变粗到一定程度,然后它就会比全体网格单元宽度还要宽。当它是一条单独的线时,这不是问题。但当它被绘制成一个网格时,在它变黑的地方,一个像素比多个重叠的网格宽度还要宽。但我们仍旧只在每个像素中绘制一组线条,而不是多个网格单元。
我在编写这些Shader时卡住很永劫光的地方是接下来该做什么。我花了很多韶光试图弄清楚如何精确打算淡化线条的值,但彷佛没有什么能真正精确地办理它。我相信这是可以办理的,但请记住我之前说过我是艺术学校辍学?是的,我不会是那个弄清楚的人。我走这条路是由于我对数学不善于,无法用“精确”的办法来做。
我在这条路上走得最靠近的一次是考试测验将我用来除以线条宽度的值胁迫到最大值 1.0。我的理论是,如果线条不能比一个像素宽,就不要除以大于 1 的值。虽然这更好,但它仍旧禁绝确。
grid2 = saturate(lineWidth / max(1.0, drawWidth));
非常奇妙,但这会导致在单独可区分的线条与视界处的大致纯色之间涌现一个暗色的“沟壑”。
RGSS Texture Grid vs failed attempt example
如前所示,过滤后的脉冲串和盒式过滤网格确实办理了这个问题。不是通过精确地淡化线条,而是通过始终打算当前像素覆盖范围内所有可能线条的总覆盖率。但正如我所展示的,它们都没有精确处理这些线条的抗锯齿!
再次解释,我是艺术学校辍学。我没有足够的知识来像他们那样做。
那么现在该怎么办?
Right At The Wrong Place在经由几年的研究后,我仍旧没有取得任何进展,最近我坐下来,试着更多地思考这个问题。为什么这段代码不起浸染?它觉得该当起浸染,那么我错过了什么?
好吧,事实证明我做的是精确的事情。我只是在代码中的缺点位置做了。如果我限定了实际的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);
RGSS Texture Grid vs correctly clamped draw width
是的,摩尔纹更明显,但整体亮度终于精确了!
一个奇怪的事情是,我将绘制宽度胁迫到 0.5,而不是 1.0。如果我利用 1.0,它在视界处会再次太暗。
float2 drawWidth = clamp(lineWidth, uvDeriv, 1.0);
RGSS Texture Grid vs failed attempt example
如果你想的是“好吧,大概你只须要在之前的考试测验中利用 0.5?”不,那样太亮了!
grid2 = saturate(lineWidth / max(0.5, drawWidth));
RGSS Texture Grid vs failed attempt example
为什么 0.5 是胁迫绘制宽度的精确值?好吧,这与线条抗锯齿的事情办法有关。
如果我们看一下一些没有淡化代码的宽度限定线条。如果我们手动覆盖利用的uvDeriv,我们可以看到线条是如何随着它们阔别相机而扩展和平滑的。
当限定到 0.5 的宽度时,如上所示,这意味着在 0.5(红线)之上和之下的区域面积相等。这意味着在uvDeriv为 0.5 时,全体垂直方向上的均匀值为 0.5。
这个 0.5 的均匀值意味着当我们淡出线条时,以及除以 0.5 时,我们是在除以我们知道这些像素的(均匀)强度。
如果限定到 1.0 的宽度,我们会得到这个。
现在,在uvDeriv为 1.0 之后,任何地方都高于 0.5 的均匀值,高于多少取决于uvDeriv的大小。但它也不是 1.0 的均匀值!
这一点很主要,由于淡化它的数学假设它是 1.0,导致它变得太暗,这便是我们在失落败示例 2 中看到的征象。
如果我们不限定线条宽度,而只限定我们用来除以的值,那么“0.5”点就会完备消逝,由于它被网格单元的边缘割断了,这意味着均匀亮度乃至更高于 0.5,但仍旧不是 1.0!
这意味着如果我们只将淡化打算中我们用来除以的值胁迫到 0.5,它就会保持太亮,这便是我们在失落败示例 3 中看到的征象。
这可能是全体过程中最难阐明的部分,以是如果它仍旧让你感到困惑,我表示歉意。
It’s A Moiré(抑制滋扰模式)但是我们仍旧有那些更明显的摩尔纹。这是由于我们仍旧没有处理网格单元小于一个像素的情形。它会精确地均匀到适当的值,但这并不是唯一的问题。而这便是我决定轻微作弊的地方。请记住,我的紧张目标之一是尽可能地限定摩尔纹?好吧,这是一个我想从基于纹理的网格或乃至真实情形的外不雅观中严重偏离的地方。它们总是会有一些摩尔纹伪影,由于这确实是不雅观看风雅网格时发生的事情。
以是,与其弄清楚如何用精确的办法打算所有数学运算,为什么不淡化到纯色呢?是的,我知道我之前批评过很多其他实现的这种做法,但我不会仅仅根据某个任意间隔来淡化。我将根据我知道摩尔纹涌现的韶光来淡化。怎么做?很大略!
利用我们已经在用于抗锯齿的相同的 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);
RGSS Texture Grid vs Moire suppression
这里的想法是,一旦导数大于 1.0,网格单元就小于一个像素,这时摩尔纹会变得更加明显。以是当导数为 0.5 时,它就开始淡化到纯色,这时抗锯齿线条开始合并。当导数为 1.0 时,它就完成淡化。
便是这样!
我关于“完美”网格Shader的六个哀求都知足了!
以是我们完成了,对吧?
好吧,差不多吧。当你试图使网格线条的宽度大于 0.5 时会发生什么?什么都不会发生,由于我们将线条宽度胁迫到 0.5。这显然是一个非常利基的用例,但从技能上讲,我只成功地知足了“0.0 或 1.0 该当显示完备隐蔽或添补”哀求的一半。线条宽度为 0.0 将完备隐蔽该轴,但 1.0 将限定在 0.5 的宽度。但如果我们让线条的宽度超过 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- 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);
我对这个Shader做了一个末了的非常小的调度。那便是我没有利用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 在他关于棋盘格过滤(https://iquilezles.org/articles/checkerfiltering/)的文章中认为,精确的方法是获取导数的绝对最大值。
float ddMax(float a){ return max(abs(ddx(a), abs(ddy(a)));}
好吧,让我们比较一下,看看哪个看起来更好。这将须要非常近间隔地放大,由于差异很小。
导数长度打算的比较
在这里,我认为length()选项是精确的。它在锐度和抗锯齿之间取得了平衡,与其他两个选项比较。该当把稳的是,fwidth()从来就不是为了精确性而设计的,它只是一个快速的近似值。而且它确实更快,但对付当代 GPU 来说,差异可以忽略不计。max()方法也不“缺点”,只是对付这个用例来说是缺点的。Inigo Quilez 的可过滤程序的事情办法与这个Shader不同,因此它很可能对那个用例来说是精确的。虽然他的 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);
Pristine Grid
让我们将它与其他看起来最好的选项进行比较,即基于纹理的网格和盒式过滤网格。
Pristine Grid
RGSS Texture Grid artifacts
Box Filtered Grid artifacts
结论现在,我们终于有了最平滑、最没有走样、摩尔纹最少、最纯粹的网格Shader。在我看来,它在视觉上超越了基于纹理的网格,以及之前最好的选项。
至少在 Quilez 师长西席写出超越它的Shader之前是这样☺。
其他想法比线条更宽的抗锯齿是的,抗锯齿像素宽度比最小线条像素宽度更宽。这确实会轻微降落某些情形下线条的最大亮度。但不会以任何可感知的办法。更宽的抗锯齿填补了任何感知的亮度丢失。它实际上只在像素恰好落在线条中央的罕见情形下才能丈量出来,而这种情形并不常见。
这些smoothstep()线条看起来更暗了吗?
RGSS Texture Grid 中的走样有些人把稳到本文中基于纹理的网格示例比他们预期的走样要多一些。这在一定程度上是 RGSS Shader本身的毛病。由于它利用旋转网格进行超采样,以是如果边缘与这些样本对齐,它就会涌现一些问题。4x MSAA 也有这个问题,而 RGSS Shader利用的是相同的样本模式。像这样的透视网格是 RGSS 和 4x MSAA 最难处理的东西之一,由于一定会有几条网格线受到影响。
由于 4x MSAA 模式存在这个问题,因此在利用几何体或硬件线条网格时,抗锯齿网格也很难处理,由于 4x MSAA 是最常用的。
但是,这也是利用各向异性过滤本身的伪影,它在 GPU 上是近似的。每家供应商乃至每个世代之间的近似方法都不相同。而利用的确切方法彷佛是每家公司之间严格守旧的秘密。但常日,它们方向于在少数 mip 偏差级别上尽可能少地进行额外的纹理采样。如果你有兴趣理解更多关于这方面的信息,请搜索 EWA 或 FELINE。简而言之,纵然没有 RGSS,也会涌现一些走样。
这两个示例之间的紧张差异在于线条保持清晰的间隔,以及一些垂直线条上的模糊度略低。
由于各向异性过滤的实现取决于 GPU,因此利用它时涌现的走样程度也会有所不同。例如,在 Nvidia GTX 970、RTX 2080 Super 和 RTX 3080 上,它看起来略有不同。
Kaiser 滤波器你可能想知道 Kaiser 滤波器在这个网格上的效果如何。它比默认的盒式滤波器更好吗?乃至比这个新的Shader更好吗?
不。它看起来与盒式过滤基本相同,但轻微亮一些,由于 mipmap 的均匀强度不再与之前的 mipmap 相同。Kaiser 滤波器在大多数情形下正面不雅观看时有助于缩小,但在更多斜角不雅观看时效果不佳。细的黑白线条也是 Kaiser 滤波器的最坏情形。
而且,我不会给你一个示例来进行比较。;)
Shader源码(Unity+UE)这是一个 ShaderToy 中的 GLSL 实现示例,特殊是如果你想要一个动态的示例。https://www.shadertoy.com/view/mdVfWw
Unity Shader示例PristineGrid.shader:https://pan.quark.cn/s/e48012a62254
Unreal 材质函数图示例。https://blueprintue.com/blueprint/_s_ms69e/
更繁芜的表面和 UV
这个Shader实现办法的一个好处是,它不局限于平面和规则网格。它可以推广到更繁芜的几何体,而无需修正。对付不同的 UV 布局,只须要进行少量的修正,紧张是处理线条宽度和导数不连续性。
这是一个在凹凸不平的地形上看起来不太好的Shader。
地形上的纯粹网格
这是一个利用相同技能的修正后的PristineRadialGrid.shader:https://pan.quark.cn/s/e48012a62254利用的是径向 UV,以及导数不连续性修复。
还有一个另一个版本PristineTriplanarGrid.shader:https://pan.quark.cn/s/e48012a62254支持三平面映射。(Unreal Engine 材质函数版本在这里(https://blueprintue.com/blueprint/0tm1_z62/))
还有最常被问到的功能,以是这里有一个PristineMajorMinorGrid.shader(https://pan.quark.cn/s/e48012a62254),它具有独立的紧张网格划分和轴线。
如果喜好本日的文章,请多点点赞和在看,后续就会有更多此类的文章~