xBR Filter

I confirmed that this is working in XNA, but I can reproduce in a Windows DX project. This is also working in a DesktopGL project. I’ll keep digging around, this is a pretty nasty bug.

Awesome, thanks for your time! Chasm is Monogame and they managed to get it working, so I have hope! If you want to chat please do hit me up on Twitter DMs @ pixelnicks

Discord Games (Chasm):

“We also wanted to include a good smoothing algorithm, and after some research we found the newer xBR algorithm to be hands down the best option at this time. We combined it with a subtle 2nd pass blur filter for an incredibly smooth image.”

I was curious and checked out your twitter account @Pixelnicks. The game indeed looks very good! How long are you working on it?

I tracked it down to ‘textureSize’ not being consistent in the vertex shader. I don’t have a clue where that’s coming from (yet), but if you compute tex1 in the pixel shader it works. So you can use that as a workaround for now. I.e. change the vertex shader and the start of the pixel shader to be:

void main_vertex(inout float4 pos0 : POSITION0, inout float2 tex0 : TEXCOORD0, float4 color : COLOR0)
{
    pos0 = mul(pos0, matrixTransform);
}

float4 main_fragment(float4 pos0 : SV_POSITION, float2 tex0 : TEXCOORD0) : COLOR0
{
    float2 ps = 1.0 / textureSize;
    float4 tex1 = float4(ps.x, 0, 0, ps.y);
    
    bool4 edr, edr_left, edr_up, px; // px = pixel, edr = edge detection rule
    ... // rest of the pixel shader goes here

I also noticed you can vectorize some more operations, you can change this part:

// These inequations define the line below which interpolation occurs.
    fx.x = (Ao.x * fp.y + Bo.x * fp.x > Co.x);
    fx.y = (Ao.y * fp.y + Bo.y * fp.x > Co.y);
    fx.z = (Ao.z * fp.y + Bo.z * fp.x > Co.z);
    fx.w = (Ao.w * fp.y + Bo.w * fp.x > Co.w);

    fx_left.x = (Ax.x * fp.y + Bx.x * fp.x > Cx.x);
    fx_left.y = (Ax.y * fp.y + Bx.y * fp.x > Cx.y);
    fx_left.z = (Ax.z * fp.y + Bx.z * fp.x > Cx.z);
    fx_left.w = (Ax.w * fp.y + Bx.w * fp.x > Cx.w);

    fx_up.x = (Ay.x * fp.y + By.x * fp.x > Cy.x);
    fx_up.y = (Ay.y * fp.y + By.y * fp.x > Cy.y);
    fx_up.z = (Ay.z * fp.y + By.z * fp.x > Cy.z);
    fx_up.w = (Ay.w * fp.y + By.w * fp.x > Cy.w);

to

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

Here’s a scrot


(Sorry for ruining your art :confused: it was for testing purposes :stuck_out_tongue: )

i went ahead and fixed it, but I was too late it seems. Unlike Jjagg i did the texture size in the vertex shader though.

Here is the replacement part in my shader. Add the struct at the beginning and change the replace the vertex shader

struct vsOut
{
float4 pos0 : SV_POSITION;
float2 tex0 : TEXCOORD0;
float4 tex1 : TEXCOORD1;
};

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

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));
}

vsOut main_vertex(float2 tex0 : TEXCOORD0, float4 pos0 : POSITION0)
{
vsOut output;
output.pos0 = mul(pos0, matrixTransform);
output.tex0 = tex0;
float2 ps = 1.0 / textureSize;
output.tex1 = float4(ps.x, 0, 0, ps.y);
return output;
}

// all identified optimizations have been amalgamated into this file
float2 textureSize;
float4x4 matrixTransform;

const static float coef = 2.0;
const static float3 yuv_weighted = float3(14.352, 28.176, 5.472);


// Our sampler for the texture, which is just going to be pretty simple

sampler decal : register(s0);


struct vsOut
{
	float4 pos0 : SV_POSITION;
	float2 tex0 : TEXCOORD0;
	float4 tex1 : TEXCOORD1;
};

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

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));
}

