SDF平滑合并的理解与新公式的创造

305 阅读6分钟

在IQ 这篇文章 iquilezles.org/articles/sm… 中提出了非常多丝滑合并的函数。 里面函数就像魔法一样,而最初的我看到这些函数就一个麻瓜, 毕竟作为 10 多年没有碰过数学的人,连多项式求导都忘了。 学了半年 shader之后,这个问题始终萦绕在我脑子中,经过周末的突击,也算是搞明白了。 本文将用通俗易懂的方式解释 其中一个基于二元多项式的丝滑函数背后的原理。

如果你还不大理解什么是丝滑合并,IQ博客中这两个对比图会给你直观的感觉。使用 SDF作图和我们使用其他计算机软件作图是一样,都是通过一些基本的 primitive 进行交,并,差的逻辑运算组合出复杂的图像。 而左边的图像就像是一个一个石膏几何体组合到一样,而右边的图像就丝滑很多。 2406134357

以上是通过两个不同的函数进行逻辑并运算,普通的 union就是取两个值较小的一个,smooth union引入一个 k系数作为 smooth范围,另外有一大段魔法方法。

float union( float a, float b )
{
    return min(a, b)
}


float smoothUnion( float a float b, float k )
{
    float h = clamp( 0.5 + 0.5*(b-a)/k, 0.0, 1.0 );
    float m =  k*h*(1.0-h);
    return mix( b, a, h ) - m;
}

看不懂对吧~, 别着急,跟着我的思路来, 我们从为什么 min函数就可以让两个 图形合并来理解

何为有向距离场

看下面这个图像,它是由两个函数组成,暂且咱们不去管他是什么函数,我们把它想象成为动画片里面两座小山。 2412084939 那么两条线就是两座小山的高度。如果我们要将两个小山合并呢? 是不是取两个线更大的值就可以了?

等等,前面的 SDF不是取两个值更小的值吗?怎么到这取最大值了呢。 于是我们第一个重点就来了,我换一个方式去理解,我们给两座小山画一个天空也就是最上面那个线 2412085053

现在我们不在使用 两条线的 y值了,而是使用小山到天空的距离, 也就是 3.5 - y . 这样如果再去合并两个小山,是不是就要去小山到天空距离更短的.

同样在初中地理中,常常会通过下面这种等高线图用来表述地形, 2412083720

如果我们表述一个形状,通过平面上点到形状的距离,用类似于等高线图来表示呢?以下就是一个贝塞尔曲线的等矩线图

2412085929

所以什么是 SDF(Signed Distance Field)?, 他是一个标量场,也就是针对于平面中每个坐标都有一个值,就像山的高度一样,只不过这个值是距离山表面的最短距离。 而对于复杂的形状,我们很难直接写出他的解析表达式,通过数据的方式,记录空间上任意一个点到这条曲线的距离,那么这个空间上的数据集就表示了空间上的一条曲线/面,这就是所谓的隐式表达。

何为平滑

从iOS 7开始,苹果更新了自家产品标志性的圆角图标轮廓,将工业设计中常用的曲率连续变化的概念应用到视觉层面,app图标从简单的圆角矩形(G1连续)转变为看起更顺滑的圆角矩形(G2连续)使得整个iOS系统的设计感都得到了提升,让用户看着更舒服。 2406133236

所以在工业界本来就有对Smooth的数理定义和等级划分,详细可以参考WIKI. 我们常常说的光滑其实是 Geometric continuity 几何连续。 一个曲线或曲面可以被描述为具有 Gn 连续性,n 是表示光滑度的增量,即在曲线上取一点,然后分析该点与其两侧线段的关系。

  • G0: 两侧曲线在这一点相遇(位置连续)。
  • G1: 两侧曲线这一点的切线方向相同(相切连续)。
  • G2: 两侧曲线在这一点的曲率相同(曲率连续)。

上面G0很好理解两个线只要连接了,那就是G0连续,我们的直角矩形就是G0连续, G1连续也比较好理解,以CSS或者IPHONE7以前的圆角矩形为例,直角边与圆角的切线是一条,那就是G1连续 2406134508 如果用法向量来表示, 法向量的方向不会出现瞬间跳跃 2406140223

