This pages describes and explains the Precise Shadowcasting algorithm, developed and implemented by Ondřej Žára in rot.js.
In a game level comprised of cells, this algorithm computes the set of all cells visible from a certain fixed (starting) point. This set is limited by a given maximum sight range, e.g. no cell at a distance larger than this sight range could be visible.
Cells can be either blocking (they are obstacles and stuff behind them cannot be seen) or non-blocking.
This shadowcasting is topology-invariant: its implementation is the same in all topologies. There are two basic concepts and tools:
..x..
.x.x.
x.@.x Ring 2 in 4-topology (set of all cells with distance=2)
.x.x.
..x..
xxxxx
x...x
x.@.x Ring 2 in 8-topology (set of all cells with distance=2)
x...x
xxxxx
..... Sample scenario (topology 4). Cell "#" [3,2] is blocking. It is the first cell of ring1 and thus adds [-45 .. 45] to the shadow queue.
....b
..@#a Cell "a" [4,2] is the first cell of ring2 and corresponds to arc [-22.5 .. 22.5]. Since this is a subset of the shadow queue, the cell is not visible.
.....
..... Cell "b" [4,3] is the second cell of ring2 and corresponds to arc [22.5 .. 67.5]. It is not fully shadowed, so the cell is visible.
Determining the proper arc (pair of angles) for a cell can be tricky, as the first cell does not start at angle=0:
..... Sample scenario (topology 4). Cell "A" is ring1 => size of arc is 90 degrees. Cell "B" is ring2 => size of arc is 45 degrees.
.....
.@AB. Incorrect angle assignment: A = [0 .. 90], B = [0 .. 45]
.....
..... Correct angle assignment: A = [-45 .. 45], B = [-22.5 .. 22.5]
Once the whole viewing area is shadowed, the algorithm can stop - no further cells can be seen. Detecting this situation can get tricky, based on how the shadow queue is implemented. I decided to implement the shadow queue as a list of monotonously increasing intervals. This presents a problem for cells whose angles contain zeros. A quick fix is available:
Symbolic angles
To avoid floating point chaos, I decided to represent angle values as rational numbers: fractions of two integers. Furthermore, the whole circle (360 degrees) is represented as 1. How this works:
Working with shadow queue
The shadow queue needs to be updated every time a visible AND blocking cell is encountered. Proper management of shadow queue is very important: it is necessary to merge a new arc into the existing list and this computation must be FAST. My implementation works in the following manner: