I made a real-time dithering effect for my game, and I thought I'd explain the method in case anyone finds it useful.
I assume you understand how to use Unreal Engine and create a basic post process material, and can use Photoshop, Affinity Photo, GIMP or any image editing software.
Also this is not "scientifically accurate" in any way, it doesn't actually limit the colors, it's just a visual effect that emulates a lower color depth in a convincing enough manner. For more accurate dithering methods
Theory
Before I go over the workflow, I should explain the theory and logic behind this. So first of all, what is dithering? It's a way to give the illusion of a higher color depth with a limited color palette. For example the image below is made only of 2 colors (black and white), but with dithering, it looks like there are more grayscale values in-between. It's very similar to pointillism in art, the more black dots you have, the darker that area is.
This doesn't have to be limited to black and white, or even 2 colors. In the following example there are only 32 real colors.
But for my needs I only focused on a 1-bit black and white dithering pattern and mixed it with the rendered image to give fake dithering without washing out the colors too much or causing banding, so I'll explain the method for that.
It starts with the original render, which is converted to grayscale, "split" into layers (posterize/quantize effect), then assign each of those layer the apropriate dithering pattern. Here's a 3D representation, the top (bright) layers pick the brightest dithering pattern, and the bottom ones pick the darkest.
Please note that this is only an analogy, we don't actually split the image into layers in Unreal Engine.
One obvious issue is : isn't that a lot of textures for the dither patterns? Yes, that's a lot. If you wanted to use patterns 32 values for example that would mean 32 different textures, which is way too much. Luckily we can combine all of them into one single texture.
We can combine the layers together (more details below) and use a threshold modifier "pick" which pattern we want
The threshold doesn't have to be the same for all the image. If we use a grayscale version of the render result as the threshold value, it will be like cutting the image into several layers by brightness and selecting the appropriate pattern for each, like I explained in the analogy above. I used 32 values, but the illustrations only show 8 "layers" for readability.
Implementation
First let's start with a grayscale gradient and apply dithering to it. Start with a 32x1px image, then scale it up to 1024x32px using the nearest neighbor filter. That way we get a 32x32px square for each of our 32 grayscale values.
After that, using GIMP, set the color mode to "Indexed", pick "Use black and white (1-bit) palette" and set color dithering to "positioned". After that you'll get a nice black and white dithered version of the gradient.
Make sure to manually correct any issuess, like holes or unneeded dots.
Next cut the image into individual 32x32 squares, and set each layer's opacity to 3% (100 percent divided by 32 layers is 3.125, but since Affinity Photo only uses integer value we'll round it down to 3). Set the blending mode to addition, and align them all on top of each other. You should get something like the following
Notice in the histogram that the values don't go all the way down to black and don't reach white. That's easy to fix though, I added a levels adjustment layer. I also adjusted the gamma value until the peaks were as evenly spaced as possible. It's recommended to set the document's color depth to 16-bit to reduce precision errors)
The resulting texture should look something like this.
And again showing the threshold filter in the image editor as demonstration only, do not export the texture with the threshold filter enabled.
Now the texture is ready to be imported to Unreal. There are a few compression settings we need to set first
Mip-Maps are not needed, since the image is intended to be always displayed at 1x scale. For compression settings, use "Grayscale (G8/16 RGB8 sRGB)" because it's lossless. The other compression settings reduce quality and break the effect. And finally disable sRGB, we want to use the texture in the linear color space.
Next, create a post process material in Unreal Engine. The first thing we need to do is to overlay the dither texture at exactly 1:1 scale over the screen. To do that, we need to know the resolution, for which we can use the "Screen Resolution" node. Make sure to use the "Buffer Resolution" and not the "Visible Resolution", because in many cases the visible area is smaller than what's actually rendered.
We know that our texture is 32x32 (it could be different for you), so we divide by 32, and multiply with the texture coordinates. That makes the texture pixels align exactly 1:1 with the rendered pixels.
We used a "threshold" node and used the render result ("SceneTexture:PostprocessInput0") to control the threshold value. The saturate node is to make sure the values remain between 0 and 1, and the one-minus node is to invert it. That's the entire setup you need for a black and white (1-bit) dithering effect.
Next I added it on top of the render result. Since this is addition, it means the result (original + dithered) go from 0 to 1 when the mixing strength is 0, and from 0 to 2 with the full strength, that means the image will appear brighter than it is. That's why I added brightness correction: I remapped the strength from 0-1 to 1-0.5 and multiplied it with the result. This means at 0.0 strength the result is multiplied by 1, which has no effect at all, but at full strength it is multiplied by 0.5, which corrects it. This is not exact 1:1 correction, but it will do, especially because I only want to use it subtly.
That's pretty much it. I added some minor adjustments here and there, like a 1/2 pixelation and some color corrections. Here are some in-game screenshots. Make sure to zoom in to see the full effect.

That's all. If you have any questions or thoughts please don't hesitate to ask away. If you end up using it in a project I'd like to hear about it too.
Have a nice day!