BlendedTexture2D is fairly compact XNA class derived from
RenderTarget2D (and therefore
Texture2D) that deals with some of the headaches of generating dynamic textures by drawing to an
off-screen buffer. The source includes the full BlendedTexture2D.cs class file and a simple demo program. Left-click to stamp on the checkerboard, and gasp in wonderment as it minimizes and restores without incident, and is even dragged from one display to another. The rest of the article will explore the stumbling blocks in its development and the solutions I chose.
In XNA 4.0,
RenderTarget2D became a subclass of
BlendedTexture2D depends on this relationship and will not compile in previous versions of the framework.
My first interaction with render targets in XNA was implementing functionality to “paint” onto a textured model in realtime using a Photoshop-like brush in XNA 3.0. This involved juggling RenderTargets, of course, to draw to the offscreen buffer in the first place.
GraphicsDevice resets, most commonly from the window being minimized or dragged between displays, would cause the texture to revert to its original, unmodified state, so those had to be handled. I came up with code that was good enough to demo the functionality and then forgot about it for a while.
A couple of years later, I was working on a sim involving pigs. The pigs had a base texture, and could have any of several identically-sized, alpha-blended overlays (irritation, diarrhea, etc.) drawn over it. The catch was that we were now using the Sunburn Gaming Engine, and weren’t inclined to toss together a Sunburn-friendly shader to take anywhere from one to four textures and draw them in order. This seemed like an ideal time to dust off the old “spray painter” code and generalize it into something sexier, more elegant, and above all less tedious to reuse.
The goal was to provide a simple, clean interface to heap multiple textures into one
Texture2D. We could then pass that texture as a parameter for a standard Sunburn
BlendedTexture2D is not itself Sunburn-dependent). The first, and most obvious part of the process to internalize was the spritebatch and render target management when drawing to the offscreen buffer. A close second was handling of graphics device resets. Of course, other problems came up on the way.
RenderTargets (handled internally)
This was pretty straightforward, but in a real-world situation, we quickly run into issues. Our
BlendedTexture2D clears itself to that lovely purple, as all buffers do when they become active on the graphics device. Incidentally, this includes the front and back buffers, as you can quickly confirm in a new XNA project by commenting out
Draw(…) – its initial content is default purple. In addition, a
RenderTarget2D can’t draw itself over itself (something which our API explicitly permits by casting to
Texture2D and passing it to
AddDecal(…)) and will throw an exception if we try. We can kill two birds with one stone by using a second
RenderTarget2D: we draw our existing texture onto it, draw the decal over, then switch back to our
BlendedTexture2D and draw the final result into its buffer. Because draw calls don’t necessarily happen instantly, we can’t immediately dispose
_tempBuffer; for this reason, I maintained one persistent
RenderTarget2D as a field.
Device Resets (handled internally)
Lots of things can cause a device reset, including minimizing the game window, running another Direct3d-based application in fullscreen, or dragging the game window between displays. When this happens, all of our buffers (including
_tempBuffer and our
BlendedTexture2D) get cleared to their base purple. The solution is straightforward: handle
GraphicsDevice.DeviceResetting to copy the contents of our buffer to system memory, and handle
GraphicsDevice.DeviceReset to copy it back.
are reviled as some of the slowest graphics operations you can perform, but a little performance hiccup is better than losing all your dynamic textures. Really heavy use of
BlendedTexture2D might warrant a brief user-friendly loading indicator when regaining the graphics device. If system memory is limited, this might also be an issue.
Device Resets: alternate solution
There’s another solution to recreating dynamic textures after a device reset, which is to actually do a step-by-step recreation. The first naïve solution that comes to mind is to store references to the base texture and whatever information you want on the decals (texture reference, position, scale, etc.) and iterate through drawing each of them after the reset. Under most circumstances I would expect it to be faster. In addition, it would handle device loss (see below) better than the solution I chose.
That said, I went with the
SetData(…) solution because:
- It was simpler.
- It didn’t involve arbitrary numbers of references to
ContentManager-managed content from our totally unmanaged
- I didn’t have to solve the thorny issue of recreating recursive drawing of a
BlendedTexture2D onto itself, or another
Calling SetData(…) on an active resource (handled internally)
I was getting inconsistent exceptions thrown during device resets, complaining that I “may not call
SetData on a resource while it is actively set
GraphicsDevice. Unset it from the device before calling
SetData.” Issue avoided by checking
GraphicsDevice.Textures for our texture
and simply removing it:
for (int i = 0; i < 16; i++)
if (GraphicsDevice.Textures[i] == this)
GraphicsDevice.Textures[i] = null;
There’s one thing that
BlendedTexture2D will absolutely not recover from: total device loss, as from the user locking the computer, shuffling the primary display around, resetting the resolution on any active display, etc.. This will dump every
RenderTarget2D’s buffer without giving the game a chance to back it up, unfortunately. The
GraphicsDevice reset “alternate solution” would cope better with device loss, though even then it would run smoothest with significant code outside
BlendedTexture2D for the game to monitor the state of the
GraphicsDevice and act accordingly.
Where to go from here?
I didn’t implement the full
SpriteBatch.Draw(Texture2D, …) parameter line in
AddDecal(…) because I found it unwieldy, but it would certainly make it more flexible. The (rarer) issue of full device loss is unresolved. Being able to set a level of alpha transparency in an
AddDecal(...) call, or perhaps as a property on the
BlendedTexture2D, would be very nice. Not requiring an initial
Texture2D base texture for an arbitrarily-sized
BlendedTexture2D could be useful, although not something I’ve needed.
I haven’t yet had a chance to confirm that
BlendedTexture2D behaves identically under MonoGame.
Using the code
In addition to the members inherited from RenderTarget2D:
public class BlendedTexture2D : RenderTarget2D
public BlendedTexture2D(Texture2D baseTexture, bool isDataCopied = true)
public void AddDecal(Texture2D decal, …) public Texture2D BakeToTexture()
Important caveat: If using a
BlendedTexture2D as a parameter in a shader/
Effect, you must handle
and reassign the texture to the
Effect (which has lost its handle for the volatile texture during the device reset).
Abbreviated excerpts from the example application:
Initializing textures and
Texture2D background = Content.Load<texture2d>("checkerboard");
_blendedTexture = new BlendedTexture2D(background);
_stampTexture = Content.Load<texture2d>("stamp");
_stampOffset = new Vector2(-_stampTexture.Width / 2, -_stampTexture.Height / 2);
Adding a decal:
Vector2 stampPos = new Vector2(currentMouseState.X, currentMouseState.Y) + _stampOffset;
BlendedTexture2D to a
Draw(…) call as a
spriteBatch.Draw(_blendedTexture, Vector2.Zero, Color.White);
I hope you found this article useful or interesting. If you have any concerns or suggestions about the source or the content of the article itself, don't hesitate to let me know!
- 03/25/2013 - Original submission.