Retro Post-Processing

Canvas item shader that can handle resolution scaling, color depth adjustments, and dithering, as well as handling a LUT like recoloring of the scene using a target gradient.  

Heavily based on the PS1 Post-processing shader by Mighty Duke, but is now a canvas_item shader you apply to a SubViewportContainer and should also handle transparency.  The recoloring target gradient should be based on the HSV editors RGB gradient for reference and can be seen in the linked video explaining the shader.

Usage:
Create a RenderContainer scene consisting of the following structure
SubViewportContainer -> SubViewport -> [The 3D scene you want to render]

Then add a new ShaderMaterial to the SubViewportContainer, create a new Shader, and paste the below shader code into the new shader.  The parameters should be under that shader and you can play with them to get the visual you want.

 

Shader code
shader_type canvas_item;

// Handles the resolution changes, color depth, and dithering
group_uniforms resolution_and_colors;
uniform bool change_color_depth = false;
uniform int target_color_depth : hint_range(1, 8) = 5;
uniform bool dithering = false;
uniform bool scale_resolution = false;
uniform int target_resolution_scale = 3;

// Handles the LUTish recoloring
group_uniforms gradient_recoloring;
uniform bool enable_recolor = false;
uniform sampler2D to_gradient: hint_default_black;

int dithering_pattern(ivec2 fragcoord) {
	const int pattern[] = {
		-4, +0, -3, +1, 
		+2, -2, +3, -1, 
		-3, +1, -4, +0, 
		+3, -1, +2, -2
	};
	
	int x = fragcoord.x % 4;
	int y = fragcoord.y % 4;
	
	return pattern[y * 4 + x];
}

vec3 rgb2hsv(vec3 rgb) { //Converts RGB values to HSV
	float r = rgb.r;
	float g = rgb.g;
	float b = rgb.b;

	float cmax = max(r,max(g,b));
	float cmin = min(r,min(g,b));
	float delta = cmax - cmin;

	float h = 0.f; //hue

	if (delta > 0.f){
		if (cmax == r){
			h = (g-b)/delta;
			h = mod(h,6.f);
		} else if (cmax == g){
			h = ((b - r) / delta) + 2.f;
		} else {
			h = ((r-g)/delta) + 4.f;
		}
		h = h * 60.f;
	}

	float s = 0.f; //saturation
	if (cmax > 0.f){
		s = delta / cmax;
	}

	return vec3(h,s,cmax); // Keep original alpha value 

}

vec3 hsv2rgb(vec3 hsv) { //Converts HSV values to RGB
	float h = hsv.r;
	float s = hsv.g;
	float v = hsv.b;
	float c = v * s;
	//X = C × (1 - |(H / 60°) mod 2 - 1|)
	float x = h / 60.f;
	x = mod(x,2.f);
	x = abs(x - 1.f);
	x = c * (1.f - x);

	float m = v - c;

	vec3 rgb = vec3(0.f,0.f,0.f);

	if (h < 60.f) {
		rgb = vec3(c,x,0.f);
	} else if (h < 120.f){
		rgb = vec3(x,c,0.f);
	} else if (h < 180.f){
		rgb = vec3(0.f,c,x);
	} else if (h < 240.f){
		rgb = vec3(0.f,x,c);
	} else if (h < 300.f){
		rgb = vec3(x,0.f,c);
	} else if (h < 360.f){
		rgb = vec3(c,0.f,x);
	}
	rgb[0] = rgb[0] + m;
	rgb[1] = rgb[1] + m;
	rgb[2] = rgb[2] + m;

	return rgb; 
}

void fragment() {
	ivec2 uv;
	vec3 color;
	
	if(scale_resolution){
		uv = ivec2(FRAGCOORD.xy / float(target_resolution_scale));
		color = texelFetch(TEXTURE, uv * target_resolution_scale, 0).rgb;
	} else {
		uv = ivec2(FRAGCOORD.xy);
		color = texelFetch(TEXTURE, uv, 0).rgb;
	}
	
	if(enable_recolor){
		vec3 hsv = rgb2hsv(color);
		float color_pos = (hsv.x / 360.0);
		vec3 new_color = texture(to_gradient, vec2((color_pos), 0.5)).rgb;
		vec3 new_hsv = rgb2hsv(new_color);
		hsv.x = new_hsv.x;
		vec3 final_rgb = hsv2rgb(hsv);

		color.rgb = final_rgb;
	}

	
	// Convert from [0.0, 1.0] range to [0, 255] range
	ivec3 c = ivec3(round(color * 255.0));
	
	// Apply the dithering pattern
	if (dithering) {
		c += ivec3(dithering_pattern(uv));
	}
	
	vec3 final_color;
	if(change_color_depth){
		// Truncate from 8 bits to color_depth bits
		c >>= (8 - target_color_depth);
		final_color = vec3(c) / float(1 << target_color_depth);
	} else {
		final_color = vec3(c) / float(1 << 8);
	}
	
	// Convert back to [0.0, 1.0] range
	COLOR.rgb = final_color;
}
Tags
4.x, dither, lowres, lut, psx, recolor, retro
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

Related shaders

PS1/PSX PostProcessing

Perfect Retro Pixel Shader – Godot 4

Ultimate Retro Shader Collection for Godot 4

Subscribe
Notify of
guest

7 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
dootydut
10 months ago

