Smaller Waterfall 的 Three.js 实现
效果预览
一道气势磅礴的瀑布在屏幕中央奔流而下,水面泛起层层涟漪与泡沫。整个场景仅由 SDF(Signed Distance Field,有向距离场)光线行进算法实时渲染,无模型、无纹理、无贴图 —— 纯粹的数学之美。
源码地址:https://shader.shuqin.cc/
Shader 实现原理
1. 整体架构:从两个循环到最终像素
这个 shader 的核心结构分为两个阶段:
- 外层循环 — SDF 光线行进,求出场景中每个像素对应的 3D 表面点
- 内层循环 — 在该表面点附近做纹理/噪声合成,生成瀑布的流动细节
最终的颜色经过 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)
这是一个四维超球体的 SDF:length(O) = 1 时 d = 0(表面),length(O) < 1 时 d > 0(内部),length(O) > 1 时 d < 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:
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)的伪随机数
这个 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)),所以不同网格的流速在T到2T之间随机变化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时趋近于 03— 点状水花的固定强度
最后 * o * o.w — o 是前面计算的颜色振荡(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) = 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 — 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 下运行毫无压力。其性能关键在于:
- SDF 的解析求值避免了三角形光栅化
- 所有"纹理"都是程序化生成,零显存带宽消耗
- 循环次数有硬上限(39 和 9),无动态分支
总结
Smaller Waterfall 展示了 SDF 光线行进与程序噪声的精妙结合:外层循环用球面追踪找到表面,内层循环用 Hash + SDF 合成流动纹理。没有顶点数据,没有纹理贴图,没有预计算 —— 所有视觉效果都来自实时数学运算。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)