当然以上都是一些感受上的东西,如果我们想要在Shader中使用他们,必须要用数学去定义并推导。 在wiki的丝滑定义中,提到了位置连续,相切连续和曲率连续。 可是连续是什么呢? Continuity of real functions is usually defined in terms of limits. A function f with variable x is continuous at the real number c, if the limit of 𝑓(𝑥),as x tends to c, is equal to 𝑓(c)

limxx0f(x)=f(x0)\lim\limits_{x\to x_0}f(x) = f(x_0)

连续要求两个相近的x, 其f(x)的值也一样。

何为平滑函数

考虑有以下两个函数图像,a=xa=\sqrt{x}b=(2x)2b = (2-x)^2 2406152431

min(a,b)min(a, b)的图像会产生两个尖角,这两个点是不平滑的

2406152333

用公式表达便是

min(a,b)={aif  a<bbif  a>=bmin(a, b) = \begin{cases} a \quad if \; a < b \\ b \quad if \; a >= b \end{cases}

IQ的设计函数 smooth_min(a, b, k), 输入两个函数a,b,通过k值可以控制两个函数smooth min到下图, k值是开始平滑的范围。 2406155638

从上面的图像可以观察出, 当 a-b 小于一个值的时候,图像的值完全等于a, 当大于一个值的时候,图像的值有完全等于 b。而在尖尖部不连续的地方(a=b)左右一段区域[-k , k]。选了一个比 a,b都要小的值假设为 v。 用数学公式表达便是

smooth_min(a,b)={aif  ab<kabk<1bif  ab>kabk>1vif  ab[k,k]abk[1,1]smooth\_min(a, b) = \begin{cases} a &\quad if \; a - b < -k \to \frac{a-b}{k} < -1 \\ b &\quad if \; a - b > k \to \frac{a-b}{k} > 1 \\ v &\quad if \; a-b \in [-k, k] \to \frac{a-b}{k} \in [-1, 1] \end{cases}

而上面公式换一种写法,就很像我们的 mix函数

smooth_min(a,b)={aif  h<0bif  h>1a+b2+(1h)(ab)2if  h[0,1]smooth\_min(a,b) = \begin{cases} a &\quad if \; h<0 \\ b &\quad if \; h>1 \\ \frac{a+b}{2} + \frac{(1-h)(a-b)}{2} &\quad if \; h \in [0, 1] \end{cases}

假定 x=abtx = \frac{a-b}{t}, 那么 我们求的一个 g(x)g(x) 函数能够满足以下条件。

  1. g(1)=0g(-1) = 0, 当 x越趋近于-1 时,v的更加接近于 a,
  2. g(1)=1g(1) = 1, 函数越趋近于 1时,v的值越接近与 b.

上面这两句话在换一个直观的形式来表达, 函数在[-1, 1]的边界处受到原来x=(a-b) x影响越小, 而在中间处受到影响越大。这个g(x)函数就是我们的平滑函数的核。 各根据以上特性我们可以造出无数个函数满足以上的要求

多项式

下图有三个函数图像,都满足上述的要求。 我不知道其效果如何 2412081907

f(x)g(x)h(x)
2412083929 2412083941 2412083929

以上三个不同算子的代码为


float smin_f( float a, float b, float k )
{
    k *= 4.0;
    float x = (b-a)/k;
    float g = (x> 1.0) ? x :
              (x<-1.0) ? 0.0 :
              (x*(2.0+x)+1.0)/4.0;
    return b - k * g;
}


float smin_g( float a, float b, float k )
{
    k *= 2.0;
    float x = (b-a)/k;
    float g = (x> 1.0) ? x :
              (x<-1.0) ? 0.0 :
              (x+1.0)/2.0;
    return b - k * g;
}

float smin_h( float a, float b, float k )
{
    k *= 2.0;
    float x = (b-a)/k;
    float g = (x> 1.0) ? x :
              (x<-1.0) ? 0.0 :
              ((x+1.0) /2. + (x+1.)*(x+1.) / 4. )/2.0;
    return b - k * g;
}

是不是有很强的一致性。 而 k要乘上一个系数,是因为做了正则化 normailize . 表示当 对于任意的算子,a=b的时候,g(x)要等于 1 到这里就可以理解为什么 IQ可以写出这么多算子了。 而实际上所有的算子都可以写成多项式形式,因为一个函数如果足够光滑就可以被多项式逼近,这个结论符合直觉,因为我们有泰勒展开。 2412080049

所以结果是不需要推导 🐶