Click here to Skip to main content
15,885,537 members
Articles / Game Development / XBox

BlendedTexture2D

Rate me:
Please Sign up or sign in to vote.
4.80/5 (2 votes)
25 Mar 2013CPOL6 min read 16.5K   223   5   1
A RenderTarget2D-derived class to blend textures at runtime that internally deals with some common headaches.

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:

  1. It was simpler.
  2. It didn’t involve arbitrary numbers of references to ContentManager-managed content from our totally unmanaged BlendedTexture2D
  3. 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:

C#
// Make sure this isn't one of the active texture resources.
// 16 is a magic number that should apply to all ps_2_0 and ps_3_0 GPUs.
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:

C#
public class BlendedTexture2D : RenderTarget2D
		
// Public Constructor
public BlendedTexture2D(Texture2D baseTexture, bool isDataCopied = true)
 
// Public Methods
public void AddDecal(Texture2D decal, …) // Draws decal over existing BlendedTexture2D
public Texture2D BakeToTexture() // Returns non-volatile copy of BlendedTexture2D

// Public Events
ContentRegained // Raised when the BlendedTexture2D internally recovers from device reset

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:

C#
// Loading the checkerboard texture to initialize blendedTexture from
Texture2D background = Content.Load<texture2d>("checkerboard");
_blendedTexture = new BlendedTexture2D(background);

// Precalculating distance from upper left corner to center of stamp,
// since we want it to appear centered on the cursor.
_stampTexture = Content.Load<texture2d>("stamp");
_stampOffset = new Vector2(-_stampTexture.Width / 2, -_stampTexture.Height / 2);

Adding a decal:

C#
Vector2 stampPos = new Vector2(currentMouseState.X, currentMouseState.Y) + _stampOffset;
_blendedTexture.AddDecal(_stampTexture, stampPos); 

Passing BlendedTexture2D to a Draw(…) call as a Texture2D:

C#
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer ForgeFX LLC
United States United States
Daniel has been developing serious games using XNA since 2008.

Comments and Discussions

 
GeneralMy vote of 4 Pin
Southmountain25-Mar-13 5:53
Southmountain25-Mar-13 5:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.