Retro Luma Color Reduction/Quantization (Posterize) + Dithering

A retro-style post-processing shader for Godot. Key features include a pixelation effect for resolution reduction, RGB or Luma color quantization (posterize), and two dithering methods available (ordered or texture-based [you can use something like a blue noise texture]). The shader also incorporates two color grading methods, one very simple and another one a bit more advanced.

You can find a detailed explanation of the shader on my youtube tutorial. 

THere is also a sample project on my github.

Hope you’ll like 👾

Shader code
shader_type canvas_item;

// pixelate effect (optional)
#define USE_LOW_RESOLUTION 1

// choose only one dithering method! (optional)
#define USE_DITHERING_ORDERED 0        // ordered dithering 
#define USE_DITHERING_NOISE_TEXTURE 1  // noise texture based dithering

// choose only one posterization method! (optional)
#define USE_POSTERIZE_RGB 0 // posterize RGB values
#define USE_POSTERIZE_LUMA 1 // posterize luma only

// choose only one color grad method! (optional)
#define USE_COLOR_GRAD_SIMPLE 0
#define USE_COLOR_GRAD_ADVANCED 1

uniform sampler2D screen_tex : hint_screen_texture, repeat_disable, filter_nearest; // use filter_nearest for better retro look
uniform float effect_strength : hint_range(0.0, 1.0) = 1.0; 
uniform float gamma_adjust : hint_range(1.0, 4.0) = 2.2; 

#if USE_LOW_RESOLUTION
uniform float desired_screen_width : hint_range(16.0, 1024.0) = 640.0; 
#endif

#if USE_DITHERING_ORDERED 
const float dithering_pattern[] = {
    -4.0, 0.0, -3.0, 1.0,
    2.0, -2.0, 3.0, -1.0,
    -3.0, 1.0, -4.0, 0.0,
    3.0, -1.0, 2.0, -2.0
};
#endif

#if USE_DITHERING_NOISE_TEXTURE
uniform sampler2D noise_texture : hint_default_black, filter_nearest; // only RED channel is used
#if !USE_LOW_RESOLUTION
uniform float noise_scale : hint_range(0.1, 10.0) = 4.0;
#endif
#endif

#if USE_DITHERING_ORDERED || USE_DITHERING_NOISE_TEXTURE
uniform float dither_strength : hint_range(0.0, 0.1) = 0.005;
#endif

#if USE_POSTERIZE_RGB
uniform float posterize_rgb_levels : hint_range(1.0, 32.0) = 6.0;
#endif
#if USE_POSTERIZE_LUMA
uniform float posterize_luma_levels  : hint_range(1.0, 32.0) = 6.0;
#endif

#if USE_COLOR_GRAD_SIMPLE 
uniform vec3 color_grad_shadows : source_color = vec3(0.0, 0.25, 0.25);
uniform vec3 color_grad_lights : source_color = vec3(0.75, 0.75, 0.0);
#endif

#if USE_COLOR_GRAD_ADVANCED
uniform vec3 color_grad_shadows : source_color = vec3(0.0, 0.25, 0.25);
uniform vec3 color_grad_midtones : source_color = vec3(0.5, 0.0, 0.5);
uniform vec3 color_grad_lights : source_color = vec3(1.0, 1.0, 0.0);
uniform float color_grad_midpoint : hint_range(0.1, 0.9) = 0.25;
#endif

#if USE_COLOR_GRAD_SIMPLE || USE_COLOR_GRAD_ADVANCED
uniform float color_grad_strength : hint_range(0.0, 1.0) = 0.25;
#endif


#if USE_POSTERIZE_LUMA
// rgb2hsv and hsv2rgb by sam hocevar
// taken from 
// https://stackoverflow.com/questions/15095909/from-rgb-to-hsv-in-opengl-glsl
// license: (PUBLIC DOMAIN) https://en.wikipedia.org/wiki/WTFPL 

// All components are in the range [0…1], including hue.
vec3 rgb2hsv(vec3 c)
{
    vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
    vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
    vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));

    float d = q.x - min(q.w, q.y);
    float e = 1.0e-10;
    return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}

