Menu

RENDERING PROCESS:

MISCELLANEOUS:


Mail me ! Go home

Step V: Horizontal Intersections

INTRODUCTION:
This page has quite a lot of information on it, and is basically the heart of this site (the casting routines are a big part of the engine's core). The text is maths-oriented, and to keep everything clear, I'll show the derivative steps in finding equations as well. Enjoy :)

OVERVIEW:
The basis of checking horizontal grid-lines is the diagram that is shown below, fig10:
images/fig10.jpg
fig10: A ray intersecting horizontal grid-lines

Here we see a ray, r, being cast out from the point P (this is in world coordinates, not cell coordinates) at an angle of alpha degrees (for the sake of clarity and to make life easier, alpha in the diagram isn't in our special world orientation system (as you can see it's measured from the horizontal); it makes no difference to the maths, in case you were wondering).

Along the way to the three wall cubes, you can see the ray intersects four horizontal grid-lines, labelled i1 to i4 (note that the part of the wall between i3 and i4 is ignored, as that's on a vertical grid-line...)

Another point to note is that the blue triangles are all the same size (this can be proven by using similar triangles, I believe; but you can take my word for it :) except for the one with corners at i1 and P, which is a special case. OK, now that you hopefully the diagram, it's time to get stuck in...

STARTING OFF:
To begin with, we've got to calculate the position of the first intersection, i1. In fig10, you can see that there's a small blue triangle with one corner at P and one corner at i1 (this is the one that's different to all of the other blue ones). We can deduce the Y coordinate of this intersection, as it's simply the Y coordinate of the gridline above P. This is trivial to derive, and can be done like so:

i1.y = (int(P.y / CELL_HEIGHT) * CELL_HEIGHT) - 1

Basically, the above just rounds P.y to the gridline above P (remember to round down...). However, you'll notice that there's an extra term tagged onto the end that you probably weren't expecting; a -1. This is to ensure that the intersection point is viewed as part of the cell above.

While we're on the subject of Ys, you can see that dy in the diagram is the height of a cell, and it would be negative due to the direction of the ray (remember the direction axes I presented in step I).

But, what if the ray was being cast in a downwards direction ? Well, then we would have to add CELL_HEIGHT onto i1.y to get the gridline below P, and also obviously make dy positive. A little chunk of pseudo-code to calculate these values may look like this:

if (ray going upwards)
{
	i.y = ((P.y / CELL_HEIGHT) * CELL_HEIGHT) - 1;
	dy  = -CELL_HEIGHT;
}
else
{
	i.y = ((P.y / CELL_HEIGHT) * CELL_HEIGHT) + CELL_HEIGHT;
	dy  = CELL_HEIGHT;
}

And now we've finished our warm-up...the next part enters into the wonderful world of trigonometry...'Yay !' I hear you cry :)

FINDING THE X STUFF:
As with finding the Y stuff, the first bits I'll derive will be to do with the first intersection point, i1 (shortened to just i from here on in). It's time for another diagram:
images/fig11.jpg
fig11: Finding i1's X coordinate

The above figure is just the first blue triangle from fig10 blown up, and with a few extra labels. P is the ray's origin, and i is the point of first intersection. We can see that the vertical side of the triangle has length i.y - P.y, and since we know both from the above equations, the actual length of it can be calculated.

Using basic trigonometry, the formula:

         i.y - P.y
TAN(a) = ---------
         i.x - P.x

can be constructed. A quick rearrangement yields:

            i.y - P.y
i.x - P.x = ---------
             TAN(a)

But that gives us the length of the side, and we need to know the X coordinate of the intersection; no problem. We can simply add P.x (which we already know) to both sides of the equation, which gives us:

      i.y - P.y
i.x = --------- + P.x
        TAN(a)

as the final equation; viola !

And using the same trigonometrical principles, we can determine dx as shown in fig10:

         dy
TAN(a) = --
         dx

Changing the subject of the above, the value of dx can be found, thus:

       dy
dx = ------
     TAN(a)

There is a catch, however. If the ray is facing downwards, then dx must be negated. For those of you who are interested in knowing why this is so, then I present an explanation below. For those of you who are happy just to accept the reason `because it is and I said so', then you can safely skip over these ramblings.

The reason that dx must be negated is due to the signs of tangents in different circle quadrants, and the choice of our orientation system. Consider the following diagram:
images/fig12.jpg
fig12: Tangent signs in different circle quadrants

The above shows that the tangent of any angle between 0 and 90 degrees will be positive, and so will those for angles 180 to 270 degrees. Also marked on the diagram, in red, are the signs that dx and dy must be for the ray to be headed in the right direction for each quadrant. So, to cast a ray at, say, 45°, then dx must be positive, and dy must be negative if the ray is to step through the grid in the right direction. Now, let's plug some values into our equations for calculating the two deltas (dx and dy).

Let's assume that we're casting a ray at 45°, which would mean dx would have to be positive, and dy negative. So we have:

dy = -CELL_HEIGHT

        dy
dx  = -------
      TAN(45)

Since -CELL_HEIGHT is always negative (or at least it should be...), and TAN(45°) = +1, then the value placed into dx must be negative. That satisfies the signs for the two deltas in that quadrant. Let's see if that is so for a ray being cast at 275°...

dy = CELL_HEIGHT

         dy
dx  = --------
      TAN(275)

The dy delta is positive, which is correct for the quadrant, and TAN(275°) is -11.430052303... A positive value over a negative one yields a negative value, so dx would be negative; from fig12 we can see this is WRONG ! This is why dx must be negated for the lower two circle quadrants (if alpha is > 180° using our orientation system). It may be an idea to try it yourself for the other quadrants and familiarise yourself with the concept, as getting this wrong produces strange effects that are hard to track down in the code.

With everything we've done so far in mind, a quick bit of pseudo-code may look something like:

dy = CELL_HEIGHT;
dx = dy / tangent(alpha);

if (ray going upwards)
{
	i.y = ((P.y / CELL_HEIGHT) * CELL_HEIGHT) - 1;
	dy  = -dy;
}
else
{
	i.y = ((P.y / CELL_HEIGHT) * CELL_HEIGHT) + CELL_HEIGHT;
	dx  = -dx;
}

i.x = P.x + ((i.y - P.y) / tangent(alpha));

There. We've finished the initialization code of the horizontal ray-casting function. That's right...the more observant among you will have realised we haven't actually cast the ray yet...

AFTER ALL OF THAT...
It turns out that casting the ray is a lot more simple than all the stuff I went through above. The casting process is a loop, which basically has the following form:

  • Check that the current grid intersection point is within world boundaries
  • Check for a wall in the current cell
  • If there's a wall:
    • Calculate the length of the ray
    • Return casting information, such as the cell coordinates, ray length, etc.
  • If there's no wall, then determine the next intersection point and continue to the first step

Now to look at each of the steps in more detail.

KEEPING IN BOUNDS:
To make sure that our engine doesn't ungracefully exit (core dumps and Blue Screens of Death aren't exactly elegant, are they ? :) we must check that the intersection point we have calculated isn't outside of the world boundaries. To make sure we don't fall off the end of the world, the loop condition would look something like this:

while (WORLD_TO_CELL_X(i.x) >= 0 and WORLD_TO_CELL_X(i.x) < world_width and
       WORLD_TO_CELL_Y(i.y) >= 0 and WORLD_TO_CELL_Y(i.y) < world_height) {
	/* Check for walls and do other stuff described below */

	...
};

The WORLD_TO_CELL_?() functions convert world-coordinates to cell coordinates (usually just a rounded-down division by CELL_WIDTH or CELL_HEIGHT for X and Y coordinates, respectively). The variables world_width and world_height are the numbers of cells running in the horizontal and vertical direction in the world, respectively. Now onto what we put into this loop...

CHECKING FOR A WALL:
Checking for a wall is simple. We simply convert the gridline intersection points to cell coordinates, just as in the loop above, and perform an array look-up. That point in the array will hold wall information, such as its texture, etc. In the world_cell structure I defined in step II, there's a height field. If this is set, then there's a wall occupying the cell. Thus, the test is as follows:

if (cells[WORLD_TO_CELL_Y(i.y)][WORLD_TO_CELL_X(i.x)].height > 0)
{
	/* There's a wall in this cell */

	...
}

It couldn't really be any simpler, could it ?

WE'VE FOUND A WALL !
When a wall is found, the minimum that we need to do is calculate the length of the ray and return it (more trigonometry...woo-hoo ! :) We'll also return the X coordinate of the intersection (which can be used in determining the offset into the texture bitmap, as I'll describe later) and the world_cell entry from the cells array (this gives us wall information such as its height, texture, etc.)

If you look back to fig10, you will see there's a large red triangle, with the hypotenuse marked as Ray `r'. It should be obvious that this is the ray we cast, and thus its length needs to be calculated. When we've intercepted a wall, i.x and i.y should have the coordinates of the contact, and P.x and P.y should be left unchanged. Using that knowledge, the large red triangle becomes:
images/fig13.jpg
fig13: Finding the length of the ray

As fig13 shows, there are three ways to find the length of the ray, r. The first is to use Pythagoras' Theorem, which states that the length of a right-angled triangle's hypotenuse is equal to the square root of the sum of the squares of the other two sides' lengths. There are two things wrong with using this for the finding r. The first is that is requires a square root to be performed, which is an expensive calculation for a computer. The second is that it can't easily be placed into a Look-Up-Table (LUT), as there are virtually unlimited possibilities for (iy - Py)2 + (ix - Px)2. Sine and cosine values, however, can be placed into a LUT easily, as alpha can only be in the range of 0° to 360° (and increments of less that one degree are accecptable).

The equation that will be used to find the length of the ray will be the one involving sine, as we do not wish to divide by zero (the cosines of 90° and 270° are 0, so we'd get a divide-by-zero error when checking horizontal intersections at those angles; since there is no need to cast rays that check horizontal gridlines at 0° and 180° (since they'd never intersect anything, because they run parallel with the gridlines) and the sines of those angles are both 0, then it is the perfect choice). A bit of pseudo-code may look something like:

/* We've found a wall.  Calculate the length of the ray */

ray_length = ABS((i.y - P.y) / sine(alpha));


/* Return all necessary information back to the caller */

return(i.x,				/* The intersection's X coordinate */
       cells[WORLD_TO_CELL_Y(i.y)][WORLD_TO_CELL_X(i.x)],	/* The cell */
       ray_length);			/* The length of the ray */

Note the use of the ABS() function to take the absolute value of the calculation (ie. always give a positive result). This is needed in case one of the sub-calculations produces a negative result, in which case a negative ray length would be given. A negative ray-length would mean a negative distance from the camera to the wall, which isn't really possible.

IF NO WALL WAS FOUND...
If no wall was found, then we continue execution through the loop. The next part of the loop is simply two additions; incrementing the current X and Y coordinates by the pre-calculated deltas to achieve the coordinate of the next intersection of the ray with the horizontal gridline. Pseudo-code for this would look like:

/* No wall was found on this gridline, so jump to the next one */

i.x = i.x + dx;
i.y = i.y + dy;

FALLING OFF THE END OF THE WORLD...
The test condition of our casting loop was one that checked the current intersection point against the world dimensions and made sure we were within bounds. However, what do we do if we reach the edge of the world and find no walls ? Although this shouldn't happen in a properly designed map (or indeed `good practice' perhaps dictates that the engine's world-loading code should have some saftey measures in place to work around this), it sometimes does. When this event occurs, our loop terminates. So, at the end of our loop, and indeed our ray-casting function, we shall have a default set of values returned that key in with no wall being intercepted, like so:

/* Columbus was wrong...:) */

return(0,				/* No X intersection coordinate */
       NULL,				/* No cell */
       MAX_DISTANCE);			/* Ray is at infinity */

Testing for these returned values, our engine can determine whether a slice of a wall should be drawn or not.

AND IN CONCLUSION:
I will conclude this page with a complete pseudo-code listing, piecing together all of the individual extracts given above. Even if you didn't understand everything, you still have the code below as a guide to creating your own horizontal intersection-checking raycasting function.

function cast_horiz_ray(coordinate P, angle alpha)
{
	integer dy;			/* Integer variables */

	real dx, ray_length;		/* Decimal variables */

	coordinate i;			/* Intersection point */


	/* Calculate the intersection increment deltas */

	dy = CELL_HEIGHT;
	dx = dy / tangent(alpha);


	/* Make sure the deltas have the right signs for the ray's direction */

	if (alpha < 180 DEGREES)
	{
		/* Calculate the Y coordinate of the first intersection point,
		   and reverse the sign of the Y delta */

		i.y = ((P.y / CELL_HEIGHT) * CELL_HEIGHT) - 1;
		dy  = -dy;
	}
	else
	{
		/* Calculate the Y coordinate of the first intersection point,
		   and reverse the sign of the X delta */

		i.y = ((P.y / CELL_HEIGHT) * CELL_HEIGHT) + CELL_HEIGHT;
		dx  = -dx;
	}


	/* Calculate the X coordinate of the first intersection point */

	i.x = P.x + ((i.y - P.y) / tangent(alpha));


	/* Iterate through the possible horizontal gridline intersections */

	while (WORLD_TO_CELL_X(i.x) >= 0 and WORLD_TO_CELL_Y(i.y) >= 0 and
	       WORLD_TO_CELL_X(i.x) < world_width and
	       WORLD_TO_CELL_Y(i.y) < world_height) {

		/* Check for a wall on the current gridline intersection */

		if (cells[WORLD_TO_CELL_Y(i.y)][WORLD_TO_CELL_X(i.x)].height>0)
		{
			/* We've found a wall.  Calculate the length of the
			   ray (the distance to the wall slice) */

			ray_length = ABS((i.y - P.y) / sine(alpha));


			/* Return all necessary information back to the
			   caller */

			return(i.x,
			    cells[WORLD_TO_CELL_Y(i.y)][WORLD_TO_CELL_X(i.x)],
			    ray_length);
		}


		/* No wall was found on this gridline, so jump to the next
		   one */

		i.x = i.x + dx;
		i.y = i.y + dy;
	};


	/* Columbus was wrong...:) */

	return(0, NULL, MAX_DISTANCE);
}

There will be some working C code later. The next step will deal with vertical intersection checking; you'll love that one, too :)


NEXT PAGE PREVIOUS PAGE


Last updated: 02/11/00
Copyright © Peter Restall, 1998-2000
Best viewed with Netscape Navigator in a resolution of 1600x1200