Rendering Real-Time Shadows in WebGL Using Shadow Volumes

Shadows undoubtedly make 3D scenes look more realistic. I’ve been striving to implement proper shadows since I got into WebGL. Turns out this is not as straightforward as using the box-shadow property in CSS!

My first attempt was to make Thibaut’s snake game look nice in 3D. You can play it here. The idea was to render the same object for the second time, but applying a special matrix transform, that smashes it down on the ground in the direction of light. This worked fine for the snake game, where the shadows only needed to be on the floor.

However, this method doesn’t work if you need to cast shadows on other objects — which was the situation that I faced when implementing examples for elm-physics. I still chose to go with shadows on the floor, because it looked better than no shadows at all.

My second attempt was to use shadow maps, which currently is the most widely used method. Shadow maps require multipass rendering: you first need to render the scene from the light’s point of view onto a temporary buffer, and then render the same scene again from the camera, while looking up the shadow pixels from this buffer. Unfortunately, this cannot be done using the current Elm WebGL. I spent a lot of time prototyping possible solutions, with the help of Michel van der Hulst. It turned out that such functionality cannot be properly implemented on top of the existing declarative API while at the same time being both performant and nice to use.

Does it mean we cannot render shadows in Elm? Not until now!

Ian Mackenzie once mentioned to me that he was planning to implement shadows using shadow volumes. I got really excited about the topic. After looking into the JavaScript implementation using regl I realized that this should be possible in Elm.

After returning from Elm Europe, I proposed Ian to pair program the solution. He wanted to do it by reading the Real-Time Rendering book, instead of simply porting from JavaScript. This way we could grasp the idea and understand all the math and logic behind it. We used VS Code Live Share functionality that allowed us to share the editor session, elm reactor and audio call at the same time. It worked mostly well, although it seemed that only the host could trigger saving a file.

It took us just two sessions to implement a working solution, thanks to Ian’s extensive knowledge in 3D maths. A shadow volume is created from the original object by extending its silhouette in the direction of light. The scene is first rendered as if everything was in shadow. Secondly, the invisible shadow volumes are rendered in order to create a mask in the stencil buffer. Lastly, the scene is rendered again, but only the highlighted parts, masked by the stencil buffer.

The final prototype can be seen here. On desktop, you can orbit the scene by dragging with the mouse. If you’re interested in implementation details, check the source here. If you want a high level API to render realistically looking scenes in Elm, wait a few months until elm-conf, where Ian will present his 3D rendering engine!


This was a lot of fun to help put together! Andrey is fantastic to pair with =) And keep in mind that Live Share was only maybe a week or two old at the time, so I’m sure the bugs will get ironed out over time.


whoa congratulations


Cool! My first job was actually programming shadows in a WebGL application :slight_smile:
I vaguely remember the stencil buffer not being supported at the time, I guess this is a WebGL 2 thing?

BTW, were can I find a link to the RTT proposal / discussion? I’m curious about the exact issue.

I think stencil buffer support is always guaranteed, even in WebGL 1 without extensions (which is what Elm targets). See, which seems to say that if you pass stencil = true when initializing a context then “the drawing buffer has a stencil buffer of at least 8 bits”.


Re: RTT: there was an open discussion in this issue:

1 Like

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