**Versions:** `@babylonjsbabylonjs/core@9@babylonjs4.1` + `@babylonjs/addons@9.4.1` (also reproduces with the `babylonjs.addons.js` UMD on `https://preview.babylonjs.com/\`).
**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:
```
⇒ 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
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”, 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.
