Skip to content

Improve light render parity with blender #2549

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -235,29 +235,23 @@
float dist;
Math_lengthAndNormalize(light.vector,dist,L);

float invRange=light.invRadius; // position.w
const float light_threshold = 0.01;

#ifdef SRGB
light.fallOff = (1.0 - invRange * dist) / (1.0 + invRange * dist * dist); // lightDir.w
light.fallOff = clamp(light.fallOff, 1.0 - posLight, 1.0);
#else
light.fallOff = clamp(1.0 - invRange * dist * posLight, 0.0, 1.0);
#endif

// computeSpotFalloff
if(light.type>1.){
vec3 spotdir = normalize(light.spotDirection);
float curAngleCos = dot(-L, spotdir);
// Standard falloff
float radius = 1./light.invRadius;
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


// Spot cone falloff
if (light.type > 1.0) {
float innerAngleCos = floor(light.spotAngleCos) * 0.001;
float outerAngleCos = fract(light.spotAngleCos);
float innerMinusOuter = innerAngleCos - outerAngleCos;
float falloff = clamp((curAngleCos - outerAngleCos) / innerMinusOuter, 0.0, 1.0);
#ifdef SRGB
// Use quadratic falloff (notice the ^4)
falloff = pow(clamp((curAngleCos - outerAngleCos) / innerMinusOuter, 0.0, 1.0), 4.0);
#endif
light.fallOff*=falloff;
float sharpnessFactor = 4.0;

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.

}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,9 @@ public static ColorRGBA getAsColor(JsonObject parent, String name) {
return null;
}
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 static ColorRGBA getAsColor(JsonObject parent, String name, ColorRGBA defaultValue) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
* Created by Trevor Flynn - 3/23/2021
*/
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


private final HashSet<NodeNeedingLight> pendingNodes = new HashSet<>();
private final HashMap<Integer, Light> lightDefinitions = new HashMap<>();
Expand Down Expand Up @@ -126,8 +129,6 @@ private SpotLight buildSpotLight(JsonObject obj) {

float intensity = obj.has("intensity") ? obj.get("intensity").getAsFloat() : 1.0f;
ColorRGBA color = obj.has("color") ? GltfUtils.getAsColor(obj, "color") : new ColorRGBA(ColorRGBA.White);
color = lumensToColor(color, intensity);
float range = obj.has("range") ? obj.get("range").getAsFloat() : Float.POSITIVE_INFINITY;

//Spot specific
JsonObject spot = obj.getAsJsonObject("spot");
Expand All @@ -143,6 +144,15 @@ private SpotLight buildSpotLight(JsonObject obj) {
outerConeAngle = FastMath.HALF_PI - 0.000001f;
}

if(APPLY_INTENSITY_CONVERSION) {
float solidAngle = 2.0f * FastMath.PI * (1.0f - FastMath.cos(outerConeAngle));
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.


color = lumensToColor(color, intensity);

SpotLight spotLight = new SpotLight();
spotLight.setName(name);
spotLight.setColor(color);
Expand All @@ -165,7 +175,7 @@ private DirectionalLight buildDirectionalLight(JsonObject obj) {

float intensity = obj.has("intensity") ? obj.get("intensity").getAsFloat() : 1.0f;
ColorRGBA color = obj.has("color") ? GltfUtils.getAsColor(obj, "color") : new ColorRGBA(ColorRGBA.White);
color = lumensToColor(color, intensity);
color = buildLinearLightColor(color, intensity);

DirectionalLight directionalLight = new DirectionalLight();
directionalLight.setName(name);
Expand All @@ -186,8 +196,11 @@ private PointLight buildPointLight(JsonObject obj) {

float intensity = obj.has("intensity") ? obj.get("intensity").getAsFloat() : 1.0f;
ColorRGBA color = obj.has("color") ? GltfUtils.getAsColor(obj, "color") : new ColorRGBA(ColorRGBA.White);
color = lumensToColor(color, intensity);
float range = obj.has("range") ? obj.get("range").getAsFloat() : Float.POSITIVE_INFINITY;

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);

PointLight pointLight = new PointLight();
pointLight.setName(name);
Expand All @@ -207,15 +220,16 @@ private PointLight buildPointLight(JsonObject obj) {
private void addLight(Node parent, Node node, int lightIndex) {
if (lightDefinitions.containsKey(lightIndex)) {
Light light = lightDefinitions.get(lightIndex);
light = light.clone();
parent.addLight(light);
LightControl control = new LightControl(light);
node.addControl(control);
} else {
throw new AssetLoadException("KHR_lights_punctual extension accessed undefined light at index " + lightIndex);
}
}
}

/**
/**
* Convert a floating point lumens value into a color that
* represents both color and brightness of the light.
*
Expand All @@ -236,6 +250,10 @@ private ColorRGBA lumensToColor(ColorRGBA color, float lumens) {
* @return A color representing the intensity of the given lumens
*/
private ColorRGBA lumensToColor(float lumens) {
if(SKIP_HDR_CONVERSION){
lumens = 0.003f * lumens;
return new ColorRGBA(lumens, lumens, lumens, 1f);
}
/*
Taken from /Common/ShaderLib/Hdr.glsllib
vec4 HDR_EncodeLum(in float lum){
Expand Down Expand Up @@ -286,4 +304,33 @@ private void setLightIndex(int lightIndex) {
this.lightIndex = lightIndex;
}
}


/**
* Computes the effective cutoff distance of a light based on its raw color and intensity.
* Uses inverse-square attenuation and a perceptual visibility threshold.
*
* @param color The base RGB color of the light (linear space)
* @param intensity The light's intensity in lumens (or equivalent)
* @return The cutoff distance where the light falls below a visible threshold
*/
public float getCutoffDistance(ColorRGBA color, float intensity) {
final float visibleThreshold = 0.001f;
final float maxRange = 10000f;

// Compute the max channel (R/G/B) for luminance estimation
float maxComponent = Math.max(Math.max(color.r, color.g), color.b);

if (maxComponent <= 0f || intensity <= 0f) {
return 0f;
}

// The actual light output (lux at 1 meter) per component
float effectiveIntensity = maxComponent * intensity;

// Inverse-square attenuation: intensity / d^2 = visibleThreshold
float range = (float) Math.sqrt(effectiveIntensity / visibleThreshold);
return Math.min(range, maxRange);
}

}
Loading