2 Rendering

2.4 Render Objects

In modern graphics interfaces, applications send geometric, color, and texture information to the GPU, which then applies the various computations associated with the graphics pipeline. Transferring this data to the GPU can be an expensive activity, and efficient applications try to minimize it. Modern OpenGL, in particular, requires applications to bundle the geometric, color, and texture information into buffer objects, which are transmitted to the GPU and then cached there. This generally increases efficiency because (a) large amounts of data can be transferred at once, and (b) the cached data can be reused in subsequent rendering operations.

For OpenGL renderer implementations, a buffer object must be created and transmitted to the GPU once for every invocation of a draw method or beginDraw()/endDraw() block. As a more efficient alternative, applications can create instances of render objects, which store geometric, color, and texture information and manage the transmission of this data to the GPU. Essentially, a render object provides a convenience wrapper for OpenGL-type buffer objects. However, the use of render objects is generic and not limited to OpenGL implementations of the renderer.

Render objects are implemented using the RenderObject class, which contains:

  • Attribute data, including positions, and (optionally) normals, colors, and texture coordinates.

  • vertex data, where each vertex points to a single position, as well as (optionally) a single normal, color, and texture attribute.

  • primitive data, consisting of zero or more “groups” of points, lines, and triangles.

Figure 2.18: Render object structure, showing the relationship between attributes, vertices, and primitives.

To summarize, primitives are made of vertices, which are in turn comprised of references to attributes (Figure 2.18).

Render objects can be created anywhere within the application program, although care must be taken to synchronize their modification with the render() methods. While an easy way to do this is to create them directly within the render method, care should then be taken to allow them to persist between render invocations, since each time a new object is created, all of its data must be transferred to the GPU. It is recommended to create render objects within prerender(), since this should automatically provide synchronization with both render() and any other thread this is modifying render data. A render object can itself be used to cache rendering data associated with a dynamically varying object, in which case creating (or updating) it from within the prerender method is even more appropriate.

2.4.1 Building a render object

Attributes can be added using a variety of RenderObject methods, including:

int addPosition (float px, float py, float pz);
int addPosition (Vector3d pos);
int addPosition (float[] pos);    // set by reference
int addNormal (float nx, float ny, float nz);
int addNormal (Vector3d nrm);
int addNormal (float[] nrm);      // set by reference
int addColor (float r, float g, float b, float a);
int addColor (Color color);
int addColor (byte[] rgba);       // set by reference
int addTextureCoord (float tx, float ty);
int addTextureCoord (Vector2d xy);
int addTextureCoord (float[] xy); // set by reference

Each of these creates an instance of the specified attribute, sets it to the indicated value, adds it to the render object, and assigns it a unique index, which is returned by the add method. The index can be referred to later when adding vertices, or when later changing the attribute’s value (Section 2.4.5). Indices are increased sequentially, starting from zero.

Methods above marked “set by reference” use the supplied array to directly store values within the attribute, so that subsequent changes to the array’s values will also cause the attribute’s values to change.

A vertex is defined by a 4-tuple of indices that can be used to refer to a previously defined instance of each of the four attributes: position, normal, color, and texture coordinate. A vertex does not need to reference all attributes, but it is required that all vertices have a consistent set of attributes (e.g. either all vertices have a normal reference, or none do). When adding a vertex, a negative index indicates that the attribute is not present. Since a vertex can refer to at most one of each attribute, this means that when building primitives, it may be necessary to define multiple vertices for the same position if different values are required for the other attributes (e.g., normals, colors, or texture coordinates). For example, for the corner of the cube at location (-1,-1,-1), there must be three vertices defined, one each with normals (-1,0,0), (0,-1,0), and (0,0,-1).

Referencing attributes by index allows for attributes to be reused, and also for the numbers of any given attribute to vary. For example, when rendering the faces of a solid cube, we typically need 24 vertices (4 for each of the 6 faces), but only 8 positions (one per corner) and 6 normals (one per face).

Vertices can be added using the RenderObject method

int addVertex (int pidx, int nidx, int cidx, int tidx);

where pidx, nidx, cidx, and tidx are the indices of the desired postion, normal, color and texture coordinate attributes (or -1 for attributes that are undefined). The method returns a unique index for the vertex, which can be referred to later when adding primitives. Indices are increased sequentially, starting from zero.

Once vertices are created, they can be used to define and add primitives. Three types of primitives are available: points (one vertex each), lines (two vertices each), and triangles (three vertices each). Methods for adding these include

addPoint (int v0idx);
addLine (int v0idx, int v1idx);
addTriangle (int v0idx, int v1idx, int v2idx);

