| name | libgdx-box2dlights |
| description | Use when writing libGDX Java/Kotlin code involving 2D lighting with box2dlights — RayHandler, PointLight, ConeLight, DirectionalLight, ChainLight, ambient light, shadow rendering, soft shadows, light filtering, or attaching lights to Box2D bodies. Use when debugging lights not rendering, wrong light positions, lights not casting shadows, or performance issues with many lights. |
libGDX box2dlights
Quick reference for box2dlights — a 2D lighting library using Box2D raycasting for shadow generation. Covers RayHandler, light types, filtering, rendering order, and PPM integration. This is a separate library, not part of core libGDX.
This is NOT the 3D Environment/lighting system. box2dlights is exclusively for 2D shadow-casting lights using Box2D fixtures as occluders.
Gradle Dependencies
// Core module
implementation "com.badlogicgames.box2dlights:box2dlights:1.5"
// ALSO requires gdx-box2d — add explicitly (do not rely on transitive dep)
implementation "com.badlogicgames.gdx:gdx-box2d:$gdxVersion"
// Plus gdx-box2d-platform natives per target platform (see libgdx-box2d skill)
// GWT/HTML5 — add sources
implementation "com.badlogicgames.box2dlights:box2dlights:1.5:sources"
// GdxDefinition.gwt.xml:
// <inherits name="Box2DLights" />
The published 1.5 POM references libGDX 1.6.2 but works with newer versions. Always add your own gdx-box2d dependency at the correct version.
Class Hierarchy
Light (abstract) implements Disposable
├── PositionalLight (abstract)
│ ├── PointLight
│ └── ConeLight
├── DirectionalLight
└── ChainLight
PositionalLight is an intermediate abstract class — many position/body methods live there.
RayHandler
Central manager. Requires a Box2D World (used for shadow raycasting). Even if you don't want shadows, you must pass a World.
RayHandler rayHandler = new RayHandler(world);
RayHandler rayHandler = new RayHandler(world, options);
RayHandler rayHandler = new RayHandler(world, fboWidth, fboHeight);
RayHandler rayHandler = new RayHandler(world, fboWidth, fboHeight, options);
RayHandlerOptions — pass to constructor for gamma correction, diffuse mode, pseudo-3D:
RayHandlerOptions options = new RayHandlerOptions();
options.setGammaCorrection(true);
options.setDiffuse(true);
RayHandler rayHandler = new RayHandler(world, fboWidth, fboHeight, options);
DO NOT use RayHandler.setGammaCorrection(boolean) or RayHandler.useDiffuseLight(boolean) — both are deprecated static methods with EMPTY BODIES (they do nothing). Use RayHandlerOptions at construction, or instance methods applyGammaCorrection(boolean) / setDiffuseLight(boolean) after construction.
Ambient light:
rayHandler.setAmbientLight(0.1f, 0.1f, 0.1f, 0.5f);
rayHandler.setAmbientLight(new Color(0.1f, 0.1f, 0.1f, 0.5f));
rayHandler.setAmbientLight(0.3f);
Rendering — AFTER SpriteBatch, NOT inside begin/end:
batch.begin();
batch.end();
rayHandler.setCombinedMatrix(camera);
rayHandler.updateAndRender();
uiBatch.begin();
uiBatch.end();
setCombinedMatrix overloads:
| Signature | Notes |
|---|
setCombinedMatrix(OrthographicCamera camera) | Preferred. Takes OrthographicCamera, NOT Camera |
setCombinedMatrix(Matrix4 combined, float x, float y, float vpW, float vpH) | For non-ortho cameras. x,y = camera position; vpW/vpH include zoom |
setCombinedMatrix(Matrix4 combined) | DEPRECATED — inaccurate viewport calculation |
Shadows, blur, culling:
rayHandler.setShadows(true);
rayHandler.setBlur(true);
rayHandler.setBlurNum(1);
rayHandler.setCulling(true);
Other methods:
rayHandler.resizeFBO(newWidth, newHeight);
rayHandler.removeAll();
rayHandler.setWorld(newWorld);
rayHandler.setLightMapRendering(false);
rayHandler.getLightMapTexture();
rayHandler.pointAtLight(worldX, worldY);
rayHandler.pointAtShadow(worldX, worldY);
rayHandler.dispose();
Gotchas:
updateAndRender() must be called OUTSIDE any batch.begin()/batch.end() block. RayHandler uses its own FBO and blend state internally.
setCombinedMatrix takes OrthographicCamera, not the base Camera class. For a generic Camera, use the 5-arg Matrix4 overload.
setAmbientLight(float) sets only the alpha channel, not a uniform grayscale. Use the 4-float or Color overload for full control.
- The FBO defaults to 1/4 screen resolution. Call
resizeFBO() if you resize the window or need sharper lights.
- All light coordinates are in Box2D meters, not pixels. Use the same PPM convention as your Box2D world (see libgdx-box2d skill).
Light Types
PointLight
Omnidirectional light radiating from a point.
PointLight light = new PointLight(rayHandler, rays, color, distance, x, y);
PointLight light = new PointLight(rayHandler, rays);
setDirection() is a deprecated no-op on PointLight — it radiates in all directions.
ConeLight
Directional spotlight with a cone angle.
ConeLight light = new ConeLight(rayHandler, rays, color, distance, x, y,
directionDegrees, coneDegrees);
light.setDirection(degrees);
light.setConeDegree(halfAngle);
light.getConeDegree();
Gotcha: coneDegrees is the half-angle. Setting coneDegree=45 creates a 90-degree cone.
DirectionalLight
Parallel rays covering the entire screen (like sunlight).
DirectionalLight light = new DirectionalLight(rayHandler, rays, color, directionDegrees);
light.setDirection(degrees);
light.setIgnoreBody(body);
setPosition(), setDistance(), attachToBody() are all deprecated no-ops on DirectionalLight. getX()/getY() always return 0.
ChainLight
Emits light along a chain of vertices (e.g., for lava flows, light strips).
float[] vertices = {x1,y1, x2,y2, x3,y3, ...};
ChainLight light = new ChainLight(rayHandler, rays, color, distance, rayDirection, vertices);
light.chain.clear();
light.chain.addAll(newVertices);
light.updateChain();
setDirection() is a deprecated no-op on ChainLight.
Light Base Class API
All light types inherit these methods from Light:
light.setPosition(x, y);
light.setPosition(new Vector2(x, y));
light.getX();
light.getY();
light.setColor(new Color(1, 0.8f, 0.6f, 1));
light.setColor(r, g, b, a);
light.setDistance(distMeters);
light.setSoft(true);
light.setSoftnessLength(2.5f);
light.setXray(false);
light.setStaticLight(false);
light.setActive(true);
light.remove();
light.remove(false);
light.dispose();
light.setContactFilter(categoryBits, groupIndex, maskBits);
light.setContactFilter(filter);
Light.setGlobalContactFilter(categoryBits, groupIndex, maskBits);
light.setHeight(heightDegrees);
CRITICAL: The method is setSoftnessLength(), NOT setSoftLength(). The getter is getSoftShadowLength().
Attaching Lights to Bodies
light.attachToBody(body);
light.attachToBody(body, offsetX, offsetY);
light.attachToBody(body, offsetX, offsetY, angleDegrees);
light.setIgnoreAttachedBody(true);
After attachToBody(), setPosition() is relative to the body. The body's rotation affects the offset position and the light direction.
Gotcha: setIgnoreAttachedBody() is a deprecated no-op on DirectionalLight (which cannot attach to bodies).
Collision Filtering
Lights use the same Box2D categoryBits/groupIndex/maskBits system as fixtures. A light only casts shadows from fixtures whose filters allow collision with the light's filter. Default: lights interact with all fixtures (no filtering).
light.setContactFilter(
(short) 0x0020,
(short) 0,
(short) 0x0008
);
Parameter order: categoryBits, groupIndex, maskBits (same order as the Box2D Filter fields, but note that groupIndex comes second here, not third).
Use Light.setGlobalContactFilter(...) to set the default filter applied to all newly created lights.
Rendering Internals
RayHandler renders into an internal FBO (default: 1/4 screen resolution, RGBA8888). The process:
- Bind FBO, clear to black
- Render each light's mesh into FBO with additive blending (GL_SRC_ALPHA, GL_ONE)
- Apply Gaussian blur passes if enabled
- Blend FBO texture onto screen backbuffer
| Mode | Blend Func | Effect |
|---|
| Default (shadows=true) | GL_ONE, GL_ONE_MINUS_SRC_ALPHA | Lit areas show light color; shadowed areas show ambient |
| Diffuse | GL_DST_COLOR, GL_ZERO | Multiplicative — darkens scene, only lit areas retain color |
| No shadows | GL_SRC_ALPHA, GL_ONE | Pure additive — lights brighten scene, no darkening |
Performance
| Lever | Effect | Notes |
|---|
| Ray count | Main CPU cost | 8–32 mobile, 64–128 desktop. Each ray = one Box2D raycast |
setStaticLight(true) | ~90% CPU reduction | Skips per-frame raycasting. Recalculates only when properties change. Does NOT follow attached body |
setXray(true) | ~70–80% CPU reduction | Skips raycasting entirely (no shadows) |
| FBO size | GPU fill rate | Default 1/4 screen. Smaller = faster but blurrier |
| Blur passes | GPU cost | 0 = off, 1–3 safe. Each pass = extra FBO blit |
setCulling(true) | Skips offscreen lights | Enabled by default |
setActive(false) | Disables light entirely | No raycasting, no rendering |
Platform Differences
| Aspect | Desktop/Android/iOS | GWT/HTML5 |
|---|
| Box2D backend | JNI (native C++) | jBox2D (pure Java) — slower raycasting |
| FBO support | Full | WebGL 1.0+ required (all modern browsers) |
| Performance | Full speed | Noticeably slower with many lights due to jBox2D raycasts |
| Shaders | GLSL ES + GLSL 330 fallback | GLSL ES only (330 fallback not available) |
| Setup | box2dlights artifact | box2dlights:sources + <inherits name="Box2DLights" /> |
Common Mistakes
- Calling
updateAndRender() inside batch.begin()/end() — RayHandler binds its own FBO and changes blend state. Always call after batch.end().
- Using
render() instead of updateAndRender() — render() exists but only draws the lightmap without updating light raycasts. Use updateAndRender() for the standard case.
- Using pixel coordinates for light positions — box2dlights uses Box2D's meter-based coordinate system. A light at
(400, 300) means 400 meters, not pixels.
- Forgetting
setCombinedMatrix() before rendering — Lights render at wrong positions or don't appear. Must call every frame before updateAndRender().
- Passing
Camera to setCombinedMatrix() — The method requires OrthographicCamera specifically. For a generic Camera, use the 5-arg (Matrix4, x, y, vpW, vpH) overload.
- Using deprecated static
setGammaCorrection()/useDiffuseLight() — These are no-ops (empty bodies). Use RayHandlerOptions at construction or instance methods applyGammaCorrection()/setDiffuseLight().
- Expecting
setAmbientLight(0.3f) to set gray ambient — The single-float overload sets only the alpha channel. Use setAmbientLight(0.3f, 0.3f, 0.3f, 1f) for gray ambient.
- Not disposing shapes/RayHandler —
RayHandler.dispose() disposes all lights, FBOs, and shaders. Omitting it leaks GPU resources.
- Using
setSoftLength() instead of setSoftnessLength() — The method is setSoftnessLength(float). setSoftLength does not exist.
- Setting
coneDegree to the full cone angle — ConeLight.setConeDegree() takes the half-angle. A value of 45 creates a 90-degree cone.
- Expecting
setStaticLight(true) lights to follow bodies — Static lights skip updateBody(). They only recalculate when you explicitly change a property. Use for lights on static geometry only.
- Creating/destroying lights during
updateAndRender() — Like Box2D's contact callbacks, avoid modifying the light list while the handler is iterating. Create/remove lights outside the render call.