TIC-80 CRT shader

General discussion about LÖVE, Lua, game development, puns, and unicorns.
Post Reply
User avatar
Dotto Meister
Prole
Posts: 5
Joined: Sun Nov 22, 2020 10:32 pm
Contact:

TIC-80 CRT shader

Post by Dotto Meister »

Hi,

I intend to port an improved version of the game that I am finalizing on the TIC-80:

(click to view full image)
Image Image Image Image



I would like to know if anyone can convert the CRT shader from the TIC-80 to LOVE?

Code: Select all

CRT_SHADER=[[
varying vec2 texCoord;
uniform sampler2D source;
uniform float trg_x;
uniform float trg_y;
uniform float trg_w;
uniform float trg_h;
uniform float scr_w;
uniform float scr_h;

// Emulated input resolution.
vec2 res=vec2(256.0,144.0);

// Hardness of scanline.
//  -8.0 = soft
// -16.0 = medium
float hardScan=-8.0;

// Hardness of pixels in scanline.
// -2.0 = soft
// -4.0 = hard
float hardPix=-3.0;

// Display warp.
// 0.0 = none
// 1.0/8.0 = extreme
vec2 warp=vec2(1.0/64.0,1.0/48.0); 

// Amount of shadow mask.
float maskDark=0.5;
float maskLight=1.5;

//------------------------------------------------------------------------

// sRGB to Linear.
// Assuing using sRGB typed textures this should not be needed.
float ToLinear1(float c){return(c<=0.04045)?c/12.92:pow((c+0.055)/1.055,2.4);}
vec3 ToLinear(vec3 c){return vec3(ToLinear1(c.r),ToLinear1(c.g),ToLinear1(c.b));}

// Linear to sRGB.
// Assuing using sRGB typed textures this should not be needed.
float ToSrgb1(float c){return(c<0.0031308?c*12.92:1.055*pow(c,0.41666)-0.055);}
vec3 ToSrgb(vec3 c){return vec3(ToSrgb1(c.r),ToSrgb1(c.g),ToSrgb1(c.b));}

// Nearest emulated sample given floating point position and texel offset.
// Also zero's off screen.
vec3 Fetch(vec2 pos,vec2 off){
	pos=(floor(pos*res+off)+vec2(0.5,0.5))/res;
	return ToLinear(1.2 * texture2D(source,pos.xy,-16.0).rgb);}

// Distance in emulated pixels to nearest texel.
vec2 Dist(vec2 pos){pos=pos*res;return -((pos-floor(pos))-vec2(0.5));}
		
// 1D Gaussian.
float Gaus(float pos,float scale){return exp2(scale*pos*pos);}

// 3-tap Gaussian filter along horz line.
vec3 Horz3(vec2 pos,float off){
	vec3 b=Fetch(pos,vec2(-1.0,off));
	vec3 c=Fetch(pos,vec2( 0.0,off));
	vec3 d=Fetch(pos,vec2( 1.0,off));
	float dst=Dist(pos).x;
	// Convert distance to weight.
	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 filtered sample.
	return (b*wb+c*wc+d*wd)/(wb+wc+wd);}

// 5-tap Gaussian filter along horz line.
vec3 Horz5(vec2 pos,float off){
	vec3 a=Fetch(pos,vec2(-2.0,off));
	vec3 b=Fetch(pos,vec2(-1.0,off));
	vec3 c=Fetch(pos,vec2( 0.0,off));
	vec3 d=Fetch(pos,vec2( 1.0,off));
	vec3 e=Fetch(pos,vec2( 2.0,off));
	float dst=Dist(pos).x;
	// Convert distance to weight.
	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 filtered sample.
	return (a*wa+b*wb+c*wc+d*wd+e*we)/(wa+wb+wc+wd+we);}

// Return scanline weight.
float Scan(vec2 pos,float off){
	float dst=Dist(pos).y;
	return Gaus(dst+off,hardScan);}

// Allow nearest three lines to effect pixel.
vec3 Tri(vec2 pos){
	vec3 a=Horz3(pos,-1.0);
	vec3 b=Horz5(pos, 0.0);
	vec3 c=Horz3(pos, 1.0);
	float wa=Scan(pos,-1.0);
	float wb=Scan(pos, 0.0);
	float wc=Scan(pos, 1.0);
	return a*wa+b*wb+c*wc;}

// Distortion of scanlines, and end of screen alpha.
vec2 Warp(vec2 pos){
	pos=pos*2.0-1.0;    
	pos*=vec2(1.0+(pos.y*pos.y)*warp.x,1.0+(pos.x*pos.x)*warp.y);
	return pos*0.5+0.5;}

// Shadow mask.
vec3 Mask(vec2 pos){
	pos.x+=pos.y*3.0;
	vec3 mask=vec3(maskDark,maskDark,maskDark);
	pos.x=fract(pos.x/6.0);
	if(pos.x<0.333)mask.r=maskLight;
	else if(pos.x<0.666)mask.g=maskLight;
	else mask.b=maskLight;
	return mask;}    

void main() {
	hardScan=-12.0;
	//maskDark=maskLight;
	vec2 start=gl_FragCoord.xy-vec2(trg_x, trg_y);
	start.y=scr_h-start.y;

	vec2 pos=Warp(start/vec2(trg_w, trg_h));

	gl_FragColor.rgb=Tri(pos)*Mask(gl_FragCoord.xy);
	gl_FragColor = vec4(ToSrgb(gl_FragColor.rgb), 1.0);
}
]]
Thanks for the attention!
User avatar
Davidobot
Party member
Posts: 1226
Joined: Sat Mar 31, 2012 5:18 am
Location: Oxford, UK
Contact:

Re: TIC-80 CRT shader

Post by Davidobot »

Not a conversion per se, but moonshine has a CRT shader.
PM me on here or elsewhere if you'd like to discuss porting your game to Nintendo Switch via mazette!
personal page and a raycaster
User avatar
Dotto Meister
Prole
Posts: 5
Joined: Sun Nov 22, 2020 10:32 pm
Contact:

Re: TIC-80 CRT shader

Post by Dotto Meister »

I tried the moonshine but the result is not so good.
User avatar
pgimeno
Party member
Posts: 3656
Joined: Sun Oct 18, 2015 2:58 pm

Re: TIC-80 CRT shader

Post by pgimeno »

Nice shader. What's the license?
User avatar
Dotto Meister
Prole
Posts: 5
Joined: Sun Nov 22, 2020 10:32 pm
Contact:

Re: TIC-80 CRT shader

Post by Dotto Meister »

There is no specific license for the shader, but the TIC-80 project as a whole is distributed under the MIT license.
User avatar
pgimeno
Party member
Posts: 3656
Joined: Sun Oct 18, 2015 2:58 pm

Re: TIC-80 CRT shader

Post by pgimeno »

I've made it work by replacing main() with this:

Code: Select all

vec4 effect(vec4 colour, Image texture, vec2 texpos, vec2 scrpos)
{
        hardScan=-12.0;
        //maskDark=maskLight;
        vec2 start=scrpos-vec2(trg_x, trg_y);

        vec2 pos=Warp(start/vec2(trg_w, trg_h));

        vec4 result;
        result.rgb=Tri(pos)*Mask(scrpos);
        result = vec4(ToSrgb(result.rgb), 1.0);
        return result;
}
I found it inconvenient to have to send the source image every time, though, so I also made this change:

Code: Select all

