diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1516cc42..9a412810 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,17 +27,67 @@ jobs: - name: Clone Repository uses: actions/checkout@v4 with: + ref: ${{ github.ref }} submodules: recursive + fetch-depth: 0 + + - name: Show submodule status + run: git submodule status - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: dotnet-version: '8.0.x' + - name: Setup Android Dependencies + uses: monogame/monogame-actions/install-android-dependencies@v1 + + - name: Setup DotNet on linux/Windows + if: runner.environment == 'github-hosted' && (runner.os == 'Linux' || runner.os == 'windows') + run: | + dotnet workload install android + + - name: Setup DotNet on MacOS + if: runner.environment == 'github-hosted' && runner.os == 'macos' + run: | + dotnet workload install android macos ios + + - name: Add msbuild to PATH + if: runner.os == 'Windows' + uses: microsoft/setup-msbuild@v1.0.2 + - name: Setup Premake5 uses: abel0b/setup-premake@v2.4 with: - version: "5.0.0-beta2" + version: "5.0.0-beta2" + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'microsoft' + java-version: '17' + + - name: Install Vulkan SDK + uses: humbletim/setup-vulkan-sdk@523828e49cd4afabce369c39c7ee6543a2b7a735 + with: + vulkan-query-version: 1.3.283.0 + vulkan-use-cache: true + + - name: Disable Annotations + run: echo "::remove-matcher owner=csc::" + + - name: Setup Wine + uses: monogame/monogame-actions/install-wine@v1 + + - name: Install Fonts + uses: monogame/monogame-actions/install-fonts@v1 + + - name: Build + working-directory: external/MonoGame + run: | + dotnet run --project build/Build.csproj -- --target=Default + env: + DOTNET_SYSTEM_NET_SECURITY_NOREVOCATIONCHECKBYDEFAULT: 1 # android compilation is failing randomly due to downloads from microsofts website during it - name: Restore dotnet tools run: dotnet tool restore @@ -55,4 +105,5 @@ jobs: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + + uses: actions/deploy-pages@v4 diff --git a/articles/toc.yml b/articles/toc.yml index c6171888..c0bec2ae 100644 --- a/articles/toc.yml +++ b/articles/toc.yml @@ -156,6 +156,29 @@ items: href: tutorials/building_2d_games/26_publish_to_itch/index.md - name: "27: Conclusion and Next Steps" href: tutorials/building_2d_games/27_conclusion/index.md + - name: 2D Shaders + href: tutorials/advanced/2d_shaders/index.md + items: + - name: "01: Introduction" + href: tutorials/advanced/2d_shaders/01_introduction/index.md + - name: "02: Hot Reload" + href: tutorials/advanced/2d_shaders/02_hot_reload/index.md + - name: "03: The Material Class" + href: tutorials/advanced/2d_shaders/03_the_material_class/index.md + - name: "04: Debug UI" + href: tutorials/advanced/2d_shaders/04_debug_ui/index.md + - name: "05: Transition Effect" + href: tutorials/advanced/2d_shaders/05_transition_effect/index.md + - name: "06: Color Swap Effect" + href: tutorials/advanced/2d_shaders/06_color_swap_effect/index.md + - name: "07: Sprite Vertex Effect" + href: tutorials/advanced/2d_shaders/07_sprite_vertex_effect/index.md + - name: "08: Light Effect" + href: tutorials/advanced/2d_shaders/08_light_effect/index.md + - name: "09: Shadow Effect" + href: tutorials/advanced/2d_shaders/09_shadows_effect/index.md + - name: "10: Next Steps" + href: tutorials/advanced/2d_shaders/10_next_steps/index.md - name: Console Access href: console_access.md - name: Help and Support diff --git a/articles/tutorials/advanced/2d_shaders/01_introduction/index.md b/articles/tutorials/advanced/2d_shaders/01_introduction/index.md new file mode 100644 index 00000000..2b1725ff --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/01_introduction/index.md @@ -0,0 +1,31 @@ +--- +title: "Chapter 01: Getting Started" +description: "Prepare your project and get ready" +--- + +Welcome to the advanced 2D shaders tutorial! The goal of this series is to explore several concepts in MonoGame's 2D graphics capabilities, specifically with respect to shaders. + +## The Starting Code + +This tutorial series builds directly on top of the final code from the [Building 2D Games](./../../../building_2d_games/index.md) tutorial. It is essential that you start with this project. + +> [!note] +> You can get the complete starting source code for this tutorial here: +> [The final chapter's source code](https://github.com/MonoGame/MonoGame.Samples/tree/3.8.4/Tutorials/learn-monogame-2d/src/24-Shaders/) + +Once you have the code downloaded, open it in your IDE and run the `DungeonSlime` project to make sure everything is working correctly. You should see the title screen from the previous tutorial. + +## Project Structure + +The solution is organized into two main projects: + +- **`DungeonSlime`**: The main game project. This contains our game logic and game-specific content. +- **`MonoGameLibrary`**: Our reusable class library. We will be adding new, generic helper classes here that could be used in any of your future MonoGame projects. + +Most of our shader files (`.fx`) will be created in the `Content/effects` folder within the `DungeonSlime` project to start, and later within the `MonoGameLibrary` for shared effects. + +## What is Next + +Now that our project is set up, we can get to work. The focus for the first several chapters will be to create a workflow for developing shaders in MonoGame. Once we have a hot-reload system, a class to manage the effects, and a debug UI ready, we will carry on and build up 5 effects. The effects will range from simple pixel shaders and vertex shaders up to rendering techniques. As we develop these shaders together, we will build an intuition for how to tackle shader development. + +Continue to the next chapter, [Chapter 02: Hot Reload](../02_hot_reload/index.md). diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/images/background-watcher.png b/articles/tutorials/advanced/2d_shaders/02_hot_reload/images/background-watcher.png new file mode 100644 index 00000000..7b4aa85f Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/02_hot_reload/images/background-watcher.png differ diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/images/game.png b/articles/tutorials/advanced/2d_shaders/02_hot_reload/images/game.png new file mode 100644 index 00000000..d6d292e5 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/02_hot_reload/images/game.png differ diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/index.md b/articles/tutorials/advanced/2d_shaders/02_hot_reload/index.md new file mode 100644 index 00000000..d64eff8f --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/index.md @@ -0,0 +1,386 @@ +--- +title: "Chapter 02: Hot Reload" +description: "Setup workflows to reload shaders without restarting the game" +--- + +Before we can dive in and start writing shader effects, we should first take a moment to focus on our development environment. + +In this chapter, we will build a "hot-reload" system that will automatically detect changes to our shader files, recompile them, and load them into our running game on the fly. Writing shaders is often a highly iterative process, and keeping the cycle time fast is critical to keep development momentum up. By default, MonoGame's shader workflow may feel slow, especially compared to other modern game engines. Imagine you have written 90% of a shader, but to _test_ the shader, you need to: + +1. compile the shader, +2. run your game, +3. navigate to the part of your game that _uses_ the shader, +4. and _then_ you need to decide if the shader is working properly. + +When you get all the way to step 4, and realize that you accidentally compiled the shader with the wrong variable value, or forgot to call a function, it will be frustrating and it will slow down your development. Now you will need to repeat all of the steps again, and again, as you develop the shader. Worse of all, it takes the fun out of shader development. + +A hot-reload system allows you to get to step 4, fix whatever bug appeared, and validate the fix without needing to manually compile a shader, re-run the game, or navigate back to the relevant part of the game. This is a huge time-saver that will let us iterate and experiment with our visual effects much more quickly. + +> [!note] +> The hot-reload feature will be enabled during the _development_ of your game, but this system will not allow shaders to be dynamically reloaded in the final built game. + +Time to get started! + +If you are following along with code, here is the code from the end of the previous tutorial series, [Starting Code](https://github.com/MonoGame/MonoGame.Samples/tree/3.8.4/Tutorials/learn-monogame-2d/src/24-Shaders/) + +> [!note] +> This entire chapter is optional. If you just want to skip ahead to shader code, please pick up the code at the start of [Chapter 05: Transition Effect](../05_transition_effect/index.md). + +## Compiling Shaders + +Our snake-like game already has a shader effect and we can use it to validate the _hot-reload_ system as we develop it. By default, MonoGame compiles the shader from a `.fx` file into a `.xnb` file when the game is compiled. Then, the built `.xnb` file is copied into the game's build directory so it is available to load when the game starts. Our goal is to recompile the `.fx` file, and copy the resulting `.xnb` file whenever the shader is changed. Luckily, we can re-use a lot of the existing capabilities of MonoGame. + +### MSBuild Targets + +The existing automatic shader compilation is happening because the `DungeonSlime.csproj` file is referencing the `MonoGame.Content.Builder.Task` Nuget package. + +[!code-xml[](./snippets/DungeonSlime.csproj?highlight=4)] + +Nuget packages can add custom build behaviours and the `MonoGame.Content.Builder.Task` package is adding a step to the game's build that runs the MonoGame Content Builder tool. These sorts of build extensions use a conventional `.prop` and `.target` file system. If you are interested, you can learn more about how Nuget packages may extend MSBuild systems on Microsoft's [documentation website](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild?view=vs-2022). For reference, [this](https://github.com/MonoGame/MonoGame/blob/develop/Tools/MonoGame.Content.Builder.Task/MonoGame.Content.Builder.Task.targets#L172) is the `.targets` file for the `MonoGame.Content.Builder.Task`. + +This line defines a new MSBuild step, called `IncludeContent`: + +[!code-xml[](./snippets/snippet-2-01.xml)] + +You can learn more about what all the attributes do in MSBuild. Of particular note, the `BeforeTargets` attribute causes MSBuild to run the `IncludeContent` target before the `BeforeCompile` target is run, which is a standard target in the dotnet sdk. + +The `IncludeContent` target can run manually by invoking `dotnet build` by hand. In VSCode, open the embedded terminal to the _DungeonSlime_ project folder, and run the following command: + +> [!warning] +> The `dotnet` commands need to be run from the `DungeonSlime` folder, otherwise `dotnet` will not know _which_ project to use. + +[!code-sh[](./snippets/snippet-2-02.sh)] + +You should see log output indicating that the content for the _DungeonSlime_ game was built. + +### Dotnet Watch + +There is a tool called [`dotnet watch`](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-watch) that comes with the standard installation of `dotnet`. Normally, `dotnet watch` is used to watch for changes to `.cs` code files, recompile, and reload those changes into a program without restarting the program. You can try out `dotnet watch`'s normal behaviour by opening VSCode's embedded terminal to the _DungeonSlime_ project, and running the following command. The game should start normally: + +[!code-sh[](./snippets/snippet-2-03.sh)] + +> [!Tip] +> Use the `ctrl` + `c` key at the same time to quit the `dotnet watch` terminal process. + +Once the game has started, open the `TitleScene.cs` file in the _DungeonSlime_'s "Scenes" folder and comment out the `Clear()` function call in the title screen's `Draw()` method. Save the file, and you should see the title screen immediately stop clearing the background on each frame. If you restore the line and save again, the scene will start clearing the background again: + +[!code-csharp[](./snippets/snippet-2-04.cs?highlight=3)] + +As we are focusing on only intending to Hot Reload shaders in this tutorial, we do not want to recompile `.cs` files, but rather just the `.fx` files. `dotnet watch` can be configured to execute any MSBuild target rather than just recompile code. If you want to experiment with also hot-reloading `.cs` files you can, but it is not the focus of this tutorial. + +The following command uses the existing target provided by the `MonoGame.Content.Builder.Task`. + +[!code-sh[](./snippets/snippet-2-05.sh)] + +> [!Tip] +> All arguments passed after the `--` characters are passed to the `build` command itself, not `dotnet watch`: + +Now, when you change a _`.cs`_ file, all of the content files are rebuilt into `.xnb` files. + +> [!note] +> When you run `dotnet watch`, that is actually short hand for `dotnet watch run`. The `run` command _runs_ your game, but the `build` only _builds_ your program. Going forward, the `dotnet watch build` commands will not start your game, they will just build the content. To learn more, read the official documentation for [`dotnet watch`](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-watch). + +However, the `.xnb` files are still not being copied from the `Content/bin` folder to _DungeonSlime_'s runtime folder, the `.xnb` files are only copied during the full MSBuild of the game. The `IncludeContent` target on its own does not have all the context it needs to know how to copy the files in the final game project. To solve this, we need to introduce a new `` that copies the final `.xnb` files into _DungeonSlime_'s runtime folder. + +The existing `MonoGame.Content.Builder.Task` system knows what the files are, so we can re-use properties defined in the MonoGame package. + +Add this `` block to your `.csproj` file: + +[!code-xml[](./snippets/snippet-2-06.xml)] + +Now, instead of calling the `IncludeContent` target directly, change your terminal command to invoke the new `BuildAndCopyContent` target: + +[!code-sh[](./snippets/snippet-2-07.sh)] + +If you delete the `DungeonSlime/bin/Debug/net8.0/Content` folder, make an edit to a `.cs` file and save, you should see the `DungeonSlime/bin/Debug/net8.0/Content` folder be restored. + +> [!note] +> The `DungeonSlime/bin/Debug/net8.0/Content` folder may not appear _immediately_ in your IDE's file view. Sometimes the IDE caches the file system and doesn't notice the file change right away. Try opening the folder in your operating system's file explorer instead. + +The next step is to only invoke the target when `.fx` files are edited instead of `.cs` files. These settings can be configured with custom MSBuild item configurations. Open the `DungeonSlime.csproj` file and add this `` to specify configuration settings: + +[!code-xml[](./snippets/snippet-2-08.xml)] + +Now when you re-run the command from earlier, it will only run the `IncludeContent` target when `.fx` files have been changed. All edits to `.cs` files are ignored. Try adding a blank line to the `grayscaleEffect.fx` file, and notice the `dotnet watch` process re-build the content. + +However, if you ever use `dotnet watch` for anything else in your workflow, then the configuration settings are too aggressive, because they will be applied _all_ invocations of `dotnet watch`. We need to fix this before moving on, so that `dotnet watch` is not broken for future use cases. The `ItemGroup` can be optionally included when a certain condition is met. We will introduce a new MSBuild property called `OnlyWatchContentFiles`: + +[!code-xml[](./snippets/snippet-2-09.xml?highlight=1)] + +And now when `dotnet watch` is invoked, it needs to specify the new parameter: + +[!code-sh[](./snippets/snippet-2-10.sh)] + +> [!NOTE] +> The `watch` command will still listen for `.cs` file edits in the _MonoGameLibrary_ project, because those `.cs` files are grouped as a different compile unit. This is not a huge problem, but if you want to keep your content reloads to a strict minimum, then you need to add the same `` snippet to the `MonoGameLibrary.csproj` file. In this series, we will just accept that there are some extraneous content operations. + +The command is getting long and hard to type, and if we want to add more configuration, it will likely get even longer. Instead of invoking `dotnet watch` directly, it can be run as a new `` MSBuild step. Add this `` to your `DungeonSlime.csproj` file: + +[!code-xml[](./snippets/snippet-2-11.xml)] + +And now from the terminal, run the following `dotnet build` command: + +[!code-sh[](./snippets/snippet-2-12.sh)] + +> [!CAUTION] +> What does `--tl:off` do? +> +> This tutorial series assumes you are using `net8.0`, but theoretically there is nothing stopping you from using later version of `dotnet`. However, in `net9.0`, a [breaking change](https://learn.microsoft.com/en-us/dotnet/core/compatibility/sdk/9.0/terminal-logger) was made to the `dotnet build`'s log output. There is special code that tries to optimize the log output from `dotnet build` so that it does not feel overwhelming to look at. This system is called the _terminal logger_, and sadly it hides the underlying log output from `dotnet watch`. It was _opt-in_ for `net8.0`, but in `net9.0`, it is _enabled_ by default. +> +> **If you are using `net9.0` or above, you _must_ include this option.** +> +> `--tl:off` disables the terminal logger so that the `dotnet watch` log output does not get intercepted by the terminal logger. + +We now have a way to dynamically recompile shaders on file changes and copy the `.xnb` files into the game folder! There are a few final adjustments to make to the configuration. + +### Dotnet Watch, but smarter + +First, you may notice some odd characters in the log output after putting the `dotnet watch` inside the `WatchContent` target. This is because there are _emoji_ characters in the standard `dotnet watch` log stream, and some terminals do not understand how to display those, especially when streamed between `dotnet build`. To disable the _emoji_ characters, a `DOTNET_WATCH_SUPPRESS_EMOJIS` environment variable needs to be set: + +[!code-xml[](./snippets/snippet-2-13.xml?highlight=3)] + +Next, the `IncludeContent` target is doing a little too much work for our use case. It is trying to make sure the MonoGame Content Builder tools are installed. For our use case, we can opt out of that check by disabling the existing `AutoRestoreMGCBTool` MSBuild property. It also makes sense to pass `--restore:false` as well so that Nuget packages are not restored on each content file change: + +[!code-xml[](./snippets/snippet-2-14.xml?highlight=2)] + +To experiment with the system, re-run the following command: + +[!code-sh[](./snippets/snippet-2-15.sh)] + +And then cause some sort of compiler-error in the `grayscaleEffect.fx` file, such as adding the line, `"tunafish"` to the top of the file. When you save it, you should see the terminal spit out an error containing information about the compilation failure, + +```text + error X3000: unrecognized identifier 'tunafish' +``` + +Remove the `"tunafish"` line and save again, and the watch program should log some lines similar to these: + +```text + dotnet watch : Started + C:/proj/MonoGame.Samples/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/effects/grayscaleEffect.fx + Copying Content... + + Build succeeded. + 0 Warning(s) + 0 Error(s) + + Time Elapsed 00:00:01.40 + dotnet watch : Exited + dotnet watch : Waiting for a file to change before restarting dotnet... + +``` + +## Reload shaders in-game + +Now anytime the `.fx` files are modified, they will be recompiled and copied into the game's runtime folder. However, the game itself does not know to _reload_ the `Effect` instances. In this section, we will create a utility to extend the capabilities of the [`ContentManager`](xref:Microsoft.Xna.Framework.Content.ContentManager) to enable it to respond to these dynamic file updates. + +It is important to make a distinction between assets the game _expects_ to be reloaded and assets that the game does _not_ care about reloading. This tutorial will demonstrate how to create an explicit system where individual assets opt _into_ being _hot reloadable_, rather than creating a system where all assets automatically handle dynamic reloading. + +### Extending Content Manager + +Currently, the `grayscaleEffect.fx` is being loaded in the `GameScene` 's `LoadContent()` method like this: + +[!code-csharp[](./snippets/snippet-2-16.cs)] + +The `.Load()` function in the existing `ContentManager` is almost sufficient for our needs, but it returns a regular `Effect`, which has no understanding of the dynamic nature of the new content workflow. + +1. Create a new `Content` folder within the _MonoGameLibrary_ project, add a new file named `ContentManagerExtensions.cs`, and add the following code for the foundation of the new system: + + [!code-csharp[](./snippets/snippet-2-17.cs)] + +2. Within this Extension class we will add an [extension method](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods) for the existing `MonoGame`'s `ContentManager` class and give it capabilities it does not currently have: + + [!code-csharp[](./snippets/snippet-2-18.cs)] + +3. This new `Watch` function is an opportunity to enhance how content is loaded. Use this new function to load the `_greyscaleEffect` effect in the `GameScene.cs` class `LoadContent()` method: + + [!code-csharp[](./snippets/snippet-2-19.cs)] + +4. And finally, adding the `using` statement at the top of the `GameScene` class to let it know where the new extension method we created is located: + + [!code-csharp[](./snippets/snippet-2-19-usings.cs?highlight=9)] + +### Setting the correct working path + +There are two common ways to run your game as you develop: + +- running the game from a terminal by typing `dotnet run`, +- running the game from an IDE.  + +When you use `dotnet run`, dotnet itself sets the [_working directory_](https://learn.microsoft.com/en-us/dotnet/api/system.io.directory.getcurrentdirectory?view=net-9.0) of the program to the folder that contains your `DungeonSlime.csproj` file. However, many IDEs will set the working directory to be within the `/bin` (output) folder of your project, next to the built `DungeonSlime.dll/exe` file. + +The working directory is important, because the `ContentManagerExtensions.cs` class we are writing will use the `manager.RootDirectory` to reassemble content `.xnb` file paths. The `manager.RootDirectory` is derived from the working directory, so if the working directory changes based on how we start the game, our `ContentManagerExtensions.cs` code will produce different `.xnb` paths. + +The actual `.xnb` files are copied to the `/bin` subfolder, so at the moment, running the game from the terminal will not work unless you _manually_ specify the working directory. To solve, this we can force the working directory by adding the [``](https://learn.microsoft.com/en-us/dotnet/core/project-sdk/msbuild-props#runworkingdirectory) property to the `DungeonSlime.csproj` file: + +[!code-xml[](./snippets/snippet-2-36.xml?highlight=5)] + +### The `WatchedAsset` class + +The new system will need to keep track of additional information for each asset that we plan to be _hot-reloadable_, the data will live in a new class, `WatchedAsset`. + +1. Add a new file named `WatchedAsset.cs` in the _MonoGameLibrary/Content_ folder and paste the following into the new class: + + [!code-csharp[](./snippets/snippet-2-20.cs)] + +2. Next, we need to update the `Watch` method in the _ContentManagerExtensions_ to return a `WatchedAsset` instead of the direct `Effect` it used originally: + + [!code-csharp[](./snippets/snippet-2-21.cs)] + +3. Now, any asset that requires hot-reloading, such as the `_greyscaleEffect` in the `GameScene`, also needs to change to use a `WatchedAsset` instead of simply an `Effect`: + + [!code-csharp[](./snippets/snippet-2-22.cs)] + + > [!IMPORTANT] + > This will cause a few compilation errors where the `_greyscaleEffect` is used throughout the rest of the `GameScene`. + > The compile errors appear because `_greyscaleEffect` used to be an `Effect`, but now the `Effect` is actually available as `_grayscaleEffect.Asset`. + +4. To correct the errors found, simply update the `Draw` method of the `GameScene` with the following using the guidance above: + + [!code-csharp[](./snippets/snippet-2-22-draw.cs?highlight=9,12)] + +### Reload Extension + +It is time to extend the `ContentManagerExtensions` extension method that the game code will use to "opt in" to reloading an asset. From the earlier section, anytime a `.fx` file is updated, the compiled `.xnb` file will be copied into the game's runtime folder, the operating system will keep track of the last time the `.xnb` file was written, and we can leverage that information with the `WatchedAsset.UpdatedAt` property to understand if the `.xnb` file is _newer_ than the current loaded `Effect`. + +1. The following `TryRefresh` method will take a `WatchedAsset` and update the inner `Asset` property _if_ the `.xnb` file is newer. The method returns `true` when the asset is reloaded, which will be useful later. + Add the following method to the `ContentManagerExtensions` class: + + [!code-csharp[](./snippets/snippet-2-23.cs)] + +2. Next, at the top of the `Update()` method in the `GameScene` class, add the following line to opt into reloading the `_grayscaleEffect` asset: + + [!code-csharp[](./snippets/snippet-2-24.cs?highlight=3-4)] + +3. Now, when the `grayscaleEffect.fx` file is modified, the `dotnet watch` system will compile it to an `.xnb` file, copy it to the game's runtime folder, and then in the `Update()` loop, the `TryRefresh()` method will load the new effect and the new shader code will be running live in the game. + + Try it out by adding this temporary line right before the `return` statement in the `grayscaleEffect.fx` file, make sure the `dotnet build -t:WatchContent --tl:off` is running in the terminal and [start the game in debug](/articles/tutorials/building_2d_games/02_getting_started/index.html?tabs=windows#creating-your-first-monogame-application) as normal: + + [!code-hlsl[](./snippets/snippet-2-25.hlsl?highlight=18-19)] + +This video shows the effect changing. + +| ![Figure 2-1: The reload system is working](./videos/shader-reload.mp4) | +| :---------------------------------------------------------------------: | +| **Figure 2-1: The reload system is working** | + +> [!NOTE] +> Make sure to remove the change to the shader, unless you prefer the greyscale background red. + +## Final Touches + +The _hot reload_ system is almost done, however, there are a few quality of life features to finish up. + +### File Locking + +There is an edge case in the `TryRefresh()` function that when the `.xnb` file is checked to see if it is more recent than the in-memory asset that it might still be in use (locked) by the MonoGame Content Builder, it may still be _actively_ writing the `.xnb` file when the function runs. The game will fail to read the file while it is being written. The solution to this problem is to simply wait and try loading the `.xnb` file in the next frame. The trick however, is that C# does not have a standard way to check if a file is currently locked. + +The best way to check is to simply try and open the file, and if an `Exception` is thrown, assume the file is not readable. + +1. To start, add the following function to the `ContentManagerExtensions` class: + + [!code-csharp[](./snippets/snippet-2-26.cs)] + +2. Then modify the `TryRefresh` function by returning early if the file is locked: + + [!code-csharp[](./snippets/snippet-2-27.cs?highlight=16-17)] + +### Access the old Asset on reload + +Anytime a new asset is loaded, the old asset is unloaded from the `ContentManager`. However, it will be helpful to be able to access in-memory data about the old asset version. For shaders, there is metadata and runtime configuration that should be applied to the new version. + +This will be more relevant in the next chapter, but let us handle this now: + +1. Modify the `TryRefresh` function to contain `out` parameter of the old asset, updating the method signature to the following: + + [!code-csharp[](./snippets/snippet-2-28.cs)] + +2. Before updating the `watchedAsset.Asset`, set the `oldAsset` as the previous in-memory asset: + + [!code-csharp[](./snippets/snippet-2-29.cs?highlight=3,24-25)] + +3. **Do not** forget that the place where the `grayscaleEffect` calls the `TryRefresh()` function in the `GameScene` class will also need to include a no-op out variable (essentially passing a reference, but we [discard the result](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/discards)): + + [!code-csharp[](./snippets/snippet-2-30.cs?highlight=4)] + +### Refresh Convenience Function + +Finally, we need to address a subtle usability bug in the existing code. The `TryRefresh` function may `Unload` an asset if a new version is loaded. However, it is not obvious that the `ContentManager` instance doing the `Unload` operation is the same `ContentManager` instance that loaded the original asset in the first place. + +To solve the possible collision between ContentManager instances, we need to update the following: + +1. Add a `ContentManager` property to the `WatchedAsset` class so that the asset itself knows which `ContentManager` is responsible for unloading old versions: + + [!code-csharp[](./snippets/snippet-2-31.cs)] + +2. Adjust the `WatchAsset` function of the `ContentManagerExtensions` class to fill in this new property: + + [!code-csharp[](./snippets/snippet-2-32.cs?highlight=9)] + +3. Then, in the `TryRefresh` function, a small assertion can be added to validate the `ContentManager` is the same: + + [!code-csharp[](./snippets/snippet-2-33.cs?highlight=5-9)] + + It is annoying to have use the `ContentManager` directly to call `TryRefresh` in the game loop. It would be easier to rely on the new `Owner` property, so let us fix that by adding another Refresh overload to the `WatchedAsset` to just use the manager it is already referencing: + +4. Add the following method to the `WatchedAsset` class: + + [!code-csharp[](./snippets/snippet-2-34.cs)] + +5. Finally, update the `GameScene` to use the new convenience method to refresh the `_grayscaleEffect` instead: + + [!code-csharp[](./snippets/snippet-2-35.cs?highlight=4)] + +### Auto start the watcher + +The hot reload system is working, but it has a serious weakness. You have to _remember_ to run the following command before starting development your game if you want the Hot Reload system to function: + +[!code-sh[](./snippets/snippet-2-12.sh)] + +After you run that command in a terminal, you still need to start your game normally. If you _only_ started the game, but never started the watcher, then your shaders would never be hot reloadable. This kind of error is dangerous because it can undermine trust in the hot reload system itself. + +It would be better if the watcher was started automatically when the game is run, so that you only need to do one thing, _run the game_. + +1. In the `ContentManagerExtensions.cs` file, add this function to the class: + + [!code-cs[](./snippets/snippet-2-37.cs)] + +2. Next add the following `using` statements to the top of the file: + + ```csharp + using System.Reflection; + using System.Diagnostics; + ``` + +3. Finally, call the new function from the DungeonSlime's `Program.cs` file before starting the game: + + [!code-cs[](./snippets/snippet-2-38.cs?highlight=1)] + +Now you do not need to start the watcher manually, instead, you can simply **start the game normally** (either "debug -> Start new instance, or simply `dotnet run`) and the watcher process should appear in the background/another window. + +> [!tip] +> If you are running the game via the terminal, and you do _not_ want to start the background content watcher, add the `--no-reload` command line option. +> +> `dotnet run --no-reload` + +| ![Figure 2-2: The content watcher will appear as a separate window](./images/background-watcher.png) | ![Figure 2-3: The _DungeonSlime_ game appears as normal](./images/game.png) | +| :--------------------------------------------------------------------------------------------------: | --------------------------------------------------------------------------: | +| **Figure 2-2: The content watcher will appear as a separate window** | **Figure 2-3: The _DungeonSlime_ game appears as normal** | + +> [!TIP] +> The `DungeonSlime.csproj` file declares the project's `OutputType` as `WinExe`. This means that the standard output of the game do not appear in your console by default. It is also the reason the watcher application appears in a separate window. If you ever need to see your game's console output, switch the `OutputType` to `Exe`. When you do this, the watcher will not appear in a separate window. + +## Conclusion + +And with that, we have a powerful hot-reload system in place! In this chapter, you accomplished the following: + +- Configured `dotnet watch` to monitor your `.fx` shader files. +- Created a custom MSBuild `` to automatically recompile and copy your built shaders. +- Wrote a C# wrapper class, `WatchedAsset`, to track asset file changes. +- Extended `ContentManager` with a `TryRefresh` method to load new assets into the running game. + +This new workflow is going to make the rest of our journey much more fun and productive. In the next chapter, we will build on this foundation by creating a `Material` class to help us organize and safely interact with our shaders. + +You can find the complete code sample for this chapter, [here](https://github.com/MonoGame/MonoGame.Samples/tree/3.8.4/Tutorials/2dShaders/src/02-Hot-Reload-System/). + +Continue to the next chapter, [Chapter 03: The Material Class](../03_the_material_class/index.md). diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/DungeonSlime.csproj b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/DungeonSlime.csproj new file mode 100644 index 00000000..8a04eea3 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/DungeonSlime.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/WatchedAsset.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/WatchedAsset.cs new file mode 100644 index 00000000..6bf3f638 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/WatchedAsset.cs @@ -0,0 +1,42 @@ +#region declaration +using System; +using Microsoft.Xna.Framework.Content; + +namespace MonoGameLibrary.Content; + +public class WatchedAsset +{ + +} + +#endregion +{ +#region members + /// + /// The latest version of the asset. + /// + public T Asset { get; set; } + + /// + /// The last time the was loaded into memory. + /// + public DateTimeOffset UpdatedAt { get; set; } + + /// + /// The name of the . This is the name used to load the asset from disk. + /// + public string AssetName { get; init; } + + /// + /// The instance that loaded the asset. + /// + public ContentManager Owner { get; init; } + #endregion + + #region methods + public bool TryRefresh(out T oldAsset) + { + return Owner.TryRefresh(this, out oldAsset); + } + #endregion +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/dotnet-tools.json b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/dotnet-tools.json new file mode 100644 index 00000000..afd4e2c4 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/dotnet-tools.json @@ -0,0 +1,36 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-mgcb": { + "version": "3.8.3", + "commands": [ + "mgcb" + ] + }, + "dotnet-mgcb-editor": { + "version": "3.8.3", + "commands": [ + "mgcb-editor" + ] + }, + "dotnet-mgcb-editor-linux": { + "version": "3.8.3", + "commands": [ + "mgcb-editor-linux" + ] + }, + "dotnet-mgcb-editor-windows": { + "version": "3.8.3", + "commands": [ + "mgcb-editor-windows" + ] + }, + "dotnet-mgcb-editor-mac": { + "version": "3.8.3", + "commands": [ + "mgcb-editor-mac" + ] + } + } +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-01.xml b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-01.xml new file mode 100644 index 00000000..edb9b07b --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-01.xml @@ -0,0 +1,7 @@ + diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-02.sh b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-02.sh new file mode 100644 index 00000000..7a47a07c --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-02.sh @@ -0,0 +1 @@ +dotnet build -t:IncludeContent diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-03.sh b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-03.sh new file mode 100644 index 00000000..afd23764 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-03.sh @@ -0,0 +1 @@ +dotnet watch diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-04.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-04.cs new file mode 100644 index 00000000..7c79ace0 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-04.cs @@ -0,0 +1,3 @@ +public override void Draw(GameTime gameTime) +{ + // Core.GraphicsDevice.Clear(new Color(32, 40, 78, 255)); diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-05.sh b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-05.sh new file mode 100644 index 00000000..f7dd54a2 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-05.sh @@ -0,0 +1 @@ +dotnet watch build -- --target:IncludeContent diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-06.xml b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-06.xml new file mode 100644 index 00000000..61872915 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-06.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-07.sh b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-07.sh new file mode 100644 index 00000000..87266e8c --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-07.sh @@ -0,0 +1 @@ +dotnet watch build -- --target:BuildAndCopyContent diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-08.xml b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-08.xml new file mode 100644 index 00000000..8e1efc89 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-08.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-09.xml b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-09.xml new file mode 100644 index 00000000..3b60df8f --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-09.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-10.sh b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-10.sh new file mode 100644 index 00000000..eacc26fd --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-10.sh @@ -0,0 +1 @@ +dotnet watch build --property OnlyWatchContentFiles=true -- --target:BuildAndCopyContent diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-11.xml b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-11.xml new file mode 100644 index 00000000..ae672c71 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-11.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-12.sh b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-12.sh new file mode 100644 index 00000000..ff73a7aa --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-12.sh @@ -0,0 +1 @@ +dotnet build -t:WatchContent --tl:off diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-13.xml b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-13.xml new file mode 100644 index 00000000..9748278d --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-13.xml @@ -0,0 +1,4 @@ + + + diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-14.xml b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-14.xml new file mode 100644 index 00000000..334fc16f --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-14.xml @@ -0,0 +1,4 @@ + + + diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-15.sh b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-15.sh new file mode 100644 index 00000000..ff73a7aa --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-15.sh @@ -0,0 +1 @@ +dotnet build -t:WatchContent --tl:off diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-16.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-16.cs new file mode 100644 index 00000000..01f04bac --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-16.cs @@ -0,0 +1,2 @@ +// Load the grayscale effect +_grayscaleEffect = Content.Load("effects/grayscaleEffect"); diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-17.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-17.cs new file mode 100644 index 00000000..080e36e3 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-17.cs @@ -0,0 +1,10 @@ +using System; +using System.IO; +using Microsoft.Xna.Framework.Content; + +namespace MonoGameLibrary.Content; + +public static class ContentManagerExtensions +{ + +} diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-18.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-18.cs new file mode 100644 index 00000000..d6c2ca22 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-18.cs @@ -0,0 +1,5 @@ +public static T Watch(this ContentManager manager, string assetName) +{ + var asset = manager.Load(assetName); + return asset; +} diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-19-usings.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-19-usings.cs new file mode 100644 index 00000000..a2bb18e2 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-19-usings.cs @@ -0,0 +1,11 @@ +using System; +using DungeonSlime.GameObjects; +using DungeonSlime.UI; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Graphics; +using MonoGameGum; +using MonoGameLibrary; +using MonoGameLibrary.Content; +using MonoGameLibrary.Graphics; +using MonoGameLibrary.Scenes; \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-19.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-19.cs new file mode 100644 index 00000000..275a4c71 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-19.cs @@ -0,0 +1 @@ +_grayscaleEffect = Content.Watch("effects/grayscaleEffect"); diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-20.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-20.cs new file mode 100644 index 00000000..b8c5fe9d --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-20.cs @@ -0,0 +1,22 @@ +using System; +using Microsoft.Xna.Framework.Content; + +namespace MonoGameLibrary.Content; + +public class WatchedAsset +{ + /// + /// The latest version of the asset. + /// + public T Asset { get; set; } + + /// + /// The last time the was loaded into memory. + /// + public DateTimeOffset UpdatedAt { get; set; } + + /// + /// The name of the . This is the name used to load the asset from disk. + /// + public string AssetName { get; init; } +} diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-21.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-21.cs new file mode 100644 index 00000000..5c947164 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-21.cs @@ -0,0 +1,10 @@ +public static WatchedAsset Watch(this ContentManager manager, string assetName) +{ + var asset = manager.Load(assetName); + return new WatchedAsset + { + AssetName = assetName, + Asset = asset, + UpdatedAt = DateTimeOffset.Now, + }; +} diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-22-draw.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-22-draw.cs new file mode 100644 index 00000000..9082d786 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-22-draw.cs @@ -0,0 +1,21 @@ + public override void Draw(GameTime gameTime) + { + // Clear the back buffer. + Core.GraphicsDevice.Clear(Color.CornflowerBlue); + + if (_state != GameState.Playing) + { + // We are in a game over state, so apply the saturation parameter. + _grayscaleEffect.Asset.Parameters["Saturation"].SetValue(_saturation); + + // And begin the sprite batch using the grayscale effect. + Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp, effect: _grayscaleEffect.Asset); + } + else + { + // Otherwise, just begin the sprite batch as normal. + Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp); + } + + + // Rest of the Draw method... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-22.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-22.cs new file mode 100644 index 00000000..1dfe0059 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-22.cs @@ -0,0 +1,2 @@ +// The grayscale shader effect. +private WatchedAsset _grayscaleEffect; diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-23.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-23.cs new file mode 100644 index 00000000..b0e1f9f4 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-23.cs @@ -0,0 +1,24 @@ +public static bool TryRefresh(this ContentManager manager, WatchedAsset watchedAsset) +{ + // get the same path that the ContentManager would use to load the asset + var path = Path.Combine(manager.RootDirectory, watchedAsset.AssetName) + ".xnb"; + + // ask the operating system when the file was last written. + var lastWriteTime = File.GetLastWriteTime(path); + + // when the file's write time is less recent than the asset's latest read time, + // then the asset does not need to be reloaded. + if (lastWriteTime <= watchedAsset.UpdatedAt) + { + return false; + } + + // clear the old asset to avoid leaking + manager.UnloadAsset(watchedAsset.AssetName); + + // load the new asset and update the latest read time + watchedAsset.Asset = manager.Load(watchedAsset.AssetName); + watchedAsset.UpdatedAt = lastWriteTime; + + return true; +} diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-24.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-24.cs new file mode 100644 index 00000000..a58f9122 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-24.cs @@ -0,0 +1,7 @@ +public override void Update(GameTime gameTime) +{ + // Update the grayscale effect if it was changed + Content.TryRefresh(_grayscaleEffect); + + // Ensure the UI is always updated + _ui.Update(gameTime); diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-25.hlsl b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-25.hlsl new file mode 100644 index 00000000..7cf0e889 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-25.hlsl @@ -0,0 +1,23 @@ + + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + // Sample the texture + float4 color = tex2D(SpriteTextureSampler, input.TextureCoordinates) * input.Color; + + // Calculate the grayscale value based on human perception of colors + float grayscale = dot(color.rgb, float3(0.3, 0.59, 0.11)); + + // create a grayscale color vector (same value for R, G, and B) + float3 grayscaleColor = float3(grayscale, grayscale, grayscale); + + // Linear interpolation between he grayscale color and the original color's + // rgb values based on the saturation parameter. + float3 finalColor = lerp(grayscale, color.rgb, Saturation); + + // modify the final color, just for debug visualization + finalColor *= float3(1, 0, 0); + + // Return the final color with the original alpha value + return float4(finalColor, color.a); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-26.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-26.cs new file mode 100644 index 00000000..5b3577af --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-26.cs @@ -0,0 +1,14 @@ +private static bool IsFileLocked(string path) +{ + try + { + using FileStream _ = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + // File is not locked + return false; + } + catch (IOException) + { + // File is locked or inaccessible + return true; + } +} diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-27.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-27.cs new file mode 100644 index 00000000..7b9daf44 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-27.cs @@ -0,0 +1,27 @@ +public static bool TryRefresh(this ContentManager manager, WatchedAsset watchedAsset) +{ + // get the same path that the ContentManager would use to load the asset + var path = Path.Combine(manager.RootDirectory, watchedAsset.AssetName) + ".xnb"; + + // ask the operating system when the file was last written. + var lastWriteTime = File.GetLastWriteTime(path); + + // when the file's write time is less recent than the asset's latest read time, + // then the asset does not need to be reloaded. + if (lastWriteTime <= watchedAsset.UpdatedAt) + { + return false; + } + + // wait for the file to not be locked. + if (IsFileLocked(path)) return false; + + // clear the old asset to avoid leaking + manager.UnloadAsset(watchedAsset.AssetName); + + // load the new asset and update the latest read time + watchedAsset.Asset = manager.Load(watchedAsset.AssetName); + watchedAsset.UpdatedAt = lastWriteTime; + + return true; +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-28.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-28.cs new file mode 100644 index 00000000..f41db328 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-28.cs @@ -0,0 +1 @@ +public static bool TryRefresh(this ContentManager manager, WatchedAsset watchedAsset, out T oldAsset) diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-29.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-29.cs new file mode 100644 index 00000000..4e047c01 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-29.cs @@ -0,0 +1,32 @@ +public static bool TryRefresh(this ContentManager manager, WatchedAsset watchedAsset, out T oldAsset) +{ + oldAsset = default; + + // get the same path that the ContentManager would use to load the asset + var path = Path.Combine(manager.RootDirectory, watchedAsset.AssetName) + ".xnb"; + + // ask the operating system when the file was last written. + var lastWriteTime = File.GetLastWriteTime(path); + + // when the file's write time is less recent than the asset's latest read time, + // then the asset does not need to be reloaded. + if (lastWriteTime <= watchedAsset.UpdatedAt) + { + return false; + } + + // wait for the file to not be locked. + if (IsFileLocked(path)) return false; + + // clear the old asset to avoid leaking + manager.UnloadAsset(watchedAsset.AssetName); + + // return the old asset + oldAsset = watchedAsset.Asset; + + // load the new asset and update the latest read time + watchedAsset.Asset = manager.Load(watchedAsset.AssetName); + watchedAsset.UpdatedAt = lastWriteTime; + + return true; +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-30.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-30.cs new file mode 100644 index 00000000..0274ebc3 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-30.cs @@ -0,0 +1,7 @@ +public override void Update(GameTime gameTime) +{ + // Update the grayscale effect if it was changed + Content.TryRefresh(_grayscaleEffect, out _); + + // Ensure the UI is always updated + _ui.Update(gameTime); diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-31.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-31.cs new file mode 100644 index 00000000..cffa4c20 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-31.cs @@ -0,0 +1,4 @@ +/// +/// The instance that loaded the asset. +/// +public ContentManager Owner { get; init; } diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-32.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-32.cs new file mode 100644 index 00000000..e812f0a2 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-32.cs @@ -0,0 +1,11 @@ +public static WatchedAsset Watch(this ContentManager manager, string assetName) +{ + var asset = manager.Load(assetName); + return new WatchedAsset + { + AssetName = assetName, + Asset = asset, + UpdatedAt = DateTimeOffset.Now, + Owner = manager + }; +} diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-33.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-33.cs new file mode 100644 index 00000000..542062cd --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-33.cs @@ -0,0 +1,38 @@ +public static bool TryRefresh(this ContentManager manager, WatchedAsset watchedAsset, out T oldAsset) +{ + oldAsset = default; + + // ensure the ContentManager is the same one that loaded the asset + if (manager != watchedAsset.Owner) + { + throw new ArgumentException($"Used the wrong ContentManager to refresh {watchedAsset.AssetName}"); + } + + // get the same path that the ContentManager would use to load the asset + var path = Path.Combine(manager.RootDirectory, watchedAsset.AssetName) + ".xnb"; + + // ask the operating system when the file was last written. + var lastWriteTime = File.GetLastWriteTime(path); + + // when the file's write time is less recent than the asset's latest read time, + // then the asset does not need to be reloaded. + if (lastWriteTime <= watchedAsset.UpdatedAt) + { + return false; + } + + // wait for the file to not be locked. + if (IsFileLocked(path)) return false; + + // clear the old asset to avoid leaking + manager.UnloadAsset(watchedAsset.AssetName); + + // return the old asset + oldAsset = watchedAsset.Asset; + + // load the new asset and update the latest read time + watchedAsset.Asset = manager.Load(watchedAsset.AssetName); + watchedAsset.UpdatedAt = lastWriteTime; + + return true; +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-34.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-34.cs new file mode 100644 index 00000000..254d92f4 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-34.cs @@ -0,0 +1,7 @@ +/// +/// Attempts to refresh the asset if it has changed on disk using the registered owner . +/// +public bool TryRefresh(out T oldAsset) +{ + return Owner.TryRefresh(this, out oldAsset); +} diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-35.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-35.cs new file mode 100644 index 00000000..510d01ce --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-35.cs @@ -0,0 +1,7 @@ +public override void Update(GameTime gameTime) +{ + // Update the grayscale effect if it was changed + _grayscaleEffect.TryRefresh(out _); + + // Ensure the UI is always updated + _ui.Update(gameTime); diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-36.xml b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-36.xml new file mode 100644 index 00000000..666163b1 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-36.xml @@ -0,0 +1,9 @@ + + + + + bin/$(Configuration)/$(TargetFramework) + + + + diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-37.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-37.cs new file mode 100644 index 00000000..6388338d --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-37.cs @@ -0,0 +1,74 @@ +[Conditional("DEBUG")] +public static void StartContentWatcherTask() +{ + var args = Environment.GetCommandLineArgs(); + foreach (var arg in args) + { + // if the application was started with the --no-reload option, then do not start the watcher. + if (arg == "--no-reload") return; + } + + // identify the project directory + var projectFile = Assembly.GetEntryAssembly().GetName().Name + ".csproj"; + var current = Directory.GetCurrentDirectory(); + string projectDirectory = null; + + while (current != null && projectDirectory == null) + { + if (File.Exists(Path.Combine(current, projectFile))) + { + // the valid project csproj exists in the directory + projectDirectory = current; + } + else + { + // try looking in the parent directory. + // When there is no parent directory, the variable becomes 'null' + current = Path.GetDirectoryName(current); + } + } + + // if no valid project was identified, then it is impossible to start the watcher + if (string.IsNullOrEmpty(projectDirectory)) return; + + // start the watcher process + var process = Process.Start(new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "build -t:WatchContent --tl:off", + WorkingDirectory = projectDirectory, + WindowStyle = ProcessWindowStyle.Normal, + UseShellExecute = false, + CreateNoWindow = false + }); + + // when this program exits, make sure to emit a kill signal to the watcher process + AppDomain.CurrentDomain.ProcessExit += (_, __) => + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + /* ignore */ + } + }; + AppDomain.CurrentDomain.UnhandledException += (sender, e) => + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + /* ignore */ + } + }; +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-38.cs b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-38.cs new file mode 100644 index 00000000..3e015968 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/02_hot_reload/snippets/snippet-2-38.cs @@ -0,0 +1,3 @@ +MonoGameLibrary.Content.ContentManagerExtensions.StartContentWatcherTask(); +using var game = new DungeonSlime.Game1(); +game.Run(); \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/02_hot_reload/videos/shader-reload.mp4 b/articles/tutorials/advanced/2d_shaders/02_hot_reload/videos/shader-reload.mp4 new file mode 100644 index 00000000..7f8b1711 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/02_hot_reload/videos/shader-reload.mp4 differ diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/index.md b/articles/tutorials/advanced/2d_shaders/03_the_material_class/index.md new file mode 100644 index 00000000..25ffff5a --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/index.md @@ -0,0 +1,222 @@ +--- +title: "Chapter 03: The Material Class" +description: "Create a wrapper class to help manage shaders" +--- + +In [Chapter 24](../../../building_2d_games/24_shaders/index.md) of the original [2D Game Series](../../../building_2d_games/index.md), you learned about MonoGame's [`Effect`](xref:Microsoft.Xna.Framework.Graphics.Effect) class. When `.fx` shaders are compiled, the compiled code is then loaded into MonoGame as an `Effect` instance. The `Effect` class provides a few powerful utilities for setting shader parameters, but otherwise, it is a fairly bare-bones container for the compiled shader code. + +> [!NOTE] +> MonoGame also ships with standard `Effect` sub-classes that can be useful for bootstrapping a game without needing to write any custom shader code. However, all of these standard `Effect` types are geared towards 3D games, except for _1_, called the [`SpriteEffect`](xref:Microsoft.Xna.Framework.Graphics.SpriteEffect). This will be discussed in [Chapter 7: Sprite Vertex Effect](../07_sprite_vertex_effect/index.md). + +In this chapter, we will create a small wrapper class, called the `Material`, that will handle shader parameters, hot-reload, and serve as a baseline for future additions. + +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/02-Hot-Reload-System/). + +> [!NOTE] +> The tutorial assumes you have the `watch` process running automatically as you start the _DungeonSlime_ game. Otherwise, make sure to start it manually in the terminal in VSCode: +> +> ```dotnet build -t:WatchContent --tl:off``` + +## The Material Class + +The `_grayscaleEffect` serves a very specific purpose, but imagine instead of just _decreasing_ the saturation, the effect could also _increase_ the saturation. In that hypothetical, then calling it a "grayscale" effect only captures _some_ of the shader's value. Setting the `Saturation` to `0` would configure the shader to be a grayscale effect, but setting the `Saturation` really high would configure the shader to be a super-saturation effect. A single shader can be configured to create multiple distinct visuals. Many game engines use the term, _Material_, to recognize each _configuration_ of a shader effect. + +A material definition represents a compiled `Effect` **and** the runtime configuration for the `Effect`. For example, the `_grayscaleEffect` shader has a single property called `Saturation`. The _**value**_ of the property is essential to the existence of the `_grayscaleEffect`. It would also be useful to have logic that _owns_ these shader parameter values. + +We will create a class called `Material` that manages all of our shader related metadata: + +1. Start by creating a new file in the _MonoGameLibrary/Graphics_ folder called `Material.cs`: + + [!code-csharp[](./snippets/snippet-3-01.cs)] + +2. In order to help create instances of the `Material` we will add the following method in the `ContentManagerExtensions` file we created in the previous chapter: + + [!code-csharp[](./snippets/snippet-3-02.cs)] + +3. And add the following `using` statements to the top of the `ContentManagerExtensions` class so that the `Material` and `Effect` classes can be recognised: + + [!code-csharp[](./snippets/snippet-3-02-using.cs?highlight=6-7)] + + > [!NOTE] + > The `Material` has been built to be hot-reloadable. Later in this chapter, we will see how the performance cost for supporting hot-reload is negligible by using the `[Conditional("DEBUG")]` attribute. However, if you do not want the `Material` to be hot-reloadable, then change the `Material`'s `Asset` field to be an `Effect` rather than a `WatchedAsset`. There will be some minor differences in the rest of the tutorial series, but the only major difference is that the `Material` will not be hot-reloadable during development. + +4. Next, in the `GameScene` class, adjust the `_grayscaleEffect` property to use the new `Material` class: + + [!code-csharp[](./snippets/snippet-3-03.cs)] + +Changing the `_grayscaleEffect` from an `Effect` to `Material` is going to cause a few compilation errors. The fixes are listed below. + +- When instantiating the `_grayscaleEffect` in the `LoadContent` method, use the new method: + + [!code-csharp[](./snippets/snippet-3-04.cs)] + +- In the `Update` Method, when checking if the asset needs to be reloaded for hot-reload, use the `.Asset` sub property: + + [!code-csharp[](./snippets/snippet-3-05.cs)] + +- And in the `Draw()` method, update it to use the `.Effect` shortcut property: + + [!code-csharp[](./snippets/snippet-3-06.cs)] + +### Setting Shader Parameters + +You already saw how to set a shader property by using the `Saturation` value in the `_grayscaleEffect` shader. However, as you develop shaders in MonoGame, you will eventually "_accidentally_" try to set a shader property that does not exist in your shader. When this happens, the code will throw a `NullReferenceException` rather than fail silently. +For example, if you tried to add this line of the code to the `Update` loop: + +[!code-csharp[](./snippets/snippet-3-07.cs)] + +You will see this type of `NullReference` error when the project tries to start and draw the scene: + +```text +System.NullReferenceException: Object reference not set to an instance of an object. +``` + +> [!CAUTION] +> Do not actually add the `DoesNotExist` sample, because it will break your code. + +On its own this would not be too difficult to accept. However, MonoGame's shader compiler will aggressively **remove** properties that are not actually being _used_ in your shader code. Even if you wrote a shader that had a `DoesNotExist` property, if it was not being used to compute the return value of the shader, it will be removed. The compiler is good at optimizing away unused variables.  + +For example, in the `grayscaleEffect.fx` file, change the last few lines of the `MainPS` function to the following: + +[!code-hlsl[](./snippets/snippet-3-08.hlsl?highlight=17-18)] + +If you run the game, enter the `GameScene`, and wait for the game over screen to appear, you will see a `NullReferenceException` (and the game will hard-crash) when the greyscale effect is used, e.g. Game Over. The `Saturation` shader parameter no longer exists in the shader because it was stripped out, so when the `Draw()` method tries to _set_ it, the game crashes. + +> [!NOTE] +> Leave this change in for now, to demonstrate the way to handle this error through extensions in the `Material` class. + +> [!NOTE] +> You may not _actually_ see the `NullReferenceException`, because the `DungeonSlime.csproj` is configured to be a `WinExe`, which means the standard error and output of the application are hidden. The game may appear to _just_ crash with no warning. You can change the `OutputType` to `Exe`, which means you will see the standard error directed to the terminal. + +The aggressive optimization is good for your game's performance, but when combined with the hot-reload system, it will lead to unexpected bugs. As you iterate on shader code, it is likely that at some point a shader parameter will be optimized out of the compiled shader. The hot-reload system will automatically load the newly compiled shader, and if the C# code attempts to set the previously available parameter, the game may crash. + +To solve this problem, the `Material` class can encapsulate the setting of shader properties and handle the potential error scenario. The `Effect.Parameters` variable is an instance of the [`EffectParameterCollection`](xref:Microsoft.Xna.Framework.Graphics.EffectParameterCollection) class. + +For reference, here is what the [`EffectParameterCollection`](xref:Microsoft.Xna.Framework.Graphics.EffectParameterCollection) class looks like from the MonoGame Source: + +[!code-csharp[](./snippets/snippet-3-09.cs)] + +> [!TIP] +> MonoGame is free and open source, so the entire source code is always available online, [`EffectParameterCollection`](https://github.com/MonoGame/MonoGame/blob/develop/MonoGame.Framework/Graphics/Effect/EffectParameterCollection.cs). + +The `_indexLookup` is a `Dictionary` contains a mapping of property _name_ to parameter, the `Dictionary` class has methods for checking if a given property name exists, but unfortunately we cannot access it due to the `private` access modifier.  + +Luckily, the entire `EffectParameterCollection` inherits from [`IEnumerable`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.ienumerable-1), so we can use the existing dotnet utilities to convert the entire structure into a `Dictionary`. Once we have the parameters in a `Dictionary` structure, we will be able to check what parameters exist _before_ trying to access them, thus avoiding potential `NullReferenceExceptions`. + +1. Add this new property to the `Material` class: + + [!code-csharp[](./snippets/snippet-3-10.cs)] + +2. Along with the corresponding `using` statements we will need for the implementation: + + [!code-csharp[](./snippets/snippet-3-10-using.cs?highlight=1-3)] + +3. Now you can add the following method that will convert the `EffectParameterCollection` into the new `Dictionary` property: + + [!code-csharp[](./snippets/snippet-3-11.cs)] + +4. And we must not forget to invoke this method during the constructor of the `Material`: + + [!code-csharp[](./snippets/snippet-3-12.cs?highlight=4)] + +5. With the new `ParameterMap` property, create an additional helper function that checks if a given parameter exists in the shader: + + [!code-csharp[](./snippets/snippet-3-13.cs)] + +6. And another helper method that sets a parameter value for the `Material`, but will not crash if the parameter does not actually exist: + + [!code-csharp[](./snippets/snippet-3-14.cs)] + +7. Now, instead of setting the `Saturation` for the `_grayscaleEffect` manually as before, update the `Draw()` code to use the new method in the `GameScene` class, replacing `SetValue` with `SetParameter` on the `Material` safely: + + [!code-csharp[](./snippets/snippet-3-15.cs)] + +To verify it is working, re-run the game, and instead of seeing a crash, you should see the `GameScene`'s game over menu show a completely _white_ background. This is because the shader is setting the `finalColor` to `1`. Delete the following line from the shader, wait for the hot-reload system to kick in, and the game should return to normal. + +| ![Figure 3-1: The game does not crash](./videos/shader-properties.mp4) | +| :--------------------------------------------------------------------: | +| **Figure 3-1: The game does not crash** | + +> [!NOTE] +> Make sure to remove the change to the `grayscaleEffect.fx` file if you want your program to continue working as normal, it was only a test (not a trap). + +### Reloading Properties + +When the hot-reload system loads a new compiled shader into the game's memory, the new shader does not have any of the shader parameter _values_ that the previous shader instance had. To demonstrate the problem, we will purposefully break the `_grayscaleEffect` a bit. + +1. For now, comment out the line the `GameScene`'s `Draw()` method to prevent the parameter being set in ever draw call (as it should be): + + [!code-csharp[](./snippets/snippet-3-16.cs)] + +2. Instead, add the following line to the end of the `LoadContent()` method: + + [!code-csharp[](./snippets/snippet-3-16-2.cs?highlight=3)] + +The net outcome is that the `_grayscaleEffect` will not actually work for its designed purpose, instead is is now fully saturated with a value of `1`, and will _never_ change.  + +Run the game, enter the `GameScene`, and hit the pause button, the greyscale effect does not seem to apply and the background remains in color. Now, if you cause _any_ reload of the shader (editing a comment line and saving it), the background of the `GameScene` will immediately desaturate and switch to grayscale. The newly compiled shader instance has no value for the `Saturation` parameter (because it is not being updated as normal in the `Draw` method), and since `0` is the default value for numbers, it appears grayscale, forever (or until the next run). + +To solve this problem, the `Material` class can encapsulate the handling of applying new hot-reload updates. Anytime a new shader is available to swap in, the `Material` class needs to handle re-applying the old shader parameters to the new instance. + +Add the following method to the `Material` class: + +[!code-csharp[](./snippets/snippet-3-17.cs)] + +And now instead of using the `TryRefresh()` method directly on the `_grayscaleEffect`, use the new `Update()` method in the `GameScene.Update()` call: + +[!code-csharp[](./snippets/snippet-3-18.cs?highlight=3-4)] + +If you repeat the same test as before, the game will not become grayscale after a new shader is loaded. Once you have validated this, make sure to [undo the changes](#reloading-properties) made earlier in the `LoadContent()` and `Draw()` methods, so that the `_grayscaleEffect` will use the `Saturation` value in the `Draw()` method as intended as it remembers the state of the material BEFORE it was reloaded. + +> [!NOTE] +> Do not forget to **UNDO** the [changes made](#reloading-properties) at the beginning of this section and restore the original `SetParameter` call for the `_grayscaleEffect` material. (and "obviously" remove it from `LoadContent`, but that goes without saying.) + +### Debug Builds + +When the _DungeonSlime_ game is published, it would not make sense to run the new `Material.Update()` method, because no shaders would ever be hot-reloaded in a release build. We can strip the method from the game when it is being built for _Release_. Add the following attribute to the `Material.Update()` method: + +[!code-csharp[](./snippets/snippet-3-19.cs?highlight=1)] + +And add the required `using` statement as well: + +[!code-csharp[](./snippets/snippet-3-19-using.cs?highlight=3)] + +The built _DungeonSlime_ executable will no longer contain the compiled code for the `Material.Update()` method, or any place in the code that _invoked_ the method. This means that the hot-reload system will never attempt to read the file timestamps of your `.xnb` files. There is still a _tiny_ cost for keeping the extra fields on the `WatchedAsset` type, rather than using the `Effect` directly. However, given the huge wins for your shader development workflow, paying the memory cost for a few mostly unused fields is a worthwhile trade-off. + +> [!Tip] +> Adding the [`[Conditional]`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.conditionalattribute?view=net-9.0) attribute is optional. It will only slightly increase the performance of your game, and disable `Material` hot-reload for released games automatically. + +### Supporting more Parameter Types + +In the previous sections, we have only dealt with the `grayscaleEffect.fx` shader, and that shader only has a single shader parameter called `Saturation`. The `Saturation` parameter is a `float`, and so far, the `Material` class only handles `float` based parameters. However, shaders may have many more types of parameters. In this tutorial, we will add support for the following parameter types, and leave it as an exercise to the reader to add support for more. + +- `Matrix`, +- `Vector2`, +- `Texture2D` + +Add the following methods to the `Material` class: + +[!code-csharp[](./snippets/snippet-3-20.cs)] + +And then in the `Material.Update()` method, change the `switch` statement to handle the following cases: + +[!code-csharp[](./snippets/snippet-3-21.cs)] + +And an additional `using` to cover the extra types used: + +[!code-csharp[](./snippets/snippet-3-21-using.cs?highlight=5)] + +## Conclusion + +Excellent work! Our new `Material` class makes working with shaders much safer and more convenient. In this chapter, you accomplished the following: + +- Created a `Material` class to encapsulate shader effects and their parameters. +- Solved the `NullReferenceException` that can happen when the compiler optimizes away unused parameters. +- Handled the state of shader parameters so they are automatically reapplied during a hot-reload. +- Added support for multiple parameter types like `Matrix`, `Vector2`, and `Texture2D`. + +Now that we have a solid and safe foundation for our effects, we will make them easier to tweak. In the next chapter, we will build a real-time debug UI that will let us change our shader parameters with sliders and buttons right inside the game! + +You can find the complete code sample for this chapter, [here](https://github.com/MonoGame/MonoGame.Samples/tree/3.8.4/Tutorials/2dShaders/src/03-The-Material-Class). + +Continue to the next chapter, [Chapter 04: Debug UI](../04_debug_ui/index.md). diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-01.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-01.cs new file mode 100644 index 00000000..e2ac52bf --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-01.cs @@ -0,0 +1,22 @@ +using Microsoft.Xna.Framework.Graphics; +using MonoGameLibrary.Content; + +namespace MonoGameLibrary.Graphics; + +public class Material +{ + /// + /// The hot-reloadable asset that this material is using + /// + public WatchedAsset Asset; + + /// + /// The currently loaded Effect that this material is using + /// + public Effect Effect => Asset.Asset; + + public Material(WatchedAsset asset) + { + Asset = asset; + } +} diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-02-using.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-02-using.cs new file mode 100644 index 00000000..fdad78ea --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-02-using.cs @@ -0,0 +1,7 @@ +using System; +using System.Reflection; +using System.IO; +using System.Diagnostics; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using MonoGameLibrary.Graphics; \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-02.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-02.cs new file mode 100644 index 00000000..28c88520 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-02.cs @@ -0,0 +1,10 @@ +/// +/// Load an Effect into the wrapper class +/// +/// +/// +/// +public static Material WatchMaterial(this ContentManager manager, string assetName) +{ + return new Material(manager.Watch(assetName)); +} diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-03.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-03.cs new file mode 100644 index 00000000..06165122 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-03.cs @@ -0,0 +1,2 @@ +// The grayscale shader effect. +private Material _grayscaleEffect; diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-04.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-04.cs new file mode 100644 index 00000000..a8632411 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-04.cs @@ -0,0 +1,2 @@ + // Load the grayscale effect + _grayscaleEffect = Content.WatchMaterial("effects/grayscaleEffect"); diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-05.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-05.cs new file mode 100644 index 00000000..8c3e9ce5 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-05.cs @@ -0,0 +1,2 @@ + // Update the grayscale effect if it was changed + _grayscaleEffect.Asset.TryRefresh(out _); diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-06.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-06.cs new file mode 100644 index 00000000..08055e0f --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-06.cs @@ -0,0 +1,5 @@ + // We are in a game over state, so apply the saturation parameter. + _grayscaleEffect.Effect.Parameters["Saturation"].SetValue(_saturation); + + // And begin the sprite batch using the grayscale effect. + Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp, effect: _grayscaleEffect.Effect); diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-07.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-07.cs new file mode 100644 index 00000000..b287e87b --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-07.cs @@ -0,0 +1 @@ +_grayscaleEffect.Effect.Parameters["DoesNotExist"].SetValue(0); diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-08.hlsl b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-08.hlsl new file mode 100644 index 00000000..9dd43fc4 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-08.hlsl @@ -0,0 +1,22 @@ + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + // Sample the texture + float4 color = tex2D(SpriteTextureSampler, input.TextureCoordinates) * input.Color; + + // Calculate the grayscale value based on human perception of colors + float grayscale = dot(color.rgb, float3(0.3, 0.59, 0.11)); + + // create a grayscale color vector (same value for R, G, and B) + float3 grayscaleColor = float3(grayscale, grayscale, grayscale); + + // Linear interpolation between he grayscale color and the original color's + // rgb values based on the saturation parameter. + float3 finalColor = lerp(grayscale, color.rgb, Saturation); + + // overwrite all existing operations and set the final color to white + finalColor.rgb = 1; + + // Return the final color with the original alpha value + return float4(finalColor, color.a); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-09.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-09.cs new file mode 100644 index 00000000..7ca194ae --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-09.cs @@ -0,0 +1,7 @@ +public class EffectParameterCollection : IEnumerable, IEnumerable +{ + internal static readonly EffectParameterCollection Empty = new EffectParameterCollection(new EffectParameter[0]); + private readonly EffectParameter[] _parameters; + private readonly Dictionary _indexLookup; + + // ... diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-10-using.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-10-using.cs new file mode 100644 index 00000000..9cf397eb --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-10-using.cs @@ -0,0 +1,5 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework.Graphics; +using MonoGameLibrary.Content; \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-10.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-10.cs new file mode 100644 index 00000000..fdfbc643 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-10.cs @@ -0,0 +1,4 @@ +/// +/// A cached version of the parameters available in the shader +/// +public Dictionary ParameterMap; diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-11.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-11.cs new file mode 100644 index 00000000..bf0afd57 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-11.cs @@ -0,0 +1,7 @@ +/// +/// Rebuild the based on the current parameters available in the effect instance +/// +public void UpdateParameterCache() +{ + ParameterMap = Effect.Parameters.ToDictionary(p => p.Name); +} diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-12.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-12.cs new file mode 100644 index 00000000..0b36022f --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-12.cs @@ -0,0 +1,5 @@ +public Material(WatchedAsset asset) +{ + Asset = asset; + UpdateParameterCache(); +} diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-13.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-13.cs new file mode 100644 index 00000000..96aae427 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-13.cs @@ -0,0 +1,12 @@ +/// +/// Check if the given parameter name is available in the compiled shader code. +/// Remember that a parameter will be optimized out of a shader if it is not being used +/// in the shader's return value. +/// +/// +/// +/// +public bool TryGetParameter(string name, out EffectParameter parameter) +{ + return ParameterMap.TryGetValue(name, out parameter); +} diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-14.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-14.cs new file mode 100644 index 00000000..0405308d --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-14.cs @@ -0,0 +1,11 @@ +public void SetParameter(string name, float value) +{ + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set parameter=[{name}] as it does not exist in the shader=[{Asset.AssetName}]"); + } +} diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-15.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-15.cs new file mode 100644 index 00000000..a58f289e --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-15.cs @@ -0,0 +1,2 @@ +// We are in a game over state, so apply the saturation parameter. +_grayscaleEffect.SetParameter("Saturation", _saturation); diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-16-2.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-16-2.cs new file mode 100644 index 00000000..90f2cd34 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-16-2.cs @@ -0,0 +1,3 @@ +// Load the grayscale effect +_grayscaleEffect = Content.WatchMaterial("effects/grayscaleEffect"); +_grayscaleEffect.SetParameter("Saturation", 1); \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-16.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-16.cs new file mode 100644 index 00000000..f8adde9d --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-16.cs @@ -0,0 +1 @@ +// _grayscaleEffect.SetParameter("Saturation", _saturation); diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-17.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-17.cs new file mode 100644 index 00000000..208ff961 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-17.cs @@ -0,0 +1,28 @@ +public void Update() +{ + if (Asset.TryRefresh(out var oldAsset)) + { + UpdateParameterCache(); + + foreach (var oldParam in oldAsset.Parameters) + { + if (!TryGetParameter(oldParam.Name, out var newParam)) + { + continue; + } + + switch (oldParam.ParameterClass) + { + case EffectParameterClass.Scalar: + newParam.SetValue(oldParam.GetValueSingle()); + break; + default: + Console.WriteLine("Warning: shader reload system was not able to re-apply property. " + + $"shader=[{Effect.Name}] " + + $"property=[{oldParam.Name}] " + + $"class=[{oldParam.ParameterClass}]"); + break; + } + } + } +} diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-18.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-18.cs new file mode 100644 index 00000000..0a0677ef --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-18.cs @@ -0,0 +1,7 @@ +public override void Update(GameTime gameTime) +{ + // Update the grayscale effect if it was changed + _grayscaleEffect.Update(); + + // Ensure the UI is always updated + _ui.Update(gameTime); diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-19-using.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-19-using.cs new file mode 100644 index 00000000..c9fd87a6 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-19-using.cs @@ -0,0 +1,6 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.Xna.Framework.Graphics; +using MonoGameLibrary.Content; \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-19.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-19.cs new file mode 100644 index 00000000..f640cfbb --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-19.cs @@ -0,0 +1,5 @@ +[Conditional("DEBUG")] +public void Update() +{ + // implementation left out for brevity +} diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-20.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-20.cs new file mode 100644 index 00000000..879321bc --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-20.cs @@ -0,0 +1,35 @@ +public void SetParameter(string name, Matrix value) +{ + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } +} + +public void SetParameter(string name, Vector2 value) +{ + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } +} + +public void SetParameter(string name, Texture2D value) +{ + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } +} diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-21-using.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-21-using.cs new file mode 100644 index 00000000..344d6f43 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-21-using.cs @@ -0,0 +1,7 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MonoGameLibrary.Content; \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-21.cs b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-21.cs new file mode 100644 index 00000000..7edf8287 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/03_the_material_class/snippets/snippet-3-21.cs @@ -0,0 +1,21 @@ +switch (oldParam.ParameterClass) +{ + case EffectParameterClass.Scalar: + newParam.SetValue(oldParam.GetValueSingle()); + break; + case EffectParameterClass.Matrix: + newParam.SetValue(oldParam.GetValueMatrix()); + break; + case EffectParameterClass.Vector when oldParam.ColumnCount == 2: // float2 + newParam.SetValue(oldParam.GetValueVector2()); + break; + case EffectParameterClass.Object: + newParam.SetValue(oldParam.GetValueTexture2D()); + break; + default: + Console.WriteLine("Warning: shader reload system was not able to re-apply property. " + + $"shader=[{Effect.Name}] " + + $"property=[{oldParam.Name}] " + + $"class=[{oldParam.ParameterClass}]"); + break; +} diff --git a/articles/tutorials/advanced/2d_shaders/03_the_material_class/videos/shader-properties.mp4 b/articles/tutorials/advanced/2d_shaders/03_the_material_class/videos/shader-properties.mp4 new file mode 100644 index 00000000..a5e3b0e7 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/03_the_material_class/videos/shader-properties.mp4 differ diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/gifs/imgui-hello-world.gif b/articles/tutorials/advanced/2d_shaders/04_debug_ui/gifs/imgui-hello-world.gif new file mode 100644 index 00000000..ba4da40b Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/04_debug_ui/gifs/imgui-hello-world.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/gifs/renderdoc_tex.gif b/articles/tutorials/advanced/2d_shaders/04_debug_ui/gifs/renderdoc_tex.gif new file mode 100644 index 00000000..baeb897e Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/04_debug_ui/gifs/renderdoc_tex.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/images/renderdoc_setup.png b/articles/tutorials/advanced/2d_shaders/04_debug_ui/images/renderdoc_setup.png new file mode 100644 index 00000000..c22225cb Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/04_debug_ui/images/renderdoc_setup.png differ diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/index.md b/articles/tutorials/advanced/2d_shaders/04_debug_ui/index.md new file mode 100644 index 00000000..6889c4ad --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/index.md @@ -0,0 +1,231 @@ +--- +title: "Chapter 04: Debug UI" +description: "Add ImGui.NET to the project for debug visualization" +--- + +So far, any time we need to adjust a shader's parameter values, we need to edit C# code and recompile. It would be much faster to have a debug user interface (UI) in the game itself that exposes all of the shader parameters as editable text fields and slider widgets. We can also use the sliders to change a shader's input parameter and visualize the difference in realtime, which is a fantastic way to build intuition about our shader code. + +In the previous tutorial series, you set up a UI using the [GUM framework](https://docs.flatredball.com/gum/code/monogame). GUM is a powerful tool that works wonderfully for player facing UI. However, for the debug UI we will develop in this chapter, we will bring in a second UI library called `ImGui.NET`. The two libraries will not interfere with one another, and each one works well for different use cases. `ImGui.NET` is great for rapid iteration speed and building _developer_ facing UI. The existing GUM based UI has buttons and sliders that look and feel like they belong in the _Dungeon Slime_ world. The `ImGui.NET` UI will look more like an admin console in your game. Despite the lack of visual customization, `ImGui.NET` is easy and _quick_ to write, which makes it perfect for our developer facing debug UI. + +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/03-The-Material-Class). + +## Adding a Debug UI Library + +A common approach to building debug UIs in games is to use an _Immediate Mode_ system. An immediate mode UI redraws the entire UI from scratch every frame. `ImGui.NET` is a popular choice for MonoGame. It is a port of a C++ library called called `DearImGui`. + +To add `ImGui.NET`, add the following Nuget package reference to the _MonoGameLibrary_ project: + +[!code-xml[](./snippets/snippet-4-01.xml?highlight=5)] + +In order to render the `ImGui.NET` UI in MonoGame, we need a few supporting classes that convert the `ImGui.NET` data into MonoGame's graphical representation. + +> [!note] +> There is a [sample project](https://github.com/ImGuiNET/ImGui.NET/tree/master/src/ImGui.NET.SampleProgram.XNA) on `ImGui.NET`'s public repository that we can copy for our use cases. + +Create a new folder in the _MonoGameLibrary_ project called `ImGui` and copy and paste the following files into the folder, + +- The [`ImGuiRenderer.cs`](https://github.com/ImGuiNET/ImGui.NET/blob/v1.91.6.1/src/ImGui.NET.SampleProgram.XNA/ImGuiRenderer.cs) +- The [`DrawVertDeclaration.cs`](https://github.com/ImGuiNET/ImGui.NET/blob/v1.91.6.1/src/ImGui.NET.SampleProgram.XNA/DrawVertDeclaration.cs) + +There is `unsafe` code in the `ImGui` codebase, like this snippet, so you will need to enable `unsafe` code in the `MonoGameLibrary.csproj` file. Add this property: + +[!code-xml[](./snippets/snippet-4-02.xml?highlight=4)] + +> [!note] +> Why `unsafe`? +> The unsafe keyword in C# allows code to work directly with memory addresses (pointers). This is generally discouraged for safety reasons, but it's necessary for high-performance libraries. The `ImGuiRenderer` uses pointers to efficiently send vertex data to the GPU. + +In order to play around with the new UI tool, we will set up a simple _Hello World_ UI in the main `GameScene`. As we experiment with `ImGui`, we will build towards a re-usable debug UI for future shaders. + +To get started, we need to have an instance of `ImGuiRenderer`. Similar to how there is a single `static SpriteBatch`, we will create a single `static ImGuiRenderer` to be re-used throughout the game. + +1. In the `Core.cs` file of the `MonoGameLibrary` project, add the following `using` statements at the top of the `Core.cs` file to get access to ImGui: + + ```csharp + using ImGuiNET; + using ImGuiNET.SampleProgram.XNA; + ``` + +2. Then, add the following property to the `Core` class: + + [!code-csharp[](./snippets/snippet-4-03.cs)] + +3. Then to initialize the instance, in the `Initialize()` method, add the following snippet: + + [!code-csharp[](./snippets/snippet-4-04.cs?highlight=18-20)] + +4. Similar to `SpriteBatch`'s `.Begin()` and `.End()` calls, the `ImGuiRenderer` has a start and end function call. In the `GameScene` class, add these lines to end of the `.Draw()` method: + + [!code-csharp[](./snippets/snippet-4-05.cs?highlight=9-12)] + +5. `ImGui` draws by adding draggable windows to the screen. To create a simple window that just prints out `"Hello World"`, use the following snippet: + + [!code-csharp[](./snippets/snippet-4-06.cs?highlight=8-10)] + +When you run your project, you should get a new "Debug" window as shown below: + +| ![Figure 4-1: a simple ImGui window](./gifs/imgui-hello-world.gif) | +| :----------------------------------------------------------------: | +| **Figure 4-1: a simple ImGui window** | + +## Building a Material Debug UI + +Each instance of `Material` is going to draw a custom debug window. The window will show the latest time the shader was reloaded into the game, which will help demonstrate when a new shader is being used. The window can also show the parameter values for the shader. + +1. Add the following function to the `Material` class: + + [!code-csharp[](./snippets/snippet-4-07.cs)] + + Currently however, the control is not fully bound to the the `Saturation` parameter of the `greyscale` shader, inputs will always be overridden because the `GameScene` itself keeps setting the value. In order to solve this, we introduce a custom property in the `Material` class that causes the debug UI to override the various `SetParameter()` methods. + +2. Add the following `using` statements to the top of the `Material.cs` file: + + ```csharp + using ImGuiNET; + ``` + +3. Next, add this new boolean to the `Material` class: + + [!code-csharp[](./snippets/snippet-4-08.cs)] + +4. Then, modify all of the `SetParameter()` methods (float, matrix, vector2, etc) to exit early when the `DebugOverride` variable is set to `true`: + + [!code-csharp[](./snippets/snippet-4-09.cs?highlight=3)] + +5. Then, in the `DebugDraw()` method, after the `LastUpdated` field gets drawn, add this following: + + [!code-csharp[](./snippets/snippet-4-10.cs?highlight=14-17)] + +### Turning it off + +As the number of shaders and `Material` instances grows throughout the rest of the tutorial series, it will become awkward to manage drawing all of the debug UIs manually like the `_grayscaleEffect`'s UI is being drawn. Rather, it would be better to have a single function that would draw all of the debug UIs at once. Naturally, it would not make sense to draw _every_ `Material`'s debug UI, so the `Material` class needs a setting to decide if the debug UI should be drawn or not. + +We will keep track of all the `Material` instances to draw as a [`static`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/static) variable inside the `Material` class itself. + +> [!NOTE] +> [Static](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/static) classes are a useful feature to use when you need something to be accessible globally, meaning there can only ever be one. In the case of the `Material` class, we are adding a collection which will be the same collection no matter how many different Material definitions are running in the project. But remember, with great power comes great responsibility, Static's should be carefully considered before use as they can also be a trap if used incorrectly. + +1. First, add the following at the top of the `Material.cs` class: + + [!code-csharp[](./snippets/snippet-4-11.cs)] + +2. Now, add a `boolean` property to the `Material` class, which adds or removes the given instances to the `static` set: + + [!code-csharp[](./snippets/snippet-4-12.cs)] + +3. To finish off the edits to the `Material` class, add a method that actually renders all of the `Material` instances in the `static` set, consuming the `DebugDraw` method we created earlier: + + [!code-csharp[](./snippets/snippet-4-13.cs)] + +4. Now in the `Core` class's `Draw` method, we need to call the new method in order to render the new Debug UI. and also delete the old ImGui test code used in the `GameScene` to draw the `_grayscaleEffect`'s debugUI. Replace the `Draw` method with the following version: + + [!code-csharp[](./snippets/snippet-4-14.cs?highlight=9)] + +5. The `Core` class does not yet know about the `Material` class, so we will need to add an additional using to the top of the class: + + ```csharp + using MonoGameLibrary.Graphics; + ``` + + > [!TIP] + > To keep things clean, you can also remove the old `using ImGuiNET;` as you will see it is now greyed out because it is not used anymore since we removed the test `ImGui` drawing code. + +6. Finally, in order to render the debug UI for the `_grayscaleEffect`, just enable the `IsDebugVisible` property to `true` in the `LoadContent` method of the `GameScreen` class: + + [!code-csharp[](./snippets/snippet-4-15.cs?highlight=3)] + +Now, when you run the game, you can see the debug window with the `Saturation` value as expected. If you enable the `"Override Values"` checkbox, you will also be able set the value by hand while the game is running (without setting it, the game has control, but still useful to see). + +| ![Figure 4-2: Shader parameters shown while game is running](./videos/debug-shader-ui.mp4) | +| :-----------------------------------------------------------------------------: | +| **Figure 4-2: Shader parameters shown while game is running** | + +>[!tip] +>If you do not want to see the debug UI for the `grayscaleEffect` anymore, just set `IsDebugVisible` to `false`, or delete the line entirely. + +## RenderDoc + +The debug UI in the game is helpful, but sometimes you may need to take a closer look at the actual graphics resources _MonoGame_ is managing. There are various tools that intercept the graphics API calls between an application and the graphics software. [_RenderDoc_](https://renderdoc.org/) is a great example of a graphics debugger tool. Unfortunately, it only works with MonoGame when the game is targeting the WindowsDX profile. It may not be possible to switch your game to WindowsDX under all circumstances. At this time, there are very few options for graphic debuggers tools for MonoGame when targeting openGL. + +### Switch to WindowsDX + +To switch _DungeonSlime_ to target WindowsDX, you need to modify the `.csproj` file, and make some changes to the `.mgcb` content file. + +1. First, in the `.csproj` file, remove the reference to MonoGame's openGL backend: + + [!code-xml[](./snippets/snippet-4-16.xml)] + + And replace it with this line: + + [!code-xml[](./snippets/snippet-4-17.xml)] + +2. The [`MonoGame.Framework.WindowsDX`](https://www.nuget.org/packages/MonoGame.Framework.WindowsDX) Nuget package is not available for the `net8.0` framework. Instead, it is only available specifically on the Windows variant, called `net8.0-windows`. Change the `` in your `.csproj` to the new framework: + + [!code-xml[](./snippets/snippet-4-18.xml)] + +3. Next, the `Content.mgcb` file, update the target platfrom from `DesktopGL` to `Windows`, + + ```text + /platform:Windows + ``` + +4. RenderDoc only works when MonoGame is targeting the `HiDef` graphics profile. This needs to be changed in two locations. First, in the `.mgcb` file, change the `/profile` from `Reach` to `HiDef`. + + ```text + /profile:HiDef + ``` + +5. Finally, in the `Core` constructor, set the graphics profile immediately after constructing the `Graphics` instance: + + [!code-csharp[](./snippets/snippet-4-19.cs)] + +> [!TIP] +> You can always switch your project back to DesktopGL once you are done with this chapter, or have a play with the `mgblank2dstartkit` (MonoGame Blank 2D Starter Kit) MonoGame template to see a project setup for multiple platforms and `ADD` a WindowsDX project to the solution for testing (or even shipping) + +### Using RenderDoc + +Make sure you have built _DungeonSlime_. You can build it manually by running the following command from the _DungeonSlime_ directory: + +[!code-sh[](./snippets/snippet-4-20.sh)] + +Once you have downloaded [RenderDoc](https://renderdoc.org/), open it and Go to the _Launch Application_ tab, then select your built executable in the _Executable Path_. + +For example, the path may look similar to the following: + +[!code-sh[](./snippets/snippet-4-21.sh)] + +| ![Figure 4-4: The setup for RenderDoc](./images/renderdoc_setup.png) | +| :------------------------------------------------------------------: | +| **Figure 4-4: The setup for RenderDoc** | + +Then, click the _Launch_ button in the lower right. _DungeonSlime_ should launch with a small warning text in the upper left of the game window that states the graphics API is being captured by RenderDoc. + +| ![Figure 4-5: Renderdoc analysing and capturing frames](./videos/renderdoc-capture.mp4) | +| :-----------------------------------------------------------------------------: | +| **Figure 4-5: Renderdoc analysing and capturing frames** | + +Press `F12` to capture a frame (as shown above), and it will appear in RenderDoc. In RenderDoc, Double click the frame to open the captured frame, and go to the _Texture Viewer_ tab. The draw calls are split out one by one and you can view the intermediate buffers. + +| ![Figure 4-5: RenderDoc shows the intermediate frame](gifs/renderdoc_tex.gif) | +| :---------------------------------------------------------------------------: | +| **Figure 4-5: RenderDoc shows the intermediate frame** | + +From here, you can better understand how rendering is working in your game, especially if you are having performance or graphical issues. It is a bit beyond the scope of this tutorial to go into the deep details, for now you can just examine it to get used to the tool. + +> [!TIP] +> RenderDoc is a powerful tool. To learn more about how to use the tool, please refer to the [RenderDoc Documentation](https://renderdoc.org/docs/index.html). + +## Conclusion + +What a difference a good tool makes! In this chapter, you accomplished the following: + +- Integrated the `ImGui.NET` library into a MonoGame project. +- Created a reusable `ImGuiRenderer` to draw the UI. +- Built a dynamic debug window for our `Material` class. +- Learned how to use a graphics debugger like RenderDoc to inspect frames. + +With our workflow and tooling in place, it's finally time to write some new shaders. Up next, we will dive into our first major pixel shader effect and build a classic screen wipe transition! + +You can find the complete code sample for this chapter, [here](https://github.com/MonoGame/MonoGame.Samples/tree/3.8.4/Tutorials/2dShaders/src/04-Debug-UI). + +Continue to the next chapter, [Chapter 05: Transition Effect](../05_transition_effect/index.md). diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-01.xml b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-01.xml new file mode 100644 index 00000000..e0b31da9 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-01.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-02.xml b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-02.xml new file mode 100644 index 00000000..bf46d560 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-02.xml @@ -0,0 +1,7 @@ + + + net8.0 + true + + + \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-03.cs b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-03.cs new file mode 100644 index 00000000..0e262e31 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-03.cs @@ -0,0 +1,4 @@ +/// +/// Gets the ImGui renderer used for debug UIs. +/// +public static ImGuiRenderer ImGuiRenderer { get; private set; } diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-04.cs b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-04.cs new file mode 100644 index 00000000..0894907c --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-04.cs @@ -0,0 +1,21 @@ +protected override void Initialize() +{ + base.Initialize(); + + // Set the core's graphics device to a reference of the base Game's + // graphics device. + GraphicsDevice = base.GraphicsDevice; + + // Create the sprite batch instance. + SpriteBatch = new SpriteBatch(GraphicsDevice); + + // Create a new input manager + Input = new InputManager(); + + // Create a new audio controller. + Audio = new AudioController(); + + // Create the ImGui renderer. + ImGuiRenderer = new ImGuiRenderer(this); + ImGuiRenderer.RebuildFontAtlas(); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-05.cs b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-05.cs new file mode 100644 index 00000000..9b648a2a --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-05.cs @@ -0,0 +1,15 @@ +protected override void Draw(GameTime gameTime) +{ + // If there is an active scene, draw it. + if (s_activeScene != null) + { + s_activeScene.Draw(gameTime); + } + + // Draw debug UI + Core.ImGuiRenderer.BeforeLayout(gameTime); + // Finish drawing the debug UI here + Core.ImGuiRenderer.AfterLayout(); + + base.Draw(gameTime); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-06.cs b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-06.cs new file mode 100644 index 00000000..41bc6ff5 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-06.cs @@ -0,0 +1,16 @@ +protected override void Draw(GameTime gameTime) +{ + // ... + + // Draw debug UI + Core.ImGuiRenderer.BeforeLayout(gameTime); + + ImGui.Begin("Demo Window"); + ImGui.Text("Hello world!"); + ImGui.End(); + + // Finish drawing the debug UI here + Core.ImGuiRenderer.AfterLayout(); + + base.Draw(gameTime); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-07.cs b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-07.cs new file mode 100644 index 00000000..d5b5bb50 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-07.cs @@ -0,0 +1,91 @@ +[Conditional("DEBUG")] +public void DrawDebug() +{ + ImGui.Begin(Effect.Name); + + var currentSize = ImGui.GetWindowSize(); + ImGui.SetWindowSize(Effect.Name, new System.Numerics.Vector2(MathHelper.Max(100, currentSize.X), MathHelper.Max(100, currentSize.Y))); + + ImGui.AlignTextToFramePadding(); + ImGui.Text("Last Updated"); + ImGui.SameLine(); + ImGui.LabelText("##last-updated", Asset.UpdatedAt.ToString() + $" ({(DateTimeOffset.Now - Asset.UpdatedAt).ToString(@"h\:mm\:ss")} ago)"); + + ImGui.NewLine(); + + + bool ScalarSlider(string key, ref float value) + { + float min = 0; + float max = 1; + + return ImGui.SliderFloat($"##_prop{key}", ref value, min, max); + } + + foreach (var prop in ParameterMap) + { + switch (prop.Value.ParameterType, prop.Value.ParameterClass) + { + case (EffectParameterType.Single, EffectParameterClass.Scalar): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + + var value = prop.Value.GetValueSingle(); + if (ScalarSlider(prop.Key, ref value)) + { + prop.Value.SetValue(value); + } + break; + + case (EffectParameterType.Single, EffectParameterClass.Vector): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + + var vec2Value = prop.Value.GetValueVector2(); + ImGui.Indent(); + + ImGui.Text("X"); + ImGui.SameLine(); + + if (ScalarSlider(prop.Key + ".x", ref vec2Value.X)) + { + prop.Value.SetValue(vec2Value); + } + + ImGui.Text("Y"); + ImGui.SameLine(); + if (ScalarSlider(prop.Key + ".y", ref vec2Value.Y)) + { + prop.Value.SetValue(vec2Value); + } + ImGui.Unindent(); + break; + + case (EffectParameterType.Texture2D, EffectParameterClass.Object): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + + var texture = prop.Value.GetValueTexture2D(); + if (texture != null) + { + var texturePtr = Core.ImGuiRenderer.BindTexture(texture); + ImGui.Image(texturePtr, new System.Numerics.Vector2(texture.Width, texture.Height)); + } + else + { + ImGui.Text("(null)"); + } + break; + + default: + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + ImGui.Text($"(unsupported {prop.Value.ParameterType}, {prop.Value.ParameterClass})"); + break; + } + } + ImGui.End(); +} diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-08.cs b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-08.cs new file mode 100644 index 00000000..cf5e4202 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-08.cs @@ -0,0 +1,4 @@ +/// +/// Override the default behaviour of the material properties so they can be controlled in Debug mode. +/// +public bool DebugOverride; \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-09.cs b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-09.cs new file mode 100644 index 00000000..16642b79 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-09.cs @@ -0,0 +1,13 @@ +public void SetParameter(string name, float value) +{ + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } +} diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-10.cs b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-10.cs new file mode 100644 index 00000000..6bbce514 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-10.cs @@ -0,0 +1,19 @@ +[Conditional("DEBUG")] +public void DrawDebug() +{ + ImGuiNET.ImGui.Begin(Effect.Name); + + var currentSize = ImGuiNET.ImGui.GetWindowSize(); + ImGuiNET.ImGui.SetWindowSize(Effect.Name, new System.Numerics.Vector2(MathHelper.Max(100, currentSize.X), MathHelper.Max(100, currentSize.Y))); + + ImGuiNET.ImGui.AlignTextToFramePadding(); + ImGuiNET.ImGui.Text("Last Updated"); + ImGuiNET.ImGui.SameLine(); + ImGuiNET.ImGui.LabelText("##last-updated", Asset.UpdatedAt.ToString() + $" ({(DateTimeOffset.Now - Asset.UpdatedAt).ToString(@"h\:mm\:ss")} ago)"); + + ImGuiNET.ImGui.AlignTextToFramePadding(); + ImGuiNET.ImGui.Text("Override Values"); + ImGuiNET.ImGui.SameLine(); + ImGuiNET.ImGui.Checkbox("##override-values", ref DebugOverride); + + // ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-11.cs b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-11.cs new file mode 100644 index 00000000..f7ee0f7e --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-11.cs @@ -0,0 +1,2 @@ +// materials that will be drawn during the standard debug UI pass. +private static HashSet s_debugMaterials = new HashSet(); diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-12.cs b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-12.cs new file mode 100644 index 00000000..52807c6b --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-12.cs @@ -0,0 +1,21 @@ +/// +/// Enable this variable to visualize the debugUI for the material +/// +public bool IsDebugVisible +{ + get + { + return s_debugMaterials.Contains(this); + } + set + { + if (!value) + { + s_debugMaterials.Remove(this); + } + else + { + s_debugMaterials.Add(this); + } + } +} diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-13.cs b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-13.cs new file mode 100644 index 00000000..749d3ed4 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-13.cs @@ -0,0 +1,25 @@ +[Conditional("DEBUG")] +public static void DrawVisibleDebugUi(GameTime gameTime) +{ + // first, cull any materials that are not visible, or disposed. + var toRemove = new List(); + foreach (var material in s_debugMaterials) + { + if (material.Effect.IsDisposed) + { + toRemove.Add(material); + } + } + + foreach (var material in toRemove) + { + s_debugMaterials.Remove(material); + } + + Core.ImGuiRenderer.BeforeLayout(gameTime); + foreach (var material in s_debugMaterials) + { + material.DrawDebug(); + } + Core.ImGuiRenderer.AfterLayout(); +} diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-14.cs b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-14.cs new file mode 100644 index 00000000..e2e0ceee --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-14.cs @@ -0,0 +1,12 @@ +protected override void Draw(GameTime gameTime) +{ + // If there is an active scene, draw it. + if (s_activeScene != null) + { + s_activeScene.Draw(gameTime); + } + + Material.DrawVisibleDebugUi(gameTime); + + base.Draw(gameTime); +} diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-15.cs b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-15.cs new file mode 100644 index 00000000..6b8b665e --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-15.cs @@ -0,0 +1,3 @@ +// Load the grayscale effect +_grayscaleEffect = Content.WatchMaterial("effects/grayscaleEffect"); +_grayscaleEffect.IsDebugVisible = true; diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-16.xml b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-16.xml new file mode 100644 index 00000000..2e38c8a9 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-16.xml @@ -0,0 +1 @@ + diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-17.xml b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-17.xml new file mode 100644 index 00000000..a79d85fa --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-17.xml @@ -0,0 +1 @@ + diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-18.xml b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-18.xml new file mode 100644 index 00000000..3e25b89b --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-18.xml @@ -0,0 +1 @@ +net8.0-windows diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-19.cs b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-19.cs new file mode 100644 index 00000000..b22885a5 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-19.cs @@ -0,0 +1,3 @@ +// Create a new graphics device manager. +Graphics = new GraphicsDeviceManager(this); +Graphics.GraphicsProfile = GraphicsProfile.HiDef; diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-20.sh b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-20.sh new file mode 100644 index 00000000..6b11cb56 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-20.sh @@ -0,0 +1 @@ +dotnet build diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-21.sh b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-21.sh new file mode 100644 index 00000000..c3e2163a --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/04_debug_ui/snippets/snippet-4-21.sh @@ -0,0 +1 @@ +C:\proj\MonoGame.Samples\Tutorials\2dShaders\src\04-Debug-UI\DungeonSlime\bin\Debug\net8.0-windows7.0\DungeonSlime.exe diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/videos/debug-shader-ui.mp4 b/articles/tutorials/advanced/2d_shaders/04_debug_ui/videos/debug-shader-ui.mp4 new file mode 100644 index 00000000..05301b9f Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/04_debug_ui/videos/debug-shader-ui.mp4 differ diff --git a/articles/tutorials/advanced/2d_shaders/04_debug_ui/videos/renderdoc-capture.mp4 b/articles/tutorials/advanced/2d_shaders/04_debug_ui/videos/renderdoc-capture.mp4 new file mode 100644 index 00000000..60a697e0 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/04_debug_ui/videos/renderdoc-capture.mp4 differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/center-wipe.gif b/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/center-wipe.gif new file mode 100644 index 00000000..2207cbd3 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/center-wipe.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/concave-wipe.gif b/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/concave-wipe.gif new file mode 100644 index 00000000..94908f80 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/concave-wipe.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/overview.gif b/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/overview.gif new file mode 100644 index 00000000..f4ff6ba6 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/overview.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/progress-parameter.gif b/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/progress-parameter.gif new file mode 100644 index 00000000..372a2041 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/progress-parameter.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/simple-x.gif b/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/simple-x.gif new file mode 100644 index 00000000..aacab713 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/simple-x.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/smoothstep.gif b/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/smoothstep.gif new file mode 100644 index 00000000..f15f7cd3 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/smoothstep.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/transparent-x.gif b/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/transparent-x.gif new file mode 100644 index 00000000..8fab209a Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/transparent-x.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/vertical.gif b/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/vertical.gif new file mode 100644 index 00000000..d9261df3 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/vertical.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/wipes.gif b/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/wipes.gif new file mode 100644 index 00000000..0828f46c Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/gifs/wipes.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/assets/angled.png b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/assets/angled.png new file mode 100644 index 00000000..de0160f2 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/assets/angled.png differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/assets/concave.png b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/assets/concave.png new file mode 100644 index 00000000..826e2207 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/assets/concave.png differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/assets/radial.png b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/assets/radial.png new file mode 100644 index 00000000..bd1207cf Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/assets/radial.png differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/assets/ripple.png b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/assets/ripple.png new file mode 100644 index 00000000..e137653a Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/assets/ripple.png differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/blank.png b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/blank.png new file mode 100644 index 00000000..eeed4e59 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/blank.png differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/center-wipe-value.png b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/center-wipe-value.png new file mode 100644 index 00000000..ba630304 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/center-wipe-value.png differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/concave-wipe-value.png b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/concave-wipe-value.png new file mode 100644 index 00000000..90cd38b3 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/concave-wipe-value.png differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/mgcb.png b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/mgcb.png new file mode 100644 index 00000000..454a32ba Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/mgcb.png differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/white.png b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/white.png new file mode 100644 index 00000000..84eee1bb Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/white.png differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/x-pos.png b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/x-pos.png new file mode 100644 index 00000000..c14518e6 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/x-pos.png differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/xy-pos.png b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/xy-pos.png new file mode 100644 index 00000000..8768cad0 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/xy-pos.png differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/y-pos.png b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/y-pos.png new file mode 100644 index 00000000..4b0cfa86 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/05_transition_effect/images/y-pos.png differ diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/index.md b/articles/tutorials/advanced/2d_shaders/05_transition_effect/index.md new file mode 100644 index 00000000..bcbb9ffa --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/index.md @@ -0,0 +1,466 @@ +--- +title: "Chapter 05: Transition Effect" +description: "Create an effect for transitioning between scenes" +--- + +Our game is functional, but the jump from the title screen to the game is very sudden. We can make it feel much more polished with a smooth transition instead of an instant cut. + +> [!NOTE] +> +> In the previous chapters, we focused on the shader development workflow. +> +> 1. We set up a hot-reload system in [Chapter 02](./../02_hot_reload/index.md), +> 2. We set up a `Material` class in [Chapter 03](./../03_the_material_class/index.md), +> 3. We set up a debug UI using `ImGui.NET` in [Chapter 04](./../04_debug_ui/index.md). + +> All of this prior work is going to finally going to pay off! In this chapter, you will be able to leverage the hot-reload and debug UI to experiment with shader code in realtime without ever needing to restart your game. You can start the game, make changes to the shader, tweak shader parameters, all without needing to recompile your project. + +In this chapter, we will dive into our first major pixel shader effect: a classic [Screen Wipe](https://www.youtube.com/watch?v=cGqAu9gj_F0) we will learn how to control an effect over the whole screen, how to create soft edges, and how to use textures to drive our shader logic to create all sorts of interesting patterns. + +At the end of the chapter, you will have a screen transition effect like the one below. + +| ![Figure 5-1: We will build this screen transition effect](./gifs/overview.gif) | +| :-----------------------------------------------------------------------------: | +| **Figure 5-1: We will build this screen transition effect** | + +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/04-Debug-UI). + +## The Scene Transition Effect + +Screen wipes are the bread and butter of transitions, you see them everywhere from Presentations, to window washers and even games (ok, I made the middle one up). The basic principle is sound, control what you draw and use a pattern to only draw part of an image, this is a useful technique for almost anything in game development, so let us get started with what you came here for, **BUILDING SHADERS**. + +### Getting Started + +Start by creating a new `Sprite Effect` from the MonoGame Content Builder Editor in the folder called `effects` (organization is key for both content as well as code), and name it `sceneTransitionEffect.fx`: + +| ![Figure 5-2: Create a new `Sprite Effect`](./images/mgcb.png) | +| :------------------------------------------------------------: | +| **Figure 5-2: Create a new `Sprite Effect** | + +Save and build the content project, just to be sure and switch back to your code editor, and perform the following steps: + +1. As the `Core` class has not implemented Materials previously, it will need the `using` statement to the library content features in order to be able to use the `WatchContent` extension we added in [Chapter 3](../03_the_material_class/index.md), so add a new using to the top of the class: + + ```csharp + using MonoGameLibrary.Content; + ``` + +2. Let us start by adding the following variable to the top of the `Core` class in the `MonoGameLibrary` project to keep a static reference to the transition effect: + + [!code-csharp[](./snippets/snippet-5-01.cs)] + +3. Add a new `LoadContent` method (just before the `UnloadContent` method to make it easier to find), and load the `sceneTransitionEffect` effect into a `Material`: + + [!code-csharp[](./snippets/snippet-5-02.cs)] + +4. While we are developing the effect, we should also enable the debug UI framework we added earlier: + + [!code-csharp[](./snippets/snippet-5-05.cs?highlight=5)] + +5. To benefit from hot-reload, we need also need to update the effect in the `Core`'s `Update()` loop: + + [!code-csharp[](./snippets/snippet-5-04.cs?highlight=6)] + +6. Finally, as we are providing a new "base" implementation of `LoadContent`, we need to call the `Core`'s version of `LoadContent()` from the `Game1` class in the `DungeonSlime` project. In the previous tutorial, the method was left without calling the base method (because it was not implemented and did not need to): + + [!code-csharp[](./snippets/snippet-5-03.cs?highlight=4)] + +If you run the game now, you should have a blank debug UI. + +| ![Figure 5-3: A blank slate](./images/blank.png) | +| :----------------------------------------------: | +| **Figure 5-3: A blank slate** | + +### Rendering the Effect + +Currently, the shader is compiling and loading into the game, but it is not being _used_ yet. The scene transition needs to cover the whole screen, so we need to draw a sprite over the entire screen area with the new effect. To render a sprite over the entire screen area, we need a blank texture to use for the sprite. Add the following property to the `Core` class: + +[!code-csharp[](./snippets/snippet-5-06.cs)] + +And initialize it in the `Initialize()` method: + +[!code-csharp[](./snippets/snippet-5-07.cs?highlight=7-8)] + +> [!TIP] +> The `Pixel` property is a texture we can re-use for many effects, and it is helpful to have for debugging purposes. + +In the `Core'`s `Draw()` method, use the `SpriteBatch` to draw a full screen sprite using the `Pixel` property. Make sure to put this code **`after`** the `s_activeScene` is drawn, because the scene transition effect should _cover_ whatever was rendered previously in the current scene: + +[!code-csharp[](./snippets/snippet-5-08.cs?highlight=10-12)] + +If you run the game now, you will see a white background, because the `Pixel` sprite is rendering full screen on top of the entire game scene and GUM UI. + +> [!NOTE] +> We can still see the `ImGui.NET` debug UI because the debug UI is rendered _after_ the current drawing the `Pixel`. + +| ![Figure 5-4: The shader is being used to render a white full screen quad](./images/white.png) | +| :--------------------------------------------------------------------------------------------: | +| **Figure 5-4: The shader is being used to render a white full screen quad** | + +### The Input + +We need to be able to control how much of the screen is affected by the transition effect, the transition progress in effect. + +Open the new `sceneTransitionEffect.fx` shader and add the following parameter to the shader: + +[!code-hlsl[](./snippets/snippet-5-09.hlsl?highlight=12)] + +And recall from the [Material chapter](../03_the_material_class/index.md#setting-shader-parameters), that unless the `Progress` parameter is actually **_used_** somehow in the calculation of the output of the shader, it will be stripped out of the final compilation of the shader for optimization. So, for now, we will make the shader return the `Progress` value using a red value color: + +[!code-hlsl[](./snippets/snippet-5-10.hlsl?highlight=5)] + +Now you can use the slider in the debug UI to visualize the `Progress` parameter as the red channel. + +> [!WARNING] +> But wait, how does the game know to render a `Progress` slider? +> +> Recall from [Chapter 03: The Material Class](../03_the_material_class/index.md#setting-shader-parameters)'s _Setting Shader Parameters_ section that MonoGame's `EffectParameterCollection` knows about all of the compiled shader parameters The Debug UI we created in [Chapter 04: Debug UI](./../04_debug_ui/index.md#building-a-material-debug-ui) draws a slider for each parameter in the `EffectParameterCollection`. This means that as soon as a shader parameter is included in the compiled shader code, it will appear in the Debug UI without us needing to manually add or remove it. +> +> As we add or remove shader parameters in the shader code, the hot reload system will compile the shader and reload it into the game, and the Debug UI will draw everything in the `EffectParameterCollection`. +> +> Very cool and saves you a lot of effort when playing with values for configuring shaders! + +| ![Figure 5-5: See the Progress parameter in the red value](./gifs/progress-parameter.gif) | +| :---------------------------------------------------------------------------------------: | +| **Figure 5-5: See the Progress parameter in the red value** | + +> [!NOTE] +> The astute will also note that you do not even need to check the "Override Values" checkbox to affect the screen. This is simply because the game itself is not even trying to update the value (as it did previously with the greyscale saturation). It just works. + +### Coordinate Space + +If we are making a screen wipe, then parts of the screen will be transitioning before other parts of the screen. The shader will affect the screen based on the coordinate of the pixel on the screen. For example, if we wanted to make a horizontal screen wipe, we would need to know the x-coordinate of each pixel. With the x-coordinate of a pixel, the shader could decide if that pixel should be shown or hidden based on the transition's progress parameter. + +The shader already provides the x-coordinate (and y-coordinate) of each pixel in the `input.TextureCoordinates` structure. + +>[!TIP] +> `input.TextureCoordinates` are not _actually_ the pixel coordinates +> +> In this example, the `input.TextureCoordinates` represents pixel coordinates _because_ the sprite is being drawn as a full screen quad. However, if the sprite was not taking up the entire screen, the texture coordinates would behave differently. +> +> This topic will be discussed more later on. + +The following update to the shader helps visualize the x-coordinate of each pixel: + +[!code-hlsl[](./snippets/snippet-5-11.hlsl)] + +That results in this image, where the left edge has an x-coordinate of `0`, it has no red value, and where the right edge has an x-coordinate of `1`, the image is fully red. In the middle of the image, the red value interpolates between `0` and `1`. + +> [!WARNING] +> Where did the `Progress` slider go? +> +> When the shader is compiled, the `Progress` parameter is being optimized out of the final compiled code because it was not being used in the final output of the shader in any way. The MonoGame shader compiler is good at optimizing away unused parameters, which is good because it helps the performance of your game. However, it can be confusing, because the `Progress` parameter appears to _vanish_ from the shader. +> +> It no longer appears in the `EffectParameterCollection`, so the debug UI has no way of knowing it exists to render it. You need to watch out for these things when building shaders and understand when something goes wrong, it is most certainly your own fault. + +| ![Figure 5-6: the x coordinate of each pixel represented in the red channel](./images/x-pos.png) | +| :----------------------------------------------------------------------------------------------: | +| **Figure 5-6: the x coordinate of each pixel represented in the red channel** | + +The same pattern holds true for the y-coordinate. Observe the following shader code putting the y-coordinate of each pixel in the green channel: + +[!code-hlsl[](./snippets/snippet-5-12.hlsl)] + +As you can see, the top of the screen has a `0` value for the y-coordinate, and the bottom of the screen has a `1`. + +| ![Figure 5-7: The y-coordinate of each pixel represented in the green channel](./images/y-pos.png) | +| :------------------------------------------------------------------------------------------------: | +| **Figure 5-7: The y-coordinate of each pixel represented in the green channel** | + +When these shaders are combined, the resulting image is the classic UV texture coordinate space: + +[!code-hlsl[](./snippets/snippet-5-13.hlsl)] + +>[!TIP] +> Remember that MonoGame uses the **top** of the image for "y = 0". +> +> Other game engines treat the _bottom_ of the image as "y = 0", but MonoGame (like XNA before it) uses the top of the image for where y is 0. There is a long winded reason for this, which we shall not go into here, except to simply state "Everyone has their own way of doing screen space coordinates", so you should always check. + +| ![Figure 5-8: x and y coordinates in the red and green channels](./images/xy-pos.png) | +| :-----------------------------------------------------------------------------------: | +| **Figure 5-8: x and y coordinates in the red and green channels** | + +> [!TIP] +> Did you remember that we have a hot reload system running? This means you do not need to keep closing the game for every shader change! If you have been closing the game, stop doing that (although you might want to mute the audio to save your sanity) + +### Simple Horizontal Screen Wipe + +Now that you have a visualization of the coordinate space, we can build some intuition for a screen wipe. To start, imagine creating a horizontal screen wipe, where the image turns to black from left to right. Remember that the x-coordinate goes from `0` on the left edge to `1` on the right edge. We can re-introduce the `Progress` parameter and compare the values. If the `Progress` parameter is greater than the x-coordinate, then that part of the image should transition. + +In the following shader code, the blue channel of the final image represents in that coordinate should be in the transitioned state: + +[!code-hlsl[](./snippets/snippet-5-14.hlsl)] + +Use the slider to control the `Progress` parameter to see how the image changes. + +| ![Figure 5-9: a simple horizontal screen wipe](./gifs/simple-x.gif) | +| :-----------------------------------------------------------------: | +| **Figure 5-9: a simple horizontal screen wipe** | + +That looks pretty close to a screen wipe already! However, instead of using blue and black the effect should be using black and a transparent color. The following snippet of shader code puts the `transitioned` value in the alpha channel of the final color. When the alpha value is zero, the pixel fragment is drawn as invisible: + +[!code-hlsl[](./snippets/snippet-5-15.hlsl)] + +| ![Figure 5-10: A transparent screen wipe](./gifs/transparent-x.gif) | +| :----------------------------------------------------------------: | +| **Figure 5-10: A transparent screen wipe** | + +The transition works, but the edge between black and transparent is very hard, often in screen wipes the transition has a smooth or blurry edge. The reason the current shader has a hard edge is because the `transitioned` variable is either `0` or `1` depending on the outcome of the `Progress > uv.x;` expression. + +Ideally, it would be nice to smooth the `transitioned` variable: + +- Setting it to `0` when the `Progress` is some small number like `.05`. +- Setting it to `1` when it is finished and the `Progress` is `.1`. +- Then smoothly interpolate between `0` to `1` in-between. + +That way, it replaces the hard cut-off with a much smoother falloff/edge. We could write this by hand, but shader languages have a built in function called `smoothstep` which does essentially what we want. + +#### Smoothstep + +The `smoothstep` function takes 3 parameters: + +- A `min` value +- A `max` value +- And an input variable often called `x` (or `t` (time) depending on who you ask). + +The function returns `0` when the given `x` parameter is at or below the `min` value, and `1` when `x` is at or above the `max` value. Between these ranges it uses a smooth function to blend between the two bounds, `min` and `max`. + +> [!TIP] +> You can learn more about the `smoothstep` function in [The Book Of Shaders](https://thebookofshaders.com/glossary/?search=smoothstep) + +This would be the most basic way to adjust the code to use `smoothstep`, but right away, using a fixed `.05` value should jump out as alarming: + +[!code-hlsl[](./snippets/snippet-5-16.hlsl)] + +Using **"magic numbers"** in shader code is a dangerous pattern, because it is unclear if the value `.05` is there for a mathematical reason, or just an aesthetic choice. At minimum, we should extract the value into a named variable, so that the reader of the code can attribute _some_ sort of meaning to what `.05` represents: + +[!code-hlsl[](./snippets/snippet-5-17.hlsl?highlight=6)] + +However, at this point, it would be nice to extract the `edgeWidth` as a second shader parameter next to `Progress`: + +[!code-hlsl[](./snippets/snippet-5-18.hlsl?highlight=4)] + +Now you can control the edge width slider to see the smooth edge between transitioned and not. + +| ![Figure 5-11: A smooth edge](./gifs/smoothstep.gif) | +| :-------------------------------------------------: | +| **Figure 5-11: A smooth edge** | + +After we find an `EdgeWidth` value that looks good, we can set it in C# after the `SceneTransitionMaterial` is loaded in the `Core` class: + +[!code-csharp[](./snippets/snippet-5-19.cs?highlight=5)] + +>[!warning] +> Shader parameters do not use initializer expressions. +> +> If you set a default expression for a shader parameter, like setting `EdgeWidth=.05`, MonoGame's shader compiler ignores the `=.05` part. +> +> **You will always need to set this value from C#.** + +### More Interesting Wipes + +So far the shader has been using `uv.x` to create a horizontal screen wipe. It would be easy to use `uv.y` to create a vertical screen wipe: + +[!code-hlsl[](./snippets/snippet-5-20.hlsl)] + +| ![Figure 5-12: A vertical screen wipe](./gifs/vertical.gif) | +| :---------------------------------------------------------: | +| **Figure 5-12: A vertical screen wipe** | + +But what if we wanted to create more complicated wipes that did not simply go in one direction? + +So far, we have passed `uv.x` and `uv.y` along as the argument to compare against the `Progress` shader parameter, but we could use any value we wanted. If you pull out the expression into a separate variable, `value`, and experiment with some different mathematical functions. + +For example, here is a wipe that comes in from the left and right towards the center: + +[!code-hlsl[](./snippets/snippet-5-21.hlsl)] + +| ![Figure 5-13: A more interesting wipe](./gifs/center-wipe.gif) | +| :-------------------------------------------------------------: | +| **Figure 5-13: A more interesting wipe** | + +That is cool, but if we wanted an even more interesting wipe, the math would start to become challenging. In the final effect, it would also be nice to change the _type_ of wipe dynamically from the game, and changing entire shader functions, or writing completely separate shader effects would be very cumbersome, instead, we can build a more generalized approach without the need to write ever increasing complex mathematical functions to encode the wipe's progress. + +To build intuition, we start by visualizing _just_ the `value` that is compared against the `Progress` parameter: + +[!code-hlsl[](./snippets/snippet-5-22.hlsl?highlight=6-7)] + +| ![Figure 5-14: Just the value for the center wipe](./images/center-wipe-value.png) | +| :--------------------------------------------------------------------------------: | +| **Figure 5-14: Just the value for the center wipe** | + +> [!NOTE] +> Yes, we have lost the `progress` parameter for the moment, it will return later. + +The display is not very interesting in of itself, but it does convey meaning of the transition effect _will_ render later. The darker areas of the image are going to transition sooner than the brighter areas, and the brightest areas will be the _last_ areas to transition when the `Progress` parameter is set all the way to `1`. At the end of the day, the image is just a grayscale gradient. + +You could imagine _other_ grayscale gradient images. In fact, there is a fantastic pack of _free_ gradient images made by _Screaming Brain Studios_ on [opengameart.org](https://opengameart.org/content/300-gradient-textures). Here are few samples, + +| ![Figure 5-15: A sample gradient texture](./images/assets/ripple.png) | ![Figure 5-16: A sample gradient texture](./images/assets/angled.png) | ![Figure 5-17: A sample gradient texture](./images/assets/concave.png) | ![Figure 5-18: A sample gradient texture](./images/assets/radial.png) | +| :-------------------------------------------------------------------: | :-------------------------------------------------------------------: | :-------------------------------------------------------------------: | :-------------------------------------------------------------------: | +| **Figure 5-15: A ripple gradient** | **Figure 5-16: An angled gradient** | **Figure 5-17: A concave gradient** | **Figure 5-18: A radial gradient** | + +Theoretically, it is possible to derive mathematical expressions that would result in those exact grayscale gradient images, but given that we have the images already, we could just change the transition shader to _read_ from the texture given the pixel's coordinate. + +Right-click and download (save as) those files and add them to your MonoGame content into the `Content/images` folder (named as per the `Content.Load` methods shown below, by default they should already be named appropriately). + +1. We will store these texture references in the `Core` class as a `static` property, similar to how the `SceneTransitionMaterial` is already being kept: + + [!code-csharp[](./snippets/snippet-5-23.cs)] + +2. Adding the required collections using statement we need for using the `List` type: + + ```csharp + using System.Collections.Generic; + ``` + +3. In the `Core`'s `LoadContent()` method, load the new images: + + [!code-csharp[](./snippets/snippet-5-24.cs?highlight=8-12)] + +4. Instead of using the `Pixel` debug image to draw the `SceneTransitionMaterial`, we will use one of these new textures: + + [!code-csharp[](./snippets/snippet-5-25.cs?highlight=11)] + +5. In the shader, we read the texture data at the given `uv` coordinate by using the `tex2D` function. Modify the shader so that the `value` is only using the red-channel of the given texture: + + [!code-hlsl[](./snippets/snippet-5-26.hlsl)] + + > [!NOTE] + > Note we did not need to add a "Texture" parameter because the default MonoGame shader effect already comes with a default "main" texture. But if you wanted more textures, you would need to add them yourselves and `Set` the parameters in C#. + +6. Since the code above is referencing the `concave` image, the result looks like this, + + | ![Figure 5-19: The concave value for a wipe](./images/concave-wipe-value.png) | + | :---------------------------------------------------------------------------: | + | **Figure 5-19: The concave value for a wipe** | + + > [!NOTE] + > If you do not see any change, it is likely you have not added the downloaded images to the MGCB content project. By doing so you will also have to restart the project as we are ONLY watching the shader files and ignoring other changes. + > + > Watch out you do not leave any old terminal windows running the watch in the background... + +7. Now, modify the shader to use the `Progress` parameter instead of just returning the `value`: + + [!code-hlsl[](./snippets/snippet-5-27.hlsl?highlight=7-8)] + +8. As you play with the `Progress` parameter slide, you can see the more interesting wipe pattern. + + | ![Figure 5-20: The concave wipe in action](./gifs/concave-wipe.gif) | + | :-----------------------------------------------------------------: | + | **Figure 5-20: The concave wipe in action** | + + > [!TIP] + > Make sure not to make the "edge" value too large or it will never fully transition, because your "buffer" is too large. + +9. Now it is as easy as changing the texture being used to draw the scene transition to completely change the wipe pattern. + +Try playing around with the other textures, or make one of your own. + +### Controlling the Effect + +So far we have implemented a transition effect that wipes a black-out across the screen, but nothing triggers the effect automatically when the scene actually changes. In this section, we will create some C# code to control the shader parameter programmatically. + +We will create a new class called `SceneTransition` that holds all the data for an active scene transition. + +1. Add this class to your `MonoGameLibrary/Scenes` folder: + + [!code-csharp[](./snippets/snippet-5-28.cs)] + +2. Then add the following `static` methods to the new `SceneTransition` class: + + [!code-csharp[](./snippets/snippet-5-29.cs)] + +3. Switching back over to the `Core` class, we need to add a new `static` property to hold all the possible transitions for the game: + + [!code-csharp[](./snippets/snippet-5-30.cs)] + +4. We need to ensure that anytime the `Core` class changes scene it should also create a new _closing_ transition: + + [!code-csharp[](./snippets/snippet-5-31.cs?highlight=8)] + +5. And when the effect starts the `TransitionScene()` method, it should create an _open_ transition: + + [!code-csharp[](./snippets/snippet-5-32.cs?highlight=3)] + +6. Then we need to actually _set_ the `Progress` shader parameter given the current scene transition value. In the `Update()` method: + + [!code-csharp[](./snippets/snippet-5-33.cs?highlight=6)] + +7. Finally, the scene material needs to be drawn with the right texture: + +[!code-csharp[](./snippets/snippet-5-34.cs?highlight=11)] + +When you run the game and change between scenes and you will see a random arrangement of screen wipes! + +| ![Figure 5-21: the shader is done!](./gifs/wipes.gif) | +| :---------------------------------------------------: | +| **Figure 5-21: the shader is done!** | +| | + +## Shared Content + +The shader looks great! But organizationally, it feels odd that the `Material` loads a shader effect that is not part of the _MonoGameLibrary_ project. If the code is ever used in another project, it would be clunky to need to copy individual pieces of shader content out from the _DungeonSlime_ game just so that the _MonoGameLibrary_ project can work in a new project. + +> [!IMPORTANT] +> As we need to work with dotnet tools, specifically the MGCB editor, the `dotnet-tools.json` (located in a `.config` folder) needs to be more accessible to the project as it is currently located in the `DungeonSlime` project folder, for the `MonoGameLibrary` to be able to consume it for the MGCB editor, it needs moving to the ROOT of the project folder. +> +> **Move the `.config` folder from the `DungeonSlime` folder up to the solution root.** (where both the DungeonSlime and MonoGameLibrary live) +> +> As a rule, the dotnet tools `.config` folder should **ALWAYS** be in your project root, especially if you are working with multi-project/platform projects. + +To solve this problem, we will introduce a second `.mgcb` file in the _MonoGameLibrary_ project. + +1. Create a new folder called `SharedContent` in the _MonoGameLibrary_ project. +1. Open the "MGCB Editor" and create a new blank `.mgcb` file `file -> New..` in that directory called `SharedContent.mgcb`. +1. Add new folders called "effects" and "images" in the editor. +1. Right-Click on the "effects" folder and use "Add -> Existing Item..", then select the `sceneTransitionEffect.fx` from the original _DungeonSlime/Content/effects_ folder. Select "Copy" when asked. +1. Right-Click on the "images" folder and use "Add -> Existing Item..", then select the transition images from the original _DungeonSlime/Content/images_ folder. Select "Copy" when asked. + +> [!NOTE] +> Make sure you **SAVE** before exiting the MGCB editor, otherwise your content configuration may be lost. It is just good practice. + +This gives you a new dedicated Content project for just the MonoGameLibrary project. + +> [!IMPORTANT] +> Remember to remove the old files from the original DungeonSlime Content project and folder, to avoid duplication as these will no longer be used. (delete files and then "exclude" them in the MGCB editor for the original mgcb content) + +In order for the _DungeonSlime_ project to load the new Content Project, we need to make a few changes. + +1. In the `DungeonSlime.csproj` file, add the following `MonoGameContentReference`, this enables it to include `mgcb` files from both projects: + + [!code-xml[](./snippets/snippet-5-35.xml)] + +2. Also, in order for the shader hot-reload to work with the shared content, modify the `Watch` element to look like this: + + [!code-xml[](./snippets/snippet-5-36.xml?highlight=3)] + + Next, the existing `ContentManager` instance in the `Core` class will only load content from the _/Content_ folder, which will not include the `sceneTransitionEffect.fx` file, because it is stored in the _/SharedContent_ folder. For this tutorial, we will create a second `ContentManager` in the `Core` class called `SharedContent` which will be configured to only load content from the _/SharedContent_ folder. + +3. Add the following property next to the existing `Content` property in the `Core.cs` file: + + [!code-csharp[](./snippets/snippet-5-37.cs)] + +4. Then you will need to set the new `SharedContent` in the `Core` constructor, next to where the existing `Content` property is being set: + + [!code-csharp[](./snippets/snippet-5-38.cs?highlight=13)] + +5. Finally, use the `SharedContent` instead of `Content` to load all the content, replacing the `LoadContent()` method with the following: + + [!code-csharp[](./snippets/snippet-5-39.cs)] + +## Conclusion + +Our game is already starting to feel more polished with this new transition effect. In this chapter, you accomplished the following: + +- Drew a full-screen quad to act as a canvas for a post-processing effect. +- Used UV coordinates and the `smoothstep` function to create a soft-edged wipe. +- Switched to a texture-based approach to drive the wipe logic with complex patterns. +- Created a `SceneTransition` class to control the effect programmatically. +- Refactored shared content into its own content project. + +This was our first deep dive into pixel shaders, and we have created a very flexible system. In the next chapter, we will keep the momentum going by tackling another popular and powerful shader: a color-swapping effect. + +You can find the complete code sample for this chapter, [here](https://github.com/MonoGame/MonoGame.Samples/tree/3.8.4/Tutorials/2dShaders/src/05-Transition-Effect). + +Continue to the next chapter, [Chapter 06: Color Swap Effect](../06_color_swap_effect/index.md) diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-01.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-01.cs new file mode 100644 index 00000000..45fedb10 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-01.cs @@ -0,0 +1,4 @@ +/// +/// The material that is used when changing scenes +/// +public static Material SceneTransitionMaterial { get; private set; } diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-02.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-02.cs new file mode 100644 index 00000000..06937b82 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-02.cs @@ -0,0 +1,5 @@ +protected override void LoadContent() +{ + base.LoadContent(); + SceneTransitionMaterial = Content.WatchMaterial("effects/sceneTransitionEffect"); +} diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-03.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-03.cs new file mode 100644 index 00000000..6fd48633 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-03.cs @@ -0,0 +1,7 @@ +protected override void LoadContent() +{ + // Allow the Core class to also load content. + base.LoadContent(); + // Load the background theme music. + _themeSong = Content.Load("audio/theme"); +} diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-04.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-04.cs new file mode 100644 index 00000000..96be581f --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-04.cs @@ -0,0 +1,9 @@ +protected override void Update(GameTime gameTime) +{ + // ... + + // Check if the scene transition material needs to be reloaded. + SceneTransitionMaterial.Update(); + + base.Update(gameTime); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-05.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-05.cs new file mode 100644 index 00000000..5bf68cc7 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-05.cs @@ -0,0 +1,7 @@ +protected override void LoadContent() +{ + base.LoadContent(); + SceneTransitionMaterial = Content.WatchMaterial("effects/sceneTransitionEffect"); + SceneTransitionMaterial.IsDebugVisible = true; +} + diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-06.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-06.cs new file mode 100644 index 00000000..ee6772e0 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-06.cs @@ -0,0 +1,4 @@ +/// +/// Gets a runtime generated 1x1 pixel texture. +/// +public static Texture2D Pixel { get; private set; } diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-07.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-07.cs new file mode 100644 index 00000000..cd7277cb --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-07.cs @@ -0,0 +1,9 @@ +protected override void Initialize() +{ + base.Initialize(); + // ... + + // Create a 1x1 white pixel texture for drawing quads. + Pixel = new Texture2D(GraphicsDevice, 1, 1); + Pixel.SetData(new Color[]{ Color.White }); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-08.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-08.cs new file mode 100644 index 00000000..864637e3 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-08.cs @@ -0,0 +1,17 @@ +protected override void Draw(GameTime gameTime) +{ + // If there is an active scene, draw it. + if (s_activeScene != null) + { + s_activeScene.Draw(gameTime); + } + + // Draw the scene transition quad + SpriteBatch.Begin(effect: SceneTransitionMaterial.Effect); + SpriteBatch.Draw(Pixel, GraphicsDevice.Viewport.Bounds, Color.White); + SpriteBatch.End(); + + Material.DrawVisibleDebugUi(gameTime); + + base.Draw(gameTime); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-09.hlsl b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-09.hlsl new file mode 100644 index 00000000..cc5cd124 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-09.hlsl @@ -0,0 +1,20 @@ +#if OPENGL +#define SV_POSITION POSITION +#define VS_SHADERMODEL vs_3_0 +#define PS_SHADERMODEL ps_3_0 +#else +#define VS_SHADERMODEL vs_4_0_level_9_1 +#define PS_SHADERMODEL ps_4_0_level_9_1 +#endif + +// ... + +float Progress; + +Texture2D SpriteTexture; + +sampler2D SpriteTextureSampler = sampler_state { + Texture = ; +}; + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-10.hlsl b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-10.hlsl new file mode 100644 index 00000000..8d2ef723 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-10.hlsl @@ -0,0 +1,7 @@ +// ... + +float4 MainPS(VertexShaderOutput input) : COLOR { + return float4(Progress, 0, 0, 1); +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-11.hlsl b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-11.hlsl new file mode 100644 index 00000000..0bce417e --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-11.hlsl @@ -0,0 +1,9 @@ +// ... + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float2 uv = input.TextureCoordinates; + return float4(uv.x, 0, 0, 1); +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-12.hlsl b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-12.hlsl new file mode 100644 index 00000000..dd62ea88 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-12.hlsl @@ -0,0 +1,9 @@ +// ... + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float2 uv = input.TextureCoordinates; + return float4(0, uv.y, 0, 1); +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-13.hlsl b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-13.hlsl new file mode 100644 index 00000000..16ac6520 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-13.hlsl @@ -0,0 +1,9 @@ +// ... + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float2 uv = input.TextureCoordinates; + return float4(uv.x, uv.y, 0, 1); +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-14.hlsl b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-14.hlsl new file mode 100644 index 00000000..258ee872 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-14.hlsl @@ -0,0 +1,10 @@ +// ... + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float2 uv = input.TextureCoordinates; + float transitioned = Progress > uv.x; + return float4(0, 0, transitioned, 1); +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-15.hlsl b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-15.hlsl new file mode 100644 index 00000000..c1fd8ce6 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-15.hlsl @@ -0,0 +1,10 @@ +// ... + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float2 uv = input.TextureCoordinates; + float transitioned = Progress > uv.x; + return float4(0, 0, 0, transitioned); +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-16.hlsl b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-16.hlsl new file mode 100644 index 00000000..6b454eb5 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-16.hlsl @@ -0,0 +1,10 @@ +// ... + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float2 uv = input.TextureCoordinates; + float transitioned = smoothstep(Progress, Progress + .05, uv.x); + return float4(0, 0, 0, transitioned); +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-17.hlsl b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-17.hlsl new file mode 100644 index 00000000..554985c1 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-17.hlsl @@ -0,0 +1,11 @@ +// ... + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float2 uv = input.TextureCoordinates; + float edgeWidth = .05; + float transitioned = smoothstep(Progress, Progress + edgeWidth, uv.x); + return float4(0, 0, 0, transitioned); +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-18.hlsl b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-18.hlsl new file mode 100644 index 00000000..a98b9be1 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-18.hlsl @@ -0,0 +1,15 @@ +// ... + +float Progress; +float EdgeWidth; + +// ... + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float2 uv = input.TextureCoordinates; + float transitioned = smoothstep(Progress, Progress + EdgeWidth, uv.x); + return float4(0, 0, 0, transitioned); +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-19.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-19.cs new file mode 100644 index 00000000..d4249a67 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-19.cs @@ -0,0 +1,7 @@ +protected override void LoadContent() +{ + base.LoadContent(); + SceneTransitionMaterial = Content.WatchMaterial("effects/sceneTransitionEffect"); + SceneTransitionMaterial.SetParameter("EdgeWidth", .05f); + SceneTransitionMaterial.IsDebugVisible = true; +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-20.hlsl b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-20.hlsl new file mode 100644 index 00000000..610388d7 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-20.hlsl @@ -0,0 +1,10 @@ +// ... + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float2 uv = input.TextureCoordinates; + float transitioned = smoothstep(Progress, Progress + EdgeWidth, uv.y); + return float4(0, 0, 0, transitioned); +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-21.hlsl b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-21.hlsl new file mode 100644 index 00000000..782b5a61 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-21.hlsl @@ -0,0 +1,12 @@ +// ... + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float2 uv = input.TextureCoordinates; + float value = 1 - abs(.5 - uv.x) * 2; + float transitioned = smoothstep(Progress, Progress + EdgeWidth, value); + + return float4(0, 0, 0, transitioned); +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-22.hlsl b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-22.hlsl new file mode 100644 index 00000000..0f244ad1 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-22.hlsl @@ -0,0 +1,9 @@ +// ... + +float4 MainPS(VertexShaderOutput input) : COLOR { + float2 uv = input.TextureCoordinates; + float value = 1 - abs(.5 - uv.x) * 2; + return float4(value, value, value, 1); +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-23.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-23.cs new file mode 100644 index 00000000..d393c854 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-23.cs @@ -0,0 +1,4 @@ +/// +/// A set of grayscale gradient textures to use as transition guides +/// +public static List SceneTransitionTextures { get; private set; } diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-24.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-24.cs new file mode 100644 index 00000000..85cd29e8 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-24.cs @@ -0,0 +1,13 @@ +protected override void LoadContent() +{ + base.LoadContent(); + SceneTransitionMaterial = Content.WatchMaterial("effects/sceneTransitionEffect"); + SceneTransitionMaterial.SetParameter("EdgeWidth", .05f); + SceneTransitionMaterial.IsDebugVisible = true; + + SceneTransitionTextures = new List(); + SceneTransitionTextures.Add(Content.Load("images/angled")); + SceneTransitionTextures.Add(Content.Load("images/concave")); + SceneTransitionTextures.Add(Content.Load("images/radial")); + SceneTransitionTextures.Add(Content.Load("images/ripple")); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-25.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-25.cs new file mode 100644 index 00000000..69dd6101 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-25.cs @@ -0,0 +1,17 @@ +protected override void Draw(GameTime gameTime) +{ + // If there is an active scene, draw it. + if (s_activeScene != null) + { + s_activeScene.Draw(gameTime); + } + + // Draw the scene transition quad + SpriteBatch.Begin(effect: SceneTransitionMaterial.Effect); + SpriteBatch.Draw(SceneTransitionTextures[1], GraphicsDevice.Viewport.Bounds, Color.White); + SpriteBatch.End(); + + Material.DrawVisibleDebugUi(gameTime); + + base.Draw(gameTime); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-26.hlsl b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-26.hlsl new file mode 100644 index 00000000..9830220a --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-26.hlsl @@ -0,0 +1,10 @@ +// ... + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float2 uv = input.TextureCoordinates; + float value = tex2D(SpriteTextureSampler, uv).r; + return float4(value, value, value, 1); +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-27.hlsl b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-27.hlsl new file mode 100644 index 00000000..e34910d0 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-27.hlsl @@ -0,0 +1,11 @@ +// ... + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float2 uv = input.TextureCoordinates; + float value = tex2D(SpriteTextureSampler, uv).r; + float transitioned = smoothstep(Progress, Progress + EdgeWidth, value); + return float4(0, 0, 0, transitioned); +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-28.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-28.cs new file mode 100644 index 00000000..35ee7e7e --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-28.cs @@ -0,0 +1,31 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGameLibrary.Scenes; + +public class SceneTransition +{ + public DateTimeOffset StartTime; + public TimeSpan Duration; + + /// + /// true when the transition is progressing from 0 to 1. + /// false when the transition is progressing from 1 to 0. + /// + public bool IsForwards; + + /// + /// The index into the + /// + public int TextureIndex; + + /// + /// The 0 to 1 value representing the progress of the transition. + /// + public float ProgressRatio => MathHelper.Clamp((float)(EndTime - DateTimeOffset.Now).TotalMilliseconds / (float)Duration.TotalMilliseconds, 0, 1); + + public float DirectionalRatio => IsForwards ? 1 - ProgressRatio : ProgressRatio; + + public DateTimeOffset EndTime => StartTime + Duration; + public bool IsComplete => DateTimeOffset.Now >= EndTime; +} diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-29.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-29.cs new file mode 100644 index 00000000..b01bc9bb --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-29.cs @@ -0,0 +1,23 @@ +/// +/// Create a new transition +/// +/// +/// how long will the transition last in milliseconds? +/// +/// +/// should the transition be animating the Progress parameter from 0 to 1, or 1 to 0? +/// +/// +public static SceneTransition Create(int durationMs, bool isForwards) +{ + return new SceneTransition + { + Duration = TimeSpan.FromMilliseconds(durationMs), + StartTime = DateTimeOffset.Now, + TextureIndex = Random.Shared.Next(), + IsForwards = isForwards + }; +} + +public static SceneTransition Open(int durationMs) => Create(durationMs, true); +public static SceneTransition Close(int durationMs) => Create(durationMs, false); diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-30.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-30.cs new file mode 100644 index 00000000..df929fbc --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-30.cs @@ -0,0 +1,4 @@ +/// +/// The current transition between scenes +/// +public static SceneTransition SceneTransition { get; protected set; } = SceneTransition.Open(1000); diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-31.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-31.cs new file mode 100644 index 00000000..617f3b08 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-31.cs @@ -0,0 +1,10 @@ +public static void ChangeScene(Scene next) +{ + // Only set the next scene value if it is not the same + // instance as the currently active scene. + if (s_activeScene != next) + { + s_nextScene = next; + SceneTransition = SceneTransition.Close(250); + } +} diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-32.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-32.cs new file mode 100644 index 00000000..1616438e --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-32.cs @@ -0,0 +1,5 @@ +private static void TransitionScene() +{ + SceneTransition = SceneTransition.Open(500); + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-33.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-33.cs new file mode 100644 index 00000000..2a230a48 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-33.cs @@ -0,0 +1,10 @@ +protected override void Update(GameTime gameTime) +{ + // ... + + // Check if the scene transition material needs to be reloaded. + SceneTransitionMaterial.SetParameter("Progress", SceneTransition.DirectionalRatio); + SceneTransitionMaterial.Update(); + + base.Update(gameTime); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-34.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-34.cs new file mode 100644 index 00000000..de19b7d0 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-34.cs @@ -0,0 +1,17 @@ +protected override void Draw(GameTime gameTime) +{ + // If there is an active scene, draw it. + if (s_activeScene != null) + { + s_activeScene.Draw(gameTime); + } + + // Draw the scene transition quad + SpriteBatch.Begin(effect: SceneTransitionMaterial.Effect); + SpriteBatch.Draw(SceneTransitionTextures[SceneTransition.TextureIndex % SceneTransitionTextures.Count], GraphicsDevice.Viewport.Bounds, Color.White); + SpriteBatch.End(); + + Material.DrawVisibleDebugUi(gameTime); + + base.Draw(gameTime); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-35.xml b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-35.xml new file mode 100644 index 00000000..437e1ba4 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-35.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-36.xml b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-36.xml new file mode 100644 index 00000000..a3dbe0f1 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-36.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-37.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-37.cs new file mode 100644 index 00000000..d187a8a6 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-37.cs @@ -0,0 +1,4 @@ +/// +/// Gets the content manager that can load global assets from the SharedContent folder. +/// +public static ContentManager SharedContent { get; private set; } diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-38.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-38.cs new file mode 100644 index 00000000..f2d9b221 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-38.cs @@ -0,0 +1,16 @@ +public Core(string title, int width, int height, bool fullScreen) +{ + // ... + + // Set the core's content manager to a reference of hte base Game's + // content manager. + Content = base.Content; + + // Set the root directory for content + Content.RootDirectory = "Content"; + + // Set the core's shared content manager, pointing to the SharedContent folder. + SharedContent = new ContentManager(Services, "SharedContent"); + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-39.cs b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-39.cs new file mode 100644 index 00000000..1c721b97 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/05_transition_effect/snippets/snippet-5-39.cs @@ -0,0 +1,13 @@ +protected override void LoadContent() +{ + base.LoadContent(); + SceneTransitionMaterial = SharedContent.WatchMaterial("effects/sceneTransitionEffect"); + SceneTransitionMaterial.SetParameter("EdgeWidth", .05f); + SceneTransitionMaterial.IsDebugVisible = true; + + SceneTransitionTextures = new List(); + SceneTransitionTextures.Add(SharedContent.Load("images/angled")); + SceneTransitionTextures.Add(SharedContent.Load("images/concave")); + SceneTransitionTextures.Add(SharedContent.Load("images/radial")); + SceneTransitionTextures.Add(SharedContent.Load("images/ripple")); +} diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/gifs/color-saturation.gif b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/gifs/color-saturation.gif new file mode 100644 index 00000000..e8574ad7 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/gifs/color-saturation.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/gifs/color-swap-2.gif b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/gifs/color-swap-2.gif new file mode 100644 index 00000000..448c6fe6 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/gifs/color-swap-2.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/gifs/color-swap-even-seconds.gif b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/gifs/color-swap-even-seconds.gif new file mode 100644 index 00000000..cf7f69b7 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/gifs/color-swap-even-seconds.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/gifs/color-swap-slime.gif b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/gifs/color-swap-slime.gif new file mode 100644 index 00000000..8b04d6d1 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/gifs/color-swap-slime.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/gifs/color-swap.gif b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/gifs/color-swap.gif new file mode 100644 index 00000000..a87f8e04 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/gifs/color-swap.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/gifs/grayscale.gif b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/gifs/grayscale.gif new file mode 100644 index 00000000..8e5d75ff Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/gifs/grayscale.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/color-map-1.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/color-map-1.png new file mode 100644 index 00000000..b5e3dc5a Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/color-map-1.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/color-map-2.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/color-map-2.png new file mode 100644 index 00000000..2789bee8 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/color-map-2.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/color-map-dark-purple.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/color-map-dark-purple.png new file mode 100644 index 00000000..ffe9516e Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/color-map-dark-purple.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/color-map-green.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/color-map-green.png new file mode 100644 index 00000000..87656c81 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/color-map-green.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/color-map-pink.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/color-map-pink.png new file mode 100644 index 00000000..e8910ded Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/color-map-pink.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/color-map.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/color-map.png new file mode 100644 index 00000000..c31e440b Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/color-map.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/example-dark-purple.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/example-dark-purple.png new file mode 100644 index 00000000..113c5947 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/example-dark-purple.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/example-green.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/example-green.png new file mode 100644 index 00000000..ad04d52f Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/example-green.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/example-multi.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/example-multi.png new file mode 100644 index 00000000..e6b24766 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/example-multi.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/example-pink.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/example-pink.png new file mode 100644 index 00000000..5bce3dc1 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/example-pink.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/example-runtime.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/example-runtime.png new file mode 100644 index 00000000..eb248a70 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/example-runtime.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/mgcb.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/mgcb.png new file mode 100644 index 00000000..f521350f Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/mgcb.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/overview-1.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/overview-1.png new file mode 100644 index 00000000..5f7dc2c2 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/overview-1.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/overview-2.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/overview-2.png new file mode 100644 index 00000000..46ecf375 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/overview-2.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/overview-3.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/overview-3.png new file mode 100644 index 00000000..def24ffd Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/overview-3.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/overview-4.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/overview-4.png new file mode 100644 index 00000000..a80f0631 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/overview-4.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/pink.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/pink.png new file mode 100644 index 00000000..6ad04392 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/pink.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/slime-blue-color.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/slime-blue-color.png new file mode 100644 index 00000000..c8a5f35e Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/slime-blue-color.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/test.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/test.png new file mode 100644 index 00000000..f1fcf137 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/test.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/texture-map-original.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/texture-map-original.png new file mode 100644 index 00000000..0a05749c Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/texture-map-original.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/texture-map.png b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/texture-map.png new file mode 100644 index 00000000..b749064e Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/images/texture-map.png differ diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/index.md b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/index.md new file mode 100644 index 00000000..ab49a22e --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/index.md @@ -0,0 +1,443 @@ +--- +title: "Chapter 06: Color Swap Effect" +description: "Create a shader to change the colors of the game" +--- + +In this chapter we will create a powerful color‑swapping effect. We will learn a common and flexible technique that uses textures as look‑up tables (LUTs) to map original colors to new ones. This will give us precise control over the look and feel of our game's sprites. + +At the end of this chapter, we will be able to fine-tune the colors of the game. Here are a few examples: + +| ![Figure 6-1: The default colors](./images/overview-1.png) | ![Figure 6-2: A green variant of the game](./images/overview-2.png) | +| :----------------------------------------------------------------: | :------------------------------------------------------------------: | +| **Figure 6-1: The default colors** | **Figure 6-2: A green variant of the game** | +| ![Figure 6-3: A pink variant of the game](./images/overview-3.png) | ![Figure 6-4: A purple variant of the game](./images/overview-4.png) | +| **Figure 6-3: A pink variant of the game** | **Figure 6-4: A purple variant of the game** | + +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/05-Transition-Effect). + +## The Basic Color Swap Effect + +At the moment, the game uses a lot of blue and gray textures. A common feature in retro-style games is to be able to change the color palette of the game. Another feature is to change the character's color during certain in-game events. For example, maybe the character flashes white when taking damage, or sparkles a gold color when picking up a combo. There are a few broad categories for implementing these styles of features: + +1. Have duplicate assets for all the variations required. +2. Re-draw all of the game assets using each color palette. +3. Use some sort of _color swap_ shader effect to dynamically control the colors of sprites at runtime. + +For simple use cases, sometimes it makes sense to simply re-draw the assets with different colors. However, the second option is more flexible and will enable more features, and since this is a shader tutorial, we will explore option 3 (as it is also the most efficient). + +### Getting Started + +Start by creating a new `Sprite Effect` in the _SharedContent_ MonoGame Content Builder file located in the `MonoGameLibrary` project, and name it `colorSwapEffect.fx`. + +> [!NOTE] +> If the MGCB editor does not open, see the [note from the previous chapter](../05_transition_effect/index.md#shared-content) that requires you to MOVE the `.config` folder containing the `dotnet-tools.json` configuration, from the DungeonSlime folder to its parent folder for the solution. + +| ![Figure 6-5: Add the `colorSwapEffect.fx` to MGCB Editor](./images/mgcb.png) | +| :---------------------------------------------------------------------------: | +| **Figure 6-5: Add the `colorSwapEffect.fx` to MGCB Editor** | + +Switch back to your code editor, and in the `GameScene`, we need to do the following steps to start working with the new `colorSwapEffect.fx`, + +1. Add a new property for the new `Material` instance: + + [!code-csharp[](./snippets/snippet-6-01.cs)] + +2. Load the new `colorSwapEffect` shader in the `LoadContent()` method: + + [!code-csharp[](./snippets/snippet-6-02.cs)] + +3. Update the `Material` in the `Update()` method to enable hot-reload support: + + [!code-csharp[](./snippets/snippet-6-03.cs)] + +4. Finally, we need to _use_ the `colorSwapMaterial` when drawing the sprites for the `GameScene`. For now, as we explore the color swapping effect, we are going to disable the `grayscaleEffect` functionality. In the `Draw()` method replace the `_grayscaleEffect` for the `_colorSwapMaterial`. Also, add the effect to the `else` block, like this: + + [!code-csharp[](./snippets/snippet-6-04.cs?highlight=12,17)] + +Now when you run the game, it will look the same, but the new shader is being used to draw all the sprites in the `GameScene`. To verify, you can try changing the shader function in the `colorSwapEffect.fx` to force the red channel to be `1`, just to see some visually striking confirmation the new shader is being used: + +[!code-hlsl[](./snippets/snippet-6-05.hlsl?highlight=5-7)] + +| ![Figure 6-6: Confirm the shader is being used](./images/test.png) | +| :----------------------------------------------------------------: | +| **Figure 6-6: Confirm the shader is being used** | + +> [!WARNING] +> The menu will not use the color swapper. +> +> The game's menu is being drawn with GUM, and we are not configuring any shaders on the GUM menu yet. For now, it will continue to draw with its old colors. + +For debugging purposes, we will disable the game's update logic so the player and bat are not moving. This will let us focus on developing the look of the shader without getting distracted by the movement and game logic of game-over menus and score. + +The easiest way to disable all of the game logic is to `return` early from the `GameScene`'s `Update()` method, thus short circuiting all of the game logic: + +[!code-csharp[](./snippets/snippet-6-06.cs?highlight=11)] + +### Hard Coding Color Swaps + +The goal is to be able to change the color of the sprites drawn with the `_colorSwapMaterial`. To build some intuition, one of the most straightforward ways to change the color is to hard-code a table of colors in the `colorSwapEffect.fx` file. The texture atlas used to draw the slime character uses a color value of `rgb(32, 40, 78)` for the body of the slime. + +| ![Figure 6-7: The slime uses a dark blue color](./images/slime-blue-color.png) | +| :----------------------------------------------------------------------------: | +| **Figure 6-7: The slime uses a dark blue color** | + +The shader code _could_ just do an `if` check for this color, and when any of the pixels are that color, return a hot-pink color instead. Try swapping out the "MainPS" shader function in the `colorSwapEffect.fx` file to the following, the highlighted section shows where the hard coded color swap is happening: + +[!code-hlsl[](./snippets/snippet-6-07.hlsl?highlight=14)] + +That would produce an image like this, + +| ![Figure 6-8: The blue color is hardcoded to pink](./images/pink.png) | +| :-------------------------------------------------------------------: | +| **Figure 6-8: The blue color is hardcoded to pink** | + +### Using a Color Map + +The problem with the hard-coded approach is that we would need to have an `if` check for _each_ color that should be swapped. Depending on your work ethic, there are already too many colors in the _Dungeon Slime_ assets to hardcode them all in a shader. Instead of hard coding the color swaps as `if` statements, we can create a _table_ of colors that maps asset color to final color. + +Conceptually, a _table_ structure is a series of `key` -> `value` pairs. We could represent each asset color as a `key`, and store the swap color as a `value`. To build up a good example, let us find a few more colors from the _Dungeon Slime_ assets. + +| ![Figure 6-9: The colors from the assets](./images/color-map.png) | +| :---------------------------------------------------------------: | +| **Figure 6-9: The colors from the assets** | + +And here they are written out, + +1. dark-blue - `rgb(32, 40, 78)` +2. gray-blue - `rgb(115, 141, 157)` +3. white - `rgb(255, 255, 255)` +4. light gray-blue - `rgb(214, 225, 233)` + +Our goal is to treat those colors as `keys` into a table that results in a final color `value`. Fortunately, all of the `red` channels are unique across all 4 input colors. The `red` channels are `32`, `115`, `255`, and `214`. + +As a demonstration, if we were using C# to create a table, it might look like this: + +> [!NOTE] +> You do not need to add this to your project. This code is just for conversation. + +[!code-csharp[](./snippets/snippet-6-08.cs)] + +Unfortunately, shaders do not support the `Dictionary<>` type, so we need to find another way to represent the table in a shader friendly format. Shaders however, are extremely good at reading data from textures, so we will encode the table information inside a custom texture. Imagine a custom texture that was 256 pixels wide, but only 1 pixel _tall_. We could treat the `key` values from above (`32`, `115`, `255`, and `214`) as _locations_ along the x-axis of the image, and the color of each pixel as the `value`. + +These images are not to scale, because a 256x1 pixel image would not show well on a web browser. Here are the original colors laid out in a 256x1 pixel image, with the color's red channel value written below the pixel. + +| ![Figure 6-10: The original colors](./images/texture-map-original.png) | +| :-------------------------------------------------------------------: | +| **Figure 6-10: The original colors** | + +We could produce a second texture that puts different color values in the same key positions. + +| ![Figure 6-11: An abstract view of a 255 x 1 texture](./images/texture-map.png) | +| :----------------------------------------------------------------------------: | +| **Figure 6-11: An abstract view of a 255 x 1 texture** | + +Here is the actual texture with the swapped colors. Download [this image](./images/color-map-1.png) and add it to your DungeonSlime Content's "images" folder. + +> [!note] +> This image is not to scale. Remember, it is a 256x1 pixel image. This preview is being scaled up to a height of 32 pixels just for visualization. + +| Figure 6-12: The color table texture | +| :--------------------------------------------------------------: | +| **Figure 6-12: The color table texture** | + +We need to load and pass the texture to the `colorSwapEffect` shader. + +1. Add a `_colorMap` property to the `GameScene` class to hold a reference for the Color map texture: + + [!code-csharp[](./snippets/snippet-6-09-1.cs)] + +2. Then add the following to the following to the `LoadContent` method, just after loading the `_colorSwapMaterial`: + + [!code-csharp[](./snippets/snippet-6-09-2.cs?highlight=9-10)] + +3. And update the `colorSwapEffect.fx` shader needs to be updated to accept the color map: + + [!code-hlsl[](./snippets/snippet-6-10.hlsl?highlight=8-18)] + +The `Texture2D` and `sampler2D` declarations are required to read from textures in a MonoGame shader. A `Texture2D` represents the pixel data of the image. A `sampler2D` defines _how_ the shader is allowed to read data from the `Texture2D`. + +The `ColorMapSampler` has a lot of extra properties (`MinFilter`, `MagFilter`, `MipFilter`, `AddressU`, and `AddressV`) that control exactly how the texture data is read from the `ColorMap`. + +By default, when a sampler reads data from a texture in a shader, it will subtly blend nearby pixel values to increase the visual quality. However, when a texture is being used as a look-up table, this blending is problematic because it will distort the data stored in the texture. The `Point` value given to the `Filter` properties tells the sampler to only read _one_ pixel value. + +When a sampler is reading a texture, there is always some _location_ being used to read pixel data from. The texture coordinate space is from 0 to 1, in both the `u` and `v` axes. + +- By default, if a value greater than 1, or less than 0 is given, the sampler will _wrap_ the value around to be within the range 0 to 1. For example, `1.15` would become `0.15`. +- The `Clamp` value prevents the wrapping and cuts the input off at the min and max values. For example, `1.15` becomes `1.0`. + +> [!tip] +> More information on Samplers. +> +> The [MonoGame Docs](https://docs.monogame.net/articles/getting_to_know/whatis/graphics/WhatIs_Sampler.html) have more details on samplers. + +The shader function can now do two steps to perform the color swap, + +1. Read the original color value of the pixel, +2. Use the original color's red value as the `key` in the look-up texture to extract the swap color `value`. + +To help visualize the effect, it will be helpful to visualize the original color _and_ the swap color. + +1. Add a control parameter that can be used to select between the two colors in the `colorSwapEffect` shader: + + [!code-hlsl[](./snippets/snippet-6-11.hlsl?highlight=4)] + +2. Then replace the `MainPS` shader function to the following, with the highlight showing the progressive effect: + + [!code-hlsl[](./snippets/snippet-6-12.hlsl?highlight=15)] + + Now in the game, we can visualize the color swap by adjusting the control parameter. Perhaps the colors we picked do not look very nice. + + | ![Figure 6-13: The color swap effect is working!](./gifs/color-swap.gif) | + | :---------------------------------------------------------------------: | + | **Figure 6-13: The color swap effect is working!** | + + That looks pretty good, but changing between original and swap colors reveals a visual glitch. The color table did not account for _some_ of the original colors. All of the colors get mapped, and our default color in the map was _white_, so some of the game's art is just turning white. For example, look at the torches on the top-wall. + +3. To fix this, we can adjust the color look-up map to use transparent values by default. Use [this texture](./images/color-map-2.png) instead, saving over the original `color-map-1.png` in the DungeonSlime's Content image folder to avoid having to change anything else. + + | Figure 6-14: The color map with a transparent default color | + | :-------------------------------------------------------------------------------------: | + | **Figure 6-14: The color map with a transparent default color** | + + Now, anytime the swapped color value has an `alpha` value of zero, the implication is that the color was not part of the table. In that case, the shader should default to the original color instead of the non-existent mapped value. + +4. Back in the shader, before the final `return` line, add this snippet: + +[!code-hlsl[](./snippets/snippet-6-13.hlsl?highlight=15-19)] + +| ![Figure 6-15: Colors that are not in the map do not change color](./gifs/color-swap-2.gif) | +| :-----------------------------------------------------------------------------------------: | +| **Figure 6-15: Colors that are not in the map do not change color** | + +One final glitch becomes apparent if you stare at that long enough, which is that the center pixel in the torch is changing color from its original _white_, to our mapped orange color. In a way, that is _by design_, because the white values are being mapped. Fixing this would require modifying the original assets to change the color of the torch center; that is left as an exercise for the reader. + +> [!TIP] +> Color `lerp()` is a short-cut +> +> In the example, we are using linear interpolation to find a color between `swappedColor` and `originalColor`. It works okay, but, the interpolation is happening in RGB color space. RGB is just _one_ possible way to represent a color. It turns out that converting the colors to a different color space, like HSL, interpolating there, and then converting the result _back_ to RGB can produce more visually pleasing results. Check out this great in depth [article](https://www.makingsoftware.com/chapters/color-spaces-models-and-gamuts) on the topic. + +### Nicer Colors + +The colors used above are not the nicest. They were used for demonstration purposes. For you to experiment with, here are some nicer textures to use that produce better results. + +
+ +| ![Figure 6-16: A dark purple look](./images/example-dark-purple.png) | ![Figure 6-17: A green look](./images/example-green.png) | ![Figure 6-18: A pink look](./images/example-pink.png) | +| :------------------------------------------------------------------: | :------------------------------------------------------: | :----------------------------------------------------: | +| **Figure 6-16: A dark purple look** | **Figure 6-17: A green look** | **Figure 6-18: A pink look** | +| Figure 6-12: The color table texture | Figure 6-12: The color table texture | Figure 6-12: The color table texture | +| **Download the [dark-purple](./images/color-map-dark-purple.png) color map** | **Download the [green](./images/color-map-green.png) color map** | **Download the [pink](./images/color-map-pink.png) color map** | + +## Creating Dynamic Color Maps + +So far, we have created color maps and brought them into the game as content. However, it would be cool to create these color maps dynamically with a C# script based on gameplay values and provide that texture to the shader in realtime. Our goal will be to modify the snake's color piece by piece when a the player eats a bat. + +To get started, we first need to devise a way to create a custom color map and pass it to the shader. + +1. Create a new class under the _MonoGameLibrary/Graphics_ folder called `RedColorMap`: + + [!code-csharp[](./snippets/snippet-6-14.cs)] + +2. Add another property to store the reference to the Color mapping code from `RedColorMap` in the `GameScene` class: + + [!code-csharp[](./snippets/snippet-6-14-2.cs)] + +3. Add we are going to use a `Dictionary` for the new mapping structure, we will also need an additional using, so add the following to the top of the `GameScene` class: + + ```csharp + using System.Collections.Generic; + ``` + +4. Now, to check if it's working, create the data to store in the new `_slimeColorMap` variable at the end of the `LoadContent()` in the `GameScene`: + + [!code-csharp[](./snippets/snippet-6-15.cs)] + +| ![Figure 6-19: Changing the colors from runtime](./images/example-runtime.png) | +| :----------------------------------------------------------------------------: | +| **Figure 6-19: Changing the colors from runtime** | + +### Changing Slime Color + +The goal is to change the color of the slime independently from the rest of the game. The `SpriteBatch` will try to make as few draw calls as possible and because all of the game assets are in a sprite-atlas, any shader parameters will be applied for _all_ sprites. + +However, you can change the `sortMode` to `Immediate` to change the `SpriteBatch`'s optimization to make it draw sprites immediately with whatever _current_ shader parameters exist. + +> [!WARNING] +> The `Immediate` sort mode stops the `SpriteBatch` from _batching_ your sprites into a single draw call, and instead, makes a draw call _per_ sprite. More draw calls means more work between the CPU and GPU, and that means slower performance compared to fewer draw calls. However, game programming is about identifying acceptable trade offs between performance and gameplay value. +> +> _Dungeon Slime_ is a simple game, and it is not worth the effort it to try and optimize this part of the game, _at this point_. +> +> As an exercise for the reader, try to think of a way to re-write this section with two draw calls instead of one per sprite. +> +> _hint: there are only 2 color maps that are ever on screen at any given moment._ + +1. Change both the `SpriteBatch.Begin()` calls in the `Draw` method of the `GameScene` class to look like this: + + [!code-csharp[](./snippets/snippet-6-16.cs?highlight=3)] + +2. Then update the draw code itself to update the shader parameter between drawing the slime and the rest of the game:  + Note the `_bat.Draw()` call has also moved up to ensure it is not affected when drawing the Slime: + + [!code-csharp[](./snippets/snippet-6-17.cs?highlight=5-6,11-12,14-15)] + +Now the slime appears with one color swap configuration and the rest of the scene uses the color swap configured via the content. + +> [!NOTE] +> The [pink](./images/color-map-pink.png) color map is being used instead of the `color-map-1.png` from earlier. + +| ![Figure 6-20: The slime is a different color configuration than the game](./images/example-multi.png) | +| :----------------------------------------------------------------------------------------------------: | +| **Figure 6-20: The slime is a different color configuration than the game** | + +If we want to swap the color of the slime between two color maps, we need a way to clone an existing color map into the dynamic color table. + +1. Add this method to the `RedColorMap` class: + + [!code-csharp[](./snippets/snippet-6-18.cs)] + +2. Then modify the instance in the `GameScene` to start the color map based on whatever color map texture was loaded: + + [!code-csharp[](./snippets/snippet-6-19.cs?highlight=7-12)] + +3. Now in the `Draw()` method, we can _optionally_ change the color map based on some condition. In this example, the color map only being set on every other second: + + [!code-csharp[](./snippets/snippet-6-20.cs)] + +| ![Figure 6-21: The slime's color changes based on time](./gifs/color-swap-even-seconds.gif) | +| :-----------------------------------------------------------------------------------------: | +| **Figure 6-21: The slime's color changes based on time** | + +Ultimately, it would be nice to control the color value _per_ slime segment, not the entire slime. When the player eats a bat, the slime segments should change color in an animated way so that it looks like the color is "moving" down the slime segments. + +1. To do this, modify the `Slime.Draw()` method in the `Slime.cs` class in the _DungeonSlime/GameObjects_ folder to look like this: + + [!code-csharp[](./snippets/snippet-6-21.cs)] + +2. As we want to see how long the slime is for the effect, we need to expose how long the Slime's segments are, so add the following public property to the Slime: + + ```csharp + // The size of the slime + public int Size => _segments.Count; + ``` + +3. Then, in the `GameScene`'s logic, we need to add a local field to remember the last time the slime's `Grow()` method was called: + + [!code-csharp[](./snippets/snippet-6-22.cs)] + +4. In order to capture the time that has passed, we need to be able to check this for collisions, so in the `Update` method, pass `gameTime` to the `CollisionChecks` method as shown below: + + [!code-csharp[](./snippets/snippet-6-23-update.cs?highlight=9)] + +5. In the `CollisionChecks` method, add this line after the `Grow()` method is invoked: + + [!code-csharp[](./snippets/snippet-6-23.cs?highlight=1,12)] + +6. Now, in the `Draw()` method, modify the _slime_'s draw invocation to use the new `configureSpriteBatch` callback: + + [!code-csharp[](./snippets/snippet-6-24.cs)] + +7. A bit of cleanup, if you left in the old time highlight code, make sure to **REMOVE** it from the `Draw` method too (because the updated `slime.Draw` now handles this). + + ```csharp + // REMOVE ME + // if ((int)gameTime.TotalGameTime.TotalSeconds % 2 == 0) + // { + // _colorSwapMaterial.SetParameter("ColorMap", _slimeColorMap.ColorMap); + // } + ``` + +8. Finally, play around with the colors until you find something you like. + +> [!NOTE] +> If nothing is playing, [remember](#getting-started) we disabled the gameplay earlier in this chapter for testing, so remove the extra `return;` statement in the `Update` method of the `GameScene` class to get it running again. + +| ![Figure 6-22: The slime's color changes when it eats](./gifs/color-swap-slime.gif) | +| :---------------------------------------------------------------------------------: | +| **Figure 6-22: The slime's color changes when it eats** | + +> [!NOTE] +> The[dark-purple](./images/color-map-dark-purple.png) color map is being used instead of the pink from earlier. + +## Fixing the GrayScale + +The color swap shader is working well, but to experiment with it, we had previously _removed_ the pause screen's grayscale effect. Both effects are trying to modify the color of the game, so they naturally conflict with each other. To solve the problem, the shaders can be merged together into a single effect. + +1. Extract the logic of the grayscale effect into a separate function and copy it into the `colorSwapEffect.fx` file: + + [!code-hlsl[](./snippets/snippet-6-25.hlsl)] + +2. In order for this to work, do not forget to add the `Saturation` shader parameter to the `colorSwapEffect.fx` file: + + [!code-hlsl[](./snippets/snippet-6-26.hlsl)] + +3. For readability, extract the logic of the color swap effect into a new function as well: + + [!code-hlsl[](./snippets/snippet-6-27.hlsl)] + +4. And now you can replace the main shader function so that it can chain these methods together: + + [!code-hlsl[](./snippets/snippet-6-28.hlsl)] + +> [!WARNING] +> Function Order Matters! +> +> Make sure that the `Grayscale` and `SwapColors` functions appear _before_ the `MainPS` function in the shader, otherwise the compiler will not be able to resolve the functions. + +Now you can control the saturation manually with the debug slider, + +| ![Figure 6-23: Combining the color swap and saturation effect](./gifs/color-saturation.gif) | +| :-----------------------------------------------------------------------------------------: | +| **Figure 6-23: Combining the color swap and saturation effect** | + +The last thing to do is remove the old `grayscaleEffect` and re-write the game logic to set the `Saturation` parameter on the new effect. + +1. In the `Draw()` method, instead of having an `if` case to start the `SpriteBatch` with different settings, it can always be configured to start with the `_colorSwapMaterial`: + + [!code-csharp[](./snippets/snippet-6-29.cs?highlight=3)] + +2. In the `Update()` method, we just need to set the `_saturation` back to `1` if the game is being played: + + [!code-csharp[](./snippets/snippet-6-30.cs?highlight=23)] + +| ![Figure 6-24: The grayscale effect has been restored](./gifs/grayscale.gif) | +| :--------------------------------------------------------------------------: | +| **Figure 6-24: The grayscale effect has been restored** | + +At this point, you can remove the `_grayscaleEffect` from the `GameScene`. +- Remove the declaration, +- Remove where it was loaded in the `LoadContent()` method, +- Remove where it was used in the `Update()` method. + +You can also remove the shader itself from MGCB. + +## Color Look-Up Textures (LUTs) + +The approach we used above is a simplified version of a broader technique called _Color Look-Up Tables_, or Color LUTs. In the version we wrote above, there is a large limitation about which colors can be used in the table. The `key` in the color table was the `red` channel value of the input colors. If you had two different input colors that shared the same `red` channel value, the technique would not work. + +The limitation is _often_ acceptable in game assets because you own the assets themselves and can author the textures to avoid the case where colors overlap on key values. However, when it is unavoidable, the `key` must be more complex than only the `red` value. For example, it could be unavoidable if your game needed more than 256 _unique_ colors. + +The next logical step is to make the `key` _2_ color channels like `red` _and_ `green`. In that case, the color texture would not be a 256x1 texture, it would be a 256x256 texture. The `x` axis would still represent the `red` channel, and now the `y` axis would represent the `green` channel. Now the game could have `256 * 256` unique colors, or `65,536`. + +Finally, if you need _more_ colors, the final color channel can be included in the `key`, making the `key` be the combination of `red`, `green`, and `blue` channels. In the first case, the look-up texture was 256x1 pixels. In the second case, it was 256x256 pixels. The final case is a texture of size 2048x2048 pixels. Imagine a texture made up of smaller 256x256 textures, stored in an 8x8 grid. + +Color LUTs are used in post-processing to adjust the final look and feel of games across the industry. The technique is called _Tone-Mapping_. + +If you are interested in Color LUTs, check out the following articles, + +1. [GPU Gems 2: Chapter 24](https://developer.nvidia.com/gpugems/gpugems2/part-iii-high-quality-rendering/chapter-24-using-look-up-tables-accelerate-color) offers a sweeping overview of 3D Color LUTs. +2. [Frost Kiwi's Color LUT Article](https://blog.frost.kiwi/WebGL-LUTS-made-simple/) is a fantastic exploration of the topic with lots of additional sources to explore. + +## Conclusion + +That was a really powerful technique! In this chapter, you accomplished the following: + +- Implemented a color-swapping system using a 1D texture as a Look-Up Table (LUT). +- Created a `RedColorMap` class to dynamically generate these LUTs from C# code. +- Used `SpriteSortMode.Immediate` to apply different materials to different sprites in the same frame. +- Combined the color swap and grayscale effects into a single, more versatile shader. + +So far, all of our work has been in the pixel shader, which is all about changing the color of pixels. In the next chapter, we will switch gears and explore the vertex shader to manipulate the geometry of our sprites and add some surprising 3D flair to our 2D game. + +You can find the complete code sample for this chapter, [here](https://github.com/MonoGame/MonoGame.Samples/tree/3.8.4/Tutorials/2dShaders/src/06-Color-Swap-Effect). + +Continue to the next chapter, [Chapter 07: Sprite Vertex Effect](../07_sprite_vertex_effect/index.md) diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-01.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-01.cs new file mode 100644 index 00000000..3d71c45e --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-01.cs @@ -0,0 +1,2 @@ +// The color swap shader material. +private Material _colorSwapMaterial; diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-02.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-02.cs new file mode 100644 index 00000000..b2036ddb --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-02.cs @@ -0,0 +1,10 @@ +public override void LoadContent() +{ + // ... + + // Load the colorSwap material + _colorSwapMaterial = Core.SharedContent.WatchMaterial("effects/colorSwapEffect"); + _colorSwapMaterial.IsDebugVisible = true; + + // ... +} diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-03.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-03.cs new file mode 100644 index 00000000..0d369330 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-03.cs @@ -0,0 +1,9 @@ +public override void Update(GameTime gameTime) +{ + // ... + + // Update the colorSwap material if it was changed + _colorSwapMaterial.Update(); + + // ... +} diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-04.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-04.cs new file mode 100644 index 00000000..89694e3a --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-04.cs @@ -0,0 +1,21 @@ +public override void Draw(GameTime gameTime) +{ + // Clear the back buffer. + Core.GraphicsDevice.Clear(Color.CornflowerBlue); + + if (_state != GameState.Playing) + { + // We are in a game over state, so apply the saturation parameter. + _grayscaleEffect.SetParameter("Saturation", _saturation); + + // And begin the sprite batch using the grayscale effect. + Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp, effect: _colorSwapMaterial.Effect); + } + else + { + // Otherwise, just begin the sprite batch as normal. + Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp, effect: _colorSwapMaterial.Effect); + } + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-05.hlsl b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-05.hlsl new file mode 100644 index 00000000..d9ec94a2 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-05.hlsl @@ -0,0 +1,10 @@ +// ... + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float4 originalColor = tex2D(SpriteTextureSampler,input.TextureCoordinates) * input.Color; + originalColor.r = 1; // force the red-channel + return originalColor; +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-06.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-06.cs new file mode 100644 index 00000000..dbfd371d --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-06.cs @@ -0,0 +1,14 @@ +public override void Update(GameTime gameTime) +{ + // Ensure the UI is always updated + _ui.Update(gameTime); + + // Update the grayscale effect if it was changed + _grayscaleEffect.Update(); + _colorSwapMaterial.Update(); + + // Prevent the game from actually updating. TODO: remove this when we are done playing with shaders! + return; + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-07.hlsl b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-07.hlsl new file mode 100644 index 00000000..08719579 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-07.hlsl @@ -0,0 +1,23 @@ +// ... + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float4 originalColor = tex2D(SpriteTextureSampler,input.TextureCoordinates) * input.Color; + + // the color values are stored between 0 and 1, + // this converts the 0 to 1 range to 0 to 255, and casts to an int. + int red = originalColor.r * 255; + int green = originalColor.g * 255; + int blue = originalColor.b * 255; + + // check for the hard-coded blue color + if (red == 32 && green == 40 && blue == 78) + { + float4 hotPink = float4(.9, 0, .7, 1); + return hotPink; + } + + return originalColor; +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-08.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-08.cs new file mode 100644 index 00000000..7f3e4307 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-08.cs @@ -0,0 +1,8 @@ +var map = new Dictionary +{ + // picked some random colors for the values + [32] = Color.MonoGameOrange, + [115] = Color.CornflowerBlue, + [255] = Color.Firebrick, + [214] = Color.Salmon +}; diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-09-1.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-09-1.cs new file mode 100644 index 00000000..cd83fe39 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-09-1.cs @@ -0,0 +1 @@ +private Texture2D _colorMap; \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-09-2.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-09-2.cs new file mode 100644 index 00000000..9459201c --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-09-2.cs @@ -0,0 +1,11 @@ +public override void LoadContent() +{ + // ... + + // Load the colorSwap material + _colorSwapMaterial = Core.SharedContent.WatchMaterial("effects/colorSwapEffect"); + _colorSwapMaterial.IsDebugVisible = true; + + _colorMap = Core.Content.Load("images/color-map-1"); + _colorSwapMaterial.SetParameter("ColorMap", _colorMap); +} diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-10.hlsl b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-10.hlsl new file mode 100644 index 00000000..758d4b9e --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-10.hlsl @@ -0,0 +1,20 @@ +// the main Sprite texture passed to SpriteBatch.Draw() +Texture2D SpriteTexture; +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + +// the custom color map passed to the Material.SetParameter() +Texture2D ColorMap; +sampler2D ColorMapSampler = sampler_state +{ + Texture = ; + MinFilter = Point; + MagFilter = Point; + MipFilter = Point; + AddressU = Clamp; + AddressV = Clamp; +}; + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-11.hlsl b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-11.hlsl new file mode 100644 index 00000000..c265cad3 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-11.hlsl @@ -0,0 +1,11 @@ +// ... + +// a control variable to lerp between original color and swapped color +float OriginalAmount; + +// ... + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + // ... +} diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-12.hlsl b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-12.hlsl new file mode 100644 index 00000000..128a3120 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-12.hlsl @@ -0,0 +1,18 @@ +// ... + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + // read the original color value + float4 originalColor = tex2D(SpriteTextureSampler,input.TextureCoordinates); + + // produce the key location + float2 keyUv = float2(originalColor.r, 0); + + // read the swap color value + float4 swappedColor = tex2D(ColorMapSampler, keyUv) * originalColor.a; + + // return the result color + return lerp(swappedColor, originalColor, OriginalAmount); +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-13.hlsl b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-13.hlsl new file mode 100644 index 00000000..1fc875e9 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-13.hlsl @@ -0,0 +1,25 @@ +// ... + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + // read the original color value + float4 originalColor = tex2D(SpriteTextureSampler,input.TextureCoordinates); + + // produce the key location + float2 keyUv = float2(originalColor.r, 0); + + // read the swap color value + float4 swappedColor = tex2D(ColorMapSampler, keyUv) * originalColor.a; + + // ignore the swap if the map does not have a value + bool hasSwapColor = swappedColor.a > 0; + if (!hasSwapColor) + { + return originalColor; + } + + // return the result color + return lerp(swappedColor, originalColor, OriginalAmount); +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-14-2.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-14-2.cs new file mode 100644 index 00000000..5af21325 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-14-2.cs @@ -0,0 +1 @@ +private RedColorMap _slimeColorMap; diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-14.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-14.cs new file mode 100644 index 00000000..2af182a0 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-14.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGameLibrary.Graphics; + +public class RedColorMap +{ + public Texture2D ColorMap { get; set; } + + public RedColorMap() + { + ColorMap = new Texture2D(Core.GraphicsDevice, 256, 1, false, SurfaceFormat.Color); + } + + /// + /// Given a dictionary of red-color values (0 to 255) to swapColors, + /// Set the values of the so that it can be used + /// As the ColorMap parameter in the colorSwapEffect. + /// + public void SetColorsByRedValue(Dictionary map, bool overWrite = true) + { + var pixelData = new Color[ColorMap.Width]; + ColorMap.GetData(pixelData); + + for (var i = 0; i < pixelData.Length; i++) + { + // if the given color dictionary contains a color value for this red index, use it. + if (map.TryGetValue(i, out var swapColor)) + { + pixelData[i] = swapColor; + } + else if (overWrite) + { + // otherwise, default the pixel to transparent + pixelData[i] = Color.Transparent; + } + } + + ColorMap.SetData(pixelData); + } +} diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-15.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-15.cs new file mode 100644 index 00000000..c051a025 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-15.cs @@ -0,0 +1,20 @@ +public override void LoadContent() +{ + // ... + + _slimeColorMap = new RedColorMap(); + _slimeColorMap.SetColorsByRedValue(new Dictionary + { + // main color + [32] = Color.Khaki, + // wall color + [115] = Color.Coral, + // shadow color + [214] = Color.MonoGameOrange, + // floor + [255] = Color.Tomato + }); + + _colorSwapMaterial.SetParameter("ColorMap", _slimeColorMap.ColorMap); + +} diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-16.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-16.cs new file mode 100644 index 00000000..09736871 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-16.cs @@ -0,0 +1,4 @@ +Core.SpriteBatch.Begin( + samplerState: SamplerState.PointClamp, + sortMode: SpriteSortMode.Immediate, + effect: _colorSwapMaterial.Effect); \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-17.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-17.cs new file mode 100644 index 00000000..be998cc9 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-17.cs @@ -0,0 +1,25 @@ +public override void Draw(GameTime gameTime) +{ + // ... + + // Update the colorMap + _colorSwapMaterial.SetParameter("ColorMap", _colorMap); + + // Draw the tilemap + _tilemap.Draw(Core.SpriteBatch); + + // Draw the bat. + _bat.Draw(); + + // Update the colorMap for the slime + _colorSwapMaterial.SetParameter("ColorMap", _slimeColorMap.ColorMap); + + // Draw the slime. + _slime.Draw(); + + // Always end the sprite batch when finished. + Core.SpriteBatch.End(); + + // Draw the UI + _ui.Draw(); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-18.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-18.cs new file mode 100644 index 00000000..e81b129e --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-18.cs @@ -0,0 +1,13 @@ +public void SetColorsByExistingColorMap(Texture2D existingColorMap) +{ + var existingPixels = new Color[256]; + existingColorMap.GetData(existingPixels); + + var map = new Dictionary(); + for (var i = 0; i < existingPixels.Length; i++) + { + map[i] = existingPixels[i]; + } + + SetColorsByRedValue(map); +} diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-19.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-19.cs new file mode 100644 index 00000000..98d6875d --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-19.cs @@ -0,0 +1,13 @@ + +public override void LoadContent() +{ + // ... + + _slimeColorMap = new RedColorMap(); + _slimeColorMap.SetColorsByExistingColorMap(_colorMap); + _slimeColorMap.SetColorsByRedValue(new Dictionary + { + // main color + [32] = Color.Yellow, + }, false); +} diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-20.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-20.cs new file mode 100644 index 00000000..e3d45aa4 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-20.cs @@ -0,0 +1,15 @@ +public override void Draw(GameTime gameTime) +{ + // ... + + // Update the colorMap for the slime + if ((int)gameTime.TotalGameTime.TotalSeconds % 2 == 0) + { + _colorSwapMaterial.SetParameter("ColorMap", _slimeColorMap.ColorMap); + } + + // Draw the slime. + _slime.Draw(); + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-21.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-21.cs new file mode 100644 index 00000000..c31033de --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-21.cs @@ -0,0 +1,22 @@ +/// +/// Draws the slime. +/// +public void Draw(Action configureSpriteBatch) +{ + // Iterate through each segment and draw it + for (var i = 0 ; i < _segments.Count; i ++) + { + var segment = _segments[i]; + // Calculate the visual position of the segment at the moment by + // lerping between its "at" and "to" position by the movement + // offset lerp amount + Vector2 pos = Vector2.Lerp(segment.At, segment.To, _movementProgress); + + // Allow the sprite batch to be configured before each call. + configureSpriteBatch(i); + + // Draw the slime sprite at the calculated visual position of this + // segment + _sprite.Draw(Core.SpriteBatch, pos); + } +} diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-22.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-22.cs new file mode 100644 index 00000000..08255c09 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-22.cs @@ -0,0 +1 @@ +private TimeSpan _lastGrowTime; diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-23-update.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-23-update.cs new file mode 100644 index 00000000..5a731bed --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-23-update.cs @@ -0,0 +1,10 @@ +public override void Update(GameTime gameTime) +{ + // ... + + // Update the bat; + _bat.Update(gameTime); + + // Perform collision checks + CollisionChecks(gameTime); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-23.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-23.cs new file mode 100644 index 00000000..b8b69f74 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-23.cs @@ -0,0 +1,18 @@ +private void CollisionChecks(GameTime gameTime) +{ + // ... + if (slimeBounds.Intersects(batBounds)) + { + // ... + + // Tell the slime to grow. + _slime.Grow(); + + // Remember when the last time the slime grew + _lastGrowTime = gameTime.TotalGameTime; + + // ... + } + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-24.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-24.cs new file mode 100644 index 00000000..c7186d23 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-24.cs @@ -0,0 +1,22 @@ +public override void Draw(GameTime gameTime) +{ + // ... + + // Draw the slime. + _slime.Draw(segmentIndex => + { + const int flashTimeMs = 125; + var map = _colorMap; + var elapsedMs = (gameTime.TotalGameTime.TotalMilliseconds - _lastGrowTime.TotalMilliseconds); + var intervalsAgo = (int)(elapsedMs / flashTimeMs); + + if (intervalsAgo < _slime.Size && (intervalsAgo - segmentIndex) % _slime.Size == 0) + { + map = _slimeColorMap.ColorMap; + } + + _colorSwapMaterial.SetParameter("ColorMap", map); + }); + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-25.hlsl b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-25.hlsl new file mode 100644 index 00000000..f28d4418 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-25.hlsl @@ -0,0 +1,15 @@ +float4 Grayscale(float4 color) +{ + // Calculate the grayscale value based on human perception of colors + float grayscale = dot(color.rgb, float3(0.3, 0.59, 0.11)); + + // create a grayscale color vector (same value for R, G, and B) + float3 grayscaleColor = float3(grayscale, grayscale, grayscale); + + // Linear interpolation between he grayscale color and the original color's + // rgb values based on the saturation parameter. + float3 finalColor = lerp(grayscale, color.rgb, Saturation); + + // Return the final color with the original alpha value + return float4(finalColor, color.a); +} diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-26.hlsl b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-26.hlsl new file mode 100644 index 00000000..b31d915f --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-26.hlsl @@ -0,0 +1 @@ +float Saturation; diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-27.hlsl b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-27.hlsl new file mode 100644 index 00000000..83b60de2 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-27.hlsl @@ -0,0 +1,19 @@ +float4 SwapColors(float4 color) +{ + // produce the key location + // note the x-offset by half a texel solves rounding errors. + float2 keyUv = float2(color.r , 0); + + // read the swap color value + float4 swappedColor = tex2D(ColorMapSampler, keyUv) * color.a; + + // ignore the swap if the map does not have a value + bool hasSwapColor = swappedColor.a > 0; + if (!hasSwapColor) + { + return color; + } + + // return the result color + return lerp(swappedColor, color, OriginalAmount); +} diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-28.hlsl b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-28.hlsl new file mode 100644 index 00000000..4c9cdbf6 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-28.hlsl @@ -0,0 +1,10 @@ +float4 MainPS(VertexShaderOutput input) : COLOR +{ + // read the original color value + float4 originalColor = tex2D(SpriteTextureSampler,input.TextureCoordinates); + + float4 swapped = SwapColors(originalColor); + float4 saturated = Grayscale(swapped); + + return saturated; +} diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-29.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-29.cs new file mode 100644 index 00000000..cadbbaf5 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-29.cs @@ -0,0 +1,11 @@ +public override void Draw(GameTime gameTime) +{ + _colorSwapMaterial.SetParameter("Saturation", _saturation); + + Core.SpriteBatch.Begin( + samplerState: SamplerState.PointClamp, + sortMode: SpriteSortMode.Immediate, + effect: _colorSwapMaterial.Effect); + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-30.cs b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-30.cs new file mode 100644 index 00000000..786a3356 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/06_color_swap_effect/snippets/snippet-6-30.cs @@ -0,0 +1,27 @@ +public override void Update(GameTime gameTime) +{ + // Ensure the UI is always updated + _ui.Update(gameTime); + + // Update the colorSwap material if it was changed + _colorSwapMaterial.Update(); + + if (_state != GameState.Playing) + { + // The game is in either a paused or game over state, so + // gradually decrease the saturation to create the fading grayscale. + _saturation = Math.Max(0.0f, _saturation - FADE_SPEED); + + // If its just a game over state, return back + if (_state == GameState.GameOver) + { + return; + } + } + else + { + _saturation = 1; + } + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/basic-2.gif b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/basic-2.gif new file mode 100644 index 00000000..05eeacca Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/basic-2.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/basic.gif b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/basic.gif new file mode 100644 index 00000000..e7f24835 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/basic.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/cam-follow.gif b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/cam-follow.gif new file mode 100644 index 00000000..969c348a Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/cam-follow.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/final.gif b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/final.gif new file mode 100644 index 00000000..ae014ed6 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/final.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/overview-1.gif b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/overview-1.gif new file mode 100644 index 00000000..b72a2630 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/overview-1.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/overview-2.gif b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/overview-2.gif new file mode 100644 index 00000000..c4f0c92a Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/overview-2.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/spin-1.gif b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/spin-1.gif new file mode 100644 index 00000000..f0d6c003 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/spin-1.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/spin-2.gif b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/spin-2.gif new file mode 100644 index 00000000..a282a5f3 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/spin-2.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/spin-3.gif b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/spin-3.gif new file mode 100644 index 00000000..8a5d5222 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/spin-3.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/spin-4.gif b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/spin-4.gif new file mode 100644 index 00000000..49dd65dc Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/spin-4.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/uber.gif b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/uber.gif new file mode 100644 index 00000000..7fe358c6 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/gifs/uber.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/images/basic.png b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/images/basic.png new file mode 100644 index 00000000..0d7171e2 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/images/basic.png differ diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/images/mgcb.png b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/images/mgcb.png new file mode 100644 index 00000000..c0f72e1c Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/images/mgcb.png differ diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/images/missing-text.png b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/images/missing-text.png new file mode 100644 index 00000000..d0e4f4c8 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/images/missing-text.png differ diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/index.md b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/index.md new file mode 100644 index 00000000..f6d1af3b --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/index.md @@ -0,0 +1,511 @@ +--- +title: "Chapter 07: Sprite Vertex Shaders" +description: "Learn about vertex shaders and how to use them on sprites" +--- + +Every shader has two main parts: the pixel shader, which we have been using to change the colors of our sprites, and the vertex shader. The vertex shader runs first, and its job is to determine the final shape and position of our geometry. Up until now, we have been using MonoGame's default vertex shader, which just draws our sprites as flat 2D rectangles. + +In this chapter, we are going to unlock the power of the vertex shader. we will write our own custom vertex shader from scratch, which will allow us to break out of the 2D plane. We will learn how to use a perspective projection to give our flat world a cool, dynamic 3D feel. + +At the end of this chapter, we will be able to make our sprites appear with 3d perspective. + +| ![Figure 7-1: The main menu will have a 3d-esque title ](./gifs/overview-1.gif) | ![Figure 7-2: The game will have a 3d-esque world ](./gifs/overview-2.gif) | +| :-----------------------------------------------------------------------------: | :------------------------------------------------------------------------: | +| **Figure 7-1: The main menu will have a 3d-esque title** | **Figure 7-2: The game will have a 3d-esque world** | + +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/06-Color-Swap-Effect). + +## Default Vertex Shader + +So far in this series, we have only dealt with pixel shaders. To recap, the job of a pixel shader is to convert some input `(u,v)` coordinate into an output color `(r,g,b,a)` value. + +There has been a second shader function running all along behind the scenes, called the vertex shader. The vertex shader runs _before_ the pixel shader whose job is to convert world-space vertex data into clip-space vertex data. Technically every call in MonoGame that draws data to the screen must provide a vertex shader function and a pixel shader function. However, the `SpriteBatch` class has a default implementation of the vertex shader that runs automatically. + +The default `SpriteBatch` vertex shader takes the vertices that make up the sprite's corners, and applies an [_orthographic projection_](https://en.wikipedia.org/wiki/Orthographic_projection). The orthographic projection creates a 2d (flat) effect where shapes have no perspective, even when they are closer or further away from the origin. + +The vertex shader that is being used can be found [here](https://github.com/MonoGame/MonoGame/blob/develop/MonoGame.Framework/Platform/Graphics/Effect/Resources/SpriteEffect.fx#L29), and is detailed below: + +[!code-hlsl[](./snippets/snippet-7-01.hlsl)] + +The `SpriteVertexShader` looks different from our pixel shaders in a few important ways, + +1. The inputs and outputs are different. + - The return type is not just a `float4`, it is an entire struct, `VSOutput`, + - The inputs are not the same as the pixel shader. The pixel shader got a `Color` and `TextureCoordinates`, but this vertex shader has a `position`, a `color`, and a `texCoord`. +2. There is a `MatrixTransform` shader parameter available to this shader. + +### Input Semantics + +The inputs to the vertex shader mirror the information that the [`SpriteBatchItem`](https://github.com/MonoGame/MonoGame/blob/develop/MonoGame.Framework/Graphics/SpriteBatchItem.cs) class bundles up for each vertex. If you look at the `SpriteBatchItem`, you will see that each sprite is made up of 4 `VertexPositionColorTexture` instances (one vertex/index for each corner): + +[!code-csharp[](./snippets/snippet-7-02.cs)] + +> [!NOTE] +> The `SpriteBatchItem` is part of the implementation of `SpriteBatch`, but `SpriteBatchItem` is not part of the public MonoGame API. + +The [`VertexPositionColorTexture`](https://docs.monogame.net/api/Microsoft.Xna.Framework.Graphics.VertexPositionColorTexture) class is a standard MonoGame implementation of the `IVertexType`, and it defines a `Position`, a `Color`, and a `TextureCoordinate` for each vertex. These should look familiar, because they align with the inputs to the vertex shader function. The alignment is not happenstance, it is enforced by ["semantics"](https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-semantics) that are applied to each field in the vertex. + +> [!NOTE] +> A `VertexPositionColorTexture` contains: +> +> - A Vertex (corner) +> - A Position (UV coordinate) +> - A Color +> - A Texture +> +> Just as the name states, you can check the other [VertexDeclaration](https://docs.monogame.net/api/Microsoft.Xna.Framework.Graphics.VertexDeclaration.html) types in the API documentation for reference. You can always create your own custom declarations if any of the built in ones do not suffice, but you would not be able to use it with `SpriteBatch` by default. + +This snippet from the `VertexPositionColorTexture` class defines the semantics for each field in the vertex by specifying the `VertexElementUsage`: + +[!code-csharp[](./snippets/snippet-7-03.cs)] + +> [!TIP] +> MonoGame is free and open source, so you can always go read the full source code for the [`VertexPositionColorTexture`](https://github.com/MonoGame/MonoGame/blob/develop/MonoGame.Framework/Graphics/Vertices/VertexPositionColorTexture.cs)) + +The vertex shader declares a semantic for each input using the `:` syntax: + +[!code-hlsl[](./snippets/snippet-7-04.hlsl)] + +The semantics align with the values from the `VertexElementUsage` values. The table shows the correlation of the common semantics. + +| Shader Semantic | `VertexElementUsage` Value | +| :-------------- | :------------------------------------- | +| `POSITION` | `VertexElementUsage.Position` | +| `COLOR` | `VertexElementUsage.Color` | +| `TEXCOORD` | `VertexElementUsage.TextureCoordinate` | + +These semantics are responsible for mapping the values written into the `VertexPositionColorTexture` to the corresponding inputs in the shader file. + +>[!warning] +> The `SpriteBatch` class does not offer any way to change the vertex semantics that are passed to the vertex shader function. + +### Output Semantics + +The same concept of semantics applies to the output of the shader. Here is the output type of the vertex shader function. Notice that the fields also have the `:` semantic syntax. These semantics instruct the graphics pipeline how to use the data: + +[!code-hlsl[](./snippets/snippet-7-05.hlsl)] + +This is the _input_ struct for the standard pixel shaders from previous chapters. Notice how the fields are named slightly differently, but the _semantics_ are identical: + +[!code-hlsl[](./snippets/snippet-7-06.hlsl)] + +> [!TIP] +> What is the difference between `SV_Position` and `POSITION0` ? +> +> In various places in the shader code, you may notice semantics using `SV_Position` and `POSITION` interchangeably. The `SV_Position` semantic is actually specific to [Direct3D 10's System-Value Semantics](https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-semantics?redirectedfrom=MSDN#system-value-semantics). In fact, `SV_Position` is _not_ a valid semantic in DesktopGL targets, so _how_ can it be used interchangeably with `POSITION`? +> +> MonoGame's default shader has a trick to re-map `SV_Position` to `POSITION` only when the target is `OPENGL`: +> [!code-hlsl[](./snippets/snippet-7-sv.hlsl?highlight=2)] +> +> The `#define` line tells the shader parser to replace any instance of `SV_POSITION` with `POSITION`. +> This implies that `SV_POSITION` is converted to `POSITION` when you are targetting `OPENGL` platforms, and left "as is" when targeting DirectX. + +### Matrix Transform + +The default sprite vertex shader uses a [`mul()`](https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-mul) expression: + +[!code-hlsl[](./snippets/snippet-7-07.hlsl?highlight=8)] + +The reason this line exists is to convert the vertices from world-space to clip-space. + +> [!TIP] +> A vertex is a 3d coordinate in "world-space". But a monitor is a 2d display. Often, the screen's 2d coordinate system is called "clip-space". The vertex shader is converting the 3d world-space coordinate into a 2d clip-space coordinate. That conversion is a vector and matrix multiplication, using the `MatrixTransform`. +> +> Read more about clip space on [Wikipedia](https://en.wikipedia.org/wiki/Clip_coordinates). +> We will cover more about _how_ the conversion happens later in this chapter, in the perspective projection section. + +The `MatrixTransform` is computed by the [`SpriteEffect`](xref:Microsoft.Xna.Framework.Graphics.SpriteEffect) class. The full source is available, [`here`](https://github.com/MonoGame/MonoGame/blob/develop/MonoGame.Framework/Graphics/Effect/SpriteEffect.cs#L63). The relevant lines are copied below: + +[!code-csharp[](./snippets/snippet-7-08.cs)] + +There are two common types of projection matrices, + +1. Orthographic (The default used by `SpriteBatch`), +2. Perspective + +The orthographic projection matrix produces the classic 2d sprite effect, where sprites have no perspective when they are on the sides of the screen. + +> [!TIP] +> Read more about these projection matrixes on [MonoGame's Camera Article](https://docs.monogame.net/articles/getting_to_know/whatis/graphics/WhatIs_Camera.html). +> +> Along with many other "How To" and "What Is" articles explaining MonoGame architecture. + +## Custom Vertex Shader + +Now that you understand the default vertex shader being used by `SpriteBatch`, we can replace the shader with a custom shader. The new shader must accomplish the basic requirements, + +1. convert the vertices from world-space to clip-space +2. provide the input semantics required for the pixel shader. + +To experiment with this, create a new Sprite Effect called `3dEffect` in the _MonoGameLibrary_'s shared content effects folder. + +| ![Figure 7-3: Adding the `3dEffect` to MGCB](./images/mgcb.png) | +| :----------------------------------------------------------------------------------------: | +| **Figure 7-3: Adding the `3dEffect` to MGCB** | + +Follow along with the steps to set up the effect. + +1. We need to add a vertex shader function. To do that, we need to add a new `struct` that holds all the input semantics passed from `SpriteBatch`: + + [!code-hlsl[](./snippets/snippet-7-09.hlsl)] + + > [!TIP] + > Use a struct for inputs and outputs. + > + > The default vertex shader accepts all 3 inputs (`position`, `color`, and `texCoord`) as direct parameters. However, when you have more than 1 semantic, it is helpful to organize all of the inputs in a `struct`, it is simply best practice and avoids common mistakes with "magic numbers" or "random variables". + > + > If you look through other shaders, you may see shader functions using individual parameters which works, but it is not a good practice. + +2. Now add the stub for the vertex shader function: + + [!code-hlsl[](./snippets/snippet-7-10.hlsl)] + + > [!WARNING] + > Constructs and parameters **MUST** appear or be defined **BEFORE** they are used, so if you add the above function BEFORE the struct for the `VertexShaderOutput`, the struct will not be recognized. + > + > So be sure to place the function AFTER the struct and it work as intended. + +3. Now, modify the `technique` to _include_ the vertex shader function. Until now, the `MainVS()` function is just considered as any average function in your shader, and because it was not used by the `MainPS` pixel shader, it would be compiled out of the shader. + + > [!IMPORTANT] + > When you specify the `MainVS()` vertex shader function, you are overriding the default `SpriteBatch` vertex shader function: + + [!code-hlsl[](./snippets/snippet-7-11.hlsl?highlight=6)] + +4. The shader will not compile yet, because the `VertexShaderOutput` has not been completely initialized. We need to replicate the `MatrixTransform` step to convert the vertices from world-space to clip-space. + Add the `MatrixTransform` shader parameter: + + [!code-hlsl[](./snippets/snippet-7-12.hlsl)] + +5. And then assign all of the output semantics in the vertex shader, replacing the `MainVS` stub we added earlier: + + [!code-hlsl[](./snippets/snippet-7-13.hlsl)] + +6. To validate this is working, we should try to use the new effect. For now, we will experiment in the `TitleScene` class in the `DungeonSlime` project. Create a class member for the new `Material`: + + [!code-csharp[](./snippets/snippet-7-14.cs)] + +7. Load the shader using the hot reload system in the `LoadContent` method: + + [!code-csharp[](./snippets/snippet-7-15.cs)] + +8. Add the using statement to the `MonoGame.Content` namespace at the top of the class, to access where the `WatchMaterial` extension is defined: + + ```csharp + using MonoGameLibrary.Content; + ``` + +10. Make sure to enable the hot-reload for the shader by adding this to the `Update` method: + + ```csharp + // Enable hot reload + _3dMaterial.Update(); + ``` + +10. Then, in the `Draw` method, use the effect when drawing the title text: + + [!code-csharp[](./snippets/snippet-7-16.cs?highlight=4)] + +11. When the game runs, the text will be missing because we never created a projection matrix to assign to the `MatrixTransform` shader parameter for the new effect (remember, we are overriding the default `SpriteBatch` behaviour with our own implementation). + + | ![Figure 7-4: The main menu title is missing](./images/missing-text.png) | + | :----------------------------------------------------------------------------------------: | + | **Figure 7-4: The main menu title is missing** | + +12. Replace the original `_3dMaterial` parameter initialization with the following code when loading the material in the `LoadContent` method: + + [!code-csharp[](./snippets/snippet-7-17.cs)] + +13. And now you should see the text normally again. + +| ![Figure 7-5: The main menu, but rendered with a custom vertex shader](./images/basic.png) | +| :----------------------------------------------------------------------------------------: | +| **Figure 7-5: The main menu, but rendered with a custom vertex shader** | + +### Making it Move + +As a quick experiment, we can show that the vertex shader can indeed modify the vertex positions further if we want to. For now, add a temporary shader parameter called `DebugOffset` in the `3dEffect.fx` shader: + +[!code-hlsl[](./snippets/snippet-7-18.hlsl)] + +And change the vertex shader (`MainVS`), add the `DebugOffset` to the `output.Position` after the clip-space conversion: + +[!code-hlsl[](./snippets/snippet-7-19.hlsl?highlight=7)] + +The sprites now move around as we adjust the shader parameter values. + +| ![Figure 7-6: We can control the vertex positions](./gifs/basic.gif) | +| :------------------------------------------------------------------: | +| **Figure 7-6: We can control the vertex positions** | + +It is important to build intuition for the different coordinate systems involved. Instead of adding the `DebugOffset` _after_ the clip-space conversion, if you try to add it _before_, like in the code below: + +[!code-hlsl[](./snippets/snippet-7-20.hlsl?highlight=6-8)] + +Then you will not see much movement at all. This is because the `DebugOffset` values only go from `0` to `1`, and in world space, this really only amounts to a single pixel. In fact, exactly how much an addition of _`1`_ happens to make is entirely defined _by_ the conversion to clip-space. The `projection` matrix we created treats world space coordinates with an origin around the screen's center, where 1 unit maps to 1 pixel. + +> [!NOTE] +> Sometimes this is exactly what you want, and sometimes it can be just confusing. The important thing to remember is which coordinate space you are doing your math in. + +| ![Figure 7-7: Changing coordinates before clip-space conversion](./gifs/basic-2.gif) | +| :----------------------------------------------------------------------------------: | +| **Figure 7-7: Changing coordinates before clip-space conversion** | + +### Perspective Projection + +The world-space vertices can have their `x` and `y` values modified in the vertex shader, but what about the `z` component? The orthographic projection essentially _ignores_ the `z` component of a vertex and treats all vertices as though they are an equal distance away from the camera. If you change the `z` value, you may _expect_ the sprite to appear closer or further away from the camera, but the orthographic projection matrix prevents that from happening. + +To check, try to modify the shader code to adjust the `z` value based on one of the debug values: + +[!code-hlsl[](./snippets/snippet-7-21.hlsl?highlight=7)] + +> [!TIP] +> Near and Far plane clipping. +> +> Keep in mind that if you modify the `z` value _too_ much, it will likely step outside of the near and far planes of the orthographic projection matrix. If this happens, the sprite will vanish, because it the projection matrix does not handle coordinates outside of the near and far planes. The value must be between the near and far plane of the matrix we created a few steps ago. We set the values in the `CreateOrthographicOffCenter()` function to `0` and `1`. +> +> [!code-csharp[](./snippets/snippet-7-22.cs)] +> + +Nothing happens! + +To fix this, we need to use a _perspective_ projection matrix instead of an orthographic projection matrix. MonoGame has a built in method called `Matrix.CreatePerspectiveFieldOfView()` that will do most of the heavy lifting for us. Once we have a perspective matrix, it would also be helpful to control _where_ the camera is looking. The math is easy, but it would be helpful to put it in a new helper class. + +1. Create a new file in the _MonoGameLibrary_'s graphics folder called `SpriteCamera3d.cs`, and paste the following code. We are going to skip over the math internals: + + [!code-csharp[](./snippets/snippet-7-23.cs)] + +2. Now, instead of creating an orthographic matrix in the `TitleScene`, we can use the new class (getting rid of the old dusty `CreateOrthographicOffCenter` function): + + [!code-csharp[](./snippets/snippet-7-24.cs?highlight=9-10)] + +3. Moving the `z` value uniformly in the shader will not be visually stimulating. A more impressive demonstration of the _perspective_ projection would be to rotate the vertices around the center of the sprite, back in thr `3dEffect.fx` shader, replace the `MainVS` function with the following: + + [!code-hlsl[](./snippets/snippet-7-25.hlsl?highlight)] + +And now when the debug `X` parameter is adjusted (y does nothing at this point), the text spins in a way that was not possible with the default `SpriteBatch` vertex shader. + +| ![Figure 7-8: A spinning text](./gifs/spin-1.gif) | +| :-----------------------------------------------: | +| **Figure 7-8: A spinning text** | + +The text disappears for half of the rotation. That happens because as the vertices are rotated, the triangle itself started to point _away_ from the camera. By default, `SpriteBatch` will cull any faces that point away from the camera. + +Change the `rasterizerState` to `CullNone` when beginning the sprite batch of the `TitleScreen`'s `Draw` method: + +[!code-csharp[](./snippets/snippet-7-26.cs?highlight=4)] + +And voilà, the text no longer disappears on its flip side. + +| ![Figure 7-9: A spinning text with reverse sides](./gifs/spin-2.gif) | +| :------------------------------------------------------------------: | +| **Figure 7-9: A spinning text with reverse sides** | + +> [!NOTE] +> What is _Culling_? +> +> The term, "Culling", is used to describe a scenario when some triangles are not drawn due to some sort of optimization. There are many _types_ of culling, but in this case, we are discussing a specific type of optimization called "Back-face Culling". Learn more about it on [wikipedia](https://en.wikipedia.org/wiki/Back-face_culling). + +You may find that the field of view is too high for your taste. Try lowering the field of view to 60 (in the `SpriteCamera3d.cs` properties), and you will see something similar to this, + +| ![Figure 7-10: A spinning text with reverse sides with smaller fov](./gifs/spin-3.gif) | +| :-----------------------------------------------------------------------------------: | +| **Figure 7-10: A spinning text with reverse sides with smaller fov** | + +As a final touch, we should remove the hard-coded `screenSize` variable from the shader, and extract it as a shader parameter. While we are at it, clean up and remove the debug parameters as well. + +1. Remove the old hardcoded/debug variables from the `3dEffect.fx` shader: + + [!code-hlsl[](./snippets/snippet-7-27-remove.hlsl)] + +2. Add the following two parameters so we can do things more dynamically: + + [!code-hlsl[](./snippets/snippet-7-27.hlsl)] + +3. Update the `MainVS` function to utilize the two new shader parameters: + + [!code-hlsl[](./snippets/snippet-7-27-add.hlsl?highlight=7-12)] + +4. Then back in the `TitleScreen.cs` `LoadContent` method, make sure to set the new `ScreenSize` parameter correctly from C#: + + [!code-csharp[](./snippets/snippet-7-28.cs?highlight=3)] + +5. And instead of manually controlling the spin angle, we can make the title spin gentle following the mouse position. In the `Update()` function, add the following snippet: + + [!code-csharp[](./snippets/snippet-7-29.cs?highlight=3-7)] + +| ![Figure 7-11: Spin controlled by the mouse](./gifs/spin-4.gif) | +| :------------------------------------------------------------: | +| **Figure 7-11: Spin controlled by the mouse** | + +Now when ever you move your Mouse left to right, the title will spin accordingly. + +## Applying it to the Game + +It was helpful to use the `TitleScene` to build intuition for the vertex shader, but now it is time to apply the perspective vertex shader to the game itself to add immersion and a sense of depth to the gameplay. The goal is to use the same effect in the `GameScene`. + +### Making One Big Shader + +A problem emerges right away. The `GameScene` is already using the color swapping effect to draw the sprites, and `SpriteBatch` can only use a single shader per batch. + +To solve this problem, we will collapse our shaders into a single shader that does it all, _both_ the color swapping _and_ the vertex manipulation. Writing code to be re-usable is a challenge for all programming languages, and shader languagess are no different. + +> [!NOTE] +> The _Uber_ Shader +> +> Sometimes when people collapse lots of shaders into a single shader, it is called an _"Uber"_ shader. For _Dungeon Slime_, that term is premature, but the spirit is the same. +> Finding ways to collapse code into simpler code pathways is helpful for keeping your code easy to understand, but it can also keep your shader's compilation times faster, and runtime performance higher. +> +> For an insightful read, check out this article on [shader permutations](https://therealmjp.github.io/posts/shader-permutations-part1/). + +MonoGame shaders can reference code from multiple files by using the `#include` syntax. MonoGame itself [uses](https://github.com/MonoGame/MonoGame/blob/develop/MonoGame.Framework/Platform/Graphics/Effect/Resources/SpriteEffect.fx#L8) this technique itself in the default vertex shader for `SpriteBatch`. We can move some of the code from our existing `.fx` files into a _new_ `.fxh` file, re-write the existing shaders to `#include` the new `.fxh` file, and then be able to write additional `.fx` files that `#include` multiple of our files and compose the functions into a single effect. + +> [!TIP] +> `.fxh` vs `.fx`. +> +> `.fxh` is purely convention. Technically you can use whatever file extension you want, but `.fxh` implies the usage of the file is for shared code, and does not contain a standalone effect itself. The `h` is simply referred to as a `header` file. + +Follow the steps below to refactor the shader code, and to use the `#include` syntax for referring to the new `.fxh` files. + +1. Before we get started, we are going to be editing `.fxh` files, so it would be nice if the hot-reload system also listened to these `.fxh` file changes. Update the `Watch` configuration in the `DungeonSlime.csproj` file to include the `.fxh` file type: + + [!code-xml[](./snippets/snippet-7-30.xml?highlight=3)] + +2. Time to start factoring out some shared components into a few different `.fxh` files. + + Create a file in the _MonoGameLibrary_'s `SharedContent/effects` folder called `common.fxh`. + + > [!TIP] + > `.fxh` files do not be added to your MonoGame Content Builder file. + + This file will contain utilities that can be shared for all effects, such as the `struct` types that define the inputs and outputs of the vertex and pixel shaders: + + [!code-hlsl[](./snippets/snippet-7-31.hlsl)] + + >[!tip] + > Include Guards. + > + > The `#include` syntax is taking the referenced file and inserting it into the code. If the same file was included twice, then the contents that file would be written out as code _twice_. Defining a `struct` or function this way would cause the compiler to fail, because the `struct` would be declared twice, which is illegal. + > + > To work around this, _a_ solution is to use a practice called "include guards", where the file itself defines a symbol (in the case above, the symbol is `COMMON`). The file only compiles to anything if the symbol has not yet been defined. The `#ifndef` stands for "if not yet defined". + > + > Once the `COMMON` symbol is defined once, any future inclusions of the file will not match the `#ifndef` clause. + +3. Then, in the `3dEffect.fx` file, remove the `VertexShaderInput` and `VertexShaderOutput` structs and replace them with this line: + + [!code-hlsl[](./snippets/snippet-7-32.hlsl)] + + > [!NOTE] + > If you recall what was stated earlier about the processing order of the shader, the file is read in sequence, so the include MUST be defined BEFORE its contents are used. You can put it at the top of the file (like a using) but only if it does not interfere with the shader processing. There is no hard and fast rule, so just use common sense, if it does not compile or it errors, then you need to change it. + +4. If you run the game, nothing should change, except that the shader code is more modular. To continue, create another header file next to the `3dEffect.fx` shader called `3dEffect.fxh` in the same folder. + Paste the contents: + + [!code-hlsl[](./snippets/snippet-7-33.hlsl)] + +5. Now in the `3dEffect.fx`, instead of `#include common.fxh`, we can directly reference `3dEffect.fxh` instead. We should also remove the code that was just pasted into the new common header file. Here is the slimmed down `3dEffect.fx` file (much cleaner): + + [!code-hlsl[](./snippets/snippet-7-34.hlsl)] + +6. It is time to do the same thing for the `colorSwapEffect.fx` file. The goal is to split the file apart into a header file that defines the components of the effect, and leave the `fx` file itself without much _implementation_. Create a new file called `colors.fxh`, and paste the following: + + [!code-hlsl[](./snippets/snippet-7-35.hlsl)] + + > [!NOTE] + > Ignore any red squiggles in the editor, this header is not being written to run independently like the `3dEffect.fxh` header, it is just a code template to be used in the main shader. How you break up your shader code is always a preference. + +7. Then, the `colorSwapEffect.fx` file can be replaced with the following code: + + [!code-hlsl[](./snippets/snippet-7-36.hlsl)] + + Now most of the components we would like to combine into a single effect have been split into various `.fxh` header files, but their relative location is **CRUCIAL** when refering to related functionality, to demonstrate this, we will "break" a shader and show how to fix it. + +8. Create a new "sprite effect" using the MGCB editor in the **`_DungeonSlime_`'s** content `effects` folder called `gameEffect.fx`, and simply add `#include "common.fxh"` to refer to the previously created common header file, you will see an error like this: + + > [!WARNING] + > We have been adding a lot of files to the _MonoGameLibrary_, but this shader should go into the _DungeonSlime_ project, because it is a game specific shader. + + ```text + error PREPROCESS01: File not found: common.fxh in .(MonoGame.Effect.Preprocessor+MGFile) + ``` + + This happens because the `gameEffect.fx` file is in a different folder than the `common.fxh` file, and the `"common.fxh"` is treated as a relative _file path_ lookup.  + Instead, in the `gameEffect.fx` file, use this line: + + [!code-hlsl[](./snippets/snippet-7-37.hlsl)] + + > [!NOTE] + > You should see errors in the editor or when building the shader now because the `common.fxh` already contains the `VertexSHaderOutput` struct, hence the version in `gameeffect.fx` is now a duplicate. + +9. `gameEffect.fx` file can also reference the other two `.fxh` files we just created, so now add the following additional includes: + + [!code-hlsl[](./snippets/snippet-7-38.hlsl)] + + > [!NOTE] + > These replace the `common.fxh` you just added for test, as the other effect headers already reference `common.fxh`, even shaders have inheritance. You "could" still include it because the `fxh` files use the `#ifndef` check, meaning it will ignore duplicates, but why make it more complicated? + +10. And the only thing the `gameEffect.fx` file needs to specify is which functions to use for the vertex shader and pixel shader functions: + + [!code-hlsl[](./snippets/snippet-7-39.hlsl)] + + To keep things simple, the entire contents of the `gameEffect.fx` is shown below: + + [!code-hlsl[](./snippets/snippet-7-40.hlsl)] + +11. To load it into the `GameScene`, we need to _delete_ the old class member for `_colorSwapMaterial`, and add a new one, as well as adding a `SpriteCamera3d` definition: + + [!code-csharp[](./snippets/snippet-7-41.cs)] + +12. Finally, apply all of the parameters to the new single material in the `LoadContent` method: + + [!code-csharp[](./snippets/snippet-7-42.cs)] + +13. Somewhat optionally, remember to add the `rasterizerState: RasterizerState.CullNone` to the `SpriteBatch.Draw()` call if you do not want the game to vanish when the `SpinAmount` goes beyond half a rotation. In practice, we will not be be spinning the game world that much, so it does not really matter. + +Any remaining places where the old `_colorSwapMaterial` is being referenced should be changed to use the `_gameMaterial` instead, including in the `Update` and `Draw` methods. (if you still have any old references to the `_grayscaleEffect` make sure to remove those as well as they are no longer used). + +Now, if you run the game, the color swap controls are still visible, but we can also manually control the tilt of the map. + +| ![Figure 7-12: All of the effects in one](./gifs/uber.gif) | +| :-------------------------------------------------------: | +| **Figure 7-12: All of the effects in one** | + +### Adjusting the Game + +Now that the 3d effect can be applied to the game objects, it would be good to make the world tilt slightly towards the player character to give the movement more weight. Instead of spinning the entire map, an easier approach will be to modify the `MatrixTransform` that is being passed to the shader. + +Add this snippet to the top of the `GameScene`'s `Update()` method: + +[!code-csharp[](./snippets/snippet-7-43.cs?highlight=6-12)] + +Now the project should compile and give you the following effect, pretty cool! + +| ![Figure 7-13: Camera follows the slime](./gifs/cam-follow.gif) | +| :------------------------------------------------------------: | +| **Figure 7-13: Camera follows the slime** | + +The clear color of the scene can be seen in the corners (the `CornflowerBlue`). Pick whatever clear color you think looks good for the color swapping: + +[!code-csharp[](./snippets/snippet-7-44.cs)] + +And to finish this chapter, the game looks like this, + +| ![Figure 7-14: vertex shaders make it pop](./gifs/final.gif) | +| :---------------------------------------------------------: | +| **Figure 7-14: vertex shaders make it pop** | + +## Conclusion + +Our game has a whole new dimension! In this chapter, you accomplished the following: + +- Learned the difference between a vertex shader and a pixel shader. +- Wrote a custom vertex shader to override the `SpriteBatch` default. +- Replaced the default orthographic projection with a perspective projection to create a 3D effect. +- Refactored shader logic into modular `.fxh` header files for better organization. +- Combined vertex and pixel shader effects into a single "uber shader". + +The world feels much more alive now that it tilts and moves with the player. In the next chapter, we will build on this sense of depth by tackling a 2D dynamic lighting system. + +You can find the complete code sample for this chapter, [here](https://github.com/MonoGame/MonoGame.Samples/tree/3.8.4/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect). + +Continue to the next chapter, [Chapter 08: Light Effect](../08_light_effect/index.md) diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-01.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-01.hlsl new file mode 100644 index 00000000..3d272f21 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-01.hlsl @@ -0,0 +1,23 @@ +// ... + +float4x4 MatrixTransform _vs(c0) _cb(c0); + +// ... + +struct VSOutput +{ + float4 position : SV_Position; + float4 color : COLOR0; + float2 texCoord : TEXCOORD0; +}; + +VSOutput SpriteVertexShader( float4 position : POSITION0, + float4 color : COLOR0, + float2 texCoord : TEXCOORD0) +{ + VSOutput output; + output.position = mul(position, MatrixTransform); + output.color = color; + output.texCoord = texCoord; + return output; +} diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-02.cs b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-02.cs new file mode 100644 index 00000000..6134252a --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-02.cs @@ -0,0 +1,4 @@ +public VertexPositionColorTexture vertexTL; +public VertexPositionColorTexture vertexTR; +public VertexPositionColorTexture vertexBL; +public VertexPositionColorTexture vertexBR; diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-03.cs b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-03.cs new file mode 100644 index 00000000..28b6fb97 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-03.cs @@ -0,0 +1,10 @@ +static VertexPositionColorTexture() +{ + var elements = new VertexElement[] + { + new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), + new VertexElement(12, VertexElementFormat.Color, VertexElementUsage.Color, 0), + new VertexElement(16, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0) + }; + VertexDeclaration = new VertexDeclaration(elements); +} diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-04.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-04.hlsl new file mode 100644 index 00000000..94f4d166 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-04.hlsl @@ -0,0 +1,10 @@ +// ... + +VSOutput SpriteVertexShader(float4 position : POSITION0, + float4 color : COLOR0, + float2 texCoord : TEXCOORD0) +{ + // ... +} + +// ... diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-05.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-05.hlsl new file mode 100644 index 00000000..a64ab547 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-05.hlsl @@ -0,0 +1,6 @@ +struct VSOutput +{ + float4 position : SV_Position; + float4 color : COLOR0; + float2 texCoord : TEXCOORD0; +}; diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-06.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-06.hlsl new file mode 100644 index 00000000..ade34295 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-06.hlsl @@ -0,0 +1,6 @@ +struct VertexShaderOutput +{ + float4 Position : SV_POSITION; + float4 Color : COLOR0; + float2 TextureCoordinates : TEXCOORD0; +}; diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-07.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-07.hlsl new file mode 100644 index 00000000..5f58a56f --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-07.hlsl @@ -0,0 +1,14 @@ +// ... + +VSOutput SpriteVertexShader( float4 position : POSITION0, + float4 color : COLOR0, + float2 texCoord : TEXCOORD0) +{ + VSOutput output; + output.position = mul(position, MatrixTransform); + output.color = color; + output.texCoord = texCoord; + return output; +} + +// ... diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-08.cs b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-08.cs new file mode 100644 index 00000000..37bc9d10 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-08.cs @@ -0,0 +1,12 @@ +// cache the shader parameter for the MatrixTransform +_matrixParam = Parameters["MatrixTransform"]; + +// ... some code left out for readability + +// create a projection matrix in the _projection variable +Matrix.CreateOrthographicOffCenter(0, vp.Width, vp.Height, 0, 0, -1, out _projection); + +// ... some code left out for readability + +// assign the projection matrix to the MatrixTransform +_matrixParam.SetValue(_projection); diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-09.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-09.hlsl new file mode 100644 index 00000000..0c26d5d6 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-09.hlsl @@ -0,0 +1,6 @@ +struct VertexShaderInput +{ + float4 Position : POSITION0; + float4 Color : COLOR0; + float2 TexCoord : TEXCOORD0; +}; diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-10.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-10.hlsl new file mode 100644 index 00000000..bb31447b --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-10.hlsl @@ -0,0 +1,6 @@ +VertexShaderOutput MainVS(VertexShaderInput input) +{ + VertexShaderOutput output; + + return output; +} diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-11.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-11.hlsl new file mode 100644 index 00000000..3c47f82f --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-11.hlsl @@ -0,0 +1,8 @@ +technique SpriteDrawing +{ + pass P0 + { + PixelShader = compile PS_SHADERMODEL MainPS(); + VertexShader = compile VS_SHADERMODEL MainVS(); + } +}; diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-12.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-12.hlsl new file mode 100644 index 00000000..7ea5f3b3 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-12.hlsl @@ -0,0 +1,5 @@ +// ... + +float4x4 MatrixTransform; + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-13.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-13.hlsl new file mode 100644 index 00000000..9a1f8307 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-13.hlsl @@ -0,0 +1,7 @@ +VertexShaderOutput MainVS(VertexShaderInput input) { + VertexShaderOutput output; + output.Position = mul(input.Position, MatrixTransform); + output.Color = input.Color; + output.TextureCoordinates = input.TexCoord; + return output; +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-14.cs b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-14.cs new file mode 100644 index 00000000..bdad9216 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-14.cs @@ -0,0 +1,2 @@ +// The 3d material +private Material _3dMaterial; diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-15.cs b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-15.cs new file mode 100644 index 00000000..5295b003 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-15.cs @@ -0,0 +1,2 @@ +// Load the 3d effect +_3dMaterial = Core.SharedContent.WatchMaterial("effects/3dEffect"); diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-16.cs b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-16.cs new file mode 100644 index 00000000..6e45a47e --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-16.cs @@ -0,0 +1,4 @@ +// Begin the sprite batch to prepare for rendering. +Core.SpriteBatch.Begin( + samplerState: SamplerState.PointClamp, + effect: _3dMaterial.Effect); diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-17.cs b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-17.cs new file mode 100644 index 00000000..90c00558 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-17.cs @@ -0,0 +1,14 @@ +// Load the 3d effect +_3dMaterial = Core.SharedContent.WatchMaterial("effects/3dEffect"); +_3dMaterial.IsDebugVisible = true; + +// this matrix code is taken from the default vertex shader code. +Matrix.CreateOrthographicOffCenter( + left: 0, + right: Core.GraphicsDevice.Viewport.Width, + bottom: Core.GraphicsDevice.Viewport.Height, + top: 0, + zNearPlane: 0, + zFarPlane: -1, + out var projection); +_3dMaterial.SetParameter("MatrixTransform", projection); diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-18.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-18.hlsl new file mode 100644 index 00000000..8e6cfc44 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-18.hlsl @@ -0,0 +1,5 @@ +// ... + +float2 DebugOffset; + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-19.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-19.hlsl new file mode 100644 index 00000000..8ab79fc1 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-19.hlsl @@ -0,0 +1,13 @@ +// ... + +VertexShaderOutput MainVS(VertexShaderInput input) +{ + VertexShaderOutput output; + output.Position = mul(input.Position, MatrixTransform); + output.Position.xy += DebugOffset; + output.Color = input.Color; + output.TextureCoordinates = input.TexCoord; + return output; +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-20.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-20.hlsl new file mode 100644 index 00000000..78a787ad --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-20.hlsl @@ -0,0 +1,14 @@ +// ... + +VertexShaderOutput MainVS(VertexShaderInput input) +{ + VertexShaderOutput output; + float4 pos = input.Position; + pos.xy += DebugOffset; + output.Position = mul(pos, MatrixTransform); + output.Color = input.Color; + output.TextureCoordinates = input.TexCoord; + return output; +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-21.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-21.hlsl new file mode 100644 index 00000000..fe95eff7 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-21.hlsl @@ -0,0 +1,14 @@ +// ... + +VertexShaderOutput MainVS(VertexShaderInput input) +{ + VertexShaderOutput output; + float4 pos = input.Position; + pos.z -= DebugOffset.x; + output.Position = mul(pos, MatrixTransform); + output.Color = input.Color; + output.TextureCoordinates = input.TexCoord; + return output; +} + +// ... \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-22.cs b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-22.cs new file mode 100644 index 00000000..8358050f --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-22.cs @@ -0,0 +1 @@ +zNearPlane: 0, zFarPlane: -1 diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-23.cs b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-23.cs new file mode 100644 index 00000000..72e7c16d --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-23.cs @@ -0,0 +1,51 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGameLibrary.Graphics; + +public class SpriteCamera3d +{ + /// + /// The field of view for the camera. + /// + public int Fov { get; set; } = 120; + + /// + /// By default, the camera is looking at the center of the screen. + /// This offset value can be used to "turn" the camera from the center towards the given vector value. + /// + public Vector2 LookOffset { get; set; } = Vector2.Zero; + + /// + /// Produce a matrix that will transform world-space coordinates into clip-space coordinates. + /// + /// + public Matrix CalculateMatrixTransform() + { + var viewport = Core.GraphicsDevice.Viewport; + + // start by creating the projection matrix + var projection = Matrix.CreatePerspectiveFieldOfView( + fieldOfView: MathHelper.ToRadians(Fov), + aspectRatio: Core.GraphicsDevice.Viewport.AspectRatio, + nearPlaneDistance: 0.0001f, + farPlaneDistance: 10000f + ); + + // position the camera far enough away to see the entire contents of the screen + var cameraZ = (viewport.Height * 0.5f) / (float)Math.Tan(MathHelper.ToRadians(Fov) * 0.5f); + + // create a view that is centered on the screen + var center = .5f * new Vector2(viewport.Width, viewport.Height); + var look = center + LookOffset; + var view = Matrix.CreateLookAt( + cameraPosition: new Vector3(center.X, center.Y, -cameraZ), + cameraTarget: new Vector3(look.X, look.Y, 0), + cameraUpVector: Vector3.Down + ); + + // the standard matrix format is world*view*projection, + // but given that we are skipping the world matrix, its just view*projection + return view * projection; + } +} diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-24.cs b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-24.cs new file mode 100644 index 00000000..7588d1c8 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-24.cs @@ -0,0 +1,11 @@ +public override void LoadContent() +{ + // ... + + // Load the 3d effect + _3dMaterial = Core.SharedContent.WatchMaterial("effects/3dEffect"); + _3dMaterial.IsDebugVisible = true; + + var camera = new SpriteCamera3d(); + _3dMaterial.SetParameter("MatrixTransform", camera.CalculateMatrixTransform()); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-25.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-25.hlsl new file mode 100644 index 00000000..4e25fc40 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-25.hlsl @@ -0,0 +1,39 @@ +VertexShaderOutput MainVS(VertexShaderInput input) +{ + VertexShaderOutput output; + + float4 pos = input.Position; + + // hardcode the screen-size for now. + float2 screenSize = float2(1280, 720); + + // create the center of rotation + float2 centerXZ = float2(screenSize.x * .5, 0); + + // convert the debug variable into an angle from 0 to 2 pi. + // shaders use radians for angles, so 2 pi = 360 degrees + float angle = DebugOffset.x * 6.28; + + // pre-compute the cos and sin of the angle + float cosA = cos(angle); + float sinA = sin(angle); + + // shift the position to the center of rotation + pos.xz -= centerXZ; + + // compute the rotation + float nextX = pos.x * cosA - pos.z * sinA; + float nextZ = pos.x * sinA + pos.z * cosA; + + // apply the rotation + pos.x = nextX; + pos.z = nextZ; + + // shift the position away from the center of rotation + pos.xz += centerXZ; + + output.Position = mul(pos, MatrixTransform); + output.Color = input.Color; + output.TextureCoordinates = input.TexCoord; + return output; +} diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-26.cs b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-26.cs new file mode 100644 index 00000000..dc0962c0 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-26.cs @@ -0,0 +1,5 @@ +// Begin the sprite batch to prepare for rendering. +Core.SpriteBatch.Begin( + samplerState: SamplerState.PointClamp, + rasterizerState: RasterizerState.CullNone, + effect: _3dMaterial.Effect); diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-27-add.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-27-add.hlsl new file mode 100644 index 00000000..fd4ea750 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-27-add.hlsl @@ -0,0 +1,14 @@ +VertexShaderOutput MainVS(VertexShaderInput input) { + VertexShaderOutput output; + + float4 pos = input.Position; + + // create the center of rotation + float2 centerXZ = float2(ScreenSize.x * .5, 0); + + // convert the debug variable into an angle from 0 to 2 pi. + // shaders use radians for angles, so 2 pi = 360 degrees + float angle = SpinAmount * 6.28; + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-27-remove.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-27-remove.hlsl new file mode 100644 index 00000000..94b5e179 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-27-remove.hlsl @@ -0,0 +1,14 @@ +// ... + +float2 DebugOffset; // remove this line + +// ... + +VertexShaderOutput MainVS(VertexShaderInput input) { + // ... + + // hardcode the screen-size for now. + float2 screenSize = float2(1280, 720); // remove this line (and comment) + + // ... +} diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-27.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-27.hlsl new file mode 100644 index 00000000..f4692ef2 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-27.hlsl @@ -0,0 +1,7 @@ +// ... + +float2 ScreenSize; +float SpinAmount; + +// ... + diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-28.cs b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-28.cs new file mode 100644 index 00000000..8912fb48 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-28.cs @@ -0,0 +1,3 @@ +var camera = new SpriteCamera3d(); +_3dMaterial.SetParameter("MatrixTransform", camera.CalculateMatrixTransform()); +_3dMaterial.SetParameter("ScreenSize", new Vector2(Core.GraphicsDevice.Viewport.Width, Core.GraphicsDevice.Viewport.Height)); \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-29.cs b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-29.cs new file mode 100644 index 00000000..a6ba6fee --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-29.cs @@ -0,0 +1,10 @@ +public override void Update(GameTime gameTime) +{ + _3dMaterial.Update(); + + var spinAmount = Core.Input.Mouse.X / (float)Core.GraphicsDevice.Viewport.Width; + spinAmount = MathHelper.SmoothStep(-.1f, .1f, spinAmount); + _3dMaterial.SetParameter("SpinAmount", spinAmount); + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-30.xml b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-30.xml new file mode 100644 index 00000000..53950bf6 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-30.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-31.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-31.hlsl new file mode 100644 index 00000000..acec35aa --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-31.hlsl @@ -0,0 +1,15 @@ +#ifndef COMMON +#define COMMON + +struct VertexShaderInput { + float4 Position : POSITION0; + float4 Color : COLOR0; + float2 TexCoord : TEXCOORD0; +}; + +struct VertexShaderOutput { + float4 Position : SV_POSITION; + float4 Color : COLOR0; + float2 TextureCoordinates : TEXCOORD0; +}; +#endif diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-32.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-32.hlsl new file mode 100644 index 00000000..49c60d92 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-32.hlsl @@ -0,0 +1 @@ +#include "common.fxh" diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-33.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-33.hlsl new file mode 100644 index 00000000..58a3cb9c --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-33.hlsl @@ -0,0 +1,45 @@ +#ifndef EFFECT_3DEFFECT +#define EFFECT_3DEFFECT +#include "common.fxh" + +float4x4 MatrixTransform; +float2 ScreenSize; +float SpinAmount; + +VertexShaderOutput MainVS(VertexShaderInput input) +{ + VertexShaderOutput output; + + float4 pos = input.Position; + + // create the center of rotation + float2 centerXZ = float2(ScreenSize.x * .5, 0); + + // convert the debug variable into an angle from 0 to 2 pi. + // shaders use radians for angles, so 2 pi = 360 degrees + float angle = SpinAmount * 6.28; + + // pre-compute the cos and sin of the angle + float cosA = cos(angle); + float sinA = sin(angle); + + // shift the position to the center of rotation + pos.xz -= centerXZ; + + // compute the rotation + float nextX = pos.x * cosA - pos.z * sinA; + float nextZ = pos.x * sinA + pos.z * cosA; + + // apply the rotation + pos.x = nextX; + pos.z = nextZ; + + // shift the position away from the center of rotation + pos.xz += centerXZ; + + output.Position = mul(pos, MatrixTransform); + output.Color = input.Color; + output.TextureCoordinates = input.TexCoord; + return output; +} +#endif diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-34.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-34.hlsl new file mode 100644 index 00000000..e7222292 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-34.hlsl @@ -0,0 +1,31 @@ +#if OPENGL + #define SV_POSITION POSITION + #define VS_SHADERMODEL vs_3_0 + #define PS_SHADERMODEL ps_3_0 +#else + #define VS_SHADERMODEL vs_4_0_level_9_1 + #define PS_SHADERMODEL ps_4_0_level_9_1 +#endif + +Texture2D SpriteTexture; + +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + +#include "3dEffect.fxh" + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + return tex2D(SpriteTextureSampler,input.TextureCoordinates) * input.Color; +} + +technique SpriteDrawing +{ + pass P0 + { + PixelShader = compile PS_SHADERMODEL MainPS(); + VertexShader = compile VS_SHADERMODEL MainVS(); + } +}; diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-35.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-35.hlsl new file mode 100644 index 00000000..a47eec43 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-35.hlsl @@ -0,0 +1,69 @@ +#ifndef COLORS +#define COLORS + +#include "common.fxh" + +// the custom color map passed to the Material.SetParameter() +Texture2D ColorMap; +sampler2D ColorMapSampler = sampler_state +{ + Texture = ; + MinFilter = Point; + MagFilter = Point; + MipFilter = Point; + AddressU = Clamp; + AddressV = Clamp; +}; + +// a control variable to lerp between original color and swapped color +float OriginalAmount; +float Saturation; + +float4 Grayscale(float4 color) +{ + // Calculate the grayscale value based on human perception of colors + float grayscale = dot(color.rgb, float3(0.3, 0.59, 0.11)); + + // create a grayscale color vector (same value for R, G, and B) + float3 grayscaleColor = float3(grayscale, grayscale, grayscale); + + // Linear interpolation between he grayscale color and the original color's + // rgb values based on the saturation parameter. + float3 finalColor = lerp(grayscale, color.rgb, Saturation); + + // Return the final color with the original alpha value + return float4(finalColor, color.a); +} + +float4 SwapColors(float4 color) +{ + // produce the key location + // note the x-offset by half a texel solves rounding errors. + float2 keyUv = float2(color.r , 0); + + // read the swap color value + float4 swappedColor = tex2D(ColorMapSampler, keyUv) * color.a; + + // ignore the swap if the map does not have a value + bool hasSwapColor = swappedColor.a > 0; + if (!hasSwapColor) + { + return color; + } + + // return the result color + return lerp(swappedColor, color, OriginalAmount); +} + +float4 ColorSwapPS(VertexShaderOutput input) : COLOR +{ + // read the original color value + float4 originalColor = tex2D(SpriteTextureSampler,input.TextureCoordinates); + + float4 swapped = SwapColors(originalColor); + float4 saturated = Grayscale(swapped); + + return saturated; +} + +#endif diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-36.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-36.hlsl new file mode 100644 index 00000000..2030a9a0 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-36.hlsl @@ -0,0 +1,25 @@ +#if OPENGL + #define SV_POSITION POSITION + #define VS_SHADERMODEL vs_3_0 + #define PS_SHADERMODEL ps_3_0 +#else + #define VS_SHADERMODEL vs_4_0_level_9_1 + #define PS_SHADERMODEL ps_4_0_level_9_1 +#endif + +// the main Sprite texture passed to SpriteBatch.Draw() +Texture2D SpriteTexture; +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + +#include "colors.fxh" + +technique SpriteDrawing +{ + pass P0 + { + PixelShader = compile PS_SHADERMODEL ColorSwapPS(); + } +}; diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-37.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-37.hlsl new file mode 100644 index 00000000..51ecffcd --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-37.hlsl @@ -0,0 +1 @@ +#include "../../../MonoGameLibrary/SharedContent/effects/common.fxh" diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-38.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-38.hlsl new file mode 100644 index 00000000..324660a3 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-38.hlsl @@ -0,0 +1,2 @@ +#include "../../../MonoGameLibrary/SharedContent/effects/3dEffect.fxh" +#include "../../../MonoGameLibrary/SharedContent/effects/colors.fxh" diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-39.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-39.hlsl new file mode 100644 index 00000000..d65d1cde --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-39.hlsl @@ -0,0 +1,8 @@ +technique SpriteDrawing +{ + pass P0 + { + VertexShader = compile VS_SHADERMODEL MainVS(); + PixelShader = compile PS_SHADERMODEL ColorSwapPS(); + } +}; diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-40.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-40.hlsl new file mode 100644 index 00000000..238a77fd --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-40.hlsl @@ -0,0 +1,27 @@ +#if OPENGL + #define SV_POSITION POSITION + #define VS_SHADERMODEL vs_3_0 + #define PS_SHADERMODEL ps_3_0 +#else + #define VS_SHADERMODEL vs_4_0_level_9_1 + #define PS_SHADERMODEL ps_4_0_level_9_1 +#endif + +Texture2D SpriteTexture; + +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + +#include "../../../MonoGameLibrary/SharedContent/effects/3dEffect.fxh" +#include "../../../MonoGameLibrary/SharedContent/effects/colors.fxh" + +technique SpriteDrawing +{ + pass P0 + { + VertexShader = compile VS_SHADERMODEL MainVS(); + PixelShader = compile PS_SHADERMODEL ColorSwapPS(); + } +}; diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-41.cs b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-41.cs new file mode 100644 index 00000000..9f86a584 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-41.cs @@ -0,0 +1,3 @@ +// The uber material for the game objects +private Material _gameMaterial; +private SpriteCamera3d _camera; \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-42.cs b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-42.cs new file mode 100644 index 00000000..d5661d45 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-42.cs @@ -0,0 +1,12 @@ +public override void LoadContent() +{ + // ... + + // Load the game material + _gameMaterial = Content.WatchMaterial("effects/gameEffect"); + _gameMaterial.IsDebugVisible = true; + _gameMaterial.SetParameter("ColorMap", _colorMap); + _camera = new SpriteCamera3d(); + _gameMaterial.SetParameter("MatrixTransform", _camera.CalculateMatrixTransform()); + _gameMaterial.SetParameter("ScreenSize", new Vector2(Core.GraphicsDevice.Viewport.Width, Core.GraphicsDevice.Viewport.Height)); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-43.cs b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-43.cs new file mode 100644 index 00000000..8be2e7fc --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-43.cs @@ -0,0 +1,15 @@ +public override void Update(GameTime gameTime) +{ + // Ensure the UI is always updated + _ui.Update(gameTime); + + // Set the camera view to look at the player slime + var viewport = Core.GraphicsDevice.Viewport; + var center = .5f * new Vector2(viewport.Width, viewport.Height); + var slimePosition = new Vector2(_slime?.GetBounds().X ?? center.X, _slime?.GetBounds().Y ?? center.Y); + var offset = .01f * (slimePosition - center); + _camera.LookOffset = offset; + _gameMaterial.SetParameter("MatrixTransform", _camera.CalculateMatrixTransform()); + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-44.cs b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-44.cs new file mode 100644 index 00000000..017ebfd7 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-44.cs @@ -0,0 +1,7 @@ +public override void Draw(GameTime gameTime) +{ + // Clear the back buffer. + Core.GraphicsDevice.Clear(new Color(32, 16, 20)); + + // ... +} diff --git a/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-sv.hlsl b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-sv.hlsl new file mode 100644 index 00000000..04d8bdb1 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/07_sprite_vertex_effect/snippets/snippet-7-sv.hlsl @@ -0,0 +1,8 @@ +#if OPENGL + #define SV_POSITION POSITION + #define VS_SHADERMODEL vs_3_0 + #define PS_SHADERMODEL ps_3_0 +#else + #define VS_SHADERMODEL vs_4_0_level_9_1 + #define PS_SHADERMODEL ps_4_0_level_9_1 +#endif \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/composite-ambient.gif b/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/composite-ambient.gif new file mode 100644 index 00000000..d2b0f7b7 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/composite-ambient.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/composite-light-no-vert.gif b/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/composite-light-no-vert.gif new file mode 100644 index 00000000..9cf6f9a9 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/composite-light-no-vert.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/final.gif b/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/final.gif new file mode 100644 index 00000000..b5a9d688 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/final.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/light-no-vertex.gif b/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/light-no-vertex.gif new file mode 100644 index 00000000..d1bdc382 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/light-no-vertex.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/normals.gif b/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/normals.gif new file mode 100644 index 00000000..af542082 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/normals.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/normals_demo_none.gif b/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/normals_demo_none.gif new file mode 100644 index 00000000..a15cd468 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/normals_demo_none.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/normals_demo_some.gif b/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/normals_demo_some.gif new file mode 100644 index 00000000..341c1374 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/normals_demo_some.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/point-light-brightness.gif b/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/point-light-brightness.gif new file mode 100644 index 00000000..86610a0f Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/point-light-brightness.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/point-light-range.gif b/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/point-light-range.gif new file mode 100644 index 00000000..8e33b8b9 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/point-light-range.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/point-light-sharpness.gif b/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/point-light-sharpness.gif new file mode 100644 index 00000000..2d076382 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/gifs/point-light-sharpness.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/images/atlas-normal.png b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/atlas-normal.png new file mode 100644 index 00000000..ae3ae78a Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/atlas-normal.png differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/images/atlas.png b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/atlas.png new file mode 100644 index 00000000..f7def20f Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/atlas.png differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/images/color-buffer.png b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/color-buffer.png new file mode 100644 index 00000000..c76a0a7d Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/color-buffer.png differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/images/composite-1.png b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/composite-1.png new file mode 100644 index 00000000..e3d2acd1 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/composite-1.png differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/images/light-broken.png b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/light-broken.png new file mode 100644 index 00000000..dec19be4 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/light-broken.png differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/images/light-buffer-1.png b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/light-buffer-1.png new file mode 100644 index 00000000..d1ab6fb9 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/light-buffer-1.png differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/images/light-buffer-2.png b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/light-buffer-2.png new file mode 100644 index 00000000..ebbc002c Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/light-buffer-2.png differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/images/light-normal.png b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/light-normal.png new file mode 100644 index 00000000..8faba456 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/light-normal.png differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/images/light-screen.png b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/light-screen.png new file mode 100644 index 00000000..907c0a64 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/light-screen.png differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/images/light-with-normal.png b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/light-with-normal.png new file mode 100644 index 00000000..dc7f58aa Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/light-with-normal.png differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/images/mgcb-dce.png b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/mgcb-dce.png new file mode 100644 index 00000000..bb646bc5 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/mgcb-dce.png differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/images/mgcb-ple.png b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/mgcb-ple.png new file mode 100644 index 00000000..0af2217f Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/mgcb-ple.png differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/images/normal-buffer-red.png b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/normal-buffer-red.png new file mode 100644 index 00000000..4b663746 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/normal-buffer-red.png differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/images/normal-buffer.png b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/normal-buffer.png new file mode 100644 index 00000000..3f66a646 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/normal-buffer.png differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/images/point-light-blue.png b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/point-light-blue.png new file mode 100644 index 00000000..7d146324 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/point-light-blue.png differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/images/point-light-dist.png b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/point-light-dist.png new file mode 100644 index 00000000..c5834e4e Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/point-light-dist.png differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/images/point-light-falloff-1.png b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/point-light-falloff-1.png new file mode 100644 index 00000000..4be8caa5 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/point-light-falloff-1.png differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/images/slime-normal.png b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/slime-normal.png new file mode 100644 index 00000000..1a2f40fd Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/08_light_effect/images/slime-normal.png differ diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/index.md b/articles/tutorials/advanced/2d_shaders/08_light_effect/index.md new file mode 100644 index 00000000..5245b734 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/index.md @@ -0,0 +1,640 @@ +--- +title: "Chapter 08: Light Effect" +description: "Add dynamic 2D lighting to the game" +--- + +In this chapter, we are going to add a dynamic 2D lighting system to _Dungeon Slime_. At the end of this chapter, the game will look something like this: + +| ![Figure 8-1: The game will have lighting](./gifs/final.gif) | +| :-------------------------------------------------: | +| **Figure 8-1: The game will have lighting** | + +So far, the game's rendering has been fairly straightforward. The game consists of a bunch of sprites, and all those sprites are drawn straight to the screen using a custom shader effect. Adding lights is going to complicate the rendering, because now each sprite must consider "_N_" number of lights before being drawn to the screen. + +There are two broad categories of strategies for rendering lights in a game, + +1. [_Forward_](https://en.wikipedia.org/wiki/Shading) rendering, and +2. [_Deferred_](https://en.wikipedia.org/wiki/Deferred_shading) rendering. + +In the earlier days of computer graphics, forward rendering was ubiquitous. Imagine a simple 2D scene where there is a single sprite with 3 lights nearby. The sprite would be rendered 3 times, once for each light. Each individual pass would layer any existing passes with the next light. This technique is forward rendering, and there are many optimizations that make it fast and efficient. However, in a scene with lots of objects and lots of lights, each object needs to be rendered for each light, and the amount of rendering can scale poorly. The amount of work the renderer needs to do is roughly proportional to the number of sprites (`S`) multiplied by the number of lights (`L`), or `S * L`. + +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`. + +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. + +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). + +## Adding Deferred Rendering + +Writing a simple deferred renderer can be worked out in a few steps, + +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. +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. +3. create yet another off-screen texture, called the light texture, where lights are layered on top of each other using the normal texture, +4. finally, create a rendering to the screen based on the lighting texture and the color texture. + +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. + +> [!TIP] +> _Texture_ vs _Map_ vs _Buffer_ +> +> 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. + +### Drawing to an off-screen texture + +1. To get started, we need to draw the main game sprites to an off-screen texture instead of directly to the screen. + + Create a new file in the shared _MonoGameLibrary_ `graphics` folder called `DeferredRenderer.cs` and populate it with the following code: + + [!code-csharp[](./snippets/snippet-8-01.cs)] + +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. + + The `SetRenderTarget()` function instructs all future MonoGame draw operations to render into the `ColorBuffer`. Add this function to the `DeferredRenderer` class: + + [!code-csharp[](./snippets/snippet-8-02.cs)] + +3. Once all of the rendering is complete, we need to switch the primary render target back to the _screen_ so that we can actually see anything. + + > [!NOTE] + > Set the render target to `null` to draw to the screen. + > + > `RenderTarget`s are off-screen buffers that MonoGame can draw graphics into. If the render target is `null`, then there is no off-screen buffer to use, and as such, the only place to render the graphics in this case is directly to the screen. + + Add the following method to the `DeferredRenderer` class. + + [!code-csharp[](./snippets/snippet-8-03.cs)] + +4. Now we can use this new off-screen texture in the `GameScene`. Add a new class member in the `GameScene` of the `DungeonSlime` project: + + [!code-csharp[](./snippets/snippet-8-04.cs)] + +5. And initialize it in the `Initialize()` method: + + [!code-csharp[](./snippets/snippet-8-05.cs)] + +6. Then, to actually _use_ the new off-screen texture, we need to invoke the `StartColorPhase()` and `Finish()` methods in the `Draw()` method of the `GameScene`. + + Right before the `SpriteBatch.Begin()` class, invoke the `StartColorPhase()` method. Here is the `Draw()` method with most of the code left out, but it demonstrates where the `StartColorPhase()` and `Finish()` methods belong: + + [!code-csharp[](./snippets/snippet-8-06.cs?highlight=6,17)] + +7. If you run the game now, once you have started a new game from the title screen, the screen will appear blank except for the UI (and you will get the game over prompt rather quickly as the game logic itself is still actually running). That is because the game is rendering to an off-screen texture, but nothing is rendering the off-screen texture _back_ to the screen. For now, we will add some diagnostic visualization of the off-screen texture. + + Add the following function to the `DeferredRenderer` class. + + This function starts a new sprite batch and draws the `ColorBuffer` to the top-left corner of the screen, with an orange border around it to indicate it is a debug visualization: + + [!code-csharp[](./snippets/snippet-8-07.cs)] + +8. And call this new method from the end of the `Draw()` method, after the GUM UI draws: + + [!code-csharp[](./snippets/snippet-8-08.cs?highlight=8-9)] + +Now when you run the game, you should see the diffuse texture of the game that was generated in this first pass, appearing in the upper-left corner of the screen. + +| ![Figure 8-2: The color buffer debug view](./images/color-buffer.png) | +| :-------------------------------------------------------------------: | +| **Figure 8-2: The color buffer debug view** | + +### Setting up the Light Buffer + +The next step is to create some lights and render them to a second off-screen texture. + +1. To start, add a second `RenderTarget2D` property to the `DeferredRenderer` class: + + [!code-csharp[](./snippets/snippet-8-09.cs)] + +2. And initialize it in the constructor exactly the same as the `ColorBuffer` was initialized: + + [!code-csharp[](./snippets/snippet-8-10.cs)] + +3. We need to add another method to switch MonoGame into drawing sprites onto the new off-screen texture: + + [!code-csharp[](./snippets/snippet-8-11.cs)] + +4. Then, we need to call the new method in the `GameScene`'s `Draw()` method between the current `SpriteBatch.End()` call and the `deferredRenderer.Finish()` call: + + [!code-csharp[](./snippets/snippet-8-12.cs?highlight=8-11)] + +5. To finish off with the light implementation changes for now, add the `LightBuffer` to the `DebugDraw()` view of `DeferredRenderer` as well: + + [!code-csharp[](./snippets/snippet-8-13.cs?highlight=17-26,35-39)] + +Now when you run the game, you will see a blank texture in the top-right. It is blank because there are no lights yet. + +| ![Figure 8-3: A blank light buffer](./images/light-buffer-1.png) | +| :--------------------------------------------------------------: | +| **Figure 8-3: A blank light buffer** | + +### Point Light Shader + +Each light will be drawn using a shader so that the fall-off and intensity can be adjusted in real time. + +1. Use the `mgcb-editor` to create a new Sprite Effect called `pointLightEffect` in the _SharedContent_ `effects` folder of the `MonoGameLibrary` project. For now, leave it as the default shader. Remember, save before exiting the MGCB editor. + + | ![Figure 8-4: Adding a point light effect shader](./images/mgcb-ple.png) | + | :-----------------------------------------------------------------: | + | **Figure 8-4: Adding a point light effect shader** | + +2. We need to load the new `pointLightEffect` in the `Core` class. First, create a new class member in the `Core` class of the `MonoGameLibrary` project: + + [!code-csharp[](./snippets/snippet-8-14.cs)] + +3. And then load the `Material` in the `LoadContent()` method: + + [!code-csharp[](./snippets/snippet-8-15.cs?highlight=7)] + +4. And do not forget to enable the hot-reload by adding the `Update()` line in the `Update()` method: + + [!code-csharp[](./snippets/snippet-8-16.cs?highlight=5)] + +5. In order to handle multiple lights, it will also be helpful to have a class that represents each light. Create a new file in the _MonoGameLibrary_'s graphics folder called `PointLight.cs` and populate it with the following: + + [!code-csharp[](./snippets/snippet-8-17.cs)] + +6. Back to the `GameScene` class in the `DungeonSlime` project, create a `List` as a new property: + + [!code-csharp[](./snippets/snippet-8-18.cs)] + +7. In order to start building intuition for the point light shader, we will need a debug light to experiment with. Add this snippet to the `Initialize()` method: + + [!code-csharp[](./snippets/snippet-8-19.cs)] + +8. Next, we need to draw the `PointLight` list using the new `PointLightMaterial`. Add the following function to the `PointLight` class: + + [!code-csharp[](./snippets/snippet-8-20.cs)] + +9. And back in `GameScene`, call it from the `Draw()` method, after the `StartLightPhase()` invocation: + + [!code-csharp[](./snippets/snippet-8-21.cs?highlight=7)] + +Now when you run the game, you will see a blank white square where the point light is located (at 300,300). + +> [!NOTE] +> Sorry for all the jumping back and to between classes in this chapter, but it was critical to help you understand the flow of the lighting, which is critical with any deferred rendering pattern. + +| ![Figure 8-5: The light buffer with a square](./images/light-buffer-2.png) | +| :------------------------------------------------------------------------: | +| **Figure 8-5: The light buffer with a square** | + +> [!NOTE] +> If you want to clear up all the other Debug windows for the previous effects, feel free to go through the `LoadContent` methods in each of the classes and either set `Material.IsDebugVisible = false;` or just remove those lines. + +The next task is to write the `pointLightEffect.fx` shader file so that the white square looks more like a point light. There are several ways to create the effect, some more realistic than others. For _DungeonSlime_, a realistic light falloff is not going to look great, so we will develop something custom. + +1. To start, open the `pointLightEffect.fx` shader in the `MonoGameLibrary` _SharedContent/effects_ folder and replace the `MainPS` function with the following, which calculates the distance from the center of the image and renders it to the red-channel: + + [!code-hlsl[](./snippets/snippet-8-22.hlsl)] + + > [!NOTE] + > 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. + > + > If you want to do that too, change the `DebugDraw()` method to use the entire viewport for the `lightBorderRect`, like this: + > + > [!code-hlsl[](./snippets/snippet-8-22-2.cs)] + > + > Just do not forget to revert this change later! + + + > [!TIP] + > Add a pause mechanic! + > + > 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. + > Add a class variable called ``: + > + > ```csharp + > private bool _debugPause = false; + > ``` + > + > And then add this snippet to the top of the `Update()` method: + > + > ```csharp + > if (Core.Input.Keyboard.WasKeyJustPressed(Keys.P)) + > { + > _debugPause = !_debugPause; + > } + > if (_debugPause) return; + > ``` + > + > And do not forget to add the `using` statement: + > + > ```csharp + > using Microsoft.Xna.Framework.Input; + > ``` + > + > 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! + + + | ![Figure 8-6: Showing the distance from the center of the light in the red channel](./images/point-light-dist.png) | + | :----------------------------------------------------------------------------------------------------------------: | + | **Figure 8-6: Showing the distance from the center of the light in the red channel** | + + > [!NOTE] + > Remember, you can leave the project running now, while we implement these effect changes to see it in realtime. + +2. This is starting to look like a light, but in reverse. Create a new variable, `falloff` which inverts the distance. The [`saturate`](https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-saturate) function is shorthand for clamping the value between `0` and `1`: + + [!code-hlsl[](./snippets/snippet-8-23.hlsl?highlight=5,7)] + + | ![Figure 8-7: Invert the distance](./images/point-light-falloff-1.png) | + | :--------------------------------------------------------------------: | + | **Figure 8-7: Invert the distance** | + +3. That looks more light-like. Now it is time to add some artistic control parameters to the shader. First, it would be good to be able to increase the brightness of the light. Multiplying the `falloff` by some number larger than 1 would increase the brightness, but leave the unlit sections completely unlit: + + [!code-hlsl[](./snippets/snippet-8-24.hlsl?highlight=1,7)] + + | ![Figure 8-8: A LightBrightness parameter](./gifs/point-light-brightness.gif) | + | :---------------------------------------------------------------------------: | + | **Figure 8-8: A LightBrightness parameter** | + +4. It would also be good to control the sharpness of the falloff. The `pow()` function raises the `falloff` to some exponent value: + + [!code-hlsl[](./snippets/snippet-8-25.hlsl?highlight=2,9)] + + | ![Figure 8-9: A LightSharpness parameter](./gifs/point-light-sharpness.gif) | + | :-------------------------------------------------------------------------: | + | **Figure 8-9: A LightSharpness parameter** | + +5. Finally, the shader parameters currently range from `0` to `1`, but it would be nice to push the brightness and sharpness beyond `1`. Add an additional `range` multiplier in the shader code: + + [!code-hlsl[](./snippets/snippet-8-26.hlsl?highlight=8, 10-11)] + + | ![Figure 8-10: Increase the range of the artistic parameters](./gifs/point-light-range.gif) | + | :----------------------------------------------------------------------------------------: | + | **Figure 8-10: Increase the range of the artistic parameters** | + +6. The final touch is to return the `Color` of the light, instead of just the red debug value. The `input.Color` carries the `Color` passed through the `SpriteBatch`, so we can use that. We can also multiply the alpha channel of the color by the `falloff` to _fade_ the light out without changing the light color itself: + + [!code-hlsl[](./snippets/snippet-8-27.hlsl?highlight=13-15)] + + > [!WARNING] + > 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! + +7. And change the `blendState` of the light's `SpriteBatch` draw call in the `PointLight` class to additive for effect: + + [!code-csharp[](./snippets/snippet-8-29.cs)] + +8. In the `GameScene` we can replace the initialization of the light to change its color in C# to `CornflowerBlue` in the `Initialize` method: + + [!code-csharp[](./snippets/snippet-8-28.cs)] + +9. Finally, in the `Core` class `LoadContent` method, set the default shader parameter values for brightness and sharpness to something you like: + + [!code-csharp[](./snippets/snippet-8-30.cs?highlight=7,8)] + +| ![Figure 8-11: The point light in the light buffer](./images/point-light-blue.png) | +| :-------------------------------------------------------------------------------: | +| **Figure 8-11: The point light in the light buffer** | + +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. + +### Combining Light and Color + +Now that the light and color buffers are being drawn to separate off screen textures, we need to _composite_ them to create the final screen render. + +1. Create a new Sprite Effect in the `SharedContent` folder of the `MonoGameLibrary` project called `deferredCompositeEffect` in the MGCB editor. + + | ![Figure 8-12: Adding the `deferredCompositeEffect`](./images/mgcb-dce.png) | + | :------------------------------------------------------------------------: | + | **Figure 8-12: Adding the `deferredCompositeEffect`** | + +2. Open the `Core.cs` class so we can add the new shader to the pipeline, add a new property to hold the material: + + [!code-csharp[](./snippets/snippet-8-31.cs)] + +3. Then load the effect in the `LoadContent()` method: + + [!code-csharp[](./snippets/snippet-8-32.cs)] + +4. To enable hot-reload support, also add the `Update()` method: + + [!code-csharp[](./snippets/snippet-8-33.cs)] + +5. Next, create a new method in the `DeferredRenderer` class that will draw the composited image: + + [!code-csharp[](./snippets/snippet-8-34.cs)] + +6. And instead of calling the `DebugDraw()` from the `GameScene`, call the new method before the GUM UI is drawn: + + [!code-csharp[](./snippets/snippet-8-35.cs?highlight=6)] + + If you run the game now, it will appear as it did when we started the chapter! Now it is time to factor in the `LightBuffer`. The `deferredCompositeEffect` shader needs to get the `LightBuffer` and multiply it with the `ColorBuffer`. The `ColorBuffer` is being passed in as the main sprite from `SpriteBatch`, so we will need to add a second texture and sampler to the shader to get the `LightBuffer`. + +7. Open the new `deferredCompositeEffect.fx` shader and add the following property: + + [!code-hlsl[](./snippets/snippet-8-36.hlsl)] + +8. The main pixel function for the shader reads both the color and light values and returns their product, replace the `MainPS` function with the following: + + [!code-hlsl[](./snippets/snippet-8-38.hlsl)] + +9. Back in the `DeferredRenderer` class, in the `DrawComposite` function before the sprite batch starts, make sure to pass the `LightBuffer` to the material: + + [!code-csharp[](./snippets/snippet-8-37.cs?highlight=3)] + + The light is working! However, the whole scene is too dark to see what is going on or play the game. + + | ![Figure 8-13: The light and color composited](./images/composite-1.png) | + | :----------------------------------------------------------------------: | + | **Figure 8-13: The light and color composited** | + +10. To solve this, we can add a small amount of ambient light to the `deferredCompositeEffect` shader: + + [!code-hlsl[](./snippets/snippet-8-39.hlsl)] + + | ![Figure 8-14: Adding ambient light](./gifs/composite-ambient.gif) | + | :----------------------------------------------------------------: | + | **Figure 8-14: Adding ambient light** | + +11. Find a value of ambient that you like and then set the parameter from code in the `DrawComposite` method of the `DeferredRenderer`: + + [!code-csharp[](./snippets/snippet-8-40.cs)] + + | ![Figure 8-15: A constant ambient value](./gifs/composite-light-no-vert.gif) | + | :--------------------------------------------------------------------------: | + | **Figure 8-15: A constant ambient value** | + + > [!WARNING] + > The light is not moving with the rest of the game as the world rotates around the camera. It does not look too bad because the light effect is just being applied statically over the top of the screen. We are going to fix this soon. + +### Normal Textures + +The lighting is working, but it still feels a bit flat. Ultimately, the light is being applied to our flat 2D sprites uniformly, so there the sprites do not feel like they have any depth. Normal mapping is a technique designed to help make flat surfaces appear 3D by changing how much the lighting affects each pixel depending on the "Normal" of the surface at the given pixel. + +Normal textures encode the _direction_ (also called the _normal_) of the surface at each pixel. The direction of the surface is a 3D vector where the `x` component lives in the `red` channel, the `y` component lives in the `green` channel, and the `z` component lives in the `blue` channel. The directions are encoded as colors, so each component can only range from `0` to `1`. The _direction_ vector components need to range from `-1` to `1`, so a color channel value of `.5` results in a `0` value for the direction vector. + +> [!NOTE] +> +> If you want to learn more about the foundations of normal mapping, check out this article on [Normal Mapping](https://learnopengl.com/Advanced-Lighting/Normal-Mapping) from [LearnOpenGL.com](https://learnopengl.com/) + +Generating normal maps is an art form. Generally, you find a _normal map picker_, similar to a color wheel, and paint the directions on top of your existing artwork. This page on [open game art](https://opengameart.org/content/pixelart-normal-map-handpainting-helper) has a free normal map wheel that shows the colors for various directions along a low-resolution sphere. + +| ![Figure 8-16: A normal picker wheel](https://opengameart.org/sites/default/files/styles/medium/public/normalmaphelper.png) | +| :-------------------------------------------------------------------------------------------------------------------------: | +| **Figure 8-16: A normal picker wheel** | + +For this effect to work, we need an extra texture for every frame of every sprite we are drawing in the game. Given that the textures are currently coming from an atlas, the easiest thing to do will be to create a _second_ texture that shares the same layout as the first, but uses normal data instead. + +For reference, the existing texture atlas is on the left, and a version of the atlas with normal maps is on the right. + +| ![Figure 8-17: The existing texture atlas](./images/atlas.png) | ![Figure 8-18: The normal texture atlas](./images/atlas-normal.png) | +| :------------------------------------------------------------: | :-----------------------------------------------------------------: | +| **Figure 8-17: The existing texture atlas** | **Figure 8-18: The normal texture atlas** | + +> [!WARNING] +> 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. + +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. + +Now that we have the art assets, it is time to work the normal maps into the code. + +1. Every time one of the game sprites is being drawn, we need to draw the corresponding normal texture information to yet another off-screen texture, called the `NormalBuffer`. + + Start by adding a new `RenderTarget2D` to the `DeferredRenderer` class: + + [!code-csharp[](./snippets/snippet-8-41.cs)] + +2. And initialize it in the `DeferredRenderer`'s constructor: + + [!code-csharp[](./snippets/snippet-8-42.cs)] + +3. So far in the series, all of the pixel shaders have returned a _single_ `float4` with the `COLOR` semantic. MonoGame supports _Multiple Render Targets_ by having a shader return a `struct` with _multiple_ fields each with a unique `COLOR` semantic. + + Add the following `struct` to the `gameEffect.fx` file: + + [!code-hlsl[](./snippets/snippet-8-43.hlsl)] + +4. At the moment, the `gameEffect.fx` is just registering the `ColorSwapPS` function as the pixel function, but we will need to extend the logic to support the normal values. + + Create a new function in the file that will act as the new pixel shader function: + + [!code-hlsl[](./snippets/snippet-8-44.hlsl)] + +5. And do not forget to update the `technique` to reference the new `MainPS` function: + + [!code-hlsl[](./snippets/snippet-8-45.hlsl?highlight=6)] + +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. + + Replace the `StartColorPhase()` function in the `DeferredRenderer` with the following: + + [!code-csharp[](./snippets/snippet-8-46.cs)] + + > [!NOTE] + > [The _GBuffer_](https://learnopengl.com/advanced-lighting/deferred-shading) + > + > The `ColorBuffer` and `NormalBuffer` are grouped together and often called the _Geometry-Buffer_ (G-Buffer). In other deferred renderers, there is even more information stored in the G-Buffer as additional textures, such as depth information, material information, or game specific data. + +7. To visualize the `NormalBuffer`, we will update the `DebugDraw()` method. + + The `NormalBuffer` will be rendered in the lower-left corner of the screen: + + [!code-csharp[](./snippets/snippet-8-47.cs)] + +8. Do not forget to restore the call to the `DebugDraw()` method at the end of the `GameScene`'s `Draw()` method (`_deferredRenderer.DebugDraw();`). You will see a completely `red` `NormalBuffer`, because the shader is hard coding the value to `float4(1,0,0,1)`. + +| ![Figure 8-17: A blank normal buffer](./images/normal-buffer-red.png) | +| :-------------------------------------------------------------------: | +| **Figure 8-17: A blank normal buffer** | + +To start rendering the normal values themselves, we need to load the normal texture into the `GameScene` and pass it along to the `gameEffect.fx` effect. + +1. First, in the `GameScene` class, create a new `Texture2D` property: + + [!code-csharp[](./snippets/snippet-8-48.cs)] + +2. Then load the texture in the `LoadContent()` method and pass it to the `_gameEffect` material as a parameter: + + [!code-csharp[](./snippets/snippet-8-49.cs?highlight=5-6,14)] + +3. The `GameEffect` shader needs to expose a `Texture2D` and `Sampler` state for the new normal texture, so add the following property to the shader: + + [!code-hlsl[](./snippets/snippet-8-51.hlsl)] + +4. And then finally the `MainPS` shader function needs to be updated to read the `NormalMap` data for the current pixel: + + [!code-hlsl[](./snippets/snippet-8-52.hlsl)] + +5. Now the `NormalBuffer` is being populated with the normal data for each sprite. + + | ![Figure 8-18: The normal map](./images/normal-buffer.png) | + | :--------------------------------------------------------: | + | **Figure 8-18: The normal map** | + +### Combining Normals with Lights + +When each individual light is drawn into the `LightBuffer`, it needs to use the `NormalBuffer` information to modify the amount of light being drawn at each pixel. To set up, the `PointLightMaterial` is going to need access to the `NormalBuffer`. + +1. Starting with the `PointLight` class in the `MonoGameLibrary` project, update the `Draw()` method to take in the `NormalMap` as a `Texture2D`, and set it as a parameter on the `PointLightMaterial`: + + [!code-csharp[](./snippets/snippet-8-53.cs?highlight=1,3)] + +2. And then to pass the `NormalBuffer`, modify the `GameScene`'s `Draw()` method to pass the buffer: + + [!code-csharp[](./snippets/snippet-8-54.cs)] + +3. The `pointLightEffect.fx` shader also needs to accept the `NormalBuffer` as a new `Texture2D` and `Sampler`: + + [!code-hlsl[](./snippets/snippet-8-55.hlsl)] + + The challenge here is to find the normal value of the pixel that the light is currently shading in the pixel shader. However, the shader's `uv` coordinate space is relative to the light itself. The `NormalBuffer` is relative to the entire screen, not the light. + + We need to be able to convert the light's `uv` coordinate space into screen space, which can be done in a custom vertex shader. The vertex shader's job is to convert the world space into clip space, which in a 2D game like _Dungeon Slime_, essentially _is_ screen space. We need a way to calculate the screen coordinate for each pixel being drawn for the `pointLightEffect` pixel shader. To achieve this, we are going to send the output from the projection matrix multiplication from the vertex shader to the pixel shader. Then, the pixel shader can convert the result into a screen space coordinate + + In order to override the vertex shader function, we will need to repeat the `MatrixTransform` work from the previous chapter. However, it would be better to _re-use_ the work from the previous chapter so that the lights also tilt and respond to the `MatrixTransform` that the rest of the game world uses. + +4. Add a reference to the `3dEffect.fxh` file in the `pointLightEffect.fx` shader: + + [!code-hlsl[](./snippets/snippet-8-56.hlsl)] + +5. We need to _extend_ the vertex function and add the extra field. + + Create a new struct in the `pointLightEffect.fx` file, replacing the old `VertexShaderOutput` struct: + + [!code-hlsl[](./snippets/snippet-8-57.hlsl?highlight=6)] + +6. Next, create a new vertex function that uses the new `LightVertexShaderOutput`. This function will call to the existing `MainVS` function that does the 3D effect, and add the screen coordinates afterwards: + + [!code-hlsl[](./snippets/snippet-8-58.hlsl)] + + > [!NOTE] + > Why are we using the `w` component? + > + > Long story short, when we output the `.Position`, it is a _4_ dimensional vector so it includes the understandable `x`, `y`, and `z` components of the position... But it _also_ includes a 4th component, _`w`_. The `w` component is used by the graphics pipeline between the vertex shader and the pixel shader to handle perspective correction. There is a lot of math to dig into, but the core issue at play is how the graphics pipeline delivers interpolated values to your pixel shader at _every_ pixel between all of the vertices being rendered. We are not going to cover the mathematics here, but please read about [homogenous coordinates](https://www.tomdalling.com/blog/modern-opengl/explaining-homogenous-coordinates-and-projective-geometry/) and [perspective divide](https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/projection-matrix-GPU-rendering-pipeline-clipping.html). + > + > If you like to learn from videos, then [this video by Acerola](https://www.youtube.com/watch?v=y84bG19sg6U) is a great exploration of the topic as well. At first the video is about simulating _Playstation 1_ graphics, but later, the video discusses how the interpolation between graphics stages can cause strange visual artifacts. + > + > We need the `w` component because the graphics pipeline is not going to automatically perform the _perspective divide_ correctly in this case, so we will need to re-create it ourselves later in the pixel shader. And to do that, we will need the `w` component value. + +7. Make sure to update the `technique` to use the new vertex function: + + [!code-hlsl[](./snippets/snippet-8-59.hlsl?highlight=5)] + +8. In the pixel function, to visualize the screen coordinates, we will short-circuit the existing light code and just render out the screen coordinates by replacing the `MainPS` function with one that accepts the new `LightVertexShaderOutput` struct and make the function immediately calculate and return the screen coordinates in the red and green channel: + + > [!NOTE] + > Remember, the `.ScreenData.xy` carries a coordinate in _clip_ space. However, the graphics card did not receive it in a `POSITION` semantic, so it did not automatically noramlize the values by the `w` channel. Therefor, we need to do that ourselves. The range of clip space is from `-1` to `1`, so we need to convert it back to normalized `0` to `1` coordinates. + + [!code-hlsl[](./snippets/snippet-8-61.hlsl)] + +9. Be careful, if you run the game now, it will not look right as we need to make sure to send the `MatrixTransform` parameter from C# as well. + + The `ScreenSize` parameter needs to be set for the effect in the `Update` method of the `GameScene` class to pass the `MatrixTransform` to _both_ the `_gameMaterial` _and_ the `Core.PointLightMaterial`: + + [!code-csharp[](./snippets/snippet-8-62.cs)] + + | ![Figure 8-19: ](./images/light-screen.png) | + | :--------------------------------------------------------------------------------: | + | **Figure 8-19: The point light can access screen space** | + + > [!NOTE] + > The `LightBuffer` is showing that red/greenish color gradient because we forced the shader to return the `input.ScreenCoordinates.xy`. This is only to verify that the `ScreenCoordinates` are working as expected. + +10. Now, the `pointLightEffect` can use the screen space coordinates to sample the `NormalBuffer` values. To build intuition, start by just returning the values from the `NormalBuffer`. + + Start by updating the `MainPS` in the `pointLightEffect` shader to read the values from the `NormalBuffer` texture, and then return immediately: + + [!code-hlsl[](./snippets/snippet-8-63.hlsl)] + +11. Strangely, this will return a `white` box, instead of the normal data as expected, this happening because of a misunderstanding between the shader compiler and `SpriteBatch`. + + | ![Figure 8-20: A white box instead of the normal data?](./images/light-broken.png) | + | :--------------------------------------------------------------------------------: | + | **Figure 8-20: A white box instead of the normal data?** | + + _Most_ of the time when `SpriteBatch` is being used, there is a single `Texture` and `Sampler` being used to draw a sprite to the screen. The `SpriteBatch`'s draw function passes the given `Texture2D` to the shader by setting it in the `GraphicsDevice.Textures` array [directly](https://github.com/MonoGame/MonoGame/blob/develop/MonoGame.Framework/Graphics/SpriteBatcher.cs#L212). The texture is not being passed _by name_, it is being passed by _index_. In the lighting case, the `SpriteBatch` is being drawn with the `Core.Pixel` texture (a white 1x1 image we generated in the earlier chapters). + + However, the shader compiler will aggressively optimize away data that is not being used in the shader. The current `pointLightEffect.fx` does not _use_ the default texture or sampler that `SpriteBatch` expects by default. The default texture is _removed_ from the shader during compilation, because it is not used anywhere and has no effect. The only texture that is left is the `NormalBuffer`, which now becomes the first indexable texture. + + Despite passing the `NormalBuffer` texture to the named `NormalTexture` `Texture2D` parameter in the shader before calling `SpriteBatch.Draw()`, the `SpriteBatch` code itself then overwrites whatever is in texture slot `0` with the texture passed to the `Draw()` call, the white pixel. + + There are two workarounds: + + - Modify the shader code to read data from the main `SpriteTextureSampler` and use the resulting color "_somehow_" in the computation for the final result of the shader. + > [!NOTE] + > For example, You could multiply the color by a very small constant, like `.00001`, and then add the product to the final color. It would have no perceivable effect, but the shader compiler would not be able to optimize the sampler away. However, this is useless and silly work. Worse, it will likely confuse anyone who looks at the shader in the future. + - The better approach is to pass the `NormalBuffer` to the `Draw()` function directly, and not bother sending it as a shader parameter at all. + + +12. Change the `Draw()` method in the `PointLight` class to pass the `normalBuffer` to the `SpriteBatch.Draw()` method _instead_ of passing it in as a parameter to the `PointLightMaterial`. + + Here is the updated `Draw()` method: + + [!code-csharp[](./snippets/snippet-8-64.cs)] + + And now the normal map is being rendered where the light exists. + + | ![Figure 8-21: The light shows the normal map entirely](./images/light-normal.png) | + | :--------------------------------------------------------------------------------: | + | **Figure 8-21: The light shows the normal map entirely** | + + Now it is time to _use_ the normal data in conjunction with the light direction to decide how much light each pixel should receive. + +13. Replace the `MainPS` function in the `pointLightEffect` shader code with the following: + + [!code-hlsl[](./snippets/snippet-8-65.hlsl)] + + > [!NOTE] + > The `normalDir`, `lightDir`, and [`dot`](https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-dot) product are a simplified version of the [Blinn-Phong shading model](https://en.wikipedia.org/wiki/Blinn%E2%80%93Phong_reflection_model). + + | ![Figure 8-22: The light with the normal](./images/light-with-normal.png) | + | :-----------------------------------------------------------------------: | + | **Figure 8-22: The light with the normal** | + +To drive the effect for a moment, this gif shows the normal effect being blended in. Notice how the wings on the bat shade differently based on their position towards the light as the normal effect is brought in. (I added a test property to alter the normal strength just for observation to demonstrate) + +| ![Figure 8-23: The lighting on the bat with normals](./gifs/normals.gif) | +| :----------------------------------------------------------------------: | +| **Figure 8-23: The lighting on the bat with normals** | + +The effect can be very subtle. To help illustrate it, here is a side by side comparison of the bat with the light moving around it in a circle. The ambient background light has been completely disabled. On the left, there is no normal mapping so the bat feels "flat". On the right, normal mapping is enabled, and the bat feels "lit" by the light. + +| ![Figure 8-24: No normals enabled](./gifs/normals_demo_none.gif) | ![Figure 8-25: Normals _enabled_](./gifs/normals_demo_some.gif) | +| :--------------------------------------------------------------: | :-----------------------------------------------------------------: | +| **Figure 8-24: No normals enabled** | **Figure 8-25: Normals _enabled_** | + +### Gameplay + +Now that we have lights rendering in the game, it is time to hook a few more up in the game. There should be a light positioned next to each torch along the upper wall, and maybe a few lights that wonder around the level. + +1. Create a function in the `GameScene` that will initialize all of the lights. Feel free to add more: + + [!code-csharp[](./snippets/snippet-8-67.cs)] + +2. Then replace the original code that created a single light with a call to the new `InitializeLights` method: + + [!code-csharp[](./snippets/snippet-8-67-initialize.cs)] + +3. Given that the lights have a dynamic nature to them with the normal maps, it would be good to move some of them around. + + Add this function to the `GameScene`: + + [!code-csharp[](./snippets/snippet-8-68.cs)] + +4. And call it from the `Update()` method: + + [!code-csharp[](./snippets/snippet-8-69.cs)] + +And now when the game runs, it looks like this (provided you also turned off the `_deferredRenderer.DebugDraw` call in the `Draw` method in `GameScene`, and the `IsDebugVisible` for the materials). + +| ![Figure 8-26: The final results](./gifs/final.gif) | +| :-------------------------------------------------: | +| **Figure 8-26: The final results** | + +## Conclusion + +In this chapter, you accomplished the following: + +- Learned the theory behind deferred rendering. +- Set up a rendering pipeline with multiple render targets (G-buffers) for color and normals. +- Created a point light shader. +- Used normal maps to allow 2D sprites to react to light as if they had 3D depth. +- Wrote a final composite shader to combine all the buffers into the final lit scene. + +Our world is so much more atmospheric now, but there is one key ingredient missing... shadows! In our next and final effects chapter, we will bring our lights to life by making them cast dynamic shadows. + +You can find the complete code sample for this chapter, [here](https://github.com/MonoGame/MonoGame.Samples/tree/3.8.4/Tutorials/2dShaders/src/08-Light-Effect). + +Continue to the next chapter, [Chapter 09: Shadows Effect](../09_shadows_effect/index.md) diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-01.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-01.cs new file mode 100644 index 00000000..767cca04 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-01.cs @@ -0,0 +1,26 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGameLibrary.Graphics; + +public class DeferredRenderer +{ + /// + /// A texture that holds the unlit sprite drawings + /// + public RenderTarget2D ColorBuffer { get; set; } + + public DeferredRenderer() + { + var viewport = Core.GraphicsDevice.Viewport; + + ColorBuffer = new RenderTarget2D( + graphicsDevice: Core.GraphicsDevice, + width: viewport.Width, + height: viewport.Height, + mipMap: false, + preferredFormat: SurfaceFormat.Color, + preferredDepthFormat: DepthFormat.None); + + } +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-02.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-02.cs new file mode 100644 index 00000000..f822db02 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-02.cs @@ -0,0 +1,6 @@ +public void StartColorPhase() +{ + // all future draw calls will be drawn to the color buffer + Core.GraphicsDevice.SetRenderTarget(ColorBuffer); + Core.GraphicsDevice.Clear(Color.Transparent); +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-03.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-03.cs new file mode 100644 index 00000000..6a4f6199 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-03.cs @@ -0,0 +1,6 @@ +public void Finish() +{ + // all future draw calls will be drawn to the screen + // note: 'null' means "the screen" in MonoGame + Core.GraphicsDevice.SetRenderTarget(null); +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-04.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-04.cs new file mode 100644 index 00000000..caff73f7 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-04.cs @@ -0,0 +1,2 @@ +// The deferred rendering resources +private DeferredRenderer _deferredRenderer; diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-05.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-05.cs new file mode 100644 index 00000000..70e1f8c6 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-05.cs @@ -0,0 +1,2 @@ +// Create the deferred rendering resources +_deferredRenderer = new DeferredRenderer(); diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-06.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-06.cs new file mode 100644 index 00000000..b3175bd5 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-06.cs @@ -0,0 +1,21 @@ + public override void Draw(GameTime gameTime) + { + // ... configure the sprite batch + + // Start rendering to the deferred renderer + _deferredRenderer.StartColorPhase(); + Core.SpriteBatch.Begin( + samplerState: SamplerState.PointClamp, + sortMode: SpriteSortMode.Immediate, + rasterizerState: RasterizerState.CullNone, + effect: _gameMaterial.Effect); + + // ... all of the actual draw code. + + // Always end the sprite batch when finished. + Core.SpriteBatch.End(); + _deferredRenderer.Finish(); + + // Draw the UI + _ui.Draw(); + } diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-07.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-07.cs new file mode 100644 index 00000000..c869ff48 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-07.cs @@ -0,0 +1,25 @@ +public void DebugDraw() +{ + var viewportBounds = Core.GraphicsDevice.Viewport.Bounds; + + // the debug view for the color buffer lives in the top-left. + var colorBorderRect = new Rectangle( + x: viewportBounds.X, + y: viewportBounds.Y, + width: viewportBounds.Width / 2, + height: viewportBounds.Height / 2); + + // shrink the color rect by 8 pixels + var colorRect = colorBorderRect; + colorRect.Inflate(-8, -8); + + Core.SpriteBatch.Begin(); + + // draw a debug border for the color buffer + Core.SpriteBatch.Draw(Core.Pixel, colorBorderRect, Color.MonoGameOrange); + + // draw the color buffer + Core.SpriteBatch.Draw(ColorBuffer, colorRect, Color.White); + + Core.SpriteBatch.End(); +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-08.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-08.cs new file mode 100644 index 00000000..610d99d7 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-08.cs @@ -0,0 +1,10 @@ +public override void Draw(GameTime gameTime) +{ + // ... + + // Draw the UI + _ui.Draw(); + + // Render the debug view for the game + _deferredRenderer.DebugDraw(); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-09.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-09.cs new file mode 100644 index 00000000..2ba2c96e --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-09.cs @@ -0,0 +1,4 @@ +/// +/// A texture that holds the drawn lights +/// +public RenderTarget2D LightBuffer { get; set; } diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-10.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-10.cs new file mode 100644 index 00000000..e7fc6517 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-10.cs @@ -0,0 +1,12 @@ +public DeferredRenderer() +{ + // ... + + LightBuffer = new RenderTarget2D( + graphicsDevice: Core.GraphicsDevice, + width: viewport.Width, + height: viewport.Height, + mipMap: false, + preferredFormat: SurfaceFormat.Color, + preferredDepthFormat: DepthFormat.None); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-11.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-11.cs new file mode 100644 index 00000000..581c79a6 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-11.cs @@ -0,0 +1,6 @@ +public void StartLightPhase() +{ + // all future draw calls will be drawn to the light buffer + Core.GraphicsDevice.SetRenderTarget(LightBuffer); + Core.GraphicsDevice.Clear(Color.Black); +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-12.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-12.cs new file mode 100644 index 00000000..3065a8f1 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-12.cs @@ -0,0 +1,17 @@ +public override void Draw(GameTime gameTime) +{ + // ... + + // Always end the sprite batch when finished. + Core.SpriteBatch.End(); + + // start rendering the lights + _deferredRenderer.StartLightPhase(); + + // TODO: draw lights + + // finish the deferred rendering + _deferredRenderer.Finish(); + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-13.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-13.cs new file mode 100644 index 00000000..e2088032 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-13.cs @@ -0,0 +1,42 @@ +public void DebugDraw() +{ + var viewportBounds = Core.GraphicsDevice.Viewport.Bounds; + + // the debug view for the color buffer lives in the top-left. + var colorBorderRect = new Rectangle( + x: viewportBounds.X, + y: viewportBounds.Y, + width: viewportBounds.Width / 2, + height: viewportBounds.Height / 2); + + // shrink the color rect by 8 pixels + var colorRect = colorBorderRect; + colorRect.Inflate(-8, -8); + + // the debug view for the light buffer lives in the top-right. + var lightBorderRect = new Rectangle( + x: viewportBounds.Width / 2, + y: viewportBounds.Y, + width: viewportBounds.Width / 2, + height: viewportBounds.Height / 2); + + // shrink the light rect by 8 pixels + var lightRect = lightBorderRect; + lightRect.Inflate(-8, -8); + + Core.SpriteBatch.Begin(); + + // draw a debug border for the color buffer + Core.SpriteBatch.Draw(Core.Pixel, colorBorderRect, Color.MonoGameOrange); + + // draw the color buffer + Core.SpriteBatch.Draw(ColorBuffer, colorRect, Color.White); + + // draw a debug border for the light buffer + Core.SpriteBatch.Draw(Core.Pixel, lightBorderRect, Color.CornflowerBlue); + + // draw the light buffer + Core.SpriteBatch.Draw(LightBuffer, lightRect, Color.White); + + Core.SpriteBatch.End(); +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-14.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-14.cs new file mode 100644 index 00000000..422d57e7 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-14.cs @@ -0,0 +1,4 @@ +/// +/// The material that draws point lights +/// +public static Material PointLightMaterial { get; private set; } diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-15.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-15.cs new file mode 100644 index 00000000..41dc4660 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-15.cs @@ -0,0 +1,9 @@ +protected override void LoadContent() +{ + base.LoadContent(); + + // ... + + PointLightMaterial = SharedContent.WatchMaterial("effects/pointLightEffect"); + PointLightMaterial.IsDebugVisible = true; +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-16.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-16.cs new file mode 100644 index 00000000..82bca3c7 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-16.cs @@ -0,0 +1,8 @@ +protected override void Update(GameTime gameTime) +{ + // ... + + PointLightMaterial.Update(); + + // ... +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-17.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-17.cs new file mode 100644 index 00000000..f099fa61 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-17.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGameLibrary.Graphics; + +public class PointLight +{ + /// + /// The position of the light in world space + /// + public Vector2 Position { get; set; } + + /// + /// The color tint of the light + /// + public Color Color { get; set; } = Color.White; + + /// + /// The radius of the light in pixels + /// + public int Radius { get; set; } = 250; +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-18.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-18.cs new file mode 100644 index 00000000..a07fa124 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-18.cs @@ -0,0 +1,2 @@ +// A list of point lights to be rendered +private List _lights = new List(); diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-19.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-19.cs new file mode 100644 index 00000000..f8f6c207 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-19.cs @@ -0,0 +1,9 @@ +public override void Initialize() +{ + // ... + + _lights.Add(new PointLight + { + Position = new Vector2(300, 300) + }); +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-20.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-20.cs new file mode 100644 index 00000000..976d3188 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-20.cs @@ -0,0 +1,15 @@ +public static void Draw(SpriteBatch spriteBatch, List pointLights) +{ + spriteBatch.Begin( + effect: Core.PointLightMaterial.Effect + ); + + foreach (var light in pointLights) + { + var diameter = light.Radius * 2; + var rect = new Rectangle((int)(light.Position.X - light.Radius), (int)(light.Position.Y - light.Radius), diameter, diameter); + spriteBatch.Draw(Core.Pixel, rect, light.Color); + } + + spriteBatch.End(); +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-21.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-21.cs new file mode 100644 index 00000000..598c6106 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-21.cs @@ -0,0 +1,10 @@ +public override void Draw(GameTime gameTime) +{ + // ... + + // start rendering the lights + _deferredRenderer.StartLightPhase(); + PointLight.Draw(Core.SpriteBatch, _lights); + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-22-2.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-22-2.cs new file mode 100644 index 00000000..cbd6aa0a --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-22-2.cs @@ -0,0 +1,15 @@ +public void DebugDraw() +{ + // ... + + // the debug view for the light buffer lives in the top-right. + // var lightBorderRect = new Rectangle( + // x: viewportBounds.Width / 2, + // y: viewportBounds.Y, + // width: viewportBounds.Width / 2, + // height: viewportBounds.Height / 2); + + var lightBorderRect = viewportBounds; // TODO: remove this; it makes the light rect take up the whole screen. + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-22.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-22.hlsl new file mode 100644 index 00000000..f0f8b774 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-22.hlsl @@ -0,0 +1,5 @@ +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float dist = length(input.TextureCoordinates - .5); + return float4(dist, 0, 0, 1); +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-23.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-23.hlsl new file mode 100644 index 00000000..ca24c191 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-23.hlsl @@ -0,0 +1,8 @@ +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float dist = length(input.TextureCoordinates - .5); + + float falloff = saturate(.5 - dist); + + return float4(falloff, 0, 0, 1); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-24.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-24.hlsl new file mode 100644 index 00000000..68867b79 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-24.hlsl @@ -0,0 +1,10 @@ +float LightBrightness; + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float dist = length(input.TextureCoordinates - .5); + + float falloff = saturate(.5 - dist) * (LightBrightness + 1); + + return float4(falloff, 0, 0, 1); +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-25.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-25.hlsl new file mode 100644 index 00000000..34d0df89 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-25.hlsl @@ -0,0 +1,12 @@ +float LightBrightness; +float LightSharpness; + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float dist = length(input.TextureCoordinates - .5); + + float falloff = saturate(.5 - dist) * (LightBrightness + 1); + falloff = pow(falloff, LightSharpness + 1); + + return float4(falloff, 0, 0, 1); +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-26.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-26.hlsl new file mode 100644 index 00000000..bfb17e55 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-26.hlsl @@ -0,0 +1,14 @@ +float LightBrightness; +float LightSharpness; + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float dist = length(input.TextureCoordinates - .5); + + float range = 5; // arbitrary maximum. + + float falloff = saturate(.5 - dist) * (LightBrightness * range + 1); + falloff = pow(falloff, LightSharpness * range + 1); + + return float4(falloff, 0, 0, 1); +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-27.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-27.hlsl new file mode 100644 index 00000000..b6fe226e --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-27.hlsl @@ -0,0 +1,16 @@ +float LightBrightness; +float LightSharpness; + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float dist = length(input.TextureCoordinates - .5); + + float range = 5; // arbitrary maximum. + + float falloff = saturate(.5 - dist) * (LightBrightness * range + 1); + falloff = pow(falloff, LightSharpness * range + 1); + + float4 color = input.Color; + color.a = falloff; + return color; +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-28.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-28.cs new file mode 100644 index 00000000..3b300ec5 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-28.cs @@ -0,0 +1,5 @@ +_lights.Add(new PointLight +{ + Position = new Vector2(300,300), + Color = Color.CornflowerBlue +}); diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-29.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-29.cs new file mode 100644 index 00000000..8aa42c4d --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-29.cs @@ -0,0 +1,9 @@ +public static void Draw(SpriteBatch spriteBatch, List pointLights, Texture2D normalBuffer) +{ + spriteBatch.Begin( + effect: Core.PointLightMaterial.Effect, + blendState: BlendState.Additive + ); + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-30.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-30.cs new file mode 100644 index 00000000..1672acea --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-30.cs @@ -0,0 +1,9 @@ +protected override void LoadContent() +{ + // ... + + PointLightMaterial = SharedContent.WatchMaterial("effects/pointLightEffect"); + PointLightMaterial.IsDebugVisible = true; + PointLightMaterial.SetParameter("LightBrightness", .25f); + PointLightMaterial.SetParameter("LightSharpness", .1f); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-31.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-31.cs new file mode 100644 index 00000000..01e93a82 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-31.cs @@ -0,0 +1,4 @@ +/// +/// The material that combines the various off screen textures +/// +public static Material DeferredCompositeMaterial { get; private set; } diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-32.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-32.cs new file mode 100644 index 00000000..a8e94dd5 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-32.cs @@ -0,0 +1,7 @@ +protected override void LoadContent() +{ + // ... + + DeferredCompositeMaterial = SharedContent.WatchMaterial("effects/deferredCompositeEffect"); + DeferredCompositeMaterial.IsDebugVisible = true; +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-33.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-33.cs new file mode 100644 index 00000000..057ea47c --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-33.cs @@ -0,0 +1,8 @@ +protected override void Update(GameTime gameTime) +{ + // ... + + DeferredCompositeMaterial.Update(); + + base.Update(gameTime); +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-34.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-34.cs new file mode 100644 index 00000000..bd72248f --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-34.cs @@ -0,0 +1,9 @@ +public void DrawComposite() +{ + var viewportBounds = Core.GraphicsDevice.Viewport.Bounds; + Core.SpriteBatch.Begin( + effect: Core.DeferredCompositeMaterial.Effect + ); + Core.SpriteBatch.Draw(ColorBuffer, viewportBounds, Color.White); + Core.SpriteBatch.End(); +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-35.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-35.cs new file mode 100644 index 00000000..b04b038f --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-35.cs @@ -0,0 +1,10 @@ +public override void Draw(GameTime gameTime) +{ + // ... + + _deferredRenderer.Finish(); + _deferredRenderer.DrawComposite(); + + // Draw the UI + _ui.Draw(); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-36.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-36.hlsl new file mode 100644 index 00000000..99c92ab6 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-36.hlsl @@ -0,0 +1,5 @@ +Texture2D LightBuffer; +sampler2D LightBufferSampler = sampler_state +{ + Texture = ; +}; diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-37.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-37.cs new file mode 100644 index 00000000..f65f908e --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-37.cs @@ -0,0 +1,10 @@ +public void DrawComposite() +{ + Core.DeferredCompositeMaterial.SetParameter("LightBuffer", LightBuffer); + var viewportBounds = Core.GraphicsDevice.Viewport.Bounds; + Core.SpriteBatch.Begin( + effect: Core.DeferredCompositeMaterial.Effect + ); + Core.SpriteBatch.Draw(ColorBuffer, viewportBounds, Color.White); + Core.SpriteBatch.End(); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-38.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-38.hlsl new file mode 100644 index 00000000..f2ea91aa --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-38.hlsl @@ -0,0 +1,7 @@ +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float4 color = tex2D(SpriteTextureSampler,input.TextureCoordinates) * input.Color; + float4 light = tex2D(LightBufferSampler,input.TextureCoordinates) * input.Color; + + return color * light; +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-39.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-39.hlsl new file mode 100644 index 00000000..2a038dfe --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-39.hlsl @@ -0,0 +1,10 @@ +float AmbientLight; + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float4 color = tex2D(SpriteTextureSampler,input.TextureCoordinates) * input.Color; + float4 light = tex2D(LightBufferSampler,input.TextureCoordinates) * input.Color; + + light = saturate(light + AmbientLight); + return color * light; +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-40.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-40.cs new file mode 100644 index 00000000..f5b42b21 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-40.cs @@ -0,0 +1,6 @@ +public void DrawComposite(float ambient=.4f) +{ + Core.DeferredCompositeMaterial.SetParameter("AmbientLight", ambient); + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-41.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-41.cs new file mode 100644 index 00000000..c9996846 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-41.cs @@ -0,0 +1,4 @@ +/// +/// A texture that holds the normal sprite drawings +/// +public RenderTarget2D NormalBuffer { get; set; } diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-42.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-42.cs new file mode 100644 index 00000000..87f8df32 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-42.cs @@ -0,0 +1,12 @@ +public DeferredRenderer() +{ + // ... + + NormalBuffer = new RenderTarget2D( + graphicsDevice: Core.GraphicsDevice, + width: viewport.Width, + height: viewport.Height, + mipMap: false, + preferredFormat: SurfaceFormat.Color, + preferredDepthFormat: DepthFormat.None); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-43.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-43.hlsl new file mode 100644 index 00000000..bf83acc2 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-43.hlsl @@ -0,0 +1,4 @@ +struct PixelShaderOutput { + float4 color: COLOR0; + float4 normal: COLOR1; +}; diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-44.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-44.hlsl new file mode 100644 index 00000000..634a2c04 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-44.hlsl @@ -0,0 +1,7 @@ +PixelShaderOutput MainPS(VertexShaderOutput input) +{ + PixelShaderOutput output; + output.color = ColorSwapPS(input); + output.normal = float4(1, 0, 0, 1); // for now, hard-code the normal to be red. + return output; +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-45.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-45.hlsl new file mode 100644 index 00000000..ac8e4706 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-45.hlsl @@ -0,0 +1,8 @@ +technique SpriteDrawing +{ + pass P0 + { + VertexShader = compile VS_SHADERMODEL MainVS(); + PixelShader = compile PS_SHADERMODEL MainPS(); + } +}; diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-46.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-46.cs new file mode 100644 index 00000000..77e52623 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-46.cs @@ -0,0 +1,13 @@ +public void StartColorPhase() +{ + // all future draw calls will be drawn to the color buffer and normal buffer + Core.GraphicsDevice.SetRenderTargets(new RenderTargetBinding[] + { + // gets the results from shader semantic COLOR0 + new RenderTargetBinding(ColorBuffer), + + // gets the results from shader semantic COLOR1 + new RenderTargetBinding(NormalBuffer) + }); + Core.GraphicsDevice.Clear(Color.Transparent); +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-47.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-47.cs new file mode 100644 index 00000000..f0ffa6c0 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-47.cs @@ -0,0 +1,25 @@ +public void DebugDraw() +{ + // ... + + // the debug view for the normal buffer lives in the bottom-left. + var normalBorderRect = new Rectangle( + x: viewportBounds.X, + y: viewportBounds.Height / 2, + width: viewportBounds.Width / 2, + height: viewportBounds.Height / 2); + + // shrink the normal rect by 8 pixels + var normalRect = normalBorderRect; + normalRect.Inflate(-8, -8); + + // ... + + // draw a debug border for the normal buffer + Core.SpriteBatch.Draw(Core.Pixel, normalBorderRect, Color.MintCream); + + // draw the normal buffer + Core.SpriteBatch.Draw(NormalBuffer, normalRect, Color.White); + + Core.SpriteBatch.End(); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-48.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-48.cs new file mode 100644 index 00000000..a43dab47 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-48.cs @@ -0,0 +1,2 @@ +// The normal texture atlas +private Texture2D _normalAtlas; diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-49.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-49.cs new file mode 100644 index 00000000..3a0850a0 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-49.cs @@ -0,0 +1,15 @@ +public override void LoadContent() +{ + // ... + + // Load the normal maps + _normalAtlas = Content.Load("images/atlas-normal"); + + _gameMaterial = Content.WatchMaterial("effects/gameEffect"); + _gameMaterial.IsDebugVisible = false; + _gameMaterial.SetParameter("ColorMap", _colorMap); + _camera = new SpriteCamera3d(); + _gameMaterial.SetParameter("MatrixTransform", _camera.CalculateMatrixTransform()); + _gameMaterial.SetParameter("ScreenSize", new Vector2(Core.GraphicsDevice.Viewport.Width, Core.GraphicsDevice.Viewport.Height)); + _gameMaterial.SetParameter("NormalMap", _normalAtlas); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-50.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-50.cs new file mode 100644 index 00000000..c7050abd --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-50.cs @@ -0,0 +1,13 @@ +public override void LoadContent() +{ + // ... + + // Load the normal maps + _normalAtlas = Content.Load("images/atlas-normal"); + + // ... + + _gameMaterial.SetParameter("NormalMap", _normalAtlas); + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-51.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-51.hlsl new file mode 100644 index 00000000..06b16ec2 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-51.hlsl @@ -0,0 +1,5 @@ +Texture2D NormalMap; +sampler2D NormalMapSampler = sampler_state +{ + Texture = ; +}; diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-52.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-52.hlsl new file mode 100644 index 00000000..b92282b6 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-52.hlsl @@ -0,0 +1,11 @@ +PixelShaderOutput MainPS(VertexShaderOutput input) +{ + PixelShaderOutput output; + output.color = ColorSwapPS(input); + + // read the normal data from the NormalMap + float4 normal = tex2D(NormalMapSampler,input.TextureCoordinates); + output.normal = normal; + + return output; +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-53.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-53.cs new file mode 100644 index 00000000..f74b04fa --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-53.cs @@ -0,0 +1,6 @@ +public static void Draw(SpriteBatch spriteBatch, List pointLights, Texture2D normalBuffer) +{ + Core.PointLightMaterial.SetParameter("NormalBuffer", normalBuffer); + // ... + +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-54.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-54.cs new file mode 100644 index 00000000..292cd927 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-54.cs @@ -0,0 +1,10 @@ +public override void Draw(GameTime gameTime) +{ + // ... + + // start rendering the lights + _deferredRenderer.StartLightPhase(); + PointLight.Draw(Core.SpriteBatch, _lights, _deferredRenderer.NormalBuffer); + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-55.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-55.hlsl new file mode 100644 index 00000000..76c6d450 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-55.hlsl @@ -0,0 +1,5 @@ +Texture2D NormalBuffer; +sampler2D NormalBufferSampler = sampler_state +{ + Texture = ; +}; diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-56.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-56.hlsl new file mode 100644 index 00000000..d4bd9b60 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-56.hlsl @@ -0,0 +1 @@ +#include "3dEffect.fxh" diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-57.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-57.hlsl new file mode 100644 index 00000000..f4067b57 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-57.hlsl @@ -0,0 +1,7 @@ +struct LightVertexShaderOutput +{ + float4 Position : SV_POSITION; + float4 Color : COLOR0; + float2 TextureCoordinates : TEXCOORD0; + float3 ScreenData : TEXCOORD1; +}; diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-58.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-58.hlsl new file mode 100644 index 00000000..604f2a56 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-58.hlsl @@ -0,0 +1,17 @@ +LightVertexShaderOutput LightVS(VertexShaderInput input) +{ + LightVertexShaderOutput output; + + VertexShaderOutput mainVsOutput = MainVS(input); + + // forward along the existing values from the MainVS's output + output.Position = mainVsOutput.Position;// / mainVsOutput.Position.w; + output.Color = mainVsOutput.Color; + output.TextureCoordinates = mainVsOutput.TextureCoordinates; + + // pack the required position variables, x, y, and w, into the ScreenData + output.ScreenData.xy = output.Position.xy; + output.ScreenData.z = output.Position.w; + + return output; +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-59.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-59.hlsl new file mode 100644 index 00000000..bdcb0afe --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-59.hlsl @@ -0,0 +1,8 @@ +technique SpriteDrawing +{ + pass P0 + { + VertexShader = compile VS_SHADERMODEL LightVS(); + PixelShader = compile PS_SHADERMODEL MainPS(); + } +}; diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-60.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-60.hlsl new file mode 100644 index 00000000..61f84ced --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-60.hlsl @@ -0,0 +1,4 @@ +float4 MainPS(LightVertexShaderOutput input) : COLOR +{ + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-61.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-61.hlsl new file mode 100644 index 00000000..59828b82 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-61.hlsl @@ -0,0 +1,11 @@ +float4 MainPS(LightVertexShaderOutput input) : COLOR +{ + // correct the perspective divide. + input.ScreenData /= input.ScreenData.z; + + // put the clip-space coordinates into screen space. + float2 screenCoords = .5*(input.ScreenData.xy + 1); + screenCoords.y = 1 - screenCoords.y; + + return float4(screenCoords, 0, 1); +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-62.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-62.cs new file mode 100644 index 00000000..6e1f4209 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-62.cs @@ -0,0 +1,6 @@ +// _gameMaterial.SetParameter("MatrixTransform", _camera.CalculateMatrixTransform()); <- Replace this line with: + +var matrixTransform = _camera.CalculateMatrixTransform(); +_gameMaterial.SetParameter("MatrixTransform", matrixTransform); +Core.PointLightMaterial.SetParameter("MatrixTransform", matrixTransform); +Core.PointLightMaterial.SetParameter("ScreenSize", new Vector2(Core.GraphicsDevice.Viewport.Width, Core.GraphicsDevice.Viewport.Height)); diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-63.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-63.hlsl new file mode 100644 index 00000000..81d5cbe6 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-63.hlsl @@ -0,0 +1,12 @@ +float4 MainPS(LightVertexShaderOutput input) : COLOR +{ + // correct the perspective divide. + input.ScreenData /= input.ScreenData.z; + + // put the clip-space coordinates into screen space. + float2 screenCoords = .5*(input.ScreenData.xy + 1); + screenCoords.y = 1 - screenCoords.y; + + float4 normal = tex2D(NormalBufferSampler, screenCoords); + return normal; +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-64.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-64.cs new file mode 100644 index 00000000..19a788eb --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-64.cs @@ -0,0 +1,16 @@ +public static void Draw(SpriteBatch spriteBatch, List pointLights, Texture2D normalBuffer) +{ + spriteBatch.Begin( + effect: Core.PointLightMaterial.Effect, + blendState: BlendState.Additive + ); + + foreach (var light in pointLights) + { + var diameter = light.Radius * 2; + var rect = new Rectangle((int)(light.Position.X - light.Radius), (int)(light.Position.Y - light.Radius), diameter, diameter); + spriteBatch.Draw(normalBuffer, rect, light.Color); + } + + spriteBatch.End(); +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-65.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-65.hlsl new file mode 100644 index 00000000..5d57fff3 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-65.hlsl @@ -0,0 +1,31 @@ +float4 MainPS(LightVertexShaderOutput input) : COLOR { + float dist = length(input.TextureCoordinates - .5); + float range = 5; // arbitrary maximum. + + float falloff = saturate(.5 - dist) * (LightBrightness * range + 1); + falloff = pow(abs(falloff), LightSharpness * range + 1); + + // correct the perspective divide. + input.ScreenData /= input.ScreenData.z; + + // put the clip-space coordinates into screen space. + float2 screenCoords = .5*(input.ScreenData.xy + 1); + screenCoords.y = 1 - screenCoords.y; + + float4 normal = tex2D(NormalBufferSampler,screenCoords); + // flip the y of the normals, because the art assets have them backwards. + normal.y = 1 - normal.y; + + // convert from [0,1] to [-1,1] + float3 normalDir = (normal.xyz-.5)*2; + + // find the direction the light is travelling at the current pixel + float3 lightDir = float3(normalize(.5 - input.TextureCoordinates), 1); + + // how much is the normal direction pointing towards the light direction? + float lightAmount = (dot(normalDir, lightDir)); + + float4 color = input.Color; + color.a *= falloff * lightAmount; + return color; +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-66.hlsl b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-66.hlsl new file mode 100644 index 00000000..1c80bbd4 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-66.hlsl @@ -0,0 +1,9 @@ + +float4 MainPS(LightVertexShaderOutput input) : COLOR +{ + // ... + + float4 color = input.Color; + color.a *= falloff * lightAmount; + return color; +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-67-initialize.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-67-initialize.cs new file mode 100644 index 00000000..b1c36a04 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-67-initialize.cs @@ -0,0 +1,14 @@ +public override void Initialize() +{ + // ... + + // Replace this..... + // _lights.Add(new PointLight + // { + // Position = new Vector2(300, 300), + // Color = Color.CornflowerBlue + // }); + + // With this new call to initialize lights for the scene + InitializeLights(); +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-67.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-67.cs new file mode 100644 index 00000000..1118320c --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-67.cs @@ -0,0 +1,45 @@ +private void InitializeLights() +{ + // torch 1 + _lights.Add(new PointLight + { + Position = new Vector2(260, 100), + Color = Color.CornflowerBlue, + Radius = 500 + }); + // torch 2 + _lights.Add(new PointLight + { + Position = new Vector2(520, 100), + Color = Color.CornflowerBlue, + Radius = 500 + }); + // torch 3 + _lights.Add(new PointLight + { + Position = new Vector2(740, 100), + Color = Color.CornflowerBlue, + Radius = 500 + }); + // torch 4 + _lights.Add(new PointLight + { + Position = new Vector2(1000, 100), + Color = Color.CornflowerBlue, + Radius = 500 + }); + + // random lights + _lights.Add(new PointLight + { + Position = new Vector2(Random.Shared.Next(50, 400),400), + Color = Color.MonoGameOrange, + Radius = 500 + }); + _lights.Add(new PointLight + { + Position = new Vector2(Random.Shared.Next(650, 1200),300), + Color = Color.MonoGameOrange, + Radius = 500 + }); +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-68.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-68.cs new file mode 100644 index 00000000..0f29d77d --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-68.cs @@ -0,0 +1,12 @@ +private void MoveLightsAround(GameTime gameTime) +{ + var t = (float)gameTime.TotalGameTime.TotalSeconds * .25f; + var bounds = Core.GraphicsDevice.Viewport.Bounds; + bounds.Inflate(-100, -100); + + var halfWidth = bounds.Width / 2; + var halfHeight = bounds.Height / 2; + var center = new Vector2(halfWidth, halfHeight); + _lights[^1].Position = center + new Vector2(halfWidth * MathF.Cos(t), .7f * halfHeight * MathF.Sin(t * 1.1f)); + _lights[^2].Position = center + new Vector2(halfWidth * MathF.Cos(t + MathHelper.Pi), halfHeight * MathF.Sin(t - MathHelper.Pi)); +} diff --git a/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-69.cs b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-69.cs new file mode 100644 index 00000000..6f5708b7 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/08_light_effect/snippets/snippet-8-69.cs @@ -0,0 +1,7 @@ +public override void Update(GameTime gameTime) +{ + // ... + + // Move some lights around for artistic effect + MoveLightsAround(gameTime); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/bat_shadow.gif b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/bat_shadow.gif new file mode 100644 index 00000000..9799817c Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/bat_shadow.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/box-blur-extreme.gif b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/box-blur-extreme.gif new file mode 100644 index 00000000..46867bf3 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/box-blur-extreme.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/less-is-more.gif b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/less-is-more.gif new file mode 100644 index 00000000..b3ac7891 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/less-is-more.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/overview.gif b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/overview.gif new file mode 100644 index 00000000..a0260e16 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/overview.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/shadow-intensity.gif b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/shadow-intensity.gif new file mode 100644 index 00000000..8ba2fd3b Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/shadow-intensity.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/shadow-length.gif b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/shadow-length.gif new file mode 100644 index 00000000..0e65a250 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/shadow-length.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/shadow-no-self.gif b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/shadow-no-self.gif new file mode 100644 index 00000000..ff485fcb Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/shadow-no-self.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/snake_shadow.gif b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/snake_shadow.gif new file mode 100644 index 00000000..7848e77e Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/snake_shadow.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/stencil_working.gif b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/stencil_working.gif new file mode 100644 index 00000000..514e3f6d Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/stencil_working.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/wall_shadow.gif b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/wall_shadow.gif new file mode 100644 index 00000000..e2909c9b Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/gifs/wall_shadow.gif differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_light.pdn b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_light.pdn new file mode 100644 index 00000000..576f9fc0 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_light.pdn differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_light_map.png b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_light_map.png new file mode 100644 index 00000000..42d50093 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_light_map.png differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_light_map_multiplied.png b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_light_map_multiplied.png new file mode 100644 index 00000000..90890b0c Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_light_map_multiplied.png differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_light_shadow.pdn b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_light_shadow.pdn new file mode 100644 index 00000000..b30ad57a Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_light_shadow.pdn differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_light_shadow.png b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_light_shadow.png new file mode 100644 index 00000000..3b3f04f5 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_light_shadow.png differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_shadow_map.pdn b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_shadow_map.pdn new file mode 100644 index 00000000..b83e1d40 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_shadow_map.pdn differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_shadow_map.png b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_shadow_map.png new file mode 100644 index 00000000..acc0b28e Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/dbg_shadow_map.png differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/light_math.pdn b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/light_math.pdn new file mode 100644 index 00000000..41a03236 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/light_math.pdn differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/light_math.png b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/light_math.png new file mode 100644 index 00000000..cd31c7f0 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/light_math.png differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/mgcb.png b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/mgcb.png new file mode 100644 index 00000000..113feef2 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/mgcb.png differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/pixel_math.pdn b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/pixel_math.pdn new file mode 100644 index 00000000..f7e22072 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/pixel_math.pdn differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/pixel_math.png b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/pixel_math.png new file mode 100644 index 00000000..f6fdb22b Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/pixel_math.png differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map.png b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map.png new file mode 100644 index 00000000..e9ceb776 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map.png differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map_backwards.png b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map_backwards.png new file mode 100644 index 00000000..6cab9e07 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map_backwards.png differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map_blank.png b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map_blank.png new file mode 100644 index 00000000..aecc11c0 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map_blank.png differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map_hex.png b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map_hex.png new file mode 100644 index 00000000..28b932a7 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map_hex.png differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map_hex_2.png b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map_hex_2.png new file mode 100644 index 00000000..86ca72d7 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map_hex_2.png differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map_hex_3.png b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map_hex_3.png new file mode 100644 index 00000000..aadc4888 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map_hex_3.png differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map_working.png b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map_working.png new file mode 100644 index 00000000..22186e04 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/shadow_map_working.png differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/starting.png b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/starting.png new file mode 100644 index 00000000..f92d965f Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/starting.png differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/stencil_blank.png b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/stencil_blank.png new file mode 100644 index 00000000..7e1eaa14 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/stencil_blank.png differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/stencil_blend.png b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/stencil_blend.png new file mode 100644 index 00000000..1c9c95e4 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/stencil_blend.png differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/stencil_lights.png b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/stencil_lights.png new file mode 100644 index 00000000..6ae020ea Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/stencil_lights.png differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/stencil_noclear.png b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/stencil_noclear.png new file mode 100644 index 00000000..9af300fd Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/stencil_noclear.png differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/stencil_pre.png b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/stencil_pre.png new file mode 100644 index 00000000..0a241296 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/images/stencil_pre.png differ diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/index.md b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/index.md new file mode 100644 index 00000000..9f99407e --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/index.md @@ -0,0 +1,901 @@ +--- +title: "Chapter 09: Shadow Effect" +description: "Add dynamic shadows to the game" +--- + +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. + +In this final effects chapter: + +* We are 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. + +By the end of this chapter, your game will look something like this: + +| ![Figure 9-1: The final shadow effect](./gifs/overview.gif) | +| :---------------------------------------------------------: | +| **Figure 9-1: The final shadow effect** | + +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/08-Light-Effect). + +## 2D Shadows + +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. + +| ![Figure 9-2: A light with no shadows](./images/starting.png) | +| :-----------------------------------------------------------: | +| **Figure 9-2: A light with no shadows** | + +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. + +![9.2: A hand drawn shadow](./images/dbg_light_shadow.png) + +The pink section is called the _shadow hull_. We can split up the entire effect into two distinct stages. + +1. We need a way to calculate and render the shadow hulls from all objects that we want to cast shadows (such as bats and slime segments), +2. We need a way to _use_ the shadow hull to actually mask the lighting from the previous chapter. + +Step 2 is actually a lot easier to understand than step 1. Imagine that the shadow hulls were drawn to an off-screen texture, like the one in the image below. The black sections represent shadow hulls, and the white sections are places where no shadow hulls exist. + +This resource is called the `ShadowBuffer`. + +| ![Figure 9-3: A shadow map](./images/dbg_shadow_map.png) | +| :------------------------------------------------------: | +| **Figure 9-3: A shadow map** | + +We would need to have a `ShadowBuffer` for each light source, and if we did, then when the light was being rendered, we could pass in the `ShadowBuffer` as an additional texture resource to the `_pointLightEffect.fx`, and use the pixel value of the `ShadowBuffer` to mask the light source. + +In the sequence below, the left image is just the `LightBuffer`. The middle image is the `ShadowBuffer`, and the right image is the product of the two images. Any pixel in the `ShadowBuffer` that was `white` means the final image uses the color from the `LightBuffer`, and any `black` pixel from the `ShadowBuffer` becomes black in the final image as well. The multiplication of the `LightBuffer` and `ShadowBuffer` complete the shadow effect. + +
+ +| ![Figure 9-4: a light buffer](./images/dbg_light_map.png) | ![Figure 9-5: A shadow map](./images/dbg_shadow_map.png) | ![Figure 9-6: The multiplication](./images/dbg_light_map_multiplied.png) | +| --------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------ | +| **Figure 9-4: The `LightBuffer`** | **Figure 9-5: The `ShadowBuffer`** | **Figure 9-6: The multiplication of the two images** | + +The mystery to unpack is step 1, how to render the `ShadowBuffer` in the first place. + +## Rendering the Shadow Buffer + +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 below, there is a single light source at position `L`, and a line segment between points `A`, and `B`. + +| ![Figure 9-7: A diagram of a simple light and line segment](./images/light_math.png) | +| :----------------------------------------------------------------------------------: | +| **Figure 9-7: A diagram of a simple light and line segment** | + +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. + +> [!NOTE] +> `A` and `a` naming convention. +> +> The `A` and `a` points lay on the same ray from the light starting at `L`. The uppercase `A` denotes that the position is _first_ from the light's point of view. The same pattern holds for `B` and `b`. + +However, the `SpriteBatch` usually only renders rectangular shapes. By default, it appears `SpriteBatch` cannot help us draw these sorts of shapes, but fortunately, since the shadow hull has exactly _4_ vertices, and `SpriteBatch` draws quads with exactly _4_ vertices, we can use a custom vertex function. + +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`. + +| ![Figure 9-8: A diagram showing a pixel](./images/pixel_math.png) | +| :---------------------------------------------------------------: | +| **Figure 9-8: A diagram showing a pixel** | + +Our goal is to 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. + +| Pixel Point | Shadow Hull Point | +| ----------- | ----------------- | +| S | A | +| D | a | +| F | b | +| G | B | + +Each vertex (`S`, `D`, `F`, and `G`) has additional metadata beyond positional data. The diagram includes `P`, but that point is the point specified to _`SpriteBatch`_, and it is not available in the shader function. The vertex shader runs once for each vertex, but completely in isolation of the other vertices. Remember, the input for the standard vertex shader is as follows: + +[!code-hlsl[](./snippets/snippet-9-01.hlsl)] + +The `TexCoord` data is a two dimensional value that tells the pixel shader how to map an image onto the rectangle. The values for `TexCoord` can be set by the `SpriteBatch`'s `sourceRectangle` field in the `Draw()`, but if left unset, they default to `0` through `1` values. The default mapping is in the table below, + +| Vertex | TexCoord.x | TexCoord.y | +| ------ | ---------- | ---------- | +| S | 0 | 0 | +| D | 1 | 0 | +| F | 1 | 1 | +| G | 0 | 1 | + +If we use the defaults, then we could use these values to compute a unique ID for each vertex in the pixel. The function, `x + y*2` will produce a unique hash of the inputs for the domain we care about. The following table shows the unique values. + +| Vertex | TexCoord.x | TexCoord.y | unique ID | +| ------ | ---------- | ---------- | --------- | +| S | 0 | 0 | 0 | +| D | 1 | 0 | 1 | +| F | 1 | 1 | 3 | +| G | 0 | 1 | 2 | + +The unique value is important, because it gives the vertex shader the ability to know _which_ vertex is being processed, rather than _any_ arbitrary vertex. For example, now the shader can know if it is processing `S`, or `D` based on if the unique ID is `0` and `1`. The math for mapping `S` --> `A` may be quite different than the math for mapping `D` --> `a`. + +Additionally, the default `TexCoord` values allow the vertex shader to take any arbitrary positions, (`S`, `D`, `F`, and `G`), and produce the point `P` where the `SpriteBatch` is drawing the sprite in world space. If you recall from the previous chapter, MonoGame uses the screen size as a basis for generating world space positions, and then the default projection matrix transforms those world space positions into clip space. Given a shader parameter, `float2 ScreenSize`, the vertex shader can convert back from the world-space positions (`S`, `D`, `F`, and `G`) to the `P` position by subtracting `.5 * ScreenSize * TexCoord` from the current vertex. + +The `Color` data is _usually_ used to tint the resulting sprite in the pixel shader, but in our use case, for a shadow hull we do not really need a color whatsoever. Instead, we can use this `float4` field as arbitrary data. The trick is that we will need to pack whatever data we need into a `float4` and pass it via the `Color` type in MonoGame. This color comes from the `Color` value passed to the `SpriteBatch`'s `Draw()` call. + +The `Position` and `Color` both use `float4` in the standard vertex shader input, and it _may_ appear as though they should have the same precision, however, they are not passed from MonoGame's `SpriteBatch` as the same type. When `SpriteBatch` goes to draw a sprite, it uses a `Color` for the `Color`, and a `Vector3` for the `Position`. A `Color` has 4 `bytes`, but a `Vector3` has 12 `bytes`. This can be seen in the [`VertexPositionColorTexture`](https://github.com/MonoGame/MonoGame/blob/develop/MonoGame.Framework/Graphics/Vertices/VertexPositionColorTexture.cs#L103) class. The takeaway is that we can only pack a third as much data into the `Color` semantic as the `Position` gets, and that may limit the types of values we want to pack into the `Color` value. + +Finally, the light's position must be provided as a shader parameter, `float2 LightPosition`. The light's position should be in the same world-space coordinate system in which the light is drawn. + +### Vertex Shader Theory + +Now that we have a good understanding of the available inputs, and the goal of the vertex function, we can begin moving towards a solution. Unlike the previous chapters, we are going to build up a fair bit of math before converting any of this is to a working shader. + +To begin: + +* We draw the pixel _at_ the start of the line segment itself, `A`. +* The position where the pixel is drawn is definitionally `P` +* Drawing the pixel at `A`, we have set `A = P`. + +Every point (`S`, `D`, `F`, and `G`) needs to find `P`. To do that, the `TexCoord` can be treated as a direction from `P` to the current point, and the `ScreenSize` shader parameter can be used to find the right amount of distance to travel along that direction: + +> [!NOTE] +> The next few snippets of shader code are pseudocode. Just follow along with the text and the full shader will be available later in the next section. + +[!code-hlsl[](./snippets/snippet-9-02.hlsl)] + +Next, we pack the `Color` value as the vector `(B - A)`: + +* The `x` component of the vector can live in the `red` and `green` channels of the `Color`. +* The `y` component will live in the `blue` and `alpha` channels. +* In the vertex shader, the `B` can be derived by unpacking the `(B - A)` vector from the `COLOR` semantic and _adding_ it to the `A`. + +The reason we pack the _difference_ between `B` and `A` into the `Color`, and not `B` itself is due to the lack of precision in the `Color` type. There are only 4 `bytes` to pack all the information, which means 2 `bytes` per `x` and `y`. Likely, the line segment will be small, so the values of `(B - A)` will fit easier into a 2-`byte` channel: + +[!code-hlsl[](./snippets/snippet-9-03.hlsl)] + +The point `a` must lay _somewhere_ on the ray cast from the `LightPosition` to the start of the line segment, `A`. Additionally, the point `a` must lay _beyond_ `A` from the light's perspective. The direction of the ray can be calculated as: + +[!code-hlsl[](./snippets/snippet-9-04.hlsl)] + +Then, given some `distance`, beyond `A`, the point `a` can be produced as: + +[!code-hlsl[](./snippets/snippet-9-05.hlsl)] + +The same can be said for `b`: + +[!code-hlsl[](./snippets/snippet-9-06.hlsl)] + +Now the vertex shader function knows all positions, `A`, `a`, `b`, and `B`. The `TexCoord` can be used to derive a unique ID, and the unique ID can be used to select one of the points: + +[!code-hlsl[](./snippets/snippet-9-07.hlsl)] + +Once all of the positions are mapped, our goal is complete! We have a vertex function and strategy to convert a single pixel's 4 vertices into the 4 vertices of a shadow hull! + +### Implementation + +To start implementing the effect, create a new Sprite Effect in the `MonoGameLibrary`'s _SharedContent_ effect folder called `shadowHullEffect.fx`. Load it into the `Core` class as before in the previous chapters. + +> [!NOTE] +> As we did back in [Chapter 6](../06_color_swap_effect/index.md#hard-coding-color-swaps), temporarily disable the `GameScene` update by adding a `return;` statement AFTER setting all the material properties. Just to make it easier when looking at the shadow effect. Or, add the game pause mechanic from [Chapter 8](../08_light_effect/index.md), and make it start paused. +> +> Otherwise, the shadows are going to be hard to develop, because they will be moving around as the game plays out. + +| ![Figure 9-9: Create the `shadowHullEffect` in MGCB](./images/mgcb.png) | +| :---------------------------------------------------------------------: | +| **Figure 9-9: Create the `shadowHullEffect` in MGCB** | + +1. Add the following `ShadowHullMaterial` property to the `Core.cs` class in the `MonoGameLibrary` project and replace its contents with the following: + + [!code-csharp[](./snippets/snippet-9-08.cs)] + +2. Load it as watched content in the `LoadContent()` method: + + [!code-csharp[](./snippets/snippet-9-09.cs)] + +3. Finally, update the `Update()` method on the `Material` in the `Core`'s `Update()` method. Without this, hot-reload will not work: + + [!code-csharp[](./snippets/snippet-9-10.cs)] + +### The Shadow caster + +To represent the shadow casting objects in the game, we will create a new class called `ShadowCaster` in the _MonoGameLibrary_'s graphics folder. For now, keep the `ShadowCaster` class as simple as possible while we build the basics. It will just hold the positions of the line segment from the theory section, `A`, and `B`. Create the class and follow the steps to integrate it with the rest of the project. + +1. Create a new class called `ShadowCaster.cs` in the `MonoGameLibrary` project under the `Graphics` folder: + + [!code-csharp[](./snippets/snippet-9-11.cs)] + +2. In the `GameScene`, add a class member to hold all the various `ShadowCasters` that will exist in the game: + + [!code-csharp[](./snippets/snippet-9-12.cs)] + +3. For now, to keep things simple, re-configure the `InitializeLights()` function in the `GameScene` to have a single `PointLight` and a single `ShadowCaster`: + + [!code-csharp[](./snippets/snippet-9-13.cs)] + + > [!TIP] + > If you are not pausing the game logic, make sure to comment out the `MoveLightsAround()` function, because it assumes there will be more than 1 light. + +4. Every `PointLight` needs its own `ShadowBuffer`. If you recall, the `ShadowBuffer` is an off-screen texture that will have _white_ pixels where the light is visible, and _black_ pixels where light is not visible due to a shadow. + + Open the `PointLight` class in the `MonoGameLibrary` project and add a new `RenderTarget2D` field: + + [!code-csharp[](./snippets/snippet-9-14.cs)] + +5. And instantiate the `ShaderBuffer` in a new constructor to the `PointLight` class: + + [!code-csharp[](./snippets/snippet-9-15.cs)] + +6. Now, we need to find a place to render the `ShadowBuffer` _per_ `PointLight` before the deferred renderer draws the light itself. + + Copy this function into the `PointLight` class: + + [!code-csharp[](./snippets/snippet-9-16.cs)] + + > [!warning] + > + > The `(B-A)` vector is not being packed into the color channel, **_yet_**. + > + > We will come back to that soon! + +7. Next, create a second method in the `PointLight` class that will call the `DrawShadowBuffer` function for a list of lights and shadow casters: + + [!code-csharp[](./snippets/snippet-9-17.cs)] + +8. Finally, back in the `GameScene` class, call the `DrawShadows()` method in the `Draw` call, right before the `DeferredRenderer`'s `StartLightPass()` method: + + [!code-csharp[](./snippets/snippet-9-18.cs?highlight=7)] + +9. For debug visualization purposes, also add this snippet to the end of the `GameScene`'s `Draw()` just so you can see the `ShaderBuffer` as we debug it: + + [!code-csharp[](./snippets/snippet-9-19.cs?hightlight=5-7)] + +When you run the game, you will see a totally blank game (other than the GUI). 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. + +| ![Figure 9-10: A blank shadow buffer](./images/shadow_map_blank.png) | +| :-----------------------------------------------------------------: | +| **Figure 9-10: A blank shadow buffer** | + +> [!WARNING] +> If you have set your project back to the `Exe` in the `.csproj` file, then you will see some warnings like these: +> ``` +> Warning: cannot set shader parameter=[LightPosition] because it does not exist in the compiled shader=[effects/shadowHullEffect] +> ``` +> +> Do not worry, we will be cleaning this up soon! The reason these appear is because we have not implemented the shader yet. + +### Bit Packing + +We cannot implement the vertex shader theory until we can pack the `(B-A)` vector into the `Color` argument for the `SpriteBatch`. For this we will use a technique called _bit-packing_. + +For the sake of brevity, we will skip over the derivation of these functions. + +> [!TIP] +> _Bit-packing_ is a broad category of algorithms that change the underlying bit representation of some variable. The most basic idea is that all of your variables are just _bits_, and its up to you how you want to arrange them. To learn more, check out the following articles, +> +> 1. [Wikipedia article on Bit Operations](https://en.wikipedia.org/wiki/Bitwise_operation) +> 2. [A quick overview from Cornell](https://www.cs.cornell.edu/courses/cs3410/2024fa/notes/bitpack.html) +> 3. [The Art of Packing Data](https://www.elopezr.com/the-art-of-packing-data/) + +1. Add this function to your `PointLight` class: + + [!code-csharp[](./snippets/snippet-9-20.cs)] + +2. Next we consume the packing function in the `DrawShadowBuffer` function in the `PointLight` class instead of passing `Color.White`. To do that we need to create the `bToA` vector, pack it inside a `Color` instance, and then pass it to the `SpriteBatch`: + + [!code-csharp[](./snippets/snippet-9-21.cs)] + +3. On the shader side, open the `shadowHullEffect.fx` file and add the following function: + + [!code-hlsl[](./snippets/snippet-9-22.hlsl)] + + Now we have the tools to start implementing the vertex shader! Anytime you want to override the default `SpriteBatch` vertex shader the shader needs to fulfill the world-space to clip-space transformation. For this we can re-use the work done in the [previous chapter](./../08_light_effect/index.md#combining-normals-with-lights). + +4. Replace the `VertexShaderOutput` struct with the following line `#include "3dEffect.fxh"`. + +5. Add the `ShadowHullVS` vertex shader function derived from the [Vertex Shader theory discussed earlier in the chapter](#vertex-shader-theory): + + [!code-hlsl[](./snippets/snippet-9-27.hlsl)] + +6. Set the technique for the vertex shader function: + + [!code-hlsl[](./snippets/snippet-9-24.hlsl?highlight=6)] + +7. Update the pixel shader function for the `shadowHullEffect`, as it needs to ignore the `input.Color` and just return a solid color: + + [!code-hlsl[](./snippets/snippet-9-26.hlsl)] + +8. The last step to make sure the default vertex shader works, is to pass the `MatrixTransform` and `ScreenSize` shader parameters in the `GameScene`'s `Update()` loop, next to where they are being configured for the existing `PointLightMaterial`: + + [!code-csharp[](./snippets/snippet-9-25.cs?highlight=11-12)] + + > [!WARNING] + > If you added a pause mechanic to the game for development, **make sure** the `Update()` is able to set all the shader parameters before the early `return` statement. Otherwise you may **see a black screen** instead of the expected effect. + +Now if you run the game, you will see the white shadow hull. + +| ![Figure 9-11: A shadow hull](./images/shadow_map.png) | +| :---------------------------------------------------: | +| **Figure 9-11: A shadow hull** | + +### Integrating the shadow with the lighting + +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. + +1. Add an additional texture and sampler to the `pointLightEffect.fx` file: + + [!code-hlsl[](./snippets/snippet-9-28.hlsl)] + +2. Then, in the `MainPS` of the light effect, read the current value from the shadow buffer and use it as a multiplier at the end when calculating the final light color: + + [!code-hlsl[](./snippets/snippet-9-29.hlsl?highlight=7,14)] + +3. Before running the game, we need to pass the `ShadowBuffer` to the point light's draw invocation. + + In the `Draw()` method in the `PointLight` class, change the `SpriteBatch` to use `Immediate` sorting, and forward the `ShadowBuffer` to the shader parameter for each light: + + [!code-csharp[](./snippets/snippet-9-31.cs?highlight=6,11)] + + Disable the debug visualization to render the `ShadowMap` on top of everything else at the end of the `Draw` method in the `GameScene` class, and run the game. + + | ![Figure 9-8: The light is appearing inverted](./images/shadow_map_backwards.png) | + | :--------------------------------------------------------------------------------: | + | **Figure 9-8: The light is appearing inverted** | + + Oops, the shadows and lights are appearing opposite of where they should! This is because the `ShadowBuffer` is inverted. + +4. In the `PointLight` class, change the clear color for the `ShadowBuffer` to _white_: + + [!code-csharp[](./snippets/snippet-9-32.cs?highlight=5)] + +5. And change the `ShadowHullEffect` pixel shader to return a solid black rather than white: + + [!code-hlsl[](./snippets/snippet-9-33.hlsl)] + +And now the shadow appears correctly for our simple single line segment! + +| ![Figure 9-12: A working shadow!](./images/shadow_map_working.png) | +| :----------------------------------------------------------------: | +| **Figure 9-12: A working shadow!** | + +## More Segments + +So far, we have built up an implementation for the shadow caster system using a single line segment. Now, we will combine several line segments to create primitive shapes. We will also approximate the slime character as a hexagon. + +1. Instead of only having `A` and `B` in the `ShadowCaster` class, **replace** the class content to use a `Position` and a list of points: + + [!code-csharp[](./snippets/snippet-9-34.cs)] + +2. Then, to create simple polygons add this method to the `ShadowCaster` class: + + [!code-csharp[](./snippets/snippet-9-35.cs)] + +3. Swapping over to the `GameScene` class, in the `InitializeLights()` method, instead of constructing a `ShadowCaster` with the `A` and `B` properties, we can use the new `SimplePolygon` method: + + [!code-csharp[](./snippets/snippet-9-36.cs)] + +4. Finally, the last place we need to change is the `DrawShadowBuffer()` method in the `PointLight` class. Currently it is just drawing a single pixel with the `ShadowHullMaterial`, but now we need to draw a pixel _per_ line segment. + + Update the `foreach` block to loop over all the points in the `ShadowCaster`, and connect the points as line segments: + + [!code-csharp[](./snippets/snippet-9-37.cs)] + + When you run the game, you will see a larger shadow shape. + + | ![Figure 9-10: A shadow hull from a hexagon](./images/shadow_map_hex.png) | + | :-----------------------------------------------------------------------: | + | **Figure 9-10: A shadow hull from a hexagon** | + + 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. + +5. After both `A` and `B` are calculated, but before `a` and `b`, add this to the shader: + + [!code-hlsl[](./snippets/snippet-9-38.hlsl?highlight=7-9)] + + And now the visual artifact has gone away. + + | ![Figure 9-11: The visual artifact has been fixed](./images/shadow_map_hex_2.png) | + | :-------------------------------------------------------------------------------: | + | **Figure 9-11: The visual artifact has been fixed** | + + The next item to consider in the `ShadowHullEffect` shader, is that the "inside" of the slime is not 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: + +6. Add the following immediately after the previous addition in the `ShadowHullEffect` shader: + + [!code-hlsl[](./snippets/snippet-9-39.hlsl)] + + > [!TIP] + > This technique is called [back-face culling](https://en.wikipedia.org/wiki/Back-face_culling). + +7. Then in the pixel shader function, add this line to the top. The [`clip`](https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-clip) function will completely discard the fragment and not draw anything to the `ShadowBuffer`: + + [!code-hlsl[](./snippets/snippet-9-40.hlsl?highlight=2)] + +Now the slime looks well lit and shadowed! In the next section, we will line up the lights and shadows with the rest of the level. + +| ![Figure 9-13: The slime is well lit](./images/shadow_map_hex_3.png) | +| :------------------------------------------------------------------: | +| **Figure 9-13: The slime is well lit** | + +## Gameplay + +Now that we can draw shadows in the lighting system, we should rig up shadows to the slime, the bat, and the walls of the dungeon. + +1. First, start by adding back the `InitializeLights()` method in the `GameScene` class as it existed at the start of the chapter. Feel free to add or remove lights as you see fit. Here is a version of the function: + + [!code-csharp[](./snippets/snippet-9-41.cs)] + +2. Now, we will focus on the slime shadows. Add a new `List` property to the `Slime` class: + + [!code-csharp[](./snippets/snippet-9-42.cs)] + +3. And in the `Slime`'s `Update()` method, add this snippet: + + [!code-csharp[](./snippets/snippet-9-43.cs)] + +4. Now, modify the `GameScene`'s `Draw()` method to replace the existing `PointLight.DrawShadows` call and create a master list of all the `ShadowCasters` and pass that into the `DrawShadows()` function: + + [!code-csharp[](./snippets/snippet-9-44.cs?highlight=8-12)] + +And now the slime has shadows around the segments! + +> [!NOTE] +> You can remove the `return;` from the `Update` method in the `GameScene` class to resume normal gameplay and see the shadows in operation. + +| ![Figure 9-14: The slime has shadows](./gifs/snake_shadow.gif) | +| :------------------------------------------------------------: | +| **Figure 9-14: The slime has shadows** | + +Next up, the bat needs some shadows! + +1. Add a `ShadowCaster` property to the `Bat` class: + + [!code-csharp[](./snippets/snippet-9-45.cs)] + +2. And instantiate it in the constructor: + + [!code-csharp[](./snippets/snippet-9-46.cs?highlight=5)] + +3. In the `Bat`'s `Update()` method, update the position of the `ShadowCaster`: + + [!code-csharp[](./snippets/snippet-9-47.cs)] + +4. And finally add the `ShadowCaster` to the master list of shadow casters during the `GameScene`'s `Draw()` method together with the slimes shadows: + + [!code-csharp[](./snippets/snippet-9-48.cs?highlight=5)] + +And now the bat is casting a shadow as well! + +| ![Figure 9-15: The bat casts a shadow](./gifs/bat_shadow.gif) | +| :-----------------------------------------------------------: | +| **Figure 9-15: The bat casts a shadow** | + +Lastly, the walls should cast shadows to help ground the lighting in the world. +Add a shadow caster in the `InitializeLights()` function to represent the edge of the playable tiles: + +[!code-csharp[](./snippets/snippet-9-49.cs)] + +| ![Figure 9-16: The walls have shadows](./gifs/wall_shadow.gif) | +| :------------------------------------------------------------: | +| **Figure 9-16: The walls have shadows** | + +## The Stencil Buffer + +The light and shadow system is working! However, there is a non-trivial amount of memory overhead for the effect. Every light has a full screen sized `ShadowBuffer`. At the moment, each `ShadowBuffer` is a `RenderTarget2D` with `32` bits of data per pixel. At our screen resolution of `1280` x `720`, that means every light adds roughly (`1280 * 720 * 32bits`) 3.6 _MB_ of overhead to the game! Our system is not taking full advantage of those 32 bits per pixel. Instead, all we really need is a _single_ bit, for "in shadow" or "not in shadow". In fact, all the `ShadowBuffer` is doing is operating as a _mask_ for the point light. + +Image masking is a common task in computer graphics and there is a built-in feature of MonoGame called the _Stencil Buffer_ that handles image masking without the need for any custom `RenderTarget` or shader logic. In fact, we will be able to remove a lot of the existing code and leverage the stencil instead. + +The stencil buffer is a part of an existing `RenderTarget`, but we need to opt into using it. In the `DeferredRenderer` class, where the `LightBuffer` is being instantiated: + +* Change the `preferredDepthFormat` to `DepthFormat.Depth24Stencil8` in the `DeferredRenderer` class: + + [!code-csharp[](./snippets/snippet-9-50.cs?highlight=7)] + +The `LightBuffer` itself has `32` bits per pixel of `Color` data, _and_ an additional `32` bits of data split between the depth and stencil buffers. As the name suggests, the `Depth24Stencil8` format grants the depth buffer `24` bits of data, and the stencil buffer `8` bits of data. `8` bits are enough for a single `byte`, which means it can represent integers from `0` to `255`. + +For our use case, we will deal with the stencil buffer in two distinct steps. + +* First, all of the shadow hulls will be drawn into the stencil buffer _instead_ of a unique `ShadowBuffer`. Anywhere a shadow hull is drawn, the stencil buffer will have a value of `1`, and anywhere without a shadow hull will have a value of `0`. +* Then, in the second step, when the point lights are drawn, the stencil buffer can be used as a mask where pixels are only drawn where the stencil buffer has a value of `0` (which means there was no shadow hull present in the previous step). + +The stencil buffer can be cleared and re-used between each light, so there is no need to have a buffer _per_ light. We will be able to completely remove the `ShadowBuffer` from the `PointLight` class. That also means we will not need to send the `ShadowBuffer` to the point light shader or read from it in shader code any longer. + +1. To get started, create a new method in the `DeferredRenderer` class called `DrawLights()`. This new method is going to completely replace some of our existing methods, but we will clean the unnecessary ones up when we are done with the new approach: + + [!code-csharp[](./snippets/snippet-9-51.cs)] + +2. Add a using for the `System.Collections.Generic` namespace to support the new `List` type: + + ```csharp + using System.Collections.Generic; + ``` + +3. In the `GameScene`'s `Draw()` method, call the new `DrawLights()` method instead of the `DrawShadows()`, `StartLightPhase()` _and_ `PointLight.Draw()` methods. Here is a snippet of the `Draw()` method: + + [!code-csharp[](./snippets/snippet-9-52.cs?highlight=11-12)] + +4. Next, in the `pointLightEffect.fx` shader, we will not be using the `ShadowBuffer` anymore, so remove: + + * The `Texture2D ShadowBuffer` + * The `sampler2D ShadowBufferSampler` + * Remove the `tex2D` read from the shadow image + * And the final multiplication of the `shadow`. + +  The end of the `pointLightEffect.fx` shader should read as follows: + + [!code-hlsl[](./snippets/snippet-9-53.hlsl)] + +If you run the game now, you will not see any of the lights anymore. + +| ![Figure 9-17: Back to square one](./images/stencil_blank.png) | +| :------------------------------------------------------------: | +| **Figure 9-17: Back to square one** | + +In the new `DrawLights()` method of the `DeferredRenderer` class, we need to iterate over all the lights, and draw them. + +1. First, we need to set the current render target to the `LightBuffer` so it can be used in the deferred renderer composite stage: + + [!code-csharp[](./snippets/snippet-9-54.cs)] + + Now the lights are back, but of course no shadows yet. + + | ![Figure 9-17: Welcome back, lights](./images/stencil_lights.png) | + | :---------------------------------------------------------------: | + | **Figure 9-17: Welcome back, lights** | + +2. As each light is about to draw, we need to draw the shadow hulls. To achieve this, replace the `foreach` loop of the `DrawLights` method with the following: + + [!code-csharp[](./snippets/snippet-9-55.cs?highlight=14-34)] + + This produces strange results. So far, the stencil buffer is not 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. + + | ![Figure 9-18: Worse shadows](./images/stencil_pre.png) | + | :-----------------------------------------------------: | + | **Figure 9-18: Worse shadows** | + + 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`](https://docs.monogame.net/api/Microsoft.Xna.Framework.Graphics.DepthStencilState) variable. The `DepthStencilState` is a MonoGame primitive that describes how draw call operations should interact with the stencil buffer. + +3. Create a new variable in the `DeferredRenderer` class: + + [!code-csharp[](./snippets/snippet-9-56.cs)] + +4. And initialize it in the constructor: + + [!code-csharp[](./snippets/snippet-9-57.cs)] + +5. The `_stencilWrite` variable is a declarative structure that tells MonoGame how the stencil buffer should be used during a `SpriteBatch` draw call. The next step is to actually pass the `_stencilWrite` declaration into the `SpriteBatch`'s `DrawLights()` call in the `DeferredRenderer` class when the shadow hulls are being rendered: + + [!code-csharp[](./snippets/snippet-9-58.cs?highlight=7)] + + Unfortunately, there is not a good way to visualize the state of the stencil buffer, so if you run the game, it is hard to tell if the stencil buffer contains any data. Instead, we will try and _use_ the stencil buffer's data when the point lights are drawn. The point lights will not interact with the stencil buffer in the same way the shadow hulls did. + +6. To capture the new behavior, create a second `DepthStencilState` class variable in the `DeferredRenderer` class: + + [!code-csharp[](./snippets/snippet-9-59.cs)] + +7. And initialize it in the constructor: + + [!code-csharp[](./snippets/snippet-9-60.cs)] + +8. And now pass the new `_stencilTest` state to the `SpriteBatch` `DrawLights()` call that draws the point lights: + + [!code-csharp[](./snippets/snippet-9-61.cs?highlight=8)] + + 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`... + + | ![Figure 9-19: The shadows still look funky](./images/stencil_blend.png) | + | :----------------------------------------------------------------------: | + | **Figure 9-19: The shadows still look funky** | + + 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`](https://docs.monogame.net/api/Microsoft.Xna.Framework.Graphics.BlendState) that ignores all color channel writes. + +9. Create another new variable in the `DeferredRenderer`: + + [!code-csharp[](./snippets/snippet-9-62.cs)] + +10. And initialize it in the constructor: + + [!code-csharp[](./snippets/snippet-9-63.cs)] + + > [!TIP] + > + > Setting the `ColorWriteChannels` to `.None` means that the GPU still rasterizes the geometry, but no color will be written to the `LightBuffer`. + +11. Finally, pass it to the shadow hull `SpriteBatch` call: + + [!code-csharp[](./snippets/snippet-9-64.cs?highlight=9)] + + Now the shadows look closer, but there is one final issue. + + | ![Figure 9-20: The shadows are back](./images/stencil_noclear.png) | + | :----------------------------------------------------------------: | + | **Figure 9-20: The shadows are back** | + + The `LightBuffer` is only being cleared at the start of the entire `DrawLights()` method. This means the `8` bits for the stencil data are not being cleared between lights, so shadows from one light are overwriting into all subsequent lights. + +12. To fix this, we just need to clear the stencil buffer data in the `DrawLights` method before rendering the shadow hulls: + + [!code-csharp[](./snippets/snippet-9-65.cs?highlight=14)] + +And now the shadows are working again! The current state of the new `DrawLights()` method is written below: + +[!code-csharp[](./snippets/snippet-9-66.cs)] + +| ![Figure 9-18: Lights using the stencil buffer](./gifs/stencil_working.gif) | +| :-------------------------------------------------------------------------: | +| **Figure 9-18: Lights using the stencil buffer** | + +We can now remove a lot of unnecessary code. + +1. The `DeferredRenderer.StartLightPhase()` function is no longer called. Remove it. +2. The `PointLight.DrawShadows()` function is no longer called. Remove it. +3. The `PointLight.Draw()` function is no longer called. Remove it. +4. The `PointLight.DrawShadowBuffer()` function is no longer called. Remove it. +5. The `PointLight.ShadowBuffer` `RenderTarget2D` is no longer used. Remove it. Anywhere that referenced the `ShadowBuffer` can also be removed, such as the constructor. + +## Improving the Look and Feel + +The shadow technique we have developed looks _cool_, but the visual effect leaves a lot to be desired. The shadows look sort of 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 [using 1d shadow maps](https://github.com/mattdesl/lwjgl-basics/wiki/2D-Pixel-Perfect-Shadows). + +> [!NOTE] +> The 1d shadow mapping article references a classic article by Catalin Zima, 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/) + +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. + +### Less Is More + +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. With so many lights it causes a lot of shadows, and as the shadows move around, they distract you from the main object of the game, _eating bats_. + +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 torches by modifying the `tilemap-definition.xml` in the `DungeonSlime` _Content/Images_ folder: + + [!code-xml[](./snippets/snippet-9-67.xml?highlight=6)] + +2. Next we will update the `InitializeLights()` method in the `GameScene` class to simplify our point lights: + * Remove the center lights that were over the two wall lights we have omitted. + * Replace the 2 moving lights for a single large light that sits at the bottom of the level. + * We can also get rid of the shadow caster for the walls of the level. + + Here is the updated `InitializeLights()` method: + + [!code-csharp[](./snippets/snippet-9-68.cs)] + +3. Also Remove the `MoveLightsAround()` method and its call from `Update` as well to keep things simple. + +Now there is less visual shadow noise going on. + +| ![Figure 9-19: Fewer lights mean fewer shadows](./gifs/less-is-more.gif) | +| :----------------------------------------------------------------------: | +| **Figure 9-19: Fewer lights mean fewer shadows** | + +### Blur the Shadows + +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. + +We will be using a simple blur technique called [box blur](https://en.wikipedia.org/wiki/Box_blur). + +1. Add this snippet to your `deferredCompositeEffect.fx`: + + [!code-hlsl[](./snippets/snippet-9-69.hlsl)] + +2. Then, in the `MainPS` function of the shader, instead of reading the `LightBuffer` directly, get the value from the new `Blur` function. + + [!code-hlsl[](./snippets/snippet-9-71.hlsl?highlight=4)] + + > [!WARNING] + > Remember that function declaration order matters in shader languages. Make sure the `Blur()` function is defined _above_ the `MainPS()` function, otherwise you will get a compiler error. + +3. Notice that the box blur needs access to the `ScreenSize`, which we need to set in the `Core`'s `Update()` method: + + [!code-csharp[](./snippets/snippet-9-70.cs?highlight=5)] + +Now, as we adjust the `BoxBlurStride` size, we can see the shadows blur in and out. + +> [!NOTE] +> We could get higher quality blur by increasing the `kernelSize` in the shadow, but that comes at the cost of runtime performance. + +| ![Figure 9-23: Bluring the shadows](./gifs/box-blur-extreme.gif) | +| :--------------------------------------------------------------: | +| **Figure 9-23: Bluring the shadows** | + +> [!NOTE] +> If you are not seeing the ImGui window for the `deferredCompositeEffect`, make sure to add back in the `DeferredCompositeMaterial.IsDebugVisible = true;` setting in the `Core`'s `LoadContent` method. + +4. It is up to you to find a `BoxBlurStride` value that fits your preference, but I like something around `.18`, set the value just after the `ScreenSize` parameter in the `Update` method: + +```csharp +DeferredCompositeMaterial.SetParameter("BoxBlurStride", .18f); +``` + +### Shadow Length + +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. + +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. + +| ![Figure 9-20: An example of a dithered image](https://upload.wikimedia.org/wikipedia/commons/e/ef/Michelangelo%27s_David_-_Bayer.png) | +| :------------------------------------------------------------------------------------------------------------------------------------: | +| **Figure 9-20: An example of a dithered image** | + +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. + +1. Add the following snippet to the `shadowHullEffect.fx` file, + + ```hlsl + // Bayer 4x4 values normalized + static const float bayer4x4[16] = { + 0.0/16.0, 8.0/16.0, 2.0/16.0, 10.0/16.0, + 12.0/16.0, 4.0/16.0, 14.0/16.0, 6.0/16.0, + 3.0/16.0, 11.0/16.0, 1.0/16.0, 9.0/16.0, + 15.0/16.0, 7.0/16.0, 13.0/16.0, 5.0/16.0 + }; + + float ShadowFadeStartDistance; + float ShadowFadeEndDistance; + ``` + +2. And update the `MainPS` function to the following: + + ```hlsl + float4 MainPS(VertexShaderOutput input) : COLOR + { + // get an ordered dither value + int2 pixel = int2(input.TextureCoordinates * ScreenSize); + int idx = (pixel.x % 4) + (pixel.y % 4) * 4; + float ditherValue = bayer4x4[idx]; + + // produce the fade-out gradient + float maxDistance = ScreenSize.x + ScreenSize.y; + float endDistance = ShadowFadeEndDistance; + float startDistance = ShadowFadeStartDistance; + float fade = saturate((input.TextureCoordinates.x - endDistance) / (startDistance - endDistance)); + + if (ditherValue > fade){ + clip(-1); + } + + clip(input.Color.a); + return float4(0,0,0,1); // return black + } + ``` + + > [!NOTE] + > Why use `input.TextureCoordinates.x` ? + > + > 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 which 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. + + Now when you run the game, you can play around with the shader parameters to create a falloff gradient for the shadow. + + | ![Figure 9-25: Controlling shadow length](./gifs/shadow-length.gif) | + | :-----------------------------------------------------------------: | + | **Figure 9-23: Controlling shadow length** | + + It is worth calling out that this dithering technique only works well because the box blur is covering the pixelated output. Try disabling the blur entirely, and pay attention to the shadow falloff gradient. + +3. You will need to pick values that you like for the shadow falloff. I like `.013` for the start and `.13` for the end and set them in the `Update` method of the `Core` class before updating the `ShadowHullMaterial`: + + ```csharp + ShadowHullMaterial.SetParameter("ShadowFadeStartDistance", .013f); + ShadowHullMaterial.SetParameter("ShadowFadeEndDistance", .13f); + ``` + + > [!NOTE] + > 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: + > + > ```hlsl + > float endDistance = ShadowFadeEndDistance / maxDistance; + > float startDistance = ShadowFadeStartDistance / maxDistance; + > ``` + > + > 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. + + > [!NOTE] + > There are other dithering techniques, too! + > + > Using a bayer matrix is perhaps the most "standard" way to perform dithering, but it may not be the best suited to the design challenge at hand. Check out [this article](https://tannerhelland.com/2012/12/28/dithering-eleven-algorithms-source-code.html) that details several different algorithms, and this article from [frost kiwi](https://blog.frost.kiwi/GLSL-noise-and-radial-gradient/) about using dithering to escape color banding. + +### Shadow Intensity + +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. + +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. + + [!code-hlsl[](./snippets/snippet-9-72.hlsl?highlight=3,17)] + + Now you can experiment with different intensity values and fade out the entire shadow. + + | ![Figure 9-21: Controlling shadow intensity](./gifs/shadow-intensity.gif) | + | :-----------------------------------------------------------------------: | + | **Figure 9-21: Controlling shadow intensity** | + +2. Pick a value that looks good to you, but I like `.85` and enter it in the `Core` class `Update` method. + + ```csharp + ShadowHullMaterial.SetParameter("ShadowIntensity", .85f); + ``` + +### No Self Shadows + +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 did not receive any shadows what-so-ever. To do that, we will need to extend the stencil buffer logic. + +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. + +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. + +In this new edition, the values of the stencil buffer are outlined below, + +| Stencil Value | Description | +| :--------------- | :-------------------------------- | +| `0` | The snake is occupying this pixel | +| `1` | An empty pixel | +| `2` (or greater) | A pixel "in shadow" | + +Follow the steps to modify the code so that the snake appears stenciled out of the shadows. + +1. First in the `DeferredRenderer` class, change the stencil buffer `.Clear()` call to clear the stencil buffer to `1` instead of `0` inside the `DrawLights` method: + + [!code-csharp[](./snippets/snippet-9-73.cs?highlight=14)] + +2. Then, add a new `DepthStencilState` property to the `DeferredRenderer` class: + + ```csharp + /// + /// The state that will be ignored from shadows + /// + private DepthStencilState _stencilShadowExclude; + ``` + +3. Next, we need to initialize the `_stencilShadowExclude` state in the constructor: + + [!code-csharp[](./snippets/snippet-9-74.cs)] + +4. Then update the existing states to take the new value into account: + + [!code-csharp[](./snippets/snippet-9-75.cs?highlight=7,11,27,30)] + + 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 an additional draw call. + +5. Modify the `DrawLights()` function like so: + + [!code-csharp[](./snippets/snippet-9-76.cs?highlight=1,17)] + +6. Adding the necessary `using` for the `Action` definition: + + ```csharp + using System; + ``` + +7. Then, the `GameScene`'s `Draw()` method should be updated to re-draw the snake segments in this callback: + + ```csharp + // start rendering the lights + _deferredRenderer.DrawLights(_lights, casters, (blend, stencil) => + { + Core.SpriteBatch.Begin( + effect: _gameMaterial.Effect, + depthStencilState: stencil, + blendState: blend); + _slime.Draw(_ => {}); + Core.SpriteBatch.End(); + }); + ``` + +8. Finally, there is a quick change to the `gameEffect.fx` in the _DungeonSlime_ project's _Content/effects_ folder. The stencil buffer will be set to `0` anywhere the slime textures are drawn, even if the pixel in the slime's sprite happens to be completely transparent. That creates an artifact where the whole slime's texture rectangle is excluded from receiving shadows, instead of _just_ the slime art. To fix that, we can `clip` the pixels in the `gameEffect.fx` file that have an empty alpha value. + + Add this line to the `gameEffect.fx` shader to avoid writing the pixels to the stencil buffer: + + [!code-csharp[](./snippets/snippet-9-78.hlsl?highlight=6-7)] + +Now even when the snake character is heading directly into a light, the segments in the back do not receive any shadows. + +| ![Figure 9-21: No self shadows](./gifs/overview.gif) | +| :--------------------------------------------------------: | +| **Figure 9-21: No self shadows** | + +## Conclusion + +And with that, our lighting and shadow system is complete! In this chapter, you accomplished the following: + +* Learned the theory behind generating 2D shadow geometry from a light and a line segment. +* Wrote a vertex shader to generate a "shadow hull" quad on the fly. +* Implemented a shadow system using a memory-intensive texture-based approach. +* Refactored the system to use the Stencil Buffer for masking. +* Developed several techniques for improving the look and feel of the stencil shadows. + +In the final chapter, we will wrap up the series and discuss some other exciting graphics programming topics you could explore from here. + +You can find the complete code sample for this tutorial series [here](https://github.com/MonoGame/MonoGame.Samples/tree/3.8.4/Tutorials/2dShaders/src/09-Shadows-Effect/). + +Continue to the next chapter, [Chapter 10: Next Steps](../10_next_steps/index.md) diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-01.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-01.hlsl new file mode 100644 index 00000000..0c26d5d6 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-01.hlsl @@ -0,0 +1,6 @@ +struct VertexShaderInput +{ + float4 Position : POSITION0; + float4 Color : COLOR0; + float2 TexCoord : TEXCOORD0; +}; diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-02.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-02.hlsl new file mode 100644 index 00000000..a1b9d35a --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-02.hlsl @@ -0,0 +1,8 @@ +// start by putting the world space position into a variable... +float2 pos = input.Position.xy; + +// `P` is the center of the shape, but the `pos` is half a pixel off. +float2 P = pos - (.5 * input.TexCoord) / ScreenSize; + +// now we have identified `A` as `P` +float2 A = P; diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-03.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-03.hlsl new file mode 100644 index 00000000..90c98999 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-03.hlsl @@ -0,0 +1,5 @@ +// the `input.Color` has the (B-A) vector +float2 aToB = unpack(input.Color); + +// to find `B`, start at `A`, and move by the delta +float2 B = A + aToB; diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-04.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-04.hlsl new file mode 100644 index 00000000..032487cf --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-04.hlsl @@ -0,0 +1 @@ +float2 lightRayA = normalize(A - LightPosition); diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-05.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-05.hlsl new file mode 100644 index 00000000..e6e20eaf --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-05.hlsl @@ -0,0 +1 @@ +float2 a = A + distance * lightRayA; diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-06.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-06.hlsl new file mode 100644 index 00000000..507b3ed1 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-06.hlsl @@ -0,0 +1,2 @@ +float2 lightRayB = normalize(B - LightPosition); +float2 b = B + distance * lightRayB; diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-07.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-07.hlsl new file mode 100644 index 00000000..c3a7da23 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-07.hlsl @@ -0,0 +1,10 @@ +int id = input.TexCoord.x + input.TexCoord.y * 2; +if (id == 0) { // S --> A + pos = A; +} else if (id == 1) { // D --> a + pos = a; +} else if (id == 3) { // F --> b + pos = b; +} else if (id == 2) { // G --> B + pos = B; +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-08.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-08.cs new file mode 100644 index 00000000..e900ea88 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-08.cs @@ -0,0 +1,4 @@ +/// +/// The material that draws shadow hulls +/// +public static Material ShadowHullMaterial { get; private set; } diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-09.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-09.cs new file mode 100644 index 00000000..435fbec4 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-09.cs @@ -0,0 +1,9 @@ +protected override void LoadContent() +{ + base.LoadContent(); + + // ... + + ShadowHullMaterial = SharedContent.WatchMaterial("effects/shadowHullEffect"); + ShadowHullMaterial.IsDebugVisible = true; +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-10.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-10.cs new file mode 100644 index 00000000..f46fd986 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-10.cs @@ -0,0 +1,8 @@ +protected override void Update(GameTime gameTime) +{ + // ... + + ShadowHullMaterial.Update(); + + base.Update(gameTime); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-11.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-11.cs new file mode 100644 index 00000000..8116422b --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-11.cs @@ -0,0 +1,8 @@ +using Microsoft.Xna.Framework; +namespace MonoGameLibrary.Graphics; + +public class ShadowCaster +{ + public Vector2 A; + public Vector2 B; +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-12.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-12.cs new file mode 100644 index 00000000..837f9a80 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-12.cs @@ -0,0 +1,2 @@ +// A list of shadow casters for all the lights +private List _shadowCasters = new List(); diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-13.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-13.cs new file mode 100644 index 00000000..3151f2ce --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-13.cs @@ -0,0 +1,17 @@ +private void InitializeLights() +{ + // torch 1 + _lights.Add(new PointLight + { + Position = new Vector2(500, 360), + Color = Color.CornflowerBlue, + Radius = 700 + }); + + // simple shadow caster + _shadowCasters.Add(new ShadowCaster + { + A = new Vector2(700, 320), + B = new Vector2(700, 400) + }); +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-14.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-14.cs new file mode 100644 index 00000000..61f53d49 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-14.cs @@ -0,0 +1,4 @@ +/// +/// The render target that holds the shadow map +/// +public RenderTarget2D ShadowBuffer { get; set; } diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-15.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-15.cs new file mode 100644 index 00000000..f344098f --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-15.cs @@ -0,0 +1,5 @@ +public PointLight() +{ + var viewPort = Core.GraphicsDevice.Viewport; + ShadowBuffer = new RenderTarget2D(Core.GraphicsDevice, viewPort.Width, viewPort.Height, false, SurfaceFormat.Color, DepthFormat.None, 0, RenderTargetUsage.PreserveContents); +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-16.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-16.cs new file mode 100644 index 00000000..119fa2f8 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-16.cs @@ -0,0 +1,19 @@ +public void DrawShadowBuffer(List shadowCasters) +{ + Core.GraphicsDevice.SetRenderTarget(ShadowBuffer); + Core.GraphicsDevice.Clear(Color.Black); + + Core.ShadowHullMaterial.SetParameter("LightPosition", Position); + var screenSize = new Vector2(ShadowBuffer.Width, ShadowBuffer.Height); + Core.SpriteBatch.Begin( + effect: Core.ShadowHullMaterial.Effect, + rasterizerState: RasterizerState.CullNone + ); + foreach (var caster in shadowCasters) + { + var posA = caster.A; + // TODO: pack the (B-A) vector into the color channel. + Core.SpriteBatch.Draw(Core.Pixel, posA, Color.White); + } + Core.SpriteBatch.End(); +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-17.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-17.cs new file mode 100644 index 00000000..e19f2e43 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-17.cs @@ -0,0 +1,9 @@ +public static void DrawShadows( + List pointLights, + List shadowCasters) +{ + foreach (var light in pointLights) + { + light.DrawShadowBuffer(shadowCasters); + } +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-18.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-18.cs new file mode 100644 index 00000000..7ca25b9d --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-18.cs @@ -0,0 +1,13 @@ +public override void Draw(GameTime gameTime) +{ + // ... + Core.SpriteBatch.End(); + + // render the shadow buffers + PointLight.DrawShadows(_lights, _shadowCasters); + + // start rendering the lights + _deferredRenderer.StartLightPhase(); + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-19.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-19.cs new file mode 100644 index 00000000..59e96aa2 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-19.cs @@ -0,0 +1,11 @@ +public override void Draw(GameTime gameTime) +{ + // ... + + Core.SpriteBatch.Begin(); + Core.SpriteBatch.Draw(_lights[0].ShadowBuffer, Vector2.Zero, Color.White); + Core.SpriteBatch.End(); + + // Draw the UI + _ui.Draw(); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-20.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-20.cs new file mode 100644 index 00000000..8ac8904c --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-20.cs @@ -0,0 +1,15 @@ +public static Color PackVector2_SNorm(Vector2 vec) +{ + // Clamp to [-1, 1) + vec = Vector2.Clamp(vec, new Vector2(-1f), new Vector2(1f - 1f / 32768f)); + + short xInt = (short)(vec.X * 32767f); // signed 16-bit + short yInt = (short)(vec.Y * 32767f); + + byte r = (byte)((xInt >> 8) & 0xFF); + byte g = (byte)(xInt & 0xFF); + byte b = (byte)((yInt >> 8) & 0xFF); + byte a = (byte)(yInt & 0xFF); + + return new Color(r, g, b, a); +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-21.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-21.cs new file mode 100644 index 00000000..ccf9e7df --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-21.cs @@ -0,0 +1,14 @@ +public void DrawShadowBuffer(List shadowCasters) +{ + /// ... + + foreach (var caster in shadowCasters) + { + var posA = caster.A; + var aToB = (caster.B - caster.A) / screenSize; + var packed = PackVector2_SNorm(aToB); + Core.SpriteBatch.Draw(Core.Pixel, posA, packed); + } + + Core.SpriteBatch.End(); +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-22.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-22.hlsl new file mode 100644 index 00000000..8f964ae8 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-22.hlsl @@ -0,0 +1,19 @@ +float2 UnpackVector2FromColor_SNorm(float4 color) +{ + // Convert [0,1] to byte range [0,255] + float4 bytes = color * 255.0; + + // Reconstruct 16-bit unsigned ints (x and y) + float xInt = bytes.r * 256.0 + bytes.g; + float yInt = bytes.b * 256.0 + bytes.a; + + // Convert from unsigned to signed short range [-32768, 32767] + if (xInt >= 32768.0) xInt -= 65536.0; + if (yInt >= 32768.0) yInt -= 65536.0; + + // Convert from signed 16-bit to float in [-1, 1] + float x = xInt / 32767.0; + float y = yInt / 32767.0; + + return float2(x, y); +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-23.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-23.hlsl new file mode 100644 index 00000000..e74a66bd --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-23.hlsl @@ -0,0 +1,5 @@ +VertexShaderOutput ShadowHullVS(VertexShaderInput input) +{ + VertexShaderOutput output = MainVS(input); + return output; +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-24.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-24.hlsl new file mode 100644 index 00000000..a8917689 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-24.hlsl @@ -0,0 +1,8 @@ +technique SpriteDrawing +{ + pass P0 + { + PixelShader = compile PS_SHADERMODEL MainPS(); + VertexShader = compile VS_SHADERMODEL ShadowHullVS(); + } +}; diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-25.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-25.cs new file mode 100644 index 00000000..b0798d4f --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-25.cs @@ -0,0 +1,15 @@ +public override void Update(GameTime gameTime) +{ + // ... + + + var matrixTransform = _camera.CalculateMatrixTransform(); + _gameMaterial.SetParameter("MatrixTransform", matrixTransform); + Core.PointLightMaterial.SetParameter("MatrixTransform", matrixTransform); + Core.PointLightMaterial.SetParameter("ScreenSize", new Vector2(Core.GraphicsDevice.Viewport.Width, Core.GraphicsDevice.Viewport.Height)); + + Core.ShadowHullMaterial.SetParameter("MatrixTransform", matrixTransform); + Core.ShadowHullMaterial.SetParameter("ScreenSize", new Vector2(Core.GraphicsDevice.Viewport.Width, Core.GraphicsDevice.Viewport.Height)); + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-26.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-26.hlsl new file mode 100644 index 00000000..d2b909ca --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-26.hlsl @@ -0,0 +1,4 @@ +float4 MainPS(VertexShaderOutput input) : COLOR +{ + return 1; // return white +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-27.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-27.hlsl new file mode 100644 index 00000000..560a44b5 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-27.hlsl @@ -0,0 +1,33 @@ +float2 LightPosition; +VertexShaderOutput ShadowHullVS(VertexShaderInput input) +{ + VertexShaderInput modified = input; + float distance = ScreenSize.x + ScreenSize.y; + float2 pos = input.Position.xy; + + float2 P = pos - (.5 * input.TexCoord) / ScreenSize; + float2 A = P; + + float2 aToB = UnpackVector2FromColor_SNorm(input.Color) * ScreenSize; + float2 B = A + aToB; + + float2 lightRayA = normalize(A - LightPosition); + float2 a = A + distance * lightRayA; + float2 lightRayB = normalize(B - LightPosition); + float2 b = B + distance * lightRayB; + + int id = input.TexCoord.x + input.TexCoord.y * 2; + if (id == 0) { // S --> A + pos = A; + } else if (id == 1) { // D --> a + pos = a; + } else if (id == 3) { // F --> b + pos = b; + } else if (id == 2) { // G --> B + pos = B; + } + + modified.Position.xy = pos; + VertexShaderOutput output = MainVS(modified); + return output; +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-28.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-28.hlsl new file mode 100644 index 00000000..90a2e6f9 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-28.hlsl @@ -0,0 +1,5 @@ +Texture2D ShadowBuffer; +sampler2D ShadowBufferSampler = sampler_state +{ + Texture = ; +}; diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-29.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-29.hlsl new file mode 100644 index 00000000..4881ef48 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-29.hlsl @@ -0,0 +1,16 @@ +float4 MainPS(LightVertexShaderOutput input) : COLOR { + // ... + + float2 screenCoords = .5*(input.ScreenData.xy + 1); + screenCoords.y = 1 - screenCoords.y; + + float shadow = tex2D(ShadowBufferSampler,screenCoords).r; + + float4 normal = tex2D(NormalBufferSampler,screenCoords); + + // ... + + float4 color = input.Color; + color.a *= falloff * lightAmount * shadow; + return color; +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-30.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-30.hlsl new file mode 100644 index 00000000..6d589a95 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-30.hlsl @@ -0,0 +1 @@ +color.a *= falloff * lightAmount * shadow; diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-31.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-31.cs new file mode 100644 index 00000000..60bbe81d --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-31.cs @@ -0,0 +1,18 @@ +public static void Draw(SpriteBatch spriteBatch, List pointLights, Texture2D normalBuffer) +{ + spriteBatch.Begin( + effect: Core.PointLightMaterial.Effect, + blendState: BlendState.Additive, + sortMode: SpriteSortMode.Immediate + ); + + foreach (var light in pointLights) + { + Core.PointLightMaterial.SetParameter("ShadowBuffer", light.ShadowBuffer); + var diameter = light.Radius * 2; + var rect = new Rectangle((int)(light.Position.X - light.Radius), (int)(light.Position.Y - light.Radius), diameter, diameter); + spriteBatch.Draw(normalBuffer, rect, light.Color); + } + + spriteBatch.End(); +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-32.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-32.cs new file mode 100644 index 00000000..74e92765 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-32.cs @@ -0,0 +1,8 @@ +public void DrawShadowBuffer(List shadowCasters) +{ + Core.GraphicsDevice.SetRenderTarget(ShadowBuffer); + // clear the shadow buffer to white to start + Core.GraphicsDevice.Clear(Color.White); + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-33.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-33.hlsl new file mode 100644 index 00000000..0d9fc932 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-33.hlsl @@ -0,0 +1,4 @@ +float4 MainPS(VertexShaderOutput input) : COLOR +{ + return float4(0,0,0,1); // return black +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-34.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-34.cs new file mode 100644 index 00000000..629a2210 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-34.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; + +namespace MonoGameLibrary.Graphics; + +public class ShadowCaster +{ + /// + /// The position of the shadow caster + /// + public Vector2 Position; + + /// + /// A list of at least 2 points that will be used to create a closed loop shape. + /// The points are relative to the position. + /// + public List Points; +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-35.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-35.cs new file mode 100644 index 00000000..3855147f --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-35.cs @@ -0,0 +1,16 @@ +public static ShadowCaster SimplePolygon(Point position, float radius, int sides) +{ + var anglePerSide = MathHelper.TwoPi / sides; + var caster = new ShadowCaster + { + Position = position.ToVector2(), + Points = new List(sides) + }; + for (var angle = 0f; angle < MathHelper.TwoPi; angle += anglePerSide) + { + var pt = radius * new Vector2(MathF.Cos(angle), MathF.Sin(angle)); + caster.Points.Add(pt); + } + + return caster; +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-36.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-36.cs new file mode 100644 index 00000000..6d18226f --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-36.cs @@ -0,0 +1,7 @@ +private void InitializeLights() +{ + // ... + + // simple shadow caster + _shadowCasters.Add(ShadowCaster.SimplePolygon(_slime.GetBounds().Location, radius: 30, sides: 6)); +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-37.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-37.cs new file mode 100644 index 00000000..f84f600a --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-37.cs @@ -0,0 +1,19 @@ +public void DrawShadowBuffer(List shadowCasters) +{ + /// ... + + foreach (var caster in shadowCasters) + { + for (var i = 0; i < caster.Points.Count; i++) + { + var a = caster.Position + caster.Points[i]; + var b = caster.Position + caster.Points[(i + 1) % caster.Points.Count]; + + var aToB = (b - a) / screenSize; + var packed = PackVector2_SNorm(aToB); + Core.SpriteBatch.Draw(Core.Pixel, a, packed); + } + } + + // ... +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-38.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-38.hlsl new file mode 100644 index 00000000..5b42f184 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-38.hlsl @@ -0,0 +1,17 @@ +VertexShaderOutput ShadowHullVS(VertexShaderInput input) { + // ... + + float2 aToB = UnpackVector2FromColor_SNorm(input.Color) * ScreenSize; + float2 B = A + aToB; + + float2 direction = normalize(aToB); + A -= direction; // move A back along the segment by one unit + B += direction; // move B forward along the segment by one unit + + float2 lightRayA = normalize(A - LightPosition); + float2 a = A + distance * lightRayA; + float2 lightRayB = normalize(B - LightPosition); + float2 b = B + distance * lightRayB; + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-39.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-39.hlsl new file mode 100644 index 00000000..b8d0cd75 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-39.hlsl @@ -0,0 +1,6 @@ +// cull faces +float2 normal = float2(-direction.y, direction.x); +float alignment = dot(normal, (LightPosition - A)); +if (alignment < 0){ + modified.Color.a = -1; +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-40.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-40.hlsl new file mode 100644 index 00000000..f181f2e9 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-40.hlsl @@ -0,0 +1,4 @@ +float4 MainPS(VertexShaderOutput input) : COLOR { + clip(input.Color.a); + return float4(0,0,0,1); // return black +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-41.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-41.cs new file mode 100644 index 00000000..1118320c --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-41.cs @@ -0,0 +1,45 @@ +private void InitializeLights() +{ + // torch 1 + _lights.Add(new PointLight + { + Position = new Vector2(260, 100), + Color = Color.CornflowerBlue, + Radius = 500 + }); + // torch 2 + _lights.Add(new PointLight + { + Position = new Vector2(520, 100), + Color = Color.CornflowerBlue, + Radius = 500 + }); + // torch 3 + _lights.Add(new PointLight + { + Position = new Vector2(740, 100), + Color = Color.CornflowerBlue, + Radius = 500 + }); + // torch 4 + _lights.Add(new PointLight + { + Position = new Vector2(1000, 100), + Color = Color.CornflowerBlue, + Radius = 500 + }); + + // random lights + _lights.Add(new PointLight + { + Position = new Vector2(Random.Shared.Next(50, 400),400), + Color = Color.MonoGameOrange, + Radius = 500 + }); + _lights.Add(new PointLight + { + Position = new Vector2(Random.Shared.Next(650, 1200),300), + Color = Color.MonoGameOrange, + Radius = 500 + }); +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-42.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-42.cs new file mode 100644 index 00000000..139447f4 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-42.cs @@ -0,0 +1,4 @@ +/// +/// A list of shadow casters for all of the slime segments +/// +public List ShadowCasters { get; private set; } = new List(); diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-43.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-43.cs new file mode 100644 index 00000000..3f1aacdc --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-43.cs @@ -0,0 +1,23 @@ +public void Update(GameTime gameTime) +{ + // ... + + // Update the shadow casters + if (ShadowCasters.Count != _segments.Count) + { + ShadowCasters = new List(_segments.Count); + for (var i = 0; i < _segments.Count; i++) + { + ShadowCasters.Add(ShadowCaster.SimplePolygon(Point.Zero, radius: 30, sides: 12)); + } + } + + // move the shadow casters to the current segment positions + for (var i = 0; i < _segments.Count; i++) + { + var segment = _segments[i]; + Vector2 pos = Vector2.Lerp(segment.At, segment.To, _movementProgress); + var size = new Vector2(_sprite.Width, _sprite.Height); + ShadowCasters[i].Position = pos + size * .5f; + } +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-44.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-44.cs new file mode 100644 index 00000000..b33b27cc --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-44.cs @@ -0,0 +1,19 @@ +public override void Draw(GameTime gameTime) +{ + // ... + + // Always end the sprite batch when finished. + Core.SpriteBatch.End(); + + // render the shadow buffers + var casters = new List(); + casters.AddRange(_shadowCasters); + casters.AddRange(_slime.ShadowCasters); + PointLight.DrawShadows(_lights, casters); + + // start rendering the lights + _deferredRenderer.StartLightPhase(); + PointLight.Draw(Core.SpriteBatch, _lights, _deferredRenderer.NormalBuffer); + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-45.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-45.cs new file mode 100644 index 00000000..cd70b725 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-45.cs @@ -0,0 +1,4 @@ +/// +/// The shadow caster for this bat +/// +public ShadowCaster ShadowCaster { get; private set; } diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-46.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-46.cs new file mode 100644 index 00000000..4e219d23 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-46.cs @@ -0,0 +1,6 @@ +public Bat(AnimatedSprite sprite, SoundEffect bounceSoundEffect) +{ + // ... + + ShadowCaster = ShadowCaster.SimplePolygon(Point.Zero, radius: 10, sides: 12); +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-47.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-47.cs new file mode 100644 index 00000000..31360c40 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-47.cs @@ -0,0 +1,8 @@ +public void Update(GameTime gameTime) +{ + // ... + + // Update the position of the shadow caster. Move it up a bit due to the bat's artwork. + var size = new Vector2(_sprite.Width, _sprite.Height); + ShadowCaster.Position = Position - Vector2.UnitY * 10 + size * .5f; +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-48.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-48.cs new file mode 100644 index 00000000..219f6ef6 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-48.cs @@ -0,0 +1,6 @@ +// render the shadow buffers +var casters = new List(); +casters.AddRange(_shadowCasters); +casters.AddRange(_slime.ShadowCasters); +casters.Add(_bat.ShadowCaster); +PointLight.DrawShadows(_lights, casters); diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-49.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-49.cs new file mode 100644 index 00000000..45512586 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-49.cs @@ -0,0 +1,16 @@ +private void InitializeLights() +{ + // ... + + var tileUnit = new Vector2(_tilemap.TileWidth, _tilemap.TileHeight); + var size = new Vector2(_tilemap.Columns, _tilemap.Rows); + _shadowCasters.Add(new ShadowCaster + { + Points = new List + { tileUnit * new Vector2(1, 1), + tileUnit * new Vector2(size.X - 1, 1), + tileUnit * new Vector2(size.X - 1, size.Y - 1), + tileUnit * new Vector2(1, size.Y - 1), + } + }); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-50.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-50.cs new file mode 100644 index 00000000..9c395174 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-50.cs @@ -0,0 +1,7 @@ +LightBuffer = new RenderTarget2D( + graphicsDevice: Core.GraphicsDevice, + width: viewport.Width, + height: viewport.Height, + mipMap: false, + preferredFormat: SurfaceFormat.Color, + preferredDepthFormat: DepthFormat.Depth24Stencil8); diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-51.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-51.cs new file mode 100644 index 00000000..736c3a21 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-51.cs @@ -0,0 +1,4 @@ +public void DrawLights(List lights, List shadowCasters) +{ + // +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-52.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-52.cs new file mode 100644 index 00000000..252405b3 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-52.cs @@ -0,0 +1,18 @@ +public override void Draw(GameTime gameTime) +{ + // ... + + // render the shadow buffers + var casters = new List(); + casters.AddRange(_shadowCasters); + casters.AddRange(_slime.ShadowCasters); + casters.Add(_bat.ShadowCaster); + + // start rendering the lights + _deferredRenderer.DrawLights(_lights, casters); + + // finish the deferred rendering + _deferredRenderer.Finish(); + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-53.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-53.hlsl new file mode 100644 index 00000000..dff286a9 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-53.hlsl @@ -0,0 +1,9 @@ +float4 MainPS(LightVertexShaderOutput input) : COLOR { + // ... + // how much is the normal direction pointing towards the light direction? + float lightAmount = (dot(normalDir, lightDir)); + + float4 color = input.Color; + color.a *= falloff * lightAmount; + return color; +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-54.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-54.cs new file mode 100644 index 00000000..c217aa4c --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-54.cs @@ -0,0 +1,22 @@ +public void DrawLights(List lights, List shadowCasters) +{ + Core.GraphicsDevice.SetRenderTarget(LightBuffer); + Core.GraphicsDevice.Clear(Color.Black); + + foreach (var light in lights) + { + Core.SpriteBatch.Begin( + effect: Core.PointLightMaterial.Effect, + blendState: BlendState.Additive + ); + + var diameter = light.Radius * 2; + var rect = new Rectangle( + (int)(light.Position.X - light.Radius), + (int)(light.Position.Y - light.Radius), + diameter, diameter); + Core.SpriteBatch.Draw(NormalBuffer, rect, light.Color); + Core.SpriteBatch.End(); + + } +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-55.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-55.cs new file mode 100644 index 00000000..4e22f8b2 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-55.cs @@ -0,0 +1,44 @@ + public void DrawLights(List lights, List shadowCasters) + { + Core.GraphicsDevice.SetRenderTarget(LightBuffer); + Core.GraphicsDevice.Clear(Color.Black); + + foreach (var light in lights) + { + var diameter = light.Radius * 2; + var rect = new Rectangle( + (int)(light.Position.X - light.Radius), + (int)(light.Position.Y - light.Radius), + diameter, diameter); + + Core.ShadowHullMaterial.SetParameter("LightPosition", light.Position); + Core.SpriteBatch.Begin( + effect: Core.ShadowHullMaterial.Effect, + blendState: BlendState.Opaque, + rasterizerState: RasterizerState.CullNone + ); + + foreach (var caster in shadowCasters) + { + for (var i = 0; i < caster.Points.Count; i++) + { + var a = caster.Position + caster.Points[i]; + var b = caster.Position + caster.Points[(i + 1) % caster.Points.Count]; + + var screenSize = new Vector2(LightBuffer.Width, LightBuffer.Height); + var aToB = (b - a) / screenSize; + var packed = PointLight.PackVector2_SNorm(aToB); + Core.SpriteBatch.Draw(Core.Pixel, a, packed); + } + } + Core.SpriteBatch.End(); + + Core.SpriteBatch.Begin( + effect: Core.PointLightMaterial.Effect, + blendState: BlendState.Additive + ); + + Core.SpriteBatch.Draw(NormalBuffer, rect, light.Color); + Core.SpriteBatch.End(); + } + } \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-56.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-56.cs new file mode 100644 index 00000000..085e226c --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-56.cs @@ -0,0 +1,4 @@ +/// +/// The state used when writing shadow hulls +/// +private DepthStencilState _stencilWrite; diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-57.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-57.cs new file mode 100644 index 00000000..9f9efdec --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-57.cs @@ -0,0 +1,18 @@ +_stencilWrite = new DepthStencilState +{ + // instruct MonoGame to use the stencil buffer + StencilEnable = true, + + // instruct every fragment to interact with the stencil buffer + StencilFunction = CompareFunction.Always, + + // every operation will replace the current value in the stencil buffer + // with whatever value is in the ReferenceStencil variable + StencilPass = StencilOperation.Replace, + + // this is the value that will be written into the stencil buffer + ReferenceStencil = 1, + + // ignore depth from the stencil buffer write/reads + DepthBufferEnable = false +}; diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-58.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-58.cs new file mode 100644 index 00000000..3ad7f23c --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-58.cs @@ -0,0 +1,17 @@ +public void DrawLights(List lights, List shadowCasters) +{ + // ... + + Core.ShadowHullMaterial.SetParameter("LightPosition", light.Position); + Core.SpriteBatch.Begin( + depthStencilState: _stencilWrite, + effect: Core.ShadowHullMaterial.Effect, + blendState: BlendState.Opaque, + rasterizerState: RasterizerState.CullNone + ); + + foreach (var caster in shadowCasters) + + // ... + +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-59.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-59.cs new file mode 100644 index 00000000..cc216595 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-59.cs @@ -0,0 +1,4 @@ +/// +/// The state used when drawing point lights +/// +private DepthStencilState _stencilTest; diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-60.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-60.cs new file mode 100644 index 00000000..3989e5fa --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-60.cs @@ -0,0 +1,18 @@ +_stencilTest = new DepthStencilState +{ + // instruct MonoGame to use the stencil buffer + StencilEnable = true, + + // instruct only fragments that have a current value EQUAl to the + // ReferenceStencil value to interact + StencilFunction = CompareFunction.Equal, + + // shadow hulls wrote `1`, so `0` means "not" shadow. + ReferenceStencil = 0, + + // do not change the value of the stencil buffer. KEEP the current value. + StencilPass = StencilOperation.Keep, + + // ignore depth from the stencil buffer write/reads + DepthBufferEnable = false +}; diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-61.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-61.cs new file mode 100644 index 00000000..ca29add1 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-61.cs @@ -0,0 +1,18 @@ +public void DrawLights(List lights, List shadowCasters) +{ + // ... + + Core.SpriteBatch.End(); + + Core.SpriteBatch.Begin( + depthStencilState: _stencilTest, + effect: Core.PointLightMaterial.Effect, + blendState: BlendState.Additive + ); + + Core.SpriteBatch.Draw(NormalBuffer, rect, light.Color); + Core.SpriteBatch.End(); + + // ... + +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-62.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-62.cs new file mode 100644 index 00000000..67418521 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-62.cs @@ -0,0 +1,4 @@ +/// +/// A custom blend state that wont write any color data +/// +private BlendState _shadowBlendState; diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-63.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-63.cs new file mode 100644 index 00000000..aedcfaa6 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-63.cs @@ -0,0 +1,5 @@ +_shadowBlendState = new BlendState +{ + // no color channels will be written into the render target + ColorWriteChannels = ColorWriteChannels.None +}; diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-64.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-64.cs new file mode 100644 index 00000000..ce3fba40 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-64.cs @@ -0,0 +1,17 @@ +public void DrawLights(List lights, List shadowCasters) +{ + // ... + + Core.ShadowHullMaterial.SetParameter("LightPosition", light.Position); + Core.SpriteBatch.Begin( + depthStencilState: _stencilWrite, + effect: Core.ShadowHullMaterial.Effect, + blendState: _shadowBlendState, + rasterizerState: RasterizerState.CullNone + ); + + foreach (var caster in shadowCasters) + + // ... + +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-65.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-65.cs new file mode 100644 index 00000000..3067f5a1 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-65.cs @@ -0,0 +1,21 @@ +public void DrawLights(List lights, List shadowCasters) +{ + Core.GraphicsDevice.SetRenderTarget(LightBuffer); + Core.GraphicsDevice.Clear(Color.Black); + + foreach (var light in lights) + { + var diameter = light.Radius * 2; + var rect = new Rectangle( + (int)(light.Position.X - light.Radius), + (int)(light.Position.Y - light.Radius), + diameter, diameter); + + Core.GraphicsDevice.Clear(ClearOptions.Stencil, Color.Black, 0, 0); + + Core.ShadowHullMaterial.SetParameter("LightPosition", light.Position); + + // ... + } + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-66.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-66.cs new file mode 100644 index 00000000..15f00fe7 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-66.cs @@ -0,0 +1,48 @@ +public void DrawLights(List lights, List shadowCasters) +{ + Core.GraphicsDevice.SetRenderTarget(LightBuffer); + Core.GraphicsDevice.Clear(Color.Black); + + foreach (var light in lights) + { + var diameter = light.Radius * 2; + var rect = new Rectangle( + (int)(light.Position.X - light.Radius), + (int)(light.Position.Y - light.Radius), + diameter, diameter); + + Core.GraphicsDevice.Clear(ClearOptions.Stencil, Color.Black, 0, 0); + + Core.ShadowHullMaterial.SetParameter("LightPosition", light.Position); + Core.SpriteBatch.Begin( + depthStencilState: _stencilWrite, + effect: Core.ShadowHullMaterial.Effect, + blendState: _shadowBlendState, + rasterizerState: RasterizerState.CullNone + ); + + foreach (var caster in shadowCasters) + { + for (var i = 0; i < caster.Points.Count; i++) + { + var a = caster.Position + caster.Points[i]; + var b = caster.Position + caster.Points[(i + 1) % caster.Points.Count]; + + var screenSize = new Vector2(LightBuffer.Width, LightBuffer.Height); + var aToB = (b - a) / screenSize; + var packed = PointLight.PackVector2_SNorm(aToB); + Core.SpriteBatch.Draw(Core.Pixel, a, packed); + } + } + Core.SpriteBatch.End(); + + Core.SpriteBatch.Begin( + depthStencilState: _stencilTest, + effect: Core.PointLightMaterial.Effect, + blendState: BlendState.Additive + ); + + Core.SpriteBatch.Draw(NormalBuffer, rect, light.Color); + Core.SpriteBatch.End(); + } +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-67.xml b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-67.xml new file mode 100644 index 00000000..8c9f9a44 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-67.xml @@ -0,0 +1,15 @@ + + + images/atlas + + 00 01 02 01 02 01 02 01 02 01 02 01 02 01 02 03 + 04 05 05 06 05 05 05 05 05 05 05 05 06 05 05 07 + 08 09 09 09 09 09 09 09 09 09 09 09 09 09 09 11 + 04 09 09 09 09 09 09 09 10 09 09 09 09 10 09 07 + 08 09 10 09 09 09 09 09 09 09 09 09 09 09 09 11 + 04 09 09 09 09 09 09 09 09 09 09 09 09 09 09 07 + 08 10 09 09 09 09 09 09 09 09 10 09 09 09 09 11 + 04 09 09 09 09 09 10 09 09 09 09 09 09 09 09 07 + 12 13 14 13 14 13 14 13 14 13 14 13 14 13 14 15 + + diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-68.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-68.cs new file mode 100644 index 00000000..50100372 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-68.cs @@ -0,0 +1,26 @@ +private void InitializeLights() +{ + // torch 1 + _lights.Add(new PointLight + { + Position = new Vector2(260, 100), + Color = Color.CornflowerBlue, + Radius = 600 + }); + + // torch 2 + _lights.Add(new PointLight + { + Position = new Vector2(1000, 100), + Color = Color.CornflowerBlue, + Radius = 600 + }); + + // underlight + _lights.Add(new PointLight + { + Position = new Vector2(600, 660), + Color = Color.MonoGameOrange, + Radius = 1200 + }); +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-69.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-69.hlsl new file mode 100644 index 00000000..82427e37 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-69.hlsl @@ -0,0 +1,24 @@ +float2 ScreenSize; +float BoxBlurStride; + +float4 Blur(float2 texCoord) +{ + float4 color = float4(0, 0, 0, 0); + + float2 texelSize = 1 / ScreenSize; + int kernalSize = 1; + float stride = BoxBlurStride * 30; // allow the stride to range up a size of 30 + for (int x = -kernalSize; x <= kernalSize; x++) + { + for (int y = -kernalSize; y <= kernalSize; y++) + { + float2 offset = float2(x, y) * texelSize * stride; + color += tex2D(LightBufferSampler, texCoord + offset); + } + } + + int totalSamples = pow(kernalSize*2+1, 2); + color /= totalSamples; + color.a = 1; + return color; +} diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-70.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-70.cs new file mode 100644 index 00000000..1972accc --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-70.cs @@ -0,0 +1,9 @@ + protected override void Update(GameTime gameTime) + { + // ... + + DeferredCompositeMaterial.SetParameter("ScreenSize", new Vector2(GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height)); + DeferredCompositeMaterial.Update(); + + base.Update(gameTime); + } \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-71.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-71.hlsl new file mode 100644 index 00000000..9c1e7dfd --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-71.hlsl @@ -0,0 +1,11 @@ +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float4 color = tex2D(SpriteTextureSampler,input.TextureCoordinates) * input.Color; + float4 light = Blur(input.TextureCoordinates) * input.Color; + + float3 toneMapped = light.xyz / (.5 + dot(light.xyz, float3(0.299, 0.587, 0.114))); + light.xyz = toneMapped; + + light = saturate(light + AmbientLight); + return color * light; +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-72.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-72.hlsl new file mode 100644 index 00000000..ca3e6a83 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-72.hlsl @@ -0,0 +1,25 @@ +float ShadowFadeStartDistance; +float ShadowFadeEndDistance; +float ShadowIntensity; + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + // get an ordered dither value + int2 pixel = int2(input.TextureCoordinates * ScreenSize); + int idx = (pixel.x % 4) + (pixel.y % 4) * 4; + float ditherValue = bayer4x4[idx]; + + // produce the fade-out gradient + float maxDistance = ScreenSize.x + ScreenSize.y; + float endDistance = ShadowFadeEndDistance; + float startDistance = ShadowFadeStartDistance; + float fade = saturate((input.TextureCoordinates.x - endDistance) / (startDistance - endDistance)); + fade = min(fade, ShadowIntensity); + + if (ditherValue > fade){ + clip(-1); + } + + clip(input.Color.a); + return float4(0,0,0,1); // return black +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-73.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-73.cs new file mode 100644 index 00000000..9793e86e --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-73.cs @@ -0,0 +1,20 @@ +public void DrawLights(List lights, List shadowCasters) +{ + Core.GraphicsDevice.SetRenderTarget(LightBuffer); + Core.GraphicsDevice.Clear(Color.Black); + foreach (var light in lights) + { + var diameter = light.Radius * 2; + var rect = new Rectangle( + (int)(light.Position.X - light.Radius), + (int)(light.Position.Y - light.Radius), + diameter, diameter); + + // initialize the stencil to '1'. + Core.GraphicsDevice.Clear(ClearOptions.Stencil, Color.Black, 0, 1); + + // ... + } + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-74.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-74.cs new file mode 100644 index 00000000..f3516a98 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-74.cs @@ -0,0 +1,17 @@ +_stencilShadowExclude = new DepthStencilState +{ + // instruct MonoGame to use the stencil buffer + StencilEnable = true, + + // in the setup, always set the pixel to '0' + StencilFunction = CompareFunction.Always, + + // Write a '0' anywhere we don't want a shadow to appear + ReferenceStencil = 0, + + // Overwrite the current value + StencilPass = StencilOperation.Replace, + + // ignore depth from the stencil buffer write/reads + DepthBufferEnable = false +}; \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-75.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-75.cs new file mode 100644 index 00000000..54f8c189 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-75.cs @@ -0,0 +1,37 @@ +_stencilWrite = new DepthStencilState +{ + // instruct MonoGame to use the stencil buffer + StencilEnable = true, + + // instruct every fragment to interact with the stencil buffer + StencilFunction = CompareFunction.LessEqual, + + // every operation will increase the shadow value (up to the max of 255), but only when the original + // stencil value was greater or equal to '1'. ('1' is the default clear value) + StencilPass = StencilOperation.IncrementSaturation, + + // this is the value that will be written into the stencil buffer + ReferenceStencil = 1, + + // ignore depth from the stencil buffer write/reads + DepthBufferEnable = false +}; + +_stencilTest = new DepthStencilState +{ + // instruct MonoGame to use the stencil buffer + StencilEnable = true, + + // instruct only fragments that have a current value greater or equal to the + // ReferenceStencil value to interact + StencilFunction = CompareFunction.GreaterEqual, + + // '1' and `0` are the "non shadow" values + ReferenceStencil = 1, + + // don't change the value of the stencil buffer. KEEP the current value. + StencilPass = StencilOperation.Keep, + + // ignore depth from the stencil buffer write/reads + DepthBufferEnable = false +}; \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-76.cs b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-76.cs new file mode 100644 index 00000000..25dd8b48 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-76.cs @@ -0,0 +1,24 @@ +public void DrawLights(List lights, List shadowCasters, Action prepareStencil) +{ + Core.GraphicsDevice.SetRenderTarget(LightBuffer); + Core.GraphicsDevice.Clear(Color.Black); + foreach (var light in lights) + { + var diameter = light.Radius * 2; + var rect = new Rectangle( + (int)(light.Position.X - light.Radius), + (int)(light.Position.Y - light.Radius), + diameter, diameter); + + // initialize the stencil to '1'. + Core.GraphicsDevice.Clear(ClearOptions.Stencil, Color.Black, 0, 1); + + // Anything that draws in this setup will set the stencil back to '0'. This '0' acts as a "don't draw a shadow here". + prepareStencil?.Invoke(_shadowBlendState, _stencilShadowExclude); + + // ... + + } + + // ... +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-78.hlsl b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-78.hlsl new file mode 100644 index 00000000..61fa9fb5 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/09_shadows_effect/snippets/snippet-9-78.hlsl @@ -0,0 +1,14 @@ +PixelShaderOutput MainPS(VertexShaderOutput input) +{ + PixelShaderOutput output; + output.color = ColorSwapPS(input); + + // do not even render the pixel if the alpha is blank. + clip(output.color.a - 1); + + // read the normal data from the NormalMap + float4 normal = tex2D(NormalMapSampler,input.TextureCoordinates); + output.normal = normal; + + return output; +} \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/10_next_steps/index.md b/articles/tutorials/advanced/2d_shaders/10_next_steps/index.md new file mode 100644 index 00000000..7a99422f --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/10_next_steps/index.md @@ -0,0 +1,53 @@ +--- +title: "Chapter 10: Next Steps" +description: "Review your accomplishments and consider next steps" +--- + +Congratulations on making it to the end of the advanced 2D shaders tutorial! We have come an incredibly long way from our starting point. Let's take a moment to look back at everything we've accomplished. + +We started by building a robust **hot-reloading workflow** to make shader development fast and interactive. We then built a `Material` class and a real-time **debug UI** to make our shader code safer and easier to work with. + +With that foundation, we dove into creating effects. We built: +- A flexible, texture-driven **screen transition effect**. +- A powerful **color-swapping system** using a Look-Up Table (LUT). +- A **3D perspective effect** by writing our first custom vertex shader. +- A complete **2D lighting and shadow system** using advanced techniques like deferred rendering, normal mapping, and the stencil buffer. + +You have the tools to develop shaders in MonoGame, and you have implemented a set of effects that bring 2D games to life. + +## Next Steps + +The world of graphics programming is vast and there is always more to learn. If you are excited and want to keep going, here are a few topics to explore. + +#### Advanced Post-Processing + +We touched on post-processing with our scene transition, but there's a whole world of effects you can apply to the final rendered image. Effects like bloom, depth of field, chromatic aberration, and film grain can all be implemented as shaders that process the entire screen to give your game a unique stylistic look. Check out the bloom effect in the [NeonShooter](https://github.com/MonoGame/MonoGame.Samples/blob/3.8.4/NeonShooter/NeonShooter.Core/Content/Shaders/BloomCombine.fx) sample. + +#### Beyond SpriteBatch + +In this tutorial, we worked within the confines of the default `SpriteBatch` vertex format (`VertexPositionColorTexture`). MonoGame can draw arbitrary vertex buffer data with the `GraphicsDevice.DrawPrimitives()` functions. You can draw shapes with more than 4 corners. You can pass extra data _per_ vertex, like a unique id, custom data, or whatever you need for your effects. If you want to get into 3d game development, then you'll need to expand beyond `SpriteBatch`. + +## Continued Reading + +This tutorial series was an exploration of various shader topics, with a focus on MonoGame's tooling. As you continue to develop new effects and shaders for your games, you will undoubtedly need to research far and wide on the internet for help. Graphics code can be notoriously hard. Here are a few resources that may help you. + +- [GDC Vault](https://gdcvault.com/browse?keyword=graphics) has lots of free videos where game developers showcase their groundbreaking graphics advancements in video games. +- [HLSL Intrinsic](https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-intrinsic-functions) lists out the functions you can use in HLSL. +- [The Book Of Shaders](https://thebookofshaders.com/) goes from shader fundamentals to building complicated effects. It has live code editors in the browser so you can tweak the shader and see the result right away! +- [Shadertoy](https://www.shadertoy.com/) is an online shader editor with a live preview in the browser. It also functions as a vibrant library of cool effects and techniques you can learn from. Often, if there is a technique you want to learn, some wizard has implemented it on Shadertoy. +- [Inigo Quilez](https://iquilezles.org/) is a legend in the shader community and has written _many_ fantastic tutorials. +- There are a lot of wonderful Youtube videos that cover shaders. Here are just a few. + - [Dan Moran](https://www.youtube.com/playlist?list=PLJ4rOFLQFH4BUVziWikfHvL8TbNGJ6M_f) makes the _Makin' Stuff Look Good In Video Games_. This channel has videos examining effects in existing games and recreating them by hand. + - [The Art Of Code](https://www.youtube.com/playlist?list=PLGmrMu-IwbguU_nY2egTFmlg691DN7uE5) has several videos writing shaders from scratch with Shadertoy. + - [Acerola](https://www.youtube.com/playlist?list=PLUKV95Q13e_U5g00d5M5MOacpVMiEbW9u) has great case study videos recreating effects from existing games. + - [Freya Holmér](https://www.youtube.com/watch?v=MOYiVLEnhrw) has many great videos that delve into mathematics for game developers. + + +## A Note From The Author + +Hey friend, +If you read through this whole series, then _thank you_ for lending me your time and patience. Hopefully you learned some good information, or at least enjoyed the ride. I love shader programming, but it took me a _long_ time to build up any intuition around computer graphics. I hope that you find the spark in graphics programming that leads you to build beautiful things. + +Best, + +-[Chris Hanna](https://github.com/cdhanna) \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/index.md b/articles/tutorials/advanced/2d_shaders/index.md new file mode 100644 index 00000000..f2af98b3 --- /dev/null +++ b/articles/tutorials/advanced/2d_shaders/index.md @@ -0,0 +1,46 @@ +--- +title: Advanced 2D Shaders in MonoGame +description: A tutorial series on creating advanced 2D visual effects with custom shaders in MonoGame. +--- + +This tutorial series picks up where the [Building 2D Games](./../../building_2d_games/index.md) tutorial left off, diving deep into the world of custom shaders. The goal is to develop tooling for MonoGame to facilitate shader development, write some custom effects for _DungeonSlime_, and build intuition for how to approach shader programming. + +## What We Will Build + +Throughout this series, we will take our _Dungeon Slime_ game and add a whole new layer of visual polish. By the end, you will have implemented a variety of advanced shader effects, including: + +- A scene transition effect, +- A color swap effect, +- A 3d effect using `SpriteBatch`, +- A lighting system using techniques from deferred rendering, +- A shadow system using stencil buffers. + +![_Dungeon Slime_ will look like this at the end of this series](./videos/final.mp4) + +## Who This Is For + +This documentation is for intermediate MonoGame users. It assumes that you have a solid understanding of C# and have _already completed the entire "Building 2D Games" tutorial series._ + +> [!IMPORTANT] +> The concepts and code from the original tutorial, especially the [Shaders](./../../building_2d_games/24_shaders/index.md) chapter, are a mandatory prerequisite. We will be building directly on top of the final project from that series. + +## Table of Contents + +This documentation is organized to be read sequentially, as each chapter builds upon the techniques and code from the previous one. + +| Chapter | Summary | Source Files | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Chapter 01: Introduction](01_introduction/index.md) | An overview of the advanced shader techniques covered in this tutorial series. | [Final Chapter from Building-2D-Games Series](https://github.com/MonoGame/MonoGame.Samples/tree/3.8.5/Tutorials/learn-monogame-2d/src/24-Shaders/) | +| [Chapter 02: Hot Reload](02_hot_reload/index.md) | Set up a workflow with a "hot-reload" system to recompile and view shader changes in real-time without restarting the game. | [02-Hot-Reload](https://github.com/MonoGame/MonoGame.Samples/tree/3.8.5/Tutorials/2dShaders/src/02-Hot-Reload-System/) | +| [Chapter 03: The Material Class](03_the_material_class/index.md) | Create a `Material` class to manage shader parameters and handle the complexities of the shader compiler and our hot-reload system. | [03-Material-Class](https://github.com/MonoGame/MonoGame.Samples/tree/3.8.5/Tutorials/2dShaders/src/03-The-Material-Class) | +| [Chapter 04: Debug UI](04_debug_ui/index.md) | Integrate ImGui.NET to build a real-time debug UI, allowing for runtime manipulation of shader parameters. | [04-Debug-UI](https://github.com/MonoGame/MonoGame.Samples/tree/3.8.5/Tutorials/2dShaders/src/04-Debug-UI) | +| [Chapter 05: Transition Effect](05_transition_effect/index.md) | Create a flexible, texture-driven screen wipe for smooth scene transitions. | [05-Transition-Effect](https://github.com/MonoGame/MonoGame.Samples/tree/3.8.5/Tutorials/2dShaders/src/05-Transition-Effect) | +| [Chapter 06: Color Swap Effect](06_color_swap_effect/index.md) | Implement a powerful color-swapping system using a 1D texture as a Look-Up Table (LUT), allowing for dynamic color palette changes. | [06-Color-Swap-Effect](https://github.com/MonoGame/MonoGame.Samples/tree/3.8.5/Tutorials/2dShaders/src/06-Color-Swap-Effect) | +| [Chapter 07: Sprite Vertex Effect](07_sprite_vertex_effect/index.md) | Dive into the vertex shader to manipulate sprite geometry, break out of the 2D plane, and give the game a dynamic 3D perspective. | [07-Sprite-Vertex-Effect](https://github.com/MonoGame/MonoGame.Samples/tree/3.8.5/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect) | +| [Chapter 08: Light Effect](08_light_effect/index.md) | Build a 2D dynamic lighting system from scratch using a deferred rendering approach, complete with color, light, and normal map buffers. | [08-Light-Effect](https://github.com/MonoGame/MonoGame.Samples/tree/3.8.5/Tutorials/2dShaders/src/08-Light-Effect) | +| [Chapter 09: Shadows Effect](09_shadows_effect/index.md) | Add dynamic 2D shadows to the lighting system by using a vertex shader and the stencil buffer for efficient light masking. | [09-Shadows-Effect](https://github.com/MonoGame/MonoGame.Samples/tree/3.8.5/Tutorials/2dShaders/src/09-Shadows-Effect/) | +| [Chapter 10: Conclusion & Next Steps](10_next_steps/index.md) | Review the techniques learned throughout the series and preview further graphics programming topics to continue your journey. | | + +## Get Started + +- [Chapter 01: Introduction](01_introduction/index.md) \ No newline at end of file diff --git a/articles/tutorials/advanced/2d_shaders/videos/final.mp4 b/articles/tutorials/advanced/2d_shaders/videos/final.mp4 new file mode 100644 index 00000000..1e9b4667 Binary files /dev/null and b/articles/tutorials/advanced/2d_shaders/videos/final.mp4 differ diff --git a/ci.docfx.json b/ci.docfx.json index 1fc4b077..b865cf99 100644 --- a/ci.docfx.json +++ b/ci.docfx.json @@ -50,6 +50,7 @@ "resource": [ { "files": [ + "**/gifs/**", "**/images/**", "**/videos/**", "**/files/**", diff --git a/docfx.json b/docfx.json index 36d1fa7d..1e878578 100644 --- a/docfx.json +++ b/docfx.json @@ -50,6 +50,7 @@ "resource": [ { "files": [ + "**/gifs/**", "**/images/**", "**/videos/**", "**/files/**", diff --git a/external/MonoGame b/external/MonoGame index 0ac4f81b..2387ae37 160000 --- a/external/MonoGame +++ b/external/MonoGame @@ -1 +1 @@ -Subproject commit 0ac4f81ba7ed44f36b7a4dbee34069b423f3d7b9 +Subproject commit 2387ae37d37f50d0de37c9839ccb8dec53fb5454 diff --git a/templates/monogame/layout/_master.tmpl b/templates/monogame/layout/_master.tmpl index bfd763ff..c01357f4 100644 --- a/templates/monogame/layout/_master.tmpl +++ b/templates/monogame/layout/_master.tmpl @@ -77,9 +77,27 @@
{{/_enableSearch}} + {{>partials/footer}} + + {{! + DocFX 2.75 does not include hlsl, and hides its internal highlight.js version. + This code re-imports highlight.js, (which feels wasteful, but I cannot figure anything else out) + and then adds a custom hlsl plugin, and highlights elements that were marked with the `hlsl` lang. + }} + + + + + {{/redirect}} diff --git a/templates/monogame/public/hlsl.min.js b/templates/monogame/public/hlsl.min.js new file mode 100644 index 00000000..04293421 --- /dev/null +++ b/templates/monogame/public/hlsl.min.js @@ -0,0 +1,15 @@ +// origin: https://github.com/highlightjs/highlightjs-hlsl +hljs.registerLanguage("hlsl",(()=>{"use strict";const e={className:"number", +begin:"(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?([hHfFlL]?)|\\.\\d+)([eE][-+]?\\d+)?([hHfFlL]?))", +relevance:0};return r=>{ +let t=["","1","2","3","4","1x1","1x2","1x3","1x4","2x1","2x2","2x3","2x4","3x1","3x2","3x3","3x4","4x1","4x2","4x3","4x4"],a=[] +;for(let e of"bool double float half int uint min16float min10float min16int min12int min16uint".split(" "))for(let r of t)a.push(e+r) +;let s="SV_Coverage SV_Depth SV_DispatchThreadID SV_DomainLocation SV_GroupID SV_GroupIndex SV_GroupThreadID SV_GSInstanceID SV_InnerCoverage SV_InsideTessFactor SV_InstanceID SV_IsFrontFace SV_OutputControlPointID SV_Position SV_PrimitiveID SV_RenderTargetArrayIndex SV_SampleIndex SV_StencilRef SV_TessFactor SV_VertexID SV_ViewportArrayIndex, SV_ShadingRate",o="BINORMAL BLENDINDICES BLENDWEIGHT COLOR NORMAL POSITION PSIZE TANGENT TEXCOORD TESSFACTOR DEPTH SV_ClipDistance SV_CullDistance SV_DepthGreaterEqual SV_DepthLessEqual SV_Target SV_CLIPDISTANCE SV_CULLDISTANCE SV_DEPTHGREATEREQUAL SV_DEPTHLESSEQUAL SV_TARGET",n=o.split(" ") +;for(let e of o.split(" "))for(let r of Array(16).keys())n.push(e+r.toString()) +;return{name:"HLSL",keywords:{ +keyword:"AppendStructuredBuffer asm asm_fragment BlendState break Buffer ByteAddressBuffer case cbuffer centroid class column_major compile compile_fragment CompileShader const continue ComputeShader ConsumeStructuredBuffer default DepthStencilState DepthStencilView discard do DomainShader dword else export extern false for fxgroup GeometryShader groupshared Hullshader if in inline inout InputPatch interface line lineadj linear LineStream matrix namespace nointerpolation noperspective NULL out OutputPatch packoffset pass pixelfragment PixelShader point PointStream precise RasterizerState RenderTargetView return register row_major RWBuffer RWByteAddressBuffer RWStructuredBuffer RWTexture1D RWTexture1DArray RWTexture2D RWTexture2DArray RWTexture3D sample sampler SamplerState SamplerComparisonState shared snorm stateblock stateblock_state static string struct switch StructuredBuffer tbuffer technique technique10 technique11 texture Texture1D Texture1DArray Texture2D Texture2DArray Texture2DMS Texture2DMSArray Texture3D TextureCube TextureCubeArray true typedef triangle triangleadj TriangleStream uint uniform unorm unsigned vector vertexfragment VertexShader void volatile while", +type:a.join(" ")+" Buffer vector matrix sampler SamplerState PixelShader VertexShader texture Texture1D Texture1DArray Texture2D Texture2DArray Texture2DMS Texture2DMSArray Texture3D TextureCube TextureCubeArray struct typedef", +built_in:"POSITIONT FOG PSIZE VFACE VPOS "+n.join(" ")+" "+s+" "+s.toUpperCase()+" abort abs acos all AllMemoryBarrier AllMemoryBarrierWithGroupSync any asdouble asfloat asin asint asuint atan atan2 ceil CheckAccessFullyMapped clamp clip cos cosh countbits cross D3DCOLORtoUBYTE4 ddx ddx_coarse ddx_fine ddy ddy_coarse ddy_fine degrees determinant DeviceMemoryBarrier DeviceMemoryBarrierWithGroupSync distance dot dst errorf EvaluateAttributeAtCentroid EvaluateAttributeAtSample EvaluateAttributeSnapped exp exp2 f16tof32 f32tof16 faceforward firstbithigh firstbitlow floor fma fmod frac frexp fwidth GetRenderTargetSampleCount GetRenderTargetSamplePosition GroupMemoryBarrier GroupMemoryBarrierWithGroupSync InterlockedAdd InterlockedAnd InterlockedCompareExchange InterlockedCompareStore InterlockedExchange InterlockedMax InterlockedMin InterlockedOr InterlockedXor isfinite isinf isnan ldexp length lerp lit log log10 log2 mad max min modf msad4 mul noise normalize pow printf Process2DQuadTessFactorsAvg Process2DQuadTessFactorsMax Process2DQuadTessFactorsMin ProcessIsolineTessFactors ProcessQuadTessFactorsAvg ProcessQuadTessFactorsMax ProcessQuadTessFactorsMin ProcessTriTessFactorsAvg ProcessTriTessFactorsMax ProcessTriTessFactorsMin radians rcp reflect refract reversebits round rsqrt saturate sign sin sincos sinh smoothstep sqrt step tan tanh tex1D tex1Dbias tex1Dgrad tex1Dlod tex1Dproj tex2D tex2Dbias tex2Dgrad tex2Dlod tex2Dproj tex3D tex3Dbias tex3Dgrad tex3Dlod tex3Dproj texCUBE texCUBEbias texCUBEgrad texCUBElod texCUBEproj transpose trunc", +literal:"true false"},illegal:'"', +contains:[r.C_LINE_COMMENT_MODE,r.C_BLOCK_COMMENT_MODE,e,{className:"meta", +begin:"#",end:"$"}]}}})()); \ No newline at end of file diff --git a/templates/monogame/public/main.css b/templates/monogame/public/main.css index 674e5d69..49b08972 100644 --- a/templates/monogame/public/main.css +++ b/templates/monogame/public/main.css @@ -248,6 +248,14 @@ td > .xref { word-break: normal; } +/* + Sometimes the default table styling will leave later columns in a table squished. + Put a
tag rigt before a table to force constant width. +*/ +.fixed-table + .table-responsive table { + table-layout: fixed !important; +} + /******************************************************************************* *** Section: Question and Answer Sections *** Styling for the questions and answers sections in tutorials