CRT shader issue

Hello everybody.
I want to apply a CRT shader to my game. I downloaded an example from https://github.com/Jjagg/mg-crt/tree/81bcf73e02f1087a6223b14aa97145478ead45fd. I modified the example for testing. I fill the screen with “zeros” using my bitmap font and it works correctly. The problem is that this shader seems to work only with OpenGL projects. So I downloaded a version for Windows from https://github.com/Jjagg/mg-crt/blob/8d3949114ac2daf92e526638a023e15982273d5b/Game1/Content/crt-lottes-mg.fx (I changed the PS_SHADERMODEL from ps_4_0_level_9_1 to ps_4_0_level_9_3 otherwise it doesn’t work) and done the same test. It works but there is a bug. As you can see in the image in certain situations it draws a line of incorrect pixels. I tried to compare the OpenGL shader with the Windows one but I’ve not been able to find the issue (I’m very new to shaders). Can someone please help me (maybe Jjagg)?

…Using a differente scaling I get the issue even vertically…

Ok! After a lot of experiments (even if I’m very new to shaders) I found the bug. I changed the following lines:

float4 crt_lottes(float2 tex)
{
	float2 pos = tex.xy;
	float3 outColor = Tri(pos, textureSize);
...

to:

float4 crt_lottes(float2 tex)
{
	float2 pos = tex.xy;
	pos = pos * 2.0 - 1.0;
	pos = pos * 0.5 + 0.5;
	float3 outColor = Tri(pos, textureSize);
...

and the problem is solved !!!

1 Like

If someone should be interested (even if it seems no one is) here is the shader cut to the bone (only 104 lines):

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

#define brightboost 1.0
#define hardPix -10.0
#define hardScan -6.0

float2 textureSize;

sampler2D DecalSampler = sampler_state
{
};

float3 ToSrgb(float3 c)
{
	return pow(c, 1 / 2.2);
}

float3 Fetch(float2 pos, float2 off, float2 texture_size)
{
	pos = (floor(pos * texture_size.xy + off) + float2(0.5, 0.5)) / texture_size.xy;
	return brightboost * pow(tex2D(DecalSampler, pos.xy).rgb, 2.2);
}

float2 Dist(float2 pos, float2 texture_size)
{
	pos = pos * texture_size.xy; 
	return -(frac(pos) - float2(0.5, 0.5));
}

float Gaus(float pos, float scale)
{
	return exp2(scale * pos * pos);
}

float3 Horz3(float2 pos, float off, float2 texture_size)
{
	float3 b = Fetch(pos, float2(-1.0, off), texture_size);
	float3 c = Fetch(pos, float2(0.0, off), texture_size);
	float3 d = Fetch(pos, float2(1.0, off), texture_size);
	float dst = Dist(pos, texture_size).x;
	float scale = hardPix;
	float wb = Gaus(dst - 1.0, scale);
	float wc = Gaus(dst + 0.0, scale);
	float wd = Gaus(dst + 1.0, scale);
	return (b * wb + c * wc + d * wd) / (wb + wc + wd);
}

float3 Horz5(float2 pos, float off, float2 texture_size) {
	float3 a = Fetch(pos, float2(-2.0, off), texture_size);
	float3 b = Fetch(pos, float2(-1.0, off), texture_size);
	float3 c = Fetch(pos, float2(0.0, off), texture_size);
	float3 d = Fetch(pos, float2(1.0, off), texture_size);
	float3 e = Fetch(pos, float2(2.0, off), texture_size);
	float dst = Dist(pos, texture_size).x;
	float scale = hardPix;
	float wa = Gaus(dst - 2.0, scale);
	float wb = Gaus(dst - 1.0, scale);
	float wc = Gaus(dst + 0.0, scale);
	float wd = Gaus(dst + 1.0, scale);
	float we = Gaus(dst + 2.0, scale);
	return (a * wa + b * wb + c * wc + d * wd + e * we) / (wa + wb + wc + wd + we);
}

float Scan(float2 pos, float off, float2 texture_size) {
	float dst = Dist(pos, texture_size).y;
	return Gaus(dst + off, hardScan);
}

float3 Tri(float2 pos, float2 texture_size) {
	float3 a = Horz3(pos, -1.0, texture_size);
	float3 b = Horz5(pos, 0.0, texture_size);
	float3 c = Horz3(pos, 1.0, texture_size);
	float wa = Scan(pos, -1.0, texture_size);
	float wb = Scan(pos, 0.0, texture_size);
	float wc = Scan(pos, 1.0, texture_size);
	return a * wa + b * wb + c * wc;
}

float4 crt_lottes(float2 tex)
{
	float2 pos = tex.xy;
	pos = pos * 2.0 - 1.0;
	pos = pos * 0.5 + 0.5;
	float3 outColor = Tri(pos, textureSize);
	return float4(ToSrgb(outColor.rgb), 1.0);
}

float4 main_fragment(VertexShaderOutput VOUT) : COLOR0
{
	return crt_lottes(VOUT.texCoord);
}

technique
{
	pass
	{
		PixelShader = compile ps_4_0_level_9_3 main_fragment();
	}
}

FORGET IT !!! Using certain scalings the bug is still there !!! :grimacing:

Can you try using a PointClamp SamplerState in your SpriteBatch.Begin using the shader?

If you don’t see this issue on DesktopGL and the shader is exactly the same, I suspect the problem is the so-called half-pixel offset. From the link:

The root cause is that viewport is shifted by half a pixel compared to where we want it to be. […] the vertex shaders output clip space position. We can adjust the clip space position, to shift everything by half a viewport pixel. Essentially we need to do this:

// clipPos is float4 that contains position output from vertex shader
// (POSITION/SV_Position semantic):
clipPos.xy += renderTargetInvSize.xy * clipPos.w;

Yes, the shader is the same but I see the issue only on the Windows project.
Following your suggestion, I tried to modify the following lines:

float4 crt_lottes(float2 texture_size, float2 video_size, float2 output_size, float2 tex, sampler2D s0)
{
	float2 pos = Warp(tex.xy*(texture_size.xy / video_size.xy))*(video_size.xy / texture_size.xy);
	float3 outColor = Tri(pos, texture_size);

	return float4(ToSrgb(outColor.rgb), 1.0);
}

to:

float4 crt_lottes(float2 texture_size, float2 video_size, float2 output_size, float2 tex, sampler2D s0)
{
	float2 pos = Warp(tex.xy*(texture_size.xy / video_size.xy))*(video_size.xy / texture_size.xy);
	float3 outColor = Tri(pos, texture_size);

	float4 clipPos = float4(ToSrgb(outColor.rgb), 1.0);
	clipPos.xy += texture_size.xy * clipPos.w;
	return clipPos;
}

The issue is still there with the difference that the background is yellow instead of black. But, I repeat, I’m very new to shaders and I’m not sure at all about what I’ve written…

I also tried SamplerState.PointClamp but nothing changes.
I want to point out that I see the issue only with very few scalings of the target in the SpriteBatch.Draw. It seems that scaling the target by integer values the issue doesn’t occur, but perhaps is just a coincidence, and
I’m not able to “predict” with which scaling the issue occurs…

Taking into account that it could be a DX9 problem, I made some further “ramdom” experiment. I changed the lines:

float4 crt_lottes(float2 texture_size, float2 video_size, float2 output_size, float2 tex, sampler2D s0)
{
	float2 pos = Warp(tex.xy*(texture_size.xy / video_size.xy))*(video_size.xy / texture_size.xy);
	float3 outColor = Tri(pos, texture_size);
	return float4(ToSrgb(outColor.rgb), 1.0);
}

to:

float4 crt_lottes(float2 texture_size, float2 video_size, float2 output_size, float2 tex, sampler2D s0)
{
	float2 pos = Warp(tex.xy*(texture_size.xy / video_size.xy))*(video_size.xy / texture_size.xy);
	pos -= 0.0001; // <--------------------------------------------- Line added
	float3 outColor = Tri(pos, texture_size);
	return float4(ToSrgb(outColor.rgb), 1.0);
}

That way it seems to work on every scaling of the target in SpriteBatch.Draw. Even writing “pos += 0.0001;” it works. Does this make sense?

clipPos.xy += texture_size.xy * clipPos.w;

Should be inverted size (so 1 / texture_size.xy).

I tried

clipPos.xy += (1 / texture_size.xy) * clipPos.w;

The background is black but the issue is still there…

I don’t have the time to look into this better :confused: I hope you figure it out!

As I said, the following lines seem to work perfectly:

float4 crt_lottes(float2 texture_size, float2 video_size, float2 output_size, float2 tex, sampler2D s0)
{
	float2 pos = Warp(tex.xy*(texture_size.xy / video_size.xy))*(video_size.xy / texture_size.xy);
	pos -= 0.0001; // This line solves a DX9 issue !!!???
	float3 outColor = Tri(pos, texture_size);
	return float4(ToSrgb(outColor.rgb), 1.0);
}

I will stay with this solution at the moment, even if it’s rummy…

1 Like