当前位置: 首页 > news >正文

14-SOFA_使用 Python Controller 与正在运行的仿真交互_16-python3-particle-interactive.py

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, flush=True) 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", threshold=1e-9, tolerance=1e-9, iterations=200, ) new_particle.addObject( "MechanicalObject", template="Rigid3d", name="MechanicalObject", position=[ x_position, y_position, 0.0, 0.0, 0.0, 0.0, 1.0, ], showObject=True, showObjectScale=0.20, ) new_particle.addObject( "UniformMass", totalMass=1.0, ) new_particle.addObject( "ConstantForceField", name="CFF", 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", name="loadSOFAModules", pluginName=[ "Sofa.Component.AnimationLoop", "Sofa.Component.LinearSolver.Iterative", "Sofa.Component.Mass", "Sofa.Component.MechanicalLoad", "Sofa.Component.ODESolver.Backward", "Sofa.Component.StateContainer", ], ) root.addObject( "DefaultAnimationLoop", computeBoundingBox=False, ) # 固定显示范围,防止运动后画面范围不停变化 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", threshold=1e-9, tolerance=1e-9, iterations=200, ) particle.addObject( "MechanicalObject", template="Rigid3d", name="MechanicalObject", position=[ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, ], showObject=True, showObjectScale=0.30, ) particle.addObject( "UniformMass", totalMass=1.0, ) cff = particle.addObject( "ConstantForceField", name="CFF", totalForce=[ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], ) root.addObject( ManageParticles( name="MyParticlesController", ForceField=cff, rootNode=root, listening=True, printLog=True, ) ) 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", name="CFF", 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()

可以同时兼容小写am和大写AM

按下增加力的快捷键后,粒子开始运动,因为ConstantForceField中的 X 方向恒力被增大了。

3.ManageParticles Controller

这里添加的不是普通的 SOFA 内置组件名称,例如:

root.addObject("MechanicalObject")

而是一个由我们自己编写的 Python 类实例:

root.addObject( ManageParticles( name="MyParticlesController", ForceField=cff, rootNode=root, listening=True, printLog=True, ) )

ManageParticles继承自:

Sofa.Core.Controller

完整形式是:

class ManageParticles(Sofa.Core.Controller): ...

4.Controller

Controller 是用于监听和处理 SOFA 仿真事件的组件。

它可以处理:键盘按键;鼠标操作;一个时间步开始;一个时间步结束;仿真初始化;GUI中的用户交互。

因此它相当于Python 代码和仿真之间的交互接口。

5.设置 listening

self.listening = True

添加 Controller 时也写了:

listening=True

它表示这个 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.5

self.CFF

保存初始粒子的ConstantForceField

ForceField=cff

以后按下加力或减力按键时,可以直接修改:

self.CFF.totalForce

self.rootNode

保存整个场景的根节点:

rootNode=root

以后需要动态添加或删除节点时,可以通过它操作整个 Scene Graph。

self.iteration

记录下一个粒子的编号:

self.iteration = 1

因为场景中已经存在 Particle0,所以后来创建的粒子从 Particle1 开始。

self.created_particles

self.created_particles = []

它保存运行时创建过的粒子名称,例如:

[ "Particle1", "Particle2", "Particle3", ]

删除粒子时,可以直接找到最后创建的那个粒子,避免误删最初的Particle0

self.force_step

self.force_step = 0.5

它表示每按一次加力或减力按键,力改变多少。

因此:

self.change_force(self.force_step)

表示增加0.5

self.change_force(-self.force_step)

表示减少0.5

7.日志输出

def log(self, message): print(message, flush=True) Sofa.msg_info(self, message)

其中:

print(...)

把信息输出到 PyCharm 控制台。

Sofa.msg_info(...)

把信息输出到 SOFA 界面底部的 Log 面板。

8.运行中修改 totalForce

8.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​←Fx​+0.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", threshold=1e-9, tolerance=1e-9, iterations=200, )

