2025_01_21_voronoi





2025_01_21_voronoi


Health warning

This article is about using Fortune’s Algorithm to generate Voronoi
Diagrams in O(nlogn) time. If I’d known how hard it would be I wouldn’t
have started it. If you are intending to implement it with the intent to
actually use it, rather than as an exercise, I would recommend not using
Fortune’s. Unless you are doing a lot of large diagrams – like
multiple large diagrams every second – I’d suggest looking at an O(n^2)
implementation, which I’ve heard is much easier. Or even better, using a
library. While it was very frustrating at times, it is a cool looking
algorithm when you get it working.

What is a Voronoi diagram?

A Voronoi diagram is method of partitioning a plane into regions. It
is often used to procedurally generate maps.

You pick a bunch of points on a plane called ‘sites’, and the region,
or ‘cell’, corresponding to that site is the area enclosing every point
which is closer to that site than to any other site.

Put another way, the edge of each cell is where it is equidistant to
two sites (top drawing). The ‘corners’ of the cells, where edges meet,
are called ‘Voronoi Vertices’. These are the points which are
equidistant from three sites (bottom drawing).

There are a few methods for generating the diagrams. A popular one is
Fortune’s Algorithm, and that’s the one I’ll describe here.

Fortune’s Algorithm

Seen visually, this method creates a line which ‘sweeps’ from the
left edge of the plane to the right, pixel by pixel – called the
sweep-line. When the line encounters a site it creates
what looks like a ‘bubble’ around it (actually, a parabolic arc). The
bubble grows as the sweep line gets further away. The magic happens when
two arcs from two different sites collide: the point of collision
becomes the edge of the cells, it being equidistant between the two
points. And when two of those edges collide, it creates a
corner in the diagram. The ‘frontier’ of all the active bubbles is
called the beachline, I guess because it sort of looks
like a coast?

In reality you don’t actually go pixel by pixel. Interesting things
happen at certain points which can be calculated – irritatingly called
events – and we just look at those. There’s a lot to
unpack here, and we need to do some groundwork before really getting
into it.

Glossary of Terms

There are a few terms that are core to understanding the process.
I’ll define them briefly here, but most of them won’t make sense until
you understand how they relate to the algorithm. they’re just here for
reference so you can come back to them later.

A site, sometimes called a seed, is a 2d point. The
sites are what determine the shape of the resulting Voronoi diagram,
because the edges of the cells are equidistant from two sites.

The sweep line is the vertical line that sweeps
across the region. As the sweep line passes each event in the event
queue, that event is processed.

The beach line is a line comprised of a series of
arcs. When an event is processed, the beach line is affected by either
adding or removing arcs. Each arc corresponds to a site, though each
site can have multiple, or no, arcs on the beachline at any given point
in time.

An intersection is a point where two arcs on the
beachline meet eachother. These intersections are also equidistant
between the sites to which the arcs relate.

The event queue is where the site and circle events
are stored, ordered by ascending x-coordinate. The algorithm progresses
by popping the next event off the queue, and processing it, until there
are none left. This is a conceptually a priority queue, though isn’t
implemented as one here.

A site event is one of the two types of event on the
event queue, and the simpler one. It is defined by the coordinates of a
corresponding site. Processing a site event results in a new arc being
added to the beach line. Since the sites are known when you begin the
algorithm, they are added to the queue before the algorithm starts
processing events.

A circle event is the other type of event on the
queue. It is defined by the three arcs which sit on the perimeter of the
circle. Apart from the center and radius of the circle, another
important attribute of the circle is the circle point,
which is the right-most point of the circle. It’s the x-coordinate of
this circle point which determines where it sits in the event queue.
Circle points are not known ahead of time, but are generated as arcs are
added to or removed from the beach line. Processing a circle event
results in an arc being removed from the beach line, and a Voronoi
Vertex and two half edges being created.

A Voronoi Vertex is a point equidistant between 3
sites, where three equiedges meet. They are the corners of the cells of
the Voronoi diagram. They are also the center of the circles of circle
events.

An equiedge is an line which is equidistant between
two sites in the Voronoi diagram.

An incomplete edge is a line which has a fixed point
on one side, but the other side is defined as the intersection of two
parabola focus points. Since the parabola intersections change with the
sweep line, this end of the edge is not fixed. Incomplete edges become
‘complete’ when the two incomplete edges meet eachother (at a circle
event), and in doing so generate Voronoi Vertices and half edges.

A half edge is a part of the Voronoi data structure.
It has a origin point (a Voronoi Vertex) and a ‘twin’ half edge. Half
edges are generated from the ‘completion’ of an incomplete edge.

A Parabolic Tangent

