CRT Lite: scanlines, curved glass and vignette

A light CRT screen for the whole game: scanlines that stay even at any resolution, a curved glass with clean rounded corners, a vignette and an optional RGB mask.

How to use: add a CanvasLayer on top of your game, put a full-screen ColorRect in it (anchors: full rect, mouse filter: ignore) and give it this shader. Keep interface elements that must stay sharp on a higher layer.

Settings:

  • curvature, corner_radius, border_color.
  • scanlines, scanline_size (in screen pixels, so no moire).
  • vignette, brightness.
  • rgb_mask, mask_strength.

Works with the Compatibility renderer.

Free download with a demo project (MIT): https://dimensionalmios.itch.io/almios-free-crt-lite
Want more screens? Retro Display FX has ten: CRT with aberration and bloom, arcade monitor, composite video, VHS, Game Boy, limited palettes, LCD, glitch and a glowing terminal: https://dimensionalmios.itch.io/almios-retro-display-fx

Shader code
shader_type canvas_item;

// CRT Lite (free) by Dimension Almios, MIT licence.
// A light CRT look for the whole screen: curved glass with rounded corners,
// scanlines, vignette and an optional RGB mask. Put it on a ColorRect that covers
// the screen, inside a CanvasLayer drawn above your game (layer 100, for
// example): it reads what is already on screen. Anything on a higher layer (your
// interface, if you like) stays clean.

uniform sampler2D screen_tex : hint_screen_texture, filter_linear_mipmap;

uniform float curvature : hint_range(0.0, 0.3) = 0.1;       // 0 = flat screen
uniform float corner_radius : hint_range(0.0, 0.3) = 0.06;  // as a fraction of the screen height
uniform vec4 border_color : source_color = vec4(0.0, 0.0, 0.0, 1.0);
group_uniforms Scanlines;
uniform float scanlines : hint_range(0.0, 1.0) = 0.35;      // how dark the gaps between lines are
uniform float scanline_size : hint_range(2.0, 8.0) = 3.0;   // height of one line, in screen pixels
group_uniforms Look;
uniform float vignette : hint_range(0.0, 1.0) = 0.35;
uniform float brightness : hint_range(0.5, 2.0) = 1.1;      // makes up for the darker gaps
uniform bool rgb_mask = false;                               // red, green and blue stripes like a real tube
uniform float mask_strength : hint_range(0.0, 1.0) = 0.3;

float hash(vec2 p) {
	p = fract(p * vec2(0.1031, 0.1030));
	p += dot(p, p.yx + 33.33);
	return fract((p.x + p.y) * p.x);
}

void fragment() {
	vec4 node_tint = COLOR;
	vec2 screen_px = 1.0 / SCREEN_PIXEL_SIZE;
	float aspect = screen_px.x / screen_px.y;

	// barrel curvature: each axis bends by the square of the other, so the edges
	// bow inward and the corners fall off the glass
	vec2 cc = SCREEN_UV * 2.0 - 1.0;
	vec2 bent = cc + cc * cc.yx * cc.yx * curvature;
	bent *= 1.0 + curvature * 0.25;   // a little overscan: a thin frame shows all round
	vec2 uv = bent * 0.5 + 0.5;

	// the glass: a rounded rectangle, anti-aliased by its own screen-space slope
	vec2 half_size = vec2(aspect, 1.0);
	vec2 p = bent * half_size;
	float rad = corner_radius;
	vec2 q = abs(p) - (half_size - rad);
	float d = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - rad;
	float inside = clamp(0.5 - d / max(fwidth(d), 1e-5), 0.0, 1.0);

	vec3 col = textureLod(screen_tex, clamp(uv, 0.0, 1.0), 0.0).rgb;

	// scanlines are laid on the screen's own pixel rows: every line has the same
	// height and a soft cosine profile, so they never beat into moire bands
	float line = FRAGCOORD.y / scanline_size;
	float s = 0.5 + 0.5 * cos(line * 6.2832);
	// bright pixels bloom across the gap, like a real beam
	float lum = dot(col, vec3(0.3, 0.59, 0.11));
	col *= mix(1.0, mix(1.0 - scanlines, 1.0, s), 1.0 - lum * 0.4);

	if (rgb_mask) {
		float k = mod(floor(FRAGCOORD.x), 3.0);
		vec3 m = k < 1.0 ? vec3(1.0, 0.7, 0.7) : (k < 2.0 ? vec3(0.7, 1.0, 0.7) : vec3(0.7, 0.7, 1.0));
		col *= mix(vec3(1.0), m * 1.18, mask_strength);
	}

	vec2 v = uv * (1.0 - uv);
	col *= mix(1.0, pow(clamp(v.x * v.y * 16.0, 0.0, 1.0), 0.3), vignette);
	col *= brightness;
	col += (hash(FRAGCOORD.xy) - 0.5) / 255.0;                 // dither: no banding in the vignette

	COLOR = vec4(mix(border_color.rgb, col, inside), 1.0);
	COLOR *= node_tint;
}
Live Preview
Tags
CRT, pixel-art, Post processing, retro, scanlines, screen
The shader code and all code snippets in this post are under MIT license and can be used freely. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from DimensionAlmios

Related shaders

guest

0 Comments
Oldest
Newest Most Voted