Raycasting engine created in System Verilog. Submitted as the final project for CS350C at UT Austin by Dylan Dang.
To build this project you will need:
- C++ toolchain
- verilator
- SDL2
This project was tested in Ubuntu 22.04, however it should work on other platforms.
To install the dependencies on Debian-based distros, you may run:
sudo apt install build-essential verilator libsdl2-devTo build the project, you can run:
make buildThis should output the compiled binary to obj_dir/raycaster. From there, you
may execute it directly or use the make command:
make runTo clean, build, and run in one step, you may use:
make startThis raycaster module outputs a 3d projection of level on a 640x480 screen by casting rays from a player point to to determine intersections with a grid of cells given by a level rom. This algorithm is ran in the blanking interval and outputs colored pixels every clock cycle while the data enable pin is high. It recieves movement key inputs to update the internal player and rerender the scene. It supports custom bitmap textures and custom levels. This module is compiled into a C++ object by Verilator where it can be simulated by simulate.cpp with a virtual screen window created by SDL2.
raycaster.demo.mp4
Firstly we have a level populated by N by M cells, and each cell has 4 walls.
Each dimension of a cell one scaled unit long. When casting a ray from the
player, we want to check every possible intersection with a grid line of the map
and get the closest wall. Grid lines can be split into two cases, horizontal
and vertical. Let's do the horizontal case. We want to get the ray's closest
horizontal gridline in front. We can get this by rounding down the
players y position, let's call this
Now we want to the x value on this grid line a ray is looking at. To do
this, we can imagine a right triangle with a height equal to the difference
between
The same process can be done for vertical grid lines as well, except we swap
Ranging
Writing this raycaster taught me a lot about verilog and how utilize several techniques to creaate a impressive pseudo 3d world. Furthermore, I employed various strategies in order to optimize hardware synthesis some of which will be listed below.
Unlike software-oriented languages, Verilog does not come with floating point arithmetic, at least not for synthesis; rather, Verilog only comes equipped with integer operations. In order to do floating point operations, you would need to create your own floating point unit. However, doing so is complex and slow. This makes it tricky to perform accurate mathematics in order to project the world onto the scene. To get around this limitation, I utlilized a simple trick: fixed point representation.
Fixed point representation is a simple yet powerful way to represent fractional
values with the use of fast integer arithmetic circuits. It comes by treating a
portion of the binary digit as fractional, however such distinction only exists
in the programmers' mind. In order to keep track of which registers hold fixed
point numbers, I defined a 32-bit type fix_t to follow a Q16.16 format. All
operations remain the same except multiplication since it outputs in Q32.32 so
it must be shifted right 16 bits, handled by the mult function.
In hardware, division by an arbitrary amount is slow. To remedy this, this raycaster uses no divisions, only multiplication and addition. You may be thinking, "but divisions are necessary to perform the calculations needed for raycasting." However, there are a few more tricks to get around this.
- Using bitshifts when dividing by a power of 2.
ALthough this helps with some of the divisions, this does not work for all cases, so we must use another tactic.
- Multiplying by the reciprocal
Since we are using fixed point representation, we can actually multiply by the
reciprocal and still get the same result! The problem now is actually getting
the reciprocal. If the divisor is constant, such as those from the settings, we
can simply get the reciprocal at compile time. Using the function to_fix, I
am able to convert compile time only real values to fixed point reciprocals.
However, the problem still remains for getting inverse of square root and trig
functions necessary for calculating angles and mapping world space to screen
space. To perform these calculation, I have one final trick up my sleeve.
- Look up tables
This is the last resort when needing to multiply a number by dynamic divisor. However, this only occurs with trigonometry functions or the inverse square root. Which will be explained in the two sections.
Working with angles inherantly requires us to use trignometry, but how do
compute them? The answer is we don't! To compute the sine of an angle, I simply
take evenly spaced samples of
This is all fine and dandy for sine and cosine, but
To get the height of a line on the screen. We need the inverse of the ray distance. That is:
Everything is fine, except for the pesky sqrt and division. Killing two birds
one stone, we reduce our problem to finding
Using Newton's method we can get a sucessively better approximations using
fixed number of iterations to finding root of a function with an good initial
guess. Applying Newton's method to
So now,
Last problem is that our square distance, inv_sqrt we divide it by
Near the top of raycaster.sv many macros exist in order to customize the
compiled program. "boolean" types are true when the desired setting is merely
defined. Some things, like textures and cell types, may only be customized by
editing the code directly.
Levels are stored under the levels folder. They consist of white space
seperated hex values pertaining the the type of cell it an index is. Type 0 is
air and the rest may be viewed and edited in the cell_t enum.
| setting | type | usage |
|---|---|---|
| LEVEL | string | path to level file to load |
| MAP_X | integer | number of cells in the world x direction (level width) |
| MAP_Y | integer | number of cells in the world y direction (level height) |
| MAP_SCALE_X | real | units a cell takes in the world x direction |
| MAP_SCALE_Y | real | units a cell takes in the world y direction |
| MAP_SCALE_Z | real | units a cell takes in the world z direction (wall height) |
| MAP_WRAP | boolean | whether to repeat map cells when exiting map bounds |
Textures are stored as bitmaps under the textures/ folder and are mapped to
the textures array through load_bmp ran at compile time. A known limitation
is that TEX_X and TEX_Y must be divisible by 4 since bitmap specifications
require rows be 4 byte aligned.
| settings | type | usage |
|---|---|---|
| TEX_X | integer | width in pixels of all textures |
| TEX_Y | integer | height in pixels of all textures |
| setting | type | usage |
|---|---|---|
| PLAYER_SPEED | real | units the player should move in one frame |
| PLAYER_TURN_SPEED | real | radians the player should turn in one frame |
| PLAYER_INIT_X | real | world x position in units player starts |
| PLAYER_INIT_Y | real | world y position in units player starts |
| PLAYER_INIT_ANGLE | real | direction in radians player begins facing |
| setting | type | usage |
|---|---|---|
| FOV | real | field of view in radians |
| setting | type | usage |
|---|---|---|
| TRIG_SAMPLES | integer | number of samples sine and secant LUTs should contain |
Map screen dimensions are equal to the (overlay scale)*(map dimension)*(map scale) within each direction.
| setting | type | usage |
|---|---|---|
| MAP_OVERLAY | boolean | whether to draw map overlay |
| OVERLAY_SCALE_X | real | scale of map overlay in screen x direction |
| OVERLAY_SCALE_Y | real | scale of map overlay in screen y direction |
| OVERLAY_OFFSET_X | real | pixel offset of map overlay in the screen x direction |
| OVERLAY_OFFSET_Y | real | pixel offset of map overlay in the screen y direction |
| OVERLAY_PLAYER_SIZE | real | width and height of player indicicator in map overlay |
Collection of links I found helpful while creating this: