海边一角 · 3D:把那片海搬进 WebGL
海边一角 · 3D 是那片 Canvas 海的真 3D 版本。产品约定、四种素材、交互手势和存档格式都沿用 2D 版,换掉的只是渲染层——从 22 条正弦波线,变成了一片可以转着看的海面。
怎么玩
玩法和 2D 版 基本一致,多了一件事:你可以转着看这片海。
| 操作 | 效果 |
|---|---|
| 点底部素材(海星 / 小鱼 / 贝壳 / 海草) | 进入放置模式,鼠标下的海面出现半透明预览 |
| 点海面 | 在真实波面上放下装饰,沙滩上放不下去 |
| 拖动装饰 | 沿波面移动;拖东西时相机会暂时锁定,松手恢复 |
| 双击装饰,或选中后按 Delete / Backspace | 删除 |
Ctrl/Cmd + Z |
撤销,最多 40 步 |
Esc |
取消选中、退出放置模式 |
| 拖动空白处 | 环视海面,俯仰和方位角都有限制,不会钻到水下 |
| 滚轮 / 双指捏合 | 缩放 |
| 右上角按钮 | 重置视角 / 撤销 / 清空 / 保存画面为 PNG |
小鱼放下后会自己游;布置自动存在 localStorage 里,刷新还在。清空后刷新依然是空的——不会偷偷给你长回默认装饰。
这片海是怎么立起来的
技术栈是 three.js + Vite + 原生 ES Module,没有前端框架,也没有任何外部图片、模型或字体——所有东西都是运行时用代码生成的。
波浪:五列 Gerstner 波,GPU 和 CPU 共用一本账
2D 版的浪是画在画布上的 22 条线;3D 版的浪是真的会把顶点推上推下的 Gerstner 波。
Gerstner 波的思路不难理解:想象五个人同时往水面扔石子,每颗石子激起一圈波,五圈波叠加在一起,就是海面。每个方向、振幅、波长、相位都可以单独调。
关键在于,同一组波浪参数同时喂给 GPU 和 CPU。顶点着色器用它们把水面网格的顶点推到正确高度;JS 里也用同一组参数实现了一个 sampleWater(x, z, time),返回任意一点被波推到了哪里。
为什么要这么麻烦?因为装饰要放在真实的波面上。如果只让 GPU 知道波长什么样、CPU 不知道,那你点水面放海星时,射线算出来的落点会和眼睛看到的水面差一截——“看着点这里,东西放到那里”。现在 CPU 也能查波高,点击落点会迭代修正几次,海星就老老实实趴在你点的那块浪尖上。
小鱼上下起伏、贝壳随波倾斜,用的也是同一个函数。整片海只有一本“波浪账”,渲染和交互不会各说各话。
水面反光:用数学算,而不是再画一遍
真实水面会反射天空和太阳。工业级做法是把场景再渲染一遍到一张反射贴图上,贴到水面。这很贵——等于场景画两遍,移动端容易扛不住。
3D 版走了另一条路:解析天空反射。天空本身就是一个用 shader 画的渐变球,颜色可以写成一个关于方向的函数。水面片元着色器直接拿这个函数,算反射方向上的天空是什么颜色,再用 Fresnel 混合深水色、浅水色和天光,加上太阳高光和碎光闪点。
对一片以黄昏天空为主的海来说,这已经足够像了。代价是水面不会反射远处的帆船和岛屿——但换来的是少画一遍场景、少一张反射贴图,这是移动端能跑住的关键。
海星小鱼都是代码捏的
四种装饰没有用 GLB 模型,全是 creatures.js 里用 BufferGeometry、Shape、TubeGeometry 现捏的:
- 海星:五角星截面挤出倒角,腕上再加点小凸起
- 小鱼:椭球身体加几片鳍,顶点色涂出背腹渐变
- 贝壳:扇形网格加放射状棱脊
- 海草:几根 CatmullRom 曲线套上 TubeGeometry
这么做没有二进制资产、没有加载器、没有模型下载,部署只需要 JS 和 CSS,风格和代码一起可调。代价是造型偏风格化,不是写实资产。如果以后想换成 Blender 做的模型,工厂接口可以保留,只换里面的实现。
相机:能看,但不能乱看
OrbitControls 做了几道限制:俯仰角、方位角、距离都有范围,相机高度夹在水面以上,所以你转着转着不会一头钻进水里看鱼肚子。拖装饰时会临时关掉相机控制,避免“想拖海星结果把视角转飞了”。
几个有意为之的取舍
- 不做实时阴影。 装饰底下用一层程序化的接触阴影,低机位下看起来不像悬空。开实时 shadow map 成本不低,对这个体量的场景划不来。
- 移动端自动降级。 启动后 2.6 秒采样一次帧率,低于 42fps 就把渲染分辨率降到 75%;触屏设备像素比上限 1.6,水面网格也减半。
- 照顾偏好。 系统开启「减弱动态效果」时,波浪和鱼的游动速度都降到 34%。
- 没有 WebGL2 就给个台阶。 老浏览器会看到一张回退卡片,链接指向 2D 版海边。
去哪里看
网页入口在 网页 板块,3D 版在 海边一角 · 3D,2D 版在 海边一角。两个版本共享同一套玩法,可以对比着玩——同一个产品想法,用两种画法各画一遍,挺有意思的。