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);
}
Tags
ps1, retro
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from Mighty Duke

Mandelbrot Set

PS1 Shader

Related shaders

VHS Post Processing

Retro Post-Processing

3D Post-Processing: Dithering + Color Palettes

Subscribe
Notify of
guest

23 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Zorochase
3 years ago

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.

Last edited 3 years ago by Zorochase
Cyanide
Cyanide
3 years ago

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

  • Mode 4: 4-bit CLUT (16 colors)
  • Mode 8: 8-bit CLUT (256 colors)
  • Mode 15: 15-bit direct (32,768 colors)
  • Mode 24: 24-bit (16,777,216 colors)

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.

freddles
freddles
3 years ago

any chance of a GLES2 compatible version? it’s kinda weird to use a higher-quality renderer to make my graphics look worse

Exuin
3 years ago

Wait, this isn’t a canvasitem shader…

admin
Admin
2 years ago
Reply to  Exuin

Changed the category to Spatial. Better late than never 🙂

Guessy
2 years ago

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!

comment image

Only downside is it no longer works in Editor.

Last edited 2 years ago by Guessy
Lemin
Lemin
2 years ago
Reply to  Guessy

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

Last edited 2 years ago by Lemin
godotuser342432134
godotuser342432134
1 year ago
Reply to  Lemin

thanks you legend

CoffeeMoog
CoffeeMoog
1 year ago

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.

Ksigon
Ksigon
1 year ago

can you do a gles 2 version? Please!

Firerabbit
1 year ago
Reply to  Ksigon

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.

Last edited 1 year ago by Firerabbit
Firerabbit
1 year ago
Reply to  Firerabbit
// This Version is working in GLES2, but it will need some further improvements
// You can not change the resolution.

shader_type canvas_item;

uniform int color_depth : hint_range(1, 8) = 5;
uniform bool dithering = true;

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() {
    vec2 pixel_size = 1.0 / SCREEN_PIXEL_SIZE;
    vec3 color = texture(SCREEN_TEXTURE, round(SCREEN_UV * pixel_size) / pixel_size).rgb;
    
    // Convert from [0.0, 1.0] range to [0, 255] range
    ivec3 c = ivec3(round(color * 255.0));
    
    if (dithering) {
        c += ivec3(dithering_pattern(ivec2(FRAGCOORD.xy)));
    }
    
    // Truncate from 8 bits to color_depth bits
    c = c / int(pow(2.0, float(8 - color_depth)));
    
    // Convert back
    c = c * int(pow(2.0, float(8 - color_depth)));
    
    COLOR.rgb = vec3(c) / 255.0;
    
}
dragon1freak
1 year ago

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.

jimmyjonson
jimmyjonson
6 months ago
Reply to  dragon1freak

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

Noname
Noname
4 days ago
Reply to  jimmyjonson

This happened to me as well, but it was just missing a curly bracket } at the end of the code.

Last edited 4 days ago by Noname
breather
1 year ago

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

Last edited 1 year ago by breather
sixten
6 months ago

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…

maxi.havixbeck@gmail.com
maxi.havixbeck@gmail.com
6 months ago

both shaders don’t work anymore since godot 4

GRimm
GRimm
5 months ago

It does, u only have to put on the Flip Faces and add the changes the engine indicates.

glokk5teen
glokk5teen
4 months ago

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

Last edited 4 months ago by glokk5teen
xthejetx
xthejetx
17 days ago
Reply to  glokk5teen

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?