Tech talk: Faking lights in 2D games

Tech talk: Faking lights in 2D games

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.


Come live chat with the developer and other gamers, get exclusive information on new games, features, discounts and BETA access, join the Discord: https://discord.gg/orangepixel