Tutorial 01 – Lightmap to achieve cool 2D fire effect

downloadDownload the ready to use Eclipse project for this tutorial

Overview

In this page you will learn how to achieve a cool flickering fire light effect for your 2D game, using a frame buffer and shaders with the LibGDX game framework. Though this technique can be easily applied to any game dev environment you’re using, such as LWJGL, XNA/MonoGame or simply raw C++/OpenGL code.

But before we go any deeper, here is the final effect we are going to produce:

Note: assets used for this tutorial

The assets used in this tutorial are free ressources. First the RPG tileset:

RPG tileset

Credits: Zabin, Daneeklu, Jetrel, Hyptosis, Redshrike, Bertram. License: CC BY SA 3.0

Then the fire animation

 

Campfire animation

Credits: Zabin and Jetrel. License CC BY SA 3.0

And finally the light texture. This one is a quick photoshop brush made by myself and is CC0. Do whatever you want with is ;-)

Light map

Light map texture

 

Sources:

RPG Tileset: http://opengameart.org/content/rpg-tiles-cobble-stone-paths-town-objects
Campfire animation: http://opengameart.org/content/camp-fire-animation-finished

 

Introduction

That being said, let’s crack on with things now. We are going to need 3 things to achieve this effect:

1. The normal, effectless render of our map:

Default Shader

2. The ambient light applied to it to achieve a sort of night time coloration:

Ambient Light

3. A framebuffer containing our lightmap:

Lightmap

4. And this it. All together, we will combine this with an smart shader to produce this effect:

Final Result

 

Building the ambient light

Ambient light is nothing special so we will go quickly over it. Basically you just take your basic color, multiply it by another color (the ambient light) and boom you obtain your ambient lighting. More specifically, since we want to obtain a night time effect, we will use a dark blue:

public static final float ambientIntensity = .7f;
public static final Vector3 ambientColor = new Vector3(0.3f, 0.3f, 0.7f);

Now, the the “ambient intensity” variable is not really necessary, but it’s a nice little candy. By using this scalar, you can adjust the brightness of your anbient color quite easily. So an intensity of 1.0 would mean we would use 0.3, 0.3, 0.7 for our ambient color, but 0.5 intensity would result in an ambient color of 0.15, 0.15, 0.35. Pretty neat if you want to modulate dynamically your light.

Now, for our shader we need to be able to pass these values, and do the multiplication to obtain:

Default Shader * Ambient Blue=Ambient Light

Our shader, is, therefore:

varying LOWP vec4 vColor;
varying vec2 vTexCoord;

//texture samplers
uniform sampler2D u_texture; //diffuse map

//additional parameters for the shader
uniform LOWP vec4 ambientColor;

void main() {
	vec4 diffuseColor = texture2D(u_texture, vTexCoord);
	vec3 ambient = ambientColor.rgb * ambientColor.a;

	vec3 final = vColor * diffuseColor.rgb * ambient;
	gl_FragColor = vec4(final, diffuseColor.a);
}

 

Note, in the shader, that ambient color is multiplied by alpha. This is because we don’t pass to our shader a vector3 containing the RGB color and then a float containing the intensity we want to use. Instead, we send the data at once in the form of a vector4. It is simply more effective:

ambientShader.begin();
ambientShader.setUniformf("ambientColor", ambientColor.x, ambientColor.y,
		ambientColor.z, ambientIntensity);
ambientShader.end();

 

Building the lightmap

Now, to build the lightmap we will use a framebuffer. A framebuffer is just another drawing surface for OpenGL, which allow us to draw to it rather than drawing to the default framebuffer. You might find this trick labelled elsewhere as “drawing to a texture”. With libgdx, a framebuffer OpenGL object is encapsulated into the FrameBuffer class. Simply put, we declare it as:

fbo = new FrameBuffer(Format.RGBA8888, width, height, false);

width and height is our resolution, RGBA8888 specifies that we want a 32 bits framebuffer. The false boolean parameter allows us to create in addition a depth buffer. We don’t need it, but if you’re interested in going further with frame buffers, you can have a look at the official OpenGL documentation.

Now, we simply have to render our light texture to wherever the campfire is. In addition, we will make the the size of the light oscillate with a sin function and a random noise factor. This will create a nice flickering effect:

