Skip to content

Partial Occlusion Field-of-View

by pixelpracht on August 5th, 2012

It seems like there are problems that I have to solve over and over again. Pathfinding is one of them. Another is visibility determination.

When you need to know if there’s an unobstructed line of sight between point A and point B you typically just cast a ray from A to B and see if it hits something. The set of all points that can be reached with an unobstructed line from A are referred to as A’s Field of View. Calculating it accurately would require an unlimited amount of rays, so you typically sacrifice accuracy by partitioning space in some discrete data structure. Grids, Trees, Pixels, Voxels… that kind of thing.

In this post I’ll focus on FoV calculations on square grids. Using a low resolution square grid is a pretty huge abstraction, and the whole point of this article is to show how to get some accuracy back without losing too much performance! :) But let’s start simple…

Calculate Field of View with Raycasting

As long as you only care for a Boolean result (is a square B visible from square A in the center) it’s straight forward to calculate using raycasting.
First of all consider how many rays you need: Just enough so that every field that could potentially be part of the Field of View is passed by at least one ray. So better define a max range or you would still have to cast an unlimited number of rays despite working on a low resolution square grid. Now select fields at maximum range so that those fields completely enclose your center. Draw lines to them and all closer fields are automatically tested too!

Calculate FoV by raycasting

Raycasting only towards fields at max-range reduces the algorithms complexity from O(n³) to O(n²). Thtat’s a huge difference! Imagine how the illustration would look like if all cells would be target of  a raycast…

For line-drawing you can just use Bresenham’s Line Algorithm. All clear fields covered by the line are part of the field of view. As soon as you hit an opaque field abort the raycast. It’s not part of the field of view and all other fields that follow along the direction of the ray are occluded by it.

There’s room for improvement

When I tried to use the raycasting approach to implement dynamic lighting for an isometric 2D game engine I found that I would have to find an improved algorithm to make it look great.

Consider an opaque field casting a shadow. A lot of fields will neither be fully lit nor fully in shadow. From light source’s point of view those fields are partially occluded! So I wanted my fields to specify their membership in the Field of View as a percentage (0 – 100%) and not just a Boolean (true or false).

The other limitation I hoped to overcome were integer coordinates. It didn’t suffice to say a light-source is on Field (x,y) – I wanted to specify the exact position on the field the light would come from. So I needed to be able to precisely define the center of my Field of View by using non-integer coordinates.

The Mission

I hope I managed to convey an idea of what problem the algorithm I’m about to present is trying to solve. To be sure let’s sum up the situation:

  • We operate on a square grid that partitions a 2D plane in equally sized fields.
  • Some squares are clear (will allow light to pass through) while others are opaque (will block light).
  • We define a point for which we want to calculate the Field of View.
  • We want to know the degree of occlusion for each square closer then the max range.

My research didn’t yield any established solution so I accepted the challenge to find a new one (or reinvent the wheel, who knows). Here’s a demonstration of my solution in action:

An interactive Flash Demonstration of Raycasting and Partial Occlusion based FoV

The full source of the demo is available under the MIT license. However, deducting an algorithm from poorly documented AS3 source code might be a little inconvenient if you want to understand the principles behind it. So I conclude the post with a explanation of the algorithm.

Partial Occlusion Field of View

It helps to think of center of the Field of View as a light source from which light is spreading equally in all directions. The spreading light will exit a square on the edges facing away from the light source and enter a neighbouring square. So for each square you can clearly identify one or two neighbouring cells that all the light is coming from.

Now all you have to do to know how much light is going through a particular square is to ask the relevant neighbours to describe their contribution. If a neighbouring square is opaque it will contribute no light. Otherwise it will contribute a portion of the light it itself received – which can be anything between zero and the width of the shared edge.

Circular sectors to represent beams of light

A simple way to express the “portion of light” is a circular sector. With the center known we just have to store two angles to define such a sector.

Two angle's are enough to describe light contribution as a circular sector.

This illustration shows how the light passing between two occluders can be described by specifying two angles: Alpha and Theta.

Due to the nature of the grid one sector (e.g. two angles) per neighbour is enough to describe it’s contribution! This is because occluders are always one square in size and thus the shadows they cast can’t be smaller then one square. With this method of describing lightbeams it’s easy to form unions and intersections of lightbeams, too. Unions, when we want to combine the light coming from two different neighbouring squares and Intersections when we constrain the result to the size of our current square.

The Algorithm

The algorithm can be summed up as follows:

The following illustration demonstrates the process on a square that happens to be half occluded. Only one neighbour contributes light, while the other is opaque.

This animated illustration shows how to calculate the degree of occlusion of a field based on two relevant neighbours.

This animated illustration shows how to calculate the occlusion of a square based on the occlusion of the two of it’s neighbours closer to the center.

Calculating a Squares circular Sector

