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,避免用组合场的导数给另一条线定宽。局部尖点处导数未必连续;细线的抗锯齿是显示处理,不会把不精确的组合场改成精确距离。
常见问题
- **B 一旋转就绕画面中心转圈:**先从
p减去 B 中心,再乘逆向旋转矩阵。 - **放大 B 后描边距离值偏小:**坐标除以正的
scale后,SDF 返回值还要乘回scale。 - **差集把圆外也填上:**检查
max(dA,-dB)的顺序与 B 的符号翻转。 - 并集填充正确但等距线在重叠处怪异:
min的符号正确,距离幅值在隐藏边界附近不保证精确。 - **平滑并集把两形状粘得太宽:**降低
k;确认k与距离使用同一坐标单位。 - **非等比缩放后线宽各方向不同:**零边界与精确距离是两回事;需要专门的距离修正或使用适配该形状的精确公式。
本章小结
用逆变换把查询点送回形状局部空间,平移和旋转保持距离,正的等比缩放须把返回值乘回系数。min、max、max(dA,-dB) 给出并集、交集、差集的符号与边界;平滑并集改变边界形状。组合后要用原轮廓与等距线检查距离数值,尤其在重叠和切割处。下一章将把这些距离映射为视觉效果。