Skip to content

Fix repeated PBR lighting contributions across light batches - #2999

Open
toaster0123 wants to merge 1 commit into
jMonkeyEngine:masterfrom
toaster0123:fix/pbr-static-light-contributions-20261002
Open

toaster0123 wants to merge 1 commit into
jMonkeyEngine:masterfrom
toaster0123:fix/pbr-static-light-contributions-20261002

Conversation

@toaster0123

Copy link
Copy Markdown
Contributor

Problem

PBR objects become brighter when the same lights are split into more batches. Each extra draw adds the material's emission and baked light again, even if those lights are black. Some multi-probe scenes also add environment lighting again.

For example, an emissive RGB value of (0.1, 0.2, 0.3) stays correct with three black directional lights in one batch. Splitting those lights into three batches changes the rendered value to (0.3, 0.6, 0.9).

Fix

Mark the first lighting pass in SinglePassAndImageBasedLightingLogic. In the final PBR color, add emission, baked lighting, and environment lighting only on that pass. Direct lighting still accumulates on every pass.

The marker is reset for each material render, including objects with no direct lights. Material parameters, shader defines, alpha, fog, and debug output behavior are unchanged.

Validation

  • Eight regression tests exercise the real PBR material with a recording renderer: no lights, ambient-only, probe-only, one/multiple/partial batches, mixed probes and direct lights, and shader reuse. All eight fail against the old pass-upload logic and pass with this change.
  • Core build plus desktop, effects, plugins, and terrain checks: 568 passed, two existing skips, no failures. The new test file has no Checkstyle warnings.
  • Separate native framebuffer checks use the production LWJGL3 renderer on Mesa OpenGL 4.5 core and OpenGL ES 3.2, with both RGBA8 and floating-point targets. Emission and baked-light contributions stay constant across batch sizes, while positive direct lights still add their expected contribution. A desktop three-probe case is corrected as well.
  • The native checks also rerender the same material after removing its lights to verify that the first-pass state resets.

The native checks are separate from the unit tests. The existing OpenGL ES shader-compilation issue with two or three probes prevents those GLES cases from drawing and is outside this patch. Existing additive alpha accumulation is also unchanged.

Upload a first-pass marker and add baked light, environment light, and emission only once. Keep direct lighting in every batch and cover the per-draw contract with regression tests.
Uniform lightData = shader.getUniform("g_LightData");
lightData.setVector4Length(numLights * 3);//8 lights * max 3
Uniform lightCount = shader.getUniform("g_LightCount");
// Emission and indirect lighting belong to the first pass only.

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.

Small hardening thought: this logic class can be used by techniques of the MaterialDef whose fragment shader doesn't declare g_IsFirstLightPass (glow/shadow/post variants), and Shader.getUniform() returns null for unknown names — that would turn into an NPE at render time. The other uploads in this method assume their uniforms exist, so a one-line guard here keeps the new uniform safe to add:

Suggested change
// Emission and indirect lighting belong to the first pass only.
Uniform firstLightPass = shader.getUniform("g_IsFirstLightPass");
if (firstLightPass != null) {
firstLightPass.setValue(VarType.Boolean, startIndex == 0);
}

(Uniform is already imported for g_AmbientLightColor below.) Fully optional, but cheap insurance.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Checked at 2725428: Shader.getUniform() creates and stores a Uniform when the name is absent, then returns it (Shader.java, lines 293–301). It does not return null here. If the GLSL does not declare that uniform, GLRenderer resolves its location to -1 and skips the upload. The existing call is safe for that case, so I’ve left it unchanged.

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.

Good catch — you're right on both counts, and I checked before answering.

Shader.getUniform(name) creates the Uniform if the name isn't present and returns it, so it never comes back null here. And when the GLSL doesn't declare it, the location resolves to -1 and the upload is skipped, so nothing blows up for a technique like glow/shadow/post.

That also means the existing call already matches the surrounding code (g_LightData, g_LightCount and g_AmbientLightColor are all fetched and set the same way), so the current version is more consistent than my suggestion. Sorry for the churn — good fix, and my previous nit is withdrawn.

@jaime-jmebot jaime-jmebot 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.

Nice fix — the root cause is clear and the solution is minimal and well targeted.

  • Moving emission, baked light and environment light behind a "first pass only" guard in PBRLighting.frag is the right lever: direct lighting still accumulates per pass, so batched lights keep working while static contributions are applied exactly once.
  • The flag is set unconditionally from startIndex == 0 in updateLightListUniforms, which also covers objects with no lights at all, so state can't leak into the next material render.
  • The new regression tests are a good match for the change: they exercise the real PBR Lighting material through a recording renderer across batch splits, probes and shader reuse, which is exactly the regression class we're guarding against.

Only nit is inline: make the new uniform upload null-safe so a technique without that declaration can't NPE. Otherwise this looks ready to go.

@riccardobl
riccardobl marked this pull request as ready for review October 2, 2026 17:46
@jaime-jmebot

Copy link
Copy Markdown
Contributor

Small correction to my review above: I was wrong about the null-guard nit. Shader.getUniform() creates and returns a Uniform when the name isn't present, and one that isn't in the compiled GLSL just gets no GL location and is skipped on upload — so the existing call is already safe for techniques that don't declare the flag. I've dropped that inline comment.

Everything else in the review stands: applying emission, baked light and environment light only on the first pass is the right fix, and the tests cover the batch-split regression well. Looks good to merge from my side. 👍

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.

2 participants