Skip to content

Commit 014a349

Browse files
committed
some chap 8 comments
1 parent d95ce15 commit 014a349

File tree

5 files changed

+106
-29
lines changed

5 files changed

+106
-29
lines changed

articles/tutorials/advanced/2d_shaders/08_light_effect/index.md

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ In the earlier days of computer graphics, forward rendering was ubiquitous. Imag
2020

2121
In the 2000's, the deferred rendering strategy was [introduced](https://sites.google.com/site/richgel99/the-early-history-of-deferred-shading-and-lighting) and popularized by games like [S.T.A.L.K.E.R](https://developer.nvidia.com/gpugems/gpugems2/part-ii-shading-lighting-and-shadows/chapter-9-deferred-shading-stalker). In deferred rendering, each object is drawn _once_ without _any_ lights to an off-screen texture. Then, each light is drawn on top of the off-screen texture. To make that possible, the initial rendering pass draws extra data about the scene into additional off-screen textures. Theoretically, a deferred renderer can handle more lights and objects because the work is roughly approximate to the sprites (`S`) _added_ to the lights (`L`), or `S + L`.
2222

23-
Deferred rendering was popular for several years. MonoGame is an adaptation of XNA, which came out in the era of deferred rendering. However, deferred renderers are not a silver bullet for performance and graphics programming. The crux of a deferred renderer is to bake data into off-screen textures, and as monitor resolutions have gotten larger and larger, the 4k resolutions making off-screen texture more expensive than before. Also, deferred renderers cannot handle transparent materials. Many big game projects use deferred rendering for _most_ of the scene, and a forward renderer for the final transparent components of the scene. As with all things, which type of rendering to use is a nuanced decision. There are new types of forward rendering strategies (see, [clustered rendering](https://github.com/DaveH355/clustered-shading), or [forward++](https://www.gdcvault.com/play/1017627/Advanced-Visual-Effects-with-DirectX) rendering) that can out perform deferred renderers. However, for our use cases, the deferred rendering technique is sufficient.
23+
Deferred rendering was popular for several years. MonoGame is an adaptation of XNA, which came out in the era of deferred rendering. However, deferred renderers are not a silver bullet for performance and graphics programming. The crux of a deferred renderer is to bake data into off-screen textures, and as monitor resolutions have gotten larger and larger, the 4k resolutions makimakeng off-screen texture more expensive than before. Also, deferred renderers cannot handle transparent materials. Many big game projects use deferred rendering for _most_ of the scene, and a forward renderer for the final transparent components of the scene. As with all things, which type of rendering to use is a nuanced decision. There are new types of forward rendering strategies (see, [clustered rendering](https://github.com/DaveH355/clustered-shading), or [forward++](https://www.gdcvault.com/play/1017627/Advanced-Visual-Effects-with-DirectX) rendering) that can out perform deferred renderers. However, for our use cases, the deferred rendering technique is sufficient.
2424

2525
If you are 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/07-Sprite-Vertex-Effect).
2626

@@ -30,10 +30,15 @@ Writing a simple deferred renderer can be worked out in a few steps,
3030

3131
1. take the scene as we are drawing it currently, and store it in an off-screen texture. This texture is often called the diffuse texture, or color texture.
3232
2. render the scene again, but instead of drawing the sprites normally, draw their _Normal_ maps to an off-screen texture, called the normal texture.
33-
3. create a new off-screen texture, called the light texture, where each light is layered on-top of each other,
33+
3. create yet another off-screen texture, called the light texture, where lights are layered on top of each other using the normal texture,
3434
4. finally, create a rendering to the screen based on the lighting texture and the color texture.
3535

36-
The second stage references a new term, called the _Normal_ Map. We will come back to this later in the chapter. For now, we will focus on the other steps.
36+
The second stage references a new term, called the _Normal_ texture. We will come back to this later in the chapter. For now, we will focus on the other steps.
37+
38+
> [!TIP]
39+
> _Texture_ vs _Map_ vs _Buffer_
40+
>
41+
> It is very common for people to refer to textures as _maps_ or _buffers_ in computer graphics, so if you see the terms "color map", "color texture", or "color buffer"; they very likely refer to the same thing. The terms are synonmous.
3742
3843
### Drawing to an off-screen texture
3944

@@ -45,7 +50,7 @@ The second stage references a new term, called the _Normal_ Map. We will come ba
4550

4651
2. The `ColorBuffer` property is a [`RenderTarget2D`](xref:Microsoft.Xna.Framework.Graphics.RenderTarget2D), which is a special type of [`Texture2D`](xref:Microsoft.Xna.Framework.Graphics.Texture2D) that MonoGame can draw into. In order for MonoGame to draw anything into the `ColorBuffer`, it needs to be bound as the current render target. Add the following function to the `DeferredRenderer` class.
4752

48-
The `SetRenderTarget()` function instructs all future MonoGame draw operations to render into the `ColorBuffer`:
53+
The `SetRenderTarget()` function instructs all future MonoGame draw operations to render into the `ColorBuffer`. Add this function to the `DeferredRenderer` class:
4954

5055
[!code-csharp[](./snippets/snippet-8-02.cs)]
5156

@@ -184,6 +189,42 @@ The next task is to write the `pointLightEffect.fx` shader file so that the whit
184189

185190
> [!NOTE]
186191
> For the sake of clarity, these screenshots show only the `LightBuffer` as full screen, that way we can focus on the distance based return value.
192+
>
193+
> If you want to do that too, change the `DebugDraw()` method to use the entire viewport for the `lightBorderRect`, like this:
194+
>
195+
> [!code-hlsl[](./snippets/snippet-8-22-2.cs)]
196+
>
197+
> Just do not forget to revert this change later!
198+
199+
200+
> [!TIP]
201+
> Add a pause mechanic!
202+
>
203+
> It can be really hard to debug the graphics stuff while the game is being played. Earlier in the series, we just added an early-return in the `GameScene`'s `Update()` method. We could do that again, or we could add a debug key to pause the game.
204+
> Add a class variable called ``:
205+
>
206+
> ```csharp
207+
> private bool _debugPause = false;
208+
> ```
209+
>
210+
> And then add this snippet to the top of the `Update()` method:
211+
>
212+
> ```csharp
213+
> if (Core.Input.Keyboard.WasKeyJustPressed(Keys.P))
214+
> {
215+
> _debugPause = !_debugPause;
216+
> }
217+
> if (_debugPause) return;
218+
> ```
219+
>
220+
> And do not forget to add the `using` statement:
221+
>
222+
> ```csharp
223+
> using Microsoft.Xna.Framework.Input;
224+
> ```
225+
>
226+
> Now you will be able to hit the `p` key to pause the game without showing the menu. Remember to take this out before shipping your game!
227+
187228
188229
| ![Figure 8-6: Showing the distance from the center of the light in the red channel](./images/point-light-dist.png) |
189230
| :----------------------------------------------------------------------------------------------------------------: |
@@ -228,14 +269,17 @@ The next task is to write the `pointLightEffect.fx` shader file so that the whit
228269
229270
[!code-hlsl[](./snippets/snippet-8-27.hlsl?highlight=13-15)]
230271
231-
7. In the `GameScene` we can replace the initialization of the light to change its color in C# to `CornflowerBlue` in the `Initialize` method:
232-
233-
[!code-csharp[](./snippets/snippet-8-28.cs)]
272+
> [!WARNING]
273+
> Wait, the light broke! If you run the project at this state, the light seems to revert back to a fully opaque square! That is because of the `blendState` of the `SpriteBatch`. Even though the `a` (or alpha) channel of the color in the shader is being set to a nice `falloff`, the default `blendState` sees any positive alpha as "fully opaque". We are going to fix this right away!
234274
235-
8. And change the `blendState` of the light's `SpriteBatch` draw call in the `PointLight` class to additive for effect:
275+
7. And change the `blendState` of the light's `SpriteBatch` draw call in the `PointLight` class to additive for effect:
236276
237277
[!code-csharp[](./snippets/snippet-8-29.cs)]
238278
279+
8. In the `GameScene` we can replace the initialization of the light to change its color in C# to `CornflowerBlue` in the `Initialize` method:
280+
281+
[!code-csharp[](./snippets/snippet-8-28.cs)]
282+
239283
9. Finally, in the `Core` class `LoadContent` method, set the default shader parameter values for brightness and sharpness to something you like:
240284
241285
[!code-csharp[](./snippets/snippet-8-30.cs?highlight=7,8)]
@@ -244,7 +288,7 @@ The next task is to write the `pointLightEffect.fx` shader file so that the whit
244288
| :-------------------------------------------------------------------------------: |
245289
| **Figure 8-11: The point light in the light buffer** |
246290
247-
The light looks good! When we revert the full-screen `LightBuffer` and render the `LightBuffer` next to the `ColorBuffer`, a graphical bug will become clear. The world in the `ColorBuffer` is rotating with the vertex shader from the previous chapter, but the `LightBuffer` does not have the same effect, so the light appears broken.
291+
The light looks good! When we revert the full-screen `LightBuffer` and render the `LightBuffer` next to the `ColorBuffer`, a graphical bug will become clear. The world in the `ColorBuffer` is rotating with the vertex shader from the previous chapter, but the `LightBuffer` does not have the same effect, so the light appears broken. We will fix this later on in the chapter. But for now, we are going to accept this visual glitch for the next few sections.
248292
249293
### Combining Light and Color
250294
@@ -286,16 +330,16 @@ Now that the light and color buffers are being drawn to separate off screen text
286330
287331
[!code-hlsl[](./snippets/snippet-8-38.hlsl)]
288332
289-
| ![Figure 8-13: The light and color composited](./images/composite-1.png) |
290-
| :----------------------------------------------------------------------: |
291-
| **Figure 8-13: The light and color composited** |
292-
293333
9. Back in the `DeferredRenderer` class, in the `DrawComposite` function before the sprite batch starts, make sure to pass the `LightBuffer` to the material:
294334
295335
[!code-csharp[](./snippets/snippet-8-37.cs?highlight=3)]
296336
297337
The light is working! However, the whole scene is too dark to see what is going on or play the game.
298338
339+
| ![Figure 8-13: The light and color composited](./images/composite-1.png) |
340+
| :----------------------------------------------------------------------: |
341+
| **Figure 8-13: The light and color composited** |
342+
299343
10. To solve this, we can add a small amount of ambient light to the `deferredCompositeEffect` shader:
300344
301345
[!code-hlsl[](./snippets/snippet-8-39.hlsl)]
@@ -336,6 +380,9 @@ For reference, the existing texture atlas is on the left, and a version of the a
336380
| :------------------------------------------------------------: | :-----------------------------------------------------------------: |
337381
| **Figure 8-17: The existing texture atlas** | **Figure 8-18: The normal texture atlas** |
338382
383+
> [!WARNING]
384+
> This is not the most efficient way to integrate normal maps into your game, because now there are _two_ texture atlases. Another approach would be to add the normal maps to existing sprite atlas, and modify the `Sprite` code to have two regions. That is an exercise for the reader.
385+
339386
Download the [atlas-normal.png](./images/atlas-normal.png) texture, add it to the _DungeonSlime_'s `Content/Images` folder and include it in the mgcb content file.
340387
341388
Now that we have the art assets, it is time to work the normal maps into the code.
@@ -364,7 +411,7 @@ Now that we have the art assets, it is time to work the normal maps into the cod
364411
365412
5. And do not forget to update the `technique` to reference the new `MainPS` function:
366413
367-
[!code-hlsl[](./snippets/snippet-8-45.hlsl?highlight=6)]
414+
[!code-hlsl[](./snippets/snippet-8-45.hlsl?highlight=6)]
368415
369416
6. In C#, when the `GraphicsDevice.SetRenderTarget()` function is called, it sets the texture that the `COLOR0` semantic will be sent to. However, there is an overload called `SetRenderTargets()` that accepts _multiple_ `RenderTarget2D`s, and each additional texture will be assigned to the next `COLOR` semantic.
370417
@@ -449,6 +496,18 @@ When each individual light is drawn into the `LightBuffer`, it needs to use the
449496
450497
[!code-hlsl[](./snippets/snippet-8-58.hlsl)]
451498
499+
> [!NOTE]
500+
> What does `float4 normalized = output.Position / output.Position.w;` do?
501+
>
502+
> Long story short, the `w` component of the `.Position` must be `_1_`. Dividing any number by itself results in _1_, so the dividing `output.Position` by its own `w` component does two things,
503+
>
504+
> 1. sets the `w` component to _1_,
505+
> 2. uniformly adjusts the other components to accomodate the change.
506+
>
507+
> Remember that the purpose of the matrix projection is to convert the coordinates into clip space. The graphics card silently normalizes the coordinates using this math, but since we are not actually passing the data through the rest of the graphics pipeline, we need to do the normalization ourselves.
508+
>
509+
> The math to fully explain why this is required is beyond the scope of this tutorial series. Read about [homogenous coordinates](https://www.tomdalling.com/blog/modern-opengl/explaining-homogenous-coordinates-and-projective-geometry/) and the [perspective divide](https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/projection-matrix-GPU-rendering-pipeline-clipping.html)
510+
452511
7. Make sure to update the `technique` to use the new vertex function:
453512
454513
[!code-hlsl[](./snippets/snippet-8-59.hlsl?highlight=5)]

articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-14.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,3 @@
22
/// The material that draws point lights
33
/// </summary>
44
public static Material PointLightMaterial { get; private set; }
5-
PointLightMaterial.IsDebugVisible = true;

articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-15.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ protected override void LoadContent()
55
// ...
66

77
PointLightMaterial = SharedContent.WatchMaterial("effects/pointLightEffect");
8+
PointLightMaterial.IsDebugVisible = true;
89
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
public void DebugDraw()
2+
{
3+
// ...
4+
5+
// the debug view for the light buffer lives in the top-right.
6+
// var lightBorderRect = new Rectangle(
7+
// x: viewportBounds.Width / 2,
8+
// y: viewportBounds.Y,
9+
// width: viewportBounds.Width / 2,
10+
// height: viewportBounds.Height / 2);
11+
12+
var lightBorderRect = viewportBounds; // TODO: remove this; it makes the light rect take up the whole screen.
13+
14+
// ...
15+
}
Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
LightVertexShaderOutput LightVS(VertexShaderInput input)
22
{
3-
LightVertexShaderOutput output;
3+
LightVertexShaderOutput output;
44

5-
VertexShaderOutput mainVsOutput = MainVS(input);
5+
VertexShaderOutput mainVsOutput = MainVS(input);
66

7-
// forward along the existing values from the MainVS's output
8-
output.Position = mainVsOutput.Position;
9-
output.Color = mainVsOutput.Color;
10-
output.TextureCoordinates = mainVsOutput.TextureCoordinates;
11-
12-
// normalize from -1,1 to 0,1
13-
output.ScreenCoordinates = .5 * (float2(output.Position.xy) + 1);
14-
15-
// invert the y coordinate, because MonoGame flips it.
16-
output.ScreenCoordinates.y = 1 - output.ScreenCoordinates.y;
7+
// forward along the existing values from the MainVS's output
8+
output.Position = mainVsOutput.Position;
9+
output.Color = mainVsOutput.Color;
10+
output.TextureCoordinates = mainVsOutput.TextureCoordinates;
1711

18-
return output;
19-
}
12+
// normalize the clip-space position
13+
float4 normalized = output.Position / output.Position.w;
14+
15+
// normalize from -1,1 to 0,1
16+
output.ScreenCoordinates = .5 * (float2(normalized.xy) + 1);
17+
18+
// invert the y coordinate, because MonoGame flips it.
19+
output.ScreenCoordinates.y = 1 - output.ScreenCoordinates.y;
20+
21+
return output;
22+
}

0 commit comments

Comments
 (0)