Tutorial 23: Render Targets for Off-Screen Rendering

Graphics  ·  RenderTarget2D  ·  Off-Screen  ·  Minimap

A render target is a texture that the GPU can draw into instead of the back buffer. Once you have rendered a scene into a render target you can use that texture as the source for subsequent draw calls — scaling it, applying effects to it, or compositing it onto the screen. This tutorial covers creation, usage, and common patterns.

What is a render target?

Normally, everything you draw goes into the back buffer — the texture that is presented to the display each frame. A RenderTarget2D is an off-screen texture you create and control. You redirect GPU output to it, draw your scene, then switch back to the back buffer and draw the render target as a texture.

Common use cases:

  • Post-processing — render the world to a texture, then apply a shader (grayscale, bloom, vignette) before presenting.
  • Minimap — render the world at small scale to a tiny render target, display as a HUD element.
  • UI canvas — render complex UI once per state change rather than every frame.
  • Shadow maps — render the scene from the light's perspective, store depth in a render target.
  • Portals / mirrors — render the reflection scene into a texture, sample it on the mirror surface.

RenderTarget2D creation

#include "Microsoft/Xna/Framework/Graphics/RenderTarget2D.hpp"

using namespace Microsoft::Xna::Framework::Graphics;

// Create a render target matching the back buffer size
auto& gd = getGraphicsDeviceProperty();
renderTarget_ = std::make_unique<RenderTarget2D>(
    gd,
    800,                         // width  (pixels)
    600,                         // height (pixels)
    false,                       // mipMap — usually false for render targets
    SurfaceFormat::Color,        // pixel format
    DepthFormat::Depth24          // depth buffer format (None if not needed)
);

For a post-process target that matches the window size dynamically:

Viewport vp = gd.getViewport();
renderTarget_ = std::make_unique<RenderTarget2D>(
    gd,
    vp.Width, vp.Height,
    false,
    SurfaceFormat::Color,
    DepthFormat::None    // no depth needed for 2D post-process
);

GraphicsDevice.SetRenderTarget()

// Redirect rendering to the render target
gd.SetRenderTarget(renderTarget_.get());

// Now everything you draw goes to renderTarget_ (not the screen)
gd.Clear(Color::Transparent);   // clear the RT, not the back buffer

// ... draw your scene here ...

// Restore the back buffer
gd.SetRenderTarget(nullptr);    // nullptr = back buffer

// Now everything you draw goes to the screen again

After calling SetRenderTarget(nullptr) the render target texture is finalised and ready to sample as a normal Texture2D.

Drawing to texture and back to screen

void Draw(const GameTime&) override {
    auto& gd = getGraphicsDeviceProperty();

    // --- Pass 1: render scene into render target ---
    gd.SetRenderTarget(renderTarget_.get());
    gd.Clear(Color::CornflowerBlue);

    spriteBatch_->Begin();
    DrawWorldSprites();   // all world content
    spriteBatch_->End();

    gd.SetRenderTarget(nullptr);

    // --- Pass 2: draw the render target onto the screen ---
    gd.Clear(Color::Black);

    spriteBatch_->Begin();
    // Draw the render target as a texture — stretch to fill screen
    spriteBatch_->Draw(*renderTarget_,
                        Rectangle(0, 0, 800, 600),
                        Color::White);
    spriteBatch_->End();

    gd.Present();
}

At this point the output is identical to rendering directly to the back buffer. The power comes when you add a shader or transformation in the second pass (see Tutorial 24).

Minimap example

A minimap renders the world at reduced scale into a small render target, then displays it as a HUD corner element:

// In LoadContent():
minimapRT_ = std::make_unique<RenderTarget2D>(gd, 200, 150,
    false, SurfaceFormat::Color, DepthFormat::None);

// In Draw():
void Draw(const GameTime&) override {
    auto& gd = getGraphicsDeviceProperty();

    // --- Minimap pass (render world at 1/10 scale) ---
    gd.SetRenderTarget(minimapRT_.get());
    gd.Clear(Color::Black);
    {
        // Scale the camera to fit the entire world into 200x150 px
        Matrix minimapTransform =
            Matrix::CreateTranslation(-worldBounds_.X, -worldBounds_.Y, 0.0f) *
            Matrix::CreateScale(
                200.0f / (Single)worldBounds_.Width,
                150.0f / (Single)worldBounds_.Height,
                1.0f);

        spriteBatch_->Begin(SpriteSortMode::Deferred,
                             nullptr, nullptr, nullptr, nullptr, nullptr,
                             minimapTransform);
        DrawWorldTiles();      // just the tile layer, not UI
        DrawEntityDots();      // simplified coloured dots for entities
        spriteBatch_->End();
    }
    gd.SetRenderTarget(nullptr);

    // --- Main scene pass ---
    gd.Clear(Color::CornflowerBlue);
    spriteBatch_->Begin(SpriteSortMode::Deferred,
                         nullptr, nullptr, nullptr, nullptr, nullptr,
                         camera_->getTransform());
    DrawWorld();
    spriteBatch_->End();

    // --- HUD pass (no camera transform) ---
    spriteBatch_->Begin();
    // Draw minimap in top-right corner with a 2 px border
    spriteBatch_->Draw(*minimapRT_,
                        Rectangle(590, 10, 200, 150),
                        Color::White);
    // Draw minimap border
    // (use a 1x1 white pixel texture scaled to border shape)
    spriteBatch_->End();

    gd.Present();
}

Handling lost device / resize

On Windows a device can be lost (e.g. on resolution change or Alt-Tab on some drivers). Render targets must be recreated after device loss. CNA fires the GraphicsDevice::DeviceReset event — hook it to recreate your render targets:

void Initialize() override {
    Game::Initialize();
    getGraphicsDeviceProperty().DeviceReset +=
        [this]() { RecreateRenderTargets(); };
}

void RecreateRenderTargets() {
    auto& gd = getGraphicsDeviceProperty();
    Viewport vp = gd.getViewport();
    renderTarget_ = std::make_unique<RenderTarget2D>(
        gd, vp.Width, vp.Height, false, SurfaceFormat::Color, DepthFormat::None);
}