Bloom and Sharpening

Bloom, Chromatic Aberration and Sharpening

Bloom imitates light scattering in a lens: bloomThreshold picks the pixels that bleed, bloomWeight how strongly, bloomKernel how far. Chromatic aberration shifts the color channels apart toward the edges (aberrationAmount, radialIntensity). Sharpening restores edge contrast that FXAA or a low resolution took (edgeAmount; colorAmount 0 leaves only the edges). The figure gives each effect its own camera, viewport and pipeline:

Bloom, chromatic aberration and sharpening, one per viewportHTML
<canvas id="view" style="width: 620px; height: 150px"></canvas>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script src="bookstore.js"></script>
<script>
{
  const engine = new BABYLON.Engine(view, true, {}, true);
  const scene = BookNestStore.createScene(engine);
  BookNestStore.createRoom(scene);
  const left = BookNestStore.createBookcase(scene, 'bookcaseLeft', -1.8);
  BookNestStore.coverBooks(scene, BookNestStore.placeBooks(scene, left));
  BookNestStore.lightRoom(scene);
  const looks = [(p) => Object.assign(p, { bloomEnabled: true, bloomThreshold: 0.3,
    bloomWeight: 1, bloomKernel: 96 }),
  (p) => Object.assign(p, { chromaticAberrationEnabled: true }).chromaticAberration
    .aberrationAmount = 60,
  (p) => Object.assign(p, { sharpenEnabled: true }).sharpen.edgeAmount = 1.5];
  const target = new BABYLON.Vector3(-1.05, 1.3, 2.5);
  looks.forEach((look, i) => {
    const camera = new BABYLON.ArcRotateCamera(`view${i}`, -1.85, 1.45, 2.1, target, scene);
    camera.viewport = new BABYLON.Viewport(i / 3, 0, 1 / 3, 1);   // left, middle, right third
    scene.activeCameras.push(camera);
    look(new BABYLON.DefaultRenderingPipeline(`look${i}`, true, scene, [camera]));
  });
  BookNestStore.renderWhenReady(engine, scene, 10);
}
</script>
Browser output of Listing 7.102
Browser output of 102

On the left the sunlit wall bleeds over the bookcase's edge; in the middle edges split into colored fringes; on the right the boards gain outlines. In production, keep aberration to a few pixels and the threshold near 1.

The same shelf through three cameras and pipelines: bloom, chromatic aberration and sharpeningHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
  canvas { width: 100%; max-width: 600px; height: 240px; display: block; touch-action: none; outline: none; }
  #labels { max-width: 600px; display: flex; } #labels span { flex: 1; text-align: center; padding: 4px; }
</style>
<canvas id="view"></canvas>
<div id="labels"><span>bloom</span><span>chromatic aberration</span><span>sharpen</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 ----

const canvas = document.getElementById('view');
const engine = new BABYLON.Engine(canvas, true, {}, true);
const scene = createScene(engine);
scene.getLightByName('sky').intensity = 0.6;
createRoom(scene);
const left = createBookcase(scene, 'bookcaseLeft', -1.8);
placeBooks(scene, left);
const lamp = new BABYLON.PointLight('lamp', new BABYLON.Vector3(-0.9, 2.2, 2.2), scene);   // a hot spot for the bloom
lamp.diffuse = BABYLON.Color3.FromHexString('#ffd9a0');
lamp.intensity = 1.2;
const looks = [
  (p) => Object.assign(p, { bloomEnabled: true, bloomThreshold: 0.3, bloomWeight: 1, bloomKernel: 96 }),
  (p) => { p.chromaticAberrationEnabled = true; p.chromaticAberration.aberrationAmount = 60; },
  (p) => { p.sharpenEnabled = true; p.sharpen.edgeAmount = 1.5; },
];
const target = new BABYLON.Vector3(-1.5, 1.35, 2.5);
looks.forEach((look, i) => {
  const camera = new BABYLON.ArcRotateCamera(`view${i}`, -1.85, 1.45, 2.4, target, scene);
  camera.minZ = 0.05;
  camera.viewport = new BABYLON.Viewport(i / 3, 0, 1 / 3, 1);   // left, middle, right third
  scene.activeCameras.push(camera);
  look(new BABYLON.DefaultRenderingPipeline(`look${i}`, true, scene, [camera]));   // one pipeline each
});
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>