Each of these takes a set of vertex indices, creates the corresponding primitive, and adds it to the current group for the that primitive (primitive groups are discussed in Section 2.4.7).

Once all the primitives have been added, the Renderer method draw(RenderObject) can then be used to draw all the primitives in the object using the current graphics state. A variety of other draw methods are available for drawing subsets of primitives; these are detailed in Section 2.4.6.

There are no methods to remove individual attributes, vertices, or primitives. However, as described in Section 2.4.5, it is possible to use clearAll() to clear the entire object, after which it may be rebuilt, or clearPrimitives() to clear just the primitive sets.

Listing 3 gives a complete example combining the above operations to create a render object that draws the open tetrahedron described in Section 2.3.2 and Figure 2.7. In this example, the object itself is created using the method createTetRenderObject(). This is in turn called once within prerender() to create the object and store it in the member field myRob, allowing it to then be used as needed within render(). As indicated above, it is generally recommended to create or update render objects within the prerender method, particularly if they need to be modfied to reflect dynamically changing geometry or colors.

Listing 3: Construction and use of a render object to draw a partial tetrahedron.
import maspack.render.*;
import maspack.render.Renderer.FaceStyle;
...
   RenderObject myRob;
   private RenderObject createTetRenderObject() {
      // the corners of the tetrahedron
      RenderObject robj = new RenderObject();
      // add positions and normals
      int pi0 = robj.addPosition (0, 0, 0);
      int pi1 = robj.addPosition (1, 0, 0);
      int pi2 = robj.addPosition (0, 1, 0);
      int pi3 = robj.addPosition (0, 0, 1);
      int ni0 = robj.addNormal (0, 0, -1);
      int ni1 = robj.addNormal (-1, 0, 0);
      int ni2 = robj.addNormal (0, -1, 0);
      // add three vertices per triangle, each with position and normal
      // information and color and texture coords undefined, and then
      // use these to define a triangle primitive
      int v0, v1, v2;
      // first triangle
      v0 = robj.addVertex (pi0, ni0, -1, -1);
      v1 = robj.addVertex (pi2, ni0, -1, -1);
      v2 = robj.addVertex (pi1, ni0, -1, -1);
      robj.addTriangle (v0, v1, v2);
      // second triangle
      v0 = robj.addVertex (pi0, ni1, -1, -1);
      v1 = robj.addVertex (pi3, ni1, -1, -1);
      v2 = robj.addVertex (pi2, ni1, -1, -1);
      robj.addTriangle (v0, v1, v2);
      // third triangle
      v0 = robj.addVertex (pi0, ni2, -1, -1);
      v1 = robj.addVertex (pi1, ni2, -1, -1);
      v2 = robj.addVertex (pi3, ni2, -1, -1);
      robj.addTriangle (v0, v1, v2);
      return robj;
   }
   public void prerender (RenderList list) {
      if (myRob == null) {
         myRob = createTetRenderObject();
      }
   }
   public void render (Renderer renderer, int flags) {
      renderer.setFaceStyle (FaceStyle.FRONT_AND_BACK);
      renderer.draw (myRob);  // draw the render object
      renderer.setFaceStyle (FaceStyle.FRONT);
   }

2.4.2 “Current” attributes

Keeping track of attribute indices as described in Section 2.4.1 can be tedious. Instead of doing this, one can use the fact that every attribute add method records the index of the added attribute, which then denotes the “current” value for that attribute. The following methods can then be used to add a vertex using various current attribute values:

// use current position, normal, color and texture coords:
int addVertex ();
// use position pidx with current normal, color and texture coords:
int addVertex (int pidx);
// use position pidx and normal nidx with current color and texture coords:
int addVertex (int pidx, int nidx);

If any of the attributes have no “current” value, then the corresponding index value is -1 and that attribute will be undefined for the vertex.

If desired, it is possible to set or query the current attribute index, using methods of the form

void setCurrent<Attribute>(int idx);
int getCurrent<Attribute>();

where <Attribute> is Position, Normal, Color, or TextureCoord and idx is the index of a currently added attribute. For convenience, another set of methods,

int vertex (float px, float py, float pz);
int vertex (Vector3d pos);

will create a new position at the specified location, and then also create a vertex using that position along with the current normal, color and texture coords.

We now give some examples. First, Listing 4 changes the tetrahedron code in Listing 3 to use a current normal in conjuction with vertex(px, py, pz).

