第 22 章练习:三维 SDF 与 Ray Marching

对应教程:22 三维 SDF 与 Ray Marching
起始示例:三维距离场与逐步前进,需要修改时使用示例源码。

练习 1:手算两次距离前进

有一个球心在原点、半径为 1 的球,射线起点 ro=(0,0,3)、单位方向 rd=(0,0,-1)。假设 uHitEpsilon=0.002、MAX_DISTANCE=16。

**目标:**写出第一次和第二次查询时的 t、采样点和球体距离 d,判断第几次查询命中。如果把 uStepLimit 改为 1,说明示例的 march 返回什么命中标志。

**完成标准:**能区分“查询次数”与“前进次数”,并说明上限为 1 时虽然第一次前进到表面,却没有预算完成第二次命中检测。

练习 2:加入三维圆环

**目标:**在 #shape 菜单新增“圆环”模式。圆环以 Y 轴为轴,外侧悬在现有地面之上,中心的洞能看到背景或地面。选择法线查看模式时,圆环内外侧应有连续的方向变化。

**要求:**在现有 mapObject(vec3 p) 中加入一个分支,用 length(p.xz) 求到 Y 轴的水平距离,再在“水平半径差、竖向高度”构成的二维截面中减去管半径。建议主半径 0.48、管半径 0.18、中心高度 -0.54,使最底端刚好触及 y=-0.72 的地面。其他模式保持原样。

**完成标准:**圆环的中心不是实心圆盘;改变虚拟相机角度仍保持三维环形;阴影和法线模式都来自新增的同一个距离场。

练习 3:比较主射线和阴影射线成本

**目标:**在“明暗与阴影”模式添加一个“计算阴影”开关,使关掉开关时仍保留相同的主射线、形状、法线和漫反射颜色,但不调用 objectShadow。

**要求:**用布尔值或整数 uniform 控制阴影查询;法线与步数模式不受影响。观察地面上的投影与物体暗侧,并解释为什么步数热图不能直接表示开启阴影后的全部场查询次数。

**完成标准:**开关关闭时地面上的物体阴影消失,球体或盒体的基本明暗仍在;开启时恢复。能指出中心差分和阴影射线都是主射线之外的额外场查询。

参考解答


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

第 22 章练习参考解答

对应练习:三维 SDF 与 Ray Marching。

练习 1:手算两次距离前进

查询次数 查询前 t 采样点 ro+rd*t d=length(p)-1 操作
1 0 (0,0,3) 2 未命中,令 t=2
2 2 (0,0,1) 0 0<0.002,命中

所以实际前进只有一次,距离场查询有两次。uStepLimit=1 时,循环第二轮在查询前因 i>=uStepLimit 中断,返回的命中标志为 0.0,即使上一次前进已经恰好到达表面。这说明步数预算也影响“是否来得及确认命中”。

练习 2:加入三维圆环

在示例 HTML 的 #shape 中增加 <option value="4">圆环</option>。在片元 shader 的 mapObject 开头、现有球体分支之前加上:

// 片元阶段;p 是虚拟世界空间坐标。圆环中心在 y=-0.54。
if ( uShape == 4 ) {
  vec3 q = p - vec3( 0.0, -0.54, 0.0 );
  vec2 section = vec2( length( q.xz ) - 0.48, q.y );
  return length( section ) - 0.18;
}

length(q.xz)-0.48 是采样点的水平半径与圆环主半径之差;再与 q.y 组成截面位置,到截面圆心的距离减管半径 0.18。轴心处 length(q.xz)=0,在中心高度的场值是 0.48-0.18=0.30>0,所以洞不会被填满。最低点在 -0.54-0.18=-0.72,恰好接触原地面。若把管半径设得大于或等于主半径,中心洞会闭合,练习要求的视觉结果就不成立。

练习 3:比较主射线和阴影射线成本

在 HTML 控件区加入:

<label><input id="shadow-enabled" type="checkbox" checked /> 计算阴影</label>

在 ShaderMaterial 的 uniforms 中加入 uShadowEnabled: { value: 1 };在片元 shader 的 uniform 声明区加入 uniform int uShadowEnabled;。把原来的阴影查询行改成:

// 片元阶段;p、n、lightDir 均在虚拟世界空间。
float shadow = 1.0;
if ( uShadowEnabled == 1 ) {
  shadow = objectShadow( p, n, lightDir );
}

最后在 JavaScript 的 updateControls() 中,紧挨其他 uniform 赋值加入:

material.uniforms.uShadowEnabled.value =
  document.querySelector( '#shadow-enabled' ).checked ? 1 : 0;

现有 document.querySelectorAll('input, select') 已给新增 checkbox 注册 input 事件,无须另写监听器。关闭时 shadow=1.0,漫反射和高光公式仍照常执行,主射线与法线估计也不变;开启时阴影查询最多增加 40 次 mapObject 采样。热图使用 march 返回的主射线查询次数,法线中心差分的六次 mapScene 采样与二次阴影射线的查询均不在其中,因此它不能表示完整片元成本。