Author Topic: Shadowcast Java algorithm  (Read 21445 times)

Woog

  • Newcomer
  • Posts: 3
  • Karma: +0/-0
    • View Profile
    • Email
Shadowcast Java algorithm
« on: November 13, 2014, 05:10:32 PM »
Hello everyone!

I'm currently trying to implement this FOV algorithm (The Java one) (www.roguebasin.com/index.php?title=FOV_using_recursive_shadowcasting_-_improved) into my roguelike but I'm having some trouble figuring out what the values are supposed to be.

Right now it's the actor who has these methods to determine what they can see and here's what I've got.

Code: [Select]
public void playerFOV_Recursive(){

for(int xd = -1; xd < 2; xd++){
for(int yd = -1; yd < 2; yd++){
if(yd != 0 && xd != 0){
castLight(1, 1.0f, 0.0f, 0, xd, yd, 0);
                castLight(1, 1.0f, 0.0f, xd, 0, 0, yd);
}
}
}
for(int xv = 0; xv < 63; xv++){
for(int yv = 0; yv < 63; yv++){
if(lightMap[xv][yv] >= 1.0){
getCurrentLevel().getMapArray(xv,yv).visible = true;
}
}
}
}
public void castLight(int row, float start, float end, int xx, int xy, int yx, int yy){
float newStart = 0.0f;
float radius = 100;
    if (start < end) {
        return;
    }
    boolean blocked = false;
    for (int distance = row; distance <= radius && !blocked; distance++) {
        int deltaY = -distance;
        for (int deltaX = -distance; deltaX <= 0; deltaX++) {
            int currentX = getPosX() + deltaX * xx + deltaY * xy;
            int currentY = getPosY() + deltaX * yx + deltaY * yy;
            float leftSlope = (deltaX - 0.5f) / (deltaY + 0.5f);
            float rightSlope = (deltaX + 0.5f) / (deltaY - 0.5f);

            if (!(currentX >= 0 && currentY >= 0 && currentX < 63 && currentY < 63) || start < rightSlope) {
                continue;
            } else if (end > leftSlope) {
                break;
            }

            //check if it's within the lightable area and light if needed
            if ((deltaX + deltaY) <= radius) {
                float bright = (float) (1 - ((deltaX + deltaY) / radius));
                lightMap[currentX][currentY] = bright;
               
            }

            if (blocked) { //previous cell was a blocking one
                if (lightMap[currentX][currentY] >= 1) {//hit a wall
                    newStart = rightSlope;
                    continue;
                } else {
                    blocked = false;
                    start = newStart;
                }
            } else {
                if (lightMap[currentX][currentY] >= 1 && distance < radius) {//hit a wall within sight line
                    blocked = true;
                    castLight(distance + 1, start, leftSlope, xx, xy, yx, yy);
                    newStart = rightSlope;
                }
            }
        }
    }
}

Right now it only lights the tiles next to the player.
Basically I'm not sure what startx,starty was supposed to be or if I adapted things incorrectly. Any help is much appreciated.

Woog

  • Newcomer
  • Posts: 3
  • Karma: +0/-0
    • View Profile
    • Email
Re: Shadowcast Java algorithm
« Reply #1 on: November 13, 2014, 09:26:31 PM »
Update: I've got it mostly working but I still have a problem with corners like this

@ = player
. = floor
# = wall
S = Shadow

.@.
##.
##.
##S


The player can't see 'S' but 'S' can see the player

Eben

  • Rogueliker
  • ***
  • Posts: 339
  • Karma: +0/-0
  • Controversializer
    • View Profile
    • SquidPony!
Re: Shadowcast Java algorithm
« Reply #2 on: February 10, 2015, 03:45:14 AM »
The problem is that shadowcasting isn't symmetric. Which means that not every tile that can be seen from a given space will necessarily be able to see that given space in return.

This lack of symmetry is pretty common in most FOV systems. I've found that shadowcasting is performant enough that if you have a dungeon without changing walls, you can run it for every single tile in the dungeon and take the max lit value from pairs of tiles. So for any two tiles if even one can see the other, they're both marked as visible to that one.

This strategy does take up quite a bit of memory, but for normal rogue sized dungeons it's not a big deal.



This post is old, but since it was referring to my code I figured I should mention what's going on.

