Realtime 2D Lighting with Shadows on Isometric Tiles in Godot 4.4
- Connor Wolf
- Jun 6
- 7 min read

A few months ago when I was working on Empty Spiral, I had a dream. Using Godot's built-in 2D lighting engine, I wanted to light my world with beautiful lights and deep shadows. Unfortunately, that project was pretty large for me to handle, and though I tried on several (frustrating and lengthy) occasions to get it working, I came up empty.
Recently, Godot 4.4 released, and with it came a handful of bugfixes aimed at the 2D lighting system. I saw a wonderful tutorial on YouTube by CashewOldDew that broke down Godot's lighting system in extreme detail. If you're looking for a crash course on the system, I highly recommend the video. After watching it, I felt inspired and energized to try taking another crack the problem. This time, I got it.
So, this post serves as a tutorial for how I got my lighting set up. If you are like me and were frantically searching the internet for any sort of documentation on how to do this, rejoice.
What To Expect
This system is built using Isometric TileMap Layers, Godot's Built-In Lighting (Occluders, CanvasModulate, Point/Directional Lights), and a handful of Unshaded Shader Materials. I find that last one to be particularly important to call out because a lot of 2D sauce comes from custom shaders. In this system, any object that you want to be unaffected by lights must use an unlit shader. In addition, we'll also be using z-index to control the levels of the TileMap Layers.
What You'll Need
Godot 4.4+
A set of tile sprites
A sprite-editing program (ie. Aesprite, Piskel, Paint, Photoshop, etc.)
For this tutorial, I'll be using Aesprite and a pixel-art tileset I made. If you've never done isometric pixel art, I really like this video by Brandon James Greer as an explanation and application.
Step 1: Tilesets & TileMap Layers
To start, we're going to set up an environment we can apply the lighting to. We'll do this using Godot's Tilesets and TileMap Layers. (Note: I have not tested this with the now deprecated TileMap, so if you're trying to use that proceed at your own risk.)

Create a Node2D to serve as the Main scene.
Create two different TileMap Layers, "Ground" and "Obstacles".
Enable Y Sort on both.
Set the Z Index of the "Ground" layer to -1.
Make sure their Texture Filter is set to Nearest if using pixel-art.
Create a new Tileset, and save it as a Resource in your project.
Set this Tileset's Tile Shape to Isometric.
Adjust the Tile Size to fit your sprites. (In this tutorial, 32x16 for 32x32 sprites.)
Add your spritesheet into the Tile Sources.
When prompted, do not automatically create tiles.
In the spritesheet Atlas, change the Texture Region to fit your sprites.
Use Godot's three-dot menu to create tiles for your sprites.
While in Select mode, shift click on each of your Base Tiles to select them.
Adjust the Texture Origin under Rendering to put the origin of your sprites at their bases. (For 32x32 sprites, this will be 8 px down.)
We're now at the point where we can use our Tileset and TileMap Layers to create a level. Feel free to draw whatever type of level you would like, but be sure to include a good-sized floor on the "Ground" layer and a few tiles on the "Obstacles" layer as well.
Step 2: 2D Lighting
If you have never used Godot's 2D Lighting, I recommend watching the tutorial linked at the top of this post. For this tutorial, we'll be using a Canvas Modulate node to tint our screen, and PointLight 2D nodes to provide out light.

Add a Canvas Modulate node to your scene.
Adjust the Color attribute to represent the darkness level of your scene.
Add a PointLight 2D to your scene.
For the Texture, create a new GradientTexture2D.
Set the height of the Texture to be half of the width.
In the Texture's Gradient, change the black color to a transparent white.
Reverse the Gradient, so the transparent color is on the right.
Pull the transparent color back to about halfway between the gradient.
Set the Texture's Fill mode to be Radial, and the From value to be 0.5 and 0.5.
Use the Texture Scale to resize the PointLight 2D as you see fit.
You may also wish to decrease the Energy of the light in the case of washout.
At this point, you should have a pair of TileMap Layers that are being lit. You can change the color of your PointLight 2D and your Canvas Modulate to add more interesting-looking lighting to your scene rather than simple white and black.
Step 3: Shadows
At this point, we essentially have a spotlight that we can shine wherever we'd like in our scene. In order to make the lighting seem realistic, we'll have to use Godot's Light Occluders to block light from passing through our Obstacle tiles. We'll also need to make use of Light Masks to ensure that the correct TileMap layers are receiving shadows.

