xBR shader for MonoGame

I’ve been trying to make xBR shader work in MG for a while and just got it done. Here is the code:

=Effect file=
This shader was originally written by Hyllian and I translated it from GLSL to HLSL.
Note: You can try out those different value sets for those four parameters.

    //XBR_SCALE "xBR Scale" 4.0 1.0 5.0 1.0
    //XBR_Y_WEIGHT "Y Weight" 48.0 0.0 100.0 1.0
    //XBR_EQ_THRESHOLD "Eq Threshold" 25.0 0.0 50.0 1.0
    //XBR_LV2_COEFFICIENT "Lv2 Coefficient" 2.0 1.0 3.0 0.1

    float XBR_SCALE = 4.0;
    float XBR_Y_WEIGHT = 48.0;
    float XBR_EQ_THRESHOLD = 25.0;
    float XBR_LV2_COEFFICIENT = 2.0;

    float2 textureSize;
    float4x4 matrixTransform;
    sampler decal : register(s0);
    // END PARAMETERS //

// Uncomment just one of the three params below to choose the corner detection
#define CORNER_A
//#define CORNER_B
//#define CORNER_C
//#define CORNER_D

    const static float4 Ao = float4( 1.0, -1.0, -1.0, 1.0 );
    const static float4 Bo = float4( 1.0,  1.0, -1.0,-1.0 );
    const static float4 Co = float4( 1.5,  0.5, -0.5, 0.5 );
    const static float4 Ax = float4( 1.0, -1.0, -1.0, 1.0 );
    const static float4 Bx = float4( 0.5,  2.0, -0.5,-2.0 );
    const static float4 Cx = float4( 1.0,  1.0, -0.5, 0.0 );
    const static float4 Ay = float4( 1.0, -1.0, -1.0, 1.0 );
    const static float4 By = float4( 2.0,  0.5, -2.0,-0.5 );
    const static float4 Cy = float4( 2.0,  0.0, -1.0, 0.5 );
    const static float4 Ci = float4(0.25, 0.25, 0.25, 0.25);

    const static float3 Y = float3(0.2126, 0.7152, 0.0722);


    float4 df(float4 A, float4 B)
    {
    	return float4(abs(A-B));
    }

    float c_df(float3 c1, float3 c2) 
    {
            float3 df = abs(c1 - c2);
            return df.r + df.g + df.b;
    }

    bool4 eq(float4 A, float4 B)
    {
    	return (df(A, B) < float4(XBR_EQ_THRESHOLD,XBR_EQ_THRESHOLD,XBR_EQ_THRESHOLD,XBR_EQ_THRESHOLD));
    }

    float4 weighted_distance(float4 a, float4 b, float4 c, float4 d, float4 e, float4 f, float4 g, float4 h)
    {
    	return (df(a,b) + df(a,c) + df(d,e) + df(d,f) + 4.0*df(g,h));
    }


    struct out_vertex {
    	float4 position : POSITION;
    	float4 color    : COLOR;
    	float2 texCoord : TEXCOORD0;
    	float4 t1       : TEXCOORD1;
    	float4 t2       : TEXCOORD2;
    	float4 t3       : TEXCOORD3;
    	float4 t4       : TEXCOORD4;
    	float4 t5       : TEXCOORD5;
    	float4 t6       : TEXCOORD6;
    	float4 t7       : TEXCOORD7;
    };

    /*    VERTEX_SHADER    */
    out_vertex main_vertex
    (
    	float4 position	: POSITION,
    	float4 color	: COLOR,
    	float2 texCoord : TEXCOORD0
    )
    {
    	out_vertex OUT;

    	OUT.position = mul(position, matrixTransform);
    	OUT.color = color;

    	float2 ps = float2(1.0/textureSize.x, 1.0/textureSize.y);
    	float dx = ps.x;
    	float dy = ps.y;

    	//    A1 B1 C1
    	// A0  A  B  C C4
    	// D0  D  E  F F4
    	// G0  G  H  I I4
    	//    G5 H5 I5

    	OUT.texCoord = texCoord;
    	OUT.t1 = texCoord.xxxy + float4( -dx, 0, dx,-2.0*dy); // A1 B1 C1
    	OUT.t2 = texCoord.xxxy + float4( -dx, 0, dx,    -dy); //  A  B  C
    	OUT.t3 = texCoord.xxxy + float4( -dx, 0, dx,      0); //  D  E  F
    	OUT.t4 = texCoord.xxxy + float4( -dx, 0, dx,     dy); //  G  H  I
    	OUT.t5 = texCoord.xxxy + float4( -dx, 0, dx, 2.0*dy); // G5 H5 I5
    	OUT.t6 = texCoord.xyyy + float4(-2.0*dx,-dy, 0,  dy); // A0 D0 G0
    	OUT.t7 = texCoord.xyyy + float4( 2.0*dx,-dy, 0,  dy); // C4 F4 I4

    	return OUT;
    }


    /*    FRAGMENT SHADER    */
    float4 main_fragment(in out_vertex VAR) : COLOR0
    {
    	bool4 edri, edr, edr_left, edr_up, px; // px = pixel, edr = edge detection rule
    	bool4 interp_restriction_lv0, interp_restriction_lv1, interp_restriction_lv2_left, interp_restriction_lv2_up;
    	float4 fx, fx_left, fx_up; // inequations of straight lines.

    	float4 delta         = float4(1.0/XBR_SCALE, 1.0/XBR_SCALE, 1.0/XBR_SCALE, 1.0/XBR_SCALE);
    	float4 deltaL        = float4(0.5/XBR_SCALE, 1.0/XBR_SCALE, 0.5/XBR_SCALE, 1.0/XBR_SCALE);
    	float4 deltaU        = deltaL.yxwz;

    	float2 fp = frac(VAR.texCoord*textureSize);

    	float3 A1 = tex2D(decal, VAR.t1.xw).rgb;
    	float3 B1 = tex2D(decal, VAR.t1.yw).rgb;
    	float3 C1 = tex2D(decal, VAR.t1.zw).rgb;

    	float3 A  = tex2D(decal, VAR.t2.xw).rgb;
    	float3 B  = tex2D(decal, VAR.t2.yw).rgb;
    	float3 C  = tex2D(decal, VAR.t2.zw).rgb;

    	float3 D  = tex2D(decal, VAR.t3.xw).rgb;
    	float3 E  = tex2D(decal, VAR.t3.yw).rgb;
    	float3 F  = tex2D(decal, VAR.t3.zw).rgb;

    	float3 G  = tex2D(decal, VAR.t4.xw).rgb;
    	float3 H  = tex2D(decal, VAR.t4.yw).rgb;
    	float3 I  = tex2D(decal, VAR.t4.zw).rgb;

    	float3 G5 = tex2D(decal, VAR.t5.xw).rgb;
    	float3 H5 = tex2D(decal, VAR.t5.yw).rgb;
    	float3 I5 = tex2D(decal, VAR.t5.zw).rgb;

    	float3 A0 = tex2D(decal, VAR.t6.xy).rgb;
    	float3 D0 = tex2D(decal, VAR.t6.xz).rgb;
    	float3 G0 = tex2D(decal, VAR.t6.xw).rgb;

    	float3 C4 = tex2D(decal, VAR.t7.xy).rgb;
    	float3 F4 = tex2D(decal, VAR.t7.xz).rgb;
    	float3 I4 = tex2D(decal, VAR.t7.xw).rgb;

    	float4 b = mul( float4x3(B, D, H, F), XBR_Y_WEIGHT*Y );
    	float4 c = mul( float4x3(C, A, G, I), XBR_Y_WEIGHT*Y );
    	float4 e = mul( float4x3(E, E, E, E), XBR_Y_WEIGHT*Y );
    	float4 d = b.yzwx;
    	float4 f = b.wxyz;
    	float4 g = c.zwxy;
    	float4 h = b.zwxy;
    	float4 i = c.wxyz;

    	float4 i4 = mul( float4x3(I4, C1, A0, G5), XBR_Y_WEIGHT*Y );
    	float4 i5 = mul( float4x3(I5, C4, A1, G0), XBR_Y_WEIGHT*Y );
    	float4 h5 = mul( float4x3(H5, F4, B1, D0), XBR_Y_WEIGHT*Y );
    	float4 f4 = h5.yzwx;

    	// These inequations define the line below which interpolation occurs.
    	fx      = (Ao*fp.y+Bo*fp.x); 
    	fx_left = (Ax*fp.y+Bx*fp.x);
    	fx_up   = (Ay*fp.y+By*fp.x);

            interp_restriction_lv1 = interp_restriction_lv0 = ((e!=f) && (e!=h));

    #ifdef CORNER_B
    	interp_restriction_lv1      = (interp_restriction_lv0  &&  ( !eq(f,b) && !eq(h,d) || eq(e,i) && !eq(f,i4) && !eq(h,i5) || eq(e,g) || eq(e,c) ) );
    #endif
    #ifdef CORNER_D
    	float4 c1 = i4.yzwx;
    	float4 g0 = i5.wxyz;
    	interp_restriction_lv1      = (interp_restriction_lv0  &&  ( !eq(f,b) && !eq(h,d) || eq(e,i) && !eq(f,i4) && !eq(h,i5) || eq(e,g) || eq(e,c) ) && (f!=f4 && f!=i || h!=h5 && h!=i || h!=g || f!=c || eq(b,c1) && eq(d,g0)));
    #endif
    #ifdef CORNER_C
    	interp_restriction_lv1      = (interp_restriction_lv0  && ( !eq(f,b) && !eq(f,c) || !eq(h,d) && !eq(h,g) || eq(e,i) && (!eq(f,f4) && !eq(f,i4) || !eq(h,h5) && !eq(h,i5)) || eq(e,g) || eq(e,c)) );
    #endif

    	interp_restriction_lv2_left = ((e!=g) && (d!=g));
    	interp_restriction_lv2_up   = ((e!=c) && (b!=c));

    	float4 fx45i = saturate((fx      + delta  -Co - Ci)/(2*delta ));
    	float4 fx45  = saturate((fx      + delta  -Co     )/(2*delta ));
    	float4 fx30  = saturate((fx_left + deltaL -Cx     )/(2*deltaL));
    	float4 fx60  = saturate((fx_up   + deltaU -Cy     )/(2*deltaU));

    	float4 wd1 = weighted_distance( e, c, g, i, h5, f4, h, f);
    	float4 wd2 = weighted_distance( h, d, i5, f, i4, b, e, i);

    	edri     = (wd1 <= wd2) && interp_restriction_lv0;
    	edr      = (wd1 <  wd2) && interp_restriction_lv1;
    #ifdef CORNER_A
    	edr      = edr && (!edri.yzwx || !edri.wxyz);
    	edr_left = ((XBR_LV2_COEFFICIENT*df(f,g)) <= df(h,c)) && interp_restriction_lv2_left && edr && (!edri.yzwx && eq(e,c));
    	edr_up   = (df(f,g) >= (XBR_LV2_COEFFICIENT*df(h,c))) && interp_restriction_lv2_up && edr && (!edri.wxyz && eq(e,g));
    #endif
    #ifndef CORNER_A
    	edr_left = ((XBR_LV2_COEFFICIENT*df(f,g)) <= df(h,c)) && interp_restriction_lv2_left && edr;
    	edr_up   = (df(f,g) >= (XBR_LV2_COEFFICIENT*df(h,c))) && interp_restriction_lv2_up && edr;
    #endif

    	fx45  = edr*fx45;
    	fx30  = edr_left*fx30;
    	fx60  = edr_up*fx60;
    	fx45i = edri*fx45i;

    	px = (df(e,f) <= df(e,h));

    	float4 maximos = max(max(fx30, fx60), max(fx45, fx45i));

    	float3 res1 = E;
    	res1 = lerp(res1, lerp(H, F, px.x), maximos.x);
    	res1 = lerp(res1, lerp(B, D, px.z), maximos.z);
    	
    	float3 res2 = E;
    	res2 = lerp(res2, lerp(F, B, px.y), maximos.y);
    	res2 = lerp(res2, lerp(D, H, px.w), maximos.w);
    	
    	float3 res = lerp(res1, res2, step(c_df(E, res1), c_df(E, res2)));

    	return float4(res, 1.0);
    }

    technique T0
    {
    	pass P0
    	{
    		VertexShader = compile vs_4_1 main_vertex();
    		PixelShader = compile ps_4_1 main_fragment();
    	}
    }

