HDR and .env

HDR Environment Textures and the .env Format

Image-based lighting needs high dynamic range: a sun hundreds of times brighter than the sky beside it. HDRCubeTexture projects an equirectangular .hdr onto a cube and, with prefilterOnLoad, blurs a mip chain for rough reflections on every visit. Babylon 89,603 's .env format stores the result: a JSON manifest, the spherical harmonics, and each prefiltered mip face as an RGBD-encoded PNG or WebP (a divisor in alpha keeps HDR values in 8 bits). EnvironmentTextureTools writes one, here from Greg Zaal's CC0 Umhlanga Sunrise (Poly Haven 82,537 ), which Babylon's asset CDN hosts at 1K:

Converting an HDR photograph into BookNest's .env fileJavaScript
const url = 'https://assets.babylonjs.com/ibl/umhlanga_sunrise_1k.hdr';     // 1,577,804 bytes
const started = performance.now();
const hdr = new BABYLON.HDRCubeTexture(url, scene, 256, false, true, false, true); // prefilter
await new Promise((resolve) => hdr.onLoadObservable.addOnce(resolve));
console.log(`HDR loaded and prefiltered in ${Math.round(performance.now() - started)} ms`);
const env = await BABYLON.EnvironmentTextureTools.CreateEnvTextureAsync(hdr,
  { imageType: 'image/webp', imageQuality: 0.8 });
console.log(`.env: ${env.byteLength} bytes`);     // save it as booknest-sunrise.env
Output
HDR loaded and prefiltered in 1724 ms
.env: 188893 bytes

The .env is an eighth of the .hdr's size and needs no prefiltering on load. Babylon's IBL Texture Tool (babylonjs.com/tools/textures) converts by drag and drop. Check every environment's licence: Poly Haven's are CC0, Babylon's asset library CC BY 4.0.

An HDR sky prefiltered into a mip chain and packed into Babylon's .env format, with the sizes comparedHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
  canvas { width: 100%; max-width: 600px; height: 330px; display: block; touch-action: none; outline: none; }
  #info { max-width: 600px; margin: 0; padding: 4px 8px; font-family: monospace; white-space: pre-wrap; }
</style>
<canvas id="view"></canvas>
<pre id="info">Prefiltering…</pre>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script src="https://cdn.jsdelivr.net/npm/babylonjs-gui@9.28.0/babylon.gui.min.js"></script>
<script>
// A procedural HDR sunrise sky, computed texel by texel into six float faces. It stands in for
// an .hdr or .env download, which this sandboxed preview cannot fetch.
function createSkyTexture(scene, size = 128) {
  const sunDir = new BABYLON.Vector3(0.45, 0.28, 0.85).normalize();
  const faceDirection = [                           // (s, t) in -1..1 to a direction, per face
    (s, t) => [1, -t, -s], (s, t) => [-1, -t, s], (s, t) => [s, 1, t],
    (s, t) => [s, -1, -t], (s, t) => [s, -t, 1], (s, t) => [-s, -t, -1]];
  const faces = faceDirection.map((toDirection) => {
    const data = new Float32Array(size * size * 4);
    for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
      const [dx, dy, dz] = toDirection((2 * x + 1) / size - 1, (2 * y + 1) / size - 1);
      const len = Math.hypot(dx, dy, dz), up = dy / len;
      const k = Math.sqrt(Math.max(up, 0));            // 0 at the horizon, 1 straight up
      const ground = Math.min(Math.max(-up * 8, 0), 1);   // fade to the ground below the horizon
      const r = (1 - 0.62 * k) * (1 - ground) + 0.36 * ground;    // peach horizon, blue zenith,
      const g = (0.82 - 0.22 * k) * (1 - ground) + 0.3 * ground;  // warm brown ground
      const b = (0.68 + 0.3 * k) * (1 - ground) + 0.24 * ground;
      const cosSun = Math.max((dx * sunDir.x + dy * sunDir.y + dz * sunDir.z) / len, 0);
      const sun = 30 * Math.pow(cosSun, 900) + 0.35 * Math.pow(cosSun, 24);   // disc + glow, HDR
      const i = (y * size + x) * 4;
      data[i] = r + sun; data[i + 1] = g + sun * 0.85; data[i + 2] = b + sun * 0.6; data[i + 3] = 1;
    }
    return data;
  });
  const sky = new BABYLON.RawCubeTexture(scene, faces, size, BABYLON.Constants.TEXTUREFORMAT_RGBA,
    BABYLON.Constants.TEXTURETYPE_FLOAT, true, false, BABYLON.Constants.TEXTURE_TRILINEAR_SAMPLINGMODE);
  sky.gammaSpace = false;                            // linear HDR values, like a loaded .hdr
  return sky;
}

