Triangle Consolidator 1.1

Triangle Consolidator 1.1


I imagine most people would only have come looking for TC if they knew what they wanted in the first place. The programmers' guide that you probably want is in the next section. The following is just a little purpose and history.

TC, or "triangle consolidator", constructs triangle strips or triangle fans from independent triangle data. The geometric transformation of 3D points onto a 2D screen involves some heavy math, and you can realize a performance benefit by reusing the results of that math where possible. Each new triangle strip or fan is constructed from one of the edges of the previous triangle and a new vertex.

You can achieve 3x performance minus the cost of transforming the first two vertices per strip. Thus it's important that strips and fans are made as long as possible (fewer strips and fans). Some architectures have a maximum preferred strip length, after which the engine may stall.

I wrote meshifier in 1995 because Sharon Clay at Silicon Graphics asked me how I might construct strips from a lattice of triangles as an interview question. It has some fundamental architectural limitations. Some, like an error in the reset code and being able to specify that triangles should not be output in reverse order, have been fixed by Neal Tringham and are incorporated in the new version available from his web page. Others, such as the limitation on numbers of vertices and connectivity, are harder to repair.

In "TC" I've attempted to fix those old errors. TC attempts to consolidate triangles limited only by the limits of the machine' available memory and speed. I have changed the API, using OpenGL as a loose model (although OpenGL arguably has its shape because it provides a hardware abstraction layer), so that more algorithmic abstractions and optimizations are possible. I have tried to build in features which are useful to modelers and offline optimization tools, such as whether or not to honor vertex ordering. I have also spent a few days trying to test the code nominally and attempt to have some confidence that even its low-memory behavior is predictable.

I hope you find it useful. ---Brad Grantham

Triangle Consolidator API

TC is meant to be used in an offline modeling process. It is probably too slow to be used to optimize data per frame for the purpose of interactive display. It also does not provide any facility (yet) for merging spacial coordinates together to form an indexed vertex list. TC only operates on the indices of indexed triangles. Maintaining the actual vertex array (containing the traditional v3f, n3f, etc) is up to the application. I perceive this is useful, however, in that TC needs no knowledge of how vertex data is stored; it only needs to know connectivity information. An application program creates an ACTCData structure and then repeats a cycle of optionally setting flags, inputting triangles, and then receiving the output primitives.

Language and Namespace

TC 1.1 is provided with a C language interface. Mostly this is because many people (including me) still prefer to program in C and I don't want to force people to write C++. TC is also implemented in C, but I don't think this is that important, since you could implement the library internals in C++ and still export a C API.

The namespace of all functions and identifiers in TC is protected using the initials of my software organization Applied Conjecture and TC. All functions in TC are prefixed with actc. All symbolic constants and #defines are prefixed with ACTC_. All exported data structures are prefixed with ACTC.

Initialization and Errors

An ACTCData consolidator object is created using the following code. This structure contains state data about optional parameters and triangle connectivity. actcNew returns either an initialized ACTCData object if successful or NULL if memory cannot be allocated.

    #include <ac/tc.h>

    ACTCData *tc;

    tc = actcNew();
    if(tc == NULL) {
	/* memory allocation failed */
	/* print error here and exit or whatever */
Any errors encountered during execution are typically both returned as results from a TC function and stored in the structure. The error can be retrieved using actcGetError. Here is an example of adding a triangle and checking the error.

    actcAddTriangle(tc, v1, v2, v3);
    if((e = actcGetError(tc)) != ACTC_NO_ERROR) {
	/* something bad happened */
	/* print error and exit or whatever */
A more compact representation may not use actcGetError at all, since many functions return either the error or ACTC_NO_ERROR.

    if((e = actcAddTriangle(tc, v1, v2, v3)) != ACTC_NO_ERROR) {
	/* something bad happened */
	/* print error and exit or whatever */
Since the error is reset in the structure by actcGetError, it makes sense to go with either one style or the other, to avoid being confused when a later return from actcGetError returns an error which has already been handled. Errors are represented by negative integers, so some functions may return non-error information as a positive integer. Slightly smarter applications may try to detect some failure conditions and proceed after some kind of repair.

    actcAddTriangle(tc, v1, v2, v3);
    e = actcGetError(tc);
    if(e == ACTC_ALLOC_FAILED) {
	/* couldn't alloc memory using one of the malloc(3) family */
	/* application can free unused memory and try again */
    } else if(e != ACTC_NO_ERROR) {
	/* something bad happened other than malloc failure */
	/* print error and exit or whatever */
For simplicity, examples in the remainder of the programmers' guide will not check for errors but will mention where errors may occur. Several functions can result in the error ACTC_DATABASE_CORRUPT but are not noted here; that particular error return indicates corruption of ACTC's internal data structures and really requires debugging (as described below).


There are several options that may be set in the TC structure. For example, whether TC honors triangle vertex ordering is controlled by the ACTC_OUT_HONOR_WINDING symbolic constant. These values are set by actcParami and retrieved by actcGetParami.

    int isHonoringWinding;

    actcGetParami(tc, ACTC_OUT_HONOR_WINDING, &isHonoringWinding);
actcGetParami accepts two additional symbolic constants which may not be set by the application. ACTC_MAJOR_VERSION and ACTC_MINOR_VERSION return the major version number and minor version number, respectively.

Parameters are differentiated by the direction of the data they control. OUT parameters control how TC hands its consolidated primitives back to you. IN parameters tell TC the limits you intend to guarantee on your input data, which may used for optimization purposes. ALG parameters directly affect the internal algorithms used by TC. Some parameters require the extended range of unsigned int, and they can be set and queried by actcParamu and actcGetParamu, but both sets of Param... functions take the same symbolic constants and convert to-and-from the range of their parameter or return variable. Here are the parameters available in TC 1.1:

EnumerantData typeMeaningPrefer Paramu
ACTC_OUT_MIN_FAN_VERTSintegerMinimum number of vertices returnable in a triangle fanNo
ACTC_OUT_HONOR_WINDINGbooleanWhether triangles may be output in reverse vertex orderNo
ACTC_OUT_MAX_PRIM_VERTSintegerMaximum number of vertices to output in a primitiveNo
ACTC_IN_MIN_VERTintegerMinimum vertex indexYes
ACTC_IN_MAX_VERTintegerMaximum vertex indexYes
ACTC_IN_MAX_VERT_SHARINGintegerMaximum number of edges that will share a vertexNo
ACTC_IN_MAX_EDGE_SHARINGintegerMaximum number of triangles that will share an edgeNo

If called during INPUT or OUTPUT, actcParami can result in ACTC_DURING_INPUT, or ACTC_DURING_OUTPUT, respectively. If called to set ACTC_IN_MIN_VERT or ACTC_IN_MAX_VERT so the vertex range is negative, or to set ACTC_OUT_MAX_PRIM_VERTS to less than 3, or to set a value associated with a symbolic constant ACTC does not know about, then the error ACTC_INVALID_VALUE will result.


A TC structure is always in one of three modes; idle, inputting, or outputting. Some operations are valid only in some modes. For example, setting a parameter is an error unless the consolidator object is idle. Triangle data may only be entered in input mode. Primitives may only be fetched in output mode. Here's an example of how the modes work. Please keep in mind that this example assumes a particular primitive.

    actcParami(tc, ACTC_OUT_MAX_PRIM_VERTS, 14);
    actcAddTriangle(tc, 1, 2, 3);
    prim = actcStartNextPrim(tc, &v1, &v2);
    actcGetNextVert(tc, &v3);
    /* v1, v2, and v3 are now set to 1, 2, and 3. */
actcEndInput and actcEndOutput are intended as checkpoints for optimization, compaction, etc. Your application can call actcGetIsDuringInput and actcGetIsDuringOutput to determine if the TC object is currently ready in the input or output stages (or is otherwise idle). If actcParami is called during input or output, an error of either ACTC_DURING_INPUT or ACTC_DURING_OUTPUT results, respectively.

Giving Triangles as Input

Each triangle is entered into the TC database using actcAddTriangle, and the possible results may be ACTC_DURING_OUTPUT if TC is in output mode, ACTC_IDLE if TC is not in input mode, or ACTC_ALLOC_FAILED if an internal data structure could not be allocated. In the last case, the database is not altered. An application could perform compaction on its own data and try actcAddTriangle again, or it could attempt to retrieve all the primitives currently available from the database. This would probably give worse results (more primitives, fewer vertices per primitive) than retrieving primitives from the complete database but may be better than getting no primitives at all.

     int triCount;
     int tris[][3]; 

     for(i = 0; i < triCount; i++)
	 actcAddTriangle(tc, tris[i][0], tris[i][1], tris[i][2]);

Ranges of Various Input Data

Vertex indices can be any unsigned int, and so anywhere from 0 to UINT_MAX, inclusive. Internal data structures allow quantity INT_MAX vertices, edges, triangles, vertex valences, and edge valences to be input. With the data structures TC uses as of version 1.1, on most machines at the time of writing, a process' memory space will be exhausted before the range of any of these items is exhausted. As of version 1.1, TC has not been tested on an architecture with integers or pointers larger that 32 bits.

Getting the Output Primitives

Strips and fans are retrieved one at a time from TC starting with actcStartNextPrim. The primitive type is returned, either ACTC_PRIM_STRIP or ACTC_PRIM_FAN. actcStartNextPrim may also return ACTC_DATABASE_EMPTY if all triangles input so far have been returned in primitives. actcStartNextPrim stores the first two vertices by reference, and actcGetNextVert provides each subsequent vertex. actcGetNextVert will return ACTC_PRIM_COMPLETE when their are no more vertices to return.
     int prim;
     int v1, v2;

     while((prim = actcStartNextPrim(tc, &v1, &v2) != ACTC_DATABASE_EMPTY) {
	 /* start a primitive of type "prim" with v1 and v2 */
	 while(actcGetNextVert(tc, &v3) != ACTC_PRIM_COMPLETE)
	     /* continue primitive using v3 */

One-Stop-Shopping (Convenience Function)

There is a single function called actcTrianglesToPrimitives which inputs triangles and extracts primitives, including attempting to recover from low-memory situations by first draining the primitive database, and then by cleaning up unused memory if draining is not sufficient. Since TC cannot know in advance how many primitives it can find (and thus how many vertices), the application will have to provide primitive storage large enough to hold the input data as independent triangles. The application may also tell actcTrianglesToPrimitives how many triangles to input to TC before draining the database. The following example shows how an application might call actcTrianglesToPrimitives; here the application has provided INT_MAX as the maximum batch size in triangles, which effectively means input all the triangles first before outputting.

    int triangleCount;
    int triangles[][3];
    int *primLengths;
    int *primVerts;
    int *primCount;

    /* get triangleCount and triangles from somewhere */
    primLengths = (int *)malloc(sizeof(int) * triangleCount);
    primTypes = (int *)malloc(sizeof(int) * triangleCount);
    primVerts = (int *)malloc(sizeof(int) * 3 * triangleCount);
    primCount = actcTrianglesToPrimitives(tc, triangleCount, triangles,
	primTypes, primLengths, primVerts, INT_MAX);
    if(primCount < 0) {
	/* something bad happened */
	/* print error and exit or whatever */
actcTrianglesToPrimitives calls actc{Begin,End}{Input,Output}, actcAddTriangle, actcGetNext{Prim,Vertex}, and actcMakeEmpty. It returns any unrecoverable error it may receive from these functions, or it will return the total number of strips and fans it finds. The types and length in vertices of all primitives are stored in primTypes and primLengths, and then the vertices for each primitive in turn are stored sequentially in primVerts.

Memory Usage Statistics

Call actcGetMemoryAllocation to make TC search how much memory is being used in data structures. This may be an expensive operation in TC 1.1 in that it searches the allocated data structures.

Dumping Internal State

See below for information on compiled TC so that it automatically dumps debugging information on an internal error. If you believe you are getting incorrect results from TC, your application can call actcDumpState to print out the internal state of a TC object (in no particular format). In order for me to help you debug a problem, it would be useful to print out the TC version number, the triangles you input to TC (in the order you input them), and the output of actcDumpState at the beginning and end of your output phase. actcDumpState has no return value.

Cleaning Up

To stop input or output with TC and empty the database for a particular TC object, use actcMakeEmpty; the TC object will be reset to idle, contain no triangles, and will be ready for another set of input triangles. actcMakeEmpty returns any error that occurs during cleanup, but for the 1.1 release it can only return ACTC_NO_ERROR.

To free the memory associated with a TC object, call actcDelete with the pointer to the object; this function effectively calls actcMakeEmpty and then frees the TC internal data. Further use of that TC object pointer will have undefined results but will probably crash your application.

Specific Solutions

Producing Only Strips

To produce only triangle strips, set ACTC_OUT_MIN_FAN_VERTS to INT_MAX. Technically, a very large triangle fan could still be formed, but when someone creates a two billion vertex triangle fan with TC, I will consider it a success rather than a failure.

    actcParami(tc, ACTC_OUT_MIN_FAN_VERTS, INT_MAX);

Producing Limited-Length Primitives

I have heard that Silicon Graphics' RealityEngine graphics and derivatives have an optimium strip size of 12 vertices. Set ACTC_OUT_MAX_PRIM_VERTS to set the maximum number of vertices to produce per primitive.

    actcParami(tc, ACTC_OUT_MAX_PRIM_VERTS, 12);

Ignoring Triangle Winding

If your triangle set does not have a preferred triangle winding, set ACTC_OUT_HONOR_WINDING to FALSE or 0. This has the effect of allowing a triangle to be included in a primitive even if its vertices would be drawn in reverse order. Note that every odd triangle in a strip (counting from 0) will normally be wound backwards if winding is honored.



Building TC

My preference for using this module is to compile tc into a .a library. The makefile provided by default has targets for the library and the sample programs:
    make libactc.a
To turn on some simple debugging assistance, compile TC with the DEBUG preprocessor symbol defined (-DDEBUG for most UNIX compilers). Error conditions should print the reason and the function in which the error occurred. Any function finding an unexpected inconsistency in data structures (like performing a table search and not finding an element which must be there) will call abort() and presumably dump core. I wouldn't expect any ordinary application developer to be able to debug TC from a core file, of course. If INFO is also defined, then the abort() call will be preceded by a dump of the internal data structures in tc. If you would like me to debug TC based on your input, it would help me to have this output as well as the set of triangles originally input to TC.

Sample Programs and Tests

The actctest program makes sure the basic TC functions work. If it prints anything when compiled with actc.c without DEBUG and INFO, then something bad happened.

The actctest2 program repeatedly inputs random data into TC and makes sure it gets the same thing out as it received. You can specify settings for any of the OUT parameters, and change the seed that actctest2 uses to initialize the random number generator.

The actcsample program takes in a particular word-based format containing vertices and triangles, and converts that data into strips and fans. It passes the vertex list through verbatim. Someone could use this either as an element in a tool pipeline or as the basis for a new offline or interactive stripping tool. Here's what the input format looks like:

    [x y z nx ny nz r g b]
    [x y z nx ny nz r g b]
    triangle v1 v2 v3 end
    [triangle v1 v2 v3 end]
In this example, x, y, and z are the vertex coordinates, nx, ny, and nz are the vertex normals, and r, g, and b are the vertex colors. The set of vertices are terminated by the word end. Vertices are optional and are only passed straight through. For each triangle, v1, v2, and v3 are the indices of each vertex in the triangle. Each triangle is terminated by the word end, and the list of triangles is terminated by end. The output format is similar, except that the triangle section is replaced by a list of primitives. Here's what the output format looks like:
    [x y z nx ny nz r g b]
    [x y z nx ny nz r g b]
    strip v1 v2 v3 [v4 [v5 [...]]] end
    fan v1 v2 v3 [v4 [v5 [...]]] end


TC should be pretty portable, for the most part. I placed my "timer" snippet in actctest2, and it will need tweaking on anything but Linux, IRIX, and Windows 98. If you'd like to submit a patch to "timer" for another operating system, I will place it on my web page and incorporate it into TC.



I've spent some time testing TC against some strange triangle sets. I've gone so far as to create random triangle data (random numbers of triangles with random ranges of vertices), put it in to TC, and check to make sure the same triangles come out as went in. I've even gone as far as to artificially reduce the amount of memory available to 8M, under which TC seems to reliably consolidate triangles, but the data set may degenerate under such conditions to being exactly the input data.

Runtime Performance

TC 1.1 looks like it can consolidate somewhere around one hundred thousand triangles per second on a 400 MHz Pentium III for data sets in the thousands of vertices, and somewhere around thirty thousand triangles per second for data sets around three hundred thousand vertices. I suspect this could be improved only a little bit more with some clever avoidance of searching the vertex-to-edge mapping. Most of the data structures internal to TC are constant-time operations, especially if the vertex range is small.

I have tested TC's performance against an optimized meshifier modified by Randall Frank at Lawrence Livermore National Labs and meshifier appears to run about twice as fast, probably because it doesn't check for errors internally very well and makes some assumptions about vertex and edge connectivity. Perhaps TC could approach that performance level if the internal consistency checks were made to compile conditionally.

Stripping Performance

TC uses a least-connected-vertex approach to making triangle strips, under the assumption that such vertices will be at the corners of patches of connected triangles, and the stripification algorithm will hopefully peel triangle strips off the outside of the patch first, in order to avoid fragmenting the triangle set. This is the same approach Neal Tringham's improved meshifier used. I have yet to test TC's vertices-per-strip performance against other triangle strippers, but it should do at least as well as meshifier.

The algorithm attempts to remove all the triangle fans first. I've found that this works for fewer than half of the simple data files I've tried. For 1.1 the default value associated with ACTC_OUT_MIN_FAN_VERTS is set to INT_MAX. Other good values appear to be near 8 and 16. You'll have to try it yourself to see how it works with your data.

One could conceivably implement simulated annealing with TC. An application would repeatedly calculate solutions to triangle sets with TC, swapping the order of some number of triangles, each time choosing the better solution. As time goes on, the number of triangles rotated could be reduced. Simulated annealing will typically give a pretty good solution while allowing the application to choose how much time it cares to spend to find a solution.

Revision Control Information: $Header: /home/grantham/cvsroot/projects/web/actc/manual.html,v 1.5 2000/11/22 20:19:36 grantham Exp $