Introduction
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 Texture2D
; BlendedTexture2D
depends on this relationship and will not compile in previous versions of the framework.
Background
The Problem
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.
Enter BlendedTexture2D
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 LightingEffect
(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 GraphicsDevice.Clear(…)
from 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. GetData(…)
and SetData(…)
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 GetData(…)
/SetData(…)
solution because:
- It was simpler.
- It didn’t involve arbitrary numbers of references to
ContentManager
-managed content from our totally unmanaged BlendedTexture2D
- I didn’t have to solve the thorny issue of recreating recursive drawing of a
BlendedTexture2D
onto itself, or another BlendedTexture2D
, etc..
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
on the 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;
}
}
Device Loss
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
API
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()
ContentRegained
Important caveat: If using a BlendedTexture2D
as a parameter in a shader/Effect
, you must handle BlendedTexture2D.ContentRegained
and reassign the texture to the Effect
(which has lost its handle for the volatile texture during the device reset).
Example
Abbreviated excerpts from the example application:
Initializing textures and BlendedTexture2D
:
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;
_blendedTexture.AddDecal(_stampTexture, stampPos);
Passing BlendedTexture2D
to a Draw(…)
call as a Texture2D
:
spriteBatch.Begin();
spriteBatch.Draw(_blendedTexture, Vector2.Zero, Color.White);
spriteBatch.End();
Final Comments
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!
History
- 03/25/2013 - Original submission.