Hey, are you sure this is the right shader? It seems to be a different one responsible for a CRT filter.

RuverQ
10 months ago
Reply to  dootydut

yeah, i’m so cringe. I remade the comment, sorry

Last edited 10 months ago by RuverQ
dootydut
10 months ago
Reply to  RuverQ

Its okay 😀

RuverQ
10 months ago

Cool shader!
But I find it confusing with this **SubViewportContainer -> SubViewport** thing

so I rewrote it to use screen_texture
to use this you need to add ColorRect to your scene and just adjust the size of it to fullscreen

shader_type canvas_item;


// Handles the resolution changes, color depth, and dithering
uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest;


group_uniforms resolution_and_colors;
uniform bool change_color_depth = false;
uniform int target_color_depth : hint_range(1, 8) = 5;
uniform bool dithering = false;
uniform bool scale_resolution = false;
uniform int target_resolution_scale = 3;


// Handles the LUTish recoloring
group_uniforms gradient_recoloring;
uniform bool enable_recolor = false;
uniform sampler2D to_gradient: hint_default_black;


int dithering_pattern(ivec2 fragcoord) {
	const int pattern[] = {
		-4, +0, -3, +1, 
		+2, -2, +3, -1, 
		-3, +1, -4, +0, 
		+3, -1, +2, -2
	};
	
	int x = fragcoord.x % 4;
	int y = fragcoord.y % 4;
	
	return pattern[y * 4 + x];
}


vec3 rgb2hsv(vec3 rgb) { //Converts RGB values to HSV
	float r = rgb.r;
	float g = rgb.g;
	float b = rgb.b;


	float cmax = max(r,max(g,b));
	float cmin = min(r,min(g,b));
	float delta = cmax - cmin;


	float h = 0.f; //hue


	if (delta > 0.f){
		if (cmax == r){
			h = (g-b)/delta;
			h = mod(h,6.f);
		} else if (cmax == g){
			h = ((b - r) / delta) + 2.f;
		} else {
			h = ((r-g)/delta) + 4.f;
		}
		h = h * 60.f;
	}


	float s = 0.f; //saturation
	if (cmax > 0.f){
		s = delta / cmax;
	}


	return vec3(h,s,cmax); // Keep original alpha value 


}


vec3 hsv2rgb(vec3 hsv) { //Converts HSV values to RGB
	float h = hsv.r;
	float s = hsv.g;
	float v = hsv.b;
	float c = v * s;
	//X = C × (1 - |(H / 60°) mod 2 - 1|)
	float x = h / 60.f;
	x = mod(x,2.f);
	x = abs(x - 1.f);
	x = c * (1.f - x);


	float m = v - c;


	vec3 rgb = vec3(0.f,0.f,0.f);


	if (h < 60.f) {
		rgb = vec3(c,x,0.f);
	} else if (h < 120.f){
		rgb = vec3(x,c,0.f);
	} else if (h < 180.f){
		rgb = vec3(0.f,c,x);
	} else if (h < 240.f){
		rgb = vec3(0.f,x,c);
	} else if (h < 300.f){
		rgb = vec3(x,0.f,c);
	} else if (h < 360.f){
		rgb = vec3(c,0.f,x);
	}
	rgb[0] = rgb[0] + m;
	rgb[1] = rgb[1] + m;
	rgb[2] = rgb[2] + m;


	return rgb; 
}


void fragment() {
	
	vec2 iResolution = 1.0 / SCREEN_PIXEL_SIZE;
    vec2 q = FRAGCOORD.xy / iResolution.xy;
	ivec2 uv = ivec2(q);
	
	vec3 color = texture(screen_texture, vec2(q.x,q.y) ).xyz;
	
	if(scale_resolution){
		uv = ivec2(FRAGCOORD.xy / float(target_resolution_scale));
		color = texelFetch(screen_texture, uv * target_resolution_scale, 0).rgb;
	} else {
		uv = ivec2(FRAGCOORD.xy);
		color = texelFetch(screen_texture, uv, 0).rgb;
	}
	
	if(enable_recolor){
		vec3 hsv = rgb2hsv(color);
		float color_pos = (hsv.x / 360.0);
		vec3 new_color = texture(to_gradient, vec2((color_pos), 0.5)).rgb;
		vec3 new_hsv = rgb2hsv(new_color);
		hsv.x = new_hsv.x;
		vec3 final_rgb = hsv2rgb(hsv);


		color.rgb = final_rgb;
	}


	
	// Convert from [0.0, 1.0] range to [0, 255] range
	ivec3 c = ivec3(round(color * 255.0));
	
	// Apply the dithering pattern
	if (dithering) {
		c += ivec3(dithering_pattern(uv));
	}
	
	vec3 final_color;
	if(change_color_depth){
		// Truncate from 8 bits to color_depth bits
		c >>= (8 - target_color_depth);
		final_color = vec3(c) / float(1 << target_color_depth);
	} else {
		final_color = vec3(c) / float(1 << 8);
	}
	
	// Convert back to [0.0, 1.0] range
	COLOR.rgb = final_color;
}
Luke
8 months ago
Reply to  RuverQ

Awesomely sweet shader with an awesomely sweet adjustment.

marius35mm
7 months ago
Reply to  RuverQ

Is this method working with a 3D scene?

SCRATXH
6 months ago
Reply to  RuverQ

<3