Fog and Blending

Combining Fog with Alpha Blending

Fog is applied in each object's own fragment shader, before blending, so transparent objects need care:

Fog also hides the far plane: with fog that reaches zero before far, objects never pop out at the clipping distance, the original reason games of the 1990s used it.

Glass panes and additive glows in fog: fogging alpha and glows toward the fog colour (left) against the right way (right)HTMLLive
<!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; font-size: 10px; }
  .names div { flex: 1; padding: 4px 2px; }
</style>
<canvas id="c" width="1200" height="380"></canvas>
<div class="names">
  <div>wrong: mix(fog, vec4(rgb, a), f); glow + fog colour</div>
  <div>right: vec4(mix(fog, rgb, f), a); glow * f</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, vLocal;
void main() { vec4 w = uModel * vec4(aPosition, 1.0); vPosition = w.xyz; vNormal = aNormal; vLocal = aPosition;
  gl_Position = uViewProjection * w; }`], [gl.FRAGMENT_SHADER, `#version 300 es
precision highp float;
in vec3 vPosition, vNormal, vLocal;
uniform vec4 uColor; uniform vec3 uEye, uFogColor; uniform bool uRight, uGlow;
out vec4 outColor;
void main() {
  float fog = exp(-pow(0.16 * distance(uEye, vPosition), 2.0));
  if (uGlow) {                                        // additive glow: a soft disc
    float g = max(0.0, 1.0 - length(vLocal.xy) * 2.0);
    vec3 glow = uColor.rgb * g * g;
    outColor = vec4(uRight ? glow * fog : glow + uFogColor * (1.0 - fog) * g, 1.0);
    return;
  }
  vec3 lit = uColor.rgb * (0.5 + 0.5 * max(dot(normalize(vNormal), normalize(vec3(-0.4, 0.8, 0.5))), 0.0));
  outColor = uRight ? vec4(mix(uFogColor, lit, fog), uColor.a)          // fog the colour, keep alpha
                    : mix(vec4(uFogColor, 1.0), vec4(lit, uColor.a), fog);   // alpha fogs toward 1
}`]]) {
  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);

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 opaque = [{ color: [0.72, 0.66, 0.58, 1], model: m4.model(0, -0.05, -6, 8, 0.1, 18) }];
for (let k = 0; k < 10; k++) opaque.push({ color: [...palette[k % 6], 1], model: m4.model(-1.2 + (k % 2) * 2.4, 0.7, 1 - k * 1.3, 0.6, 1.4, 0.9) });
// Glass doors at several depths (already far to near) and glowing lamps between the stacks.
const glass = [-9, -6, -3, 0].map(z => ({ color: [0.6, 0.8, 0.95, 0.3], model: m4.model(0, 0.8, z, 1.6, 1.6, 0.02) }));
const glows = [-10, -7, -4, -1].map((z, i) => ({ color: [[1, 0.6, 0.2], [0.3, 0.6, 1], [1, 0.4, 0.6], [0.5, 1, 0.4]][i], z }));
const eye = [0.4, 1.2, 3.4], fogColor = [0.93, 0.91, 0.87];
gl.uniform3fv(u('uEye'), eye); gl.uniform3fv(u('uFogColor'), fogColor);
gl.uniformMatrix4fv(u('uViewProjection'), false, m4.multiply(m4.perspective(45, 600 / 380, 0.1, 40), m4.lookAt(eye, [0, 0.8, -4])));
function draw(list) {
  for (const { color, model } of list) {
    gl.uniformMatrix4fv(u('uModel'), false, model); gl.uniform4fv(u('uColor'), color);
    gl.drawArrays(gl.TRIANGLES, 0, 36);
  }
}
function frame(ms) {
  for (let pane = 0; pane < 2; pane++) {
    gl.viewport(pane * 600, 0, 600, 380); gl.scissor(pane * 600 + 3, 0, 594, 380);
    gl.clearColor(...fogColor, 1);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    gl.uniform1i(u('uRight'), pane); gl.uniform1i(u('uGlow'), 0);
    gl.disable(gl.BLEND);
    draw(opaque);
    gl.enable(gl.BLEND); gl.depthMask(false);
    gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
    draw(glass);
    gl.blendFunc(gl.ONE, gl.ONE);                       // additive: no sorting needed
    gl.uniform1i(u('uGlow'), 1);
    glows.forEach(({ color, z }, i) => {
      gl.uniformMatrix4fv(u('uModel'), false, m4.model(Math.sin(ms * 0.001 + i) * 0.5, 1.6, z, 1.2, 1.2, 0.01));
      gl.uniform4f(u('uColor'), ...color, 1);
      gl.drawArrays(gl.TRIANGLES, 0, 6);                // just the front face of the cube
    });
    gl.depthMask(true);
  }
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>