资讯动态

WebGL生成艺术实战:让千只水母在浏览器中呼吸与迁徙

发布时间:2026/10/9 9:07:35 来源:尧图企业网站定制
BlooomMedusas 这个名字是临上线前三天才改的。三个 o 并不是随手敲出来的卖萌而是想表达水母群落从静默中缓慢扩散开时那种既像苏醒、又像呼吸的节奏感——你盯住屏幕三秒会觉得整个水下空间在慢慢鼓气。它本质上是一个纯浏览器端的实时生成视觉项目主题非常具体用代码在三维深水空间里构建一片会呼吸、会迁徙、还能被现场声音和触摸影响的数字水母盛景。这个项目属于生成艺术、创意编程和现场视觉装置的交集核心价值在于不依赖任何现成视频素材打开网页就能流畅运行适合长时间循环播放而不会让人产生“视频卡住了”的错觉。适合谁参考如果你想做沉浸式展厅、活动现场背景视觉、音乐演出的实时画面或者单纯想把 WebGL 用到更偏艺术的方向这篇总结应该能给你不少可落地的思路。我会把从模型生成、运动系统、群体行为到性能优化的完整过程都拆开讲包括踩过的坑和最后保留在代码里的取舍。1. 项目定位当“水母”成为可运行的艺术系统1.1 为什么选水母能带来什么做生成视觉的人都有一个共识题材选对了工程难度能直接降一个量级。水母就是这种“对”的题材。原因是水母的形态语言高度模块化——伞状的主体、下垂的触手、随着水流摆动的边缘裙摆这些结构都能用参数化几何甚至简单的数学函数表达而且观众对水母的物理精确性往往很宽容。它不像人形角色你能容忍它完全不遵守流体力学但如果你把人类角色做得稍微僵硬一点观众立刻就会觉得不舒服。我最早尝试过鱼群、飞鸟、星尘这些方向最后都放弃了。鱼群需要比较严格的转向逻辑飞鸟的翅膀动画稍微偷懒就会像纸片星尘又太抽象缺乏一个让观众情感锚定的对象。水母恰好站在中间它足够具体让人一眼能认出是生物又足够自由允许你把真实水母的动作简化成几个参数照样能营造出沉浸感。这种“半抽象”特质特别适合长时间运行的视觉装置。1.2 设计目标从屏保到展览装置项目启动前我给自己定了几个非常明确、可验收的设计目标避免做成一款“高级屏保”完全驻留浏览器打开即用无插件无客户端现场展示不需要额外装软件。所有模型和动画必须由代码生成不导入任何外部 3D 模型文件。普通笔记本上保持 50fps 以上低配机器不低于 30fps。支持麦克风音频输入和鼠标触控让画面能对现场环境实时反应。可以连续运行超过十几个小时不允许内存缓慢上涨导致崩溃。这些目标看着不复杂每条背后都有很现实的场景展厅的电脑往往不归你控制没人会提前装好软件长时间运行时的内存泄漏是现场翻车的第一大隐患而不靠视频素材生成画面意味着你可以随时调整氛围参数不需要重新渲染离线视频。1.3 边界清点哪些功能不做很多项目出问题不是因为做得不够而是因为做得太多。我把这次的功能边界也清点得非常明白。第一不做真实的生物物理模拟。水母的游动涉及粘性流场、柔体力学这些在 WebGL 里全部模拟会瞬间压垮 CPU。第二不做个体间碰撞检测——上千条水母互相碰撞的开销完全不值得我用“弱排斥场”替代碰撞视觉上几乎看不出区别。第三不做 VR、不做云端同步。这三点砍掉之后整个系统的工程量骤然收敛到一个可以自控的范围。边界意识对个人开发者特别重要。项目体量失控通常发生在“顺手再做一个功能”的念头里而这种念头在视觉项目里几乎每天都会冒出来。2. 技术选型Three.js 是我反复权衡后的答案2.1 渲染方案对比做浏览器实时视觉渲染方案的选择基本决定后续开发速度。我先后对比过原生 WebGL、Babylon.js、Three.js、Unity WebGL 和 TouchDesigner 导出方案。原生 WebGL 的优势是零中间层一切都能最大化控制但代价是开发效率太低。一个简单的半透明材质、一个实例化网格底层要写一堆缓冲区和程序管理代码这个项目体量下完全不可接受。Babylon.js 对 WebGPU 支持比较积极内置编辑器功能完整整体更偏游戏化但它的材质系统和抽象层次在我的使用习惯里偏重。Unity WebGL 的问题是首包体积太大往往几十上百 MB加载速度和浏览器融合度都不如纯 Web 方案。TouchDesigner 虽然适合做现场视觉但导出浏览器体验一般而且授权和部署在活动场景里容易成为额外麻烦。最后我选择 Three.js理由是它的 API 边界恰到好处封装了矩阵变换、相机、几何体、实例化网格这些基础能力遇到需要深度定制的地方又能直接写自定义着色器穿透到底层。配合 Vite 构建工具模块热更新对调试视觉参数非常有帮助。我在这个项目里使用的是 r150 版本WebGL2 上下文配合实例化渲染性能余量充足。2.2 工程结构与模块划分项目代码一开始是单文件实验原型后来规模扩大后我按功能拆成了清晰的模块结构src/ core/ 初始化、渲染循环、相机、调试面板 simulation/ boids 群体算法、个体状态、漂浮场 matter/ 水母几何生成、触手网格、材质与着色器 fx/ bloom、景深、色调映射等后处理 input/ 麦克风解析、鼠标触摸、音频可视化 ui/ 启动封面、参数面板、状态监控这种划分的核心原则是把“状态模拟”和“渲染表现”分离。水母的位置、速度、相位这些数据在 simulation 层维护渲染层只负责读取数据生成画面。好处是你可以单独调试群体算法不用关心画面怎么画反过来调整画面质感时也不会影响系统的运动逻辑。对我个人来说真正的转折点是加入一个覆盖全局的配置对象。所有可调参数——水母数量、颜色调色板、噪声强度、音频灵敏度——都集中在一个配置项里。现场调试时我只需要打开一个小面板改数值不需要修改代码再重新编译。2.3 资产策略全程代码生成这个项目特意没导入任何三维模型文件。所有水母都用参数化几何函数生成每只个体绑定一个随机种子。这样做的收益非常明显每只水母都能拥有独一无二的形态伞盖的扁平程度、触手长度、数量都可以随机变化。运行时可以根据设备性能调整几何精度手机端用低细分网格大屏用高细分网格。形态连续变型成为可能比如现场音频强度提高时可以动态让伞盖变得更宽、触手变得更长这是预烘焙模型很难做到的。代码生成资产听起来很有技术感实际上核心就是一组带参数的几何构造函数。关键不是把每个顶点都算得多精确而是为后续的顶点着色器动画留好属性通道。3. 核心系统拆解一千只水母是这样活过来的3.1 模型用代码雕刻一只能程序动画的水母我把一只水母简化成三个部分伞盖、口腕、触手。伞盖负责提供主要的视觉体积也是收缩动作的载体口腕是伞盖下方几条较粗的垂下结构触手是大量细线状结构用来制造飘逸感和视觉密度。伞盖的生成思路是用半球几何体加工。先在球面网格的基础上把顶部的顶点纵向拉长让外形从“半球”变成“蘑菇”再给每个顶点记录一个归一化的高度值。这个高度值后面会作为顶点着色器做收缩动画的权重——越靠近伞盖顶部收缩幅度越小越靠近边缘收缩越明显。这样当伞盖“呼吸”时边缘卷曲向内收拢中心保持稳定看起来就像水母在真实地泵水推进。触手则使用细管状的几何体沿一条下垂的曲线排列顶点每一段触手顶点都存两个属性沿着触手长度方向的位置比例以及一个随机生成的缠绕相位。这两套属性在渲染时配合时间变量可以让触手像水草一样缓慢摆动而互相不重复。3.2 运动钟体收缩与触手摆动的数学内核水母运动的核心其实不是一个复杂算法而是一条不对称的脉冲曲线。真实水母游动时收缩动作很快张开动作很慢形成一个不对称的节奏。用代码表达就是先对时间取模再用幂函数把对称的波形压偏。顶点着色器里的关键代码大概是这样uniform float uTime; attribute float aHeight; attribute float aPhase; void main() { float cycle fract(uTime * 1.2 aPhase); float pulse 1.0 - 0.4 * pow(sin(cycle * 3.14159), 3.0); vec3 pos position; pos.y * mix(0.75, 1.0, pulse); pos.xz * mix(0.9, 1.0, 0.35 0.65 * pulse); gl_Position projectionMatrix * modelViewMatrix * vec4(pos, 1.0); }这段代码的意思是随着时间变化每个顶点根据自身高度和相位经历一个收缩—扩张循环而且收缩发生在 y 方向和水平方向同时进行。这里有个细节容易忽略如果只是简单缩放整体模型水母看起来会像个弹性球而不是伞盖边缘优先卷曲。所以我把收缩幅度和顶点高度关联起来——顶部收缩得少边缘收缩得多视觉上才会有“泵水”的感觉。触手摆动采用的是沿长度方向衰减的正弦波再加上每根触手不同的初始相位。这样即使一千根触手用的是同一套波动函数彼此之间也不会出现整齐划一的“合唱团”效果。3.3 群体行为让水母产生“集体盛放”的错觉水母群和鱼群最大的区别是运动节奏。鱼群转向快、反应敏捷而水母群更像漂浮的植物群每个个体都拖着缓慢的惯性整体只是在某个“场”里缓慢漂移。我实现了一个轻量级的群体算法包括三条核心规则聚集——每个个体缓慢朝周围邻居重心移动对齐——个体朝向邻居的平均方向做极缓慢的转向避让——个体与个体距离过近时互相推开一个小距离。和传统 Boids 算法不同我没有让每个个体去遍历搜索所有邻居而是把群体划分成三维网格只检查相邻网格里的潜在邻居。这样在线程数量达到三百到五百时CPU 计算量仍然可控。另外我还给每个个体叠了一个低频的“漂浮场”——用一个连续噪声函数驱动水平漂移和垂向迁徙让群体即使静止不动也有自然起伏。单纯靠随机数运动的水母群会显得很“躁”叠上连续性噪声之后才会有安静悠长的感觉。群体行为调参时最需要注意惯性参数。惯性太小水母会变得像受惊的鱼群疯狂转向惯性太大又会出现个体之间互相交叠成一团。我最终的参数是加速度上限 0.8 个单位每秒转向角速度上限 1.2 弧度每秒配合从中心点向外缓慢扩散的离心力才达到理想中的“集体盛放”感。3.4 相机与交互系统画面有了内容之后相机决定了观众怎么“看”这场景。我选择了自动漫游和手动控制混合的方案默认状态下摄像机沿一条缓慢的李萨如曲线运动——在水平方向做两个频率不同、相位错开的简谐运动叠加让视角始终在缓慢绕行但永远不会出现机械循环的僵硬感。这样画面有一股持续流动的镜头语言观众不需要操作也能感受到空间深度。交互方面我保留了三种输入鼠标或手指滑动会以平滑插值方式改变相机朝向点击屏幕任意位置会产生一个短促的“扰动波”让水母从点击中心快速散开再缓慢回拢麦克风音频输入则控制整体脉冲节奏和触手摆动幅度。音频驱动是现场演出里最常用的功能但需要注意设置音频启动门槛环境噪声较低时画面不至于一直处于高频抖动状态。4. 画面质感生物发光与镜头语言的落地4.1 配色策略与发光逻辑水母项目最出效果的点在于生物发光。深海背景下半透明水母身体边缘的发光是整张画面的视觉锚点。配色上没有采用写实路线而是走了一条偏舞台美术的路线深海背景偏蓝黑水母群落点亮青、品红、紫、淡绿四种颜色。我在代码里定义了一套调色板每只水母初始化时从调色板里按权重抽取一个主色再用一个全局色温变量控制整体冷暖倾向。色温高时画面偏粉紫色温低时画面偏青蓝现场可以通过音频低音频率实时改变色温形成缓慢的氛围变化。发光效果分两层底层是顶点颜色水母身体的半透明材质本身就带自发光颜色顶层是边缘光效果利用菲涅尔反射的原理让身体边缘比中心更亮。菲涅尔效果在 GLSL 里实现很简单但在视觉上非常关键——它让水母从深色背景中“浮”出来而不是一团扁平的半透明影子。4.2 后处理与景深控制为了让画面更有质感我加入了 UnrealBloomPass 做泛光处理再叠加一层自定义的散景模糊模拟漂浮焦平面。泛光的强度控制在 0.6 左右太高会让画面变成一片白雾丢失深海感太低又没有发光效果。景深方面我让焦点持续在场景中心的一个虚拟平面上前方和后方都会做轻微模糊这样观众的目光会自然地聚焦在画面中景的水母群上。后期处理管线还要注意色调映射。WebGL 默认的线性输出在暗场景里容易偏灰我改成 ACES 近似色调映射后暗部更深邃亮部更柔和。这一步改动的成本极低但画面档次提升非常明显。4.3 音频联动的现场调试音频联动是基于 Web Audio API 的 AnalyserNode把麦克风输入分成低频、中频、高频三段。低频控制水母群体的整体脉冲强度——低音鼓点会让所有水母同时大幅收缩中频控制触手摆动幅度对应人声或乐器旋律高频则控制画面边缘的细小粒子扰动模拟水中漂浮的微生物被声波惊动。现场调试时有一个常见坑麦克风靠近扬声器会产生啸叫测试时就会让水母疯狂抽动。解决方法是给音频信号加一条低通滤波和自动增益控制把过高频的信号削减掉并限制瞬时动态范围。另外别忘了做输入电平可视化否则你不知道现场到底是麦克风坏了还是画面反应函数写错了。5. 性能与稳定性现场展示最怕翻车5.1 渲染优化的三个硬指标我做性能优化时只盯三个指标Draw call 数量、着色器指令数、像素发送量。Draw call 是绘制调用的次数对 WebGL 性能影响最大。第一版我在场景里放了三百只水母每只水母由伞盖、触手、口腕三个网格组成总共产生了九百多次绘制调用帧率直接掉到不到 20。后来我把同一只水母的所有网格合并成一个网格再用 Three.js 的 InstancedMesh 实例化渲染六百只水母只产生一次绘制调用帧率立刻拉回满帧。着色器指令数决定了 GPU 每像素的工作量。我在这个项目里刻意减少了片元着色器中的条件分支尽量用数学函数取代替 if-else因为 GPU 在大量像素执行分支时会明显降速。像素发送量则通过渲染分辨率的动态缩放来控制——低性能设备上我把渲染分辨率限制到画布大小的 80%视觉效果几乎无感知但帧率能提升近一倍。下面是现场展示时的参数预算表参数项高配大屏普通笔记本手机端水母数量600300120渲染分辨率100%90%75%泛光后处理开启开启降强度关闭触手细分度高中低目标帧率60fps50fps30fps5.2 移动端与低配机的降级方案移动端的最大问题不是 GPU 性能而是散热的不可控。手机运行 WebGL 一两分钟后会因过热开始强制降频帧率呈现断崖式下跌。做法是把移动端水母数量上限压在 120 只关闭后处理并且强制限制帧率到 30——与其让帧率在 30 到 60 之间跳动不如锁死在一个稳定值观众的视觉体验反而更好。移动端触控方面iOS Safari 有惯性滚动和双击放大的默认行为会在视觉画面上抢触摸事件。我通过 touch-action: none 和 preventDefault 把页面变成全屏手势保护区才能保证滑动视图流畅不出现橡皮筋效果。5.3 稳定性测试清单现场展示翻车通常不是画面的问题而是程序在长跑中慢慢崩溃。这个项目上我执行了一套相当严格的测试清单连续运行 24 小时以上每两小时记录内存和帧率。反复执行页面刷新、浏览器标签页切换检查 WebGL 上下文不会丢失。用加载脚本模拟音频输入和非音频输入的高频切换。在低端 CPU 机器上记录事件响应延迟确保不会越积越多。检查 uniformly 慢速增长的数组避免每帧创建新对象导致 GC 抖动。关于内存一个很有趣的经验WebGL 不会像 CSS 动画那样直接因为 DOM 节点过多而卡顿但 JS 侧的数据如果不断增长最终会导致帧率周期性抖动。我在 s imulation 层复用了所有临时数组每帧只重复读写同一块内存几乎杜绝了垃圾回收的爆发。6. 现场调试与常见问题实录6.1 透明渲染排布问题半透明物体的排序问题是所有 WebGL 新手都会遇到的题目。水母的伞盖和触手都是半透明材质多边形被绘制出来时如果后方的几何体先被覆盖就会导致视觉上出现“边缘断层”和“闪现”。最直接的解决思路是把半透明物体按深度排序后绘制——画家算法先画远的再画近的。但水母身体是动态变形的每帧排序的代价很高。我的折中方案是抽取每个网格实例的中心深度只做实例间排序不做网格内三角形的精细排序。这样排序数从数十万降到几百视觉上的错误也几乎不可见。同时把伞盖材质的深度写入关闭避免半透明物体之间互相遮挡产生黑色剪影。6.2 低帧率排查流程遇到帧率下降我的排查顺序是一层一层往下走先用 Renderer.info 看 Draw call 和三角形数三角形数过高就降低几何细分Draw call 过高就合并网格或者改用实例化渲染这两项合理但帧率仍低再用性能分析工具看 CPU 端耗时CPU 高就去查是否是每帧在做昂贵的数组排序或噪声计算CPU 合理但 GPU 高就降低后处理强度或降低渲染分辨率。实际操作中最容易被人忽略的是 JS 侧的垃圾回收。如果你在动画循环里频繁创建新数组、新对象帧率会有规律地像锯齿一样抖动。对比一个案例水母数量 300 只时我原先每帧更新每个实例的矩阵会新建一个临时矩阵对象修掉这个细节后帧率从平均 38fps 提升到了 55fps。6.3 适配现场设备的经验清单现场展示和本地开发完全不是一回事我整理了一张踩过坑的清单现场问题原因解决方式大屏画面模糊大屏分辨率高但浏览器缩放比例不对手动检查 window.devicePixelRatio按实际值设定渲染分辨率长时间显示后烧屏残影部分商用显示屏没有自动像素偏移展示端关闭节能画素偏移或在画面角落放一个缓慢漂移的粒子浏览器标签一晚上后掉帧系统进入休眠后定时器降频展示前关闭系统休眠浏览器开启持续动画模式音频画面联动无反应现场麦克风距离过远改用线路输入音频信号而非麦克风增益提高到设置值触摸屏误触观众误触导致视角乱晃增加一段视角平滑时长并设置复位热键这些经验看着琐碎但每一条在真实现场都可能决定项目成败。我第一次做现场展示时就是没有关闭系统休眠结果展示到中途画面变成壁纸主办方差点以为设备坏了。这个项目从头到尾最让我受益的其实是把“好看”和“能跑”之间的距离不断拉近的那个过程。水母群复杂的群体行为可以简化为三条规则炫目的发光效果只需要几行着色器代码性能危机往往来自一个不经意的新数组。最后的建议是先从一只水母开始调确认它真正“活”了再复制成一百只一千只。先让一只水母的呼吸节奏足够迷人等倍数放大时才不会出现一群面无表情的木偶。如果你在做类似的生成视觉项目不管题材是水母还是飞鸟核心方法都是相通的——把每个子系统打磨到足够简单再把它们组合起来复杂的美感往往来自简单规则的叠加。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