添加刚体机械对象:

new_particle.addObject( "MechanicalObject", template="Rigid3d", name="MechanicalObject", position=[ x_position, y_position, 0.0, 0.0, 0.0, 0.0, 1.0, ], showObject=True, showObjectScale=0.20, )

添加质量:

new_particle.addObject( "UniformMass", totalMass=1.0, )

添加恒力场:

new_particle.addObject( "ConstantForceField", name="CFF", 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;求解器;质量组件;ConstantForceField;Data和组件链接 完成初始化,否则新添加的组件可能还没有正确加入仿真计算。

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

这样可以避免删除最初的 Particle0

10.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)

这里遍历该节点中的所有组件,并逐个删除。例如:EulerImplicitSolver;CGLinearSolver;MechanicalObject;UniformMass;ConstantForceField。

使用 list(node_to_delete.objects) 先生成一份组件名单列表副本,避免在遍历过程中直接修改原集合。

10.4.从场景图中删除节点

组件删除以后,再执行:

self.rootNode.removeChild(particle_name)

把整个粒子节点从根节点中删除。

最后更新编号:

self.iteration = max( 1, self.iteration - 1, )

其中 max(1, ...) 保证编号不会降到1以下,从而保护初始节点Particle0

http://www.jsqmd.com/news/1385060/

相关文章:

  • 2026跨端开发技术选型:Flutter、React Native与HarmonyOS对比
  • 2026年数学建模国赛B题算法(39):装箱问题的首次适应降序算法研究:改进策略与性能分析
  • 3分钟解锁 Office 365 订阅版全部本地功能:Ohook 钩子激活工具使用指南
  • 数据域:从混乱到有序,构建高效数据架构的核心方法论
  • DDrawCompat 完整指南:让《红警2》《暗黑2》等经典DirectX游戏在现代Windows上重获新生
  • 线上AI服务token暴涨300%:从内存泄漏到静默失败的全链路排查
  • 酷安电脑版三步上手:免费开源,把整个数码社区搬上大屏
  • 2026全球AI网络安全产品市场现状、商业模式及国内外竞争力实战研判
  • 163MusicLyrics歌词下载工具使用指南:免费批量获取LRC歌词,网易云与QQ音乐一次搞定
  • Origin中插入LaTeX公式:从环境配置到高阶应用全解析
  • 一次搞定10余种加密音乐:unlock-music 浏览器本地解密上手实测
  • Telerik WinForms AI编码助手实战:数据网格、图表与日程控件智能生成
  • 电脑IP配置:为什么要配IP
  • AI舞蹈教学系统技术解析:从姿态估计到动作对比的工程实践
  • 软考网络工程师|第 9 章 网络诊断命令 + 故障工具 + 故障排查完整笔记
  • 2026年,医美GEO公司居然比咨询师更懂顾客?
  • 零代码构建AI漫剧流水线:从Stable Diffusion到CapCut的完整实践指南
  • Wireshark解密802.11报文:从原理到实践
  • m4s转mp4不再求人:bilibili缓存视频合并的完整通关指南
  • B站CC字幕下载与转换:一条命令把视频字幕变成本地文件
  • 【Linux】进程信号
  • 2026工业网络安全技术演进、防护体系搭建与产品选型实战指南
  • DeepSeek+Pi-Agent吊打Claude-Code
  • 微信聊天记录导出终极指南:用 WeChatMsg 永久保存并生成年度报告
  • 游戏字幕看不懂?Translumo实时屏幕翻译工具零基础上手全指南
  • 高校实验室试剂采购平台推荐:从选型到报销的全流程指南
  • 阿里云-cdn的证书到期-续期
  • Bash命令补全神器:从原理到实战,提升终端效率必备
  • Win11Debloat 快速上手:这款免费开源系统优化工具,几分钟让 Windows 11 提速又清净
  • 英雄联盟玩家的4个深夜破防瞬间:免费开源的 League Akari 辅助工具是怎么救场的