After years of patient tinkering, I've finally cracked the problem which has eluded me the longest: Multiple effects on a single texture. To most this sounds "technical" or maybe easy to understand, but Monogame makes it anything but. (This is a slightly technical read)
I don't feel so good
I love Monogame. Its everything I want in an entry-level game engine and I have spent a lot of time building my own engine on top of it. For all of its conveniences, what I found particularly tricky is using multiple effects at the same time like say a color shift and a stroke.
control color shift stroke
The color shift is a simple effect. You multiply the red value of each pixel by a factor, the green value by a factor, the blue etc. And I use it to show things being frosted or illusory.
The stroke is a little more math, but it looks for alpha (transparency) and determines whether or not to make it a solid pixel of some color, or the original pixel. It makes objects in motion pop against the colorful backgrounds:
stroke type shader: https://www.shadertoy.com/view/flc3DN
Now in Monogame, the trick is that when you apply an 'Effect', you are running it over a texture. The texture is a .png you've loaded off your hard drive and the effect is in this case is some pixel shader code which does math on the texture. The challenge in Monogame with having multiple effects is that you want each effect to run over the result of the previous effect pass. Applying multiple effects before drawing the texture however, would only use the result from the last effect.
Desired Effect:
Actual Behavior:
I am not a career game developer, so this has all been a learning experience. This problem may have been solved by all sorts of developers and organizations working within and around Monogame. However I spent a lot of hard hours on google trying to solve this problem-and if the answer is out there it is not near the surface.
For a while, I was stuck with this single Effect. You could have a border, you could have a color shift, but not both. And this is a big deal. Effects are cheap and easy. You can find a bunch on shadertoy.com. They add instant production value and can elevate a game. I was very troubled by this. I feared that if this was not possible, I would have to cut content or work harder to mimic the effects I wanted. Ultimately I might have to abandon Monogame in future projects. I'm sure Unity or Unreal could do this, but boy are those much more complicated to use.
The first naïve approach was the "supershader". Instead of writing multiple effects, write one effect and combine all the parameters and all of the code. You could combine the colorshift.fx and stroke.fx into one single effect. Need to do more? add more code. You can understand why this approach is unappealing. I preferred to keep things neat and this was as ugly a hack as you could imagine, growing with every new effect I might want to add.
About 6 months ago, I started to explore the strange and interesting concept of the RenderTargets. This is a common feature in most game engines, and once I got it started I knew this was going to be the answer. A RenderTarget is basically a blank texture. What's special about it is that you can draw to it and then draw the result. Normally when drawing a texture in Monogame, you are drawing to the screen.
Basic Monogame Render Process:
My initial thought was "how do I use a render target to store the results?" The solution would have to look like this:
Drawing to a RenderTarget:
After a few late nights and disappointing weekends, I put this problem back on the shelf. I had progressed, I had gotten the number of effects I could use up to 2 (like in the above diagram). I would make some sprites have a RenderTarget2D, and draw the texture and 1 effect to the RenderTarget. Then when drawing RenderTarget to the screen, I would take the first result and apply another shader. This had me make some effects RenderOnly and some effects DrawOnly.
At this point I knew it could be done. I had all the magic words but I couldn't put them in the right order. It just so happened a few weeks ago that I would butt up against this problem again. I had made a breakthrough and was able to put multiple effects on some textures in the lab, but not in the real thing.
I immediately got very close, I was able to render it out once, but as soon as I had to do the process every frame, things got weird.
And the morale of the story is this: the Monogame discord has some hardcore programmers that know a tremendous amount of this sticky stuff. The real guts of the engine. I bring some hard problem in there about once a year and within the limits of my understanding somebody can turn around a solution in about 10 minutes of asking.
The source of my failures was truly strange to me-I had wandered into areas of the engine I had no clue were 'undefined'. I was trying to draw a RenderTarget onto itself. This is something you shouldn't do. You are simultaneously writing and sampling from the same block of memory that is your texture.
It makes a cool effect, but unfortunately that effect is hardware dependent. You might not get it to work on everyone's computer. However the solution to this is to use not 1 but 2 RenderTargets:
The second render target allows you to draw the contents of the first without sampling the same texture you're writing to. With this you can have n amount of effects without any monster shaders. You just have to worry about which order you apply them. Then at the end, draw it to the screen.
To the non-programmers hopefully that makes some sense. And to the experts, this may fall short of your expectations. To me, this was not only a notorious issue and a bullet point in my "Engine Improvements" document. This was one of my biggest concerns with a 2D engine. Using multiple effects is really nice and even with my limited artistry I find it essential. I want to pixelate things, I want to blur things, I want to make them slightly blue. I suspect in the future I will move on to more robust engines. Unity, Unreal, and who knows what's next. I truly appreciate Monogame and for now, it can live another day.
The Aftermath
Comments