Skip to content

Improve light render parity with blender#2549

Open
riccardobl wants to merge 5 commits intojMonkeyEngine:masterfrom
riccardobl:blenderlight
Open

Improve light render parity with blender#2549
riccardobl wants to merge 5 commits intojMonkeyEngine:masterfrom
riccardobl:blenderlight

Conversation

@riccardobl
Copy link
Member

When importing a gltf model, lights look nothing like in blender.
Turns out jme doesn't use proper physically based light attenuation, and that blender might be doing some shenanigans in spot lights.

In this PR i've patched jme to make it much more closer to blender lighting.
See screenshot below:

Blender:

Screenshot_20250806_180541

JME Without this PR:

Screenshot_20250806_180514

JME With this PR:

Screenshot_20250806_180414

It is still not perfect, but much better i think.

This PR needs more work, mostly to figure out how to merge it, since it will break all the existing scenes setup.
I am looking for suggestions on this regard (maybe we can make it toggleable with a PBRLight param?)

@github-actions
Copy link

github-actions bot commented Aug 10, 2025

🖼️ Screenshot tests have failed.

The purpose of these tests is to ensure that changes introduced in this PR don't break visual features. They are visual unit tests.

📄 Where to find the report:

  • Go to the (failed run) > Summary > Artifacts > screenshot-test-report
  • Download the zip and open jme3-screenshot-tests/build/reports/ScreenshotDiffReport.html

⚠️ If you didn't expect to change anything visual:
Fix your changes so the screenshot tests pass.

If you did mean to change things:
Review the replacement images in jme3-screenshot-tests/build/changed-images to make sure they really are improvements and then replace and commit the replacement images at jme3-screenshot-tests/src/test/resources.

If you are creating entirely new tests:
Find the new images in jme3-screenshot-tests/build/changed-images and commit the new images at jme3-screenshot-tests/src/test/resources.

Note; it is very important that the committed reference images are created on the build pipeline, locally created images are not reliable. Similarly tests will fail locally but you can look at the report to check they are "visually similar".

See https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-screenshot-tests/README.md for more information

Contact @richardTingle (aka richtea) for guidance if required

float clampedDist = max(dist, 0.00001);
light.fallOff = clamp(1.0 / (clampedDist * clampedDist), 0.0, 1.0);
light.fallOff = clamp(light.fallOff, 1.0 - posLight, 1.0);
light.fallOff *= step(dist, radius);
Copy link
Member Author

Choose a reason for hiding this comment

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

this is the inverse square falloff used by blender with a radius cut-off, since jme allows to set the radius of lights

