In search of the Visibility Polygon

Implementing visibility polygon calculation based on the algorithm of Asano
Draft
12 minutes
Maths Gamedev
Table of Contents

I have been building a 2D game for fun recently. Once I implemented a basic lighting system for it, I understood that I want game objects to cast shadows. There are multiple ways to achieve that, and the approach I selected is based on visibility polygons.

Fig. 1. A frame from the game I am building, with differently colored flames lighting up different areas based on their visibility

Fig. 1. A frame from the game I am building, with differently colored flames lighting up different areas based on their visibility

A visibility polygon represents the area that, for a given arrangement of occluding objects, has a direct line of sight to a certain point (called the query point). You can read more about them here:

I find the problem of calculating the visibility polygon interesting, and in this post I will tell you how I implemented an algorithm that finds them. I think the algorithm itself and the optimisations involved are conceptually cool, so I am excited to illustrate and describe them.

Fig. 2. This is what a visibility polygon looks like. The occluding objects are purple, the polygon is yellow, and the query point is red.

Fig. 2. This is what a visibility polygon looks like. The occluding objects are purple, the polygon is yellow, and the query point is red.

The algorithm I implemented is based on a description I found in 1 under the section «3.2 Algorithm of Asano». That paper actually suggests a different algorithm for this problem, but I found the algorithm of Asano more fun. That paper also references a source for the algorithm 2 , but I found the short description more interesting to work with. Also, 2 is quite hard to access.

In this post I will describe the implementation of that algorithm. There are some basic concepts I expect the reader to be familiar with, I will cover them briefly. There will also be some additional ideas that I will explain in more detail further in the post.

Basic concepts

There are some common symbols I am going to use:

SymbolMeaning
a...\forall a ...for all aa
a...\exists a ...there exists aa such that …
aba \land baa and bb (are true)
aba \lor baa or bb (or both) (are true)
a    ba \implies baa implies bb
a    ba \iff baa if and only if bb
a:=ba := baa is defined as bb

A set is a collection of objects, which are called elements of the set. The order of the elements in the set does not matter.

{a,b,c} is a set,{3,2,1,5,8} is also a set. \alig \{a, b, c\} & \text{ is a set}, \\ \{3, 2, 1, -5, 8\} & \text{ is also a set}. \\ \ealig Empty set :={}. \text{Empty set } \varnothing := \{\}.

Sets can be declared by specifying the rules of element inclusion. For a condition cond\text{cond} and expression expr\text{expr}, a set of all values of expr\text{expr} for which cond\text{cond} is true can be denoted like this:

{exprcond}. \{\text{expr} \, | \, \text{cond}\}.

For example, the set of all integers

Z:={nn is an integer}. \mathbb{Z} := \{n \, | \, n \text{ is an integer} \}.

The following are some symbols related to sets:

SymbolMeaning
aAa \in Aaa is an element of set AA
ABA \subseteq BAA is a subset of BB
ABA \cap Bintersection of sets AA and BB
AB    aA:aB. A \subseteq B \implies \forall a \in A : a \in B. AB:={aaAaB}. A \cap B := \{a \, | \, a \in A \land a \in B \}.

The set of all real numbers 3 is denoted as R\R.

The set of all pairs of real numbers

R2:={(a,b)aRbR}. \RR := \{(a, b) \, | \, a \in \R \land b \in \R\}.

In this post, I use the following notation:

A,B,C,D,...points,AB,CD,...vectors,AB,CD,...lines,AB,CD,...segments. \alig A, B, C, D, ... & - \text{points}, \\ \vecl{AB}, \vecl{CD}, ... & - \text{vectors}, \\ \overline{AB}, \overline{CD}, ... & - \text{lines}, \\ AB, CD, ... & - \text{segments}. \\ \ealig

A point is collinear 8 with a segment if it lies on the line defined by that segment’s endpoints.

P,A,BR2:P collinear AB    PAB. \alig &\forall P, A, B \in \, \RR: \\ &P \text{ collinear } AB \iff P \in \overline{AB}. \ealig

A pair of segments is collinear 8 if all of their points are on the same line.

A,B,C,DR2:AB collinear CD    AB=CD. \alig &\forall A, B, C, D \in \, \RR: \\ &AB \text{ collinear } CD \iff \overline{AB} = \overline{CD}. \\ \ealig

An intersection of two lines 9 can be one of:

A,B,C,DR2:ABCD=ABCD={P}ABCD=AB=CD,where PR2. \alig & \forall A, B, C, D \in \RR: \\ & \overline{AB} \cap \overline{CD} = \varnothing \\ & \lor \, \overline{AB} \cap \overline{CD} = \{P\} \\ & \lor \, \overline{AB} \cap \overline{CD} = \overline{AB} = \overline{CD}, \\ \text{where } & P \in \, \RR. \\ \ealig

An intersection of two segments 10 can be one of:

A,B,C,DR2:ABCD=ABCD={P}ABCD=EF,where PR2,EFABEFCD. \alig & \forall A, B, C, D \in \RR: \\ & AB \cap CD = \varnothing \\ & \lor \, AB \cap CD = \{P\} \\ & \lor \, AB \cap CD = EF, \\ \text{where } & P \in \, \RR, \\ & EF \subseteq AB \land EF \subseteq CD. \ealig

