How to: Apply a Pixel Shader to Sprites

This article demonstrates how to apply a pixel shader to sprites.

This example uses an offscreen render target to render a number of sprites, then applies a pixel shader to the full scene. This allows the pixel shader to shade the entire screen instead of each individual sprite separately.

To create a sprite pixel shader

  1. Create a sampler object with a Texture member. This is the texture that will be changed by the pixel shader. Although we declare the texture as an external parameter, in truth the SpriteBatch will populate the texture when it draws to the screen.

    uniform extern texture ScreenTexture;    
    
    

sampler ScreenS = sampler_state { Texture = <ScreenTexture>;
};

  1. A pixel shader is only required to return a color for the current pixel. This pixel shader uses the tex2D fuction to pick a pixel on the sampler to display. Because this is a distortion shader, the pixel chosen is not necessarily the "current" pixel.

    float4 PixelShader(float2 texCoord: TEXCOORD0) : COLOR
    

{ ... // pick a pixel on the screen for this pixel, based on // the calculated offset and direction float4 color = tex2D(ScreenS, texCoord+(sinoffset*sinsign));

return color;

}

To apply a pixel shader to sprites

  1. In LoadGraphicsContent, in the loadAllContent section, create a SpriteBatch, load the Effect containing your pixel shader, and load any sprites you wish to draw.

  2. In the section of LoadGraphicsContent for loading manual resources, create a RenderTarget2D for offscreen rendering, and a Texture2D using the same settings. The Texture2D will hold the scene that we later pass to the pixel shader.

    SpriteBatch batch;
    

RenderTarget2D ShaderRenderTarget; Texture2D ShaderTexture; Effect ripple; protected override void LoadGraphicsContent( bool loadAllContent ) { if (loadAllContent) { grid = content.Load<Texture2D>( "grid" ); batch = new SpriteBatch( graphics.GraphicsDevice ); ripple = content.Load<Effect>( "ripple" ); ... }

ShaderRenderTarget = new RenderTarget2D( graphics.GraphicsDevice,
    graphics.GraphicsDevice.PresentationParameters.BackBufferWidth,
    graphics.GraphicsDevice.PresentationParameters.BackBufferHeight,
    1,
    graphics.GraphicsDevice.PresentationParameters.BackBufferFormat,
    graphics.GraphicsDevice.PresentationParameters.MultiSampleType,
    graphics.GraphicsDevice.PresentationParameters.MultiSampleQuality );
ShaderTexture = new Texture2D( graphics.GraphicsDevice, ShaderRenderTarget.Width, ShaderRenderTarget.Height,
    1, ResourceUsage.ResolveTarget, ShaderRenderTarget.Format, ResourceManagementMode.Manual );

...

}

  1. In Draw, cache the current render target using GetRenderTarget, then switch to an offscreen render target using SetRenderTarget.

  2. Clear the offscreen render target, and then draw all the sprites that comprise the scene.

  3. Resolve the render target with ResolveRenderTarget, and retrieve the scene with GetTexture. Then reset the render target to the previous render target (normally the back buffer).

    // Change to our offscreen render target.
    

RenderTarget2D temp = (RenderTarget2D)graphics.GraphicsDevice.GetRenderTarget( 0 ); graphics.GraphicsDevice.SetRenderTarget( 0, ShaderRenderTarget );

graphics.GraphicsDevice.Clear( Color.Black ); // Render a simple scene. batch.Begin(); TileSprite( batch, grid ); batch.End();

// Change back to the back buffer, and get our scene // as a texture. graphics.GraphicsDevice.ResolveRenderTarget( 0 ); ShaderTexture = ShaderRenderTarget.GetTexture(); graphics.GraphicsDevice.SetRenderTarget( 0, temp );

  1. Using the Texture2D containing the scene to shade, call Begin on your SpriteBatch, specifying SpriteSortMode.Immediate.

  2. Call Begin on the Effect, and Begin on the pass containing your pixel shader.

  3. Call Draw on the SpriteBatch. The pixel shader you specified will be used to render the sprite.

  4. Call End on the SpriteBatch, the EffectPass, and the Effect.

    // Use Immediate mode and our effect to draw the scene
    

// again, using our pixel shader. batch.Begin( SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.None ); ripple.Begin(); ripple.CurrentTechnique.Passes[0].Begin(); batch.Draw( ShaderTexture, Vector2.Zero, Color.White ); batch.End(); ripple.CurrentTechnique.Passes[0].End(); ripple.End();

The Complete Example

uniform extern texture ScreenTexture;    

sampler ScreenS = sampler_state
{
    Texture = <ScreenTexture>;    
};

float wave;                // pi/.75 is a good default
float distortion;        // 1 is a good default
float2 centerCoord;        // 0.5,0.5 is the screen center

float4 PixelShader(float2 texCoord: TEXCOORD0) : COLOR
{
    float2 distance = abs(texCoord - centerCoord);
    float scalar = length(distance);

    // invert the scale so 1 is centerpoint
    scalar = abs(1 - scalar);
        
    // calculate how far to distort for this pixel    
    float sinoffset = sin(wave / scalar);
    sinoffset = clamp(sinoffset, 0, 1);
    
    // calculate which direction to distort
    float sinsign = cos(wave / scalar);    
    
    // reduce the distortion effect
    sinoffset = sinoffset * distortion/32;
    
    // pick a pixel on the screen for this pixel, based on
    // the calculated offset and direction
    float4 color = tex2D(ScreenS, texCoord+(sinoffset*sinsign));    
            
    return color;
}
technique
{
    pass P0
    {
        PixelShader = compile ps_2_0 PixelShader();
    }
}
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;

