重映射函数与算法

Number of views 142

介绍

在编写着色器或进行任何程序化创建过程(纹理、建模、着色、动画等)时,您经常需要以不同方式修改逻辑,使它们按照您的需求表现。通常会使用 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在线地址上尝试

image1739165692044.png

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在线地址上尝试

image1739158512411.png

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 的加速度曲线计算位置值。这种方法确保了物体的运动是平滑且无减速的。这是测试地址

image1739166089398.png

脉冲

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}$的组合生成一个指数脉冲曲线。通过调整𝑘,可以改变脉冲的宽度和形状,使其适用于各种需要快速上升和缓慢下降的场景。这是在线地址

image1739166557700.png

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}
$$

通过调整𝑘 和𝑛,您可以灵活地控制脉冲的形状和衰减速率,适用于需要不同特性的脉冲信号场景。可在线尝试

image1739167003152.png

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,可以灵活地设计具有不同上升和释放特性的脉冲信号。

image1739167533134.png

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 函数非常适合模拟具有衰减振荡的行为,例如弹跳或其他周期性现象。然而,由于其可能产生负值,在需要非负信号的应用中需要注意。可在线尝试

image1739167787761.png

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 时精确地变为零,从而形成一个在有限范围内平滑衰减的效果。

这种函数非常适合需要明确边界的应用场景,例如:

  1. 可控范围的光照衰减。
  2. 距离受限的阴影效果。
  3. 局部影响的模拟(如粒子系统或力场)。

这是在线地址

image1739168102501.png

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 时不会突然停止变化。

特点:

  1. 平滑过渡:该函数在整个 [0, 1] 区间内平滑地从 0 变化到 1。
  2. 简单高效:作为三次多项式,计算成本低,适合实时应用。
  3. 适用场景:可以用于需要轻微非线性调整的场合,例如图像对比度调整、动画曲线设计或程序化生成中的形状控制。

与 smoothstep() (下图绿线)相比,这个函数更适合需要在终点处保持线性变化速率的场景。可在线尝试

image1739168936710.png

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() 函数,您可以轻松实现从简单线性映射到复杂非线性调整的转换,非常适合需要精细控制的应用场合。可在线尝试

image1739169477573.png

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时,抛物线变得更宽,中间部分更平缓。

image1739169836157.png

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 的计算以提高效率。

image1739170046377.png

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() 函数,您可以轻松实现从高动态范围到低动态范围的颜色转换,同时保持图像的视觉质量。这种函数广泛应用于图形学、摄影和视频处理领域。

image1739170367696.png

脉冲、凸起和阶梯状

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);
}

image1739170913912.png

Exponential Step(指数阶梯函数)

自然的衰减是一种线性递减量的指数形式,例如粉色曲线exp(−𝑥)。而高斯函数则是二次递减量的指数形式,例如紫色曲线$exp(−𝑥^2)$。你可以进一步推广这一概念,并不断增加幂次,从而得到越来越尖锐的 S 形曲线。对于非常高的𝑛值,你可以近似得到一个完美的台阶函数step()。为了使曲线通过点(1/2,1/2),我在$𝑥^𝑛$项前添加了一个系数。

float expStep(float x, float n)
{
    return exp2(-exp2(n) * pow(x, n));
}

image1739171368615.png

0 Answers