Algorithm inputs and outputs

The algorithm needs the following inputs:

No segment in S\mathbf{S} should include QQ.

sS:Qs. \forall s \in \mathbf{S} : Q \notin s.

No two distinct segments in S\mathbf{S} are allowed to intersect, unless their intersection is a single point that is an endpoint of both of those segments.

AB,CDS,ABCD:ABCD=ABCD={E},where E{A,B}E{C,D} \alig & \forall AB, CD \in \mathbf{S}, \, AB \neq CD : \\ & AB \cap CD = \varnothing \\ & \lor \, AB \cap CD = \{E\}, \\ \text {where } & E \in \{A, B\} \land E \in \{C, D\} \\ \ealig

Running the algorithm produces a set of segments SQ\mathbf{S}_Q that represents the surfaces visible from QQ. Each segment in SQ\mathbf{S}_Q is a subset of some segment from S\mathbf{S}.

SQ:=calculateVisibility(Q,S). \mathbf{S}_Q := \text{calculateVisibility} (Q, \mathbf{S}). \\ qSQ,sS:qs. \forall q \in \mathbf{S}_Q, \exists s \in \mathbf{S} : q \subseteq s.

From SQ\mathbf{S}_Q, the actual polygon can be obtained by combining the endpoints of each of the segments in SQ\mathbf{S}_Q with QQ to form triangles. The polygon is the union of all such triangles

VSQ:={PABSQ,PABQ}. \mathbf{V}_{\mathbf{S}Q} := \{P \, | \, AB \in \mathbf{S}_Q, P \in \triangle ABQ \}.

The outline of the polygon can also be trivially generated with a slight modification to the algorithm, which is left as an exercise for the reader.

Algorithm overview

To calculate the visibility polygon, we will «visit» all of the segment endpoints, keeping track of which segment is the nearest to QQ at all times.

This is a sweep line algorithm 11. The sweep line in this case is the line that connects QQ with the current endpoint. As the current endpoint changes, the line rotates around QQ.

Let nn be the number of segments in the input.

This problem can be solved naively in O(n2)O(n^2) time. For example:

  1. Sort segment endpoints by their position relative to QQ.
  2. For each endpoint EE, find all intersections between EQEQ and all other segments. Use that information to keep track of which segment is the nearest to QQ.
  3. Whenever the segment nearest to QQ changes, register that, and build up the visibility polygon out of the relevant subsegments.

An optimisation allows us to cut down the time to O(nlogn)O(n \, \text{log} \, n). Instead of keeping track of just the nearest segment, we will keep track of all segments that are currently intersected by our sweep line (henceforth called active segments).

As the sweep line goes through segment endpoints, we will be «activating» and «deactivating» corresponding segments. In practice, given some endpoint, it can be difficult to tell what to do with it on the fly. For this reason, all endpoints are designated as Start\text{Start} or End\text{End} events. See Ordering endpoints within a segment.

To keep track of currently active segments, an ordered set can be used. Common implementations of ordered sets 12 can do insertion and removal in O(logn)O(\text{log} \, n) time – that’s exactly what we need!

Note, though, that we need to define the ordering between the segments to store them in an ordered set. It’s a bit tricky to do, so see Ordering segments by distance to a point.

Summary

Time to bring this all together. This is what the algorithm does:

  1. Prepare the input data:
    • Create a set of all segment endpoints, keeping track of which segment they came from;
    • For each of the endpoints, designate it is a Start\text{Start} or an End\text{End} event;
    • Order the endpoints based on the direction from QQ to the endpoint.
  2. Determine the initial state – choose the first endpoint and find all segments that are active when the sweep line passes through it.
  3. Go over all events, doing the following:
    • Activate the segment if this is a Start\text{Start} event,
    • Deactivate the segment if this is an End\text{End} event,
    • Save a new visibility segment whenever the segment closest to QQ changes.

Let’s go through an example, and then we can look into the fun implementation detais.

Example

In this example, we will …

Let’s define some example input data. The input data consists of the point QQ and a set of segments S\mathbf{S}. You can see the input configuration in Fig. 3.1. It contains the point QQ (the star in the middle) and 16 segments (purple lines) connecting 16 points.

Note the 4 segments R1R2,R2R3,R3R4,R4R1R_1R_2, R_2R_3, R_3R_4, R_4R_1 that together form a rectangle that surrounds everything else. This guarantees that there is at least one segment in any direction from QQ, as explained in the Side note: Implications on the output.

Fig. 3.1. Example input data.

Fig. 3.1. Example input data.

For simplicity, let’s say that Q=(0,0)Q = (0, 0). Let’s also choose coordinates for all of the points:

