Skip to content

Commit 168f4aa

Browse files
committed
figure update
1 parent 0ea6c63 commit 168f4aa

File tree

1 file changed

+46
-39
lines changed
  • articles/tutorials/advanced/2d_shaders/09_shadows_effect

1 file changed

+46
-39
lines changed

articles/tutorials/advanced/2d_shaders/09_shadows_effect/index.md

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,24 @@ description: "Add dynamic shadows to the game"
55

66
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.
77

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+
| ![Figure 9-1: The final shadow effect](./gifs/overview.gif) |
13+
| :---------------------------------------------------------: |
14+
| **Figure 9-1: The final shadow effect** |
15+
916

1017
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).
1118

1219
## 2D Shadows
1320

1421
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.
1522

16-
| ![Figure 9-1: A light with no shadows](./images/starting.png) |
23+
| ![Figure 9-2: A light with no shadows](./images/starting.png) |
1724
| :-----------------------------------------------------------: |
18-
| **Figure 9-1: A light with no shadows** |
25+
| **Figure 9-2: A light with no shadows** |
1926

2027
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.
2128

@@ -46,9 +53,9 @@ The mystery to unpack is step 1, how to render the `ShadowBuffer` in the first p
4653

4754
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`.
4855

49-
| ![Figure 9-6: A diagram of a simple light and line segment](./images/light_math.png) |
56+
| ![Figure 9-4: A diagram of a simple light and line segment](./images/light_math.png) |
5057
| :----------------------------------------------------------------------------------: |
51-
| **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** |
5259

5360
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.
5461

@@ -61,9 +68,9 @@ However, the `SpriteBatch` usually only renders rectangular shapes. Naively, it
6168

6269
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`.
6370

64-
| ![Figure 9-7: A diagram showing a pixel](./images/pixel_math.png) |
71+
| ![Figure 9-5: A diagram showing a pixel](./images/pixel_math.png) |
6572
| :---------------------------------------------------------------: |
66-
| **Figure 9-7: A diagram showing a pixel** |
73+
| **Figure 9-5: A diagram showing a pixel** |
6774

6875
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.
6976

@@ -199,9 +206,9 @@ For debug visualization purposes, add this snippet at the end of the `GameScene`
199206

200207
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.
201208

202-
| ![Figure 9-8: A blank shadow buffer](./images/shadow_map_blank.png) |
209+
| ![Figure 9-6: A blank shadow buffer](./images/shadow_map_blank.png) |
203210
| :-----------------------------------------------------------------: |
204-
| **Figure 9-8: A blank shadow buffer** |
211+
| **Figure 9-6: A blank shadow buffer** |
205212

206213
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:
207214

@@ -237,9 +244,9 @@ The vertex shader function is derived from the theory section above, but is writ
237244

238245
Now if you run the game, you will see the white shadow hull.
239246

240-
| ![Figure 9-9: A shadow hull](./images/shadow_map.png) |
247+
| ![Figure 9-7: A shadow hull](./images/shadow_map.png) |
241248
| :---------------------------------------------------: |
242-
| **Figure 9-9: A shadow hull** |
249+
| **Figure 9-7: A shadow hull** |
243250

244251
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:
245252

@@ -259,9 +266,9 @@ Before running the game, make sure to pass the `ShadowBuffer` to the point light
259266

260267
Disable the debug visualization to render the `ShadowMap` on top of everything else, and run the game.
261268

262-
| ![Figure 9-10: The light is appearing inverted](./images/shadow_map_backwards.png) |
269+
| ![Figure 9-8: The light is appearing inverted](./images/shadow_map_backwards.png) |
263270
| :--------------------------------------------------------------------------------: |
264-
| **Figure 9-10: The light is appearing inverted** |
271+
| **Figure 9-8: The light is appearing inverted** |
265272

266273
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_:
267274

@@ -273,9 +280,9 @@ And change the pixel shader to return a solid black rather than white:
273280

274281
And now the shadow appears correctly for our simple single line segment!
275282

276-
| ![Figure 9-11: A working shadow!](./images/shadow_map_working.png) |
283+
| ![Figure 9-9: A working shadow!](./images/shadow_map_working.png) |
277284
| :----------------------------------------------------------------: |
278-
| **Figure 9-11: A working shadow!** |
285+
| **Figure 9-9: A working shadow!** |
279286

280287
## More Segments
281288

@@ -299,19 +306,19 @@ Finally, the last place we need to change is the `DrawShadowBuffer()` method. Cu
299306

300307
When you run the game, you will see a larger shadow shape.
301308

302-
| ![Figure 9-12: A shadow hull from a hexagon](./images/shadow_map_hex.png) |
309+
| ![Figure 9-10: A shadow hull from a hexagon](./images/shadow_map_hex.png) |
303310
| :-----------------------------------------------------------------------: |
304-
| **Figure 9-12: A shadow hull from a hexagon** |
311+
| **Figure 9-10: A shadow hull from a hexagon** |
305312

306313
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:
307314

308315
[!code-hlsl[](./snippets/snippet-9-38.hlsl)]
309316

310317
And now the visual artifact has gone away.
311318

312-
| ![Figure 9-13: The visual artifact has been fixed](./images/shadow_map_hex_2.png) |
319+
| ![Figure 9-11: The visual artifact has been fixed](./images/shadow_map_hex_2.png) |
313320
| :-------------------------------------------------------------------------------: |
314-
| **Figure 9-13: The visual artifact has been fixed** |
321+
| **Figure 9-11: The visual artifact has been fixed** |
315322

316323
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:
317324

