|
| 1 | +# Required import for python |
| 2 | +import Sofa |
| 3 | +import numpy as np |
| 4 | +import math |
| 5 | + |
| 6 | + |
| 7 | +# Main function taking a boolean as parameter to choose whether or not to use the GUI |
| 8 | +def main(use_gui=True): |
| 9 | + import SofaImGui |
| 10 | + import Sofa.Gui |
| 11 | + |
| 12 | + root = Sofa.Core.Node("root") |
| 13 | + createScene(root) |
| 14 | + Sofa.Simulation.initRoot(root) |
| 15 | + |
| 16 | + if not USE_GUI: |
| 17 | + import SofaQt |
| 18 | + |
| 19 | + for iteration in range(10): |
| 20 | + Sofa.Simulation.animate(root, root.dt.value) |
| 21 | + else: |
| 22 | + Sofa.Gui.GUIManager.Init("myscene", "imgui") |
| 23 | + Sofa.Gui.GUIManager.createGUI(root, __file__) |
| 24 | + Sofa.Gui.GUIManager.SetDimension(1080, 1080) |
| 25 | + Sofa.Gui.GUIManager.MainLoop(root) |
| 26 | + Sofa.Gui.GUIManager.closeGUI() |
| 27 | + |
| 28 | + |
| 29 | +def createScene(root): |
| 30 | + root.gravity=[0, -9.81, 0] |
| 31 | + root.dt=0.02 |
| 32 | + |
| 33 | + root.addObject("RequiredPlugin", pluginName=[ 'Sofa.Component.Collision.Detection.Algorithm', |
| 34 | + 'Sofa.Component.Collision.Detection.Intersection', 'Sofa.Component.Collision.Geometry', |
| 35 | + 'Sofa.Component.Collision.Response.Contact', 'Sofa.Component.Constraint.Projective', |
| 36 | + 'Sofa.Component.IO.Mesh','Sofa.Component.LinearSolver.Iterative', |
| 37 | + 'Sofa.Component.Mapping.Linear', 'Sofa.Component.Mass', 'Sofa.Component.ODESolver.Backward', |
| 38 | + 'Sofa.Component.SolidMechanics.FEM.Elastic','Sofa.Component.StateContainer', |
| 39 | + 'Sofa.Component.Topology.Container.Dynamic','Sofa.Component.Visual', |
| 40 | + 'Sofa.GL.Component.Rendering3D','Sofa.Component.Constraint.Lagrangian.Correction', |
| 41 | + 'Sofa.Component.Constraint.Lagrangian.Solver', 'Sofa.Component.MechanicalLoad', |
| 42 | + 'Sofa.Component.LinearSolver.Direct','Sofa.Component.AnimationLoop' |
| 43 | + ]) |
| 44 | + |
| 45 | + root.addObject('FreeMotionAnimationLoop') |
| 46 | + # Constraint solver computing the constraint/contact forces, stored in the constraint space (normal , tangential_1, tangential_2) |
| 47 | + constraint_solver = root.addObject('GenericConstraintSolver', maxIterations=1000, tolerance=1e-6, computeConstraintForces=True) |
| 48 | + |
| 49 | + root.addObject('VisualStyle', displayFlags="showCollisionModels hideVisualModels showForceFields") |
| 50 | + root.addObject('CollisionPipeline', name="collision_pipeline") |
| 51 | + root.addObject('BruteForceBroadPhase', name="broad_phase") |
| 52 | + root.addObject('BVHNarrowPhase', name="narrow_phase") |
| 53 | + root.addObject('DiscreteIntersection') |
| 54 | + root.addObject('CollisionResponse', name="collision_response", response="FrictionContactConstraint", responseParams="mu=0.1") |
| 55 | + |
| 56 | + root.addObject('MeshOBJLoader', name="load_liver_surface", filename="mesh/liver-smooth.obj") |
| 57 | + |
| 58 | + liver = root.addChild('Liver') |
| 59 | + liver.addObject('EulerImplicitSolver', name="cg_odesolver", rayleighStiffness=0.1, rayleighMass=0.1) |
| 60 | + liver.addObject('SparseLDLSolver', name="linear_solver", template="CompressedRowSparseMatrixMat3x3d") |
| 61 | + liver.addObject('MeshGmshLoader', name="loader_liver_volume", filename="mesh/liver.msh") |
| 62 | + liver.addObject('TetrahedronSetTopologyContainer', name="topo", src="@loader_liver_volume") |
| 63 | + # Liver MechanicalObject where the constraint/contact forces will be stored in the (x,y,z) coordinate system |
| 64 | + liverMO = liver.addObject('MechanicalObject', name="dofs", src="@loader_liver_volume") |
| 65 | + liver.addObject('TetrahedronSetGeometryAlgorithms', template="Vec3d", name="geom_algo") |
| 66 | + liver.addObject('DiagonalMass', name="mass", massDensity=1.0) |
| 67 | + liver.addObject('TetrahedralCorotationalFEMForceField', template="Vec3d", name="FEM", method="large", poissonRatio=0.3, youngModulus=3000, computeGlobalMatrix=False) |
| 68 | + liver.addObject('FixedProjectiveConstraint', name="fixed_constraint", indices=[3,39,64]) |
| 69 | + liver.addObject('LinearSolverConstraintCorrection') |
| 70 | + |
| 71 | + # Forcefield only used for visualization purposes (of the contact fborces) |
| 72 | + contactVisu = liver.addChild('RenderingContactForces') |
| 73 | + contactVisu.addObject('VisualStyle', displayFlags="showVisualModels") |
| 74 | + renderingForces = contactVisu.addObject('VisualVectorField', name="drawing_contact_forces", vectorScale="100", vector="@../dofs.lambda", position="@../dofs.position", color="orange", drawMode="Arrow") |
| 75 | + |
| 76 | + visu = liver.addChild('Visu') |
| 77 | + visu.addObject('OglModel', name="visual_model", src="@../../load_liver_surface") |
| 78 | + visu.addObject('BarycentricMapping', name="visual_mapping", input="@../dofs", output="@visual_model") |
| 79 | + |
| 80 | + surf = liver.addChild('Surf') |
| 81 | + surf.addObject('SphereLoader', name="loader_sphere_model", filename="mesh/liver.sph") |
| 82 | + surf.addObject('MechanicalObject', name="spheres", position="@loader_sphere_model.position") |
| 83 | + surf.addObject('SphereCollisionModel', name="collision_model", listRadius="@loader_sphere_model.listRadius") |
| 84 | + surf.addObject('BarycentricMapping', name="collision_mapping", input="@../dofs", output="@spheres") |
| 85 | + |
| 86 | + |
| 87 | + particle = root.addChild('Particle') |
| 88 | + particle.addObject('EulerImplicitSolver') |
| 89 | + particle.addObject('CGLinearSolver', threshold=1e-09, tolerance=1e-09, iterations=200) |
| 90 | + # Particle MechanicalObject where the constraint/contact forces will be stored in the (x,y,z) coordinate system |
| 91 | + particleMO = particle.addObject('MechanicalObject', showObject=True, position=[-2, 10, 0, 0, 0, 0, 1], name=f'particle_DoFs', template='Rigid3d') |
| 92 | + particle.addObject('UniformMass', totalMass=1) |
| 93 | + particle.addObject('ConstantForceField', name="CFF", totalForce=[0, -1, 0, 0, 0, 0] ) |
| 94 | + particle.addObject('SphereCollisionModel', name="SCM", radius=1.0 ) |
| 95 | + particle.addObject('UncoupledConstraintCorrection') |
| 96 | + |
| 97 | + |
| 98 | + # Python controller accessing and displaying the contact forces in the ConstantForceField |
| 99 | + root.addObject(AccessContactForces('AccessContactForces', name='AccessContactForces', |
| 100 | + constraint_solver=constraint_solver, |
| 101 | + soft_liver=liverMO, |
| 102 | + forces_visu=renderingForces, |
| 103 | + root_node=root)) |
| 104 | + |
| 105 | + |
| 106 | +class AccessContactForces(Sofa.Core.Controller): |
| 107 | + |
| 108 | + def __init__(self, *args, **kwargs): |
| 109 | + Sofa.Core.Controller.__init__(self, *args, **kwargs) |
| 110 | + self.constraint_solver = kwargs.get("constraint_solver") |
| 111 | + self.soft_liver = kwargs.get("soft_liver") |
| 112 | + self.forces_visu = kwargs.get("forces_visu") |
| 113 | + self.root_node = kwargs.get("root_node") |
| 114 | + # Initialize the rendered vector with zero vec<Vec3> |
| 115 | + self.forces_visu.vector.value = np.zeros((181,3)) |
| 116 | + |
| 117 | + def onAnimateEndEvent(self, event): |
| 118 | + |
| 119 | + lambda_vector = self.constraint_solver.constraintForces.value |
| 120 | + # If there is a contact |
| 121 | + if(len(lambda_vector) > 0): |
| 122 | + print(f"At time = {round(self.root_node.time.value,3)}, forces in the contact space (n, t1, t2) equals:\n {lambda_vector} ") |
| 123 | + |
| 124 | + # Compute the inverse (reaction force) |
| 125 | + self.forces_visu.vector.value = -self.soft_liver.getData("lambda").value |
| 126 | + |
| 127 | + # Scale automatically the displayed force vector |
| 128 | + visuScale = self.forces_visu.vectorScale.value |
| 129 | + fact = np.max(lambda_vector) |
| 130 | + self.forces_visu.vectorScale.value = 10/fact |
| 131 | + |
| 132 | + # If no contact |
| 133 | + else: |
| 134 | + self.forces_visu.vector.value = np.zeros((181,3)) |
| 135 | + |
| 136 | + |
| 137 | + |
| 138 | +# Function used only if this script is called from a python environment |
| 139 | +if __name__ == '__main__': |
| 140 | + main(True) |
0 commit comments