2D topdown lights with shadows demo

I just wanted to share (a little part of) something I’ve been working on and to give my thanks to @singmajesty and other contributors for all the work on GL framebuffer rendering and shaders, because of which the lighting system for my in-development game is now possible!

It’s obviously not really optimised at the moment, but it’s coming out pretty nice.

4 Likes

It’s so cool.
Congratulations.
Would you mind writing a blog post teaching newbies like me how to do it ?

Thanks.

I probably will at some point, but as a fellow noob of 2.5 years in game development (the point is that my game is still even nearly not done :smiley: ), I’m a bit afraid to teach misinformation / bad practices. But some insight, how I’m currently doing it:

The starting point is your game world in… normal lighting conditions, mine looks like this:


(ignore gaps between tiles, some problem with OpenFL sprite upscaling, I’m working on it).

Then you basically draw a “shadow map”, which is composed of dark colour rectangle (which will produce darkness) and lights. For lights, you will usually use circular gradient of white (or some other light colour, that way you can produce colourful lights) from alpha = 1 on light position to alpha = 0 at maximum distance of the light. As for the shape of the light, you can either draw simple rectangle, or some more complicated shape in case you want shadows. Example of the combination of two lights:

As for the shadows, I basically do a “ray trace” in set amount of directions from the center of the light, compute how far can light go in this direction and then add this point to Vector.

Pseudo code:

var step = light.range / light.numberOfRays;

while (angle > Math.PI - light.angle - light.range / 2){

    var distance = 0;

    // Raytrace the enviroment & generate points
    for (testDistance in 0...light.radius){
        var testx = light.x + Math.sin(angle) * testDistance;
        var testy = light.y + Math.cos(angle) * testDistance;

        distance = testDistance;

        if (there is light barrier on position testx, testy){
            break;
        }
    }

    commands.push(GraphicsPathCommand.LINE_TO);
    points.push(light.x + Math.sin(angle) * distance);
    points.push(light.y + Math.cos(angle) * distance);

    angle -= step;
}

After that I use shadowMap.graphics.drawPath() to draw everything in one batch. It could probably be a lot faster to use shaders, or at least drawTriangles to draw it. Combined “shadow map” of some white and yellow lights could look like this:

All this is drawn to a Shape, which is not on stage (you will not use addChild() on it) every frame, don’t forget to use graphics.clear() on it before drawing every frame (or use some other drawing method). After that, I render the “shadow map” to texture (Render sprites to texture for GL targets) – on stage I have two Bitmap objects with their bitmap.bitmapData of resolution proportional to stage and scaled (I have pixel art graphics) on which I use bitmapData.draw(shadowMap) on every frame. This effectively flattens “shadow map”, so the blend mode is applied correctly.

After that, it’s just matter of right blend mode. I use custom blend modes (BlendMode & GradientFill problems on native), on bitmap on bottom I have

case HARDLIGHT:

    gl.blendEquation (gl.FUNC_ADD);
    gl.blendFunc (gl.DST_COLOR, gl.ONE);

and on the top one

case OVERLAY:

    gl.blendEquation (gl.FUNC_ADD);
    gl.blendFunc (gl.DST_COLOR, gl.SRC_COLOR);

and also apply gaussian blur with shader (bitmap.filters = [new ShaderFilter(shader)]), mine currently looks like this, as I’m GLSL noob:

shader.glFragmentSource = "
    uniform sampler2D uImage0;
    varying vec2 vTexCoord;

    vec4 blur(sampler2D image, vec2 uv) {
        vec4 color = vec4(0.0);

        color += texture2D(image, uv + vec2(-0.0075,-0.0075)) * 0.025641025645;
        color += texture2D(image, uv + vec2(-0.0075,0))     * 0.125;
        color += texture2D(image, uv + vec2(-0.0075,0.0075))    * 0.025641025645;
        color += texture2D(image, uv + vec2(0,-0.0075))     * 0.125;
        color += texture2D(image, uv + vec2(0,0))       * 0.25;
        color += texture2D(image, uv + vec2(0,0.0075))  * 0.125;
        color += texture2D(image, uv + vec2(0.0075,-0.0075))    * 0.025641025645;
        color += texture2D(image, uv + vec2(0.0075,0))  * 0.125;
        color += texture2D(image, uv + vec2(0.0075,0.0075))     * 0.025641025645;

        return color;
    }

    void main() {

        vec4 color = blur(uImage0, vTexCoord);
        gl_FragColor = color;
    }
";

You can experiment with shadow colour and alpha to polish results to lighting conditions you want. The result then looks like this:

And that’s pretty much it.

You will find more information here:


5 Likes

Nifty!

A few years ago I was trying to create a flashlight in as3 and had really bad performance using raytracing approach. I found this article, and I recommend it if you need high performance light/shadow stuff.

Yes, if you have geometry based world, this is by all means the preferred way. However if you need pixel-perfect shadows as I do (major parts of my game world are procedurally generated as individual “pixels” – I have no way to get their geometry other than bitmap trace, which would be slow), you will in most cases probably need some kind of ray-trace.

In next days, I’ll be working on shader based approach, loosely inspired by this, because performance of my current approach is indeed an issue, as expected.


I’ll post progress, when I make some :slight_smile: .

It’s very impressive. If a physics library like Nape will be used for ray tracing would make a difference in overall performance? But as you said you need pixel perfect shadows, and i think Nape is based on geometry if i am not mistaken. Anyway, it’s a very cool result you got there. :slight_smile:

This post is very instructive.

Thank you for sharing.

UPDATE: Shader based approach

Obviously there is some optimisation to be done, but it’s starting to look good. The way it’s done is something like this:

  1. Create custom extended classes of BitmapFilter and Shader. Inside BitmapFilter change __numPasses variable to what you need for multipass shader.

  2. Render / create few textures and then inside shader.__update method send it to shader (shader.data and respective uniform GLSL variables). Amongst others these are texture of light barriers, texture of scene below (to which lights will be applied to) and image of light data converted to pixels (light positions, angles, luminosity, etc.) together with other non-image data, such as scene resolution, etc. Also inside bitmapFilter.__initShader method assign current shader pass to shader.data.

  3. From this step everything is done in GLSL in fragment shader. The idea is, that at the end, the colour of every pixel will be blend of scene pixel colour and light pixel colour, where the light pixel colour is colour of the darkness, unless there is light affecting this point (distance from light to current pixel is small enough to affect it (equation of light propagation with distance, modified to look good) and there is no light barrier between this two points (basic line equation with for cycle from light to current point). Compute this for every light in the area and blend to final colur of the light.

  4. (optional) You don’t have to compute light for every pixel of the screen if you have pixel-art graphics. If so, just compute lights for every pixel (or whatever else resolution) and in next shader pass copy it to rest of the scene pixel area.

  5. Blur

  6. Blend light colour with scene colour (something like Overlay or Hardlight equation, or something between).

Currently only major problem I have is FPS drop due to Blur step. Without it I can run stable 60 FPS, when I turn on 2 pass Gaussian blur, the FPS is reduced to 20 on my Intel HD3000 in WXGA resolution (first world problems, I know).

If anybody has any suggestions or questions, they will be appreciated.

2 Likes