Single post

Simple 2D Lighting System in C# and Monogame

This tutorial will walk you through a simple lighting tutorial.
You can see a live example of what this system does in a top-down 2D rpg.

c# 2d lighting exampleScreenshot 2015-06-24 18.29.06

 

The live gif lost some quality, the second screen shot shows you how it really looks.
Okay, lets get into it!

Go into your current project, and make a new file called

lighteffect.fx

This file will control the way our light will be drawn to the screen. This is an HLSL style program at this point. Other tutorials on HLSL will be available in the full course which will allow you to do some wicked cool things like; distorting space and the map, spinning, dizzyness, neon glowing, perception warping, and a bunch of other f?#%! amazing things!

Here is the full lighteffect file.

    
	sampler s0;  
		
    texture lightMask;  
    sampler lightSampler = sampler_state{Texture = lightMask;};  
      
    float4 PixelShaderLight(float2 coords: TEXCOORD0) : COLOR0  
    {  
        float4 color = tex2D(s0, coords);  
        float4 lightColor = tex2D(lightSampler, coords);  
        return color * lightColor;  
    }  

	      
    technique Technique1  
    {  
        pass Pass1  
        {  
            PixelShader = compile ps_2_0 PixelShaderLight();  
        }  
    }  

 

Now, don’t get overwhelmed at this code if you aren’t familiar with HLSL. Basically, this effect will be called every time we draw the screen (in the Draw() function). This .fx file manipulates each pixel on the texture that is loaded into it, in this case it would be the sampler variable.

 

sampler s0;

This represents the texture that you are manipulating. It will be automatically loaded when we call the effect. s0 is a sample register that SpriteBatch uses to draw textures, so it is already initialized. Your last draw function initializes this register, so you don’t need to worry about it!

(I explain more about this below)

 

 

RenderTarget2D
Render targets are textures that are made on the fly by drawing onto them using spriteBatch, rather than drawing directly to the back buffer.

 

texture lightMask;  
sampler lightSampler = sampler_state{Texture = lightMask;};  

The lightMask variable is our render target that will be created on the fly using additive blending and our light’s locations. I’ll explain more about this soon, here we are just putting the render target into a register that HLSL can use (called lightSampler).

 

Before I can explain the main part of the HLSL effect, I need to show you what exactly is happening behind the scenes.

First, we need the actual light effect that will appear over our lights.

lightmask

I’m showing you this version because the one that I use in the demo is a white transparent gradient, it won’t show up on the website.

If you want a link to the gradient that I used in the demos above, you can find that here.

Otherwise, your demo will look like the image below.

You can see black outlines around the circles if you look close.
lightmaskdemo

 

Whatever gradient you download, call it

lightmask.png

Moving into your main game’s class, create a couple variables to store your textures in;

public static Texture2D lightMask;
public static Effect effect1;
RenderTarget2D lightsTarget;
RenderTarget2D mainTarget;

Now load these in the LoadContent() function. lightMask is going to be lightmask.png

effect1 will be lighteffect.fx

This is how I initialize my render targets:

var pp = GraphicsDevice.PresentationParameters;
lightsTarget = new RenderTarget2D(
GraphicsDevice, pp.BackBufferWidth, pp.BackBufferHeight);
mainTarget = new RenderTarget2D(
GraphicsDevice, pp.BackBufferWidth, pp.BackBufferHeight);

With that stuff out of the way, now we can finally focus on the drawing.

In your Draw() function, lets begin by drawing the lights Target;

GraphicsDevice.SetRenderTarget(lightsTarget);
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Additive);
//draw light mask where there should be torches etc...
spriteBatch.Draw(lightMask, new Vector2(X, Y), Color.White);
spriteBatch.Draw(lightMask, new Vector2(X, Y), Color.White);

spriteBatch.End();

Some of that is psuedo code, you have to put in your own coordinates for the lightMask. Basically you want to draw a lightMask at every location you want a light, simple right?

What you get is something like this: (The light gradient is highlighted in red just for demonstration)

 

lightmaskdemo2

Now in simple, basic theory, we want to draw the game UNDER this texture, with the ability to blend into it so it looks like a natural lighting scene.

If you noticed above, we draw the light render scene with BlendState.Additive because we will end up adding this on top of our main scene.

What I do next is I draw the main game scene onto mainTarget. What works here for me will not work for you, unless you have followed my camera & map handling tutorial.

