Steam
BlogContact
<- back

Dev-Log: Ray Traced Shadows

6/6/2026

Every now and then I try to experiment with the visuals of my game to see if there's anything I can improve on. This time I wanted to see if I could implement dynamic shadows to make my game look a little less flat.

This was hugely inspired by the shadows in Rain World and Iron Bramble.

First: Results!

The top video is the before and below is the after.

It's a very subtle change, but I think it achieves my goal of giving my world more visual depth.

First of all, my game is made in (my custom fork of) Godot so some of the implementation details will be specific to that, but the general idea can be applied to other environments.

Alright, onto how I made this. Since I was inspired by Rain World I first tried to see if there were any resources on how they implemented their shadows. I found this Twitter thread which explains nicely how they approached their implementation. I used this as a starting point, but ended up diverging from it and working on my own approach.

Rendering the depth map(s)

The first thing we need is a depth map of our scene. I did this by assigning each object (CanvasItem) in my scene a depth value and an optional depth texture in case I want the depth to vary across the object. Each object also has a boolean called is_solid which decides if the object is solid (meaning that no light rays can pass behind the object), or if the object is flat (meaning that light can pass behind the object, just not through the object). Things like doors are solid since I don't want any light to pass through them. Additionally, every light source also has a depth, which comes into play later.

Then I create two additional render passes (using SubViewports in Godot) which render my depth maps. They use the aforementioned depth values and textures and calculate the objects total depth based on those (depth value +/- depth in the texture). The first pass renders all the solid objects, the second one all the flat (non-solid) ones. These get stored in textures and are used in my shader.

Technical Note

The depth value was implemented by adding a depth property to the CanvasItem node.
The depth texture was done by adding a depth texture property to the CanvasTexture resource.
Both of these properties get passed onto the canvas shader.


The render passes were done by adding an overwrite material property to Viewports. This overwrite material gets applied to all elements rendered in the viewport and handles the depth.

The depth maps end up looking something like this:

First the solid one, second the flat one.

Rendering the shadows

Now that we have depth maps "all" that is left is to actually calculate the shadows. This is done in my main shader which is used for every CanvasElement. I have a function in my fragment shader that gets executed for every fragment of every light source.

The basic idea is this: We shoot a ray from our current position to the light's position and if we hit anything, then we are in shade.

In detail this means the following:

  1. We get the lights depth value. If the depth value is 0.0 then no shadows will be drawn (we don't want this light to draw shadows).
  2. Then we get the current position by taking the SCREEN_UV.
  3. Then we sample both depth maps (the solid one and the thin one) at the current position and save the higher depth as our start depth value.
  4. If our start depth is higher than the light depth no shadows will be drawn (the object is in front of the light).
  5. We then assemble two vec3s one is the spatial start position (start_pos.xy, start_depth), and one is the spatial light position (light_pos.xy, light_depth).
  6. We then step from the spacial start position to the spacial light position in increments of a constant step size (This step size determines how well we catch every shadow caster. A step size of half a pixel has worked well for me).
  7. At every interpolated position we first sample the solid depth map and check if our current interpolated depth is less than the sampled depth. If it is smaller, then that means there is a solid object casting a shadow on us, and we can break out of the loop.
  8. After not finding a solid object casting a shadow on us, we sample the thin depth map. If our current interpolated depth is within a small error margin of our sampled depth, then that means there is a thin object casting a shadow on us, and we can also break out of the loop.
  9. If we don't find either a solid or a thin object casting a shadow on us, we continue stepping towards the light.
  10. If we reach the light without finding any shadow casters, then that means that we are in view of the light and we are fully lit.
  11. Finally, we multiply the current Light color based on if we found a shadow or not. Multiplying it with 0.0 would mean that things in shade are fully unlit. I've also found that interpolating the factor based on how far away the shadow caster is gives a nice decaying shadow effect.

That's basically it. As you can imagine tracing a ray for every fragment can get quite expensive and only works since my games resolution is so small (320x180). I've also included an option to disable ray traced shadows in case weaker machines struggle doing all of those calculations.

Technical Note

I added a custom function to the base canvas shader that I can overwrite in my shaders that gets called for every light. It is similar to the light function but does not overwrite any default light behaviour, so a similar implementation could definitely be implemented using the default light function.

For any further questions feel free to contact me.

-soraslove
<- back