The core of the textured version of the raycaster is almost the same, only at
the end some extra calculations need to be done for the textures, and a loop in
the y-direction is required to go through every pixel to determinate which texel
(texture pixel) of the texture should be used for it.
The vertical stripes can't be drawn with the vertical line command anymore,
instead every pixel has to be drawn seperately. The best way is to use a 2D
array as screen buffer this time, and copy it to the screen at once, that goes a
lot faster than using pset.
Of course we now also need an extra array for the textures, and since the "drawbuffer"
function works with single integer values for colors (instead of 3 separate
bytes for R, G and B), the textures are stored in this format as well. Normally,
you'd load the textures from a texture file, but for this simple example some
dumb textures are generated instead.
The code is mostly the same as the previous example, the bold parts are new.
Only new parts are explained.
The screenWidth and screenHeight are now defined in the beginning because we
need the same value for the screen function, and to create the screen buffer.
Also new are the texture width and height that are defined here. These are
obviously the width and height in texels of the textures.
The world map is changed too, this is a more complex map with corridors and
rooms to show the different textures. Again, the 0's are empty walkthrougable
spaces, and each positive number corresponds to a different texture.
The screen buffer and texture arrays are declared here. The texture array is an
array of std::vectors, each with a certain width * height pixels.
int main(int /*argc*/, char */*argv*/[])
{
double posX = 22.0, posY = 11.5; //x and y start position
double dirX = -1.0, dirY = 0.0; //initial direction vector
double planeX = 0.0, planeY = 0.66; //the 2d raycaster version of camera plane
double time = 0; //time of current frame
double oldTime = 0; //time of previous frame
Uint32 buffer[screenWidth][screenHeight];
std::vector texture[8];
for(int i = 0; i < 8; i++) texture[i].resize(texWidth * texHeight);
The main function now begins with generating the textures. We have a double loop
that goes through every pixel of the textures, and then the corresponding pixel
of each texture gets a certain value calculated out of x and y. Some textures
get a XOR pattern, some a simple gradient, others a sort of brick pattern,
basicly it are all quite simple patterns, it's not going to look all that
beautiful, for better textures see the next chapter.
screen(screenWidth,screenHeight, 0, "Raycaster");
//generate some textures
for(int x = 0; x < texWidth; x++)
for(int y = 0; y < texHeight; y++)
{
int xorcolor = (x * 256 / texWidth) ^ (y * 256 / texHeight);
//int xcolor = x * 256 / texWidth;
int ycolor = y * 256 / texHeight;
int xycolor = y * 128 / texHeight + x * 128 / texWidth;
texture[0][texWidth * y + x] = 65536 * 254 * (x != y && x != texWidth - y); //flat red texture
with black cross
texture[1][texWidth * y + x] = xycolor + 256 * xycolor + 65536 * xycolor; //sloped greyscale
texture[2][texWidth * y + x] = 256 * xycolor + 65536 * xycolor; //sloped yellow gradient
texture[3][texWidth * y + x] = xorcolor + 256 * xorcolor + 65536 * xorcolor; //xor greyscale
texture[4][texWidth * y + x] = 256 * xorcolor; //xor green
texture[5][texWidth * y + x] = 65536 * 192 * (x % 16 && y % 16); //red bricks
texture[6][texWidth * y + x] = 65536 * ycolor; //red gradient
texture[7][texWidth * y + x] = 128 + 256 * 128 + 65536 * 128; //flat grey texture
}
This is again the start of the gameloop and initial declarations and
calculations before the DDA algorithm. Nothing has changed here.
//start the main loop
while(!done())
{
for(int x = 0; x < w; x++)
{
//calculate ray position and direction
double cameraX = 2*x/double(w)-1; //x-coordinate in camera space
double rayPosX = posX;
double rayPosY = posY;
double rayDirX = dirX + planeX*cameraX;
double rayDirY = dirY + planeY*cameraX;
//which box of the map we're in
int mapX = int(rayPosX);
int mapY = int(rayPosY);
//length of ray from current position to next x or y-side
double sideDistX;
double sideDistY;
//length of ray from one x or y-side to next x or y-side
double deltaDistX = sqrt(1 + (rayDirY * rayDirY) / (rayDirX * rayDirX));
double deltaDistY = sqrt(1 + (rayDirX * rayDirX) / (rayDirY * rayDirY));
double perpWallDist;
//what direction to step in x or y-direction (either +1 or -1)
int stepX;
int stepY;
int hit = 0; //was there a wall hit?
int side; //was a NS or a EW wall hit?
//calculate step and initial sideDist
if (rayDirX < 0)
{
stepX = -1;
sideDistX = (rayPosX - mapX) * deltaDistX;
}
else
{
stepX = 1;
sideDistX = (mapX + 1.0 - rayPosX) * deltaDistX;
}
if (rayDirY < 0)
{
stepY = -1;
sideDistY = (rayPosY - mapY) * deltaDistY;
}
else
{
stepY = 1;
sideDistY = (mapY + 1.0 - rayPosY) * deltaDistY;
}
This is again the DDA loop, and the calculations of the distance and height,
nothing has changed here either.
//perform DDA
while (hit == 0)
{
//jump to next map square, OR in x-direction, OR in y-direction
if (sideDistX < sideDistY)
{
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
}
else
{
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
//Check if ray has hit a wall
if (worldMap[mapX][mapY] > 0) hit = 1;
}
//Calculate distance of perpendicular ray (oblique distance will give fisheye effect!)
if (side == 0) perpWallDist = fabs((mapX - rayPosX + (1 - stepX) / 2) / rayDirX);
else perpWallDist = fabs((mapY - rayPosY + (1 - stepY) / 2) / rayDirY);
//Calculate height of line to draw on screen
int lineHeight = abs(int(h / perpWallDist));
//calculate lowest and highest pixel to fill in current stripe
int drawStart = -lineHeight / 2 + h / 2;
if(drawStart < 0) drawStart = 0;
int drawEnd = lineHeight / 2 + h / 2;
if(drawEnd >= h) drawEnd = h - 1;
The following calculations are new however, and replace the color chooser of the
untextured raycaster.
The variable texNum is the value of the current map square minus 1, the reason
is that there exists a texture 0, but map tile 0 has no texture since it
represents an empty space. To be able to use texture 0 anyway, substract 1 so
that map tiles with value 1 will give texture 0, etc...
The value wallX represents the exact value where the wall was hit, not just the
integer coordinates of the wall. This is required to know which x-coordinate of
the texture we have to use. This is calculated by first calculating the exact x
or y coordinate in the world, and then substracting the integer value of the
wall off it. Note that even if it's called wallX, it's actually an y-coordinate
of the wall if side==1, but it's always the x-coordinate of the texture.
Finally, texX is the x-coordinate of the texture, and this is calculated out of
wallX.
//texturing calculations
int texNum = worldMap[mapX][mapY] - 1; //1 subtracted from it so that texture 0 can be used!
//calculate value of wallX
double wallX; //where exactly the wall was hit
if (side == 1) wallX = rayPosX + ((mapY - rayPosY + (1 - stepY) / 2) / rayDirY) * rayDirX;
else wallX = rayPosY + ((mapX - rayPosX + (1 - stepX) / 2) / rayDirX) * rayDirY;
wallX -= floor((wallX));
//x coordinate on the texture
int texX = int(wallX * double(texWidth));
if(side == 0 && rayDirX > 0) texX = texWidth - texX - 1;
if(side == 1 && rayDirY < 0) texX = texWidth - texX - 1;
Now that we know the x-coordinate of the texture, we know that this coordinate
will remain the same, because we stay in the same vertical stripe of the screen.
Now we need a loop in the y-direction to give each pixel of the vertical stripe
the correct y-coordinate of the texture, called texY.
The value of texY is calculated with integer math for more speed, but all the
values that will be divided through a value are multiplied by 256 first, and
afterwards it's divided through 256 again, for more precision. Otherwise some
pretty ugly effects show up.
The color of the pixel to be drawn is then simply gotten from
texture[texNum][texX][texY], which is the correct texel of the correct texture.
Like the untextured raycaster, here too we'll make the color value darker if an
y-side of the wall was hit, because that looks a little bit better (like there
is a sort of lighting). However, because the color value doesn't exist out of a
separate R, G and B value, but these 3 bytes sticked together in a single
integer, a not so intuitive calculation is used.
The color is made darker by dividing R, G and B through 2. Dividing a decimal
number through 10, can be done by removing the last digit (e.g. 300/10 is 30:
the last zero is removed). Similarly, dividing a binary number through 2, which
is what is done here, is the same as removing the last bit. This can be done by
bitshifting it to the right with >>1. But, here we're bitshifting a 24-bit
integer (actually 32-bit, but the first 8 bits aren't used). Because of this,
the last bit of one byte will become the first bit of the next byte, and that
screws up the color values! So after the bitshift, the first bit of every byte
has to be set to zero, and that can be done by binary "AND-ing" the value with
the binary value 011111110111111101111111, which is 8355711 in decimal. So the
result of this is indeed a darker color.
Finally, the current buffer pixel is set to this color, and we move on to the
next y.
for(int y = drawStart; y<drawEnd; y++)
{
int d = y * 256 - h * 128 + lineHeight * 128; //256 and 128 factors to avoid floats
int texY = ((d * texHeight) / lineHeight) / 256;
Uint32 color = texture[texNum][texHeight * texY + texX];
//make color darker for y-sides: R, G and B byte each divided through two with a "shift"
and an "and"
if(side == 1) color = (color >> 1) & 8355711;
buffer[x][y] = color;
}
}
Now the buffer still has to be drawn, and after that it has to be cleared (where
in the untextured version we simply had to use "cls"). The rest of this code is
again the same.
drawBuffer(buffer[0]);
for(int x = 0; x < w; x++) for(int y = 0; y < h; y++) buffer[x][y] = 0; //clear the buffer
instead of cls()
//timing for input and FPS counter
oldTime = time;
time = getTicks();
double frameTime = (time - oldTime) / 1000.0; //frametime is the time this frame has taken,
in seconds
print(1.0 / frameTime); //FPS counter
redraw();
//speed modifiers
double moveSpeed = frameTime * 5.0; //the constant value is in squares/second
double rotSpeed = frameTime * 3.0; //the constant value is in radians/second
And here's again the keys, nothing has changed here either. If you like you can
try to add strafe keys (to strafe to the left and right). These have to be made
the same way as the up and down keys, but use planeX and planeY instead of dirX
and dirY.
readKeys();
//move forward if no wall in front of you
if (keyDown(SDLK_UP))
{
if(worldMap[int(posX + dirX * moveSpeed)][int(posY)] == false) posX += dirX * moveSpeed;
if(worldMap[int(posX)][int(posY + dirY * moveSpeed)] == false) posY += dirY * moveSpeed;
}
//move backwards if no wall behind you
if (keyDown(SDLK_DOWN))
{
if(worldMap[int(posX - dirX * moveSpeed)][int(posY)] == false) posX -= dirX * moveSpeed;
if(worldMap[int(posX)][int(posY - dirY * moveSpeed)] == false) posY -= dirY * moveSpeed;
}
//rotate to the right
if (keyDown(SDLK_RIGHT))
{
//both camera direction and camera plane must be rotated
double oldDirX = dirX;
dirX = dirX * cos(-rotSpeed) - dirY * sin(-rotSpeed);
dirY = oldDirX * sin(-rotSpeed) + dirY * cos(-rotSpeed);
double oldPlaneX = planeX;
planeX = planeX * cos(-rotSpeed) - planeY * sin(-rotSpeed);
planeY = oldPlaneX * sin(-rotSpeed) + planeY * cos(-rotSpeed);
}
//rotate to the left
if (keyDown(SDLK_LEFT))
{
//both camera direction and camera plane must be rotated
double oldDirX = dirX;
dirX = dirX * cos(rotSpeed) - dirY * sin(rotSpeed);
dirY = oldDirX * sin(rotSpeed) + dirY * cos(rotSpeed);
double oldPlaneX = planeX;
planeX = planeX * cos(rotSpeed) - planeY * sin(rotSpeed);
planeY = oldPlaneX * sin(rotSpeed) + planeY * cos(rotSpeed);
}
}
}
Here's a few screenshots of the result:
Note: Usually images are stored by horizontal scanlines, but for a raycaster the
textures are drawn as vertical stripes. Therefore, to optimally use the cache of
the CPU and avoid page misses, it might be more efficient to store the textures
in memory vertical stripe by vertical stripe, instead of per horizontal scanline.
To do this, after generating the textures, swap their X and Y by (this code only
works if texWidth and texHeight are the same):
//swap texture X/Y since they'll be used as vertical stripes
for(size_t i = 0; i < 8; i++)
for(size_t x = 0; x < texSize; x++)
for(size_t y = 0; y < x; y++)
std::swap(texture[i][texSize * y + x], texture[i][texSize * x + y]);
Or just swap X and Y where the textures are generated, but in many cases after
loading an image or getting a texture from other formats you'll have it in
scanlines anyway and have to swap it this way.
When getting the pixel from the texture then, use the following code instead:
Uint32 color = texture[texNum][texSize * texX + texY];
Wolfenstein 3D Textures
Instead of just generating some textures, let's load a few from bmp's instead!
For example the following 8 textures, which come from Wolfenstein 3D and are
copyright by ID Software.
Just replace the part of the code that generates the texture patterns with the
following (and make sure those textures are in the correct path). You can
download the textures
here.
Note that this loadBMP function is a new and improved version of the original
one, now it also supports loading BMP's into 32-bit integer arrays where the RGB
color is a single integer value.
In the original Wolfenstein 3D, the colors of one side was also made darker than
the color of the other side of a wall to create the shadow effect, but they used
a seperate texture everytime, a dark and a light one. Here however, only one
texture is used for each wall and the line of code that divided R, G and B
through 2 is what makes the y-sides darker.