Baking Fractional Brownian Motion into a texture

I’ve recently tried to reproduce this technique in order to add global volumetric fog to my three.js scene. Everything is working as expected when using FBM with a simplex noise function as described in the video. At the end of the tutorial though, right here, Simon is mentioning an optimization technique I’m really curios about.

Basically, as far as I understood it’s better to use fractional Brownian motion by sampling a noise texture in order to avoid doing some extra work in the fragment shader (snoise function), which actually makes sense. Unfortunately, I’m not an expert in GLSL shaders, so after trying for a while, I’ve come out with a result I’m pretty happy about, and now I’m wondering if this is a proper way to do it, or if I was just lucky.

Or maybe, even if my solution worked quite well, there’s even a better way to implement this. I’d really like to learn as much as possible about FBM and different ways an purposes to implement noise algorithms in shaders, so any help / advice will be much appreciated! And here is the code I’ve come out with.

JS override of three.js default fog shaders:

ShaderChunk.fog_pars_fragment = Config.Settings.bakedFog
  ? `#define USE_BAKED_FOG\n\n${parsFrag}` : parsFrag;

ShaderChunk.fog_pars_vertex = parsVert;
ShaderChunk.fog_fragment    = fogFrag;
ShaderChunk.fog_vertex      = fogVert;

“fog_pars_fragment” shader:

#ifdef USE_FOG
  #include ../fBm;
  uniform float fogTime;

  uniform vec3 fogColor;
  varying vec3 vWorldPosition;

  #ifdef FOG_EXP2
    uniform float fogDensity;

  #else
    uniform float fogNear;
    uniform float fogFar;

  #endif
#endif

“fog_fragment” shader:

#ifdef USE_FOG
  float noiseSample;
  const int FBM_OCTAVES = 6;
  const float fogHeight = 0.5;

  vec3 fogOrigin = cameraPosition;
  vec3 sampleCoord = vWorldPosition * 0.00025;

  float fogDepth = distance(vWorldPosition, fogOrigin);
  vec3 fogDirection = normalize(vWorldPosition - fogOrigin);

  #ifdef USE_BAKED_FOG
    sampleCoord += vec3(fogTime * 0.025, 0.0, fogTime * 0.025);

    noiseSample = fBmTex(
      sampleCoord + fBmTex(sampleCoord)
    );

  #else
    sampleCoord += vec3(0.0, 0.0, fogTime * 0.05);

    noiseSample = fBm(sampleCoord + fBm(
        sampleCoord, FBM_OCTAVES
      ), FBM_OCTAVES
    );
  #endif

  fogDepth *= mix(noiseSample * 0.5 + 0.5, 1.0, saturate((fogDepth - 5000.0) / 5000.0));
  fogDepth *= fogDepth;

  float fogFactor = fogHeight * exp(-fogOrigin.y * fogDensity) * (
    1.0 - exp(-fogDepth * fogDirection.y * fogDensity)
  );

  gl_FragColor.rgb = mix(
    gl_FragColor.rgb, fogColor, saturate(
      saturate(fogFactor / fogDirection.y)
    ) * 0.75
  );
#endif

And this is fBm.glsl with fBm & fBmTex functions, included in “fog_pars_fragment” shader:

// Fractional Brownian Motion by Inigo Quilez:
// https://www.iquilezles.org/www/articles/fbm/fbm.htm
// https://www.iquilezles.org/www/articles/warp/warp.htm

precision highp float;

#ifdef USE_BAKED_FOG
  uniform sampler2D noise; // simplex noise texture

#else
 #include snoise; // https://github.com/ashima/webgl-noise/blob/ff3b5d34eafb7ad97985e896235546ceaea5b2cf/src/noise3D.glsl
#endif

#ifndef USE_BAKED_FOG
  float fBm (vec3 point, int octaves) {
    float value = 0.0;
    float amplitude = 0.5;

    for (int o = 0; o < octaves; o++) {
      value += amplitude * snoise(point);
      point = point * 2.0 + 100.0;
      amplitude *= 0.5;
    }

    return value;
  }

#else
  // Is this is all for baked FBM ???
  float fBmTex (vec3 point) {
    return texture(noise, point.zx).y * 0.5;
  }
#endif

/cc