![]() |
Binary Space
Partitioning Tutorial Part II |
![]() |
![]() |
written by Gary Simmons Code by Gary Simmons & Adam Hoult Special Thanks to: Adam Hoult, Matthias Liertzer & Klaus Hartmanm |
Are we infront of Node B? if Yes then Render Back Leaf Polygons and then Render Front Leaf Polygons else Render Front Leaf Polygons and then Render Back Leaf Polygons |
void DrawTree (long CameraLeaf)
{
for (int
i=0;i<NumberOfLeafs;i++)
{
long offset=CameraLeaf*numberOfLeafs;
if
(PVSData[offset+i]==1)
{
RenderLeaf(i);
}
}
}
void DrawTree(long
leaf)
{
POLYGON *CurrentPoly;
int i;
long
PVSOFFSET=LeafArray[leaf].PVSIndex;
BYTE
*PVSPointer=PVSData;
PVSPointer+=PVSOFFSET;
long currentleaf=0;
while
(currentleaf<NumberOfLeafs)
{
if (*PVSPointer!=0)
{
for (i=0;i<8;i++)
{
BYTE mask=1<<i;
BYTE
pvs=*PVSPointer;
if (pvs&mask)
{
Render The Polygons in Leaf [
currentleaf ]; }
}// end for if pvsdata
currentleaf++;
}// end for
i;
PVSPointer++;
}
else
{// we have hit a zero so
read in the next byte and see how long the run of zeros is
PVSPointer++;
BYTE
RunLength=*PVSPointer;
PVSPointer++;
currentleaf+=RunLength*8;
}
struct NODE
{
unsigned char IsLeaf;
unsigned long
Plane;
unsigned long Front;
signed long Back;
BOUNDINGBOX
BoundingBox;
};
Looking at the above structure you can see that
there are no Front and Back pointers but instead there are front and back "long"
variables.Instead of having pointers pointing to memory addresses that hold the
front and back nodes instead we simply store a long value that is the index of
another Node in the Node array.If Back is set to -1 then it does not point to
another node in the array but instead indicates that there is solid space behind
this Node (remembering that you never have Back Leafs).If IsLeaf=0 then the
Front value is the index of another Node in the Node array however if IsLeaf=1
then this means that to the front of this node is a Leaf.In this case "Front"
does not hold the value of an index of a node in the node array but instead
holds the index of a Leaf in the Leaf array.Also remember that we no longer use
the Polygons themselves as splitters but instead just the Planes of the polygons
are stored.Therefore the "Plane" variable in the Node structure is an index in
to the Plane Array which is an array where all the Nodes Planes are
stored.Please do no not worry about what the BoundingBox variable is for we will
look at this in a moment.The Plane structure looks like
this:-struct PLANE
{
D3DXVECTOR3
PointOnPlane;
D3DXVECTOR3 Normal;
} ;
The Plane structure
just contains two Vectors that describe the plane for the node.Node 1 in the
array will use Plane 1 in the array and Node 2 in the Node array will use Plane
2 in the plane array etc.Figure G below better decribes the memory organization
we will be employing. struct LEAF{
long StartPolygon;
long EndPolygon;
long PortalIndexList[50];
long NumberOfPortals;
long PVSIndex;
BOUNDINGBOX BoundingBox;
};
There will be a few variables
in there that you do not understand yet such as NumberOfPortals and
PortalIndexList but we do not need those for tree creation so will ignore them
for now.They will be used later to calculate the PVS set. "PVSIndex" will
eventually hold the position in to the Master PVS array where this leafs
visibility information begins.That will all become clearer later.For now we are
only interested in StartPolygon and EndPolygon. As with the Node based compiler
we developed in Part 1 we will pass in our polygon data into the Compiler as a
linked list of polygons just like before.The difference is that once we find we
have a list of polygons that are convex (they have all been used as splitters)
we can delete it in the linked list and copy its memory in to a Master Polygon
array.The great thing about this is that because a list of polygons in a Nodes
Front list will only get copied into the Polygon array when they form a leaf it
means the polygons get written into the array Leaf at a time.This means
Leaf[0]'s polygons all follow each other in the array followed by Leaf[1]'s
polygons etc. This means all we have to store in the Leaf structure is where in
the array it's polygons start and end. Actually 'EndPolygon' as you can see from
the diagram is the first polygon in the Next Leaf. This is because I render them
in a loop using " For
(poly=Leaf[n].StartPolygon;poly<Leaf.[n].EndPolygon;poly++)". In other words
we render from Start to End-1 but thats just standard c++ loop stuff. void
InitPolygons(void)
{
ReserveInitialMemoryForArrays();
PolygonList=NULL;
PolygonList=LoadMWM("demolevel.mwm");
LoadTextures();
BuildBspTree(0,PolygonList);
...
...
...
}
The
InitPolygons function is called just after the D3D environment has been set
up.The first thing it must do is call 'ReserveInitialMemoryForArrays' function
which is in MemAlloc.Cpp.We will see this function in a moment after we have
just had a look at the net couple of lines.The Next 'InitPolygons' does is sets
the PolygonList pointer to NULL for safety.The PolygonList pointer is a global
pointer that will point to the head of a linked list of polygons that will be
fed into the compiler.This is virtually exactly the same as the previous
tutorial except the Polygon Structure is a little different.Here is the Polygon
Structure.struct POLYGON
{
D3DLVERTEX
*VertexList;
D3DXVECTOR3 Normal;
WORD NumberOfVertices;
WORD
NumberOfIndices;
WORD *Indices;
POLYGON * Next;
bool
BeenUsedAsSplitter;
long TextureIndex;
};
This is almost
identicel to the last tutorial.The Polygons will be linked together by the
'Next' pointer and the Polygon itself will be made from an indexed triangle
list.The 'AddPolygon' function we will look at in a moment is responsable for
taking an n sided polygon and breaking it down into indexed triangle.This was
covered in detail in the last tutorial.Each polygon also has a texture index.In
the demo I load in 25 textures into an array of IDirect3dTexture8 objects so
this is basically just an index in to this array describing what texture the
polygon uses.Also note the 'BeenUsedasSplitter' flag that is set during
compilation of the bsp tree to signify that this polygon has already been used
so can not be used as a split plane again.API Notice for DX8: With DX8 the D3DLVERTEX built in vertex structure is no longer present.You now specify vertex types using the flexible vertex format.I have named my Custom Vertex structure 'D3DLVERTEX' after the structure that it emulates from Dx7.It is defined in 'pvs.h' as follows:- struct
D3DLVERTEX The Flexible Vertex Format Flags for this vertex
are as follows:->#define D3DFVF_LVERTEX ( D3DFVF_XYZ |
D3DFVF_DIFFUSE | D3DFVF_SPECULAR | D3DFVF_TEX1
) |
PolygonList=LoadMWM("demolevel.mwm");
long MAXNUMBEROFNODES
=100;
long MAXNUMBEROFPOLYGONS=100;
long MAXNUMBEROFPLANES =100;
long
MAXNUMBEROFLEAFS =100;
long MAXNUMBEROFPORTALS =100;
POLYGON *PolygonArray;
NODE *NodeArray;
LEAF
*LeafArray;
PLANE *PlaneArray;
PORTAL **PortalArray;
BYTE
*PVSData;
Do not worry about the PortalArray for now this is
something we will discuss way on down the tutorial.The last few globals to be
declared in memalloc.cpp are the actual variables to keep track of how many
Planes,Nodes,Polygons etc we have in our array.Remember that how many NOdes we
have in the Node Array is not the same as MAXNUMBEROFNODES as this contains how
many nodes we can store in our array before we need to resize.In other words if
we have just added our 101st Node to the node array then the Number Of Nodes
will be 101 but the MAXNUMBEROFNODES will be 200 as it is resized on the 100
boundry.long NumberOfPolygons=0;
long NumberOfNodes=0;
long
NumberOfLeafs=0;
long NumberOfPlanes=0;
long
NumberOfPortals=0;
Once again do not worry about NumberOfPortals
yet.OK now we know what the Global arrays look like lets have a look at that
'ReserveInitialMemoryForArrays' function.void
ReserveInitialMemoryForArrays()
{
NodeArray= (NODE *) malloc
(MAXNUMBEROFNODES *sizeof(NODE));
PolygonArray=(POLYGON *)malloc
(MAXNUMBEROFPOLYGONS *sizeof(POLYGON));
PlaneArray =(PLANE *) malloc
(MAXNUMBEROFPLANES *sizeof(PLANE));
LeafArray =(LEAF *) malloc
(MAXNUMBEROFLEAFS *sizeof(LEAF));
PortalArray =(PORTAL**) malloc
(MAXNUMBEROFPORTALS *sizeof(PORTAL *));
ZeroMemory(NodeArray,
sizeof(NODE) *MAXNUMBEROFNODES);
ZeroMemory(LeafArray, sizeof(LEAF)
*MAXNUMBEROFLEAFS);
ZeroMemory(PlaneArray, sizeof(PLANE)
*MAXNUMBEROFPLANES);
ZeroMemory(PolygonArray,sizeof(POLYGON)
*MAXNUMBEROFPOLYGONS);
ZeroMemory(PortalArray, sizeof(PORTAL*)
*MAXNUMBEROFPORTALS);
}
Nothing to explain here.Each array is
dynamically initiated as being large enough to hold 100 elements of their
type.Then the arrays are initialized to zero for safety.So we now have a Node
Array, and Plane Array a Leaf Array and a Polygon Array each large enough to
hold 100 elements but for the moment are empty.PolygonList=LoadMWM("demolevel.mwm");
WORD | Number of Polygons in File |
WORD | Number of Vertices in Polygon N |
D3DLVERTEX | Vertex Number M |
WORD | Texture Index for Polygon N |
POLYGON *
LoadMWM(char *filename)
{
FILE *stream;
POLYGON *Root=NULL;
POLYGON
* Child=NULL;
WORD PolyCount,numofverts;
int i,b;
D3DLVERTEX
xyzBuffer[50];
WORD TextureIndex;
DWORD uselessinfo;
stream =
fopen(filename, "rb");
fread(&PolyCount, sizeof( WORD ), 1, stream
);
for
(i=0;i<PolyCount;i++)
{
fread(&numofverts,sizeof(WORD),1,stream);
for
(b=0;b<numofverts;b++)
{
fread(&xyzBuffer[b].x,
sizeof(float),1,stream);
fread(&xyzBuffer[b].y,
sizeof(float),1,stream);
fread(&xyzBuffer[b].z,
sizeof(float),1,stream);
fread(&uselessinfo, sizeof(DWORD),1,stream
);
fread(&xyzBuffer[b].color,
sizeof(D3DCOLOR),1,stream);
fread(&xyzBuffer[b].specular,sizeof(D3DCOLOR),1,stream);
fread(&xyzBuffer[b].tu,
sizeof(float),1,stream);
fread(&xyzBuffer[b].tv,
sizeof(float),1,stream);
}
fread(&TextureIndex,sizeof(WORD),1,stream);
if
(i==0)
{
Root=AddPolygon(NULL,&xyzBuffer[0],numofverts);
Child=Root;
}
else
{
Child=AddPolygon(Child,&xyzBuffer[0],numofverts);
}
Child->TextureIndex=TextureIndex;
}
fclose
(stream);
return Root;
}
As you can see there is
nothing fancy about the file extraction.We simply read in the Number of polygons
in the file and then do a for next/next loop for each polygon.For each polygon
we read in its number of vertices and then the vertex list which is just a bunch
of D3DLVERTEX structures. Notice how I load in one DWORD of useless
information.This is because my Polygon Editor was written in DX7 where the built
in D3DLVERTEX structure had a reserved DWORD value after the first vector. The
D3DLVERTEX is no longer internal to DX8 so I have emulated it using the Flexible
Vertex Format. Our Custom D3DLVERTEX structure though does not have that
Reserved DWORD so we simply read it in and discard it just as a simple means of
moving the file read counter on to the next useful bit of information.
POLYGON * AddPolygon(POLYGON* Parent,D3DLVERTEX
*Vertices,WORD NOV)
{
int loop;
POLYGON * Child=new
POLYGON;
Child->NumberOfVertices=NOV;
Child->NumberOfIndices=(NOV-2)*3;
Child->Next=NULL;
Child->BeenUsedAsSplitter=false;
//
Reserve space for Vertex and Index Lists
Child->VertexList =new D3DLVERTEX
[Child->NumberOfVertices];
Child->Indices =new WORD
[Child->NumberOfIndices];
for
(loop=0;loop<NOV;loop++)
{
Child->VertexList[loop]=Vertices[loop];
}
//calculate
indices
WORD v0,v1,v2;
for
(loop=0;loop<Child->NumberOfIndices/3;loop++)
{
if
(loop==0)
{
v0=0;
v1=1;
v2=2;
}
else
{
v1=v2;
v2++;
}
Child->Indices[loop*3]=v0;
Child->Indices[(loop*3)+1]=v1;
Child->Indices[(loop*3)+2]=v2;
}
//
generate polygon normal
D3DXVECTOR3 * vec0=(D3DXVECTOR3 *)
&Child->VertexList[0];
D3DXVECTOR3 * vec1=(D3DXVECTOR3 *)
&Child->VertexList[1];
D3DXVECTOR3 * vec2=(D3DXVECTOR3 *)
&Child->VertexList[Child->NumberOfVertices-1];// the last
vert
D3DXVECTOR3 edge1=(*vec1)-(*vec0);
D3DXVECTOR3
edge2=(*vec2)-(*vec0);
D3DCVec3Cross(&Child->Normal,&edge1,&edge2)
// child->normal=CrossProduct of edge1/edge2
D3DXVec3Normalize(&Child->Normal,&Child->Normal);// and then
Normalize
if
(Parent!=NULL)
{
Parent->Next=Child;
}
return
Child;
}
If you have read the previous tutorial (and I really
hope you have) then this code will be instantly familiar to you. The only
difference here is that I now dynamically allocate the memory for both the
Vertex and Index lists. Like I said before this code was explained in detail in
Part 1 of this tutorial.API Notice for DX8/D3DX: For those of you not familiar with the D3DX functions that have now become almost an integral part if DX8 (replacing most of the framework) some of the above function may be a little strange to you.I have used them throughout this tutorial to calulate the CrossProduct,DotProduct and for Normalizing Vectors.The functions to do this are as follows:- CrossProduct(v1,v2)=D3DXVec3Cross(&result,&v1,&v2); DotProduct(v1,v2)=D3DXVec3Dot(&v1,&v2); Normalize(v1)=D3DXVec3Normalize(&result,&v1); |
void
InitPolygons(void)
{
ReserveInitialMemoryForArrays();
PolygonList=NULL;
PolygonList=LoadMWM("demolevel.mwm");
LoadTextures();
BuildBspTree(0,PolygonList);
...
...
...
}
You
can see that the next line of code to be executed is called LoadTextures and
there are no prizes for guessing what this function does. We have a global array
of Direct3DTexture8 surface pointers defined like
so:-LPDIRECT3DTEXTURE8 lpTextureSurface[25];
void
LoadTextures(void)
{
D3DXCreateTextureFromFileA(lpDevice,"checkered_floor1.jpg",&lpTextureSurface[0]);
D3DXCreateTextureFromFileA(lpDevice,"brick2.jpg", &lpTextureSurface[1]);
D3DXCreateTextureFromFileA(lpDevice,"metalrustyfloor1.jpg",&lpTextureSurface[2]);
D3DXCreateTextureFromFileA(lpDevice,"brick3.jpg", &lpTextureSurface[3]);
D3DXCreateTextureFromFileA(lpDevice,"curvyfloor.jpg",
&lpTextureSurface[4]);
D3DXCreateTextureFromFileA(lpDevice,"doomfloor.jpg",
&lpTextureSurface[5]);
D3DXCreateTextureFromFileA(lpDevice,"crate.jpg",
&lpTextureSurface[6]);
D3DXCreateTextureFromFileA(lpDevice,"stones1.jpg",
&lpTextureSurface[7]);
D3DXCreateTextureFromFileA(lpDevice,"wood1.jpg",
&lpTextureSurface[8]);
D3DXCreateTextureFromFileA(lpDevice,"wood2.jpg",
&lpTextureSurface[9]);
D3DXCreateTextureFromFileA(lpDevice,"celtic.jpg",
&lpTextureSurface[10]);
D3DXCreateTextureFromFileA(lpDevice,"celtic1.jpg",
&lpTextureSurface[11]);
D3DXCreateTextureFromFileA(lpDevice,"rock1.jpg",
&lpTextureSurface[12]);
D3DXCreateTextureFromFileA(lpDevice,"oldmetalriveted.jpg",
&lpTextureSurface[13]);
D3DXCreateTextureFromFileA(lpDevice,"stone2.jpg",
&lpTextureSurface[14]);
D3DXCreateTextureFromFileA(lpDevice,"brick1.jpg",
&lpTextureSurface[15]);
D3DXCreateTextureFromFileA(lpDevice,"concrete1.jpg",
&lpTextureSurface[16]);
D3DXCreateTextureFromFileA(lpDevice,"brickz2.jpg",
&lpTextureSurface[17]);
D3DXCreateTextureFromFileA(lpDevice,"construct2.jpg",
&lpTextureSurface[18]);
D3DXCreateTextureFromFileA(lpDevice,"construct2c.jpg",
&lpTextureSurface[19]);
D3DXCreateTextureFromFileA(lpDevice,"doomgrey1.jpg",
&lpTextureSurface[20]);
D3DXCreateTextureFromFileA(lpDevice,"doomgrey2.jpg",
&lpTextureSurface[21]);
D3DXCreateTextureFromFileA(lpDevice,"granitefloor.jpg",
&lpTextureSurface[22]);
D3DXCreateTextureFromFileA(lpDevice,"stained_glass1.jpg",
&lpTextureSurface[23]);
D3DXCreateTextureFromFileA(lpDevice,"stained_glass2.jpg",
&lpTextureSurface[24]);
}
Now, we have everything
set up.Textures Loaded,memory allocated for the initial arrays and a POLYGON *
(PolygonList) that points to the root of a linked polygon list of every polygon
in our level.I would just like to say something here that may be confusing you.We have a linked list of POLYGON structures and we also have an Array of 100 POLYGON structures.At the moment the array is totally blank. When our BSP compiler finds a convex leaf as its front list it will copy those polygons into the PolygonArray (resizing the array if needed) and will then delete the original polygons from the linked list to free up their memory because they are no longer needed. You can think of the Linked list as the unsorted data and as our BSP compiler finds the polygon that make up convex leafs they will be removed from the list and copied over to the PolygonArray ordered by the leaf they are in. In other words we never have two duplicate sets of polygons in memory at the same time.Polygons are added to the Array as they are removed from the linked list. |
BuildBspTree(0,PolygonList);
void BuildBspTree(long Node,POLYGON *
PolyList)
{
POLYGON *polyTest=NULL;
POLYGON *FrontList=NULL;
POLYGON
*BackList=NULL;
POLYGON *NextPolygon=NULL;
POLYGON
*FrontSplit=NULL;
POLYGON *BackSplit=NULL;
D3DXVECTOR3
vec1,vec2;
D3DXVECTOR3 a,b;
float result;
NodeArray[Node].Plane=SelectBestSplitter(PolyList);
polyTest=PolyList;
//
set up dummy bounding boxes for the node so it can be
altered
NodeArray[Node].BoundingBox.BoxMax=D3DXVECTOR3(-40000,-40000,-40000);
NodeArray[Node].BoundingBox.BoxMin=D3DXVECTOR3(40000,40000,40000);
First
we set up some local variables to hold the front and back lists etc just like in
the previous tutorial and then we call the SelectBestSplitter function to choose
a polygon from the list to use as a split plane.Once again the code to this
function is nearly identicle to the previous tutorial but we will look at it in
detail a little later because there are a few differences.They are:-
while (polyTest!=NULL
)
{
NextPolygon=polyTest->Next;// remember because polytest->Next
will be altered
switch
(ClassifyPoly(&PlaneArray[NodeArray[Node].Plane],polyTest))
{
case
CP_ONPLANE:
a=PlaneArray[NodeArray[Node].Plane].Normal;
b=polyTest->Normal;
result=(float)fabs ((a.x-b.x)+(a.y-b.y)+(a.z-b.z));
if
(result<0.1)
{
polyTest->Next=FrontList;
FrontList=polyTest;
}
else
{
polyTest->Next=BackList;
BackList=polyTest;
}
break;
First we initiate a 'while'
loop that will continue on through the linked list of polygons until there are
no more left.This loop will be the what takes all the polygons passed into the
function and will group them in two Linked lists (FrontLists & BackLists)
depending on their relation to the split plane.In other words when this loop
exits we will have two more linked lists Front and Back containing the polygons
that lay in that sub space. Then we make a backup copy of the Current Polygons
'Next' pointer.This is because the current polygon will be added to either the
Front or Back list using its next pointer whichl means we will loose any way of
accessing the next polygon in the initial linked list.This is no different from
what we did in the first tutorial.case
CP_FRONT:
polyTest->Next=FrontList;
FrontList=polyTest;
break;
case
CP_BACK:
polyTest->Next=BackList;
BackList=polyTest;
break;
Nothing to really talk about there then. The last
'case' we need to consider is if the current Polygon is Spanning the Current
Nodes Split Plane.In this case we have to split this polygon in two and attach
the Front half to the Front List and the back half to the back list. Once again,
if you have read the previous tutorial then all this will be instantly familiar
to you as the code is very similiar. Below shows the code for the last 'case' as
well as the tail end of the main 'while' loop.case CP_SPANNING:
FrontSplit=new POLYGON;
BackSplit=new
POLYGON;
ZeroMemory(FrontSplit,sizeof(POLYGON));
ZeroMemory(BackSplit,sizeof(POLYGON));
SplitPolygon(polyTest,&PlaneArray[NodeArray[Node].Plane],FrontSplit,BackSplit);
FrontSplit->BeenUsedAsSplitter=polyTest->BeenUsedAsSplitter;
BackSplit->BeenUsedAsSplitter=polyTest->BeenUsedAsSplitter;
FrontSplit->TextureIndex=polyTest->TextureIndex;
BackSplit->TextureIndex=polyTest->TextureIndex;
DeletePolygon
(polyTest);
FrontSplit->Next=FrontList;
FrontList=FrontSplit;
BackSplit->Next=BackList;
BackList=BackSplit;
break;
default:
break;
}
//switch
polyTest=NextPolygon;
}// end while loop
I have
highlighted bits that are new from the last tutorial.The SplitPolygon function
is virtually the same so although I will show you the code later I will not talk
about it in depth as I did this in painstaking detail in the previous
tutorial.So whats happening? We create two new polygons FrontSplit and
BackSplit.We then call SplitPolygon to split the polygon by the current Nodes
Split plane.The two splits will be returned in FrontSplit and BackSplit. The
next bit is VERY IMPORTANT. Before we delete the parent polygon (because we no
longer need it) we copy over the state of its 'BeenUsedAsSplitter' variable to
its children.This means that if the polygon that has just been split has itself
been used as a split plane in the past, then we dont want to divide the world by
the same Split plane twice.So we copy it over to the children so they can not be
selected as splitter again also.Remember that if this polygon has NOT been used
as a split plane then that state is carried over also, meaning that the Child
polygons can be used as split planes in the future. We also have to copy over
into the FrontSplit and BackSplit polygons what texture the parent used because
obviously they will use the same texture also. Remember from the previous
tutorial that the split polygon function takes care of recalculating each splits
new texture coordinates and also takes care of retriangulating (is that a word ?
:) ) the polygon to still make sure it remains as a triangle list.CalculateBox(&NodeArray[Node].BoundingBox,FrontList);
BOUNDINGBOX
LeafBox=NodeArray[Node].BoundingBox;
CalculateBox(&NodeArray[Node].BoundingBox,BackList);
int count=0;
POLYGON *
tempf=FrontList;
while (tempf!=NULL)
{
if
(tempf->BeenUsedAsSplitter==false) count++;
tempf=tempf->Next;
}
So now it is time to deal with the front and back lists and see
if we have to create more Nodes and carry on splitting etc.First we check if
'count==0' in which case the current Front List IS a leaf. Remember when looking
at the following code that both 'NumberOfPolygons' & 'NumberOfLeafs' are
global variables that keep track thoughout the building of the BSP tree how many
polygons and leafs have been added to the Polygon and Leaf array respectively.
Remember also that at start up we allocated enough space for 100 elements in
each array.Also you can see that the on the first recurse of this code both
NumberOfPolygons and NumberOfLeafs would be zero. if (
count==0)
{
POLYGON *Iterator=FrontList;
POLYGON
*Temp;
LeafArray[NumberOfLeafs].StartPolygon=NumberOfPolygons;
while
(Iterator!=NULL)
{
PolygonArray[NumberOfPolygons]=*Iterator;
IncreaseNumberOfPolygons();
Temp=Iterator;
Iterator=Iterator->Next;
delete
Temp;
}
LeafArray[NumberOfLeafs].EndPolygon=NumberOfPolygons;
LeafArray[NumberOfLeafs].BoundingBox=LeafBox;
NodeArray[Node].Front=NumberOfLeafs;
NodeArray[Node].IsLeaf=1;
IncreaseNumberOfLeafs();
}
OK its time to fill out a new Leaf Element in the Leaf
Array.Obviously this will be Leaf[0] for the first leaf created.We set its
StartPolygon value to the current number of polygons added to the Polygon
Array.This once again will be zero if this is the first leaf because polygons
are only added to the polygon array once they end up in leafs.Infact this is the
code that adds them. We then loop through every polygon in the front list and
copy it into the Polygon Array using 'PolygonArray[NumberOfPolygons]=*Iterator'.
Next up we call the function 'IncreaseNumberOfPolygons' which needs explaining.
This function basically just adds 1 on to the NumberOfPolygons variable BUT if
'NumberOfPolygons' gets larger than MAXNUMBEROFPOLYGONS (which initially starts
off at 100 remember) then the Array is resized using realloc. Each of the global
arrays (PolygonArray,NodeArray,PlaneArray etc) has a corresponding function like
this.else
{
NodeArray[Node].IsLeaf=0;
NodeArray[Node].Front=NumberOfNodes+1;
IncreaseNumberOfNodes();
BuildBspTree(NumberOfNodes,FrontList);
}
You
can see that we set 'IsLeaf' to zero because the Current Nodes 'Front' field
will not contain the index of a leaf but instead an index of another node in the
Node Array.So we set this Nodes 'Front' to index the 'Next Node we are About to
Create(NumberOfNodes+1)' and then increase the NumberOfNodes variable which is
now holding the index of this new node.Therefore the function calls itself
passing in the New Node (CurrentNode+1) index in the array and passing in the
CurrentFront list as the New Nodes PolygonList. Thats the Front List dealt
with.if
(BackList==NULL)
{
NodeArray[Node].Back=-1;
}
else
{
NodeArray[Node].Back=NumberOfNodes+1;
IncreaseNumberOfNodes();
BuildBspTree(NumberOfNodes,BackList);
}
}// end function
BuildBSPTree function complete. I will now
show the function in its entirety so you can view it in its complete form and
after that we will talk about the 'support' functions that BuildBSPTree
uses.void BuildBspTree(long Node,POLYGON *
PolyList)
{
POLYGON *polyTest=NULL;
POLYGON *FrontList=NULL;
POLYGON
*BackList=NULL;
POLYGON *NextPolygon=NULL;
POLYGON
*FrontSplit=NULL;
POLYGON *BackSplit=NULL;
D3DXVECTOR3
vec1,vec2;
D3DXVECTOR3 a,b;
float result;
NodeArray[Node].Plane=SelectBestSplitter(PolyList);
polyTest=PolyList;
NodeArray[Node].BoundingBox.BoxMax=D3DXVECTOR3(-40000,-40000,-40000);
NodeArray[Node].BoundingBox.BoxMin=D3DXVECTOR3(40000,40000,40000);
while
(polyTest!=NULL )
{
NextPolygon=polyTest->Next;// remember because
polytest->Next will be altered
switch
(ClassifyPoly(&PlaneArray[NodeArray[Node].Plane],polyTest))
{
case
CP_ONPLANE:
a=PlaneArray[NodeArray[Node].Plane].Normal;
b=polyTest->Normal;
result=(float)fabs ((a.x-b.x)+(a.y-b.y)+(a.z-b.z));
if
(result<0.1)
{
polyTest->Next=FrontList;
FrontList=polyTest;
}
else
{
polyTest->Next=BackList;
BackList=polyTest;
}
break;
case
CP_FRONT:
polyTest->Next=FrontList;
FrontList=polyTest;
break;
case
CP_BACK:
polyTest->Next=BackList;
BackList=polyTest;
break;
case CP_SPANNING:
FrontSplit=new POLYGON;
BackSplit=new
POLYGON;
ZeroMemory(FrontSplit,sizeof(POLYGON));
ZeroMemory(BackSplit,sizeof(POLYGON));
SplitPolygon(polyTest,&PlaneArray[NodeArray[Node].Plane],FrontSplit,BackSplit);
FrontSplit->BeenUsedAsSplitter=polyTest->BeenUsedAsSplitter;
BackSplit->BeenUsedAsSplitter=polyTest->BeenUsedAsSplitter;
FrontSplit->TextureIndex=polyTest->TextureIndex;
BackSplit->TextureIndex=polyTest->TextureIndex;
DeletePolygon
(polyTest);
FrontSplit->Next=FrontList;
FrontList=FrontSplit;
BackSplit->Next=BackList;
BackList=BackSplit;
break;
default:
break;
}
//switch
polyTest=NextPolygon;
}// end while loop
int
count=0;
POLYGON * tempf=FrontList;
while (tempf!=NULL)
{
if
(tempf->BeenUsedAsSplitter==false) count++;
tempf=tempf->Next;
}
CalculateBox(&NodeArray[Node].BoundingBox,FrontList);
BOUNDINGBOX
LeafBox=NodeArray[Node].BoundingBox;
CalculateBox(&NodeArray[Node].BoundingBox,BackList);
if
( count==0)
{
POLYGON *Iterator=FrontList;
POLYGON
*Temp;
LeafArray[NumberOfLeafs].StartPolygon=NumberOfPolygons;
while
(Iterator!=NULL)
{
PolygonArray[NumberOfPolygons]=*Iterator;
IncreaseNumberOfPolygons();
Temp=Iterator;
Iterator=Iterator->Next;
delete
Temp;
}
LeafArray[NumberOfLeafs].EndPolygon=NumberOfPolygons;
LeafArray[NumberOfLeafs].BoundingBox=LeafBox;
NodeArray[Node].Front=NumberOfLeafs;
NodeArray[Node].IsLeaf=1;
IncreaseNumberOfLeafs();
}
else
{
NodeArray[Node].IsLeaf=0;
NodeArray[Node].Front=NumberOfNodes+1;
IncreaseNumberOfNodes();
BuildBspTree(NumberOfNodes,FrontList);
}
if
(BackList==NULL)
{
NodeArray[Node].Back=-1;
}
else
{
NodeArray[Node].Back=NumberOfNodes+1;
IncreaseNumberOfNodes();
BuildBspTree(NumberOfNodes,BackList);
}
}// end function
You can see above that I have highlighted
in Red where support functions have been used.Read the above code and make sure
you understand how the tree is being built.It does not matter whether or not you
know how the support functions work we will look at these in a moment but make
sure you can see above what they are being used for and what they are returning
to the calling function. NodeArray[Node].Plane=SelectBestSplitter(PolyList);
long
SelectBestSplitter(POLYGON *PolyList)
{
POLYGON*
Splitter=PolyList;
POLYGON* CurrentPoly=NULL;
unsigned long
BestScore=1000000;
POLYGON * SelectedPoly=NULL;
while
(Splitter!=NULL)
{
if
(Splitter->BeenUsedAsSplitter!=true)
{
PLANE
SplittersPlane;
SplittersPlane.Normal=Splitter->Normal;
SplittersPlane.PointOnPlane=*(D3DXVECTOR3
*)&Splitter->VertexList[0];
CurrentPoly=PolyList;
unsigned long
score,splits,backfaces,frontfaces;
score=splits=backfaces=frontfaces=0;
while
(CurrentPoly!=NULL)
{
int
result=ClassifyPoly(&SplittersPlane,CurrentPoly);
switch (
result)
{
case CP_ONPLANE:
break;
case
CP_FRONT:
frontfaces++;
break;
case
CP_BACK:
backfaces++;
break;
case
CP_SPANNING:
splits++;
break;
default:
break;
}//
switch
CurrentPoly=CurrentPoly->Next;
}// end while current
poly
score=abs(frontfaces-backfaces)+(splits*3);
if
(score<BestScore)
{
BestScore=score;
SelectedPoly=Splitter;
}
}//
end if this splitter has not been used yet
Splitter=Splitter->Next;
}//
end while splitter != null
SelectedPoly->BeenUsedAsSplitter=true;
PlaneArray[NumberOfPlanes].PointOnPlane=*((D3DXVECTOR3
*)&SelectedPoly->VertexList[0]);
PlaneArray[NumberOfPlanes].Normal=SelectedPoly->Normal;
IncreaseNumberOfPlanes();
return
(NumberOfPlanes-1);
} // End Function
You should recognize most
of this stuff from the previous tutorial but there are some differences so I
will summerize.void
CalculateBox(BOUNDINGBOX *Box,POLYGON *Polylist)
{
WORD i;
POLYGON
*PolyPointer=Polylist ;
while (PolyPointer!=NULL)
{
for
(i=0;i<PolyPointer->NumberOfVertices;i++)
{
// check Minimum
Bounds
if (PolyPointer->VertexList[i].x<Box->BoxMin.x)
Box->BoxMin.x=PolyPointer->VertexList[i].x;
if
(PolyPointer->VertexList[i].y<Box->BoxMin.y)
Box->BoxMin.y=PolyPointer->VertexList[i].y;
if
(PolyPointer->VertexList[i].z<Box->BoxMin.z)
Box->BoxMin.z=PolyPointer->VertexList[i].z;
// check Maximum
Bounds
if (PolyPointer->VertexList[i].x>Box->BoxMax.x)
Box->BoxMax.x=PolyPointer->VertexList[i].x;
if
(PolyPointer->VertexList[i].y>Box->BoxMax.y)
Box->BoxMax.y=PolyPointer->VertexList[i].y;
if
(PolyPointer->VertexList[i].z>Box->BoxMax.z)
Box->BoxMax.z=PolyPointer->VertexList[i].z;
}
PolyPointer=PolyPointer->Next;
}
}
Another
support function used by the BuildBSPTree function is one called 'DeletePolygon'
and this function can be found in 'memoryalloc.cpp'.We call this function
whenever we want to remove a polygon from memory.Remember that calling the c++
'delete' command will remove the memory allocated by the Polygon structure
itself BUT will not remove the memory allocated dynamically for the Vertex and
Index arrays that are pointed to by the polygon structure.This would cause a
memory leak.In short then we have to delete the Index and Vertex List memory
first (while we still have pointers to the memory) and then delete the Polygon
structure. Here is the DeletePolygon function.void DeletePolygon
(POLYGON *Poly)
{
delete Poly->VertexList;
delete
Poly->Indices;
delete Poly;
}
Another thing that can look
quite confusing is the fact that when we copy over the FrontList polygons over
into a new Leaf (if count==0) we do infact call the c++ command 'delete' instead
of our function 'DeletePolygons'.This is because although we no longer need the
polygon structure (the one in the front list) itself any more the memory for the
Vertex Array and the Index Array will still be pointed at by the polygon
structure in the PolygonArray.There for we do not want the vertex or index
memory released. void
IncreaseNumberOfNodes(void)
{
NumberOfNodes++;
if
(NumberOfNodes==MAXNUMBEROFNODES)
{
MAXNUMBEROFNODES+=100;
NodeArray=(NODE
*)realloc (NodeArray,MAXNUMBEROFNODES *
sizeof(NODE));
}
}
You can see that we increase
NumberOfNodes variable and also check if this takes the number of nodes past the
current size of the array.If so we add 100 to the size of the array and resize
the memory block calling the C function 'realloc'. int ClassifyPoly(PLANE
*Plane,POLYGON * Poly)
{
int Infront=0;
int Behind=0;
int
OnPlane=0;
float result;
D3DXVECTOR3 *vec1=(D3DXVECTOR3
*)&Plane->PointOnPlane;
for (int
a=0;a<Poly->NumberOfVertices;a++)
{
D3DXVECTOR3 *vec2=(D3DXVECTOR3
*)&Poly->VertexList[a];
D3DXVECTOR3
Direction=(*vec1)-(*vec2);
result=D3DXVec3Dot(Direction,Plane->Normal);
if
(result>0.001)
{
Behind++;
}
else
if
(result<-0.001)
{
Infront++;
}
else
{
OnPlane++;
Infront++;
Behind++;
}
}
if (OnPlane==Poly->NumberOfVertices) return
CP_ONPLANE;
if (Behind==Poly->NumberOfVertices) return
CP_BACK;
if (Infront==Poly->NumberOfVertices) return CP_FRONT;
return
CP_SPANNING;
}
The last function in the BuildBSPTree function
that we have not talked about is the SplitPolygon function.As I said earlier
there really was a quite large and detailed explanation about this function in
the Part 1 tutorial so please refer back to this tutorial if you are having
trouble remembering or if you do not understand the code. For those of you that
can remember I will just show it here as a refresher and to show whats changed
(as of writing I have not yet updated Part1 from dx7 to dx8 so here it is in its
new form using the D3DX functions instead of the D3D Framework from dx7. Apart
from these changes it is exactly the same).void SplitPolygon(POLYGON
*Poly,PLANE *Plane,POLYGON *FrontSplit,POLYGON *BackSplit)
{
D3DLVERTEX
FrontList[40],BackList[40],FirstVertex;
D3DXVECTOR3
PlaneNormal,IntersectPoint,PointOnPlane,PointA,PointB;
WORD
FrontCounter=0,BackCounter=0,loop=0,CurrentVertex=0;
float percent; // used
for return storage of percentage
// Find any vertex on the plane to use
later in plane intersect
routine
PointOnPlane=Plane->PointOnPlane;
// first we find out if
the first vertex belongs in front or back
list
FirstVertex=Poly->VertexList[0];
switch (ClassifyPoint(
(D3DXVECTOR3 *)&FirstVertex,Plane))
{
case
CP_FRONT:
FrontList[FrontCounter++]=FirstVertex;
break;
case
CP_BACK:
BackList[BackCounter++]=FirstVertex;
break;
case
CP_ONPLANE:
BackList[BackCounter++]=FirstVertex;
FrontList[FrontCounter++]=FirstVertex;
break;
default:
PostQuitMessage(0);;
}
for
(loop=1;loop<Poly->NumberOfVertices+1;loop++)
{
if
(loop==Poly->NumberOfVertices)
{
CurrentVertex=0;
}
else
{
CurrentVertex=loop;
}
PointA=*(D3DXVECTOR3
*)&Poly->VertexList[loop-1];
PointB=*(D3DXVECTOR3
*)&Poly->VertexList[CurrentVertex];
PlaneNormal=Plane->Normal;
int
Result=ClassifyPoint(&PointB,Plane);
if
(Result==CP_ONPLANE)
{
BackList[BackCounter++]=Poly->VertexList[CurrentVertex];
FrontList[FrontCounter++]=Poly->VertexList[CurrentVertex];
}
else
{
if
(Get_Intersect(&PointA,&PointB,&PointOnPlane,&PlaneNormal,&IntersectPoint,
&percent)==true)
{
// create new vertex and calculate new texture
coordinate
float
deltax,deltay,texx,texy;
deltax=Poly->VertexList[CurrentVertex].tu-Poly->VertexList[loop-1].tu;
deltay=Poly->VertexList[CurrentVertex].tv-Poly->VertexList[loop-1].tv;
texx=Poly->VertexList[loop-1].tu+(deltax*percent);
texy=Poly->VertexList[loop-1].tv+(deltay*percent);
D3DLVERTEX
copy;//=D3DLVERTEX(IntersectPoint,D3DCOLOR_ARGB(255,255,255,255),0,texx,texy);
copy.x=IntersectPoint.x;
copy.y=IntersectPoint.y;
copy.z=IntersectPoint.z;
copy.color=D3DCOLOR_ARGB(255,255,255,255);
copy.specular=0;
copy.tu=texx;
copy.tv=texy;
if
(Result==CP_FRONT )
{
BackList[BackCounter++]=copy;
FrontList[FrontCounter++]=copy;
if
(CurrentVertex!=0)
{
FrontList[FrontCounter++]=Poly->VertexList[CurrentVertex];
}
}
if
(Result==CP_BACK)
{
FrontList[FrontCounter++]=copy;
BackList[BackCounter++]=copy;
if
(CurrentVertex!=0)
{
BackList[BackCounter++]=Poly->VertexList[CurrentVertex];
}
}
}// end get intersect
else
{
if
(Result==CP_FRONT)
{
if
(CurrentVertex!=0)
{
FrontList[FrontCounter++]=Poly->VertexList[CurrentVertex];
}
}
if
(Result==CP_BACK)
{
if
(CurrentVertex!=0)
{
BackList[BackCounter++]=Poly->VertexList[CurrentVertex];
}
}
}
}//
end for not onplane
}//end for loop
//OK THEN LETS BUILD THESE TWO
POLYGONAL BAD BOYS :)
FrontSplit->NumberOfVertices=0;
BackSplit->NumberOfVertices=0;
//
Reserve Memory for Front and Back Vertex Lists
FrontSplit ->VertexList=
new D3DLVERTEX [FrontCounter];
BackSplit ->VertexList= new D3DLVERTEX
[BackCounter ];
for
(loop=0;loop<FrontCounter;loop++)
{
FrontSplit->NumberOfVertices++;
FrontSplit->VertexList[loop]=FrontList[loop];
}
for
(loop=0;loop<BackCounter;loop++)
{
BackSplit->NumberOfVertices++;
BackSplit->VertexList[loop]=BackList[loop];
}
BackSplit->NumberOfIndices=(BackSplit->NumberOfVertices-2)*3;
FrontSplit->NumberOfIndices=(FrontSplit->NumberOfVertices-2)*3;
//
Reserve Memory for Front and Back Index Lists
FrontSplit ->Indices= new
WORD [FrontSplit->NumberOfIndices];
BackSplit ->Indices= new WORD
[BackSplit ->NumberOfIndices];
// Fill in the Indices
WORD
v0,v1,v2;
for
(loop=0;loop<FrontSplit->NumberOfIndices/3;loop++)
{
if (loop==0)
{
v0=0;
v1=1;
v2=2;
}
else
{
v1=v2;
v2++;
}
FrontSplit->Indices[loop*3]=v0;
FrontSplit->Indices[(loop*3)+1]=v1;
FrontSplit->Indices[(loop*3)+2]=v2;
}
for
(loop=0;loop<BackSplit->NumberOfIndices/3;loop++)
{
if (loop==0)
{
v0=0;
v1=1;
v2=2;
}
else
{
v1=v2;
v2++;
}
BackSplit->Indices[loop*3]=v0;
BackSplit->Indices[(loop*3)+1]=v1;
BackSplit->Indices[(loop*3)+2]=v2;
}
FrontSplit->Normal=Poly->Normal;
BackSplit->Normal=Poly->Normal;
}
void RenderTree(D3DVECTOR pos)
{
int Node=0;
int
leaf=0;
This function will be called once per frame to render the tree
and will be passed in the Position of the Camera/Player in the game world.. This
code finds the LEAF the camera is in and then calls DrawTree to render that
leaf.We set 'Node' to zero at first obviously because we need to start testing
our camera position at the root of the tree.while(1)
{
switch(ClassifyPoint(&pos,&PlaneArray[NodeArray[Node].Plane]))
{
case CP_FRONT:
if
(NodeArray[Node].IsLeaf!=0)
{
leaf=NodeArray[Node].Front;
DrawTree(leaf);
return;
}
else
{
Node
= NodeArray[Node].Front;
}
break;
If the camera position is
infront of this 'Node' then we need to travel down this Nodes front tree to
further refine the search.HOWEVER, the Front of this tree may not point to
another Node and may in fact be the Leaf we are looking for so we check the
Nodes 'IsLeaf' variable to see of this is the case.If 'IsLeaf!=0' then we are at
the end branch of this tree and this Nodes Front pointer actually contains the
Index(into the LeafArray) of the Leaf we are in.This means our search is over.We
simply call 'DrawTree' and pass in this Index so that the DrawTree function can
render the Leafs PVS Set. Then we return from this function because the job is
done.case CP_BACK:
if
(NodeArray[Node].Back==-1)
{
return;
}
else
{
Node =
NodeArray[Node].Back;
}
break;
As with the 'CP_FRONT' case
we have to check to see that we can go down the Back of this node.If this Nodes
'Back' variable equals '-1' then there are no more Nodes to travserse and the
camera is infact in solid space. This should NEVER happen becuse it would mean
that your camera is inside a wall or something like that but we dont want any
crashes before you implement your collision detection because then you could
walk through a wall accidentally.To keep things safe I simply return if the
camera is in solid space which means nothing gets rendered.Like I said though,
this should never happen.case
CP_ONPLANE:
if
(NodeArray[Node].IsLeaf!=0)
{
leaf=NodeArray[Node].Front;
DrawTree(leaf);
return;
}
else
{
Node
= NodeArray[Node].Front;
}
default:
break;
}
}// end
while
}// End Function
void RenderTree(D3DVECTOR pos)
{
int
Node=0;
int
leaf=0;
while(1)
{
switch(ClassifyPoint(&pos,&PlaneArray[NodeArray[Node].Plane]))
{
case CP_FRONT:
if
(NodeArray[Node].IsLeaf!=0)
{
leaf=NodeArray[Node].Front;
DrawTree(leaf);
return;
}
else
{
Node
= NodeArray[Node].Front;
}
break;
case CP_BACK:
if
(NodeArray[Node].Back==-1)
{
return;
}
else
{
Node =
NodeArray[Node].Back;
}
break;
case
CP_ONPLANE:
if
(NodeArray[Node].IsLeaf!=0)
{
leaf=NodeArray[Node].Front;
DrawTree(leaf);
return;
}
else
{
Node
= NodeArray[Node].Front;
}
default:
break;
}
}// end
while
}// End Function
void DrawTree(long leaf)
{
POLYGON *CurrentPoly;
int
i;
long PVSOFFSET=LeafArray[leaf].PVSIndex;
BYTE
*PVSPointer=PVSData;
PVSPointer+=PVSOFFSET;
long
currentleaf=0;
Above you can see the code creates a BYTE pointer
to point to the first BYTE of this Leafs Visibility information.The Leaf itself
holds the Number of Bytes in from the beginning of the array (stored in
PVSIndex) so this is the offset into the master PVSData array where this leafs
visibility information starts.So we create a pointer to the PVSData array and
simply add the offset on.PVSPointer now points at this Leafs PVS information.
Current Leaf is set to zero. This variable will keep track of which leaf in THIS
leafs PVS information we are currently checking the visibility BIT of. When
Current Leaf equals NumberOfLeafs we know we have checked all the leafs in this
leafs PVS information and nothing more has to be rendered.// Set
All the Texture Batch Pointers to NULL
for
(i=0;i<25;i++)
{
pTexturePolygonList[i]=NULL;
}
Ok
thats done. Now its time to check the Visibility information for this leaf.We
create a while loop that wil exit once CurrentLeaf=NumberOfLeafs because this
means we have checked all the visibility information and we now have our 25
linked lists of polygons ready to be rendered.. while
(currentleaf<NumberOfLeafs)
{
if (*PVSPointer!=0)
{
for
(i=0;i<8;i++)
{
BYTE mask=1<<i;
BYTE
pvs=*PVSPointer;
if (pvs&mask)
{
First we check if
the current Byte is non zero.If it is non zero then it means some of the BITs
are set to one in this byte so at least one(but could be all) of the leafs
represented by this byte must be visible. if (LeafInFrustum(currentleaf)==true ||
DontFrustumReject==true)
{
unsigned long
start=LeafArray[currentleaf].StartPolygon;
unsigned long
finish=LeafArray[currentleaf].EndPolygon;
unsigned long Count=0;
for
(Count=start;Count<finish;Count++) // Collect the polys and add to Texture
List
{
CurrentPoly=&PolygonArray[Count];
CurrentPoly->Next=pTexturePolygonList[CurrentPoly->TextureIndex];
pTexturePolygonList[CurrentPoly->TextureIndex]=CurrentPoly;
}
}//
end if leaf infrustum
}// end for if pvsdata
currentleaf++;
}// end for
i;
PVSPointer++;
}
else
{// we have hit a zero so read in next byte for run
length
PVSPointer++;
BYTE
RunLength=*PVSPointer;
PVSPointer++;
currentleaf+=RunLength*8;
}
}//
end for while
Above shows what happen if the Current Byte the PVS
data is zero. We read in the next Byte which will tell us how many zero bytes
are in a run and we adjust our 'currentleaf' parameter so that it skips past all
the leafs in the run. Remember that RunLength is the number of bytes in th run
and each byte hold 8 bits of information for each leaf so that is why we adjust
'currentleaf' by 'Runlength*8'.for (i=0;i<NumberOfTextures;i++)
{
if
(pTexturePolygonList[i]!=NULL)
{
lpDevice->SetTexture(0,lpTextureSurface[i]);
CurrentPoly=pTexturePolygonList[i];
lpDevice->SetVertexShader(
D3DFVF_LVERTEX );
while
(CurrentPoly!=NULL)
{
lpDevice->DrawIndexedPrimitiveUP(D3DPT_TRIANGLELIST,0,CurrentPoly->NumberOfVertices,
CurrentPoly->NumberOfIndices/3,&CurrentPoly->Indices[0],
D3DFMT_INDEX16
,&CurrentPoly->VertexList[0],sizeof(D3DLVERTEX));
CurrentPoly=CurrentPoly->Next;
}
}
}
lpDevice->SetTexture(0,NULL);
}//End
Function
All done . Scene rendered. Above we Set the current
texture in TextureStage 0 and also use the Temporary variable 'CurrentPoly' to
point at the head of each List.This will be used to step though each polygon
list.We then use 'CurrentPoly' to iterate through the Linked List until we reach
the end of that list.When there is a polygon we Render it using
DrawIndexedPrimitiveUP.DX8 NOTE For those of you that have not used DrawIndexedPrimitiveUP before the syntax is:- DrawIndexedPrimitiveUP( 'MinIndex' is the index of the
Minimum Vertex in the vertex array that will be used by this call.Because
every Polygon in the scene has its own Vertex List we want to use all the
vertices for this polygon so we start at zero. Primitive Count is a new
parameter also.Instead of specifying the Number OF indices you now specify
how many primitives will have to be rendered to make up this
polygon.Remember your polygon may have been broken down into many
triangles by the 'AddPolygon' function. For triangle Lists it works out we
can just divide the Number Of Indices by three to get the primitive count.
Above I have used D3DFMT_INDEX16 to tell direct3d that my Indices array
holds 16bit (WORD) values.You can have 32 bit indices now if you need
them. The one from last parameter is just a pointer to your vertex data
and the last parameter describes the size to step between each vertex to
get to the next one in the array (in bytes).This is equal tothe size of
out D3DLVERTX structure. |
void DrawTree(long
leaf)
{
POLYGON *CurrentPoly;
int i;
long
PVSOFFSET=LeafArray[leaf].PVSIndex;
BYTE
*PVSPointer=PVSData;
PVSPointer+=PVSOFFSET;
long currentleaf=0;
//
Set All the Texture Batch Pointers to NULL
for
(i=0;i<25;i++)
{
pTexturePolygonList[i]=NULL;
}
while
(currentleaf<NumberOfLeafs)
{
if (*PVSPointer!=0)
{
for
(i=0;i<8;i++)
{
BYTE mask=1<<i;
BYTE pvs=*PVSPointer;
if
(pvs&mask)
{
if (LeafInFrustum(currentleaf)==true ||
DontFrustumReject==true)
{
unsigned long
start=LeafArray[currentleaf].StartPolygon;
unsigned long
finish=LeafArray[currentleaf].EndPolygon;
unsigned long Count=0;
for
(Count=start;Count<finish;Count++)
{
CurrentPoly=&PolygonArray[Count];
CurrentPoly->Next=pTexturePolygonList[CurrentPoly->TextureIndex];
pTexturePolygonList[CurrentPoly->TextureIndex]=CurrentPoly; }
}// end if
leaf infrustum
}// end for if pvsdata
currentleaf++;
}// end for
i;
PVSPointer++;
}// if pvspointer!=0; In other words if this is not a
compressed byte
else
{// we have hit a zero so read in the next
byte and see how long the run of zeros is
PVSPointer++;
BYTE
RunLength=*PVSPointer;
PVSPointer++;
currentleaf+=RunLength*8;
}
}//
end for while
//Render Polygons in the textures lists.
for
(i=0;i<NumberOfTextures;i++)
{
if
(pTexturePolygonList[i]!=NULL)
{
lpDevice->SetTexture(0,lpTextureSurface[i]);
CurrentPoly=pTexturePolygonList[i];
lpDevice->SetVertexShader(
D3DFVF_LVERTEX );
while
(CurrentPoly!=NULL)
{
lpDevice->DrawIndexedPrimitiveUP(D3DPT_TRIANGLELIST,0,CurrentPoly->NumberOfVertices,
CurrentPoly->NumberOfIndices/3,&CurrentPoly->Indices[0],
D3DFMT_INDEX16
,&CurrentPoly->VertexList[0],sizeof(D3DLVERTEX));
CurrentPoly=CurrentPoly->Next;
}
}
}
lpDevice->SetTexture(0,NULL);
}//End
Function
bool LineOfSight (D3DXVECTOR3
*Start,D3DXVECTOR3 *End, long Node)
{
float temp;
D3DXVECTOR3
intersection;
NODE *CurrNode=&NodeArray[Node];
PLANE
*Plane=&PlaneArray[CurrNode->Plane];
int
PointA=ClassifyPoint(Start, Plane);
int PointB=ClassifyPoint(End,
Plane);
Point A and Point B hold the Start and End points of the
Ray we are using to test against the tree.If BOTH points are on the Current
Nodes Planes then we will simply treat it as being in front of the plane and
recur down the Front of this node.We check this Nodes IsLeaf variable just to
check that there is another Node down the front tree of this node.If there is
then we recurse down that tree with the Ray.If IsLeaf is non zero however then
there is not another NOde and the Front Variable holds the Leaf index.We can
return true because Leafs are empty space and both points are obviously in this
leaf.if (PointA==CP_ONPLANE &&
PointB==CP_ONPLANE)
{
if (CurrNode->IsLeaf==0)// this is not a leaf so
recurse
{
return
LineOfSight(Start,End,CurrNode->Front);
}
else
{
return true; //
This is a front leaf.Front Leafs are always empty so this is empty
space
}
}
if
(PointA==CP_FRONT && PointB==CP_BACK)
{
if (CurrNode->Back==-1)
return false;
Get_Intersect
(Start,End,&Plane->PointOnPlane,&Plane->Normal,&intersection,&temp);
if
(CurrNode->IsLeaf==0)
{
return
LineOfSight(Start,&intersection,CurrNode->Front) &&
LineOfSight(End,&intersection,CurrNode->Back)
;
}
else
{
return true &&
LineOfSight(End,&intersection,CurrNode->Back);
}
}
if
(PointA==CP_BACK && PointB==CP_FRONT)
{
if (CurrNode->Back==-1)
return false;
Get_Intersect
(Start,End,&Plane->PointOnPlane,&Plane->Normal,&intersection,&temp);
if
(CurrNode->IsLeaf==0)
{
return
LineOfSight(End,&intersection,CurrNode->Front) &&
LineOfSight(Start,&intersection,CurrNode->Back)
;
}
else
{
return true &&
LineOfSight(Start,&intersection,CurrNode->Back);
}
}
If
we get as far as here it must mean one of the points is on the plane and the
other is not.In this case we treat the point ON the plane as being the same as
the Point not on the plane.In other words we send it down the side of the tree
that the other point lays in.
if (PointA==CP_FRONT ||
PointB==CP_FRONT)
{
if (CurrNode->IsLeaf==0)
{
return
LineOfSight(Start,End,CurrNode->Front);
}
else
{
return
true;
}
}
else
{
if
(CurrNode->Back==-1)
{
return false;
}
else
{
return
LineOfSight(Start,End,CurrNode->Back);
}
}
return
true;
}
Thankfully we have now Ported over all our Solid
Node Based functionality to work with the Solid Leaf Tree which means its time
to move on to the interesting stuff. Please make sure you understand everything
we have talked about above before reading on because we are now going to cover
calculating the PVS for the tree and it is vital that you understand how the
Solid Leaf Tree works and how it is laid out in memory. With that said, lets
move onto the good stuff. struct LEAF{
long
StartPolygon;
long EndPolygon;
long PortalIndexList[50];
long NumberOfPortals;
long PVSIndex;
BOUNDINGBOX
BoundingBox;
};
Above I have highlighted the two fields we have
not yet discussed.NumberOfPortals simply holds the NumberOfPortals that this
leaf has in it.Remember that a room could have many many doorways.
PortalIndexList is just an array in indexes into the master Portal Array.We will
talk about the Portal Stucture in a moment but just know for now that this array
is just like the other master arrays (LeafArray,NodeArray etc) except that it
holds Pointers to PORTALs.When the Valid Portals are created they will be added
to this array.struct PORTAL
{
D3DLVERTEX *VertexList;
D3DXVECTOR3 Normal;
WORD
NumberOfVertices;
WORD NumberOfIndices;
WORD *Indices;
PORTAL *
Next;
PORTAL * Prev;
BYTE NumberOfLeafs;
long
LeafOwnerArray[2];
};
This first five fields of this structure
are the same as the POLYGON structure.This allows us to easily cast a PORTAL
pointer to a POLYGON Pointer so that we can pass in Portals to our
'SplitPolygon','ClassifyPoly' & 'ClassifyPoint' functions.This structure
though has a Next and a Prev pointer that will be used to point to other portals
in linked lists.We use the previous portal so that we can remove portals from
the linked list without destroying the chain.We will see this in action
shortly.PORTAL * CalculateInitialPortal(long
Node)
{
D3DXVECTOR3
MaxP,MinP,CB,CP;
MaxP=NodeArray[Node].BoundingBox.BoxMax;// Get this Nodes
Bounding Box ranges
MinP=NodeArray[Node].BoundingBox.BoxMin;
D3DXVECTOR3
PlaneNormal=PlaneArray[NodeArray[Node].Plane].Normal;
CB=(MaxP+MinP)/2;
float
DistanceToPlane=D3DXVec3Dot(&(PlaneArray[NodeArray[Node].Plane].PointOnPlane-CB),&PlaneNormal);
CP=CB+(PlaneNormal*DistanceToPlane);
First
we get the Nodes Bounding Box MAX and MIN points and use them to calculate the
Center of the Bounding Box ( CB ). Next we use the dot product to find the
distance from 'CB' to the Plane. This is just standard DotProduct stuff so if
you are not familiar you may want to read our DotProduct
Tutorial. Anyway if we create a Vector from the Box Center Point (CP) to a Point
known to be on the Plane (we have this stored in the Nodes Plane structure) and
then Dotproduct that vector with the Plane Normal we get returned the Shorted
Distance to the plane from this point. If we travel in the direction of the
Plane Normal from 'CB' we will be on the plane at point 'CP'.CP will be the
Center of the Portal we are going to create. Look below at Figure Q which shows
a Portal being calculated (SIDE VIEW like a cross section).
D3DXVECTOR3 A=D3DXVECTOR3(0.0f,0.0f,0.0f);
if(
fabs(PlaneNormal.y) > fabs(PlaneNormal.z) )
{
if( fabs(PlaneNormal.z)
< fabs(PlaneNormal.x) )
{
A.z = 1;
}
else
{
A.x = 1;
}
}
else
{
if (fabs(PlaneNormal.y )<= fabs(PlaneNormal.x) )
{
A.y = 1;
}
else
{
A.x =
1;
}
}
At this point, Vector 'A' will either contain
(1,0,0),(0,1,0) or (0,0,1) depending on which vector is least aligned with the
plane normal. All we have to do now is Cross Vector A with the Plane Normal to
get Vector U (our right vector).We Normalize (make it a unit vector) U and then
Cross 'U' with the Plane Nomral to get Vector 'V' our up vector.Once again
Normalizing the Result.D3DXVec3Cross
(&U,&A,&PlaneNormal);
D3DXVec3Normalize(&U,&U);
D3DXVec3Cross
(&V,&U,&PlaneNormal);
D3DXVec3Normalize(&V,&V);
We
now have our U and V vectors which along with our Plane Normal vector describes
the Local Axis of the Portal. At this point I am now going to show Figure Q)
again to save you scrolling back up everytime I refer to the diagram.
D3DXVECTOR3 BoxHalfLength=(MaxP-CB);
float
Length=D3DXVec3Length(&BoxHalfLength);
U=U*Length;
V=V*Length;
With
these Vectors now scaled like this we can create the the four vertices for our
portal like so:-D3DXVECTOR3 P[4];
P[0] = CP + U -
V;// Bottom Right
P[1] = CP + U + V;// Top Right
P[2] = CP - U + V;// Top
Left
P[3] = CP - U - V;// Bottom Left
Thats the vertices
created for the portal.Now we just fill in the Portal Structure much the same
way we fill in the Polygon Structure in the 'AddPolygon' structure.We break the
square into two triangles indexed by 6 indices.PORTAL
*Portal=new
PORTAL;
ZeroMemory(Portal,sizeof(PORTAL));
Portal->Normal=PlaneNormal;
Portal->NumberOfVertices=4;
Portal->NumberOfIndices=6;
Portal->VertexList
= new D3DLVERTEX [Portal->NumberOfVertices];
Portal->Indices = new WORD
[Portal->NumberOfIndices ];
for (int
i=0;i<4;i++)
{
Portal->VertexList[i].x=P[i].x;
Portal->VertexList[i].y=P[i].y;
Portal->VertexList[i].z=P[i].z;
Portal->VertexList[i].color=D3DCOLOR_RGBA(rand()%255,rand()%255,rand()%255,240);
Portal->VertexList[i].specular=0;
Portal->VertexList[i].tu=0;
Portal->VertexList[i].tv=0;
}
Portal->Indices[0]=0;
Portal->Indices[1]=1;
Portal->Indices[2]=3;
Portal->Indices[3]=3;
Portal->Indices[4]=1;
Portal->Indices[5]=2;
Portal->Next=NULL;
Portal->Prev=NULL;
Portal->NumberOfLeafs=0;
return
Portal;
}
Thats the 'CalculateInitialPortal' function
done then. I will just show it in its complete form now (without my
interruptions every couple of lines) so you can follow it more
easily.PORTAL * CalculateInitialPortal(long
Node)
{
D3DXVECTOR3
MaxP,MinP,CB,CP;
MaxP=NodeArray[Node].BoundingBox.BoxMax;// Get this Nodes
Bounding Box ranges
MinP=NodeArray[Node].BoundingBox.BoxMin;
D3DXVECTOR3
PlaneNormal=PlaneArray[NodeArray[Node].Plane].Normal;
CB=(MaxP+MinP)/2;
float
DistanceToPlane=D3DXVec3Dot(&(PlaneArray[NodeArray[Node].Plane].PointOnPlane-CB),&PlaneNormal);
CP=CB+(PlaneNormal*DistanceToPlane);
D3DXVECTOR3 A=D3DXVECTOR3(0.0f,0.0f,0.0f);
if( fabs(PlaneNormal.y)
> fabs(PlaneNormal.z) )
{
if( fabs(PlaneNormal.z) <
fabs(PlaneNormal.x) )
{
A.z = 1;
}
else
{
A.x = 1;
}
}
else
{
if (fabs(PlaneNormal.y )<= fabs(PlaneNormal.x) )
{
A.y = 1;
}
else
{
A.x =
1;
}
}
D3DXVec3Cross
(&U,&A,&PlaneNormal);
D3DXVec3Normalize(&U,&U);
D3DXVec3Cross
(&V,&U,&PlaneNormal);
D3DXVec3Normalize(&V,&V);
D3DXVECTOR3
BoxHalfLength=(MaxP-CB);
float
Length=D3DXVec3Length(&BoxHalfLength);
U=U*Length;
V=V*Length;
D3DXVECTOR3 P[4];
P[0] = CP + U - V;// Bottom Right
P[1] = CP + U +
V;// Top Right
P[2] = CP - U + V;// Top Left
P[3] = CP - U - V;// Bottom
Left
PORTAL *Portal=new
PORTAL;
ZeroMemory(Portal,sizeof(PORTAL));
Portal->Normal=PlaneNormal;
Portal->NumberOfVertices=4;
Portal->NumberOfIndices=6;
Portal->VertexList
= new D3DLVERTEX [Portal->NumberOfVertices];
Portal->Indices = new WORD
[Portal->NumberOfIndices ];
for (int
i=0;i<4;i++)
{
Portal->VertexList[i].x=P[i].x;
Portal->VertexList[i].y=P[i].y;
Portal->VertexList[i].z=P[i].z;
Portal->VertexList[i].color=D3DCOLOR_RGBA(rand()%255,rand()%255,rand()%255,240);
Portal->VertexList[i].specular=0;
Portal->VertexList[i].tu=0;
Portal->VertexList[i].tv=0;
}
Portal->Indices[0]=0;
Portal->Indices[1]=1;
Portal->Indices[2]=3;
Portal->Indices[3]=3;
Portal->Indices[4]=1;
Portal->Indices[5]=2;
Portal->Next=NULL;
Portal->Prev=NULL;
Portal->NumberOfLeafs=0;
return
Portal;
}
That wasn't so bad was it ?PORTAL * ClipPortal(long Node,PORTAL
*Portal)
{
PORTAL
*PortalList=NULL,*FrontPortalList=NULL,*BackPortalList=NULL,*Iterator=NULL,*FrontSplit=NULL,*BackSplit=NULL;
switch
(ClassifyPoly(&PlaneArray[NodeArray[Node].Plane],(POLYGON
*)Portal))
{
We will call ClipPortal initially from out
BuildPortals function (which we will look at later) and pass in the
InitialPortal we created by calling 'CalculateInitialPortal'. We will also pass
in a value of Zero because we want the clipping to start for the portal at the
Root node of the BSP Tree. Next we do something you should be very familiar with
by now, we classify the Portal against the Node Plane. Notice above that because
the first part of our PORTAL structure is the same as our POLYGON structure we
can safely cast the PORTAL to type POLYGON to save have to re write the
functions to deal with portals. The Rest of the function simply is divided into
four 'Case' blocks for our switch statement with instructions on what to do if
the Portal is CP_FRONT,CP_BACK,CP_SPANNING or CP_ONPLANE. We will look at these
cases one at a time starting with CP_FRONT. Also it may help to refer back to
Diagrams K through P while reading the code as this is where we showed the
Portal being clipped Graphically which may help you picture it in your head.
case CP_FRONT:
if (NodeArray[Node].IsLeaf==0)
// Not a Leaf
{
PortalList=ClipPortal(NodeArray[Node].Front,Portal);
return PortalList;
}
else // Is a
Leaf
{
Portal->LeafOwnerArray[Portal->NumberOfLeafs]=NodeArray[Node].Front;
Portal->NumberOfLeafs++;
Portal->Next=NULL;
Portal->Prev=NULL;
return
Portal;
}
break;
Phew that was nice and easy.
Luckily the CP_BACK case is equally as stress free.case
CP_BACK:
if (NodeArray[Node].Back!=-1)// another Node
exists
{
PortalList=ClipPortal(NodeArray[Node].Back,Portal);
return
PortalList;
}
else// were are in solid space
{
DeletePortal (
Portal);
return NULL;
}
break;
So as you can see
if a portal (or more likely a portal fragment) end up in the Front or Back
'case' it is either passed down the front or back of the tree,deleted or
returned having had its Leaf information saved. It is not very likely that our
Initial Portal will ever fall into these two cases because it is very large and
will be spanning several polygons.The next 'case' (CP_SPANNING) is the code that
is responsible for splitting the Portals into fragments and sending each
Fragment down the tree keeping track of which fragments survive and returning
all the fragments from the function in a Linked List.case CP_SPANNING:
FrontSplit=new
PORTAL;
BackSplit=new
PORTAL;
ZeroMemory(FrontSplit,sizeof(PORTAL));
ZeroMemory(BackSplit,sizeof(PORTAL));
SplitPortal(Portal,&PlaneArray[NodeArray[Node].Plane],FrontSplit,BackSplit);
DeletePortal
(Portal);
You can see above that we create two new Portals
'FrontSplit' and 'BackSplit' and pass them into the 'SplitPortal' function to be
filled.The parent Portal is then deleted.if (NodeArray[Node].IsLeaf==0)//There is
another Front
NODE
{
FrontPortalList=ClipPortal(NodeArray[Node].Front,FrontSplit);
}
else//
Its about to get pushed into a
Leaf
{
FrontSplit->LeafOwnerArray[FrontSplit->NumberOfLeafs]=NodeArray[Node].Front;
FrontSplit->NumberOfLeafs++;
FrontSplit->Prev=NULL;
FrontSplit->Next=NULL;
FrontPortalList=FrontSplit;
}
This
should look a bit familiar to you by now.The first thing we have to do is send
the Front Split down the Front Tree of the Current Node if a Front Tree
exists.If not then the Front Split has ended up in a Leaf so we record in the
Portal which leaf it has ended up in.The idea is that we will send the Front
Split down the Front Tree and get returned a Linked List of fragments that
Survived the Front Tree. A pointer to these fragments is returned and stored in
'FrontPortalList'. You can see that if the Front Split end up in a Leaf then we
simply set 'FrontPortalList' to point at the Front Split portal because this is
obviously the only Portal in the front because it will not get clipped into any
more fragments.If however there is Front Tree to send the Front Split down then
'FrontPortalList' will contain either the Fragments of the Front Split that
survived or NULL if nothing of the Front Split survived the Front Tree (totally
Clipped Away). if (NodeArray[Node].Back!=-1)
{
BackPortalList=ClipPortal(NodeArray[Node].Back,BackSplit);
}
else
// in solid space so delete
{
DeletePortal (
BackSplit);
}
As you can see above, if there is a back tree we
get either the Portal Fragments of the Back Split returned in BackPortalList or
NULL if nothing of the Back Split survived.Else if there is no back tree this
means the Back Split is in solid space and there for is completely removed
(clipped out).if
(FrontPortalList!=NULL)
{
Iterator=FrontPortalList;
while
(Iterator->Next!=NULL)
{
Iterator=Iterator->Next;
}
if
(BackPortalList!=NULL)
{
Iterator->Next=BackPortalList;
BackPortalList->Prev=Iterator;
}
return
FrontPortalList;
}//something in the front list
else ////There is
nothing in the front list
{
if (BackPortalList!=NULL) return
BackPortalList;
return NULL;
}
return
NULL;
break;
All we do above is attach the
'BackPointerList' to the end of the 'FrontPortalList'.First we check if there
are any fragments in the Front Portal List. If there is then we find the last
fragment in the front list and if there are fragments in the back list we attach
the last portal in the Front List to the Back list.In other words, instead of
the last portal in the front list haveing its 'Next' pointer set to null we now
set the 'Next' pointer to point at the Back List.We have now merged the lists in
to one large one.Obviously if there are no portals in the back List then just
Front List is returned.case CP_ONPLANE:
if
(NodeArray[Node].IsLeaf!=0) // this is a Leaf Node
{
Portal->LeafOwnerArray[Portal->NumberOfLeafs]=NodeArray[Node].Front;
Portal->NumberOfLeafs++;
Portal->Next=NULL;
Portal->Prev=NULL;
FrontPortalList=Portal;
}
else
{
FrontPortalList=ClipPortal(NodeArray[Node].Front,Portal);
}
if
(FrontPortalList==NULL) return NULL;
if (NodeArray[Node].Back==-1) return
FrontPortalList;
while (FrontPortalList!=NULL)
{
PORTAL *tempnext=FrontPortalList->Next;
BackPortalList=NULL;
BackPortalList=ClipPortal(NodeArray[Node].Back,FrontPortalList);//
fragment this fragment into the back tree
So we start a while loop
to step through each Portal (Fragment) in PortalFrontList.Each time through the
loop we store in 'tempnext' a pointer to the Next Portal in FrontPortalList
because the current portal will be getting sent down the back tree and possibly
deleted which means when the function returns FrontPortalList could be NULL so
'FrontPortalList->Next' would hold complete gibberish and we would loose a
way to step though the list.As you can see above we send the Current Fragment
(pointed to by 'FrontPortalList' down the back tree and store a pointer to the
list of Fragments that have survived in 'BackPortalList'. 'BackPortalList' will
be NULL if nothing of the Current Front List fragment survived the
tree.if
(BackPortalList!=NULL)
{
Iterator=BackPortalList;
while
(Iterator->Next!=NULL)
{
Iterator=Iterator->Next;
}
//
attach the last fragment to the first fragment from a previos
loop.
Iterator->Next=PortalList;
if
(PortalList!=NULL)
{
PortalList->Prev=Iterator;
}
PortalList=BackPortalList;
// portal List now points at the current complete list of fragment collected
from each loop
}
FrontPortalList=tempnext;
}
return
PortalList;
// ALL DONE
break;
Well admittedly that function
can fry your brain a little bit first time through but if you are comfortable
with recursion then there is nothing here very different from any other
recursive function. Its good to try and and do a dry run in your head and
following the code imagine the root a portal might take.Anyway, to give you a
much better view of the function I will now show you it in its complete form.I
have colored the different section to make easier reading. PORTAL * ClipPortal(long Node,PORTAL *Portal)
{
PORTAL
*PortalList=NULL,*FrontPortalList=NULL,*BackPortalList=NULL,*Iterator=NULL,*FrontSplit=NULL,*BackSplit=NULL;
switch
(ClassifyPoly(&PlaneArray[NodeArray[Node].Plane],(POLYGON
*)Portal))
{
case CP_ONPLANE: Send down Both
Trees
if (NodeArray[Node].IsLeaf!=0) // this is a Leaf Node
{
// The
Front is a
Leaf
Portal->LeafOwnerArray[Portal->NumberOfLeafs]=NodeArray[Node].Front;
Portal->NumberOfLeafs++;
Portal->Next=NULL;
Portal->Prev=NULL;
FrontPortalList=Portal;
}
else
{
// Send the Portal Down the Front List and get returned a
list of PortalFragments that survived the Front
Tree
FrontPortalList=ClipPortal(NodeArray[Node].Front,Portal);
}
//
Then send each fragment down the back tree.
if (FrontPortalList==NULL) return
NULL;
if (NodeArray[Node].Back==-1) return FrontPortalList;
while
(FrontPortalList!=NULL)
{
PORTAL
*tempnext=FrontPortalList->Next;
BackPortalList=NULL;
BackPortalList=ClipPortal(NodeArray[Node].Back,FrontPortalList);
if
(BackPortalList!=NULL)
{
Iterator=BackPortalList;
while
(Iterator->Next!=NULL)
{
Iterator=Iterator->Next;
}
// attach
the last fragment to the first fragment from a previos loop.
Iterator->Next=PortalList;
if
(PortalList!=NULL)
{
PortalList->Prev=Iterator;
}
PortalList=BackPortalList;
// portal List now points at the current complete list of fragment collected
from each loop
}
FrontPortalList=tempnext;
}
return
PortalList;
break;
case CP_FRONT: // Either
send it down the front tree or add it to the portal list because it has come out
//Empty Space
if
(NodeArray[Node].IsLeaf==0)
{
PortalList=ClipPortal(NodeArray[Node].Front,Portal);
return
PortalList;
}
else // The Front Node is a Empty Leaf so Add it to the
Portal
List
{
Portal->LeafOwnerArray[Portal->NumberOfLeafs]=NodeArray[Node].Front;
Portal->NumberOfLeafs++;
Portal->Next=NULL;
Portal->Prev=NULL;
return
Portal;
}
break;
case CP_BACK:// either
asend it downthe back tree or Delete it if no back tree exists
if
(NodeArray[Node].Back!=-1)
{
PortalList=ClipPortal(NodeArray[Node].Back,Portal);
return
PortalList;
}
else
{
DeletePortal ( Portal);
return NULL;
}
break;
case
CP_SPANNING:
FrontSplit=new PORTAL;
BackSplit=new
PORTAL;
ZeroMemory(FrontSplit,sizeof(PORTAL));
ZeroMemory(BackSplit,sizeof(PORTAL));
SplitPortal(Portal,&PlaneArray[NodeArray[Node].Plane],FrontSplit,BackSplit);
DeletePortal
(Portal);
if
(NodeArray[Node].IsLeaf==0)
{
FrontPortalList=ClipPortal(NodeArray[Node].Front,FrontSplit);
}
else//
Its about to get pushed into a
Leaf
{
FrontSplit->LeafOwnerArray[FrontSplit->NumberOfLeafs]=NodeArray[Node].Front;
FrontSplit->NumberOfLeafs++;
FrontSplit->Prev=NULL;
FrontSplit->Next=NULL;
FrontPortalList=FrontSplit;
}
if
(NodeArray[Node].Back!=-1) // the backsplit is in solid
space
{
BackPortalList=ClipPortal(NodeArray[Node].Back,BackSplit);
}
else
// delete it its in solid space
{
DeletePortal ( BackSplit);
}
if
(FrontPortalList!=NULL)// Find the End of the list and attach it to Front Back
List
{
Iterator=FrontPortalList;
while
(Iterator->Next!=NULL)
{
Iterator=Iterator->Next;
}
if
(BackPortalList!=NULL)
{
Iterator->Next=BackPortalList;
BackPortalList->Prev=Iterator;
}
return
FrontPortalList;
}// there is something in the front list
else
////There is nothing in the front list
{
if (BackPortalList!=NULL) return
BackPortalList;
return NULL;
}
return
NULL;
break;
default:
return NULL;
break;
} //end
switch
return NULL;
}
Well I have to say that if you
have understood everything up until now then its all gonna get a little bit
easier from here on in.In my opinion 'Portal Generation' is the hardest thing to
grasp and also rather hard to explain or even to show on paper for that
matter.Well I did mention earlier when we were looking at the CP_SPANNING case
of the above function that it uses a function called 'SplitPortal' to wrap the
'SplitPolygon' function call and also copy over the extra portal information
over from the parent about to be split into the two child splits.This
information that had to be retained was mainly just what leafs the Portal the
parent had landed in up to that point.I will just show you the 'SplitPortal'
function for completeness.void SplitPortal(PORTAL
*Portal,PLANE *Plane,PORTAL *FrontSplit,PORTAL
*BackSplit)
{
SplitPolygon((POLYGON *)Portal,Plane,(POLYGON
*)FrontSplit,(POLYGON
*)BackSplit);
FrontSplit->NumberOfLeafs=Portal->NumberOfLeafs;
BackSplit->NumberOfLeafs=Portal->NumberOfLeafs;
memcpy(FrontSplit->LeafOwnerArray,Portal->LeafOwnerArray,sizeof(long)*Portal->NumberOfLeafs);
memcpy(BackSplit->LeafOwnerArray,Portal->LeafOwnerArray,sizeof(long)*Portal->NumberOfLeafs);
}
void InitPolygons(void)
{
ReserveInitialMemoryForArrays();
PolygonList=NULL;
PolygonList=LoadMWM("demolevel.mwm");
LoadTextures();
BuildBspTree(0,PolygonList);
BuildPortals();
...
...
}
BuildPortals
is the main PortalCreation function and the only one that the main application
has to call to generate the portals for the tree. Its job is to step through
every Node in our BSP tree and at each Node it calls 'CreateInitialPortal' to
create the Initial Portal for that Node and then Calls 'ClipPortal' to send that
Portal down the tree. After the call to 'ClipPortal' , our BuildPortals function
will then have a list of Portal Fragments for the Current Node.It will then Loop
through the portal list and remove any Portals that are not in Two leafs.This is
a simple case of checking the Portals 'NumberOfLeafs' flag to see that it is
equal to Two.struct NODESTACK
{
long
Node;
BYTE JumpBackPoint;
};
Everytime we have to traverse
the tree we will record the current node we are using and also were we have come
from so that we can jump back that point.This is basically just simulting what a
C++ function call would do.If you are half way through 'Function 1' and you call
'Function 2' the current Variables and line that is currently being executed in
'Function 1' is saved on the stack.When function two returns the computer can
return to function one at the same place it left off by popping the information
back off the stack so that the execution of 'Function 1' can again resume from
where it left off before 'Function 2' was called.void BuildPortals(void)
{
long
stackpointer=0;
NODESTACK *NodeStack=new NODESTACK[NumberOfNodes+1]
int
portalindex;
NodeStack[stackpointer].Node=0;// root
node
NodeStack[stackpointer].JumpBackPoint=0;
Above is the code
that sets up our Stack. The variable 'stackpointer' is set to zero because this
will be the Node that we start off with.You can see that after we have safely
allocated enough memory to hold more than enough nodes we fill in the current
NodeStack position with the Node we are going to start with (Node 0).
JumpBackPoint is set to zero meaning there is no jump back point for the first
node.Once Node 0 is done it is time to Return from the function because the
whole tree will have been traversed so this is why we just set the jump back
point to zero.START:
PORTAL
*InitialPortal=CalculateInitialPortal(NodeStack[stackpointer].Node);
PORTAL
*PortalList=ClipPortal(0,InitialPortal);
PORTAL *Iterator=PortalList; // Step through
the Portal List
while (Iterator!=NULL)
{
if
(Iterator->NumberOfLeafs!=2)// not in two leafs so delete
{
PORTAL
*temp=Iterator->Next;
RemovePortalFromList(Iterator);
Iterator=temp;
}
else
{
if
(CheckDuplicatePortal(Iterator,&portalindex)==true)
{
PORTAL
*temp=Iterator->Next;
RemovePortalFromList(Iterator);
Iterator=temp;
}
The
'CheckDuplicatePortal' array returns 'TRUE' if there is a Portal already in the
portal array that is larger then the current one we are testing.In other words,
if it returns true then the portal we are testing is no good and we want to
delete it. Notice that we pass in the pointer to an INT 'Portalndex'.The
contents of this int is modified by the 'CheckDuplicatePortals' function.If the
function returns false it means we DO want to keep this portal and add it to the
main portal array.'PortalIndex' will hold the index of the entry in the array
that we need to put this portal.If there is not a duplicate portal then
'PortalIndex' will simply hold the next element on the end of the Portal Array
so we will just be adding a new pointer to the end of the array (PortalIndex
will equal NumberOfPortals) however, if there is a duplicate portal in the array
and this new one is bigger then we want to replace the portal that is already in
the array with this one ,so 'PortalIndex' will hold the position where we should
place this new portal in the array to replace the one thats already there.
else
{
PortalArray[portalindex]=Iterator;
if
(portalindex==NumberOfPortals)
{
for (int
a=0;a>Iterator-<NumberOfLeafs;a++)
{
long
Index=Iterator->LeafOwnerArray[a];
LeafArray[Index].PortalIndexList[LeafArray[Index].NumberOfPortals]=NumberOfPortals;
LeafArray[Index].NumberOfPortals++;
}
IncreaseNumberOfPortals();
}
Iterator=Iterator->Next;
} // if not a duplicate portal
}
// end else
}// END WHILE LOOP
Above concludes the main
while loop.If we have made it here this portal needs to be added.As we have said
'PortalIndex' holds the position in the master Portal Array where this portal
should be added.If 'PortalIndex==NumberOfPortals' however it means we are NOT
simply replacing a portal that is already there but are adding a new one to the
end of the array.We then loop through this portals 'LeafOwnerArray' to gain
access to the Two leafs this portal is in and then record this portals index
number in the array in the Leaf Structures 'PortalIndexList' array and increase
the Leafs portal count 'Leaf.NumberOfPortals'. This means we now have in the
Portal which leafs it exists in and we also have in each leaf a reference to
this portal.This will come in handy when we calculate the PVS.Remember a Leaf
could have many portals in it.if
(NodeArray[NodeStack[stackpointer].Node].IsLeaf==0)
{
NodeStack[stackpointer+1].Node=NodeArray[NodeStack[stackpointer].Node].Front;
NodeStack[stackpointer+1].JumpBackPoint=1;
stackpointer++
;
goto START;
}
BACK:
if
(NodeArray[NodeStack[stackpointer].Node].Back!=-1)
{
NodeStack[stackpointer+1].Node=NodeArray[NodeStack[stackpointer].Node].Back;
NodeStack[stackpointer+1].JumpBackPoint=2;
stackpointer++
;
goto START;
};
END:
stackpointer--;// This is like
returning from a function
if (stackpointer>-1)
{
if
(NodeStack[stackpointer+1].JumpBackPoint==1) goto BACK;
else
if
(NodeStack[stackpointer+1].JumpBackPoint==2) goto END;
}
delete []
NodeStack;
}
Hopefully you can see what is happening
here.If there is a Front Node then we put this Front Node in the Next Position
in the stack array (stackpointer+1) and also store in that stack positions
'JumpBackPoint' variable a Label Code indicating which label we will need to
jump back to continue processing the Current Node.When the we get to the bottom
of the function we simulate a stack by decreasing the Stack pointer so we are
back at the previous node and jump back to either label BACK:(JumpBackPoint=1)
or label END:(JumpBackPoint=2) or we dont jump anywhere and just return from the
BuildPortals function if (JumpBackPosition=0) which should only be true for the
root node.You can see that if there is a Back node then we do the same but place
a value of '2' in the JumpBackPoint variable. Unfortunately things look a little
uglier when we loose the clean looking Rescursive approach but thats just
something we have to put up with in this case.You can unroll all your recursive
functions this way and will probably get a speed boost by not using the
stack.This is at the expense of some readablility though.If you read through the
above code and follow the program flow you should see what is happening without
any problems, we are simply restarting the a loop each time by jumping back to
the START: label everytime a new node is reached but are storing in the stack
the current position we are at in the code before the jump.This way, when we
have dealt with the new node will reach the bottom and will decrease the
stackpointer variable so we are back at the Node we were at before the jump.But
we also have to get back to the position in the code we were at before the jump
which is exactly the purpose of the JumpBackPoint variable.void
BuildPortals(void)
{
long stackpointer=0;
NODESTACK *NodeStack=new
NODESTACK[NumberOfNodes+1]
int
portalindex;
NodeStack[stackpointer].Node=0;// root
node
NodeStack[stackpointer].JumpBackPoint=0;
START:
PORTAL
*InitialPortal=CalculateInitialPortal(NodeStack[stackpointer].Node);
PORTAL
*PortalList=ClipPortal(0,InitialPortal);
PORTAL
*Iterator=PortalList; // Step through the Portal List
while
(Iterator!=NULL)
{
if (Iterator->NumberOfLeafs!=2)// not in two leafs
so delete
{
PORTAL
*temp=Iterator->Next;
RemovePortalFromList(Iterator);
Iterator=temp;
}
else
{
if
(CheckDuplicatePortal(Iterator,&portalindex)==true)
{
PORTAL
*temp=Iterator->Next;
RemovePortalFromList(Iterator);
Iterator=temp;
}
else
{
PortalArray[portalindex]=Iterator;
if
(portalindex==NumberOfPortals)
{
for (int
a=0;a>Iterator-<NumberOfLeafs;a++)
{
long
Index=Iterator->LeafOwnerArray[a];
LeafArray[Index].PortalIndexList[LeafArray[Index].NumberOfPortals]=NumberOfPortals;
LeafArray[Index].NumberOfPortals++;
}
IncreaseNumberOfPortals();
}
Iterator=Iterator->Next;
} // if not a duplicate portal
}
// end else
}// END WHILE LOOP
if
(NodeArray[NodeStack[stackpointer].Node].IsLeaf==0)
{
NodeStack[stackpointer+1].Node=NodeArray[NodeStack[stackpointer].Node].Front;
NodeStack[stackpointer+1].JumpBackPoint=1;
stackpointer++
;
goto START;
}
BACK:
if
(NodeArray[NodeStack[stackpointer].Node].Back!=-1)
{
NodeStack[stackpointer+1].Node=NodeArray[NodeStack[stackpointer].Node].Back;
NodeStack[stackpointer+1].JumpBackPoint=2;
stackpointer++
;
goto START;
};
END:
stackpointer--;// This is like
returning from a function
if (stackpointer>-1)
{
if
(NodeStack[stackpointer+1].JumpBackPoint==1) goto BACK;
else
if
(NodeStack[stackpointer+1].JumpBackPoint==2) goto END;
}
delete []
NodeStack;
}
void
RemovePortalFromList(PORTAL *RemovePortal)
{
PORTAL
*temp=RemovePortal;
PORTAL
*PrevPortal,*NextPortal;
if(RemovePortal->Prev!=NULL)
{
PrevPortal=RemovePortal->Prev;
if
(RemovePortal->Next!=NULL)
{
PrevPortal->Next=RemovePortal->Next;
}
else
{
PrevPortal->Next=NULL;
}
}//
if there is a prev
if
(RemovePortal->Next!=NULL)
{
NextPortal=RemovePortal->Next;
if
(RemovePortal->Prev!=NULL)
{
NextPortal->Prev=RemovePortal->Prev;
}
else
{
NextPortal->Prev=NULL;
}
}
DeletePortal
( temp );
}
Before we move away from Portal Generation
we will just have a look at the last support function called by the
'BuildPortals' function. Remember that if valid Portal List was returned from
'ClipPortal' we still had to check that a Portal that connected the same two
leafs did'nt already exist in the master Portal Array.If one did, then the
'CheckDuplicatePortal' functions job was to compare the size of the new Portal
with the one already in the array and discard the smaller one and keep the
larger one.You may want to take a quick flick back to the 'BuildPortals' code
just to refresh your self how this function was used.bool CheckDuplicatePortal (PORTAL *
CheckPortal,int *index)
{
long
CheckPortalLeaf1=CheckPortal->LeafOwnerArray[0];
long
CheckPortalLeaf2=CheckPortal->LeafOwnerArray[1];
First
get both the leafs that the Portal exists in (this is stored in the Portal
LeafOwnerArray remember).We will need these leafs to check all the other portal
in the Portal Array and see if any other Portal exists in the Same two leafs.If
so then we have found a duplicate portal. long
PALeaf1=0;
long PALeaf2=0;
for (long
i=0;i<NumberOfPortals;i++)
{
PALeaf1=PortalArray[i]->LeafOwnerArray[0];
PALeaf2=PortalArray[i]->LeafOwnerArray[1];
if
((CheckPortalLeaf1==PALeaf1 && CheckPortalLeaf2==PALeaf2) ||
(CheckPortalLeaf1==PALeaf2 &&
CheckPortalLeaf2==PALeaf1))
{
Above we loop through each
portal in the array and get the two leafs that the portal exists in.If this
Portal exists in the same two leafs as our 'CheckPortal' then we have found a
match and need to compare sizes as shown below. D3DXVECTOR3 Max1,Min1,Max2,Min2;
GetPolygonBounds((POLYGON
*)CheckPortal,&Min1,&Max1);
GetPolygonBounds((POLYGON
*)PortalArray[i],&Min2,&Max2);
float
NewSize=D3DXVec3Length(&(Max1-Min1));// Measure the Lengths of the vector
to
// see which is bigger
float
OldSize=D3DXVec3Length(&(Max2-Min2));
Above we set up
some Vectors that will be used to hold a Bounding Box for the polygon.We then
call 'GetPolygonBounds' which is a simple helper function that just tests each
vertex in the polygon and returns a Minimum Point and a Maximum Point that
bounds the Polygon.We call this function twice to retrieve the Bounding Box for
both the Portal that we are checking ('CheckPortal') and the one that is
currently already in the array.if
(fabs(NewSize)>fabs(OldSize))
{
PORTAL
*temp=PortalArray[i];
DeletePortal ( temp );
*index=i;
return
false;
}
else
{
return true;// This portal is already in the
array
}
}
}
As you can see above if the new portal
'CheckPortal' is larger than the one already in the array then we delete the
portal currently in the array and record the index number of the portal in the
array so that it is returned to the calling function 'BuildPortals' and the
calling function has an index to place the new portal.The function then returns
'false' informing the calling function that the Portal passed in to the function
to be checked is valid and should be added to the array at the position held in
'index'.*index=NumberOfPortals;
return false;// This portal was not found
inthe array
}
Here is the complete
code:-bool CheckDuplicatePortal (PORTAL *
CheckPortal,int *index)
{
long
CheckPortalLeaf1=CheckPortal->LeafOwnerArray[0];
long
CheckPortalLeaf2=CheckPortal->LeafOwnerArray[1];
long PALeaf1=0;
long
PALeaf2=0;
for (long
i=0;i<NumberOfPortals;i++)
{
PALeaf1=PortalArray[i]->LeafOwnerArray[0];
PALeaf2=PortalArray[i]->LeafOwnerArray[1];
if
((CheckPortalLeaf1==PALeaf1 && CheckPortalLeaf2==PALeaf2) ||
(CheckPortalLeaf1==PALeaf2 &&
CheckPortalLeaf2==PALeaf1))
{
D3DXVECTOR3
Max1,Min1,Max2,Min2;
GetPolygonBounds((POLYGON
*)CheckPortal,&Min1,&Max1);
GetPolygonBounds((POLYGON
*)PortalArray[i],&Min2,&Max2);
float
NewSize=D3DXVec3Length(&(Max1-Min1));// Measure the Lengths of the vector
to
// see which is bigger
float
OldSize=D3DXVec3Length(&(Max2-Min2));
if
(fabs(NewSize)>fabs(OldSize))
{
PORTAL
*temp=PortalArray[i];
DeletePortal ( temp );
*index=i;
return
false;
}
else
{
return true;// This portal is already in the
array
}
}
}
*index=NumberOfPortals;
return false;// This portal
was not found inthe array
}
I am not going to cover the
code to the 'GetPolygonBounds' function because it is just a simplified version
of the function 'CalculateBox' function that we wrote earlier to build a
BoundingBox for our leafs and nodes.The only difference is that it only has to
build it for one polygon. You can check out the code anyway if you want.It can
be found in 'Portals.cpp'Please note: I am using the terms 'Bottom Side' and 'Top Side' to describe the relation ship of the Portals to the planes in the above diagram a little loosely here purely for the sake of explaining the diagram. In fact the Normals for the Planes can be facing in any direction changing what is considered top and what is considered bottom. This does not matter at all, all we need to know is that one Portal lays on one side and the other Portal lays on the other side.The orientation of the plane is not important. |
![]() |
A you can see once again the Source,Destination and Generator Leafs are all marked as visible from the Source Leaf and the Clip Planes (aka Anti-penumbra) are built from the Source portal to the Destination Portal describing exactly how much of the Generator Leaf is visible through the Destination portal as viewed from the Source portal. You can see that out of the two Generator Portals only one is partially with in the Clip Planes.The portal outside the planes is ignored and not processed, but above you can see that the other portal is partially in side.So we clip away the part of the generator portal that is Outside so we are just left with a fragment that is within the penumbra. What happens next is really intersting. |
![]() |
Because the Generator portal is partially visible it also means the Leaf on the other side of this portal is visible so we mark it as so by adding it to the source leafs PVS. Next this new Leaf becomes the New Generator Leaf and the Old generator Leaf becomes the new Destination Leaf meaning the old generator portal now becomes the new destination Portal .This means we now build a new Anti-Penumbra from the same source Portal but to the new Destination portal (the old Generator Portal that was clipped) fully describing what area in the new Generator leaf is still visible.What is great about this is as we recursively move from leaf to leaf doing this (making old Generator Portal new Destination portal and rebuilding the anti-penumbra) and clipping each generator portal to the Anti-Penumbra,the Anti-Penumbra gets smaller and smaller with each recur until in the end no portals in the current generator leaf are in side. |
![]() |
Here is an exact example of what I mean.You can see that as we go into each new Generator Leaf the Anti-Penumbra get narrower and narrower raising the chances of clipping out all of the Next Generator Leafs portals(Generator Portals).As we enter every Generator leaf that leaf is added to the Source leafs PVS. We now have a PVS for the current Source Leaf.The diagram to the right shows in grey the leafs that could possibly be seen from the Source Leaf. |
void
InitPolygons(void)
{
ReserveInitialMemoryForArrays();
PolygonList=NULL;
PolygonList=LoadMWM("demolevel.mwm");
LoadTextures();
BuildBspTree(0,PolygonList);
BuildPortals();
BytesPerSet=(NumberOfLeafs+7)>>3;
PVSData=(BYTE *)malloc
(NumberOfLeafs*BytesPerSet);
ZeroMemory(PVSData,NumberOfLeafs*BytesPerSet);
PVSCompressedSize=CalculatePVS();
PVSData=(BYTE *)
realloc(PVSData,PVSCompressedSize);
} // END
FUNTION
The first thing we do is calculate the initial
space needed to hold the PVS Data for the tree.At this point we have no idea how
much the PVS data will be compressed so we have to allocate enough space for a
worst case senario or no compression.Once we have the PVS we will also have the
total size of it as well so we can then Re Size this array to the actual amount
needed. We need to allocate enough memory initially so that there is enough
memory for every leaf to have 1 bit for every other leaf in the
tree.long
CalculatePVS()
{
BYTE * LeafPVS=new BYTE[BytesPerSet];
long
PVSMasterWritePointer=0;
First we allocate a temporary buffer
'LeafPVS' that will be used to hold the uncompressed PVS information for each
Leaf.Remember that we calculated the value of the global variable 'BytesPerSet'
prior to the function call.Once this function has calculated the PVS information
for a Leaf into this buffer,the buffer is copied into the master PVSData array
being compressed as it is copied. The variable PVSMasterWritePointer will be
used to hold the current write position in the 'PVSData' array. This is a little
like a File pointer when reading or writing to a file.The variable holds the
offset into the master PVSData array that this Leafs visibility information will
be copied to.This starts at zero obviously for the first leaf.As each Leafs PVS
Information is added to the master 'PVSData' array the PVSMasterWritePointer
will keep track of the position that the Next Leafs information will be written
to. This is also the Offset we will store in the LEAF structures 'PVSIndex'
array that we used earlier in our 'DrawTree' function to offset into the PVSData
array at the correct position for the current leaf the camera was
in.for (long
Leaf=0;Leaf<NumberOfLeafs;Leaf++)
{
LeafArray[Leaf].PVSIndex=PVSMasterWritePointer;
ZeroMemory(LeafPVS,BytesPerSet);
SetPVSBit(LeafPVS,Leaf);
Above
we set up a for next loop that will loop through each leaf in the LeafArray and
calculate the Visibilty information for that leaf.We start by storing the value
of 'PVSMasterWritePointer' into the leaf structure so that we can offset in the
array during Rendering and get to any Leafs Visibility information.for (long
SPI=0;SPI<LeafArray[Leaf].NumberOfPortals;SPI++)
{
PORTAL *
SourcePortal=PortalArray[LeafArray[Leaf].PortalIndexList[SPI]];
long
TargetLeaf=SourcePortal->LeafOwnerArray[0];
if (TargetLeaf==Leaf)
{
TargetLeaf=SourcePortal->LeafOwnerArray[1];
}
SetPVSBit(LeafPVS,TargetLeaf);
for (long
DPI=0;DPI<LeafArray[TargetLeaf].NumberOfPortals;DPI++)
{
PORTAL *
TargetPortal=PortalArray[LeafArray[TargetLeaf].PortalIndexList[DPI]];
if
(SourcePortal!=TargetPortal &&
ClassifyPoly(&GetPortalPlane(SourcePortal),(POLYGON
*)TargetPortal)!=CP_ONPLANE)
{
RecursePVS(Leaf,SourcePortal,TargetPortal,TargetLeaf,LeafPVS);
} // End for If Source != Dest
} // End for DPI
} // End for
SPI
PVSMasterWritePointer+=CompressLeafSet(LeafPVS,PVSMasterWritePointer);
}
// End for Leaf
delete [] LeafPVS;
return PVSMasterWritePointer;
}
// End Function
long CalculatePVS()
{
BYTE * LeafPVS=new BYTE[BytesPerSet];
long
PVSMasterWritePointer=0;
for (long
Leaf=0;Leaf<NumberOfLeafs;Leaf++)
{
LeafArray[Leaf].PVSIndex=PVSMasterWritePointer;
ZeroMemory(LeafPVS,BytesPerSet);
SetPVSBit(LeafPVS,Leaf);
for
(long SPI=0;SPI<LeafArray[Leaf].NumberOfPortals;SPI++)
{
PORTAL *
SourcePortal=PortalArray[LeafArray[Leaf].PortalIndexList[SPI]];
long
TargetLeaf=SourcePortal->LeafOwnerArray[0];
if (TargetLeaf==Leaf)
{
TargetLeaf=SourcePortal->LeafOwnerArray[1];
}
SetPVSBit(LeafPVS,TargetLeaf);
for
(long DPI=0;DPI<LeafArray[TargetLeaf].NumberOfPortals;DPI++)
{
PORTAL *
TargetPortal=PortalArray[LeafArray[TargetLeaf].PortalIndexList[DPI]];
if
(SourcePortal!=TargetPortal &&
ClassifyPoly(&GetPortalPlane(SourcePortal),(POLYGON
*)TargetPortal)!=CP_ONPLANE)
{
RecursePVS(Leaf,SourcePortal,TargetPortal,TargetLeaf,LeafPVS);
} // End for If Source != Dest
} // End for DPI
} // End for
SPI
PVSMasterWritePointer+=CompressLeafSet(LeafPVS,PVSMasterWritePointer);
}
// End for Leaf
delete [] LeafPVS;
return PVSMasterWritePointer;
}
// End Function
void SetPVSBit (BYTE *VisArray,long
DestLeaf)
{
long ByteToSet=DestLeaf>>3;
BYTE
BitToSet=(BYTE)(DestLeaf-(ByteToSet<<3));
VisArray[ByteToSet]|=1<<BitToSet;
}
As you can see it is very small.To find the Correct BYTE to
set in the temporary Visibility Buffer we divide by 8 (>>3). So for
example, if we called this function to mark Leaf 19 as being visible (remember
''VisArray' is a temporary Visibility Buffer for the current Leaf having its PVS
calculated) then 19/8=2 (truncated to 2 because of rounding) which is correct.We
know that Bit 19 is in the 3rd Byte (VisArray[2] because its zero based).To find
the Bit we wish to set in that Byte we then Multiply the Byte it is in (2) by 8
(<<3) which gives us 2*8=16. We then take this away from the index of the
Leaf we wish to set (DestLeaf) like so 19-16=3.This tells us we need to set the
4th (zero basd again) bit in the third Byte. We do this by shifting '1' into the
fourth position in the Byte (shifting it by three puts it in position four
because the '1' is already in position 1 before any shifting is done).We then
'OR' this Bit with the Visibility array so that if the Bit is not already set it
is Set, and if the Bit is already set then then the Bit still remains Set.
void
RecursePVS(long SourceLeaf,PORTAL *SrcPortal,PORTAL *TargetPortal,long
TargetLeaf,BYTE *LeafPVS)
{
long
GeneratorLeaf=TargetPortal->LeafOwnerArray[0];
if
(GeneratorLeaf==TargetLeaf)
GeneratorLeaf=TargetPortal->LeafOwnerArray[1];
SetPVSBit(LeafPVS,GeneratorLeaf);
D3DXVECTOR3
SourceLeafCenter=(LeafArray[SourceLeaf].BoundingBox.BoxMax+LeafArray[SourceLeaf].BoundingBox.BoxMin)/2;
D3DXVECTOR3
TargetLeafCenter=(LeafArray[TargetLeaf].BoundingBox.BoxMax+LeafArray[TargetLeaf].BoundingBox.BoxMin)/2;
int
SourceLeafLocation=ClassifyPoint(&SourceLeafCenter,&GetPortalPlane(SrcPortal));
int
TargetLeafLocation=ClassifyPoint(&TargetLeafCenter,&GetPortalPlane(TargetPortal));
for (long
GPI=0;GPI<LeafArray[GeneratorLeaf].NumberOfPortals;GPI++)
{
if
(PortalArray[LeafArray[GeneratorLeaf].PortalIndexList[GPI]]==TargetPortal){delete
GeneratorPortal;delete SourcePortal;continue;}
PORTAL *SourcePortal=new
PORTAL;
*SourcePortal=*SrcPortal;
PORTAL *GeneratorPortal=new
PORTAL;
*GeneratorPortal=*PortalArray[LeafArray[GeneratorLeaf].PortalIndexList[GPI]];
int
GeneratorLocation=ClassifyPoly(&GetPortalPlane(SourcePortal),(POLYGON*)
GeneratorPortal);
if (GeneratorLocation==CP_ONPLANE ||
GeneratorLocation==SourceLeafLocation) {delete GeneratorPortal;delete
SourcePortal;continue;}
GeneratorLocation=ClassifyPoly(&GetPortalPlane(TargetPortal),(POLYGON*)
GeneratorPortal);
if (GeneratorLocation==CP_ONPLANE ||
GeneratorLocation==TargetLeafLocation) {delete GeneratorPortal;delete
SourcePortal;continue;}
If we have made it here then we have a
Generator portal that could Possibly be seen so we have to test it against the
Anti-Penumbra planes to find out.GeneratorPortal=ClipToAntiPenumbra(SourcePortal,TargetPortal,GeneratorPortal);
if
(GeneratorPortal==NULL)
{
if (SourcePortal) delete
SourcePortal;continue;
continue;
}
Above we send into the
'ClipToAntiPenumbra' function the Source,Target and current Generator portal
being tested.This function Creates the Clip Planes for the Penumbra and Clips
the Generator portal to them returning the result.If the function returns 'NULL'
then the Generator Portal was completely clipped away which means it was outside
the Anti-Penumbra and therefore we dont have to recurse any further for this
Portal because it is not visible.In this case we simply delete the copy of the
Source Portal that we made and move onto the next iteration of the loop where we
will test the next Generator portal.SourcePortal=
ClipToAntiPenumbra(GeneratorPortal,TargetPortal,SourcePortal);
if
(SourcePortal==NULL)
{
if (GeneratorPortal) delete
GeneratorPortal;continue;
}
If a the Generator portal and the
Source Portal still exist (have not been completely clipped away) then we will
make this function call itself BUT this time passing in the Current Generator
Portal and Generator Leaf as theTarget Portal and Target Leaf respectively.This
means the next time round in the recur a New Generator Leaf on the opposing side
of the old Generator Portal (the new target portal) will be added to the
visibility buffer and the Anti-Penumbra will be built between the source Portal
and the new Target portal (old Generator
portal).RecursePVS(SourceLeaf,SourcePortal,GeneratorPortal,GeneratorLeaf,LeafPVS);
delete
GeneratorPortal;
delete SourcePortal;
}
}
That
was not as bad as it could have been.Read through again until you understand the
recursive process involved.The key to understanding it is in understanding how
the next time the function calls itself it uses the Old Generator portal as the
new target portal to find a new Generator Leaf on the opposing side of this new
Target Portal.Remember that each new Generator Leaf found is considered to be
visible and has its Bit set in the Visibility buffer.void RecursePVS(long
SourceLeaf,PORTAL *SrcPortal,PORTAL *TargetPortal,long TargetLeaf,BYTE
*LeafPVS)
{
long
GeneratorLeaf=TargetPortal->LeafOwnerArray[0];
if
(GeneratorLeaf==TargetLeaf)
GeneratorLeaf=TargetPortal->LeafOwnerArray[1];
SetPVSBit(LeafPVS,GeneratorLeaf);
D3DXVECTOR3
SourceLeafCenter=(LeafArray[SourceLeaf].BoundingBox.BoxMax+LeafArray[SourceLeaf].BoundingBox.BoxMin)/2;
D3DXVECTOR3
TargetLeafCenter=(LeafArray[TargetLeaf].BoundingBox.BoxMax+LeafArray[TargetLeaf].BoundingBox.BoxMin)/2;
int
SourceLeafLocation=ClassifyPoint(&SourceLeafCenter,&GetPortalPlane(SrcPortal));
int
TargetLeafLocation=ClassifyPoint(&TargetLeafCenter,&GetPortalPlane(TargetPortal));
for
(long
GPI=0;GPI<LeafArray[GeneratorLeaf].NumberOfPortals;GPI++)
{
if
(PortalArray[LeafArray[GeneratorLeaf].PortalIndexList[GPI]]==TargetPortal){continue;}
PORTAL
*SourcePortal=new PORTAL;
*SourcePortal=*SrcPortal;
PORTAL
*GeneratorPortal=new
PORTAL;
*GeneratorPortal=*PortalArray[LeafArray[GeneratorLeaf].PortalIndexList[GPI]];
int
GeneratorLocation=ClassifyPoly(&GetPortalPlane(SourcePortal),(POLYGON*)
GeneratorPortal);
if (GeneratorLocation==CP_ONPLANE ||
GeneratorLocation==SourceLeafLocation) {delete GeneratorPortal;delete
SourcePortal;continue;}
GeneratorLocation=ClassifyPoly(&GetPortalPlane(TargetPortal),(POLYGON*)
GeneratorPortal);
if (GeneratorLocation==CP_ONPLANE ||
GeneratorLocation==TargetLeafLocation) {delete GeneratorPortal;delete
SourcePortal;continue;}
GeneratorPortal=ClipToAntiPenumbra(SourcePortal,TargetPortal,GeneratorPortal);
if
(GeneratorPortal==NULL)
{
if (SourcePortal) delete
SourcePortal;continue;
continue;
}
SourcePortal=
ClipToAntiPenumbra(GeneratorPortal,TargetPortal,SourcePortal);
if
(SourcePortal==NULL)
{
if (GeneratorPortal) delete
GeneratorPortal;continue;
}
RecursePVS(SourceLeaf,SourcePortal,GeneratorPortal,GeneratorLeaf,LeafPVS);
delete
GeneratorPortal;
delete
SourcePortal;
}
}
struct
CLIPPLANES
{
WORD NumberOfPlanes;
PLANE
*Planes;
};
No real surprises here then. The CLIPPLANES
structure contains a pointer to an array of Clip Planes and also a Count of how
many planes are in the array. PORTAL
* ClipToAntiPenumbra(PORTAL *SourcePortal,PORTAL *TargetPortal,PORTAL
*GeneratorPortal)
{
D3DXVECTOR3 EdgeVector1,EdgeVector2,Normal;
int
PortalLocation;
PORTAL
*FrontSplit,*BackSplit,*TempSource,*TempTarget;
CLIPPLANES ClipPlanes; //
Create a ClipPlane
set
ClipPlanes.NumberOfPlanes=0;
ClipPlanes.Planes=new PLANE
[SourcePortal->NumberOfVertices*TargetPortal->NumberOfVertices*2];
PLANE
TempPlane;
int NextVertex=0;
We start of by delaring
some local variables that we will be using shortly including a CLIPPLANE
structure called 'ClipPlanes'.We set its count to zero because we have not added
any Planes to the list yet and also allocate enough memory in the CLIPPLANE
structure to hold the maximum number of Clip Planes thats could possibly be
created.In practice much fewer Clip Planes will be added to this list because
remember that we are only going to accept clip planes that divide the Source and
Target portals and many will not so will never be added. But to be on the safe
side we reserve enough memory so that it could contain EVERY plane that was
creates from the Source to Target and from the Target to Source.We multiply this
by two because remember that we are also going to generate a set of clip planes
from the Target to the Source Portal as well as from the Source to the Target
Portal.for (int loop=0;loop<2;loop++)
{
if
(loop==0)
{
TempSource=SourcePortal;
TempTarget=TargetPortal;
}
else
{
TempSource=TargetPortal;
TempTarget=SourcePortal;
}
We
now how our Temporary Source and Target portal pointers set up to point to the
correct Portal depending on the current iterartion of the loop.Now its time to
create the planes from each vertex in the Source portal to each Edge in the
Destination portal.First we set up a for next loop to loop through each vertex
in the Source (temporary Source that is).This will be the First point needed to
construct the plane.What we do have to check for though is that the Source
Portals Vertex is not ON the Target Portals planes .This can happen if say for
example 2 portals meet in an 'L' shaped configuration.In this situation the Clip
Plane can not be built so we simply skip the vertex and move on to the next one.
for (int
SV=0;SV<TempSource->NumberOfVertices;SV++)
{
PortalLocation=ClassifyPoint((D3DXVECTOR3
*)&TempSource->VertexList[SV],&GetPortalPlane(TempTarget));
if
(PortalLocation==CP_ONPLANE) continue; // Skip It. Because it;s 'L'
shaped
Providing that the Source Vertex is not ON_PLANE with the
Target Portal we now have to loop through each edge in the Target portal.Once
again we have to do a little bit of conditional logic at the top of this inner
loop because if we just loop through each vertex in the Target Portal and use
''Vertex' and ''Vertex+1' as our edge we will get to the point where we are at
the Last vertex in the Target Portal so ''Vertex+1' does not exist.All we have
to do is check if we are at the last vertex in the edge and if so wrap back
around so that the last edge uses ''Vertex' and 'FirstVertex' like
so:-for (int
TP=0;TP<TempTarget->NumberOfVertices;TP++)
{
if
(TP==TempTarget->NumberOfVertices-1)
{
NextVertex=0;
}
else
{
NextVertex=TP+1
;
}
You can see that TP will hold the Current Vertex (which
will be used as the first vertex in each edge) and 'NextVertex' will hold the
secondVertex in the edge.This will just be set to 'TP+1' unless 'TP' is the Last
vertex in the Target portal in which case we will set 'NextVertex' to Zero( The
First Vertex in the Target Portal).EdgeVector1=*((D3DXVECTOR3
*)&TempSource->VertexList[SV])-*((D3DXVECTOR3
*)&TempTarget->VertexList[TP]);
EdgeVector2=*((D3DXVECTOR3
*)&TempTarget->VertexList[NextVertex])-*((D3DXVECTOR3
*)&TempTarget->VertexList[TP]);
D3DXVec3Cross(&Normal,&EdgeVector1,&EdgeVector2);//
create normal
D3DXVec3Normalize(&TempPlane.Normal,&Normal); // make
it a unit normal
TempPlane.PointOnPlane=*(D3DXVECTOR3
*)&TempSource->VertexList[SV];//Any vertex will
do
if(ClassifyPoly(&TempPlane,(POLYGON*)TempSource)==
CP_FRONT)
{
if(ClassifyPoly(&TempPlane,(POLYGON*)TempTarget)==
CP_BACK)
{
ClipPlanes.Planes[ClipPlanes.NumberOfPlanes] =
TempPlane;
ClipPlanes.NumberOfPlanes++;
//Save clip
plane
}
}
else
if(ClassifyPoly(&TempPlane,(POLYGON
*)TempSource) ==
CP_BACK)
{
if(ClassifyPoly(&TempPlane,(POLYGON*)TempTarget) ==
CP_FRONT)
{
ClipPlanes.Planes[ClipPlanes.NumberOfPlanes] =
TempPlane;
ClipPlanes.NumberOfPlanes++;
//Save clip plane
}
}
}
// end for tp
} // end for sp
} // END FOR LOOP
At this
point we have exited all the loops we started and 'ClipList' hold the Number of
Clip Planes in the Anti-Penumbra and also contains a pointer to those Clip
Planes.At last we have our Anti-penumbra built.All we have to do now is see if
the Generator portal is Inside the Anti-penumbra or not. We do this by looping
though each Clip Plane and clip each generator Portal to the Clip Plane..Just to
jog your memory I will show you Diagram W) again so you can see how the clipping
works:-for(int I = 0; I <
ClipPlanes.NumberOfPlanes; i++)
{
PortalLocation=
ClassifyPoly(&ClipPlanes.Planes[i],(POLYGON *)GeneratorPortal);
int
SourcePortalLocation=ClassifyPoly(&ClipPlanes.Planes[i],(POLYGON
*)SourcePortal);
if(PortalLocation == SourcePortalLocation ||
PortalLocation==CP_ONPLANE)// its entirley out side the anitpenumbra so loose
it
{
delete Generator portal;
delete ClipPlanes.Planes;
return
NULL;
}
if((PortalLocation == CP_BACK &&
SourcePortalLocation==CP_FRONT) || (PortalLocation == CP_FRONT &&
SourcePortalLocation==CP_BACK))// inside Anti Penumbra
{
//keep
it
continue;
}
if(PortalLocation == CP_SPANNING)
{
//Clip the
portal
FrontSplit=new PORTAL;
BackSplit=new
PORTAL;
ZeroMemory(FrontSplit,sizeof(PORTAL));
ZeroMemory(BackSplit,sizeof(PORTAL));
SplitPortal(GeneratorPortal,&ClipPlanes.Planes[i],FrontSplit,BackSplit);
if
(SourcePortalLocation==CP_FRONT)
{
delete FrontSplit;
delete
GeneratorPortal;
GeneratorPortal=BackSplit;
}
else
if
(SourcePortalLocation==CP_BACK)
{
delete BackSplit;
delete
GeneratorPortal;
GeneratorPortal=FrontSplit;
}
}
}
delete
ClipPlanes.Planes;
return GeneratorPortal;
} // end
function
WOW, we are nearly all done with this PVS
stuff. We have certainly done all the hard bits all that left to do now is
compress each Visibility Buffer and add it to the Master PVSData array.Before we
do that however lets see the 'ClipToAntiPenumbra' function in its
entiretyPORTAL * ClipToAntiPenumbra(PORTAL
*SourcePortal,PORTAL *TargetPortal,PORTAL *GeneratorPortal)
{
D3DXVECTOR3
EdgeVector1,EdgeVector2,Normal;
int PortalLocation;
PORTAL
*FrontSplit,*BackSplit,*TempSource,*TempTarget;
CLIPPLANES ClipPlanes; //
Create a ClipPlane
set
ClipPlanes.NumberOfPlanes=0;
ClipPlanes.Planes=new PLANE
[SourcePortal->NumberOfVertices*TargetPortal->NumberOfVertices*2];
PLANE
TempPlane;
int NextVertex=0;
for (int
loop=0;loop<2;loop++)
{
if
(loop==0)
{
TempSource=SourcePortal;
TempTarget=TargetPortal;
}
else
{
TempSource=TargetPortal;
TempTarget=SourcePortal;
}
for
(int
SV=0;SV<TempSource->NumberOfVertices;SV++)
{
PortalLocation=ClassifyPoint((D3DXVECTOR3
*)&TempSource->VertexList[SV],&GetPortalPlane(TempTarget));
if
(PortalLocation==CP_ONPLANE) continue;
for (int
TP=0;TP<TempTarget->NumberOfVertices;TP++)
{
if
(TP==TempTarget->NumberOfVertices-1)
{
NextVertex=0;
}
else
{
NextVertex=TP+1
;
}
EdgeVector1=*((D3DXVECTOR3
*)&TempSource->VertexList[SV])-*((D3DXVECTOR3
*)&TempTarget->VertexList[TP]);
EdgeVector2=*((D3DXVECTOR3
*)&TempTarget->VertexList[NextVertex])-*((D3DXVECTOR3
*)&TempTarget->VertexList[TP]);
D3DXVec3Cross(&Normal,&EdgeVector1,&EdgeVector2);//
create normal
D3DXVec3Normalize(&TempPlane.Normal,&Normal); // make
it a unit normal
TempPlane.PointOnPlane=*(D3DXVECTOR3
*)&TempSource->VertexList[SV];//Any vertex will
do
if(ClassifyPoly(&TempPlane,(POLYGON*)TempSource)==
CP_FRONT)
{
if(ClassifyPoly(&TempPlane,(POLYGON*)TempTarget)==
CP_BACK)
{
ClipPlanes.Planes[ClipPlanes.NumberOfPlanes] =
TempPlane;
ClipPlanes.NumberOfPlanes++;
//Save clip
plane
}
}
else
if(ClassifyPoly(&TempPlane,(POLYGON
*)TempSource) ==
CP_BACK)
{
if(ClassifyPoly(&TempPlane,(POLYGON*)TempTarget) ==
CP_FRONT)
{
ClipPlanes.Planes[ClipPlanes.NumberOfPlanes] =
TempPlane;
ClipPlanes.NumberOfPlanes++;
//Save clip plane
}
}
}
// end for tp
} // end for sp
} // END FOR LOOP
// We now have our
PLane List so lets clip the Generator Portal against each one.
for(int I
= 0; I < ClipPlanes.NumberOfPlanes; i++)
{
PortalLocation=
ClassifyPoly(&ClipPlanes.Planes[i],(POLYGON *)GeneratorPortal);
int
SourcePortalLocation=ClassifyPoly(&ClipPlanes.Planes[i],(POLYGON
*)SourcePortal);
if(PortalLocation == SourcePortalLocation ||
PortalLocation==CP_ONPLANE)// its entirley out side the anitpenumbra so loose
it
{
delete Generator portal;
delete ClipPlanes.Planes;
return
NULL;
}
if((PortalLocation == CP_BACK &&
SourcePortalLocation==CP_FRONT) || (PortalLocation == CP_FRONT &&
SourcePortalLocation==CP_BACK))// inside Anti Penumbra
{
//keep
it
continue;
}
if(PortalLocation == CP_SPANNING)
{
//Clip the
portal
FrontSplit=new PORTAL;
BackSplit=new
PORTAL;
ZeroMemory(FrontSplit,sizeof(PORTAL));
ZeroMemory(BackSplit,sizeof(PORTAL));
SplitPortal(GeneratorPortal,&ClipPlanes.Planes[i],FrontSplit,BackSplit);
if
(SourcePortalLocation==CP_FRONT)
{
delete FrontSplit;
delete
GeneratorPortal;
GeneratorPortal=BackSplit;
}
else
if
(SourcePortalLocation==CP_BACK)
{
delete BackSplit;
delete
GeneratorPortal;
GeneratorPortal=FrontSplit;
}
}
}
delete
ClipPlanes.Planes;
return GeneratorPortal;
} // end
function
Ummm, there is no doubt about it, that function
can look like one ugly son of a bitch on first viewing, but if you break it down
into its two parts 'Creating The Planes' and 'Clipping the Generator portal to
the Planes' it starts looking a bit more friendly.PVSMasterWritePointer+=CompressLeafSet(LeafPVS,PVSMasterWritePointer);
long CompressLeafSet (BYTE *
VisArray, long WritePos)
{
int j;
int rep;
BYTE
*dest=&PVSData[WritePos];
BYTE *dest_p;
dest_p = dest;
for (j=0
; j<BytesPerSet ; j++)
{
*dest_p++ = VisArray[j];
if
(VisArray[j])
continue;
rep = 1;
for ( j++; j<BytesPerSet ;
j++)
if (VisArray[j] || rep == 255)
break;
else
rep++;
*dest_p++
= rep;
j--;
}
return dest_p - dest;
}
It is
sometimes hard to believe that such a small function could save so much
memory.if (pvs&mask)
{
if
(LeafInFrustum(currentleaf)==true)
{
...
Draw the
Leaf
...
During the RenderTree function we loop through each Bit in
the Camera Leafs PVS (the Leaf the Camera is in) and if it has its bit set to
'1' (if pvs&mask) then the Leaf is in the PVS set and is potentially visible
from the Camera Leaf. HOWEVER, instead of just rendering the Leaf we do a second
check by calling the 'LeafInFrustum' function.This function takes a Leaf index
as a parameter and returns TRUE if any part of the Leafs Bounding Box is within
the View Frustum(Cameras View). Therefore we don't have to render any leafs
which make this Function return false. In the above diagram, this function would
return false for all leave except Leaf 1 as this is the only Leaf that can be
partially seen from the Camera.These tests are actually remarkably Quick because
our Leafs Bounding Boxes are whats known as being 'Axis Aligned Bounding Boxes'
which means they are aligned to the Major Axis of the 3D world.In other Words
the Bounding Box Z Maximum reaches in the same direction as the Worlds Positive
Z Axis and the Box's X Maximum is in the direction of the Worlds Positive X axis
etc etc.This provides advantages because Culling Axis Aligned Bounding Boxes
(AABB's) from the frustum can be done very quickly D3DXMatrixPerspectiveFovLH(&proj_m,
1.0f,0.77777f,0.3f, 500.0f );
struct PLANE2
{
float Distance;
D3DXVECTOR3
Normal;
};
We also define at the start of our program an Global
array of six PLANE2 structures that will be used to hold the frustum Planes each
frame. PLANE2 FrustumPlanes[6];
void
renderframe(void)
{
UpdateViewPos();
ExtractFrustumPlanes(FrustumPlanes);
lpDevice->Clear(0,NULL,D3DCLEAR_TARGET
|
D3DCLEAR_ZBUFFER,D3DCOLOR_XRGB(0,0,0),1.0f,0);
lpDevice->BeginScene();
RenderTree(CameraPosition);
lpDevice->EndScene();
}
This
all makes sense but before I get a load of emails telling me how my Demo does
not do it this way round let me just say that this is because of some trickery I
had to pull off for the Top Down View mode so that when you look down from above
the Frustum Planes are NOT where the camera is (because it is up in the air
looking down) but instead the Frustum Planes remain in the same position as they
were when you were in First Person View.This allows you to View the Clipping
process from above.In case you are interested , i did this by using two view
matrices.The First one is where the Person is on the ground (or the camera if
you are in First Person Mode) and the second is the acutal camera position.In
the Demo i Build the Player location View Matrix First and use THIS (not the
camera) matrix to extract the planes and clip the tree.Then after the tree has
been culled i set the View matrix to the actual Camera Position for rendering.
Any way all of this is besides the point, in a first person game you will use a
render loop like the one i have used above.You can view the source code to see
the changes i had to make to the Top Down mode.A Word Of Thanks:- I would like to say a very big thankyou to Klaus Hartmann for bringing the following code to my attention. Klaus is keen to point out that he did not invent this and it is a guy called 'Gil Gribb' who deserves the credit.However, it was Klaus that was good enough to explain Not Only the following code but also the technique to Cull AABB's to the View Frustum and the code to the following two functions is based very tightly on code that he gave to me.Thanks again Klaus. |
void
ExtractFrustumPlanes(PLANE2 *Planes)
{
D3DXMATRIX
ViewMatrix,ProjMatrix,ViewProj;
lpDevice->GetTransform(D3DTS_PROJECTION,&ProjMatrix);
lpDevice->GetTransform(D3DTS_VIEW,&ViewMatrix);
D3DXMatrixMultiply(&ViewProj,&ViewMatrix,&ProjMatrix);
//Combine Them
// Left clipping plane
Planes[0].Normal.x =
-(ViewProj._14 + ViewProj._11);
Planes[0].Normal.y = -(ViewProj._24 +
ViewProj._21);
Planes[0].Normal.z = -(ViewProj._34 +
ViewProj._31);
Planes[0].Distance = -(ViewProj._44 + ViewProj._41);
//
Right clipping plane
Planes[1].Normal.x = -(ViewProj._14 -
ViewProj._11);
Planes[1].Normal.y = -(ViewProj._24 -
ViewProj._21);
Planes[1].Normal.z = -(ViewProj._34 -
ViewProj._31);
Planes[1].Distance = -(ViewProj._44 - ViewProj._41);
//
Top clipping plane
Planes[2].Normal.x = -(ViewProj._14 -
ViewProj._12);
Planes[2].Normal.y = -(ViewProj._24 -
ViewProj._22);
Planes[2].Normal.z = -(ViewProj._34 -
ViewProj._32);
Planes[2].Distance = -(ViewProj._44 - ViewProj._42);
//
Bottom clipping plane
Planes[3].Normal.x = -(ViewProj._14 +
ViewProj._12);
Planes[3].Normal.y = -(ViewProj._24 +
ViewProj._22);
Planes[3].Normal.z = -(ViewProj._34 +
ViewProj._32);
Planes[3].Distance = -(ViewProj._44 + ViewProj._42);
//
Near clipping plane
Planes[4].Normal.x = -(ViewProj._14 +
ViewProj._13);
Planes[4].Normal.y = -(ViewProj._24 +
ViewProj._23);
Planes[4].Normal.z = -(ViewProj._34 +
ViewProj._33);
Planes[4].Distance = -(ViewProj._44 + ViewProj._43);
//
Far clipping plane
Planes[5].Normal.x = -(ViewProj._14 -
ViewProj._13);
Planes[5].Normal.y = -(ViewProj._24 -
ViewProj._23);
Planes[5].Normal.z = -(ViewProj._34 -
ViewProj._33);
Planes[5].Distance = -(ViewProj._44 -
ViewProj._43);
// NORMALIZE THE PLANES (Not Really Needed in Our
Demo)
for (int i=0;i<6;i++)
{
float denom = 1.0f /
D3DXVec3Length(&Planes[i].Normal);// Get magnitude of
Vector
Planes[i].Normal.x *= denom;
Planes[i].Normal.y *=
denom;
Planes[i].Normal.z *= denom;
Planes[i].Distance *=
denom;
}
}// End Function Extract Clip
Planes
![]() |
Lets imagine that first of all we want to check the Left (L) Clip
plane of the Frustum and the Camera is facing due North.In this case the
Left Clip planes Normal would be facing in the Negative X Direction which
means that we will use the AABB X Maximum Point as our Near Point X
Component.The same is true for the the Z component of the Plane
Normal.This too would be a Negative component meaning we would use the
AABB Maximum Z Component as the Near points Z Component (Forget about Y
for now because we are looking top down and the plane has no Y
orientation). In this case then the Near Point (corner point of the AABB) used will be the Top Right of the Box as indicated by the Green square.Look at this Corner Point on 'BOX A' and remembering that Planes are Infinite you should be able to see that if this 'Near Point' is In Front of the Left Plane the the WHOLE AABB must be also. When testing the Right Plane a Different 'Near' Point is Used because the Plane Normals X Component now faces Positive which means we use the AABB's X Minimum component and the Near Point X Component.Z is still facing the same way however (Negative) so we still use the AABB's Max Z component.This gives us the Corner indicated by the RED box's in the Diagram. Look at 'Box B' you should also be able to see that in this case if the RED corner is Infront of the Right Plane then the entire AABB must be also. |
bool LeafInFrustum (long Leaf)
{
D3DXVECTOR3
bMax=LeafArray[Leaf].BoundingBox.BoxMax;
D3DXVECTOR3
bMin=LeafArray[Leaf].BoundingBox.BoxMin;
D3DXVECTOR3
NearPoint,FarPoint;
PLANE2 *Plane=FrustumPlanes;
for (int
i=0;i<6;i++)
{
if (Plane->Normal.x > 0.0f)
{
if
(Plane->Normal.y > 0.0f)
{
if (Plane->Normal.z >
0.0f)
{
NearPoint.x = bMin.x; NearPoint.y = bMin.y;
NearPoint.z = bMin.z;
}
else
{
NearPoint.x =
bMin.x; NearPoint.y = bMin.y; NearPoint.z = bMax.z;
}
}
else
{
if (Plane->Normal.z >
0.0f)
{
NearPoint.x = bMin.x; NearPoint.y = bMax.y;
NearPoint.z = bMin.z;
}
else
{
NearPoint.x =
bMin.x; NearPoint.y = bMax.y; NearPoint.z =
bMax.z;
}
}
}
else
{
if (Plane->Normal.y >
0.0f)
{
(Plane->Normal.z >
0.0f)
{
NearPoint.x = bMax.x; NearPoint.y = bMin.y;
NearPoint.z = bMin.z;
}
else
{
NearPoint.x = bMax.x;
NearPoint.y = bMin.y; NearPoint.z = bMax.z;
}
}
else
{
if
(Plane->Normal.z > 0.0f)
{
NearPoint.x = bMax.x;
NearPoint.y = bMax.y; NearPoint.z =
bMin.z;
}
else
{
NearPoint.x = bMax.x; NearPoint.y =
bMax.y; NearPoint.z = bMax.z;
}
}
}
// near extreme point is
outside, and thus
// the AABB is Totally outside the polyhedron
if
(D3DXVec3Dot(&Plane->Normal,&NearPoint)+Plane->Distance>0)
return false;
Plane++; }
return true;
}
Thats
it!!!. Although we have used the above function to test the Leafs of our
Bounding Box against the Frustum , the above technique works with any AABB and
so can readily be used in any other applications that need to perform Frustum
Culling of AABB's. Although Frustum Culling and Extracting the Frustum Planes
are NOT really BSP exclusive subjects and are generic to all 3D Applications i
felt by including it in this tutoria it would make the Tutorial more complete
and you well on your way to writing a lightning fast 3D engine.BSP Compiler Structures | Loader and Renderer Structures |
struct NODE { unsigned char IsLeaf; unsigned long Plane; unsigned long Front; signed long Back; BOUNDINGBOX BoundingBox; }; |
struct NODE { unsigned char IsLeaf; unsigned long Plane; unsigned long Front; signed long Back; }; |
You can see that when the Compiler saves the Node Array it does not save the Bounding Box.This means our loader and renderer application can leave this field out of its Node Structure | |
struct LEAF{ long StartPolygon; long EndPolygon; long PortalIndexList[50]; long NumberOfPortals; long PVSIndex; BOUNDINGBOX BoundingBox; }; |
struct LEAF{ long StartPolygon; long EndPolygon; long PVSIndex; BOUNDINGBOX BoundingBox; }; |
When we save the Leaf array from the compiler we do not bother to save the Portal Information because this will not be used by the Loader and Renderer.The portals were only needed to calculate the PVS.We will not save the Portal Array at all. | |
struct POLYGON { D3DLVERTEX *VertexList; D3DXVECTOR3 Normal; WORD NumberOfVertices; WORD NumberOfIndices; WORD *Indices; POLYGON * Next; bool BeenUsedAsSplitter; long TextureIndex; }; |
struct POLYGON { D3DLVERTEX *VertexList; D3DXVECTOR3 Normal; WORD NumberOfVertices; WORD NumberOfIndices; WORD *Indices; POLYGON * Next; long TextureIndex; }; |
Not so much of a saving here.We obviously do not need the 'BeenUsedAsSplitter' variable any more as this was only used for compilation.Obviously we also do not save off the 'Next' pointer as this would be meaningless when loaded back in (because it points to an actual memory address) BUT our but we still have to have this field in out 'Loader and Renderer' application because if you remember, the 'Next' pointer was also used for Texture Batching during rendering.We will still want this functionailty when in our Loader and Renderer Application. |
void SaveBSPTree(char
*filename)
{
FILE *stream;
long a;
stream = fopen(filename,
"w+b");
NumberOfNodes++;
fwrite(&NumberOfNodes,sizeof(long),1,stream);
NODE
*n=NodeArray;
for
(a=0;a<NumberOfNodes;a++)
{
fwrite(&n->IsLeaf,sizeof (unsigned
char),1,stream);
fwrite(&n->Plane, sizeof (unsigned
long),1,stream);
fwrite(&n->Front, sizeof (unsigned
long),1,stream);
fwrite(&n->Back , sizeof (signed
long),1,stream);
n++;
}
// Write the Plane
Array
fwrite(&NumberOfPlanes,sizeof(long),1,stream);
fwrite(PlaneArray,sizeof(PLANE),NumberOfPlanes,stream);
// Write Leaf
Array.This also has some reduntant Run Time fields so we shall remove
//
these when writing.
fwrite(&NumberOfLeafs,sizeof(long),1,stream); LEAF
*l=LeafArray;
for
(a=0;a&lyNumberOfLeafs;a++)
{
fwrite(&l->StartPolygon,sizeof(long),1,stream);
fwrite(&l->EndPolygon,sizeof(long),1,stream);
fwrite(&l->PVSIndex,sizeof(long),1,stream);
fwrite(&l->BoundingBox,sizeof(BOUNDINGBOX),1,stream);
l++;
}
//
WritePolygonArray.There are fields in this structure that were used for
sompiling and are
// not needed at runtime."BeenUsedAsSplitter' is not needed
and neither is the 'Next'
Pointer.
fwrite(&NumberOfPolygons,sizeof(long),1,stream);
POLYGON
*p=PolygonArray;
for
(a=0;a<NumberOfPolygons;a++)
{
fwrite(&p->NumberOfVertices,sizeof(WORD),1,stream);
fwrite(p->VertexList,sizeof(D3DLVERTEX),p->NumberOfVertices,stream);
fwrite(&p->NumberOfIndices,sizeof(WORD),1,stream);
fwrite(p->Indices,sizeof(WORD),p->NumberOfIndices,stream);
fwrite(&p->Normal,sizeof(D3DXVECTOR3),1,stream);
fwrite(&p->TextureIndex,sizeof(long),1,stream);
p++;
}
//
Now all we have to do is write the PVS Data
itself
fwrite(&PVSCompressedSize,sizeof(long),1,stream);
fwrite(PVSData,sizeof(BYTE),PVSCompressedSize,stream);
//
All done
fclose (stream);
}
As you can see we just
write the Arrays out in the following Order .1)NodeArray 2)PlaneArray 3)Leaf
Array 4)PolygonArray 5)PVSData. It is a shame actually that we have to miss out
certain fields or other wise we could have written this function in just a few
lines of code.Look at how the PLANE's array is written.We just throw the Whole
array into the file in one go because we need it all.void LoadBSPTree(char *filename)
{
FILE
*stream;
long a;
stream = fopen(filename,
"rb");
fread(&NumberOfNodes,sizeof(long),1,stream);
NodeArray=
(NODE *) malloc (NumberOfNodes*sizeof(NODE));
NODE *n=NodeArray;
for
(a=0;a<NumberOfNodes;a++)
{
fread(&n->IsLeaf,sizeof (unsigned
char),1,stream);
fread(&n>Plane, sizeof (unsigned
long),1,stream);
fread(&n>Front, sizeof (unsigned
long),1,stream);
fread(&n>Back , sizeof (signed
long),1,stream);
n++;
}
// Write the Plane
Array
fread(&NumberOfPlanes,sizeof(long),1,stream);
PlaneArray =(PLANE
*) malloc
(NumberOfPlanes*sizeof(PLANE));
fread(PlaneArray,sizeof(PLANE),NumberOfPlanes,stream);
//
Write Leaf Array.This also has some reduntant Run Time fields so we shall
remove
// these when
writing.
fread(&NumberOfLeafs,sizeof(long),1,stream);
LeafArray =(LEAF
*)malloc (NumberOfLeafs*sizeof(LEAF));
fread
(LeafArray,sizeof(LEAF),NumberOfLeafs,stream);
//
WritePolygonArray.
fread(&NumberOfPolygons,sizeof(long),1,stream);
PolygonArray=(POLYGON
*)malloc (NumberOfPolygons*sizeof(POLYGON));
POLYGON *p=PolygonArray;
for
(a=0;a<NumberOfPolygons;a++)
{
fread(&p>NumberOfVertices,sizeof(WORD),1,stream);
p>VertexList=new
D3DLVERTEX[p>NumberOfVertices];
fread(p>VertexList,sizeof(D3DLVERTEX),p>NumberOfVertices,stream);
fread(&p>NumberOfIndices,sizeof(WORD),1,stream);
p>Indices=new
WORD
[p>NumberOfIndices];
fread(p>Indices,sizeof(WORD),p>NumberOfIndices,stream);
fread(&p>Normal,sizeof(D3DXVECTOR3),1,stream);
fread(&p>TextureIndex,sizeof(DWORD),1,stream);
p++;
}
//
Now all we have to do is write the PVS Data
itself
fread(&PVSCompressedSize,sizeof(long),1,stream);
PVSData =(BYTE
*)malloc
(PVSCompressedSize*sizeof(BYTE));
fread(PVSData,sizeof(BYTE),PVSCompressedSize,stream);
//
All done
fclose (stream);
}
Piece of cake
hey?