Working around Godot's 2D light limitations for a dark game

Godot Version

Godot v4.2.1 and v4.3-stable

Question

My project is dark by default, and I’ve hit a wall trying to create a specific lighting vision. Every solution I’ve come up with brings its own problems.

Goal
-A dark gameboard grid with moveable game pieces.
-The screen and gameboard itself are fully dark while the game pieces are providing light.
-Lights from the pieces interact with normal maps on both the gameboard and the other pieces.
-Differently colored lights blend colors together when overlapping.

Solution - PointLight2D and Sprite2D nodes out of the box
-CanvasModulate node to darken the canvas to black.
-A gameboard made up of tiled Sprite2Ds with a CanvasTexture (solid white diffuse texture / normal map texture with white being highest point and black being lowest).
-Game pieces are instantiated from a scene containing a Sprite2D (textured similar to the gameboard tiles) and a PointLight2D (colored depending on game piece type.)

The idea being that PointLights add their color and light information to the sprite, which would show a rounded tile (thanks to the normal map) blended with the various colors of light that hit it.

Problem 1 - Cap on 2D Lights
When things weren’t working right, I searched further and discovered Godot’s cap of 15 for lights. Once a sprite renders lights to this cap it ignores any additional lights. Since my game pieces are 64x64 pixel sprites with 320x320 pixel light textures, and these pieces place right next to each other onto a grid of 64x64 squares, the light cap is reached very easily. There doesn’t seem to be a way to select which lights are rendered, so trying to prioritize either moving lights or the closest 15 lights doesn’t seem to be an option.

The most widely recommended solutions I found was to chop up the sprites into smaller bits or reduce the radius of light textures to avoid a situation where a sprite sees too many lights. Sprites are already only 64x64 pixels, but I decided to try compromising the effect and reducing the light texture to under 192x192 pixels. This reduced the lit area, and now a tile surrounded completely by other game pieces is only hit by 9 lights. Tiles moving over the pieces have a 6 light cushion (it still hits the cap when more tiles pass over it but it’s far less obvious at all times).

It compromises the full vision I had but it still seemed workable at this point.

Problem 2 - Mix Blend Mode Doesn’t Work
Lights kept adding their brightness to each other, washing out the colors and sprites and getting very bright. Height peaks on the normal map were behaving oddly when lights hit it from both sides. Changing the blend mode to Mix instead of Add solved both these visual issues, and since brightness is pretty similar on all my lights the interpolation between colors is not an issue.

However, PointLight2Ds set to Mix blend mode only work on sprites that are in the scene when the light is instantiated. Any sprites added to the scene after a light will not render that light at all. Doing nothing but changing the blend mode back to Add fixes the problem immediately, but nothing I have tried can make it work with Mix mode. I opened a bug report:

Whether it is a bug in the engine or some unique user error, I can’t find further information about solving this issue. Ultimately this makes my preferred solution unusable for the time being.

Solution - Additive Sprites:

-Use additive sprites for lighting instead of light 2Ds.

I found this suggested as a cheaper and easier way to handle lighting.

Problem - No Normal Maps
To my knowledge there isn’t a way to use normal maps using this method. Since the normal maps are the only piece of the Sprite2D that has visual information, any method that doesn’t use them won’t look right.

Solution - Lighting via Shader:
-Use a custom shader that takes in distance to all light locations, and does the math on the fragment pixels based on those distances, light colors, and normal map values.

Problem - Don’t Know What I Don’t Know
To this point I’ve used and made shaders only minimally, so I’d have to dive in and upgrade my knowledge to get something like this going. At the end of the day I don’t know enough to understand how feasible this is. A lot of the restrictions on 2D lights seem to be from trying to simulate a 3D space in a 2D environment, and redraws to the Canvas take a lot of resources.

If I go this route won’t I be faced with the same problems as those who made the original 2D light nodes anyways? Will I spend all my time on this only to find the same hard limits already imposed on me?

Solution - Make the Project in 3D

-Just do it in 3D. Lights are cheaper so go nuts.

