Tutorial 23: Render Targets for Off-Screen Rendering
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);
}