Game Boy Post Processing shader for Unity
There was a small talk on making a Game Boy post processing shader on Unity dev chat on Discord (http://fromsmiling.tumblr.com/post/95495323142/tutorial-game-boy-shader-post-processing), but it‘s using a Shader Forge for a lot of post processing steps and uses a not very optimized version of pixelization. Also, doing some research, I haven’t found any drag-n-drop solutions for this seemingly simple effect (a few people sell it for around 1$-5$ for Game Maker or as plain shaders,)
So here we go. Before and After (scene from NatureStarterKit2):
The post effect is simple:
- Downscale it so it becomes 144 by height (the original Game Boy screen height)
- Find pixel brightness (luma)
- Posterize it (reduce to 4 colors)
- Using the logic from Taylor Bai-Woo, color this posterized version to four original Game Boy colors
1. Down we go.
To achieve pixelization effect, we can either go with some complicated UV truncation logic and calculate everything in full-screen resolution, or just make a smaller target render texture. It will save us some work and will also be faster to calculate.
Create a new C# Script called
Gameboy. Let’s start with creating a downscaled render texture.
We will need a private
RenderTexture field. We create this
RenderTexture in the
OnEnable function so we can safely
Destroy it when we disable our effect.
We consider the Height of our screen to be the constant value of 144 pixels. But the width will depend on aspect ratio (it won’t always be 160 like in original Game Boy), so we need to calculate it. To do it, just multiply the camera
aspect ratio value by height. We also need to set its
Point so it doesn’t try to smooth the pixels out.
And don’t forget to
Destroy it in the
Now we need to actually do some post processing with it. Let’s start with the simple stuff — an
Identity shader that will just copy current pixel values to another target
Render Texture. Why do we need it? Because using a simple way of Post Processing in Unity requires us to use the passed
destination Render Texture, or it will not render anything. There are, of course, different ways to handle it (I will write about it in some other post), but for simplicity lets just use
OnRenderImage for now.
Create a new Image Effect shader in Unity and put it somewhere:
Now open it and rename it to
Identity, and change the fragment shader logic to the following:
Now create a new Material and change its shader to
It misses a texture, but don’t worry, it will be filled automatically.
Let’s add an Identity Material reference to our
Gameboy script and add an OnRenderImage function where we use this material to blit the source texture to the target.
Notice lines 7 and 23–26.
You can now add this script to the Camera and link an Identity material there:
This won’t have any visible effects though as the Identity image effect does nothing. You’ll get why we need it a bit later.
2. Effect itself
Finally we can start making the effect itself.
First, create a new
Image Effect Shader like you did for the
Identity shader and rename it to
Gameboy. We will need four additional color parameters. I’ve done some research and here’s the original Game Boy colors (https://designpieces.com/palette/game-boy-original-color-palette-hex-and-rgb/):
We then need to add these parameters as uniforms to our shader:
And now we can use these colors in our calculations.
First, let’s find a brightness value of the pixel. Usual pattern in this case is to find a dot product of a pixel RGB value and a Luma vector. Luma vector represents the “weight” of the color on the overall brightness. A tempting thing is to just use a uniform
(0.33, 0.33, 0.33) vector for all colors, and it will get us a grayscale image, but this is not how sRGB works. sRGB color requires us to use a
(0.2126, 0.7152, 0.0722) vector. It’s still common to see an old and legacy luma vector
(0.3,0.59,0.11), (even the ShaderForge uses it), but it’s wrong. It was used for the NTSC RGB color space and it somehow still lives to this day in this form. sRGB requires us to use the brightness vector that I’ve showed earlier. This comparison, as well as other color space brightness vectors can be found here — http://www.brucelindbloom.com/index.html?WorkingSpaceInfo.html
With all that said -
Line 10 — just getting a current pixel value
Line 11 — calculating pixel brightness (desaturating it)
Now starts the tricky part. We need to posterize our image and reduce the total number of colors to 4. The formula is pretty simple:
Line 12 — flooring the luma value times number of steps, divided by number of steps minus 1. I deliberately left the math in this verbose form. You can either replace
3, or use another uniform parameter to control the total number of colors.
Now goes the original lerping technique by Taylor Bai-Woo:
Three lerps between four colors and in the end we get a colored version of our brightness-polarized pixel value. Lerps in this case are completely discrete and don’t have any semi-tones.
Here’s the full shader code — https://gist.github.com/KumoKairo/2c42db9c4219eb76903831500f1ffa42
Now create a new material named
Gameboy, change its shader to
Gameboy and link it to a new
Material field in your
Gameboy C# Script (lot’s of Gameboys, don’t mess it up). You’ve done the same thing with the
It will look like this:
The only thing that’s left is to add this material to our pipeline. Change the
OnRenderImage function to use this new
Line 5 — added a new material field.
Line 26 and 27 — first pass is to blit the Source (render image without effects) into our downscaled texture using our posterize-n-colorize shader. And then we copy this downscaled texture into a fullscreen texture as
OnRenderImage function logic requires us.
Done, you’re awesome.
P.S. There are other ways to handle usage of shaders and materials like creating them on the fly at runtime. It’s a good option, but for simplicity’s sake I’ve decided to just make it all as Assets.