4 Mechanical Models II

4.4 Custom rendering

It is often useful to add custom rendering to an application. For example, one may wish to render forces acting on bodies, or marker positions that are not otherwise assigned to a component. Custom rendering is relatively easy to do, as described in this section.

4.4.1 Component render() methods

All renderable ArtiSynth components implement the IsRenderable interface, which contains the methods prerender() and render(). As its name implies, prerender() is called prior to rendering and is discussed in Section 4.4.4. The render() method, discussed here, performs the actual rendering. It has the signature

  void render (Renderer renderer, int flags)

where the Renderer supplies a large set of methods for performing the rendering, and flags is described in the API documentation. The methods supplied by the renderer include ones for drawing simple primitives such as points, lines, and triangles; simple 3D shapes such as cubes, cones, cylinders and spheres; and text components.

A full description of the Renderer can be obtained by checking its API documentation and also by consulting the “Rendering” section of the Maspack Reference Manual.

A small sampling of the methods supplied by a Renderer include:

Control render settings:
void setColor(Color c)

set the current color.

void setPointSize(float size)

Set the point size, in pixels.

void setLineWidth(float width)

Set the line width, in pixels.

void setShading(Shading shading)

Set the shading model.

Draw simple pixel-based primitives:
void drawPoint(Vector3d p)

Draw a point.

void drawLine(Vector3d p0, Vector3d p1)

Draw a line between points.

void drawLine(Vector3d p0, Vector3d p1, Vector3d p2)

Draw a triangle from three points.

Draw solid 3D shapes:
void drawSphere(Vector3d p, double radius)

Draw a sphere centered at p.

void drawCylinder(Vector3d p0, Vector3d p1, double radius, Mboolean capped)

Draw a cylinder between p0 and p1.

void drawArrow(Vector3d p, Vector3d dir, double scale, Mdouble radius, boolean capped)

Draw an arrow starting at point p and extending to point p + scale*dir.

void drawBox(Vector3d p, Vector3d widths)

Draw a box centered on p.

Draw points and lines based on specified render properties:
void drawPoint(RenderProps props, Vector3d p, boolean highlight)

Draw a point.

void drawLine(RenderProps props, Vector3d p0, Vector3d p1, Mboolean highlight)

Draw a line between points.

For example, the render() implementation below renders three spheres connected by pixel-based lines, as show in Figure 4.3 (left):

public void render (Renderer renderer, int flags) {
   // sphere centers
   Vector3d p0 = new Vector3d (-0.5, 0, 0.5);
   Vector3d p1 = new Vector3d (0.5, 0, 0.5);
   Vector3d p2 = new Vector3d (0, 0, -0.5);
   // draw the spheres using a golden-yellow color
   renderer.setColor (new Color (1f, 0.8f, 0f));
   double radius = 0.1;
   renderer.drawSphere (p0, radius);
   renderer.drawSphere (p1, radius);
   renderer.drawSphere (p2, radius);
   // connect spheres by pixel-based lines
   renderer.setColor (Color.RED);
   renderer.setLineWidth (3);
   renderer.drawLine (p0, p1);
   renderer.drawLine (p1, p2);
   renderer.drawLine (p2, p0);
}
Figure 4.3: Rendered images produced with sample implementations of render().

A Renderer also contains draw mode methods to implement the drawing of points, lines and triangles in a manner similar to the immediate mode of legacy OpenGL,

   renderer.beginDraw (drawModeType);
      ... define vertices and normals ...
   renderer.endDraw();

where drawMode is an instance of Renderer.DrawMode and includes points, lines, line strips and loops, triangles, and triangle strips and fans. The draw mode methods include:

void beginDraw(DrawMode mode)

Begin a draw mode sequence.

void addVertex(Vector3d vtx)

Add a vertex to the object being drawn.

void setNormal(Vector3d nrm)

Sets the current vertex normal for the object being drawn.

void endDraw()

Finish a draw mode sequence.

The render() implementation below uses draw mode to render the image shown in Figure 4.3, right:

