Skip to content

engine: support up to 4 additional shadowless directional lights - #10248

Open
yuvaltassa wants to merge 2 commits into
google:mainfrom
yuvaltassa:multi-directional
Open

engine: support up to 4 additional shadowless directional lights#10248
yuvaltassa wants to merge 2 commits into
google:mainfrom
yuvaltassa:multi-directional

Conversation

@yuvaltassa

Copy link
Copy Markdown

Allow a scene to contain up to CONFIG_MAX_EXTRA_DIRECTIONAL_LIGHTS (4) directional lights in addition to the dominant one; the additional lights are evaluated without shadows. The dominant directional light (highest intensity) behaves exactly as before — it owns slot 0 of the light SoA, cascaded shadow maps, and the sun disc — and directional lights beyond the cap are ignored, preserving the previous dominant-light selection semantics as the degenerate case.

The feature is automatic and free for the common single-directional-light case: a new dynamic specialization constant (RUNTIME_CONFIG_HAS_EXTRA_DIRECTIONAL_LIGHTS, following the RUNTIME_CONFIG_HAS_DYNAMIC_LIGHTING pattern from #10095) is set per view when the scene contains more than one directional light, so the extra-lights loop compiles away entirely in programs specialized for single-light scenes.

Core changes:

  • Scene: FScene::prepare collects all directional lights and partial-sorts them by intensity; the dominant one is stored at slot 0 as before, the next 4 are kept in SceneCacheData side arrays (direction + LightManager instance). They never enter the froxelizer or ShadowMapManager.
  • Uniforms: new extraLightDirection[4], extraLightColorIntensity[4] and extraLightCount fields in PerViewUib, carved out of the existing reserved slack (the struct remains exactly 2 KiB). Light channels are packed into extraLightDirection[i].w.
  • Shaders: new evaluateExtraDirectionalLights() loop in surface_light_directional.fs under the existing VARIANT_HAS_DIRECTIONAL_LIGHTING — no new variants. The loop is gated by the dynamic specialization constant; extras skip shadowing and the sun disc.
  • DynamicSpecConstKey gets an EXTRA_DIRECTIONAL_LIGHTS bit, set from FView::hasExtraDirectionalLights() when building render passes; canSupportExtraDirectionalLights limits it to lit surface variants with directional lighting, bounding the program-key growth.
  • MATERIAL_VERSION bumped to 75 (new PerViewUib layout and dynamic specialization constant).
  • LightManager.h documentation and release notes updated.

Tests:

  • FilamentTest.MultipleDirectionalLights: dominant selection, extras ordering by intensity, direction transforms, truncation past the cap, and the single-light case.
  • RenderingTest.MultipleDirectionalLights: headless render of a quad first lit by a single green directional light (verifying no red contribution), then with a weaker red directional light added (verifying red appears) — which also exercises the automatic program re-specialization.

@poweifeng poweifeng added the internal Issue/PR does not affect clients label Jul 28, 2026

@pixelflinger pixelflinger left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I'd like @show50726 to review the spec constant stuff.

Comment on lines +149 to +152
// Sort the directional lights by decreasing intensity. The most intense one is the
// dominant directional light (stored at index 0 of the LightSoa); it is the only one
// that casts shadows and can be a sun. Up to CONFIG_MAX_EXTRA_DIRECTIONAL_LIGHTS
// additional directional lights are evaluated without shadows; the rest are ignored.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will do for now, but I'm starting to think we should have a setDominantLight() on scene or view, or something.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged.

return;
}
for (int i = 0; i < CONFIG_MAX_EXTRA_DIRECTIONAL_LIGHTS; i++) {
if (i < frameUniforms.extraLightCount) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason of not breaking the loop earlier?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feature level 0, mostly. This loop also compiles and, with several directional lights in the scene, runs at ES2, where the constant is emulated via the injected SPIRV_CROSS_CONSTANT_ID define rather than a real spec constant. Strict GLSL ES 1.00 front-ends only guarantee constant-bounded loops, so I didn't want to rely on a data-dependent break being accepted there. Happy to change it if you'd rather special-case FL0.

@show50726

Copy link
Copy Markdown
Contributor

We might want to update the material precompile api as well:

void FEngine::compile(
CompilerPriorityQueue const priority,
FMaterial const* material,
FView const* view,
tribool const shadowReceiver,
tribool const skinning,
CallbackHandler* handler,
Invocable<void(Material*)>&& callback) {
auto const variants = getMaterialCompileVariants(view, material, shadowReceiver, skinning);
auto const dynamicSpecConstKeys = getMaterialCompileDynamicSpecConstKey(view, material);
const_cast<FMaterial*>(material)->compile(priority, variants, dynamicSpecConstKeys, handler,
std::move(callback));

You will have to update getMaterialCompileDynamicSpecConstKey to set the extra light according to the view parameter

@yuvaltassa

Copy link
Copy Markdown
Author

@show50726 getMaterialCompileDynamicSpecConstKey is updated in this PR. Your link points at the file on main, so the hunk may have been easy to miss. Let me know if you had something beyond that in mind.

baseKey.setExtraDirectionalLights(isMaterialLit && view->hasExtraDirectionalLights());

Allow a scene to contain up to CONFIG_MAX_EXTRA_DIRECTIONAL_LIGHTS (4)
directional lights in addition to the dominant one; the additional
lights are evaluated without shadows. The dominant directional light
(highest intensity) behaves as before: it owns slot 0 of the light
SoA, cascaded shadow maps and the sun disc. Directional lights beyond
the cap are ignored, preserving the previous dominant-light selection
semantics as the degenerate case.

The feature is automatic and adds no cost to the common single-light
case: a new dynamic specialization constant
(RUNTIME_CONFIG_HAS_EXTRA_DIRECTIONAL_LIGHTS, following the
RUNTIME_CONFIG_HAS_DYNAMIC_LIGHTING pattern) is set per view when the
scene contains more than one directional light, so the extra lights
loop compiles away entirely in programs specialized for single-light
scenes.

- Scene: FScene::prepare collects all directional lights and
  partial-sorts them by intensity; the extras are kept in
  SceneCacheData and never enter the froxelizer or ShadowMapManager.
- Uniforms: new extraLightDirection[4], extraLightColorIntensity[4]
  and extraLightCount fields in PerViewUib, carved out of the existing
  reserved slack (the struct remains exactly 2 KiB). Light channels
  are packed into extraLightDirection[i].w.
- Shaders: new evaluateExtraDirectionalLights() loop in
  surface_light_directional.fs, gated by the dynamic specialization
  constant, under the existing VARIANT_HAS_DIRECTIONAL_LIGHTING
  variant, so no new variants are introduced.
- DynamicSpecConstKey gets an EXTRA_DIRECTIONAL_LIGHTS bit, set from
  FView::hasExtraDirectionalLights() when building render passes.
- MATERIAL_VERSION bumped to 75 (new PerViewUib layout and dynamic
  specialization constant).
- LightManager.h documentation and release notes updated.

Tested by FilamentTest.MultipleDirectionalLights (dominant selection,
extras ordering, direction transforms, truncation past the cap) and
RenderingTest.MultipleDirectionalLights (headless render of a quad
first lit by a single green directional light, verifying no red
contribution, then adding a weaker red directional light and
verifying the red contribution appears, which also exercises the
automatic program re-specialization).
@yuvaltassa

Copy link
Copy Markdown
Author

Rebased onto main to resolve a release-notes conflict from the 1.74.1 branch. While at it, canSupportExtraDirectionalLights now excludes depth/SSR variants before reading the directional-lighting bit, mirroring the dynamic-lighting guard after #10241.

s.extraLightColorIntensity[i] = float4{
lcm.getColor(li), lcm.getIntensity(li) * exposure };
}
s.extraLightCount = int32_t(count);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: use static_cast<int32_t>

@show50726 show50726 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec const part looks good to me!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

internal Issue/PR does not affect clients

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants