I’m playing with a Commodore 64 post process shading in Godot 4.4.1. Here’s a breakdown of how it works. The shader can be found below.
There are two steps: I first render the scene normally in a subviewport at 320×200 pixels. In my UI layer I have a ColorRect with this canvas shader added.

Shader Updated!
I’ve updated the shader so you can turn on and off per feature. I’ve also added an initial exposure to be able to control the input brightness.
shader_type canvas_item;
uniform float exposure = 1.0; // 1.0 = no change, >1 = brighter, <1 = darker
uniform sampler2D screen_texture : hint_screen_texture;
uniform sampler2D palette_texture : repeat_disable, filter_nearest;
uniform int palette_size = 16;
uniform float dither_strength = 0.5;
uniform int posterize_levels = 0; // 0 = off
uniform bool use_dither = true;
uniform bool use_palette = true;
float get_bayer_threshold(ivec2 pixel_coords) {
int x = pixel_coords.x % 4;
int y = pixel_coords.y % 4;
int index = y * 4 + x;
float[16] bayer = float[](
0.0, 8.0, 2.0, 10.0,
12.0, 4.0, 14.0, 6.0,
3.0, 11.0, 1.0, 9.0,
15.0, 7.0, 13.0, 5.0
);
return bayer[index] / 16.0;
}
vec3 posterize(vec3 c, int levels) {
return floor(c * float(levels)) / float(levels);
}
void fragment() {
vec3 color = texture(screen_texture, SCREEN_UV).rgb * exposure;
ivec2 pixel_coords = ivec2(FRAGCOORD.xy);
float threshold = get_bayer_threshold(pixel_coords);
// Posterize
if (posterize_levels > 0) {
color = posterize(color, posterize_levels);
}
// Dither
if (use_dither && dither_strength > 0.0) {
vec3 dither = vec3((threshold - 0.5) * 2.0 * dither_strength / float(palette_size));
color = clamp(color + dither, 0.0, 1.0);
}
// Palette remapping
if (use_palette) {
float min_dist = 999.0;
vec3 closest = vec3(0.0);
for (int i = 0; i < palette_size; i++) {
float u = (float(i) + 0.5) / float(palette_size);
vec3 pal_color = texture(palette_texture, vec2(u, 0.5)).rgb;
float dist = distance(color, pal_color);
if (dist < min_dist) {
min_dist = dist;
closest = pal_color;
}
}
color = closest;
}
COLOR = vec4(color, 1.0);
}
This shader first apply a pixel dithering effect that you set with the exposed dither_strength variable.
Then we use a palette texture to map the rendered input to the nearest color. I’ve used a 16×1 pixel image of the Commodore 64 color palette.

The final step is to upscale the processed Color Rect to full screen TextureRect node. Here’s how my scene looks.

Here you can see the input before the shader is applied.
