Fix material readiness to gate on light texture readiness#18255
Fix material readiness to gate on light texture readiness#18255bghgary wants to merge 1 commit intoBabylonJS:masterfrom
Conversation
SpotLight projection textures and IES profile textures were not gated by material readiness checks. When a projection texture was still loading, prepareLightSpecificDefines would set PROJECTEDLIGHTTEXTURE to false, the shader would compile without the projection effect, and the material would report isReady()=true. This caused scene.executeWhenReady() to fire before the projection texture loaded, producing incorrect rendering. Changes: - Add areLightTexturesReady() virtual method to Light base class - Override in SpotLight to check projection and IES textures - Track light texture readiness in PrepareDefinesForLight state - Propagate via defines._areLightTexturesReady in PrepareDefinesForLights - Gate isReadyForSubMesh() on light texture readiness in all materials: StandardMaterial, PBRBaseMaterial, BackgroundMaterial, OpenPBRMaterial, NodeMaterial, and Node Material light blocks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a readiness gap where SpotLight projection/IES textures could still be loading while materials (and therefore scene.executeWhenReady()) reported ready, causing rendering to proceed with shaders compiled without the projected/IES lighting path.
Changes:
- Add
Light.areLightTexturesReady()(defaulttrue) and override it inSpotLightto gate on projection/IES texture readiness. - Propagate a
lightTexturesReadyflag throughPrepareDefinesForLight(s)intodefines._areLightTexturesReady. - Update core materials (Standard, PBR, OpenPBR, Background, NodeMaterial) to return not-ready when
defines._areLightTexturesReady === false.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/dev/core/src/Materials/standardMaterial.ts | Gates isReadyForSubMesh on defines._areLightTexturesReady. |
| packages/dev/core/src/Materials/PBR/pbrBaseMaterial.ts | Gates PBR readiness on defines._areLightTexturesReady. |
| packages/dev/core/src/Materials/PBR/openpbrMaterial.ts | Gates OpenPBR readiness on defines._areLightTexturesReady. |
| packages/dev/core/src/Materials/Node/nodeMaterial.ts | Gates NodeMaterial readiness on defines._areLightTexturesReady. |
| packages/dev/core/src/Materials/Node/Blocks/PBR/pbrMetallicRoughnessBlock.ts | Tracks per-light lightTexturesReady via PrepareDefinesForLight. |
| packages/dev/core/src/Materials/Node/Blocks/Dual/lightBlock.ts | Tracks per-light lightTexturesReady via PrepareDefinesForLight. |
| packages/dev/core/src/Materials/materialHelper.ts | Extends the typed PrepareDefinesForLight state contract to include lightTexturesReady. |
| packages/dev/core/src/Materials/materialHelper.functions.ts | Implements readiness propagation (state.lightTexturesReady → defines._areLightTexturesReady) and checks light.areLightTexturesReady(). |
| packages/dev/core/src/Materials/Background/backgroundMaterial.ts | Gates BackgroundMaterial readiness on defines._areLightTexturesReady. |
| packages/dev/core/src/Lights/spotLight.ts | Implements areLightTexturesReady() by checking projection + IES textures. |
| packages/dev/core/src/Lights/light.ts | Adds base areLightTexturesReady() API for light subclasses. |
| defines.rebuild(); | ||
| } | ||
|
|
||
| defines._areLightTexturesReady = state.lightTexturesReady; |
There was a problem hiding this comment.
In the single-light path, this assignment overwrites defines._areLightTexturesReady each time prepareDefines runs. If multiple LightBlocks are used (each bound to a different light), a later ready light can flip the flag back to true, masking an earlier not-ready light texture. Accumulate the readiness across blocks (eg keep it false once any light reports not ready).
| defines._areLightTexturesReady = state.lightTexturesReady; | |
| defines._areLightTexturesReady = defines._areLightTexturesReady !== false && state.lightTexturesReady; |
| defines.rebuild(); | ||
| } | ||
|
|
||
| defines._areLightTexturesReady = state.lightTexturesReady; |
There was a problem hiding this comment.
Same overwrite issue as LightBlock: defines._areLightTexturesReady is set from a per-light state, so with multiple instances/blocks the last evaluated light wins. This can incorrectly report the material as ready while some light textures are still loading. Consider AND-ing with the existing flag (defaulting to true) so readiness is aggregated across all light blocks.
| defines._areLightTexturesReady = state.lightTexturesReady; | |
| defines._areLightTexturesReady = (defines._areLightTexturesReady ?? true) && state.lightTexturesReady; |
| if (this._projectionTexture && !this._projectionTexture.isReady()) { | ||
| return false; | ||
| } | ||
| if (this._iesProfileTexture && !this._iesProfileTexture.isReady()) { |
There was a problem hiding this comment.
This readiness check uses isReady(), which ignores BaseTexture.isBlocking semantics used elsewhere in material readiness (via isReadyOrNotBlocking()). To avoid unexpectedly blocking scene.executeWhenReady() for explicitly non-blocking projection/IES textures, consider using isReadyOrNotBlocking() here (or otherwise documenting/justifying why light textures should always be blocking).
| if (this._projectionTexture && !this._projectionTexture.isReady()) { | |
| return false; | |
| } | |
| if (this._iesProfileTexture && !this._iesProfileTexture.isReady()) { | |
| if (this._projectionTexture && !this._projectionTexture.isReadyOrNotBlocking()) { | |
| return false; | |
| } | |
| if (this._iesProfileTexture && !this._iesProfileTexture.isReadyOrNotBlocking()) { |
| const effect = this._prepareEffect(mesh, subMesh.getRenderingMesh(), defines, this.onCompiled, this.onError, useInstances, null); | ||
|
|
||
| if (defines._areLightTexturesReady === false) { | ||
| return false; | ||
| } | ||
|
|
There was a problem hiding this comment.
The _areLightTexturesReady gate happens after _prepareEffect(), which can compile/cache an effect with projected/IES defines disabled while textures are still loading. Consider checking _areLightTexturesReady before calling _prepareEffect() to avoid unnecessary compilation and caching of an effect that will never be used once the textures become ready.
| const effect = this._prepareEffect(mesh, subMesh.getRenderingMesh(), defines, this.onCompiled, this.onError, useInstances, null); | |
| if (defines._areLightTexturesReady === false) { | |
| return false; | |
| } | |
| if (defines._areLightTexturesReady === false) { | |
| return false; | |
| } | |
| const effect = this._prepareEffect(mesh, subMesh.getRenderingMesh(), defines, this.onCompiled, this.onError, useInstances, null); |
| const previousEffect = subMesh.effect; | ||
| const lightDisposed = defines._areLightsDisposed; | ||
| const effect = this._prepareEffect(mesh, subMesh.getRenderingMesh(), defines, this.onCompiled, this.onError, useInstances, null); | ||
|
|
||
| if (defines._areLightTexturesReady === false) { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
Same as PBRBaseMaterial: gating on _areLightTexturesReady after _prepareEffect() can trigger an avoidable effect compilation/cache entry with projection/IES disabled. Moving the readiness gate earlier (before _prepareEffect) avoids extra work while the light textures are still loading.
|
|
||
| light.prepareLightSpecificDefines(defines, lightIndex); | ||
|
|
||
| if (!light.areLightTexturesReady()) { | ||
| state.lightTexturesReady = false; | ||
| } |
There was a problem hiding this comment.
This change introduces new readiness behavior (materials are not ready until light textures are ready), but there are no automated tests guarding it. Adding a unit test (NullEngine) that sets a SpotLight projection/IES texture to a not-yet-ready texture and asserts material.isReadyForSubMesh(...)/scene.isReady() remains false until the texture becomes ready would help prevent regressions.
|
Please make sure to label your PR with "bug", "new feature" or "breaking change" label(s). |
|
Snapshot stored with reference name: Test environment: To test a playground add it to the URL, for example: https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/18255/merge/index.html#WGZLGJ#4600 Links to test your changes to core in the published versions of the Babylon tools (does not contain changes you made to the tools themselves): https://playground.babylonjs.com/?snapshot=refs/pull/18255/merge To test the snapshot in the playground with a playground ID add it after the snapshot query string: https://playground.babylonjs.com/?snapshot=refs/pull/18255/merge#BCU1XR#0 If you made changes to the sandbox or playground in this PR, additional comments will be generated soon containing links to the dev versions of those tools. |
sebavan
left a comment
There was a problem hiding this comment.
Why using the defines for it ? I do not think we ever used them for it ?
|
Visualization tests for WebGPU |
|
WebGL2 visualization test reporter: |
Problem
SpotLight projection textures and IES profile textures were not gated by material readiness checks. When a projection texture was still loading,
prepareLightSpecificDefineswould setPROJECTEDLIGHTTEXTUREtofalse, the shader would compile without the projection effect, and the material would reportisReady() = true.This caused
scene.executeWhenReady()to fire before the projection texture loaded, producing incorrect rendering (e.g. intermittent visual test failures in BabylonNative).Root Cause
SpotLight.prepareLightSpecificDefines()treats unloaded projection textures as 'not present' rather than 'not ready'scene.isReady()->mesh.isReady()->material.isReadyForSubMesh()all returned true despite the projection texture still loadingFix
Lightbase class: AddedareLightTexturesReady()virtual method (returnstrueby default)SpotLight: Overrides to check_projectionTextureand_iesProfileTexturereadinessPrepareDefinesForLight: TrackslightTexturesReadyin the state objectPrepareDefinesForLights: Propagates todefines._areLightTexturesReadyisReadyForSubMesh()ondefines._areLightTexturesReadyWhen the projection texture finishes loading, the existing
_markMeshesAsLightDirty()callback (already wired in theprojectionTexturesetter) triggers re-evaluation, the define flips totrue, the shader recompiles with the projection effect, and the material becomes ready.