After Writing to a RenderTarget, How to Efficiently Clone the Output?
Asked Answered
I

3

9

XNA noob here, learning every day. I just worked out how to composite multiple textures into one using a RenderTarget2D. However, while I can use the RenderTarget2D as a Texture2D for most purposes, there's a critical difference: these rendered textures are lost when the backbuffer is resized (and no doubt under other circumstances, like the graphics device running low on memory).

For the moment, I'm just copying the finished RenderTarget2D into a new non-volatile Texture2D object. My code to do so is pretty fugly, though. Is there a more graceful way to do this? Maybe I'm just tired but I can't find the answer on Google or SO.

Slightly simplified:

public static Texture2D  MergeTextures(int width, int height, IEnumerable<Tuple<Texture2D, Color>> textures)
    {
    RenderTarget2D  buffer = new RenderTarget2D(_device, width, height);

    _device.SetRenderTarget(buffer);
    _device.Clear(Color.Transparent);

    SpriteBatch  spriteBatch = new SpriteBatch(_device);
    spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied);

    // Paint each texture over the one before, in the appropriate color
    Rectangle  rectangle = new Rectangle(0, 0, width, height);
    foreach (Tuple<Texture2D, Color> texture in textures)
        spriteBatch.Draw(texture.Item1, rectangle, texture.Item2);

    spriteBatch.End();
    _device.SetRenderTarget((RenderTarget2D)null);

    // Write the merged texture to a Texture2D, so we don't lose it when resizing the back buffer
    // This is POWERFUL ugly code, and probably terribly, terribly slow
    Texture2D  mergedTexture = new Texture2D(_device, width, height);
    Color[]    content       = new Color[width * height];
    buffer.GetData<Color>(content);
    mergedTexture.SetData<Color>(content);
    return mergedTexture;
    }

I suppose I should check for IsContentLost and re-render as needed, but this happens in the middle of my main drawing loop, and of course you can't nest SpriteBatches. I could maintain a "render TODO" list, handle those after the main SpriteBatch ends, and then they'd be available for the next frame. Is that the preferred strategy?

This code is only called a few times, so performance isn't a concern, but I'd like to learn how to do things right.

Inconvertible answered 1/4, 2011 at 4:49 Comment(1)
+1. Your question and Andrew's answer helped me a lot :)Feld
H
4

Actually your code is not so bad if you're generating textures in a once-off process when you'd normally load content (game start, level change, room change, etc). You're transferring textures between CPU and GPU, same thing you'd be doing loading plain ol' textures. It's simple and it works!

If you're generating your textures more frequently, and it starts to become a per-frame cost, rather than a load-time cost, then you will want to worry about its performance and perhaps keeping them as render targets.

You shouldn't get ContentLost in the middle of drawing, so you can safely just respond to that event and recreate the render targets then. Or you can check for IsContentLost on each of them, ideally at the start of your frame before you render anything else. Either way everything should be checked before your SpriteBatch begins.

(Normally when using render targets you're regenerating them each frame anyway, so you don't need to check them in that case.)

Hidalgo answered 1/4, 2011 at 5:50 Comment(3)
It is indeed a one-time initialization process, other than recreating the textures if their contents are lost. The only reason I'm not doing this on the fly if IsContentLost comes back false is because I need to "hijack" the device drive to write to the RenderTarget2D, which I can't do in the middle of Draw() because I have an open SpriteBatch. So unless I'm missing something, you're suggesting that I validate and recreate volatile resources before starting my main sprite drawing loop? I only hesitate because it may mean rerendering textures that won't actually be needed this frame.Inconvertible
@Jon: Yes, I mean that you should recreate your targets before you draw anything else that frame. You can actually do other things mid-sprite-batch (see SpriteSortMode.Deferred). The problem is changing render target mid-frame will clear your framebuffer by default (due to Xbox 360 performance, see RenderTargetUsage.DiscardContents).Hidalgo
If you want to lazy-create them: you could skip drawing them that one frame and queue them for recreation, then draw them at the start of the next frame - but this is ugly. Normally for dynamic resources they are either always needed, or you can easily detect if they are needed, so you can prepare them before-hand. But, as I said, seeing as these are static resources, you should treat them as such and put them in Texture2Ds. Probably during LoadContent (yes, you can use the graphics device in LoadContent).Hidalgo
S
2

Replace

Texture2D  mergedTexture = new Texture2D(_device, width, height);
Color[]    content       = new Color[width * height];
buffer.GetData<Color>(content);
mergedTexture.SetData<Color>(content);
return mergedTexture;

with

return buffer;

Because RenderTarget2D extends Texture2D you will just get Texture2D class data returned. Also in case you are interested here's a class i made for building my GUI library's widgets out of multiple textures. In case you need to be doing this sort of thing a lot.

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.IO;

namespace Voodo.Utils {

    /// <summary>
    /// 
    /// </summary>
    public class TextureBaker {

        private readonly SpriteBatch _batch;
        private readonly RenderTarget2D _renderTarget;
        private readonly GraphicsDevice _graphicsDevice;