Omnivore

  • Rogueliker
  • ***
  • Posts: 154
  • Karma: +0/-0
    • View Profile
Re: Shadowcast Java algorithm
« Reply #3 on: February 10, 2015, 04:27:41 AM »
Another option to get around the lack of symmetry with any FoV algorithm and that works with destructible/modifiable terrain quite well is to calculate the player's FoV using the maximum sight distance of all the mobs on the level and the player.  If the player's sight distance is shorter than the max, a fast calculation of the player's actual FoV can be done by iterating through the lit cells in the main FoV and checking the distance.  Likewise for mobs within the main FoV, whose sight distance is shorter than max, do a distance check to determine if they can see the player. 

If your game requires that the mob ai take into account other mobs that are visible, instead of doing a full FoV, just do an axis aligned bounding box check (simple point in rect test) and if in the box, do a distance check, and then if in range, check the direct los using the supercover line algorithm to check each intervening square. 
« Last Edit: March 25, 2015, 12:12:42 PM by Omnivore »

Eben

  • Rogueliker
  • ***
  • Posts: 339
  • Karma: +0/-0
  • Controversializer
    • View Profile
    • SquidPony!
Re: Shadowcast Java algorithm
« Reply #4 on: February 11, 2015, 05:21:29 AM »
Another option to get around the lack of symmetry with any FoV algorithm and that works with destructible/modifiable terrain quite well is to calculate the player's FoV using the maximum sight distance of all the mobs on the level and the player.  If the player's sight distance is shorter than the max, a fast calculation of the player's actual FoV can be done by iterating through the lit cells in the main FoV and checking the distance.  Likewise for mobs within the main FoV, whose sight distance is shorter than max, do a distance check to determine if they can see the player. 

If your game requires that the mob ai take into account other mobs that are visible, instead of doing a full FoV, just do an axis aligned bounding box check (simple point in rect test) and if in the box, do a distance check, and then if in range, check the direct los using bresenham, anti-aliasing or supercover line algorithm to check each intervening square.

This is definitely the way to go if space is limited and/or during-play calculation speed isn't a problem (which it probably wouldn't be).

Where's the button to give Omnivore win points? :)

stefoid

  • Newcomer
  • Posts: 46
  • Karma: +0/-0
    • View Profile
    • Email
Re: Shadowcast Java algorithm
« Reply #5 on: March 23, 2015, 10:14:36 PM »
dungeonbash uses quite a small number of tiles in its viewport: 11x11.  5 tile radius

To get around the symmetry problem, it (pre) calculates rays for a 15 tile radius viewport - this effectively increases the 'resolution' of the rays within the inner 5 tile viewport.

i.e. for a 5 tile radius viewport,you are tracing from the centre of a square out to 11 rays on each side of the square = 44 rays, more or less.

For a 15 tile radius viewport, I am tracing from the center of a square out to 31 rays on each side of a square = 121 rays.  BUT, I dont need to go all the way to the edge because I am only interested in a LOS radius of 5 tiles, so once I get to edge of the inner 5 tile radius, I stop tracing along that ray even if I havent hit anything.


Since I am tracing 144 rays within that 5 tile radius inner square, the 'resolution' is quite high and you get perfect symmetry and can see into corners without any special code.

This results in a fairly forgiving FOV, because the resolution of the rays is quite high, but I dont mind that for dungeonbash.
My squad-based tactical roguelike for mobiles:
http://www.dungeonbash.com

Tilded

  • Newcomer
  • Posts: 49
  • Karma: +0/-0
    • View Profile
Re: Shadowcast Java algorithm
« Reply #6 on: March 25, 2015, 02:08:59 AM »
If you want perfect symmetry with recursive shadow casting, you can light a tile based on where the light ray intersects it. If the center of a tile is in the shadow, keep it dark, and if the center is in the light, light it. This ensures that any two tiles can see each other if there is a LOS between the centers of their tiles.

Omnivore

  • Rogueliker
  • ***
  • Posts: 154
  • Karma: +0/-0
    • View Profile
Re: Shadowcast Java algorithm
« Reply #7 on: March 25, 2015, 11:54:24 AM »
Or just realize that all FOV algorithms are premature optimizations of the brute force 'draw a line from origin to every point within the AABB' algorithm.  Oh, and if you're concerned with symmetry and wall lighting artifacts are undesirable, do *not* use Bresenham's line drawing algorithm, for what you are interested in (line of sight) use the 'supercover' variant.

