5 min read
2D Lighting

Lighting. Lighting. It is an unruly beast of a concept. It has high complexity for computation, implementation, and makes a major impact on design and mood.

There are as many approaches to lighting as there are games, but for Skymagi, I considered the following options:

  • Raycast: A classic roguelike technique, you raycast towards collidables, create ‘shadow’ and ‘light’ lightmap based on those rays (e.g. Roll20).
  • Screen Space Lightmaps: Rather than creating a lightmap based on rays, just have an explicit lightmap that can be rendered and multiplied as desired (e.g. Stardew Valley).
  • LibGDX Environment (3D lights): Traditional 3d lights typically work with 2d games still, but with a z coordinate.

I chose to build a lightmap using raycasting. Since mages will be walking around dark castle interiors, I thought rays within corners would give the game a feeling of exploration. I also wanted to use lighting to represent Fog of War, so having mages have a light radius remniscent of Diablo 2 gives that dungeon crawling vibe.

Raycasting

There are so many phenomenal resources for raycast implementations. I stumbled upon Red Thread Games walkthrough and Sight & Light’s demonstration years ago to understand the technique. However, I still didn’t quite grasp how to build a lightmap in code, or what those shaders would look like. Noel Berry’s remake of Celeste’s lighting showed me a clever technique and how to get fast lighting without rays.

I tried several different approaches:

  • Applying character light gradients, multiplying my navmesh areas, subtracting box2d statics as a lightmap.
  • Raycasting to navmesh points, subtracting box2d statics.

In the end, Libgdx’s box2dlights library had almost all the features I wanted (and worked way better).

I added the library, created a LightingSystem that would pool box2dlight light objects based on Light components, and called the Rayhandler render method.

And… viola?

Box2dlights handled raycasting, applying the lightmap, it even blurred shadows/light.

It did feel like magic, but there three major problems:

  • The light looks atrocious with this lightmap multiplier.
  • The raycasting is good, but the resolution of lightmap does not match the pixel art.
  • The background sky should not be dark (except at night).

Fixing the aesthetics

This issue was the simplest to fix just by editing box2dlight settings. I wanted a higher baseline (ambient) light, and the actual lights should be diffuse. Diffuse changes the blend function and shader for the applied lightmap to preserve colors.

RayHandler.useDiffuseLight(true);
this.rayHandler.setAmbientLight(0.5f, 0.5f, 0.5f, 0.1f); // changes night/day

After enabling diffuse

Much closer to my imagined goal.

Pixel Perfect Lightmap

I render the game at the client’s native resolution (e.g. 1920x1080) and upscale assets (960x540) for pixelation. Unfortunately, the default lightmap rendered at a high resolution than the assets (1920x1080).

You can see the quarter-pixel lightmap on the druid’s hair:

Lighting fidelity

It’s a common performance enhancement to change the lightmap resolution (typically lower) and use blur to match the native resolution. In my case, I wanted to ensure the lightmap would always match the pixel native resolution (960x540) and upscale with nearest neighbor (to preserve pixel chunks).

My viewport is already designed to maintain the pixel native scale, so this ended up being a quick fix:

this.rayHandler = new RayHandler(
    world,
    // frame buffer size MATCH pixel native render with nearest scaling
    viewport.getNativeWidth(), viewport.getNativeHeight()
);
// use nearest neighbor scaling for the lightmap texture
this.rayHandler.getLightMapTexture().setFilter(
    Texture.TextureFilter.Linear, Texture.TextureFilter.Nearest
);

And now pixels and lightmap line up:

fixed pixel fidelity

While this might not seem like it had much of an effect, matching the pixel grid greatly improved the output:

grid-aligned

More tweaks

I won’t walk you through the hours I spent editing light radius values, colors, and experimenting with blurs (and don’t worry, I am not finished tweaking).

I also added box2d collision filters for ‘blocks light’ so that I could independently control whether an entity body blocks light regardless of its collision type.

But the result is looking much closer to what I imagined:

There are still some issues:

  • Background isn’t lit (this turned out to be quite difficult)
  • My static objects need better collision bodies
  • Shadows dance a bit too much

But we’ll save those for another time. Now that we have our lighting system, we can finally implement… fog of war!