Problem - Would Have to Give Up on Project Vision:
I’m smitten on the stylistic choice of dark 2D sprites with normal maps lit up by light textures. No one would confuse it for realistic 3D lighting but the unique look of it is the charm for me on this project. I would need to head back to the drawing board with my artist and re-conceptualize the art direction, then ask for all new assets.

Conclusion
I haven’t been able to solve my problem, and am not sure which, if any, of the solutions I’ve came up with are worth persuing. Does anyone have any suggestions, ways they’ve solved a similar problem, or see anything I’ve completely missed? Is my best bet to call it on the 2D lights and just convert to 3D, get the project out, and move on with my life?

Thanks in advance for any thoughts!

EDIT - Here’s what I ended up doing:

I switched over to using Node3D for my project. I couldn’t get Sprite3D nodes to work how I wanted with the normal maps but was able to come up with a work around.

I created a MeshInstance3D node, and added a new QuadMesh under its mesh property, and a new StandardMaterial3D under its material property.

For the material I made my solid color sprite the Albedo texture, enabled Normal Map, and added the normal map texture.

I’m using OmniLight3D with shadows disabled, and I’ve set the Camera3D projection property to Orthogonal to eliminate perspective.

The first thing I tried with the new setup was to float over 30 instances of my lit tile scene over a single large Quad. Unlike the 2D lighting, nothing was cut. For the time being I’m moving forward with this solution. It does look a little different than the 2D version but it’s close enough and doesn’t have the technical limitations I was running into trying to light up the 2D renderer.

3 Likes

I had a quick look and I found, lightoccluder2d, this mightn’t help or your already using it i wouldn’t i haven’t done anything to do with light and I haven’t done much with 2d

2 Likes

You pretty much hit the nail on the head regarding your write up of the lighting system. I’ve ran into this issue before as well and it’s frustrating for sure. Losing out on normal maps is a bummer

I don’t have a solution to the lighting issue but how does this sound for another potential workaround, if it fits the vision of your game?:

When two pieces and their lights get close, you can merge the two lights into one big light? As in hide/queue free the lights and replace them with a new light that mimics their combined color/other properties? When you create the new light, you can tell it how many ingredient lights it was created by, and then re-create those ingredient lights if the pieces move apart again.

This might be interesting if you have a chess board situation, if two opposing pieces are next to eachother it will create a new color. Say if team A has red lights and team B has green lights, when two pieces become adjacent it will mix the lights. Chess might be a bad analogy though since you’d probably confuse people if you had two knights next to eachother, since they can’t capture eachother but the lights are still getting mixed so that is sending mixed signals to the player. Just spitballing/brainstorming though, maybe your game has different rules of engagement

Also there is a Sprite3D node, so if you locked all the necessary rotations in 3D, you can probably recreate the 2D effect in a 3D world, using Sprite3D

2 Likes

LightOccluder2D is for blocking lights and creating shadows in the 2D light system. The only light effect I want is the directional information from the normal map, which is an odd request I know.

Thanks for checking into it and replying.

2 Likes

I played around with a similar idea at one point; using the concept of combining lights into one when close together. The problem with doing this is that PointLight2D nodes have a center point which the light radiates outwards from. Having the calculation point for the normal maps suddenly switch from multiple points to one point in between pieces was odd and jarring.

Honestly, I really like this idea. I’ve been trying to do all sorts of madness to get the 2D renderer to light up sprites when it wasn’t really designed to do so. I’m going to try playing around with my sprites in a 3D environment and report back.

Thanks for the suggestion!

2 Likes

Good luck! I’m interested to find out the results!

1 Like

I posted a more detailed update on the initial post, but I ended up taking your suggestion of recreating the 2D effect in a 3D space and built on that. Thanks again, your idea that got me thinking in the right direction!

2 Likes

I’m also looking for an easy way to solve the lighting problem.
Actually, my problem should be much simpler since I don’t need shadows and normal mapping.

https://www.reddit.com/r/godot/comments/1fl5yre/i_still_cant_find_the_best_way_to_handle_2d/