Listing 4: Constructing a render object using current normals.
   // add three vertices per triangle, each with position and normal
   // information and color and texture coords undefined, and then
   // use these to define a triangle primitive
   int v0, v1, v2;
   // first triangle
   robj.addNormal (0, 0, -1);
   v0 = robj.vertex (0, 0, 0);
   v1 = robj.vertex (0, 1, 0);
   v2 = robj.vertex (1, 0, 0);
   robj.addTriangle (v0, v1, v2);
   // second triangle
   robj.addNormal (-1, 0, 0);
   v0 = robj.vertex (0, 0, 0);
   v1 = robj.vertex (0, 0, 1);
   v2 = robj.vertex (0, 1, 0);
   robj.addTriangle (v0, v1, v2);
   // third triangle
   robj.addNormal (0, -1, 0);
   v0 = robj.vertex (0, 0, 0);
   v1 = robj.vertex (1, 0, 0);
   v2 = robj.vertex (0, 0, 1);
   robj.addTriangle (v0, v1, v2);

One issue with using vertex(px, py, pz) is that it creates a new position for every vertex, even in situations where vertices can be shared. The example above (implicitly) creates 9 positions where only 4 would be sufficient. Instead, one can create the positions separately (as in Listing 3), and then use vertex(pidx) to add vertices created from predefined positions along with current attribute values. Listing 5 does this for the tetrahedron, while also using a current color to give each face it’s own color. The rendered results are shown in Figure 2.19.

Figure 2.19: An open tetrahedron drawn using a render object constructed using a different current color for each face.
Listing 5: Constructing a render object using current normals and colors.
   // add positions and normals
   int pi0 = robj.addPosition (0, 0, 0);
   int pi1 = robj.addPosition (1, 0, 0);
   int pi2 = robj.addPosition (0, 1, 0);
   int pi3 = robj.addPosition (0, 0, 1);
   // add three vertices per triangle, each with position and normal
   // information and color and texture coords undefined, and then
   // use these to define a triangle primitive
   int v0, v1, v2;
   // first triangle
   robj.addNormal (0, 0, -1);
   robj.addColor (Color.CYAN);
   v0 = robj.addVertex (pi0);
   v1 = robj.addVertex (pi2);
   v2 = robj.addVertex (pi1);
   robj.addTriangle (v0, v1, v2);
   // second triangle
   robj.addNormal (-1, 0, 0);
   robj.addColor (Color.WHITE);
   v0 = robj.addVertex (pi0);
   v1 = robj.addVertex (pi3);
   v2 = robj.addVertex (pi2);
   robj.addTriangle (v0, v1, v2);
   // third triangle
   robj.addNormal (0, -1, 0);
   robj.addColor (Color.MAGENTA);
   v0 = robj.addVertex (pi0);
   v1 = robj.addVertex (pi1);
   v2 = robj.addVertex (pi3);
   robj.addTriangle (v0, v1, v2);

2.4.3 Maintaining consistent attributes

As mentioned earlier, all vertices within a render object must have a consistent set of attributes. That means that if some vertices are defined with normals or colors, they all must be defined with normals or colors, even if it means giving some vertices “dummy” versions of these attributes for primitives that don’t need them.

Figure 2.20: A triangle with a separate red border.

For example, suppose we wish to create an oject that draws a triangular face surrounding by an outer border (Figure 2.20). One might write the following code to create and draw the render object:

import java.awt.Color;
import maspack.render.*;
import maspack.render.Renderer.Shading;
...
   RenderObject createRenderObj () {
      RenderObject robj = new RenderObject();
      // add vertices for outer border - created without a normal
      robj.vertex (-0.8f, -0.5f, 0);
      robj.vertex ( 0.8f, -0.5f, 0);
      robj.vertex ( 0.0f,  1.0f, 0);
      // add points for triangle - create with a normal
      robj.addNormal (0, 0, 1f);
      robj.vertex (-0.64f, -0.4f, 0);
      robj.vertex ( 0.64f, -0.4f, 0);
      robj.vertex ( 0.0f,   0.8f, 0);
      // add outer border using first three vertices
      robj.addLine (0, 1);
      robj.addLine (1, 2);
      robj.addLine (2, 0);
      // add triangle using next three vertices
      robj.addTriangle (3, 4, 5);
      return robj;
   }
   RenderObject myRob;
   public void render (Renderer renderer, int flags) {
      if (myRob == null) {
         myRob = createRenderObj ();
      }
      // draw border
      renderer.setShading (Shading.NONE); // turn off lighting
      renderer.setColor (Color.RED);
      renderer.setLineWidth (3);
      renderer.drawLines (myRob);
      // draw triangle
      renderer.setShading (Shading.FLAT); // reset shading
      renderer.setColor (Color.GRAY);
      renderer.drawTriangles (myRob);
   }

