PSX Style Camera Shader – Distance Fog, Dithering, Color Limiter, Noise

Only works on Godot 4.3 and later! For a 4.2 and below compatible version, go to my Github and check the releases

4.2 and Below Release

This is a Camera shader for Godot 4 to add distance fog with optional noise, color limiting, and dither. There are a lot of fragmented and partial solutions out there and I wanted to combine various techniques to make an easy to use quad-based shader. Each setting can be enabled/disabled and customized. I have tested this in small maps (100×100) as well as large maps (6000×6000) with no issues.

For the fog, I took direct inspiration and direction from the excellent Godot4-ScreenSpacePostFX-DepthFog by zvodd. His technique is different (using viewports) and contains features mine does not, so please check it out! The color reduction and dithering techniques are my own.

If starting from stratch, create a new 3D scene and add a camera. Create a new MeshInstance3D as a child of the camera, assign it a QuadMesh, then make the size 2×2. Check the box “flip faces”. Move the mesh so it’s in front of the camera very slightly. Then all you need to do is give the QuadMesh a new material -> New ShaderMaterial -> New Shader. Assign the psx_camera_shader.gdshader to the material and boom you’re done. Check the shader parameters to make adjustments and edit everything:

Enable Fog – Enables/disables fog  
Fog Color –
Color of the fog  
Noise Color –
Color of the noise overlayed on top of the fog  
Fog Distance –
The distance in units away from the camera the fog is completely opaque  
Fog Fade Range –
How much distance the fog will take to fade to opaque  
Noise Time Fac –
The amount and movement of the noise  
Enable Color Limitation –
Enables/disables limiting the colors  
Color Levels –
Amount of colors allowed onscreen if Enable Color Limitation is checked  
Enable Dithering –
Enables/disables dithering  
Dither Strength –
Higher number mean bigger dots 

NOTE: If you have any transparent objects in your scene (meshes, sprites, etc) this shader won’t “see” them as the render mode doesn’t allow alpha transparency. In order to have transparent objects been seen you must change the “Render Priority” of each object to a number higher than “0” so the transparent object is drawn after the shader kicks in. Everything in the editor is set to “0” for a new project, so in most cases setting this to “1” for Each transparent object will be adequete.

License: MIT, use however you’d like. I hope this helps you on your Godot Journey!

Shader code
shader_type spatial;

render_mode cull_disabled, unshaded;
uniform sampler2D depth_texture : source_color, hint_depth_texture;
uniform sampler2D screen_texture : source_color, hint_screen_texture, repeat_disable, filter_nearest;

uniform bool enable_fog = true; 
uniform vec3 fog_color : source_color;
uniform vec3 noise_color : source_color;
uniform float fog_distance : hint_range(1, 6000) = 100;
uniform float fog_fade_range : hint_range(1, 6000) = 50;
uniform bool enable_noise = true;
uniform float noise_time_fac : hint_range(0.1, 10) = 4;
uniform bool enable_color_limitation = true;
uniform int color_levels : hint_range(2, 256) = 32;
uniform bool enable_dithering = true;
uniform float dither_strength : hint_range(0.0, 1.0) = 0.3; 

float hashOld12(vec2 p){
	return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453 + (sin((TIME)) / noise_time_fac)) ;
}

vec3 color_mix(vec3 a, vec3 b, float f ){
	f = clamp(abs(f), 0,1);
	float invf = 1.0 - f;
	return (a*f) + (b*invf);
}

vec3 quantize_color(vec3 color, int levels) {
    float quantizer = float(levels - 1);
    return floor(color * quantizer + 0.5) / quantizer;
}

float dither(vec2 position, float brightness) {
    int x = int(mod(position.x, 4.0));
    int y = int(mod(position.y, 4.0));
    int index = x + y * 4;
    float dithering[16] = float[](
        0.0, 0.5, 0.125, 0.625,
        0.75, 0.25, 0.875, 0.375,
        0.1875, 0.6875, 0.0625, 0.5625,
        0.9375, 0.4375, 1.0, 0.8125
    );
    float threshold = dithering[index];
    return brightness < threshold ? 0.0 : 1.0;
}

void vertex() {
  POSITION = vec4(VERTEX.xy, 1.0, 1.0);
}

void fragment(){
	vec3 screen_color = texture(screen_texture, SCREEN_UV).rgb;
	vec2 screen_coords = SCREEN_UV * 2.0 - 1.0;
	float depth = texture(depth_texture, SCREEN_UV).x;
	vec3 ndc = vec3(screen_coords, depth);
	
	vec4 view = INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
	view.xyz /= view.w;
	
	float linear_depth = -view.z;
	
	float depth_mask_inv = clamp((linear_depth - (fog_distance - fog_fade_range)) / fog_fade_range, 0.0, 1.0);
	
	vec3 final_color = screen_color;
	
	if (enable_noise){
		vec3 twocolornoise = color_mix(fog_color, noise_color, hashOld12(screen_coords));
		final_color = color_mix(twocolornoise, final_color, depth_mask_inv);
	}
	if (enable_fog){
		final_color = color_mix(fog_color, final_color, depth_mask_inv);
	}
	if (enable_color_limitation){
		final_color = quantize_color(final_color, color_levels);
	}
	if (enable_dithering){
		float brightness = dot(final_color, vec3(0.3, 0.59, 0.11));
		brightness += dither_strength * (dither(FRAGCOORD.xy, brightness) - 0.5);
		final_color *= (1.0 + dither_strength * (dither(FRAGCOORD.xy, brightness) - 0.5));
	}
	ALBEDO = final_color;
}
Tags
Camera Shader, Color Limiting, dither, Fog, psx
The shader code and all code snippets in this post are under MIT license and can be used freely. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from GEG-fairbear8974

PSX Style Water Surface – Pixelation, Waves, Scrolling Textures

Related shaders

Moving gradient noise fog/ mist for Godot 4

QoS Style World Space Blue Noise Dither Effect

Fog of war with alpha cut off as white color

Subscribe
Notify of
guest

7 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Darfine
6 months ago

Looks great! I will test it when I get back home 🙂

babbdi
6 months ago

that is absolutly an ball breaker cock and balls even torture of a shader, just created a account so i can comment that 💥💥💥💥🔥🔥🔥🔥🔥🔥

stramzer
stramzer
5 months ago

A real work of art !! Thanks a lot ! <3

Orrte
Orrte
2 months ago

Puse el render priority de mis mallas transparentes en 1 y aun así no se ven 🙁