介绍
在编写着色器或进行任何程序化创建过程(纹理、建模、着色、动画等)时,您经常需要以不同方式修改逻辑,使它们按照您的需求表现。通常会使用 smoothstep()
来设定某些值的阈值,或者使用 pow()
来塑造信号形态,clamp()
来裁剪信号,mod()
使其重复,mix()
来混合两个信号,exp()
用于衰减等。所有这些函数在大多数语言中默认情况下都是可用的。然而,也有一些相对常用的操作并未默认包含在任何语言中。下面列出了我在工作中反复使用的一些函数(或者说函数族)。每个函数都附有Graphoy链接,您可以通过这些链接交互式地探索它们:
Almost Identity(几乎恒等函数版本) V1
想象一下,除非信号降至零或接近零,否则您不希望修改该信号。在这种情况下,您想要将值替换为一个小的正数常量。那么,与裁剪值并引入不连续性相比,您可以将信号平滑地混合到所需的裁剪值。设m为阈值(高于m的所有值保持不变),n为当信号为零时取的值。那么,下面的函数可以实现这种软裁剪(以三次多项式式):
float almostIdentity(float x, float m, float n)
{
if (x > m) return x;
float a = 2.0 * n - m;
float b = 2.0 * m - 3.0 * n;
float t = x / m;
return (a * t + b) * t * t + n;
}
这段代码首先检查输入x是否大于阈值m,如果是,则直接返回x,即不对信号做任何改变。如果x不大于m,则计算辅助变量a和b,并通过将x标准化到[0, 1]范围内的t值来调整信号。最后,根据一个基于a、b和t的三次多项式表达式,计算并返回一个新的值,该值在x接近0时平滑过渡到n,同时在x>m时不改变原始信号,从而实现了平滑的软裁剪效果。可以在GrapToy在线地址上尝试。
Almost Identity(几乎恒等函数版本) V2
实现近似恒等的另一种方法是通过偏置平方的平方根。我第一次看到这种技术是在Shadertoy上由用户 "omeometo"
编写的着色器中。根据硬件的不同,这种方法可能比上面的三次方法稍慢一些,但我最近发现自己经常使用它,特别是在处理“平滑镜像”
形状时,因为它表现得几乎就像x的绝对值。虽然它的导数为零,但其二阶导数非零,因此如果它在您的应用中引起问题,请留意这一点。
float almostIdentity(float x, float n)
{
return sqrt(x*x + n*n);
}
这段代码通过对x的平方与一个较小正数n的平方之和取平方根,来实现一种接近于恒等的变换。它特别适用于需要平滑处理信号的地方,同时保持了类似于绝对值函数的行为特征。需要注意的是,由于该函数具有非零的二阶导数,在某些应用场景下可能会引发问题,因此在使用时要小心检查。下图为 sqrt(x*x+.1)
所呈现的结果。可以在GrapToy在线地址上尝试。
Smoothstep Integral(平滑步进积分)
如果您将 smoothstep 用于速度信号(例如,您希望平稳地将静止物体加速到恒定速度运动),则需要对 smoothstep() 进行时间上的积分,以获得动画的实际位置值。下面的函数正是为此设计的,表示一个通过 smoothstep 加速的物体的位置。请注意,其导数永远不会大于 1,因此不会发生减速。
float integralSmoothstep(float x, float T)
{
if (x > T) return x - T / 2.0;
return x * x * x * (1.0 - x * 0.5 / T) / T / T;
}
该函数计算了在时间 x 内,使用 smoothstep 加速的物体的位置。如果时间 x 超过了总持续时间 T,则返回匀速运动的部分(即 x - T/2.0)。否则,按照 smoothstep 的加速度曲线计算位置值。这种方法确保了物体的运动是平滑且无减速的。这是测试地址
脉冲
Exponential Impulse(指数脉冲)
脉冲非常适合用于触发行为或为音乐和动画创建包络线。基本上,任何快速增长然后缓慢衰减的情况都可以使用脉冲。以下是指数脉冲函数。使用参数𝑘来控制函数的拉伸程度。该函数的最大值为 1,且恰好发生在
𝑥=1/𝑘。
float expImpulse(float x, float k)
{
float h = k * x;
return h * exp(1.0 - h);
}
这个函数通过ℎ=𝑘⋅𝑥 和 $ h⋅e^{1.0-h}$的组合生成一个指数脉冲曲线。通过调整𝑘,可以改变脉冲的宽度和形状,使其适用于各种需要快速上升和缓慢下降的场景。这是在线地址。
Polynomial Impulse(多项式脉冲)
另一种不使用指数函数的脉冲函数可以通过多项式设计。使用参数𝑘来控制函数的衰减速度。例如,可以使用二次多项式,其峰值出现在:
$$
x=\sqrt{1/k}
$$
float quaImpulse(float k, float x)
{
return 2.0 * sqrt(k) * x / (1.0 + k * x * x);
}
您可以很容易地将其推广到其他幂次,以获得不同的衰减形状,其中𝑛是多项式的次数:
float polyImpulse(float k, float n, float x)
{
return (n / (n - 1.0)) *
pow((n - 1.0) * k, 1.0 / n) *
x / (1.0 + k * pow(x, n));
}
这些广义脉冲的峰值出现在:
$$
x=[k(n-1)]^{-1/n}
$$
通过调整𝑘 和𝑛,您可以灵活地控制脉冲的形状和衰减速率,适用于需要不同特性的脉冲信号场景。可在线尝试
Sustained Impulse(持续脉冲)
与前面的脉冲类似,但此函数允许独立控制上升阶段的宽度(通过参数 “k”
)和释放阶段的宽度(通过参数 “f”
)。此外,该脉冲在值为 1 而非 0 时开始释放。
float expSustainedImpulse(float x, float f, float k)
{
float s = max(x - f, 0.0);
return min(x * x / (f * f), 1.0 + (2.0 / f) * s * exp(-k * s));
}
解释:
x
: 输入值,表示时间或某个变量。f
: 控制释放阶段的参数,决定了脉冲从峰值下降的速度。k
: 控制上升阶段的参数,决定了脉冲达到峰值的速度。s
: 计算释放阶段的时间偏移量,确保仅在 x > f 时开始释放。
该函数生成一个脉冲信号,其上升部分由 x * x / (f * f) 决定,而释放部分则由指数衰减项 1.0 + (2.0 / f) * s * exp(-k * s) 控制。通过调整参数 f 和 k,可以灵活地设计具有不同上升和释放特性的脉冲信号。
Sinc脉冲
一个相位偏移的 sinc 曲线在某些情况下非常有用,特别是在它从零开始并以零结束时,适用于一些弹跳行为(由 Hubert-Jan 提议)。通过给参数𝑘赋予不同的整数值,可以调整弹跳的次数。该函数的最大值为 1.0,但它也可能取负值,在某些应用中这可能会使其不可用。
float sinc(float x, float k)
{
float a = PI * (k * x - 1.0);
return sin(a) / a;
}
解释:
x
: 输入值,通常表示时间或某个变量。k
: 控制弹跳次数的参数。赋予其不同的整数值可以调整曲线的周期和振荡次数。a
: 计算相位偏移后的角度值。
该函数生成一个 sinc 曲线,其形状类似于正弦波除以其角度值的结果。由于其特性,sinc 函数非常适合模拟具有衰减振荡的行为,例如弹跳或其他周期性现象。然而,由于其可能产生负值,在需要非负信号的应用中需要注意。可在线尝试。
Falloff(衰减函数)
这是一种二次衰减函数,类似于基于物理的点光源中的衰减函数,但它在给定的距离 "m"
处达到零,而不是像传统函数那样仅在无穷远处渐近趋于零。这种函数非常适合用于范围可控的阴影等场景。
float trunc_falloff(float x, float m)
{
x /= m;
return (x - 2.0) * x + 1.0;
}
解释:
x
: 输入值,通常表示距离或某个变量。m
: 控制衰减范围的参数,指定函数在何处达到零值。
该函数首先将输入值 x 归一化为相对于 m 的比例,然后通过计算 (x - 2.0) * x + 1.0
得到一个二次多项式的结果。这个结果会在 x = m
时精确地变为零,从而形成一个在有限范围内平滑衰减的效果。
这种函数非常适合需要明确边界的应用场景,例如:
- 可控范围的光照衰减。
- 距离受限的阴影效果。
- 局部影响的模拟(如粒子系统或力场)。
这是在线地址
Unitary remappings(单位区间重映射)
以下这些函数将 [0, 1] 区间重新映射到 [0, 1] 区间。它们可以用于调整图像对比度、塑造地形坡度、调制运动、雕刻形状等场景。其中,一个非常常见的函数是 smoothstep(),由于它无处不在,这里不再包含。
这些函数的核心作用是对输入值进行非线性变换,从而改变数据的分布特性。例如,在图像处理中,可以通过这些函数增强或减弱对比度;在 procedural generation(程序化生成)中,可以用来控制地形的高度变化或物体的形状。
以下是一些典型的单位区间重映射函数示例:
Almost Unit Identity(近似单位恒等函数)
这是一个将单位区间映射到自身的近似恒等函数。它与 smoothstep() 类似,因为它们都将 0 映射为 0,1 映射为 1,并且在原点处导数为 0。然而,与 smoothstep() 不同的是,该函数在 x = 1 处的导数为 1,而不是 0。它相当于上面提到的 Almost Identity 函数中参数 n=0 和 m=1 的特殊情况。由于它是一个三次多项式,就像 smoothstep() 一样,计算速度非常快。
float almostUnitIdentity(float x)
{
return x * x * (2.0 - x);
}
- x: 输入值,范围为 [0, 1]。
- 该函数通过公式 x * x * (2.0 - x) 实现了一种平滑的非线性变换。
- 它在 x = 0 和 x = 1 处保持边界条件,同时在 x = 1 处的导数为 1,确保了输出值在接近 1 时不会突然停止变化。
特点:
- 平滑过渡:该函数在整个 [0, 1] 区间内平滑地从 0 变化到 1。
- 简单高效:作为三次多项式,计算成本低,适合实时应用。
- 适用场景:可以用于需要轻微非线性调整的场合,例如图像对比度调整、动画曲线设计或程序化生成中的形状控制。
与 smoothstep() (下图绿线)相比,这个函数更适合需要在终点处保持线性变化速率的场景。可在线尝试
Gain Function(增益函数)
通过扩展区间两端并压缩中间部分,同时保持 1/2 映射为 1/2 的方式,可以将单位区间重新映射到自身。这可以通过 gain() 函数实现。这个函数在 Renderman Shading Language (RSL)
的教程中非常常见。当参数 k=1 时,该函数表现为恒等曲线;当 k<1 时,生成经典的增益形状;而当 k>1 时,生成 "S" 形曲线。对于 k=a 和 k=1/a,这些曲线是对称且互为反函数的。
float gain(float x, float k)
{
float a = 0.5 * pow(2.0 * ((x < 0.5) ? x : 1.0 - x), k);
return (x < 0.5) ? a : 1.0 - a;
}
x
: 输入值,范围为 [0, 1]。k
: 控制曲线形状的参数。- 当 k=1 时,函数表现为线性恒等映射。
- 当 k<1 时,曲线在中间部分被压缩,两端被扩展,形成经典的增益形状。
- 当 k>1 时,曲线呈现 "S" 形,适合用于增强对比度或非线性调整。
对称性
:对于参数 k=a 和 k=1/a,生成的曲线是彼此对称且互为反函数的。
特点:
- 灵活控制:通过调整参数 k,可以灵活地改变曲线的形状,适用于多种场景。
- 保持中点不变:无论参数 k 如何变化,输入值 1/2 始终映射为输出值 1/2。
- 平滑过渡:整个曲线平滑连续,无突变。
应用场景:
- 图像处理:用于调整图像对比度,增强或减弱中间色调的变化。
- 动画设计:用于创建非线性的运动曲线,使动画更加自然。
- 程序化生成:用于控制地形坡度、材质渐变或其他需要非线性变换的场景。
通过 gain() 函数,您可以轻松实现从简单线性映射到复杂非线性调整的转换,非常适合需要精细控制的应用场合。可在线尝试。
Parabola(抛物线函数)
这是一个将区间 [0, 1] 映射回 [0, 1] 的优雅选择,其中区间的两端(角落)被映射为 0,而中心被映射为 1。之后,您可以将抛物线提升到幂次𝑘,以控制其形状。可在线尝试
float parabola(float x, float k)
{
return pow(4.0 * x * (1.0 - x), k);
}
x
: 输入值,范围为 [0, 1]。k
: 控制抛物线形状的参数。- 当
𝑘=1
时,函数生成标准的抛物线形状。 - 当
𝑘>1
时,抛物线变得更加尖锐,中间部分更接近 1,而两端更接近 0。 - 当
𝑘<1
时,抛物线变得更宽,中间部分更平缓。
- 当
Power Curve(幂次曲线)
这是对上面提到的 抛物线函数(Parabola) 的一种泛化。它同样将区间 [0, 1] 映射到 [0, 1],并且保持两端(角落)映射为 0。然而,在这种泛化版本中,您可以分别控制曲线两侧的形状,这在创建叶子、眼睛以及其他许多有趣的形状时非常有用。
float pcurve(float x, float a, float b)
{
float k = pow(a + b, a + b) / (pow(a, a) * pow(b, b));
return k * pow(x, a) * pow(1.0 - x, b);
}
x
: 输入值,范围为 [0, 1]。a 和 b
: 控制曲线两侧形状的参数。- 参数 a 控制曲线从 0 到峰值的变化速率。
- 参数 b 控制曲线从峰值到 1 的变化速率。
k
: 一个归一化因子,确保曲线在最大值处恰好达到 1(为了便于说明)。但在许多实际应用中,曲线通常需要重新缩放,因此可以省略 k 的计算以提高效率。
Tonemap(色调映射函数)
这个函数将 0 映射为 0,1 映射为 1,同时提升中间色调,类似于指数小于 1 的幂函数。然而,当分子不是𝑘+1,而是其他值(通常是更大的值)时,这个函数通常被用作颜色色调映射的传递函数(类似于 Reinhard 色调映射器),因此得名 "Tonemap"。通常情况下,𝑘>0,但您也可以将 𝑘 设置在 0 和 -1 之间,使曲线向内弯曲,类似于正幂函数的效果。
float tonemap(float x, float k)
{
return x / (x + k);
}
- x: 输入值,范围通常是 [0, ∞),表示未映射的亮度或颜色强度。
- k: 控制曲线形状的参数。
- 当k > 0,函数的行为类似于色调映射器,压缩高光部分并提升阴影部分。
- 当 k 在 0 和 -1 之间时,曲线会向内弯曲,类似于正幂函数的效果。
通过 tonemap() 函数,您可以轻松实现从高动态范围到低动态范围的颜色转换,同时保持图像的视觉质量。这种函数广泛应用于图形学、摄影和视频处理领域。
脉冲、凸起和阶梯状
Cubic Pulse(三次脉冲函数)
很可能你经常会用 smoothstep(c-w, c, x) - smoothstep(c, c+w, x) 来选择一个以
𝑐为中心、从 𝑐−𝑤 到 𝑐+𝑤 的区域。我知道我经常这样做,这就是为什么我创建了下面的 cubicPulse() 函数。你也可以将其作为局部支持的高斯函数的替代品。
float cubicPulse(float c, float w, float x)
{
x = abs(x - c); // 计算 x 到中心 c 的距离
if (x > w) return 0.0; // 如果超出范围 w,则返回 0
x /= w; // 将距离归一化到 [0, 1] 区间
return 1.0 - x * x * (3.0 - 2.0 * x); // 使用三次多项式生成平滑脉冲
}
c
: 脉冲的中心位置。w
: 脉冲的宽度(半径)。x
: 输入值,表示当前位置。- 函数首先计算输入值𝑥到中心𝑐的距离,并将其归一化到 [0, 1] 区间。
- 然后使用三次多项式
1.0 - x * x * (3.0 - 2.0 * x)
生成平滑的脉冲形状。
Rational Bump(有理函数凸起)
如果可以接受无限支持(即无论在实数轴上走多远,该函数的值永远不会精确达到零),那么这个函数有时可以作为高斯函数的替代品。
float rationalBump(float x, float k)
{
return 1.0 / (1.0 + k * x * x);
}
Exponential Step(指数阶梯函数)
自然的衰减是一种线性递减量的指数形式,例如粉色曲线exp(−𝑥)。而高斯函数则是二次递减量的指数形式,例如紫色曲线$exp(−𝑥^2)$。你可以进一步推广这一概念,并不断增加幂次,从而得到越来越尖锐的 S 形曲线。对于非常高的𝑛值,你可以近似得到一个完美的台阶函数step()。为了使曲线通过点(1/2,1/2),我在$𝑥^𝑛$项前添加了一个系数。
float expStep(float x, float n)
{
return exp2(-exp2(n) * pow(x, n));
}