[For TL;DR types, you can stop here.]

The advantage of just doing brute force is that it satisfies your criteria of symmetry, it does not have artifacts, and it is very simple to implement.

If your game runs slow, profile it, if the results show that FOV calculation is the bottleneck, then look at ways to speed it up.  The best optimization is to not perform unnecessary FOV calcs in the first place*.   If you still have a performance issue, then consider the following:

None of the 'improved' algorithms in common use fulfill the same contract, they relax the constraints, they no longer satisfy the symmetry invariant (and incidentally the wall artifact issue is part of this).  That is, the improved field of view algorithms in common use  solve a different problem.  If the relaxed constraints are satisfactory for your use, fine, implement, profile, and you are most likely done.

If you do not want to solve a different problem, if you want to solve the original problem but with fewer calculations, and in less time, then consider classic routes to performance improvement, a memory for speed trade off.  Use memoization to reduce the cost of map queries if that is a bottleneck.  For a given order of evaluation, you can get the same end results without using any line drawing or any calculation besides simple point translation and perhaps a distance check, by precalculating the effect each individual cell position has on visibility within the bounds (effectively memoizing the 'cast_ray' function with an origin of {0,0}).  You can use quadrant, or even octant, folding to reduce the memory requirements.** 

Or, as Tilded noted above, you may be able to change the math of one of the common algorithms to achieve the same end result.  For reasonable values of vision radius, I doubt you will see much difference in final runtime performance on modern desktop hardware.  For mobile environments, try both and profile.

Irregardless of which algorithm you end up using, you may still be unsatisfied with the result in certain situations like, visibility at corners or with diagonal gaps.  Consider evaluating the immediate surroundings (the neighbor cells) to detect those cases and doing a second, composite, FOV calc from the appropriate neighboring cell (perhaps of shorter range) to obtain a more satisfactory result.

I'll say it again.  Do not use Bresenham's line algorithm for line of sight purposes, regardless of how many clueless idiots out there immediately recommend it as the 'obvious' solution.  Use a 'supercover' variant which checks each and every cell the line actually passes through.  Having been one of those clueless idiots in the past, and likely will be again in regards to other topics, no offense is intended.


*There is no reason (except if you have memory and cycles to throw away) to do full blown FOV calcs for anything other than the primary viewpoint.  There are simpler solutions, even if you need to use a quadtree to reduce working sets.

**There is another step involved where you realize that you don't need to cast the lines (even in memoized form) at all.  As long as you are walking the space in a 'closest first' manner, you only need to answer the question, "What cells are blocked by the current cell that the map query tells me is blocking further vision?".  Solving for that answer reduces the order of the algorithm.
« Last Edit: March 25, 2015, 12:25:14 PM by Omnivore »

Krice

  • (Banned)
  • Rogueliker
  • ***
  • Posts: 2316
  • Karma: +0/-2
    • View Profile
    • Email
Re: Shadowcast Java algorithm
« Reply #8 on: March 25, 2015, 05:20:33 PM »
I'll say it again.  Do not use Bresenham's line algorithm for line of sight purposes, regardless of how many clueless idiots out there immediately recommend it as the 'obvious' solution.  Use a 'supercover' variant which checks each and every cell the line actually passes through.

Well you could argue that bresenham also checks all tiles it's passing through, it's just a thinner line. More importantly that supercover routine will absolutely not fix the typical 'slope' problem of line drawing in FOV routines. So it depends on the FOV routine and my guess is that supercover will not give expected result in some FOV routines.

Omnivore

  • Rogueliker
  • ***
  • Posts: 154
  • Karma: +0/-0
    • View Profile
Re: Shadowcast Java algorithm
« Reply #9 on: March 25, 2015, 05:26:22 PM »

This algorithm == full supercover.  Note the difference.

mushroom patch

  • Rogueliker
  • ***
  • Posts: 554
  • Karma: +0/-0
    • View Profile
Re: Shadowcast Java algorithm
« Reply #10 on: March 25, 2015, 05:53:35 PM »
I'll say it again.  Do not use Bresenham's line algorithm for line of sight purposes, regardless of how many clueless idiots out there immediately recommend it as the 'obvious' solution.  Use a 'supercover' variant which checks each and every cell the line actually passes through.

