| DevMaster.net |
| Home | Forums | 3D Engines Database | Wiki | Articles/Tutorials | Game Dev Jobs | IRC Chat Network | Contact Us | |
| Quake 3 BSP Collision Detection |
|
|
|
16/08/2003
|
||
1. IntroductionThis document is written to be a tutorial to teach how to implement collision detection for the BSP46 (Quake 3's) map format. It is written for people who already understand how the Quake 3 BSP structure works (and, preferrably, have written a renderer of at least a basic level for the map format). All of the code in the document is written in pseudo-code. Anything you see referenced with BSP.* means the properties shown of the BSP file itself, after it has been parsed. I don't bother worrying about specific types for structures or classes at this point - it would only be confusing without any frame of reference. When I've written the other tutorials on the map format itself, I will change this code to reflect the types I explain there. As such, I will only be using the following types in my code:
2. OverviewIn a Quake 3 BSP, each leaf in the world has a set of brushes that are associated with it. Each brush, in turn, has a set of brush planes which are associated with it. When clipped together, these brush planes form a convex volume which represents the brush that you are trying to perform collision detection against. So, the way that id Software approached an interface to this collision detection, and the way I, too, decided to design it, was to provide a single trace function for the game to call which will determine if there is a collision with the world (among other uses). This trace function takes several arguments. First, it takes the starting point for a line segment. Second, it takes the point that you are trying to end up at. It also takes a bounding box around this point. Just to clarify, this box represents the bounding area for the object that you are trying to move through the world. It is not the bounding box for the line segment. The trace function moves along the line between this starting and ending point, and checks each leaf it encounters. If it collides with something, it stores a value representing how far along this line it got before it ran into something, and then progresses on. Each time it runs into something, if the collision point is closer than the last collision point, it stores the new one instead. This will give you the closest point of collision along the line as an end result. Now for the more technical details. As you know, a BSP tree is made up of many nodes, each with a dividing plane, splitting and splitting until it results in a leaf with no children. To progress through the BSP tree, you start at the root node and check your point against each splitting plane, and progress down the front or back split of the node, depending on where you are located in relation to the plane. The knowledge of how this works is essential to understanding the collision detection process, so if you do not understand this, let me know and I will explain it in more detail. 3. The VariablesSo, the pieces of data we have available to us are as follows: INPUTS
OUTPUTS
We'll declare them (outputs as globals, inputs as parameters to the function) and the initial Trace function, as follows: float outputFraction; vector outputEnd; boolean outputStartsOut; boolean outputAllSolid; void Trace( vector inputStart, vector inputEnd ) { // ... } Don't worry about the Trace function just yet - we'll get into that later. Note that I wouldn't normally lay out my code with global input and output variables like this, but it just makes it a lot simpler for the purposes of the document. 4. Tracing with a LineLet's look at how we would perform a trace using this data with a single ray first, disregarding the bounding box data. This is a lot simpler, and will make the rest of it easier to comprehend. 4A. Checking the NodesIn the beginning, the process is very similar to the one you would use to walk through a BSP tree for other purposes (such as rendering). We start at the root node, and we check start and end against the node's splitting plane using the plane equation, like we normally would: void CheckNode( int nodeIndex, float startFraction, float endFraction, vector start, vector end ) { object *node = &BSP.nodes[nodeIndex]; object *plane = &BSP.planes[node->planeIndex]; float startDistance = DotProduct( start, plane->normal ) - plane->distance; float endDistance = DotProduct( end, plane->normal ) - plane->distance; } This gives us two floats which represent the distances of each point from the splitting plane. We can then use these values to determine which nodes to check. The text in gray is the code we've already gone over, and anything else is new. void CheckNode( int nodeIndex,
float startFraction, float endFraction,
vector start, vector end )
{
object *node = &BSP.nodes[nodeIndex];
object *plane = &BSP.planes[node->planeIndex];
float startDistance = DotProduct( start, plane->normal ) - plane->distance;
float endDistance = DotProduct( end, plane->normal ) - plane->distance;
if (startDistance >= 0 && endDistance >= 0) // A
{ // both points are in front of the plane
// so check the front child
CheckNode( node->children[0], startFraction, endFraction, start, end );
}
else if (startDistance < 0 && endDistance < 0) // B
{ // both points are behind the plane
// so check the back child
CheckNode( node->children[1], startFraction, endFraction, start, end );
}
else // C
{ // the line spans the splitting plane
/* ... (will fill in) ... */
}
}
What does the code above do? Well, we have three situations in this case. Just like when you walk the BSP tree for rendering, our point can either be in front (A) or behind (B) the splitting plane (we treat points that are coplanar with the plane as in front of it). But, since we're dealing with a line segment, we have a third option: the line can span both sections of the node, or cross the splitting plane (C). In the case of A, we know the entire line segment is in front of the splitting plane, so we only have to progress down the front child of the current node. B is the same, but for the back child instead. For C, we have to check both children of the node. As you've probably noticed with distress, this section isn't filled in on the code listing above. That's because it takes a little bit more code than the other two, so I've expanded it below: void CheckNode( int nodeIndex,
float startFraction, float endFraction,
vector start, vector end )
{
object *node = &BSP.nodes[nodeIndex];
object *plane = &BSP.planes[node->planeIndex];
float startDistance = DotProduct( start, plane->normal ) - plane->distance;
float endDistance = DotProduct( end, plane->normal ) - plane->distance;
if (startDistance >= 0 && endDistance >= 0)
{ // both points are in front of the plane
// so check the front child
CheckNode( node->children[0], startFraction, endFraction, start, end );
}
else if (startDistance < 0 && endDistance < 0)
{ // both points are behind the plane
// so check the back child
CheckNode( node->children[1], startFraction, endFraction, start, end );
}
else
{ // the line spans the splitting plane
int side;
float fraction1, fraction2, middleFraction;
vector middle;
// STEP 1: split the segment into two
if (startDistance < endDistance)
{
side = 1; // back
float inverseDistance = 1.0f / (startDistance - endDistance);
fraction1 = (startDistance + EPSILON) * inverseDistance;
fraction2 = (startDistance + EPSILON) * inverseDistance;
}
else if (endDistance < startDistance)
{
side = 0; // front
float inverseDistance = 1.0f / (startDistance - endDistance);
fraction1 = (startDistance + EPSILON) * inverseDistance;
fraction2 = (startDistance - EPSILON) * inverseDistance;
}
else
{
side = 0; // front
fraction1 = 1.0f;
fraction2 = 0.0f;
}
// STEP 2: make sure the numbers are valid
if (fraction1 < 0.0f) fraction1 = 0.0f;
else if (fraction1 > 1.0f) fraction1 = 1.0f;
if (fraction2 < 0.0f) fraction2 = 0.0f;
else if (fraction2 > 1.0f) fraction2 = 1.0f;
// STEP 3: calculate the middle point for the first side
middleFraction = startFraction +
(endFraction - startFraction) * fraction1;
for (i = 0; i < 3; i++)
middle[i] = start[i] + fraction1 * (end[i] - start[i]);
// STEP 4: check the first side
CheckNode( node->children[side], startFraction, middleFraction,
start, middle );
// STEP 5: calculate the middle point for the second side
middleFraction = startFraction +
(endFraction - startFraction) * fraction2;
for (i = 0; i < 3; i++)
middle[i] = start[i] + fraction2 * (end[i] - start[i]);
// STEP 6: check the second side
CheckNode( node->children[!side], middleFraction, endFraction,
middle, end );
}
}
At this point, you're probably a little confused. I've labeled each step in the code with numbers, so let's go through them one by one.
We're almost there! Right now we have the code to walk all the way down the nodes to the leaves we might possibly collide with. But how do we know when we've reached a leaf, and what do we do when we find one? Quake 3's map format actually makes this very easy to detect. As you know, each node has two values for its children (node->children[0 and 1]) which are indexes into the node array. If the value is negative, it means that the next child is a leaf. So how do we use this? Let's add something to the starting of our function. void CheckNode( int nodeIndex,
float startFraction, float endFraction,
vector start, vector end )
{
if (nodeIndex < 0)
{ // this is a leaf
object *leaf = &BSP.leaves[-(nodeIndex + 1)];
for (i = 0; i < leaf->numLeafBrushes; i++)
{
object *brush = &BSP.brushes[BSP.leafBrushes[leaf->firstLeafBrush + i]];
if (brush->numSides > 0 &&
(BSP.shaders[brush->shaderIndex].contentFlags & 1))
CheckBrush( brush );
}
// don't have to do anything else for leaves
return;
}
// this is a node
object *node = &BSP.nodes[nodeIndex];
object *plane = &BSP.planes[node->planeIndex];
float startDistance = DotProduct( start, plane->normal ) - plane->distance;
float endDistance = DotProduct( end, plane->normal ) - plane->distance;
...
}
The code above does one thing: if the current node is a leaf, it goes through each brush in that leaf, and, if needed, checks the brush for a collision. I say if needed because some brushes should not be checked. Those include those without any brush sides (or planes), and those that aren't solid. Very Important: You can see that I underlined a section of the code above, and it is relevant to this point. When I first implemented collision detection with this BSP format, I was banging my head on the wall trying to figure out why it wasn't working. It turns out I wasn't performing the check I do above, and because of this, I was checking against brushes that weren't supposed to be solid. This value we're doing the bitwise AND against should actually be specified by the game itself, as the Quake games do, but I didn't want to complicate it. The check here just makes sure that it is solid (1 == CONTENTS_SOLID in the Quake source code). If the brush is supposed to be checked, we call the function CheckBrush. CheckBrush is the next part of our collision detection solution. 4B. Checking the BrushesEach brush in the BSP has a number of brush sides that are associated with it. Each of these brush sides contains an index into the plane array for the plane that represents it. In order for us to check for collisions against a brush, we have to loop through this list of brushes, and check each individually. void CheckBrush( object *brush ) { for (int i = 0; i < brush->numSides; i++) { object *brushSide = &BSP.brushSides[brush->firstSide + i]; object *plane = &BSP.planes[brushSide->planeIndex]; } } As you can see, there isn't anything here except the loop itself, so let's fill it out some more. Our first few steps, again, are much the same as always with planes: void CheckBrush( object *brush )
{
float startFraction = -1.0f;
float endFraction = 1.0f;
boolean startsOut = false;
boolean endsOut = false;
for (int i = 0; i < brush->numSides; i++)
{
object *brushSide = &BSP.brushSides[brush->firstSide + i];
object *plane = &BSP.planes[brushSide->planeIndex];
float startDistance = DotProduct( inputStart, plane->normal ) -
plane->distance;
float endDistance = DotProduct( inputEnd, plane->normal ) -
plane->distance;
if (startDistance > 0)
startsOut = true;
if (endDistance > 0)
endsOut = true;
// make sure the trace isn't completely on one side of the brush
if (startDistance > 0 && endDistance > 0)
{ // both are in front of the plane, its outside of this brush
return;
}
if (startDistance <= 0 && endDistance <= 0)
{ // both are behind this plane, it will get clipped by another one
continue;
}
// MMM... BEEFY
if (startDistance > endDistance)
{ // line is entering into the brush
float fraction = (startDistance - EPSILON) / (startDistance - endDistance); // *
if (fraction > startFraction)
startFraction = fraction;
}
else
{ // line is leaving the brush
float fraction = (startDistance + EPSILON) / (startDistance - endDistance); // *
if (fraction < endFraction)
endFraction = fraction;
}
}
}
The first 2 lines of this addition should look familiar: it's the plane equation again. We use these two distances to find out if the line segment actually intersects with each brush plane. Ignore the startsOut and endsOut for now, we'll address that later. Look at the code below it. We only want to test this line segment if it traverses one of the brush planes. The first check verifies that its inside the brush at all; if a line segment is completely in front of any of the planes in a brush, we can disregard that brush. The second if statement confirms that it actually crosses the plane. If both the points are behind the plane, we don't bother testing it - it will be clipped by one of the other brush sides. Now we get to the third part, labelled by the underlined comment. This is the meat of the entire algorithm. At this point we know that the line crosses the plane (and collides with the brush), we just have to find out which side collides, and how much of it. The greater distance is the one that is in front of the plane. Once we know which side is in front, we calculate the fractional value with the code which have commented red articstik on the two lines above. If this fractional value is less than the appropriate variable (startFraction or endFraction), it means that its a closer collision than the last one, so we set the variable to it. We only have one more thing to add to this function. void CheckBrush( object *brush )
{
float startFraction = -1.0f;
float endFraction = 1.0f;
boolean startsOut = false;
boolean endsOut = false;
for (int i = 0; i < brush->numSides; i++)
{
object *brushSide = &BSP.brushSides[brush->firstSide + i];
object *plane = &BSP.planes[brushSide->planeIndex];
float startDistance = DotProduct( inputStart, plane->normal ) -
plane->distance;
float endDistance = DotProduct( inputEnd, plane->normal ) -
plane->distance;
if (startDistance > 0)
startsOut = true;
if (endDistance > 0)
endsOut = true;
// make sure the trace isn't completely on one side of the brush
if (startDistance > 0 && endDistance > 0)
{ // both are in front of the plane, its outside of this brush
return;
}
if (startDistance <= 0 && endDistance <= 0)
{ // both are behind this plane, it will get clipped by another one
continue;
}
if (startDistance > endDistance)
{ // line is entering into the brush
float fraction = (startDistance - EPSILON) / (startDistance - endDistance);
if (fraction > startFraction)
startFraction = fraction;
}
else
{ // line is leaving the brush
float fraction = (startDistance + EPSILON) / (startDistance - endDistance);
if (fraction < endFraction)
endFraction = fraction;
}
}
if (startsOut == false)
{
outputStartOut = false;
if (endsOut == false)
outputAllSolid = true;
return;
}
if (startFraction < endFraction)
{
if (startFraction > -1 && startFraction < outputFraction)
{
if (startFraction < 0)
startFraction = 0;
outputFraction = startFraction;
}
}
}
Now we can talk about the section I told you to ignore before. If either end's distance is greater than zero, it means that end starts outside of the brush. So, we check for this and set the startsOut and endsOut variables to true if this is true for any of the brush sides. If, after checking all of the brush sides, both of these variables are false, the trace is stuck inside of the brush, so we set outputAllSolid to false, and return. The last conditional block in this function is the one that will actually apply the collision information to the output data. If the following conditions are satisfied:
Then we set the outputFraction equal to this fraction. 4C. Using the Check FunctionsSo now that we have these two functions that we can use to check for the collision of a ray against the BSP, how do we actually use them? I've written an example implementation of a general Trace function below. Just like the other source listings, this is generic pseudo-code, and I'm just setting the output* variables instead of actually returning data. You'd have to adjust it for your own engine. Refer to SECTION4.TXT for a full source listing for this section. void Trace( vector inputStart, vector inputEnd ) { outputStartsOut = true; outputAllSolid = false; outputFraction = 1.0f; // walk through the BSP tree CheckNode( 0, 0.0f, 1.0f, inputStart, inputEnd ); if (outputFraction == 1.0f) { // nothing blocked the trace outputEnd = inputEnd; } else { // collided with something for (i = 0; i < 3; i++) { outputEnd[i] = inputStart[i] + outputFraction * (inputEnd[i] - inputStart[i]); } } } 5. Tracing with a SphereThe details in modifying our code to allow us to check against a sphere instead of a ray aren't that complicated - but I'm going to add a bit of code to provide an interface to the game for us to specify what type of trace we want to do. Sometimes you may only want to trace by a ray (when finding the path of a bullet, for example), and other times you may want to trace with a sphere, so we'll provide a method for the game to easily to either or. First, we're going to add some variables to our code. They keep track of the type of trace we're performing, and what constraints we want to put on that trace. Here are the variables: #define TT_RAY 0 #define TT_SPHERE 1 int traceType; float traceRadius; Our first variable is traceType. This is set to let our tracing functions change how they work depending on what type of trace we want to perform. The possible values of this variable are #defined above it. Like much that I do in this document, I'm using #defines for simplicity - I would actually recommend using an enum instead. The second variable, represents the radius of the bounding sphere of the object we are trying to trace through the world. This value should be positive. Now let's create two functions for the game to call for the different types of traces. void TraceRay( vector inputStart, vector inputEnd ) { traceType = TT_RAY; Trace( inputStart, inputEnd ); } void TraceSphere( vector inputStart, vector inputEnd, float inputRadius ) { traceType = TT_SPHERE; traceRadius = inputRadius; Trace( inputStart, inputEnd ); } Both of these functions just set the trace* variables appropriately, and then pass off control to the Trace function for the rest of it. I don't set traceRadius in TraceRay because it will never be used for a ray based trace... as you will soon see. Now we must modify the CheckNode function to account for the extra padding we have around our object in sphere tracing mode. The modifications are shown below. void CheckNode( int nodeIndex,
float startFraction, float endFraction,
vector start, vector end )
{
[ ... unchanged code ... ]
// this is a node
object *node = &BSP.nodes[nodeIndex];
object *plane = &BSP.planes[node->planeIndex];
float startDistance, endDistance, offset;
startDistance = DotProduct( start, plane->normal ) - plane->distance;
endDistance = DotProduct( end, plane->normal ) - plane->distance;
if (traceType == TT_RAY)
{
offset = 0;
}
else if (traceType == TT_SPHERE)
{
offset = traceRadius;
}
if (startDistance >= offset && endDistance >= offset)
{ // both points are in front of the plane
// so check the front child
CheckNode( node->children[0], startFraction, endFraction, start, end );
}
else if (startDistance < -offset && endDistance < -offset)
{ // both points are behind the plane
// so check the back child
CheckNode( node->children[1], startFraction, endFraction, start, end );
}
else
{ // the line spans the splitting plane
int side;
float fraction1, fraction2, middleFraction;
vector middle;
// split the segment into two
if (startDistance < endDistance)
{
side = 1; // back
float inverseDistance = 1.0f / (startDistance - endDistance);
fraction1 = (startDistance - offset + EPSILON) * inverseDistance;
fraction2 = (startDistance + offset + EPSILON) * inverseDistance;
}
else if (endDistance < startDistance)
{
side = 0; // front
float inverseDistance = 1.0f / (startDistance - endDistance);
fraction1 = (startDistance + offset + EPSILON) * inverseDistance;
fraction2 = (startDistance - offset - EPSILON) * inverseDistance;
}
else
{
side = 0; // front
fraction1 = 1.0f;
fraction2 = 0.0f;
}
[ ... unchanged code ... ]
}
}
We've made a simple but essential modification to this function. In the first change, we've added a new variable: offset. This variable tells us how close something has to be to a plane to be considered a collision. In the case of the ray trace, it has to be directly against something to hit it, so we set offset to zero. In the case of a sphere, we set it to the radius of the sphere. Our second change pads the middle point by the amount we chose for the offset, so that we're that much on either side of the split. Now we need to modify the CheckBrush function. void CheckBrush( object *brush )
{
float startFraction = -1.0f;
float endFraction = 1.0f;
boolean startsOut = false;
boolean endsOut = false;
for (int i = 0; i < brush->numSides; i++)
{
object *brushSide = &BSP.brushSides[brush->firstSide + i];
object *plane = &BSP.planes[brushSide->planeIndex];
float startDistance, endDistance;
if (traceType == TT_RAY)
{
startDistance = DotProduct( inputStart, plane->normal ) -
plane->distance;
endDistance = DotProduct( inputEnd, plane->normal ) -
plane->distance;
}
else if (traceType == TT_SPHERE)
{
startDistance = DotProduct( inputStart, plane->normal ) -
(plane->distance + traceRadius);
endDistance = DotProduct( inputEnd, plane->normal ) -
(plane->distance + traceRadius);
}
if (startDistance > 0)
startsOut = true;
if (endDistance > 0)
endsOut = true;
[ ... unchanged code ... ]
}
The modification we have done here is very similar to the one we made to CheckNode. We're just padding the distance with the radius of the sphere. And that's it! As you can see the modifications are pretty simple once you understand how the collision detection system works. If you call TraceSphere with the parameters you want, it will keep your movement from getting right up against things. Check out SECTION5.TXT for a full source listing of this section. 6. Tracing with a BoxSome situations call for the use of a bounding box instead of a bounding sphere. Adding this functionality to our system isn't too hard. First, let's add a new trace type and some trace variables: #define TT_RAY 0
#define TT_SPHERE 1
#define TT_BOX 2
int traceType;
float traceRadius;
vector traceMins;
vector traceMaxs;
vector traceExtents;
The first two variables represent two corners of the bounding box which surrounds our object. These values are in object space, like the radius. For example: for a box that is 20 units in size in all directions and is centered around the origin of the object, traceMins would be (-10,-10,-10) and traceMaxs would be (10,10,10). traceExtents stores the maximum of the absolute value of each axis in the box. For example, if traceMins was (-100,-3,-15) and traceMaxs was (55,22,7), traceExtents would be (100,22,15). Now, let's add our new trace function: void TraceBox( vector inputStart, vector inputEnd, vector inputMins, vector inputMaxs ) { if (inputMins[0] == 0 && inputMins[1] == 0 && inputMins[2] == 0 && inputMaxs[0] == 0 && inputMaxs[1] == 0 && inputMaxs[2] == 0) { // the user called TraceBox, but this is actually a ray TraceRay( inputStart, inputEnd ); } else { // setup for a box traceType = TT_BOX; traceMins = inputMins; traceMaxs = inputMaxs; traceExtents[0] = -traceMins[0] > traceMaxs[0] ? -traceMins[0] : traceMaxs[0]; traceExtents[1] = -traceMins[1] > traceMaxs[1] ? -traceMins[1] : traceMaxs[1]; traceExtents[2] = -traceMins[2] > traceMaxs[2] ? -traceMins[2] : traceMaxs[2]; Trace( inputStart, inputEnd ); } } After writing TraceRay and TraceSphere, this function shouldn't be too hard to grasp. The first if statement just verifies that they actually gave us a bounding box. If all of the values are zero, there is no point in using a box test - it is just a ray, so we pass it off. If it is indeed a box, we setup all of our trace* variables accordingly. traceMins and traceMaxs are just copied over from the input variables, and traceExtents is built from those to get the values that we described above. We have to make some modifications to our other functions to do a box trace. The modifications to the CheckNode function are as follows: void CheckNode( int nodeIndex,
float startFraction, float endFraction,
vector start, vector end )
{
[ ... unchanged code ... ]
if (traceType == TT_RAY)
{
offset = 0;
}
else if (traceType == TT_SPHERE)
{
offset = traceRadius;
}
else if (traceType == TT_BOX)
{
// this is just a dot product, but we want the absolute values
offset = (float)(fabs( traceExtents[0] * plane->normal[0] ) +
fabs( traceExtents[1] * plane->normal[1] ) +
fabs( traceExtents[2] * plane->normal[2] ) );
}
[ ... unchanged code ... ]
}
What we're doing here is taking the absolute value of the extents for each axis, and multiplying it by the plane normal, then adding them together to get our offset. How does this give us our offset? If you remember back to your dot product math (as this really is just a dot product, but we're using the absolute values of the multiplication), the dot product of a point and the normalized vector for a plane give us the distance of the point from the plane. By doing what we do above, we're basically taking the bounding box, padding it with the maximum size on any axis, and finding how far away that is from the plane. We can use the value this results in to find out if they have a possibility of colliding. Here are the changes to the CheckBrush function: void CheckBrush( object *brush )
{
[ ... unchanged code ... ]
if (traceType == TT_RAY)
{
startDistance = DotProduct( inputStart, plane->normal ) -
plane->distance;
endDistance = DotProduct( inputEnd, plane->normal ) -
plane->distance;
}
else if (traceType == TT_SPHERE)
{
startDistance = DotProduct( inputStart, plane->normal ) -
(plane->distance + traceRadius);
endDistance = DotProduct( inputEnd, plane->normal ) -
(plane->distance + traceRadius);
}
else if (traceType == TT_BOX)
{
vector offset;
for (int j = 0; j < 3; j++)
{
if (plane->normal[j] < 0)
offset[j] = traceMaxs[j];
else
offset[j] = traceMins[j];
}
startDistance = (inputStart[0] + offset[0]) * plane->normal[0] +
(inputStart[1] + offset[1]) * plane->normal[1] +
(inputStart[2] + offset[2]) * plane->normal[2] -
plane->distance;
endDistance = (inputEnd[0] + offset[0]) * plane->normal[0] +
(inputEnd[1] + offset[1]) * plane->normal[1] +
(inputEnd[2] + offset[2]) * plane->normal[2] -
plane->distance;
}
[ ... unchanged code ... ]
}
This works pretty much the same way that it does for the sphere tracing, except we have to calculate a padding vector, since we're working with a bounding box with the possibility of being a different size on each axis. So, we calculate the offset and perform a dot product, adding the offset to the two input points. And you're all done! See the file SECTION6.TXT for a full source listing. 7. ConclusionAfter reading this tutorial, you should have a good understanding of how you can use the data stored in a Quake 3 BSP file to do collision detection in your engine. If, for any reason, you don't and have some questions or comments, I urge you to e-mail me at nostgard@lvcm.com. I eagerly await your messages. Some informed readers may notice that I don't take advantage of some tricks the Quake 3 engine uses (such as optimizations for axis aligned planes). The document was long enough already, and I didn't want to get into this yet. I will be creating a second document offering ways to optimize your code. If you don't want to wait for that, you should take a look at the Quake 2 source code. My code is heavily based on it, and you should be able to see the optimizations immediately. Here are some links you can look at:
I hope you enjoyed the tutorial, and remember to send me your comments! Download Section 4 source code listing (Tracing with a
Line) |
|
|
| © 2003-2004 DevMaster.net. All Rights Reserved. Terms of Use & Privacy Policy | Want to write for us? Click here |