Thursday, May 25, 2017

Simple lighting in 2D games + shader (GLSL)


    Using some sort of light system in your game is kind of a double-edged knife. It may improve some scenes visually, but may also oversaturate, overexpose or simply clutter (especially when the engine you use also "creates" shadows) others.

    In the times where games were still written in Assembly and presenting an eighty Kilobyte background image to your lead programmer to cram into the cartridge could cost you your head, Scene lighting in a 2d game was usually "baked" into the tilesets. So for example a dark cave or forest used darker and more muted colors, while a sunny village had a more vibrant palette and strong yellow highlights could also indicate the presence of an sun overhead. Dynamically changing the lighting of a scene during runtime, barring some palette animation tricks, was usually not possible.

A few decades ago, all it took was some dark, dull colors...

...or some bright, vibrant ones.

    These years we have shaders and raster effects and we can pretty much alter the value of our output pixels however we want. It is not uncommon for a 2D game to have diffuse, self-illumination, normal maps and more, in order to achieve a pixel-perfect, accurately lit environment with shadows! But sometimes, all we need is a simple light map that makes some areas brighter and others darker.

    A simple way to achieve lighting in a game is by using blending modes. You have your "light map"


    And your "base image"



    And you want to determine a way about how the pixels of the one are combined with the other to give the final result:



    I will be using the Photoshop terminology for certain blend modes. You don't need to be a Photoshop expert to understand what they do, however it pays to have a general idea of how some blend modes affect your image. Some of those blending modes can be achieved by altering the raster parameters[*]For example, in Game Maker, it's draw_set_blend_mode(...). Others are a bit more complex or involve some pixel checking logic and therefore shaders are the way to go.


Add: Simple but no darkening


    This is pretty straightforward: The pixel values of the source (red, green, blue, alpha) are added to those of the destination. A very mild light source can be OK, but for stronger lights the image is ruined due to overexposure. Plus, it is impossible to "darken" areas of the image with this mode: You can only firther brighten up the scene.

Add blend mode

    Of course, overexposure could be what we wanted from the beginning, maybe because we are creating a hallucination scene, but let's assume a "standard" scenario where there is a day/night cycle in your game, it's nighttime and your village scene is illuminated by lamp posts or torches.


Overlay: Effective, but beware of unnatural results


    Overlay is kind of difficult to explain without going into two other blend modes, Multiply and Screen. The gist for those two is: Multiply darkens your output pixel, while Screen brightens it. What Overlay does is compare the initial brightness of the base pixel: If it's >50%, apply the lightmap pixel with the Screen filter. If it's <50%, apply it with the Multiply filter.

Overlay offers a crisp, high-contrast, pronounced lighting scheme, as if light hits metal.

    So essentially this blending mode makes your bright pixels brighter and your dark pixels darker. But because at the same time Multiply and Screen can also alter a pixel's color aside from its brightness, in most cases you get a nice lighting effect with a strong contrast between highlights and shadows. This usually works for most scenes. I used it for lighting in the barricade defense parts of Emerge: Cities of the Apocalypse.

Overlay worked well for Emerge...

    You could say that Overlay lighting is almost perfect. Almost. Here is a scenario I recently discovered where it didn't quite work the way I wanted it to: In Gleaner Heights, there is a day/night cycle. I want night to actually be dark, not just a blue-ish tint of everything. I thought that Overlay would cut it, but...

...but look what it does for a night scene in Gleaner Heights. Winter night (right) looks almost like not a night at all.

    Lighter objects or areas are unnaturally pronounced during night, as if infected by some sort of bioluminescent bacteria. The effect is even more pronounced the whiter the scene is, like in this winter screenshot:

    This made me wonder if there is another blending mode that can work in dark scenes. Oddly enough, the solution came from...


Hard Light: Effective if used mildly


    There are many ways to blend pixels between two images. I mean, anyone who's used some sort of Photoshop-like editor has stumbled upon them at some point, and usually completely oblivious to their inner workings[*]Yes, I know there are extensive books/online lessons about How To Be A Photoshop Savant In 1,286 Hours that (hopefully) go into some degree of detail about this stuff. No, I never had the time to learn this way. Plus I like discovering certain things myself.. Some of those blending modes tend to fly completely under the radar. I mean, who ever uses Pin Light? Or Dissolve[*]I actually found an awesome use for pixel-y graphics, worthy of its own article.?

Hard Light gives a duller, softer light effect, as if light hits terracotta.

    Enter Hard Light. This one is quite elusive because if you use a very bright or very dark lightmap the original image tends to vanish. But with some mild saturation and brightness use, one can achieve some sort of non-shiny lighting that also controls overall image brightness. Surprisingly, as a blending mode, Hard Light works exactly the same as Overlay, only the base image and the lightmap are swapped. This means, if I've go it correctly, that this time you apply the base image to the lightmap instead of the other way around that happens in Overlay mode. As I said before, using strongly saturated or very bright/dark colors tends to "drown" your original image. Actually, using a gray (128,128,128) lightmap leaves your image unchanged[*]This is also true for Overlay., so you may want to have "pure" gray as some sort of reference point and work your way from there.

Hard Light blending mode used in Gleaner Heights.

    In the image above the unnatural shininess has disappeared. We now have a natural, dark night. The weak point of Hard Light is that, when you tend to have lots of lights close to one another, your lightmap tends to be too bright in those places and this leads to color drowning, and an unsightly overexposure. This can be mitigated by manually taking care of your light sources as to not be too strong/clustered, but then again, you might just want some lights to be that way.

Excessive lightmap values can lead to drowning the original.

    Here is the GLSL ES (fragment) shader for using the Hard Light blending mode. Vertex shader remains unchanged.

varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform sampler2D texLmap;

void main()
{
    vec4 upper = texture2D(texLmap, v_vTexcoord);
    vec4 lower = texture2D(gm_BaseTexture, v_vTexcoord);
    vec4 outColor = vec4(0.0, 0.0, 0.0, upper.a);
    
    if (upper.r > 0.5) {
        outColor.r = (1.0 - (1.0-lower.r) * (1.0-2.0*(upper.r-0.5)));
    } else {
        outColor.r = lower.r * (2.0*upper.r);
    }

    if (upper.g > 0.5) {
        outColor.g = (1.0 - (1.0-lower.g) * (1.0-2.0*(upper.g-0.5)));
    } else {
        outColor.g = lower.g * (2.0*upper.g);
    }

    if (upper.b > 0.5) {
        outColor.b = (1.0 - (1.0-lower.b) * (1.0-2.0*(upper.b-0.5)));
    } else {
        outColor.b = lower.b * (2.0*upper.b);
    }
                
    gl_FragColor = vec4(outColor.r,outColor.g,outColor.b,1.0);
} 

    It's pretty simple. No other functions except main (multiply and screen could have been written as separate functions), and output alpha was set to 1 since I was dealing with the whole image which I want to be opaque. The texLmap parameter is the lightmap. Also, with a little tinkering, it can be transformed to an Overlay shader.

No comments:

Post a Comment