Monday, March 12, 2007

2D portal visibility, Part 1

Many games feature a map that starts out empty and fills in as the player's character moves through the world. There are lots of algorithms for determining what portion of the level is visible from a given vantage point. This article presents one of my favorites for two-dimensional environments.

The world is made up of connected, convex polygonal regions through which sightlines can travel. For grid-based games a natural choice for the regions is the squares of the grid.

The basic idea is simple: Start with a clear view frustum, in the region containing the viewer. At each region, if it is solid it blocks the view. Otherwise, clip the view frustum into bits that pass through each of the region's edges, and recurse to the regions that touch those edges. Pass the clipped frustum into them, so they know what portion of the view they have to concern themselves with. In pseudocode it looks like this:

compute_visibility(frustum, region): mark region as visible if region is not solid: for each edge of region: clip frustum to the portion exiting through that edge if clipped frustum is valid: compute_visibility(clipped frustum, region beyond edge)

One advantage of this algorithm over raycasting, another common algorithm, is that it does not have aliasing problems. An example you can see in some Roguelikes is when you stand close to a long straight wall and it is not continuously visible all the way down, but has invisible spots along the way. This is because the rays only partially sample the visibility, so they may miss spots. Another example is when you look through a doorway and see spots in the room beyond but the spots are not connected together into a continuous whole.

To crop the view frustum down as the recursion proceeds the algorithm makes use of what is sometimes called the “perp-dot product,” so called because it's a dot product with the second argument turned 90 degrees counterclockwise. It is also the Z component of the vector cross product, if you expand each 2D vector (x, y) to a 3D vector (x, y, 0).

The formula for the perp-dot product is:

perp_dot(a, b) = a.x * b.y - a.y * b.x

If perp_dot(A, B) is greater than zero then the vector A lies on the right when you are standing at the origin looking in the direction of vector B. If it's less than zero then vector A lies to the left. If it is equal to zero then vector A may be pointing either in the same direction as vector B, or in the opposite direction of vector B.

This algorithm is currently written to mark solid squares that terminate the recursion as visible, as is typical for Roguelikes. However, the solid square diagonally adjacent to an inside corner will not be reached by the portal algorithm, since the recursion terminates at the other two solid squares neighboring the inside corner. This results in displays like this:

#####.....#.....#.....#

If the solid squares are regarded as being completely solid, this is technically correct: from inside the room, we cannot see which of these two configurations is correct:

If diagonally-adjacent inside corners like option #2 are disallowed, then you could fill in the corners with a post-process that identifies corner situations. On the other hand, if a situation like #2 means the player can move through the diagonal, then the squares on either side are not actually completely solid, and the algorithm would need to be reworked in a different way. You can subdivide squares into sub-pieces that are solid or empty depending on the configuration of neighboring squares, for example. I will present my implementation of this in a future post.

The coordinate system used for portal endpoint offsets is twice the resolution of the map grid, to be able to represent the viewer's position as being at the center of a grid square. This is why dx and dy are multiplied by 2. The offsets to the corners of the squares are then +/- 1 in each direction.

Recursion depth is equal to the maximum line of sight. You may wish to impose a distance cutoff if your world is large.

There is a major subtlety at startup. This implementation starts with the viewer and target in the same square, in order to avoid duplicate code. However, in this situation there is a potential ambiguity. For example, let's say our current view frustum is out the right-hand edge. Its left endpoint is at (1, 1) relative to the viewer, and its right endpoint is at (1, -1). When we clip the opposite portal out of the square against the view frustum we have a potential problem. The portal's left corner is (-1, -1), and its right corner is (-1, 1). The algorithm clips the left edge of the frustum by taking the rightmost of (1, 1) and (-1, -1). But these lie on a parallel line, so neither is rightmost. The same is true for the right edges of the frustum and portal. If in the case of ties we take either both view frustum edges or both portal edges, then we end up with what looks like a valid clipped frustum, even though it's not. This would cause us to recurse in the direction opposite the view frustum.

To get around this problem, the algorithm above breaks ties differently for the left and right edges. One of them gets the view frustum edge, and the other gets the portal edge. For normal cases this doesn't change anything, since it doesn't matter which one we use to define the view frustum boundary. In the startup case, though, it ensures that the resulting portal will be invalid (because its right edge will be left of its left edge).

Next Monday, I hope to go into some of the ways in which this algorithm can be enhanced.

2 comments:

Anonymous
said...

I have a different problem. I want to store a 3d terrain map modeled using triangles.I want to load only part of the map as user moves through the world.Do you have any idea how to store a big map(thousand or millions of traingle) but index very small part of the world map faster as user roams the world. I dont need any leve of details algorithms but just efficent storage and fast indexing an arbitrary part of the world.