简介:本资源为基于移动最小二乘物质点法(MLS-MPM)的高性能物理仿真求解器,源自SIGGRAPH 2018并发表于ACM图形学交易,采用C++实现,面向图形学、计算力学与物理引擎方向的开发者及研究者,可用于弹性体、断裂、切割与刚体双向耦合等复杂场景的仿真实验。压缩包共175个文件,约600KB,以C++源码为主,含24个cpp与57个h头文件,另有23个Python脚本、20个C文件及Makefile、configure.ac等构建配置,并附带许可证与说明文档,便于编译与二次开发。目前已有963人学习下载。资源完整呈现了CPIC求解器的核心实现,涵盖位移不连续性处理、双向刚体耦合与切割功能,并已采用MIT许可,可自由用于商业项目;其中mls-mpm88-explained.cpp等版本降低了阅读门槛,配合浏览器端与Unity实现线索,适合作为学习MLS-MPM算法与搭建仿真系统的实践参考。
1. 从一份 C++ 求解器源码说起:taichi_mpm 到底能跑出什么
如果你正在找一份能直接编译、能改、能拿来做图形学仿真实验的 C++ 求解器源码,taichi_mpm 大概率就是你要的东西。它实现的是移动最小二乘物质点法(MLS-MPM),一个在 SIGGRAPH 2018 上被系统提出的高性能仿真方案,配套论文发在 ACM 图形交易上。简单说,它把连续介质(雪、沙、果冻、布料这类东西)离散成粒子,用物质点法做积分,再用移动最小二乘做背景网格上的力插值,最终在 CPU 或 GPU 上跑出可交互的物理动画。适合谁?做图形学课程项目的研究生、想复现 SIGGRAPH 论文的开发者、需要给游戏或影视做可破坏/可变形物体的工程师。它不是一个开箱即用的引擎,而是一份结构清晰的 C++ 参考实现,读懂它,你就读懂了 MLS-MPM 从数学到代码的完整链路。
2. 架构拆解:粒子、网格与求解循环怎么串起来
2.1 物质点法的三个核心数据结构
MLS-MPM 的代码骨架其实就三样东西:粒子(Particle)、背景网格(Grid)、以及连接两者的插值核。粒子存质量、速度、形变梯度、体积这些物理量;网格存动量、质量,每帧清零重算。我一般会先看头文件里这几个结构体的字段,因为它们决定了你能改什么。
// 粒子结构:每个粒子携带的物理量 struct Particle { Vector3f x; // 当前位置 Vector3f v; // 速度 Matrix3f F; // 形变梯度,决定弹性/塑性 float Jp; // 塑性体积比,用于雪/沙的压实 float mass; // 质量 float vol; // 初始体积 }; // 背景网格节点:每帧只存动量和质量 struct GridNode { Vector3f mom; // 动量 float mass; // 质量 };逻辑说明:粒子是“永久”的,网格是“临时”的。每帧开始网格清零,粒子把质量和动量通过插值核散射到网格(P2G),网格上算完力再插值回粒子(G2P)。参数上,F和Jp是区分材料的关键——雪需要塑性屈服,果冻需要弹性,改这两个量的更新规则就能换材料。
2.2 P2G 与 G2P:一帧求解的完整链路
整个求解循环可以压缩成四步:粒子到网格(P2G)、网格更新、网格到粒子(G2P)、粒子位置更新。下面这段是 P2G 的核心,用二次 B 样条核把粒子量散射到周围 3×3×3 个网格节点。
// P2G:粒子量散射到网格 for (auto& p : particles) { // 计算粒子在网格中的归一化坐标 Vector3f base = (p.x / dx).array() - 0.5f; // 二次 B 样条权重,每个维度 3 个节点 for (int i = 0; i < 3; ++i) for (int j = 0; j < 3; ++j) for (int k = 0; k < 3; ++k) { Vector3f node = base + Vector3f(i, j, k); float w = weight(base - node); // 三维权重乘积 // 动量 += 质量 * 速度 * 权重 grid[node].mom += p.mass * p.v * w; grid[node].mass += p.mass * w; } }逻辑说明:weight是二次 B 样条函数,保证插值连续。dx是网格间距,通常取 1.0 归一化。参数怎么改?网格分辨率越高,仿真越精细但越慢;粒子数一般取网格数的 2~4 倍每维,太少会有空洞,太多浪费算力。
2.3 材料模型:雪、沙、果冻的差异在哪
taichi_mpm 里材料差异集中在 G2P 之后对形变梯度F的处理。弹性材料直接F = (I + dt * grad_v) * F;雪和沙需要做塑性投影,把F的奇异值限制在屈服面内。
// 塑性投影:以雪为例 Matrix3f F_trial = (Matrix3f::Identity() + dt * grad_v) * p.F; // SVD 分解 JacobiSVD<Matrix3f> svd(F_trial, ComputeFullU | ComputeFullV); Vector3f sig = svd.singularValues(); // 限制奇异值范围,模拟压实和屈服 sig = sig.array().max(1.0f - 0.5f).min(1.0f + 0.5f); p.F = svd.matrixU() * sig.asDiagonal() * svd.matrixV().transpose(); p.Jp *= (sig.prod() / F_trial.determinant());逻辑说明:sig的上下限就是屈服参数,改小让材料更硬,改大更软。Jp累积塑性体积变化,用于雪从蓬松到压实的过程。常见做法是把这些参数放到配置文件或命令行,方便调参。
3. 跑起来:编译、依赖与第一个仿真
3.1 环境准备与依赖清单
这份源码是标准 C++ 项目,依赖 Eigen 做线性代数,CMake 做构建。我一般会先确认三件事:编译器支持 C++17、Eigen 头文件路径、以及是否要开 OpenMP 或 CUDA。下面是最小依赖表。
| 依赖 | 用途 | 常见版本 |
|---|---|---|
| C++ 编译器 | 编译源码 | GCC 9+ / Clang 10+ |
| CMake | 构建系统 | 3.16+ |
| Eigen | 矩阵与 SVD | 3.3+ |
| OpenMP | CPU 并行 | 可选 |
| CUDA | GPU 加速 | 可选,11.0+ |
3.2 编译命令与常见报错
# 典型构建流程 mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release make -j8 # 运行一个内置场景 ./mpm_sim --scene snow --frames 200逻辑说明:-DCMAKE_BUILD_TYPE=Release必须开,Debug 下仿真慢到没法用。--scene和--frames是常见参数,具体名字看 main 函数里的解析。如果报 Eigen 找不到,手动指定-DEIGEN_INCLUDE_DIR=/path/to/eigen。OpenMP 没开的话,P2G 循环是单线程,粒子一多就卡。
3.3 验证跑通:看什么指标
跑起来后别急着改代码,先确认三件事:粒子位置在动、网格动量每帧清零、总能量没有爆炸。我一般会在 G2P 后打印最大速度,如果超过网格间距除以 dt 的几倍,说明时间步太大或刚度太高,需要调小 dt 或降低弹性模量。
提示:第一次跑建议用 32×32×32 网格、每维 2 个粒子,帧数 100 以内,确认逻辑正确再放大。
4. 避坑与排查:那些让我重跑一整天的细节
4.1 仿真爆炸:速度突然变成 NaN
现象:跑几十帧后粒子飞出去,打印速度是 NaN。原因:时间步 dt 不满足 CFL 条件,或者弹性模量太大导致力插值溢出。解决:把 dt 降到网格间距除以最大声速的 0.5 倍以下,或者把弹性模量除以 10 先跑通再调回。
4.2 粒子穿透网格:物体直接掉下去
现象:粒子穿过地面网格,没有碰撞。原因:碰撞处理是在网格上做的,如果网格节点速度没清零或边界条件没设对,粒子会直接穿。解决:检查网格更新后是否对边界节点做了速度归零或反弹,常见做法是把边界节点的动量设为零。
4.3 编译通过但结果全黑:粒子没渲染
现象:程序正常退出,但可视化窗口全黑。原因:粒子位置没更新,或者渲染器读的是旧缓冲区。解决:在粒子位置更新后加一句打印最大最小坐标,确认数值在合理范围;渲染器那边检查是否每帧重新上传粒子数据。
4.4 多线程下结果每次不一样
现象:开 OpenMP 后每次跑的结果有微小差异。原因:P2G 散射到网格时存在写冲突,多个粒子同时写同一个节点。解决:用原子操作或者按粒子分块归约,常见做法是每个线程维护局部网格再合并。
4.5 换材料后参数不生效
现象:改了塑性参数但仿真看起来没变化。原因:参数没传到材料更新函数,或者被默认值覆盖。解决:在 G2P 后打印一次F的奇异值,确认塑性投影真的执行了;检查参数解析是否在初始化时被硬编码覆盖。
5. 二次开发:从改参数到换材料模型
5.1 加一个新材料的完整步骤
想加一个“橡皮泥”材料,核心是改 G2P 后的塑性投影。我一般会新建一个Material枚举,在updateParticle里加分支。下面是一个最小改动示例。
// 在材料更新处加分支 if (mat == Material::Rubber) { // 橡皮泥:只限制体积,不限制剪切 float J = p.F.determinant(); if (J < 0.8f) p.F *= std::pow(0.8f / J, 1.0f/3.0f); if (J > 1.2f) p.F *= std::pow(1.2f / J, 1.0f/3.0f); }逻辑说明:J是体积比,限制在 0.8~1.2 之间模拟不可压但可轻微压缩。参数 0.8 和 1.2 就是你要调的“软硬”。改完记得在场景配置里把材料名映射到这个枚举。
5.2 性能优化:从单线程到多线程
P2G 和 G2P 是天然并行的,但 P2G 有写冲突。常见做法是每个线程维护一份网格副本,最后归约。代码上就是把grid换成grid[thread_id],循环后累加。注意内存会翻倍,网格大时用分块归约更省。
5.3 验证改造是否成功:三个检查点
改完材料或并行后,我强制走一遍三个检查:第一,跑一个已知场景,对比总动量是否守恒;第二,把粒子数翻倍,看耗时是否线性增长;第三,打印最大形变梯度的奇异值,确认在预期范围内。从那以后我每次改求解器核心都强制走一遍这三个检查,不然很容易在某个角落埋一个 NaN。希望帮到你。
本文还有配套的精品资源,点击获取