关于 3d-underwater-god-rays
面向 3D 水下场景开发者的可复用 Three.js 着色器模块,用于渲染水下光束。它沿视线整合漂移涌浪与焦散,使光束随相机移动而相对场景物体滑动。
主要功能
- 01Three.js 水下光束着色器
- 02光束依涌浪和太阳方向定位
- 03整合焦散条纹
- 04半分辨率 HalfFloat 光线步进
- 05支持减少动态效果与页面可见性
适用场景
- 01为 Three.js 水下场景添加阳光光束
- 02相机移动时让光束相对鱼群移动
- 03通过演示控件调整光束效果
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mengto/ skills --skill 3d-underwater-god-rays安装后在 Claude Code 中输入/3d-underwater-god-rays确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlassets/underwater-light.mjsdemo/build.mjsPROMPT.mddemo/src/ui.cssui.htmlui.jswater.js详细介绍
3D 水下上帝光芒
当摄像机位于水下、光线穿过水面照射下来时,请使用此方案。以下情况请改用同类方案:
- **空气中的阳光光束,被树木和屋檐切割:**使用
3d-sky-rays。它通过屏幕空间对遮挡蒙版进行径向模糊,适合这种情况。它无法产生视差,也无法呈现涟漪,因此在水下只将它作为鱼影的低强度辅助效果(Blue Hour 将其设为 0.25)。 - **从上方看到的海面,带有地平线:**使用
3d-ultra-realistic-water。 - **明亮室内中穿过一扇窗户的自定义光束平面:**使用
3d-wood-lighting-scorecard。
来源:摘录自 Blue Hour,这是一项基于 Three.js iPhone Duo 研究的动态海洋壁纸。2,200 条用 Blender 制作的沙丁鱼组成的鱼群在玻璃设备后方游动。光线必须在完整的 38 s 摄像机推轨过程中,以及设备自身的屏幕上,呈现出阳光穿透水面的效果。
已验证的技术栈:Three.js r170、WebGL2、GLSL ShaderMaterials、一个 HalfFloat 渲染目标,以及手写的 ACES 渲染通道。未使用后期处理库。
实现机制
**沿着太阳方向采样一个漂移的涌浪场,并沿每条视线积分。**水中的每个点,其受光情况由该点沿垂直于太阳方向的位置处的涌浪值决定。因此,光束是在世界空间中平行排列的柱体。它们像铁轨汇聚于消失点那样汇聚到太阳处,并在摄像机移动时从鱼群旁滑过。在屏幕空间绘制相同的光束,则会让它们固定在镜头上。
复用现有效果
复制 assets/underwater-light.mjs。传入自己的 THREE 即可,该模块不会再导入另一份副本。演示在 Blue Hour 的渲染器中运行同一个模块。demo/build.mjs 会根据 Blue Hour 源代码重新构建该模块。
import { createUnderwaterLight, LIGHT_GLSL, WATER_GLSL } from './underwater-light.mjs';
// One uniforms object shared by the water, fish and particle materials.
const uniforms = {
uSunDirection: { value: new THREE.Vector3(.25, .40, -1).normalize() }, // toward the sun
uTime: { value: 0 },
uCameraPosition: { value: camera.position },
uCameraWorld: { value: camera.matrixWorld },
uInverseProjection: { value: camera.projectionMatrixInverse },
};
const light = createUnderwaterLight(THREE, { uniforms }); // before creating the materials below
light.setSize(innerWidth * dpr, innerHeight * dpr); // drawing-buffer pixels, again on resize
// Water: a full-screen quad. Declare uSunDirection, uCameraPosition, uTime,
// uShafts and uShaftTexel, then add ${LIGHT_GLSL}${WATER_GLSL} and in main():
// vec3 sunRay = ray;
// color += surfaceUnderside(ray, sunRay); // also bends sunRay through the ripples
// ...draw your sun with sunRay...
// color += underwaterShafts(vUv, ray);
// Fish: declare uSunDirection, then add ${LIGHT_GLSL} and multiply the sun key
// by underwaterDapple(worldPosition, uTime).
// Every frame, in this order:
uniforms.uTime.value = time;
camera.updateMatrixWorld();
light.render(renderer); // the march, into its own half-resolution target
// then the water, the fish, and one tone map at the end
实际使用的参数
| 图层 | 常量 |
|---|---|
| 涌浪 | 沿 (.86, .51)、(−.42, .91)、(.13, −.99)、(−.97, −.24) 方向的 4 个正弦波,频率分别为 ×1、1.23、0.87、1.41;漂移速度为 0.29、0.24、0.19、0.15 rad/s;域扭曲为 0.62 |
| 沿太阳方向 | 缩放 1.3,因此主周期约为 4.8 个世界单位;鱼群所在位置处,画面宽约 22 个单位 |
| 光束聚焦 | smoothstep(−0.05, 1, swell)²,并乘以 exp(−0.012 · 沿太阳方向到水面的距离) 进行淡出 |
| 焦散网格 | 再次使用涌浪,频率为 ×2.6,速度为 1.35 倍,`(1 − |
| 射线步进 | 26 步,t = 4 + 50·f²,权重为 exp(−0.06t)·(0.08 + f),再除以权重总和;使用稳定的逐像素抖动(交错梯度噪声),而非时间噪声 |
| 太阳混合 | total = mix(total, 0.16, μ^28),其中 μ = ray·sun |
| 相位 | 0.7 + 1.1μ^5 + 0.9μ^40;地平线以下的光线再乘以 mix(0.45, 1, smoothstep(−0.45, 0.35, ray.y)) |
| 增益 | (0.16, 0.40, 0.47) × 3.6,线性 HDR,仅在最后进行色调映射 |
| 缓冲区 | 绘制缓冲区的 0.5 倍大小,HalfFloat,单采样(射线步进效果较柔和,MSAA 没有收益),使用半个纹素偏移的 4 抽头帐篷滤波进行上采样 |
| 水面底侧 | 平面位于 y = 12;网络为 `(1 − |
| 鱼群 | 太阳主光 × 0.62 + 1.5 · shaftLight |
| 画面其余部分 | 太阳光晕减半至 (0.043, 0.095, 0.123)·μ^48;屏幕空间光线通道的强度从 0.82 降至 0.25 |
规则及其避免的问题
这些结论来自 Blue Hour 画面的 A/B 渲染:相同的摄像机、相同的静止鱼群,每次只改一项。
- 将涌浪塑造成稀疏的波峰,而不是波脊。
1 − |swell|会形成一张由薄片组成的网,光线穿过多层薄片后会将它们平均成一片平淡的雾。在静止画面中,波脊网让水面的平均亮度提高了 20%,并将像素列之间的对比度从 0.247 降至 0.118。演示中的 Ridge web 开关可以展示这一效果。 - 从 4 个单位外开始步进。 从 1.5 个单位处开始时,掠过摄像机上方的光束会用一道明亮的楔形光填满四分之一画面。用
exp(−0.06t)对采样加权:较近的采样模糊得最少,因此能保住光束边缘。 - 当光线指向太阳时,将光束融合进太阳的辉光。 沿着正上方的一列观察,积分结果只是一个涌浪值。在摄像机位于光束之间的位置,这会在太阳上留下一个暗洞。
mix(total, 0.16, μ^28)可以填上它。 - 在步进过程中积分焦散网。 如果改为以全分辨率采样 4 次,焦散网看起来就像是绘制在水面上的水池底部图案。在半分辨率的 26 步步进中,它会变成沿每道光束延伸的细条纹。演示中的 Painted on screen 开关可以展示这一效果。
- 调低画面中已有的均匀辉光。 Blue Hour 的日冕和屏幕空间光线处理让画面顶部均匀变亮。以最终发布时附近的增益来看,首次加入的光束在合成画面中几乎看不见。只有将日冕减半、把径向光线处理从 0.82 降到 0.25 后,光束才清晰可见,同时光束之间仍保持暗色。
- 在合成后的页面上判断增益,而不是看原始画布。 增益为 2.2 时,画布上的效果看起来正好。但在页面中,画布的不透明度为 82%,下方还有一层使画面变暗的 CSS 遮罩,同一画面几乎什么都看不见。3.6 才能在页面上保留效果。
- 将太阳抬到顶部边缘以上,让光束铺满整个画面。 太阳位于 21° 时仍在画面中:汇聚点会呈现为一团辉光,光束也比较短。角度介于 30° 和 44° 之间时,太阳会越过顶部边缘,光束则会向下铺满整个画面。演示中的 Sun 滑块可以调整角度。
- 每个着色器中,每个 uniform 只声明一次。
LIGHT_GLSL会声明自己的 uniform(uSurfaceY、uShaftScale、uShaftFloor和uCompare),并要求宿主先声明uSunDirection。重复声明会导致编译错误,材质也会悄无声息地不绘制任何内容。
开销
在机器负载较高时(平均负载约为 60),无头 Chrome 在 ANGLE Metal 上以 2048×1440 分辨率渲染完整的 Blue Hour 画面需要 6–10 ms。该画面包含 2,200 条实例化沙丁鱼、水体、1024×720 分辨率的步进、屏幕空间光线处理以及色调映射。跳过步进后,在 5 轮交替测试(每轮 20 帧)中,中位耗时的变化小于每轮运行间的噪声(约 ±2 ms),因此步进不是这里的瓶颈。如果它出现在你的性能分析结果中,请降低缓冲区缩放比例或步数。光束属于低频效果,因此这两种做法主要会降低柔和度,但在依赖这些做法之前,先测量实际效果。
生命周期与无障碍
- 以 30 fps 推进模拟,每步最长不超过 0.1 s。Blue Hour 会将水族箱渲染到固定的 2048×1440 画布(宽度低于 800 px 时为 1600×1125),再以覆盖方式适配,因此步进开销不会随屏幕大小变化。
- 在
prefers-reduced-motion: reduce下,将画面停留在 t = 0,仅在控件变化时重新渲染。抖动在每个像素上都保持稳定,因此静止画面不会爬动。 - 在
document.hidden时停止,并在visibilitychange时重置帧时钟,避免恢复后的第一帧积分整个暂停期间的时间。
验证
- 环绕摄像机旋转:光柱相对于鱼移动,但仍汇聚于太阳。如果它们随镜头移动,则说明它们绘制在屏幕空间中。
- 开启 脊状光网 后,水面会变成雾状。切回稀疏峰值模式后,可以看到清晰的光束,光束之间是暗色水面。
- 开启 绘制在屏幕上 后,池底光网会覆盖水面。在光束行进路径内,细微的条纹会沿着每束光延伸。
- 直视太阳时,不会出现黑洞。
- 将太阳从 21° 扫到 44°:汇聚点会移出画面顶部,光束则会延伸至画面各处。
- 鱼穿过光束时会变亮,游出光束后又会变暗。
- 减少动态效果时会保持构图完整的静止画面;控件仍会重新渲染画面,并且在 1440×900 和 390×844 分辨率下控制台没有错误。