前阵子我把主力开发机从 Mac 切到了 Windows。系统、快捷键、终端这些两天就适应了,真正让我别扭了很久的,是桌面那块区域——Mac 上那个 3D 鱼缸屏保,一群灯鱼在深色水面下游动,偶尔追尾、顶气泡,看习惯了之后,Windows 这边放什么静态壁纸都像白墙。
我花了一个周末找替代品,结果不太理想。不是收费贵,就是鱼的种类少得可怜,或者干脆是录好的视频循环播放——鱼永远按同一条路径游,看五分钟就穿帮。于是我冒出一个念头:既然现在 AI 编程助手这么能打,我能不能自己把 Mac 下的 3D 鱼缸搬到 Windows 来?这个项目没什么商业目的,纯粹是为了让工作机桌面"活过来"。但它涉及的问题一点也不简单:跨平台渲染差异、水族箱氛围搭建、鱼群行为模拟、性能调优,每一步都有坑。
这篇就完整记录一下从选型、搭建到踩坑的全过程。如果你手上也有"某个平台独占的小应用、小效果,想搬到另一个平台"的需求,这篇的迁移思路和 AI 辅助开发的方法论应该能直接用。
1. 起因:这台 Windows 工作机上一直缺一块"活的水族箱"
1.1 替代品为什么全部失败
可能有人会说,直接去找个 Windows 版的 3D 鱼缸屏保不就行了?我试了一圈,问题集中在三类:
- 收费的做得确实精细,但 UI 老旧,订阅制,想关个水草密度要去翻英文配置文件,体验像是在用 2010 年的共享软件。
- 免费的要么鱼少,要么玻璃缸体反射是贴死图,鱼游过去能从画面反光里看出抠图边缘,非常出戏。
- 所谓"动态壁纸"直接就是视频循环,鱼永远走同一条路线,而且视频 4K 片源动辄几个 GB,纯粹是拿硬盘换氛围。
这里有个关键问题:我要的不是"会动的画面",而是"能交互、每时每刻都在随机变化的水族箱"。鱼要自己选择游动路线,偶尔聚群,偶尔散开,鱼尾巴摆动的频率要跟游速对得上,水面和缸底的光斑得跟着水的扰动走。视频循环永远做不到这些。
1.2 项目验收标准
在动手之前,我给自己定了几个硬指标,避免做成一坨"看上去像那么回事但处处穿帮"的半成品:
| 指标 | 目标值 | 说明 |
|---|---|---|
| 同时活跃鱼数 | 20 条以上 | 低于这个数缸里会显得很空 |
| 帧率 | 60 FPS 稳定 | 4K 分辨率下窗口化运行 |
| GPU 占用 | 不超过 30% | 不能让鱼缸吃掉游戏份额 |
| 鱼群行为 | 自然可信 | 随机聚散,路径不重复 |
| 环境细节 | 气泡、水草摆动、焦散光斑 | 缺了这些就是个塑料水箱 |
| 安装包体量 | 200 MB 以内 | 独立应用,不要让我去装运行时 |
这个验收表后来帮了大忙。每次性能调优或者功能迭代,我都会拿它过一遍,有一条不满足就继续改。
1.3 为什么敢让 AI 参与这个项目
说实话,我的图形学基础停留在"会用 Three.js 摆几个物体"的水平。真要自己从零写一套鱼缸渲染,光是水面折射和鱼群行为就够啃两三个月。
但 AI 辅助开发的模式这几年已经很成熟了,我给自己定的用法是:AI 负责搭结构和翻译伪代码,我负责人体理解和对最终效果负责。换句话说,AI 是经验丰富的实习生,我依然是那个签字上岗的负责人。遇到拿不准的参数,我让它给出"为什么是这个值"的推导过程,再拿实时画面去验证,而不是无脑接受。
这个分工在后续开发中反复被验证是对的——AI 帮我省掉的 70% 时间,是查文档和调试编译;而我花掉的 30% 时间,几乎全在调"看起来自然"的参数上。
2. 方案选型:三条路走下来,只有一条值得继续
2.1 直译原始代码:最先被否决的方案
说到"搬到 Windows",我的第一反应是去翻 Mac 上原版鱼缸用的渲染引擎,把 OpenGL/Metal 代码直译成 DirectX。这个想法刚冒头就被自己按死了。
原因很实在:一份生产级别的渲染代码,背后往往挂着大量平台特性调用(Metal 的 Tessellation、macOS 的图层合成、MetalFX 画质增强这类),搬运过程中任何一环对不上,排错都是按天计算的。我自己查了一下午资料,发现光是"纹理格式兼容"这一项就能写一篇论文,果断放弃。
提示:跨平台移植的第一原则是"行为等价优先,代码等价靠后"。不要纠结原项目用了什么,要盯着"最终画面看起来是什么"。
2.2 游戏引擎导出:学习成本比想象中大
第二条路是用现成的跨平台游戏引擎,把鱼缸做成一个场景导出 Windows 包。Unity 和虚幻都不缺水面和鱼类资源商店,理论上扔进去拼一拼就行。
实际试了才发现另一座山:引擎编辑器本身的复杂度。我要的鱼缸效果很轻,但为了导出我需要的轻薄桌面窗口,得先学习项目管理流程、场景烘焙、打包配置。一套流程下来,学习成本远超"做一个鱼缸"本身。更麻烦的是,引擎默认的水面 Shader 往往是为大场景设计的,体积和效果都偏重,塞进鱼缸这种尺度里反而不自然。
2.3 最终选择:Web 技术栈 + 桌面壳
最后我选了 Electron + Three.js,整个渲染跑在 WebGL2 上。这个选择不是因为它多高级,而是因为它和 AI 辅助开发的配合度最高:
- Three.js 在开源社区的存在感极强,AI 训练数据里相关代码量巨大,生成出来的代码很少出现"看似合理但 API 不存在"的情况。
- 浏览器渲染和桌面平台的差别被框架抹平了大半,我写一遍渲染逻辑,Mac 和 Windows 都能跑。调试的时候甚至可以先用浏览器直接打开页面调 Shader,确认效果再拷贝进桌面壳。
- 鱼缸本质上是"小场景、少物体、高感官密度",WebGL2 的负载完全扛得住,没有必要动用游戏引擎这种重武器。
如果你也想去移植一个视觉效果类应用,我给的建议是:先问"这个效果的核心是渲染逻辑还是平台集成能力",如果是前者,Web 技术栈几乎是当前时代性价比最高的答案。
3. 先把水缸立起来:水体、灯光、底床的搭建细节
3.1 水体不是"蓝色半透明盒子"
很多新手做水域,第一反应是拉一个半透明蓝色球体或者立方体。事实证明,鱼缸里的水感不是靠颜色,而是靠"折射 + 散射 + 衰减"三层叠出来的。
我最终的水体方案是一面覆盖视口的透明平面 + 一套自定义材质。透明平面上实时计算菲涅尔效果,垂直看的时候透明度高一点,侧着看的时候反射强一点。水的颜色则交给"深度雾"来控制——我写了一个简单的雾化函数,让水体深处的颜色自动从浅蓝过渡到深蓝黑。这样 40 厘米深的缸底看起来自然幽暗,而不是整缸水一个颜色。
这个效果如果用一句话概括:水的颜色取决于你看多深,而不是水的整体透明度。这跟真实鱼缸的视觉逻辑是一致的。
3.2 灯光方案:克制是美德
一开始我脑子热,给缸里挂了 5 个点光源,想让水草和鱼鳞都闪闪发亮。结果开起来画面一片惨白——多重光源叠加导致亮部过曝,鱼的轮廓被光晕吃掉了。
后来我看着 Mac 原版的效果反思:人家缸里其实只感觉到一处主光,光源位置在斜上方,模拟的是房间顶部灯具打下来的感觉。我照样抄作业,改成:
- 一个方向光作为主光,从斜上方 45 度打下来,负责在鱼和底砂上产生清晰的阴影。
- 一个很弱的环境色补充,保证鱼肚子的暗部不至于死黑。
- 一个强度极低的背光,用来在水草边缘勾出轮廓,让画面有深度。
这个改动带来的画面提升立竿见影,缸里立刻分出明暗层次,鱼游过时身体的受光面、阴影面都清晰可见。所以别迷信光源数量,缸内有限空间里,1 个主光 + 2 个辅助光的组合已经是上限了。
3.3 底砂、石头与水草:都是程序化生成的
这段最能体现 AI 辅助的效率。我在对话式 AI 工具里描述需求:"生成一个 Three.js 函数,创建圆盘状底床,顶点加入 Perlin 噪声扰动,从中心向边缘颜色渐变,形成沙地效果。"它十几秒就给出了完整代码,我拿过来一跑,沙地的随机起伏和颜色过渡就都是对的了。
石头用了另一套逻辑:随机生成 6 到 9 个顶点的不规则多面体,再往上叠加两次细分平滑,得到圆润但不规则的卵石形状。水草则是一根根贝塞尔曲线,配合顶点着色器让每根草叶在时间里做低频来回摆动,模拟水流带动。
程序化生成的优势是:所有元素都不是美术"画"出来的,每次启动位置略有不同,而且包体几乎不占空间。这一部分把安装包控制在 200MB 以内起到了决定性作用。
4. 让鱼游得像鱼:路径、群体行为与尾巴动画的结合
4.1 为什么不给鱼一个终点让它直游
鱼缸里最违和的画面,就是鱼不停的直线折返——像遥控车。真实的鱼在水族箱里不是这样,它们沿着平滑的弧线转弯,游到某个区域后会停顿、悬停、再出发。
我给每条鱼动态生成了一条 Catmull-Rom 曲线,鱼沿着曲线每一帧向前挪动一段距离。曲线不生硬的关键在于控制点的密度:转弯处放密一点,直线段放疏一点。鱼的朝向则跟速度方向做插值——转向时头部会微微延迟,而不是瞬间对齐路径切线。
这三行左右的代码,效果上撑起了 90% 的"像鱼"感:
// 鱼沿着 Catmull-Rom 曲线前进,速度固定 const pos = swimCurve.getPointAt(swimDistance) const tangent = swimCurve.getTangentAt(swimDistance) // 朝向并不直接等于路径切线,而是每帧插值逼近 fish.quaternion.slerp( new THREE.Quaternion().setFromUnitVectors(fishForward, tangent), 0.08 )4.2 简化版 Boids:让鱼群有聚散感
鱼群行为我用了一个轻量化的 Boids 模型(分离、对齐、聚合三力模型),但和教科书写法做了两点改动:
- 全局管理者(我称为"引导点")沿着一条大范围路径缓慢移动,鱼群在常规路径之外会去靠拢引导点,于是天然形成了"鲨鱼游过,小鱼跟随"的群落感。
- 分离力只在鱼间距小于某个阈值时才激活,避免鱼群动不动就弹开,破坏行云流水的视觉。
实际调参过程中,"鱼碰到缸壁如何响应"是最磨人的。物理反射会让鱼像弹球一样弹回来,极其生硬。我的解决方案是:鱼在接近缸壁前约四分之一身位处就触发预判转向——路径规划算法里增加一个"墙前偏移量",让曲线自动绕开边界。
4.3 尾巴摆动的相位同步
这是最容易被忽略但最影响观感的细节。鱼游动时尾巴摆动频率和游泳速度是正相关的,游得快尾巴扇得快,紧急转弯时尾巴还会猛地甩一下。
我实现的逻辑很朴素:尾巴的摆动角频率 = 游动速度 / 当前身长系数,摆动相位则跟路径距离挂钩。每帧将相位值传给鱼尾骨骼的 shader 属性,让尾巴做正弦摆动。转弯时,额外的摆动幅度叠加到角落变量里,产生"甩尾"视觉。
有一点必须提醒:鱼的摆动幅度和频率不是越大越好。刚调完时我把摆动幅度设得很大,鱼缸里每条鱼都像溺水抽搐,后来把幅度压到原来的 40% 才看着正常。这个 40% 的经验值直接记录在了参数表里,AI 生成的初始值几乎都是偏大的。
4.4 AI 在这个模块里到底干了什么
鱼群行为这个模块,是我整个项目里和 AI 协作最有价值的环节。我负责描述"鱼要有追逐感、要成群但不死板、转弯要平滑",它负责把这些描述拆成可执行的函数结构:曲线生成器、Boids 力累积器、转向插值器。在此基础上我再微调参数和边界条件。
如果没有 AI,我大概会在"把抽象想法变成代码骨架"这一步卡上一周;有了它,我一天内就拿到了能跑的最小实现。但同样地,AI 给的四五版转向算法全都存在壁面穿透问题,一个能直接用的都没有。它擅长的是"把话说全",不擅长的是"在真实场景中验收"——所以这笔账我个人觉得很划算。
5. 水面、焦散与气泡:氛围细节决定鱼缸真假
5.1 水面波浪:顶点挪一挪,法线算一算
水面我用了 128 x 128 的平面网格,每一帧在顶点着色器里叠加两组正弦波,频率和相位错开,形成两个方向的水波干涉。关键是每帧必须根据位移重新计算法线方向,否则光照打在水面上会是死的。
法线计算如果放在 CPU 做,每帧消耗巨大。正确做法是在着色器里求相邻顶点的偏移差值,来恢复法线方向。Three.js 的面片材质默认没做这件事,我花了点时间在 onBeforeCompile 回调里注入自定义顶点代码。
严格说这算不上高级水面渲染,但配合前文提到的菲涅尔透明层,画面已经"很像那么回事了"。有水波、有折射、有反射,水面的动态感直接翻倍。
5.2 反射不做实时 CubeMap 的真实原因
很多教程讲水面必然推实时反射(把场景渲染到一个立方体贴图,然后水面采样反射)。我在 Mac 原版上仔细观察了很久,发现它的"反射"其实非常有限——主要反射的是顶部光照和缸内物体的模糊身影。
这意味着实时 CubeMap 是严重的性能浪费。鱼缸里物体的数量很少,而且视觉上一团模糊更接近真实。我最终选择了预烘焙环境贴图加模糊采样,反射只用来增加水体表面的层次感,不做精确倒影。实测在 4K 分辨率下,这个选择让 GPU 占用低了 8% 左右,画面效果几乎没有折扣。
5.3 焦散光斑:用低分辨率贴图骗眼睛
焦散(Caustics)是鱼缸氛围的灵魂。阳光穿过水面,在水底和鱼身上投下亮斑,这些亮斑会随着水面波动不断扭曲变形。
最省的做法是使用一张动态无缝噪声纹理,通过投影映射的方式投到地面和鱼身上,纹理坐标加上时间偏移,让光斑流动。为了避免高分辨率画这种噪声烧 GPU,我把焦散贴图降低到了 256 x 256 分辨率,再用双线性插值放大。因为焦散本身就是模糊光斑,降低分辨率后视觉差异可以忽略。
这个光斑一定不能只投在底砂上,必须同步投给鱼的身体。我一开始只做了底砂焦散,鱼游过去就像一束光照穿了它,极其穿帮。把焦散材质单独抽出来对鱼做第二层投影后,终于实现了"鱼从光斑下游过,身体被水纹光影掠过"的自然感。
5.4 气泡与残影:细节里的生命力
气泡系统用粒子实现,每秒钟在 4 到 8 个随机位置发射粒子束。气泡的模拟逻辑包括:从缸底竖直上升、上升途中做正弦横向偏移、到水面附近变大然后淡出、消失时留一点涟漪效果。
为了避免气泡看起来像помехи到"满天星",我把同一时刻活跃的气泡总量限制在 50 颗以内,并且让发射位置集中在假想过滤器出水口那一侧——多数真实鱼缸的气泡本来就集中在一角。
水草摆动再补充一句:这个动效不是骨骼动画,而是顶点着色器按时间偏移,幅度从根部向叶尖递增。AI 给了我一个巨大的初始摆动幅度,我调到 0.2 倍才算自然。这类"数值看起来不突兀、但实际差很多"的经历多了之后,我养成了新习惯:凡是 AI 给的幅度类参数,一律先除以十再观察。
6. 搬到 Windows 之后踩到的坑:GPU 调度、DPI、打包
6.1 双显卡电脑默认用核显:水面噪点告急
最开始的版本在 Mac 上一路顺畅,信心满满地打包到 Windows 一跑,水面全是明显的噪点和锯齿。查了半天发现,Windows 这台机器虽然独显性能不差,但系统默认让 Electron 应用跑在核显上,WebGL 的硬件能力被大幅削弱。
解决办法有两步:
- 在应用启动命令里加入 switch 参数,强制 Electron 使用高性能 GPU。
- 在 Windows 系统的"显卡"设置里,把应用手动指定为高性能模式。
做完之后再开,水面噪点立刻消失,帧率也从浮动 35 左右稳定到了 58 以上。
注意:即便强制指定了独显,也要留一个环境变量开关,方便在出现兼容性问题的机器上回退到自动模式。别把性能优化写成死配置。
6.2 Windows 缩放 125%/150% 下画面发糊
macOS 的 Retina 缩放和 Windows 的 DPI 缩放机制不同,导致同一套 Canvas 在 Windows 上存在发糊问题。核心原因是 Electron 窗口的 DPI 感知没有正确打开,渲染尺寸被系统当成了缩放后的物理尺寸,逻辑像素和物理像素对不上。
修复方式是配置窗口选项中的高 DPI 支持,同时在窗口内容加载后强制设置缩放因子为 1.0,保证渲染粒度跟物理像素一一对应。这个坑其实不大,但排查起来特别费时间,因为画面是"处处模糊",你不会第一时间联想到 DPI 而不是渲染错误。
6.3 electron-builder 打包安装包被杀软误报
Windows 下打包首选 electron-builder,但在实际使用中有一个非常讨厌的坑:NSIS 生成的安装包经常被 Windows Defender 和其他杀软标记为风险文件。原因是 Electron 应用的结构比较特殊,加上没有代码签名证书,极容易被误判。
我的处理方式有三个:去掉不必要的 NSIS 配置,改用更紧凑的打包模式;在 GitHub 上提了 issue 但没抱指望;最终主用 portable 免安装版本,只有需要开机启动时才跟用户说明手动加入信任区。如果后续真的要做分发,一个代码签名证书几乎是绕不开的。
6.4 WebGL2 的深度精度问题:z-fighting 维修
鱼缸的底砂是噪声扰动过的沙地模型,表面高低起伏较小,当相机离得比较近时,沙地和石头的深度精度经常性冲突,出现画面闪烁。这个在 Mac 上几乎没有出现过,但在 Windows 上用非最佳 GPU 管线时暴露得特别明显。
解法是启用 Three.js 的对数深度缓冲区。这个选项在绝大多数显卡上都能保证充足的深度精度,代价是少量性能损耗。在鱼缸这个规模的应用里,性能损耗可以忽略,我直接全局开启,闪烁问题从此消失。
6.5 全屏切窗口时帧率被锁死后的相位跳跃
Windows 下切换显示器模式(比如全屏和窗口化切换)会导致渲染上下文重建,帧率被短暂锁在 30。更麻烦的是,水草摆动、鱼尾巴摆动这些依赖时间增量的动画,在我用"帧数计数"而非"真实时间差"做驱动时,切换后会出现一瞬间的大跃迁。
修复方式很简单:所有动画的时间驱动统一改为 performance.now() 计算真实时间增量乘以速度系数,而不是累计帧数。这样无论渲染中断多久,恢复时动画都能连续,不再产生跳跃感。这个教训也在参数表里单列了一条,防止未来其他模块再犯。
| 问题 | 根因 | 处理方式 |
|---|---|---|
| 水面噪点 | 默认使用核显 | 强制独显 + 高性能模式 |
| 画面模糊 | DPI 感知未开启 | 窗口高 DPI 配置 + SetZoomFactor(1.0) |
| 安装包误报 | 无代码签名 | 主推 portable 版,备用 NSIS |
| z-fighting | 深度精度不足 | 开启对数深度缓冲区 |
| 动画跳变 | 帧数驱动而非时间驱动 | 统一改用真实时间差驱动 |
7. 一个多月的调参心得:哪些参数最影响"像不像鱼缸"
7.1 参数影响等级表
整个项目做完后,我对着参数表复盘了一遍,哪些参数对最终效果影响最大,哪些调了很久其实帮助有限,我的判断如下:
| 参数 | 影响等级 | 备注 |
|---|---|---|
| 转向插值系数 | 极高 | 决定鱼是否像遥控车的关键 |
| 尾巴摆动幅度 | 极高 | 幅度偏大 = 溺水抽搐 |
| 水体雾化深度范围 | 极高 | 决定水缸是否有纵深感 |
| 焦散贴图分辨率 | 中 | 256 到 512 视觉差异很小 |
| 气泡上限数量 | 中 | 超过 60 颗画面凌乱 |
| 水草摆动幅度 | 中 | 需要比 AI 初始小 80% |
| 相机晃动幅度 | 低 | 加太多反而头晕 |
7.2 鱼缸体积与鱼数量的经验公式
如果你的目标也是做鱼缸,我提供一个经验公式可以直接用:鱼缸模型体积(单位:标准瓶)大约需要 300 毫升水体积空间,每增加 1 条鱼,至少增加 50 毫升空间,否则画面会拥堵。这是拿我自己项目实测出来的,20 条鱼对应 1.2 升以上空间时视觉效果最佳。
这里的"体积"是指鱼可活动的自由空间,减去石头、水草占用的体积。空间太挤时,Boids 分离力会频繁触发,鱼群一直保持弹开状态,完全来不及聚散,行为就显得机械。
7.3 性能账单
最终跑起来,在 4K 分辨率窗口化场景下,性能数据给一个参考:
- 20 条同时活跃的鱼,整缸三角形预算约 18 万。
- 帧生成时间稳定在 16.8 毫秒附近(60FPS 上下)。
- GPU 占用率约 22%,CPU 占用约 6%。
- 显存占用约 200MB。
- 安装包 portable 版本约 85MB。
最贵的两个部分是水面顶点网格的波浪计算和鱼鳞高光的逐像素采样。如果你希望进一步提高帧率,优先把水面网格从 128 x 128 降到 96 x 96,视觉损失很小,性能提升显著。
7.4 我对 AI 辅助开发的最后一个判断
整个项目做下来,我对"AI 帮我把 Mac 下的 3D 鱼缸搬到 Windows"这个标题有了更具体的体会。AI 在这里的角色像一个特别勤奋、特别了解开源库的实习生:
- 它能帮我把想法变成可运行的代码,在"搭骨架"和"查 API"两个环节省下大量时间。
- 它能提供多种方案选择,比如水面反射技术就给了实时反射、静态贴图、环境贴图三种路线,并且附上各自的优劣分析。
- 但它给的所有"美术感参数",几乎都不靠谱。AI 没有经历过"看着屏幕判断画面自然度"的过程,它无法为一幅画面负责。
所以整个项目真正让我花时间的地方,不是代码写不出来,而是"看着屏幕一遍遍调参数直到眼睛觉得舒服"。这部分没有任何捷径,AI 也很难模仿。它可以把效率提升到 300%,但最终那个"按下回车、看到鱼缸里游过一群自然生动的鱼"的满足感,还是得你亲自盯着屏幕验收才会发生。
最后分享一个小技巧:每次调完一组参数,别急着改下一组。截一张图,隔 30 分钟再回来看一眼,你的眼睛会告诉你"过了"还是"不够"。这种延迟反馈,比连续盯着屏幕硬调要准确得多。