public void render (Renderer renderer, int flags) {
   // the corners of the square
   Vector3d p0 = new Vector3d (0, 0, 0);
   Vector3d p1 = new Vector3d (1, 0, 0);
   Vector3d p2 = new Vector3d (1, 0, 1);
   Vector3d p3 = new Vector3d (0, 0, 1);
   renderer.setShading (Shading.NONE);  // turn off lighting
   renderer.setPointSize (6);
   renderer.beginDraw (DrawMode.POINTS);
   renderer.setColor (Color.RED);
   renderer.addVertex (p0);
   renderer.addVertex (p1);
   renderer.addVertex (p2);
   renderer.addVertex (p3);
   renderer.endDraw();
   renderer.setLineWidth (3);
   renderer.setColor (Color.BLUE);
   renderer.beginDraw (DrawMode.LINE_LOOP);
   renderer.addVertex (p0);
   renderer.addVertex (p1);
   renderer.addVertex (p2);
   renderer.addVertex (p3);
   renderer.endDraw();
   renderer.setShading (Shading.FLAT);  // restore lighting  // sphere centers
}

Finally, a Renderer contains methods for the rendering of render objects, which are collections of vertex, normal and color information that can be rendered quickly; it may be more efficient to use render objects for complex rendering involving large numbers of primitives. Full details on this, and other features of the rendering interface, are given in the “Rendering” section of the Maspack Reference Manual.

4.4.2 Implementing custom rendering

There are two easy ways to add custom rendering to a model:

  1. 1.

    Override the root model’s render() method;

  2. 2.

    Define a custom rendering component, with its own render() method, and add it to the model. This approach is more modular and makes it easier to reuse the rendering between models.

To override the root model render method, one simply adds the following declaration to the model’s definition:

   void render (Renderer renderer, int flags) {
      super.render (renderer, flags);
      ... custom rendering code goes here ...
   }

A call to super.render() is recommended to ensure that any rendering done by the model’s base class will still occur. Subsequent statements should then use the renderer object to perform whatever rendering is required, using methods such as described in Section 4.4.1 or in the “Rendering” section of the Maspack Reference Manual.

To create a custom rendering component, one can simply declare a subclass of RenderableComponentBase with a custom render() method:

import artisynth.core.modelbase.*;
import maspack.render.*;
class MyRenderComp extends RenderableComponentBase {
   void render (Renderer renderer, int flags) {
      ... custom rendering code goes here ...
   }
}

There is no need to call super.render() since RenderableComponentBase does not provide an implementation of render(). It does, however, provide default implementations of everything else required by the Renderable interface. This includes exporting render properties via the composite property renderProps, which the render() method may use to control the rendering appearance in a manner consistent with Section 4.3.2. It also includes an implementation of prerender() which by default does nothing. As discussed more in Section 4.4.4, this is often acceptable unless:

  1. 1.

    rendering will be adversely affected by quantities being updated asynchronously within the simulation;

  2. 2.

    the component contains subcomponents that also need to be rendered.

Once a custom rendering component has been defined, it can be created and added to the model within the build() method:

   MechModel mech;
   ...
   MyRenderComp rcomp = new MyRenderComp();
   mech.addRenderable (rcomp);

In this example, the component is added to the MechModel’s subcomponent list renderables, which ensures that it will be found by the rendering code. Methods for managing this list include:

void addRenderable(Renderable r)

Adds a renderable to the model.

boolean removeRenderable(Renderable r)

Removes a renderable from the model.

void clearRenderables()

Removes all renderables from the model.

ComponentListView<RenderableComponent> renderables()

Returns the list of all renderables.

4.4.3 Example: rendering body forces

Figure 4.4: BodyForceRendering being run in ArtiSynth.

The application model

  artisynth.demos.tutorial.BodyForceRendering

gives an example of custom rendering to draw the force and moment vectors acting on a rigid body as cyan and green arrows, respectively. The model extends RigidBodySpring (Section 3.2.2), defines a custom renderable class named ForceRenderer, and then uses this to render the forces on the box in the original model. The code, with the include files omitted, is listed below:

