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

Related shaders

Comments 16

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

    4
  2. stramzer

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

    1
  3. Orrte

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

    0
  4. KodidomGamingd

    can you split this up to multiple individual shaders, instead of a big one containing all of them?

    0
    1. Hi there! Yes, I'm actually working on a more lightweight version that only does color reduction and a more efficient dithering pattern that scales with screen size, usable as a canvaslayer->colorrect. Let me know if something like that is what you are looking for.

      2
  5. Kyler

    This only seems to apply to the background of my scene, not any of the objects in it?

    0
    1. Please see my response to Redcat for help with this issue! It could very well be a render layer issue that overrides what the shader is trying to do. If not, please submit a ticket on the github page so I can help you out.

      0
  6. Amazing shader, but for somereason when something is transparent you just cant see it, any fix?

    0
    1. Yes! It has to do with the render priority of each transparent object:

       

      "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 adequate."

       

      Remember you have to set each transparent object's render priority! If that doesn't work, click the "See more about this shader" link above and submit an issue to the github. I'll help you sort it out :)

      0
  7. Ported it to godot 3, opengl 2 renderer

     

    shader_type spatial;

     

    render_mode cull_disabled, unshaded;

     

    uniform bool enable_fog = true; 
    uniform vec4 fog_color : hint_color;
    uniform vec4 noise_color : hint_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 return_dither(int index)
    {
    float value=0.0;
      if (index == 0)
      {
        value = 0.0;
      }
      if (index == 1)
      {
        value = 0.5;
      }
      if (index == 2)
      {
        value = 0.125;
      }
      if (index == 3)
      {
        value = 0.625;
      }
      if (index == 4)
      {
        value = 0.75;
      }
      if (index == 5)
      {
        value = 0.25;
      }
      if (index == 6)
      {
        value = 0.875;
      }
      if (index == 7)
      {
        value = 0.375;
      }
      if (index == 8)
      {
        value = 0.1875;
      }
      if (index == 9)
      {
        value = 0.6875;
      }
      if (index == 10)
      {
        value = 0.0625;
      }
      if (index == 11)
      {
        value = 0.5625;
      }
      if (index == 12)
      {
        value = 0.9375;
      }

     

        if (index == 13)
      {
        value = 0.4375;
      }

     

        if (index == 14)
      {
        value = 1.0;
      }
        if (index == 15)
      {
        value = 0.8125;
      }

     

      return value;
    }

     

    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 threshold = return_dither(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.rgb, noise_color.rgb, hashOld12(screen_coords));
        final_color = color_mix(twocolornoise, final_color, depth_mask_inv);
      }
      if (enable_fog){
        final_color = color_mix(fog_color.rgb, final_color.rgb, 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;
    }

    0
    1. shader_type spatial;
      render_mode cull_disabled, unshaded;
      uniform bool enable_fog = true; 
      uniform vec4 fog_color : hint_color;
      uniform vec4 noise_color : hint_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 return_dither(int index)
      {
      float value=0.0;
        if (index == 0)
        {
          value = 0.0;
        }
        if (index == 1)
        {
          value = 0.5;
        }
        if (index == 2)
        {
          value = 0.125;
        }
        if (index == 3)
        {
          value = 0.625;
        }
        if (index == 4)
        {
          value = 0.75;
        }
        if (index == 5)
        {
          value = 0.25;
        }
        if (index == 6)
        {
          value = 0.875;
        }
        if (index == 7)
        {
          value = 0.375;
        }
        if (index == 8)
        {
          value = 0.1875;
        }
        if (index == 9)
        {
          value = 0.6875;
        }
        if (index == 10)
        {
          value = 0.0625;
        }
        if (index == 11)
        {
          value = 0.5625;
        }
        if (index == 12)
        {
          value = 0.9375;
        }
          if (index == 13)
        {
          value = 0.4375;
        }
          if (index == 14)
        {
          value = 1.0;
        }
          if (index == 15)
        {
          value = 0.8125;
        }
        return value;
      }
      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 threshold = return_dither(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.rgb, noise_color.rgb, hashOld12(screen_coords));
          final_color = color_mix(twocolornoise, final_color, depth_mask_inv);
        }
        if (enable_fog){
          final_color = color_mix(fog_color.rgb, final_color.rgb, 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;
      }
      
      0