Linear and Exponential Fog

Fog blends a fragment's color toward a fog color by a fog factor f, 1 for a clear view and 0 for complete fog: mix(fogColor, color, f). Three classic curves compute f from the distance d between the eye and the fragment: linear, f = (end - d) / (end - start), clamped, which is clear up to start and opaque from end; exponential, f = e^-(density x d), physically right for uniform haze; and exponential squared, f = e^-(density x d)^2, which keeps the foreground clear longer and then closes in. The listing evaluates all three with show():

Linear, exponential and exponential-squared fog factors on the GPUJavaScript
for (const d of ['1.0', '3.0', '5.0', '8.0', '12.0']) {       // distances from the eye
  show(`float d = ${d}; result = vec4(d, clamp((10.0 - d) / (10.0 - 2.0), 0.0, 1.0),
    exp(-0.2 * d), exp(-pow(0.2 * d, 2.0)));`);
}
Output
1, 1, 0.818731, 0.960789
3, 0.875, 0.548812, 0.697676
5, 0.625, 0.367879, 0.367879
8, 0.25, 0.201897, 0.077305
12, 0, 0.090718, 0.003151

The columns are d, linear (from 2 to 10), exponential and exponential squared (density 0.2). Plain exponential fog already hides 18% at distance 1, which dulls the whole scene; the squared curve keeps 96% there and hides the far distance almost completely, which is why GLH.shelfFS uses it. Linear fog is easiest to art-direct and reaches exactly zero, so objects beyond end can be culled. Always set the fog color to the background's (here the canvas's clear color), or distant objects fade into a color that does not match the sky behind them.

The same receding shelves under linear, exponential and exponential-squared fog, with the three curvesHTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  canvas { display: block; width: 100%; max-width: 600px; }
  .names { display: flex; max-width: 600px; text-align: center; font-family: monospace; }
  .names div { flex: 1; padding: 4px 2px; }
</style>
<canvas id="c" width="1200" height="400"></canvas>
<div class="names"><div>linear, 2 to 10</div><div>exp(-0.2 d)</div><div>exp(-(0.2 d)&sup2;)</div></div>
<script>
const m4 = {
  perspective(fovy, aspect, near, far) {
    const f = 1 / Math.tan(fovy * Math.PI / 360), d = near - far;
    return [f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, (far + near) / d, -1, 0, 0, 2 * far * near / d, 0];
  },
  lookAt(eye, target, up = [0, 1, 0]) {
    const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
    const unit = (v) => v.map((c) => c / Math.hypot(...v));
    const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
    const z = unit(eye.map((c, i) => c - target[i])), x = unit(cross(up, z)), y = cross(z, x);
    return [x[0], y[0], z[0], 0, x[1], y[1], z[1], 0, x[2], y[2], z[2], 0, -dot(x, eye), -dot(y, eye), -dot(z, eye), 1];
  },
  multiply(a, b) {
    const out = new Array(16).fill(0);
    for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++) for (let k = 0; k < 4; k++) out[c * 4 + r] += a[k * 4 + r] * b[c * 4 + k];
    return out;
  },
  model: (x, y, z, sx, sy, sz) => [sx, 0, 0, 0, 0, sy, 0, 0, 0, 0, sz, 0, x, y, z, 1],
};
function cube() {
  const faces = [[[0,0,1],[1,0,0],[0,1,0]], [[0,0,-1],[-1,0,0],[0,1,0]], [[1,0,0],[0,0,-1],[0,1,0]],
                 [[-1,0,0],[0,0,1],[0,1,0]], [[0,1,0],[1,0,0],[0,0,-1]], [[0,-1,0],[1,0,0],[0,0,1]]];
  const out = [];
  for (const [n, u, v] of faces) for (const [a, b] of [[-1,-1],[1,-1],[1,1],[-1,-1],[1,1],[-1,1]])
    out.push(...[0, 1, 2].map(i => (n[i] + a * u[i] + b * v[i]) / 2), ...n);
  return new Float32Array(out);
}
const gl = document.getElementById('c').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
layout(location = 0) in vec3 aPosition; layout(location = 1) in vec3 aNormal;
uniform mat4 uViewProjection, uModel; out vec3 vPosition, vNormal;
void main() { vec4 w = uModel * vec4(aPosition, 1.0); vPosition = w.xyz; vNormal = aNormal; gl_Position = uViewProjection * w; }`],
  [gl.FRAGMENT_SHADER, `#version 300 es
