Shader-RayMarch
光线步进(Ray Marching)是我在 Shader 里最喜欢的玩法:不用模型、不用贴图,几行距离函数就能在现场“长”出一个世界。
这篇把自己这些年踩过的点整理一遍:从为什么要“走”而不是“算”,到距离场的性质、软阴影/AO、体积云、分形,再到 URP 里怎么把它塞进管线。

上面这个曼德球(Mandelbulb)没有网格、没有顶点,整张图是一个像素一个像素步进出来的。
1. 先分清三件事
| 方式 | 怎么得到像素 | 擅长 | 不擅长 |
|---|---|---|---|
| 光栅化 | 把三角形投影到屏幕,填像素 | 快、有硬件支持、适合海量网格 | 透明、体积、精确反射/折射 |
| 光线追踪 | 查询射线与场景交互 | 可用于反射、阴影与光传输 | 需要加速与采样;并不自动物理正确 |
| 光线步进 | 射线按“距离场”一步步往前挪,撞到就停 | 体积、分形、变形表面、程序化场景 | 每像素成本高,没有专用硬件 |
光线步进是沿射线推进的一类方法,也可以成为光线追踪中的求交或体积积分手段。其中球体追踪依赖距离或保守距离界;普通固定步长体积积分并不要求每步知道最近表面距离。
程序化场、体积和隐式表面常适合步进,但仍受表示精度、边界、步长保守性和预算限制,不是所有形状都能无条件稳定渲染。
2. 从求交到步进
射线查询可以使用解析求交、数值方法或距离场追踪,并结合 BVH 等加速结构;不要求所有几何都有封闭解析解。
光线步进换了个思路:不求解,只前进。

起点是相机,方向是像素对应的射线。每一步都问一次“最近表面有多远”,得到一个距离 d,然后直接前进 d。理由是:以当前点为中心、d 为半径的球里不可能有任何表面,这段路是绝对安全的,可以放心跳过。
这个方法叫球体追踪(Sphere Tracing),John C. Hart 在 1996 年提出。它的效率完全取决于距离函数的质量:距离估得准,几大步就能到;估得保守,就得慢慢挪。
1 | |
map(p) 就是场景的完整描述——几何、材质、甚至动画,全在里面。这也是为什么步进的场景写起来像在“编程”而不是“建模”。

原笔记配图:球体追踪使用安全距离推进的二维示意,步长不能随意大于可保证的空域距离。
3. 距离场:SDF
SDF(Signed Distance Field)是一个标量函数:点在表面外为正,表面内为负,绝对值是到最近表面的距离。
1 | |
这些解析场可以通过刚体变换和组合构造程序化模型。缩放需要同步修正距离尺度,复杂几何的表达与执行成本也不能概括为固定几十字节。
SDF 最重要的一条性质是 Lipschitz 连续:
1 | |
翻译过来就是“距离函数变化得不会比空间距离更快”,所以用 sdf(p) 当步长永远跨不过表面,这是球体追踪成立的前提。
非精确场并不一定破坏保守性;需要检查它是否可能高估到表面的距离。噪声位移、非均匀缩放和距离估计都可能改变距离界,应按具体构造处理:
- 经验系数可以缓解小幅过估,但不能代替保守界的证明;
- 若有可靠的 Lipschitz 上界,可据此约束步长。局部
f/length(grad(f))是一种近似修正,不保证对任意场恢复全局安全距离,不能视为普遍正确的替代。

原笔记配图:点集构造距离场的旧实验示意,距离必须说明相对于哪些几何对象。

原笔记配图:从雪花二值形状生成距离纹理的原示例;纹理中的值需要另行约定符号和尺度。
4. 组合:并、交、差、平滑
有了距离,布尔运算就是几个 min/max:
1 | |
平滑并集(smooth union)是步进的独门绝技——光栅化里两个网格要融合得靠建模软件烘焙,步进里改一个参数就行:
1 | |
k 越大,过渡越“黏”,可以做出融化的金属、水滴粘连、黏土质感。

