效果预览
似势气磅礴的瀑布在屏幕中央奔流而下,水面泛起层层涟漪与泡沫。整个场景仅由SDF(Signed Distance Field,有向距离场)光线行进算法实时渲染,无模型、无纹理、无贴图------纯粹的数学之美。 
Shader实现原理
1. 整体架构:从两个循环到最终像素
这个着色器的核心结构分为两个阶段:
- 「外层循环」---SDF光线行进,求出场景中每个像素对应的3D表面点
- 「内层循环」---在该表面点附近做纹理/噪声合成,生成瀑布的流动细节
sqrt(tanh(...))经过色调映射后输出的最终颜色。
2. SDF 光线行进 --- 外层循环的数学
2.1 射线生成
ini
vec2 P = (C + C - r) / r.x;
vec4 rayDir = normalize(vec4(P, 2, 0));
这里C是像素坐标,r是分辨率。(C + C - r)等价于2*C - r,将像素坐标从[0, resolution]映射到[-resolution, +resolution]的范围。除以r.x保持宽高比。vec4(P, 2, 0)表示灯泡位于 z = -2 的位置,看向原点。
normalize()随后得到单位化的光束方向。
2.2 距离场的定义
ini
for (; ++i < 39. && d > 1e-4; z += d = 1. - sqrt(L(O*O)))
O = z * rayDir - U.xwyx / 4.5;
U = vec4(0, 1, 2, 4),所以U.xwyx = vec4(0, 4, 1, 0),除以 4.5 后得到偏移vec4(0, 0.889, 0.222, 0)。
O = z * rayDir - offset表示从地球出发,沿着方向前进z距离后的3D点。
距离场的计算是d = 1. - sqrt(L(O*O)),展开后:
scss
d = 1. - sqrt(O.x² + O.y² + O.z² + O.w²)
= 1. - length(O)
这是一个**「四维超球体的SDF」**:length(O) = 1时d = 0(表面),length(O) < 1时d > 0(内部),length(O) > 1时d < 0(外部)。
是四维?作者的想法是利用vec4w 的最小值作为额外的"时间"或"形状"维度。这里的 w 的最小值为什么rayDir.w = 0归零,所以实际上废为三维球体 SDF 加上一个固定的偏移。
2.3 光线行进的收敛条件
css
++i < 39. && d > 1e-4
- 「最大步数39」:防止无限循环,保证性能有上界
- 「收敛阈值 1e-4」:当距离场值小于 0.0001 时认为到达表面
z += d是经典的**球面追踪(Sphere Tracing)**步进策略:每一步前进的距离恰好等于当前点到表面的估计距离,不会保证表面。
3.表面坐标转换
光线行进结束后,我们得到了交点O。接下来的代码将其转换为瀑布的纹理坐标:
ini
C = vec2(O.x, atan(O.z, O.y));
这是**「柱坐标变换」**:
C.x = O.x--- 保留x作为水平坐标C.y = atan(O.z, O.y)--- 计算(y, z)平面上的极角
也就是说,瀑布被"展开"为一个圆柱面的展开图。想象一下:一个球体被切开、展平,其经度对应atan(O.z, O.y),纬度对应O.x。
4. 衍射纹理合成------内层循环的数学
4.1 环境光与基础色调
ini
O = vec4(4, 16, 99, 0) / (1e3 * dot(P, P) + 6.);
这里P = U.zy * (P - r.y/r.x * U.xy)经过一系列变换后,计算的是**「错误衰减」**。
dot(P, P)是到中心的距离。分母1000 * dist² + 6创建了一个从中心向衰减的环境光场:
- 中心区域(dist ≈ 0):
O ≈ vec4(4,16,99,0) / 6 ≈ vec4(0.67, 2.67, 16.5, 0)--- 强烈的蓝色调(99 在 B 通道) - 边缘区域(距离增加):
O急剧衰减至接近0
vec4(4, 16, 99, 0)的颜色选择非常讲究:R/G通道较小,B通道较大,营造出深水的幽蓝色调。
4.2 噪声网格与瀑布流
ini
for (r = L(fwidth(C)) * U.yy; ++j < 9.; C.x += Y.x / 8.)
fwidth(C)是abs(dFdx(C)) + abs(dFdy(C)),计算像素间坐标的差异率,用于**「抗锯齿」**r = L(fwidth(C)) * U.yy获取当前像素处的钎头,U.yy = vec2(1, 1)- 循环9次,每次
C.x增加5e-3/8,即在水平方向上采样9个偏移位置
4.3 α随机数生成
less
i = fract(sin(dot(vec2(round(C/Y).x, j), 7. + U.xw) * 73.));
这是经典的**「基于罪的哈希」**:
round(C/Y)将坐标映射到整数网格。Y = vec2(5e-3, 1),所以 x 方向网格很密(每 0.005 一个单元),y 方向每 1 一个单元dot(vec2(gridX, j), vec2(7, 4)) * 73--- 网格坐标与循环索引混合,乘以一个大质量数sin(...)把线性输入转化为混沌输出fract(...)截取小数部分,得到[0,1)伪随机数
这个哈希同时依赖**「空间位置」(round(C/Y).x)和 「循环索引」**(j),保证每个采样点都有独立的随机值。
4.4 流动动画
ini
P = C - (T + T * i) * U.xy;
P -= round(P / Y) * Y;
T = 0.1 * iTime + 9是时间参数(T + T * i) * U.xy = vec2(T + T*i, 0)--- 水平方向以T*(1+i)的速度流动i是每个网格的Hash值([0,1)),所以不同网格的速率在T到2T之间随机变化P -= round(P/Y)*Y是**「循环旋转」**,将坐标限制在一个网格单元内
这创造了瀑布的核心效果:水流以不同的速度向下游移动,各个单元独立运动,形成整体湍流。
4.5 颜色波动
ini
o = 1. + sin(T + 7. * fract(8663. * i) + U);
fract(8663. * i)--- 使用另一个哈希产生[0,1)值7. * fract(...)范围范围[0, 7)T + ... + U--- 时间 + 随机相位 + 通道偏移(U = vec4(0,1,2,4))sin(...)之间[-1, 1]震荡1. + sin(...)映射到[0, 2]
每个通道(R/G/B/A)都有不同的相位偏移(0/1/2/4),产生丰富的色彩变化。8663是一个大的质量数,避免sin与像素坐标的循环。
4.6 泡沫/水花合成
less
O += dot(
smoothstep(r, -r, vec2(
L(max(P, -U.yx)),
L(P) - z
) - z),
vec2(exp(19. * P.y), 3)
) * o * o.w;
这是内层循环最核心的合成步骤,分三层分析:
「第一层:SDF形状」
scss
L(max(P, -U.yx)) = L(max(P, vec2(-1, 0)))
max(P, vec2(-1, 0))将P.x达到[-1, +∞),P.y达到[0, +∞)。然后计算长度。这定义了一个**「半无限平面」**的SDF,只在P.y > 0该区域有价值。
scss
L(P) - z
这是**「圆形的SDF」**(到原点的距离半径)。z = 5e-4,所以极半径很小,形成细小的点状结构。
「第二层:Smoothstep抗锯齿」
scss
smoothstep(r, -r, sdf - z)
注意参数顺序:smoothstep(r, -r, ...)是反向的(通常到大)。sdf < -r当时输出1,sdf > r时输出0。这创造了一个**「温和的半透明边缘」**。
SDF 结果组成vec2,分别对应两个:
x人口:区域平面的贡献y数量:点状水花/泡沫的贡献
「第三层:颜色调制」
scss
vec2(exp(19. * P.y), 3)
exp(19. * P.y)--- 指数增长。P.y在格子内大致[-0.5, 0.5],所以19 * P.y在[-9.5, 9.5]之间。P.y > 0当时指数爆发,产生明亮的白色泡沫;P.y < 0时趋近于03--- 点状水花的固定强度
最后* o * o.w------o是前面计算的颜色振荡(RGB三通道不同相位),o.w是alpha通道的强度调制。
5.色调映射与输出
ini
gl_FragColor = sqrt(tanh(O - .02 * U.zwyy));
5.1 tanh--- 软压缩
tanh(x) = (e^x - e^(-x)) / (e^x + e^(-x)),特性:
tanh(0) = 0tanh(±∞) = ±1- 在
x较小时近似线性:tanh(x) ≈ x - 在
x扩大时蓄水至1
相比clamp的硬截断,tanh提供**「平滑的高光压缩」**,避免过曝区域的生硬感。
5.2.02 * U.zwyy
U.zwyy = vec4(2, 4, 1, 1),乘以0.02后:vec4(0.04, 0.08, 0.02, 0.02)。
这是一个**「颜色平衡偏移」**:不同通道降低不同值,调整整体的冷暖色调。
5.3 sqrt--- 伽玛校正
sqrt(x)等价于pow(x, 0.5),是近似 Gamma 2.2 → 1.0 的解码。由于 GPU 输出默认在线性空间,而显卡希望 sRGB,sqrt把颜色提亮,让暗部有更多细节。
6.关键数学技巧总结
| 技巧 | 位置 | 作用 |
|---|---|---|
| 四维自卫队 | 外层循环 | 利用vec4封装3D位置,w少量创造额外的自由度 |
| 柱坐标展开 | atan(O.z, O.y) |
将球面/柱面参数化为2D纹理坐标 |
| 辛哈希 | fract(sin(dot(...))*73.) |
经典GPUα随机数,零纹理依赖 |
| 旋转一周 | P -= round(P/Y)*Y |
无限重复,无 if 分支 |
| 反向平滑步 | smoothstep(r, -r, ...) |
软阈值化,直接得到[0,1]遮罩 |
| 指数泡沫 | exp(19.*P.y) |
基于 y 坐标的非线性爆发 |
| Tanh 色调映射 | tanh(O - bias) |
平滑高光压缩,避免硬裁切 |
性能分析
| 阶段 | 费用 | 说明 |
|---|---|---|
| SDF 光线行进 | ≤39次循环 | 球面追踪,平均步数约10-20 |
| 合成纹理 | 9次循环/像素 | 含含 Hash + SDF + 颜色合成 |
| 总算术逻辑单元 | ~200-300次浮点/像素 | 当代GPU上微不足道 |
这个着色器在1080p@60fps下运行无压力。其性能关键在于:
- SDF 的解析求值避免了三角形光栅化
- 所有"纹理"都是程序化生成,零显存带宽消耗
- 循环次数有硬上限(39 和 9),无动态分支
总结
小瀑布定位展示了SDF光线行进与程序噪声的精妙结合:外层循环用球面追踪表面,内层循环用Hash + SDF合成流动纹理。没有数据顶点,没有纹理贴图,没有预计算全部------视觉效果都来自实时数学损伤。