This creates a render object containing vertices for the border and triangle, along with the line and triangle primitives. Then render() first draws the border and the triangle, using the renderer’s drawLines() and drawTriangles() methods (described in Section 2.4.6). Because the border is drawn with lighting disabled, no normal is required and so its vertices are created without one. However, as written, this example will crash, because the triangle vertices do contain a normal, and therefore the border vertices must as well. The example can be fixed by moving the addNormal() call in front of the creation of the first three vertices, which will then contain a normal even though it will remain unused.

2.4.4 Adding primitives in “build” mode

The RenderObject can also be systematically constructed using a “build mode”, similar to the draw mode described in Section 2.3.4. Build mode can be invoked for any of the primitive types defined by DrawMode (Table 2.1).

Primitive construction begins with beginBuild(DrawMode) and ends with endBuild(). While in build mode, the application adds vertices using any of the methods described in the previous sections. Then, when endBuild() is called, the render object uses those those vertices to create the primitives that were specified by the mode argument of beginBuild(mode).

Listing 6 shows a complete example where build mode is used to create a RenderObject for a cylinder. In this example, we first reserve memory for the required attributes, vertices and triangles. This is not a required step, but does help with internal storage. Then, we use a triangle strip to construct the rounded sides of the cylinder, and triangle fans to construct the caps. When constructing the sides, we use vertex(px,py,pz) to create positions and vertices at the same time. Then when constucting the caps, we use addVertex(pidx) to add vertices that reuse the positions created for the sides (knowing that the position indices start at 0). The final cylinder is shown using flat shading in Figure 2.21.

Figure 2.21: Render object cylinder created in Listing 6.
Listing 6: Building a cylinder.
RenderObject cylinder = new RenderObject();
int nSlices = 32;
float height = 2;
// reserve memory
cylinder.ensurePositionCapacity(2*nSlices);  // top and bottom ring
cylinder.ensureNormalCapacity(nSlices+2);    // sides and caps
cylinder.ensureVertexCapacity(4*nSlices);    // top/bottom sides, top/bottom caps
cylinder.ensureTriangleCapacity(2*nSlices+2*(nSlices-2));  // sides, caps
// create cylinder sides
cylinder.beginBuild(DrawMode.TRIANGLE_STRIP);
for (int i=0; i<nSlices; i++) {
   double angle = 2*Math.PI/nSlices*i;
   float x = (float)Math.cos(angle);
   float y = (float)Math.sin(angle);
   cylinder.addNormal(x, y, 0);
   cylinder.vertex(x, y, height);  // top
   cylinder.vertex(x, y, 0);       // bottom
}
cylinder.endBuild();
// connect ends around cylinder
cylinder.addTriangle(2*nSlices-2, 2*nSlices-1, 0);
cylinder.addTriangle(0, 2*nSlices-1, 1);
// create top cap, using addVertex(pidx) to reuse positions that
// were added when building the sides
cylinder.beginBuild(DrawMode.TRIANGLE_FAN);
cylinder.addNormal(0,0,1);
for (int i=0; i<nSlices; i++) {
   cylinder.addVertex(2*i);    // even positions (top)
}
cylinder.endBuild();
// create bottom cap
cylinder.beginBuild(DrawMode.TRIANGLE_FAN);
cylinder.addNormal(0,0,-1);
cylinder.addVertex(1);
for (int i=1; i<nSlices; i++) {
   int j = nSlices-i;
   cylinder.addVertex(2*j+1);  // odd positions (bottom)
}
cylinder.endBuild();

2.4.5 Modifying render objects

Sometimes, an application will build a render object once, and then never change any of its attributes, vertices, or primitives. Such objects are called static, and are the most efficient for rendering purposes since their data only needs to be transmitted to the GPU once. After the renderer first draws the object (using any of the draw methods described in Section 2.4.6), it can continue to draw a static object as many times as needed without having to send more information to the GPU. (Note however that such objects can still be repositioned within the scene by adjusting the model matrix as described in Section 2.2.4). Therefore, applications should attempt to use static render objects whenever possible.

However, often it is necessary to modify a render object. Such modifications may take three forms:

  • Vertex changes involving changes to the vertex structure;

  • Primitive changes involving changes to the primitive structure;

  • Attribute changes involving changes to the attribute structure or the modification of existing attribute values.

Vertex changes occur whenever new vertices are added (using any of the add methods described in the previous sections), or the entire object is cleared using clearAll(). These generally require retransmission of the vertex and attribute information to the GPU.

Primitive changes occur when new points, lines or triangles are added, when all the existing primitives are cleared using clearPrimitives(), or clearAll() is called. Primitive changes generally require retransmission of primitive index data to the GPU.

