Shadow casting is weird like that... It's a tradeoff between accuracy and visual appeal.
You may have better luck with restrictive precise angle shadow casting. Java, but I am sure you can adapt it to the language of your choice. This code is not thread-safe (unless of course you use a different instance in each thread).
Don't forget to post-process to make visible any walls adjacent to any visible floors.
/*
* Copyright (c) 2011, L. Adamson / Dizzy Dragon Games. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* We'd love to hear from you if you're using this software for something or
* have any questions or comments. You can contact us at via email at
* support@dizzydragon.net.
*/
package retrocrawl.engine.game.visionandmovement;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
abstract public class FieldOfView2dRpas extends FieldOfView2d
{
private static final long serialVersionUID = 1L;
// This value can be 1, 2, or 3. It indicates the number of angles that
// must be blocked before a cell is considered opaque.
final private static int PERMISSIVENESS = 2;
// This value spreads the start and end angles a little wider than usual.
// This acts somewhat like permissiveness, but is a more fine-grained
// control. It helps to eliminate some visual artifacts.
final private static float OUTER_ANGLE_SWEEP = 0.2f;
final private static class AngleRange
{
private float startAngle;
private float endAngle;
private AngleRange( float startAngle, float endAngle )
{
this.startAngle = startAngle;
this.endAngle = endAngle;
}
}
protected FieldOfView2dRpas()
{
super( true );
}
final private ArrayList<AngleRange> blockedAngles = new ArrayList<AngleRange>();
final private boolean isBlockedAngle( float angle )
{
for( AngleRange ar : blockedAngles )
{
if( angle >= ar.startAngle && angle <= ar.endAngle )
{
return true;
}
}
return false;
}
@Override
final public Set<FieldOfView2dCoordinate> getFieldOfViewFromImpl( FieldOfView2dCoordinate origin, int radius )
{
HashSet<FieldOfView2dCoordinate> results = new HashSet<FieldOfView2dCoordinate>();
results.add( origin );
for( boolean flip = false; ; flip = true )
{
for( int xdir = -1; xdir <= 1; xdir += 2 )
{
for( int ydir = -1; ydir <= 1; ydir += 2 )
{
blockedAngles.clear();
boolean lineOpen = true;
for( int line = 1; line <= radius && lineOpen; line++ )
{
lineOpen = false;
for( int cell = 0; cell <= line; cell++ )
{
int x;
int y;
if( flip )
{
x = line*xdir;
y = cell*ydir;
}
else
{
x = cell*xdir;
y = line*ydir;
}
float range = 1.0f/(float)(line+1);
float startAngle = range * (float)(cell);
float midAngle = startAngle + range*0.5f;
float endAngle = startAngle + range;
startAngle -= range * OUTER_ANGLE_SWEEP;
endAngle += range * OUTER_ANGLE_SWEEP;
int nBlockedAngles = 0;
if( isBlockedAngle(startAngle) )
nBlockedAngles++;
if( isBlockedAngle(midAngle) )
nBlockedAngles++;
if( isBlockedAngle(endAngle) )
nBlockedAngles++;
FieldOfView2dCoordinate coordinate = new FieldOfView2dCoordinate(origin.x()+x, origin.y()+y);
if( nBlockedAngles < PERMISSIVENESS )
{
results.add( coordinate );
lineOpen = true;
}
if( cellIsOpaque(coordinate) )
blockedAngles.add( new AngleRange(startAngle,endAngle) );
}
}
}
}
if( flip )
break;
}
return results;
}
}
(This parent class just does some caching and the post-process filtering. The algorithm in the first class is the one you want.)
/*
* Copyright (c) 2011, L. Adamson / Dizzy Dragon Games. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* We'd love to hear from you if you're using this software for something or
* have any questions or comments. You can contact us at via email at
* support@dizzydragon.net.
*/
package retrocrawl.engine.game.visionandmovement;
import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
abstract public class FieldOfView2d implements Serializable
{
private static final long serialVersionUID = 1L;
final public static class FieldOfView2dCoordinate implements Serializable
{
private static final long serialVersionUID = 1L;
private int x;
private int y;
/**
* Instantiates a new FieldOfView2dCoordinate with the specified x and y values.
*
* @param x
* the x coordinate
* @param y
* the y coordinate
*/
public FieldOfView2dCoordinate( int x, int y )
{
this.x = x;
this.y = y;
}
/**
* @return the x value of this coordinate.
*/
public int x()
{
return x;
}
/**
* @return the y value of this coordinate.
*/
public int y()
{
return y;
}
@Override
public int hashCode()
{
return x ^ Integer.reverse( y );
}
@Override
public boolean equals( Object o )
{
if( o instanceof FieldOfView2dCoordinate )
return ( (FieldOfView2dCoordinate)o ).x == x && ( (FieldOfView2dCoordinate)o ).y == y;
return false;
}
@Override
public String toString()
{
return x + "," + y;
}
}
private boolean requiresPostProcessing = false;
final private HashMap<FieldOfView2dCoordinate,Boolean> opaqueCells = new HashMap<FieldOfView2dCoordinate,Boolean>();
final private HashMap<FieldOfView2dCoordinate,Boolean> cellContainsData = new HashMap<FieldOfView2dCoordinate,Boolean>();
protected FieldOfView2d( boolean requiresPostProcessing )
{
this.requiresPostProcessing = requiresPostProcessing;
}
abstract protected Set<FieldOfView2dCoordinate> getFieldOfViewFromImpl( FieldOfView2dCoordinate origin, int radius );
abstract protected boolean cellIsOpaqueImpl( FieldOfView2dCoordinate coordinate );
abstract protected boolean cellContainsDataImpl( FieldOfView2dCoordinate coordinate );
final protected boolean cellIsOpaque( int x, int y )
{
return cellIsOpaque( new FieldOfView2dCoordinate(x,y) );
}
final protected boolean cellIsOpaque( FieldOfView2dCoordinate coordinate )
{
Boolean isOpaque = opaqueCells.get( coordinate );
if( isOpaque == null )
{
isOpaque = cellIsOpaqueImpl( coordinate );
opaqueCells.put( coordinate, isOpaque );
}
return isOpaque;
}
final protected boolean cellContainsData( int x, int y )
{
return cellContainsData( new FieldOfView2dCoordinate(x,y) );
}
final protected boolean cellContainsData( FieldOfView2dCoordinate coordinate )
{
Boolean containsData = cellContainsData.get( coordinate );
if( containsData == null )
{
containsData = cellContainsDataImpl( coordinate );
cellContainsData.put( coordinate, containsData );
}
return containsData;
}
final public Set<FieldOfView2dCoordinate> getFieldOfViewFrom( FieldOfView2dCoordinate origin, int radius )
{
opaqueCells.clear();
cellContainsData.clear();
Set<FieldOfView2dCoordinate> results = getFieldOfViewFromImpl( origin, radius );
if( requiresPostProcessing )
{
// This filter ensures that opaque cells cardinally adjacent to
// non-opaque cells are drawn.
HashSet<FieldOfView2dCoordinate> resultsBuf = new HashSet<FieldOfView2dCoordinate>( results );
for( FieldOfView2dCoordinate c : resultsBuf )
{
if( !cellIsOpaque(c) )
{
for( int d = -1; d<=1; d += 2 )
{
FieldOfView2dCoordinate c2;
c2 = new FieldOfView2dCoordinate(c.x()+d,c.y());
if( cellIsOpaque(c2) && cellContainsData(c2) )
results.add( c2 );
c2 = new FieldOfView2dCoordinate(c.x(),c.y()+d);
if( cellIsOpaque(c2) && cellContainsData(c2) )
results.add( c2 );
}
}
}
// This filter ensures that ordinal corners next to two adjacent
// cardinal opaque cells which are next to a non-opaque cell in
// a convex fashion are drawn. (That is, it gets rid of the
// missing corner syndrome.)
resultsBuf = new HashSet<FieldOfView2dCoordinate>( results );
for( FieldOfView2dCoordinate c : resultsBuf )
{
if( !cellIsOpaque(c) )
{
for( int dy=-1; dy<=1; dy+=2 )
{
for( int dx=-1; dx<=1; dx+=2 )
{
FieldOfView2dCoordinate c2 = new FieldOfView2dCoordinate(c.x()+dx,c.y()+dy);
if( cellContainsData(c2) && cellIsOpaque(c2) )
{
FieldOfView2dCoordinate c3a = new FieldOfView2dCoordinate(c2.x()-dx,c2.y());
FieldOfView2dCoordinate c3b = new FieldOfView2dCoordinate(c2.x(),c2.y()-dy);
if( cellIsOpaque(c3a) && resultsBuf.contains( c3a ) && cellContainsData(c3a) && cellIsOpaque(c3b) && resultsBuf.contains( c3b ) && cellContainsData(c3b) )
results.add( c2 );
}
}
}
}
}
}
opaqueCells.clear(); // Allow entries to be GCed.
cellContainsData.clear(); // Allow entries to be GCed.
return results;
}
final public Set<FieldOfView2dCoordinate> getFieldOfViewFrom( int x, int y, int radius )
{
return getFieldOfViewFrom( new FieldOfView2dCoordinate(x,y), radius );
}
}