7 min read

Rendering to a Texture

Introduction

Before we get going with this tutorial, I want to explain what it means to render to a texture, and why anyone would ever want to do that. Usually, when we draw, we draw to the screen, or at least to a buffer that will very soon be drawn straight to the screen. Instead of doing this, we can draw to a texture, which is essentially, just an image.

There are a lot of reasons why you would want to do this. For one, now that the entire game is drawn to an image, you can now draw that image on the screen with a simple call to SpriteBatch.Draw(). Only instead of drawing it full size, you can apply all sorts of interesting effects to it. You can even apply 2D pixel shaders and get some interesting effects, like bloom, sepia tone, black and white, or even edge detection for a toon shader. So in this tutorial, we will go through the process of rendering to a scene.

Now, remember that when you do this, you don’t need to render the whole scene if you don’t want to. In fact, you could render anything that you want! For instance, I have in my mind a racing type game where the player is seeing the game from the driver’s seat, instead of a 3rd-person view from behind the car. You could render the scene from the point of view of the rear-view mirror, or side mirrors, and then texture the mirrors with the rendered image, and the player will be able to actually use the mirrors! Rendering to a texture is frequently used for reflection stuff. I’ve seen it used for other things too, like shadows and water. And imagine rendering your entire game to a texture, and then applying that texture to a 3D model! You could have your entire game on the surface of a sphere or something. With all of these cool things that you could do with it, I think it is well worth discussing it.

Rendering to a Texture

Rendering to a texture is a pretty simple task. In fact, it only requires a few lines of code, outside of the normal drawing code. Right now, we will go through the process of rendering to a texture and drawing the texture back to the screen. You should be able to add this to any other project that you have made fairly easily. At the bottom of this tutorial, I have attached my final main game source code in case you want to use it. If you do, you will have to also download the helicopter model from the 3D Model Library or switch it to a different model.

Creating a Render Target

The graphics device uses objects called render targets. A render target is simply a place to draw to. By default, it draws to a render target that is then immediately drawn to the screen. We will need to create a different render target that our game can draw to. We don’t want to create this every time, because there is quite a bit of overhead associated with creating a new render target. In my game, I create an instance variable that is a render target, by adding the following line to the class, with all of the other instance variables:

// Create a new render target
RenderTarget2D renderTarget;

Then, put the following code to initialize the render target at some point before you start drawing (but not in the Draw() or Update() method, or you will end up recreating it every time). I have put mine in the Initialize() method.

renderTarget = new RenderTarget2D(
                GraphicsDevice,
                GraphicsDevice.PresentationParameters.BackBufferWidth,
                GraphicsDevice.PresentationParameters.BackBufferHeight,
                false,
                GraphicsDevice.PresentationParameters.BackBufferFormat,
                DepthFormat.Depth24);

The render target requires a reference to the graphics device, as well as a specific size (width and height). We also need to specify the back buffer format, which has to do with the color depth and stuff. We are just going to use the back buffer format that the graphics device is already using.

Drawing to a Texture

The next step is to draw to the render target, and create a texture from it. I have created a method in my program that accomplishes all of this:

/// <summary>
/// Draws the entire scene in the given render target.
/// </summary>
/// <returns>A texture2D with the scene drawn in it.</returns>
protected void DrawSceneToTexture(RenderTarget2D renderTarget)
{
    // Set the render target
    GraphicsDevice.SetRenderTarget(renderTarget);

    GraphicsDevice.DepthStencilState = new DepthStencilState() { DepthBufferEnable = true };

    // Draw the scene
    GraphicsDevice.Clear(Color.CornflowerBlue);
    DrawModel(model, world, view, projection);

    // Drop the render target
    GraphicsDevice.SetRenderTarget(null);
}

The first line sets the render target. We then proceed to draw the stuff we want. Usually, this is stuff that we would have in our Draw() method, and you might be able to get away with cutting and pasting the stuff you currently have in the Draw() method, but I would recommend that you actually create a method called DrawScene(), and call that from here. The next thing we do is to revert back to the original render target, which is the screen. So we say GraphicsDevice.SetRenderTarget(null);, which effectively removes our render target and allows the graphics device to go back to the screen, and we’re done.

Redrawing the Texture to the Screen

We have now created a texture that contains our scene. What you want to do with it is really up to you. I am going to go back and draw it on the screen, but I’m going to scale it a little and tint it to show that it is working. So I have reconstructed my Draw() method to look like this:

protected override void Draw(GameTime gameTime)
{
    DrawSceneToTexture(renderTarget);

    GraphicsDevice.Clear(Color.Black);

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

    spriteBatch.Draw(renderTarget, new Rectangle(0, 0, 400, 240), Color.Red);

    spriteBatch.End();

    base.Draw(gameTime);
}

