9-SOFA_Collision Model(碰撞模型)与 Collision Pipeline(碰撞流水线)
11-collision-pipeline.scn
<?xml version="1.0"?> <!-- Step 1: add the global collision pipeline --> <Node name="root" dt="0.01" gravity="0 0 0"> <Node name="plugins"> <RequiredPlugin pluginName="Sofa.Component.AnimationLoop"/> <RequiredPlugin pluginName="Sofa.Component.Collision.Detection.Algorithm"/> <RequiredPlugin pluginName="Sofa.Component.Collision.Detection.Intersection"/> <RequiredPlugin pluginName="Sofa.Component.Collision.Geometry"/> <RequiredPlugin pluginName="Sofa.Component.Collision.Response.Contact"/> <RequiredPlugin pluginName="Sofa.Component.Constraint.Projective"/> <RequiredPlugin pluginName="Sofa.Component.IO.Mesh"/> <RequiredPlugin pluginName="Sofa.Component.LinearSolver.Iterative"/> <RequiredPlugin pluginName="Sofa.Component.Mapping.Linear"/> <RequiredPlugin pluginName="Sofa.Component.Mass"/> <RequiredPlugin pluginName="Sofa.Component.ODESolver.Backward"/> <RequiredPlugin pluginName="Sofa.Component.SolidMechanics.FEM.Elastic"/> <RequiredPlugin pluginName="Sofa.Component.StateContainer"/> <RequiredPlugin pluginName="Sofa.Component.Topology.Container.Constant"/> <RequiredPlugin pluginName="Sofa.Component.Topology.Container.Dynamic"/> <RequiredPlugin pluginName="Sofa.Component.Visual"/> <RequiredPlugin pluginName="Sofa.GL.Component.Rendering3D"/> </Node> <DefaultAnimationLoop computeBoundingBox="false"/> <VisualStyle displayFlags="showVisualModels showBehaviorModels showCollisionModels"/> <!-- Global collision pipeline --> <CollisionPipeline/> <BruteForceBroadPhase/> <BVHNarrowPhase/> <MinProximityIntersection name="Proximity" alarmDistance="0.5" contactDistance="0.25"/> <CollisionResponse name="Response" response="PenalityContactForceField"/> <MeshGmshLoader name="meshLoaderCoarse" filename="mesh/liver.msh"/> <MeshOBJLoader name="meshLoaderFine" filename="mesh/liver-smooth.obj"/> <Node name="Liver"> <EulerImplicitSolver/> <CGLinearSolver iterations="200" tolerance="1e-09" threshold="1e-09"/> <TetrahedronSetTopologyContainer name="topo" src="@../meshLoaderCoarse"/> <TetrahedronSetGeometryAlgorithms template="Vec3d" name="GeomAlgo"/> <MechanicalObject template="Vec3d" name="MechanicalModel" showObject="1" showObjectScale="3"/> <TetrahedronFEMForceField name="FEM" youngModulus="1000" poissonRatio="0.4" method="large"/> <MeshMatrixMass massDensity="1" topology="@topo"/> <!-- 老师视频旧名称是 FixedConstraint --> <FixedProjectiveConstraint indices="1 3 50"/> <Node name="Visual"> <OglModel name="VisualModel" src="@../../meshLoaderFine"/> <BarycentricMapping name="VisualMapping" input="@../MechanicalModel" output="@VisualModel"/> </Node> </Node> </Node>一个完整物体可以具有三种表示:
Liver
│
├── Mechanical Model
│ 负责力学计算和变形
│
├── Visual Model
│ 负责显示外观
│
└── Collision Model
负责碰撞检测
- 力学模型保存真正参与求解的状态;
- 视觉模型可以使用光滑表面;
- 碰撞模型由适合检测的几何图元组成。
三个表示不一定使用同一套网格,但需要通过 Mapping 同步。
1.Collision Pipeline 的整体流程
SOFA 的碰撞流水线负责调度碰撞重置、碰撞检测和碰撞响应;这一节主要按“粗检测—窄检测—响应”的直观顺序讲。
碰撞过程分成:
Broad Phase
宽阶段、粗检测
↓
Narrow Phase
窄阶段、精细定位
↓
Intersection + Response
确认接触并产生响应
也可以概括成:
先找可能碰撞的物体
↓
再找可能接触的局部区域
↓
检查具体图元是否接触
↓
创建接触响应
2.CollisionPipeline
<CollisionPipeline/>CollisionPipeline 的作用是组织碰撞系统中各个阶段按照正确顺序执行。
可以类比成AnimationLoop:
DefaultAnimationLoop
仿真总负责人
↓ 发出“开始处理碰撞”的命令
CollisionPipeline
碰撞子流程负责人
↓
调用粗检测、窄检测、接触响应
也就是:
AnimationLoop 管整个时间步
CollisionPipeline 管时间步里的碰撞部分
3.BruteForceBroadPhase
第一种算法组件:
<BruteForceBroadPhase/>它先比较不同碰撞物体的整体包围盒:
物体A包围盒 物体B包围盒
┌────────┐ ┌────────┐
│ A │ │ B │
└────────┘ └────────┘
如果包围盒相距很远:直接排除,不再做更细的检查;
如果包围盒相交或非常接近:把这一对物体交给 Narrow Phase。
4.BVHNarrowPhase
第二个组件:
<BVHNarrowPhase/>BVH是:Bounding Volume Hierarchy 包围体层次结构
假设整个 liver 外面有一个大盒子:
┌──────────────────┐
│ Liver │
└──────────────────┘
仅凭这个盒子只能知道可能发生碰撞,不能知道到底是 liver 的哪个区域,于是 BVH 会继续分层,它通过从大盒子逐步进入小盒子,快速定位接触大概发生在哪个局部区域。
官方说明,BVHNarrowPhase会遍历碰撞图元的层次结构,继续排除不可能相交的图元对,最后把剩下的候选对交给相交检测方法。
4.MinProximityIntersection
第三个检测组件:
<MinProximityIntersection name="Proximity" alarmDistance="0.5" contactDistance="0.25"/>BVH 负责精确定位可能接触的位置,而MinProximityIntersection最终检查:
primitive against primitive
图元对图元
例如:
球体 against 三角形
点 against 三角形
线段 against 线段
4.1.alarmDistance
alarmDistance="0.5"
当两组碰撞图元的距离小于0.5时:认为它们已经足够接近 ,需要继续进行接触检查。
它相当于警戒距离。
4.2.contactDistance
contactDistance="0.25"
距离小于0.25时:真正创建一个接触 DetectionOutput
所以关系是:
距离 > 0.5
忽略
0.25 < 距离 < 0.5
进入警戒和精细检测
距离 < 0.25
创建接触
5.CollisionResponse
发现两个物体接触了,下一步怎么处理?
<CollisionResponse name="Response" response="PenalityContactForceField"/>这里讲的是penalty method(惩罚法),SOFA 组件的历史拼写是 PenalityContactForceField,该组件的作用就是用排斥弹簧处理接触。
两种碰撞响应:Penalty method 惩罚法、Lagrange multiplier constraint 拉格朗日乘子约束法
这个教程现在使用DefaultAnimationLoop,因此先集中讲惩罚法;约束型响应需要另一套约束求解流程。
惩罚法怎样产生力:
如果两个物体发生穿透:
物体A ─────┐
│ 穿透距离 d
物体B ─────┘
惩罚法根据穿透程度和接触刚度,产生排斥力:
其中:
:接触违反或穿透的程度;
:接触刚度;
:把两个物体推开的接触力。
惩罚法通过力来减少穿透,但不能严格保证时间步结束时穿透一定等于零。
接触刚度太小时,物体可能明显穿进去;接触刚度太大时,又可能导致数值问题。
因此它是一种近似处理,而不是严格的几何不可穿透约束。惩罚法是DefaultAnimationLoop下当前使用的响应方式.
6.碰撞算法可以替换
Broad Phase——BruteForceBroadPhase
Narrow Phase——BVHNarrowPhase、DirectSAPNarrowPhase
Intersection method——MinProximityIntersection、LocalMinDistance
这些组件可以根据项目需要替换,SOFA 用户文档中可以查到每种算法的细节。
我们当前使用的是这一组:
<BruteForceBroadPhase/> <BVHNarrowPhase/> <MinProximityIntersection .../>最终流程是:
碰撞对象对
↓
包围盒筛选
↓
BVH局部筛选
↓
具体图元成对检查
↓
创建接触
7.全局流水线代码放在哪里
需要在:
<DefaultAnimationLoop/>后面增加五行代码:
<CollisionPipeline/> <BruteForceBroadPhase/> <BVHNarrowPhase/> <MinProximityIntersection name="Proximity" alarmDistance="0.5" contactDistance="0.25"/> <CollisionResponse name="Response" response="PenalityContactForceField"/>这些组件放在root中,因为它们管理的是整个场景中的碰撞流程,不是只属于某一个 liver。
所以要区分:
- root 下的 CollisionPipeline → 整个场景共用的碰撞算法
- 每个物体中的 Collision 子节点 → 该物体自己的碰撞表示
11-collision-model.scn
<?xml version="1.0"?> <!-- Step 2: add a collision representation to Liver --> <Node name="root" dt="0.01" gravity="0 0 0"> <Node name="plugins"> <RequiredPlugin pluginName="Sofa.Component.AnimationLoop"/> <RequiredPlugin pluginName="Sofa.Component.Collision.Detection.Algorithm"/> <RequiredPlugin pluginName="Sofa.Component.Collision.Detection.Intersection"/> <RequiredPlugin pluginName="Sofa.Component.Collision.Geometry"/> <RequiredPlugin pluginName="Sofa.Component.Collision.Response.Contact"/> <RequiredPlugin pluginName="Sofa.Component.Constraint.Projective"/> <RequiredPlugin pluginName="Sofa.Component.IO.Mesh"/> <RequiredPlugin pluginName="Sofa.Component.LinearSolver.Iterative"/> <RequiredPlugin pluginName="Sofa.Component.Mapping.Linear"/> <RequiredPlugin pluginName="Sofa.Component.Mass"/> <RequiredPlugin pluginName="Sofa.Component.ODESolver.Backward"/> <RequiredPlugin pluginName="Sofa.Component.SolidMechanics.FEM.Elastic"/> <RequiredPlugin pluginName="Sofa.Component.StateContainer"/> <RequiredPlugin pluginName="Sofa.Component.Topology.Container.Constant"/> <RequiredPlugin pluginName="Sofa.Component.Topology.Container.Dynamic"/> <RequiredPlugin pluginName="Sofa.Component.Visual"/> <RequiredPlugin pluginName="Sofa.GL.Component.Rendering3D"/> </Node> <DefaultAnimationLoop computeBoundingBox="false"/> <VisualStyle displayFlags="showVisualModels showBehaviorModels showCollisionModels"/> <!-- Global collision pipeline --> <CollisionPipeline/> <BruteForceBroadPhase/> <BVHNarrowPhase/> <MinProximityIntersection name="Proximity" alarmDistance="0.5" contactDistance="0.25"/> <CollisionResponse name="Response" response="PenalityContactForceField"/> <!-- Mechanical volumetric mesh --> <MeshGmshLoader name="meshLoaderCoarse" filename="mesh/liver.msh"/> <!-- Fine surface mesh, reused by visual and collision representations --> <MeshOBJLoader name="meshLoaderFine" filename="mesh/liver-smooth.obj"/> <Node name="Liver"> <EulerImplicitSolver/> <CGLinearSolver iterations="200" tolerance="1e-09" threshold="1e-09"/> <TetrahedronSetTopologyContainer name="topo" src="@../meshLoaderCoarse"/> <TetrahedronSetGeometryAlgorithms template="Vec3d" name="GeomAlgo"/> <MechanicalObject template="Vec3d" name="MechanicalModel" showObject="1" showObjectScale="3"/> <TetrahedronFEMForceField name="FEM" youngModulus="1000" poissonRatio="0.4" method="large"/> <MeshMatrixMass massDensity="1" topology="@topo"/> <FixedProjectiveConstraint indices="1 3 50"/> <!-- Visual representation --> <Node name="Visual"> <OglModel name="VisualModel" src="@../../meshLoaderFine"/> <BarycentricMapping name="VisualMapping" input="@../MechanicalModel" output="@VisualModel"/> </Node> <!-- Collision representation --> <Node name="Collision"> <MeshTopology name="CollisionTopology" src="@../../meshLoaderFine"/> <MechanicalObject template="Vec3d" name="StoringForces"/> <TriangleCollisionModel name="CollisionModel" contactStiffness="3"/> <BarycentricMapping name="CollisionMapping" input="@../MechanicalModel" output="@StoringForces"/> </Node> </Node> </Node>8.给 Liver 增加 Collision 子节点
前面为了渲染,我们建立过:
<Node name="Visual"> ... </Node>现在按照同样思路,再建立:
<Node name="Collision"> ... </Node>场景结构变成:
Liver
│
├── MechanicalModel
│
├── Visual
│ └── VisualModel
│
└── Collision
└── CollisionModel
这次创建子节点不是为了 rendering,而是为了 collision detection。
9.Collision 子节点里的四个组件
<Node name="Collision"> <MeshTopology src="@../../meshLoaderFine"/> <MechanicalObject name="StoringForces"/> <TriangleCollisionModel name="CollisionModel" contactStiffness="3"/> <BarycentricMapping name="CollisionMapping" input="@../" output="@StoringForces"/> </Node>9.1.MeshTopology
<MeshTopology src="@../../meshLoaderFine"/>它定义碰撞表面的点、边、三角形连接关系。
这里使用 meshLoaderFine,也就是前面红色、较平滑的表面网格。
../../是层级寻址:向上回溯两级父节点,再找到meshLoaderFine这个节点。
因此当前例子是:
- Mechanical Model——使用 liver.msh 的四面体体网格
- Visual Model——使用 liver-smooth.obj
- Collision Model——也使用 liver-smooth.obj
并没有要求碰撞网格必须和视觉网格相同,只是这个例子选择复用同一个细表面网格。
9.2.MechanicalObject name="StoringForces"
<MechanicalObject name="StoringForces"/>它保存 Collision 子节点的状态。
SOFA 规则: 每个力学 / 碰撞计算,都需要一个MechanicalObject存顶点坐标、速度、受力。
Visual 节点里没有额外的MechanicalObject,为什么 Collision 节点里却需要一个?我们将在11小节进行解释。
9.3.TriangleCollisionModel
SOFA 内置碰撞算法:基于三角形面片碰撞检测。
<TriangleCollisionModel name="CollisionModel" contactStiffness="3"/>它表示把 MeshTopology 中的三角形 作为碰撞检测图元。
- 读取上面
MeshTopology的肝脏三角面; - 场景里其他物体碰到肝脏时,计算穿透、挤压;
contactStiffness="3"接触刚度,数值越大碰撞反弹越硬。 算出的碰撞力会作用在StoringForces这套质点上。在12小节会详细解释。
SOFA 还支持 PointCollisionModel、LineCollisionModel、SphereCollisionModel、CubeCollisionModel、TriangleCollisionModel。但我们当前只选择了三角形。官方文档确认,CollisionModel 就是由某类碰撞图元组成的列表,而contactStiffness主要用于惩罚法接触响应。
9.4.BarycentricMapping
功能: 用重心坐标插值,把主肝脏模型每一步的形变位置、位移,实时同步复制到碰撞副本StoringForces。
<BarycentricMapping name="CollisionMapping" input="@../" output="@StoringForces"/>input="@../"表示回到上一级Liver节点,找到上层的力学状态。
它等价于更明确地写:
input="@../MechanicalModel"输出:
output="@StoringForces"表示映射到 Collision 子节点的MechanicalObject。
9.5.完整数据流
关系是:
上层主节点:Liver/MechanicalModel(肝脏真实形变质点)
↓ 数据传递靠 BarycentricMapping
当前节点:Collision/StoringForces(碰撞专用副本质点)
↓ 碰撞力作用在这套质点上
TriangleCollisionModel(三角碰撞检测器)
完整数据流分两步:
- 正向映射(形变同步)Liver 主模型形变 → BarycentricMapping 插值 → StoringForces 同步位置 保证碰撞网格和肝脏外形完全一致。
- 反向受力传递(碰撞反馈)TriangleCollisionModel 算出碰撞接触力 → 先存到 StoringForces 再通过映射反向传回主 Liver/MechanicalModel,让肝脏感受到碰撞挤压、产生形变反馈。
碰撞模型通常需要通过 Mapping 和父节点的力学表示连接。
10.MeshTopology 与 SetTopologyContainer 的区别
MeshTopology 和前面学过的:
<TetrahedronSetTopologyContainer/>一样,都保存拓扑关系。
- 几何(坐标):每个点
(x,y,z)在哪,是位置,会动; - 拓扑(Topology):点和点怎么连、拼成什么面 / 体,是连接关系。
这里强调一个主要区别:
TetrahedronSetTopologyContainer 属于可更新的拓扑容器,配合相应 Modifier,可以支持拓扑变化,例如:切割、撕裂、增加或删除拓扑元素。
MeshTopology 在这里被作为静态拓扑使用:加载完成后拓扑保持不变。也就是说,节点位置可以跟随 Mapping 移动;但是点、边、三角形的连接关系不改变;不用于切割或者撕裂。
所以选择原则是:
- 拓扑永远不改变 → 可以使用 MeshTopology
- 需要切割、撕裂或动态更新拓扑 → 使用动态 SetTopologyContainer 和 Modifier
它和其他拓扑容器一样,可以通过:
src="@../../meshLoaderFine"从 Loader 读取所有拓扑信息。
11.为什么 Collision 里需要 MechanicalObject
<Node name="Collision"> <MeshTopology src="@../../meshLoaderFine"/> <MechanicalObject name="StoringForces"/> <TriangleCollisionModel name="CollisionModel" contactStiffness="3"/> <BarycentricMapping name="CollisionMapping" input="@../MechanicalModel" output="@StoringForces"/> </Node>Visual 节点里没有额外的MechanicalObject,为什么 Collision 节点里却需要一个?
11.1.Visual Model 是完全被动的
视觉模型只负责显示:
Mechanical Model
↓ Mapping
Visual Model
力学模型算出新位置后,Mapping 把位置传给视觉模型;视觉模型不计算接触力,也不需要把任何物理信息返回给力学模型。所以它基本是单向的。
11.2. Collision Model 不只是接收位置,还要返回力
碰撞模型首先也要跟随力学模型运动:
Mechanical Model
↓位置、速度
Collision Model
但发生碰撞后,碰撞模型会得到:接触位置、接触法向、两物体之间的距离、穿透距离、惩罚接触力。这些信息最终必须返回给力学模型:
Collision Model
↑接触力
Mechanical Model
这仍然由 Mapping 完成。向下传播状态,向上传播力。
碰撞模型的位置由力学模型位置
映射得到。
而碰撞力返回力学模型时,通常按 Mapping 的转置关系传递:
其中:
:碰撞模型上计算出的接触力;
:返回到力学节点上的力;
:Mapping 的雅可比矩阵。
11.3.StoringForces
<MechanicalObject name="StoringForces"/>仿真时间步中,碰撞检测通常发生在物理求解之前。
流程是:
先检测碰撞
↓
根据穿透距离创建接触力
↓
但此时物理方程还没求解
↓
暂时保存这些力
↓
随后力学求解器使用这些力
因此需要一个 Collision 节点中的机械状态,用来承接碰撞模型的位置,也承接接触响应产生的力。我们把它命名为 StoringForces ,意思是“存储接触力”。
但要注意,这只是我们给它取的名字,不是特殊组件名称,它本质上仍然是普通的 <MechanicalObject/> 。
随后:
<BarycentricMapping input="@../MechanicalModel" output="@StoringForces"/>会自动把 Collision 上的力反向传播给父节点的MechanicalModel。
12.contactStiffness
<TriangleCollisionModel name="CollisionModel" contactStiffness="3"/>contactStiffness 用于前面讲过的 Penalty method 惩罚法
两个物体发生了穿透,假设球体进入肝脏表面:
肝脏表面 ─────────────
球
部分进入表面
两个碰撞模型之间存在一个穿透距离
惩罚法会在接触位置建立一根虚拟弹簧:
物体A ←────虚拟弹簧────→ 物体B
排斥力近似为 :
其中:
:穿透距离;
:
contactStiffness;:把两个物体推开的接触力。
因此:
contactStiffness="3"表示惩罚弹簧的刚度系数是 3。
13.碰撞处理完整步骤
假设:球体和一个三角形之间发生了 1 mm 的穿透。
第一步:Broad Phase 检查整体包围盒
SOFA 为每个碰撞对象计算包围盒。
包围盒通常与世界坐标的轴对齐:
先检查两个大盒子是否相交。如果物体真的已经发生穿透,那么它们的包围盒通常也会相交,因此进入下一阶段。
Broad Phase 的目的只是快速判断这一对物体是否可能碰撞,不是判断具体哪个三角形发生接触。
第二步:Narrow Phase 定位局部区域
接下来,BVH Narrow Phase 用逐层细分的包围盒,定位可能发生接触的小区域。
整个物体 ↓ 较大的局部区域 ↓ 更小的局部区域 ↓ 具体球体与三角形
第三步:Proximity Intersection 确认穿透距离
MinProximityIntersection检查具体图元,例如:Sphere against Triangle 球体 对 三角形
它确认:球体穿入三角表面 1 mm,即
第四步:Collision Response 创建虚拟弹簧
碰撞响应根据 穿透距离 d + contactStiffness k 创建惩罚弹簧。
这里举的数值是:contactStiffness = 3 穿透距离 = 1 mm
在国际单位制中,因此
如果刚度单位是 N/m,那么力就是
这个力暂时保存在 Collision 节点中,再通过 Mapping 传回 Mechanical Model,随后由力学求解器使用。
第五步:惩罚法不是直接移动节点
这里要特别纠正了一个容易产生的误解:发现穿透 1 mm 后,不是直接把球往外移动 1 mm。
SOFA 在这一阶段只是给节点施加排斥力,随后由质量、材料刚度、时间步长、速度、求解器 共同决定节点下一步实际移动多少。
所以流程不是:检测到穿透1 mm → 直接修正位置1 mm
而是:
检测到穿透1 mm → 生成接触力 → 求解物理方程 → 物体在接触力作用下运动
13.1.惩罚法不能保证完全不穿透
因为它只是施加力,并不强制满足,时间步结束后,可能仍有少量穿透。
contactStiffness太小:排斥力不足 → 物体继续穿入
contactStiffness太大:排斥力突然非常大 → 仿真抖动,求解困难,甚至不稳定
此外它还受以下因素影响:两个物体相撞的速度、时间步长、物体质量、材料刚度、求解器参数。
所以contactStiffness对调参非常敏感。