=How to use it=
Follow this example from this link. Basically just load the effect file, pass the texture size and game window size to it and call it in Draw().

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

namespace xbr
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {

        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        RenderTarget2D renderTarget;
        Effect xbrEffect;
        Matrix projection;
        Matrix halfPixelOffset = Matrix.CreateTranslation(-0.5f, -0.5f, 0);
        Texture2D pretend240x160Scene;

        // the bounds of your 1:1 scene
        Rectangle renderBounds = new Rectangle(0, 0, 240, 160);

        // the bounds of your output scene (same w:h ratio)
        Rectangle outputBounds = new Rectangle(0, 0, 720, 480);

        public Game1()
        {
           base.Content.RootDirectory = "Content";

           this.graphics = new GraphicsDeviceManager(this);
           this.graphics.PreferredBackBufferWidth = outputBounds.Width;
           this.graphics.PreferredBackBufferHeight = outputBounds.Height;
        }

        /// <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

            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.
            this.spriteBatch = new SpriteBatch(base.GraphicsDevice);
            this.xbrEffect = Content.Load<Effect>("xbr");

            // a fake scene that is a 240x160 image
            this.pretend240x160Scene = base.Content.Load<Texture2D>("240x160Scene");
            this.renderTarget = new RenderTarget2D(base.GraphicsDevice, this.renderBounds.Width, this.renderBounds.Height);

            // default vertex matrix for the vertex method
            this.projection = Matrix.CreateOrthographicOffCenter(0, this.outputBounds.Width, this.outputBounds.Height, 0, 0, 1);

            // set the values of this effect, should only have to do this once
            this.xbrEffect.Parameters["matrixTransform"].SetValue(halfPixelOffset * projection);
            this.xbrEffect.Parameters["textureSize"].SetValue(new Vector2 { renderBounds.Width, renderBounds.Height });
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
        }

        /// <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)
        {
            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)
        {
            base.GraphicsDevice.Clear(Color.CornflowerBlue);
            base.GraphicsDevice.SetRenderTarget(this.renderTarget);

            // draw your scene here scaled 1:1. for now I'll just draw
            // my fake 240x160 texture
            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, 
                              SamplerState.PointClamp, null, null);

            spriteBatch.Draw(this.pretend240x160Scene, this.renderBounds, this.renderBounds, Color.White);

            spriteBatch.End();

            // now we'll draw to the back buffer
            base.GraphicsDevice.SetRenderTarget(null);

            // this renders the effect
            spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied, 
                              SamplerState.PointClamp, null, null, this.xbrEffect);

            spriteBatch.Draw(this.renderTarget, this.outputBounds, this.renderBounds, Color.White);
            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}

