JIAN2587's RAYCASTING TUTORIAL - jian2587@hotmail.com RAYCASTING TUTORIAL BY TZE JIAN CHEAR INTRODUCTION If U read this tut, means you don't understand raycast eventhough you read plenty of similiar tuts, just as I once was. But now, I'll explain all the stuffs and thin' behind raycasting and make sure you understand it! Okay, I assume you have no prior XP nor knowledge regarding raycasting. Now, let'ta tut begin! Lesson 1 ABOUT COSINE AND SINUSE AND WHAT ARE THEIR FUNCTIONS Okay, I won't explain raycasting first. Instead somethin' about COS/SIN. Have you ever wonder how QB draws a circle with just center X,Y and radius as its parameter? I mean, how QB know the exact position of each pixel of the circle? Can U figure it out? Some of my lamer QB friends said by incrementing both X and Y position and then the other way round to produce a circle, now this is wrong!(If ur like him)Try this then you'll understand. 'Lamer's way SCREEN 12 CenterX = 320: CenterY = 240: Radius = 50 X = CenterX Y = CenterY - Radius DO UNTIL X = CenterX + Radius X = X + 1 Y = Y + 1 PSET (X, Y), 15 LOOP DO UNTIL X = CenterX X = X - 1 Y = Y - 1 PSET (X, Y), 14 LOOP 'Run it. Got what I meant? Okay, that's impossible. Don't think of any other ways.(Though I am sure that there's other way to draw a circle by using plus and subtract) Use Cosine and Sinuse. So try this source. 'Intermediate's way SCREEN 12 CenterX = 320: CenterY = 240: Radius = 50 DO UNTIL DEGREE = 360 DEGREE = DEGREE + 1 RADIAN = DEGREE / 57.2958 X = COS(RADIAN) * Radius Y = SIN(RADIAN) * Radius PSET (CenterX + X, CenterY + Y), 15 LOOP 'Run it. What U got? Now, you can try to figure out the source. Given a distance, that is radius, and degree, you can find the exact position with COS/SIN. See this diagram. DIAGRAM 1 {Top view} | | | | | / <--Length is 3 unit, so you got this position | / <--Length is 2 unit, so you got this position |/ <--Length is 1 unit, so you got this position o <--This is your center position **The above diagram was applying 30 degree.**(Estimated) COS(RADIAN) * Radius and SIN(RADIAN) * Radius what do u know about this? It just let you know the position for the given degree and after multiplying it with the distance you get the distanced position. Which means, to find position of a point where it's about j degree away and l distance from the center position, you would use RADIAN = DEGREE / 57.2958 X = CenterX + (COS(RADIAN) * Distance) Y = CenterY + (SIN(RADIAN) * Distance) So, if U doesn't understand the source but this diagram, never mind. Just remember the source. Beyond this point, I assume you know about COS/SIN. Lesson 2A SIMPLE RAYCASTING AND EXAMPLE SOURCE CODE Now that U know the method I explained in lesson 1, please don't bother it now. I am gonna explain sumthin' else. Yeah, a bit about raycasting. So, let's preview what's raycasting that is so rockin' till ID uses it in DOOM. Imagine you are on a ship, you have a radar, and you gotta know what's outside. So the radar beams a sonic pulse to every direction. When a sonic pulse hit somethin', it was reflected back and a censor detected the sonic pulse, thus, we know there's somethin' outside. You know the direction and distance of the objects you detected. So your radar screen draw the thin' accordingly. Raycasting is somethin' like that, where the RAY is the SONIC PULSE. Now, for a bit more advance, I'll explain it detailfully. On a 2D map(Didn't U know that raycasting is a 2D technique fooling the player that it's 3D?), from your viewing field at your current position, you fire a ray from left to right of your viewing field and it travel straight till it hit a wall, then u got'ta position of the wall and you just draw it(if it's a far object, logically we will draw it small to make kinda perspective feel). So, no wall? Then don't draw it! And while you are moving, check if you are on a wall position, if yes, don't let ya got into it and don't draw it. So, why DOOM has multiple level where you can go up a ladder, shoot the flying monsters, and thin's like that? And since raycasting has no Z coordinate (only X and Y, that is 2D) That's easy, coz' it has multiple 2D map. Just like a layer cake. If ur Z position is on a higher position, then change the map! Man, you still have to render all the maps, only if it falls into ur viewing field.(Viewing field is about 18% of 360 degrees according to Dieter Mafurt's source) So, ahhh, any source code for me to try on? Yes, but that's darn long. I changed it a bit from DOOM.BAS by unknown. So try it on. 'My first Raycaster - by Tze Jian Chear - jian2587@hotmail.com DEFSNG A-Z SCREEN 13 DIM S(-31 TO 360 + 32), C(-31 TO 360 + 32), Map%(12, 12) BlockWidth = 900: Phi = 100: Strafe = .1 FOR Y = 1 TO 12 FOR X = 1 TO 12 READ Map%(X, Y) NEXT: NEXT PX = 9: PY = 11: Heading = 0: Stride = 3: Turn = 5 Factor = (ATN(1) * 8) / 360 FOR Degree% = 0 TO 359 Angle = CSNG(Degree%) * Factor S(Degree%) = SIN(Angle) * .1 C(Degree%) = COS(Angle) * .1 NEXT FOR Ray% = -31 TO -1 S(Ray%) = S(Ray% + 360) C(Ray%) = C(Ray% + 360) NEXT FOR Ray% = 360 TO 360 + 32 S(Ray%) = S(Ray% - 360) C(Ray%) = C(Ray% - 360) NEXT GOSUB Raycast DO K$ = INKEY$ IF K$ <> "" THEN SELECT CASE RIGHT$(K$, 1) CASE "K" 'strafe left Heading = (Heading + Turn) MOD 360: GOSUB Raycast CASE "M" 'strafe Right Heading = (Heading + (360 - Turn)) MOD 360: GOSUB Raycast CASE "H" 'forward NewPX = PX - (S(Heading) * Stride) NewPY = PY - (C(Heading) * Stride) IF Map%(NewPX, NewPY) = 0 THEN PX = NewPX: PY = NewPY: GOSUB Raycast ELSE BEEP CASE "P" 'backward NewPX = PX + (S(Heading) * Stride) NewPY = PY + (C(Heading) * Stride) IF Map%(NewPX, NewPY) = 0 THEN PX = NewPX: PY = NewPY: GOSUB Raycast ELSE BEEP CASE CHR$(27): SCREEN 0: WIDTH 80, 25: END END SELECT END IF LOOP Raycast: X1 = 0 CLS : WAIT &H3DA, 8 FOR Ray% = Heading + 32 TO Heading - 31 STEP -1 StepX = S(Ray%): StepY = C(Ray%) XX = PX: YY = PY: BlockDist = 0 DO XX = XX - StepX: YY = YY - StepY BlockDist = BlockDist + 1 K = Map%(XX, YY) LOOP UNTIL K DD = BlockWidth \ BlockDist Height = DD + DD: DT = Phi - DD LINE (X1, DT)-STEP(4, Height), K, BF X1 = X1 + 5 NEXT RETURN DATA 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1 DATA 1, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 9 DATA 9, 0, 2, 10, 0, 0, 0, 0, 0, 14, 0, 1 DATA 1, 0, 10, 2, 0, 0, 0, 0, 0, 4, 0, 9 DATA 9, 0, 0, 0, 0, 0, 0, 0, 13, 14, 0, 1 DATA 1, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 9 DATA 9, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 1 DATA 1, 0, 13, 0, 0, 0, 0, 8, 0, 12, 0, 9 DATA 9, 0, 5, 0, 0, 8, 0, 7, 0, 4, 0, 1 DATA 1, 0, 13, 0, 0, 0, 0, 8, 0, 12, 0, 9 DATA 9, 0, 5, 0, 0, 0, 0, 7, 0, 4, 0, 1 DATA 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9 Okay, try it on. Yucks! Flickers? Yeah, I don't have time for that, coz' putting unrelated stuff would make u more complicated. LESSON 2B SIMPLE RAYCASTING AND WALKTHROUGH Now, let's go through this code. First you define all the variables. DIM S(-31 TO 360 + 32)...blah is to make a lookup table for speed. This is common, coz' COS/SIN functions are slow. Then the map%(12, 12), you make it 2 dimension with 12x12 grids.**DIM Map%(12, 12)** BlockWidth,Phi will be explained later. Okay, u read the map from data. **FOR Y = 1 TO 12...READ Map%(X, Y)...NEXT** PX = 9: PY = 11 this is ur position in the map. Heading is where u are facing. Stride is how many steps to go per pressure of key. Turn is how many degrees to turn per pressure of key. Between Factor = (ATN(1) * 8) / 360 and C(Degree%) = COS(Angle) * .1 NEXT This is to build up ur COS/SIN lookup table. WAIT! Why multiply COS(Angle) with .1 ? After kinda analyzation of this piece code of mine which has some common places with other source codes, this .1 seems to be controlling the width of every grid indirectly. Ahh, don't care 'bout it. Just proceed. Between FOR Ray% = -31 TO -1 and NEXT Well, this is sumthin' used with the raycaster loop and your viewing field. from -31 to + 32, there's 64 rays.(Okay, wait a minute. Now, to speed up stuffs, we gotta used only 64 rays and not 320 rays!(SCREEN 13 = 320x200x256)Of course 320 rays would make thin's clearer but also slows down everythin' unless u use ASM or whatever.) You might get confused a bit. Our viewing field(The range ur eye can see in one direction) is as same size as the screen, and the screen has 320 pixels horizontally(SCREEN 13). So, we fire 64 rays from our eye, and these rays will detect the walls and give u back its distance and position, then we draw the walls accordingly. Why 64 rays? These rays will cover about 5 pixels width, (64x5=320pixels), in fact, it doesn't matter how many rays you fired, but here I used 64 rays to speed up stuff coz' I just have to draw 64 times, though, it's blocky. If U found me kept on repeating some points, I just 1 2 make sure u really understand with kinda examples and diagrams. DIAGRAM 3 {Top view} Wall /-------Wall / / /| / /|| /<--THESE are your rays.So when ur rays hit the wall, you get the distance and its /\|| / position. /\\|| / \||/ % <--This is ur eye. Okay, got what I mean? WAIT! How we apply the COS/SIN method I explained earlier? You can use it to determine where should ur ray go. Okay, let's revise the source code. FOR Ray% = Heading + 32 TO Heading - 31 STEP -1 'For each rays StepX = S(Ray%): StepY = C(Ray%) 'Get the increment steps for our ray first 'we gotta increment the rays position with these steps 'every time to let it(the ray) know where it's going This should be easy. If you know what do I mean about incrementing the rays position every time, you can skip below. DIAGRAM 4 {Top view} | |~~~/~~~ <--Hit a wall, so we got the wall's position and distance | / <--With distance 3 units and 30 degrees, ur ray got this position } | / <--ur ray get here with an increment of stepX from previous position } |/ <--With distance 1 unit and 30 degrees, ur ray got this position with the COS/SIN } Eye, -->0 method. or viewing field. Okay, how does DIAGRAM 4 works in the source code? See below. FOR Ray% = Heading + 32 TO Heading - 31 STEP -1 'For each ray... StepX = S(Ray%): StepY = C(Ray%) 'Yeah, we let the ray know how much should it move XX = PX: YY = PY: BlockDist = 0 'Our position. BlockDist will increase indicating 'our ray's travelling further and it affects how should 'our wall drew DO 'This is the raycasting loop XX = XX - StepX: YY = YY - StepY 'Our ray's travelling... BlockDist = BlockDist + 1 'Increase the distance... K = Map%(XX, YY) 'On current ray position, is there a wall? LOOP UNTIL K 'Just loop till we found one DD = BlockWidth \ BlockDist 'Divide the distance we get with BlockWidth Height = DD + DD: DT = Phi - DD 'So we have the height and the starting point 2 draw LINE (X1, DT)-STEP(4, Height), K, BF 'Okay, draw it(Go2 kindegarten if u don't know LINE) X1 = X1 + 5 'We increase the X1 by 5 pixels for next ray NEXT 'So NEXT While walking, our position is also variable as the ray's position did. So, to update our position, we use the same method. Just figure all that by ur self starting from the line SELECT CASE RIGHT$(K$, 1) Okay. Mehehe PeePoPeePo WooWooWoo Weeeeeeeuuuuu poinpoinpoin tututututu pingpangkalapang 'Skip this >:D LESSON 3 KINDA ADVANCE TOPICS REGARDING RAYCASTING Lesson 2 is too long, I should have break it into 2 chapters. Okay, for somethin' more advance, I'll explain the Phi variable. I thought I read some tutorial said that you really can't see what's above u nor below ur feet in raycasting. Theoratically this is right, but not exactly right. Go search for DOOMZ. I found one DOOMBOT and played around with it.(Pretty much like Counter-Strike)It uses SVGA screenmodes and with tons of playing method.(Capture flag, teamplay.. .blah,Oops, I shouldn't talk 'bout DOOM) DOOMZ lets you aim up and down, just like Half-life or Quake. How we **see** up and down in raycasting? Easy, change the Phi variable. Higher value would make u see above and lower would do the opposite. Try modifying the above source. Assign a couplar of keys to change the Phi variable, so u can aim. To make thin's better, put kinda mouse routine in it.Merhehe. If I wasn't wrong, they might trace the map horizontally also, so when you fire ur rocket launcher, the rocket would upwards if ur aiming up side. (DOOM2 has bit autoaiming system, when u fire, even if ur enemy is above u, ur bullet or rocket would go upward)Don't understand? Never mind. U need no concern 'bout this. But anyway, I'll explain my idea(or maybe it's true that they use this method). DIAGRAM 5 {Side view} $<--Enemy, call the **enemy blasting-off animation routine** / / <--Ur rocket keeps on travelling / <--U fire ur gun, and it trace on like this(just like the rays) Eye-->0/_________<--Floor Okay, got that? If not, please goto lesson 1. LESSON 4 ANOTHER EXAMPLE OF RAYCASTING I analyzed this source code by Jack Mallah, so let's go through the code. horizon = 100: pi = ATN(1) * 4: px = 2: py = 2: Ch = 99: C = 900 / pi SCREEN 13 1 : FOR xs = -160 TO 159 R = 20: theta = ATN(xs / C): ang = pa + theta dis(0) = (10 - px) / COS(ang) dis(1) = (10 - py) / COS(pi / 2 - ang) dis(2) = px / COS(pi - ang) dis(3) = py / COS(pi * 1.5 - ang) FOR w = 0 TO 3 IF dis(w) > 0 AND dis(w) < R THEN wall = w: R = dis(w) NEXT x = xs + 160: Z = R * COS(theta): scrht = Ch / Z LINE (x, 200)-(x, horizon + scrht), 6 LINE -(x, horizon - scrht), wall + 2 LINE -(x, -1), 8 NEXT i$ = RIGHT$(INKEY$, 1) IF i$ = "H" THEN px = px + COS(pa) / 5: py = py + SIN(pa) / 5 IF i$ = "P" THEN px = px - COS(pa) / 5: py = py - SIN(pa) / 5 IF i$ = "K" THEN pa = pa - .1 ELSE IF i$ = "M" THEN pa = pa + .1 IF px < .4 THEN px = .4 ELSE IF px > 9.6 THEN px = 9.6 IF py < .4 THEN py = .4 ELSE IF py > 9.6 THEN py = 9.6 IF i$ <> CHR$(27) GOTO 1 'Unbelievable! Why da' code's so short? Me curious too! But the power and freedom of programming 'let's do so. Okay, I'll go on very fast. Variable horizon is kinda like Phi.(Phi is not Pi) Variable pi is the pi of a circle.(Since 3D stuffs're all 'bout rotation so that's why we gotta learn Pi and circle stuffs) px and py is ur position. Ch and C controls the wall's size.(Just experiment with it and u'll understand) FOR XS = -160... this is the raycasting loop I don't know what R controls. It seems that if its above 0 it won't mess up with others. Theta is the complement of Phi. Ang is the angle. Pa is ur heading. dis(0)... till scrht = Ch / Z calculates all the raycasting stuffs. Then we draw it with LINE. Get input. Then loop if no ESC. Just that easy. Err...If JACK read this tut, please don't feel insulted or whatever if I misinterpret ur source or whatever. LESSON 5 STUFFS BESIDES RAYCASTING Get DOOM2 for USD25.00 from ID. Play it, feel it. What've u got? Nothin'? Never mind, just smack ur CPU then try it and see. What've u got? Wallet's crying? Never mind, change a new one. Okay, after playing so far, DOOM2 seems to be pushing ur CPU's limit(for 386DX or 486DX), pumping out stunning but not detail gfx, complete with texture-mapping, texture-shadding,and stuff like that. Okay, that's what u gotta concern 'bout ur raycasting game. Wether it's racing, sports, FPS or whatever, u gotta know all these tricks. I won't explain how u do all these stuffs, but instead try my best to tell you what's it. I might cover it on my next tutorial. Or may be raycasting tut part-2. Let's cut the crap, and let's start with texture mapping as this gives you the realistic feel to ur game. TEXTURE MAPPING There're tonnes of tricks and techniques to map a texture on a polygon or in our case, raycasting. Texture mapping is a technique where you put a texture(a 2D image,it could be a door or a window, depends on how u design ur map) on the calculated wall. We don't just GET and PUT it in, as the wall is in 3D form, and as further places will be smaller, u gotta know the trick. See this DIAGRAM. DIAGRAM 6 {3D view} ___________CEILING____ | __ _ THIS | |DO| [_] <--Window IS |____|OR|_____ A / WALL / / / FLOOR / / / / / See the wall? It doesn't look like a rectangular, as it should be, coz' this is in 3D view in certain direction and distance. So you got a 2D square wall texture, but can you just put it like that? Of course not. Now, this is another tricky part. I saw some tutorial regarding texture- mapping, and it's sort of pixel-by-pixel calculations and plotting. Thin's have been nightmare 4 u, aren't u? Anyway, after I analyzed a few source code regarding texture-mapping, I'll write another tutorial 4 u guyz. So long, and Adios! Wait 4 my next tut guyz! Completed on 12:22PM, Saturday, November 03, 2001 AD. ------------------------------------------------------------------------------------------------- Here is the complete source code: ------------------------------------------------------------------------------------------------- 'JIAN2587 RAYCASTING ALGORITHMS 'Story behind raycasting (please jump to the end) 'First, you have to initialize a few variables. X,Y of the player, steps covered by one key 'press, heading of the player, and most important, the 2D map. 'Why I dim extra -31 to +32? That's the 64 rays I am gonna send out '0.Dim SinuseTable(-31 TO 360 + 32), CosineTable(-31 TO 360 + 32) '1.First, read the map into a 2 dimension array. '2.Caculate the Sinuse/Cosine table to speed up calculations. '''*****Stride - Steps moved per movement*****''' CONST BlockWidth = 900 CONST Phi = 100 '/*Calculates Cosine/Sinuse table*/' Factor = (ATN(1) * 8) / 360 FOR D = 0 TO 359 Angle = CSNG(D) * Factor SinuseTable(D) = SIN(Angle) * .1 '.1 is the size of every grid X CosineTable(D) = COS(Angle) * .1 '.1 is the size of every grid Y NEXT '/*Calculates steps covered(bit hard 2 explain, go down for exact explanation)*/' FOR A = -31 TO -1 SinuseTable(A) = SinuseTable(A + 360) CosineTable(A) = CosineTable(A + 360) NEXT FOR A = 360 TO 360 + 32 SinuseTable(A) = SinuseTable(A - 360) CosineTable(A) = CosineTable(A - 360) NEXT '3.Update it 'Left Heading = (Heading + Turn) MOD 360 'Right Heading = (Heading + (360 - Turn)) MOD 360 'Forward NewPositionX = CurrentPositionX - (SinuseTable(Heading) * Stride) NewPositionY = CurrentPositionY - (CosineTable(Heading) * Stride) '/*Checks if it's walkthroughable*/' 'Backward NewPositionX = CurrentPositionX + (SinuseTable(Heading) * Stride) NewPositionY = CurrentPositionY + (CosineTable(Heading) * Stride) '/*Checks if it's walkthroughable*/' '4.For every movement and after computations, update the graphic '**PCOPY Background1, WorkPage: Swaps Background1 with 2 'For each viewing field vertical ray from left to right you are viewing... X1 = 0 FOR A = Heading + 32 TO Heading - 31 STEP -1 StepX = SinuseTable(A): StepY = CosineTable(A) XX = CurrentPositionX: YY = CurrentPositionY L = 0 DO 'OK, this loop test if there's any wall to draw... XX = XX - StepX: YY = YY - StepY L = L + 1 K = Map's XX and YY LOOP UNTIL K 'Yeah, found one, draw it DD = BlockWidth \ L 'BlockWidth=900|This is the wall's width H = DD + DD 'Height of your EYE from the ground DT = Phi - DD 'A higher value would see the ceiling 'Lower would see the floor :) LINE (X1, DT)-STEP(4, H), K ,BF X1 = X1 + 5 'Why 5? See below for further explanation NEXT 'Okay, See that FOR A = Heading + 32 TO Heading - 31 STEP -1 ? 'That's about 64 loops, so each wall block will be cut into 64 pieces and drew separately 'Hey, that's blocky(Coz' it speeds up everythin') So, since we are using 320X200 so tha' 'X1 = X1 + 5 makes us draw each pieces with width of 5 and 64 * 5 = 320 horizontal 'phew!