One property picks the filter that softens Framebuffers and Shadows's hard comparison: usePercentageCloserFiltering (PCF, PCF Soft Shadows), usePoissonSampling, useBlurExponentialShadowMap and useBlurCloseExponentialShadowMap (exponential maps blurred like images), or useContactHardeningShadow (PCSS, whose penumbra widens away from the caster); filteringQuality trades taps for speed. A small 256-pixel map shows the differences:
<div id="row" style="display: flex; gap: 4px; font: 13px sans-serif; text-align: center"></div>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script>
{
const filters = {
hard: () => {},
PCF: (sg) => { sg.usePercentageCloserFiltering = true; },
'blurred CESM': (sg) => { sg.useBlurCloseExponentialShadowMap = true; },
PCSS: (sg) => { sg.useContactHardeningShadow = true; },
};
let pending = Object.keys(filters).length;
for (const [label, useFilter] of Object.entries(filters)) {
const panel = document.createElement('div');
panel.innerHTML = `<canvas style="width: 152px; height: 115px"></canvas><br>${label}`;
row.append(panel);
const engine = new BABYLON.Engine(panel.firstChild, true, {}, true);
const scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#eceff1ff');
const target = new BABYLON.Vector3(-0.2, 0.2, 0);
new BABYLON.ArcRotateCamera('cam', -2.1, 0.95, 1.9, target, scene);
new BABYLON.HemisphericLight('fill', new BABYLON.Vector3(0, 1, 0), scene).intensity = 0.4;
const sun = new BABYLON.DirectionalLight('sun', new BABYLON.Vector3(-0.6, -1, 0.5), scene);
Object.assign(sun, { shadowMinZ: -2, shadowMaxZ: 3 }); // tight depth range
const plank = BABYLON.MeshBuilder.CreateGround('oak', { width: 2.4, height: 1.6 }, scene);
plank.material = new BABYLON.StandardMaterial('oak', scene);
plank.material.diffuseColor = BABYLON.Color3.FromHexString('#b08d6a');
plank.receiveShadows = true;
const size = { width: 0.12, height: 0.75, depth: 0.5 };
const book = BABYLON.MeshBuilder.CreateBox('book', size, scene);
book.position.y = 0.375;
book.rotation.z = 0.35; // leaning, touching the plank
const shadows = new BABYLON.ShadowGenerator(256, sun);
shadows.addShadowCaster(book);
useFilter(shadows);
scene.executeWhenReady(() => engine.runRenderLoop(() => scene.render()));
scene.onAfterRenderObservable.add(() => {
if (scene.getFrameId() === 5 && --pending === 0) window.__done = true;
});
}
}
</script>
The hard edge shows the map's texels as stair steps; PCF smooths them; the blurred close exponential map blurs every edge alike; PCSS stays sharp where the book touches the plank and spreads away from it. Every filter meets shadow acne (Acne and Bias): bias (default 0.00005) offsets the stored depth, normalBias shrinks casters along their normals, fixing sloped surfaces without a large bias's peter-panning, and forceBackFacesOnly renders back faces, a cheap fix for closed meshes.