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
+229-6Lines changed: 229 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -548,7 +548,7 @@ The `LightBuffer` is only being cleared at the start of the entire `DrawLights()
548
548
549
549
[!code-csharp[](./snippets/snippet-9-65.cs)]
550
550
551
-
And now the lights are working again! The final `DrawLights()` method is written below:
551
+
And now the shadows are working again! The final `DrawLights()` method is written below:
552
552
553
553
[!code-csharp[](./snippets/snippet-9-66.cs)]
554
554
@@ -564,12 +564,234 @@ We can remove a lot of unnecessary code.
564
564
5. The `PointLight.ShadowBuffer``RenderTarget` is no longer used. Remove it. Anywhere that referenced the `ShadowBuffer` can also be removed.
565
565
566
566
## Improving The Look and Feel
567
-
TODO
568
567
569
-
- dither fall off
570
-
- box-blur
571
-
- stencil mask
572
-
- scissor test
568
+
The shadow technique we have developed so _cool_, but the visual effect leaves a lot to be desired. The shadows sort of look like dark polygons being drawn on top of the scene, rather than what they actually are, which is the absence of light in certain areas. Part of the problem is that the shadows have hard edges, and in real life, shadows fade smoothly across the boundary between light and darkness. Unfortunately for us, creating physically accurate shadows with soft edges is _hard_. There are lots of techniques you could try, like this technique for [rendering penumbra geometry](https://www.gamedev.net/tutorials/programming/graphics/dynamic-2d-soft-shadows-r3065/), or [use 1d shadow maps](https://github.com/mattdesl/lwjgl-basics/wiki/2D-Pixel-Perfect-Shadows). _(That 1d shadow map article references a classic article in 2d shadow mapping that seems to have fallen off the internet. Luckily the Internet Archive has it available, [here](https://web.archive.org/web/20160226133242/http://www.catalinzima.com/2010/07/my-technique-for-the-shader-based-dynamic-2d-shadows/))_
569
+
570
+
Soft shadow techniques are out of the scope of this tutorial, so we will need to find other ways to improve the look and feel of our hard-edged shadows. The first thing to do is let go of the need for "physically accurate" shadows. Our 2d _Dungeon Slime_ game is not physically accurate anyway, so the shadows do not need to be either.
571
+
572
+
### Less Is More
573
+
574
+
The first thing to do is make _fewer_ lights. This is a personal choice, but I find that the lights we added earlier in the chapter are _cool_, but they are distracting. So many lights cause lots of shadows, and the shadows move around a lot and distract from the main object of the game, _eating bats_.
575
+
576
+
1. Originally, we added 4 lights at the top of the level because there were already 4 torches in the game world. Remove the two center toches by modifying the `tilemap-definition.xml`:
2. Next, remove the center lights from the `InitializeLights()` function.
581
+
3. While we are it, replace final 2 moving lights for a single large light that sits at the bottom of the level. When you remove these lights, make sure to remove the `MoveLightsAround()` function as well.
582
+
4. We can also get rid of the shadow caster for the walls of the level.
583
+
584
+
Here is the final `InitializeLights()` function:
585
+
586
+
[!code-csharp[](./snippets/snippet-9-68.cs)]
587
+
588
+
Now there is less visual shadow noise going on.
589
+
590
+
||
|**Figure 9-22: Fewer lights mean fewer shadows**|
593
+
594
+
### Blur the Shadows
595
+
596
+
Perhaps the most obvious issue with the shadows are the hard edges. It would be nice if they were _like_ soft shadows, without having to do the hard work of calculating per pixel soft shadows. One easy way to blur the shadows is to blur the `LightBuffer` when we are reading it in the final deferred rendering composite shader.
597
+
598
+
We will be using a simple blur technique called [box blur](https://en.wikipedia.org/wiki/Box_blur).
599
+
600
+
1. Add this snippet to your `deferredCompositeEffect.fx`:
601
+
602
+
[!code-hlsl[](./snippets/snippet-9-69.hlsl)]
603
+
604
+
2. Then, in the `MainPS` function of the shader, instead of reading the `LightBuffer` directly, get the value from the new `Blur` function.
The next visual puzzle is that sometimes the shadow projections look unnatural. The shadows look too _long_. It would be nice to have some artistic control from how long the shadow hulls should be. Ideally, the hulls could be faded out at some distance away from the shadow caster. However, our shadows are using the stencil buffer to literally clip fragments out of the lights, and the stencil buffer cannot be "faded" in the tranditional sense.
630
+
631
+
There is a technique called [dithering](https://surma.dev/things/ditherpunk/), which fakes a gradient by alternativing pixels on and off. The image below is from [wikipedia](https://en.wikipedia.org/wiki/Dither)'s article on dithering. The image only has two colors, _white_ and _black_. The image _looks_ shaded, but it is just in the art of spacing the black pixels further and further away in the brighter areas.
632
+
633
+
||
We can use the same dithering technique in the `shadowHullEffect.fx` file. If we had a gradient value, we could dither that value to decide if the fragment should be clipped or not.
638
+
639
+
1. Add the following snippet to the `shadowHullEffect.fx` file,
> The shader produces a `fade` value by interpolating the `input.TextureCoordinates.x` between a `startDistance` and `endDistance`. Recall from the [theory section](#rendering-the-shadow-buffer) that the texture coordinates are used to decide vertex is which. The `.x` value of the texture coordinates is `1` when the vertex is the `D` or `F` vertex, and `0` otherwise. The `D` and `F` vertices are the ones that get projected far into the distance. Thus, the `.x` value is a good approximation of the "distance" of any given fragment.
683
+
684
+
Now when you run the game, you can play around with the shader parameters to create a fall off gradient for the shadow.
It is worth calling out that this dithering technique only works well because the box blur is covering the pixellated output. Try disabling the blur entirely, and pay attention to the shadow fall off gradient.
691
+
692
+
3. You will need to pick values that you like for the shadow fall off. I like `.013` for the start and `.13` for the end.
> These gradient numbers are relative to the screen size. If you want to think in terms of pixels, divide the values by the screen size to normalize them:
> Keep in mind that the debug UI only sets shader parameters from `0` to `1`, so you will need to set these values from code.
708
+
709
+
### Shadow Intensity
710
+
711
+
The shadows are mostly solid, except for the blurring effect. However, that can create a very stark atmosphere. It would be nice if we could simply "lighten" all of the shadows. This is a fairly easy extension from the previous [shadow length](#shadow-length) technique. We could set a max value that the shadow is allowed to be before it is forcibly dithered.
712
+
713
+
1. Modify the `shadowHullEffect.fx` to introduce a new shader parameter, `ShadowIntensity`, and use it to force dithering on top of the existing fade-out.
The shadows are looking much better! For the final visual adjustment, it will look better if the snake doesn't cast shadows onto _itself_. When the snake is long, and the player curves around, sometimes the shadow from some slime segments will cast onto other slime segments. It produces a lot of visual flickering in the scene that can be distracting. It would be best if the snake didn't receive any shadows what-so-ever. To do that, we will need to extend the stencil buffer logic.
732
+
733
+
Recall from the [stencil](#the-stencil-buffer) section that the stencil buffer clears every pixel to `0` for reach light, and then shadow caster's shadow hull geometry increases the value. Later, when the lights are drawn, pixels only pass the stencil function when the pixel value is `0`. Importantly, the shadow hulls _always_ increased the stencil buffer value per pixel.
734
+
735
+
In this section, we are going to write the snake segments to the stencil buffer, and then change the shadow hull pass to only draw shadow hulls when the stencil buffer is _not_ a snake pixel.
736
+
737
+
In this new edition, the values of the stencil buffer are outlined below,
4. We also need to update the existing states to take the new value into account:
765
+
766
+
[!code-csharp[](./snippets/snippet-9-75.cs)]
767
+
768
+
5. The snake actually needs to be drawn at the right location, at the right time. The quickest way to accomplish this is to introduce a callback in the `DrawLights()` method and allow the caller to inject a draw call.
0 commit comments