vec3 hsv2rgb(vec3 c)
{
    vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
#endif

void fragment() {
	// get default UVs
	vec2 uv = UV;

#if USE_LOW_RESOLUTION
	// get screen resolution
	vec2 resolution = 1.0 / SCREEN_PIXEL_SIZE;
	// adjust UVs for lower resolution mode
    vec2 size = desired_screen_width * resolution.xy/resolution.x;
    vec2 coor = floor( FRAGCOORD.xy/resolution.xy * size);
    uv = coor / size;
#endif

   	// sample screen color
    vec4 srccol = texture(screen_tex, uv);  
    vec4 tgtcol = srccol;

    // adjust gamma
    tgtcol.rgb = pow(tgtcol.rgb, vec3(1.0 / gamma_adjust));

#if USE_DITHERING_ORDERED	
	// add dithering
	tgtcol.rgb += dithering_pattern[((int( coor.y ) % 4) * 4) + (int( coor.x ) % 4)] * dither_strength;
#endif    

#if USE_DITHERING_NOISE_TEXTURE
	// get uvs for noise	
	vec2 noise_uv = UV; 
	
#if USE_LOW_RESOLUTION
	float noise_scale = 1.0;

	// get texture size
    ivec2 noise_res = textureSize(noise_texture, 0);
	
	// autoscale
	float auto_scaling_factor = desired_screen_width / (float(noise_res.x));
	noise_uv.x *= auto_scaling_factor;
	noise_uv.y *= auto_scaling_factor;
#endif	

	// fix aspect ratio
	noise_uv.y *= (SCREEN_PIXEL_SIZE.x / SCREEN_PIXEL_SIZE.y);   	

	// sample noise texture (red channel only)
	float noise = texture(noise_texture, fract(noise_uv * noise_scale)).r;

	// add dithering 
	tgtcol.rgb += vec3(((noise - 0.5) * 8.0) * dither_strength); // * 8.0 is used to match the ordered dithering "dithering_pattern" matrix values)
#endif    

#if USE_DITHERING_ORDERED || USE_DITHERING_NOISE_TEXTURE
	tgtcol.rgb = clamp(tgtcol.rgb, vec3(0.0), vec3(1.0));
#endif	

#if USE_POSTERIZE_RGB
	tgtcol.rgb = floor(tgtcol.rgb * posterize_rgb_levels) / posterize_rgb_levels;   
#endif

#if USE_POSTERIZE_LUMA
	vec3 hsv = rgb2hsv(tgtcol.rgb);
	hsv.z = floor(hsv.z * posterize_luma_levels) / posterize_luma_levels;   
	tgtcol.rgb = hsv2rgb(hsv);
#endif

#if USE_COLOR_GRAD_SIMPLE || USE_COLOR_GRAD_ADVANCED
	// get luma (convert to grayscale)
	float luma = dot(tgtcol.rgb, vec3(0.299, 0.587, 0.114));
#endif

#if USE_COLOR_GRAD_SIMPLE
	// compute tint color
	vec3 tint_col = mix(color_grad_shadows.rgb, color_grad_lights.rgb, luma);
#endif

#if USE_COLOR_GRAD_ADVANCED
	// compute tint color
	vec3 tint_col = vec3(1.0);
	if (luma < color_grad_midpoint) {
	    float t = smoothstep(0.0, color_grad_midpoint, luma);
	    tint_col = mix(color_grad_shadows.rgb, color_grad_midtones.rgb, t);
	} else {
	    float t = smoothstep(color_grad_midpoint, 1.0, luma);
	    tint_col = mix(color_grad_midtones, color_grad_lights.rgb, t);
	}
#endif

#if USE_COLOR_GRAD_SIMPLE || USE_COLOR_GRAD_ADVANCED
	// apply tint
	tgtcol.rgb = mix(tgtcol.rgb, tint_col, color_grad_strength);
#endif

    // restore gamma
    tgtcol.rgb = pow(tgtcol.rgb, vec3(gamma_adjust));

    // apply effect strength
	COLOR = mix(srccol, tgtcol, effect_strength);
}
Live Preview
Tags
color grad, color quantization, color reduction, dither, pixelate, posterize, 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.

More from ProfesorShader

Related shaders

guest

0 Comments
Oldest
Newest Most Voted