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:
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.envHDR 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.
<!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>