508 lines
20 KiB
HTML
508 lines
20 KiB
HTML
<HTML>
|
|
<HEAD>
|
|
<META name=vsisbn content="1576101746">
|
|
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
|
|
<META name=vsauthor content="Michael Abrash">
|
|
<META name=vspublisher content="The Coriolis Group">
|
|
<META name=vspubdate content="07/01/97">
|
|
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: On Story, Two Rules, and a BSP Renderer</TITLE>
|
|
|
|
<!-- HEADER -->
|
|
<!-- Empty Reference Subhead -->
|
|
|
|
<!--ISBN=1576101746//-->
|
|
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
|
|
<!--AUTHOR=Michael Abrash//-->
|
|
<!--PUBLISHER=The Coriolis Group, Inc.//-->
|
|
<!--CHAPTER=62//-->
|
|
<!--PAGES=1149-1157//-->
|
|
<!--UNASSIGNED1//-->
|
|
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
|
|
|
|
<CENTER>
|
|
<TABLE BORDER>
|
|
<TR>
|
|
<TD><A HREF="62-01.html">Previous</A></TD>
|
|
<TD><A HREF="index.html">Table of Contents</A></TD>
|
|
<TD><A HREF="62-03.html">Next</A></TD>
|
|
</TR>
|
|
</TABLE>
|
|
</CENTER>
|
|
<P><BR></P>
|
|
<P><B>Listing 62.1 L62_1.C</B></P>
|
|
<!-- CODE //-->
|
|
<PRE>
|
|
/* Core renderer for Win32 program to demonstrate drawing from a 2-D
|
|
BSP tree; illustrate the use of BSP trees for surface visibility.
|
|
UpdateWorld() is the top-level function in this module.
|
|
Full source code for the BSP-based renderer, and for the
|
|
accompanying BSP compiler, may be downloaded from
|
|
ftp.idsoftware.com/mikeab, in the file ddjbsp2.zip.
|
|
Tested with VC++ 2.0 running on Windows NT 3.5. */
|
|
#define FIXEDPOINT(x) ((FIXEDPOINT)(((long)x)*((long)0×10000)))
|
|
#define FIXTOINT(x) ((int)(x >> 16))
|
|
#define ANGLE(x) ((long)x)
|
|
#define STANDARD_SPEED (FIXEDPOINT(20))
|
|
#define STANDARD_ROTATION (ANGLE(4))
|
|
#define MAX_NUM_NODES 2000
|
|
#define MAX_NUM_EXTRA_VERTICES 2000
|
|
#define WORLD_MIN_X (FIXEDPOINT(-16000))
|
|
#define WORLD_MAX_X (FIXEDPOINT(16000))
|
|
#define WORLD_MIN_Y (FIXEDPOINT(-16000))
|
|
#define WORLD_MAX_Y (FIXEDPOINT(16000))
|
|
#define WORLD_MIN_Z (FIXEDPOINT(-16000))
|
|
#define WORLD_MAX_Z (FIXEDPOINT(16000))
|
|
#define PROJECTION_RATIO (2.0/1.0) // controls field of view; the
|
|
// bigger this is, the narrower the field of view
|
|
typedef long FIXEDPOINT;
|
|
typedef struct _VERTEX {
|
|
FIXEDPOINT x, z, viewx, viewz;
|
|
} VERTEX, *PVERTEX;
|
|
typedef struct _POINT2 { FIXEDPOINT x, z; } POINT2, *PPOINT2;
|
|
typedef struct _POINT2INT { int x; int y; } POINT2INT, *PPOINT2INT;
|
|
typedef long ANGLE; // angles are stored in degrees
|
|
typedef struct _NODE {
|
|
VERTEX *pstartvertex, *pendvertex;
|
|
FIXEDPOINT walltop, wallbottom, tstart, tend;
|
|
FIXEDPOINT clippedtstart, clippedtend;
|
|
struct _NODE *fronttree, *backtree;
|
|
int color, isVisible;
|
|
FIXEDPOINT screenxstart, screenxend;
|
|
FIXEDPOINT screenytopstart, screenybottomstart;
|
|
FIXEDPOINT screenytopend, screenybottomend;
|
|
} NODE, *PNODE;
|
|
char * pDIB; // pointer to DIB section we’ll draw into
|
|
HBITMAP hDIBSection; // handle of DIB section
|
|
HPALETTE hpalDIB;
|
|
int iteration = 0, WorldIsRunning = 1;
|
|
HWND hwndOutput;
|
|
int DIBWidth, DIBHeight, DIBPitch, numvertices, numnodes;
|
|
FIXEDPOINT fxHalfDIBWidth, fxHalfDIBHeight;
|
|
VERTEX *pvertexlist, *pextravertexlist;
|
|
NODE *pnodelist;
|
|
POINT2 currentlocation, currentdirection, currentorientation;
|
|
ANGLE currentangle;
|
|
FIXEDPOINT currentspeed, fxViewerY, currentYSpeed;
|
|
FIXEDPOINT FrontClipPlane = FIXEDPOINT(10);
|
|
FIXEDPOINT FixedMul(FIXEDPOINT x, FIXEDPOINT y);
|
|
FIXEDPOINT FixedDiv(FIXEDPOINT x, FIXEDPOINT y);
|
|
FIXEDPOINT FixedSin(ANGLE angle), FixedCos(ANGLE angle);
|
|
extern int FillConvexPolygon(POINT2INT * VertexPtr, int Color);
|
|
// Returns nonzero if a wall is facing the viewer, 0 else.
|
|
int WallFacingViewer(NODE * pwall)
|
|
{
|
|
FIXEDPOINT viewxstart = pwall->pstartvertex->viewx;
|
|
FIXEDPOINT viewzstart = pwall->pstartvertex->viewz;
|
|
FIXEDPOINT viewxend = pwall->pendvertex->viewx;
|
|
FIXEDPOINT viewzend = pwall->pendvertex->viewz;
|
|
int Temp;
|
|
/* // equivalent C code
|
|
if (( ((pwall->pstartvertex->viewx >> 16) *
|
|
((pwall->pendvertex->viewz -
|
|
pwall->pstartvertex->viewz) >> 16)) +
|
|
((pwall->pstartvertex->viewz >> 16) *
|
|
((pwall->pstartvertex->viewx -
|
|
pwall->pendvertex->viewx) >> 16)) )
|
|
< 0)
|
|
return(1);
|
|
else
|
|
return(0);
|
|
*/
|
|
_asm {
|
|
mov eax,viewzend
|
|
sub eax,viewzstart
|
|
imul viewxstart
|
|
mov ecx,edx
|
|
mov ebx,eax
|
|
mov eax,viewxstart
|
|
sub eax,viewxend
|
|
imul viewzstart
|
|
add eax,ebx
|
|
adc edx,ecx
|
|
mov eax,0
|
|
jns short WFVDone
|
|
inc eax
|
|
WFVDone:
|
|
mov Temp,eax
|
|
}
|
|
return(Temp);
|
|
}
|
|
// Update the viewpoint position as needed.
|
|
void UpdateViewPos()
|
|
{
|
|
if (currentspeed != 0) {
|
|
currentlocation.x += FixedMul(currentdirection.x,
|
|
currentspeed);
|
|
if (currentlocation.x <= WORLD_MIN_X)
|
|
currentlocation.x = WORLD_MIN_X;
|
|
if (currentlocation.x >= WORLD_MAX_X)
|
|
currentlocation.x = WORLD_MAX_X - 1;
|
|
currentlocation.z += FixedMul(currentdirection.z,
|
|
currentspeed);
|
|
if (currentlocation.z <= WORLD_MIN_Z)
|
|
currentlocation.z = WORLD_MIN_Z;
|
|
if (currentlocation.z >= WORLD_MAX_Z)
|
|
currentlocation.z = WORLD_MAX_Z - 1;
|
|
}
|
|
if (currentYSpeed != 0) {
|
|
fxViewerY += currentYSpeed;
|
|
if (fxViewerY <= WORLD_MIN_Y)
|
|
fxViewerY = WORLD_MIN_Y;
|
|
if (fxViewerY >= WORLD_MAX_Y)
|
|
fxViewerY = WORLD_MAX_Y - 1;
|
|
}
|
|
}
|
|
// Transform all vertices into viewspace.
|
|
void TransformVertices()
|
|
{
|
|
VERTEX *pvertex;
|
|
FIXEDPOINT tempx, tempz;
|
|
int vertex;
|
|
pvertex = pvertexlist;
|
|
for (vertex = 0; vertex < numvertices; vertex++) {
|
|
// Translate the vertex according to the viewpoint
|
|
tempx = pvertex->x - currentlocation.x;
|
|
tempz = pvertex->z - currentlocation.z;
|
|
// Rotate the vertex so viewpoint is looking down z axis
|
|
pvertex->viewx = FixedMul(FixedMul(tempx,
|
|
currentorientation.z) +
|
|
FixedMul(tempz, -currentorientation.x),
|
|
FIXEDPOINT(PROJECTION_RATIO));
|
|
pvertex->viewz = FixedMul(tempx, currentorientation.x) +
|
|
FixedMul(tempz, currentorientation.z);
|
|
pvertex++;
|
|
}
|
|
}
|
|
// 3-D clip all walls. If any part of each wall is still visible,
|
|
// transform to perspective viewspace.
|
|
void ClipWalls()
|
|
{
|
|
NODE *pwall;
|
|
int wall;
|
|
FIXEDPOINT tempstartx, tempendx, tempstartz, tempendz;
|
|
FIXEDPOINT tempstartwalltop, tempstartwallbottom;
|
|
FIXEDPOINT tempendwalltop, tempendwallbottom;
|
|
VERTEX *pstartvertex, *pendvertex;
|
|
VERTEX *pextravertex = pextravertexlist;
|
|
pwall = pnodelist;
|
|
for (wall = 0; wall < numnodes; wall++) {
|
|
// Assume the wall won’t be visible
|
|
pwall->isVisible = 0;
|
|
// Generate the wall endpoints, accounting for t values and
|
|
// clipping
|
|
// Calculate the viewspace coordinates for this wall
|
|
pstartvertex = pwall->pstartvertex;
|
|
pendvertex = pwall->pendvertex;
|
|
// Look for z clipping first
|
|
// Calculate start and end z coordinates for this wall
|
|
if (pwall->tstart == FIXEDPOINT(0))
|
|
tempstartz = pstartvertex->viewz;
|
|
else
|
|
tempstartz = pstartvertex->viewz +
|
|
FixedMul((pendvertex->viewz-pstartvertex->viewz),
|
|
pwall->tstart);
|
|
if (pwall->tend == FIXEDPOINT(1))
|
|
tempendz = pendvertex->viewz;
|
|
else
|
|
tempendz = pstartvertex->viewz +
|
|
FixedMul((pendvertex->viewz-pstartvertex->viewz),
|
|
pwall->tend);
|
|
// Clip to the front plane
|
|
if (tempendz < FrontClipPlane) {
|
|
if (tempstartz < FrontClipPlane) {
|
|
// Fully front-clipped
|
|
goto NextWall;
|
|
} else {
|
|
pwall->clippedtstart = pwall->tstart;
|
|
// Clip the end point to the front clip plane
|
|
pwall->clippedtend =
|
|
FixedDiv(pstartvertex->viewz - FrontClipPlane,
|
|
pstartvertex->viewz-pendvertex->viewz);
|
|
tempendz = pstartvertex->viewz +
|
|
FixedMul((pendvertex->viewz-pstartvertex->viewz),
|
|
pwall->clippedtend);
|
|
}
|
|
} else {
|
|
pwall->clippedtend = pwall->tend;
|
|
if (tempstartz < FrontClipPlane) {
|
|
// Clip the start point to the front clip plane
|
|
pwall->clippedtstart =
|
|
FixedDiv(FrontClipPlane - pstartvertex->viewz,
|
|
pendvertex->viewz-pstartvertex->viewz);
|
|
tempstartz = pstartvertex->viewz +
|
|
FixedMul((pendvertex->viewz-pstartvertex->viewz),
|
|
pwall->clippedtstart);
|
|
} else {
|
|
pwall->clippedtstart = pwall->tstart;
|
|
}
|
|
}
|
|
// Calculate x coordinates
|
|
if (pwall->clippedtstart == FIXEDPOINT(0))
|
|
tempstartx = pstartvertex->viewx;
|
|
else
|
|
tempstartx = pstartvertex->viewx +
|
|
FixedMul((pendvertex->viewx-pstartvertex->viewx),
|
|
pwall->clippedtstart);
|
|
if (pwall->clippedtend == FIXEDPOINT(1))
|
|
tempendx = pendvertex->viewx;
|
|
else
|
|
tempendx = pstartvertex->viewx +
|
|
FixedMul((pendvertex->viewx-pstartvertex->viewx),
|
|
pwall->clippedtend);
|
|
// Clip in x as needed
|
|
if ((tempstartx > tempstartz) || (tempstartx < -tempstartz)) {
|
|
// The start point is outside the view triangle in x;
|
|
// perform a quick test for trivial rejection by seeing if
|
|
// the end point is outside the view triangle on the same
|
|
// side as the start point
|
|
if (((tempstartx>tempstartz) && (tempendx>tempendz)) ||
|
|
((tempstartx<-tempstartz) && (tempendx<-tempendz)))
|
|
// Fully clipped—trivially reject
|
|
goto NextWall;
|
|
// Clip the start point
|
|
if (tempstartx > tempstartz) {
|
|
// Clip the start point on the right side
|
|
pwall->clippedtstart =
|
|
FixedDiv(pstartvertex->viewx-pstartvertex->viewz,
|
|
pendvertex->viewz-pstartvertex->viewz -
|
|
pendvertex->viewx+pstartvertex->viewx);
|
|
tempstartx = pstartvertex->viewx +
|
|
FixedMul((pendvertex->viewx-pstartvertex->viewx),
|
|
pwall->clippedtstart);
|
|
tempstartz = tempstartx;
|
|
} else {
|
|
// Clip the start point on the left side
|
|
pwall->clippedtstart =
|
|
FixedDiv(-pstartvertex->viewx-pstartvertex->viewz,
|
|
pendvertex->viewx+pendvertex->viewz -
|
|
pstartvertex->viewz-pstartvertex->viewx);
|
|
tempstartx = pstartvertex->viewx +
|
|
FixedMul((pendvertex->viewx-pstartvertex->viewx),
|
|
pwall->clippedtstart);
|
|
tempstartz = -tempstartx;
|
|
}
|
|
}
|
|
// See if the end point needs clipping
|
|
if ((tempendx > tempendz) || (tempendx < -tempendz)) {
|
|
// Clip the end point
|
|
if (tempendx > tempendz) {
|
|
// Clip the end point on the right side
|
|
pwall->clippedtend =
|
|
FixedDiv(pstartvertex->viewx-pstartvertex->viewz,
|
|
pendvertex->viewz-pstartvertex->viewz -
|
|
pendvertex->viewx+pstartvertex->viewx);
|
|
tempendx = pstartvertex->viewx +
|
|
FixedMul((pendvertex->viewx-pstartvertex->viewx),
|
|
pwall->clippedtend);
|
|
tempendz = tempendx;
|
|
} else {
|
|
// Clip the end point on the left side
|
|
pwall->clippedtend =
|
|
FixedDiv(-pstartvertex->viewx-pstartvertex->viewz,
|
|
pendvertex->viewx+pendvertex->viewz -
|
|
pstartvertex->viewz-pstartvertex->viewx);
|
|
tempendx = pstartvertex->viewx +
|
|
FixedMul((pendvertex->viewx-pstartvertex->viewx),
|
|
pwall->clippedtend);
|
|
tempendz = -tempendx;
|
|
}
|
|
}
|
|
tempstartwalltop = FixedMul((pwall->walltop - fxViewerY),
|
|
FIXEDPOINT(PROJECTION_RATIO));
|
|
tempendwalltop = tempstartwalltop;
|
|
tempstartwallbottom = FixedMul((pwall->wallbottom-fxViewerY),
|
|
FIXEDPOINT(PROJECTION_RATIO));
|
|
tempendwallbottom = tempstartwallbottom;
|
|
// Partially clip in y (the rest is done later in 2D)
|
|
// Check for trivial accept
|
|
if ((tempstartwalltop > tempstartz) ||
|
|
(tempstartwallbottom < -tempstartz) ||
|
|
(tempendwalltop > tempendz) ||
|
|
(tempendwallbottom < -tempendz)) {
|
|
// Not trivially unclipped; check for fully clipped
|
|
if ((tempstartwallbottom > tempstartz) &&
|
|
(tempstartwalltop < -tempstartz) &&
|
|
(tempendwallbottom > tempendz) &&
|
|
(tempendwalltop < -tempendz)) {
|
|
// Outside view triangle, trivially clipped
|
|
goto NextWall;
|
|
}
|
|
// Partially clipped in Y; we’ll do Y clipping at
|
|
// drawing time
|
|
}
|
|
// The wall is visible; mark it as such and project it.
|
|
// +1 on scaling because of bottom/right exclusive polygon
|
|
// filling
|
|
pwall->isVisible = 1;
|
|
pwall->screenxstart =
|
|
(FixedMulDiv(tempstartx, fxHalfDIBWidth+FIXEDPOINT(0.5),
|
|
tempstartz) + fxHalfDIBWidth + FIXEDPOINT(0.5));
|
|
pwall->screenytopstart =
|
|
(FixedMulDiv(tempstartwalltop,
|
|
fxHalfDIBHeight + FIXEDPOINT(0.5), tempstartz) +
|
|
fxHalfDIBHeight + FIXEDPOINT(0.5));
|
|
pwall->screenybottomstart =
|
|
(FixedMulDiv(tempstartwallbottom,
|
|
fxHalfDIBHeight + FIXEDPOINT(0.5), tempstartz) +
|
|
fxHalfDIBHeight + FIXEDPOINT(0.5));
|
|
pwall->screenxend =
|
|
(FixedMulDiv(tempendx, fxHalfDIBWidth+FIXEDPOINT(0.5),
|
|
tempendz) + fxHalfDIBWidth + FIXEDPOINT(0.5));
|
|
pwall->screenytopend =
|
|
(FixedMulDiv(tempendwalltop,
|
|
fxHalfDIBHeight + FIXEDPOINT(0.5), tempendz) +
|
|
fxHalfDIBHeight + FIXEDPOINT(0.5));
|
|
pwall->screenybottomend =
|
|
(FixedMulDiv(tempendwallbottom,
|
|
fxHalfDIBHeight + FIXEDPOINT(0.5), tempendz) +
|
|
fxHalfDIBHeight + FIXEDPOINT(0.5));
|
|
NextWall:
|
|
pwall++;
|
|
}
|
|
}
|
|
// Walk the tree back to front; backface cull whenever possible,
|
|
// and draw front-facing walls in back-to-front order.
|
|
void DrawWallsBackToFront()
|
|
{
|
|
NODE *pFarChildren, *pNearChildren, *pwall;
|
|
NODE *pendingnodes[MAX_NUM_NODES];
|
|
NODE **pendingstackptr;
|
|
POINT2INT apoint[4];
|
|
pwall = pnodelist;
|
|
pendingnodes[0] = (NODE *)NULL;
|
|
pendingstackptr = pendingnodes + 1;
|
|
for (;;) {
|
|
for (;;) {
|
|
// Descend as far as possible toward the back,
|
|
// remembering the nodes we pass through on the way.
|
|
// Figure whether this wall is facing frontward or
|
|
// backward; do in viewspace because non-visible walls
|
|
// aren’t projected into screenspace, and we need to
|
|
// traverse all walls in the BSP tree, visible or not,
|
|
// in order to find all the visible walls
|
|
if (WallFacingViewer(pwall)) {
|
|
// We’re on the forward side of this wall, do the back
|
|
// children first
|
|
pFarChildren = pwall->backtree;
|
|
} else {
|
|
// We’re on the back side of this wall, do the front
|
|
// children first
|
|
pFarChildren = pwall->fronttree;
|
|
}
|
|
if (pFarChildren == NULL)
|
|
break;
|
|
*pendingstackptr = pwall;
|
|
pendingstackptr++;
|
|
pwall = pFarChildren;
|
|
}
|
|
for (;;) {
|
|
// See if the wall is even visible
|
|
if (pwall->isVisible) {
|
|
// See if we can backface cull this wall
|
|
if (pwall->screenxstart < pwall->screenxend) {
|
|
// Draw the wall
|
|
apoint[0].x = FIXTOINT(pwall->screenxstart);
|
|
apoint[1].x = FIXTOINT(pwall->screenxstart);
|
|
apoint[2].x = FIXTOINT(pwall->screenxend);
|
|
apoint[3].x = FIXTOINT(pwall->screenxend);
|
|
apoint[0].y = FIXTOINT(pwall->screenytopstart);
|
|
apoint[1].y = FIXTOINT(pwall->screenybottomstart);
|
|
apoint[2].y = FIXTOINT(pwall->screenybottomend);
|
|
apoint[3].y = FIXTOINT(pwall->screenytopend);
|
|
FillConvexPolygon(apoint, pwall->color);
|
|
}
|
|
}
|
|
// If there’s a near tree from this node, draw it;
|
|
// otherwise, work back up to the last-pushed parent
|
|
// node of the branch we just finished; we’re done if
|
|
// there are no pending parent nodes.
|
|
// Figure whether this wall is facing frontward or
|
|
// backward; do in viewspace because non-visible walls
|
|
// aren’t projected into screenspace, and we need to
|
|
// traverse all walls in the BSP tree, visible or not,
|
|
// in order to find all the visible walls
|
|
if (WallFacingViewer(pwall)) {
|
|
// We’re on the forward side of this wall, do the
|
|
// front children now
|
|
pNearChildren = pwall->fronttree;
|
|
} else {
|
|
// We’re on the back side of this wall, do the back
|
|
// children now
|
|
pNearChildren = pwall->backtree;
|
|
}
|
|
// Walk the near subtree of this wall
|
|
if (pNearChildren != NULL)
|
|
goto WalkNearTree;
|
|
// Pop the last-pushed wall
|
|
pendingstackptr—;
|
|
pwall = *pendingstackptr;
|
|
if (pwall == NULL)
|
|
goto NodesDone;
|
|
}
|
|
WalkNearTree:
|
|
pwall = pNearChildren;
|
|
}
|
|
NodesDone:
|
|
;
|
|
}
|
|
// Render the current state of the world to the screen.
|
|
void UpdateWorld()
|
|
{
|
|
HPALETTE holdpal;
|
|
HDC hdcScreen, hdcDIBSection;
|
|
HBITMAP holdbitmap;
|
|
// Draw the frame
|
|
UpdateViewPos();
|
|
memset(pDIB, 0, DIBPitch*DIBHeight); // clear frame
|
|
TransformVertices();
|
|
ClipWalls();
|
|
DrawWallsBackToFront();
|
|
// We’ve drawn the frame; copy it to the screen
|
|
hdcScreen = GetDC(hwndOutput);
|
|
holdpal = SelectPalette(hdcScreen, hpalDIB, FALSE);
|
|
RealizePalette(hdcScreen);
|
|
hdcDIBSection = CreateCompatibleDC(hdcScreen);
|
|
holdbitmap = SelectObject(hdcDIBSection, hDIBSection);
|
|
BitBlt(hdcScreen, 0, 0, DIBWidth, DIBHeight, hdcDIBSection,
|
|
0, 0, SRCCOPY);
|
|
SelectPalette(hdcScreen, holdpal, FALSE);
|
|
ReleaseDC(hwndOutput, hdcScreen);
|
|
SelectObject(hdcDIBSection, holdbitmap);
|
|
ReleaseDC(hwndOutput, hdcDIBSection);
|
|
iteration++;
|
|
}
|
|
</PRE>
|
|
<!-- END CODE //-->
|
|
<P><BR></P>
|
|
<CENTER>
|
|
<TABLE BORDER>
|
|
<TR>
|
|
<TD><A HREF="62-01.html">Previous</A></TD>
|
|
<TD><A HREF="index.html">Table of Contents</A></TD>
|
|
<TD><A HREF="62-03.html">Next</A></TD>
|
|
</TR>
|
|
</TABLE>
|
|
</CENTER>
|
|
|
|
<hr width="90%" size="1" noshade>
|
|
<div align="center">
|
|
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book © 2001 Michael Abrash</font>
|
|
</div>
|
|
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
|
|
<!-- reference_subfoot = footer -->
|
|
<!-- reference_footer = subfoot -->
|
|
|
|
<!-- BEGIN SUB FOOTER -->
|
|
</BODY>
|
|
</HTML>
|
|
|
|
<!-- END FOOTER -->
|
|
|
|
|