Applying shader on one sprite only

Hello, I’m currently messing for the first time with shaders and i’d like to experiment it on a game I’m working on with Monogame. Apparently there’s a few type of shaders and I’m most interested in post-processing (like this one) and 2d procedural generation (like this one). I have problems applying the first one to a texture in my game. Since Monogame needs HLSL and Shadertoy is GLSL, here is my convertion :

#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

float time;    
sampler TexSampler : register(s0);   

sampler texture1 = sampler_state
{ 
    Texture = <textureToChrome>; 
};

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


float4 PSMain(VertexShaderOutput input) : COLOR {
    float2 uv = input.texCoord.xy;

    float amount = 0.0;

    amount = (1.0 + sin(time*6.0)) * 0.5;
    amount *= 1.0 + sin(time*16.0) * 0.5;
    amount *= 1.0 + sin(time*19.0) * 0.5;
    amount *= 1.0 + sin(time*27.0) * 0.5;
    amount = pow(amount, 3.0);

    amount *= 0.05;

    float3 col;
    col.r = tex2D(texture1, float2(uv.x + amount, uv.y)).r;
    col.g = tex2D(texture1, uv).g;
    col.b = tex2D(texture1, float2(uv.x - amount, uv.y)).b;

    col *= (1.0 - amount * 0.5);

    return float4(col, 1.0);
}

technique ChromaticAberation
{
    pass P0
    {
        PixelShader = compile PS_SHADERMODEL PSMain();
    }
};

Let’s say I want to apply this shader on a texture I have in game, here’s my code with shader initialization, shader update and game draws :

public Effect shader_sky; //The procedural effect I know how to apply
public Effect shader_chromatic; //The chrome effect I need to know how to apply

protected override void LoadContent()
{
    shader_sky = Content.Load<Effect>("Effects/Sky");

    shader_chromatic = Content.Load<Effect>("Effects/ChromaticAberation");
}

protected override void Update(GameTime gameTime)
{
    shader_sky.Parameters["time"].SetValue((float)gameTime.TotalGameTime.TotalSeconds);
    shader_chromatic.Parameters["time"].SetValue((float)gameTime.TotalGameTime.TotalSeconds);
}

protected override void Draw(GameTime gameTime)
{
    #region Draw the sky shader
    spriteBatch.Begin(sortMode: SpriteSortMode.BackToFront,
        blendState: BlendState.AlphaBlend, 
        samplerState: SamplerState.AnisotropicWrap, 
        rasterizerState: rasterizer,
        transformMatrix: viewMatrix,
        effect: shader_sky);

    Texture2D background = new Texture2D(graphics.GraphicsDevice, 1, 1, false, SurfaceFormat.Color);
    background .SetData<Color>(new Color[] { Color.White});

    spriteBatch.Draw(texture: background ,
                     destinationRectangle: wholeScreenRectangle,
                     layerDepth: 1f,
                     scale: new Vector2(brownBackgroundRatio),
                     color: Color.White,
                     effects: SpriteEffects.FlipVertically); //Flip because othewise the shader is reverse, I don't know why

    spriteBatch.End();
    #endregion

    #region Draw the rest of the game

    spriteBatch.Draw(player.texture,
          player.position,
          player.spriteStep,
          player.color,
          player.rotation,
          player.origin,
          player.scale,
          player.spriteEffect, //Flip right or left
          player.layerDepth);

    /* 
     * And a bunch of other draws that don't need shaders
     */

    #endregion
}

And now I don’t know how to apply the chromatic effect into the player’s sprite. I tried something like this before the draw and after :

shader_chromatic.Parameters["texture1"].SetValue(player.texture);
shader_chromatic.Techniques.First<EffectTechnique>().Passes.First<EffectPass>().Apply();

But it doesn’t work and feels like it’s not the right way to do this anyway. What I could do is the same as the shader_sky (applying it on spriteBatch.Begin) but how can the shader know where to apply the texture afterwards and do I really need to do something like :

Begin
Draw
End

Begin
Draw
End

//...Etc for each texture

If I want to apply shaders to different textures?

Also, to put a picture into line of codes, this is what it looks like for now, the blue/white background being successfully generated by the shader_sky and the player being the yellow character.

Thanks.

Yes, you need separate Begin/End blocks for every effect.

SpriteBatch is called that because it batches draw operations, trying to minimize the actual GL/DX draw calls by combining sprites. Usually it only does the actual GL or DX draw calls when you call SpriteBatch.End. The shaders that are active at that time are used for the batched draw calls. You can check out the source code for implementation details.

Since two sprites with different effects cannot be batched anyway, SpriteBatch does not support switching the effect in between Begin/End. It’s the same for graphics state (e.g. sampler state). That’s why those things are passed to Begin and not to Draw.

