Choosing Shadows

Choosing a Shadow Technique by Scene Size

Shadow cost is one extra render of the casters per map (six for a point light, one per cascade), plus the filter's texture taps on every receiving pixel. Match the technique to how much world the camera sees:

Shadow techniques by scene size
Scene Technique Starting settings
Product viewer, one object ShadowGenerator, PCF or PCSS 1024 map, tight shadowMinZ/shadowMaxZ
Room or shop interior ShadowGenerator or 2-3 cascades 2048 map, normalBias against acne
Terrain, city, open world CascadedShadowGenerator 4 cascades, autoCalcDepthBounds
Static level Baked lightmaps (Vertex Colors and UVs) REFRESHRATE_RENDER_ONCE for any live map
Many small lamps No shadows; clustered lights Shadows on the key light only

Keep casters few; on low-end phones, shadow one light at 512-1024 pixels with QUALITY_LOW.

Extra shadow-map renders per frame for each technique, from a baked lightmap to a point light's cube mapHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
  canvas { width: 100%; max-width: 600px; height: 360px; 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.2, 12.5, new BABYLON.Vector3(0, 1.4, 0), scene);
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.5), scene).intensity = 0.6;
const sun = new BABYLON.DirectionalLight('sun', new BABYLON.Vector3(0.4, -1, 0.6), scene);
sun.intensity = 0.6;
const ui = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI('ui');

// Each shadow map costs one extra render of its casters (a point light's cube map six)
const OPTIONS = [
  ['Static level', 'lightmap', 0, '#9e9e9e'],
  ['Product viewer', '1 map', 1, '#1f5f8b'],
  ['Room or shop', '2-3 cascades', 3, '#3f7d3a'],
  ['Open world', '4 cascades', 4, '#e09a10'],
  ['Point light', 'cube map', 6, '#b5452f'],
];
const floor = BABYLON.MeshBuilder.CreateGround('floor', { width: 15, height: 3 }, scene);
floor.material = new BABYLON.StandardMaterial('floorMat', scene);
floor.material.diffuseColor = BABYLON.Color3.FromHexString('#e6ddd0');
floor.receiveShadows = true;
const shadows = new BABYLON.ShadowGenerator(1024, sun);   // the chart casts its own shadows
shadows.usePercentageCloserFiltering = true;

OPTIONS.forEach(([scene_, technique, passes, hex], i) => {
  const x = (i - 2) * 2.9;
  const base = new BABYLON.TransformNode('base' + i, scene);
  base.position.x = x;
  // one slab per extra render pass, stacked
  for (let p = 0; p < passes; p++) {
    const slab = BABYLON.MeshBuilder.CreateBox(`pass${i}_${p}`, { width: 1.3, height: 0.45, depth: 1.3 }, scene);
    slab.position.set(x, 0.25 + p * 0.5, 0);
    slab.material = new BABYLON.StandardMaterial(hex + p, scene);
    slab.material.diffuseColor = BABYLON.Color3.FromHexString(hex);
    shadows.addShadowCaster(slab);
  }
  const label = new BABYLON.GUI.TextBlock('', `${scene_}\n${technique}\n+${passes} render${passes === 1 ? '' : 's'}`);
  Object.assign(label, { color: '#333', fontSize: 11, resizeToFit: true });
  ui.addControl(label);
  label.linkWithMesh(base);
  label.linkOffsetY = 40;
});
const title = new BABYLON.GUI.TextBlock('', 'Shadow cost: extra renders of the casters per frame');
Object.assign(title, { color: '#555', fontSize: 14, height: '24px', top: '8px',
  verticalAlignment: BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP });
ui.addControl(title);
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>