WebGL built shadows by hand, twice: stencil shadow volumes in four passes (Stencil Shadow Volumes) and shadow mapping with bias and PCF (Framebuffers and Shadows). Babylon.js 89,603 keeps only the second, generalized:
| WebGL, by hand | Babylon.js |
|---|---|
| Depth pass and sampler2DShadow (3.13.4-3.13.7) | ShadowGenerator, receivers generated |
| Hand-tuned bias constant (3.13.8) | bias, normalBias, forceBackFacesOnly |
| 3 x 3 PCF loop (3.13.9) | usePercentageCloserFiltering, filteringQuality |
| Stencil 672,850 shadow volumes (3.11) | Not in core; use shadow maps |
| Not covered | ESM, CESM, PCSS, cascades, cube maps for point lights |
You give up the volumes' pixel-exact edges for texel-limited ones, which cascades and filters manage, and gain skinned, instanced, alpha-tested and (with transparencyShadow) transparent casters on both backends.
<!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; }
</style>
<canvas id="view"></canvas>
<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>
const canvas = document.getElementById('view');
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#f7f3ecff');
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2, 1.0, 8.5, new BABYLON.Vector3(0, 0.2, 0.6), scene);
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0, 1, 0), scene).intensity = 0.35;
const ui = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI('ui');
// Three identical stages; each has its own light and generator, touching only its own meshes
const SETUPS = [
['depth test only', (g) => { g.bias = 0.00005; }],
['PCF (the 3 x 3 loop)', (g) => {
g.usePercentageCloserFiltering = true;
g.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH;
}],
['PCSS (contact hardening)', (g) => {
g.useContactHardeningShadow = true;
g.contactHardeningLightSizeUVRatio = 0.08;
}],
];
SETUPS.forEach(([text, configure], i) => {
const x = (i - 1) * 3.1;
const tile = BABYLON.MeshBuilder.CreateGround('tile' + i, { width: 2.9, height: 3.2 }, scene);
tile.position.set(x, 0, 0.4);
tile.receiveShadows = true;
tile.material = new BABYLON.StandardMaterial('tileMat' + i, scene);
tile.material.diffuseColor = BABYLON.Color3.FromHexString('#efe6d8');
tile.material.specularColor = BABYLON.Color3.Black();
// a book leaning on a block: its shadow runs from touching the floor to far from it
const book = BABYLON.MeshBuilder.CreateBox('book' + i, { width: 0.8, height: 1.4, depth: 0.12 }, scene);
book.position.set(x - 0.3, 0.66, -0.2);
book.rotation.z = -0.25; // leaning: one edge on the floor
book.material = new BABYLON.StandardMaterial('bookMat' + i, scene);
book.material.diffuseColor = BABYLON.Color3.FromHexString(['#1f5f8b', '#3f7d3a', '#b5452f'][i]);
const sun = new BABYLON.DirectionalLight('sun' + i, new BABYLON.Vector3(0.8, -0.8, 0.9), scene); // a low sun: long shadows
sun.position = new BABYLON.Vector3(x - 4, 4, -4.5);
sun.shadowMinZ = 1;
sun.shadowMaxZ = 12; // a tight depth range, like a hand-built frustum
sun.includedOnlyMeshes = [tile, book];
const generator = new BABYLON.ShadowGenerator(512, sun); // a small map shows the filtering
generator.addShadowCaster(book);
configure(generator);
const label = new BABYLON.GUI.TextBlock('', text);
Object.assign(label, { color: '#333', fontSize: 11, resizeToFit: true });
ui.addControl(label);
label.linkWithMesh(tile);
label.linkOffsetY = 105;
});
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>