尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

用p5.js构建光之城:粒子系统与网格算法模拟城市光影演化

用p5.js构建光之城:粒子系统与网格算法模拟城市光影演化 1. 项目缘起从“光之城”到一场光影实验最近在整理硬盘里的旧项目翻到了一个名为“A City of Light”的文件夹。这个名字听起来有点宏大甚至带点科幻感但其实它源于一个非常具体且个人化的想法我想用代码和算法去模拟和构建一座纯粹由光线构成的城市。没有实体建筑没有街道网格只有光的流动、交汇与湮灭以及由此产生的秩序与美感。这个想法的萌芽源于几年前一次在机场的长时间滞留。深夜的候机大厅窗外是跑道上绵延的指示灯带远处城市的霓虹勾勒出天际线的轮廓航站楼内各种屏幕、指示灯和广告牌的光交织在一起。那一刻我突然觉得现代城市在夜晚呈现出的更像是一个由无数独立光点与光流组成的复杂系统而非白天的钢筋水泥森林。这些光点有自己的“生命周期”亮起、闪烁、熄灭有自己的“运动轨迹”车灯流、广告牌变化它们之间似乎存在着某种我们尚未完全理解的、动态的、非实体的“建筑学”。“A City of Light”项目就是试图用程序化的手段去探索这种“光的建筑学”。它不是一个游戏也不是一个可视化工具更像是一个数字沙盘或生成艺术实验。核心目标是定义一套关于“光”的生成规则然后让这个系统自行演化观察是否能从中涌现出类似城市结构的复杂模式——比如光点是否会自发聚集形成“中心区”光流是否会遵循某种“交通规则”形成主干道不同颜色和亮度的光之间是否存在“吸引”或“排斥”听起来很抽象对吧但实操起来每一步都充满了工程细节和审美抉择。接下来我就把这个项目的完整构建过程、核心算法、踩过的坑以及一些意外的发现毫无保留地分享出来。无论你是生成艺术爱好者、创意编程的实践者还是单纯对“用代码创造世界”感到好奇相信都能从中获得一些启发。2. 核心架构如何为“光”立法构建一个虚拟的光之城首要任务不是画图而是立法——为这个虚拟世界中的“光”设定一套基本物理规则和行为准则。这直接决定了最终演化出的形态是混乱的噪点还是具有美感的复杂结构。我的设计围绕几个核心实体展开光粒子Particles、光源Emitters和光感网格Grid。2.1 光粒子城市的基本居民光粒子是这座城市最基本的“居民”和“建筑材料”。每个粒子都是一个具有状态的独立对象。我为其定义了以下属性位置 (x, y): 在二维画布上的坐标。我选择二维而非三维是为了降低计算复杂度优先保证规则演化的流畅性并且二维平面更能突出“地图”或“蓝图”的意象。速度 (vx, vy): 决定粒子每帧移动的方向和距离。这是动态感的来源。能量/亮度 (energy): 一个从0.0到1.0的浮点数。它决定了粒子的显示亮度和颜色饱和度也影响着它的“生命力”。能量会随时间衰减。颜色 (hue): 用色相值表示。不同颜色的粒子可以代表不同性质的“光”比如暖光偏红黄可能代表商业区冷光偏蓝紫可能代表居住区。类型/标签 (type): 一个整数标签用于区分粒子的“种类”。这是实现复杂交互规则的关键。例如类型0的粒子可能被类型1的光源吸引但排斥类型2的粒子。粒子的行为由几个简单的规则驱动运动每帧根据速度更新位置。x vx; y vy;能量衰减每帧能量值乘以一个衰减系数如0.995模拟光在传播中的耗散。当能量低于阈值如0.01时粒子“死亡”被从系统中移除。边界处理粒子到达画布边缘时我选择了“环绕”策略从一边出去从对边进来这能创造一个无边界的、循环的宇宙更适合观察模式的无限演化。随机扰动为速度添加一个微小的随机值模拟环境中的不确定性和“布朗运动”避免系统过于僵化。注意粒子数量是性能的关键。在初期我贪心地初始化了上万个粒子结果帧率骤降。后来我意识到一个精巧的规则系统几百个粒子就足以涌现出丰富模式。“少即是多”在这个项目中是黄金法则。我最终将粒子数控制在500-2000之间并通过调节衰减速率来控制系统的总“人口”。2.2 光源城市的发动机与地标如果只有自由运动的粒子系统很容易趋于均匀的混沌。需要引入“光源”作为结构的锚点和动力源。光源是静态或半静态的实体它们的主要作用是生成新粒子和施加力场。一个典型的光源定义如下位置固定或按简单路径移动如沿椭圆轨道。发射参数包括发射频率每秒多少个粒子、初始速度、能量和颜色。力场类型吸引力场像一个重力源使一定范围内的粒子加速飞向光源。可以用来模拟“城市中心”的聚集效应。排斥力场将粒子推离光源。可以用来划定“禁区”或创造粒子流的通道。涡旋力场给粒子施加一个切向力使其绕光源旋转。能形成美丽的螺旋结构或环状交通流。光源的代码逻辑相对直接核心是在每帧判断是否达到发射间隔然后生成带有特定初始属性的新粒子。力场的实现则涉及计算粒子与光源的距离和方向然后根据力场类型和强度通常随距离衰减更新粒子的速度。// 伪代码示例吸引力场计算 function applyAttraction(particle, emitter) { let dx emitter.x - particle.x; let dy emitter.y - particle.y; let distanceSq dx*dx dy*dy; // 避免除零并设置力场作用范围 if (distanceSq 0 distanceSq forceRange*forceRange) { let distance Math.sqrt(distanceSq); // 力的大小与距离成反比或平方反比 let force emitter.strength / distance; // 归一化方向向量并施加力 particle.vx (dx / distance) * force; particle.vy (dy / distance) * force; } }2.3 光感网格隐形的城市规划师这是整个项目中最关键、也最精妙的一环。仅仅有粒子和光源它们的交互是“点对点”的计算复杂度是O(N²)且难以形成大尺度的、稳定的结构。为了解决这个问题我引入了光感网格Light-Sensing Grid。其核心思想是将整个画布划分为一个均匀的网格比如100x100的单元格。每一帧我们不仅更新粒子还更新网格。每个网格单元格记录两种关键信息光密度该单元格内所有粒子能量的总和。这反映了该区域的“亮度”或“活跃度”。光流方向可选该单元格内粒子速度的平均向量。这反映了光的“流动趋势”。然后粒子在运动时会“感知”它所在单元格及相邻单元格的网格信息并据此调整自己的行为。这就将全局的、混乱的交互转变为局部的、有据可循的决策。具体规则可以非常灵活趋向光密度梯度粒子可以设定为向光密度更高的区域移动“趋光性”模拟人群向繁华地带聚集。反之也可以设定为避开高密度区寻求“安静”。沿光流方向对齐类似于鸟群模拟中的对齐行为粒子调整自己的运动方向使其与局部平均光流方向相近。这能迅速形成协调一致的光流宛如车流。网格作为持久化痕迹光密度不会每帧清零而是以较慢的速度衰减。这样高频粒子路径会在网格中留下“光轨”痕迹这些痕迹又能引导后续的粒子形成正反馈稳定住某些通道。这模拟了“道路越走越宽”的城市生长逻辑。通过调整网格的分辨率、感知半径以及粒子对网格信息的响应强度你可以创造出从精细纹理到宏大结构的不同尺度模式。这个网格系统本质上是一个低分辨率的、动态更新的“环境记忆”它是涌现出复杂城市样貌的真正“城市规划师”。3. 实现链路从伪代码到视觉呈现有了理论架构接下来就是具体的实现。我选择使用p5.js作为创作工具因为它对创意编程极其友好能快速实现图形交互且完全在浏览器中运行便于分享和迭代。3.1 环境搭建与初始化首先创建一个标准的HTML文件引入p5.js库。在setup()函数中初始化画布、粒子数组、光源数组和网格数据结构。let particles []; let emitters []; let grid []; let gridCols, gridRows, gridSize; function setup() { createCanvas(800, 800); gridSize 10; // 每个网格单元格10像素 gridCols width / gridSize; gridRows height / gridSize; // 初始化网格为二维数组 for (let i 0; i gridCols; i) { grid[i] []; for (let j 0; j gridRows; j) { grid[i][j] { density: 0, flowX: 0, flowY: 0 }; } } // 创建几个初始光源 emitters.push(new Emitter(width/4, height/2, attract)); emitters.push(new Emitter(3*width/4, height/2, vortex)); // 创建初始粒子群 for (let i 0; i 100; i) { particles.push(new Particle(random(width), random(height))); } }3.2 主循环逻辑一场精密的协同舞蹈每一帧的draw()函数都是一次完整的系统更新顺序至关重要清空与重置网格将网格中所有单元格的光密度和光流方向重置为零或者乘以一个小于1的衰减系数以实现痕迹留存。更新并渲染光源每个光源根据其参数决定是否发射新粒子并施加力场如果有力场。光源本身可以用简单的图形绘制。更新粒子这是最耗时的部分。对每个粒子 a.应用物理规则速度衰减、边界环绕、随机扰动。 b.查询网格根据粒子当前位置计算所在的网格坐标并读取周围单元格的信息密度、流场。 c.基于网格决策根据预设规则结合查询到的网格信息计算出一个“决策力”叠加到粒子的速度上。例如决策力 趋向高密度区的力 与流场对齐的力。 d.更新位置用最终的速度更新位置。 e.贡献于网格将粒子自身的能量和速度向量累加到它当前所在的网格单元格中为下一帧的其他粒子提供环境信息。 f.能量衰减与死亡判断。渲染粒子根据粒子的能量和颜色在对应位置绘制。这里有个重要技巧为了获得光晕和混合效果不要用纯色实心圆。我使用了一个半透明的圆形渐变并且将画布的混合模式设置为blendMode(ADD)。ADD混合模式会让重叠的颜色亮度相加完美模拟光的叠加效果视觉效果瞬间提升一个档次。可选渲染网格出于调试目的可以绘制网格线甚至用颜色深浅表示网格密度这能让你直观地看到“城市规划图”是如何实时变化的。function draw() { background(0, 10); // 用带透明度的黑色清屏产生拖尾效果 blendMode(ADD); // 关键启用加法混合模式 // 1. 衰减网格记忆实现痕迹留存 for (let i 0; i gridCols; i) { for (let j 0; j gridRows; j) { grid[i][j].density * 0.98; grid[i][j].flowX * 0.95; grid[i][j].flowY * 0.95; } } // 2. 更新光源 for (let e of emitters) { e.update(); e.show(); } // 3. 更新并渲染粒子 for (let i particles.length - 1; i 0; i--) { let p particles[i]; p.update(); // 此方法内部包含查询网格、决策、贡献网格的逻辑 p.show(); if (p.isDead()) { particles.splice(i, 1); // 移除死亡粒子 } } blendMode(BLEND); // 恢复默认混合模式用于绘制UI等 // ... 绘制帧率、粒子数等调试信息 }3.3 性能优化让城市流畅运转当粒子数量上千网格分辨率较高时性能压力很大。我采用了以下几个优化策略空间划分虽然我们已经有了网格用于逻辑但渲染前的可见性判断仍可用一个简单的空间划分如四叉树来优化只渲染在视口内的粒子。不过对于全屏演算这个优化收益一般。简化计算在距离计算时优先使用平方距离进行比较避免耗时的Math.sqrt()开方运算。力场计算中使用近似函数。控制粒子生命周期确保死亡粒子被及时清理避免数组无限膨胀。降低渲染质量在draw()中可以通过判断帧率动态调整粒子的绘制细节比如用点代替圆降低透明度计算精度。实操心得调试这类动态系统一个可交互的控制面板至关重要。我使用p5.gui库快速创建了滑块用来实时调整粒子数量、衰减率、力场强度、网格影响权重等数十个参数。通过实时调节并观察效果你能直观地理解每个参数如何影响系统的宏观行为这是纯代码调试无法比拟的高效。4. 参数调优寻找秩序的涌现代码跑通只是开始让这座光之城呈现出有趣的结构才是真正的艺术。这个过程没有公式全靠对参数的反复调试和观察。我将主要参数分为几类并分享一些调优经验参数类别具体参数示例对系统的影响调优思路粒子属性初始速度、能量衰减率、随机扰动强度决定个体的“活力”和“寿命”。衰减快则城市闪烁急促扰动大则结构松散。寻求平衡过快的粒子难以形成结构过慢的粒子又显得呆滞。我通常让粒子有足够时间穿越屏幕衰减率设置在0.985-0.995之间。光源属性发射频率、力场类型与强度、作用范围是结构的“种子”和“骨架”。吸引力场创造聚集点涡旋场创造动态中心。不要所有光源都一样。设置一个强吸引力的核心光源几个弱排斥的边界光源再加一个涡旋光源制造动感结构层次就出来了。网格规则网格大小、感知半径、趋向系数、对齐系数是涌现出宏观模式的关键。网格大小决定结构的精细度趋向和对立系数决定粒子是“从众”还是“特立独行”。从极端值开始测试。先将趋向系数设为0观察无引导下的混沌再调至很高观察粒子是否疯狂涌向几个点。对齐系数能产生流线是形成“道路”感的核心。视觉渲染粒子绘制大小、颜色映射、混合模式、背景拖尾直接影响美学效果。ADD混合模式是灵魂带透明度的背景清屏能产生运动轨迹。颜色可以随机也可以与粒子能量、速度或类型挂钩。例如能量越高越偏白/黄高亮速度越快越偏蓝多普勒效应想象。一个典型的调优过程是这样的设定初始场景放置2-3个不同类型的光源初始化几百个随机粒子。关闭网格影响将所有网格相关权重设为0观察粒子和光源基础力场下的行为。此时系统通常比较简单粒子要么被吸入光源要么无序运动。引入网格趋向性逐步调高“趋向光密度高区”的权重。你会看到粒子开始在某些区域聚集这些区域往往是光源附近或粒子偶然形成的密集区。正反馈开始形成一些聚集点被强化。引入流场对齐调高“对齐局部光流”的权重。奇迹开始发生聚集点之间的粒子运动方向开始趋于一致形成清晰的“光流通道”看起来就像城市的主干道。粒子在通道中流动进一步强化了通道的痕迹。微调与破坏系统可能陷入一种过于稳定的状态。此时可以微调某个排斥光源的强度或者偶尔“注入”一股高速粒子流打破平衡观察系统如何自我重组。往往在重组过程中会出现更意想不到的美丽结构。5. 超越模拟从生成到交互当系统能够稳定地生成有趣的光影城市后我便开始思考如何让它不仅仅是“屏保”而成为一个更具互动性的体验。我尝试了以下几个方向5.1 鼠标交互成为城市的“上帝”让用户通过鼠标直接干预这个世界是最直接的交互方式。鼠标作为临时光源当鼠标移动或点击时在鼠标位置生成一个临时性的力场吸引或排斥。你可以瞬间创造一个新的“市中心”或者划出一条“禁行线”观察城市光流如何因你而改变。鼠标作为“画笔”按住鼠标拖动可以持续在路径上生成特定类型的粒子。这相当于你在亲手“规划”一条光之路系统原有的规则会基于你画的这条“路”进行后续演化。鼠标作为“橡皮擦”设定一个区域可以清除该区域内所有粒子的能量或直接移除粒子模拟一场局部的“停电”。这些交互的实现只需在mouseMoved()或mouseDragged()事件中向粒子系统施加额外的力或生成特殊粒子即可。它极大地增加了项目的可玩性和探索性。5.2 音频驱动让城市随音乐脉动将声音输入引入系统让光的强度、颜色或粒子的生成频率随着音乐的节奏、音量或频率变化可以创造出极具感染力的视听体验。我使用p5.sound库的FFT对象来分析音频。基本思路是低音频率能量控制整体粒子的能量衰减率。重鼓点响起时降低衰减率让所有粒子瞬间变亮、寿命延长产生“全场爆亮”的冲击感。中高音频率能量控制新粒子的发射频率或随机扰动强度。旋律部分可以让城市的光点跳动得更活跃。整体音量可以映射到光源的力场强度上。音乐高潮时所有光源的吸引力或涡旋力加倍让整个城市结构发生剧烈动荡。let mic, fft; function setup() { // ... 其他初始化 mic new p5.AudioIn(); mic.start(); fft new p5.FFT(); fft.setInput(mic); } function draw() { // ... 其他更新逻辑 let spectrum fft.analyze(); let bass fft.getEnergy(bass); let treble fft.getEnergy(treble); // 根据音频能量动态调整全局参数 let energyDecay map(bass, 0, 255, 0.99, 0.97); // 低音强衰减慢 let perturbation map(treble, 0, 255, 0.1, 0.5); // 高音强扰动大 for (let p of particles) { p.decayRate energyDecay; p.perturbation perturbation; } }5.3 数据映射赋予城市真实“灵魂”这是最富挑战性也最有趣的一步用真实世界的数据来驱动这座光之城的生成规则让它成为某个现实城市的“数字光影孪生”。我尝试过的一个方向是使用城市实时交通流量数据。思路如下获取数据从开放数据平台获取城市主要路段的实时平均车速或拥堵指数。数据映射将每条道路映射为画布上的一条线段或曲线。将车速映射为这条“道路”上生成的粒子流的速度。车速快粒子流快且明亮绿色车速慢粒子流慢且暗红红色。将车流量映射为粒子发射的频率。流量大这条路上生成的光粒子就多。系统演化这些由真实数据驱动的“主干道光流”被注入到我们的自主演化系统中。它们作为强大的初始条件和持续输入会影响网格的密度和流场分布进而引导那些自由粒子的运动。于是你看到的不仅是数据可视化而是一个以真实交通为“地基”自主生长演化的光之城市。晚高峰时城市的光流可能会在几个关键节点汇集、变红、扩散出拥堵的“涟漪”而在深夜自由粒子可能会在几乎空白的“道路”上演化出白天不曾见过的宁静图案。这个方向的实现需要处理数据接口、坐标映射等更多工程问题但带来的成就感和启示是巨大的。它让这个生成艺术项目与现实世界产生了深刻的连接。6. 踩坑实录与经验沉淀回顾整个项目从构思到实现再到拓展踩过的坑不计其数。这里分享几个最典型、最影响结果的教训坑一数值平衡的脆弱性最初我随意设置了力场强度、网格影响权重等参数结果系统要么迅速坍缩成一个点要么彻底发散成一团乱麻。我一度怀疑规则设计有问题。根因不同作用力的数值尺度不匹配。例如光源的吸引力强度是0.1而网格趋向力的强度是5.0后者完全主导了系统行为。解决引入归一化和系数调节。将所有力计算为加速度速度的变化量并确保它们的数量级在同一个尺度比如0.01到0.5之间。然后为每一种力创建一个范围在0-1之间的独立权重参数通过GUI滑块精细调节。永远从一个极简的、只有一种力的系统开始逐步加入新力并观察平衡是如何被打破又如何重建的。坑二性能陷阱与“杀进程”在实现音频驱动时我每帧都对所有粒子根据音频重新计算颜色和大小导致帧率从60fps暴跌至15fps风扇狂转。根因在draw()主循环中执行了过于频繁或昂贵的计算如复杂的颜色映射函数。解决节流对于音频数据不需要每帧都分析。可以每5帧或10帧分析一次数据变化本身没那么快。简化计算将颜色映射从复杂的sin()、cos()函数改为简单的线性插值或查表法。离屏计算对于固定的、昂贵的纹理或背景可以预先计算好并存储在图像对象中而不是每帧重绘。黄金法则时刻关注帧率任何新功能加入后首要检查就是对性能的影响。坑三审美疲劳与“惊喜”的丧失当一套参数调出漂亮图案后我会长时间运行它但很快发现它虽然美但变化模式固定看久了会腻。根因系统达到了一个动态平衡或吸引子状态缺乏打破平衡的机制。解决引入“慢变量”和“随机事件”。慢变量让某些关键参数如某个光源的强度、网格衰减率随着时间非常缓慢地、周期性地变化。这样城市会经历极其缓慢的“昼夜更替”或“季节变迁”格局在数分钟内悄然改变。随机事件设定一个极低的概率比如每1000帧有1%的概率触发一个“事件”随机位置爆发一群高能粒子、某个光源暂时失效、全局力场方向翻转90度等。这些偶发的“扰动”能将系统踢出当前的平衡态落入另一个可能更有趣的平衡态中从而持续产生“惊喜”。坑四过度设计迷失核心有一段时间我沉迷于添加更多粒子类型、更复杂的交互规则比如粒子间化学反映、遗传算法代码变得臃肿不堪系统行为反而难以理解最初那种简洁优雅的美感消失了。根因忘记了项目的核心是“探索光的建筑学”而不是构建一个万能模拟器。解决做减法。我备份了复杂版本然后回滚到一个只有基础粒子、一种光源和网格趋向规则的干净版本。从这个坚实的基础上每次只增加一个最必要的新特性并充分测试其单独和组合效应。复杂性应源于简单规则的迭代与交互而非一开始就堆砌复杂规则。这个项目对我而言远不止于一段代码或一个动画。它是一次关于“秩序如何从简单中涌现”的持续对话。每一次参数调整都像是在与一个未知的生命体沟通每一次意想不到的图案生成都带来纯粹的发现乐趣。它教会我在创意编程中最重要的或许不是写出多么高效的算法而是培养一种“系统思维”并拥有像园丁一样的耐心去观察、引导而非强行控制你所创造的整个世界。如果你也对创造这样的“世界”感兴趣不妨就从定义几个点、几条简单的规则开始看看光会带你走向何方。
返回列表