precision highp float;
in vec3 vPosition, vNormal;
uniform vec3 uColor, uEye, uFogColor; uniform int uFogMode; uniform bool uFlat;
out vec4 outColor;
void main() {
  vec3 n = normalize(vNormal);
  vec3 color = uColor * (0.45 + 0.55 * max(dot(n, normalize(vec3(-0.4, 0.8, 0.5))), 0.0));
  float d = distance(uEye, vPosition);
  float f;                                             // 1 = clear view, 0 = full fog
  if (uFogMode == 0) f = clamp((10.0 - d) / (10.0 - 2.0), 0.0, 1.0);   // linear
  else if (uFogMode == 1) f = exp(-0.2 * d);                           // exponential
  else f = exp(-pow(0.2 * d, 2.0));                                    // exponential squared
  outColor = vec4(uFlat ? uColor : mix(uFogColor, color, f), 1.0);
}`]]) {
  const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);
  if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
  gl.attachShader(program, s);
}
gl.linkProgram(program); gl.useProgram(program);
const u = (n) => gl.getUniformLocation(program, n);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, cube(), gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0); gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12); gl.enableVertexAttribArray(1);
gl.enable(gl.DEPTH_TEST); gl.enable(gl.CULL_FACE); gl.enable(gl.SCISSOR_TEST);

// Two rows of book stacks running away from the camera along -z.
const palette = [[0.12, 0.37, 0.55], [0.36, 0.25, 0.6], [0.88, 0.6, 0.06], [0.25, 0.49, 0.23], [0.71, 0.27, 0.18], [0.16, 0.62, 0.56]];
const items = [{ color: [0.72, 0.66, 0.58], model: m4.model(0, -0.05, -8, 6, 0.1, 20) }];
for (let k = 0; k < 14; k++) for (const side of [-1, 1])
  items.push({ color: palette[(k + (side > 0 ? 3 : 0)) % 6], model: m4.model(side * 1.1, 0.6, 1 - k * 1.2, 0.5, 1.2 + (k % 3) * 0.2, 0.8) });
const eye = [0, 1.2, 3];
const fogColor = [0.93, 0.91, 0.87];
gl.uniform3fv(u('uEye'), eye);
gl.uniform3fv(u('uFogColor'), fogColor);                  // match the background
gl.uniformMatrix4fv(u('uViewProjection'), false, m4.multiply(m4.perspective(50, 400 / 330, 0.1, 40), m4.lookAt(eye, [0, 0.7, -4])));
const curve = (mode, d) => mode === 0 ? Math.min(1, Math.max(0, (10 - d) / 8)) : mode === 1 ? Math.exp(-0.2 * d) : Math.exp(-Math.pow(0.2 * d, 2));
for (let mode = 0; mode < 3; mode++) {
  gl.viewport(mode * 400, 70, 400, 330); gl.scissor(mode * 400 + 3, 70, 394, 330);
  gl.clearColor(...fogColor, 1);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.uniform1i(u('uFogMode'), mode); gl.uniform1i(u('uFlat'), 0);
  for (const { color, model } of items) {
    gl.uniformMatrix4fv(u('uModel'), false, model);
    gl.uniform3fv(u('uColor'), color);
    gl.drawArrays(gl.TRIANGLES, 0, 36);
  }
  // Below each view: the fog factor f from d = 0 (left) to 15 (right), as a bar chart of scissored clears.
  gl.uniform1i(u('uFlat'), 1);
  for (let i = 0; i < 60; i++) {
    const f = curve(mode, i / 4);
    gl.scissor(mode * 400 + 20 + i * 6, 6, 5, Math.max(1, f * 56));
    gl.clearColor(0.12 + 0.7 * (1 - f), 0.37 + 0.5 * (1 - f), 0.55 + 0.3 * (1 - f), 1);
    gl.clear(gl.COLOR_BUFFER_BIT);
  }
}
</script>