JsonArray color = el.getAsJsonArray();
return new ColorRGBA(color.get(0).getAsFloat(), color.get(1).getAsFloat(), color.get(2).getAsFloat(), color.size() > 3 ? color.get(3).getAsFloat() : 1f);
return new ColorRGBA().setAsSrgb(
color.get(0).getAsFloat(), color.get(1).getAsFloat(), color.get(2).getAsFloat(), color.size() > 3 ? color.get(3).getAsFloat() : 1f
Copy link
Member Author

Choose a reason for hiding this comment

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

gltf colors are srgb

public class LightsPunctualExtensionLoader implements ExtensionLoader {
private static final boolean COMPUTE_LIGHT_RANGE = true;
private static final boolean APPLY_INTENSITY_CONVERSION = true;
private static final boolean SKIP_HDR_CONVERSION = true;
Copy link
Member Author

Choose a reason for hiding this comment

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

you can use these toggles to test with and without the patches

intensity = intensity / solidAngle;
}

float range = obj.has("range") ? obj.get("range").getAsFloat() : (COMPUTE_LIGHT_RANGE ? getCutoffDistance(color, intensity) : Float.POSITIVE_INFINITY);
Copy link
Member Author

@riccardobl riccardobl Aug 10, 2025

Choose a reason for hiding this comment

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

The POSITIVE_INFINITY range was an error to begin with and required a patch to turn it into FLOAT_MAX_VALUE #2058 , but it ends up introducing some floating point errors with large numbers that makes the light calculation a bit off.

Truth is: jme doesn't support infinite lights very well.
So this patch calculates the right range based on the falloff function.

vec3 spotDir = normalize(light.spotDirection);
float cosA = dot(-L, spotDir) ;
float spotAtten = clamp((cosA - outerAngleCos) / (innerAngleCos - outerAngleCos), 0.0, 1.0);
light.fallOff *= pow(spotAtten, sharpnessFactor);
Copy link
Member Author

Choose a reason for hiding this comment

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

This smooths out the angular attenuation. It doesn't look exactly like blender's.

@richardTingle
Copy link
Member

Screenshots look much nicer. With the exception of my comment on the println: looks good!

@richardTingle
Copy link
Member

richardTingle commented Aug 10, 2025

I am looking for suggestions on this regard (maybe we can make it toggleable with a PBRLight param?)

Maybe in the short term a parameter that defaults to false and at some point (on a major version change) the default flips to true and then a few versions later the parameter gets deleted

@riccardobl riccardobl added the defect Something that is supposed to work, but doesn't. Less severe than a "bug" label Aug 10, 2025
@yaRnMcDonuts
Copy link
Member

yaRnMcDonuts commented Aug 18, 2025

I never knew this was an issue, but I have certainly noticed that JME's point lights illuminate the scene way too much, and it is near impossible to tweak them properly in dark scenes without ruining the darkness.

I could never make a proper bright torch in a dark room without it completely brightening the entire room. Then if I turn the brightness or radius down to fix that issue, the torch becomes too dim close up and looks bad. Its like I could never properly tweak point lights.

So I am very excited for this PR!

Maybe in the short term a parameter that defaults to false and at some point (on a major version change) the default flips to true and then a few versions later the parameter gets deleted

This sounds like a good solution. We could probably even make it true by default right away in the next alpha version, that would be beneficial in the short term for alpha and beta testing, that way testers will be forced to test this (in my experience with 3.8, its better to force new changes on people using alpha versions, otherwise people will overlook the togglable option and it won't get tested as much). Then (depending on feedback) we can decide whether it should default to true/false for the next stable release.

if(APPLY_INTENSITY_CONVERSION) intensity = intensity / (4.0f * FastMath.PI);

float range = obj.has("range") ? obj.get("range").getAsFloat() : (COMPUTE_LIGHT_RANGE ? getCutoffDistance(color, intensity) : Float.POSITIVE_INFINITY);
color = buildLinearLightColor(color, intensity);
Copy link
Contributor

Choose a reason for hiding this comment

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

Hi @riccardobl , it seems that the buildLinearLightColor(color, intensity) method is missing. After reviewing the file, it should probably be replaced with the lumensToColor(color, intensity) method call.

Copy link
Member Author

Choose a reason for hiding this comment

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

thanks, the code got botched when i merged it from my fork. Should be fixed now

private void addLight(Node parent, Node node, int lightIndex) {
if (lightDefinitions.containsKey(lightIndex)) {
Light light = lightDefinitions.get(lightIndex);
light = light.clone();
Copy link
Contributor

@capdevon capdevon Aug 27, 2025

Choose a reason for hiding this comment

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

This is quite an important change. What kind of problem did you encounter by not cloning the light object taken from the lightDefinitions map? Could it be that the lightIndex is associated with multiple nodes, causing inconsistencies? Just curious

Copy link
Member Author

@riccardobl riccardobl Aug 27, 2025

Choose a reason for hiding this comment

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

This is a fix.
The LightControl cannot properly move a light with its parent spatial, if it shared among multiple spatials.

Enabling #2548 by default in the gltf loader, makes the light control obsolete, so it could be removed and this fix could be avoided, but i am not sure that jme can share light instances, anyway.

Copy link
Contributor

@capdevon capdevon Aug 27, 2025

Choose a reason for hiding this comment

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

Based on your explanation, cloning the object seems like the right approach. I'm not sure what effect sharing the same light source among multiple nodes would have, but I suspect it wouldn't be appropriate.
Thanks for the clarification ;)

@riccardobl
Copy link
Member Author

@gemini-code-assist do a full review of this

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces changes to improve lighting parity with Blender, specifically by implementing physically based light attenuation and updating intensity calculations for glTF lights. The changes in the GLSL shader for falloff and the Java code for light loading are significant steps toward more realistic lighting.

My review focuses on ensuring spec compliance and improving the maintainability of the new features. I've pointed out an issue with color space conversion that deviates from the glTF specification and could cause problems with models from other sources. I've also suggested making the new lighting behaviors configurable to provide a non-breaking upgrade path for existing projects, as you mentioned this is a concern. Finally, there's a suggestion to improve code quality by refactoring hardcoded constants.

Overall, this is a great improvement for PBR lighting. Addressing these points will make the changes more robust and easier to integrate.

Comment on lines +742 to +744
return new ColorRGBA().setAsSrgb(
color.get(0).getAsFloat(), color.get(1).getAsFloat(), color.get(2).getAsFloat(), color.size() > 3 ? color.get(3).getAsFloat() : 1f
);

Choose a reason for hiding this comment

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

high

According to the glTF 2.0 specification for the KHR_lights_punctual extension, light colors are defined in linear space. This change incorrectly applies an sRGB-to-linear conversion to them by using setAsSrgb. While this is correct for some material properties like baseColorFactor, it will cause lights from spec-compliant glTF files to appear darker than intended.

To ensure correctness for all glTF files, I recommend creating a separate utility method for sRGB color conversion. For example:

  1. Revert getAsColor to its original implementation (without setAsSrgb).
  2. Create a new method, e.g., getAsSrgbColor, that performs the sRGB conversion.
  3. Update the GltfLoader to use getAsSrgbColor for material properties that are in sRGB space.

This will maintain spec compliance for lights while correctly handling sRGB material colors.

        return new ColorRGBA(color.get(0).getAsFloat(), color.get(1).getAsFloat(), color.get(2).getAsFloat(), color.size() > 3 ? color.get(3).getAsFloat() : 1f);

Comment on lines +59 to +61
private static final boolean COMPUTE_LIGHT_RANGE = true;
private static final boolean APPLY_INTENSITY_CONVERSION = true;
private static final boolean SKIP_HDR_CONVERSION = true;

Choose a reason for hiding this comment

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

medium

These flags are useful for development. As you noted in the PR description, these changes might break existing scenes. To provide a smoother upgrade path and more flexibility, consider making these features configurable through GltfModelKey instead of hardcoded flags. This would allow users to opt-in to the new lighting model on a per-model basis.

Comment on lines +318 to +319
final float visibleThreshold = 0.001f;
final float maxRange = 10000f;

Choose a reason for hiding this comment

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

medium

It's good practice to define constants like visibleThreshold and maxRange as private static final fields at the class level. This improves readability and makes them easier to find and modify.

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

Labels

defect Something that is supposed to work, but doesn't. Less severe than a "bug"

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants