Black and White Monitor Pixel Shader


Modern pixel shaders have given game developers tremendous capabilities with which to improve the visuals of their games. They’ve also given developers the ability to make their games look WORSE. That’s okay though, when it’s on purpose.

For this week’s post, I’ve put together a pixel shader that takes a normal full-color game and renders it like a black-and-white screen with lots of static.

This weeks program starts off with the upscaling demo from an earlier post. To apply the black-and-white static effect to the entire screen, I start with the render target scaling version of the program and add a pixel shader to the final sprite batch call used to draw the upscale texture to the screen.

Using the desaturation sample from the App Hub as a base, we can convert color images to greyscale with the following pixel shader:

sampler TextureSampler : register(s0);

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float2 TexCoord : TEXCOORD0;
};

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
        float4 tex = tex2D(TextureSampler, input.TexCoord);
        float greyscale = dot(tex.rgb, float3(0.3, 0.59, 0.11));
    
        tex.rgb = greyscale * noiseTex;
        return tex;
}

technique Technique1
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

The next step is to create a texture filled with random noise which can be done in the LoadContent() function like so:

   rng = new Random(1024);
   noiseTexture = new Texture2D(GraphicsDevice, RENDER_TARGET_WIDTH, RENDER_TARGET_HEIGHT);
   Color[] color = new Color[RENDER_TARGET_WIDTH * RENDER_TARGET_HEIGHT];
   for (int i = 0; i < color.Length; ++i)
   {
       float whiteAmount = 0.5f + 0.5f * (float)rng.NextDouble();
       color[i] = Color.White * whiteAmount;
   }
   noiseTexture.SetData<Color>(color);

Then, we send the noise texture to the pixel shader in the Draw() function:

GraphicsDevice.Textures[1] = noiseTexture;

Finally, we modify the pixel shader to apply the noise texture to the screen:

sampler TextureSampler : register(s0);
sampler NoiseTextureSampler : register(s1);

struct VertexShaderOutput
{
    float4 Position : POSITION0;
	float2 TexCoord : TEXCOORD0;
};

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    float4 tex = tex2D(TextureSampler, input.TexCoord);
    float4 noiseTex = tex2D(NoiseTextureSampler, input.TexCoord);
    float greyscale = dot(tex.rgb, float3(0.3, 0.59, 0.11));
    
    tex.rgb = greyscale * noiseTex;
    return tex;
}

At this point, we have a decent static-covered black-and-white image, but the static really needs to move in order to complete the effect. This is easy enough to accomplish by passing a random number to the pixel shader every frame:

scaleupEffect.Parameters["RandomOffset"].SetValue((float)rng.NextDouble());

And then using the number to offset the x-coordinate of the noise sampler:

sampler TextureSampler : register(s0);
sampler NoiseTextureSampler : register(s1);

float RandomOffset;

struct VertexShaderOutput
{
    float4 Position : POSITION0;
	float2 TexCoord : TEXCOORD0;
};

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    float4 tex = tex2D(TextureSampler, input.TexCoord);
    float2 noiseTexCoord = input.TexCoord;
    noiseTexCoord.x += RandomOffset;
    float4 noiseTex = tex2D(NoiseTextureSampler, noiseTexCoord);
    float greyscale = dot(tex.rgb, float3(0.3, 0.59, 0.11));
    
    tex.rgb = greyscale * noiseTex;
    return tex;
}

technique Technique1
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

This causes the static to dance across the screen, creating the illusion of bad-reception on top of the black-and-white image.

Not bad. Now I just need to make a custom controller shaped like rabbit-ear antennas for adjusting the static.

As usual, full code is available below:

#define SCALE_WITH_RENDERTARGET
//#define SCALE_WITH_TRANSFORMATION

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;

namespace LotsOfPixels
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        SpriteFont stopWatchFont;
        System.Diagnostics.Stopwatch stopWatch;

        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        Texture2D bgTexture;
        Texture2D richterTexture;
        List<Vector2> richterPositions;

        RenderTarget2D scaleupTarget;
        Effect scaleupEffect;
        Texture2D noiseTexture;
        Random rng;

        const int RICHTER_COUNT = 64;
#if SCALE_WITH_RENDERTARGET
        const float SPRITE_SCALE = 1.0f;
#elif SCALE_WITH_TRANSFORMATION
        const float SPRITE_SCALE = 1.0f;
#else
        const float SPRITE_SCALE = 2.0f;
