9 Muscle Wrapping and Via Points

9.5 Alternate Wrapping Surfaces

Although it common to use the general mesh geometry of a RigidBody as the wrapping surface, situations may arise where it is desirable to not do this. These may include:

  • The general mesh geometry is not sufficiently smooth to form a good wrapping surface;

  • Wrapping around the default mesh geometry is not stable, in that it is too easy for the wrap strand to “slip off”;

  • Using one of the simpler analytic geometries (Table 9.1) may result in a more efficient computation.

There are a couple of ways to handle this. One, discussed in Section 9.3, involves creating a collision mesh which is separate from the general mesh geometry. However, that same collision mesh must then also be used for collision handling (Chapter 8). If that is undesirable, or if multiple wrapping surfaces are needed, then a different approach may be used. This involves creating the desired wrappable as a separate object and then attaching it to the main RigidBody. Typically, this wrappable will be created with zero mass (or density), so that it does not alter the effective mass or inertia of the main body. The general procedure then becomes:

  1. 1.

    Create the main RigidBody with whatever desired geometry and inertia is needed;

  2. 2.

    Create the additional wrappable object(s), usually with zero density/mass;

  3. 3.

    Attach the wrappables to the main body using one of the MechModel attachFrame() methods described in Section 3.8.3.

9.5.1 Example: wrapping for a finger joint

Figure 9.12: PhalanxWrapping model loaded into ArtiSynth.

An example using an alternate wrapping surface is given by artisynth.demos.tutorial.PhalanxWrapping, which shows a a muscle wrapping around a joint between two finger bones. Because the bones themselves are fairly narrow, using them as wrapping surfaces would likely lead to the muscle slipping off. Instead, a RigidCylinder is used for the wrapping and attached to one of the bones. The code, with include directives excluded, is given below:

1 public class PhalanxWrapping extends RootModel {
2
3    private static Color BONE = new Color (1f, 1f, 0.8f);
4    private static double DTOR = Math.PI/180.0;
5
6    private RigidBody createBody (MechModel mech, String name, String fileName) {
7       // creates a bone from its mesh and adds it to a MechModel
8       String filePath = PathFinder.findSourceDir(this) + "/data/" + fileName;
9       RigidBody body = RigidBody.createFromMesh (
10          name, filePath, /*density=*/1000, /*scale=*/1.0);
11       mech.addRigidBody (body);
12       RenderProps.setFaceColor (body, BONE);
13       return body;
14    }
15
16    public void build (String[] args) {
17
18       MechModel mech = new MechModel ("mech");
19       addModel (mech);
20
21       // create the two phalanx bones, and offset them
22       RigidBody proximal = createBody (mech, "proximal", "HP3ProximalLeft.obj");
23       RigidBody distal = createBody (mech, "distal", "HP3MiddleLeft.obj");
24       distal.setPose (new RigidTransform3d (0.02500, 0.00094, -0.03979));
25
26       // make the proximal phalanx non dynamic; add damping to the distal
27       proximal.setDynamic (false);
28       distal.setFrameDamping (0.03);
29
30       // create a revolute joint between the bones
31       RigidTransform3d TJW =
32          new RigidTransform3d (0.018, 0, -0.022, 0, 0, -DTOR*90);
33       HingeJoint joint = new HingeJoint (proximal, distal, TJW);
34       joint.setShaftLength (0.02); // render joint as a blue cylinder
35       RenderProps.setFaceColor (joint, Color.BLUE);
36       mech.addBodyConnector (joint);
37
38       // create markers for muscle origin and insertion points
39       FrameMarker origin = mech.addFrameMarkerWorld (
40          proximal, new Point3d (0.0098, -0.0001, -0.0037));
41       FrameMarker insertion = mech.addFrameMarkerWorld (
42          distal, new Point3d (0.0293, 0.0009, -0.0415));
43
44       // create a massless RigidCylinder to use as a wrapping surface and
45       // attach it to the distal bone
46       RigidCylinder cylinder = new RigidCylinder (
47          "wrapSurface", /*rad=*/0.005, /*h=*/0.04, /*density=*/0, /*nsegs=*/32);
48       cylinder.setPose (TJW);
49       mech.addRigidBody (cylinder);
50       mech.attachFrame (cylinder, distal);
51
52       // create a wrappable muscle using a SimpleAxialMuscle material
53       MultiPointSpring muscle = new MultiPointMuscle ("muscle");
54       muscle.setMaterial (
55          new SimpleAxialMuscle (/*k=*/0.5, /*d=*/0, /*maxf=*/0.04));
56       muscle.addPoint (origin);
57       // add an initial point to the wrappable segment to make sure it wraps
58       // around the cylinder the right way
59       muscle.setSegmentWrappable (
60          50, new Point3d[] { new Point3d (0.025, 0.0, -0.02) });
61       muscle.addPoint (insertion);
62       muscle.addWrappable (cylinder);
63       muscle.updateWrapSegments(); // “shrink wrap” around cylinder
64       mech.addMultiPointSpring (muscle);
65
66       // set render properties
67       RenderProps.setSphericalPoints (mech, 0.002, Color.BLUE);
68       RenderProps.setCylindricalLines (muscle, 0.001, Color.RED);
69       RenderProps.setFaceColor (cylinder, new Color (200, 200, 230));
70    }
71 }

