Chapter 2. Heidi Architecture

This chapter provides an overview of how Heidi is structured and how it communicates with applications and renderers.


Introduction
Heidi is an immediate mode interface specifically designed for the drawing part of the graphics software construction activity. Heidi addresses several general problems of the 3D graphics industry and adheres to the following design goals:
The architecture of Heidi is quite different from other APIs. In fact, it is helpful to think of Heidi as a design structure for building 3D graphics applications that can be dynamically bound to graphics hardware using one or more renderers. With Heidi you can build complex drawing environments made up of multiple renderers, which support many different kinds of drawing hardware.

Figure 1 shows a high-level block diagram of an example application, Heidi, and three possible Heidi device drivers. The first one is based on Microsoft GDI, the second on OpenGL, and the other is a custom driver.

Figure 1. Heidi and three possible drivers
The Renderer Stack
A fundamental concept of Heidi is the renderer stack. The renderer stack consists of one or more renderers, as shown in figure 2. Each renderer in the stack receives drawing commands from a source and sends drawing commands to a sink. For example, Renderer 2 receives commands from Renderer 3 and sends commands to Renderer 1 in the diagram. For all renderers, except for the bottom-most one, the sink is another renderer. For the bottom-most renderer, the sink is a piece of physical hardware: for example, a frame buffer or a plotter.
 
     
Figure 2. Diagram of a renderer stack
Renderer Commands
Every renderer in Heidi responds to the same set of commands including the following:
  • Configuration commands to set options on the renderer and push it on the renderer stack
  • Drawing commands such as to draw a line or draw a triangle strip
  • The update command, which synchronizes the renderer with its sink, so that any drawing commands previously received by the renderer are reflected in the sink renderer
  • Because each renderer responds to exactly the same set of commands, renderers do not need to be compiled at the same time as Heidi. New renderers can be added to an application without recompiling, enabling an application to load only those renderers it requires. Basically, the renderers are dynamically loaded at the user’s request. The application can load new renderers while it is running. Thus, users can purchase renderers with different capabilities or from different vendors and use them in any Heidi-based application.

    Physical Renderers
    The bottom-most renderer in a renderer stack is called a physical renderer. A physical renderer does not require a sink renderer. The most common example of a physical renderer is a device driver. There are also physical renderers that are not device drivers. For example, a selector is a renderer whose purpose is to test for pick hits. It does not draw to any device. For more information on selection renderers, see "Chapter 7. The Selection Renderer."

    You can think of most renderers as device drivers for virtual graphics devices. These virtual graphics devices and their associated renderers have different capabilities—some might be photorealistic, some might be especially fast, while others support hidden-surface removal using a Z buffer. (For more information on hidden line renderers, see "Chapter 9. The Hidden Line Renderer.")  They all respond to the same commands and essentially do the same things (draw lines, circles and so forth), but they perform these same tasks in different ways.

    Device drivers are renderers in Heidi, and renderers can be dynamically loaded. This means that new graphics hardware with new capabilities can be dynamically added to a Heidi-based application. This process makes it easier to add new hardware to a previously shipped base of application users and helps to promote hardware sales.

    Example Renderer Stack
    This section presents an example of a simple renderer stack. The discussion of the renderers in this section, is oversimplified to expose the major features of the Heidi architecture: that is, the discussion applies to hypothetical, simplified renderers. It does not completely reflect the implementation details of the rendering software supplied with the Heidi product.

    Figure 3. Renderer Stack example
    In figure 3, the bottom-most renderer is a driver for a graphics hardware device containing a frame buffer. The first (top) renderer in the stack is a software Z-buffer renderer which has a Z-depth buffer (shown as the software Z buffer) and a frame buffer. The memory for these buffers is allocated in the main memory of the computer.

    The application sends drawing commands to the software Z-buffer renderer. The Z-buffer renderer removes hidden surfaces using the standard Z-buffer algorithm and draws into its frame buffer. At this point nothing appears on the output screen because this renderer draws only into its software frame buffer and not into the device frame buffer. Nothing is sent to the physical renderer yet.

    When an update command is sent to the software Z-buffer renderer, it transfers the contents of its software frame buffer to its sink, the physical renderer. Depending on how the physical renderer is written, this may cause output to appear on the screen immediately. On the other hand, if the output does not appear immediately, the graphics hardware may be using double buffering. In this case, geometry sent to it does not appear until the hardware is told to switch buffers. Finally, an update command is sent to the physical renderer, and you are guaranteed that the graphics you have sent appears on the screen. The update command instructs a renderer to send any changes to its sink. This occurs only if the renderer buffers the drawing.

    Alternatively, a renderer can send output to its sink immediately after each drawing command. For example, you can build a software Z buffer that does not have its own frame buffer. In this case, as each drawing command is sent to this renderer, it rasterizes the geometry and compares the depth of the resulting pixels to the pixel depths in the Z buffer. It then sends the visible pixels to its sink. If its sink is a physical renderer that does no buffering, then the output appears immediately on the screen. Thus, as each drawing command is sent to the first renderer, output appears immediately on the screen. In this case, after all drawing is done, the application still sends an update command to each renderer. The update command, however, does nothing for this particular renderer.

    Because each update command transfers information only to the next renderer in the stack, it is important that the update commands are sent in the correct order. For a simple situation like the one previously described, an application updates the renderers from the top down—the top renderer first, then its sink, the physical renderer.

    A renderer might buffer the drawing using a frame buffer (as in the software frame buffer example) or it might buffer the graphics primitives. For example, a renderer that implements the painter’s algorithm buffers all of the graphics primitives sent to it. Then, when it receives an update command it does a depth-first sort (from back to front) on the primitives and sends them to its sink without rasterizing them. For more information on painter renderers, see the section  "Painter Renderers," in this chapter.

    Choosing a Renderer Stack
    By choosing which renderers to put in the renderer stack, an application makes trade-offs that can affect the rendering speed and image quality. In the example renderer stack (shown in figure 3), it might appear wasteful to have two separate frame buffers, one in each renderer (frame buffers can use up much memory). However, there are other factors to consider: assume that the physical renderer does not do any buffering. Having a separate frame buffer in the first renderer results in double-buffering. This reduces screen flashing and provides for smoother animation.

    Even more importantly, on many platforms (including Microsoft Windows), transferring data to the hardware frame buffer is much slower than writing to an equivalent buffer in main memory. The hardware frame buffer is usually connected to the processor over an I/O bus that is slower than the main memory bus. Each write to a hardware device, such as a frame buffer, involves a system call and usually a context switch. Thus, rendering each graphics primitive into a software frame buffer, and then transferring the buffer to the graphics device with a single system call, is usually much faster (often twice as fast) than rendering each piece of geometry into the hardware frame buffer.

    If multiple renderers in the renderer stack have their own frame buffers, it is not necessary for these frame buffers to be the same size or even the same pixel depth (number of colors). When a frame buffer is transferred to the sink renderer, it is sent using a regular renderer drawing command—the raster bit-blit command. Transferring a frame buffer is no different from any normal bit-blit operation on a renderer. Thus, the output of a renderer is always a sequence of renderer commands, which are sent to the sink renderer. The physical renderer is an exception because it has no sink.

    It is also possible for renderers to share a buffer. For example, if a renderer stack contains two software Z buffers, then these can be configured to use the same buffer.

    Sending Commands to Multiple Renderers
    An application is not limited to sending drawing commands to the first renderer. Commands can be sent to any renderer in the stack. Using the renderer stack in the example in figure 3, the application might draw a 3D scene to the software Z buffer and then send annotation text to the physical renderer so that it appears overlaid on the scene.

    Again, the application must send update commands at the proper times: in this case, an update command should be sent to the software Z-buffer renderer after the 3D scene and before any annotation text is sent to the physical renderer. The sequence should occur in the following order:

      1. Send the 3D scene to the software Z buffer.
      2. Send an update command to the software Z buffer.
      3. (optional) Send an update command to the physical renderer.

      4. (If you do, the 3D scene is guaranteed to appear on the screen immediately. Otherwise, it might not.)
      5. Send the annotation text to the physical renderer.
      6. Send an update command to the physical renderer.
      Steps 3 and 5 send update commands to the physical renderer. If you perform only step 5, then the drawing primitives are only guaranteed to appear on the screen after step 5. You probably will not see any output until step 5. Omitting step 3, however, does not guarantee or even imply that nothing will appear until step 5. That depends on whether the physical renderer buffers the scene. If the application wants each part of the image to appear as soon as possible, it should send the extra update command. If the application’s only concern is speed, it should omit the extra update command. If the physical renderer does buffer the scene, then the extra updates might slow the system down. Figure 4 shows all update cycles including step 3.
    Figure 4. Update cycles diagram
    Sending drawing commands to multiple renderers is especially useful as more renderers are added to the renderer stack. For example a photorealistic renderer may be at the top of the stack. Time can be saved by bypassing the photorealistic renderer for those parts of the scene (for example, text) that do not need the full treatment. For user interaction, the application can move an object around the screen quickly by sending it directly to the physical renderer.

    Note: The update_all method is a convenient way to allow the renderer to perform all of the work needed following all of the drawing calls comprising a complete frame, similar to update, but for all parents of the current device.

    Renderer Trees
    The renderer stack does not have to be a single linear stack, with one renderer on top of another. Instead, a renderer can have more than one source. Each renderer, however, can have only one sink. Thus, the renderer stack can actually take the form of a tree, with the physical renderer at the root of the tree. For example, figure 5 shows a renderer stack with four renderers, one physical renderer, and three software Z-buffer renderers.

    Figure 5. A renderer stack tree
    This renderer stack tree might correspond to an application that has one large window with multiple subregions (viewports). This is shown in figure 6, an example of viewports.
    Figure 6. Example of viewports

    The physical renderer corresponds to the overall window, and the top three renderers correspond to each subregion. Each of the top three renderers can have different options set. They don’t need to be the same kind of renderer. For example, one could be a photorealistic renderer.

    Each of the top three renderers has its position and screen size set separately as options. When one of these three renderers is sent an update command, it transfers its contents to its sink—the single renderer controlling the overall window. If the positions of the subregions overlap, then the last renderer updated appears on top. This means that if the application has overlapping subregions, it should update them from back to front.

    Note: Even though these subregions act like overlapping subwindows, they are not Windows system windows.

    If a renderer stack is configured as a tree, it is still called a stack. A renderer tree is built exactly like a stack—by pushing and popping renderers—however, it is all right to push more than one renderer on top of the same renderer, as in figure 5. In addition, at any one time references are made only about a single path through the tree, which is a stack.

    Multiple Renderer Stacks
    The application can also have multiple renderer stacks. This is useful for supporting multiple output devices. For example, to print a scene the application can build a renderer stack on top of a physical renderer for a printer. The same drawing commands that were used to draw the scene are then sent to this new renderer stack, and the scene is printed.

    When an application builds a new renderer stack, it starts with a different physical renderer, but any renderers pushed on top of that renderer might be required to be different as well. For example, if the physical renderer is for a high-resolution printer, then it is preferable not to use a Z-buffer renderer to remove hidden surfaces. Allocating Z-buffer memory for such a voluminous number of pixels is prohibitive. In this case a renderer based on an object space algorithm is more appropriate. Likewise, if the physical renderer is for a pen plotter, rasterizing primitives is not necessarily a good approach; you might want to draw lines as lines and add a separate renderer to do pen optimization.
     

    Selection Renderers
    For selection operations, the application can have a renderer stack built on a physical renderer that performs hit-testing. Such a renderer is called a selection renderer. The selection renderer is sent the coordinates of the pick. The scene is then sent to the stack and the selection renderer reports if any geometry was picked. For more information on selection renderers, see "Chapter 7. The Selection Renderer."

    Note: If the application uses a scene manager or stores bounding boxes for geometry, then it may be more efficient to perform hit-testing another way.
     

    Painter Renderers
    Painter Renderers provide for the back to front sorting of geometry that is passed in, allowing multiple layers of transparent objects. There are 3 options available, spatial subdivision, transparent-only and Z-sort only.

    Spatial subdivision divides the scene into increments of areas containing fewer objects. These areas are further subdivided if necessary, effectively speeding up the renderer exponentially. However, this method is usually not used and defaults to off.

    Transparent-only determines whether you capture all of the objects or just the transparent ones. If true, opaque objects are sent straight through and the transparent objects are stored and later sorted. The option defaults to true. If false, everything is sorted which is useful for Post Script printers.

    The Z-sort only option is used when speed is more important. It is not a full painter option of intersecting polygons and does no chopping. Whereas the older default renderer option does effectively chop up the objects into pieces that could appear to be over and under other objects, more useful for pen plotting.

    Support for handling 3D items has been added to the Heidi Painter's renderer.  This allows opaque objects to be drawn with full 3D hardware acceleration while holding on to transparent objects for proper sorting and subdivision if necessary. For more information on the painter renderer options, see class Paint_Options.

    Note: You can set the RGB color and indexed color simultaneously in Heidi.  This is necessary when drawing a material and using the Painter's renderer. In this case, the Painter's renderer expects the opacity to be set on the material as well as on the rendition color.


    Life Cycle of a Driver
    Heidi drivers are typically used as the lowest-level renderers in a Heidi renderer stack. As part of its operation, a driver goes through the life cycle shown in figure 7 and explained in the following steps:

    Figure 7. Life cycle diagram of a Driver
      1. The application loads the driver dynamically, if it has not been previously loaded.
      2. Once the driver is loaded, you construct an instance of it by calling a generator function.

      3. A driver instance is constructed in the context of its sink. Physical drivers are the only drivers that can be constructed independently. For example, you could construct multiple instances of a driver to draw into multiple windows.
      4. Configure the driver’s options.

      5. Different drivers have different options. For more information about setting options, see the section, "Driver Options."
        Each driver can fill in a virtual action, query_user_configuration, which communicates with Heidi users and asks for their preferences. Then query_user_configuration returns the user’s preferences to the Heidi application.
      6. Establish the driver after it is configured.

      7. The establish command reconciles the options of the current driver with those of its sink. It also causes the driver to do any necessary option-specific setup.
      8. The application informs the driver that a drawing sequence is about to begin by sending a begin_picture message to the driver.
      9. The application is ready to draw. Drawing commands are called at this point.
      10. When the application is done sending drawing commands to the driver, it calls the update command.

      11. For multiple drivers in a stack, the application sends update commands to all drivers, from the top down. At this point, a scene is displayed.
      12. The application signals to the driver that a drawing sequence has been completed by sending an end_picture message to it.

      13. The application can then either go back to drawing, or go all the way back and reconfigure the driver with different options. If the application changes any options on a driver, it must reestablish the driver and all drivers above it in the renderer stack.
      14. After the application is done with the driver, the driver must be deleted, along with all the drivers above it in the stack. This must be done in the opposite order in which they were constructed.
      Note: There are two variations of the Update command to save time for draw operations, update_all and HT_Update_Status. As mentioned previously, the update_all method  allows the renderer to perform all of the work needed following all of the drawing calls comprising a complete frame, similar to update, but for all parents of the current device.

      HT_Update_Status is an abstract base class defining the interface to support update progress or cancel functionality. It provides a mechanism to monitor the progress of potentially lengthy update() operations and to cancel update(), if necessary to save time. No mechanism is provided for the draw calls as it is assumed that each draw call will be relatively short, precluding the need for cancel. Also, within the draw call, there is no notion of "overall" progress.
       

      Driver Options
      In Heidi, configurable parameters for drivers are implemented through options. An option is a definition/value pair. These options can exist by themselves, but more likely, they are contained in a linked list called an option table. Option tables are linked lists containing definition/value pairs in which a driver may define driver-specific options indicating a capability that an application or user could control.

      For example, if your driver is capable of performing double-buffering, then it might provide an option whose name is the string Double Buffering, which can take on a boolean option value that enables or disables this feature. The option definition contains the name of the option, as well as its type specifier: in this case "Double Buffering" and Boolean, respectively. The option value also contains a matching type specifier, as well as the actual values of the option, stored in an array. Multiple options are stored in an option table, which is a linked list of options.

      The default constructors for classes HT_Renderer and HT_Device initialize the option tables to have entries for the renderer options (window_size, window_origin, buffer_depth, pixel_aspect, color_system, palette, alpha_blending and so forth) and the driver options (window_id, context_id, colormap_id). Further, the default constructors initialize these options to have trivial values, which almost certainly need to be reset at some stage of the configuration process. Generally, the initialization routine HT_Device calls a constructor for the driver object.

      Drivers may also add driver-specific options, which the application may wish to control. For example, if the device supports an upper and lower paper tray, the driver object may include an option that controls which paper tray is in effect. The constructor for the driver object has the responsibility of adding entries for these driver-specific options to the option tables.

      Driver Linkage
      A driver is a module that the application loads dynamically at runtime. In particular, a driver for Windows platforms is contained in a DLL module. This module must export a routine called HD_Device, which initializes the driver and returns a pointer to an object of the class HT_Device. The GDI driver, for example, fills in the HD_Device function as in the following:
       

        HEIDI_LOADABLE HT_Device_Data alter * HD_Device () { 
            return new Driver ();
        }
      where Driver is a derived class from HT_Device_Data, which encapsulates the functions of the particular device driver. The driver code, in general, makes use of some of the protected methods of class HT_Device and its base classes HT_Physical_Renderer and HT_Renderer.

      Action Tables
      Each renderer has an action table associated with it. Figure 8 shows a simple example. The action table contains an entry for each draw command understood by the renderer (plus a few miscellaneous commands). The draw commands are divided into three categories:
       


      DC stands for device coordinates, but in reality the 2D commands also take device coordinates. The difference between 2D and DC commands is that 2D commands do clipping, and DC commands assume that you are writing into valid pixels and do not want any error checking. 
       
       

      DC / DCI
      2D / 2DI
      3D
        Figure 8. An action table

      The action table itself is arranged into a kind of hierarchy with more complex commands at the right end and simpler commands at the left end. At the simplest level is the Draw_DC_Image  command that draws only an array of pixels.

      Heidi contains a library of standard drawing actions that can be called to draw geometry in terms of other geometry that the driver can handle. By default, Heidi initializes its action table to point to these standard drawing actions. The driver may override these actions with its own implementations that take advantage of the hardware’s specific capabilities.  For a list of the default drawing actions, see class HT_Drawing_Action.

      Every renderer is required to respond to every command in the action table. This sounds like a large amount of work, but with the help of the standard drawing actions and action inheritance, this is quite easy. If a renderer does not provide a particular entry in the action table by providing its own action or inserting a standard drawing action, it inherits the action from the sink renderer.

      This inheritance makes it easy to implement extension renderers. The purpose of these renderers is to provide specific valuable rendering services, but not to provide a complete replacement of functionality with respect to its sink. For example, there might be a different method of drawing 3D polytriangles that you might find useful in some situations. In that case, the developer need fill in only that particular action, leaving the remainder free. By not explicitly filling in the remainder of the actions with standard entries, the renderer inherits actions from its parent.

      More complex commands in the action table are normally implemented in terms of simpler commands. Consider a physical renderer for a simple full-color display card. The only two commands that must be written specifically for the display card are the Draw_DC_Image command, which provides the lowest level way to transfer pixels to the device frame buffer, and the Clear_drawing_buffer command, which provides a way to clear the background of the window. Once you have established these, all other commands in the action table can be implemented using these two commands, either directly or indirectly. For example, the Draw_3D_Polyline command can be implemented as follows:

      1. Draw_3D_Polyline projects the 3D polyline into a 2D polyline using the current viewing transformation, and calls the Draw_2D_Polyline command.
      2. Draw_2D_Polyline divides the 2D polyline into multiple 2D line segments, clips each line against the viewport and creates spans of pixels, and then calls Draw_DC_Dot to draw the spans.
      3. The Draw_DC_Dot command calls Draw_DC_Image to fill in the pixels in the frame buffer.
      Heidi supplies standard drawing actions to implement every command in the action table, except Draw_DC_Image. Commands that are not implemented by the renderer writer (that is, the driver writer) are inherited from the sink renderer. Thus, supporting a new device can be as simple as writing Draw_DC_Image (typically only a few lines of code).

      Of course, this command sequence is only one of many ways that you could implement Draw_3D_Polyline. For example, the 3D polyline can be clipped in 3D rather than 2D, or some of the steps can be done by the hardware, rather than using the Heidi utility routines.

      If your graphics device has any graphics acceleration hardware, you should probably implement more of the draw commands directly. For instance, many graphics cards have hardware to draw lines. In this case, the Draw_DC_Polyline command can be implemented using this hardware, rather than rasterizing the line in software and transferring it into the frame buffer with Draw_DC_Image. If your graphics hardware implements polylines, then the Draw_DC_Polyline command should be implemented directly. At the high end, some graphics hardware implements 3D primitives directly. For example, the best way to implement Draw_3D_Polyline might be to send the 3D polyline straight to the device.

      Implementing any command other than Draw_DC_Image is only a matter of speed optimization, and optimization can have a big effect on speed. In some cases, however, the Heidi software routines might actually be faster than using graphics hardware. The architecture of Heidi allows you to dynamically switch between hardware and software routines to compare their speeds.

      The Heidi action table implements a scaleable device interface: the interface scales easily to match a wide range of graphics hardware, from simple dumb frame buffers to high-speed graphics pipelines, to printers and plotters. For inexpensive hardware, the action table fills in with software, but it is possible to take advantage of high-end hardware when it is available.

      For information on updating the action table, see the section "Updating the Driver Action Table," in chapter 3, "Implementing a New Driver."

      Delegating Drawing Actions
      The driver writer may not wish to implement every type of drawing action that is provided by Heidi. In some cases, the display hardware may implement some drawing primitives with high end hardware assistance while others may not be implemented in hardware at all. For example, some display cards are able to draw lines or polygons, but not ellipses or elliptical arcs.

      In Heidi, drivers can delegate actions in three ways:

      The third method of delegation can be used to handle certain cases of drawing actions that the driver may handle well, but are delegated to Heidi’s standard drawing actions when the driver cannot handle them. For example, if the driver can implement polylines that have no line weight (single pixel width), but not polylines with weight (wide lines), the driver can provide a driver-specific action that checks the line weight. If it is greater than a single pixel, the action is delegated to the Heidi standard routines.

      For example, the OpenGL sample driver implements polylines through OpenGL only when there is no line cap or join style enabled. If it finds that one of these attributes is enabled, it delegates the line drawing action to the Heidi standard drawing action: HD_Standard_Draw_DC_Polyline. The code looks like this:

        void opengl_draw_dc_polyline (
            stack       WOpenGL_Driver alter *       rc,
            stack       HT_Rendition const *         hr,
            stack       int                          count,
            register    HT_DC_Point const *          points) {
        
            // ...
         
            // OpenGL can only handle butt lines with miter join styles
            if ((hr->line_cap() != HT_Line_Cap::Butt || 
                 hr->line_join() != HT_Line_Join::Miter)) {
                HD_Standard_Draw_DC_Polyline (rc, hr, count, points);
                return;
            }
         
            // Otherwise draw the polyline via OpenGL ...
        }
      It is worthwhile to notice that the driver routine opengl_draw_dc_polyline is implemented as a "friend" of the driver class, which was derived indirectly from HT_Renderer. This is required because the action table is implemented as an array of functions, and C++ does not easily allow class member functions to be stored in an array of function pointers. Therefore, by being a friend of the derived class, opengl_draw_dc_polyline has access to the private data of the derived class as though it was a member function. Also, the first argument to the routine is a pointer to the driver object. This is the pointer to the driver object that is used to access the driver’s private data.

      Drawing Primitive Actions
      Every drawing primitive corresponds to a drawing action in the renderer action table and thus to a method of the HT_Renderer class. The name of each drawing action indicates the sort of geometric figure to be drawn. Every drawing action takes some sort of geometry argument that determines where the primitive is to be drawn and, by implication, its size, orientation, and so forth. Every drawing action takes an HT_Rendition argument, which specifies the values of the applicable attributes to be used in drawing the primitive. Of course, different attributes in the rendition are applicable to different primitives. For a description of the HT_Rendition class, see appendix A, "Heidi Class Reference." Some drawing actions take additional arguments giving further data that supplement or override the attribute values in the rendition argument.

    The drawing actions are divided into three classes: draw_3d_...

    draw_2d_...

    draw_dc_...

    Using the same call, there are two formats: integer or floating-point parameters. For example, HT_DCI_Point and HT_DC_Point, respectively.

    Note:  All of the DC (floating point) calls are rounded instead of truncated. Calls less than .5 are rounded down and calls greater than .5 are rounded up. Furthermore, Heidi standard routines convert the DC version to DCI for you. If you are writing a Heidi driver or renderer, you can simply fill in the DCI version and Heidi will do the rest.

    The draw_3d_... actions are specified with respect to 3D object coordinates.

    For every action draw_2d_xxx, there is a corresponding action draw_dc_xxx  with the same xxx part of the name and the same argument list. The difference between draw_2d_xxx  and draw_dc_xxx is that the former performs clipping and the latter does not: that is, draw_dc_xxx assumes that the caller has taken care to be sure that the entire figure lies inside the applicable clipping region.

    The draw_3d_... routines may include additional data such as vertex or face normals. The draw_dc_... routines have the plainest parameters that are not grouped, whereas the 3d routines try to pack the primitive description into a "rich" data structure. For example, the draw_dc_polyline routine takes a count and an array of vertices, while draw_3d_polyline takes a class HT_Polyline as input.

    The draw_2d_... and draw_dc_... drawing actions are subject to the general attributes of color, weight, style, and so forth of the rendition argument but are not directly subject to the lighting and shading attributes.

    3D Drawing Methods
    Following is a list of 3D drawing primitives:
      draw_3d_polygon
    draw_3d_polyline
    draw_3d_polymarker
    draw_3d_polytriangle

    draw_3d_linestrip
    draw_3d_markerstrip
    draw_3d_tristrip
     
     

    2D and DC Drawing Methods
    Image and buffer drawing and access must have either anrgb32 or an index8 routine, otherwise, Heidi goes into an infinite loop when it tries to convert an image from one format to the other.

    There are multiple draw_dc_*_image and draw_dc_depth*_image functions but there is only one read_dc_color_image and read_dc_depth_image function. This is because Heidi reads in image data in any image format, and the user does not have to specify the kind of data being read. However, while the users are drawing, they know the image data format and, hence, can specify the respective draw function.

    Note: Heidi now draws the last pixel of a polyline in order to facilitate the display of weighted lines rasterized as polytriangles. In the past this pixel was drawn by applications, for example, AutoCAD through its DisplayList, adding it as an extra point to the geometry.

    Heidi drivers and renderers need to do Z interpolation, while doing a single raster scan, in other words the height of the input image is 1 in the following image routines:
     

    draw_dc_image
    draw_dc_index8_image
    draw_dc_rgb32_image
    draw_dc_depth16_image
    draw_dc_depth32_image

    draw_2d_image
     
     

    Following is a list of dc and 2d primitives with colors determined by attributes in rendition, or in the geometry arguments:
      draw_dc_polyline
    draw_dc_polytriangle
    draw_dc_polymarker
    draw_dc_polygon
    draw_dc_ellipse
    draw_dc_elliptical_arc
    draw_dc_dot
     
     

    draw_2d_polyline
    draw_2d_polytriangle
    draw_2d_polymarker
    draw_2d_polygon
    draw_2d_ellipse
    draw_2d_elliptical_arc
    draw_2d_dot

       
    Following are primitives with an array of colors for each line, marker, and triangle, respectively. When the array is a null pointer, the rendition color is applied to all primitives:
      draw_dc_colorized_polyline
    draw_dc_colorized_polymarker
    draw_dc_colorized_polytriangle

    draw_2d_colorized_polyline
    draw_2d_colorized_polymarker
    draw_2d_colorized_polytriangle

       
    The following primitives have color interpolated from vertex colors passed as arguments.
      draw_dc_gouraud_polyline
    draw_dc_gouraud_polytriangle

    draw_2d_gouraud_polyline
    draw_2d_gouraud_polytriangle

       
    Following are examples of drawing textured faces.
      draw_dc_textured_polytriangle
    draw_2d_textured_polytriangle
       
    Note: There are no draw_3d_textured_polytriangle primitives, but you can use draw_3d_tristrip for drawing 3D textured polytriangles.

    For more information, see the action specifications for each HT_Renderer::draw_... primitive in appendix A, "Heidi Class Reference."

     

    NOTE: For a continuation of Chapter 2, "Heidi Architecture", see the "Renditions" section.