Simple Ordered Dithering and Screen Pixelation

A super simple Canvas Item shader that downsamples the screen texture by a given integer, then quantises and dithers the texture using a 4×4 bayer matrix and a given bit depth. Not super advanced, but easy enough to use. This shader uses the size of the viewport when calculating downsampling, so take this into account when developing for variable resolutions.

Shader code
shader_type canvas_item;
uniform vec4 fog_color: source_color;
uniform sampler2D view: hint_screen_texture, filter_nearest, repeat_disable;
uniform float resolution_downsampling: hint_range(1.0, 8.0, 1.0);
uniform float bit_depth: hint_range(0.0, 64.0, 2.0);
const mat4 bayer_matrix_4x4 = mat4(
    vec4(    -0.5,       0.0,  -0.375,   0.125 ),
    vec4(    0.25,   -0.25,   0.375, - 0.125 ),
    vec4( -0.3125,  0.1875, -0.4375,  0.0625 ),
    vec4(  0.4375, -0.0625,  0.3125, -0.1875 )
);
const int bayer_n = 4;
void vertex() {
	// Called for every vertex the material is visible on.
}

void fragment() {
	vec2 UV_new = SCREEN_UV -  mod(SCREEN_UV, SCREEN_PIXEL_SIZE * resolution_downsampling);
	vec3 tex = texture(view, UV_new).rgb;
	//float screen_resize = SCREEN_PIXEL_SIZE.x
	vec2 pix_id = vec2(SCREEN_UV.x / (SCREEN_PIXEL_SIZE.x * resolution_downsampling), SCREEN_UV.y / (SCREEN_PIXEL_SIZE.y * resolution_downsampling));
	float bayer_shift = bayer_matrix_4x4[int(mod(pix_id.x, 4.0))][int(mod(pix_id.y, 4.0))];
	tex += vec3(bayer_shift / bit_depth);
	tex.r = round(tex.r * bit_depth-1.0) / (bit_depth-1.0);
	tex.g = round(tex.g * bit_depth-1.0) / (bit_depth-1.0);
	tex.b = round(tex.b * bit_depth-1.0) / (bit_depth-1.0);
	COLOR.rgb = tex.rgb;
	COLOR.a = 1.0;
}

//void light() {
	// Called for every pixel for every light affecting the CanvasItem.
	// Uncomment to replace the default light processing function with this one.
//}
Tags
dither, downsample, pixel, pixelate
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 ANDROGYNESERAPH

View-Matcap Based Fake Vertex Lighting

Related shaders

Arbitrary Color Reduction and Palette Ordered Dithering

Dithering / Screen Door Transparency

Animated 2D Fog(Optional Pixelation)

Subscribe
Notify of
guest

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
jorbyte
13 days ago

this is nice. I converted it into a spatial because my setup is for the quad in front of the camera thing. Here it is if anyone wants it.

shader_type spatial;
render_mode blend_add;
uniform vec4 fog_color: source_color;
uniform sampler2D view: hint_screen_texture, filter_nearest, repeat_disable;
uniform float resolution_downsampling: hint_range(1.0, 8.0, 1.0);
uniform float bit_depth: hint_range(0.0, 64.0, 2.0);


const mat4 bayer_matrix_4x4 = mat4(
    vec4(    -0.5,       0.0,  -0.375,   0.125 ),
    vec4(    0.25,   -0.25,   0.375, - 0.125 ),
    vec4( -0.3125,  0.1875, -0.4375,  0.0625 ),
    vec4(  0.4375, -0.0625,  0.3125, -0.1875 )
);


const int bayer_n = 4;


void vertex() {
	POSITION = vec4(VERTEX.xy, 1.0, 1.0);
}


void fragment() {
	vec2 UV_new = SCREEN_UV -  mod(SCREEN_UV, 1.0/VIEWPORT_SIZE * resolution_downsampling);
	vec3 tex = texture(view, UV_new).rgb;
	//float screen_resize = 1.0/VIEWPORT_SIZE.x
	vec2 pix_id = vec2(SCREEN_UV.x / (1.0/VIEWPORT_SIZE.x * resolution_downsampling), SCREEN_UV.y / (1.0/VIEWPORT_SIZE.y * resolution_downsampling));
	float bayer_shift = bayer_matrix_4x4[int(mod(pix_id.x, 4.0))][int(mod(pix_id.y, 4.0))];
	tex += vec3(bayer_shift / bit_depth);
	tex.r = round(tex.r * bit_depth-1.0) / (bit_depth-1.0);
	tex.g = round(tex.g * bit_depth-1.0) / (bit_depth-1.0);
	tex.b = round(tex.b * bit_depth-1.0) / (bit_depth-1.0);
	ALBEDO = vec3(tex.rgb);
}