Hope this helps!

1 Like

As promised @monopalle :slightly_smiling:

Great! good job @greyp
Would you considering adding it to my shaders repository ?

Hi @InfiniteProductions! Yeah you can go ahead and put it in there! The original GLSL version came from GitHub after all haha :smiley:

Ok I’ll, you can also contribute, that was the primary idea, having one single place to look for shader issue with MonoGame first.

Sick bro! :wink:

So what kind of effect will this actually apply to a 2d image? Stripes?

Something does seem wrong with this shader. I’m currently trying to locate the issue…

Very cool, I’ll have to play with this later :slightly_smiling:
If we are sharing shaders, here’s a nice pixel shader I wrote for lighting a 2d sprite (with color masking, rotation, and horizontal flip). You’ll need a tool like SpriteLamp to generate the normal maps, but it’s a pretty neat effect…

There are a few other shaders in that repo with similar effects.

Cheers!

Hi @dmanning23, thanks for sharing! And I’m sorry that there seems to be something wrong with this xBR shader I translated. I just checked with Hyllian (the guy who created the xBR algorithm and wrote the original shaders), and he told me the screenshots I sent him are not supposed to look like that. I’m trying to figure out why, but since I’m still fairly new to writing shaders, I’m not sure how long it will take. I wonder if someone more experienced with both GLSL and HLSL can hlep check out the original GLSL version and see what I did wrong? :sweat_smile:

Maybe it’s another issue: I got a System.IndexOutOfRangeException in the textureSize parameter set statement. (MG 3.5.1)

Oh, for that part, use Vector2 instead of float[], like this:
this.xbrEffect.Parameters["textureSize"].SetValue(new Vector2(renderBounds.Width, renderBounds.Height);

OK, guess I found out the problem. So there is nothing wrong with the shader. Just simply change the SpriteSortMode.Immediate parameter to SpriteSortMode.Deferred and things will look right. Changing SamplerState.PointClamp to SamplerState.LinearClamp also helps.

Edit: That halfPixelOffset is also useless in DX10 and above, so you can remove it.

I’m not convinced the issue is solved:

Compare to the gamedev.stackexchange post result, colored H/V lines should be quite blended with surrounding pixels.

Below is a comparison image from my game, and you can see that the xBR filter did make the end result look better by adding interpolation.
P.S.: I set the XBR_SCALE parameter to 2. Probably you can play with those four parameters and see if that changes anything.

Edit: Remember to set the textureSize parameter to the nearest powers of 2. For example, my game’s output is 800x480, so the textureSize should be set to 1024x512.

Edit 2: Hyllian told me this result is wrong again… :frowning:

I do believe I got it working this time. Check out my post and the comparison images in this thread.