From f3ee81798f1097ac3c6e1bd66586c23859ba4b23 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 10 Aug 2025 09:46:27 +0200 Subject: [PATCH 1/3] improve light render parity with blender --- .../pbrlighting/PBRLightingUtils.glsllib | 36 +++++------ .../jme3/scene/plugins/gltf/GltfUtils.java | 4 +- .../gltf/LightsPunctualExtensionLoader.java | 62 ++++++++++++++++--- 3 files changed, 73 insertions(+), 29 deletions(-) diff --git a/jme3-core/src/main/resources/Common/ShaderLib/module/pbrlighting/PBRLightingUtils.glsllib b/jme3-core/src/main/resources/Common/ShaderLib/module/pbrlighting/PBRLightingUtils.glsllib index 41f9e77b9c..5b720af189 100644 --- a/jme3-core/src/main/resources/Common/ShaderLib/module/pbrlighting/PBRLightingUtils.glsllib +++ b/jme3-core/src/main/resources/Common/ShaderLib/module/pbrlighting/PBRLightingUtils.glsllib @@ -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); + + // 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); } diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java index 8f015f28db..c34bf64ac3 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java @@ -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 + ); } public static ColorRGBA getAsColor(JsonObject parent, String name, ColorRGBA defaultValue) { diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java index 5b55c49b2e..cf908dadd1 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java @@ -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; private final HashSet pendingNodes = new HashSet<>(); private final HashMap lightDefinitions = new HashMap<>(); @@ -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"); @@ -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); + + color = lumensToColor(color, intensity); + SpotLight spotLight = new SpotLight(); spotLight.setName(name); spotLight.setColor(color); @@ -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); @@ -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); @@ -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. * @@ -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){ @@ -286,4 +304,34 @@ 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; // Lux or similar + 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); + System.out.println("Effective range: " + range + " for intensity: " + effectiveIntensity + " and color: " + color); + return Math.min(range, maxRange); + } + } From def0dab4f5b5800d6791e4b662b5e636a016c31d Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 10 Aug 2025 10:59:02 +0200 Subject: [PATCH 2/3] cleanup --- .../jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java index cf908dadd1..145ffe2451 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java @@ -315,7 +315,7 @@ private void setLightIndex(int lightIndex) { * @return The cutoff distance where the light falls below a visible threshold */ public float getCutoffDistance(ColorRGBA color, float intensity) { - final float visibleThreshold = 0.001f; // Lux or similar + final float visibleThreshold = 0.001f; final float maxRange = 10000f; // Compute the max channel (R/G/B) for luminance estimation @@ -330,7 +330,6 @@ public float getCutoffDistance(ColorRGBA color, float intensity) { // Inverse-square attenuation: intensity / d^2 = visibleThreshold float range = (float) Math.sqrt(effectiveIntensity / visibleThreshold); - System.out.println("Effective range: " + range + " for intensity: " + effectiveIntensity + " and color: " + color); return Math.min(range, maxRange); } From 2f64bbaf65fc8b162bf61b35954f0843ddc4edae Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 27 Aug 2025 14:58:35 +0200 Subject: [PATCH 3/3] fix merge issue --- .../scene/plugins/gltf/LightsPunctualExtensionLoader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java index 145ffe2451..a110d447e2 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java @@ -175,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 = buildLinearLightColor(color, intensity); + color = lumensToColor(color, intensity); DirectionalLight directionalLight = new DirectionalLight(); directionalLight.setName(name); @@ -200,7 +200,7 @@ private PointLight buildPointLight(JsonObject obj) { 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); + color = lumensToColor(color, intensity); PointLight pointLight = new PointLight(); pointLight.setName(name);