A1=(2,1),A2=(2,3),A3=(3,3),A4=(4,3),B1=(1,3),B2=(1,2),B3=(5,2),B4=(5,3),C1=(3,2),C2=(3,4),C3=(1,4),C4=(1,2),R1=(6,5),R2=(6,5),R3=(6,5),R4=(6,5). \alig A_1 &= (2, 1), \\ A_2 &= (2, -3), \\ A_3 &= (3, -3), \\ A_4 &= (4, -3), \\ \\ B_1 &= (-1, 3), \\ B_2 &= (-1, 2), \\ B_3 &= (5, 2), \\ B_4 &= (5, 3), \\ \\ C_1 &= (-3, -2), \\ C_2 &= (-3, -4), \\ C_3 &= (-1, -4), \\ C_4 &= (-1, -2), \\ \\ R_1 &= (-6, 5), \\ R_2 &= (-6, -5), \\ R_3 &= (6, -5), \\ R_4 &= (6, 5). \\ \ealig

And, based on those points, let’s define the set of occluding segments

S={A1A2,A2A3,A3A4,A4A1,B1B2,B2B3,B3B4,B4B1,C1C2,C2C3,C3C4,C4C1,R1R2,R2R3,R3R4,R4R1}. \alig S = \{ & A_1A_2, A_2A_3, A_3A_4, A_4A_1, \\ &B_1B_2, B_2B_3, B_3B_4, B_4B_1, \\ &C_1C_2, C_2C_3, C_3C_4, C_4C_1, \\ &R_1R_2, R_2R_3, R_3R_4, R_4R_1 \}. \\ \ealig
Fig. 3.2. Ordering points based on the angle between the XXX-axis and a vector from QQQ to that point.

Fig. 3.2. Ordering points based on the angle between the XX-axis and a vector from QQ to that point.

We need to order all segment endpoints by their position relative to QQ. For each point EE, let’s find the angle QE\overrightarrow{QE} makes with the XX-axis. It can be calculated using the function

angleQ(E):=atan2(ExQx,EyQy),whereQ=(Qx,Qy),QR2,E=(Ex,Ey),QR2. \alig \text{angle}_Q \text{(} E \text{)} & := \text{atan2(} E_x - Q_x, E_y - Q_y \text{)}, \\ where \, & Q = (Q_x, Q_y), Q \in \RR, \\ & E = (E_x, E_y), Q \in \RR. \\ \ealig Sordered=[(...,...),] \alig S_\text{ordered} = [ \\ (..., ...), \\ ] \\ \ealig
Fig. 3.3. Segments connecting QQQ with all other points.

Fig. 3.3. Segments connecting QQ with all other points.

Fig. 3.4.

Fig. 3.4.

Implementation details

Ordering endpoints within a segment

Fig. 4.1, 4.2, 4.3.

Fig. 4.1, 4.2, 4.3.

Determining whether a point lies in a half-plane

A naive implementation of cmpQ\text{cmp}_Q can …

Fig. 5.

Fig. 5.

Ordering segments by distance to a point

The algorithm features a segment comparison routine that is aimed at decreasing the cost of performing the comparison by reducing the number of required floating point operations.

The cheap segment comparison routine is only valid under the following assumptions:

  1. Any two segments can only be compared between each other if a line can be drawn that includes the query point and a point from each of the segments
  2. The segments do not intersect between each other with the exception of intersections that yield their endpoints

The comparison routine can be simplified if all segments collinear with the query point are removed from the input data. For completeness, cases when the query point is collinear with the segments are still considered below.

Both segments are collinear with the query point

Fig. 6.1, 6.2.

Fig. 6.1, 6.2.

One of the segments is collinear with the query point

Fig. 6.3, 6.4.

Fig. 6.3, 6.4.

Fig. 6.5, 6.6.

Fig. 6.5, 6.6.

Fig. 6.7, 6.8.

Fig. 6.7, 6.8.

None of the segments are collinear with the query point

Fig. 6.9, 6.10.

Fig. 6.9, 6.10.

Fig. 6.11, 6.12.

Fig. 6.11, 6.12.

Fig. 6.13, 6.14. 6.15.

Fig. 6.13, 6.14. 6.15.

Fig. 6.16, 6.17.

Fig. 6.16, 6.17.

Fig. 6.18, 6.19.

Fig. 6.18, 6.19.

Reducing input size


  1. Francisc Bungiu, Michael Hemmer, John Hershberger, Kan Huang, Alexander Kröller (2014). Efficient Computation of Visibility Polygons↩︎

  2. Asano, Tetsuo (1985). An efficient algorithm for finding the visibility polygon for a polygonal region with holes. Institute of Electronics, Information, and Communication Engineers. Vol. 68. pp. 557–559. ↩︎ ↩︎

  3. Real number on Wikipedia ↩︎

  4. Euclidean plane on Wikipedia ↩︎

  5. Euclidean vector on Wikipedia ↩︎

  6. Line on Wikipedia ↩︎

  7. Line segment on Wikipedia ↩︎

  8. Collinearity on Wikipedia ↩︎ ↩︎

  9. Line–line intersection on Wikipedia ↩︎

  10. Intersection of two line segments on Wikipedia ↩︎

  11. Sweep line algorithm on Wikipedia. ↩︎

  12. For example, BTreeSet in Rust. Read about BTrees on Wikipedia. ↩︎