Attribute changes occur when new attributes are added, existing attribute data is modified, or clearAll() is called. These may require retransmission of the attribute and vertex data to the GPU.

The need to modify existing attribute data often arises when the render object represents some entity that is changing over time, perhaps as the result of a simulation. For instance, if the object represents a deformable surface, the positions and normals associated with that surface will typically be time varying. There are two main ways to modify attribute data. The first is to call one of the render object’s methods for directly setting the attribute’s value,

void setPosition (int idx, float px, float py, float pz);
void setPosition (int idx, Vector3d pos);
void setPosition (int idx, float[] pos);    // set by reference
void setNormal (int idx, float nx, float ny, float nz);
void setNormal (int idx, Vector3d nrm);
void setNormal (int idx, float[] nrm);      // set by reference
void setColor (int idx, float r, float g, float b, float a);
void setColor (int idx, Color color);
void setColor (int idx, byte[] rgba);       // set by reference
void setTextureCoord (int idx, float tx, float ty);
void setTextureCoord (int idx, Vector2d xy);
void setTextureCoord (int idx, float[] xy); // set by reference

where idx is the index of the attribute. This will set the attribute’s value within its current group. As with the add attribute methods, those methods marked “set by reference” use the specified array to directly store the values within the attribute, so that later changes to the array’s values will also cause the attribute values to change. (However, if a non-referencing set method is subsequently called, the attribute will allocate its own internal storage, and the reference will be lost.)

This indicates the second way in which attribute data may be modified: if the value was last set using a reference-based add or set method, the attribute can be changed by directly changing the values of that array. However, when this is done, the render object has no way to know that the corresponding attribute data was modfied, and so the application must notify the object directly, using one of the methods

void notifyPointsModified();
void notifyNormalsModified();
void notifyColorsModified();
void notifyTextureCoordsModified();

To facilitate the detection of changes, each RenderObject maintains a set of “version” numbers for its attributes, vertices, and primitives, which get incremented whenever changes are made to these quantities. While applications typically do not need to be concerned with this version information, it can be used by renderer implementations to determine what information needs to be retransmitted to the GPU and when. Version numbers can be queried using the following methods:

int getPositionsVersion();      // positions were added or modified
int getNormalsVersion();        // normals were added or modified
int getColorsVersion();         // colors were added or modified
int getTextureCoordsVersion();  // texture coordinates were added or modified
int getVerticesVersion();       // vertices were added
int getPointsVersion();         // points were added
int getLinesVersion();          // lines were added
int getTrianglesVersion();      // triangles were added
int getVerstion();              // one or more of the above changes occured

2.4.6 Drawing RenderObjects

In addition to draw(RenderObject), a variety of other Renderer methods allow the drawing of different subsets of a render objects’s primitives. These include:

// draw all primitives in the first group for each:
void draw(RenderObject robj);
// draw all points in the first point group:
void drawPoints(RenderObject robj);
void drawPoints(RenderObject robj, PointStyle style, float rad);
// draw all line in the first line group:
void drawLines(RenderObject robj);
void drawLines(RenderObject robj, LineStyle style, float rad);
// draw all triangles in the first triangle group:
void drawTriangles(RenderObject robj);
// draw all vertices, using them to create implicit primitives according
// to the indicated mode:
void drawVertices(RenderObject robj, DrawMode mode);

Point, line and triangle groups are presented in Section 2.4.7. The method drawPoints(robj,style,rad) draws the indicated points using the specified PointStyle, with rad being either the pixel size or radius, as appropriate. Likewise, drawLines(robj,style,rad) draws the indicated lines using the specified style, with rad being either the line width (in pixels) or the cylinder/spindle/arrow radius.

A common reason for drawing different graphics primitives separately is so that they can be drawn with different settings of the graphics state. For example, Listing 7 creates a render object for a simple grid in the x-y plane, and the render method draws the points and lines in different colors. One way to do this would be to assign the appropriate colors to the vertices of all the point and line primitives. Another way, as done in the example, is to simply draw the points and lines separately, with different color settings in the graphics state (this also allows different colors to be used in subsequent renders, without having to modify the graphics object). The result in shown in Figure 2.22.

