package box2dLight; /** @author kalle_h */ import java.util.HashMap; import box2dLight.shaders.LightShader; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.Mesh; import com.badlogic.gdx.graphics.glutils.FrameBuffer; import com.badlogic.gdx.graphics.glutils.ShaderProgram; import com.badlogic.gdx.math.Matrix4; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.physics.box2d.Filter; import com.badlogic.gdx.physics.box2d.Fixture; import com.badlogic.gdx.physics.box2d.RayCastCallback; import com.badlogic.gdx.physics.box2d.World; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Disposable; public class RayHandler implements Disposable { private static final int DEFAULT_MAX_RAYS = 1023; final static int MIN_RAYS = 3; boolean culling = true; boolean shadows = true; boolean blur = true; int blurNum = 1; Color ambientLight = new Color(); Color ambientColor = Color.BLACK; int MAX_RAYS; World world; ShaderProgram lightShader; boolean depthMasking; /** gles1.0 shadows mesh */ private Mesh box; /** * @param combined * matrix that include projection and translation matrices */ final private Matrix4 combined = new Matrix4(); /** camera matrix corners */ float x1, x2, y1, y2; private LightMap lightMap; /** * This Array contain all the lights. * * NOTE: DO NOT MODIFY THIS LIST */ final public Array<Light> lightList = new Array<Light>( false, 16, Light.class ); /** * This Array contain all the disabled lights. * * NOTE: DO NOT MODIFY THIS LIST */ final public Array<Light> disabledLights = new Array<Light>( false, 16, Light.class ); /** how many lights passed culling and rendered to scene */ public int lightRenderedLastFrame = 0; /** * Construct handler that manages everything related to updating and * rendering the lights MINIMUM parameters needed are world where collision * geometry is taken. * * Default setting: culling = true, shadows = true, blur = * true(GL2.0),blurNum = 1, ambientLight = 0.0f; * * NOTE1: rays number per lights are capped to 1023. For different size use * other constructor * * NOTE2: On GL 2.0 FBO size is 1/4 * screen size and used by default. For * different sizes use other constructor * * @param world * @param camera */ public RayHandler( World world, boolean depthMasking ) { this( world, DEFAULT_MAX_RAYS, Gdx.graphics.getWidth() / 4, Gdx.graphics.getHeight() / 4, depthMasking ); } /** * Construct handler that manages everything related to updating and * rendering the lights MINIMUM parameters needed are world where collision * geometry is taken. * * Default setting: culling = true, shadows = true, blur = * true(GL2.0),blurNum = 1, ambientLight = 0.0f; * * * @param world * @param camera * @param maxRayCount * @param fboWidth * @param fboHeigth */ public RayHandler( World world, int maxRayCount, int fboWidth, int fboHeigth, boolean depthMasking ) { this.world = world; this.depthMasking = depthMasking; MAX_RAYS = maxRayCount < MIN_RAYS ? MIN_RAYS : maxRayCount; m_segments = new float[ maxRayCount * 8 ]; m_x = new float[ maxRayCount ]; m_y = new float[ maxRayCount ]; m_f = new float[ maxRayCount ]; lightMap = new LightMap( this, fboWidth, fboHeigth, depthMasking ); lightShader = LightShader.createLightShader(); } /** * Set combined camera matrix. Matrix will be copied and used for rendering * lights, culling. Matrix must be set to work in box2d coordinates. Matrix * has to be updated every frame(if camera is changed) * * * NOTE: Matrix4 is assumed to be orthogonal for culling and directional * lights. * * If any problems detected Use: [public void setCombinedMatrix(Matrix4 * combined, float x, float y, float viewPortWidth, float viewPortHeight)] * Instead * * * @param combined * matrix that include projection and translation matrices */ public void setCombinedMatrix( Matrix4 combined ) { System.arraycopy( combined.val, 0, this.combined.val, 0, 16 ); // updateCameraCorners float invWidth = combined.val[Matrix4.M00]; final float halfViewPortWidth = 1f / invWidth; final float x = -halfViewPortWidth * combined.val[Matrix4.M03]; x1 = x - halfViewPortWidth; x2 = x + halfViewPortWidth; float invHeight = combined.val[Matrix4.M11]; final float halfViewPortHeight = 1f / invHeight; final float y = -halfViewPortHeight * combined.val[Matrix4.M13]; y1 = y - halfViewPortHeight; y2 = y + halfViewPortHeight; } /** * EXPERT USE Set combined camera matrix. Matrix will be copied and used for * rendering lights, culling. Matrix must be set to work in box2d * coordinates. Matrix has to be updated every frame(if camera is changed) * * NOTE: this work with rotated cameras. * * @param combined * matrix that include projection and translation matrices * * @param x * combined matrix position * @param y * combined matrix position * @param viewPortWidth * NOTE!! use actual size, remember to multiple with zoom value * if pulled from OrthoCamera * @param viewPortHeight * NOTE!! use actual size, remember to multiple with zoom value * if pulled from OrthoCamera */ public void setCombinedMatrix( Matrix4 combined, float x, float y, float viewPortWidth, float viewPortHeight ) { System.arraycopy( combined.val, 0, this.combined.val, 0, 16 ); // updateCameraCorners final float halfViewPortWidth = viewPortWidth * 0.5f; x1 = x - halfViewPortWidth; x2 = x + halfViewPortWidth; final float halfViewPortHeight = viewPortHeight * 0.5f; y1 = y - halfViewPortHeight; y2 = y + halfViewPortHeight; } boolean intersect( float x, float y, float side ) { return (x1 < (x + side) && x2 > (x - side) && y1 < (y + side) && y2 > (y - side)); } /** * Remember setCombinedMatrix(Matrix4 combined) before drawing. * * Don't call this inside of any begin/end statements. Call this method * after you have rendered background but before UI. Box2d bodies can be * rendered before or after depending how you want x-ray light interact with * bodies */ public final void updateAndRender() { update(); updateLightMap(); } /** * Manual update method for all lights. Use this if you have less physic * steps than rendering steps. */ public final void update() { final int size = lightList.size; for( int j = 0; j < size; j++ ) { lightList.items[j].update(); } } /** * Manual rendering method for all lights. * * NOTE! Remember to call updateRays if you use this method. * Remember * setCombinedMatrix(Matrix4 combined) before drawing. * * * Don't call this inside of any begin/end statements. Call this method * after you have rendered background but before UI. Box2d bodies can be * rendered before or after depending how you want x-ray light interact with * bodies */ public void updateLightMap() { lightRenderedLastFrame = 0; if( depthMasking ) { Gdx.gl.glDepthMask( true ); Gdx.gl.glDisable( GL20.GL_DEPTH_TEST ); } Gdx.gl.glEnable( GL20.GL_BLEND ); Gdx.gl.glBlendFunc( GL20.GL_SRC_ALPHA, GL20.GL_ONE ); renderWithShaders(); } void renderWithShaders() { if( shadows || blur ) { lightMap.frameBuffer.begin(); Gdx.gl20.glClearColor( 0, 0, 0, 0 ); if( depthMasking ) { Gdx.gl20.glClearDepthf( 1 ); Gdx.gl20.glClear( GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT ); } else { Gdx.gl20.glClear( GL20.GL_COLOR_BUFFER_BIT ); } } lightShader.begin(); { lightShader.setUniformMatrix( "u_projTrans", combined ); final Light[] list = lightList.items; for( int i = 0, size = lightList.size; i < size; i++ ) { list[i].render(); } } lightShader.end(); if( shadows || blur ) { lightMap.frameBuffer.end(); } } public void renderLightMap( Rectangle viewport, FrameBuffer dest ) { lightMap.render( viewport, dest ); } public FrameBuffer getLightMap() { return lightMap.frameBuffer; } private void alphaChannelClear() { Gdx.gl20.glClearColor( 0f, 0f, 0f, ambientLight.a ); Gdx.gl20.glColorMask( false, false, false, true ); Gdx.gl20.glClear( GL20.GL_COLOR_BUFFER_BIT ); Gdx.gl20.glColorMask( true, true, true, true ); Gdx.gl20.glClearColor( 0f, 0f, 0f, 0f ); } @Override public void dispose() { for( int i = 0; i < lightList.size; i++ ) { lightList.items[i].lightMesh.dispose(); lightList.items[i].softShadowMesh.dispose(); } lightList.clear(); for( int i = 0; i < disabledLights.size; i++ ) { disabledLights.items[i].lightMesh.dispose(); disabledLights.items[i].softShadowMesh.dispose(); } disabledLights.clear(); if( lightMap != null ) lightMap.dispose(); if( lightShader != null ) lightShader.dispose(); } float m_segments[]; float[] m_x; float[] m_y; float[] m_f; int m_index = 0; class LightRayCastCallback implements RayCastCallback { public Light requestingLight = null; @Override final public float reportRayFixture( Fixture fixture, Vector2 point, Vector2 normal, float fraction ) { if( (requestingLight.maskBits != Light.MaskConsiderAllFixtures) && !considerFixture( fixture ) ) return -1; // if (fixture.isSensor()) // return -1; m_x[m_index] = point.x; m_y[m_index] = point.y; m_f[m_index] = fraction; return fraction; } private HashMap<Fixture, Filter> map = new HashMap<Fixture, Filter>(); final boolean considerFixture( Fixture fixture ) { Filter filter = map.get( fixture ); if( filter == null ) { filter = fixture.getFilterData(); map.put( fixture, filter ); } return ((requestingLight.maskBits & filter.categoryBits) != 0); } } final LightRayCastCallback ray = new LightRayCastCallback(); final void doRaycast( Light requestingLight, Vector2 start, Vector2 end ) { ray.requestingLight = requestingLight; world.rayCast( ray, start, end ); } public void removeAll() { while( lightList.size > 0 ) lightList.pop().remove(); while( disabledLights.size > 0 ) disabledLights.pop().remove(); } private void setShadowBox() { int i = 0; // This need some work, maybe camera matrix would needed float c = Color.toFloatBits( 0, 0, 0, 1 ); m_segments[i++] = -1000000f; m_segments[i++] = -1000000f; m_segments[i++] = c; m_segments[i++] = -1000000f; m_segments[i++] = 1000000f; m_segments[i++] = c; m_segments[i++] = 1000000f; m_segments[i++] = 1000000f; m_segments[i++] = c; m_segments[i++] = 1000000f; m_segments[i++] = -1000000; m_segments[i++] = c; box.setVertices( m_segments, 0, i ); } /** * Disables/enables culling. This save cpu and gpu time when world is bigger * than screen. * * Default = true * * @param culling * the culling to set */ public final void setCulling( boolean culling ) { this.culling = culling; } /** * Disables/enables gaussian blur. This make lights much more softer and * realistic look but also cost some precious shader time. With default fbo * size on android cost around 1ms * * default = true; * * @param blur * the blur to set */ public final void setBlur( boolean blur ) { this.blur = blur; } /** * Set number of gaussian blur passes. Blurring can be pretty heavy weight * operation, 1-3 should be safe. Setting this to 0 is same as * setBlur(false) * * default = 1 * * @param blurNum * the blurNum to set */ public final void setBlurNum( int blurNum ) { this.blurNum = blurNum; } /** * Disables/enables shadows. NOTE: If gl1.1 android you need to change * render target to contain alpha channel* default = true * * @param shadows * the shadows to set */ public final void setShadows( boolean shadows ) { this.shadows = shadows; } /** * Ambient light is how dark are the shadows. clamped to 0-1 * * default = 0; * * @param ambientLight * the ambientLight to set */ public final void setAmbientLight( float ambientLight ) { if( ambientLight < 0 ) ambientLight = 0; if( ambientLight > 1 ) ambientLight = 1; this.ambientLight.a = ambientLight; } /** * Ambient light color is how dark and what colored the shadows are. clamped * to 0-1 NOTE: color is changed only in gles2.0 default = 0; * * @param ambientLight * the ambientLight to set */ public final void setAmbientLight( float r, float g, float b, float a ) { this.ambientLight.set( r, g, b, a ); } /** * Ambient light color is how dark and what colored the shadows are. clamped * to 0-1 NOTE: color is changed only in gles2.0 default = 0,0,0,0; * * @param ambientLight * the ambientLight to set */ public final void setAmbientLight( Color ambientLightColor ) { this.ambientLight.set( ambientLightColor ); } /** * @param world * the world to set */ public final void setWorld( World world ) { this.world = world; } final static String HIGH = "highp"; final static String MED = "mediump"; final static String LOW = "lowp"; static String colorPrecision = MED; /** * set color precision to highp. Overkill quality. NOTE: this must be set * before rayHandler is constructed */ public static void setColorPrecisionHighp() { colorPrecision = HIGH; } /** * set color precision to mediump. Good quality and performance. NOTE: this * must be set before rayHandler is constructed */ public static void setColorPrecisionMediump() { colorPrecision = MED; } /** * set color precision to lowp. Worst quality, best performance. NOTE: this * must be set before rayHandler is constructed */ public static void setColorPrecisionLowp() { colorPrecision = LOW; } /** * return current color precision Note: if changed after RayHandler is * initialized, returned String is not what rayHandler is using * * @return colorPrecision */ public static String getColorPrecision() { return colorPrecision; } static boolean gammaCorrection = false; static float gammaCorrectionParameter = 1f; static boolean isDiffuse = false; final static float GAMMA_COR = 0.625f; /** * return is gamma correction enabled * * @return */ public static boolean getGammaCorrection() { return gammaCorrection; } /** * set gammaCorrection. This need to be done before creating instance of * rayHandler. NOTE: this do nothing on gles1.0. NOTE2: for match the * visuals with gamma uncorrected lights light distance parameters is * modified internal. * * @param gammeCorrectionWanted */ public static void setGammaCorrection( boolean gammeCorrectionWanted ) { gammaCorrection = gammeCorrectionWanted; if( gammaCorrection ) gammaCorrectionParameter = GAMMA_COR; else gammaCorrectionParameter = 1f; } /** * If this is set to true and shadow are on lights are blended with diffuse * algoritm. this preserve colors but might look bit darker. This is more * realistic model than normally used This might improve perfromance * slightly * * @param useDiffuse */ public static void useDiffuseLight( boolean useDiffuse ) { isDiffuse = useDiffuse; } }