1 public class BodyForceRendering extends RigidBodySpring {
2
3    // Custom rendering component to draw forces acting on a rigid body
4    class ForceRenderer extends RenderableComponentBase {
5
6       RigidBody myBody;     // body whose forces are to be rendered
7       double myForceScale;  // scale factor for force vector
8       double myMomentScale; // scale factor for moment vector
9
10       ForceRenderer (RigidBody body, double forceScale, double momentScale) {
11          myBody = body;
12          myForceScale = forceScale;
13          myMomentScale = momentScale;
14       }
15
16       public void render (Renderer renderer, int flags) {
17          if (myForceScale > 0) {
18             // render force vector as a cyan colored arrow
19             Vector3d pnt = myBody.getPosition();
20             Vector3d dir = myBody.getForce().f;
21             renderer.setColor (Color.CYAN);
22             double radius = myRenderProps.getLineRadius();
23             renderer.drawArrow (pnt, dir, myForceScale, radius,/*capped=*/false);
24          }
25          if (myMomentScale > 0) {
26             // render moment vector as a green arrow
27             Vector3d pnt = myBody.getPosition();
28             Vector3d dir = myBody.getForce().m;
29             renderer.setColor (Color.GREEN);
30             double radius = myRenderProps.getLineRadius();
31             renderer.drawArrow (pnt, dir, myMomentScale, radius,/*capped=*/false);
32          }
33       }
34    }
35
36    public void build (String[] args) {
37       super.build (args);
38
39       // get the MechModel from the superclass
40       MechModel mech = (MechModel)findComponent ("models/mech");
41       RigidBody box = mech.rigidBodies().get("box");
42
43       // create and add the force renderer
44       ForceRenderer frender = new ForceRenderer (box, 0.1, 0.5);
45       mech.addRenderable (frender);
46
47       // set line radius property to control radius of the force arrows
48       RenderProps.setLineRadius (frender, 0.01);
49    }
50 }

The force renderer is defined at lines (4-34). For attributes, it contains a reference to the rigid body, plus scale factors for rendering the force and moment. Within the render() method (lines 16-33), the force and moment vectors are draw as cyan and green arrows, starting at the current body position, and scaled by their respective factors. This scaling is needed to ensure the arrows have a size appropriate to the viewer, and separate scale factors are needed because the force and moment vectors have different units. The arrow radii are given by the renderer’s lineRadius render property (lines 22 and 30).

The build() method starts by calling the super class build() method to create the original model (line 36), and then uses findComponent() to retrieve its MechModel, within which the “box” rigid body is located (lines 40-41). A ForceRenderer is then created for this body, with force and moment scale factors of 0.1 and 0.5, and added to the MechModel (lines 44-45). The renderer’s lineRadius render property is then set to 0.01 (line 48).

To run this example in ArtiSynth, select All demos > tutorial > BodyForceRenderer from the Models menu. When run, the model should appear as in Figure 4.4, showing the force and moment vectors acting on the box.

4.4.4 The prerender() method

Component render() methods are called within ArtiSynth’s graphics thread, and so are called asynchronously with respect to the simulation thread(s). This means that the simulation may be updating component attributes, such as positions or forces, at the same time they are being accessed within render(), leading to inconsistent results in the viewer. While these inconsistencies may not be significant, particularly if the attributes are changing slowly between time steps, some applications may wish to avoid them. For this, renderable components also implement a prerender() method, with the signature

  void prerender (RenderList list);

that is called in synchronization with the simulation prior to rendering. Its purpose is to:

  • Make copies of simulation-varying attributes so that they can be used without conflict in the render() method;

  • Identify to the system any additional subcomponents that also need to be rendered.

The first task is usually accomplished by copying simulation-varying attributes into cache variables stored within the component itself. For example, if a component is responsible for rendering the forces of a rigid body (as per Section 4.4.3), it may wish to make a local copy of these forces within prerender:

   RigidBody myBody;                     // body whose forces are being rendered
   Wrench myRenderForce = new Wrench();  // copy of forces used for rendering
   void prerender (RenderList list) {
      myRenderForce.set (myBody.getForce());
   }
   void render (Renderer renderer, int flags) {
      // do rendering with myRenderForce
      ...
   }

The second task, identifying renderable subcomponents, is accomplished using the addIfVisible() and addIfVisibleAll() methods of the RenderList argument, as in the following examples:

  // additional sub components that need to be rendered
  RenderableComponent mySubCompA;
  RenderableComponent mySubCompB;
  void prerender (RenderList list) {
     list.addIfVisible (mySubCompA);
     list.addIfVisible (mySubCompB);
     ...
   }
  // list of child frame components that also need to be rendered
  RenderableComponentList<Frame> myFrames;
  void prerender (RenderList list) {
     list.addIfVisibleAll (myFrames);
     ...
   }

It should be noted that some ArtiSynth components already support cached copies of attributes that can be used for rendering. In particular, Point (whose subclasses include Particle and FemNode3d) uses prerender() to cache its position as an array of float[] which can be obtained using getRenderCoords(), and Frame (whose subclasses include RigidBody) caches its pose as a RigidTransform3d that can be obtained with getRenderFrame().