Figure 2.22: Grid render object drawn with red points and green lines.
Listing 7: Building and drawing render object for a grid.
import java.awt.Color;
import maspack.render.*;
...
   // Creates the render object for a grid in the x-y plane, with width w,
   // height h, and nx and ny points in the x and y directions
   RenderObject createGridObj (double w, double h, int nx, int ny) {
      RenderObject robj = new RenderObject();
      // create vertices and points ...
      for (int j=0; j<ny; j++) {
         for (int i=0; i<nx; i++) {
            float x = (float)(-w/2+i*w/(nx-1));
            float y = (float)(-h/2+j*h/(ny-1));
            int vi = robj.vertex (x, y, 0);
            robj.addPoint (vi);
         }
      }
      // create horizontal lines ...
      for (int j=0; j<ny; j++) {
         for (int i=0; i<nx-1; i++) {
            int v0 = j*nx + i;
            robj.addLine (v0, v0+1);
         }
      }
      // create vertical lines ...
      for (int i=0; i<nx; i++) {
         for (int j=0; j<ny-1; j++) {
            int v0 = j*nx + i;
            robj.addLine (v0, v0+nx);
         }
      }
      return robj;
   }
   RenderObject myRob; // store object between render calls
   public void render (Renderer renderer, int flags) {
      if (myRob == null) {
         myRob = createGridObj (2.0, 2.0, 5, 5);
      }
      // draw the points and lines using separate colors
      renderer.setColor (Color.RED);
      renderer.setPointSize (6);
      renderer.drawPoints (myRob);
      renderer.setColor (0f, 0.5f, 0f); // dark green
      renderer.setLineWidth (3);
      renderer.drawLines (myRob);
   }
Figure 2.23: Grid render object drawn with points as spheres and and lines as spindles.

The Renderer methods drawPoints(RenderObject,PointStyle,double) and drawLines(RenderObject,LineStyle,double), described above, can be particularly useful for drawing the points or lines of a render object using different styles. For example, the following code fragment draws the grid of Listing 7 with points drawn as spheres with radius 0.1 and lines drawn as spindles with radius 0.05, with the results shown in Figure 2.23.

   renderer.setColor (Color.RED);
   renderer.drawPoints (robj, PointStyle.SPHERE, 0.10);
   renderer.setColor (0f, 0.5f, 0f); // dark green
   renderer.drawLines (robj, LineStyle.SPINDLE, 0.05);

2.4.7 Multiple primitive groups

The RenderObject can have multiple groups of a particular primitive type. This is to allow for separate draw calls to render different parts of the object. For example, consider a triangular surface mesh consisting of a million faces that is to be drawn with the left half red, and the right half yellow. One way to accomplish this is to add a vertex color attribute to each vertex. This will end up being quite memory inefficient, since the renderer will need to duplicate the color for every vertex in the vertex buffer. The alternative is to create two distinct triangle groups and draw the mesh with two draw calls, changing the global color between them. New primitive groups can be created using the methods

int createPointGroup();
int createLineGroup();
int createTriangleGroup();

Each of these creates a new group for the associated primitive type, sets it to be the current group, and returns an index to it.

A group for a particular primitive type is created automatically, if necessary, the first time an instance of that primitive is added to a render object.

Once created, the following methods can be used to set and query the different primitive groups:

int numPointGroups();       // number of point groups
void pointGroup (gi);       // set current point group to gi
int getPointGroupIdx();     // get index of current point group
int numLineGroups();        // number of line groups
void lineGroup (gi);        // set current line group to gi
int getLineGroupIdx();      // get index of current line group
int numTriangleGroups();    // number of triangle groups
void triangleGroup (gi);    // set current triangle group to gi
int getTriangleGroupIdx();  // get index of current triangle group

Another set of methods can be used to query the primitives within a particular group:

int numPoints (gi);         // number of points in group gi
int[] getPoints (gi)        // get vertex indices for points in group gi
int numLines (gi);          // number of lines in group gi
int[] getLines (gi)         // get vertex indices for lines in group gi
int numTriangles (gi);      // number of triangles in group gi
int[] getTriangles (gi)     // get vertex indices for triangles in group gi

Finally, the draw primitives described in Section 2.4.6 all have companion methods that allow the primitive group to be specified:

// draw all points in point group gidx:
void drawPoints(RenderObject robj, int gidx);
void drawPoints(RenderObject robj, int gidx, PointStyle style, float r);
// draw all line in line group gidx:
void drawLines(RenderObject robj, int gidx);
void drawLines(RenderObject robj, int gidx, LineStyle style, float rad);
// draw all triangles in triangle group gidx:
void drawTriangles(RenderObject robj, int gidx);

To illustrate group usage, we modify the grid example of Listing 7 so that the vertical and horizontal lines are each placed into different line groups:

   // create horizontal lines inside first line group ...
   createLineGroup();
   for (int j=0; j<ny; j++) {
      for (int i=0; i<nx-1; i++) {
         int v0 = j*nx + i;
         robj.addLine (v0, v0+1);
      }
   }
   // create vertical lines inside second line group ...
   createLineGroup();
   for (int i=0; i<nx; i++) {
      for (int j=0; j<ny-1; j++) {
         int v0 = j*nx + i;
         robj.addLine (v0, v0+nx);
      }
   }