vsOut main_vertex(float2 tex0 : TEXCOORD0,
                   float4 pos0 : POSITION0)
{
	vsOut output;
	output.pos0 = mul(pos0, matrixTransform);
	output.tex0 = tex0;
	float2 ps = 1.0 / textureSize;
	output.tex1 = float4(ps.x, 0, 0, ps.y);
	return output;
}

float4 main_fragment(float4 pos0 : SV_POSITION, float2 tex0 : TEXCOORD0, 
                     float4 tex1 : TEXCOORD1) : SV_TARGET
{
    bool4 edr, edr_left, edr_up, px; // px = pixel, edr = edge detection rule
    bool4 ir_lv1, ir_lv2_left, ir_lv2_up;
    bool4 nc; // new_color
    bool4 fx, fx_left, fx_up; // inequations of straight lines.

    float2 fp = frac(tex0 * textureSize);
    float2 dx = tex1.xy;
    float2 dy = tex1.zw;

	float3 A = tex2D(decal, tex0 - dx - dy).xyz;
	
    float3 B  = tex2D(decal, tex0 - dy).xyz;
    float3 C  = tex2D(decal, tex0 + dx - dy).xyz;
    float3 D  = tex2D(decal, tex0 - dx).xyz;
	float3 E = tex2D(decal, tex0).xyz;
    float3 F  = tex2D(decal, tex0 + dx).xyz;
    float3 G  = tex2D(decal, tex0 - dx + dy).xyz;
    float3 H  = tex2D(decal, tex0 + dy).xyz;
    float3 I  = tex2D(decal, tex0 + dx + dy).xyz;
    float3 A1 = tex2D(decal, tex0 - dx - 2.0 * dy).xyz;
    float3 C1 = tex2D(decal, tex0 + dx - 2.0 * dy).xyz;
    float3 A0 = tex2D(decal, tex0 - 2.0 * dx - dy).xyz;
    float3 G0 = tex2D(decal, tex0 - 2.0 * dx + dy).xyz;
    float3 C4 = tex2D(decal, tex0 + 2.0 * dx - dy).xyz;
    float3 I4 = tex2D(decal, tex0 + 2.0 * dx + dy).xyz;
    float3 G5 = tex2D(decal, tex0 - dx + 2.0 * dy).xyz;
    float3 I5 = tex2D(decal, tex0 + dx + 2.0 * dy).xyz;
    float3 B1 = tex2D(decal, tex0 - 2.0 * dy).xyz;
    float3 D0 = tex2D(decal, tex0 - 2.0 * dx).xyz;
    float3 H5 = tex2D(decal, tex0 + 2.0 * dy).xyz;
    float3 F4 = tex2D(decal, tex0 + 2.0 * dx).xyz;

    float4 b = mul(float4x3(B, D, H, F), yuv_weighted);
    float4 c = mul(float4x3(C, A, G, I), yuv_weighted);
    float4 e = mul(float4x3(E, E, E, E), yuv_weighted);
    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), yuv_weighted);
    float4 i5 = mul(float4x3(I5, C4, A1, G0), yuv_weighted);
    float4 h5 = mul(float4x3(H5, F4, B1, D0), yuv_weighted);
    float4 f4 = h5.yzwx;

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

    // These inequations define the line below which interpolation occurs.
    fx.x = (Ao.x * fp.y + Bo.x * fp.x > Co.x);
    fx.y = (Ao.y * fp.y + Bo.y * fp.x > Co.y);
    fx.z = (Ao.z * fp.y + Bo.z * fp.x > Co.z);
    fx.w = (Ao.w * fp.y + Bo.w * fp.x > Co.w);

    fx_left.x = (Ax.x * fp.y + Bx.x * fp.x > Cx.x);
    fx_left.y = (Ax.y * fp.y + Bx.y * fp.x > Cx.y);
    fx_left.z = (Ax.z * fp.y + Bx.z * fp.x > Cx.z);
    fx_left.w = (Ax.w * fp.y + Bx.w * fp.x > Cx.w);

    fx_up.x = (Ay.x * fp.y + By.x * fp.x > Cy.x);
    fx_up.y = (Ay.y * fp.y + By.y * fp.x > Cy.y);
    fx_up.z = (Ay.z * fp.y + By.z * fp.x > Cy.z);
    fx_up.w = (Ay.w * fp.y + By.w * fp.x > Cy.w);

    ir_lv1      = ((e != f) && (e != h));
    ir_lv2_left = ((e != g) && (d != g));
    ir_lv2_up   = ((e != c) && (b != c));

    float4 w1 = weighted_distance(e, c, g, i, h5, f4, h, f);
    float4 w2 = weighted_distance(h, d, i5, f, i4, b, e, i);
    float4 df_fg = df(f, g);
    float4 df_hc = df(h, c);
    float4 t1 = (coef * df_fg);
    float4 t2 = df_hc;
    float4 t3 = df_fg;
    float4 t4 = (coef * df_hc);

    edr      = (w1 < w2)  && ir_lv1;
    edr_left = (t1 <= t2) && ir_lv2_left;
    edr_up   = (t4 <= t3) && ir_lv2_up;

    nc = (edr && (fx || edr_left && fx_left || edr_up && fx_up));

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

    float3 res = nc.x ? px.x ? F : H : 
                 nc.y ? px.y ? B : F : 
                 nc.z ? px.z ? D : B : 
                 nc.w ? px.w ? H : D : E;

    return float4(res.xyz, 1.0);
}

