A Wolf3D-like raycaster using OpenGL shaders
This is a hobby project to make a raycaster using the GPU power with Compute and Fragment Shaders. It is based on the Lode's Computer Graphics Tutorial (the raycast series), which uses a full CPU solution.
The engine uses three steps for the rendering:
- raycaster: performs the raycasting calculations for each screen column (DDL, texture size, perspective...)
- spritecaster: performs the raycasting calculations for each sprite (sprite size, draw position, transformations...)
- drawer: draws the results from the previous steps into the screen (walls, ceiling, floor and sprites)
The first two are Compute Shaders, and the last one is a Fragment Shader.
The player position and direction are handled in the CPU side, and the values are sent to the shaders each frame.
The map uses the same format as in the tutorials, but it is stored internally as a 2D texture with only red component in 8-bit unsigned int format (the map is accessed as map[x][y]
which is not the common way to do it).
All textures are stored in a 2D Texture Array, where each layer is a different texture.
The shader runs in parallel calculations for each column of the screen (width). The input is the map texture as a uimage2D
and the output is an array of structs with some data that will be used in the fragment shader. The struct has this look:
struct xdata {
ivec2 draw;
int side;
uint textureNum;
int texX;
float step;
float texPos;
float distWall;
vec2 floorWall;
};
Variables receive the same name as in tutorial (with some modifications if they are vectors).
The output uses a Shared Storage Buffer Object that allows to allocate some space in the GPUs memory to read and write arbitrary data, and can be shared with shaders. The input, instead, is bound to the shader as a image (instead of texture) so the shader can read precisely the contents of the texture using xy
coords (not uv
coords, which is the common way to access textures).
The shader also receives as input the player position
, the direction
it looks at, the plane
for the direction and the screenSize
(the visible section, not the whole window).
With this input, the shader runs for each column of the screen (width) in parallel. Each instance calculates the values for that column and puts the result in the array of struct xdata
. Uses the vertical version of the algorithm.
Currently, a screen cannot have more than 10000px, it is hardcoded like this :(
Note: the screen is always 4:3 aspect ratio, so your screen can be more than 10000px, but what cannot be true is
height * 4 / 3 > 10000
.
While the previous shader is running, this other shader prepares its run (and may even run in parallel with the raycaster). The shader runs in parallel some calculations for each sprite in the map. The input is an array of Sprite
s sorted by position (first the further sprites), and the output is an array of the result calculations. The two structs look like this:
// input
struct sprite {
float x;
float y;
uint texture;
int uDiv;
int vDiv;
float vMove;
};
// output
struct spritedata {
int spriteWidth;
int spriteHeight;
float transformY;
int spriteScreenX;
ivec2 drawX;
ivec2 drawY;
int vMoveScreen;
uint texture;
};
The shader also receives as input the player position
, the direction
it looks at, the plane
for the direction and the screenSize
(the visible section, not the whole window). Basically, the same uniform
s as in the previous shader.
The input and output data are also Shared Storage Buffer Objects. The first one is filled from the CPU side using a std::vector<Sprite>
sorted by distance (first further ones) - update process is done once per second.
Each instance of the shader, calculates the position and size of a sprite and puts the result in the output buffer, so the Fragment Shader can read the results.
A map cannot have more than 100 sprites, it is limited to that (it is also hardocded sorry).
This shader draws into a plane the results. The plane is located in front of the "camera" so it will always occupy the whole viewport. This shader receives the two shared buffers from the previous shaders and the 2D texture array that contains all textures, and draws into the plane.
Takes care of drawing the walls with its texture or the ceiling and floor, and then the sprites over. The sprites step tries to draw each sprite for each pixel. There is a if
to prevent trying to draw the sprite directly, but the loop is there (this could be optimized). The sprite drawing in the tutorial used integer operations to avoid float calculations, in the shader floats are being used because it is faster.
The game without a map is useless. Maps are stored as yaml files and contain the map itself (which will converted into a texture) and its size, the initial player position and direction, and the sprites. There is an example of map in the maps folder.
Note: map loader does not have validations, so if the schema of the yaml is wrong, the engine could crash
The first section of the file is the map
which describes how it looks like. The contents
contains how the map is. Each row is a x
coord and each column is a y
coord (remember the map is flipped from normal texture usage). Each value of the map represents a texture that will be drawed into the screen (the value must substract 1 to get the texture ID, so 1
will point to texture ID 0
). 0
means no wall, so the player can move through this section. There are some two optional values floor
and ceil
that modifies what is rendered in the floor and ceiling. The accepted values are a texture ID (3
just the number) or a color ([ 0.2, 0.2, 0.2 ]
rgb color as floats).
The second section is the initial
s values for the player position, direction and plane. When the engine is loaded, will set these values to the ones in the yaml.
The third, and last, section is the sprites
list. Each value of the list points to a sprite that will be placed in the position and the texture to draw.
As mentioned several times, the textures are stored in a 2D Texture Array. This is the list of textures (and its ID) that are loaded into the engine:
ID | texture | Purpose |
---|---|---|
0 | eagle.png |
Wall |
1 | redbrick.png |
Wall |
2 | purplestone.png |
Wall |
3 | greystone.png |
Wall |
4 | bluestone.png |
Wall |
5 | mossy.png |
Wall |
6 | wood.png |
Wall |
7 | colorstone.png |
Wall |
8 | barrel.png |
Sprite |
9 | pillar.png |
Sprite |
10 | greenlight.png |
Sprite |
The repository does not have the textures, they must be downloaded from the tutorial, which has a download link almost at the end.
The project uses git submodules and CMake. To clone the repository use:
git clone https://github.com/melchor629/raycastergl
The required external dependency is GLFW. On Linux, you can run:
#Arch/Manjaro
sudo pacman -S glfw-x11
#Debian/Ubuntu
sudo apt install -y libglfw3-dev libglfw3
#Windows (using vcpkg - for a manual installation of GLFW3 see below section)
vcpkg install glfw3
The project uses C++17, so ensure to have a very recent compiler (like GNU GCC 9, Clang 9, or MSVC 15 [2019]).
Once all requirements are met and installed, you can now use cmake
or cmake-gui
to configure the project. An example for Linux:
mkdir build
cmake -S . -B build
cd build
make -j2
./raycastergl
And for Windows (using vcpkg):
mkdir build
# If using VS 2017
cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=C:\path\to\vcpkg\scripts\buildsystems\vcpkg.cmake -G "Visual Studio 15 2017 Win64"
# If using VS 2019
cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=C:\path\to\vcpkg\scripts\buildsystems\vcpkg.cmake -G "Visual Studio 16 2019" -A Win64
# you can also open the solution in Visual Studio or open the folder in Visual Studio Code with cmake extension
cd build
cmake --build .
.\raycastergl
Please use 64-bit version for Windows instead of 32-bit. All code is tested under 64-bit and some nasty bug can appear when using 32-bit...
To debug in your local, change the Working Directory to
$(OutputPath)
.
- Go to GLFW3 page and download the latest source code (or the latest of version 3)
- Extract the zip and open a terminal there
- Create the folder
build
- Run
cmake -S . -B build -G "Visual Studio 15 2017 Win64" -DCMAKE_PREFIX_INSTALL="${PWD}\output"
orcmake -S . -B build -G "Visual Studio 16 2019" -A Win64 -DCMAKE_PREFIX_INSTALL="${PWD}\output"
cd build
cmake --build .
to build the projectcmake --install .
to install the project (it will be installed in a folder calledoutput
)- When running the
cmake
to configure the project, use-DGLFW3_DIR=C:/Path/To/Glfw3/output/lib/cmake/glfw3
instead of-DCMAKE_TOOLCHAIN_FILE=...
By default, the engine will run the default.yaml
map at resolution 1333x1000, with V-Sync enabled. All of this can be changed with arguments. Run ./raycastergl --help
to change these options.
The controls are:
W
,S
,UP Arrow
andDOWN Arrow
to move forward and backwardsA
,D
,LEFT Arrow
andRIGHT Arrow
to rotate the cameraESC
to close the gameF
to enter or exit fullscreen mode- The mouse also works to move and rotate the camera