二维 SDF:问每个像素「我在形状里吗」
2D SDF Shapes
换个问法
在一般的绘图库里,画圆就是调用一句 circle(x, y, r)。片元着色器里没有这种命令,你也没法「把一个形状放到画布上」。每个像素只能问自己一个问题:
我在这个形状的里面,还是外面?
整节课都在想办法回答这个问题。答案先是 0 和 1,后来会变成一个连续的距离,而这个距离能做的事情比 0 和 1 多得多。
方块:在格子纸上留边
想象一张 10 × 10 的格子纸,作业是在中间画一个 8 × 8 的方块。你会怎么做?把第一行、最后一行、第一列、最后一列空出来,其余都涂上。
换到着色器里,每个格子就是一个像素,它知道自己的坐标。「空出左边一列」就是 step(0.1, st.x):横坐标小于 0.1 时得 0,否则得 1。四条边各来一次,再乘在一起:只有四个都是 1,结果才是 1。乘法在这里就是逻辑「与」。
step 还能一次处理两个分量:step(vec2(0.1), st) 同时算出左边和下边。下面的版本把 step 换成了 smoothstep,这样边缘可以变软:
也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。
圆:到圆心的距离
方块好画,是因为它的边和坐标轴对齐。圆没有这种便宜可占,得换个思路:想想圆规。把一只脚扎在圆心,张开到半径,转一圈。换成「每个像素自己判断」,就是算一算我离圆心多远,然后和半径比一比。
距离有三种算法:distance(a, b)、length(a - b),以及用勾股定理手写 sqrt。三者结果完全相同。
下面先把距离直接当成亮度显示出来,再在某个半径处「切一刀」。拖「切开」滑块,在两种显示之间切换:
也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。
距离场:把空间重新解释一遍
回头看那张亮度图,把「暗」想成「高」,它就像一座圆锥的俯视图。站在锥顶往外走,不管朝哪个方向,走到同一个高度的距离都一样。在不同高度横着切一刀,就切出大小不同的圆。
这种做法就是距离场 (distance field):不直接描述形状,而是给空间里每一点算出「它离某个东西有多远」。形状只是距离场的一条等高线。字体轮廓、UI、3D 图形里都在用它。
有符号:里面是负的
把距离减去半径,就得到圆的有符号距离函数 (SDF):
有了这个符号,很多事都变成了一行:
- 实心:
d < 0.0的地方涂色; - 柔边:在
d = 0附近用smoothstep过渡; - 描边:
abs(d) - 宽度,把轮廓线加粗成一个环; - 多层轮廓:对
d取fract,等高线一圈圈排出去。
下面把一个圆角矩形和一个圆的距离场画了出来:黄色是里面,蓝色是外面,深色的线是 d = 0,淡淡的纹路是每隔 0.04 一圈的等高线,像枯山水里耙出的沙纹。
也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。
合并、相交、挖洞
距离场最方便的地方在于组合。要把两个形状放在一起,取离当前像素更近的那个距离就行,也就是 min。同样的思路还能做出交集和差集:
| 组合 | 写法 | 意思 |
|---|---|---|
| 并集 | min(d1, d2) | 两个形状都要 |
| 交集 | max(d1, d2) | 只要重叠的部分 |
| 差集 | max(d1, -d2) | 从第一个里挖掉第二个 |
| 平滑并集 | 见下面的代码 | 靠近时像水银一样黏在一起 |
平滑并集多了一个融合半径 。先算一个混合比例 ,在两个距离之间插值,再往里收一点:
最后减去的 在两个形状交界处最大,正好把它们中间的缝填满。这也是 metaball 一类「软物体」的做法。
也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。
极坐标:让半径跟着角度变
上一课用 length 和 atan 把像素换算成了极坐标:半径 r 和角度 a。画圆时,所有角度的半径都一样。如果让半径随角度变化,圆就会变成花瓣、齿轮、雪花——而控制这个变化的,正是第二课的塑形函数。
也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。
多边形:把距离场和角度结合起来
正多边形可以把两种思路结合起来,这个思路来自 Andrew Baldwin。每条边都有一个朝外的法线方向,一共 n 个,均匀地分布在一圈上。对每个像素,找出角度离它最近的那条法线,再把像素位置投影到这条法线上:投影长度就是这个像素在多边形里「走了多远」。
也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。
在游戏里怎么用
取舍与常见的坑
小结与练习
- 片元着色器画形状,靠每个像素自己判断「在不在里面」。
- 方块是四个
step相乘;圆是到圆心的距离和半径比较。 - 把距离减去半径得到 SDF:负数在里面,0 在边上,正数在外面。柔边、描边、等高线都由它而来。
min、max和平滑并集能把简单形状组合成复杂形状;极坐标和多边形距离场能做花瓣、齿轮和正多边形。
练习:只用这一课的工具,画一只企鹅的剪影:一个圆角矩形当身子,一个圆当头,用平滑并集把它们黏在一起,再用差集挖出两只眼睛。
在片元着色器里画形,不是发三角形图元,而是对每个像素问「我在形的里面还是外面」。矩形用 step/smoothstep 做轴对齐边界;圆与更复杂形用距离场 (distance field)——到特征点的距离再阈值。这是 2D SDF 程序化造型的入门路径。
矩形:网格纸思维 + step 逻辑与
把画布想成格子纸、每个格子一个线程。要画居中方块:左右上下留边。伪代码是多层 if;换成归一化坐标后用 step:
left = step(0.1, st.x)、bottom = step(0.1, st.y);乘积相当于逻辑 AND(两边都是 1 才白)。- 可一次传入
vec2:borders = step(vec2(0.1), st)。 - 右上边:把坐标翻折
1.0-st再step,等价于「翻页重复左下手续」。 - 四边乘在一起:
bl.x*bl.y*tr.x*tr.y。
练习:改比例;换 smoothstep 得柔边;用 floor 另写一版;封装可复用函数与仅描边版本;多矩形蒙德里安构图。
圆:到圆心的距离
圆规:固定半径,问每点是否在圆内 → 算像素到圆心距离。distance(a,b)、length(a-b)、sqrt 勾股三者结果相同。把距离映到亮度得径向梯度;用 step/smoothstep 在阈值切割即实心圆。可用 u_time 做心跳缩放、多圆心放置。
组合多距离场:+、*、min、max、pow 等会产生干涉状新图案。
距离场的用法与代价
把空间重解释为「到某特征的距离」即距离场:字体 SDF、UI、3D 中都常见。可视化时把坐标移到中心并缩半到 ,用 fract 看等高环。对四象限 abs 后到 (.3,.3) 的距离,min/max 组合可出圆角矩形等。
性能:依赖 sqrt 的路径偏贵;可用 dot 积构造圆形距离场的替代写法(书中工具箱提示)。
距离场一旦有公式,易加柔边、多层描边、布尔组合——这也是 Mapbox GL 标签、GPU 字体等采用 SDF 的原因。
极坐标造型
由 length 与 atan(y,x) 得 、,再让圆的半径随角变化(塑形函数调制)→ 花瓣、雪花、齿轮等。可与距离场结合:按多边形边数在极坐标下构造距离场(Andrew Baldwin 的方形成形思路)。min/max 混合多场可复刻几何 logo。
与本库 SDF 页的分工
- 隐式曲面与有符号距离函数:更一般的隐式曲面、3D SDF、光线步进等。
- 本页:BoS 二维程序化形状课——
step矩形、距离场圆/圆角、极坐标调制与多边形场。
工具箱:LYGIA draw / sdf;PixelSpirit Deck 渐进学 SDF。