效果预览

一道气势磅礴的瀑布在屏幕中央奔流而下,水面泛起层层涟漪与泡沫。整个场景仅由 SDF(Signed Distance Field,有向距离场)光线行进算法实时渲染,无模型、无纹理、无贴图 —— 纯粹的数学之美。

源码地址:https://shader.shuqin.cc/

Shader 实现原理

1. 整体架构:从两个循环到最终像素

这个 shader 的核心结构分为两个阶段:

  1. 外层循环 — SDF 光线行进,求出场景中每个像素对应的 3D 表面点
  2. 内层循环 — 在该表面点附近做纹理/噪声合成,生成瀑布的流动细节

最终的颜色经过 sqrt(tanh(...)) 的色调映射后输出。


2. SDF 光线行进 — 外层循环的数学

2.1 射线生成
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 距离场的定义
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)),展开后:

d = 1. - sqrt(O.x² + O.y² + O.z² + O.w²)
  = 1. - length(O)

这是一个四维超球体的 SDFlength(O) = 1d = 0(表面),length(O) < 1d > 0(内部),length(O) > 1d < 0(外部)。

为什么是四维?作者的意图是利用 vec4 的 w 分量作为额外的"时间"或"形状"维度。这里的 w 分量被 rayDir.w = 0 归零,所以实际上退化为三维球体 SDF 加上一个固定偏移。

2.3 光线行进的收敛条件
++i < 39. && d > 1e-4
  • 最大步数 39:防止无限循环,保证性能有上界
  • 收敛阈值 1e-4:当距离场值小于 0.0001 时认为到达表面

z += d 是经典的**球面追踪(Sphere Tracing)**步进策略:每一步前进的距离恰好等于当前点到表面的估计距离,保证不会穿透表面。


3. 表面坐标转换

光线行进结束后,我们得到了交点 O。接下来的代码将其转换为瀑布的纹理坐标:

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 环境光与基础色调
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 通道)
  • 边缘区域(dist 增大):O 迅速衰减到接近 0

vec4(4, 16, 99, 0) 的颜色选择很有讲究:R/G 通道较小,B 通道很大,营造出深水的幽蓝色调。

4.2 噪声网格与瀑布流动
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 伪随机数生成
i = fract(sin(dot(vec2(round(C/Y).x, j), 7. + U.xw) * 73.));

这是经典的 sin-based Hash

  1. round(C/Y) 将坐标映射到整数网格。Y = vec2(5e-3, 1),所以 x 方向网格很密(每 0.005 一个单元),y 方向每 1 一个单元
  2. dot(vec2(gridX, j), vec2(7, 4)) * 73 — 网格坐标与循环索引混合,乘以一个大质数
  3. sin(...) 把线性输入转化为混沌输出
  4. fract(...) 截取小数部分,得到 [0,1) 的伪随机数

这个 Hash 同时依赖空间位置round(C/Y).x)和循环索引j),确保每个采样点有独立的随机值。

4.4 流动动画
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)),所以不同网格的流速在 T2T 之间随机变化
  • P -= round(P/Y)*Y周期性包裹,将坐标限制在一个网格单元内

这创造了瀑布的核心视觉效果:水流以不同速度向下游移动,每个单元独立运动,整体形成湍流。

4.5 颜色振荡
o = 1. + sin(T + 7. * fract(8663. * i) + U);
  • fract(8663. * i) — 用另一个 Hash 产生 [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 泡沫/水花合成
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 形状

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 的区域有值。

L(P) - z

这是圆形 SDF(到原点的距离减去半径)。z = 5e-4,所以半径极小,形成细小的点状结构。

第二层:Smoothstep 抗锯齿

smoothstep(r, -r, sdf - z)

注意参数顺序:smoothstep(r, -r, ...) 是反向的(通常从小到大)。当 sdf < -r 时输出 1,sdf > r 时输出 0。这创造了一个柔和的半透明边缘

两个 SDF 结果组成 vec2,分别对应:

  • x 分量:平面区域的贡献
  • y 分量:点状水花/泡沫的贡献

第三层:颜色调制

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 时趋近于 0
  • 3 — 点状水花的固定强度

最后 * o * o.wo 是前面计算的颜色振荡(RGB 三通道不同相位),o.w 是 alpha 通道的强度调制。


5. 色调映射与输出

gl_FragColor = sqrt(tanh(O - .02 * U.zwyy));
5.1 tanh — 软压缩

tanh(x) = (e^x - e^(-x)) / (e^x + e^(-x)),特性:

  • tanh(0) = 0
  • tanh(±∞) = ±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 — Gamma 校正

sqrt(x) 等价于 pow(x, 0.5),是近似 Gamma 2.2 → 1.0 的解码。因为 GPU 输出默认在线性空间,而显示器期望 sRGB,sqrt 把颜色提亮,让暗部有更多细节。


6. 关键数学技巧总结

技巧 位置 作用
四维 SDF 外层循环 vec4 封装 3D 位置,w 分量创造额外自由度
柱坐标展开 atan(O.z, O.y) 将球面/柱面参数化为 2D 纹理坐标
Sin-Hash fract(sin(dot(...))*73.) 经典 GPU 伪随机数,零纹理依赖
周期性包裹 P -= round(P/Y)*Y 无限重复网格,无 if 分支
反向 smoothstep smoothstep(r, -r, ...) 软阈值化,直接得到 [0,1] 遮罩
指数泡沫 exp(19.*P.y) 基于 y 坐标的非线性亮度爆发
Tanh 色调映射 tanh(O - bias) 平滑高光压缩,避免硬裁切

性能分析

阶段 开销 说明
SDF 光线行进 ≤39 次循环 球面追踪,平均步数约 10-20
纹理合成 9 次循环/像素 每次含 Hash + SDF + 颜色合成
总 ALU ~200-300 次浮点运算/像素 在当代 GPU 上微不足道

这个 shader 在 1080p@60fps 下运行毫无压力。其性能关键在于:

  1. SDF 的解析求值避免了三角形光栅化
  2. 所有"纹理"都是程序化生成,零显存带宽消耗
  3. 循环次数有硬上限(39 和 9),无动态分支

总结

Smaller Waterfall 展示了 SDF 光线行进与程序噪声的精妙结合:外层循环用球面追踪找到表面,内层循环用 Hash + SDF 合成流动纹理。没有顶点数据,没有纹理贴图,没有预计算 —— 所有视觉效果都来自实时数学运算。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