#endif

        const int RENDER_TARGET_WIDTH = 640;
        const int RENDER_TARGET_HEIGHT = 360;
        float upScaleAmount;

        float scalingMatrixAmount = 2.0f;
        Matrix scalingMatrix;

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

            graphics.PreferredBackBufferWidth = 1280;
            graphics.PreferredBackBufferHeight = 720;

            this.IsFixedTimeStep = false;
            graphics.SynchronizeWithVerticalRetrace = false;
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here
            stopWatch = new System.Diagnostics.Stopwatch();
            stopWatch.Start();

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: use this.Content to load your game content here
            stopWatchFont = Content.Load<SpriteFont>("DefaultFont");

            bgTexture = Content.Load<Texture2D>("bgroom");
            richterTexture = Content.Load<Texture2D>("richterStand01");

            rng = new Random(1024);
            richterPositions = new List<Vector2>(RICHTER_COUNT);
            for (int i = 0; i < RICHTER_COUNT; ++i)
            {
                richterPositions.Add(new Vector2(rng.Next(0, (int)(bgTexture.Width * SPRITE_SCALE)), rng.Next(0, (int)(bgTexture.Height * SPRITE_SCALE))));
            }

            scaleupTarget = new RenderTarget2D(GraphicsDevice, RENDER_TARGET_WIDTH, RENDER_TARGET_HEIGHT);
            upScaleAmount = (float)GraphicsDevice.PresentationParameters.BackBufferWidth / RENDER_TARGET_WIDTH;
            scaleupEffect = Content.Load<Effect>("OldTV");
            noiseTexture = new Texture2D(GraphicsDevice, RENDER_TARGET_WIDTH, RENDER_TARGET_HEIGHT);
            Color[] color = new Color[RENDER_TARGET_WIDTH * RENDER_TARGET_HEIGHT];
            for (int i = 0; i < color.Length; ++i)
            {
                float whiteAmount = 0.5f + 0.5f * (float)rng.NextDouble();
                //float whiteAmount = 0.75f + 0.25f * (float)rng.NextDouble();
                color[i] = Color.White * whiteAmount;
            }
            noiseTexture.SetData<Color>(color);
            
            scalingMatrix = Matrix.CreateScale(new Vector3(scalingMatrixAmount, scalingMatrixAmount, 1.0f));
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
                Keyboard.GetState().IsKeyDown(Keys.Escape))
                this.Exit();

            // TODO: Add your update logic here

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            long ticks = stopWatch.ElapsedTicks;
            TimeSpan drawTime = stopWatch.Elapsed;
            stopWatch.Reset();
            stopWatch.Start();

            Vector2 richterOrigin = new Vector2((float)richterTexture.Width * 0.5f, (float)richterTexture.Height * 0.5f);
            Vector2 baseRenderPos = Vector2.UnitX * 50.0f * SPRITE_SCALE + Vector2.UnitY * 100.0f * SPRITE_SCALE;

#if SCALE_WITH_RENDERTARGET
            GraphicsDevice.SetRenderTarget(scaleupTarget);
            GraphicsDevice.Clear(Color.SeaGreen);
            spriteBatch.Begin(
                SpriteSortMode.BackToFront,
                BlendState.AlphaBlend
                );
#elif SCALE_WITH_TRANSFORMATION
            GraphicsDevice.Clear(Color.LightBlue);
            spriteBatch.Begin(
                SpriteSortMode.BackToFront,
                BlendState.AlphaBlend,
                SamplerState.PointClamp,
                null,
                null,
                null,
                scalingMatrix);
#else
            GraphicsDevice.Clear(Color.CornflowerBlue);
            spriteBatch.Begin(
                SpriteSortMode.BackToFront,
                BlendState.AlphaBlend,
                SamplerState.PointClamp,
                null,
                null,
                null);
#endif

            spriteBatch.Draw(bgTexture, baseRenderPos, null, Color.White, 0.0f, Vector2.Zero, SPRITE_SCALE, SpriteEffects.None, 1.0f);

            for (int i = 0; i < RICHTER_COUNT; ++i)
            {
                spriteBatch.Draw(richterTexture, richterPositions[i] + baseRenderPos, null, Color.White, 0.0f, richterOrigin, SPRITE_SCALE, SpriteEffects.None, 0.0f);
            }

            spriteBatch.End();
#if SCALE_WITH_RENDERTARGET
            GraphicsDevice.SetRenderTarget(null);

            scaleupEffect.Parameters["RandomOffset"].SetValue((float)rng.NextDouble());
            GraphicsDevice.Textures[1] = noiseTexture;
            spriteBatch.Begin(
                SpriteSortMode.Texture,
                BlendState.AlphaBlend,
                SamplerState.PointClamp,
                null,
                null,
                scaleupEffect);
            spriteBatch.Draw(scaleupTarget, Vector2.Zero, null, Color.White, 0.0f, Vector2.Zero, upScaleAmount, SpriteEffects.None, 0.0f);
            spriteBatch.End();
#endif            
            spriteBatch.Begin();
            spriteBatch.DrawString(stopWatchFont, "Frame Time ms: " + drawTime.TotalMilliseconds, Vector2.UnitX * 500.0f + Vector2.UnitY * 10.0f, Color.DarkMagenta);
            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}
Share this Article:
  • Digg
  • StumbleUpon
  • del.icio.us
  • Facebook
  • Yahoo! Buzz
  • Twitter
  • Google Bookmarks
  • Print

One thought on “Black and White Monitor Pixel Shader

  1. Pingback: Shades of Sepia | Game Dev Without a Cause

Comments are closed.