In your PointLight 2D, enable Shadows.
Feel free to add a Color to your Shadow, which will modulate the Shadows this light casts by that color.
Head back into your Tileset, and open the Rendering dropdown.
Open the Occlusion Layers dropdown, and add a layer.
Adding this layer will let us tell Godot which parts of our tiles should block light.
Select a tile on the Obstacles layer.
Under Rendering ~> Occlusion Layer 0, draw the shape that should block light.
I recommend drawing this at the BASE of the tile, but you can also do the top.
At this point, your tiles should be casting shadows, but you'll notice there's something a bit strange about the shadows. When lit from below, your tiles will appear completely normal, but if your PointLight 2D is above your tiles, the shadows will be cast unnaturally on them.
The way we fix this is by making use of a complex feature in Godot called Light Masks. Simply put, Light Masks allow you to determine what aspects of a light affect certain objects. Using Light Masks, we'll prevent the tiles on the "Obstacle" layer from receiving the shadows of our light. Since the tiles on the "Floor" layer will be unaffected, we'll still be able to see our nice clean shadows.

In your "Obstacles" TileMap Layer, scroll to the CanvasItem section, and open Visibility.
Change the Light Mask field from 1 to 2.
You'll now see that your tiles on the "Obstacle" layer continue to cast shadows onto the floor, while not receiving any of the light from your PointLight 2D. This lets you prevent the occluder from appearing on each one of your tiles.
Step 4: The Second Light
Now that we have shadows being properly casted onto our "Ground" layer, we need to adjust the tiles on our "Obstacles" layer to receive light. Since we can't use our existing PointLight 2D, we'll need to make a second PointLight 2D and use it to illuminate our "Obstacle" tiles. Unfortunately, we wouldn't ordinarily be able to control how light illuminates our tiles. Much in the same way our first PointLight 2D illuminates all tiles evenly, our obstacles won't appear natural if we simply add another light to the scene. Instead we'll have to get a bit technical, and use normal maps to light our tiles.
If you've never heard of normal maps as a 2D artist, you have nothing to be ashamed of! In fact, normal maps are much more common when talking about 3D art. Since isometric artwork tends to give the illusion of 3D, it makes sense that we'd have to borrow some techniques use there.

Normal maps are essentially textures that inform a rendering software how an object should be affected by light. They use a special color-wheel that represents which direction a light is coming from, and how powerful the light should appear based on that direction. There are some really nifty pieces of software that can generate normal maps automatically (See Laigter), but for this tutorial I'll show you how I made mine in Aesprite.

I start by opening up my file in Aesprite and selecting the Normal Map Color Wheel from the palette list. This wheel can then be used to determine what direction should light up what face of our sprite. For this exercise, I want the top of my obstacles to always be unlit, so I use the color in the center of the circle. Then I find the direction that the different faces on my sprite are facing, and grab the corresponding colors. I color in the faces those colors, and voila, I have a normal map for one of my sprites.
Now, since my spritesheet is filled with a series of perfect cubes, it's pretty easy for me to make the full normal map. Every cube should react to light in the exact same way, so I can just use the same normal map 16 times. I export this normal map into Godot so I can apply it to my tiles!
In order to use the normal maps with our tiles, we'll actually have to first create a special type of Resource called a CanvasTexture. This is essentially a way for us to add additional information to a texture that not all textures necessarily need.

Create a CanvasTexture Resource.
In the Diffuse dropdown, add your original spritesheet.
In the NormalMap dropdown, add your normal map spritesheet.
With your TileSet open, drag your CanvasTexture into the Tile Sources.
I recommend naming one Tile Source "Normals" and one "No Normals" to make it easier to keep track of which is which.
Do the same steps as before to set these tiles up!
When prompted, do not automatically create tiles.
In the spritesheet Atlas, change the Texture Region to fit your sprites.
Use Godot's three-dot menu to create tiles for your sprites.
While in Select mode, shift click on each of your Base Tiles to select them.
Adjust the Texture Origin under Rendering to put the origin of your sprites at their bases. (For 32x32 sprites, this will be 8 px down.)
Select a tile on the Obstacles layer.
Under Rendering ~> Occlusion Layer 0, draw the shape that should block light.
I recommend drawing this at the BASE of the tile, but you can also do the top.
Now we can replace any tiles on our "Obstacles" layer with the ones that are properly normal mapped. The last step is to finally add the second light.

Duplicate your PointLight 2D.
Add it as a child to the original PointLight 2D.
Disable Shadow on the new PointLight 2D.
In the new PointLight 2D's Range, change the Item Cull Mask from 1 to 2.
And you're done! Attach your light to whatever you'd like, and see how the shadows bounce around.
Final Thoughts:
This is something that I was trying to achieve for awhile, and I'm pretty happy with how it looks. There are certainly some limitations, and quite a bit of set up, but the joy of having 2D lighting in an isometric game is so exciting. I'm hoping to use this in a project I'm working on right now. Perhaps I shall share more soon!
If you have any questions, feel free to leave them here and I'll get to them when I get a chance.
Comments