Skip navigation

I posted the following on the Card Kingdom development blog.

Light and shadow are the two basic visual cues that give objects depth and position. Light shows off the detail of the object, giving it form. Shadow gives context to the object’s location relative to the other objects around it. These two together give great depth to an otherwise flat image.

In terms of the engine, light can be done using two types of lights: point lights and directional lights. Point lights (e.g. light bulbs, candles) give off localized light that falls off over distance. Directional lights (e.g. sun light) give off global, uniform light across all objects that it hits. Though different in there uses, the calculations are basically the same.

For point lights, the calculation of the final color is basically:

n = lightPosition - position;
distance = length( n ) / maxPointLightDistance;
n = normalize( n );
diffuse = saturate( dot( normal, n ) );
outColor = ( inColor * lightColor * diffuse ) / distance;

For directional lights, the above equation can be used and does not need to change much. By setting distance to 1, and n to the inverse of the directional light’s direction, normalized of course, the function can be written as:

n = normalize( -lightDirection );
diffuse = saturate( dot( normal, n ) );
outColor = inColor * lightColor * diffuse;

One effect that we are using for this game is a cel shaded look. This effect is quite easy to accomplish and is actually based off the above lighting equations. Instead of using the diffuse power directly, clamping the value to specific ranges gives the effect of large blocks or bands of light, and is done as the following:

if( diffuse > 0.9f ) {
	diffuse = 1.0f;
} else if( diffuse > 0.75f ) {
	diffuse = 0.7f;
} else if( diffuse > 0.4f ) {
	diffuse = 0.45f;
} else {
	diffuse = 0.25f;
}

That’s it. Changing the ranges and values gives different results and should be customized accordingly.

Some things to note about lighting:

  • Make sure all calculations on points AND normals are in the same space (world space, screen space, etc.) Otherwise, the lighting information will be inconsistent.
  • When converting normals into a space, do not convert the normal to a float4 in the shader before multiplying by the matrix. Lighting information will look okay, but is actually incorrect.
  • In forward rendering (rendering directly to a buffer and then the screen), lighting is calculated for each draw call that uses lighting. There’s a reason older graphics APIs and pipelines used a maximum number of lights; especially point lights.

True shadows and shadow maps would be an amazing feature for the engine and give the game a really great look. But, given the time constraint of not only developing a game, but a fun game in 11 weeks, we used our team’s mantra of “scope it down”™ on shadows. We use the simplest form of shadows that have been in games since the beginning: the ground shadow.

The ground shadow is just a texture, usually a circle or oval depending on the view, that is displayed under a character or object. This shadow follows the object around and does not move off the ground, even if the object is in the air. A common effect to incorporate in the ground shadow is to scale it down when the object is above the ground. This adds to the illusion of “shadow.” Aside from adding a depth cue to the object within the world, the ground shadow is also useful in determining the position of an object in air. This is very useful in platforming-type games where precise jumping and movement is required.

Our implementation of the ground shadow includes a minimum and maximum scale for the shadow and a maximum height the object can be before it’s scaled down to the minimum, so the shadow doesn’t disappear if the object is too high. The scale is calculated as the follow:

scale = 1 - ( ( currentHeight - groundHeight ) / maxHeight );
scale = ( scale * ( maxScale - minScale ) ) + minScale;

Inverting the scale makes it the largest when the current height is close to the ground and smallest when it’s farther away. Adding the minimum scale after it’s been multiplied prevents the scale from going to zero.

The following screenshot shows the shadows below the player and enemies. Notice that the player’s shadow is smaller as he is jumping:

The following shows lighting with multiple types of lights; a directional light casting some cel shaded light on the background arena and three point lights: a red one to the far left, a blue one behind the player, and a green one on the enemies:

The following is what the game looks like now with all the elements combined:

The cel shading is really prevalent on the barrels, the player’s helmet, and the arena.

The next step for lighting is setting up the overall look of the scene: changing the lights’ direction and color to set the mood of the arena. This I will leave to the designers and artists.