You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: articles/tutorials/advanced/2d_shaders/09_shadows_effect/index.md
+46-39Lines changed: 46 additions & 39 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -5,17 +5,24 @@ description: "Add dynamic shadows to the game"
5
5
6
6
Our lighting system is looking great, but the lights do not feel fully grounded in the world. They shine right through the walls, the bat, and even our slime! To truly sell the illusion of light, we need darkness. We need shadows.
7
7
8
-
In this, our final effects chapter, we're going to implement a dynamic 2D shadow system. The shadows will be drawn with a new vertex shader, and integrated into the point light shader from the previous chapter. After the effect is working, we will port the effect to use a more efficient approach.
8
+
In this, our final effects chapter, we're going to implement a dynamic 2D shadow system. The shadows will be drawn with a new vertex shader, and integrated into the point light shader from the previous chapter. After the effect is working, we will port the effect to use a more efficient approach using a tool called the _Stencil Buffer_. Lastly, we will explore some visual tricks to improve the look and feel of the shadows.
9
+
10
+
At the end of this chapter, your game will look something like this:
11
+
12
+
||
If you're following along with code, here is the code from the end of the [previous chapter](https://github.com/MonoGame/MonoGame.Samples/tree/3.8.4/Tutorials/2dShaders/src/08-Light-Effect).
11
18
12
19
## 2D Shadows
13
20
14
21
Take a look at the current lighting in _Dungeon Slime_. In this screenshot, there is a single light source. The bat and the slime do not cast shadows, and without these shadows, it is hard to visually identify where the light's position is.
15
22
16
-
||
23
+
||
If the slime was casting a shadow, then the position of the light would be a lot easier to decipher just from looking at the image. Shadows help ground the objects in the scene. Just to visualize it, this image is a duplicate of the above, but with a pink debug shadow drawn on top to illustrate the desired effect.
21
28
@@ -46,9 +53,9 @@ The mystery to unpack is step 1, how to render the `ShadowBuffer` in the first p
46
53
47
54
To build some intuition, we will start by considering a shadow caster that is a single line segment. If we can generate a shadow for a single line segment, then we could compose multiple line segments to replicate the shape of the slime sprite. In the image blow, there is a single light source at position `L`, and a line segment between points `A`, and `B`.
48
55
49
-
||
56
+
||
|**Figure 9-6: A diagram of a simple light and line segment**|
58
+
|**Figure 9-4: A diagram of a simple light and line segment**|
52
59
53
60
The shape we need to draw is the non-regular quadrilateral defined by `A`, `a`, `b`, and `B`. It is shaded in pink. These points are in world space. Given that we know where the line segment is, we know where `A` and `B` are, but we do not _yet_ know `a` and `b`'s location.
54
61
@@ -61,9 +68,9 @@ However, the `SpriteBatch` usually only renders rectangular shapes. Naively, it
61
68
62
69
This diagram shows an abstract pixel being drawn at some position `P`. The corners of the pixel may be defined as `G`, `S`, `D`, and `F`.
63
70
64
-
||
71
+
||
Our goal is define a function that transforms the positions `S`, `D`, `F`, and `G`_into_ the positions, `A`, `a`, `b`, and `B`. The table below shows the desired mapping.
69
76
@@ -199,9 +206,9 @@ For debug visualization purposes, add this snippet at the end of the `GameScene`
199
206
200
207
When you run the game, you will see a totally blank white screen. This is because the shadow map is currently being cleared to `black` to start, and the debug view renders that on top of everything else.
201
208
202
-
||
209
+
||
We cannot implement the vertex shader theory until we can pack the `(B-A)` vector into the `Color` argument for the `SpriteBatch`. For the sake of brevity, we will skip over the derivation of these functions. If you would like to know more, research bit-packing. Add this function to your `PointLight` class:
207
214
@@ -237,9 +244,9 @@ The vertex shader function is derived from the theory section above, but is writ
237
244
238
245
Now if you run the game, you will see the white shadow hull.
239
246
240
-
||
247
+
||
To get the basic shadow effect working with the rest of the renderer, we need to do the multiplication step between the `ShadowBuffer` and the `LightBuffer` in the `pointLightEffect.fx` shader. Add a second texture and sampler for the `pointLightEffect.fx` file:
245
252
@@ -259,9 +266,9 @@ Before running the game, make sure to pass the `ShadowBuffer` to the point light
259
266
260
267
Disable the debug visualization to render the `ShadowMap` on top of everything else, and run the game.
261
268
262
-
||
269
+
||
|**Figure 9-10: The light is appearing inverted**|
271
+
|**Figure 9-8: The light is appearing inverted**|
265
272
266
273
Oops, the shadows and lights are appearing opposite of where they should! That is because the `ShadowBuffer` is inverted. Change the clear color for the `ShadowBuffer` to _white_:
267
274
@@ -273,9 +280,9 @@ And change the pixel shader to return a solid black rather than white:
273
280
274
281
And now the shadow appears correctly for our simple single line segment!
275
282
276
-
||
283
+
||
There are a few problems with the current effect. First off, there is a visual artifact going horizontally through the center of the shadow caster where it appears light is "leaking" in. This is likely due to numerical accuracy issues in the shader. A simple solution is to slightly extend the line segment in the vertex shader. After both `A` and `B` are calculated, but before `a` and `b`, add this to the shader:
307
314
308
315
[!code-hlsl[](./snippets/snippet-9-38.hlsl)]
309
316
310
317
And now the visual artifact has gone away.
311
318
312
-
||
319
+
||
|**Figure 9-13: The visual artifact has been fixed**|
321
+
|**Figure 9-11: The visual artifact has been fixed**|
315
322
316
323
The next item to consider is that the the "inside" of the slime isn't being lit. All of the segments are casting shadows, but it would be nice if only the segments on the far side of the slime cast shadows. We can take advantage of the fact that all of the line segments making up the shadow caster are _wound_ in the same direction:
317
324
@@ -327,9 +334,9 @@ And then in the pixel shader function, add this line to the top. The [`clip`](ht
327
334
328
335
Now the slime looks well lit and shadowed! Feel free to play with the size of the shadow caster as well as the exact points themselves.
329
336
330
-
||
337
+
||
In the new `DrawLights()` method, we need to iterate over all the lights, and draw them. First, we need to set the current render target to the `LightBuffer` so it can be used in the deferred renderer composite stage:
425
432
426
433
[!code-csharp[](./snippets/snippet-9-54.cs)]
427
434
428
435
Now the lights are back, but of course no shadows yet.
As each light is about to draw, we need to draw the shadow hulls. Add this snippet to the top of the `foreach` loop. This code is mainly copied from our previous approach:
435
442
436
443
[!code-csharp[](./snippets/snippet-9-55.cs)]
437
444
438
445
This produces strange results. So far, the stencil buffer isn't being used yet, so all we are doing is rendering the shadow hulls onto the same image as the light data itself. Worse, the alternating order from rendering shadows to lights, back to shadows, and so on produces very visually decoherent results.
Instead of writing the shadow hulls as _color_ into the color portion of the `LightBuffer`, we only need to render the `1` or `0` to the stencil buffer portion of the `LightBuffer`. To do this, we need to create a new `DepthStencilState` variable. The `DepthStencilState` is a MonoGame primitive that describes how draw call operations should interact with the stencil buffer. Create a new class variable in the `DeferredRenderer` class
445
452
[!code-csharp[](./snippets/snippet-9-56.cs)]
@@ -466,9 +473,9 @@ And now pass the new `_stencilTest` state to the `SpriteBatch` `Draw()` call tha
466
473
467
474
The shadows look _better_, but something is still broken. It looks eerily similar to the previous iteration before passing the `_stencilTest` and `_stencilWrite` declarations to `SpriteBatch`...
468
475
469
-
||
476
+
||
This happens because the shadow hulls are _still_ being drawn as colors into the `LightBuffer`. The shadow hull shader is rendering a black pixel, so those black pixels are drawing on top of the `LightBuffer` 's previous point lights. To solve this, we need to create a custom `BlendState` that ignores all color channel writes. Create a new class variable in the `DeferredRenderer`:
473
480
474
481
[!code-csharp[](./snippets/snippet-9-62.cs)]
@@ -488,9 +495,9 @@ Finally, pas it to the shadow hull `SpriteBatch` call:
488
495
489
496
Now the shadows look closer, but there is one final issue.
490
497
491
-
||
498
+
||
The `LightBuffer` is only being cleared at the start of the entire `DrawLights()` method. This means the `8` bits for the stencil data aren't being cleared between lights, so shadows from one light are overwriting into all subsequent lights. To fix this, we just need to clear the stencil buffer data before rendering the shadow hulls:
496
503
@@ -499,9 +506,9 @@ And now the lights are working again! The final `DrawLights()` method is written
499
506
500
507
[!code-csharp[](./snippets/snippet-9-66.cs)]
501
508
502
-
||
509
+
||
0 commit comments