XML
<?xml version="1.0"?>
<!-- Loader and topology -->
<Node name="root" dt="0.01" gravity="0 0 0">
<RequiredPlugin name="Sofa.Component.Mass"/> <!-- Needed to use components [UniformMass] -->
<RequiredPlugin name="Sofa.Component.MechanicalLoad"/> <!-- Needed to use components [ConstantForceField] -->
<RequiredPlugin name="Sofa.Component.StateContainer"/> <!-- Needed to use components [MechanicalObject] -->
<RequiredPlugin name="Sofa.Component.LinearSolver.Iterative"/> <!-- Needed to use components [CGLinearSolver] -->
<RequiredPlugin name="Sofa.Component.ODESolver.Backward"/> <!-- Needed to use components [EulerImplicitSolver] -->
<RequiredPlugin name="Sofa.Component.IO.Mesh"/> <!-- Needed to use components [MeshGmshLoader] -->
<RequiredPlugin name="Sofa.Component.Topology.Container.Dynamic"/> <!-- Needed to use components [PointSetTopologyContainer] -->
<DefaultAnimationLoop computeBoundingBox="false"/>
<MeshGmshLoader name="meshLoaderCoarse" filename="mesh/liver.msh" />
<!--
<TransformEngine name="transformer"
input_position="@meshLoaderCoarse.position"
translation="0 2 0"
rotation="0 0 0" />
-->
<!-- position="@../transformer.output_position" -->
<Node name="Liver">
<EulerImplicitSolver />
<CGLinearSolver iterations="200" tolerance="1e-09" threshold="1e-09"/>
<PointSetTopologyContainer name="topo" src="@../meshLoaderCoarse" />
<MechanicalObject template="Rigid3d" name="MechanicalModel" showObject="1"/>
<UniformMass totalMass="1" />
<ConstantForceField totalForce="1 0 0 0 0 0" />
</Node>
</Node>
简化学习版:
XML
<Node name="root" dt="0.01" gravity="0 0 0">
<MeshGmshLoader name="meshLoaderCoarse" filename="mesh/liver.msh" />
<Node name="Liver">
<EulerImplicitSolver />
<CGLinearSolver iterations="200" tolerance="1e-09" threshold="1e-09"/>
<PointSetTopologyContainer name="topo" src="@../meshLoaderCoarse" />
<MechanicalObject template="Rigid3d" name="MechanicalModel" showObject="1"/>
<UniformMass totalMass="1" />
<ConstantForceField totalForce="1 0 0 0 0 0" />
</Node>
</Node>
这部分核心新增的是:
XML
<MeshGmshLoader name="meshLoaderCoarse" filename="mesh/liver.msh" />
1.MeshGmshLoader
MeshGmshLoader是一个 mesh loader,网格加载器。作用是:从 mesh/liver.msh 文件里读取网格信息。
这里的 .msh 是 Gmsh 软件常用的网格格式,所以组件叫 MeshGmshLoader。
loader 的工作方式基本都一样:在仿真初始化时,把文件里的 topology information 读出来。
2.Loader 读取的是什么:topology information
MeshGmshLoader 不是在每个 time step 都算东西。它主要在初始化时读取文件,读完以后就比较"被动"了。
它会从 .msh 文件中读取:点的数量、每个点的位置、边的信息、三角形由哪些点连接、四面体由哪些点连接。
它只是把这些数据加载出来,让后面的组件可以使用。初始化完成后,loader 的主要工作就结束了。
MeshGmshLoader = 读文件的人。它负责把 liver.msh 里的网格信息读进 SOFA,但它本身不负责物理计算。
3.为什么 loader 放在 root 下面
最好把 loader 放在场景比较靠上的位置,比如 root 下面,这样后面的节点都可以方便地引用它。
这就是为什么后面 Liver 节点里面可以写:
XML
<PointSetTopologyContainer name="topo" src="@../meshLoaderCoarse" />
这里的 @../meshLoaderCoarse 就是在引用 root 下的 loader。
4.PointSetTopologyContainer
进入 Liver 节点后,新增了:
XML
<Node name="Liver">
<PointSetTopologyContainer name="topo" src="@../meshLoaderCoarse" />
</Node>
这个组件叫 PointSetTopologyContainer,点集拓扑容器。它的作用是:从 meshLoaderCoarse 里拿到点的信息,并保存成当前节点可用的拓扑数据。
src 是 source 的意思:src="@../meshLoaderCoarse"
- @ 表示引用 SOFA 场景里的另一个组件
- .. 表示上一级节点
- meshLoaderCoarse 是上面那个 loader 的名字
所以这句话意思是:把上一级节点里的 meshLoaderCoarse 作为数据源。
container 会去 loader 里检查可用信息;因为这里用的是 PointSetTopologyContainer,所以它只关心点,不关心边、三角形、四面体。
5.PointSet:只拿"点",不拿完整网格
.msh 文件里可能有很多东西:nodes / points、edges、triangles、tetrahedra
但现在这个组件是:<PointSetTopologyContainer ... />
所以它只拿最低层级的信息:点的位置和点的列表。
我们现在只关心 nodes / points,不关心 edges,不关心 triangles。
所以这一阶段看到的画面会像一堆小坐标架/点云,而不是完整的肝脏表面或实体网格。
6.MechanicalObject 里没有 position 了
前面单个刚体粒子是这样写的:
XML
<MechanicalObject template="Rigid3d"
name="myParticle"
position="0 0 0 0 0 0 1"
showObject="1" />
但现在新文件里是:
XML
<MechanicalObject template="Rigid3d"
name="MechanicalModel"
showObject="1" />
MechanicalObject 里什么东西不见了?答案就是 position 这个 data field 不见了。
原因是:现在位置不是手写在 MechanicalObject 里了,而是从 mesh 文件里读进来。
7.SOFA 会自动把 topology 里的点位置复制给 MechanicalObject
初始化时发生的事情是:
MeshGmshLoader 读取 liver.msh
→ PointSetTopologyContainer 从 loader 拿到点信息
→ MechanicalObject 在同一个 node 里找到 topology container
→ 自动用 topology 中的点位置初始化自己的 position
如果 TopologyContainer 和 MechanicalObject 在同一个 node 里,MechanicalObject 会自动找这个 container,并用它加载出来的位置初始化自己的 position data。
所以虽然代码里没有写 position="..." ,但 SOFA 会自动补上来自网格文件的点位置。
即使手动写上某些 position 引用,或者删掉它,结果也一样,因为 MechanicalObject 会自动连接到同节点里的 topology container。
8.loader + topology + mechanical object 的基本关系。
(1)MeshGmshLoader
读取 mesh 文件
(2)PointSetTopologyContainer
从 loader 中提取点拓扑
(3)MechanicalObject
用这些点的位置初始化自己的状态向量
从这里开始,SOFA 不再靠你手写每个点的位置,而是通过 MeshLoader 读取网格文件,再通过 TopologyContainer 把点信息传给 MechanicalObject。
注释代码
XML
<!--
<TransformEngine name="transformer"
input_position="@meshLoaderCoarse.position"
translation="0 2 0"
rotation="0 0 0" />
-->
<!-- position="@../transformer.output_position" -->
9.TransformEngine
如果你加载进来的 mesh 位置不合适,比如太低、太偏、不在你想要的位置,你可以对这个 mesh 做一个变换。这里用的组件叫 TransformEngine。
Engine 可以理解成 SOFA 里的"数据处理器":输入 input→ 做某种处理→ 输出 output
这里它做的处理是:translation:平移 rotation:旋转
XML
input_position="@meshLoaderCoarse.position"
translation="0 2 0"
所以这段代码的意思是:把 meshLoaderCoarse 读出来的点位置作为输入,然后沿 Y 方向平移 2。
10.TransformEngine 不改变拓扑,只改变位置配置
这个变换不会改变 topology。
也就是说:点和点怎么连接,不变;有多少个点,不变;边/三角形/四面体关系,不变。
变的是:这些点在空间中的初始位置。
你可以给 MechanicalObject 提供一个新的 configuration,也就是新的初始 position / orientation。
MeshGmshLoader 读入原始点位置
TransformEngine 把这些点整体平移/旋转
MechanicalObject 使用变换后的点位置
11.position="@../transformer.output_position"
如果取消注释,可能会在 MechanicalObject 里写:
XML
<MechanicalObject template="Rigid3d"
name="MechanicalModel"
position="@../transformer.output_position"
showObject="1" />
@../transformer.output_position 意思是:
去上一级节点找 transformer 这个组件,
然后使用它的 output_position 作为 MechanicalObject 的 position。
TransformEngine 先接收:input_position="@meshLoaderCoarse.position"
再输出:transformer.output_position
然后 MechanicalObject 可以把这个输出作为自己的初始位置。
12.不写 position 也能运行,因为 SOFA 会自动从 topology 复制
即使不写 position="@../transformer.output_position" 也能运行。
因为 PointSetTopologyContainer 里面已经有点的位置,MechanicalObject 会在初始化时自动把这些点位置复制过来。
所以这两种方式区别是:
(1)不写 position:
MechanicalObject 用 topology 里的原始点位置。
(2)写 position="@../transformer.output_position":
MechanicalObject 用经过 TransformEngine 平移/旋转后的点位置。
13.为什么很多粒子后,速度还是一样
现在不再是一个粒子,而是很多个小刚体 frame / 点了。但此时速度的值还是跟时间的值一样。
因为 totalMass="1" 是总质量,SOFA 会把总质量均匀分配到所有节点上;totalForce="1 0 0 0 0 0" 也是总力,也会分配到所有节点上。