C Raycaster Breakdown2
In today’s article, I’m going to breakdown how the raycaster works. It’s simple math but you know, complex. Following code is modified and desolved inside the actual code. This code is for explanation.
Disclaimer
This code is written only in C. I used OpenGL to implement the 3D drawing in C. Shout out to ‘3D Sage’ who gave me an inspiration and way to make a game.
Code Breakdown
This part is about drawing rays on the 2D world. In the final section, this 2D world is drawn in 3D. Keep in mind that this is drawing 2D first.
main.c
//Drawing Player
float degToRad(int a) { return a*M_PI/180.0;}
int FixAng(int a){ if(a>359){ a-=360;} if(a<0){ a+=360;} return a;}
float px,py,pdx,pdy,pa;
void drawPlayer2D()
{
glColor3f(1,1,0); glPointSize(8); glLineWidth(4);
glBegin(GL_POINTS); glVertex2i(px,py); glEnd();
glBegin(GL_LINES); glVertex2i(px,py); glVertex2i(px+pdx*20,py+pdy*20); glEnd();
}
... ...
void display()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
drawMap2D();
drawPlayer2D();
drawRays2D();
glutSwapBuffers();
}
To draw a sprite in C, you need a OpenGL library. C by itself does not provide you some sort of drawing tools.
To draw in OpenGL, you need a function named display(){...}
. C is procedual program so, OpenGL draws in order of funtions are called.
void drawPlayer2D()
is for drawing player in 2D. Not in 3D for now on. It shows the view of map.
glColor3f(1,1,0); glPointSize(8); glLineWidth(4);
It’s drawing player with yellow color, and pixel of 8.
glBegin(GL_POINTS); glVertex2i(px,py); glEnd();
Drawing the position with px and py. Others are OpenGL functions for drawing points.
glBegin(GL_LINES); glVertex2i(px,py); glVertex2i(px+pdx*20,py+pdy*20); glEnd();
Drawing lines starting from px, py.
glVertex2i(px,py)
sets the starting point which is px, py. glVertex2i(px+pdx*20,py+pdy*20);
sets the end point of the line(ray). pdx and pdy mean the direction vector.
Drawing Rays and walls
//---------------------------Draw Rays and Walls--------------------------------
float distance(ax,ay,bx,by,ang){ return cos(degToRad(ang))*(bx-ax)-sin(degToRad(ang))*(by-ay);}
void drawRays2D()
{
glColor3f(0,1,1); glBegin(GL_QUADS); glVertex2i(526, 0); glVertex2i(1006, 0); glVertex2i(1006,160); glVertex2i(526,160); glEnd();
glColor3f(0,0,1); glBegin(GL_QUADS); glVertex2i(526,160); glVertex2i(1006,160); glVertex2i(1006,320); glVertex2i(526,320); glEnd();
int r,mx,my,mp,dof,side;
float vx,vy,rx,ry,ra,xo,yo,disV,disH;
ra=FixAng(pa+30); //ray set back 30 degrees
glBegin(GL_QUADS)
means to draw four-sided polygon and glVertex2i()
specifies the position of four angles. There are two quads each are Green and Blue.
float distance(ax,ay,bx,by,ang)
is the way to calculate distance between player and wall. There are two ways to achieve it. one is using sqare Root and other is using sin and cos.
FixAng()
function is used to normalize the angle if it’s in 0 to 360 degrees. pa
is current player’s direction so,
we are giving FixAng(pa+30)
into ra
. So, ra
will be 30 degrees to the right.
Draw Vertical Lines
for(r=0;r<60;r++)
{
//---Vertical---
dof=0; side=0; disV=100000;
float Tan=tan(degToRad(ra));
if(cos(degToRad(ra))> 0.001){
rx=(((int)px>>6)<<6)+64;
ry=(px-rx)*Tan+py;
xo= 64;
yo=-xo*Tan;
} //looking left
else if(cos(degToRad(ra))<-0.001){
rx=(((int)px>>6)<<6) -0.0001;
ry=(px-rx)*Tan+py;
xo=-64;
yo=-xo*Tan;
}//looking right
else { rx=px; ry=py; dof=8;} //looking up or down. no hit
while(dof<8)
{
mx=(int)(rx)>>6;
my=(int)(ry)>>6;
mp=my*mapX+mx;
if(mp>0 && mp<mapX*mapY && map[mp]==1){
dof=8;
disV=cos(degToRad(ra))*(rx-px)-sin(degToRad(ra))*(ry-py);
} //hit
else{ rx+=xo; ry+=yo; dof+=1;} //check next horizontal
}
vx=rx; vy=ry;
Sine and Cosine on circle | Positive or Negative |
---|---|
for(r=0;r<60;r++)
indicates how many rays should OpenGL draw. In this case, it’s 60. We can modify it by changing 60 to 30, 120, things like that. It means the angle of view. it’s drawing 60 degress from pa+30
.
if(cos(degToRad(ra))>0.001)
checks if the player is looking right. If cos value is greater than ‘small positive value(0.001)’, which means cos is between the angle of $0$ < θ< $\frac\pi2$ and $\frac{3\pi}2$ < θ<
${2\pi}$.
So, the angle is in First and Fourth Quadrant. In First Quadrant, cos and sin is both positive which means it moves from the top(0 rad) to the right($\frac\pi2$). In Fourth Quadrant, cos is positive but sin is negative.
It means that player is moving starting from bottom($\frac{3\pi}2$) to the right($2\pi$). In this case(Fourth Quadrant), player is facing the bottom so it’s moving right.
rx=(((int)px>>6)<<6)+64;
This part is drawing the vertical lines on the screen. The ‘Shift Operatior>>
’ is to first add 64 which px
is casted into integer then divide(<<
) by 64. 64 is the pixel of the tile. Adding 64 is to move right side.
ry=(px-rx)*Tan+py;
This part is to calculate the vertical line. It calculates the length of vertical line. For example, if tangent is 0.5 the length of screen will be $64px * 0.5 = 32px$ so, it’s drawn 32px.
xo=-64; yo=-xo*Tan;
is for moving and drawing. If player moves, xo and yois drawn.
D.O.F means Depth Of Field. DOF limits the distance of ray and prevents infinite loop.
mx
and my
is shifted by dividing 64. mx
and my
are x and y indcides. mp
is the index in the map array, calculated from the x and y indices (mx and my) of the map.
if(mp>0 && mp<mapX*mapY && map[mp]==1){
dof=8;
disV=cos(degToRad(ra))*(rx-px)-sin(degToRad(ra))*(ry-py);
}
This part is for checking the hit of the wall.
Buttons Control
void Buttons(unsigned char key,int x,int y)
{
if(key=='a'){ pa+=5; pa=FixAng(pa); pdx=cos(degToRad(pa)); pdy=-sin(degToRad(pa));}
if(key=='d'){ pa-=5; pa=FixAng(pa); pdx=cos(degToRad(pa)); pdy=-sin(degToRad(pa));}
if(key=='w'){ px+=pdx*5; py+=pdy*5;}
if(key=='s'){ px-=pdx*5; py-=pdy*5;}
glutPostRedisplay();
}
Buttons are for the struct that I’ve made in the “Raycaster Breakdown 1”. OpenGL needs to define each movement. If a
is pressed it moves spins left, if d
is pressed it spins right. pdx
and pdy
is for rotation.
Draw Horizontal Lines
//---Horizontal---
dof=0; disH=100000;
Tan=1.0/Tan;
if(sin(degToRad(ra))> 0.001){ ry=(((int)py>>6)<<6) -0.0001; rx=(py-ry)*Tan+px; yo=-64; xo=-yo*Tan;}
else if(sin(degToRad(ra))<-0.001){ ry=(((int)py>>6)<<6)+64; rx=(py-ry)*Tan+px; yo= 64; xo=-yo*Tan;}
else{ rx=px; ry=py; dof=8;} //looking straight left or right
while(dof<8)
{
mx=(int)(rx)>>6; my=(int)(ry)>>6; mp=my*mapX+mx;
if(mp>0 && mp<mapX*mapY && map[mp]==1){ dof=8; disH=cos(degToRad(ra))*(rx-px)-sin(degToRad(ra))*(ry-py);}//hit
else{ rx+=xo; ry+=yo; dof+=1;}
}
glColor3f(0,0.8,0);
if(disV<disH){ rx=vx; ry=vy; disH=disV; glColor3f(0,0.6,0);} //horizontal hit first
glLineWidth(2); glBegin(GL_LINES); glVertex2i(px,py); glVertex2i(rx,ry); glEnd();//draw 2D ray
glLineWidth(8);glBegin(GL_LINES);glVertex2i(r*8+530,lineOff);glVertex2i(r*8+530,lineOff+lineH);glEnd();//draw vertical wall
ra=FixAng(ra-1); //go to next ray
}
}//-----------------------------------------------------------------------------
This part is same as drawing the vertical lines above. Difference is, it’s about sine this time. If, player is facing upwards(if(sin(degToRad(ra))> 0.001)
), calculate the y position and get tangent value. Using tangent is crucial because it lets us know how much to move horizontal or vertical. For example if $tan(ra) = 1$, it means horizontal and vertical need to move in same ratio.
Now, after caculating the horizontal and vertical lines, we are drawing the shortest that first hits the wall with green.