technique T0
{
    pass P0
    {
        VertexShader = compile vs_4_0_level_9_3 main_vertex();
        PixelShader = compile ps_4_0_level_9_3 main_fragment();
    }
}

Did you find the same work around?

Wait, that worked for you? I tried this too

I think the problem doesn’t lay with the vertex shader not being able to read shader variables correctly - the output structure was not interpreted correctly I think (Maybe an issue with MGFX?)

So this was the Vertex Shader header
float4 main_vertex(inout float4 col0 : COLOR0, inout float2 tex0 : TEXCOORD0,
inout float4 pos0 : POSITION0) : TEXCOORD1

it has

  • inout float4 pos : POSITION
  • inout float2 tex0 : TEXCOORD0
  • float4 : TEXCOORD1

The first 2 were inouts, which is ok i guess and the 3rd one was the target output. Somehow the target didn’t output correctly, it was always 0 for me.
Maybe that’s an issue if you combine inouts with this target thing.

So I changed it to a modern output struct instead and it works.

struct vsOut
{
float4 pos0 : SV_POSITION;
float2 tex0 : TEXCOORD0;
float4 tex1 : TEXCOORD1;
};

Another thing for @Pixelnicks

You don’t need this
Matrix halfPixelOffset = Matrix.CreateTranslation(-0.5f, -0.5f, 0);
any more, it’s an old XNA relic

Not sure what I did wrong before, but it’s working here too now

For anyone interested I put up a gist with the full shader here:

1 Like

Oustanding results, thanks for your hard work guys! :slight_smile:

I was just looking for an xBR filter today, I guess I got lucky ^^

A question… is everybody getting some jaggies that shouldn’t be there with this filter or it’s just my stinking integrated intel GPU?

p.s. @Pixelnicks I’m also please leave an option to disable depixelizer filters. I can’t believe people prefer xBR to pixel art :slight_smile:

1 Like

What kind of filter is this? Does it address the same issue as FXAA? Meaning does it reduce edges?

Yes, but is’s specifically made for upscaling/smoothing pixel images. Wikipedia has a long list of different algorithms that can be used.

It runs perfectly on my stinking integrated graphics, holds up the frame rate 99% of the time too :slight_smile:

The default setting in my game is pixels, I included this for the people who just can’t stand pixel art.

Interesting stuff. I now remember that I found some of these algorithms in an emulator :smile:

The Wikipedia page mentions that a lot of the pixel scaling algorithms were originally developed for emulators!

Do you have the input / output resolution correct?

Hi @kosmonautgames,

I read better the thread and discovered that you said that the halfPixel offset was superfluous. Removing the offset made the xBR filter behave correctly! :slight_smile:

1 Like