//draw the light to the FBO
fbo.begin();
batch.setProjectionMatrix(cam.combined);
batch.setShader(defaultShader);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
batch.begin();
float lightSize = lightOscillate? (4.75f + 0.25f * (float)Math.sin(zAngle) + .2f*MathUtils.random()):5.0f;
batch.draw(light, tilemap.campFirePosition.x - lightSize*0.5f + 0.5f,tilemap.campFirePosition.y + 0.5f - lightSize*0.5f, lightSize, lightSize);
batch.end();
fbo.end();

Now we have produced, inside this frame buffer, a texture the size of our screen containing the light at its right position. But how to we display it? Displaying it has absolute 0 value. but when we blend it with the normal image, we will have to determine which pixel goes with which.

For this, we will provide the resolution to our shader, and declare a 2nd texture unit:

lightShader.begin();
lightShader.setUniformf("resolution", width, height);
lightShader.end();

lightShader.begin();
lightShader.setUniformi("u_lightmap", 1);
lightShader.end();

Resolution is a vector2 inside our shader, and u_lightmap is declared as being our 2nd texture unit, therefore 1 (0 being the default texture unit). At each frame, we need to pass to the shader the new texture in our slot 1:

fbo.getColorBufferTexture().bind(1); //this is important! bind the FBO to the 2nd texture unit

Then in the shader itself, we will manually do the UV mapping of the texture contained in our framebuffer. We need to do this because the vTexCoord value in our shader corresponds to the UV mapping of our first bound texture, so it is useless. How do we achieve this? Easily. In a pixel shader, we have the position of the pixel currently being drawn by accessing the global variable gl_FragCoord.xy. Then, we have just sent to our shader the resolution of our screen. Therefore, the nornalised, 0.0-1.0 UV mapping value of our current pixel being drawn is gl_FragCoord.xy / resolution.xy.

The resulting shader is this:

varying LOWP vec4 vColor;
varying vec2 vTexCoord;

//our texture samplers
uniform sampler2D u_texture; //diffuse map
uniform sampler2D u_lightmap;   //light map

//resolution of screen
uniform vec2 resolution; 

void main() {
	vec2 lighCoord = (gl_FragCoord.xy / resolution.xy);
	vec4 Light = texture2D(u_lightmap, lighCoord);

	gl_FragColor = vColor * Light;
}

And with this very small bit of code you will obtain:

Lightmap

 

… Our framebuffer displayed on screen.

 

Linking it all together

We’ve actually done the hardest part. Understanding this manual UV mapping of our framebuffer object is the key to achieve this effect. Because now we have for each pixel:

  • The standard, diffuse color, RGB value of the pixel
  • The global ambiant color
  • The light RGB value of the pixel

Putting it all together:

varying LOWP vec4 vColor;
varying vec2 vTexCoord;

//texture samplers
uniform sampler2D u_texture; //diffuse map
uniform sampler2D u_lightmap;   //light map

//additional parameters for the shader
uniform vec2 resolution; //resolution of screen
uniform LOWP vec4 ambientColor; //ambient RGB, alpha channel is intensity 

void main() {
	vec4 diffuseColor = texture2D(u_texture, vTexCoord);
	vec2 lighCoord = (gl_FragCoord.xy / resolution.xy);
	vec4 light = texture2D(u_lightmap, lighCoord);

	vec3 ambient = ambientColor.rgb * ambientColor.a;
	vec3 intensity = ambient + light.rgb;
 	vec3 finalColor = diffuseColor.rgb * intensity;

	gl_FragColor = vColor * vec4(finalColor, diffuseColor.a);
}

First we get our diffuse color and our light color. Then we add the ambient color with the light in a variable called intensity. Finally. we multiply the diffuse color by this new intensity value and we obtain the final color.

 

Final words

I would strongly suggest we poke around with the code. Explaining all this in a short article is very dense and understandably hard to understand for people who want to understand how shader works.

I would also strongly suggest you visit https://github.com/mattdesl/lwjgl-basics/wiki/Shaders , Lesson 06, which describes a great lighting effect in a single pass without using any framebuffer. Unfortunately it is not resolution independent and will be very power hungry if you have more than 1 light. It is this code who inspired this tutorial originally, although the techniques described are completely different.

 