Tips to maximize how much SpriteBatch can batch:

  • use spritesheets so textures don’t have to be switched and sprites can be batched.
  • you can use SpriteSortMode.Texture to let SpriteBatch batch sprites you draw with the same texture, even if you draw sprites with other textures in between (as long as it’s in the same Begin/End block of course)
  • if you use a custom effect for your sprites, try to draw the sprites with the same effect applied within one Begin/End block. Same goes for custom graphics state.

Okay I see, thanks for the precision on the drawing process and the tips to improve performances. Until now I hadn’t have any effect so I had only very few batches, i’ll give a try with SpriteSortMode.Texture, haven’t heard about it until now, I only used the deferred mode.

But now on more specific matters on how shaders work, let’s say I want to apply the chromatic shader above to every ground on the picture I posted above.

I’d have something like this :

spriteBatch.Begin(sortMode: SpriteSortMode.BackToFront,
        blendState: BlendState.AlphaBlend, 
        samplerState: SamplerState.AnisotropicWrap, 
        rasterizerState: rasterizer,
        transformMatrix: viewMatrix,
        effect: shader_chromatic);

foreach (Ground ground in grounds)
{
    spriteBatch.Draw(texture: ground.texture ,
            destinationRectangle: ground.destinationRectangle,
            layerDepth: 1f,
            scale: ground.scale,
            color: ground.color);
}

spriteBatch.End();

How can I tell the shader to apply it’s effect on everything that is drawn within the begin/end. Because I actually have to give a texture on my shader for it to work and give me the output I want and I don’t know how to do that.

[Edit] : Okay I may have found a way on this tutorial : https://gmjosack.github.io/posts/my-first-2d-pixel-shaders-part-1/

I’ll try some things when I get back home tonight, i’m still new with shaders so :yum:

SpriteBatch sets the texture on the active effect when it does the draw calls: https://github.com/MonoGame/MonoGame/blob/develop/MonoGame.Framework/Graphics/SpriteBatcher.cs#L254
So your example in the previous post is correct. Does that not work for you?

I don’t recommend using SpriteSortMode.Immediate, it’s generally bad for performance because it doesn’t do any batching.

Applying the effect after SpriteBatch.Begin kind of does the same as passing it to SpriteBatch.Begin, but I recommend the latter because IMO it’s clearer.

Actually I don’t know if this work since I though I had to pass the texture I wanted to be altered with shader_chromatic.Parameters["texture1"].SetValue(texture);

I’ll experiment more and try to make it work tonight, in ~8 hours. I’ll give an update then or tomorrow :wink:

Well, here I am now, I want to apply the chromatic shader to the player :

shader_chromatic.Parameters["texture1"].SetValue(player.texture);

spriteBatch.Begin(sortMode: SpriteSortMode.BackToFront,
    blendState: BlendState.AlphaBlend,
    samplerState: SamplerState.AnisotropicWrap,
    rasterizerState: rasterizer,
    transformMatrix: viewMatrix,
    effect:shader_chromatic);
                    
spriteBatch.Draw(player.texture,
      player.position,
      player.spriteStep,
      player.color,
      player.rotation,
      player.origin,
      player.scale,
      player.spriteEffect, //Flip right or left
      player.layerDepth);

spriteBatch.End();

And here is the result :

The shader works, but I’m facing three problems here.

  • Obviously, the black background, it’s an in-shader problem I suppose but right now I have yet to fix this
  • I feel like overdoing it, passing the texture twice doesn’t feel natural and I suppose there’s a bette way
  • The shader’s effect is cut by the player.texture's size, I suppose I could enlarge the empty space of the texture itself but again, I don’t feel like it’s the best solution

You should get exactly the same result of you don’t explicitly set the texture yourself before the SpriteBatch calls, does that not work?

You could split up the shader and have a separate SpriteBatch.Draw call for each color component. It’s more flexible, but will be slightly worse for performance.

No, I actually have a black square (Same dimensions as the gif above, but full black)

Oh, I missed this before, but this might be happening because of how the samplers in your shader are set up. SpriteBatch sets the texture to slot 0 (the first), but I think you have an implicit texture in slot 0 that you don’t use because of the TexSampler that you define, but don’t use. If you get rid of the TexSampler declaration things should work.

Ooh, thanks, that was it.

As for the black rectangle it was because of this line of code return float4(col, 1);. The 1 being the alpha channel so it was always visible.

1 Like

Thanks for sharing your shader, this is the solution for black bg

float4 col;
	col.r = tex2D(TexSampler, float2(uv.x + amount, uv.y)).r;
	col.g = tex2D(TexSampler, uv).g;
	col.b = tex2D(TexSampler, float2(uv.x - amount, uv.y)).b;
	col.a = tex2D(TexSampler, uv).a;
	col *= (1.0 - amount * 0.5);
	
	return col;