Once this is done, the horizontal and vertical lines can be drawn with different colors by drawing the different groups separately:

Figure 2.24: Grid render object created with two line groups, with one drawn in green and the other in yellow.
   renderer.setColor (Color.RED);
   renderer.drawPoints (grid, PointStyle.SPHERE, 0.10);
   renderer.setColor (0f, 0.5f, 0f); // dark green
   // draw lines in first line group
   renderer.drawLines (grid, 0, LineStyle.SPINDLE, 0.05);
   renderer.setColor (Color.YELLOW);
   // draw lines in second line group
   renderer.drawLines (grid, 1, LineStyle.SPINDLE, 0.05);

The results are show in Figure 2.24.

As noted in Section 2.4.5, it is possible to clear all primitives using clearPrimitives(). This will clear all primitives and their associated groups within the render object, while leaving vertices and attributes alone, allowing new primitives to then be constructed.

2.4.8 Drawing primitive subsets

In some circumstances, it may be useful to draw only a subset of the primitives in a render object, or to draw a subset of the vertices using a specified primitive type. There may be several reasons why this is necessary. The application may wish to draw different primitive subsets using different settings of the graphics context (such as different colors). Or, an application may use a single render object for drawing a collection of objects that are individually selectable. Then when rendering in selection mode (Section 2.7), it is it is necessary to render the objects separately so that the selection mechanism can distinquish them.

The two renderer methods for rendering primitive subsets are:

   void drawVertices (
      RenderObject robj, VertexIndexArray idxs, DrawMode mode);
   void drawVertices (
      RenderObject robj, VertexIndexArray idxs,
      int offset, int count, DrawMode mode);

Each of these draws primitive subsets for the render object robj, using the vertices specified by idxs and the primitive type specified by mode. VertexIndexArray is a dynamically-sized integer array specialized for vertices. The second method allows a subset of idxs to be specified by an offset and count, so that the same index array can be used to draw different features.

The following example creates a render object containing three squares, and then uses drawVertices() to render each square individually using a different color:

Listing 8: Example of rendering separate features from the same render object.
   RenderObject myRob;
   VertexIndexArray myIdxs;
   void addSquare (
      RenderObject robj, VertexIndexArray idxs,
      float x0, float y0, float x1, float y1,
      float x2, float y2, float x3, float y3) {
      // create the four vertices uses to define the square, based on
      // four points in the x-y plane
      int v0 = robj.vertex (x0, y0, 0);
      int v1 = robj.vertex (x1, y1, 0);
      int v2 = robj.vertex (x2, y2, 0);
      int v3 = robj.vertex (x3, y3, 0);
      // use these vertices to define the four line segments needed
      // to draw the square
      robj.addLine (v0, v1);
      robj.addLine (v1, v2);
      robj.addLine (v2, v3);
      robj.addLine (v3, v0);
      // and add the vertex indices for the line segments to the vertex
      // index array
      idxs.add (v0); idxs.add (v1);
      idxs.add (v1); idxs.add (v2);
      idxs.add (v2); idxs.add (v3);
      idxs.add (v3); idxs.add (v0);
   }
   RenderObject createRenderObj (VertexIndexArray idxs) {
      // create a render object consisting of three squares, along with
      // an index array to keep track of all the vertices
      RenderObject robj = new RenderObject();
      addSquare (robj, idxs, -4f, -1f, -2f, -1f, -2f,  1f, -4f,  1f);
      addSquare (robj, idxs, -1f, -1f,  1f, -1f,  1f,  1f, -1f,  1f);
      addSquare (robj, idxs,  2f, -1f,  4f, -1f,  4f,  1f,  2f,  1f);
      return robj;
   }
   public void render (Renderer renderer, int flags) {
      if (myRob == null) {
         // create render object and index array on demand
         myIdxs = new VertexIndexArray();
         myRob = createRenderObj (myIdxs);
      }
      // render each square separately using a different color
      renderer.setLineWidth (4);
      renderer.setColor (Color.RED);
      renderer.drawVertices (myRob, myIdxs,  0, 8, DrawMode.LINES);
      renderer.setColor (Color.GREEN);
      renderer.drawVertices (myRob, myIdxs,  8, 8, DrawMode.LINES);
      renderer.setColor (Color.BLUE);
      renderer.drawVertices (myRob, myIdxs, 16, 8, DrawMode.LINES);
   }

