第 18 章练习:有符号距离的直觉

对应教程:18 有符号距离函数的直觉
起始示例:圆的有符号距离,需要修改时使用示例源码。

练习 1:先算出符号和数值

设 sdCircle(p, 0.25) = length(p) - 0.25,坐标和半径均以平面高度为单位。

**目标:**分别计算 p=(0,0)、p=(0.20,0)、p=(0.30,0.40) 和 p=(0.25,0) 的 d,指出点在圆内、边界还是圆外;再判断示例的 1.0-step(0.0,d) 对四点各输出什么。

**完成标准:**能区分距离的符号与大小,并注意硬边 step 在恰好 d=0 时的归属。

练习 2:把零边界变成有厚度的提示带

**目标:**在第 18 章示例中新增一个显示模式:d < -0.04 显示内部色,-0.04 <= d < 0.04 显示醒目的边界带,其他位置显示外部色。改变圆半径,观察整条带是否始终跟着圆周移动。

**要求:**只使用现有 d 和片元颜色分支,不改变 sdCircle、顶点 shader 或网格。提示带宽度使用与 d 相同的平面单位。

**完成标准:**边界带在圆周内外各占 0.04,不会固定在画布上的某一位置;能解释它与只画 d=0 的细线有何区别。

练习 3:只在形状内部描边

**目标:**再新增一个模式:宽 0.06 的描边只出现在圆内侧,也就是大致 -0.06 < d < 0。让描边的两条边都使用已有的 aa 做抗锯齿。

**要求:**保持外侧背景不变;不要使用 abs(d),因为它会把描边扩到圆外。

**完成标准:**放大观察时,外边缘仍位于原圆周 d=0,内边缘位于 d≈-0.06;圆外没有对称的另一半描边。

参考解答


<a id=“reference-answers”></a>

第 18 章练习参考解答

对应练习:有符号距离的直觉。

练习 1:先算出符号和数值

点 p length(p) d=length(p)-0.25 位置 1.0-step(0.0,d)
(0,0) 0 -0.25 圆内 1
(0.20,0) 0.20 -0.05 圆内 1
(0.30,0.40) 0.50 0.25 圆外 0
(0.25,0) 0.25 0 边界 0

step(0.0,d) 在 d=0 时返回 1,所以这个硬边写法把恰好位于数学边界的点归到外侧。实际边界通常穿过像素,像素抗锯齿模式会混合两侧颜色。

练习 2:把零边界变成有厚度的提示带

在 HTML 的 #mode 下拉框追加 <option value="5">边界提示带</option>。把片元 shader 最后处理描边的 else { ... } 整块替换为:

// 片元阶段;d 的单位是平面高度。
else {
  if ( uMode == 5 ) {
    if ( d < -0.04 ) color = insideColor;
    else if ( d < 0.04 ) color = vec3( 1.0, 0.80, 0.30 );
    else color = outsideColor;
  } else {
    float strokeDistance = abs( d ) - 0.5 * uStroke;
    float stroke = 1.0 - smoothstep( -aa, aa, strokeDistance );
    color = mix( outsideColor, vec3( 1.0, 0.72, 0.22 ), stroke );
  }
}

保留前面的模式 0~3 分支及其结束的 };上面代码从 else { 开始,接在其后。模式 4 继续使用原描边算法。判断区间是 [-0.04,0.04),总宽 0.08 个平面高度单位。改变半径会改变 d=0 的位置,提示带随边界一起移动。它有固定的平面距离宽度;示例原有的金色零线则只在边界附近按 fwidth(d) 画约一像素宽。

练习 3:只在形状内部描边

在 HTML 的 #mode 下拉框追加 <option value="6">仅内部描边</option>。若已完成练习 2,将上面的最终 else { ... } 再替换为以下完整分支;只做练习 3 时可删除模式 5 的判断:

// 片元阶段;aa 已由 fwidth(d) 计算。
else {
  if ( uMode == 5 ) {
    if ( d < -0.04 ) color = insideColor;
    else if ( d < 0.04 ) color = vec3( 1.0, 0.80, 0.30 );
    else color = outsideColor;
  } else if ( uMode == 6 ) {
    float insideOfOuterEdge = 1.0 - smoothstep( -aa, aa, d );
    float outsideOfInnerEdge = smoothstep( -0.06 - aa, -0.06 + aa, d );
    float innerStroke = insideOfOuterEdge * outsideOfInnerEdge;
    color = mix( outsideColor, vec3( 1.0, 0.72, 0.22 ), innerStroke );
  } else {
    float strokeDistance = abs( d ) - 0.5 * uStroke;
    float stroke = 1.0 - smoothstep( -aa, aa, strokeDistance );
    color = mix( outsideColor, vec3( 1.0, 0.72, 0.22 ), stroke );
  }
}

外边缘以 d=0 为中心,内边缘以 d=-0.06 为中心;两个软边的重叠部分接近 1。模式 6 的背景始终用 outsideColor,因此圆内非描边区域也显示背景;若想保留圆内填充,可先用 insideColor 作底色,再叠加描边。模式 6 未取 abs(d),所以圆外不会出现对称描边。