public class Game1 : Microsoft.Xna.Framework.Game
{
    GraphicsDeviceManager graphics;
    ContentManager content;


    public Game1()
    {
        graphics = new GraphicsDeviceManager( this );
        content = new ContentManager( Services );
    }


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

    Texture2D grid;
    EffectParameter waveParam, distortionParam, centerCoordParam;

    SpriteBatch batch;
    RenderTarget2D ShaderRenderTarget;
    Texture2D ShaderTexture;
    Effect ripple;
    protected override void LoadGraphicsContent( bool loadAllContent )
    {
        if (loadAllContent)
        {
            grid = content.Load<Texture2D>( "grid" );
            batch = new SpriteBatch( graphics.GraphicsDevice );
            ripple = content.Load<Effect>( "ripple" );
            waveParam = ripple.Parameters["wave"];
            distortionParam = ripple.Parameters["distortion"];
            centerCoordParam = ripple.Parameters["centerCoord"];
        }

        ShaderRenderTarget = new RenderTarget2D( graphics.GraphicsDevice,
            graphics.GraphicsDevice.PresentationParameters.BackBufferWidth,
            graphics.GraphicsDevice.PresentationParameters.BackBufferHeight,
            1,
            graphics.GraphicsDevice.PresentationParameters.BackBufferFormat,
            graphics.GraphicsDevice.PresentationParameters.MultiSampleType,
            graphics.GraphicsDevice.PresentationParameters.MultiSampleQuality );
        ShaderTexture = new Texture2D( graphics.GraphicsDevice, ShaderRenderTarget.Width, ShaderRenderTarget.Height,
            1, ResourceUsage.ResolveTarget, ShaderRenderTarget.Format, ResourceManagementMode.Manual );

        Reset();
    }
    Vector2 centerCoord = new Vector2( 0.5f );
    float distortion = 1.0f;
    float divisor = 0.75f;
    float wave = MathHelper.Pi;
    private void Reset()
    {
        centerCoord = new Vector2( 0.5f );
        distortion = 1.0f;
        divisor = 0.75f;
        wave = MathHelper.Pi / divisor;
    }

    protected override void UnloadGraphicsContent( bool unloadAllContent )
    {
        if (unloadAllContent == true)
        {
            content.Unload();
        }
    }

    protected override void Update( GameTime gameTime )
    {
        // Allows the default game to exit on Xbox 360 and Windows.
        if (GamePad.GetState( PlayerIndex.One ).Buttons.Back == ButtonState.Pressed)
            this.Exit();

        GamePadState state = GamePad.GetState( PlayerIndex.One );

        // Reset.
        if (state.Buttons.Start == ButtonState.Pressed)
        {
            Reset();
        }

        float seconds = (float)gameTime.ElapsedGameTime.TotalSeconds;

        // Move the center.
        centerCoord.X = MathHelper.Clamp( centerCoord.X +
            (state.ThumbSticks.Right.X * seconds * 0.5f),
            0, 1 );
        centerCoord.Y = MathHelper.Clamp( centerCoord.Y -
            (state.ThumbSticks.Right.Y * seconds * 0.5f),
            0, 1 );

        // Change the distortion.
        distortion += state.ThumbSticks.Left.X * seconds * 0.5f;

        // Change the period.
        divisor += state.ThumbSticks.Left.Y * seconds * 0.5f;

        //wave = MathHelper.Pi / divisor;
        wave = MathHelper.Pi / divisor;
        waveParam.SetValue( wave );
        distortionParam.SetValue( distortion );
        centerCoordParam.SetValue( centerCoord );

        base.Update( gameTime );
    }

    protected override void Draw( GameTime gameTime )
    {
        // Change to our offscreen render target.
        RenderTarget2D temp = (RenderTarget2D)graphics.GraphicsDevice.GetRenderTarget( 0 );
        graphics.GraphicsDevice.SetRenderTarget( 0, ShaderRenderTarget );

        graphics.GraphicsDevice.Clear( Color.Black );
        // Render a simple scene.
        batch.Begin();
        TileSprite( batch, grid );
        batch.End();

        // Change back to the back buffer, and get our scene
        // as a texture.
        graphics.GraphicsDevice.ResolveRenderTarget( 0 );
        ShaderTexture = ShaderRenderTarget.GetTexture();
        graphics.GraphicsDevice.SetRenderTarget( 0, temp );

        // Use Immediate mode and our effect to draw the scene
        // again, using our pixel shader.
        batch.Begin( SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.None );
        ripple.Begin();
        ripple.CurrentTechnique.Passes[0].Begin();
        batch.Draw( ShaderTexture, Vector2.Zero, Color.White );
        batch.End();
        ripple.CurrentTechnique.Passes[0].End();
        ripple.End();

        base.Draw( gameTime );
    }
    private void TileSprite( SpriteBatch batch, Texture2D texture )
    {
        Vector2 pos = Vector2.Zero;
        for (int i = 0; i < graphics.GraphicsDevice.Viewport.Height; i += texture.Height)
        {
            for (int j = 0; j < graphics.GraphicsDevice.Viewport.Width; j += texture.Width)
            {
                pos.X = j; pos.Y = i;
                batch.Draw( texture, pos, Color.White );
            }
        }
    }
}

See Also

Concepts

2D Graphics Overview

Reference

Draw
SpriteSortMode.Immediate
Effect
RenderTarget2D