We have upgraded to Babylon 9.6.2 from Babylon 8.55.3 and we have found that when using a light with a shadow generator, the custom “defines” from a material plugin that extends a gaussian material are not updated properly.
Here you have a PG with an example. The hue is enabled/disabled properly until you create a light with a shadow generator (If you switch to the previous Babylon version on the playground 8.56.2 it works as expected)
We are also having this issue when setting the shDegree on a GS Mesh (in this example it just has degree 0)
Also I was wondering if using the isReadyForSubMesh() method I’m using here is the right way to update the defines, as I couldn’t make the prepareDefines() method to work on a material plugin extending a Gaussian Material.
1. The regression (HUE_ENABLED stops updating once the ShadowGenerator is added)
A recent change in GaussianSplattingMeshBase.isReady() calls shadowGenerator.isReady(...) for each scene light to make sure shadow casting is ready on the very first frame. The catch is that the GS material uses a standalone shadow depth wrapper (one that wraps a private ShaderMaterial). Calling isReady on it while engine.currentRenderPassId is still set to the main pass causes the wrapped material to overwrite the GS material’s defines on the main draw wrapper. Next frame, the GS material sees no defines (the subMesh.materialDefines getter returns null for string defines), recreates them from scratch, and any plugin define you had set is gone.
The fix swaps currentRenderPassId to each shadow generator’s render pass id while preparing it (matching the canonical pattern in Mesh.isReady).
2. Why prepareDefines() didn’t work in your plugin
Good catch — GaussianSplattingMaterial was simply never firing the PrepareDefinesBeforeAttributes / PrepareDefines plugin events, only IsReadyForSubMesh. The PR adds the two missing event invocations, so after this lands you can use prepareDefines() in a plugin extending a GS material the same way you would on StandardMaterial/PBRMaterial.
Both should land in the next nightly. Thanks again for the detailed write-up!
Thanks @Evgeni_Popov for the fixes and for the explanation. I’m not sure if it will be related to this, but also when changing the shDegree of the gaussian splat mesh, the defines or the uniforms of the material plugin seem to be resetted (in this case, setting them again works):
Yes, you’re right that it’s the same kind of issue, just triggered differently.
Setting shDegree calls material.resetDrawCache() (it has to, because SH_DEGREE affects the shader). That throws away every draw wrapper on every subMesh — including the cached MaterialDefines object. The next call to GaussianSplattingMaterial.isReadyForSubMesh rebuilds a fresh defines object with the registered defaults (so your HUE_ENABLED goes back to false), and your plugin’s isReadyForSubMesh skips the restamp because _isDirty is still false — it has no way of knowing the defines were thrown out behind its back.
The cleanest fix on your side, once #18447 lands, is to use prepareDefines() instead of relying on the dirty-flag pattern in isReadyForSubMesh:
prepareDefines runs on every isReadyForSubMesh call, so it always restamps your defines on the fresh defines object — no dirty bookkeeping needed. The MaterialDefines setter handles re-marking dirty automatically when the value actually changes.
The same applies to your bindForSubMesh / hueOff uniform: after resetDrawCache the new effect is born without it, so dropping the if (!this._isDirty) return; guard (or just always calling effect.setFloat("hueOff", this.hueOff)) is the safe pattern.
If you specifically want to keep your _isDirty pattern, the workaround is to set _isDirty = true whenever you do something that triggers resetDrawCache (shDegree change, etc.), but prepareDefines is much less error-prone.
Hi @Evgeni_Popov , sorry to bring this conversation up again. I’ve seen that v9.7.0 is released and I’m trying the fixes, but I see that using “prepareDefines()” doesn’t automatically marks as dirty the defines when the value changes (as mentioned before) and needs to call _defines.markAsUnprocessed() within prepareDefines() in order to update the shader. Is this the expected behavior?
Using only this is not working (even this._isEnabled is changed when clicking the “HUE enable/disable” button) :
Sorry, my previous wording was misleading: assigning defines["HUE_ENABLED"] in prepareDefines() does not automatically mark an already-processed defines object dirty.
The intended pattern is to mark the material dirty when the plugin state changes, and keep prepareDefines() as the place that restamps the define value:
Calling defines.markAsUnprocessed() inside prepareDefines() works because it forces the effect to be considered dirty there, but I wouldn’t recommend it: prepareDefines() is called during readiness checks, so it can cause unnecessary shader recompilations. Dirtying from the property setter is the safer pattern.