Update lights to be capped per light type and avoid shader recompilations - #2237
Merged
Conversation
three.js keys its shader programs on how many lights of each kind it can see: NUM_POINT_LIGHTS, NUM_SPOT_LIGHTS, NUM_SPOT_LIGHT_MAPS and the matching shadow counts. The light budget was enforced on a single list shared by every kind, so the *total* stayed pinned to the cap while the mix drifted as the camera moved - 10 point + 10 spot one frame, 11 + 9 the next. Every mix that had not been seen before cost a shader compile and link for each affected material, which the player feels as a dropped frame. The weighted shadow budget had the same problem from the other end: a spot light with a projected texture took two slots of a budget measured in cost, so a full budget could hold four cheap casters or two expensive ones, and the *count* moved while the weight stayed put. So the budget is now per light kind, and capped by count rather than by weight. Textured spot lights keep their cost awareness by losing ties against cheaper lights instead of taking two slots. Measured over 90 frames of camera movement, comparing against v1.0.2: 40 point + spot lights 4 -> 1 shader programs 40 point + spot, with shadows 10 -> 3 40 spot lights, half textured 10 -> 2 Light counts that change because the game itself creates, destroys or hides lights are unaffected - those genuinely change what is lit. Note that "maximum light count" is now read per light kind rather than across all of them, so a scene mixing point and spot lights can light up to that many of each.
Texture units and varyings are hard limits, not performance trade-offs.
three.js binds one texture unit per shadow map and one more for a spot
light's projected texture, and spends a varying on every shadow casting
light. Going over what the driver allows does not make the scene slower -
the shader fails to link and everything using that material renders
black. Measured on r160, with the error the driver reports:
point light + shadow 1 texture unit each
spot light + shadow 1
spot light with a projected texture 1
spot light with both 2
32 shadow casters "Could not pack varying
vPointShadowCoord"
16 of the last kind, plus a material
with 6 textures "FRAGMENT shader texture image
units count exceeds
MAX_TEXTURE_IMAGE_UNITS(32)"
Only 16 fragment texture units and 16 varyings are guaranteed across
devices, and a material wants most of those, so a fixed budget cannot be
safe everywhere. It is now derived from what the renderer reports, split
per light kind, and computed once - so the numbers three.js compiles
into its shaders still never move between frames.
Simulating the guaranteed minimum device (16 units, 16 varyings, 224
fragment uniform vectors), every scenario now stays inside 16 texture
units, worst case 14, while still lighting 7 to 20 lights. Previously a
scene of spot lights projecting textures reached 18 units on its own,
which would not have linked there.
The game's configured maxima still apply; they are clamped to the device
through a getter, so raising them at runtime keeps working. When the
device forces a lower budget it is logged once rather than silently
applied.
The budget assumed every light cost 4 fragment uniform vectors. Measured on r160 against a MeshStandardMaterial, that only holds for a point light that casts no shadow: point light 4 uniform vectors spot light 7 point light + shadow 14.6 spot light + shadow 15.6 So shadow casters were being under-counted by roughly four times. On a device reporting the guaranteed minimum of 224 fragment uniform vectors the budget allowed 10 lights per kind, which the corrected model puts at 190 vectors before the material's own are counted - close enough to the ceiling to matter. Shadow casters are now charged 12 vectors on top of the 8 a lit light is charged, and are paid for before the lit count is worked out. The guaranteed-minimum device now gets 5 lights per kind instead of 10, which lands at 192 of its 224 vectors in the worst case. Texture units and varyings are unchanged, and all eight test scenarios still link there.
D8H
reviewed
Aug 22, 2026
The device budget was split three ways unconditionally, one share per light kind the engine knows of, so a scene lit by point lights alone had three quarters of its texture units held back for kinds that never showed up. On a device reporting the common 16 fragment texture units that left 2 shadow casters where 4 were asked for and 4 used to be granted. The kind count now comes from the kinds a scene has actually used, so that same scene gets the whole budget for its one kind. Kinds are only ever added, never dropped, so the numbers three.js compiles into its shaders stay pinned: a light going away for a while must not widen the budget of the others. Measured with the real function, shadow casters per kind: device before after minimum guaranteed 1 3 desktop, 16 texture units 2 4 desktop, 32 texture units 4 4 Also correct the guaranteed varying minimum to 15: GLES3 guarantees MAX_VARYING_COMPONENTS 60, which is 15 vec4 and not 16. It changes no result (both leave one caster per kind at the minimum) but the fallback should say what the spec says. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011chSAxDN9jSEwWkowwzDBm
The budget change is a fix to what 1.0.3 already does, not a new feature series, so it stays on that version rather than opening a 1.0.4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011chSAxDN9jSEwWkowwzDBm
- Evict lights that no longer fit the moment the device budget is shared with a new light kind, so a frame never renders over the budget. - Read the renderer on every budget recomputation instead of capturing it once, so a scene created before the renderer exists is not stuck on the guaranteed minimums. - Say in the max lights count descriptions that the caps are per light kind and that devices can enforce a lower limit. - Drop a stale mention of distance inflation for ties in insertByDistance.
Collaborator
Author
|
Summary of the whole change to 3D lights:
Generated by Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See https://github.com/4ian/GDevelop/pull/8998/changes - this allows a bit more lights of different types (ensuring they don't go through the budget of the device) and also reduce the "churn" in shaders being forced to be recompiled because the amount of lights changed