This post is just to show you the code I talked about in the video: Faking LIGHTS in 2D games
Make sure to check the video to see what’s going on and what the whole idea behind this code is!
All this code is written in Java using LibGDX, but it doesn’t use a lot of java-specifics so should be easy to port to other languages. The main things you need is access to blending modes of what ever rendering system you use. Besides that it’s pretty much standard sprite-rendering.
So if you watched the video, let’s dive into the code:
First, let’s just grab the main class Lights. This is the class that handles everything from setting up the light-array, adding lights, and finally rendering lights.
package com.orangepixel.utils; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.Texture; public class Light { // hard-coded: size of a single light texture (all lights are same size) public final static int lightSize=256; // different light types public final static int UP = 0, RIGHT = 1, DOWN = 2, LEFT = 3, TENSE = 4, DEFAULT = 5, SUN = 6, BEAM = 7, FLASHLIGHT=8, NARROWBEAM = 9, FLARE = 10; public final static int propX=0, propY=1, propRotate=2; // texture coords and default rotation information for every light type just defined public final static int[][] properties = new int[][] { {256,0,0}, // LightType_Up {256,0,90}, // LightType_Right {256,0,180}, // LightType_Down {256,0,270}, // LightType_Left {512,0,0}, // LightType_SphereTense {0,0,0}, // LightType_Sphere {768,0,0}, // LightType_SUN {0,256,0}, // LightType_BEAM {256,0,0}, // LightType_FLASHLIGHT {0,512,0}, // LightType_NARROWBEAM {768,256,0}, // LightType_FLARE }; // array to hold our scene lights public static Light[] myLights = new Light[3200]; // our ambient color for the current scene public static Color ambientColor=new Color(); // our texture-atlas containing the various light sprites public static Texture mySprite; // an optional "gamma" setting for the whole scene - default 0.0f public static float gamma; // now hold our light info int lightType; // position and dimensions float x; float y; float w; float h; float distance; float rotate; // colors float r; float g; float b; float a; boolean volumetric; // this light active or not boolean active; // init the class, setup light array, texture and gamma public final static void initLights(String fileName) { for (int i=myLights.length - 1; i >= 0; i--) { myLights[i]=new Light(); myLights[i].setActive(false); } mySprite=new Texture(Gdx.files.internal(fileName), true); gamma=0.0f; } // change global gamma settings public final static void setGamma(int newGam) { gamma=newGam/50.0f; } // reset all lights - call this at start of your scene public final static void resetLights() { for (int i=0; i < myLights.length; i++) { myLights[i].setActive(false); } } // setup our ambient light public final static void setAmbientLight(float red, float green, float blue, float alpha) { ambientColor.set(red,green,blue,alpha); } // add a new light to the scene public final static void addLight(int ax, int ay, float myDist, int myType, float rotation, float myR, float myG, float myB, float myA, boolean volumetric) { int i=0; if (myR>1f) myR=1f; if (myG>1f) myG=1f; if (myB>1f) myB=1f; while (i < myLights.length && myLights[i].isActive()) i++; if (i < myLights.length) { myLights[i].setLightType(myType); myLights[i].setPosition(ax,ay); myLights[i].rotate=rotation; myLights[i].setColor(myR+gamma, myG+gamma, myB+gamma, myA); myLights[i].setDistance(myDist); myLights[i].setActive(true); myLights[i].volumetric=volumetric; } } public Light() { active=false; } public void setLightType(int myType) { lightType=myType; } public float getX() { return x; } public float getY() { return y; } public void setPosition(float ax, float ay) { x=ax; y=ay; } public void setColor(float ar, float ag, float ab, float aa) { r=ar; g=ag; b=ab; a=aa; } public void setDistance(float adistance) { distance=adistance; } private void setActive(boolean mActive) { active=mActive; } private boolean isActive() { return active; } private float getDistance() { return distance; } private Color getColor(boolean applyVolumetric) { if (applyVolumetric) return new Color(r,g,b,0.4f); else if (volumetric) return new Color(r,g,b,a/2.0f); else return new Color(r,g,b,a); } // render flare lights - these don't require rendering filters, so they are rendered after your scene+lights are rendered public final static void renderFlares() { Render.batch.begin(); for (int i=0; i<Light.myLights.length; i++) { if (Light.myLights[i].isActive() && (myLights[i].lightType == FLARE)) { float tx=(Light.myLights[i].getX()); float ty=(Light.myLights[i].getY()); float tw=((lightSize/100f)*Light.myLights[i].getDistance()); Render.batch.setColor(Light.myLights[i].getColor(false)); tx-=(tw/2); ty-=(tw/4); Render.batch.draw(mySprite, tx, ty, tw, tw/2, properties[FLARE][propX], properties[FLARE][propY], lightSize, lightSize, false, true); } } Render.batch.end(); } // render our volumetric or normal lights (non-flares) - called after your scene is rendered public final static void render(boolean volumetricPass) { float rotation; // start of our sprite-batch Render.batch.begin(); if (!volumetricPass) { // this makes our lights blend with the scene underneath Render.batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE); } else { // volumetric lights need a slightly different blending with the scene underneath Render.batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA); // set a global alpha to make the volumetric light more transparent Render.setAlpha(160); } // render every light in our list! for (int i=0; i<Light.myLights.length; i++) { // only active lights and non-flare lights if (Light.myLights[i].isActive() && myLights[i].lightType!=FLARE && (myLights[i].volumetric==volumetricPass || !volumetricPass)) { // set the color for this light-sprite Render.batch.setColor(Light.myLights[i].getColor(volumetricPass)); // calculate our offset float tx=(Light.myLights[i].getX()); float ty=(Light.myLights[i].getY()); // calculate our light size based on the "distance" setting float tw=((lightSize/100f)*Light.myLights[i].getDistance()); // rotate the light rotation=myLights[i].rotate+properties[myLights[i].lightType][propRotate]; if (rotation>=360) rotation-=360; else if (rotation<0) rotation+=360; // center the light tx-=(tw/2); ty-=(tw/2); // and add it to our sprite batch/buffer Render.batch.draw(mySprite,tx,ty, tw/2,tw, tw,tw, 1f,1f, rotation, properties[myLights[i].lightType][propX], properties[myLights[i].lightType][propY], lightSize,lightSize, false,true); } } Render.batch.end(); } }
Then all that’s left is making sure we call the light system from the right spot and at the right times.
At the start of your game init, you will need to call:
Light.initLights("lights.png"); Light.setGamma(0);
Which initializes the Light array, set up all the variables, and optionally set a global screen-gamma value (based on user preferences most likely).
At the start of your gamescene loop you will want to clear/reset the light array, as every scene lights can change based on player position in the world, or blinking lights, or flashing lights, etc. So make sure to call:
Light.resetLights();
Then in your rendering loop, the first thing you do is render your game scene as you normally would. So clearing the screen, rendering the tilemap, the sprites, and anything else that is part of the scene. Usually you want to hold off on rendering your statusbar / hud and possibly dialogs/text until after you render the lights. So render your gamescene, and then the first call we make is:
Light.render(true);
Which will render all the volumetric lights onto the scene. These don’t require special blending modes, so they will be rendered pretty much like normal sprites. Once that’s done, it’s time to start rendering all our lights to our lightframebuffer:
lightBuffer.begin(); // clear this buffer using our ambient light settings Gdx.gl.glClearColor(Light.ambientColor.r+Light.gamma,Light.ambientColor.g+Light.gamma,Light.ambientColor.b+Light.gamma,Light.ambientColor.a+Light.gamma); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); // render the normal lights (non-volumetric) Light.render(false); // end rendering to this buffer lightBuffer.end();
And then render this lightbuffer onto your gamebuffer (or to the screen, depending on where you rendered your gamescene to):
// now render the light buffer onto the game-buffer (containing the current game frame) gameCanvasBuffer.begin(); Render.batch.begin(); // make sure to use the right blending mode.. this is the key! Render.batch.setBlendFunction(GL20.GL_DST_COLOR, GL20.GL_ZERO); Render.batch.setColor(1,1,1,1); Render.batch.draw(lightBufferRegion, 0, 0, Render.width, Render.height); Render.batch.end(); gameCanvasBuffer.end();
Finally you might want to render your lens-flares onto the gamecanvas buffer, as these require no special blending mode like the ambient ones, but they need to be rendered as final call to simply have the best looking effect !
Light.renderFlares();
And that’s it! Now you can render statusbars, dialogs or any other stuff you don’t want to be affected by lights, and render it all to the screen to end your render-loop.