20 SDF 的变换与组合

前置知识:第 9 章的二维旋转、第 18 章的有符号距离、第 19 章的圆与矩形 SDF。
本章示例:变换与组合对照,完整源码。

学完本章能做什么

你将能移动、旋转和等比缩放一个 SDF 形状;用并集、交集、差集和平滑并集组合两个形状;还能判断组合结果的零边界与距离数值何时应分开看待。

先看现象

示例的 A 是左侧固定圆,B 是可移动和旋转的矩形。选择“并集”看到两形状连成一块,“交集”只留下重叠部分,“A 去掉 B”会在圆上切出矩形形状的缺口。“平滑并集”在接近交汇处形成过渡桥。勾选“显示 A/B 原轮廓”,可以直接对照组合后的金色边界与红、蓝原边界。

示例始终使用 3:2 的正面平面和 1 × 1 个分段。片元坐标 p=(vUv-0.5)*vec2(1.5,1.0),x/y 都以整块平面的高度为长度单位。组合只是改变片元颜色所依据的标量场,不会切出真实网格轮廓。

1. 变换的是查询点

第 19 章的 sdBox(q,b) 把矩形放在局部原点,并沿局部 x/y 轴。若要把矩形的中心放到 centerB,对当前点 p 查询 sdBox(p-centerB,b):坐标减中心,形状在画面中向正方向移动。旋转与等比缩放也要把查询点送回形状原来的局部空间,因此角度取负,尺寸先除以缩放倍数:

// 片元阶段;p、centerB、b 的单位都是平面高度,scale > 0。
vec2 localB = rotate2D( -angle ) * ( p - centerB ) / scale;
float dB = sdBox( localB, b ) * scale;

末尾乘回 scale 很关键。局部查询点的距离被除以 scale;若不乘回,零边界仍能落在正确位置,但描边宽度和等距线的数值不再按画面单位计算。例如等比放大两倍后,图形上离边界 0.10 个平面高度单位的点,如果只取局部 SDF,会被错误报告成 0.05。平移、旋转以及正的等比缩放并乘回系数都能保持精确欧氏距离。scale=0 会除零,示例滑块只允许 0.50~1.50。

示例的旋转函数沿用第 9 章的列向量约定:

// 片元阶段;返回二维旋转矩阵,输入角度为弧度。
mat2 rotate2D( float angle ) {
  float c = cos( angle );
  float s = sin( angle );
  return mat2( c, s, -s, c );
}

JavaScript 控件显示角度,传给 shader 前用 THREE.MathUtils.degToRad 转为弧度。公式顺序是先减 B 中心,再反向旋转,最后除以等比缩放;若先旋转 p 再减中心,矩形会绕全局原点移动,而不是绕自己的中心旋转。对 vec2 使用不同的 x/y 缩放时,sdBox(p/scaleXY,b) 的零边界仍可正确,但用一个常数乘回无法恢复所有方向的真实欧氏距离;不要把非等比缩放后的值直接当精确 SDF。

2. 用符号理解硬组合

设 dA、dB 都遵循负内正外。只看“当前点是否属于结果”就能推得三种常用组合:

结果 条件 片元阶段的距离场写法
并集 A ∪ B 在 A 或 B 内 min(dA, dB)
交集 A ∩ B 同时在 A 和 B 内 max(dA, dB)
差集 A \ B 在 A 内且不在 B 内 max(dA, -dB)

对并集,只要一个距离为负,min 就为负;对交集,只有两个都为负,max 才为负。差集把 B 的符号翻转:B 内部的 -dB 为正,迫使这些点退出 A。示例中 A 圆以 (-0.17,0) 为中心、半径 0.27;B 矩形的局部半尺寸为 (0.23,0.17),先经过上述变换再参与组合。

min/max 保证常见布尔形状的内外符号和零边界,但组合后的数值不总是到最终边界的精确距离。用一维区间即可看出问题:A=[-2,0.5]、B=[-0.5,2],并集是 [-2,2]。在 x=0,两个原区间各自最近边界仅 0.5,所以 min(dA,dB)=-0.5;但到并集边界的真实内部距离是 2。二维形状的重叠区域也可能出现这种“隐藏边界”问题。交集和差集在某些区域同样可能只是距离估计;即使颜色填充正确,等距线仍可能在组合处弯折或拥挤。

这不妨碍普通填充和抗锯齿边界。若后续要把 d 用于精确线宽、碰撞距离、阴影步进或法线梯度,需要按用途核查误差,而不能因为它仍叫 SDF 就默认处处精确。

3. 平滑并集改变了形状

硬并集在两个场交替成为最小值的位置产生折线式接缝。示例使用一个常见的多项式平滑最小值:

// 片元阶段;a、b、k 的单位相同,k 必须大于 0。
float smoothUnion( float a, float b, float k ) {
  float h = clamp( 0.5 + 0.5 * ( b - a ) / k, 0.0, 1.0 );
  return mix( b, a, h ) - k * h * ( 1.0 - h );
}

当两距离相差超过约 k 时,结果回到较小值;接近时混合并额外减去一个小量,使两个形状之间长出柔和连接。若 a=b=0,结果是 -k/4,说明它真的扩张了形状,不是只把颜色接缝模糊。k 越大,连接越宽,距离值也越不能当作精确欧氏距离。k=0 会导致除零,示例限制在 0.02~0.25。

平滑操作同样可用于其他组合,但不同公式的平滑交集、平滑差集并不只是把 min 改成 max;要逐一确认符号、边界和数值性质。本章先把一种平滑并集讲清楚。

4. 用轮廓和等距线调试

示例默认的金色线表示组合场 d=0。原轮廓开关用红色显示 dA=0、蓝色显示 dB=0;选择等距线后,还会每隔 0.06 个平面高度单位画一条 d=c 的线。比较各模式时可看到:硬并集的金线沿两者外边缘走,交集只留下公共区域边界,差集包含 B 对 A 切出的内边界。平滑并集的金线会偏离两条原边界,过渡不是颜色错觉。

填充仍用第 18 章的 1.0-smoothstep(-aa,aa,d),其中 aa=max(fwidth(d),0.0001) 是片元空间的像素宽度近似。显示原轮廓时分别对 dA、dB 求 fwidth,避免用组合场的导数给另一条线定宽。局部尖点处导数未必连续;细线的抗锯齿是显示处理,不会把不精确的组合场改成精确距离。

常见问题

本章小结

用逆变换把查询点送回形状局部空间,平移和旋转保持距离,正的等比缩放须把返回值乘回系数。min、max、max(dA,-dB) 给出并集、交集、差集的符号与边界;平滑并集改变边界形状。组合后要用原轮廓与等距线检查距离数值,尤其在重叠和切割处。下一章将把这些距离映射为视觉效果。

本章练习