        /// <summary>
        /// 
        /// </summary>
        public Rectangle Bounds {
            get { return _renderTarget.Bounds; }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="graphicsDevice"></param>
        /// <param name="size"></param>
        public TextureBaker(GraphicsDevice graphicsDevice, Vector2 size) {

            _graphicsDevice = graphicsDevice;

            _batch = new SpriteBatch(_graphicsDevice);
            _renderTarget = new RenderTarget2D(
                _graphicsDevice, 
                (int)size.X, 
                (int)size.Y);

            _graphicsDevice.SetRenderTarget(_renderTarget);

            _graphicsDevice.Clear(Color.Transparent);

            _batch.Begin(
                SpriteSortMode.Immediate, 
                BlendState.AlphaBlend, 
                SamplerState.LinearClamp,
                DepthStencilState.Default, 
                RasterizerState.CullNone);
        }

        #region Texture2D baking

        /// <summary>
        /// 
        /// </summary>
        /// <param name="texture"></param>
        public void BakeTexture(Texture2D texture) {

            _batch.Draw(
                texture,
                new Rectangle(0, 0, Bounds.Width, Bounds.Height), 
                Color.White);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="texture"></param>
        /// <param name="destination"></param>
        public void BakeTexture(Texture2D texture, Rectangle destination) {

            _batch.Draw(
                texture,
                destination,
                Color.White);
        }        

        /// <summary>
        /// 
        /// </summary>
        /// <param name="texture"></param>
        /// <param name="destination"></param>
        /// <param name="source"></param>
        public void BakeTexture(Texture2D texture, Rectangle destination, Rectangle source) {

            _batch.Draw(
                texture,
                destination,
                source,
                Color.White);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="texture"></param>
        /// <param name="sourceModification"></param>
        /// <param name="destination"></param>
        public void BakeTexture(Texture2D texture, System.Drawing.RotateFlipType sourceModification, Rectangle destination) {

            Stream sourceBuffer = new MemoryStream();
            texture.SaveAsPng(sourceBuffer, texture.Width, texture.Height);

            System.Drawing.Image sourceImage = System.Drawing.Image.FromStream(sourceBuffer);

            sourceBuffer = new MemoryStream();
            sourceImage.RotateFlip(sourceModification);
            sourceImage.Save(sourceBuffer, System.Drawing.Imaging.ImageFormat.Png);                       

            _batch.Draw(
                Texture2D.FromStream(_graphicsDevice, sourceBuffer),
                destination,
                Color.White);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="texture"></param>
        /// <param name="sourceModification"></param>
        /// <param name="destination"></param>
        /// <param name="source"></param>
        public void BakeTexture(Texture2D texture, System.Drawing.RotateFlipType sourceModification, Rectangle destination, Rectangle source) {

            Stream sourceBuffer = new MemoryStream();
            texture.SaveAsPng(sourceBuffer, texture.Width, texture.Height);

            System.Drawing.Image sourceImage = System.Drawing.Image.FromStream(sourceBuffer);

            sourceBuffer = new MemoryStream();
            sourceImage.RotateFlip(sourceModification);
            sourceImage.Save(sourceBuffer, System.Drawing.Imaging.ImageFormat.Png);

            _batch.Draw(
                Texture2D.FromStream(_graphicsDevice, sourceBuffer),
                destination,
                source,
                Color.White);
        }

        #endregion

        #region SpriteFont baking

        /// <summary>
        /// 
        /// </summary>
        /// <param name="font"></param>
        /// <param name="text"></param>
        /// <param name="location"></param>
        /// <param name="textColor"></param>
        public void BakeText(SpriteFont font, string text, Vector2 location, Color textColor) {

            _batch.DrawString(font, text, location, textColor);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="font"></param>
        /// <param name="text"></param>
        /// <param name="location"></param>
        public void BakeTextCentered(SpriteFont font, string text, Vector2 location, Color textColor) {

            var shifted = new Vector2 {
                X = location.X - font.MeasureString(text).X / 2,
                Y = location.Y - font.MeasureString(text).Y / 2
            };

            _batch.DrawString(font, text, shifted, textColor);
        }

        #endregion

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public Texture2D GetTexture() {

            _batch.End();
            _graphicsDevice.SetRenderTarget(null);

            return _renderTarget;
        }
    }
}
Singular answered 1/5, 2011 at 7:36 Comment(1)
That's what I started with, but as I outlined in the first para of my question, RenderTarget2D is volatile. I can work around that, but the question was about how to instantiate a RenderTarget so I don't have to recreate it.Inconvertible
A
0

if you are having problems with your rendertarget being dynamically resized when drawing it somewhere else, you could just have an off-screen rendertarget with a set size that you copy your finished RT to like this:

Rendertarget2D offscreenRT = new RenderTarget2D(_device, width, height);
_device.SetRenderTarget(offscreenRT);
_device.Clear(Color.Transparent);

SpriteBatch  spriteBatch = new SpriteBatch(_device);
spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied);
spriteBatch.Draw(buffer, Vector2.Zero, Color.White);
spriteBatch.End();
_device.SetRenderTarget(null);
Anadiplosis answered 1/4, 2011 at 11:2 Comment(1)
It's not being dynamically resized, no; the only problem with using the RenderTarget2D as if it was a Texture2D is that the content is volatile. I am using an off-screen render target, with a set size. I'm not sure what you're suggesting.Inconvertible

© 2022 - 2024 — McMap. All rights reserved.