跳到主要内容
3D Playground
WebGL 与着色器第 8 / 12 节

光线步进:用距离场画 3D

高级WebGL 2

每个像素发出一条光线

光栅化是“三角形找像素”,光线步进反过来,是“像素找表面”。每个像素根据相机位置 ro 和方向 rd 发出一条光线,沿着它寻找与场景的交点。场景不是网格,而是一个 3D 的有向距离函数 map(p):返回点 p 到最近表面的距离。

球体追踪

距离场有个美妙的性质:以 p 为球心、map(p) 为半径的球内一定没有任何物体。所以每一步都可以安全地前进这么远:

glsl
for (int i = 0; i < uSteps; i++) {
  float d = map(ro + rd * t);
  if (d < 0.001 * t) { hit = true; break; }
  t += d;
}

空旷处步子大,靠近表面时步子小,几十步就能精确命中。把“max march steps”调低,物体边缘会先出问题:那里光线擦边而过,需要更多步。

图元与平滑并集

球、圆环、圆角盒都有简单的 SDF。并集用 min,但 min 会留下尖锐的折痕。平滑最小值 smin 在距离相差小于 k 时把两者混合,形状就像液体一样融合:

glsl
float smin(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);
}

法线:距离场的梯度

表面的法线就是距离场增长最快的方向,也就是梯度。在命中点附近取几个微小偏移,比较 map 的差值即可得到它(本例用四面体四次采样的技巧)。

软阴影与 AO

  • 软阴影:从命中点朝光源再步进一次。如果途中某处离物体很近(h 小)且离起点很近(t 小),就处于半影里,res = min(res, k * h / t)。k 越大,阴影边缘越硬。
  • 环境光遮蔽(AO):沿法线方向采样几个距离。如果实际距离比预期小,说明附近有遮挡,就让它变暗。缝隙和接触处因此自然变暗。

最后加上 Phong 高光、菲涅尔边缘光和距离雾,再做 gamma 校正,一个“没有三角形”的 3D 场景就完成了。

光线步进每个像素要做上百次 map 调用。实时项目里常常降低分辨率渲染,这里由“render scale”控制。拖动画布可以旋转视角。

核心 API

sphere tracingsdSphere / sdTorus / sdBoxsmincalcNormalsoftShadowambientOcclusionreflectnormalize

动手试试

  1. 1把 smooth union k 调到 0.001,再调到 0.8,观察融合程度。
  2. 2在 map 里加一个 sdSphere,并用 max(a, -b) 从圆环上挖一块。
  3. 3用 mod(p.xz, 3.0) - 1.5 重复空间,变出无限多个物体。
运行中