But the ground is tilemap, so reaching the 16 limit is very easy.

I’ve been told that I can try to split the tilemap into smaller pieces, but how do I make sure that the player doesn’t place many small torches in one small place when they may place torches in the future?

I hate lights.

Yup, you can definitely cut sprites smaller to try and avoid the limit, but to my knowledge there isn’t a workaround in the 2D renderer for that many small placed objects with overlapping 2D lighting nodes.

Since you don’t need normal maps or shadows, and are having problems with additive sprites, you may want to look into making your project in 3D. Lights in 3D have more options and far less restrictions than 2D lights. You’ll have to get familiar with 3D space and all it entails, but use an orthographic camera and you’ll be hard pressed to tell it’s not a 2D game.

Here’s my 2D light setup, you can have as many lights as you want with little performance impact. There’s no normal map or shadow though.

Copy the code below and save it as a .tscn file:

[gd_scene load_steps=5 format=3 uid="uid://dros00nqhabix"]

[sub_resource type="Shader" id="Shader_lcjdq"]
code = "shader_type canvas_item;
render_mode blend_add, unshaded;
// 2D light shader by HaruYou27.

uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, repeat_disable, filter_nearest;

uniform float intensity = 10.0;

void vertex()
{
	// Disable modulate color.
	COLOR = vec4(1.0);
}

void fragment() {
    // Sample the underlying screen color
    vec3 under_color = texture(SCREEN_TEXTURE, SCREEN_UV).rgb;

	// Make it brighter
    vec3 final_color = under_color * COLOR.rgb * intensity;

    // Don't forget the alpha value
     COLOR = vec4(final_color, COLOR.a);
}"

[sub_resource type="ShaderMaterial" id="ShaderMaterial_0xacm"]
shader = SubResource("Shader_lcjdq")
shader_parameter/intensity = 3.0

[sub_resource type="Gradient" id="Gradient_krxn2"]
interpolation_color_space = 1
offsets = PackedFloat32Array(0, 0.930435)
colors = PackedColorArray(1, 0.655333, 0.53, 1, 0, 0, 0, 0)

[sub_resource type="GradientTexture2D" id="GradientTexture2D_vdn31"]
gradient = SubResource("Gradient_krxn2")
width = 1024
height = 1024
fill = 1
fill_from = Vector2(0.5, 0.5)
fill_to = Vector2(0.852761, 0.874233)

[node name="BackBufferLight" type="BackBufferCopy"]
z_index = 2000
z_as_relative = false
copy_mode = 2

[node name="light-texture" type="Sprite2D" parent="."]
material = SubResource("ShaderMaterial_0xacm")
texture = SubResource("GradientTexture2D_vdn31")
3 Likes

If there is a god in this world, it must be you.

I forgot to mention this, the way my lighting work is by reading pixel of the screen. In order for it to work correctly, you must make sure to set the z index of the light-drawing node to as high as possible so it would be draw after everything. Also it can not be a child of torches Sprite2D node (I tried) as the lighting is incorrectly drawn on the torch sprite when u place a bunch of torches together.

Set Z-index of the light drawing node to as high as possible.
Separate the light drawing node from the rest of the scene (it must not be a child of any Sprite2D node or any node that display pixel on screen.)

I don’t understand what’s the problem with putting many torches together, I put many lights (more than 16) together and it works fine. This really solves my pain in Light2D. Is there a gotcha I’m not noticing?

1 Like

Nevermind, I fixed it. The light texture now ignore modulate color from it’s parents so you can freely dim the whole scene without affecting the lights.
I also change the z-index to 2000 + disable relative z-index so you can freely place it anywhere in the SceneTree.
Also if you set the copy mode of the BackBufferCopy node to viewport, it will produce a better quality lighting when you mixed different colored torches at some performance cost.
Keep in mind that this is not real lighting and won’t works if you tainted the scene completely black.

This is the first time I see someone sharing a resource file like this and this is awesome

I tested this shader with both “ambient” and “point” lights and it actually works. I only had to deal with dark environments, not completely black ones, so it works really well.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.