The concept and properties of a parabola are really important to this
algorithm. So we need to be able to represent
parabola.1
Parabola are usually defined using an equation y=ax^2+bx+c
or some slight variation on that. We use a different, but equivalent
formulation called the ‘locus definition’. You can define a parabola
using two things: A 2d coordinate called the focus
point
, and a line called the
directrix2.
The parabola consists of all points which are equally distant from the
focus point and the directrix. If we restrict ourselves to a vertical
directrix, then the directrix becomes a single number representing the
x-coordinate of the line.

This how you can calculate the points of the parabola. Derivation is
not going to be covered here, and it’s not actually important for the
algorithm. But it’s useful for drawing
it3.

4.

5.
So in the following, the beachline is arc 1 and arc 2. Or,
[arc1,arc2]

And here it’s [arc1, arc2, arc3]

There are a few nuances though. Look at this one, where there are
three arcs: 1, 2, and 3. A full description of the frontier needs to
reflect that 2 separate parts of arc1 are on the frontier, with the
middle bit being covered by 3.

It’s actually easy to handle this: just put it in the beachline
twice: [arc1, arc3, arc1, arc2]

A final scenario I’ll mention, because it turns out to be important,
is that it’s totally possible to have a beachline where the same three
arcs appear together but in a different order. Like you might
have the sequence [i,j,k], and then later
[k,j,i]. Triples of arcs on the beachline are important for
reasons that will become clear. Each ordered triple will be
unique in the beachline. So you can’t have [i,j,k]
twice.

For each pair of arcs in the beachline we calculate the intersection,
and by drawing the arcs between the intersection, we sketch the
beachline.

6.

All good. If we carry on though, and draw the beachline with the
sweep line a bit further on, we run into a problem.

We went too far! If I dial that line back a bit, you can see what is
happening – or about to happen – more clearly:

Depending on how you look at it, 3 things are happening:

  1. The orange lines are intersecting
  2. The middle arc is being squeezed out of existance
  3. The intersection points of two pairs of arcs on the beachline are
    colliding

So when we get to that point we need to remove the middle arc from
the beachline. This ‘arc squeezing’ point is the second type of point we
need to deal with, but its a bit more complicated than the sites. So
lets look at what’s happening in more detail.

Way back at the start I said the edges of the Voronoi were where two
points were equidistant, and the ‘corners’ of the cells where the edges
meet are the points which are equidistant from 3 or more sites. If you
eyeball it in the above picture, you can see that it looks like the
where the lines intersect is about in the middle of the three
points.

We can be a bit more precise than that. What we’re saying
mathematically is that, if we draw a circle where the 3 sites all sit on
the radius, the center of the circle will be equidistant from all 3
points, and by the above definition will be where the edges of the
Voronoi intersect. A circle which puts a set of given points on its
circumference is called a circumcircle (and it’s center is a
circumcenter)7.
If we plug the numbers in and draw the circle we get this:

Which is spot on. And this is the really important point, because
this is where we finally have a Voronoi Vertex, which is the
whole point of this exercise. When we reach the Voronoi Vertex, defined
by the three arcs on the beachline [..,i,j,k,..], the
center arc j gets removed from the beachline, so the
beachline looks like [..,i,k,..]. So now we have a new
boundary between arcs i and k, where none existed
before. You can see how this looks in the diagram: there’s a new
equiedge coming out of the Voronoi Vertex we created, which is between
sites i and k

But we’re not out of the woods yet. We know we we need to process
each site (i.e. add a new arc) in order from left to right. And that
when we find an intersection, we need to process that intersection
(remove the arc from the beachline, record a Voronoi vertex.) But when
in the sequence of points can we do that? You might think, as soon as we
know those 3 points, we can calculate the circle and therefore the
intersection. But that won’t work. To see why, consider the following
scenario. The first picture is the same scenario we saw before, and you
might think you can go ahead and assume that the edges will converge.
But wait! There is another site, and the sweep line hits it
before the edges converge, breaking up the arc before it reaches the
intersection point.

The important thing is that the new site is inside the
circle
. And that is the answer to the question: You process the
edge intersection when the sweepline leaves the circle without having
encountered any sites inside the circle. The point where the sweepline
leaves the circle is the right-most point of the circle – called the
circle point.

Until now we’ve been sequentially processing site events.
Now we need to add this second type of event, called a circle
event
for obvious reasons. Every time we change the beachline,
we need to check for new circle events, and check existing circle events
to see if they are still valid. This is probably the hardest concept in
the algorithm to grok, so I’m going to spend some time on it, before
talking about what happens when our sweepline passes a circle point.

