[Shader] SpriteBatch Alpha Based Stenciling and Texture Scrolling.

Im not sure this is the best name for the effect not sure what its called but i saw it done in another game and wanted to reproduce it as its really cool and pretty simple the method i made that goes with it is also super cool.

I had planed to make this about 10x over and never got around to it.
Today i did and i figured i would share.
As it also serves as a good example of the power of using shaders with spritebatch.

So this is a shader that you give to spritebatch begin(, effect,) to draw with.
Then you can essentially alpha stencil one texture by another, the entire second texture acts as a color offset proportionally in rgba were the alpha component can be used to simply not draw part of a texture. When this is combined with scrolling it really shines.

The method i added to the Game1 further shows how you can use it even with sprites in a sheet and it allows for texture scrolling by telling the shader to use UV Wrap mode addressing.

It also allows the specifying how much of the texture to show at once as a percentage for width height since wrap is used this means you can just show part of a texture and scroll over it or you could multiply it in place for a tiling effect.

Though i didn’t upload a gif these images appear as if they are rotating or scrolling in place using the game1 and shader below.

So this is done with basically just regular textures
(i just borrowed these images to demonstrate they aren’t mine).

So here is the shader and the game class.

#if OPENGL
#define SV_POSITION POSITION
#define VS_SHADERMODEL vs_3_0
#define PS_SHADERMODEL ps_3_0
#else
#define VS_SHADERMODEL vs_4_0 //_level_9_1
#define PS_SHADERMODEL ps_4_0 //_level_9_1
#endif

// virtual soure left right and top bottom.
float4 sourceUvRect = float4(0.0f, 0.0f, 1.0f, 1.0f);

Texture2D SpriteTexture;
sampler2D SpriteTextureSampler = sampler_state
{
    //magfilter = LINEAR; //minfilter = LINEAR; //mipfilter = LINEAR; //AddressU = mirror; //AddressV = mirror; 
    AddressU = wrap;
    AddressV = wrap;
    Texture = <SpriteTexture>;
};

Texture2D AlphaStencilTexture;
sampler2D AlphaStencilTextureSampler = sampler_state
{
    Texture = <AlphaStencilTexture>;
};

struct VertexShaderOutput
{
    float4 Position : SV_POSITION;
    float4 Color : COLOR0;
    float2 TextureCoordinates : TEXCOORD0;
};

float4 MainPS(VertexShaderOutput input) : COLOR
{
    float2 texelpos = float2(input.TextureCoordinates.x, input.TextureCoordinates.y);
    float4 col = tex2D(SpriteTextureSampler, texelpos) * input.Color;
    float2 stenTexelpos = float2((texelpos.x - sourceUvRect.x) / (sourceUvRect.z - sourceUvRect.x), (texelpos.y - sourceUvRect.y) / (sourceUvRect.w - sourceUvRect.y));
    float4 stenTexelcol = tex2D(AlphaStencilTextureSampler, stenTexelpos);
    return col * stenTexelcol;
}


technique AlphaStencil
{
    pass P0
    {
        PixelShader = compile PS_SHADERMODEL MainPS();
    }
};

The game class with a extra method that makes the first image come alive.
You can put in any textures you want in place of these and play around with it.

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace YourNameSpace
{
    /// <summary>
    /// This is the main type for your game.
    /// </summary>
    public class Game1 : Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        Effect effectAlphaStencil;

        Texture2D texture;
        Texture2D texture_blueAtmosphere;
        Texture2D texture_terran02;
        Texture2D texture_clouds_light;

        // vars to make the planet rotate and map scroll.

        float planetrot = 0f;
        float cloudrot = 0f;

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

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

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

            texture = Content.Load<Texture2D>("blue-square-button-editted-md");
            texture_blueAtmosphere = Content.Load<Texture2D>("blue_atmosphere");
            texture_terran02 = Content.Load<Texture2D>("Terran02");
            texture_clouds_light = Content.Load<Texture2D>("clouds-light");

            effectAlphaStencil = Content.Load<Effect>("AlphaStencilEffect");
            effectAlphaStencil.Parameters["AlphaStencilTexture"].SetValue(texture_blueAtmosphere);
        }

        protected override void UnloadContent()
        {
        }

        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();
            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.Black);

            planetrot += .0005f;
            if (planetrot > 1.0f)
                planetrot -= 1.0f;

            cloudrot += .0004f;
            if (cloudrot > 1.0f)
                cloudrot -= 1.0f;

            // pass the shader effect to spritebatch
            spriteBatch.Begin(SpriteSortMode.Immediate, null, SamplerState.PointClamp, null, null, effectAlphaStencil, null);

            // scrolling box
            DrawScrollableAlphaStenciledImage(texture_terran02, texture, new Rectangle(250, 150, 300, 300), new Vector2(planetrot, 0), new Vector2(.4f, 1f), Color.White);
            DrawScrollableAlphaStenciledImage(texture_clouds_light, texture, new Rectangle(250, 150, 300, 300), new Vector2(planetrot, 0), new Vector2(.3f, 1f), Color.White);

            // scrolling planet
            DrawScrollableAlphaStenciledImage(texture_terran02, texture_blueAtmosphere, new Rectangle(50, 50, 300, 300), new Vector2(planetrot, 0), new Vector2(.4f, 1f), Color.White);
            DrawScrollableAlphaStenciledImage(texture_clouds_light, texture_blueAtmosphere, new Rectangle(50, 50, 300, 300), new Vector2(planetrot, 0), new Vector2(.3f, 1f), Color.White);


            // just use it regular
            effectAlphaStencil.Parameters["sourceUvRect"].SetValue(new Vector4(0f,0f,1f,1f));
            effectAlphaStencil.Parameters["AlphaStencilTexture"].SetValue(texture_terran02);
            spriteBatch.Draw(texture, new Rectangle(600, 100, 100, 100), Color.Red);

            spriteBatch.End();

            base.Draw(gameTime);
        }

        public void DrawScrollableAlphaStenciledImage(Texture2D texture, Texture2D stencilTexture, Rectangle destRectangle, Vector2 scroll, Vector2 sizePercentage, Color color)
        {
            var srcPos = new Point((int)(scroll.X * texture.Bounds.Size.X), (int)(scroll.Y * texture.Bounds.Size.Y));
            var srcSize = new Point((int)(texture.Bounds.Size.X * sizePercentage.X), (int)(texture.Bounds.Size.Y * sizePercentage.Y));
            Rectangle source = new Rectangle(srcPos, srcSize);

            // this will work for older shader models this way.
            // this is how you calculate this value for a sprite source rectangle in a texture you want to use the effect texture on.
            Vector4 virtsrcRect = new Vector4();
            virtsrcRect.X = (float)source.Left / (float)texture.Bounds.Width;
            virtsrcRect.Y = (float)source.Top / (float)texture.Bounds.Height;
            virtsrcRect.Z = (float)source.Right / (float)texture.Bounds.Width;
            virtsrcRect.W = (float)source.Bottom / (float)texture.Bounds.Height;

            effectAlphaStencil.Parameters["sourceUvRect"].SetValue(virtsrcRect);
            effectAlphaStencil.Parameters["AlphaStencilTexture"].SetValue(stencilTexture);

            spriteBatch.Draw(texture, destRectangle, source, color);
        }
    }
}
3 Likes

1 Like