SSRRenderingPipeline (since 5.51) marches each reflective pixel's reflected ray through the depth buffer and blends in the on-screen color it hits, blurred by roughness. Reflectivity comes from StandardMaterial's specular color (white by default: set it to black on matte surfaces) or PBR's metallic; what the camera cannot see, it cannot reflect. IblShadowsRenderPipeline (since 7.22) casts shadows from the environment: it voxelizes the casters you register, traces a few directions toward the image's brightest parts per frame and accumulates the noisy result:
BookNestStore.useEnvironment(scene); // IBL shadows read environmentTexture
const ssr = new BABYLON.SSRRenderingPipeline('ssr', scene, [camera], true); // geometry buffer
ssr.strength = 0.8; // how much reflection to add
const ibl = new BABYLON.IblShadowsRenderPipeline('iblShadows', scene,
{ resolutionExp: 6, sampleDirections: 2 }, [camera]); // a 64-cubed voxel grid
ibl.addShadowCastingMesh(scene.meshes.filter((mesh) => mesh.name.startsWith('book')));
ibl.addShadowReceivingMaterial(); // no argument: every material
ibl.updateSceneBounds(); // fit the grid around the casters
const started = performance.now();
ibl.onVoxelizationCompleteObservable.addOnce(() =>
console.log(`voxelized in ${Math.round(performance.now() - started)} ms`));
ibl.updateVoxelization(); // redo whenever a caster movesvoxelized in 416 ms
Both need WebGL2 or WebGPU; SSR's true makes it share the IBL shadows' geometry buffer. Voxelizing takes hundreds of milliseconds, so IBL shadows suit static furniture; moving books keep Lights and Shadows's shadow maps.
<!doctype html>
<style>
body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
canvas { width: 100%; max-width: 600px; height: 320px; display: block; touch-action: none; outline: none; }
#bar { max-width: 600px; display: flex; gap: 12px; align-items: center; padding: 6px 8px; } #info { font-family: monospace; }
</style>
<canvas id="view"></canvas>
<div id="bar"><button id="ibl">Add IBL shadows</button><span id="info"></span></div>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script>
// ---- BookNest's bookstore: the helpers the book keeps in bookstore.js ----
const ROOM = { width: 8, depth: 6, height: 3, wall: 0.15 };
const BOOKS = [
{ id: 1, title: 'The Quiet Harbor', author: 'Mara Ellison', color: '#1f5f8b', pages: 312, price: 14.99, rating: 4.6, inStock: true },
{ id: 2, title: 'Patterns of the Deep Web', author: 'Tomas Reyes', color: '#5b3f99', pages: 428, price: 32.5, rating: 4.4, inStock: true },
{ id: 3, title: 'Salt and Saffron', author: 'Priya Nair', color: '#e09a10', pages: 256, price: 24, rating: 4.8, inStock: false },
{ id: 4, title: 'Small Steps to Big Summits', author: 'Dana Whitfield', color: '#3f7d3a', pages: 198, price: 18.5, rating: 4.3, inStock: true },
{ id: 5, title: "The Clockmaker's Paradox", author: 'Julian Voss', color: '#b5452f', pages: 344, price: 16.75, rating: 4.7, inStock: true },
{ id: 6, title: 'Gardens in Glass', author: 'Lena Hart', color: '#2a9d8f', pages: 176, price: 21.3, rating: 4.5, inStock: true },
];
function createScene(engine) {
const scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#cfd8dcff');
const sky = new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.4), scene);
sky.groundColor = new BABYLON.Color3(0.35, 0.32, 0.3);
return scene;
}
function paint(scene, name, hex) { // a matte color
const material = new BABYLON.StandardMaterial(name, scene);
material.diffuseColor = BABYLON.Color3.FromHexString(hex);
material.specularColor = BABYLON.Color3.Black();
return material;
}
function createRoom(scene) { // floor and three walls, front open
const room = new BABYLON.TransformNode('room', scene);
const floor = BABYLON.MeshBuilder.CreateGround('floor', { width: ROOM.width, height: ROOM.depth }, scene);
floor.material = paint(scene, 'oak', '#b08d6a');
floor.parent = room;
const plaster = paint(scene, 'plaster', '#fff3e0');
for (const [name, width, x, z] of [['backWall', 8, 0, 3], ['leftWall', 6, -4, 0], ['rightWall', 6, 4, 0]]) {
const wall = BABYLON.MeshBuilder.CreateBox(name, { width, height: ROOM.height, depth: ROOM.wall }, scene);
wall.position.set(x, ROOM.height / 2, z);
wall.rotation.y = x ? Math.PI / 2 : 0;
wall.material = plaster;
wall.parent = room;
}
return room;
}
function createBookcase(scene, name, x) { // 8 boxes merged into 1 mesh
const [w, h, d, t] = [3, 2.2, 0.35, 0.04];
const parts = [[t, h, d, -w / 2, h / 2, 0], [t, h, d, w / 2, h / 2, 0], [w, h, t, 0, h / 2, d / 2 - t / 2]];
for (let i = 0; i < 5; i++) parts.push([w, t, d, 0, 0.1 + i * 0.5, 0]);
const bookcase = BABYLON.Mesh.MergeMeshes(parts.map(([width, height, depth, px, py, pz]) => {
const part = BABYLON.MeshBuilder.CreateBox('part', { width, height, depth }, scene);
part.position.set(px, py, pz);
return part;
}), true);
bookcase.name = name;
bookcase.material = paint(scene, 'walnut', '#6d4c33');
bookcase.position.set(x, 0, ROOM.depth / 2 - ROOM.wall / 2 - d / 2); // against the back wall
return bookcase;
}
function drawCover(scene, book) { // cover art painted on a canvas
const tex = new BABYLON.DynamicTexture('cover' + book.id, { width: 256, height: 384 }, scene);
const c = tex.getContext();
c.fillStyle = book.color; c.fillRect(0, 0, 256, 384);
c.fillStyle = 'rgba(0,0,0,0.4)'; c.fillRect(0, 0, 18, 384); // the dark left edge
c.strokeStyle = '#fff8e1'; c.lineWidth = 3; c.strokeRect(34, 24, 202, 336);
c.fillStyle = '#fff8e1'; c.textAlign = 'center'; c.font = 'bold 30px Georgia';
const lines = [''];
for (const word of book.title.split(' ')) {
const test = (lines[lines.length - 1] + ' ' + word).trim();
if (c.measureText(test).width > 180) lines.push(word); else lines[lines.length - 1] = test;
}
lines.forEach((line, i) => c.fillText(line, 135, 110 + i * 38));
c.font = '20px Georgia'; c.fillText(book.author, 135, 330);
tex.update();
return tex;
}
function placeBooks(scene, bookcase, shelf = 2) { // six books, spines out, covers on +x
const strip = new BABYLON.Vector4(0, 0, 0.07, 1);
const faceUV = [strip, strip, new BABYLON.Vector4(0, 0, 1, 1), strip, strip, strip];
let x = -0.35; // left end of the row, local to the bookcase
return BOOKS.map((record) => {
const width = record.pages / 4000 + 0.02; // spine thickness from page count
const book = BABYLON.MeshBuilder.CreateBox(`book${record.id}`, { width, height: 0.3, depth: 0.22, faceUV, wrap: true }, scene);
book.parent = bookcase;
book.position.set(x + width / 2, 0.12 + shelf * 0.5 + 0.15, -0.02);
x += width + 0.012;
book.material = new BABYLON.StandardMaterial(record.title, scene);
book.material.diffuseTexture = drawCover(scene, record);
book.material.specularColor = new BABYLON.Color3(0.15, 0.15, 0.15);
book.metadata = record; // the record, for picking
return book;
});
}
// The book cuts the storefront with CSG2 (Manifold WebAssembly); this sandbox cannot download
// the .wasm, so the older pure-JavaScript BABYLON.CSG makes the same cuts here.
function createFrontWall(scene) {
const box = (width, height, depth, x, y) => {
const mesh = BABYLON.MeshBuilder.CreateBox('cut', { width, height, depth }, scene);
mesh.position.set(x, y, -ROOM.depth / 2);
return mesh;
};
const solid = box(ROOM.width, ROOM.height, ROOM.wall, 0, ROOM.height / 2);
const windowHole = box(3.2, 1.6, 1, -1.4, 1.6), doorHole = box(1.1, 2.2, 1, 2.3, 1.1);
const wall = BABYLON.CSG.FromMesh(solid).subtract(BABYLON.CSG.FromMesh(windowHole))
.subtract(BABYLON.CSG.FromMesh(doorHole)).toMesh('frontWall', scene.getMaterialByName('plaster'), scene);
[solid, windowHole, doorHole].forEach((mesh) => mesh.dispose());
return wall;
}
// ---- end of the bookstore helpers ----
// 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 loads booknest-sunrise.env with CubeTexture.CreateFromPrefilteredData; this sandbox
// cannot download it, so useEnvironment takes the procedural sky from createSkyTexture instead.
function useEnvironment(scene, rotationY = Math.PI) {
const env = createSkyTexture(scene);
new BABYLON.HDRFiltering(scene.getEngine()).prefilter(env); // an .env arrives prefiltered
env.rotationY = rotationY; // sun in front, matching lightRoom's
scene.environmentTexture = env; // image-based light for PBR materials
scene.environmentIntensity = 0.35; // IBL ignores walls: keep it low indoors
const skybox = scene.createDefaultSkybox(env, true, 200, 0.05); // PBR skybox, slight blur
skybox.material.reflectionTexture.rotationY = rotationY; // the skybox holds a clone
skybox.isPickable = false; // clicks go to the books
return { env, skybox };
}
const canvas = document.getElementById('view');
const engine = new BABYLON.Engine(canvas, true, {}, true);
const scene = createScene(engine);
createRoom(scene);
const left = createBookcase(scene, 'bookcaseLeft', -1.8);
const books = placeBooks(scene, left, 1).concat(placeBooks(scene, left, 3));
useEnvironment(scene); // IBL shadows read environmentTexture
// SSR reflects from specular color: matte walls get black specular, the floor a polished finish
scene.getMeshByName('floor').material.specularColor = new BABYLON.Color3(0.6, 0.6, 0.6);
scene.getMeshByName('floor').material.specularPower = 128;
const camera = new BABYLON.ArcRotateCamera('cam', -1.55, 1.2, 3.2, new BABYLON.Vector3(-1.7, 0.5, 1.8), scene);
camera.minZ = 0.05;
camera.attachControl(canvas, true);
const ssr = new BABYLON.SSRRenderingPipeline('ssr', scene, [camera], true); // true: geometry buffer
ssr.strength = 0.8; // how much reflection to add
ssr.reflectionSpecularFalloffExponent = 2;
const info = document.getElementById('info');
info.textContent = 'SSR on: only what the camera sees can be reflected';
// IBL shadows voxelize the casters (hundreds of ms), so they wait for a click here
document.getElementById('ibl').addEventListener('click', (event) => {
event.target.disabled = true;
const ibl = new BABYLON.IblShadowsRenderPipeline('iblShadows', scene,
{ resolutionExp: 6, sampleDirections: 2 }, [camera]); // a 64-cubed voxel grid
ibl.addShadowCastingMesh([left, ...books]);
ibl.addShadowReceivingMaterial(); // no argument: every material
ibl.updateSceneBounds(); // fit the grid around the casters
const started = performance.now();
ibl.onVoxelizationCompleteObservable.addOnce(() => {
info.textContent = `voxelized in ${Math.round(performance.now() - started)} ms`;
});
ibl.updateVoxelization(); // redo whenever a caster moves
});
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>