PS1 Post-processing
This shader is meant to be used with the PS1 Shader. It gives your game that PS1 dithered look. Works in editor, too!
Usage:
Make a MeshInstance, give it a QuadMesh, make the size 2 by 2, set the Extra Cull Margin to the highest possible value, apply a material with this shader to it.
Known issues:
Doesn’t support transparent objects.
Uniforms:
Color Depth – Reduce color depth to X bits per color. The PS1 supported many modes, some of them being 24 bit (8 bits per color) and 15 bit (5 bits per color), the latter (5 bits per color) used in majority of 3D games and being the default.
Dithering – Apply the “dotted” look that gives an illusion of more colors.
Resolution Scale – Reduce screen resolution.
Shader code
shader_type spatial;
render_mode unshaded, shadows_disabled, depth_test_disable, depth_draw_never;
uniform int color_depth : hint_range(1, 8) = 5;
uniform bool dithering = true;
uniform int resolution_scale = 4;
int dithering_pattern(ivec2 fragcoord) {
const int pattern[] = {
-4, +0, -3, +1,
+2, -2, +3, -1,
-3, +1, -4, +0,
+3, -1, +2, -2
};
int x = fragcoord.x % 4;
int y = fragcoord.y % 4;
return pattern[y * 4 + x];
}
void vertex() {
POSITION = vec4(VERTEX, 1.0);
}
void fragment() {
ivec2 uv = ivec2(FRAGCOORD.xy / float(resolution_scale));
vec3 color = texelFetch(SCREEN_TEXTURE, uv * resolution_scale, 0).rgb;
// Convert from [0.0, 1.0] range to [0, 255] range
ivec3 c = ivec3(round(color * 255.0));
// Apply the dithering pattern
if (dithering) {
c += ivec3(dithering_pattern(uv));
}
// Truncate from 8 bits to color_depth bits
c >>= (8 - color_depth);
// Convert back to [0.0, 1.0] range
ALBEDO = vec3(c) / float(1 << color_depth);
}
Awesome shader! One problem though, it doesn’t support transparency at all. Transparent objects disappear at certain angles and are not affected by the reduced resolution and dithering.
It’s a Godot limitation, sadly. If you really need transparency, you could turn the shader into a canvas_item one and apply it to a viewport.
It works, but I’m not convinced 8 bit color is the mode used by most PS1 games, least of all Metal Gear Solid.
https://en.wikipedia.org/wiki/PlayStation_technical_specifications
I suspect Mode 15 was the most common.
Transparency could work with with this if it was suface shader rather than a post process shader.
https://menacingmecha.itch.io/godot-psx-style-demo
Here is someone else’s attempt at it – it has many problems of its own, which I’ve been trying to fix, but it does support transparency.
By “5 bit color” I meant 5 bits per component (5 bit R, G, B so 15 bits in total), sorry if that wasn’t clear. I’ll update the description.
any chance of a GLES2 compatible version? it’s kinda weird to use a higher-quality renderer to make my graphics look worse
Wait, this isn’t a canvasitem shader…
Changed the category to Spatial. Better late than never 🙂
I managed to get transparent objects (and shadows) working by converting this into a canvas item shader as per here. Conversion’s pretty painless: change the shader_type, comment out the render_mode and vertex function and switch SCREEN_TEXTURE for TEXTURE. Then plonk the material on a ViewportContainer. Tada.
The Viewport will need to have matching Shadow Atlas size for shadows to show up properly, but it does work!
Only downside is it no longer works in Editor.
What did you change the albedo line near the end to? Albedo doesn’t exist on canvasitem shaders and I can’t for the life of me figure out what i’m meant to put there instead.
EDIT: Found it. Change ALBEDO to COLOR.rgb
thanks you legend
Not sure if anyone else has made this mistake, but be sure to resize the quad mesh by clicking the mesh preview window and change the size to 2. Don’t change the size in the transform menu, otherwise only a small portion of the center of the viewport will be affected by the post-processing.
can you do a gles 2 version? Please!
shader_type canvas_item;
uniform int color_depth : hint_range(1, 8) = 5;
uniform bool dithering = true;
uniform int resolution_scale = 4;
const mat4 pattern = mat4(
vec4(-4, +0, -3, +1),
vec4(+2, -2, +3, -1),
vec4(-3, +1, -4, +0),
vec4(+3, -1, +2, -2)
);
int dithering_pattern(ivec2 fragcoord) {
int x = fragcoord.x % 4;
int y = fragcoord.y % 4;
vec4 pattern_vec4;
if (y == 0) pattern_vec4 = pattern[0];
else if (y == 1) pattern_vec4 = pattern[1];
else if (y == 2) pattern_vec4 = pattern[2];
else if (y == 3) pattern_vec4 = pattern[3];
float pattern_int;
if (x == 0) pattern_int = pattern_vec4.x;
else if (x == 1) pattern_int = pattern_vec4.y;
else if (x == 2) pattern_int = pattern_vec4.z;
else if (x == 3) pattern_int = pattern_vec4.w;
return int(pattern_int);
}
void fragment() {
ivec2 uv = ivec2(FRAGCOORD.xy / float(resolution_scale));
vec4 color = texelFetch(SCREEN_TEXTURE, uv * resolution_scale, 0);
// Convert from [0.0, 1.0] range to [0, 255] range
ivec4 c = ivec4(round(color * 255.0));
// Apply the dithering pattern
if (dithering) {
c += ivec4(dithering_pattern(uv));
}
// Truncate from 8 bits to color_depth bits
c >>= (8 – color_depth);
// Convert back to [0.0, 1.0] range
COLOR = vec4(c) / float(1 << color_depth);
}
This is a version for GLES2. Apply this shader to a full-screen ColorRect.
I was able to get this in Godot 4 pretty easily, it also seems to work with transparency and shadows as a spatial shader. Just change depth_test_disable to depth_test_disabled and make sure you place the shader in the mesh instances material, not the Geometry Instance overrides, and make sure Flip Faces is set to On. Everything else should be the same as above. For transparency, make sure your material uses Alpha Scissor and it should work with the shader. You can still convert it to a canvas shader using the steps shown by Guessy’s comment.
it doesn’t seem to work for me, down at the bottom where it says ‘ALBEDO = vec3(c) / float(0 << color_depth);’ it is listed as am error
This happened to me as well, but it was just missing a curly bracket } at the end of the code.
Howdy, I used this shader extensively in my game, Synesthesia.
It only appears in the desktop versions due to graphical constraints. It was vital to forming the aesthetic for the game. Thank you for sharing this shader with the community.
Here’s a trailer if anyone wants to see the shader in
action: https://www.youtube.com/watch?v=v2BZei_-wUs
looks sick, but i’m having performance problems with it. Frame time goes from around 2ms to 11. My old macbook isn’t the strongest computer in the world, but i’m wondering if there’d be any way to make the shader more performant. I’m new to shader scripting, so i really don’t know where to start…
both shaders don’t work anymore since godot 4
It does, u only have to put on the Flip Faces and add the changes the engine indicates.
hello! I managed to make transparent objects kinda work by changing the material’s render priority to -1. Before changing the render priority transparent objects would disappear/appear only at certain angles. Test it out for yourself and let me know how its going on.
Godot 3.4.1 & MacOS 12.7
4.2 on Windows 10, and this worked. Got my particle effects workin great, transparency set to alpha scissor, Add blend mode. Does this work because the mesh is now rendering before everything else, or after it?
I saw this wasn’t for godot 4 so any godot 4 users should use this version of the code instead
shader_type spatial;
render_mode unshaded, shadows_disabled, depth_test_disabled, depth_draw_never;
uniform sampler2D SCREEN_TEXTURE: hint_screen_texture, filter_linear_mipmap;
uniform int color_depth : hint_range(1, 8) = 5;
uniform bool dithering = true;
uniform int resolution_scale = 4;
int dithering_pattern(ivec2 fragcoord) {
const int pattern[] = {
-4, +0, -3, +1,
+2, -2, +3, -1,
-3, +1, -4, +0,
+3, -1, +2, -2
};
int x = fragcoord.x % 4;
int y = fragcoord.y % 4;
return pattern[y * 4 + x];
}
void vertex() {
POSITION = vec4(VERTEX, 1.0);
}
void fragment() {
ivec2 uv = ivec2(FRAGCOORD.xy / float(resolution_scale));
vec3 color = texelFetch(SCREEN_TEXTURE, uv * resolution_scale, 0).rgb;
// Convert from [0.0, 1.0] range to [0, 255] range
ivec3 c = ivec3(round(color * 255.0));
// Apply the dithering pattern
if (dithering) {
c += ivec3(dithering_pattern(uv));
}
// Truncate from 8 bits to color_depth bits
c >>= (8 – color_depth);
// Convert back to [0.0, 1.0] range
ALBEDO = vec3(c) / float(1 << color_depth);
}
this saved me, thank you so much