April 18, 2012

And There Was Light


Lighting with Java & OpenGL

I started game programming with Java2D. While it was enough and easy to use, adding realistic lighting for the field of view, is nearly impossible. So, the Java2D view renderer can only do it in a very primitive way: tiles out of view are drawn a bit darker by adjusting the alpha composite for the drawImage() call.

tile based field-of-view

A nice and easy to set up solution (that means no OpenGL programming knowledge is required) for OpenGL and LibGdx is box2dlights.

field-of-view with box2dlights

Here are the main steps outlined for adding one scene light as shown above:
// light camera 
pixelPerMeter = 64f;
boxLightCamera = new OrthographicCamera();
float boxLightViewportWidth = spriteCamera.viewportWidth / pixelPerMeter; 
float boxLightViewportHeight = spriteCamera.viewportHeight / pixelPerMeter; 
boxLightCamera.setToOrtho(true, boxLightViewportWidth, boxLightViewportHeight); 
boxLightCamera.update(true);

Box2dlights uses the physics simulation Box2D (in its native version as integrated with LibGdx), and Box2D prefers meters as unit of measure instead of pixels. With the rest of the game using pixels, a new camera is needed for mapping pixels to meters. A simple way to do this is to define 1 tile as 1 meter, and thus 1 meter equals 64 pixels.

// world and light setup
 world = new World(new Vector2(), true); 

RayHandler.useDiffuseLight(true);
rayHandler = new RayHandler(world);
rayHandler.setCombinedMatrix(boxLightCamera.combined);
rayHandler.setAmbientLight(ambientLight);

spriteLight = new PointLight(rayHandler, 128, lightColor, 10, 0, 0);

A new yet empty Box2D world (no gravity defined, we only care about light rays) is created.
RayHandler is the main light controller. Setting the diffuse flag is important to prevent an over-illuminated player sprite.
With ambient light, tiles out of view still get some low amount of light.
Finally, a light is created that will be later continually moved to the player sprite position and thus allowing a dynamic field-of-view.
128 rays make up a nice smooth light, 10 meters is the light range. Furthermore, a light color is defined.

As box2d is used to find out about light obstacles like walls from which shadows are created and light is blocked, the tile map must be converted into box2d world objects. To keep it Simply, for each tile one world body is created. This could be theoretically optimized by combining rows and columns of neighbour tiles to only one body, but so far I did not see any performance impact.
protected void createWorldScenery(ITileMap tileMap) {
 // build plan for wall bodies
 float halfBody = tileSize / pixelPerMeter / 2;

 PolygonShape tileShape = new PolygonShape();
 tileShape.setAsBox(0.5f, 0.5f);

 BodyDef tileBodyDef = new BodyDef();
 tileBodyDef.type = BodyType.StaticBody;

 FixtureDef fixtureDef = new FixtureDef();
 fixtureDef.shape = tileShape;
 fixtureDef.filter.groupIndex = 0;

 // create box2d bodies for all wall tiles
 for (int row = 0; row < tileMap.getRows(); row++) {
  for (int col = 0; col < tileMap.getColumns(); col++) {
   int tileClass = tileMap.getTileClassMask(col, row);
   if (Tools.anyBitSet(tileClass, obstacleMask)) {
    float bodyX = col + halfBody;
    float bodyY = row + halfBody;
    tileBodyDef.position.set(bodyX, bodyY);
    Body tileBody = world.createBody(tileBodyDef);
    tileBody.createFixture(fixtureDef);
   }
  }
 }

 tileShape.dispose();
}

Disposing box2d objects must not be forgotten. Tile classes are bit fields for identifying light blocking wall tiles.

For rendering each frame, first tiles and sprites are drawn with a separate camera, then the moving light is updated to match the player's center position:
puppetLight.setPosition(clientCenterX, clientCenterY);

Afterwards, the light camera gets updated following the scrolling tile map:
boxLightCamera.position.set(camX, camY, 0);
boxLightCamera.update();

And finally, the RayHandler does all the magic:
rayHandler.setCombinedMatrix(boxLightCamera.combined, 
boxLightCamera.position.x, boxLightCamera.position.y,
boxLightCamera.viewportWidth * boxLightCamera.zoom, 
boxLightCamera.viewportHeight * boxLightCamera.zoom);

rayHandler.updateAndRender();

That's it. Without one line of OpenGL code.

For even more fun, it is possible to add and remove lights any time, so I illuminated fired bullets in different colors. For bullets, computing power can be saved by reducing the number of rays to 16.
Additionally, a nice effect is to continuously dim bullets instead of switching them off immediately when they hit a wall. Just set new light colors while lowering the alpha composite until it reaches a value of 0.

The game uses OpenGL 2.0, it runs with OpenGL 1.1 as well, but the lighting impression is totally different, lights are much brighter, so to switch between both version, some adjustment would be necessary.

What about the performance impact ?

~ 2500 fps for rendering the scrolling tile map without lights.
~ 1000 fps for rendering with enabled lights.

While a bit of a performance drop, 1000 fps are still way beyond of what is required, so some more lights could be added, I guess :)

No comments:

Post a Comment