Well you could argue that bresenham also checks all tiles it's passing through, it's just a thinner line. More importantly that supercover routine will absolutely not fix the typical 'slope' problem of line drawing in FOV routines. So it depends on the FOV routine and my guess is that supercover will not give expected result in some FOV routines.

Quoted without comment.

Krice

  • (Banned)
  • Rogueliker
  • ***
  • Posts: 2316
  • Karma: +0/-2
    • View Profile
    • Email
Re: Shadowcast Java algorithm
« Reply #11 on: March 26, 2015, 01:10:24 PM »
I tried the supercover line just for fun. As I expected it has slope bugs, but less of them of course than with bresenham. It's surprisingly good, but didn't survive the "cross test" (see: image link below). It's also quite revealing, showing perhaps more than you would expect so that you can see in places you wouldn't expect to see.

http://koti.mbnet.fi/paulkp/temp/fov.png

The left, bottom and right (upper part) are walls which are not revealed because the slope problem. Also, with supercover you have to first clear the fov area with "empty" values. With bresenham (and fix routine after that) you can do a trick where the line drawing is covering up the entire area of FOV, by allowing the line drawing continue with empty value after wall (etc.) has been hit.

Omnivore

  • Rogueliker
  • ***
  • Posts: 154
  • Karma: +0/-0
    • View Profile
Re: Shadowcast Java algorithm
« Reply #12 on: March 26, 2015, 02:01:10 PM »
What you are calling slope bugs are not bugs.  At least, not if your invariant is symmetry.  The post process cleanup of the Jice's Basic perimeter Bresenham line 'ray-cast' algorithm actually introduces symmetry bugs when it cleans up the artifacts of gaps in wall lighting.  It may or may not 'look better' to you, but that's an opinion not fact and certainly not a bug.

That said, you're also confusing Field of View with Line of Sight.  While you can construct a Field of View using Line of Sight algorithms applied to each and every possible point, that is, in practice, rarely done.  Today's FOV algorithms are, for the most part, built upon the foundation of algorithms designed for the 80386-80486 or even earlier processors and machines with less than 1MB of memory.  Now the constraints they were designed to fit may still exist for some mobile platforms, though likely even there but assuredly on today's laptops and desktops those constraints do not exist.  Thus nearly all of them provide a solution that is an approximation of the ideal and introduce various artifacts and non-symmetrical results.

So no, simply substituting a supercover variant for a Bresenham line used in an approximation of field of view calculation such as Jice's Basic and similar, is not going to give you much of an improvement, or even much of a difference once post processing is applied. 

Look its simple, supercover is restrictive but consistent, pure Bresenham's is less restrictive but is inconsistent. 

As for that 'trick' ..... you do realize you're losing anything you gain by not doing a simple clear operation on the underlying container (which is most likely optimized by the library you are using), due to writing the same cells multiple times?  Not to mention the unnecessary calculations involved in calculating those partially overlapping lines?

« Last Edit: March 26, 2015, 03:49:53 PM by Omnivore »

Woog

  • Newcomer
  • Posts: 3
  • Karma: +0/-0
    • View Profile
    • Email
Re: Shadowcast Java algorithm
« Reply #13 on: March 27, 2015, 04:48:06 AM »
Wow didn't expect such heated debate about FOV algorithms.

Anyway I am just using something like this now.

http://i.imgur.com/3HuRUn0.png

It works and is symmetrical, although I'm not sure how fast it is. At least straight lines are easy to figure out. I'll try some of these suggestions and see how it works.

Krice

  • (Banned)
  • Rogueliker
  • ***
  • Posts: 2316
  • Karma: +0/-2
    • View Profile
    • Email
Re: Shadowcast Java algorithm
« Reply #14 on: March 27, 2015, 07:55:26 AM »
What you are calling slope bugs are not bugs.

Yes, they are a feature of line drawing and checking when the line hits an obstacle. I don't know how these complex routines are using line drawing in context of FOV, but whatever it may be it's for nothing. You get equal results just by shooting rays all around the player and then fixing the bugs or features where the line doesn't reach something which should be visible. No complex multi-threading routines needed. Just shoot rays, then fix the rest. Isn't that what you have to do with the complex routines as well, since the damn line drawing can't reach those places anyway?
« Last Edit: March 27, 2015, 07:57:07 AM by Krice »