@@ -1,11 +1,8 @@
 CRT_SHADER=[[
-varying vec2 texCoord;
-uniform sampler2D source;
 uniform float trg_x;
 uniform float trg_y;
 uniform float trg_w;
 uniform float trg_h;
-uniform float scr_w;
-uniform float scr_h;
 
 // Emulated input resolution.
 vec2 res=vec2(256.0,144.0);
@@ -46,7 +43,7 @@
 // Also zero's off screen.
 vec3 Fetch(vec2 pos,vec2 off){
 	pos=(floor(pos*res+off)+vec2(0.5,0.5))/res;
-	return ToLinear(1.2 * texture2D(source,pos.xy,-16.0).rgb);}
+	return ToLinear(1.2 * texture2D(MainTex,pos.xy,-16.0).rgb);}
 
 // Distance in emulated pixels to nearest texel.
 vec2 Dist(vec2 pos){pos=pos*res;return -((pos-floor(pos))-vec2(0.5));}
User avatar
Dotto Meister
Prole
Posts: 5
Joined: Sun Nov 22, 2020 10:32 pm
Contact:

Re: TIC-80 CRT shader

Post by Dotto Meister »

pgimeno wrote: Mon Nov 23, 2020 7:04 pm I've made it work by replacing main() with this:

Code: Select all

vec4 effect(vec4 colour, Image texture, vec2 texpos, vec2 scrpos)
{
        hardScan=-12.0;
        //maskDark=maskLight;
        vec2 start=scrpos-vec2(trg_x, trg_y);

        vec2 pos=Warp(start/vec2(trg_w, trg_h));

        vec4 result;
        result.rgb=Tri(pos)*Mask(scrpos);
        result = vec4(ToSrgb(result.rgb), 1.0);
        return result;
}
I found it inconvenient to have to send the source image every time, though, so I also made this change:

Code: Select all

@@ -1,11 +1,8 @@
 CRT_SHADER=[[
-varying vec2 texCoord;
-uniform sampler2D source;
 uniform float trg_x;
 uniform float trg_y;
 uniform float trg_w;
 uniform float trg_h;
-uniform float scr_w;
-uniform float scr_h;
 
 // Emulated input resolution.
 vec2 res=vec2(256.0,144.0);
@@ -46,7 +43,7 @@
 // Also zero's off screen.
 vec3 Fetch(vec2 pos,vec2 off){
 	pos=(floor(pos*res+off)+vec2(0.5,0.5))/res;
-	return ToLinear(1.2 * texture2D(source,pos.xy,-16.0).rgb);}
+	return ToLinear(1.2 * texture2D(MainTex,pos.xy,-16.0).rgb);}
 
 // Distance in emulated pixels to nearest texel.
 vec2 Dist(vec2 pos){pos=pos*res;return -((pos-floor(pos))-vec2(0.5));}

Could you provide a working example, please?

I tried here but I get a black screen =/
User avatar
pgimeno
Party member
Posts: 3656
Joined: Sun Oct 18, 2015 2:58 pm

Re: TIC-80 CRT shader

Post by pgimeno »

Dotto Meister wrote: Tue Nov 24, 2020 11:49 am Could you provide a working example, please?

I tried here but I get a black screen =/
You need to fill in the uniforms. trg is the viewport. For full screen:

Code: Select all

shader:send('trg_x', 0)
shader:send('trg_y', 0)
shader:send('trg_w', love.graphics.getWidth())
shader:send('trg_h', love.graphics.getHeight())
Anyway, here's a complete example (using a viewport with a 50 px margin):

Code: Select all

local
CRT_SHADER=[[
uniform float trg_x;
uniform float trg_y;
uniform float trg_w;
uniform float trg_h;

// Emulated input resolution.
vec2 res=vec2(256.0,144.0);

// Hardness of scanline.
//  -8.0 = soft
// -16.0 = medium
float hardScan=-8.0;

// Hardness of pixels in scanline.
// -2.0 = soft
// -4.0 = hard
float hardPix=-3.0;

// Display warp.
// 0.0 = none
// 1.0/8.0 = extreme
vec2 warp=vec2(1.0/64.0,1.0/48.0); 

// Amount of shadow mask.
float maskDark=0.5;
float maskLight=1.5;

//------------------------------------------------------------------------

// sRGB to Linear.
// Assuing using sRGB typed textures this should not be needed.
float ToLinear1(float c){return(c<=0.04045)?c/12.92:pow((c+0.055)/1.055,2.4);}
vec3 ToLinear(vec3 c){return vec3(ToLinear1(c.r),ToLinear1(c.g),ToLinear1(c.b));}

// Linear to sRGB.
// Assuing using sRGB typed textures this should not be needed.
float ToSrgb1(float c){return(c<0.0031308?c*12.92:1.055*pow(c,0.41666)-0.055);}
vec3 ToSrgb(vec3 c){return vec3(ToSrgb1(c.r),ToSrgb1(c.g),ToSrgb1(c.b));}

// Nearest emulated sample given floating point position and texel offset.
// Also zero's off screen.
vec3 Fetch(vec2 pos,vec2 off){
	pos=(floor(pos*res+off)+vec2(0.5,0.5))/res;
	return ToLinear(1.2 * texture2D(MainTex,pos.xy,-16.0).rgb);}

// Distance in emulated pixels to nearest texel.
vec2 Dist(vec2 pos){pos=pos*res;return -((pos-floor(pos))-vec2(0.5));}
		
// 1D Gaussian.
float Gaus(float pos,float scale){return exp2(scale*pos*pos);}

// 3-tap Gaussian filter along horz line.
vec3 Horz3(vec2 pos,float off){
	vec3 b=Fetch(pos,vec2(-1.0,off));
	vec3 c=Fetch(pos,vec2( 0.0,off));
	vec3 d=Fetch(pos,vec2( 1.0,off));
	float dst=Dist(pos).x;
	// Convert distance to weight.
	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 filtered sample.
	return (b*wb+c*wc+d*wd)/(wb+wc+wd);}

// 5-tap Gaussian filter along horz line.
vec3 Horz5(vec2 pos,float off){
	vec3 a=Fetch(pos,vec2(-2.0,off));
	vec3 b=Fetch(pos,vec2(-1.0,off));
	vec3 c=Fetch(pos,vec2( 0.0,off));
	vec3 d=Fetch(pos,vec2( 1.0,off));
	vec3 e=Fetch(pos,vec2( 2.0,off));
	float dst=Dist(pos).x;
	// Convert distance to weight.
	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 filtered sample.
	return (a*wa+b*wb+c*wc+d*wd+e*we)/(wa+wb+wc+wd+we);}

// Return scanline weight.
float Scan(vec2 pos,float off){
	float dst=Dist(pos).y;
	return Gaus(dst+off,hardScan);}

// Allow nearest three lines to effect pixel.
vec3 Tri(vec2 pos){
	vec3 a=Horz3(pos,-1.0);
	vec3 b=Horz5(pos, 0.0);
	vec3 c=Horz3(pos, 1.0);
	float wa=Scan(pos,-1.0);
	float wb=Scan(pos, 0.0);
	float wc=Scan(pos, 1.0);
	return a*wa+b*wb+c*wc;}

// Distortion of scanlines, and end of screen alpha.
vec2 Warp(vec2 pos){
	pos=pos*2.0-1.0;    
	pos*=vec2(1.0+(pos.y*pos.y)*warp.x,1.0+(pos.x*pos.x)*warp.y);
	return pos*0.5+0.5;}

// Shadow mask.
vec3 Mask(vec2 pos){
	pos.x+=pos.y*3.0;
	vec3 mask=vec3(maskDark,maskDark,maskDark);
	pos.x=fract(pos.x/6.0);
	if(pos.x<0.333)mask.r=maskLight;
	else if(pos.x<0.666)mask.g=maskLight;
	else mask.b=maskLight;
	return mask;}    

/*
void main() {
	hardScan=-12.0;
	//maskDark=maskLight;
	vec2 start=gl_FragCoord.xy-vec2(trg_x, trg_y);
	start.y=scr_h-start.y;

	vec2 pos=Warp(start/vec2(trg_w, trg_h));

	gl_FragColor.rgb=Tri(pos)*Mask(gl_FragCoord.xy);
	gl_FragColor = vec4(ToSrgb(gl_FragColor.rgb), 1.0);
}
*/

vec4 effect(vec4 colour, Image texture, vec2 texpos, vec2 scrpos)
{
	hardScan=-12.0;
	//maskDark=maskLight;
	vec2 start=scrpos-vec2(trg_x, trg_y);

	vec2 pos=Warp(start/vec2(trg_w, trg_h));

	vec4 result;
	result.rgb=Tri(pos)*Mask(scrpos);
	result = vec4(ToSrgb(result.rgb), 1.0);
	return result;
}

]]

local shader = love.graphics.newShader(CRT_SHADER)

local img = love.graphics.newImage('image1-800.jpg')
function love.draw()
  shader:send('trg_x', 50)
  shader:send('trg_y', 50)
  shader:send('trg_w', love.graphics.getWidth()-100)
  shader:send('trg_h', love.graphics.getHeight()-100)
  love.graphics.setShader(shader)
  love.graphics.draw(img)
--  love.graphics.rectangle("fill", 50, 50, 700, 500)
  love.graphics.setShader()
end

function love.keypressed(k) return k == "escape" and love.event.quit() end
User avatar
Dotto Meister
Prole
Posts: 5
Joined: Sun Nov 22, 2020 10:32 pm
Contact:

Re: TIC-80 CRT shader

Post by Dotto Meister »

it worked but the result was completely buggy. Sprites, lines and etc. were all white and without transparency, and the texts (sprite font) even show changed characters.

I think I will give up using shader, thank you so much for trying =]
User avatar
pgimeno
Party member
Posts: 3656
Joined: Sun Oct 18, 2015 2:58 pm

Re: TIC-80 CRT shader

Post by pgimeno »

The colour isn't used, maybe try replacing 'return result;' with 'return result * colour;'
Post Reply

Who is online

Users browsing this forum: Bing [Bot] and 5 guests