@@ -327,9 +334,9 @@ And then in the pixel shader function, add this line to the top. The [`clip`](ht
327334

328335
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.
329336

330-
| ![Figure 9-14: The slime is well lit](./images/shadow_map_hex_3.png) |
337+
| ![Figure 9-12: The slime is well lit](./images/shadow_map_hex_3.png) |
331338
| :------------------------------------------------------------------: |
332-
| **Figure 9-14: The slime is well lit** |
339+
| **Figure 9-12: The slime is well lit** |
333340

334341
## Gameplay
335342

@@ -351,9 +358,9 @@ Now, modify the `GameScene`'s `Draw()` method to create a master list of all the
351358

352359
And now the slime has shadows around the segments!
353360

354-
| ![Figure 9-15: The slime has shadows](./gifs/snake_shadow.gif) |
361+
| ![Figure 9-13: The slime has shadows](./gifs/snake_shadow.gif) |
355362
| :------------------------------------------------------------: |
356-
| **Figure 9-15: The slime has shadows** |
363+
| **Figure 9-13: The slime has shadows** |
357364

358365
Next up, the bat needs some shadows! Add a `ShadowCaster` property to the `Bat` class:
359366

@@ -373,18 +380,18 @@ And finally add the `ShadowCaster` to the master list of shadow casters during t
373380

374381
And now the bat is casting a shadow as well!
375382

376-
| ![Figure 9-16: The bat casts a shadow](./gifs/bat_shadow.gif) |
383+
| ![Figure 9-14: The bat casts a shadow](./gifs/bat_shadow.gif) |
377384
| :-----------------------------------------------------------: |
378-
| **Figure 9-16: The bat casts a shadow** |
385+
| **Figure 9-14: The bat casts a shadow** |
379386

380387
Lastly, the walls should cast shadows to help ground the lighting in the world.
381388
Add a shadow caster in the `InitializeLights()` function to represent the edge of the playable tiles:
382389

383390
[!code-csharp[](./snippets/snippet-9-49.cs)]
384391

385-
| ![Figure 9-17: The walls have shadows](./gifs/wall_shadow.gif) |
392+
| ![Figure 9-15: The walls have shadows](./gifs/wall_shadow.gif) |
386393
| :------------------------------------------------------------: |
387-
| **Figure 9-17: The walls have shadows** |
394+
| **Figure 9-15: The walls have shadows** |
388395

389396

390397
## The Stencil Buffer
@@ -417,29 +424,29 @@ Next, in the `pointLightEffect.fx` shader, we will not be using the `ShadowBuffe
417424

418425
If you run the game now, you will not see any of the lights anymore.
419426

420-
| ![Figure 9-15: Back to square one](./images/stencil_blank.png) |
427+
| ![Figure 9-16: Back to square one](./images/stencil_blank.png) |
421428
| :------------------------------------------------------------: |
422-
| **Figure 9-15: Back to square one** |
429+
| **Figure 9-16: Back to square one** |
423430

424431
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:
425432

426433
[!code-csharp[](./snippets/snippet-9-54.cs)]
427434

428435
Now the lights are back, but of course no shadows yet.
429436

430-
| ![Figure 9-16: Welcome back, lights](./images/stencil_lights.png) |
437+
| ![Figure 9-17: Welcome back, lights](./images/stencil_lights.png) |
431438
| :---------------------------------------------------------------: |
432-
| **Figure 9-16: Welcome back, lights** |
439+
| **Figure 9-17: Welcome back, lights** |
433440

434441
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:
435442

436443
[!code-csharp[](./snippets/snippet-9-55.cs)]
437444

438445
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.
439446

440-
| ![Figure 9-17: Worse shadows](./images/stencil_pre.png) |
447+
| ![Figure 9-18: Worse shadows](./images/stencil_pre.png) |
441448
| :-----------------------------------------------------: |
442-
| **Figure 9-17: Worse shadows** |
449+
| **Figure 9-18: Worse shadows** |
443450

444451
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
445452
[!code-csharp[](./snippets/snippet-9-56.cs)]
@@ -466,9 +473,9 @@ And now pass the new `_stencilTest` state to the `SpriteBatch` `Draw()` call tha
466473

467474
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`...
468475

469-
| ![Figure 9-18: The shadows still look funky](./images/stencil_blend.png) |
476+
| ![Figure 9-19: The shadows still look funky](./images/stencil_blend.png) |
470477
| :----------------------------------------------------------------------: |
471-
| **Figure 9-18: The shadows still look funky** |
478+
| **Figure 9-19: The shadows still look funky** |
472479
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`:
473480

474481
[!code-csharp[](./snippets/snippet-9-62.cs)]
@@ -488,9 +495,9 @@ Finally, pas it to the shadow hull `SpriteBatch` call:
488495

489496
Now the shadows look closer, but there is one final issue.
490497

491-
| ![Figure 9-18: The shadows are back](./images/stencil_noclear.png) |
498+
| ![Figure 9-20: The shadows are back](./images/stencil_noclear.png) |
492499
| :----------------------------------------------------------------: |
493-
| **Figure 9-18: The shadows are back** |
500+
| **Figure 9-20: The shadows are back** |
494501

495502
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:
496503

@@ -499,9 +506,9 @@ And now the lights are working again! The final `DrawLights()` method is written
499506

500507
[!code-csharp[](./snippets/snippet-9-66.cs)]
501508

502-
| ![Figure 9-19: Lights using the stencil buffer](./gifs/stencil_working.gif) |
509+
| ![Figure 9-21: Lights using the stencil buffer](./gifs/stencil_working.gif) |
503510
| :-------------------------------------------------------------------------: |
504-
| **Figure 9-19: Lights using the stencil buffer** |
511+
| **Figure 9-21: Lights using the stencil buffer** |
505512

506513
We can remove a lot of unnecessary code.
507514
1. The `DeferredRenderer.StartLightPhase()` function is no longer called. Remove it.

0 commit comments

Comments
 (0)