中EN
精修课概念页

二维 SDF:问每个像素「我在形状里吗」

2D SDF Shapes

更新于 2026-10-15 约 22 分钟SDF距离场GLSL程序化形状

换个问法

在一般的绘图库里,画圆就是调用一句 circle(x, y, r)。片元着色器里没有这种命令,你也没法「把一个形状放到画布上」。每个像素只能问自己一个问题:

我在这个形状的里面,还是外面?

整节课都在想办法回答这个问题。答案先是 0 和 1,后来会变成一个连续的距离,而这个距离能做的事情比 0 和 1 多得多。

方块:在格子纸上留边

想象一张 10 × 10 的格子纸,作业是在中间画一个 8 × 8 的方块。你会怎么做?把第一行、最后一行、第一列、最后一列空出来,其余都涂上。

换到着色器里,每个格子就是一个像素,它知道自己的坐标。「空出左边一列」就是 step(0.1, st.x):横坐标小于 0.1 时得 0,否则得 1。四条边各来一次,再乘在一起:只有四个都是 1,结果才是 1。乘法在这里就是逻辑「与」。

左边下边右边上边方块×××=
四个 step 各管一条边,乘起来就只剩中间的方块。右边和上边的做法是先把坐标翻过来(1.0 − st),再重复左边和下边的步骤,就像把纸翻个面。

step 还能一次处理两个分量:step(vec2(0.1), st) 同时算出左边和下边。下面的版本把 step 换成了 smoothstep,这样边缘可以变软:

用四条边拼出一个方块
改一改

也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。

柔边为 0 时就是硬边的 step;往右拖,边缘从清晰慢慢变成一片模糊。

圆:到圆心的距离

方块好画,是因为它的边和坐标轴对齐。圆没有这种便宜可占,得换个思路:想想圆规。把一只脚扎在圆心,张开到半径,转一圈。换成「每个像素自己判断」,就是算一算我离圆心多远,然后和半径比一比。

距离有三种算法:distance(a, b)、length(a - b),以及用勾股定理手写 sqrt。三者结果完全相同。

下面先把距离直接当成亮度显示出来,再在某个半径处「切一刀」。拖「切开」滑块,在两种显示之间切换:

距离 → 亮度 → 切出一个圆
改一改

也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。

切开为 0 时看到的是距离本身:中心最暗,往外越来越亮。为 1 时,距离小于半径的像素被涂成黄色。

距离场:把空间重新解释一遍

回头看那张亮度图,把「暗」想成「高」,它就像一座圆锥的俯视图。站在锥顶往外走,不管朝哪个方向,走到同一个高度的距离都一样。在不同高度横着切一刀,就切出大小不同的圆。

侧视俯视
侧视:在不同高度横切圆锥。俯视:每一刀都切出一个圆,切得越低,圆越大。

这种做法就是距离场 (distance field):不直接描述形状,而是给空间里每一点算出「它离某个东西有多远」。形状只是距离场的一条等高线。字体轮廓、UI、3D 图形里都在用它。

有符号:里面是负的

把距离减去半径,就得到圆的有符号距离函数 (SDF):

d(p)=∥p∥−rd(\mathbf{p}) = \lVert \mathbf{p} \rVert - r
d < 0d = 0d > 0里面边上外面d = length(p) - r
SDF 的值本身就告诉你在哪:小于 0 在里面,等于 0 正好在边上,大于 0 在外面。数值的大小就是离边有多远。

有了这个符号,很多事都变成了一行:

  • 实心:d < 0.0 的地方涂色;
  • 柔边:在 d = 0 附近用 smoothstep 过渡;
  • 描边:abs(d) - 宽度,把轮廓线加粗成一个环;
  • 多层轮廓:对 d 取 fract,等高线一圈圈排出去。

下面把一个圆角矩形和一个圆的距离场画了出来:黄色是里面,蓝色是外面,深色的线是 d = 0,淡淡的纹路是每隔 0.04 一圈的等高线,像枯山水里耙出的沙纹。

看见距离场
改一改

也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。

把圆角拖到 0,圆角矩形会变回直角矩形。注意离形状越远,等高线就越圆。

合并、相交、挖洞

距离场最方便的地方在于组合。要把两个形状放在一起,取离当前像素更近的那个距离就行,也就是 min。同样的思路还能做出交集和差集:

组合写法意思
并集min(d1, d2)两个形状都要
交集max(d1, d2)只要重叠的部分
差集max(d1, -d2)从第一个里挖掉第二个
平滑并集见下面的代码靠近时像水银一样黏在一起

平滑并集多了一个融合半径 kk。先算一个混合比例 hh,在两个距离之间插值,再往里收一点:

h=clamp ⁣(12+12 d2−d1k, 0, 1),d=mix(d2,d1,h)−k h(1−h)h = \mathrm{clamp}\!\left(\tfrac12 + \tfrac12\,\frac{d_2 - d_1}{k},\ 0,\ 1\right),\qquad d = \mathrm{mix}(d_2, d_1, h) - k\,h(1-h)

最后减去的 k h(1−h)k\,h(1-h) 在两个形状交界处最大,正好把它们中间的缝填满。这也是 metaball 一类「软物体」的做法。

