Shadows in wgpu

How does one get started on learning how to implement shadows into their game engine using wgpu?

shadows are a bit tricky because you essentially have to track the positions of all lights and objects that can cast shadows, i think looking into shadowcasters and how they are implemented could be a good start, the core principle is to take the geometry of an object that can cast shadow and projects it from the light source to the objects that can recieve shadows

There's a few approaches, but currently the two main options are some version of shadow maps or ray-traced shadows.

Assuming you don't have a ray-traced setup, you'll be doing shadow maps, which can get arbitrarily complicated (eg Unreal Engine 5's virtual shadow maps), but the basic idea is pretty simple in theory:

For each light:
  Render to a 1-channel depth texture (the shadow map):
    Camera at the light position
    Use a perspective projection containing the scene
    Write the depth to the texture if it's nearer
Render to the output:
  For each light:
    Determine the current pixel location in world position
    From that, get the distance to the light and position in the shadow map
    If the distance is greater than the stored depth, you're in shadow, continue to the next light
    Add the light's contribution as usual
  Render as before with the total light contribution

The devil, as usual, is in the details: there's a lot of subtle maths and traps that can trip you up.

Fortunately, WGPU provides a complete, self contained example you can follow along with:

I don't think i'm understanding which mental and api constructs I should be concerned with. Like, what's the requirement and what's the recommended and where do I place each new feature depending on my engine?

Unfortunately I don't know if there any better answers than "yeah, sorry: it's hard"

There's a bunch of tutorials and lots of existing code, but honestly all the details change depending on engine complexity and rendering API and even version of the same API.

A least to get you started, though, you should have something like a "scene" or "world" that contains everything you want to draw including their GPU resources, a "renderer" that contains the resources needed for specific rendering techniques (like pipelines and temporary buffers), and a "render context" that contains resources needed for drawing that the renderer passes to the scene.

Techniques like shadow maps and some mirror techniques mean your renderer sets up multiple "render passes" where in each you draw the scene with different camera, shaders and output target settings.

After that very high level approach (and even that's only a starting point), though, everything becomes a design choice. Especially once you start having to deal with things like culling and materials there's a lot of fiddly trade-offs that can require pretty large structural changes, but also make your engine your engine!

The only thing I can really mention here is you should absolutely look to make things as "bindless" as possible. wgpu provides a bunch of feature flags (at runtime, not cargo features) that let you provide things dynamically instead of having to declare them up front. It turns out that for modern platforms there's very little to strongly negative performance value in providing things up front, and it massively reduces the complexity of rendering to avoid binding things.

Rather than try to list these approaches here, I'll just link to the great How To Vulkan in 2026 - despite being for Vulkan specifically you can use most of these approaches in wgpu too, and the design and terminology is pretty close (wgpu is pretty clearly modeled on a simplified version of Vulkan)

Understood. Is there any reason why the example you linked creates a shadow view and then creates 3 more with different settings into a vector?

Each light source needs its own shadow map that is projected from the light’s position; in the example, there are 2 lights, each with its own target_view which is used in that light’s shadow pass. All of the shadow maps are stored as layers of a single array texture, and those views select which layer is written to.

The other texture view shadow_view is used when actually drawing the scene for display, for the shader to read the shadow maps and check whether each pixel is shadowed.

As for the design of why, I assume the main reason they went with a texture array (that is, Texture2DArray, texture2d_array resource type) is because it allows you to easily bind multiple textures as one binding without any features.

The trade-off is all shadow maps need to have the same size, an alternative is the "binding array of textures" feature TEXTURE_BINDING_ARRAY, which would let each light define it's own shadow map texture/view with different settings (eg sizes) and then create a single binding to all of them.

In practice, it doesn't really matter too much though, since resolution and maximum shadow-casting lights tend to be better as high level renderer decisions rather than settings for individual scene light resources, but with a more bindless style you're going to have big texture arrays for your material setup anyway, so you might as well.

What's with storing all the lights in a storage buffer?

As opposed to a uniform buffer, you mean? I don't think there's a huge difference either way.

Edit: for this use case, I mean. Uniform buffers may or may not be a theoretical better approach depending on how you cluster/cull lights in your program. Storage is just generally a pretty good default since they are much less likely to hit a size limit (generally kilobytes for uniform, so not a problem here), and the performance difference is minimal, if any (only Nvidia has a separate faster path here, from what I can see)

Truly amazes me how many countless ways there are to optimize one problem. Like merging lights if they both produce the same value (A cluster of lights for example)

Also, don't most gpus support storage buffers?

Most is a bit of an understatement! It's been assumed by hardware APIs like OpenGL for about a decade now. Really, they're much more of a driver feature than a hardware one, since they're literally just a VRAM allocation and the ability to read and write to that, and in more modern usage they're starting to get replaced by just direct references (pointers) directly to the underlying memory (the new feature there is really just coherent memory, effectively letting you write directly to VRAM from the CPU)

While I'm sure some engines do merge nearby lights (it's better than just turning them off), the light clustering I was referring to was actually "clustered shading" eg clustered forward rendering, etc.: breaking up the view into blocks that each get precalculated information, most importantly here which lights could affect the pixels.

Sounds like we're getting closer rust being both for the cpu and gpu standalone at the same time. gpu api may be replaced by programming languages in the future.

I also asked that because the shadows example sent checked if storage was supported before making a storage buffer. It felt kind of pointless.

With a little searching, seems like that's for the GLES backend for embedded systems, there's a mention of the Raspberry Pi, for example. I'm honestly still fairly confused why it wouldn't be supported there, likely more a driver issue than strictly hardware I would guess.

I imagine with the vast contrast between pi support and general x86_64