|
|
|
Raycasting
Introduction This is my own interpretation of Raycasting. The book "Black Art of 3D Game Programming" by Andre Lamothe had a brief one-paragraph description of the idea behind Raycasting, and from that I worked out this technique, so it's quite possible there are other and/or better ways. How it's done Raycasting works by projecting rays out from the viewer, one for each pixel the screen is wide, so in 640*480 you'll be projecting 640 rays. You extend these rays until they hit a block in your map and then use the distance between the viewers position and the point of impact to determine the height of the vertical line to draw on the screen. This is shown in figure 1.1-
And the result would look something like figure 1.2.
The ray angles angle [n] = player_facing + ( ( n * ( FOV / 640.0f ) ) - ( FOV / 2.0f ) ); where angle[] is an array of 640 floats, and FOV is the field-of-view. It also makes sense to keep your angles in the 0-360 degree range. So a complete function to calculate all 640 ray angles would look like this- void
CalculateAngles ( void ) if
( angle[i] >= 360.0f ) Extending the rays Lets say your blocks are 100*100. You have a big 2D array which is your map, and you have the position of the player which we will call player_x and player_y (say 419, 280). You also have the angle of the current ray. To work out which block of your map the player is in, which we will call map_x and map_y, obviously just divide player_x and player_y by 100, so the player is in block 4,2. Also you do the same thing after extending a ray - divide the current x, y position of the ray both by 100 and reference your map using the values you get to see if you hit anything (if not then keep extending the ray). Getting every intersection
As you can see, the ray intersects the grid lines 5 times in all, and neither of these two methods alone would catch all five intersections, so we need both. So in one case, you will be always changing x by 100 (block width) and using trig to work out how much to change y by (depending on the angle of the ray). And in the other case you change y by 100 (block height), and use trig to work out how much to change x by. This means, if you refer back to figure 1.1, you will know the exact x,y position every time the ray passes through a grid line and enters a new block. Most of the time, you will find that both methods intersect different blocks, giving you two different x,y values, in which case you use the closer point to the viewer. Moving to the first grid-line In the left hand diagram above, we extend the ray until it crosses the first vertical grid line (also the very first intersection). On the right we extend the ray until it crosses the first horizontal grid line (which is the second intersection). Calculating these two points is relatively simple. Take the picture on the left. First we have to work out whether the line is going left (ray angle between 180 and 360) or right (ray angle between 0 and 180). If the ray is going left then we just multiply map_x by 100 and subtract a small amount. This works because when we earlier converted player_x to map_x (dividing by 100), the value was rounded down because map_x is of type int (it has to be so we can reference our map array with it). If we don't subtract some small amount, then we are still in the same map block (although at the far left hand edge), whereas we need to be over the grid line just into the next block. Remember we are going to use the new values, divide them by 100 and check our map with them, so the difference between 399.99 and 400 is the difference between checking the right block and the wrong one. If the ray is going right then we multiply 1 + map_x by 100. Again it works for the same reasons. So if player_x = 419 and player_y = 280, then map_x = 4 and map_y = 2. If the ray is going left then our first x point is ( 4 * 100 ) - 0.1 = 399.9, and if the ray was going right the x point would be 1 + 4 * 100 = 500. When we eventually divide these by 100 to check against our map, this will give 3 and 5, the blocks either side of the players block. Next we have to work out the y value associated with this x value, and this is where the angle of the ray comes into play. We know the position of the player (player_x, player_y), we know the ray angle and we know the x position of the new point. If the ray is going left we can subtract 270 degrees from the angle and form a right angled triangle as shown in figure 1.4.
Figure 1.4 - Using trigonometry to calculate the y value where the ray crosses the vertical grid line for a ray traveling left.
We can then use Tangent to find the opposite. The adjacent will be equal to player_x minus our new x value. The formula for Tangent is- Tan (angle) = Opposite / Adjacent so, rearranging we get- Opposite = Tan (angle) * Adjacent Once we have the opposite we subtract it from player_y to find our y value. If the ray is going right, we subtract 90 degrees from the angle, perform the same procedure, but this time we add the opposite to player_y to find our y value Lets take stock The trigonometry here is practically identical to what I showed you above, but instead of needing to do player_x - new x value, to work out the adjacent, we already know that it's just the block width. What's next? To work out which point is closer you'll need to work out the distance from the players position to the new points. You also need these values when drawing the vertical line on your screen. To work out the distance, just use pythagorus. So- Distance = sqrt ( (player_x - new_x) * (player_x - new_x) + (player_y - new_y) * (player_y - new_y) ); And you use the lower of the two distances to work out the height to draw your vertical line. So basically- line height = some_number / distance since you want distant walls to appear smaller. I use 360 for some_number but you can experiment to find a suitable value. The last step is to work out where the top and bottom of the vertical line should be based on it's height but that's trivial. Simply- top_point
= half_screen_height - ( line_height / 2); Texture Mapping I do need to explain texture coordinates though. The v coordinate is taken care of by your texture mapped line function, or by DirectDraw, but you need to work out the u value. The u value is basically just how far along the face the ray hits, divided by the length of the face. So you have an x,y value for the point which turned out to be the closer of the two intersections. If it was on a vertical grid line, then you are interested in the y value and if it fell on a horizontal grid line then you are interested in the x value. You already have the map x,y position for the block the ray made contact with. You multiply this x,y by block_width and block_height (both 100 in our example) and subtract this from either x or y, and then divide by block width or block height. So- u = ( new_x - ( new_map_x * block_width ) ) / block_width; // ray hit a horizontal grid line or- u = (new_y - ( new_map_y * block_height) ) / block_height; // ray hit a vertical grid line and that will give you a u value between 0 and 1. Fisheye distortion
The distance from the player to the wall at the far left of the screen is greater than the distance from the player to the wall in the centre of the screen, so the wall appears smaller. The reason you don't get this fisheye effect with a traditional 'divide by z' polygon engine is that these engines only consider the distance in the z axis, not the 3D distance. So a polygon engine uses screen_x = global_x / global_z and not screen_x = global_x / distance so it works out the distance to a plane (z = 0 plane) and not to a point.
You might be surprised to find out that our eyes give roughly the same view for the same reasons. If you stand close to a long wall, facing it, more or less as shown in the screenshot above, and without turning your head or moving your eyes, examine the path the top of the wall is following, you will see that far to your left the top of the wall is heading upwards from left to right. Then look at the wall straight in front of you and you'll see the top of the wall is flat. Now look at the top of the wall to your right, and you'll see that's it's heading back down, exactly as shown above. Removing the distortion For instance, looking at the wall above, your could take the height of the middle line (x=319), which is the non distorted line, and then divide the heights of all the other lines by this value. You do this when your program starts and store all these values in an array of 640 floats. Then once your program is running you divide each line height by the value in this table and the distortion will be gone. Simple, and you've even used your own engine to fix it. It might not be completely accurate though, and you would have to set up the map as above, with the camera totally square on to a wall and render a frame 'off-camera' to complete your table of values. Yet another, and perhaps my favorite way to remove the distortion is to use trigonometry. Again, as above you will have an array of 640 floats and you pre-calculate the magnitude of the distortion. The way you do this is to use Cosine to work out how much further each ray has to travel to reach a theoretical perpendicular wall, compared with the middle line on the screen. We can work out the angle, the adjacent is whatever you want to make it (it doesn't matter since the distances are all proportional) so just use 1 to make the math's easier. So just use Cos to calculate the hypotenuse and divide it by the adjacent. Store the value in your array, and then for each ray, once you've worked out the line height, divide it by the appropriate value in the array. So in code that would look like- for
( int i = 0; i < 640; i++ ) The only thing to watch out for here is that the Win32 function cos() takes angles in radians, so to make it work with this function the code needs changing to- fish_factor[i] = cos ( 0.01745329251994 * ( ( i * (FOV / 640.0f) ) - ( FOV / 2.0f ) ) ); This is the code I use myself and it works flawlessly. By using a theoretical adjacent value of 1.0 we have removed the need for complicated code and unnecessary divisions. In conclusion |
|
All
content copyright © Simon Brown 1999-2005. |