How to cast shadows without adding another ShadowGenerator?

Hi,

Here’s a screenshot from my game:

My “torch” BABYLON.SpotLight needs shadows but a 3rd ShadowGenerator may be too costly, here is my current lighting setup:

• HemisphericLight – my main ambient light (no shadows)
• DirectionalLight #1 + ShadowGenerator (static shadows)
• DirectionalLight #2 + ShadowGenerator (dynamic shadows)
• SpotLight – the torch lights up around the player, but casts no shadows today

I really want that torch to cast its own shadows, but adding a third ShadowGenerator may hit the performance ?

Any ideas how to setup an more performance friendly with same effects?

I haven’t tested the Gemini 2.5 answer, but I know from experience that the answers are pretty good.

Gemini:

Excellent question. Your concern is very valid. Each ShadowGenerator adds at least one full render pass of your scene from the light’s perspective, which can indeed be a significant performance cost.

Your current setup is already very smart, separating static and dynamic shadows. Let’s build on that intelligence to incorporate the torch shadows without tanking your framerate.

Here are several strategies, from simplest to most advanced, to achieve your goal.

Strategy 1: The “Just Do It, But Smartly” Approach (Recommended)

This is the most direct and likely the best approach. Add the third ShadowGenerator for the SpotLight, but configure it to be extremely cheap. The key is that a torch shadow doesn’t need to be perfect; it’s for atmosphere.

// Assuming 'scene' and 'playerMesh' are already defined

// 1. Create your torch SpotLight
const torchLight = new BABYLON.SpotLight(
    "torch",
    new BABYLON.Vector3(0, 1, 0), // Initial position
    new BABYLON.Vector3(0, -1, 0), // Initial direction
    Math.PI / 3, // Angle
    10, // Exponent
    scene
);
// Make the torch follow the player
torchLight.parent = playerMesh;

// 2. Create its own ShadowGenerator
const torchShadowGenerator = new BABYLON.ShadowGenerator(512, torchLight); // <-- Key part 1

// 3. Configure it to be CHEAP
// Use a lower-quality, but faster, filtering method.
torchShadowGenerator.usePoissonSampling = true; // Cheaper than PCF
// OR for even faster, blockier shadows:
// torchShadowGenerator.useBlurExponentialShadowMap = false;
// torchShadowGenerator.usePercentageCloserFiltering = false;

// Set a bias to prevent shadow acne. You'll need to tweak this.
torchShadowGenerator.bias = 0.01;

// 4. IMPORTANT: Only add ESSENTIAL meshes to the shadow list!
// The torch only needs to cast shadows from the player and maybe a few
// nearby dynamic objects. It should NOT render the whole level.
torchShadowGenerator.getShadowMap().renderList.push(playerMesh);
// If you have other dynamic objects (e.g., a monster), add them too:
// torchShadowGenerator.getShadowMap().renderList.push(monsterMesh);

// 5. Exclude static geometry
// Your static geometry is already handled by DirectionalLight #1.
// Don't add your static level mesh to the torch's renderList.

Why this works well:

  • Low mapSize (512): The shadow map resolution is tiny. Since the torch only affects a small area, you don’t need a 1024 or 2048 map. This dramatically reduces the GPU memory and fill-rate cost.
  • Fast Filtering: usePoissonSampling is much faster than the default Percentage-Closer Filtering (PCF). The shadows will look a bit dithered/noisy instead of soft, but for a flickering torch, this can actually enhance the effect.
  • Minimal renderList: This is the biggest optimization. Instead of rendering hundreds of meshes for the shadow map, you’re only rendering one (the player). The cost of this shadow pass becomes negligible.

Strategy 2: The “Hijacking” Approach (Advanced)

This is a more complex but potentially powerful technique if you are absolutely forbidden from creating a third ShadowGenerator. The idea is to temporarily reuse one of your existing ShadowGenerators.

You could, for example, hijack the ShadowGenerator for your dynamic directional light. The process would look like this in your render loop:

  1. Before rendering, tell the dynamic ShadowGenerator to use the SpotLight as its light source.
  2. Let it render the shadow map for the torch.
  3. Immediately switch it back to using the DirectionalLight before the main scene render, so that lighting calculations in the material shaders work correctly.
// --- In your setup ---
// Let's say dynamicShadowGenerator is from your DirectionalLight #2
const dynamicDirLight = scene.getLightByName("DirectionalLight2");
const dynamicShadowGenerator = dynamicDirLight.getShadowGenerator();

// --- In your render loop ---
scene.onBeforeRenderObservable.add(() => {
    // Make the dynamic shadow generator think it belongs to the torch
    dynamicShadowGenerator.setLight(torchLight);

    // This is important: you must force the shadow generator to re-evaluate
    // its view and projection matrices based on the new light (our spotlight).
    // The 'forceCompilation' flag is a bit of a misnomer here, but it triggers
    // the necessary internal updates for the matrices.
    dynamicShadowGenerator.forceCompilation(
        dynamicShadowGenerator.getShadowMap().renderList, 
        { useInstances: true }
    );
});

scene.onAfterRenderObservable.add(() => {
    // IMPORTANT: Switch it back after the frame is rendered so it's
    // correct for the next frame's setup.
    dynamicShadowGenerator.setLight(dynamicDirLight);
});

:warning: Downsides to this approach:

  • Complexity: It’s tricky to manage and can be a source of weird bugs.
  • Wasted Resources: The shadow map is generated from the SpotLight’s perspective, but it’s still configured for a DirectionalLight. You might be using a large shadow map when a small one would do.
  • Potential Artifacts: The timing is critical. If you don’t switch it back correctly, your main directional shadows might fail or use the wrong projection.

Conclusion: This is a clever trick, but Strategy 1 is almost always preferable for its simplicity and clarity.


Strategy 3: The “Fake It” Approach (Ultra-Performant)

If even the optimized ShadowGenerator is too much, you can fake the shadow entirely. The most important shadow from a torch is the one cast by the player onto the ground.

You can achieve this with a blob shadow, which is just a soft, textured plane (a decal) that sits under your player.

  1. Create a BABYLON.Plane or use the BABYLON.DecalBuilder.
  2. Give it a StandardMaterial with a black-and-white texture of a soft, circular “blob”.
  3. Set the material’s useAlphaFromDiffuseTexture to true and set its opacity or diffuseTexture.hasAlpha.
  4. Parent this plane to your player and position it just above the ground. It will follow the player around, giving the illusion of a shadow and grounding the character in the scene.

This method is nearly free, performance-wise, but it’s not a “real” shadow. It won’t wrap around objects or be cast from the player’s arms, etc.

Final Recommendation

Go with Strategy 1.

  1. Try it: Add the third ShadowGenerator for your SpotLight.
  2. Optimize it aggressively:
    • mapSize: 512 or even 256.
    • usePoissonSampling: true.
    • renderList: Only the player mesh.
  3. Profile: Use the Babylon.js Inspector (scene.debugLayer.show()) and check the “Frame Time” and “Draw Calls” with the torch shadow on and off. You will likely find the performance impact to be very small.

This gives you the best of both worlds: the visual fidelity of real-time shadows for your torch, with a minimal and controllable performance cost.

1 Like