Each square is added to the render object using the method addSquare(), which creates and adds the necessary vertices and line segments, and also stores the line segment vertex indices in an index array. The render() method then uses subsets of this index array, specified by the offset/length pairs (0,8), (8,8), and (16,8), to render each square individually (using a different color) via a call to drawVertices(). The result is shown in Figure 2.25.

Figure 2.25: Three squares drawn individually from the same render object.

In the above example, each square uses the same number of vertices (8) to draw its line segments, making it easy to determine the offset/length pairs required for each square. However, in more general cases a render object may contain features with variable numbers of vertices, and so determining the offset/length pairs may be more difficult. In such cases the application may find it useful to instead collect the vertex indices inside a FeatureIndexArray, which allows them to be grouped on a per-feature basis, with each feature identified by a number. The usage model looks like this:

   FeatureIndexArray fidxs = new FeatureIndexArray();
   ...
   for (each feature fnum) {
      fidxs.beginFeature (fnum);
      for (each feature vertex vidx) {
         fidxs.add (vidx);
      }
   }
   fidxs.endFeature();

After the feature index array has been built, the vertex index buffer can be obtained using fidxs.getVertices(), and the feature number and offset/length pair for each feature can be recovered using

   int fnum = fidxs.getFeature (fidx);
   int offset = fidxs.getFeatureOffset (fidx);
   int length = fidxs.getFeatureLength (fidx);

where fidx is the index of the feature within the FeatureIndexArray. In some situations the feature number fnum and fidx may be the same, but in others they may be different. For example, each feature may be associated with an application component that has a unique number used for selection (Section 2.7), in which case the feature number can be set to the selection number.

The three squares example of Listing 8 can be reimplemented using FeatureIndexArray as follows:

Listing 9: Primitive subset rendering using a FeatureVertexArray.
   RenderObject myRob;
   FeatureIndexArray myFidxs;
   void addSquare (
      RenderObject robj, FeatureIndexArray fidxs, int fnum,
      float x0, float y0, float x1, float y1,
      float x2, float y2, float x3, float y3) {
      // create the four vertices uses to define the square, based on
      // four points in the x-y plane
      int v0 = robj.vertex (x0, y0, 0);
      int v1 = robj.vertex (x1, y1, 0);
      int v2 = robj.vertex (x2, y2, 0);
      int v3 = robj.vertex (x3, y3, 0);
      // use these vertices to define the four line segments needed
      // to draw the square
      robj.addLine (v0, v1);
      robj.addLine (v1, v2);
      robj.addLine (v2, v3);
      robj.addLine (v3, v0);
      // and add the vertex indices for the line segments to the feature
      // index array for the feature identified by fnum:
      fidxs.beginFeature (fnum);
      fidxs.addVertex (v0); fidxs.addVertex (v1);
      fidxs.addVertex (v1); fidxs.addVertex (v2);
      fidxs.addVertex (v2); fidxs.addVertex (v3);
      fidxs.addVertex (v3); fidxs.addVertex (v0);
      fidxs.endFeature();
   }
   RenderObject createRenderObj (FeatureIndexArray idxs) {
      RenderObject robj = new RenderObject();
      addSquare (robj, idxs, 0, -4f, -1f, -2f, -1f, -2f,  1f, -4f,  1f);
      addSquare (robj, idxs, 1, -1f, -1f,  1f, -1f,  1f,  1f, -1f,  1f);
      addSquare (robj, idxs, 2,  2f, -1f,  4f, -1f,  4f,  1f,  2f,  1f);
      return robj;
   }
   RenderObject myRob;
   FeatureIndexArray myFidxs;
   void drawSquare (
      Renderer renderer, RenderObject robj,
      FeatureIndexArray fidxs, int fidx) {
      // draw a single square using the vertex indices associated
      // with the feature fidx
      renderer.drawVertices (
         robj, fidxs.getVertices(),
         fidxs.getFeatureOffset(fnum), fidxs.getFeatureLength(fnum),
         DrawMode.LINES);
   }
   public void render (Renderer renderer, int flags) {
      if (myRob == null) {
         // create render object and feature index array on demand
         myFidxs = new FeatureIndexArray();
         myRob = createRenderObj (myFidxs);
      }
      // render each square separately using a different color
      renderer.setLineWidth (4);
      renderer.setColor (Color.RED);
      drawSquare (renderer, myRob, myFidxs, 0);
      renderer.setColor (Color.GREEN);
      drawSquare (renderer, myRob, myFidxs, 1);
      renderer.setColor (Color.BLUE);
      drawSquare (renderer, myRob, myFidxs, 2);
   }