平滑组合通常不再是精确欧氏距离,但“不精确”不等于必定过估。低估会让步进更保守,过估才可能跨过表面;应分析所用组合的距离界,而不是仅凭平滑二字固定乘安全系数。
5. 步进循环的三个参数
看似简单的循环,实际调参全在这三个地方:
1 | |
eps(命中阈值):太小 → 表面永远“差一点”,步数烧光;太大 → 表面浮起、边缘出现台阶和空心。经验值是0.0005 ~ 0.002,并且最好和t挂钩,远处放宽。maxSteps:限制最坏工作量。合理数值取决于每步复杂度、覆盖率与设备,不能按桌面/移动给出通用固定范围。tmax(最远距离):必须给,否则一条射向空处的光线会把循环跑满。
还有一个隐蔽的坑:擦边射线。射线几乎平行于表面掠过去时,每步 d 都很小,步数会在局部暴涨。这类像素占比不高,但会让最坏耗时翻倍,所以移动端常配合“半分辨率 + 上采样”来控制。
6. 法线:有限差分
步进只给出“撞到哪”,法线得自己求。一种常用方法是四点采样(四面体技巧),相对六次场查询减少两次,即约三分之一:
1 | |
采样间距要和场景尺度匹配:固定 0.001 在公里级地形上会抖,在毫米级物体上又太粗。可用 eps * t 动态缩放。
7. 软阴影与 AO
步进的另一个好处是:阴影不需要 shadow map,直接从表面上朝光源再步进一次。
1 | |
h / t 的几何含义是“这条光线在遮挡物边缘上张开的角”,取整条路径上的最小值,就是半影的浓淡。这个技巧叫 soft shadow by SDF,比 PCF 便宜得多,而且没有分辨率问题。
环境光遮蔽(AO)同理,沿法线方向采样几次距离,越近的遮挡贡献越大:
1 | |
转折处变暗、缝隙变脏,这些“GI 的廉价近似”让步进场景不至于塑料感。
8. 体积:从表面到积分
前面都是“撞到表面为止”。如果把 map 理解成密度场,步进就变成沿射线做积分——云、雾、烟、光柱都归这一类。

思路是:定步长前进,每一步累加光照贡献,同时按 Beer-Lambert 定律衰减透射率:
1 | |
几个实践点:
- 步长决定质量:定步长会出现层状条纹(banding),常见的解法是每像素随机偏移起点,再靠 TAA 或蓝噪声在时间上抹平。
- 相函数(Henyey-Greenstein 或最简单的
pow(saturate(dot(rd, L)), 8))决定云是“薄纱”还是“边缘发亮”。前向散射强的云,逆光时会有一圈亮边。 - 需要光源可见性:湿云要好看,每步还得朝太阳再步进几次算阴影(也就是所谓的 light march),这是云渲染里最贵的一块,通常用低分辨率 shadow map 或半分辨率 pass 顶替。
- 和场景混合:体积在前、硬表面在后时,用场景深度裁剪
tmax;硬表面在前就先画完再叠体积。
9. 分形:距离估计的进阶
分形是步进的主场,因为它没有“表面”这种东西——只有一个迭代规则。以 Mandelbulb 为例,距离估计(Distance Estimator)长这样:
1 | |
这类距离估计通常不是精确距离,是否为可靠下界需结合具体推导。经验安全系数可缓解部分问题,但不证明不会越界;应同时限制迭代、步长和命中阈值。
同样的套路还能做无限重复(domain repetition)、扭曲、Mandelbox、KIFS——本质都是“把点变换进一个基本空间,用基本 SDF 估计距离”。
10. 地形与高度场
高度图也能步进,而且思路和 SDF 不太一样:没有解析距离,只能“按高度差约束步长”。

