Exponential fog
One post pass that reads the depth buffer and fades distant pixels into a fog color.
- 01GoalAtmosphere
- 02TechniqueDistance & height fog
- 03ExecutionExponential fog
Parameters
Copy for your platform
- GLSLAdapt
- GodotCopy
Node chain
Built step by step, in GLSL style, with its node names.
Built step by step, in Godot style, with its node names.
No Unreal Engine 5 version of this card yet, so the nodes show the GLSL names.
No Unity version of this card yet, so the nodes show the GLSL names.
No Blender version of this card yet, so the nodes show the GLSL names.
textureTexture2Dtexturetexturetexture DepthScreenUV01 Read the scene
expExpexpexpexp In02 Light that gets through
1.0 - xOneMinus1.0 - x1.0 - x1.0 - x In03 Fog amount
mixMixmixmixmix In04 Blend toward the fog
How it works
The scene is drawn first, without fog, into a color texture and a depth bufferAn image the size of the framebuffer that holds, for each pixel, how far away the nearest surface drawn so far is. The depth test uses it to hide what is behind. Depth buffer An image the size of the framebuffer that holds, for each pixel, how far away the nearest surface drawn so far is. The depth test uses it to hide what is behind.
- Read the scene.
texture(t_screen, i.uv)fetches the pixel’s finished color, in linear light. - Measure the distance.
sceneDepth(i.uv)returns how far the surface is from the camera plane, in world units; the background gets the far plane’s distance. Subtracting the start distance and clamping at zero leaves a clear zone around the camera,travelled, the stretch of fog the light actually crosses. - Light that gets through.
exp(-p_density * travelled)is the transmittanceThe fraction of light that gets through a medium such as fog, from 1 (all of it) to 0 (none). In an even medium it falls exponentially with the distance crossed.TechniqueTransmittance
The fraction of light that gets through a medium such as fog, from 1 (all of it) to 0 (none). In an even medium it falls exponentially with the distance crossed.
T = exp(−density · distance)Sources: 1 in front of the camera, falling toward 0 with distance. Each unit of fog removes the same fraction of what is left, so twice the distance squares what survives instead of halving it. That is why fog closes in smoothly and never reaches a hard wall. - Fog amount.
1.0 - transmittanceis the share of the pixel the fog replaces: black near the camera, white far away. - Blend toward the fog.
mix(scene, p_fogColor, fog)keeps what survived of the surface and fills the rest with the fog’s own color, the light the fog scatters toward the camera. A grey-blue near the sky’s horizon color makes distant objects melt into the background.
The density is per world unit, so it depends on the scene’s scale. A rough guide: 1 / density past the start
distance, a little over a third of the surface’s light still gets through; at three times that, about 5%.
The depth is measured along the camera’s view axis, not along each pixel’s ray. Near the edges of a wide view the ray is longer than the depth, so the fog there is a little thinner than true distance would give, and it shifts slightly as the camera turns. Dividing the depth by the cosine of the angle between the ray and the view axis gives the true distance; that needs the camera’s field of view, so this card leaves it out.
Make it in GLSL
The shader is one full-screen pass, but it reads two textures your renderer has to make first. Render the scene into a framebuffer with a color texture (t_screen) and a texture that holds each pixel's view depth in its w channel (u_sceneNormalDepth), then draw a full-screen triangle with this shader to the canvas.
The scene first renders into an offscreen framebuffer with two color attachments: the color, and a second one where
the scene’s fragment shader writes its view depth, -(view * vec4(world, 1.0)).z, into w. Both are RGBA16F
textures. Then:
- Bind the default framebuffer and draw one triangle that covers the screen, passing a
v_uvfrom 0 to 1. - Bind the color texture to
t_screenand the depth one tou_sceneNormalDepth, both with linear filtering and clamp to edge. - Upload
p_density,p_startandp_fogColor(decoded from sRGB to linear) and draw.
If your renderer already keeps a depth texture, drop u_sceneNormalDepth and rewrite sceneDepth() to sample it and
convert window depth to view distance. Everything else in the shader stays as it is.
Make it in Godot
Runs as shipped on the Forward+ renderer. For Mobile, delete the u_normalRoughness uniform line; this card never reads it, and Mobile refuses that hint. The Compatibility renderer stores depth from -1 to 1, so there sceneDepth() must also build the depth coordinate as depth * 2.0 - 1.0 before the inverse projection.
- Add a MeshInstance3D as a child of your Camera3D, give it a QuadMesh with Size 2 × 2, and set its Position to (0, 0, -1) so it sits in front of the near plane.
- Give it a new ShaderMaterial and paste the shader. Its
vertex()writes the quad’s corners straight to the screen’s corners; its position only keeps it from being culled. - Set density, start and fogColor in the material. The shader reads Godot’s depth texture and turns it into a distance with the inverse projection matrix, so start and density are in your scene’s meters.
In VisualShader, the same graph starts from a LinearSceneDepth node, which gives the same distance from the camera plane, and a Texture2D node with its source set to Screen. Chain them through Subtract, Max, Exp, OneMinus and Mix as in the steps above, and write the result to Albedo on an unshaded material.
No Unreal Engine 5 tutorial for this card yet.
No Unity tutorial for this card yet.
No Blender tutorial for this card yet.
Code
#version 300 es// Distance & height fog → Exponential fog, web GLSL (GLSL ES 3.00). From Creative Center, MIT license.precision highp float;uniform float u_time;uniform vec2 u_resolution; // this pass's output size in pixelsuniform float p_density; // Density: 0.12 (0.0 to 0.5)uniform float p_start; // Start distance: 2.0 (0.0 to 20.0)uniform vec3 p_fogColor; // Fog color: vec3(0.78, 0.81, 0.86), sRGB; upload it decoded to linearuniform sampler2D t_screen; // the scene's color: linear HDR (RGBA16F), linear filter, clamp to edgeuniform sampler2D u_sceneNormalDepth; // the scene's world normal in xyz (0 where nothing was drawn) and view depth in win vec2 v_uv;in vec3 v_position;in vec3 v_normal;in vec3 v_view;out vec4 fragColor;struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};// The scene's depth at uv: distance from the camera plane in world units; the far plane's where nothing was drawnfloat sceneDepth(vec2 uv) { return texture(u_sceneNormalDepth, uv).w;}vec3 u_toSrgb(vec3 c) { return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));}// sceneDepth() is the distance from the camera plane, not along the pixel's ray, so the fog doesn't thicken toward// the corners of the screen the way true distance would; the card explains when that matters.vec4 effect(Inputs i) { vec3 scene = texture(t_screen, i.uv).rgb; float depth = sceneDepth(i.uv); float travelled = max(depth - p_start, 0.0); // Beer–Lambert: each unit of fog lets the same fraction of the light through, so what survives falls exponentially float transmittance = exp(-p_density * travelled); float fog = 1.0 - transmittance; vec3 color = mix(scene, p_fogColor, fog); return vec4(color, 1.0);}void main() { Inputs i; i.uv = v_uv; i.position = v_position; // Interpolation shortens the vectors between vertices i.normal = normalize(v_normal); i.view = normalize(v_view); i.time = u_time; i.fragCoord = gl_FragCoord.xy; i.resolution = u_resolution; vec4 frag_out = effect(i); // The output is sRGB: clamped and encoded once, here, with no tone mapping; opaque, so alpha is 1 fragColor = vec4(u_toSrgb(clamp(frag_out.rgb, 0.0, 1.0)), 1.0);}// Distance & height fog → Exponential fog, Godot 4. From Creative Center, MIT license.shader_type spatial;// A full-screen quad over the finished frame: no lighting, no culling, never hidden by depthrender_mode unshaded, cull_disabled, depth_test_disabled;uniform float density : hint_range(0.0, 0.5, 0.005) = 0.12;uniform float start : hint_range(0.0, 20.0, 0.1) = 2.0;uniform vec3 fogColor : source_color = vec3(0.78, 0.81, 0.86);uniform sampler2D t_screen : hint_screen_texture, filter_linear, repeat_disable;uniform sampler2D u_depth : hint_depth_texture, filter_nearest, repeat_disable;// Normals come from Forward+ only (the Mobile and Compatibility renderers don't keep them)uniform sampler2D u_normalRoughness : hint_normal_roughness_texture, filter_nearest, repeat_disable;struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};// vertex() hands its matrices to sceneDepth() and sceneNormal()varying flat mat4 v_invProjection;// The scene's depth at uv: distance from the camera plane in world units; the far plane's where nothing was drawnfloat sceneDepth(vec2 uv) { // The effect's uv starts at the bottom-left; the screen's at the top-left vec2 screen = vec2(uv.x, 1.0 - uv.y); // Forward+ stores reversed-Z depth: undo the projection to get the view-space point vec4 view = v_invProjection * vec4(screen * 2.0 - 1.0, texture(u_depth, screen).r, 1.0); return -view.z / view.w;}// Godot's textures start at the top-left and the effect's uv at the bottom-left: each sample flips v backvec2 u_flipUv(vec2 uv) { return vec2(uv.x, 1.0 - uv.y);}// sceneDepth() is the distance from the camera plane, not along the pixel's ray, so the fog doesn't thicken toward// the corners of the screen the way true distance would; the card explains when that matters.vec4 effect(Inputs i) { vec3 scene = texture(t_screen, u_flipUv(i.uv)).rgb; float depth = sceneDepth(i.uv); float travelled = max(depth - start, 0.0); // Beer–Lambert: each unit of fog lets the same fraction of the light through, so what survives falls exponentially float transmittance = exp(-density * travelled); float fog = 1.0 - transmittance; vec3 color = mix(scene, fogColor, fog); return vec4(color, 1.0);}void vertex() { // Put the shader on a MeshInstance3D with a 2x2 QuadMesh in front of the camera; it covers the screen wherever it is POSITION = vec4(VERTEX.xy, 1.0, 1.0); v_invProjection = INV_PROJECTION_MATRIX;}void fragment() { Inputs i; // SCREEN_UV starts at the top-left; the effect expects GLSL's bottom-left i.uv = vec2(SCREEN_UV.x, 1.0 - SCREEN_UV.y); // A flat quad facing the viewer, the same as the site's 2D preview i.position = vec3(i.uv * 2.0 - 1.0, 0.0); i.normal = vec3(0.0, 0.0, 1.0); i.view = vec3(0.0, 0.0, 1.0); i.time = TIME; // FRAGCOORD's origin is the top-left too i.resolution = VIEWPORT_SIZE; i.fragCoord = vec2(FRAGCOORD.x, i.resolution.y - FRAGCOORD.y); vec4 frag_out = effect(i); ALBEDO = frag_out.rgb;}No Unreal Engine 5 code for this card yet.
No Unity code for this card yet.
No Blender code for this card yet.
Further reading
- Better fog (opens in a new tab)Article
- Beer–Lambert law (opens in a new tab)Reference
- glFog (opens in a new tab)Reference
- exp (opens in a new tab)Reference
- mix (opens in a new tab)Reference
- Depth testing (opens in a new tab)Article