/*
  The book downloads an equirectangular .hdr (umhlanga_sunrise_1k.hdr) into an HDRCubeTexture with
  prefilterOnLoad. This sandbox cannot fetch files, so the HDR sky is computed as float cube faces
  (values far above 1 in the sun) and prefiltered with HDRFiltering, the same step prefilterOnLoad runs.
*/
const canvas = document.getElementById('view');
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2 + 0.3, 1.4, 6, new BABYLON.Vector3(0, 0.4, 0), scene);
camera.attachControl(canvas, true);
const ui = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI('ui');

const hdr = createSkyTexture(scene, 128);
scene.environmentTexture = hdr;
scene.createDefaultSkybox(hdr, true, 100, 0);
const pbr = (hex, roughness, metallic) => Object.assign(new BABYLON.PBRMaterial(hex + roughness, scene),
  { albedoColor: BABYLON.Color3.FromHexString(hex), roughness, metallic });
[['#ffffff', 0.05, 1], ['#e09a10', 0.35, 1], ['#3f7d3a', 0.6, 0]].forEach(([hex, roughness, metallic], i) => {
  const ball = BABYLON.MeshBuilder.CreateSphere('ball' + i, { diameter: 1.2, segments: 40 }, scene);
  ball.position.set((i - 1) * 1.5, 0.6, 0);
  ball.material = pbr(hex, roughness, metallic);
});

(async () => {
  const started = performance.now();
  await new BABYLON.HDRFiltering(engine).prefilter(hdr);   // blur a mip chain for rough reflections
  const prefilterMs = Math.round(performance.now() - started);
  // EnvironmentTextureTools packs the prefiltered mips as RGBD images plus the harmonics
  const env = await BABYLON.EnvironmentTextureTools.CreateEnvTextureAsync(hdr, { imageType: 'image/png' });
  const floatBytes = 6 * 128 * 128 * 4 * 4 * 4 / 3;       // RGBA float32 faces plus their mips
  document.getElementById('info').textContent =
    `prefiltered in ${prefilterMs} ms\nfloat cube + mips ≈ ${Math.round(floatBytes / 1024)} KB` +
    `   →   .env ${Math.round(env.byteLength / 1024)} KB (${(floatBytes / env.byteLength).toFixed(1)}x smaller)`;
  const bar = (name, kb, hex, top) => {             // a GUI bar chart in the top-left corner
    const rect = new BABYLON.GUI.Rectangle(name);
    Object.assign(rect, { width: `${Math.max(kb / 2048 * 260, 4)}px`, height: '22px', left: '12px', top,
      background: hex, thickness: 0,
      horizontalAlignment: BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT,
      verticalAlignment: BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP });
    ui.addControl(rect);
    const label = new BABYLON.GUI.TextBlock('', `${name}: ${Math.round(kb)} KB`);
    Object.assign(label, { color: '#222', fontSize: 12, resizeToFit: true, left: `${Math.max(kb / 2048 * 260, 4) + 20}px`,
      top, height: '22px', horizontalAlignment: BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT,
      verticalAlignment: BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP });
    ui.addControl(label);
  };
  bar('float HDR cube', floatBytes / 1024, '#b5452f', '12px');
  bar('.env (RGBD PNG mips)', env.byteLength / 1024, '#3f7d3a', '40px');
})().catch((error) => { document.getElementById('info').textContent = 'Could not build the .env: ' + error.message; });
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>