1 | |
这个示意循环用 minStep 防止停滞,但最小步长也可能跨过细节,不是保守求交保证。可靠实现需要结合坡度界、命中区间与细化;高度采样和解析场计算谁更贵,应按缓存、场复杂度与设备测量。
11. 在 URP 里怎么跑
步进在引擎里一般是全屏 Pass:拿相机的逆视图投影矩阵,为每个像素重建射线,然后算,最后和场景深度混合。
1 | |
1 | |
几个落地经验:
- UV 翻转:
Blit在不同平台上 UV 的 Y 方向会反,GL.GetGPUProjectionMatrix的renderIntoTexture参数要和实际一致,否则整屏上下颠倒或左右镜像。 - 和深度混合:读
_CameraDepthTexture还原场景深度,作为tmax,这样步进结果才能正确被墙体遮挡;只做“叠加式”的雾/体积可以不做深度测试,但云这种要。 - 分辨率:全屏步进成本取决于覆盖、每步运算与退出分布。降分辨率、引导上采样和抖动是可评估的优化,但不是所有移动场景都必须采用的固定路径。
- 不用改管线也行:Built-in 里
OnRenderImage挂 Blit 就能跑,URP 里没有这个钩子,必须走 Renderer Feature;如果只是想在场景里放“一个步进的物体”,也可以用Graphics.DrawProcedural或一张透明面片写深度。
12. 常见坑与优化
- 表面黑点/漏光:
eps小于步长导致的“穿过去”,加速系数反而会更糟;先检查是不是距离函数不精确。 - 远处锯齿和闪烁:步进天然没有抗锯齿,靠 TAA、
dither抖动初相位、或超采样。静态图可以用累积帧。 - 边缘发灰(光晕):
eps和背景混合造成的,命中后可用smoothstep做一点透明度,或者接受一点点软边。 - 性能墙:最贵的三件事——每步的
map调用次数、软阴影/体积的嵌套循环、过高分辨率。优先级排序通常是:降分辨率 > 砍阴影步数 > 简化 map > 降主步数。 - 精度:
half在长距离步进上会明显劣化(表面抖动、命中失败),距离累计用float。 - 最大步数需要上限:动态循环并不普遍要求编译期常量,但循环、分歧与展开策略会影响性能。使用清楚的上界并检查目标编译结果,不把常量当作所有平台的合法性前提。
13. 参考
- Inigo Quilez:Ray Marching and Signed Distance Functions
- Inigo Quilez:Distance functions 合集
- Inigo Quilez:Soft shadows
- Inigo Quilez:Ambient occlusion
- Wikipedia:Ray marching
- Wikipedia:Mandelbulb
14. 原配图与来源
- 曼德球渲染图:Falco Badermann,CC0,Mandelbulb fractal.png
- 球体追踪示意图:Teadrinker,CC BY-SA 4.0,Visualization of SDF ray marching algorithm.png
- 平滑并集动画:Streepje8,CC BY-SA 4.0,Raymarching blend 800x400.gif
- 实时步进地形:Heyx3,CC BY-SA 4.0,Real-time Raymarched Terrain.png
- 体积云:Thomas M Bishop,CC BY-SA 4.0,Volumetric Cloud.jpg
15. 距离与步长的适用条件
精确 SDF 的绝对值是到最近表面的距离,而非到场景原点的距离。球体追踪示例默认从表面外开始、射线归一化且场查询提供保守距离;位于物体内部时,简单 d<eps 会立刻误判,需要单独定义穿出或双面行为。
高度差并不天然是沿射线可安全前进的距离。地形掠射角和高坡度可能使固定最小步长跨过细节,须使用相交区间、保守坡度界或细化策略。
原 SDF 笔记日期为 2021 年 9 月 6 日,RayMarch 简记为 2022 年 5 月 3 日;本文按较早记录归档,后续内容保留原更新历史。
总结
光线步进的可靠性来自场表示、保守距离与误差控制。球体追踪、固定步长表面积分与体积积分需要不同前提;优化步数之前,先验证坐标、距离尺度与命中条件。