GraphicsDevice.SetRenderTarget(mainTarget);
GraphicsDevice.Clear(Color.Transparent);          
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, null, null, null, null, cam.Transform);
cam.Draw(gameTime, spriteBatch);
spriteBatch.End();

Okay, we are in the home stretch! Note: All this code is sequential to the last bit and is all located under the Draw function, just so I don’t lose any of you.

So we have our light scene drawn and our main scene drawn. NOW we need to surgically splice them together, without anything getting too bloody.

We set our program’s render target to the screen’s back buffer. This is just the default drawing space for the client’s screen. Then we color it black.

GraphicsDevice.SetRenderTarget(null);
GraphicsDevice.Clear(Color.Black);





Now we are ready to begin our splice!

spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend);

effect1.Parameters["lightMask"].SetValue(lightsTarget);
effect1.CurrentTechnique.Passes[1].Apply();                         
spriteBatch.Draw(mainTarget, Vector2.Zero, Color.White);               
spriteBatch.End();

We begin a spritebatch who’s blendstate is AlphaBlend, which is how we can blend this light scene so smoothly on top of our game.

Now we can begin to understand the lighteffect.fx file.

Remember from earlier;

sampler s0;     
texture lightMask;  
sampler lightSampler = sampler_state{Texture = lightMask;}; 

We pass the lightsTarget texture into our effect’s lightMask texture, so lightSampler will hold our light rendered scene.

tex2D is a built-in HLSL function that grabs a pixel on the texture at coords vector.

Looking back at the main guts of the effect function:

float4 color = tex2D(s0, coords);  
float4 lightColor = tex2D(lightSampler, coords);  
return color * lightColor;

Each pixel that we find in the game’s main scene (s0 variable), we look for the pixel in the same coordinates on our second render scene — the light mask (lightSampler variable).

This is where the magic happens, this line of code;

return color * lightColor;

Takes the color from our main scene and multiplies it by the color in our light rendered scene, the gradient. If lightColor is pure white (very center of the light), it leaves the color alone. If lightColor is completely black, it turns that pixel black. Colors in between(grey) simply tint the final color, which is how our light effect works!

 

Our final result (honoring the color red for demonstration):

Screenshot 2015-07-23 01.47.07

 

One more thing worth mentioning, effect1.Apply() only gets the next Draw() function ready.   When we finally call spritebatch.Draw(mainTarget), it kicks in the effect file. s0‘s register is loaded with this mainTarget and the final color effect is applied to the texture as it is drawn to the player’s screen.

 

If you have any questions, please leave them in the comments below and I will do my best to clarify anything. Be careful using this in your existing games, changing drawing blend states and sort modes could funk up some of your game’s visuals.

 

 

 

 

 

Kunga
August 10th, 2015 at 9:30 am

Well, what about let’s say blue background and red light? You will see pitch black 😉

Kobi
August 18th, 2015 at 11:02 am

Hello,
Great tutorial. I had some problems implementing it.
Do you have a sample?

Cody Red
December 11th, 2015 at 11:30 am

Hey,
Unfortunately I don’t have a sample, I locked my old computer away in storage. What are you having trouble with maybe I can try to help?

David
September 10th, 2015 at 9:24 am

Thank you for the tutorial.

I am currently trying to implement Krypton Light Engine into my project but I cannot render interface over the shadow, like the way you have drawn fps and update counters.

I would like to attempt your method but I might come across the same issue. Where is the draw for interface supposed to occur so the numbers are not rendered under the ambient light.

Cody Red
December 11th, 2015 at 11:35 am

I’m not familiar with Krypton Light Engine, but I would try drawing your interface after everything else is drawn, to put it in front of everything.

Henrique
December 9th, 2015 at 2:41 am

My screen goes black..
can you help me?
protected override void Initialize()
{
graphics.PreferredBackBufferWidth = 800;
graphics.PreferredBackBufferHeight = 600;
var pp = GraphicsDevice.PresentationParameters;
lightsTarget = new RenderTarget2D(
GraphicsDevice, pp.BackBufferWidth, pp.BackBufferHeight);
mainTarget = new RenderTarget2D(
GraphicsDevice, pp.BackBufferWidth, pp.BackBufferHeight);
graphics.ApplyChanges();
base.Initialize();
}

protected override void Draw(GameTime gameTime)
{
GraphicsDevice.SetRenderTarget(lightsTarget);
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
spriteBatch.Draw(dummyLight, new Vector2(this.GraphicsDevice.Viewport.Width / 2 – (dummyLight.Width / 2), this.GraphicsDevice.Viewport.Height / 2 – (dummyLight.Height / 2)), null, Color.OrangeRed, 0f, Vector2.Zero,
1f, SpriteEffects.None, 0f);
spriteBatch.End();
}

Cody Red
December 11th, 2015 at 11:40 am

There are no options set in your spriteBatch.Begin().
Try spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Additive);

Mattijs Leon
January 10th, 2016 at 8:55 pm

How do you load the effect exactly and where do I put the file, in the project or the projectContent solution. I have effect1 = Content.Load(“lighteffect.fx”) but that doesn’t work.

Mattijs Leon
January 11th, 2016 at 2:09 pm

Never mind, I was being dumb 🙂

tobi
October 18th, 2016 at 6:03 pm

Hey, I get a failure while compiling the .fx with MonoGame content Pipeline. I copied your .fx code.
Thats the Error:

Build started 18.10.2016 20:02:19

C:/Users/warth/Source/Repos/Good-Feel-Inc/TobisGameEngine/Content/Shaders/testShader.fx
C:/Users/warth/Source/Repos/Good-Feel-Inc/TobisGameEngine/Content/Shaders/testShader.fx: C:\Users\warth\Source\Repos\Good-Feel-Inc\TobisGameEngine\Content\Shaders\testShader.fx(7,15) : Unexpected token ‘l’ found. Expected LessThan or OpenParenthesis.
C:\Users\warth\Source\Repos\Good-Feel-Inc\TobisGameEngine\Content\Shaders\testShader.fx(7,24) : Unexpected token ‘;’ found. Expected GreaterThan or CloseParenthesis.

Build 0 succeeded, 1 failed.

Time elapsed 00:00:00.23.

Cody Red
January 23rd, 2017 at 1:35 am

Don’t just copy and paste, dig a little into it. Looks like you might have a typo somewhere or an additional ;

James Rodriguez
November 24th, 2016 at 8:43 pm

Getting an error on effect1.CurrentTechnique.Passes[1].Apply();
Changed it to effect1.CurrentTechnique.Passes[0].Apply(); and now I’m getting a black screen.
However if I change the drawing order to;

spriteBatch.Draw(mainTarget, Vector2.Zero, Color.White);
effect1.Parameters[“lightMask”].SetValue(lightsTarget);
effect1.CurrentTechnique.Passes[1].Apply();

Then I get the maintarget but no light effect. Any help

Cody Red
January 23rd, 2017 at 1:34 am

Hmm what does your .fx file look like?

Will Bender
January 24th, 2017 at 10:46 pm

I am having the same exact problem where I get an “out of index error when it is
effect1.CurrentTechnique.Passes[1].Apply();
and a black screen when it is
effect1.CurrentTechnique.Passes[0].Apply();

This is my effects file. I had to add around lightMask and I had to change from ps_2_0 to ps_4_0_level_9_1

sampler s0;

texture lightMask;
sampler lightSampler = sampler_state{Texture = ;};

float4 PixelShaderLight(float2 coords: TEXCOORD0) : COLOR0
{
float4 color = tex2D(s0, coords);
float4 lightColor = tex2D(lightSampler, coords);
return color * lightColor;
}

technique Technique1
{
pass Pass1
{
PixelShader = compile ps_4_0_level_9_1 PixelShaderLight();
//PixelShader = compile ps_2_0 PixelShaderLight();
//PixelShader = compile ps_4_0_level_9_1 PixelShaderFunction();
}
}

Will Bender
January 24th, 2017 at 10:50 pm

sampler lightSampler = sampler_state{Texture = (lessthansign)lightMask(greaterthansign);}; My greater than signs were removed from the comment.

Will Bender
January 25th, 2017 at 3:51 am

I fixed it. I needed to define my pixel shader function as

float4 PixelShaderFunction(float4 pos : SV_POSITION, float4 color1 : COLOR0, float2 coords: TEXCOORD0) : COLOR0

http://www.software7.com/blog/pitfalls-when-developing-hlsl-shader/

for it to work. However, my lighting is in an ellipse shape now. Working on that.

Demid
March 13th, 2017 at 9:21 am

Is this a “light-only” engine? Does it have shadows?

Don
May 31st, 2017 at 8:34 pm

Is there a way to get the lightmask you’ve used? Unfortunately the link seems to be broken :/

LEAVE A COMMENT

theme by teslathemes