But how to calculate a circular sector that describes the maximum width light beam that could go through a particular square (labeled ‘C’ in the above illustration)?
To find it you simply calculate alpha for all four corners of the square and pick the largest and the smallest angle to form the circular sector. The resulting sector will enclose the full square.
Two of the corners of each square define a circular sector that describes the maximum amount of light this square can receive.
The illustration shows that there’s a distinct pattern in what corners you would have to pick. So you don’t have to calculate four values and then discard two. Instead you can evaluate the sign of the x and y component of the vector pointing from lightsource to the square and to predict the correct ones. And if you’re going through the sourcecode of my reference implementation and notice these weird looking lines now you know what they do! dx and dy describe the position of the square in relation to the center and (x1, y1) and (x2, y2) are the corner’s that define the sector.

Recursion vs Iteration

Another difficulty worth mentioning is that to calculate a particular square’s occlusion the occlusion of it’s relevant neighbours must be known first. How can we make sure they are already calculated?

The obvious approach would be to use recursion, which means that whenever we query the sector of a cell that hasn’t yet been evaluated it’s sector will be calculated before the query returns the correct value. To calculate the FoV (e.g. the degree of occlusion of all squares within max-range of the center) using recursion, trigger the evaluation in the four corners of the bounding rectangle. All squares enclosed by the rectangle will be visited before the calls return. There’s just no other way for the recursion to terminate than by reaching the center tile for which the occlusion is always known. This is when the evaluation-method ceases to calling itself on neighboring cells and begins to return values. It’s a good idea to implement it so that each cell is only calculated the first time it is queried and cache the result. That way subsequent queries don’t incur an unnecessary reevaluation.

There’s nothing wrong with that approach as long the stack space suffices. But if this could be a problem it’s also possible to devise an iterator pattern that will visit squares in such an order that all relevant neighbours have already been calculated.

It's possible to iterate over the grid, starting from the center, that squares are calculated in just the right order.

The demonstration I’ve linked implements both the recursive and the iterator based version of the algorithm as well as some raycasting based FoV as a reference. Here’s the source code of the demo! It’s released under the MIT License and includes all relevant dependencies. I hope you enjoyed the lengthy read, feel free to post questions or comments! :)

  1. Keng permalink

    So cool, thanks so much for sharing :)

  2. This is the best raycasting tutorial in AS3 on the Internet. Thanks!

  3. eivuokko permalink

    Great demonstraction, well written.

  4. Kovsky permalink

    May I ask you something? I’m still trying to figure out how the raycast algorithm works, and I’m trying to make your source code to work on a test of mine.

    The problem is that it gets a little slow on a iphone4 and… I dont know if I did all right. I’ve made a png file with 100 frames, with a black tile that changes from transparent to opaque.

    With this I’m using the copyPixels method of BitmapData to draw black tiles where’s shadow to draw.

    Do you know if there’s a faster way to do this and make the app runnable on a iphone 4?

    Really, thanks. The code looks clear, but I’m a little slow with math.

  5. pixelpracht permalink

    I haven’t worked witht Flash on the iPhone and so it’s really hard to give tips. Generally you’ll want to look for a method that is GPU accelerated and copyPixels might not be.

    You could consider to actually place a sprite for each tile and assign different bitmapData when their states change.

    You could use the same graphic for all tiles and use colorTransform to shade them. Maybe that’s faster then swapping the frames.

    Or you update only the regions that change.

    Or you might want to consider to apply the lighting in a 2nd pass. If you just want to darken the tiles you could make a lightbuffer and apply it with blendmode multiply. Make it small, just one pixel per tile, then use nearest neighbour filtering and upscale it by the size of a tile (eg. 32×32)

    Did you check this guide on mobile performance optimizations by adobe?

    Good luck!

  6. Kovsky permalink

    It helped me a lot! Truly, the use of the Starling framework saves a lot of cpu cicles. The game runs at 35-40 fps on an iphone4, but I’ve also set the FOV to work with bigger shadow tiles.

    I’m still looking to save more fps, but your code is very well optmized, at least for my standards.

  7. Kovsky permalink

    May I ask you a last thing? What you recomend to read to understand better what`s going on with the raycasting-light? Something more for beginners?

  8. Tony permalink

    Great technique!

    However, I did not quite catch the animated illustration, it says (AuB)nC provides the lit area, but wouldn’t that return the same area as C in this case?

    I thought the lit area would be expressed by AnC ?

    Also, you wrote:

    1: Identify all neighbours that are not opaque
    AND share an edge AND are closer to the center.

    But in the illustration, the tile chosen for area A is in fact opaque…

    I am a bit confused… :S

  9. Jun permalink

    Thanks for nice tutorial.

    If you don’t mind, I’d like to ask something.
    I wanted to light walls only they’re in the fov at first.
    so, I made my one using your source code.

    but I realized that partial occlusion fov doesn’t support the diagonal view.
    and it makes the corner walls always opaque.
    is there any good way to solve this problem?

Leave a Reply


Subscribe to this comment feed via RSS