两个圆的各种组合0.0s
改一改

也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。

默认是平滑并集。把代码里其他几行的注释打开(同时注释掉第一行),看看并集、交集、差集和轮廓分别长什么样。

极坐标:让半径跟着角度变

上一课用 length 和 atan 把像素换算成了极坐标:半径 r 和角度 a。画圆时,所有角度的半径都一样。如果让半径随角度变化,圆就会变成花瓣、齿轮、雪花——而控制这个变化的,正是第二课的塑形函数。

一个半径随角度起伏的圆0.0s
改一改

也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。

代码里还有三行备选的半径函数,一次只留一行,看看花瓣、齿轮和雪花尖角是怎么来的。

多边形:把距离场和角度结合起来

正多边形可以把两种思路结合起来,这个思路来自 Andrew Baldwin。每条边都有一个朝外的法线方向,一共 n 个,均匀地分布在一圈上。对每个像素,找出角度离它最近的那条法线,再把像素位置投影到这条法线上:投影长度就是这个像素在多边形里「走了多远」。

正多边形的距离场0.0s
改一改

也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。

边数越多越接近圆。它算出来的是一个距离场,所以等高线、描边、组合这些招数照样都能用。

在游戏里怎么用

取舍与常见的坑

小结与练习

  • 片元着色器画形状,靠每个像素自己判断「在不在里面」。
  • 方块是四个 step 相乘;圆是到圆心的距离和半径比较。
  • 把距离减去半径得到 SDF:负数在里面,0 在边上,正数在外面。柔边、描边、等高线都由它而来。
  • min、max 和平滑并集能把简单形状组合成复杂形状;极坐标和多边形距离场能做花瓣、齿轮和正多边形。

练习:只用这一课的工具,画一只企鹅的剪影:一个圆角矩形当身子,一个圆当头,用平滑并集把它们黏在一起,再用差集挖出两只眼睛。

  1. The Book of Shaders: Shapes
  2. thebookofshaders.com/07

相关页面

图形学塑形函数Shaping Functions塑形函数把归一化到 [0,1] 的一维输入弯成可控制的曲线,是片元着色器里混合颜色、画边、做动画缓动的「基本功」。GLSL 的 pow/step/smoothstep/sin 等多为硬件加速;掌握一维再扩展到二维空间。图形学着色器中的颜色Color in Shaders在 GLSL 里颜色与空间坐标共用 vec2/vec3/vec4:可用 .rgba / .xyzw / .stpq 或下标访问,并 swizzle 任意重排。mix 在 [0,1] 上混合;HSB(HSV)比 RGB 更直观,还可映到极坐标色轮。图形学着色器二维矩阵变换2D Matrix Transforms in Shaders在片元里「移动形状」其实是移动坐标系:平移加向量;旋转与缩放用 mat2 乘到坐标上。绕画布中心变换时,要先把空间挪到原点、变换、再挪回。矩阵乘法顺序有关;同一套矩阵也可拿来做 YUV↔RGB 等颜色空间线性变换。图形学程序化图案Procedural Patterns片元着色器按像素执行:形状重复多少次,计算量几乎不变,因此特别适合平铺图案。核心手法是放大 st 再 fract 得到重复的 [0,1] 子空间,再在格子内画形、做矩阵变换,或按奇偶行错位、按格子旋转拼 Truchet。图形学隐式曲面与有符号距离函数Implicit Surfaces & Signed Distance Functions隐式曲面用 f(\mathbf{p})=0 定义:代入点结果为零即在曲面上。返回到最近表面有符号距离的 f 称为有符号距离函数 (SDF)。它们便于做光线求交、构造实体几何 (CSG)、混合与变形,通常用光线步进可视化,也可用移动立方体等方法转为三角形。图形学片元着色器与 GLSL 基础Fragment Shaders & GLSL Basics片元着色器(fragment shader)是在每个像素(片元)上并行执行的小程序:输入位置等信息,输出颜色。GLSL 是 OpenGL / OpenGL ES / WebGL 共用的着色语言;本书用全屏 billboard 上的像素着色练习,建立程序化图形的基础心智模型。图形学细胞噪声Cellular NoiseSteven Worley(1996)《A Cellular Texture Basis Function》——书中注明约在 Perlin 原始噪声发表十六年之后、其 Simplex Noise(2001)五年之前——把程序纹理建立在「到最近特征点的距离」上,即细胞噪声 / Worley 噪声。GPU 上用格子 + 邻域搜索代替「对全体点求最小距离」;再多存最近特征点身份即可得到 Voronoi 图。后续有 2×2 邻域加速、精确边界与「voro-noise」等变体。图形学噪声函数Noise FunctionsKen Perlin 为电影《电子世界争霸战》(Tron) 设计的噪声,解决「看起来随机却不像电视雪花」的程序纹理问题。入门路径是值噪声 (value noise):格子角点取伪随机,格内用平滑曲线插值;进阶有梯度噪声 (gradient noise)、五次插值改进版,以及用单纯形网格降复杂度的 Simplex Noise。

输入关键词开始搜索

滚轮或双指缩放 · 拖动平移 · 双击放大 / 还原