QUAKE 3 BSP
RAY-BSP Collision Detection
Frank Puig Placeres (fpuig)
Fpuig2003@yahoo.com
Overview:
The basic idea of this tutorial is to show us how to detect collision between a ray and the BSP. This code is very basic and very important that be fully understood because the collision detection between a sphere/box use the same principle and almost the same code that this one.
Quake 3 use Brushes to handle collision. Each of the Leaf stored in the BSP data, has a number of associated brushes. Each brush is a set of planes that form a convex volume and has a property. That property functions as a flag that tell us when the brush is a solid area, water, slime, lava, among others. To handle collision, we will start from a point and traverse the BSP like in the previous tutorial (BSP 4).
OutPut:
When we trace to detect collision we need that by some way that collision gets reported. For that we introduce the next structure
struct
sMoveData{
float
Fraction;
CVector3 EndPoint;
CVector3 CollisionNormal;
bool
StartOut;
bool
AllSolid;
};.
All collision functions, and currently the one of this tutorial that handle collision between a Ray and the BSP, will return a sMoveData structure to inform us what happens with the trace. The members of that structure, means the next:
Fraction: How long has the trace move?
Fraction = 0 means not move at all.
Fraction
= 1 means complete move by not collision. Any value in between tells how much
it move.
EndPoint: Indicate the point
where we end the move just because a collision or because we get to the target.
CollisionNormal: Store the normal of the plane we collide with.
StartOut: True if the line segment starts outside of a solid volume.
AllSolid: True if the line segment is
completely enclosed in a solid volume.
Ray-BSP collision detection
The first we are going to do when we need to check collision is store the initial information in the variables we have, in order to get access to they on the traversal check. Don’t worry about that yet, we get into next.
// Global variables used to store
needed information. This will be used on the
CheckBrush CVector3
MStart; // Where The
ray Start CVector MEnd; // The ray
target (where it will end) sMoveData
CQuake3BSP::CheckRayMove( CVector3 Start,
CVector3 End) {
// Initializations, This will
change if the traversal detect it was wrong MoveData.StartOut = true; MoveData.AllSolid = false; MoveData.Fraction = 1.0f;
// Necessary info to collide with
brushes MStart = Start; MEnd = End;
// traverse the BSP tree checking collision. // start from node 0 (root) and began
on Start Point, Width a fraction of 0 // End in the End Point with a fraction of 1. // Result on MoveData.Fraction will
be returned in the 0..1 range because that’s what
we tell it to do
CheckMoveByNode
( 0, 0.0f, 1.0f, Start, End );
Once we setup the necessary info we call CheckMoveByNode who is responsible to check collision against the tree. The first parameter is the starting node.
We want to collide with the complete BSP so we put 0, which means the root. The Ray is traced from Start to End which are the last parameters. But talking about fractions, if we say that start is point labeled by 0 and End is the point labeled by 1 the any in-between fraction (0<= t <= 1) will give me a point of the ray according to the line formula:
Point = Point0 + (Point1 – Point2)*t
Check some math book or any other tutorial if you don’t get fully into that!
So that’s why the second and third parameter are 0 and 1 respectively: We traverse from the start to the end of the ray.
CheckMoveByNode change the MoveData structure. So we see the information that is there after we call the function.
Remember that MoveData.Fraction tells us how much we can move on the ray with out collision. So if in return it has the value 1, then we reach the End Point with out a collision. So we assign MoveData.EndPoint to the Target Point.
If Fraction wasn’t equal to 1, then is any value between 0 and 1. So we linearly interpolate with the line equation and compute the ending point.
if
(MoveData.Fraction ==
1.0f) { // nothing blocked the trace MoveData.EndPoint =
End; }
else
{ // collided with something MoveData.EndPoint =
Start +
(End-Start)*MoveData.Fraction; }
return
MoveData; }
CheckMoveByNode actually traverse the tree like we did it on last tutorial when rendering. It start from the node was enter (initially the root) and call children until found a leaf. When we found the leaf, we check all the solid brushes of that leaf. Remember that a brush represent some place that has a property, for example is solid or is slime, or water, etc. Brushes are stored as a set of planes that form a convex volume. Take a look to the 2D example
Lets began with the code:
But first of all: If you collide with and brush, MoveData.Fraction will be updated accordingly. So if you are tracing on a startfraction that is greater than MoveData.Fraction then you are tracing by nothing because any brush you can collide from that point on, will be farther than the current brush you collide. That’s an optimization that allow not checking all the BSP
void
CQuake3BSP::CheckMoveByNode( int
nodeIndex, float startFraction, float endFraction,
CVector3 Start, CVector3 End ) {
// If the path was blocked by a
nearer obstacle, don't bother checking
if
(startFraction > MoveData.Fraction)
return
;
if
(nodeIndex < 0) { // this is a leaf
tBSPLeaf
*leaf = &m_pLeafs[~nodeIndex];
// Get a pointer from the m_pLeafs
Array. // Remember that NodeIndex is
negative so the corresponding leafs is // ~nodeIndex which is same of saying -(NodeIndex+1)
but faster! // Loop by all the brushes
for
(
int
i = 0; i < leaf->numOfLeafBrushes; i++) {
Take a point to the i-th
brush in the leaf
tBSPBrush
*brush = &m_pBrushes[m_pLeafBrushes[leaf->leafBrush + i]];
// If the brush has
sides (is a convex volume) and is SOLID then check it out!
if
( brush->numOfBrushSides > 0 && (m_pTextures[brush->textureID].contents
& 1)) {
CheckTouchBrush
( brush ); } }
// don't have to do
anything else for leaves
return
; }
No hard till the moment! But things became a gradually complex.
Now, we know we are a Node, so we have an splitter plane who divide the world in two pieces. We need to continue with the children till find leafs, because we just can collide with the brushes that are in the leaf. So this is what we do for traversing: If the ray is complete in one of the sides of plane, then that’s not our job, is a job of the children we have on that side. (Did that make sense?) If the ray is complete on one side, then we pass it to the child of that side so he can check collision.
// this is a node. // Get pointer to the Node it self and
to the splitter plane
tBSPNode
*node = &m_pNodes[nodeIndex];
tBSPPlane
*plane = &m_pPlanes[node->plane];
// Get the distance from the
start and end point to the plane. This is done with the Plane formula:
float
startDistance =
plane->vNormal.x * Start.x
+
plane->vNormal.y
* Start.y +
plane->vNormal.z
* Start.z -
plane->d;
float
endDistance
= plane->vNormal.x * End.x +
plane->vNormal.y
* End.y +
plane->vNormal.z
* End.z -
plane->d; // If both points are in front of the plane (the positive
side) pass the ray to the front
// child as the ray comes.
if
(startDistance >= 0 && endDistance >= 0) {
CheckMoveByNode
( node->front, startFraction,
endFraction, Start, End ); } // If both points are in behind the plane (the negative side)
pass the ray to the back
// child as the ray comes.
else
if (startDistance
< 0 && endDistance < 0) {
CheckMoveByNode
( node->back, startFraction,
endFraction, Start, End ); }
If the Start and End point are in different sides of the plane, then the ray is intersecting that plane. We need to split the ray in two pieces. One that falls complete on the front side and one that is complete on the back side of the plane. But because of computer floating operation range errors, if we split exactly in the middle then the plane can say that the middle is fully in one side and not in the plane. I make my self clear: Computers has a limited accuracy. So when we compute the distance from a point to a plane the computer make some accuracy errors. And the equation could result in 0.0001 that is truly 0.0 but with the errors the machine tell us that number. Of course he will know that 0.0001 is not 0.0 so the point is not in the plane but in one side, when truly the point is on the plane. To avoid that we introduce an epsilon and we split epsilon units from the plane. See the picture.
Epsilon is a ridicule small
value. Quake use
0.03125f which is 1/32.
Now we have the ray divided in two pieces: One for the front side
and one for the back side. So we pass those new ray segments to the front and
back children. But one important thing is to know which side to traverse first,
the front or the back? We want to collide with the first brush not the last
one. So is chosen the nearest side (the side that contains the start point).
One more thing that can bring into confusions who to determine the Smiddle (Start-middle), and Emiddle
Points:
The ray Fraction that represent Smiddle from to start to end is:
Fraction = (start-epsilon) / (start - end)
And the Middle point is interpolated with the line equation we see above.
else
{ // the line spans the splitting plane
int
side1, side2; // Variables to
hold which side to traverse first and which second
float
fraction1, fraction2 // Used to
know compute the Smiddle and Emiddle.
CVector3 middle; // middle
point (Smiddle and Emiddle)
// split the segment into
two // If we know that one is
positive and the other is negative, and startDistance
< endDistance, so // startDistance
is negative and endDistance is positive (front
and back correspondingly)
if
(startDistance < endDistance) { side1 = node->back; // the back
side contains the start point so the back will be the first side2 = node->front;
float
inverseDistance = 1.0f / (startDistance
- endDistance); //
optimization fraction1 = (startDistance
- EPSILON) * inverseDistance; // compute fraction for the start-middle fraction2 = (startDistance
+ EPSILON) * inverseDistance; // compute fraction for the middle-end }
// On
the other hand if endDistance is negative and startDistance is positive
else
if (endDistance
< startDistance) { side1 = node->front; // the front
side contains the start point so the front will be the first side2 = node->back;
float
inverseDistance = 1.0f / (startDistance
- endDistance); fraction1 = (startDistance
+ EPSILON) * inverseDistance; fraction2 = (startDistance
- EPSILON) * inverseDistance; }
else
{ side1 = node->front; side2 = node->back; fraction1 = 1.0f; fraction2 = 0.0f; }
// 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;
// calculate the middle
point for the first side middle = Start + (End - Start)*fraction1;
// get the Smiddle fraction (not just the point)
float
middleFraction = startFraction
+ (endFraction - startFraction)
* fraction1;
// check the first side
CheckMoveByNode
( side1, startFraction, middleFraction, Start, middle );
// calculate the middle
point for the second side middle = Start + (End -
Start)*fraction2;
// get the Emiddle fraction (not just the point)
middleFraction
= startFraction + (endFraction
- startFraction) * fraction2;
// check the second side
CheckMoveByNode
( side2, middleFraction, endFraction, middle, End ); } }
Wow! We know how to traverse the tree. But when we get to a leaf we test collision against all solid brushes. As we see above a brush is a convex volume made by planes (brush sides). Here is the algorithm to test a ray against a convex volume:
In 2D, we have a brush and a ray. Two float variables will be set to an small value S=-1 , and to the end point E=1.
Take each side and intersect it with the ray. Assuming that a brush plane is negative to the interior volume, then we know when Start is positive and when is negative (in the pictures when Start is positive then the side is colored red and when end is positive black).
Made this for all sides:
If the side is red and the intersection fraction is greater than S then move S to the intersection.
If the side is black and the intersection fraction is smaller than E then move E to the intersection.
Let’s see what’s happen with a line that collide and with one that is don’t.
|
Ray
not collide with the brush |
Ray
collide with the brush |
Side
0 |
|
|
Side
1 |
|
|
Side
2 |
|
|
Side
3 |
|
|
Final
Result |
|
|
Can any one tell me the difference? Of course the E < S when no collision took place. And S < E when a Collision took place.
An advice: take a pencil, draw a brush and check the algorithm for several lines. If doesn’t work then go to quake and there HAS to be a place where you MUST not collide! J
We also can know the collision normal. Because is the plane that change the S the last time! That’s why in the code I has a variable named CandidateToHitNormal and every time I change S, the variable took the plane’s normal. By that way if at the end I found collision, CandidateToHitNormal is the normal.! Did that make sense to you?
void
CQuake3BSP::CheckTouchBrush( tBSPBrush
*brush) {
float
startFraction = -1.0f; // The S in the
pictures
float
endFraction = 1.0f; // The E in the pictures
bool
startsOut
= false; // If not
plane change this value then the ray start IN
bool
endsOut
= false; // If not plane change this value then the ray end IN CVector3 CandidateToHitNormal; // Go for every brush side
for
(
int
i = 0; i < brush->numOfBrushSides; i++) {
// take a pointer to the
brush side and the plane it represent
tBSPBrushSide
*brushSide = &m_pBrushSides[brush->brushSide + i];
tBSPPlane
*plane =
&m_pPlanes[brushSide->plane];
// Compute distances from
the plane to the MStart and MEnd
(the values enter at the begging of the trace float
startDistance
= plane->vNormal.x * MStart.x + plane->vNormal.y * MStart.y + plane->vNormal.z * MStart.z - plane->d;
float
endDistance
= plane->vNormal.x * MEnd.x + plane->vNormal.y * MEnd.y + plane->vNormal.z * MEnd.z - plane->d; // if the start point is in front this plane, then it can be IN
the brush so it’s OUT
if
(startDistance > 0) startsOut = true; // if the end point is in front this plane, then it can be IN
the brush so it’s OUT
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
; } // The line is Red so is
entering the brush. // Compute the new S
(fraction) // if (the new S) > S
move S and
update CandidateToHitNormal
if
(startDistance > endDistance) {
float
fraction = (startDistance - EPSILON) / (startDistance - endDistance);
if
(fraction > startFraction){
startFraction
= fraction; CandidateToHitNormal
= plane->vNormal; } }
else
// The line is Black so is
leaving the brush. // Compute the new E
(fraction) // if (the new E) < E move E {
float
fraction = (startDistance + EPSILON) / (startDistance - endDistance);
if
(fraction < endFraction)
endFraction
= fraction; } } // After checking all sides if startOut remain false, then the Start Point is complete
in the brush
if
(startsOut == false) { MoveData.StartOut =
false;
// if also
the EndPoint is in this brush, then the ray is
complete in this brush
if
(endsOut == false) MoveData.AllSolid =
true;
return
; }
// if there was collision against
the brush (S < E)
if
(startFraction < endFraction) {
// And
of course if we move S (if we don’t then we don’t enter the brush
// and we don’t collide
with some nearest brush
if
(startFraction > -1 && startFraction < MoveData.Fraction) {
// UpDate OutPut values
if
(startFraction < 0) startFraction
= 0; MoveData.Fraction = startFraction; MoveData.CollisionNormal
= CandidateToHitNormal; //
The candidate gets postulate! } } }
Till here you have all you need to detect BSP-Ray collision. Check the code the see it in practice and if you don’t understand anything, email me and I will try to be more clear.
If you understand all that stuff, then you can understand very easily the BSP-Sphere and BSP-Box, just because they use the same algorithm. I will post other tutorial covering them.
Usefull Links:
Really
nice collision page:
http://www.nathanostgard.com/tutorials/quake3/collision/
Box-BSP
code: http://www.gametutorials.com/forum/topic.asp?TOPIC_ID=3810&whichpage=1
Quake
2 source code:
ftp://ftp.idsoftware.com/idstuff/source/q2source-3.21.zip
Quake
Fusion Proyect:
http://hkitchen.quakesrc.org/
Frank Puig Placeres ( fpuig2003@yahoo.com )
Last Revised