
16-python3-particle-interactive.py 如何使用 Python Controller 与正在运行的仿真交互。import Sofa import SofaRuntime from Sofa.constants import Key # 加载新版 ImGui GUI SofaRuntime.importPlugin(SofaImGui) class ManageParticles(Sofa.Core.Controller): def __init__(self, *args, **kwargs): # 必须把参数传给父类 Sofa.Core.Controller.__init__(self, *args, **kwargs) # 允许接收事件 self.listening True # 允许 Sofa.msg_info 显示在 SOFA 的 Log 面板 self.printLog True self.CFF kwargs.get(ForceField) self.rootNode kwargs.get(rootNode) # Particle0 已经存在所以从 Particle1 开始 self.iteration 1 # 记录后来动态创建的粒子 self.created_particles [] # 每按一次 或 -力变化的大小 self.force_step 0.5 def log(self, message): 同时打印到 PyCharm 控制台和 SOFA 底部 Log。 print(message, flushTrue) Sofa.msg_info(self, message) def get_current_force(self): 读取 ConstantForceField 当前的六维力/力矩。 return [ float(value) for value in self.CFF.totalForce.value ] def change_force(self, amount): 修改 Particle0 在 X 方向受到的力。 force self.get_current_force() force[0] amount self.CFF.totalForce.value force self.log( Particle0 的 X 方向力已修改为 str(force[0]) ) def add_particle(self): 向场景图中动态添加一个刚体粒子。 particle_number self.iteration particle_name Particle str(particle_number) # 将新粒子排列在画面中央附近 column (particle_number - 1) % 5 row (particle_number - 1) // 5 x_position -0.8 column * 0.4 y_position 0.8 - row * 0.4 new_particle self.rootNode.addChild(particle_name) new_particle.addObject( EulerImplicitSolver ) new_particle.addObject( CGLinearSolver, threshold1e-9, tolerance1e-9, iterations200, ) new_particle.addObject( MechanicalObject, templateRigid3d, nameMechanicalObject, position[ x_position, y_position, 0.0, 0.0, 0.0, 0.0, 1.0, ], showObjectTrue, showObjectScale0.20, ) new_particle.addObject( UniformMass, totalMass1.0, ) new_particle.addObject( ConstantForceField, nameCFF, totalForce[ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], ) # 动态添加节点后必须初始化这个新节点 new_particle.init() self.created_particles.append(particle_name) self.iteration 1 self.log( 已添加粒子 particle_name ) def remove_particle(self): 删除最后添加的粒子但不删除原始 Particle0。 if len(self.created_particles) 0: self.log(没有可以删除的新粒子。) return particle_name self.created_particles.pop() node_to_delete self.rootNode.getChild(particle_name) if node_to_delete is None: self.log( 没有找到节点 particle_name ) return # 先删除节点中的组件 for sofa_object in list(node_to_delete.objects): node_to_delete.removeObject(sofa_object) # 再从场景图中删除节点 self.rootNode.removeChild(particle_name) self.iteration max( 1, self.iteration - 1, ) self.log( 已删除粒子 particle_name ) def onKeypressedEvent(self, event): SOFA v25.12 键盘事件。 增加 Particle0 的 X 方向力 - 减小 Particle0 的 X 方向力 A 添加一个粒子 M 删除最后添加的粒子 key event.get(key, ) # 只要事件成功进入控制器SOFA 底部 Log 就会出现这一行 self.log( 收到按键 repr(key) ) # 主键盘 通常可能返回 或 if key in (Key.plus, , ): self.change_force(self.force_step) # 主键盘 - 可能返回 - 或 _ elif key in (Key.minus, -, _): self.change_force(-self.force_step) elif str(key).upper() A: self.add_particle() elif str(key).upper() M: self.remove_particle() def onKeyreleasedEvent(self, event): 用于确认按键释放事件是否成功传入。 key event.get(key, ) self.log( 释放按键 repr(key) ) def createScene(root): root.name root root.dt 0.01 root.gravity [0.0, 0.0, 0.0] root.addObject( RequiredPlugin, nameloadSOFAModules, pluginName[ Sofa.Component.AnimationLoop, Sofa.Component.LinearSolver.Iterative, Sofa.Component.Mass, Sofa.Component.MechanicalLoad, Sofa.Component.ODESolver.Backward, Sofa.Component.StateContainer, ], ) root.addObject( DefaultAnimationLoop, computeBoundingBoxFalse, ) # 固定显示范围防止运动后画面范围不停变化 root.bbox [ [-2.0, -2.0, -2.0], [2.0, 2.0, 2.0], ] # 原始粒子 Particle0 particle root.addChild(Particle0) particle.addObject( EulerImplicitSolver ) particle.addObject( CGLinearSolver, threshold1e-9, tolerance1e-9, iterations200, ) particle.addObject( MechanicalObject, templateRigid3d, nameMechanicalObject, position[ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, ], showObjectTrue, showObjectScale0.30, ) particle.addObject( UniformMass, totalMass1.0, ) cff particle.addObject( ConstantForceField, nameCFF, totalForce[ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], ) root.addObject( ManageParticles( nameMyParticlesController, ForceFieldcff, rootNoderoot, listeningTrue, printLogTrue, ) ) return root def main(): import Sofa.Gui root Sofa.Core.Node(root) createScene(root) Sofa.Simulation.initRoot(root) print( Supported GUIs:, Sofa.Gui.GUIManager.ListSupportedGUI(), ) Sofa.Gui.GUIManager.Init( myscene, imgui, ) Sofa.Gui.GUIManager.createGUI( root, __file__, ) Sofa.Gui.GUIManager.SetDimension( 1080, 800, ) Sofa.Gui.GUIManager.MainLoop(root) Sofa.Gui.GUIManager.closeGUI() if __name__ __main__: main()1.运行场景16粒子开始时不运动初始粒子Particle0包含一个恒力场cff particle.addObject( ConstantForceField, nameCFF, totalForce[ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], )初始力为因此粒子开始时不会运动。totalForce 中的前3个数是平移力Fx Fy Fz 后3个数是力矩Tx Ty Tz这是一个Rigid3d物体因此需要6维力。2.通过键盘修改正在运行的仿真快捷键功能Ctrl Shift 增大恒力实际对应字符或Ctrl Shift -减小恒力Ctrl Shift A添加粒子Ctrl Shift M删除粒子通过def onKeypressedEvent(self, event): key event.get(key, )读取按键。为了兼容 ImGui、不同键盘布局以及主键盘符号代码写成if key in (Key.plus, , ): self.change_force(self.force_step) elif key in (Key.minus, -, _): self.change_force(-self.force_step) elif str(key).upper() A: self.add_particle() elif str(key).upper() M: self.remove_particle()其中str(key).upper()可以同时兼容小写a、m和大写A、M。按下增加力的快捷键后粒子开始运动因为ConstantForceField中的 X 方向恒力被增大了。3.ManageParticles Controller这里添加的不是普通的 SOFA 内置组件名称例如root.addObject(MechanicalObject)而是一个由我们自己编写的 Python 类实例root.addObject( ManageParticles( nameMyParticlesController, ForceFieldcff, rootNoderoot, listeningTrue, printLogTrue, ) )ManageParticles继承自Sofa.Core.Controller完整形式是class ManageParticles(Sofa.Core.Controller): ...4.ControllerController 是用于监听和处理 SOFA 仿真事件的组件。它可以处理键盘按键鼠标操作一个时间步开始一个时间步结束仿真初始化GUI中的用户交互。因此它相当于Python 代码和仿真之间的交互接口。5.设置 listeningself.listening True添加 Controller 时也写了listeningTrue它表示这个 Controller 需要监听由 SOFA 场景传播过来的事件。如果没有启用监听Controller 即使已经出现在 Scene Graph 中也可能不会处理键盘和仿真事件。6.构造函数保存的对象构造函数为def __init__(self, *args, **kwargs): Sofa.Core.Controller.__init__(self, *args, **kwargs) self.listening True self.printLog True self.CFF kwargs.get(ForceField) self.rootNode kwargs.get(rootNode) self.iteration 1 self.created_particles [] self.force_step 0.5self.CFF保存初始粒子的ConstantForceFieldForceFieldcff以后按下加力或减力按键时可以直接修改self.CFF.totalForceself.rootNode保存整个场景的根节点rootNoderoot以后需要动态添加或删除节点时可以通过它操作整个 Scene Graph。self.iteration记录下一个粒子的编号self.iteration 1因为场景中已经存在 Particle0所以后来创建的粒子从 Particle1 开始。self.created_particlesself.created_particles []它保存运行时创建过的粒子名称例如[ Particle1, Particle2, Particle3, ]删除粒子时可以直接找到最后创建的那个粒子避免误删最初的Particle0。self.force_stepself.force_step 0.5它表示每按一次加力或减力按键力改变多少。因此self.change_force(self.force_step)表示增加0.5self.change_force(-self.force_step)表示减少0.5。7.日志输出def log(self, message): print(message, flushTrue) Sofa.msg_info(self, message)其中print(...)把信息输出到 PyCharm 控制台。Sofa.msg_info(...)把信息输出到 SOFA 界面底部的 Log 面板。8.运行中修改 totalForce8.1.读取当前恒力def get_current_force(self): return [ float(value) for value in self.CFF.totalForce.value ]这里self.CFF.totalForce是 SOFA 的 Data。self.CFF.totalForce.value读取这个 Data 当前保存的值。然后通过列表推导式把每个元素转换成普通 Python 浮点数[ float(value) for value in ... ]8.2.修改 X方向的力def change_force(self, amount): force self.get_current_force() force[0] amount self.CFF.totalForce.value force其中 force[0] 是刚体力向量的第一个分量也就是按下增加力快捷键时self.change_force(self.force_step)因此Fx←Fx0.5按下减小力快捷键时self.change_force(-self.force_step)因此Fx←Fx−0.5最后通过self.CFF.totalForce.value force把修改后的普通 Python 列表重新写回 SOFA Data。9.动态添加场景节点9.1.添加粒子按下Ctrl Shift A触发elif str(key).upper() A: self.add_particle()add_particle()首先生成新粒子的名称particle_number self.iteration particle_name Particle str(particle_number)例如Particle1、Particle2、Particle3然后在根节点下面动态创建新节点new_particle self.rootNode.addChild(particle_name)9.2.给新节点添加完整的力学组件新粒子首先添加时间积分求解器new_particle.addObject( EulerImplicitSolver )然后添加线性求解器new_particle.addObject( CGLinearSolver, threshold1e-9, tolerance1e-9, iterations200, )添加刚体机械对象new_particle.addObject( MechanicalObject, templateRigid3d, nameMechanicalObject, position[ x_position, y_position, 0.0, 0.0, 0.0, 0.0, 1.0, ], showObjectTrue, showObjectScale0.20, )添加质量new_particle.addObject( UniformMass, totalMass1.0, )添加恒力场new_particle.addObject( ConstantForceField, nameCFF, totalForce[ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], )新版本中新粒子的初始力为零所以新粒子创建后默认不会自行运动。9.3.计算新粒子的位置column (particle_number - 1) % 5 row (particle_number - 1) // 5 x_position -0.8 column * 0.4 y_position 0.8 - row * 0.4它会把新粒子按行列排列避免所有新粒子都创建在同一个坐标上。这部分只是为了方便观察。9.4.动态节点需要 init()add_particle()创建粒子时最后执行new_particle.init()因为这个节点是在整个场景已经初始化完成后才动态创建的。因此需要单独初始化新节点使其中的 MechanicalObject求解器质量组件ConstantForceFieldData和组件链接 完成初始化否则新添加的组件可能还没有正确加入仿真计算。9.5.更新粒子记录节点初始化完成后self.created_particles.append(particle_name) self.iteration 1把新粒子的名称加入记录同时令编号加一。10.动态删除场景节点10.1.删除粒子按下 Ctrl Shift M触发elif str(key).upper() M: self.remove_particle()首先判断是否存在运行时新创建的粒子if len(self.created_particles) 0: self.log(没有可以删除的新粒子。) return这样可以避免删除最初的 Particle010.2.找到最后创建的节点particle_name self.created_particles.pop()pop()会取出列表中的最后一个名称。然后在根节点中查找node_to_delete self.rootNode.getChild(particle_name)10.3.删除节点中的组件for sofa_object in list(node_to_delete.objects): node_to_delete.removeObject(sofa_object)这里遍历该节点中的所有组件并逐个删除。例如EulerImplicitSolverCGLinearSolverMechanicalObjectUniformMassConstantForceField。使用 list(node_to_delete.objects) 先生成一份组件名单列表副本避免在遍历过程中直接修改原集合。10.4.从场景图中删除节点组件删除以后再执行self.rootNode.removeChild(particle_name)把整个粒子节点从根节点中删除。最后更新编号self.iteration max( 1, self.iteration - 1, )其中 max(1, ...) 保证编号不会降到1以下从而保护初始节点Particle0。