The method createBody() (lines 6-14) creates a rigid body from a geometry mesh stored in a file in the directory “data” beneath the source directory, using the utility class PathFinder used to determine the file path (Section 2.6).

Within the build() method, a MechModel is created containing two rigid bodies representing the bones, proximal and distal, with proximal fixed and distal free to move with a frame damping of 0.03 (lines 18-28). A cylindrical joint is then added between the bones, along with markers describing the muscle’s origin and insertion points (lines 31-42). A RigidCylinder is created to act as a wrapping obstacle and attached to the distal bone in the same location as the joint (lines 46-50); since it is created with a density of 0 it has no mass and hence does not affect the bone’s inertia. The muscle itself is created at lines 53-64, using a SimpleAxialMuscle as a material and an extra initial point specified to setSegmentWrappable() to ensure that it wraps around the cylinder in the correct way (Section 9.4). Finally, some render properties are set at lines 67-69.

To run this example in ArtiSynth, select All demos > tutorial > PhalanxWrapping from the Models menu. The model should load and initially appear as in Figure 9.12. When running the model, one can move the distal bone either by using the pull tool (Section “Pull Manipulation” in the ArtiSynth User Interface Guide), or selecting the muscle in the GUI, invoking a property dialog by choosing Edit properties ... from the right-click context menu, and adjusting the excitation property.

9.5.2 Example: toy muscle arm with wrapping

Figure 9.13: Left: ToyMuscleArm loaded into ArtiSynth. Right: demo running with exciter values set from the control panel.

A more complex example of wrapping, combined with multiple joints and point-to-point muscles, is given by artisynth.demos.tutorial.ToyMuscleArm, which depicts a two-link “arm”, connected by hinge joints, with opposing muscles controlling the poses of each link. The code, with import directives excluded, is given below:

