From 9d90dc24b9619d734fa3238342752b344d6a62ef Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 5 Aug 2025 19:10:53 +0200 Subject: [PATCH 1/4] add support for global lights --- .../java/com/jme3/light/DirectionalLight.java | 36 ++++++++ .../src/main/java/com/jme3/light/Light.java | 23 +++++ .../main/java/com/jme3/light/LightList.java | 50 ++++++++-- .../main/java/com/jme3/light/PointLight.java | 61 +++++++++++++ .../main/java/com/jme3/light/SpotLight.java | 91 +++++++++++++++++++ .../src/main/java/com/jme3/scene/Node.java | 19 ++++ .../src/main/java/com/jme3/scene/Spatial.java | 21 ++++- .../gltf/LightsPunctualExtensionLoader.java | 6 +- 8 files changed, 291 insertions(+), 16 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java b/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java index ba142071f4..2d71d322a3 100644 --- a/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java +++ b/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java @@ -59,6 +59,17 @@ public class DirectionalLight extends Light { * Creates a DirectionalLight */ public DirectionalLight() { + super(); + } + + /** + * Creates a DirectionalLight + * @param global if true, the light affects the entire tree from the root node, + * otherwise it only affects the children of the node in which it is attached. + */ + public DirectionalLight(boolean global) { + this(); + this.global = global; } /** @@ -66,9 +77,21 @@ public DirectionalLight() { * @param direction the light's direction */ public DirectionalLight(Vector3f direction) { + super(); setDirection(direction); } + /** + * Creates a DirectionalLight with the given direction + * @param direction the light's direction + * @param global if true, the light affects the entire tree from the root node, + * otherwise it only affects the children of the node in which it is attached. + */ + public DirectionalLight(Vector3f direction, boolean global) { + this(direction); + this.global = global; + } + /** * Creates a DirectionalLight with the given direction and the given color * @param direction the light's direction @@ -79,6 +102,19 @@ public DirectionalLight(Vector3f direction, ColorRGBA color) { setDirection(direction); } + /** + * Creates a DirectionalLight with the given direction and the given color + * @param direction the light's direction + * @param color the light's color + * @param global if true, the light affects the entire tree from the root node, + * otherwise it only affects the children of the node in which it is attached. + */ + public DirectionalLight(Vector3f direction, ColorRGBA color, boolean global) { + this(direction, color); + this.global = global; + } + + @Override public void computeLastDistance(Spatial owner) { // directional lights are after ambient lights diff --git a/jme3-core/src/main/java/com/jme3/light/Light.java b/jme3-core/src/main/java/com/jme3/light/Light.java index cebb4ae2a0..4694243822 100644 --- a/jme3-core/src/main/java/com/jme3/light/Light.java +++ b/jme3-core/src/main/java/com/jme3/light/Light.java @@ -116,6 +116,7 @@ public int getId(){ * The light name. */ protected String name; + protected boolean global = false; boolean frustumCheckNeeded = true; boolean intersectsFrustum = false; @@ -123,10 +124,30 @@ public int getId(){ protected Light() { } + + protected Light(boolean global) { + this.global = global; + } + + protected Light(ColorRGBA color, boolean global) { + setColor(color); + } + protected Light(ColorRGBA color) { setColor(color); } + + /** + * Returns true if this light affects the entire tree from the root node, + * otherwise returns false, meaning it only affects the children of the node + * in which it is attached. + * @return true if the light is global, otherwise false. + */ + public boolean isGlobal() { + return global; + } + /** * Returns the color of the light. * @@ -265,6 +286,7 @@ public void write(JmeExporter ex) throws IOException { oc.write(color, "color", null); oc.write(enabled, "enabled", true); oc.write(name, "name", null); + oc.write(global, "global", false); } @Override @@ -273,6 +295,7 @@ public void read(JmeImporter im) throws IOException { color = (ColorRGBA) ic.readSavable("color", null); enabled = ic.readBoolean("enabled", true); name = ic.readString("name", null); + global = ic.readBoolean("global", false); } /** diff --git a/jme3-core/src/main/java/com/jme3/light/LightList.java b/jme3-core/src/main/java/com/jme3/light/LightList.java index 97c4c29820..d6179dd986 100644 --- a/jme3-core/src/main/java/com/jme3/light/LightList.java +++ b/jme3-core/src/main/java/com/jme3/light/LightList.java @@ -38,6 +38,7 @@ import com.jme3.util.SortUtil; import java.io.IOException; import java.util.*; +import java.util.function.Predicate; /** * LightList is used internally by {@link Spatial}s to manage @@ -230,6 +231,20 @@ public void sort(boolean transformChanged) { * @param parent the parent's world-space LightList */ public void update(LightList local, LightList parent) { + update(local, parent, null); + } + + + /** + * Updates a "world-space" light list, using the spatial's local-space + * light list, its parent's world-space light list and an optional filter. + * + * @param local the local-space LightList (not null) + * @param parent the parent's world-space LightList + * @param filter an optional filter to apply to the lights + * (null means no filtering) + */ + public void update(LightList local, LightList parent, Predicate filter) { // clear the list as it will be reconstructed // using the arguments clear(); @@ -237,30 +252,32 @@ public void update(LightList local, LightList parent) { while (list.length <= local.listSize) { doubleSize(); } - - // add the lights from the local list - System.arraycopy(local.list, 0, list, 0, local.listSize); - for (int i = 0; i < local.listSize; i++) { -// list[i] = local.list[i]; - distToOwner[i] = Float.NEGATIVE_INFINITY; + + int localListSize = 0;// local.listSize; + for(int i=0;i{ + LightList childLights = sx.getLocalLightList(); + for (Light l : childLights) { + if (l.isGlobal()) { + worldLights.add(l); + } + } + }); + } + } + if ((refreshFlags & RF_TRANSFORM) != 0) { // combine with parent transforms- same for all spatial // subclasses. diff --git a/jme3-core/src/main/java/com/jme3/scene/Spatial.java b/jme3-core/src/main/java/com/jme3/scene/Spatial.java index 2fe1775d3e..9faf963355 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Spatial.java +++ b/jme3-core/src/main/java/com/jme3/scene/Spatial.java @@ -124,7 +124,8 @@ public enum BatchHint { RF_BOUND = 0x02, RF_LIGHTLIST = 0x04, // changes in light lists RF_CHILD_LIGHTLIST = 0x08, // some child need geometry update - RF_MATPARAM_OVERRIDE = 0x10; + RF_MATPARAM_OVERRIDE = 0x10, + RF_GLOBAL_LIGHTS = 0x20; // world affecting lights changed protected CullHint cullHint = CullHint.Inherit; protected BatchHint batchHint = BatchHint.Inherit; @@ -282,11 +283,22 @@ protected void setTransformRefresh() { setBoundRefresh(); } + protected boolean hasGlobalLights(){ + for(int i = 0;i!l.isGlobal()); refreshFlags &= ~RF_LIGHTLIST; } else { assert (parent.refreshFlags & RF_LIGHTLIST) == 0 : "Illegal light list update. Problem spatial name: " + getName(); - worldLights.update(localLights, parent.worldLights); + worldLights.update(localLights, parent.worldLights, l->!l.isGlobal()); refreshFlags &= ~RF_LIGHTLIST; } } 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..9a9ecb7287 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 @@ -143,7 +143,7 @@ private SpotLight buildSpotLight(JsonObject obj) { outerConeAngle = FastMath.HALF_PI - 0.000001f; } - SpotLight spotLight = new SpotLight(); + SpotLight spotLight = new SpotLight(true); spotLight.setName(name); spotLight.setColor(color); spotLight.setSpotRange(range); @@ -167,7 +167,7 @@ private DirectionalLight buildDirectionalLight(JsonObject obj) { ColorRGBA color = obj.has("color") ? GltfUtils.getAsColor(obj, "color") : new ColorRGBA(ColorRGBA.White); color = lumensToColor(color, intensity); - DirectionalLight directionalLight = new DirectionalLight(); + DirectionalLight directionalLight = new DirectionalLight(true); directionalLight.setName(name); directionalLight.setColor(color); directionalLight.setDirection(Vector3f.UNIT_Z.negate()); @@ -189,7 +189,7 @@ private PointLight buildPointLight(JsonObject obj) { color = lumensToColor(color, intensity); float range = obj.has("range") ? obj.get("range").getAsFloat() : Float.POSITIVE_INFINITY; - PointLight pointLight = new PointLight(); + PointLight pointLight = new PointLight(true); pointLight.setName(name); pointLight.setColor(color); pointLight.setRadius(range); From 08565997238cd230b65845fee36ef70fd7c9beda Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 10 Aug 2025 12:34:27 +0200 Subject: [PATCH 2/4] set refresh flag before light is removed --- jme3-core/src/main/java/com/jme3/scene/Spatial.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jme3-core/src/main/java/com/jme3/scene/Spatial.java b/jme3-core/src/main/java/com/jme3/scene/Spatial.java index 9faf963355..450a2065b3 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Spatial.java +++ b/jme3-core/src/main/java/com/jme3/scene/Spatial.java @@ -1215,8 +1215,8 @@ public void addLight(Light light) { * @see Spatial#addLight(com.jme3.light.Light) */ public void removeLight(Light light) { - localLights.remove(light); setLightListRefresh(); + localLights.remove(light); } /** From 3de0cd00c05a63fc324efbc8b7157271f8e953b4 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Mon, 11 Aug 2025 19:28:38 +0200 Subject: [PATCH 3/4] optimize global lights refresh by following only paths with RF_GLOBAL_LIGHTS flag --- .../src/main/java/com/jme3/scene/Node.java | 31 +++++++++++++------ .../src/main/java/com/jme3/scene/Spatial.java | 6 ++++ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/scene/Node.java b/jme3-core/src/main/java/com/jme3/scene/Node.java index 97ce830392..1e5c60fac2 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Node.java +++ b/jme3-core/src/main/java/com/jme3/scene/Node.java @@ -242,6 +242,26 @@ public void updateLogicalState(float tpf) { } } + private void findGlobalLights(Spatial sp, LightList list) { + LightList lights = sp.getLocalLightList(); + for (Light l : lights) { + if (l.isGlobal()) { + list.add(l); + } + } + + if(sp instanceof Node){ + Node n = (Node) sp; + List children = n.getChildren(); + for (int i = 0; i < children.size(); i++) { + Spatial child = children.get(i); + if ((child.refreshFlags & RF_GLOBAL_LIGHTS)!= 0) { + findGlobalLights(child, list); + } + } + } + } + @Override public void updateGeometricState() { if (refreshFlags == 0) { @@ -254,18 +274,11 @@ public void updateGeometricState() { boolean updateGlobalLights = (refreshFlags & RF_GLOBAL_LIGHTS) != 0; if (updateGlobalLights){ - refreshFlags &= ~RF_GLOBAL_LIGHTS; // if root node, we collect the global lights if (getParent() == null){ - depthFirstTraversal(sx->{ - LightList childLights = sx.getLocalLightList(); - for (Light l : childLights) { - if (l.isGlobal()) { - worldLights.add(l); - } - } - }); + findGlobalLights(this, worldLights); } + refreshFlags &= ~RF_GLOBAL_LIGHTS; } if ((refreshFlags & RF_TRANSFORM) != 0) { diff --git a/jme3-core/src/main/java/com/jme3/scene/Spatial.java b/jme3-core/src/main/java/com/jme3/scene/Spatial.java index 450a2065b3..0715479b56 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Spatial.java +++ b/jme3-core/src/main/java/com/jme3/scene/Spatial.java @@ -299,6 +299,9 @@ protected void setLightListRefresh() { // to update lights. Spatial p = parent; boolean hasGlobalLights = hasGlobalLights(); + if (hasGlobalLights){ + refreshFlags |= RF_GLOBAL_LIGHTS; + } while (p != null) { if ((p.refreshFlags & RF_CHILD_LIGHTLIST) != 0) { // The parent already has this flag, @@ -967,6 +970,9 @@ public void updateGeometricState() { if ((refreshFlags & RF_MATPARAM_OVERRIDE) != 0) { updateMatParamOverrides(); } + if ((refreshFlags & RF_GLOBAL_LIGHTS) != 0) { + refreshFlags &= ~RF_GLOBAL_LIGHTS; + } assert refreshFlags == 0 : "Illegal refresh flags state: " + refreshFlags + " for spatial " + getName(); } From 13edd167198e2424ca51019491e5e3aee3efff92 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 24 Aug 2025 13:41:03 +0200 Subject: [PATCH 4/4] fix global lights refresh flag propagation --- jme3-core/src/main/java/com/jme3/scene/Node.java | 9 ++++++++- jme3-core/src/main/java/com/jme3/scene/Spatial.java | 6 +++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/scene/Node.java b/jme3-core/src/main/java/com/jme3/scene/Node.java index 1e5c60fac2..3aa37476ca 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Node.java +++ b/jme3-core/src/main/java/com/jme3/scene/Node.java @@ -256,7 +256,7 @@ private void findGlobalLights(Spatial sp, LightList list) { for (int i = 0; i < children.size(); i++) { Spatial child = children.get(i); if ((child.refreshFlags & RF_GLOBAL_LIGHTS)!= 0) { - findGlobalLights(child, list); + findGlobalLights(child, list); } } } @@ -298,6 +298,13 @@ public void updateGeometricState() { // NOTE 9/19/09 // Although it does save a round trip, for (Spatial child : children.getArray()) { + if (updateGlobalLights){ + // we might have new global lights coming from a different + // branch of the scene graph, so we need to propagate the + // refresh flags down to all the children of every branch + child.refreshFlags |= RF_LIGHTLIST; + child.refreshFlags |= RF_GLOBAL_LIGHTS; + } child.updateGeometricState(); } } diff --git a/jme3-core/src/main/java/com/jme3/scene/Spatial.java b/jme3-core/src/main/java/com/jme3/scene/Spatial.java index 0715479b56..384bbf643a 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Spatial.java +++ b/jme3-core/src/main/java/com/jme3/scene/Spatial.java @@ -303,7 +303,10 @@ protected void setLightListRefresh() { refreshFlags |= RF_GLOBAL_LIGHTS; } while (p != null) { - if ((p.refreshFlags & RF_CHILD_LIGHTLIST) != 0) { + if ( + (p.refreshFlags & RF_CHILD_LIGHTLIST) != 0 + && (!hasGlobalLights || (p.refreshFlags & RF_GLOBAL_LIGHTS) != 0) + ) { // The parent already has this flag, // so must all ancestors. return; @@ -311,6 +314,7 @@ protected void setLightListRefresh() { p.refreshFlags |= RF_CHILD_LIGHTLIST; if (hasGlobalLights) { p.refreshFlags |= RF_GLOBAL_LIGHTS; + p.refreshFlags |= RF_LIGHTLIST; } p = p.parent; }