Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Design

RaveKit: A Portable Graphics Framework


Dr. Dobb's Journal July 1997: RaveKit: A Portable Graphics Framework

Mark is principal developer at HeadsOff Software and can be contacted at [email protected].


Just as text-based terminals were an improvement over punch cards and Xerox-inspired GUIs were an easier interface for casual users, recent trends suggest a transition to 3-D depth-sorting to break down the complexity of multi-windowed applications. In this article, I present a general-purpose, cross-platform framework called "RaveKit" for creating interactive 3-D environments that provide an alternative way of presenting graphical data and obtaining user input. RaveKit demonstrates how you can use a multiplatform compiler (Metrowerks CodeWarrior Gold) to create a framework for producing high-resolution interactive 3-D graphics -- without being tied to any particular commercially available renderer (QuickDraw 3D, DirectX, Renderware, and the like). I implement this renderer-independent strategy by deriving from a generic Renderer class and implementing the specifics in the subclass. The actual 3-D transformations and projections onto 2-D space are done on a vertex-by-vertex basis by the class Vector3D, while the prerasterizing processing of the output triangles is handled by a linked list of Tri3D objects that reference the vertexes of the world through indexes.

The RaveKit Framework

One of the purposes of the RaveKit framework (available electronically; see "Availability," page 3) is to provide a simple API for 3-D programming without being tied to any particular renderer. To this end, I provide a Renderer-derived class, called Raver, which uses Apple's RAVE API along with the default built-in RAVE engine. RAVE is the independent "immediate-mode" rasterizing module and Hardware Abstraction Layer (HAL) used by QuickDraw 3D.

The primary difference between "retained mode" and "immediate mode" is the amount of work left to the programmer. Retained mode provides storage of your 3-D world, shape generation, lighting, and so on, while immediate mode simply rasterizes the triangles you send it. This rule is not rigid, however. For example, Direct3D immediate mode offers lighting and a few other services, while RAVE tends to stick to the bare essentials. While retained mode, with its larger feature list, is probably more appropriate to a 3-D scene builder or VRML designer/player, immediate mode offers the compactness and speed that is more appropriate to games or plug-in software.

The RAVE HAL is freely available from Apple (http://www.quickdraw3d.apple .com/) and it runs on PowerPC MacOS and 32-bit Windows. One of the most attractive features of RAVE is that it also serves as a standard 3-D API, regardless of the rasterizing software. This means third parties can provide an alternative RAVE engine while maintaining a consistent API. The RAVE documentation (available from Apple as a PDF document) provides specifications for creating your own engine, along with recommendations on which facilities must be provided and which are optional. For 3-D software and content developers, this removes the pain of having to learn a different API for each rendering engine. For engine developers, it offers a method of packaging a hardware-accelerated engine in a way that can be easily evaluated and incorporated into RAVE-savvy applications (including QuickDraw 3D).

Dropping in your favorite immediate-mode rendering library (as an alternative to RAVE) simply requires deriving a class from Renderer that implements the specifics of your renderer. For example, if Windows is your only required platform, you could call up the Direct3D immediate-mode API (which also offers rasterizing and HAL services, and is freely distributable). I have used triangles, as they are the most widely used primitive in commercial renderers. With some modification (mostly relating to clipping and depth sorting), you could incorporate quadrilaterals, ellipsoids, or other types as primitives. The heart of the rasterizing process occurs in the derived class's TimeSlice() function. TimeSlice() is passed an unsigned long containing bit values for any key combination returned from the Scanner class in main(). Scanner is a cross-platform asynchronous keyboard-scanning module, a frequently used tool in my class-library package. Note in Listing One hat the RAVE calls are preceded by "QA," while RAVE data types are preceded by "TQA." Similar to the Direct3D COM interface, RAVE calls are implemented as macros that refer to the virtual functions within the various modules. The macros also act as a protection for the programmer against future modifications to the vtable structures.

Compared to RAVE, Direct3D immediate mode offers more services (for example, lighting and clipping). RAVE, on the other hand, offers the bare minimum, but is a much simpler API. Both make extensive use of floating-point arithmetic, and therefore require either the PowerPC (in the case of RAVE on MacOS) or Pentium on Win32 (not compulsory, but advisable). Both also respond well to hardware acceleration. A test I did on the program presented here on a prerelease Mac with the ATI RAGE chipset on the motherboard produced startling performance at 640×480×16, even in RaveKit's current unoptimized format.

The Vertex3D class (Listing Two) does a lot of the useful work and helps to delineate the abstract "world" rendering stage (done in Renderer) from the viewport projection stage. Having this clear demarcation between stages will make it much easier to incorporate extra functionality later.

The projection stage, handled by each of the vertex objects (which are actually aliases of Vector3D objects), is one of the most arbitrary in the process. Until everyone has PCs with hardware on par with a dedicated graphics workstation, real-time 3-D is the art of the possible. As programmers and designers, we are not necessarily concerned with physically accurate representations of reality, but are looking for a way to present our concepts as faithfully as resources allow. This usually means a constantly shifting compromise between ease-of-use, accuracy, detail, and frame rate. With RaveKit, I focus on ease-of-use and detail. The default pixel resolution is 640×480 with a 16-bit color depth. 16-bit color is, incidentally, RAVE's home turf.

The Vertex3D object also plays a part in the world transformation stage by transforming its own world coordinates according to instructions passed from the Renderer class's MoveWorld() function; see Listing Three.

One obvious optimization to be done in the Vector3DTransform stage (Listing Four) is to create lookup tables for the sine and cosine values that are required if any rotations are necessary. This is straightforward, as there are currently only three possible increment values for interactive Roll, Pitch, and Yaw. For this article, however, I haven't bothered, because, with the relatively small number of vertices in this world, there is little visible difference. (On the Mac, I've used "LibMoto," Motorola's optimized PowerPC math library, which probably helps minimize the difference.)

For storing all the required vertices in the "solid" part of the world (that is, the vertices that move as one unit), I've used an array, allowing fast lookup from the linked list of Tri3D objects, which store the triangles as indices into the storage array (Listing Five). In a more demanding application, this array would need dynamic-sizing capability in, say, 1-KB chunks.

For demonstration, I've included a rudimentary mesh builder that allows an initial transformation and translation of a four-sided "panel" and a simple DXF importer. The DXF importer works on the POLYLINE/VERTEX combination (with the mesh specification) on DXF files exported from my little Swivel3D program, but you may need to tune it a little to read alternative DXF configurations. Since this program deals with triangles, it shouldn't be too difficult to get graphic data from other programs into this format.

RaveKit Enhancements

Adding a few bells and whistles to the RaveKit mini-renderer isn't difficult. I've simulated basic static lighting by calculating a light "strength" for each Tri3D object by reading a combination of distance-from-light-source and triangle surface normal, then tweaking the RGB values of each vertex accordingly. Colored lighting is basically there for free if you want it, by biasing light intensities toward the chosen color. Shadowing could be implemented with a bit of raycasting, but it just depends how much processing power you're prepared to donate. Texture mapping is already supported by RaveKit, and functions are provided in the Raver subclass for storing, animating, and attaching bitmaps.

While this approach doesn't offer the range of built-in features in a comprehensive renderer like Direct3D or QuickDraw 3D, there are some benefits to using an immediate-mode 3-D API, rather than relying on the more proprietary retained-mode interface:

  • It keeps the top level API very lean, so incorporating new rasterizing technology doesn't require writing a lot of new calls.
  • You can make more use of some of the excellent code examples provided by leading game developers and, of course, the reams of source code available on the Internet (it took me about 45 seconds to get information on the DXF file format for this article).
  • A small package like this is very easy to install. The core program itself is tiny, and the RAVE run time is only around 120 KB. It can be dropped into the local directory, so you don't need to inflate the user's extensions or system folder with yet more DLLs that they will rarely use. This also makes it a candidate for a utility ActiveX component.
  • Perhaps most important -- especially for game developers -- is the ease with which you can port to other platforms. As long as the API handles triangles, you can use the same code with a few customizations in your Renderer-derived class. This is of particular significance for porting to console platforms since, to my knowledge, there are no commercial retained-mode libraries that run on both consoles and PCs.
  • Collision detection is practically automatic. Rather than duplicating a lot of the processing already done by a retained-mode renderer in order to sort nearby objects and surfaces, you already have this information, since each Vertex3D object has explicitly tested its relationship with the player.

Conclusion

Having explained the benefits of immediate mode over retained mode, there are also advantages to using an immediate-mode 3-D rasterizing engine instead of writing your own. These advantages include built-in hardware acceleration support, Gouraud shading, texture mapping, transparency, and z-buffering. (Actually, an optimization of this example program would be to switch off RAVE's built-in z-buffering, which may be overkill if your world is fairly simple, and provide your own instead.) And for those of you clever and resourceful enough to write your own rasterizing stage and HAL, there may be benefits to packaging it into a standard API specification such as RAVE and licensing it to graphics application developers.

The advantages of having that third dimension available, at relatively little extra cost in effort, are apparent once you start making use of it.


Listing One

void Raver::TimeSlice(long keys){
    TQAVGouraud v1, v2, v3;
    TQAVTexture t1, t2, t3;
    TQATexture *tex = NULL;
    Tri3D *this_tri = tri_list;


</p>
    // The generic Renderer base class handles moving the 3D "world". The 
    // vertex objects within the world take care of "projecting" the 
    // visible vertices of that world onto a 2D viewport and the Tri3D 
    // class contains the Gouraud shading and/or texture info
    
    // call base class, handle vertex transformations
    // according to input info in keys bitfield 
    Renderer::MoveWorld(keys); 
   
    QARenderStart (drawContext, NULL, cache); 
    // sadly, no cache cap in default engine
    
    while (this_tri) {
        // In this example, clipping is only at triangle level. With a bit
        // of edge checking, the gaps could be filled in with a maximum of 
        // 2-for-1 triangles (3-for-1 max on corners)
        // uv co-ords also need to be offset        
        Vertex3D &v3a = vertex_list[this_tri->vi1];
        Vertex3D &v3b = vertex_list[this_tri->vi2];
        Vertex3D &v3c = vertex_list[this_tri->vi3];
        if (this_tri->GetClip(vertex_list)) {
            if (this_tri->texmap > 0 && 
                this_tri->texmap < num_textures) {
                tex = texTable[this_tri->texmap].tp;
                if (tex) {  
                    QASetPtr(drawContext, 
                        kQATag_Texture, tex);
                    t1.x = v3a.pix_point.x;
                    t1.y = v3a.pix_point.y;
                    t1.z = v3a.invZ; 
                    t1.invW = v3a.invZ;;
                    t1.a = 1.0f;
                    t1.r = 1.0f;
                    t1.g = 1.0f;
                    t1.b = 1.0f;
                    t1.uOverW = v3a.u;
                    t1.vOverW = v3a.v;
                    t2.x = v3b.pix_point.x;
                    t2.y = v3b.pix_point.y;
                    t2.z = v3b.invZ; 
                    t2.invW = v3b.invZ;
                    t2.a = 1.0f;
                    t2.r = 1.0f;
                    t2.g = 1.0f;
                    t2.b = 1.0f;
                    t2.uOverW = v3b.u;
                    t2.vOverW = v3b.v;


</p>
                    t3.x = v3c.pix_point.x;
                    t3.y = v3c.pix_point.y;
                    t3.z = v3c.invZ; 
                    t3.invW = v3c.invZ;
                    t3.a = 1.0f;
                    t3.r = 1.0f;
                    t3.g = 1.0f;
                    t3.b = 1.0f;
                    t3.uOverW = v3c.u;
                    t3.vOverW = v3c.v;
                    QADrawTriTexture(drawContext, &t1, &t2,
                                                   &t3, kQATriFlags_None);
                }
           }           
            else {
                v1.x = v3a.pix_point.x;
                v1.y = v3a.pix_point.y;
                v1.z = v3a.invZ; 
                v1.invW = 1;
                v1.a = 1.0f;


</p>
                v2.x = v3b.pix_point.x;
                v2.y = v3b.pix_point.y;
                v2.z = v3b.invZ; 
                v2.invW = 1;
                v2.a = 1.0f;


</p>
                v3.x = v3c.pix_point.x;
                v3.y = v3c.pix_point.y;
                v3.z = v3c.invZ; 
                v3.invW = 1;
                v3.a = 1.0f;
                
                if (this_tri->type == TRI_MOVEABLE) { 
                    // calc dynamic lighting here
                    // no-op for this demo
                }
                else {
                    v1.r = this_tri->rgb1.r;
                    v1.g = this_tri->rgb1.g;
                    v1.b = this_tri->rgb1.b;
                    
                    v2.r = this_tri->rgb2.r;
                    v2.g = this_tri->rgb2.g;
                    v2.b = this_tri->rgb2.b;
                    
                    v3.r = this_tri->rgb3.r;
                    v3.g = this_tri->rgb3.g;
                    v3.b = this_tri->rgb3.b;
                }
                QADrawTriGouraud(drawContext,&v1,&v2,&v3,kQATriFlags_None);
            }
        }
        this_tri = this_tri->next;
    }       
    QARenderEnd (drawContext, NULL);        
}

Back to Article

Listing Two

int Vector3D::Project(){   
    if (z <= view_plane) return -1000;
    invZ = 1.0f - view_plane / z;   
    scaleZ = scaleZX / z;
    pix_point.x = (int)(raster_centerX + x * scaleZ + 0.5f);
    pix_point.y = (int)(raster_centerY + y * scaleZ + 0.5f);    
   return ClipBounds();
}

Back to Article

>Listing Three

// Renderer::MoveWorld() - called by derived class. Processes user input and // sets up transformation & rotation values which it passes to TransformWorld()


</p>
void Renderer::TransformWorld(
    float tx, float ty, float tz, 
    float rx, float ry, float rz)
{
    for (int n = 0; n < num_vertices; n++)
        vertex_list[n].Transform(tx, ty, tz,    
            rx, ry, rz, T_PREROTATION | T_RELATIVE);
}

BACK TO ARTICLE

Listing Four

// Snippet from Vector3D::Transform()// ...
    else if (t & T_RELATIVE) {  
        if(t&T_PREROTATION){    
            x += tx;
            y += ty;
            z += tz;        
            Rotate(rx, ry, rz);
        }
// ...
// The rotate component of Transform(). Defined as inline
void Vector3D::Rotate(float rx, float ry, float rz)
{
    float sin_a, cos_a, temp;
    if (ry) {
        sin_a = sin(ry);
        cos_a = cos(ry);                
        temp = x * cos_a - z * sin_a;       
        z = x * sin_a + z * cos_a; 
        x = temp;
    }           
    if (rx) {
        sin_a = sin(rx);
        cos_a = cos(rx);
        temp = y * cos_a - z * sin_a;       
        z = y * sin_a + z * cos_a;
        y = temp;
    }
    if (rz) {
        sin_a = sin(rz);
        cos_a = cos(rz);                
        temp = x * cos_a - y * sin_a;       
        y = x * sin_a + y * cos_a; 
        x = temp;
    }               
}

BACK TO ARTICLE

Listing Five

// Triangles are added to the linked list as indexes into the vertex array. // Although RAVE has built-in support for meshs, I've kept it abstracted from 
// the renderer. In Direct3D IM, for example, you can pass your own vertex 
// index list with stride info and so avoid duplication of services
void Renderer::AddTri(
    int vi1, int vi2, int vi3,  // vertex lookup indices
    Uv uv1, Uv uv2, Uv uv3,     // texture coords
    Rgb &p_rgb,                 // the "center" color
    int tmp, int type)          // texmap index, type "hint"
{
    Tri3D *active_tri, *new_tri;
    active_tri = tri_list;
    while (active_tri) {
        if (active_tri->next)
            active_tri = active_tri->next;
        else
            break;  
    }
    new_tri = new Tri3D(
    vi1, vi2, vi3, uv1, uv2, uv3, p_rgb, tmp, type, 
        lightList, vertex_list);    
    if (!active_tri) // must be first one
        tri_list = new_tri;
    else
        active_tri->next = new_tri;     
}
// Vertexes are stored in an array for fast random lookup
int Renderer::AddVertex(
    float x, float y, float z, 
    float r, float g, float b)
{
    if (num_vertices >= MAX_VERTICES) 
        // allocate some more storage here
        // ...
        return -1;
    Vertex3D &vert = vertex_list[num_vertices++];
    vert.Install(x, y, z, r, g, b);
    return num_vertices - 1;    
}

BACK TO ARTICLE

DDJ


Copyright © 1997, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.