The P3DC Drawing Pipeline

Rendering with P3DC is straightforward but unfortunately it is a bit time dependent (it is hard to keep a consistent frame rate without threads). The model is as follows:

If you were to program a game using this engine, it would have some additional layers:

These things will be present in my first game "Fantasy Castle."

Drilling Down the Pixel Pipeline

At the top level the drawing functions are pretty straight forward, however it is more interesting when we look at the individual drawing functions. Each function has the same basic internal structure, it operates as follows.

p3dc_draw_xxxx(p3dc_VIEW *vp, p3dc_XFRM *view, int flags, p3dc_XXXX *thing_to_draw)

From this function call, the parameters are used as follows:

The simplest case is drawing points although all of the others are basically the same. The code for points is simply:

void
p3dc_draw_point(p3dc_VIEW *vp, p3dc_XFRM *view, int flags, p3dc_POINT *p) {
	__p3dc_total_parts++;

	/* Step 1: Light the point */
	if (flags & P3DC_DRAW_SHADED) {
		p3dc_light_point(e, p);
	} 

	/* Step 2: Transform the point into view space */
	p3dc_xfrm_vrtx(NULL, view, p->p0);

	/* Step 3: Clip the result. */
	if ((flags & P3DC_DRAW_CLIPPED) && (p3dc_clip_point(p->p0)))
		return;
	__p3dc_drawn_parts++;

	/* Step 4: Draw the point. */
	__hw_draw_point(e, p);
}

In this code the variable p3dc_total_parts is used to give a number of "things" rendered in a given prepare/flip block. As you can see the flag P3DC_DRAW_SHADED is used to determine if the point should be lit according to a simple ambient light model. If set the point is lit using the function p3dc_light_point. The code in p3dc_light_point takes into account the origin point of the camera, the position of the lights (attached to the camera structure) and the position of the point to come up with an intensity modifier for the point's color.

A Note on Accellerators

No, not the 3D kind like 3Dfx Voodoo, the ones that let you render 3D scenes quickly and get on with things.

P3DC contains a number of techniques that are used to speed up the rendering pipeline, the most common technique is pre-caching results. There are three results that benefit greatly by being saved:

  1. Vertex Transformation -- These are saved by storing the transformed vertex data in the vertex structure with the identifier of the transform that created that data. Then when you call p3dc_xfrm_vertex if the id in the vertex matches the id in the transform then you're done and the math can be skipped.

    For this to work however two things have to be true, first, whenever a transform changes its ID must change, and second, the ID used must be unique within the time domain of its usage. By manipulating the transforms only with the transform manipulation library, the identifiers are always changed, by serializing the generation of identifiers to a single routine that simply increments the id each time it gives one out, uniqueness is maintained. By making the ID 16 bits you get 64K transforms before they roll over and so far that hasn't been a problem.
  2. Outcode management -- When a vertex is transformed, and the value in w is not 1.0, the vertex is probably going to participate in a clipping round at some point. To speed that up the "outcode" which is an encoding describing whether or not the vertex represents a point that is visible on the screen, is computed and stored with the vertex. This outcode won't change unless the vertex is re-transformed! The clipping code uses it to make fast decisions about those things it needn't bother clipping.
  3. Clipped vertices -- In lines the values of the vertices post clipping are stored so that when the line is re-clipped if the view or its vertices haven't changed it can be re-drawn without re-clipping it.

The P3DC Functions

This section provides a catalog of the various functions in Project: 3D Craft.

p3dc_prepare_frame(p3dc_CLR *color)

Returns: void

This function prepares the frame for drawing. Generally it clears the entire screen to the color specified. Normally, you would want to specify whatever the default fog color is here. This is so that when fogging you can ignore things that are "holes" (nothing is rendered in front of them).

p3dc_flip_frame(p3dc_BOOL wait)

Returns: void

This function causes the last frame drawn to be "flipped" to the front. The current frame is then pushed into the stack of buffers available to draw on. Usually there will be two buffers the "shown" buffer and the "hidden" buffer. By double buffering like this we don't get nasty drawing artifacts if the screen changes while the monitor has displayed it.

p3dc_clear_region(p3dc_CLR *color, p3dc_FLOAT x, y, width, height)

Returns: void

This function clears a region of the screen to the indicated color. This is useful when you're not using the whole screen for rendering (say a cockpit window or rear view mirror).

p3dc_adjust_color(p3dc_CLR *color, p3dc_FLOAT factor, p3dc_CLR *result)

Returns: void

This function multiplies the RGB components of a color struct by a factor that is presumably less than 1.0. This is used in the lighting code to adjust the color of a vertex when a polygon is off angle from the light.

p3dc_draw_point(p3dc_VIEW *vp, p3dc_XFRM *view, int flags, p3dc_POINT *point)

Returns: void

This function renders a point on the screen. The point is clipped if it falls outside the visible screen boundary.

p3dc_xfrm_vrtx(p3dc_VRTX *result, p3dc_XFRM *transform, p3dc_VRTX *src)

Returns: void

This function applies the 3D transformation in transform to the vertex in src. Note that technically the vertex is a 4x1 column vector and the transform is a 4 x 4 matrix, however we take advantage of the fact that most of the time we're actually doing the transform in 3 space and don't bother multiplying the last row (0, 0, 0, 1) by the vector to get the value 1.0 in the w output.

Now there are a couple of things to note about this routine. First, the transform carries around with it an identifier called id. This identifier is changed any time the transform is changed. The vertex carries around both its original co-ordiantes and its transformed co-ordinates. The transformed co-ordinates include a value xid which is the identifier of the transform last used on the vertex's co-ordinates. Thus if we're trying to transform this vertex a second time with the same transform, nothing really happens and the function returns.

If the value result is non-NULL, then the transformed vertex co-ordinates are stored in the results "world" co-ordinates. This is how you move a vertex from one co-ordinate space into another co-ordinate space and it is what happens to models when they are transformed into world co-ordinates.

p3dc_clip_line(p3dc_LINE *line, float *delta1, float *delta2)

Returns: int -- The integer returned is a clip code, the defined codes are:

This function clips a line based on its transformed vertices. It uses the value w in the transformed version of the co-ordinates to clip the endpoints to values -w <= x <= w, -w <= y <= w, 0 <= z <= w. The results are stored in two p3dc_PNT4 structures c1 and c2 (clipped vertex 1 and clipped vertex 2). When the p3dc_draw_line function is called it checks the clip code and if it needs to use an adjusted version of the line structure it creates a temporary line structure and uses that.

p3dc_clip_point(p3dc_POINT *point)

Returns: int - The integer clip code for the point, only one of:

This function clips a point structure and returns a code to indicate if the point should actually be drawn. There is one accellerator here in that the vertex outcode is cached for a particular transform so that the clipping routine can do its thing.

RCS Logging information for document control.

$Id: p3dc.htm,v 1.1 1999-08-17 18:32:50-07 cmcmanis Exp $
$Log: p3dc.htm,v $
Revision 1.1  1999/08/18 01:32:50  cmcmanis
Initial revision

Revision 1.1  1999/07/26 03:11:46  cmcmanis
Initial revision