05 smoothstep 与平滑边缘

前置知识:第 4 章的 step、mix 和遮罩。
本章示例:硬边与软边,完整源码。

学完本章能做什么

你将能解释 smoothstep 的三个参数,控制边缘位置和过渡宽度,并用多个平滑遮罩做出柔和条带与矩形。

先看现象

打开示例,默认画面上半部分使用 step,下半部分使用 smoothstep。拖动“羽化半宽”:上半部分的分界仍突然改变;下半部分出现更宽或更窄的颜色过渡。切换到“柔和竖条”或“柔和矩形”,可以看到两侧或四侧的边缘共同作用。

1. 三个参数分别做什么

float t = smoothstep( edge0, edge1, x );

在 edge0 < edge1 时,可以这样读:

x 所处范围 输出
x <= edge0 0.0
edge0 < x < edge1 从 0 平滑变化到 1
x >= edge1 1.0

与 step 只有一个跳变位置不同,smoothstep 有一个过渡区间。示例使用:

float soft = smoothstep(
  uCenter - uFeather,
  uCenter + uFeather,
  vUv.x
);

过渡区间中心是 uCenter,总宽度是 2.0 * uFeather。例如中心为 0.5、半宽为 0.1,颜色从 x = 0.4 开始变化,到 x = 0.6 达到另一端。若只是把中心从 0.5 改成 0.7,边缘会移动而宽度不变。

不要倒置两个 edge。 GLSL 对 edge0 >= edge1 的结果没有定义。想让颜色从 1 降到 0,应写 1.0 - smoothstep(edge0, edge1, x),仍保持 edge0 < edge1。

2. 为什么看起来比线性过渡柔和

可以把 smoothstep 想成先把输入归一化到 0.0~1.0,再改变它的变化速度:

float q = clamp( ( x - edge0 ) / ( edge1 - edge0 ), 0.0, 1.0 );
float t = q * q * ( 3.0 - 2.0 * q );

中间值仍为 0.5,但在区间起点和终点附近,变化会放慢,接到两侧常量区域时更自然。这里的表达式用于理解原理,实际代码直接调用 smoothstep 即可。

smoothstep 只根据当前片元的一个输入值计算输出;它不会读取周围像素,也不是图像模糊滤镜。过渡宽度若比屏幕上的一个像素还窄,仍可能出现锯齿或闪烁。第 21 章讲 SDF 边缘时会结合屏幕导数处理这一问题。

3. 同一边界的硬、软比较

本章示例在上半区域选硬边,下半区域选软边:

float hard = step( uCenter, vUv.x );
float soft = smoothstep( uCenter - uFeather, uCenter + uFeather, vUv.x );
float mask = vUv.y >= 0.5 ? hard : soft;

最后仍用 mix(darkColor, lightColor, mask) 输出。两部分的中心一样,因此唯一改变的是边界附近如何过渡。

4. 把两个软边夹成一条带

一条从左进入、从右离开的软带可以写成:

float left = smoothstep( leftEdge - feather, leftEdge + feather, vUv.x );
float right = 1.0 - smoothstep( rightEdge - feather, rightEdge + feather, vUv.x );
float verticalBand = left * right;

left 从 0 升到 1;right 从 1 降到 0;相乘后只有两者都接近 1 的中间区域是亮的。与第 4 章的 step 差值带不同,这两个边缘拥有连续过渡。示例以 uCenter ± 0.18 确定两条边界。

要让中间保持明显的平坦区域,左右边缘应留出足够距离。如果 feather 过大,两个过渡区间互相覆盖,中间也可能达不到 1.0。这并非编译错误,而是两个软遮罩相乘的结果。

5. 从软带到软矩形

横向再做一次“进入与离开”的遮罩,然后与竖带相乘:

float bottom = smoothstep( 0.32 - feather, 0.32 + feather, vUv.y );
float top = 1.0 - smoothstep( 0.68 - feather, 0.68 + feather, vUv.y );
float rectangle = verticalBand * bottom * top;

乘法在四角会把两个较小的软值再次相乘,因此角落通常比单侧边缘更暗。若改用 min(verticalBand, bottom * top),角落的过渡外观会不同。后续 SDF 章节会给出按距离控制整个轮廓边缘的方法。

常见问题

本章小结

smoothstep 把阈值扩展成一个可控的平滑区间。两个相反方向的软边构成条带,横竖条带再组合成矩形。下一章会利用 floor 与 fract 把同一段局部图案复制到许多 UV 单元中。

本章练习