# \[bug\] AtmospherePBRMaterialPlugin auto-attaches to PBRMaterials created after \`new Atmosphere(...)\` and breaks their shader compile when env is a cube

**URL:** <https://forum.babylonjs.com/t/bug-atmospherepbrmaterialplugin-auto-attaches-to-pbrmaterials-created-after-new-atmosphere-and-breaks-their-shader-compile-when-env-is-a-cube/63276>\
**Category:** Bugs\
**Tags:** material\
**Created:** [April 27, 2026, 6:04am UTC](https://forum.babylonjs.com/t/bug-atmospherepbrmaterialplugin-auto-attaches-to-pbrmaterials-created-after-new-atmosphere-and-breaks-their-shader-compile-when-env-is-a-cube/63276 "2026-04-27T06:04:05Z")\
**Posts on this page:** 3\
**Page:** 1

<div class="post-metadata">

**Author:** ![jprietopreference](https://sea2.discourse-cdn.com/flex024/user_avatar/forum.babylonjs.com/jprietopreference/32/30477_2.png) [@jprietopreference](https://forum.babylonjs.com/u/jprietopreference)\
**Post date:** [April 27, 2026, 6:04am UTC](https://forum.babylonjs.com/t/bug-atmospherepbrmaterialplugin-auto-attaches-to-pbrmaterials-created-after-new-atmosphere-and-breaks-their-shader-compile-when-env-is-a-cube/63276/1 "2026-04-27T06:04:06Z")

</div>

**\*\*Versions:\*\*** `@babylonjsbabylonjs/core@9@babylonjs4.1` + `@babylonjs/addons@9.4.1` (also reproduces with the `babylonjs.addons.js` UMD on `[https://preview.babylonjs.com/\`](https://preview.babylonjs.com/%5C%60)).

**\*\*Engine:\*\*** WebGL2.

**\*\*Browsers tested:\*\*** Chromium 1217 (Playwright), reproduces deterministically.

**\*\*OS:\*\*** Windows 11.

**## Summary**

`new Atmosphere(…)` calls `RegisterMaterialPlugin(…)` in its constructor, which subscribes a factory to `Material.OnEventObservable` for `MaterialPluginEvent.Created`. From that point on, **\*\*every\*\*** `PBRMaterial` constructed in the scene auto-attaches an `AtmospherePBRMaterialPlugin` instance.

The plugin’s `getCustomCode()` unconditionally injects

```glsl

vec3 environmentIrradiance = lightIntensity \* sampleReflection(irradianceSampler, uv).rgb;

```

into the PBR fragment shader (gated by the plugin’s `USE_CUSTOM_REFLECTION` define, which is set in the plugin’s constructor based on `_atmosphere.diffuseSkyIrradianceLut !== null`).

When `scene.environmentTexture` is a regular cube map (i.e. the user is not in atmosphere mode), Babylon’s `pbrBlockReflection.js` only declares `irradianceSampler` under `#ifdef USEIRRADIANCEMAP`, which is **\*\*not set for cube envs\*\***. The plugin’s injected code then references an undeclared identifier and the compile fails:

```

BJS - Unable to compile effect:

Error: FRAGMENT SHADER ERROR: 0:: ‘irradianceSampler’ : undeclared identifier

Offending line: vec3 environmentIrradiance = lightIntensity \* sampleReflection(irradianceSampler, uv).rgb;

```

The new material renders invisible.

**## What `setEnabled` and `dispose` do (and don’t)**

- `Atmosphere.setEnabled(false)` only flips `_isEnabled`. It does **\*\*not\*\*** unregister the global plugin factory and does **\*\*not\*\*** detach plugin instances from already-bound materials.

- `Atmosphere.dispose()` calls `UnregisterMaterialPlugin(MaterialPlugin)` so future materials are no longer affected, but plugin instances on materials that were already bound at the time of dispose remain attached.

So the bug fires whenever a `PBRMaterial` is created during the lifetime of a (constructed-but-currently-disabled) `Atmosphere`, and that material then renders against a cube env.

**## Standalone repro**

A 5-step minimal repro reproduces the bug deterministically. The full script is at the end of this post.

Sequence (identical in both ESM and the UMD `babylonjs.addons.js` paths):

1. Build a scene with a cube `environmentTexture`.

2. Create `matA = new PBRMaterial(…)` and let it render at least one frame. **\*\*No atmosphere plugin attached\*\*** because `Atmosphere` doesn’t exist yet.

3. `const atmosphere = new Atmosphere(…)` and `setEnabled(true)`. The constructor registers the global `MaterialPluginEvent.Created` factory.

4. `atmosphere.setEnabled(false); scene.environmentTexture = cube;` and `markAllMaterialsAsDirty(MATERIAL_AllDirtyFlag)`. Plugin factory is **\*\*still registered\*\***.

5. Create `matB = new PBRMaterial(…)`. Inspect `matB.pluginManager._plugins` — it contains `AtmospherePBRMaterialPlugin`. Force compile:

```

D\] matB plugins: \[PBRBRDF, PBRClearCoat, PBRIridescence, PBRAnisotropic, Sheen, PBRSubSurface, DetailMap, AtmospherePBRMaterialPlugin

⇒ AtmospherePBRMaterialPlugin LEAKED ONTO matB

```

```

BJS - Unable to compile effect:

…

Error: FRAGMENT SHADER ERROR: 0:1231: ‘irradianceSampler’ : undeclared identifier

Offending line: vec3 environmentIrradiance = lightIntensity \* sampleReflection(irradianceSampler, uv).rgb;

```

`matB` is invisible. `matA` (created before the Atmosphere existed) renders normally because the plugin was never attached to it.

**## How we know the error is caused by the plugin (not by the material)**

Three independent proofs:

**\*\*1. The offending GLSL line exists** _\*only\*_ **in the addon’s plugin source.\*\***

```

$ grep -rE "environmentIrradiance.\*=.\*lightIntensity.\*sampleReflection@babylonjs \

node\_modules/@babylonjs/

addons/atmosphere/atmospherePBRMaterialPlugin.js:

vec3 environmentIrradiance = lightIntensity \* sampleReflection(irradianceSampler, uv).rgb;

```

That exact line is the one Babylon’s compiler reports as `Offending line`. It’s emitted by `AtmospherePBRMaterialPlugin.getCustomCode()` inside its `CUSTOM_REFLECTION` chunk and is nowhere in core PBR shader code.

**\*\*2. Without the plugin, the** _\*same\*_ **matB compiles cleanly.\*\***

A control run that’s identical to the repro except it never constructs `Atmosphere`:

```

=== CONTROL: no atmosphere, only matA + matB ===

matA plugins: [PBRBRDF, PBRClearCoat, PBRIridescence, PBRAnisotropic, Sheen, PBRSubSurface, DetailMap]

matB plugins: [PBRBRDF, PBRClearCoat, PBRIridescence, PBRAnisotropic, Sheen, PBRSubSurface, DetailMap]

matB: compiled OK ← clean (7 plugins, no AtmospherePBRMaterialPlugin)

=== TRIGGER: atmosphere alive then disabled, matB created ===

matB plugins: […, AtmospherePBRMaterialPlugin] ← 8 plugins

→ 16× FRAGMENT SHADER ERROR: ‘irradianceSampler’ : undeclared identifier

```

Same scene, same env (`environmentSpecular.env` cube), same `PBRMaterial` props (`metallic=1, roughness=0.05`), same engine, same Babylon version. The only difference is whether `new Atmosphere(…)` is ever called. If the bug were inherent to the material, the control would fail too. It doesn’t.

**\*\*3. The plugin emits a sampler reference that core PBR doesn’t declare for cube envs.\*\***

`node\_modules/@babylonjs/core/Shaders/ShadersInclude/pbrBlockReflection.js` gates the sampler:

```glsl

#ifdef USEIRRADIANCEMAP

#ifdef REFLECTIONMAP\_3D

,in samplerCube irradianceSampler

#endif

,in sampler2D irradianceSampler

#endif

#endif

```

`USEIRRADIANCEMAP` is not set when `scene.environmentTexture` is a cube HDRI (irradiance there comes from spherical harmonics). The plugin’s `CUSTOM_REFLECTION` chunk uses `irradianceSampler` regardless, and gates its emission only on its own private `USE_CUSTOM_REFLECTION` define (set to `_atmosphere.diffuseSkyIrradianceLut !== null` once at plugin construction). The two defines are not coordinated, so the moment the plugin is attached to a material whose env is a cube, the compile fails.

This rules out: material misconfiguration (control 1 has identical material), engine effect-cache pollution (control 1 has the same engine + same matA-then-matB sequence), bad envtex (control 1 uses the same cube), and a bug in core PBR (offending line isn’t in core).

**## Babylon Playground**

**\*\*Repro:\*\*** [https://playground.babylonjs.com/#9O0Y6G#1](https://playground.babylonjs.com/#9O0Y6G#1)

Just open and press Run — the snippet self-loads `babylonjs.addons.js` from `preview.babylonjs.com`. Open the JS console (F12) to see the leaked plugin name on `matB` and the `‘irradianceSampler’ : undeclared identifier` errors.

Snippet contents (also embedded at the URL above):

```js

const createScene = async function () {

const scene = new BABYLON.Scene(engine);

const camera = new BABYLON.ArcRotateCamera(“cam”, -Math.PI / 2, Math.PI / 2.4, 3.5,

BABYLON.Vector3.Zero(), scene);

camera.attachControl(canvas, true);

const sun = new BABYLON.DirectionalLight(“sun”, new BABYLON.Vector3(-0.2, -1, -0.1), scene);

const envTex = BABYLON.CubeTexture.CreateFromPrefilteredData(

“[https://assets.babylonjs.com/environments/environmentSpecular.env](https://assets.babylonjs.com/environments/environmentSpecular.env)”, scene);

scene.environmentTexture = envTex;

scene.createDefaultSkybox(envTex, true, 1000, 0.18);

// [A] matA created BEFORE Atmosphere — no plugin attached.

const sphereA = BABYLON.MeshBuilder.CreateSphere(“sA”, { diameter: 1 }, scene);

sphereA.position.x = -1.2;

const matA = new BABYLON.PBRMaterial(“matA”, scene);

matA.metallic = 1; matA.roughness = 0.05;

sphereA.material = matA;

console.log(“[A] matA plugins:”,

(matA.pluginManager.\_plugins).map(p =\> p.name).join(", "));

await new Promise((r) =\> setTimeout(r, 800));

// [B] Construct Atmosphere — registers the global plugin factory.

const Addons = window.ADDONS;

const atmosphere = new Addons.Atmosphere(“atmo”, scene, [sun], {

physicalProperties: new Addons.AtmospherePhysicalProperties(),

isLinearSpaceComposition: true,

isLinearSpaceLight: true,

isSkyViewLutEnabled: true,

isAerialPerspectiveLutEnabled: true,

});

atmosphere.setEnabled(true);

await new Promise((r) =\> setTimeout(r, 1500));

// [C] Disable Atmosphere, restore cube env. Plugin factory still registered.

atmosphere.setEnabled(false);

scene.environmentTexture = envTex;

scene.markAllMaterialsAsDirty(BABYLON.Constants.MATERIAL\_AllDirtyFlag);

await new Promise((r) =\> setTimeout(r, 800));

// [D] matB created AFTER Atmosphere — plugin auto-attaches.

const sphereB = BABYLON.MeshBuilder.CreateSphere(“sB”, { diameter: 1 }, scene);

sphereB.position.x = 1.2;

const matB = new BABYLON.PBRMaterial(“matB”, scene);

matB.metallic = 1; matB.roughness = 0.05;

sphereB.material = matB;

const matBPlugins = matB.pluginManager.\_plugins.map(p =\> p.name);

console.log(“[D] matB plugins:”, matBPlugins.join(", "));

console.log(matBPlugins.includes(“AtmospherePBRMaterialPlugin”)

? " ⇒ AtmospherePBRMaterialPlugin LEAKED ONTO matB"

: " ⇒ no leak");

// [E] Force compile — emits the irradianceSampler shader error.

matB.forceCompilation(sphereB,

() =\> console.log(“[E] matB compile callback (success)”),

undefined, false);

return scene;

};

```

Open the browser console after running. You’ll see the leaked plugin name on `matB`, then ~16 `‘irradianceSampler’ : undeclared identifier` shader errors. The right-hand sphere (matB) is invisible; the left-hand sphere (matA) renders normally.

**## Suggested fix(es)**

Pick whichever fits the addon’s design intent best:

1. `Atmosphere.setEnabled(false)` should detach the plugin from all PBRMaterials in the scene **\*\*and\*\*** unregister the global factory; `setEnabled(true)` re-registers and re-attaches.

2. Or, `AtmospherePBRMaterialPlugin.getCustomCode()` should gate the `irradianceSampler` reference so it only emits when the surrounding PBR shader is configured to declare it (i.e. tie it to the same conditions as `USEIRRADIANCEMAP` in `pbrBlockReflection.js`).

3. Or, document the required lifecycle and provide a public API to remove the plugin from a material (currently there is no public `MaterialPluginManager.removePlugin`).

**## Workaround we are using**

In our app, we now `atmosphere.dispose()` (and null the reference) on every asset swap (immediately before `LoadAssetContainerAsync` for the next glTF). The next “Daylight” toggle reconstructs a fresh `Atmosphere`. We also defer initial `Atmosphere` construction until at least one asset is in the scene (otherwise an early auto-restore from a persisted “daylight” preference creates a half-initialized atmosphere with the fallback camera, degrading the first asset’s render).

This avoids both the shader-compile failure on the new material and a related cold-init artifact.

Happy to provide the full `Playwright`-driven trace, the disposed materials’ compile-time defines, or the full ~3K-line failing fragment shader on request.

 ![babylon_repro_proof](https://us1.discourse-cdn.com/flex024/uploads/babylonjs/original/3X/c/1/c1aa41a7d5439bb9e1ab3adf3ea26c51eac00f83.png)

---

<div class="post-metadata">

**Author:** ![Evgeni\_Popov](https://sea2.discourse-cdn.com/flex024/user_avatar/forum.babylonjs.com/evgeni_popov/32/4432_2.png) [@Evgeni\_Popov](https://forum.babylonjs.com/u/Evgeni_Popov)\
**Post date:** [April 27, 2026, 9:22am UTC](https://forum.babylonjs.com/t/bug-atmospherepbrmaterialplugin-auto-attaches-to-pbrmaterials-created-after-new-atmosphere-and-breaks-their-shader-compile-when-env-is-a-cube/63276/2 "2026-04-27T09:22:21Z")

</div>

Thanks for the very thorough write-up — your diagnosis nails it.

**Root cause confirmed.** `AtmospherePBRMaterialPlugin` sets `USE_CUSTOM_REFLECTION` once in its constructor based on `_atmosphere.diffuseSkyIrradianceLut !== null`, and from then on it always emits a `CUSTOM_REFLECTION` chunk that references `irradianceSampler`. Core PBR only declares `irradianceSampler` under `USEIRRADIANCEMAP`, which the addon only wires up when the active env has `.irradianceTexture` set (the diffuse sky LUT). When the active env is a cube using spherical harmonics, `USESPHERICALFROMREFLECTIONMAP` is set instead — `irradianceSampler` does not exist, and the compile fails exactly as you traced.

**Fix.** I went with your option #2: gate the plugin’‘s `USE_CUSTOM_REFLECTION` dynamically in `prepareDefines` against the parent material’‘s `USEIRRADIANCEMAP`. When the irradiance map isn’'t active for that material, `USE_CUSTOM_REFLECTION` becomes `false` and the standard PBR reflection block runs (no hijack, no undeclared identifier). When you switch back to atmosphere mode, `USEIRRADIANCEMAP` flips on, the plugin re-enables its custom reflection, and a recompile is triggered automatically.

This is the narrowest, safest fix — it doesn’'t change the registration/attachment semantics of the plugin, so existing atmosphere scenes are unaffected. The “attached but inactive” plugin on a cube-env material is now a true no-op for reflection.

A separate, more invasive change to make `setEnabled(false)` detach plugin instances (your option #1) and to expose a public `MaterialPluginManager.removePlugin` (your option #3) is worth doing but is a bigger conversation about the addon’‘s lifecycle contract — happy to track that as a follow-up if you’'d like.

PR: [[Addons] Fix AtmospherePBRMaterialPlugin breaking PBRMaterial compile for non-irradiance-map envs by Popov72 · Pull Request #18385 · BabylonJS/Babylon.js · GitHub](https://github.com/BabylonJS/Babylon.js/pull/18385)

Your `atmosphere.dispose()` workaround on asset swaps remains a fine guardrail until that ships.

---

<div class="post-metadata">

**Author:** ![jprietopreference](https://sea2.discourse-cdn.com/flex024/user_avatar/forum.babylonjs.com/jprietopreference/32/30477_2.png) [@jprietopreference](https://forum.babylonjs.com/u/jprietopreference)\
**Post date:** [April 27, 2026, 9:52am UTC](https://forum.babylonjs.com/t/bug-atmospherepbrmaterialplugin-auto-attaches-to-pbrmaterials-created-after-new-atmosphere-and-breaks-their-shader-compile-when-env-is-a-cube/63276/3 "2026-04-27T09:52:01Z")

</div>

Great reaction Evgeni, I keep waiting until fix ships.

José
