Skip to content
Dmytro Toronchenko
Go back

Exponential Fog in a GPU Shader

Fog is one of the cheapest ways to add depth and atmosphere to a scene. In this post we’ll break down exponential fog: where the formula comes from, how to compute it in a shader, and how to blend the result with the object color. The shaders are written in Slang, but the logic translates directly to any shading language — GLSL, HLSL, MSL.

Here’s the scene before fog:

Cubes without fog

Full shader code

Vertex shader

struct Input
{
    float3 pos : POSITION;
    float2 uv : TEXCOORD;
}

struct Output
{
    float4 sv_position : SV_Position;
    float2 uv : TEXCOORD;
    float depth : TEXCOORD1;
}

struct Transform
{
    column_major float4x4 model;
    column_major float4x4 view;
    column_major float4x4 proj;
}

ParameterBlock<Transform> uTransform;

[shader("vertex")]
Output main(Input input)
{
    float4 worldPos = mul(uTransform.model, float4(input.pos, 1.0f));
    float4 viewPos  = mul(uTransform.view, worldPos);
    Output output;
    output.sv_position = mul(uTransform.proj, viewPos);
    output.uv          = input.uv;
    output.depth       = length(viewPos.xyz);
    return output;
}

Fragment shader

struct Input
{
    float4 sv_position : SV_Position;
    float2 uv : TEXCOORD;
    float depth : TEXCOORD1;
}

[shader("fragment")]
float4 main(Input input) : SV_Target
{
    float3 objectColor = float3(0.2f, 1.0f, 0.1f);
    float3 fogColor    = float3(0.1f, 0.1f, 0.15f);
    float  density     = 0.5f;

    float fogFactor = exp(-density * input.depth);
    fogFactor = clamp(fogFactor, 0.0f, 1.0f);

    float3 finalColor = lerp(fogColor, objectColor, fogFactor);

    return float4(finalColor, 1.0f);
}

Breaking it down

Vertex shader input

struct Input
{
    float3 pos : POSITION;
    float2 uv  : TEXCOORD;
}

Standard vertex attributes: position in local space and UV coordinates. The UVs aren’t used for anything meaningful here — they’re present because vertex attributes are built from shader reflection and the cube vertex buffer already includes texture coordinates.

Vertex shader output

struct Output
{
    float4 sv_position : SV_Position;
    float2 uv          : TEXCOORD;
    float  depth       : TEXCOORD1;
}

Everything standard, plus one key field — depth. It stores the distance from the camera to this point in space and gets passed to the fragment shader as an interpolated value.

Uniform buffer

struct Transform
{
    column_major float4x4 model;
    column_major float4x4 view;
    column_major float4x4 proj;
}

ParameterBlock<Transform> uTransform;

A plain MVP uniform buffer: model matrix, view matrix, projection matrix.

Vertex shader — computing depth

float4 worldPos = mul(uTransform.model, float4(input.pos, 1.0f));
float4 viewPos  = mul(uTransform.view, worldPos);

output.sv_position = mul(uTransform.proj, viewPos);
output.uv          = input.uv;
output.depth       = length(viewPos.xyz);

We transform the vertex position to world space, then to view space. The clip-space position comes from multiplying by the projection matrix — the usual path.

depth is computed as length(viewPos.xyz) — the Euclidean distance from the camera to the vertex in view space. The farther away the vertex, the larger depth and the stronger the fog.

View space is the right place to measure this. In clip/NDC space the position is already distorted by the projection, so distances are no longer meaningful.

Fragment shader input

struct Input
{
    float4 sv_position : SV_Position;
    float2 uv          : TEXCOORD;
    float  depth       : TEXCOORD1;
}

The GPU interpolates depth across the triangle, so the fragment shader receives a smoothly varying distance for every pixel — not just at the vertices.

Fragment shader — the fog formula

float3 objectColor = float3(0.2f, 1.0f, 0.1f);
float3 fogColor    = float3(0.1f, 0.1f, 0.15f);
float  density     = 0.5f;

objectColor is the surface color, fogColor is the fog color (dark blueish), and density controls how thick the fog is. The higher the density, the faster objects disappear into it.

float fogFactor = exp(-density * input.depth);
fogFactor = clamp(fogFactor, 0.0f, 1.0f);

This is the exponential fog formula:

fogFactor = e^(-density * depth)

At depth = 0 (object right in front of the camera): e^0 = 1 — fog has no effect.
At large depth: e^(-large) → 0 — the object is completely hidden by fog.

The clamp keeps the value in [0, 1]. It’s technically redundant since exp with positive arguments never leaves that range, but it’s a harmless safety net.

float3 finalColor = lerp(fogColor, objectColor, fogFactor);

Linear interpolation between the fog color and the object color, driven by fogFactor. When fogFactor = 1 you get the pure object color. When fogFactor = 0 you get the pure fog color.

return float4(finalColor, 1.0f);

Return the final color with full opacity.

Result

Cubes with fog

Distant objects fade smoothly into the dark fog. The effect is cheap: one exp and one lerp per fragment, no extra geometry or textures required.

Tweaking the parameters

ParameterEffect
Higher densityThicker fog, objects disappear closer to the camera
Lower densityLighter fog, objects visible from farther away
Lighter fogColorLooks like haze or overcast sky
Darker fogColorDark atmosphere, like night fog

Share this post on:

Next Post
Command List Pool: Recycling Vulkan Command Buffers Without Stalling