知道了距离就能画出来
一句话总结
如果有一个函数能告诉我们任意点到表面的最短距离,就可以让光线每次安全前进这段距离,最终找到表面。不需要三角形,也不需要顶点缓冲区。
为什么需要了解这些
光栅化以三角形为前提。改变形状时必须重新生成顶点,而合并或相减两种形状属于几何运算,处理起来很棘手。要用三角形表现两滴液体平滑融合,就必须每帧重新生成网格。
距离函数则用数学表达式描述形状。球体只需一行 length(p) - r,两个形状的并集也只需一行 min(a, b)。平滑融合也只需几行代码。由于没有几何数据,几乎不占用内存;改变形状就等于修改表达式。
工作原理
有符号距离函数(SDF)接收一个点,并返回该点到表面的最短距离。位于内部时为负数,位于外部时为正数,恰好在表面上时为 0。
구 length(p) - r
평면 p.y - h
상자 q = abs(p) - b
length(max(q, 0)) + min(max(q.x, q.y, q.z), 0)
关键在于,这个值就是安全的前进距离。如果当前位置与最近表面相距 d,那么无论朝哪个方向移动,前进 d 的距离都不会遇到任何物体。因此,让光线跳过 d 也不会越过表面。重复这一过程,就是球体追踪(sphere tracing),通常也称为光线步进(ray marching)。
t = 0
반복:
d = scene(ro + rd * t)
if d < eps: 맞았다
t += d
if t > 최대거리: 빗나갔다
在空旷区域会大步跳跃,接近表面时则逐步靠近。因此,擦着表面掠过的光线成本最高——距离始终很小,步长会被切得很细。如果按迭代次数着色,就会看到轮廓边缘明亮发光的画面,这是用视觉方式观察性能问题的好方法。
组合形状同样通过函数完成。
합집합 min(a, b)
교집합 max(a, b)
차집합 max(a, -b) # a 에서 b 를 파낸다
平滑并集会混合两个距离。
h = clamp(0.5 + 0.5 * (b - a) / k, 0, 1)
mix(b, a, h) - k * h * (1 - h)
最后一项会使连接区域膨胀,形成平滑过渡。k 越大,融合范围越宽。
法线无需单独保存,可以通过距离函数的梯度求得。只需沿每个坐标轴移动一个极小距离,观察距离差值。距离函数的梯度按定义指向增长最快的方向,也就是表面的外侧方向。
阴影也只需再发射一条光线。从命中点朝光源方向再次步进,如果碰到物体,该点就在阴影中。光栅化中制作阴影贴图、转换坐标时遇到的各种问题,在这里都不存在;代价是每个屏幕像素都要发射两次光线。
性能特征也不同于光栅化。 光栅化成本取决于三角形数量与覆盖的像素数,而光线步进成本取决于每个像素的步数。因此,只要屏幕分辨率不变,无论形状多么复杂,成本都不会显著增加;但分辨率翻倍时,成本会变成四倍。形状变复杂导致性能下降,只会发生在距离函数本身计算变重时。
它还有一项有趣特性:空间重复。对点坐标执行取余运算后再传给距离函数,只需一行函数就能生成同一形状无限重复的场景,而且不会增加任何内存使用。不过,这时距离函数可能大于实际距离,因此需要同时考虑重复周期与形状大小,进行适当修正。
生产现场会看到什么
这项技术成长于用一个着色器绘制整个场景的演示场景与 Shadertoy 文化,如今也用于实际产品。例如云和烟雾等体积表现、物体熔化融合效果,以及无论分辨率如何都能清晰绘制字体和图标的 SDF 纹理。
需要注意的是,距离函数绝不能大于实际距离。如果估计过大,光线会越过表面,造成孔洞。因此,对形状进行非均匀缩放或重复时,需要保守地缩小距离进行修正。
最后说明一下如何与光栅化结合。实际工作中很少用光线步进绘制整个场景,通常会先光栅化一个覆盖屏幕的矩形,再在其片元着色器内执行步进。随后把结果写入深度缓冲区,就能与用三角形绘制的物体自然融合。云和烟雾等体积效果正是这样实现的:形状用距离函数描述,其余场景用三角形绘制,再通过深度把两者合成。
下一次实验要做什么
创建几个距离函数与组合运算,并通过球体追踪寻找表面。先只绘制轮廓,再使用梯度法线添加明暗;随后绘制由交集、差集和平滑并集组合出的形状。最后放置一个地面平面,发射阴影光线,生成球体在地面投下阴影的图像。