Precise Shadowcasting in JavaScript
This pages describes and explains the Precise Shadowcasting algorithm, developed and implemented by Ondřej Žára in rot.js.
WORK IN PROGRESS
About
In a cellular level, this algorithm computes a 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 in a distance larger than that 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:
1. A ring is a set of all cells with a constant distance from a center.
..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
2. Shadow queue is a list of all angles which are blocked (by a blocking cells). This list is intially empty; as cells are examined, some of them (those who are blocking) cast shadows, which are added to the shadow queue.
General algorithm workflow
- Let
[x,y]
be the player coordinates - Initialize the empty shadow queue
- For
R=1
up to maximum visibility range do:- Retrieve all cells whose range from
[x,y]
isR
- Make sure these cells are in correct order (clockwise or counter-clockwise; every iteration starting at the same angle)
- For every cell in this "ring":
- Determine the corresponding arc
[a1,a2]
- Consult the shadow queue to determine whether
[a1,a2]
is fully shadowed - If no part of
[a1,a2]
is visible, mark the cell as not visible and advance to next cell - If some part of
[a1,a2]
is visible, merge it into the shadow queue; mark the cell as visible
- Determine the corresponding arc
- Retrieve all cells whose range from
..... 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.
Advanced topics: tricks and tweaks
Half-angle backward shift
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]
Cutoff and angle wrapping
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:
- First cell in ring0 corresponds to 90 degrees, i.e. [-45..45] after backward shift.
- Recursively split this into two sub-arcs: [0..45] and [315..360]
- The cell in question is visible when any of these two arcs is visible
- Cutoff happens when the shadow queue contains only one interval, [0..360]
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:
- First cell in ring1 (4-topology) corresponds to 90 degrees, which translates to 0..1/4
- Backward shift - subtract 1/8: resulting arc is -1/8..1/8
- Angle wrapping/splitting: two arcs 0/8..1/8, 7/8..8/8
- Angle P/Q can be compared to R/S using simple arithmetics: P*S == R*Q (integer equality)