Contents

2D 距离场

前提概要

https://papalqi.cn/mathsdf/ 中我们讨论了纹理距离场,他们都没有带符号"signed"。如果不重新映射范围,它们就不是负值。这当然也是可行的,他可以做到类似以下内容这样:

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/pictureglow.gif

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picturemorph-1.gif

这很牛,但是我们如何使用数学方法生成距离场而不使用纹理呢。这不光节省贴图资源,有的时候一些特殊的效果也只能靠这种方法实现。math SDFs 可以做出非常惊人的效果。如果依靠单纯的纹理来进行距离场的构建,那资源的精度和数量将非常多,不可接受。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picturesdfExamples.gif

例如,你想为某个图标设置一个漂亮的圆形背景。经典的方法可能是在 Photoshop 这样的图像编辑器中画一个圆圈,将其保存为纹理,导入到引擎中,然后将其放在图标下面。但是,更为有用的种方法是使用 MATH 来生成圆。

在这里先推荐一个纯数学的UE4开源工程,基本的图形实例都可以在这里找到。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20220330203955.png

https://github.com/papalqi/UE4-Math-SDF

在引擎中创建 SDF 可以灵活地更改大小/渐变/动画等等。让我们从这个圆的例子开始:

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20220330204130.png

现在要将这个等式翻译成 Unreal 或 Unity的节点。用这个简单的方程,我只需要计算 UV 的长度,然后减去我们圆圈的半径r。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20220330204236.png

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20220330204246.png

把这个方程式从公式中分解出来,节点P就是TexCoord[0],然后取长度,减去调用的参数Circle Size 圆的大小。这样我就成了一个圆。

但是有些地方不对劲,圆实际上是在左上角,这并不是在材质的中心。这是因为UV坐标系在一个0-1范围内。我会画一个非常粗略的图表来说明这一点:

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picturecoord.gif

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20220330204512.png

这张图上的 x 轴代表红色通道,y 轴代表绿色通道。基于这个图,SDF 它的中心在[0,0] ,它的半径在0.5(我们的圆大小)。

现在为了好玩,我们可以看看 Unity ,你可以看到 Unity 的 y 坐标翻转了,因为它总是在左下角是[0,0]点 :

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20220330204547.png

所以 Unity 绘制的 SDF 有点不同,它实际上是在左下角显示,而不是像 Unreal 那样在左上角显示。因为如果你看一下 UV 坐标,它们的 y 轴从底部开始为0,而不是Unreal从坐标顶部开始为0的。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20220330204621.png

不管引擎是什么,我都需要把坐标系放在材质的中间。为了做到这一点,我需要实际缩放和平移坐标,并把它移动过来。下面是一张通过 Photoshop 变换在着色器中做的动图:

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picturephotoshopTransforms.gif

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20220330204710.png

如果我想要的范围从0-1到-1-1,我只需要减去0.5,乘以2。如果我只是乘以2,因为偏移缩放,它只会变成0-2的范围。

所以我们首先需要减去0.5,因为这将改变范围在负的-0.5-0.5,然后当你乘以2,你得到-1到1的范围。

因为范围是 -1-1之间。所以现在SDF是有真正的负值,这将为我们当前是内部距离还是外部距离。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20220330205014.png

在引擎中我们可以表示为:

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20220330205057.png

虚幻也有一个constant bias node 常数偏置节点。它可以在一个节点上完成减法/加法和乘法,所以我大多数时候都是这么做的:

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20220330205135.png

很好,现在我给自己弄了一个数学方法的 SDF 圆… 我能用它做什么?像Texture SDF,最基础的我可以使它羽化。那么我们在引擎中如何进行这个操作呢。

我们可以通过添加一个smoothstep node 平滑节点,然后把颜色反转过来one minus node.我的 smoothstep 值是 Min: 0和 Max: 0.01。

smoothstep这个节点给与一个最大值和最小值,如果给出的点小于最小值,那么就是0,如果大于最大值,那么就是1,如果这个值处于最大最小中间,那么会给你归一化到0-1之间。比如我们给了0和0.01,那么它会把0-0.01的值归一化到0-1。这样就可以实现羽化的效果。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20220330205317.png

如何让不同的大小的圆运动起来?这很简单,我在0.5做了一个小圆圈,所以我可以用一些时间动画代替它, 将时间编辑到0-1之间:

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20220330205403.png

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/pictureanimateSize.gif

更进一步,我可以做一个圆环:

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picturestrokeRange.gif

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20220330205617.png

组合 SDF:

接下来我们将以Unity作为展示的引擎,当然本质上还是一样的。

只是简单的SDF动画,当然无法满足我们的需求,我们还需要更为高级复杂的效果。这就需要最为基本的变化加以组合。

为了组合,我需要给自己做另一个形状。所以我要添加另一个圆圈到我的材质在这里,这个圆圈是运动的,从上到下进行运动。下面的 Unity 图表显示了我是如何从材质的顶部开始动画圆圈到底部的:

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20220330205836.png

主要的方法是取time,它是一个浮点值,time是一个游戏开始时数值为0,一直不断累加的时间数值,单位是秒。然后我们取frac,它是取小数。这样的话,如果把time链接frac,就会变成一个从0.0-0.999的一个变化。然后添加到UV的V轴上,就可以进行一个V轴简单平移操作。

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picturecircles.gif

现在我演示如何将结合静态圆A和滚动顶部向下的圆B结合。为此,我使用minimum最小节点:

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picturemin.gif

这样就可以把静态的圆圈和动态的圆圈叠加在一起。主要就是取最小值,黑的部分。

MASKING SDFS:

如果我使用Min来组合 SDF,使用Max 来遮蔽 SDF。

Negate是取 SDF 的负值,并通过第二个SDF的Max进行遮蔽Mask。交点只是两个 SDF 之间的最大节点。也就是我只需要max(-d1,d2)。就可以实现下面的效果

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picturesubtraction.gif

通过调整想要减去的圆的参数,来调整最后的形状。这里的两个圆圈使用不同的减法方法制作有趣的月亮/日食动画。

从动画圆中减去顶部的静态圆有点产生日食 ,而另一个减法看起来像月亮正在经历它的整个月亮周期的月相:

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picturemoon1.gif

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picturemoon2.gif 有趣的是,在将它们都放入最大节点之前查看哪个 SDF 是负数会改变掩蔽效果!经过半复杂的减法设置后,交叉点就容易多了。Intersection 只是两个 SDF 之间的最大节点,您可以看到它仅显示它们相交的位置:

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/pictureintersection.gif

Unreal 没有负节点,因此只需将您的 SDF 乘以-1:

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20220330210527.png

混合 SDFS:

另一个有趣的事情是使用线性插值节点或简称 Lerp 混合两个 SDF , 这将允许您确定每个 SDF 尝试混合在一起的权重。这产生了以下结果:

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picturelerpBlend.gif

Lerping 不是混合 SDF 的唯一方法,也可能不是最有用的数学方法。为了实现这种混合技术,我制作了一个自定义 HLSL 节点自定义 SmoothMinimum 的结果与 lerp 混合的结果完全不同,但混合效果非常棒:

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20220402102808.png

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picture20220402102820.png

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/pictureblendSmooth_1.gif

这里展示了数学 SDF 的力量。结合这两个距离场的视觉效果可以得到粘糊糊的中间区域,这很难用纹理来完成。

当圆圈彼此靠近时,混合会起作用,它们几乎会合并成一个圆圈,而当它们离得更远时,它们会变成单独的圆圈。与 lerp 不同,lerp 是两种形状之间的权重,其形状在视觉上更占主导地位,这种混合实际上会在两个形状靠近时合并,而在分开时将它们分开。我还可以选择使用多少混合,所以在上面的这个 gif 中,一点点混合可以让每个圆圈保持自己独特的形状,更多的混合会将圆圈合并成一个大块。

三次和二次混合也有不同的方程。我相信这种混合是最简单的,并且以相当便宜的成本获得一般的结果,所以我想这就是我要在这里演示的那个。但是,如果您愿意,可以使用其他的方案。

现在,我们来组合一下之前的方案:

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picturesubtraction1.gif

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/pictureblendMax.gif

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picturelerpMax.gif

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/pictureblend1.gif

https://papalqiblog.oss-cn-beijing.aliyuncs.com/blog/picturelerp1.gif