1 public class ToyMuscleArm extends RootModel {
2    // geodir is folder in which to locate mesh data:
3    protected String geodir = PathFinder.getSourceRelativePath (this, "data/");
4    protected double density = 1000.0;   // default density
5    protected double stiffness = 1000.0; // passive muscle stiffness
6
7    protected MechModel myMech;     // mech model
8    protected FrameMarker myTipMkr; // marker at tip of the arm
9    protected HingeJoint myHinge0;  // lower hinge joint
10    protected HingeJoint myHinge1;  // upper hinge joint
11    protected RigidBody myLink0;    // lower link
12    protected RigidBody myLink1;    // upper link
13
14    /**
15     * Add an axial muscle between body0 and body1 using markers attached at
16     * world coords (x0,0,z0) and (x1,0,z1), respectively.
17     */
18    public Muscle addMuscle (
19       RigidBody body0, double x0, double z0,
20       RigidBody body1, double x1, double z1) {
21
22       FrameMarker l0 = myMech.addFrameMarkerWorld (body0, new Point3d(x0, 0, z0));
23       FrameMarker l1 = myMech.addFrameMarkerWorld (body1, new Point3d(x1, 0, z1));
24       Muscle muscle = new Muscle ();
25       muscle.setMaterial (
26          new SimpleAxialMuscle (stiffness, /*damping=*/0, /*fmax=*/1000));
27       myMech.attachAxialSpring (l0, l1, muscle);
28       return muscle;
29    }
30
31    /**
32     * Add an wrapped muscle between body0 and body1 using markers attached at
33     * world coords (x0,0,z0) and (x1,0,z1) and wrapping around wrapBody.
34     */
35    public MultiPointMuscle addWrappedMuscle (
36       RigidBody body0, double x0, double z0,
37       RigidBody body1, double x1, double z1, Wrappable wrapBody) {
38
39       FrameMarker l0 = myMech.addFrameMarkerWorld (body0, new Point3d(x0, 0, z0));
40       FrameMarker l1 = myMech.addFrameMarkerWorld (body1, new Point3d(x1, 0, z1));
41       MultiPointMuscle muscle = new MultiPointMuscle ();
42       muscle.setMaterial (
43          new SimpleAxialMuscle (stiffness, /*damping=*/0, /*fmax=*/500));
44       muscle.addPoint (l0);
45       muscle.setSegmentWrappable (/*numknots=*/50);
46       muscle.addPoint (l1);
47       muscle.addWrappable (wrapBody);
48       muscle.updateWrapSegments(); // shrink wrap to current wrappable
49       myMech.addMultiPointSpring (muscle);
50       return muscle;
51    }
52
53    /**
54     * Adds a hinge joint between body0 and body1, at world coordinates
55     * (x0,0,z0) and with the joint axis parallel to y. Joint limits are set to
56     * minDeg and maxDeg (in degrees).
57     */
58    public HingeJoint addHingeJoint (
59       RigidBody body0, RigidBody body1,
60       double x0, double z0, double minDeg, double maxDeg) {
61
62       HingeJoint hinge = new HingeJoint (
63          body0, body1, new Point3d(x0, 0, z0), new Vector3d (0, 1, 0));
64       myMech.addBodyConnector (hinge);
65       hinge.setThetaRange (minDeg, maxDeg);
66       // set render properties for the hinge
67       hinge.setShaftLength (0.4);
68       RenderProps.setFaceColor (hinge, Color.BLUE);
69       return hinge;
70    }
71
72    public void build (String[] args) throws IOException {
73       myMech = new MechModel ("mech");
74       myMech.setInertialDamping (1.0);
75       addModel (myMech);
76
77       // create base body
78       PolygonalMesh mesh = new PolygonalMesh (geodir+"bracketedBase.obj");
79       RigidBody base = RigidBody.createFromMesh ("base", mesh, density, 1.0);
80       base.setDynamic (false);
81       myMech.addRigidBody (base);
82
83       // create rigid body for link0 and place it above the origin
84       mesh = MeshFactory.createRoundedBox (0.6, 0.2, 0.2, /*nslices=*/20);
85       myLink0 = RigidBody.createFromMesh ("link0", mesh, density, /*scale=*/1.0);
86       myLink0.setPose (new RigidTransform3d (0, 0, 0.3));
87       myMech.addRigidBody (myLink0);
88
89       // create rigid body for link1 and place it after link0
90       mesh = MeshFactory.createRoundedBox (0.6, 0.10, 0.15, /*nslices=*/20);
91       myLink1 = RigidBody.createFromMesh ("link1", mesh, density, /*scale=*/1.0);
92       myLink1.setPose (new RigidTransform3d (0, 0, 0.9));
93       myMech.addRigidBody (myLink1);
94
95       // create massless cylinder for wrapping surface and attach it to link1
96       RigidCylinder cylinder = new RigidCylinder (
97          "wrapSurface", /*rad=*/0.12, /*h=*/0.25, /*density=*/0, /*nsegs=*/32);
98       cylinder.setPose (new RigidTransform3d (0, 0, 0.6,  0, 0, Math.PI/2));
99       myMech.addRigidBody (cylinder);
100       myMech.attachFrame (cylinder, myLink1);
101
102       // add a hinge joints between links
103       myHinge0 = addHingeJoint (myLink0, base, 0, 0, -70, 70);
104       myHinge1 = addHingeJoint (myLink1, myLink0, 0, 0.6, -120, 120);
105
106       Muscle muscleL0 = addMuscle (myLink0, -0.1, 0.4, base, -0.48, -0.15);
107       Muscle muscleR0 = addMuscle (myLink0,  0.1, 0.4, base,  0.48, -0.15);
108       MultiPointMuscle muscleL1 = addWrappedMuscle (
109          myLink1, -0.05, 1.2, myLink0, -0.1, 0.5, cylinder);
110       MultiPointMuscle muscleR1 = addWrappedMuscle (
111          myLink1,  0.05, 1.2, myLink0,  0.1, 0.5, cylinder);
112
113       // add a marker at the tip
114       myTipMkr = myMech.addFrameMarkerWorld (
115          myLink1, new Point3d(0.0, 0.0, 1.25));
116
117       // reposition the model using the joint angles to move it into a
118       // non-upright position so it will move when first run
119       myHinge0.setTheta (-20);
120       myHinge1.setTheta (38.4);
121       myMech.updateWrapSegments(); // update muscle wrapping for new config
122
123       // set muscle rest lengths to their initial lengths
124       muscleL0.setRestLength (muscleL0.getLength());
125       muscleR0.setRestLength (muscleR0.getLength());
126       muscleL1.setRestLength (muscleL1.getLength());
127       muscleR1.setRestLength (muscleR1.getLength());
128
129       // add a control panel for muscle excitations
130       ControlPanel panel = new ControlPanel ();
131       panel.addWidget ("excitation L0", muscleL0, "excitation");
132       panel.addWidget ("excitation R0", muscleR0, "excitation");
133       panel.addWidget ("excitation L1", muscleL1, "excitation");
134       panel.addWidget ("excitation R1", muscleR1, "excitation");
135       addControlPanel (panel);
136
137       // render properties: muscles as red spindles, points as white spheres,
138       // bodies blue-gray, tip marker green, and wrap cylinder purple-gray
139       RenderProps.setSpindleLines (myMech, 0.02, Color.RED);
140       RenderProps.setSphericalPoints (myMech, 0.02, Color.WHITE);
141       RenderProps.setFaceColor (myMech, new Color (0.71f, 0.71f, 0.85f));
142       RenderProps.setPointColor (myTipMkr, Color.GREEN);
143       RenderProps.setFaceColor (cylinder, new Color (0.75f, 0.61f, 0.75f));
144    }
145 }

