Dither Gradient Shader
This is a dither shader inspired by Lucas Pope’s Return of the Obra Dinn.
The shader itself is a simple post-process screen shader which will take a gradient palette and apply it with a dither effect to any scene.
A fully comprehensive Godot demo for the shader is available on Itch (with HTML5 build) and Github.
Uniforms
- u_dither_tex – This is the texture that’s used to create the dither pattern. It is most commonly a bayer matrix (8×8 or 16×16) but could technically be anything. Make sure your import settings are correct, it needs to tile (Repeat enabled) and have no filtering. The dither textures supplied in the demo are by tromero (MIT licensed).
- u_color_tex – This is the palette used to determine which colours to dither between. It can contain as many colours as you’d like, but the more colours, the less of an effect the dither has. The texture can be any size, as long as the colours change linearly along the X axis (ideally darkest on the left). Lospec is a great source of palettes.
- u_bit_depth – If you’d like a more banded appearance, lower this value.
- u_contrast – Adjusts luminance scale around the mid-point, linearly. Increasing contrast will darken shadows and brighten highlights.
- u_offset – Shifts luminance up or down, allowing you to tweak how dark or light the output is.
- u_dither_size – Adjust size of each dither pixel. Increase for a more pixelated look.
References
- Lucas Pope’s devlog for Return of the Obra Dinn – https://forums.tigsource.com/index.php?topic=40832.msg1363742#msg1363742
- Initial implementation reference – https://danielilett.com/2020-02-26-tut3-9-obra-dithering/
- Bayer matrix textures – https://github.com/tromero/BayerMatrix
- Blue noise texture – http://momentsingraphics.de/BlueNoise.html
- Used in Geiger, a game I made for Mini Jam 76 – https://sambigos.itch.io/geiger
- Follow me on Twitter – https://twitter.com/Calneon
Shader code
/*
This shader is under MIT license. Feel free to use, improve and
change this shader according to your needs and consider sharing
the modified result to godotshaders.com.
*/
shader_type canvas_item;
uniform sampler2D u_dither_tex;
uniform sampler2D u_color_tex;
uniform int u_bit_depth;
uniform float u_contrast;
uniform float u_offset;
uniform int u_dither_size;
void fragment()
{
// sample the screen texture at the desired output resolution (according to u_dither_size)
// this will effectively pixelate the resulting output
vec2 screen_size = vec2(textureSize(TEXTURE, 0)) / float(u_dither_size);
vec2 screen_sample_uv = floor(UV * screen_size) / screen_size;
vec3 screen_col = texture(TEXTURE, screen_sample_uv).rgb;
// calculate pixel luminosity (https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color)
float lum = (screen_col.r * 0.299) + (screen_col.g * 0.587) + (screen_col.b * 0.114);
// adjust with contrast and offset parameters
float contrast = u_contrast;
lum = (lum - 0.5 + u_offset) * contrast + 0.5;
lum = clamp(lum, 0.0, 1.0);
// reduce luminosity bit depth to give a more banded visual if desired
float bits = float(u_bit_depth);
lum = floor(lum * bits) / bits;
// to support multicolour palettes, we want to dither between the two colours on the palette
// which are adjacent to the current pixel luminosity.
// to do this, we need to determine which 'band' lum falls into, calculate the upper and lower
// bound of that band, then later we will use the dither texture to pick either the upper or
// lower colour.
// get the palette texture size mapped so it is 1px high (so the x value however many colour bands there are)
ivec2 col_size = textureSize(u_color_tex, 0);
col_size /= col_size.y;
float col_x = float(col_size.x) - 1.0; // colour boundaries is 1 less than the number of colour bands
float col_texel_size = 1.0 / col_x; // the size of one colour boundary
lum = max(lum - 0.00001, 0.0); // makes sure our floor calculation below behaves when lum == 1.0
float lum_lower = floor(lum * col_x) * col_texel_size;
float lum_upper = (floor(lum * col_x) + 1.0) * col_texel_size;
float lum_scaled = lum * col_x - floor(lum * col_x); // calculates where lum lies between the upper and lower bound
// map the dither texture onto the screen. there are better ways of doing this that makes the dither pattern 'stick'
// with objects in the 3D world, instead of being mapped onto the screen. see lucas pope's details posts on how he
// achieved this in Obra Dinn: https://forums.tigsource.com/index.php?topic=40832.msg1363742#msg1363742
ivec2 noise_size = textureSize(u_dither_tex, 0);
vec2 inv_noise_size = vec2(1.0 / float(noise_size.x), 1.0 / float(noise_size.y));
vec2 noise_uv = UV * inv_noise_size * vec2(float(screen_size.x), float(screen_size.y));
float threshold = texture(u_dither_tex, noise_uv).r;
// adjust the dither slightly so min and max aren't quite at 0.0 and 1.0
// otherwise we wouldn't get fullly dark and fully light dither patterns at lum 0.0 and 1.0
threshold = threshold * 0.99 + 0.005;
// the lower lum_scaled is, the fewer pixels will be below the dither threshold, and thus will use the lower bound colour,
// and vice-versa
float ramp_val = lum_scaled < threshold ? 0.0f : 1.0f;
// sample at the lower bound colour if ramp_val is 0.0, upper bound colour if 1.0
float col_sample = mix(lum_lower, lum_upper, ramp_val);
vec3 final_col = texture(u_color_tex, vec2(col_sample, 0.5)).rgb;
// return the final colour!
COLOR.rgb = final_col;
}
Idk if it was just me being dumb, but if anyone else gets the dithering in just a little section of the corner, make sure to have repeat enabled on the dithering texture and reimport it.
It even says it in the description here… *facepalm*
Hi, just wondering why there is a strip of the dithered palette on the left side of the screen?
oh it’s just that texture rectangle with the gradient on it, my bad!
To make work with Godot 4 change line 9 from
to
see Here and Here for more info.
Awesome! Thank you so much.
To get it to work, I also had to:
Replace
TEXTURE
with the recommendedscreen_texture
uniform. (See Screen-reading shaders on Godot Docs.)Usefilter_nearest
on the textures.Ultimately, here’s the code that I ended up with: https://pastes.dev/UlpTD3zm4C. It’s working on Godot 4.2.I’m unable to get it to work on Godot 4. Where should the shader be? On a ColorRect that covers the screen? I always only get the white color rect on the screen. No dithering.
Hey! been trying to find a working dither + colour limiting shader for ages now. Your code paste has expired, don’t suppose you still have it and could send it again? Thanks!
OMG This is SO COOL!
does it work in 2d my brothe wants to know uwu