The first thing I do is draw the scene to a texture. Then I clear the screen with black and draw the texture as we would normally do. I then draw the texture, scaling it and tinting it.

So my original scene looked something like this:

Screenshot 1

And my new scene, where I render to a texture and redraw the texture on the screen looks like this:

Screenshot 2

Now, for many of the cool effects that you might want to do, you may want to draw the texture the full size of the screen without a tint, which would look something like this for the default window size:

spriteBatch.Draw(texture, new Rectangle(0, 0, 800, 480), Color.White);
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace RenderToTexture
{
    public enum Mode { Object, Camera };
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        Matrix world = Matrix.CreateTranslation(0, 0, 0);
        Matrix view = Matrix.CreateLookAt(new Vector3(0, 10, 10), new Vector3(0, 0, 0), new Vector3(0, 1, 0));
        Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), 800f / 480f, 0.1f, 100f);
        float angle = 0;
        float distance = 10;
        Vector3 viewVector;
        float objectAngle = 0;

        // Create a new render target
        RenderTarget2D renderTarget;

        Model model;
        Mode currentMode = Mode.Camera;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        protected override void Initialize()
        {
            base.Initialize();

            renderTarget = new RenderTarget2D(
                            GraphicsDevice,
                            GraphicsDevice.PresentationParameters.BackBufferWidth,
                            GraphicsDevice.PresentationParameters.BackBufferHeight,
                            false,
                            GraphicsDevice.PresentationParameters.BackBufferFormat,
                            DepthFormat.Depth24);
        }

        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);

            model = Content.Load<Model>("Models/Helicopter");
        }

        protected override void UnloadContent()
        {
        }

        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            KeyboardState keyboardState = Keyboard.GetState();

            if (keyboardState.IsKeyDown(Keys.O))
            {
                currentMode = Mode.Object;
            }
            if (keyboardState.IsKeyDown(Keys.C))
            {
                currentMode = Mode.Camera;
            }

            if (currentMode == Mode.Camera)
            {
                if (keyboardState.IsKeyDown(Keys.Left))
                {
                    angle -= 0.01f;
                }
                else if (keyboardState.IsKeyDown(Keys.Right))
                {
                    angle += 0.01f;
                }
            }
            if (currentMode == Mode.Object)
            {
                if (keyboardState.IsKeyDown(Keys.Left))
                {
                    objectAngle -= 0.01f;
                }
                else if (keyboardState.IsKeyDown(Keys.Right))
                {
                    objectAngle += 0.01f;
                }

                world = Matrix.CreateRotationY(objectAngle);
            }


            Vector3 cameraLocation = distance * new Vector3((float)Math.Sin(angle), .5f, (float)Math.Cos(angle));
            Vector3 cameraTarget = new Vector3(0, 0, 0);
            viewVector = Vector3.Transform(cameraTarget - cameraLocation, Matrix.CreateRotationY(0));
            viewVector.Normalize();
            view = Matrix.CreateLookAt(cameraLocation, cameraTarget, new Vector3(0, 1, 0));

            base.Update(gameTime);
        }

        /// <summary>
        /// Draws the entire scene in the given render target.
        /// </summary>
        /// <returns>A texture2D with the scene drawn in it.</returns>
        protected void DrawSceneToTexture(RenderTarget2D renderTarget)
        {
            // Set the render target
            GraphicsDevice.SetRenderTarget(renderTarget);

            GraphicsDevice.DepthStencilState = new DepthStencilState() { DepthBufferEnable = true };

            // Draw the scene
            GraphicsDevice.Clear(Color.CornflowerBlue);
            DrawModel(model, world, view, projection);

            // Drop the render target
            GraphicsDevice.SetRenderTarget(null);
        }


        protected override void Draw(GameTime gameTime)
        {
            DrawSceneToTexture(renderTarget);

            GraphicsDevice.Clear(Color.Black);

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

            spriteBatch.Draw(renderTarget, new Rectangle(0, 0, 400, 240), Color.Red);

            spriteBatch.End();

            base.Draw(gameTime);
        }


        private void DrawModel(Model model, Matrix world, Matrix view, Matrix projection)
        {
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (ModelMeshPart part in mesh.MeshParts)
                {
                    BasicEffect effect = (BasicEffect)part.Effect;

                    effect.EnableDefaultLighting();
                    effect.PreferPerPixelLighting = true;
                    effect.World = mesh.ParentBone.Transform * world;
                    effect.View = view;
                    effect.Projection = projection;
                }
                mesh.Draw();
            }
        }
    }
}

Click here for the complete solution: RenderToTexture.zip