粒子越拖越慢,流体实验场怎样控制更新范围
在 PARTICLE FIELD 04 这个实验场里我把发射器从点源改成喷射粒子数量一路涨到 12,480。一开始帧率稳稳 60等我把涡旋强度拉到 0.78、再拖几下发射器画面就从边缘开始掉帧了——粒子密集的地方卡得最厉害拖动发射器的手势跟着发飘。右下角帧率 60 fps的字样没变但肉眼已经能看出粒子在跳格子。问题出在哪我第一反应是渲染慢。把每个粒子的 Circle 换成纯色矩形、关掉描边帧率没回来。直到我在 step 函数里埋了个计数器才发现每帧要跑上亿次距离判断。这个数怎么来的12,480 个粒子两两配对就是 12,480 × 12,480 ÷ 2约 7,790 万对再乘上取平方根和乘加单帧轻松破亿。渲染反而没占多少是邻域检索把 CPU 吃满了。12,480 个粒子第一版是全量两两配对第一版的 step 长得非常直白step(dt:number):void{for(leti0;ithis.particles.length;i){constathis.particles[i]for(letji1;jthis.particles.length;j){constbthis.particles[j]constdxb.x-a.xconstdyb.y-a.yconstd2dx*dxdy*dyif(d20d2this.h2){// h2 32² 1024constdMath.sqrt(d2)constfthis.pressureForce(d)// 按距离算排斥a.vxf*(dx/d)a.vyf*(dy/d)b.vx-f*(dx/d)b.vy-f*(dy/d)}}}}这段逻辑没问题两个粒子距离小于 32px 就产生相互作用。坑在复杂度上——它是 O(n²)n 从几百涨到一万二以后每帧的运算量直接爆掉。我一开始还不信在 for 循环外面包了个计时12,480 个粒子跑一次 step 要 46ms。60fps 的帧预算是 16.7ms一个 step 吃掉将近三帧画面不卡才怪。渲染层背不了这个锅。Circle 在 ArkUI 里是原生组件一万多个粒子只要不重排布局GPU 画得动。慢的是这段 O(n²) 的 CPU 计算。46ms 花在哪先计时再动手46ms 不能靠感觉。step 外面包了计时连跑 100 帧取中位数避开首帧干扰。顺手挂了个常驻帧统计interfaceFrameStat{stepMs:number// 邻域检索与受力rebuildMs:number// 网格重建pairs:number// 实际距离判断次数fps:number}classFrameProfiler{privatestat:FrameStat{stepMs:0,rebuildMs:0,pairs:0,fps:60}privatelastLogAt:number0report(now:number):void{if(now-this.lastLogAt1000)return// 每秒最多一行this.lastLogAtnowconsole.info([Fluid] step${this.stat.stepMs.toFixed(1)}ms rebuild${this.stat.rebuildMs.toFixed(1)}ms pairs${this.stat.pairs}fps${this.stat.fps})}}第一版的日志长这样step46.2ms rebuild0.0ms pairs77880000 fps15。pairs 就是两两配对总数12,480 选 2 约 7,790 万。换成网格后同一行变成step2.1ms rebuild0.4ms pairs4700000 fps60——热点在哪一行不用猜。这行日志还揪出一个没想到的地方rebuild 第一版永远是 0因为压根没有网格加分帧重建后涨到 6~8ms才逼出后面 taskpool 那一串折腾。换掉两两配对让粒子只问附近的邻居想清楚以后方向就变了不抠循环细节直接换检索结构。粒子之间的相互作用有个物理前提——距离超过 32pxh就不算数。这意味着我根本不用问所有粒子两两之间什么关系只要问我附近 32px 以内有谁。空间哈希网格就是干这个的。把画布按 32px 切成格子每个粒子记到它所在的格子里。查询时只看自己的格子加上周围 8 个其余格子里的粒子距离必然超过 h直接跳过。先给数据建模。粒子和场分开粒子管自己的状态场管集合和检索exportinterfaceParticle{id:numberx:numbery:numbervx:numbervy:numberkind:number// 0 青 #70F4CD / 1 蓝 #7A83FF / 2 粉 #FF83C5phase:number// 涡旋相位让同一圈粒子错开}exportclassFluidField{privateparticles:Particle[][]privategrid:Mapnumber,number[]newMap()privatereadonlycellSize:number32privatereadonlyh2:number1024// 32²相互作用半径平方privategeneration:number0// 每次清空/重建自增作废旧任务constructor(){for(leti0;i12480;i){this.particles.push({id:i,x:90(i*73)%540,y:50(i*97)%360,vx:0,vy:0,kind:i%3,phase:(i*0.618)%1})}}}粒子颜色沿用界面里的三个色板值 #70F4CD、#7A83FF、#FF83C5按i % 3轮着分。phase存黄金分割小数旋涡模式下用来让同一圈粒子错开相位不至于挤成一条线。格子的键值用整数把二维坐标压成一维。场景 640×480格子 32px横向最多 20 个格子key 编码成cx * 4096 cyprivatekeyOf(cx:number,cy:number):number{returncx*4096cy}privaterebuildGrid():void{this.grid.clear()for(leti0;ithis.particles.length;i){constpthis.particles[i]constcxMath.floor(p.x/this.cellSize)constcyMath.floor(p.y/this.cellSize)constkeythis.keyOf(cx,cy)constbucketthis.grid.get(key)if(bucketundefined){this.grid.set(key,[i])}else{bucket.push(i)}}}有个细节容易被忽略格子 32px相互作用半径也是 32px一个粒子要看的邻居不光是自己的格子还有周边 8 个。因为粒子可能落在格子边缘离它 30px 远的粒子已经跨到隔壁格子去了。邻域查询要把 3×3 的格子都扫一遍再用距离二次校验。格子和半径对齐以后查邻居就是顺着同一套坐标走一遍。privateneighborsOf(p:Particle):number[]{constout:number[][]constcxMath.floor(p.x/this.cellSize)constcyMath.floor(p.y/this.cellSize)for(letdx-1;dx1;dx){for(letdy-1;dy1;dy){constbucketthis.grid.get(this.keyOf(cxdx,cydy))if(bucketundefined)continuefor(constjofbucket){constqthis.particles[j]if(q.idp.id)continueconstddxq.x-p.xconstddyq.y-p.yif(ddx*ddxddy*ddythis.h2){out.push(j)}}}}returnout}continue跳过空桶是必要的——3×3 九个格子里靠近边界或粒子稀的地方一大半是空的。这个分支是性能的关键真正做了距离判断的只有自己格子周围的几十个粒子全场 12,480 个根本轮不到逐个问。改完我重新计时一次 step 从 46ms 掉到 2.1ms稳稳进了 16.7ms 的帧预算。场里的粒子数、涡旋强度这些面板值不用动画面回来了。数值上也说得通1.5 亿次两两距离判断换成网格以后每帧只剩 470 万次左右砍掉 30 倍。界面这一侧的截图就是当时的状态右侧面板的粒子数量 12,480涡旋强度 0.78粘度 0.24还是那组数据把重建拆进每一帧别再让主线程卡住网格索引本身也有成本。12,480 个粒子每帧都要重新入桶rebuildGrid一次要跑 1.2 万次Math.floor加 Map 插入低端机上就是 6~8ms。它和 step 一样跑在主线程两者叠加又会顶到帧预算。我第一版想把它丢进 taskpool 后台线程。跑起来才发现kit.ArkTS的 taskpool 任务返回值只认可序列化的类型Mapnumber, number[]不能直接传回来要么序列化成数组反而更慢要么把网格常驻在 worker 里维护同步又成了新问题。折腾半天干脆没用多线程改成主线程分帧重建——把 12,480 个粒子的入桶拆成每帧 2048 个中间让出事件循环asyncrebuildGridChunked(gen:number):Promiseboolean{constCHUNK2048this.grid.clear()for(letstart0;startthis.particles.length;startCHUNK){if(this.generation!gen){returnfalse// 用户中途清空本次重建作废}constendMath.min(startCHUNK,this.particles.length)for(letistart;iend;i){constpthis.particles[i]constcxMath.floor(p.x/this.cellSize)constcyMath.floor(p.y/this.cellSize)constkeythis.keyOf(cx,cy)constbucketthis.grid.get(key)if(bucketundefined){this.grid.set(key,[i])}else{bucket.push(i)}}awaitnewPromisevoid((resolve)setTimeout(resolve,0))}returntrue}那个generation检查就是失败分支。为什么需要它用户点一下清除按钮this.particles瞬间清空但上一轮rebuildGridChunked可能才跑到第 6000 个粒子await让出以后它还会继续往下跑把已经删掉的粒子重新塞回网格。所以清空时自增generation旧任务每批开头对一下发现编号变了就return false不再写任何东西。setTimeout(resolve, 0)的代价是每批多等一个宏任务2048 个一批12,480 个要拆 7 批多出来的延迟在 1ms 以内肉眼无感但主线程的输入事件能插进来了拖动发射器不再发飘。工程结构不复杂逻辑集中在特征层页面只负责摆组件FluidParticles/ ├─ entry/src/main/ets/ │ ├─ pages/Index.ets # 侧栏、画布与场参数面板 │ ├─ feature/FluidField.ets # 粒子集合、网格索引与邻域查询 │ └─ entryability/EntryAbility.ets # 应用入口 ├─ entry/src/main/resources/ # 颜色与文字资源 └─ build-profile.json5h 换成 48网格要不要重切网格能跑快全靠 cellSize 等于相互作用半径 h。现在都是 32查 3×3 就够h 改成 48 而格子还是 32隔壁格子的粒子只差 16px 就能撞上3×3 扫不到力就漏了。h 要变网格必须跟着重切exportfunctioncheckGridConfig(cellSize:number,h:number):boolean{// cellSize 小于 h 时3×3 邻域覆盖不全会漏检跨格邻居returncellSizeh}另一个边界是画布本身。粒子被涡旋甩出 640×480 后格子坐标算出负值keyOf没做偏移就会和别的格子撞键。入桶之前先把位置夹回画布越界的按反弹处理速度乘 0.5 折回来。钳制放在 step 之后、rebuild 之前顺序不能反先钳后重建网格里存的全是合法坐标反过来的话负坐标已经进了 Map下一帧的邻域查询全带着脏键。边界说明这套空间网格只保证画布内的邻域检索正确粒子被推出 640×480 的边界后格子坐标会算出负值keyOf里没做偏移就会撞键。实际应用里我加了边界钳制粒子位置先夹回 [0, 640] × [0, 480] 再入桶越界粒子按反弹处理不进检索。另外网格的 cellSize 必须等于相互作用半径 h如果以后改了粘度让 h 变成 48网格还得跟着重切否则边界格子里的邻居会漏检。这段代码纯粹是本地流体实验的数据结构思路不涉及真实流体力学求解器别拿它去做工程仿真。

相关新闻

从零了解 Langfuse 6:Agent 与 Tool 调用观测

从零了解 Langfuse 6:Agent 与 Tool 调用观测

内容核验:2026-10-15,参照当日 Langfuse 官方 v4 文档。本文以概念和设计示意为主,不是完整可运行项目;示例金额、分数、通过率和耗时均非实测数据。AgentScope/MCP 节点名称是业务埋点示例,需要显式配置语义映射。版本…

2026/10/19 7:17:12 阅读更多 →
让 AI 重构旧代码,怎样把“行为不变”变成可检查的要求?

让 AI 重构旧代码,怎样把“行为不变”变成可检查的要求?

“行为不变”,要先说清楚哪些行为 旧代码功能正常,只是函数太长、命名难懂、重复判断越来越多。这时让 AI 帮忙整理,很容易得到一份看起来更漂亮的代码。但代码变短了、分支变少了,并不能说明重构成功。 真正需要验收的是&#…

2026/10/19 7:17:12 阅读更多 →
基于Python与face_recognition的人脸识别考勤系统:从摄像头到SQLite完整实现

基于Python与face_recognition的人脸识别考勤系统:从摄像头到SQLite完整实现

简介:面向计算机相关专业毕业设计与课程设计需求,这份基于深度学习的人脸识别考勤系统源码,提供了人脸检测、特征提取、打卡记录等完整流程。项目经导师指导并通过评审,得分98分,适合正在做大作业、期末项目或毕设开发…

2026/10/19 7:17:12 阅读更多 →

最新新闻

基础模型重塑广告竞价环境建模:Bid2X 解读

基础模型重塑广告竞价环境建模:Bid2X 解读

广告竞价这个领域,真正难的不是“怎么出价”,而是“出价之后到底会发生什么”。KDD25 这篇 Bid2X,标题里最抓人的就是“环境建模”这四个字——它把视角从策略本身拉远到了策略所处的整个动态拍卖市场。基础模型视角的引入,让环境…

2026/10/19 7:59:32 阅读更多 →
Linux下Gitee托管实战:从SSH配置到高频报错排查

Linux下Gitee托管实战:从SSH配置到高频报错排查

年前有个朋友从 Windows 桌面端切到 Linux 环境干活,第一天就差点把键盘拍碎——他在 Gitee 上建好了仓库,本地也写好代码了,结果对着那个黑底白字的终端窗口发了好一会儿呆,完全不知道该敲什么。后来他问我,Gitee 在 …

2026/10/19 7:59:32 阅读更多 →
LSTM财务因子选股:时间序列建模与A股非平稳数据实战

LSTM财务因子选股:时间序列建模与A股非平稳数据实战

简介:本资源是一套基于LSTM神经网络的财务因子选股预测模型完整实现,专为计算机、人工智能及财经交叉方向的本科生毕业设计与课程设计打造,面向AI、自动化、电子信息等专业学生及初入量化领域的学习者,解决金融时序数据建模与股票…

2026/10/19 7:59:32 阅读更多 →
室内墙面缺陷检测实战:VOC/YOLO双格式数据集与目标检测训练要点

室内墙面缺陷检测实战:VOC/YOLO双格式数据集与目标检测训练要点

做了几年计算机视觉相关的工作,我接过不少“看着简单、做起来抓狂”的项目,墙面缺陷检测就是其中一个。前段时间参与一个旧房翻新前评估的项目,需要统计室内墙面的剥落、污渍和滴痕分布,我一开始天真地想拿通用目标检测模型直接跑…

2026/10/19 7:59:32 阅读更多 →
MSP432+OpenMV寻迹小车控制方案:从寄存器到PID调优

MSP432+OpenMV寻迹小车控制方案:从寄存器到PID调优

简介:2022年电子设计竞赛C题江苏省赛区一等奖的完整工程资料,主控采用MSP432,适合电赛备赛学生、嵌入式开发者与机器视觉入门者参考。作品围绕主从双车寻迹与OpenMV视觉导航展开,包含主车图像处理、停止线识别、障碍物测距以及双车…

2026/10/19 7:59:32 阅读更多 →
U8G2 OLED中文显示全攻略:字体包安装与编译配置

U8G2 OLED中文显示全攻略:字体包安装与编译配置

简介:本资源是面向Arduino嵌入式开发者的U8G2中文字体定制工具包,专为解决OLED/SSD1306等小型显示屏在U8G2库中无法直接显示中文的痛点而设计。包内集成GUI字体生成工具GUITool及完整u8g2-master源码,支持用户自定义字库提取、BDF格式转换与C…

2026/10/19 7:58:32 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介:本资源为电源技术方向的学术研究文档,聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略,面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址: https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集,共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →