Advertisement

2D Top Down Open World Lighting

Started by August 02, 2018 12:31 PM
6 comments, last by Scouting Ninja 6 years, 3 months ago

I'm working on a 2D project with large and open maps, and am looking for ways to tackle large numbers of lights in such an environment. I have something that technically works but feel it is the wrong approach.

 

Generally what I have been doing in 2D games for lighting is drawing all the lights additively to get like a simple lightmap on a flat plane, then multiplying that with the rendered scene.

Previously these have only been lights I defined by having the lights as textures/sprites of appropriate colour and brightness, and with the view focused on a character can only see a few and it looked OK not using a proper lighting curve (way more linear so as to reach 0 after a few hundred pixels) in confined spaces. But it doesn't look so good in an open map, especially when zoomed out with lights feeling like spots and very dark areas between.

Now just computing the proper light value in a pixel shader for any distance and brightness is fairly simple, "intensity / distance^2", this has the problem with never reaching zero (in fact with just that equation, Id need a fullscreen quad for every point light in existance...).

I thought of just limiting it to a value that does not contribute much, e.g 1/255 of the highest of RGB (so if a light has an RGB of say 1.0, 0.9, 0.9 at 1m, I only draw it up to about 16 metres). But these quads are still massive and kill performance for 100's of lights. For example with just 15 lights:

image.png.94f8de1827b06056e22ff20daa35540a.png

Worse if there is multiple lights a similar distance from a point, the cutoff is clearly visible as they would have accumulated to something significant (consider that a lot of brighter LED lights are just a pack of individually less powerful LED's). Rendering each light out to a further distance with this solution will have even more performance consequences, while not fully solving the problem.

image.png.c4b6397e3b420fd5d2cf91c7d22a6aed.png

Is this a sensible lighting approach? Is there another equation to use that looks real while reaching 0 quickly? Do games do something clever to "combine" light sources at greater distances?

At this time I am not worrying about shadows as making 2D angled objects cast correct shadows presents a bunch of new problems, but I may want to in the future if can be done without having 3D models for every building, etc.

 


float4 main(Input input) : SV_TARGET
{
    float4 delta = input.light_position - input.world_position;
    float dist_sqr = dot(delta, delta);
    return input.color / dist_sqr;
}

 

11 hours ago, SyncViews said:

Is there another equation to use that looks real while reaching 0 quickly?

Actually your formula is correct but you don't have a infinite universe to work with. This is where inverse (1-x) is used.

The problem with 1-x is that it should be written as: 100% - x where if x <= 100%. But 1-x is easy to remember.

 

So if our intensity is 1 and our MaxDistance is 200pixels = 1/200 = 0.005. What that means is that every pixel we have to reduce the light by 0.005 to reach 0 at 200pixels. Because (0.005 * 200 = 1).

Linear formula = Intensity / MaxDistance

So to inverse it we use (intensity - ((Intensity / MaxDistance)*distance)). We use distance as the current distance from the center, let's try it:

0pixels = (1- (0.005*0)) =  1-0 = 1. || 50pixels = ( 1-(0.005*50)) = 1 - 0.25 = 0.75 || 100pixels = ( 1-(0.005*100)) = 1 - 0.5 = 0.5 And last 200pixels = 1-(0.005 *200) = 1-1 = 0.

1 -> 0.75 -> 0.5 -> 0.25 -> 0 (linear formula) (lerping between 1 -> 0)

 

Now the fall of formula you had is the Inverse-square law and is correct as you are using it, but games are not infinite, so we need to cap it using the inverse formula:

(intensity - ((Intensity / MaxDistance)*(sqrt(distance)*sqrt(MaxDistance)). Lets test:

0pixels = 1- (0.005*0) =  1-0 =1.

50pixels =  1-(0.005*(sqrt(50)*sqrt(200)) = 1 - (0.005* 7.071* 14.142 ) =1 - 0.5  = 0.5

100pixels = 1-(0.005*(sqrt(100)*sqrt(200)) = 1 - (0.005* 10* 14.142 ) =1 - 0.707 = 0.293

150pixels = 1-(0.005*(sqrt(150)*sqrt(200)) = 1 - (0.005* 12.247 * 14.142 ) =1 - 0.866 = 0.134

And last 200pixels = 1-(0.005*(sqrt(200)*sqrt(200)) = 1 - (0.005* 14.142* 14.142 ) = 1 - (0.005* 200 )  =1 - 1 = 0

1 -> 0.5 -> 0.293 -> 0.134 -> 0 (Inverse-square law) (circle lerping 1 -> 0).

If you know your Sin and Cos functions, you will realize these values correspond to the important angles, like Cos(45) = 0.707 what this means is your falloff should always look round and neat, and intensity scales the sphere, while distance decides the clamp.

Note: if intensity is bigger than 1 you need to adjust the formula to keep it 1-x:

Last 200pixels with intensity 4 = 4- ((0.005*(sqrt(200)*sqrt(200))*4) = 1 - ((0.005* 14.142* 14.142 )*4) = 4 - ((0.005* 200 )*4)  =4 - 4 = 0

The full formula: (intensity - ( (Intensity / MaxDistance) * ( sqrt(distance)*sqrt(MaxDistance) ) *intensity)

 

Hope this helps.

Advertisement

I think I got this right. I did find that intensity was multiplied in twice giving negative numbers, so I removed one: "(intensity - ( (Intensity / MaxDistance) * ( sqrt(distance)*sqrt(MaxDistance) ) *intensity)".

 

Assuming I got it right, gives a much flatter light with a sharp falloff, need to think about if that is desirable. With a max distance of 16, which also need some experimentation:

One Light:

image.png.bea65ced68a8fab1c06a2530d13ed029.png

Multiple Lights:

image.png.fbc2b0937a9b473626294a8b53d08d53.png

Before:

image.png.d89cfe3d5647f1127e8a3ad8228376c5.png

image.png.6c065b0fd3a224e5dfb085315e73e74c.png

 

EDIT: Picking the distance also has a huge impact, so need to experiment with ways to determine that. Images use 16m, but as can see in the chart if I got it right, going to say 10m makes it brighter when very close (assuming keep 1m = 1.0) and darker after.

  

Tried some different approaches to equations based on what you suggested using different equations I came up with.

So I was thinking to keep the quick and then gradual fall off, Id want to keep the "1/d^2" component somewhere in the equation, but then combine it to hit zero while keeping the curve very close. Two ideas, one is to just calculate "1/d^2" for my desired distance then offset the whole thing (`0 = 1/d^2 - 1/max_dist^2`), the other to multiply based on another function that reaches 0, such as `1 - d^2 / max_dist^2`:

image.png.b74ab27a1eb742d01152c611f302f98a.png

image.png.d5f9b18d711e7cc66c860f101929ec7c.png

5 hours ago, SyncViews said:

I think I got this right. I did find that intensity was multiplied in twice giving negative numbers, so I removed one

Floating point errors? Could cap it with a min() to stop this.

5 hours ago, SyncViews said:

Assuming I got it right, gives a much flatter light with a sharp falloff,

Yes, that looks right. It is a sphere formula, ment for rendering the top half of a sphere:

LightFormula.jpg.76dc076414bd674731fb537da7d9f71c.jpg

Here you can see how I am using it with a offset formula in my own game, to create the shield impact depending on what side the shield is hit. This image, better shows how it is a 3D sphere.

 

2 hours ago, SyncViews said:

Tried some different approaches to equations based on what you suggested using different equations I came up with.

Good too see you catch on so quick to it. I like the last one, it accumulates perfectly.

Some things you can try is to add as a extra is a gradient reader, that allows you to enter 2D sprites and use them as a light falloff. Like this:

2DGrad.png.5a5e40cab29b52ad0ee10deedf1cf7de.png

The nice thing about this will be that you can hand edit the curve, making it very easy to adjust lights and even use sprite sheets to create flickering lights.

If you also want to add some electric torch lights you can even implement IES lights. These have nice effects:

ies-lights.jpg?version=1&modificationDate=1373453636000&api=v2

23 minutes ago, Scouting Ninja said:

Floating point errors? Could cap it with a min() to stop this.

No, it was pretty far off, "(intensity - ( (Intensity / MaxDistance) * ( sqrt(distance)*sqrt(MaxDistance) ) *intensity)".
 e.g. given intensity=4, distance=4, maxDistance=4

4 - ((4 / 10 * (sqrt(10)*sqrt(10)) * 4 you get -12. Dropping that last multiply intensity gives 0, since already have it in the 4/10 part so I am guessing that is how was intended?

 

23 minutes ago, Scouting Ninja said:

Some things you can try is to add as a extra is a gradient reader, that allows you to enter 2D sprites and use them as a light falloff. Like this:

Having a 1D texture is a good idea. Having the entire light as a 2D sprite previously meant the images got pretty large, and possibly should have gone to 16bit to avoid some banding issues, and maybe the possibility to go over 1.0 (I guess technically what I have already could if used say a FP16 target but need some more thought on that).

If you also want to add some electric torch lights you can even implement IES lights. These have nice effects:

Those look really nice. I has been thinking of ways to do spot lights (for search lights, helicopters, headlights, etc.) based on the pixels direction relative to a direction vector (so fades off towards zero at the edge of the spots "triangle"). For those are you sampling the texture based on distance and the angle from the spots direction vector  in the pixel shader? Of course maybe going back to just a sprite for 2D would work just as well in the end

 

47 minutes ago, Scouting Ninja said:

Here you can see how I am using it with a offset formula in my own game, to create the shield impact depending on what side the shield is hit. This image, better shows how it is a 3D sphere.

Shield domes are pretty cool, but think would give me problems in 2D (intersecting buildings and the like), as much as I think they look great in big battles.

Advertisement
1 hour ago, SyncViews said:

already have it in the 4/10 part so I am guessing that is how was intended?

Yes that is correct, I must have missed a bracket or something in the formula; could also be because I have intensity and power.

 

1 hour ago, SyncViews said:

Shield domes are pretty cool, but think would give me problems in 2D (intersecting buildings and the like), as much as I think they look great in big battles.

Actually I don't use it as a pure dome. I have a flat polygon that is the shape of the ship, above the ship that constraints the effect:

Shield.gif.ebfd979890f338df454dfb4889a73374.gif

I am not ready to reveal much about the game yet, as you can see it is still in the early stages and only now the art is starting to pickup.

You could make a similar polygon inside of buildings to cap the effect.

 

This topic is closed to new replies.

Advertisement