downloadDownload the ready to use Eclipse project for this tutorial

10 Comments

  • [...] Tutorial 01 – Lightmap to achieve cool 2D fire effect [...]

  • Leandro Beni says:

    Very good! This is the kind of post I would like to see happen more often on the internet! More advanced contents and less “hello worlds”.
    Congratulations for text. Thank you.

  • RUB77 says:

    Thank you. Very nice tutorial!

  • Maik Macho says:

    Thanks for such an informative and great tutorial, but I think I’ve found a better way to achieve the same effect. After playing around with it I simplified it by ommiting shaders completely. That’s how I’m doing it right now:
    1. Clear light FBO with ambient color
    2. Draw lights to FBO with additive blending
    3. Draw game content to screen
    4. Draw FBO to screen with multiplicative blending
    The shader definitely is no more accessing two textures at once everytime something gets rendered and thus saving some bandwidth. Also, the ambient light is in the light FBO already.

    I’ve tried ommiting the FBO but that just ended in a weird mess. Still, for simple light purposes and an entry into the world of shaders this article is perfect. Great work!

    • Tony Pottier says:

      Hello Maik,

      Thanks, I really appreciate your comment! I think there’s lots of things you can achieve through using blending in a smart way, but it’s a bit like OpenGL 1.x Transform & Lighting: this knowledge is slowly getting lost as programmable pipelines are getting traction.

      One thing I don’t mention is that a lot of Android devices do not support 32 bits FBO and keeping your FBO black and white can allow for good performance boosts by creating a 8 bits grayscale FBO instead.

      It’d be interesting to compare both methods.

      Cheers!

  • Logan says:

    Doing it as a shader instead of drawing a lightmap over top of everything allows you to control which objects are lit (by adjusting the ambient value passed into the shader or by using a different shader), so I like this method. With some tweaking, it is just as fast, or faster, than Maik’s method, too.

    The main performance problem is the dependent texture read in the pixel shader. Using gl_FragCoord to do a texture lookup is very slow! The solution is to pass the coordinate you want from the vertex to the pixel shader as a varying, which the shader hardware can pre-calculate and use to pre-fetch the light texel.

    In the vertex shader, add:
    varying vec2 v_lightCoord;

    and after you do the vertex transform to screen space, copy the vertex coords into the lighting coords:
    v_lightCoord = gl_Position.xy;

    Finally, in the pixel shader, use the light coord we calculated in the pixel shader:
    varying vec2 v_lightCoord;

    vec4 light = texture2D(u_lightmap, v_lightCoord);

    Thanks for your post by the way, it was exactly what I was looking for and was a great help for the lighting in my game!

  • Kedu says:

    Hi, thx for the tutorial. I needed a pretty simple lighting effect and thus wanted to avoid shaders. So if someone just wants to add a simple movable spotlight in their game this might be the way to go.

    everythigElse.draw(batch);
    batch.setBlendFunction(GL20.GL_DST_COLOR, GL20.GL_SRC_ALPHA);
    spotLight.draw(batch, parentAlpha);
    batch.setBlendFunction(GL20.GL_SRC_ALPHA,GL20.GL_ONE_MINUS_SRC_ALPHA);

    Where the spotLight is the lightMap texture from this article.

  • Dmitry Tikhonov says:

    Hello! Article is awesome! But i have a some kind of bug or something like that. I have two screen – menu screen and screen from article. After changing screen from fireCamp to mainScreen and again to fireCamp i can see that light have a square texture! What maybe wrong?

    • Tony Pottier says:

      Hello Dmitry,

      My guess is that the framebuffer is not created properly or is discarded, or the same happen to the light texture.
      When setScreen(screen) is called, the current screen’s hide() method is called, the new screen becomes the current screen, and its show() method is called. Then the new screen’s render() method is called each frame by Game. Only one screen is rendered at a time.

      In your screen “show” method, you could recreate the light texture or framebuffer. But this is really poor design. Alternatively, you should use the AssetManager class from libgdx which is an amazing little tool.

Leave a Reply

css.php