Say you have a beachline containing the three arcs
[..,i,j,k,..]. Because arcs are references to sites
(remember, an ‘arc’ in our datamodel is just a 2d point, with each 2d
point being a site.) i,j and k in addition to being
arcs are also sites. The significance of them being next to eachother in
the beachline is that the equiedges between i and j, and j and k,
could intercept at the center of the circumcircle they
describe, becoming a Voronoi Vertex. This is what a circle event really
is: A reference to a triple of arcs on the beachline (not every
triple on the beachline, but we’ll park that for now).

If there is a change to the beachline affecting that triple before
those lines intersect, then there is no intersection. When I say
‘change’, I mean either a new arc added to the beachline by a site
parameter or one has been removed. In the case of an addition, the
beachline becomes [..,i,j,L,j,k,..]. The triple
i,j,k has gone – and so has any circle event associated with
it. And there are two new triples i,j,L and L,j,k. So
we need to check both of those to see if they create generate circle
events. That’s what happened in the following picture: the arc which is
being ‘squeezed’ is actually being split by the arc created by the new
site before the squeeze happens.

Incomplete edges

A couple of sections ago I started adding those orange lines to the
diagram to indicate the equiedges, but I didn’t talk about what they
were exactly. They ultimately become the (half) edges of the Voronoi.
But they start out as incomplete edges. These are edges
where one end is a fixed point, but the other is the intersection of two
arcs. That intersection isn’t a fixed point, but changes depending on
where the sweep line is. Hence why it’s incomplete: because one of the
ends isn’t fixed yet.

We create two new incomplete edges whenever we put a new arc on the
beachline. The fixed point of both is the coordinate where the new arc
intersected the old one. And the arc-intersection are the two
intersections between the new arc and the one that it splits. If the new
arc is j and the old one is i, the arc-intersections
are [i,j] and [j,i].

8.
A negative determinant means a counterclockwise orientation (and so a
circle event). A positive one means a clockwise one (and so no circle
event). A determinant of zero means the points are in a straight line
(no circle!)

Summary

OK, so we have everything we need to do the algorithm now. To
summarize the whole thing:

  • Given a set of sites put all the site point in a queue, as ‘site’
    events, ordered by the x-value.
  • While the queue is not empty, pop the next event off the queue.
  • If the popped event is a site-type, do the following:
    • Go through all circle events that are ahead of it in the queue, and
      determine if this site is inside the circle. If so, remove that circle
      event from the queue.
    • Find the arc directly across from the site. Let that be j
      in the beachline [..,i,j,k,..]
    • insert the arc defined by that point (L) into the beachline
      ‘splitting’ j, so it becomes [..,i,j,L,j,k,..].
    • add two imcomplete edges, both with the fixed point at the point the
      new arc intersected the beachline, and the arc-intersections of the
      split arc vs. new arc, and new arc vs. split arc.
    • Check the new triples i,j,L and L,j,k to see if
      they form new circle events, and if so insert them in the event
      queue.
  • If the popped event is a circle-type do the following
    • Find the center of the circle, and add it as a vertex to the Voronoi
      diagram.
    • Find the arc that is being squeezed out of existance, let it be
      j in [..,h,i,j,k,l,..]
    • Go through all circle events that are ahead of it, and check if the
      removed arc invalidates the event.
    • remove it from the beachline, leaving
      [..,h,i,k,l,..].
    • Check the new triples h,i,k and i,k,l to see if
      they form new circle events, and if so insert them in the event
      queue.
  • When the queue is empty, go through all remaining incomplete edges,
    and if they are within the bounds of the diagram, extend them to the
    diagram side, creating Voronoi Vertices at the point the hit the
    side

Implementing Fortune’s
Algorithm

Datastructure

A bit of housekeeping: a V2 is a [2]int. A
PointPair is a pair of V2s, i.e. a
[2][2]int.

An Event is a struct of {site:bool, a,b,c:V2}. The
boolean indicates what type of event it is: a site (true) or circle
(false). The three V2s have different semantics depending
on the event. If it’s a site, then a is just the
coordinates of the site, and the other two are unused. If it’s a circle,
then the three V2’s are the arcs in the beachline that generated the
event. Or put another way, the three sites that site on the perimeter of
the the circle.

For the algorithm itself, we need to store the following things:

  • The queue is an array of Events. Technically it’s
    better to have this as a priority queue, but it’s not really important
    to the implementation of the algorithm.
  • The beachline is an array of V2s. Again,
    for efficiency you this implement this as a binary tree.
  • incomplete_edges is a dictionary/map of
    PointPair->V2

The algorithm

This is the code, written in the wonderful C alternative Odin. The comments should hopefully
explain what is being done and why where it’s not clear from the
code9.

Leave a Reply