Raycasting 111
Raycasting 111
html
Back to index
Introduction
The articles Raycasting and Raycasting II described how to make textured walls and floors, but something is missing. In a game, a world with only walls and floors is empty, for a game to work,
there have to be goodies, enemies, objects like barrels or trees, ... These can't be drawn as wall or floor, and, in the time when raycasting games were made, can't be drawn as 3D models either.
Instead, they used sprites, 2D pictures always facing to you (so they're easy to draw and require a single picture), but that become smaller if they're further away.
You can download the full source code of this tutorial here.
How it Works
The technique used to draw the sprites is totally different from the raycasting technique. Instead, it works very similar to how sprites are drawn in a 3D engine with projections, like in the "Points,
Sprites and Moving" article. Only, we have to do the projection only in 2D, and some extra techniques to combine it with raycasting are used.
Drawing the sprites is done after the walls and floor are already drawn. Here are the steps used to draw the sprites:
1: While raycasting the walls, store the perpendicular distance of each vertical stripe in a 1D ZBuffer
2: Calculate the distance of each sprite to the player
3: Use this distance to sort the sprites, from furthest away to closest to the camera
4: Project the sprite on the camera plane (in 2D): subtract the player position from the sprite position, then multiply the result with the inverse of the 2x2 camera matrix
5: Calculate the size of the sprite on the screen (both in x and y direction) by using the perpendicular distance
6: Draw the sprites vertical stripe by vertical stripe, don't draw the vertical stripe if the distance is further away than the 1D ZBuffer of the walls of the current stripe
7: Draw the vertical stripe pixel by pixel, make sure there's an invisible color or all sprites would be rectangles
You don't have to update the ZBuffer while drawing the stripes: since they're sorted, the ones closer to you will be drawn last, so they're drawn over the further away ones.
How to project the sprite on screen is explained in full 3D rendering mathematics (with 3D matrices and camera), for true 3D rasterizer or raytracer 3D engines, and is not explained here in the
raycaster tutorial. However, here it's done in 2D and no fancy camera class is used. To bring the sprite's coordinates to camera space, first subtract the player's position from the sprite's position,
then you have the position of the sprite relative to the player. Then it has to be rotated so that the direction is relative to the player. The camera can also be skewed and has a certain size, so it isn't
really a rotation, but a transformation. The transformation is done by multiplying the relative position of the sprite with the inverse of the camera matrix. The camera matrix is in our case
[planeX dirX]
[planeY dirY]
Then you get the X and Y coordinate of the sprite in camera space, where Y is the depth inside the screen (in a true 3D engine, Z is the depth). To project it on screen, divide X through the depth,
and then translate and scale it so that it's in pixel coordinates.
To place the objects in the level, many things can be done. Each object can have its own floating point coordinates, it doesn't have to be exactly in the center of floor tiles. You can make a list of
each object and give its coordinates and texture one by one, or you can place the objects by making a second map (a 2D array), and for every tile coordinate place one or no object, the same way as
placing the walls. If you do that, then let the program read that map and create a list of objects out of it, with every object placed in the center of the corresponding map tile. Coordinates in the
center of a tile are a halve, e.g. (11.5, 15.5), while whole coordinates will be the corners of tiles.
The code presented below uses a small list of objects instead of a map.
The Code
The code tries to load the wolfenstein textures, with extra textures for 3 sprites, you can download them here (copyright by id Software). If you don't want to load textures, you can use the part of
code that generates textures from the previous raycasting tutorial instead, but it looks less good. You'll also have to invent something yourself for the sprites, black is the invisible color.
The full code of the whole thing is given, it's similar to the code of Raycasting II, but with added code here and there.
The new variables for sprite casting are the struct Sprite, containing the position and texture of the sprite, the value numSprites: the number of sprites, the sprite array: defines the positions and
textures of all the Sprites, the declaration of the bubbleSort function: this will sort the sprites, and the arrays used as arguments for this function: spriteOrder and spriteDistance, and finally, ZBuffer:
this is the 1D equivalent of the ZBuffer in a true 3D engine. In the sprite array, every third number is the number of its texture, you can see which number means what there where the textures are
loaded.
int worldMap[mapWidth][mapHeight] =
1 of 8 3/20/2025, 4:36 PM
Raycasting III: Sprites https://lodev.org/cgtutor/raycasting3.html
{
{8,8,8,8,8,8,8,8,8,8,8,4,4,6,4,4,6,4,6,4,4,4,6,4},
{8,0,0,0,0,0,0,0,0,0,8,4,0,0,0,0,0,0,0,0,0,0,0,4},
{8,0,3,3,0,0,0,0,0,8,8,4,0,0,0,0,0,0,0,0,0,0,0,6},
{8,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6},
{8,0,3,3,0,0,0,0,0,8,8,4,0,0,0,0,0,0,0,0,0,0,0,4},
{8,0,0,0,0,0,0,0,0,0,8,4,0,0,0,0,0,6,6,6,0,6,4,6},
{8,8,8,8,0,8,8,8,8,8,8,4,4,4,4,4,4,6,0,0,0,0,0,6},
{7,7,7,7,0,7,7,7,7,0,8,0,8,0,8,0,8,4,0,4,0,6,0,6},
{7,7,0,0,0,0,0,0,7,8,0,8,0,8,0,8,8,6,0,0,0,0,0,6},
{7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,6,0,0,0,0,0,4},
{7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,6,0,6,0,6,0,6},
{7,7,0,0,0,0,0,0,7,8,0,8,0,8,0,8,8,6,4,6,0,6,6,6},
{7,7,7,7,0,7,7,7,7,8,8,4,0,6,8,4,8,3,3,3,0,3,3,3},
{2,2,2,2,0,2,2,2,2,4,6,4,0,0,6,0,6,3,0,0,0,0,0,3},
{2,2,0,0,0,0,0,2,2,4,0,0,0,0,0,0,4,3,0,0,0,0,0,3},
{2,0,0,0,0,0,0,0,2,4,0,0,0,0,0,0,4,3,0,0,0,0,0,3},
{1,0,0,0,0,0,0,0,1,4,4,4,4,4,6,0,6,3,3,0,0,0,3,3},
{2,0,0,0,0,0,0,0,2,2,2,1,2,2,2,6,6,0,0,5,0,5,0,5},
{2,2,0,0,0,0,0,2,2,2,0,0,0,2,2,0,5,0,5,0,0,0,5,5},
{2,0,0,0,0,0,0,0,2,0,0,0,0,0,2,5,0,5,0,5,0,5,0,5},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5},
{2,0,0,0,0,0,0,0,2,0,0,0,0,0,2,5,0,5,0,5,0,5,0,5},
{2,2,0,0,0,0,0,2,2,2,0,0,0,2,2,0,5,0,5,0,0,0,5,5},
{2,2,2,2,1,2,2,2,2,2,2,1,2,2,2,5,5,5,5,5,5,5,5,5}
};
struct Sprite
{
double x;
double y;
int texture;
};
#define numSprites 19
Sprite sprite[numSprites] =
{
{20.5, 11.5, 10}, //green light in front of playerstart
//green lights in every room
{18.5,4.5, 10},
{10.0,4.5, 10},
{10.0,12.5,10},
{3.5, 6.5, 10},
{3.5, 20.5,10},
{3.5, 14.5,10},
{14.5,20.5,10},
//1D Zbuffer
double ZBuffer[screenWidth];
std::vector<Uint32> texture[11];
for(int i = 0; i < 11; i++) texture[i].resize(texWidth * texHeight);
3 new textures are loaded: the sprites. There's nothing that stops you from loading more textures, or making the textures higher resolution.
2 of 8 3/20/2025, 4:36 PM
Raycasting III: Sprites https://lodev.org/cgtutor/raycasting3.html
Here's the main loop which starts with raycasting the floors and walls, same code as before.
// Horizontal distance from the camera to the floor for the current row.
// 0.5 is the z position exactly in the middle between floor and ceiling.
float rowDistance = posZ / p;
// calculate the real world step vector we have to add for each x (parallel to camera plane)
// adding step by step avoids multiplications with a weight in the inner loop
float floorStepX = rowDistance * (rayDirX1 - rayDirX0) / screenWidth;
float floorStepY = rowDistance * (rayDirY1 - rayDirY0) / screenWidth;
// real world coordinates of the leftmost column. This will be updated as we step to the right.
float floorX = posX + rowDistance * rayDirX0;
float floorY = posY + rowDistance * rayDirY0;
floorX += floorStepX;
floorY += floorStepY;
// floor
color = texture[floorTexture][texWidth * ty + tx];
color = (color >> 1) & 8355711; // make a bit darker
buffer[y][x] = color;
// WALL CASTING
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 rayDirX = dirX + planeX * cameraX;
double rayDirY = dirY + planeY * cameraX;
3 of 8 3/20/2025, 4:36 PM
Raycasting III: Sprites https://lodev.org/cgtutor/raycasting3.html
//Calculate distance of perpendicular ray (Euclidean distance would give fisheye effect!)
if(side == 0) perpWallDist = (sideDistX - deltaDistX);
else perpWallDist = (sideDistY - deltaDistY);
After raycasting the wall, the ZBuffer has to be set. This ZBuffer is 1D, because it only contains the distance to the wall of every vertical stripe, instead of having this for every pixel. This also ends
the loop through every vertical stripe, because rendering the sprites will be done outside this loop.
After the floors and walls are finally drawn, the sprites can be drawn. This code is very unoptimized, a few improvements are explained later. First it sorts the sprites from far to close, so that the far
ones will be drawn first. Then it projects each sprite, calculates the size it should have on screen, and draws it stripe by stripe. The matrix multiplication for the projection is very easy because it's
only a 2x2 matrix. It comes in very handy again that they raycaster already used a 2D camera matrix, instead of representing the player with an angle and a position instead like some raycasters do.
The distance calculated for the sorting of the sprites is never used later on, because the perpendicular distance is used instead. For sorting the sprites it doesn't matter if you take the square root of
the distance or not, so no calculation time is wasted for that.
//SPRITE CASTING
//sort sprites from far to close
for(int i = 0; i < numSprites; i++)
{
spriteOrder[i] = i;
4 of 8 3/20/2025, 4:36 PM
Raycasting III: Sprites https://lodev.org/cgtutor/raycasting3.html
spriteDistance[i] = ((posX - sprite[i].x) * (posX - sprite[i].x) + (posY - sprite[i].y) * (posY - sprite[i].y)); //sqrt not taken, unneeded
}
sortSprites(spriteOrder, spriteDistance, numSprites);
double invDet = 1.0 / (planeX * dirY - dirX * planeY); //required for correct matrix multiplication
After it all is drawn, the screen is updated, and the input keys are handled.
drawBuffer(buffer[0]);
for(int y = 0; y < h; y++) for(int x = 0; x < w; x++) buffer[y][x] = 0; //clear the buffer instead of cls()
//speed modifiers
double moveSpeed = frameTime * 3.0; //the constant value is in squares/second
double rotSpeed = frameTime * 2.0; //the constant value is in radians/second
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);
}
5 of 8 3/20/2025, 4:36 PM
Raycasting III: Sprites https://lodev.org/cgtutor/raycasting3.html
}
}
The sortSprites sorts the sprites from farthest away to closest by distance. It uses the standard std::sort function provided by C++. But since we need to sort two arrays using the same order here
(order and dist), most of the code is spent moving the data into and out of a vector of pairs.
//sort algorithm
//sort the sprites based on distance
void sortSprites(int* order, double* dist, int amount)
{
std::vector<std::pair<double, int>> sprites(amount);
for(int i = 0; i < amount; i++) {
sprites[i].first = dist[i];
sprites[i].second = order[i];
}
std::sort(sprites.begin(), sprites.end());
// restore in reverse order to go from farthest to nearest
for(int i = 0; i < amount; i++) {
dist[i] = sprites[amount - i - 1].first;
order[i] = sprites[amount - i - 1].second;
}
}
}
The green light is a very small sprite, but the program still goes through all its invisible pixels to check their color. It could be made faster by telling which sprites have large invisible parts, and
only drawing a smaller rectangular part of them containing all visible pixels.
To make some objects unwalkthroughable, you can either check the distance of the player to every object when he moves for collision detection, or, make another 2D map that contains for every
square if it's walkthroughable or not, this can be used for walls as well.
In for example Wolfenstein 3D, some objects (for example the soldiers) have 8 different pictures when viewing it from different angles, to make it appear as if the sprite is really 3D. You can get the
angle of the object to the player for example with the atan2 function, and then choose 1 of 8 textures depending on the angle. You can also give the sprites even more textures for animation.
Scaling Sprites
It's pretty easy to let the program draw the sprites larger or smaller, and move the sprites up or down. To shrink the sprite, divide spriteWidth and spriteHeight through something. If you halve the
height of the sprites, for example the pillar, then the bottom will move up so that the pillar appears to be floating. That's why in the code below, apart from the parameters uDiv and vDiv to shrink
the sprite, also a parameter vMove is added to move the sprite down if it has to stand on the floor, or up if it has to hang on the ceiling. vMoveScreen is vMove projected on the screen by dividing it
through the depth.
6 of 8 3/20/2025, 4:36 PM
Raycasting III: Sprites https://lodev.org/cgtutor/raycasting3.html
}
}
}
When uDiv = 2, vDiv = 2, vMove = 0.0, the sprites are half as big, and float:
Put it back on the ground by setting vMove to 64.0 (the size of the texture):
If you make vMove even higher to place the sprites under the ground, they'll still be drawn through the ground, because the ZBuffer is 1D and can only detect if the sprite is in front or behind a
wall.
Of course, by lowering the barrels, the green light is lower too so it doesn't hang on the ceiling anymore. To make this useful, you have to give each sprite its own uDiv, vDiv and vMove
parameters, for example you can put them in the sprite struct.
Translucent Sprites
Because we're working in RGB color, making sprites translucent is very simple. All you have to do is take the average of the old color in the buffer and the new color of the sprite. Old games like
for example Wolfenstein 3D used a palette of 256 colors with no logical mathematical rules for the colors in the palette, so there translucency wasn't so easy. Change the following line of code
if((color & 0x00FFFFFF) != 0) buffer[y][stripe] = color; //paint pixel if it isn't black, black is the invisible color
into
if((color & 0x00FFFFFF) != 0) buffer[y][stripe] = RGBtoINT(INTtoRGB(buffer[y][stripe]) / 2 + INTtoRGB(color) / 2); //paint pixel if it isn't black, black is the invisible color
7 of 8 3/20/2025, 4:36 PM
Raycasting III: Sprites https://lodev.org/cgtutor/raycasting3.html
if((color & 0x00FFFFFF) != 0) buffer[y][stripe] = RGBtoINT(3 * INTtoRGB(buffer[y][stripe]) / 4 + INTtoRGB(color) / 4); //paint pixel if it isn't black, black is the invisible color
You can also try more special tricks, for example translucent sprites, that make the walls behind them of negative color:
if((color & 0x00FFFFFF) != 0) buffer[y][stripe] = RGBtoINT((RGB_White - INTtoRGB(buffer[y][stripe])) / 2 + INTtoRGB(color) / 2); //paint pixel if it isn't black, black is the invisible color
To be useful for a game, it would be more handy to give each sprite its own translucency effect (if any), with an extra parameter in the sprite struct. For example the green light could be translucent,
but a pillar certainly not.
8 of 8 3/20/2025, 4:36 PM