Within the build() method, the mech model is created in the usual way and inertial damping is set to 1.0 (lines 73-75). Next, the rigid bodies depicting the base and links are created (lines 78-93). All bodies are created from meshes, with the base being made non-dynamic and the poses of the links set so that they line up vertically in their start position (Figure 9.13, left). A massless cylinder is created to provide a wrapping surface around the second hinge joint and attached to link1 (lines 96-109).

Hinge joints (Section 3.5.1) are then added between the base and link0 and link0 and link1 (lines 103-104), using the convenience method addHingeJoint() (lines 58-70). This method creates a hinge joint at a prescribed position in the x-z plane, uses it to connect two bodies, sets the range for the joint angle theta, and sets properties to render the shaft in blue.

Next, point-to-point muscles are placed on opposite sides of each link (lines 106-111). For link0, two Muscle components are used, added with the method addMuscle() (lines 18-29); this method creates a muscle connecting two bodies, using frame markers whose positions are specified in world coordinates in the x-z plane, and a SimpleMuscleMaterial (Section 4.5.1.1) with prescribed stiffness and maximum active force. For link1, MultiPointMuscle components are used instead so that they can be wrapped around the wrapping cylinder. These muscles are created with the method addWrappedMuscle() (lines 35-51), which creates a multipoint muscle connecting two bodies, using a single wrappable segment between two frame markers (also specified in the x-z plane). After the muscle is placed, its updateWrapSegments() method is called to “shrink wrap” it around the cylinder.

A marker is attached to the tip on link1 at lines 114-115, and then the links are repositioned, by setting the joint angles, so that the arm is not initially upright and will hence move when first run (lines 119-120). After repositioning, all wrap paths are updated by calling the MechModel method updateWrapSegments() (line 121), and the rest lengths for all muscles are set to their current lengths so that they will not initially exert passive tension (lines 124-127).

A control panel is created to allow runtime adjustment of the four muscle excitation values (lines 130-135), and render properties are set at lines 139-143.

To run this example in ArtiSynth, select All demos > tutorial > ToyMuscleArm from the Models menu. The loaded model will appear as in Figure 9.13, left. When the model is run, the links can be moved by adjusting the muscle excitation values in the control panel (Figure 9.13, right).