每个像素发出一条光线
光栅化是“三角形找像素”,光线步进反过来,是“像素找表面”。每个像素根据相机位置 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把 smooth union k 调到 0.001,再调到 0.8,观察融合程度。
- 2在 map 里加一个 sdSphere,并用 max(a, -b) 从圆环上挖一块。
- 3用 mod(p.xz, 3.0) - 1.5 重复空间,变出无限多个物体。
还没有输出。console.log 的内容会显示在这里。
运行中