11/*
2- * Copyright (c) 2009-2024 jMonkeyEngine
2+ * Copyright (c) 2009-2026 jMonkeyEngine
33 * All rights reserved.
44 *
55 * Redistribution and use in source and binary forms, with or without
5353 *
5454 * Supports directional, point, and spot lights.
5555 *
56- * Created by Trevor Flynn - 3/23/2021
56+ * @author Trevor Flynn, Riccardo Balbo
5757 */
5858public class LightsPunctualExtensionLoader implements ExtensionLoader {
59+ private static final boolean COMPUTE_LIGHT_RANGE = true ;
60+ private static final float GLTF_LIGHT_COMPAT_SCALE = 0.0009f ;
5961
6062 private final HashSet <NodeNeedingLight > pendingNodes = new HashSet <>();
6163 private final HashMap <Integer , Light > lightDefinitions = new HashMap <>();
@@ -126,13 +128,11 @@ private SpotLight buildSpotLight(JsonObject obj) {
126128
127129 float intensity = obj .has ("intensity" ) ? obj .get ("intensity" ).getAsFloat () : 1.0f ;
128130 ColorRGBA color = obj .has ("color" ) ? GltfUtils .getAsColor (obj , "color" ) : new ColorRGBA (ColorRGBA .White );
129- color = lumensToColor (color , intensity );
130- float range = obj .has ("range" ) ? obj .get ("range" ).getAsFloat () : Float .POSITIVE_INFINITY ;
131131
132132 //Spot specific
133133 JsonObject spot = obj .getAsJsonObject ("spot" );
134- float innerConeAngle = spot != null && spot .has ("innerConeAngle" ) ? spot .get ("innerConeAngle" ).getAsFloat () : 0f ;
135- float outerConeAngle = spot != null && spot .has ("outerConeAngle" ) ? spot .get ("outerConeAngle" ).getAsFloat () : (( float ) Math .PI ) / 4f ;
134+ float innerConeAngle = ( spot != null && spot .has ("innerConeAngle" ) ) ? spot .get ("innerConeAngle" ).getAsFloat () : 0f ;
135+ float outerConeAngle = ( spot != null && spot .has ("outerConeAngle" )) ? spot .get ("outerConeAngle" ).getAsFloat () : (FastMath .PI / 4f ) ;
136136
137137 /*
138138 Correct floating point error on half PI, GLTF spec says that the outerConeAngle
@@ -143,9 +143,12 @@ private SpotLight buildSpotLight(JsonObject obj) {
143143 outerConeAngle = FastMath .HALF_PI - 0.000001f ;
144144 }
145145
146+ float scaledIntensity = toCompatIntensity (intensity );
147+
148+ float range = obj .has ("range" ) ? obj .get ("range" ).getAsFloat () : (COMPUTE_LIGHT_RANGE ? getCutoffDistance (color , scaledIntensity ) : Float .POSITIVE_INFINITY );
146149 SpotLight spotLight = new SpotLight (true );
147150 spotLight .setName (name );
148- spotLight .setColor (color );
151+ spotLight .setColor (applyScaledIntensity ( color , scaledIntensity ) );
149152 spotLight .setSpotRange (range );
150153 spotLight .setSpotInnerAngle (innerConeAngle );
151154 spotLight .setSpotOuterAngle (outerConeAngle );
@@ -165,11 +168,11 @@ private DirectionalLight buildDirectionalLight(JsonObject obj) {
165168
166169 float intensity = obj .has ("intensity" ) ? obj .get ("intensity" ).getAsFloat () : 1.0f ;
167170 ColorRGBA color = obj .has ("color" ) ? GltfUtils .getAsColor (obj , "color" ) : new ColorRGBA (ColorRGBA .White );
168- color = lumensToColor ( color , intensity );
171+ float scaledIntensity = toCompatIntensity ( intensity );
169172
170173 DirectionalLight directionalLight = new DirectionalLight (true );
171174 directionalLight .setName (name );
172- directionalLight .setColor (color );
175+ directionalLight .setColor (applyScaledIntensity ( color , scaledIntensity ) );
173176 directionalLight .setDirection (Vector3f .UNIT_Z .negate ());
174177
175178 return directionalLight ;
@@ -185,13 +188,16 @@ private PointLight buildPointLight(JsonObject obj) {
185188 String name = obj .has ("name" ) ? obj .get ("name" ).getAsString () : "" ;
186189
187190 float intensity = obj .has ("intensity" ) ? obj .get ("intensity" ).getAsFloat () : 1.0f ;
188- ColorRGBA color = obj .has ("color" ) ? GltfUtils .getAsColor (obj , "color" ) : new ColorRGBA (ColorRGBA .White );
189- color = lumensToColor (color , intensity );
190- float range = obj .has ("range" ) ? obj .get ("range" ).getAsFloat () : Float .POSITIVE_INFINITY ;
191+ ColorRGBA color = obj .has ("color" ) ? GltfUtils .getAsColor (obj , "color" )
192+ : new ColorRGBA (ColorRGBA .White );
193+
194+ float scaledIntensity = toCompatIntensity (intensity );
195+
196+ float range = obj .has ("range" ) ? obj .get ("range" ).getAsFloat () : (COMPUTE_LIGHT_RANGE ? getCutoffDistance (color , scaledIntensity ) : Float .POSITIVE_INFINITY );
191197
192198 PointLight pointLight = new PointLight (true );
193199 pointLight .setName (name );
194- pointLight .setColor (color );
200+ pointLight .setColor (applyScaledIntensity ( color , scaledIntensity ) );
195201 pointLight .setRadius (range );
196202
197203 return pointLight ;
@@ -206,7 +212,7 @@ private PointLight buildPointLight(JsonObject obj) {
206212 */
207213 private void addLight (Node parent , Node node , int lightIndex ) {
208214 if (lightDefinitions .containsKey (lightIndex )) {
209- Light light = lightDefinitions .get (lightIndex );
215+ Light light = lightDefinitions .get (lightIndex ). clone () ;
210216 parent .addLight (light );
211217 LightControl control = new LightControl (light );
212218 control .setInvertAxisDirection (true );
@@ -216,55 +222,17 @@ private void addLight(Node parent, Node node, int lightIndex) {
216222 }
217223 }
218224
219- /**
220- * Convert a floating point lumens value into a color that
221- * represents both color and brightness of the light.
222- *
223- * @param color The base color of the light
224- * @param lumens The lumens value to convert to a color
225- * @return A color representing the intensity of the given lumens encoded into the given color
226- */
227- private ColorRGBA lumensToColor (ColorRGBA color , float lumens ) {
228- ColorRGBA brightnessModifier = lumensToColor (lumens );
229- return color .mult (brightnessModifier );
225+ private float toCompatIntensity (float intensity ) {
226+ return intensity * GLTF_LIGHT_COMPAT_SCALE ;
230227 }
231228
232- /**
233- * Convert a floating point lumens value into a grayscale color that
234- * represents a brightness.
235- *
236- * @param lumens The lumens value to convert to a color
237- * @return A color representing the intensity of the given lumens
238- */
239- private ColorRGBA lumensToColor (float lumens ) {
240- /*
241- Taken from /Common/ShaderLib/Hdr.glsllib
242- vec4 HDR_EncodeLum(in float lum){
243- float Le = 2.0 * log2(lum + epsilon) + 127.0;
244- vec4 result = vec4(0.0);
245- result.a = fract(Le);
246- result.rgb = vec3((Le - (floor(result.a * 255.0)) / 255.0) / 255.0);
247- return result;
248- */
249- float epsilon = 0.0001f ;
250-
251- double Le = 2f * Math .log (lumens * epsilon ) / Math .log (2 ) + 127.0 ;
252- ColorRGBA color = new ColorRGBA ();
253- color .a = (float ) (Le - Math .floor (Le )); //Get fractional part
254- float val = (float ) ((Le - (Math .floor (color .a * 255.0 )) / 255.0 ) / 255.0 );
255- color .r = val ;
256- color .g = val ;
257- color .b = val ;
258-
259- return color ;
229+ private ColorRGBA applyScaledIntensity (ColorRGBA color , float scaledIntensity ) {
230+ return color .mult (scaledIntensity );
260231 }
261232
262- /**
263- * A bean to contain the relation between a node and a light index
264- */
265233 private static class NodeNeedingLight {
266- private Node node ;
267- private int lightIndex ;
234+ private final Node node ;
235+ private final int lightIndex ;
268236
269237 private NodeNeedingLight (Node node , int lightIndex ) {
270238 this .node = node ;
@@ -275,16 +243,35 @@ private Node getNode() {
275243 return node ;
276244 }
277245
278- private void setNode (Node node ) {
279- this .node = node ;
280- }
281-
282246 private int getLightIndex () {
283247 return lightIndex ;
284248 }
249+ }
250+ /**
251+ * Computes the effective cutoff distance of a light based on its raw color and intensity. Uses
252+ * inverse-square attenuation and a perceptual visibility threshold.
253+ *
254+ * @param color
255+ * The base RGB color of the light (linear space)
256+ * @param intensity
257+ * The light's intensity in lumens (or equivalent)
258+ * @return The cutoff distance where the light falls below a visible threshold
259+ */
260+ private float getCutoffDistance (ColorRGBA color , float scaledIntensity ) {
261+ final float visibleThreshold = 0.001f ;
262+ final float maxRange = 10000f ;
285263
286- private void setLightIndex (int lightIndex ) {
287- this .lightIndex = lightIndex ;
264+ // Compute the max channel (R/G/B) for luminance estimation
265+ float maxComponent = Math .max (Math .max (color .r , color .g ), color .b );
266+
267+ if (maxComponent <= 0f || scaledIntensity <= 0f ) {
268+ return 0f ;
288269 }
270+
271+ // The actual light output (lux at 1 meter) per component
272+ float effectiveIntensity = maxComponent * scaledIntensity ;
273+ // Inverse-square attenuation: intensity / d^2 = visibleThreshold
274+ float range = (float ) Math .sqrt (effectiveIntensity / visibleThreshold );
275+ return Math .min (range , maxRange );
289276 }
290- }
277+ }
0 commit comments