Skip to content

Update lights to be capped per light type and avoid shader recompilations - #2237

Merged
4ian merged 7 commits into
mainfrom
claude/threejs-gdevelop-lighting-3h4nka
Aug 23, 2026
Merged

Update lights to be capped per light type and avoid shader recompilations#2237
4ian merged 7 commits into
mainfrom
claude/threejs-gdevelop-lighting-3h4nka

Conversation

@4ian

@4ian 4ian commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

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

claude added 3 commits August 22, 2026 00:46
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.
@4ian
4ian requested a review from a team as a code owner August 22, 2026 15:19
Comment thread extensions/reviewed/Light3D.json Outdated
4ian and others added 4 commits August 22, 2026 23:58
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.

4ian commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator Author

Summary of the whole change to 3D lights:

  • Lights are now budgeted per light kind (point lights, spot lights, spot lights projecting a texture) instead of a single weighted total, so the numbers three.js compiles into its shaders stay stable and the mix of visible lights no longer forces shader recompilations (a stutter the player feels).
  • The budget is derived from the device's real WebGL capabilities (texture units, varyings, uniform vectors) and shared between the kinds a scene actually uses — going past what the device can take previously made shaders fail to link, rendering everything black.
  • The nearest lights and shadow casters are selected by distance to the camera; when a new light kind appears and the budget shrinks, lights that no longer fit are evicted immediately so a frame never renders over budget.
  • The "max lights count" settings and descriptions now reflect that the caps apply per light kind, with the device enforcing a lower limit when needed.

Generated by Claude Code

@4ian
4ian merged commit 1ba297a into main Aug 23, 2026
5 checks passed
@4ian
4ian deleted the claude/threejs-gdevelop-lighting-3h4nka branch August 23, 2026 16:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants