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 53528cf9d0..80d1b5fdcd 100644 --- a/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java +++ b/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java @@ -110,6 +110,7 @@ public final void setDirection(Vector3f dir){ if (!direction.isUnitVector()) { direction.normalizeLocal(); } + updateStates(null); } @Override 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 4cbea59208..e78cc9667c 100644 --- a/jme3-core/src/main/java/com/jme3/light/Light.java +++ b/jme3-core/src/main/java/com/jme3/light/Light.java @@ -37,6 +37,7 @@ import com.jme3.math.ColorRGBA; import com.jme3.renderer.Camera; import com.jme3.scene.Spatial; +import com.jme3.util.StatefulObject; import com.jme3.util.TempVars; import java.io.IOException; @@ -45,7 +46,7 @@ *

* All light source types have a color. */ -public abstract class Light implements Savable, Cloneable { +public abstract class Light extends StatefulObject implements Savable, Cloneable { /** * Describes the light type. @@ -171,6 +172,7 @@ public float getLastDistance(){ */ public void setColor(ColorRGBA color){ this.color.set(color); + updateStates(null); } @@ -189,6 +191,7 @@ public boolean isEnabled() { */ public void setEnabled(boolean enabled) { this.enabled = enabled; + updateStates(null); } public boolean isFrustumCheckNeeded() { diff --git a/jme3-core/src/main/java/com/jme3/light/PointLight.java b/jme3-core/src/main/java/com/jme3/light/PointLight.java index 4d9f306a06..5fb6badb8e 100644 --- a/jme3-core/src/main/java/com/jme3/light/PointLight.java +++ b/jme3-core/src/main/java/com/jme3/light/PointLight.java @@ -136,6 +136,7 @@ public Vector3f getPosition() { */ public final void setPosition(Vector3f position) { this.position.set(position); + updateStates(null); } /** @@ -171,6 +172,7 @@ public final void setRadius(float radius) { } else { this.invRadius = 0; } + updateStates(null); } /** diff --git a/jme3-core/src/main/java/com/jme3/light/SpotLight.java b/jme3-core/src/main/java/com/jme3/light/SpotLight.java index 417e845f0b..1ec884e090 100644 --- a/jme3-core/src/main/java/com/jme3/light/SpotLight.java +++ b/jme3-core/src/main/java/com/jme3/light/SpotLight.java @@ -317,6 +317,7 @@ public Vector3f getDirection() { public final void setDirection(Vector3f direction) { this.direction.set(direction); + updateStates(null); } public Vector3f getPosition() { @@ -325,6 +326,7 @@ public Vector3f getPosition() { public final void setPosition(Vector3f position) { this.position.set(position); + updateStates(null); } public float getSpotRange() { @@ -354,6 +356,7 @@ public void setSpotRange(float spotRange) { } else { this.invSpotRange = 0; } + updateStates(null); } /** @@ -387,6 +390,7 @@ public void setSpotInnerAngle(float spotInnerAngle) { } this.spotInnerAngle = spotInnerAngle; computeAngleParameters(); + updateStates(null); } /** @@ -413,6 +417,7 @@ public void setSpotOuterAngle(float spotOuterAngle) { } this.spotOuterAngle = spotOuterAngle; computeAngleParameters(); + updateStates(null); } /** diff --git a/jme3-core/src/main/java/com/jme3/renderer/Camera.java b/jme3-core/src/main/java/com/jme3/renderer/Camera.java index eb7cca3ae4..f47b21aa35 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/Camera.java +++ b/jme3-core/src/main/java/com/jme3/renderer/Camera.java @@ -45,6 +45,9 @@ import com.jme3.math.Vector2f; import com.jme3.math.Vector3f; import com.jme3.math.Vector4f; +import com.jme3.export.*; +import com.jme3.math.*; +import com.jme3.util.StatefulObject; import com.jme3.util.TempVars; import java.io.IOException; import java.util.logging.Level; @@ -70,7 +73,7 @@ * @author Mark Powell * @author Joshua Slack */ -public class Camera implements Savable, Cloneable { +public class Camera extends StatefulObject implements Savable, Cloneable { private static final Logger logger = Logger.getLogger(Camera.class.getName()); @@ -1247,6 +1250,7 @@ public void updateViewProjection() { //viewProjectionMatrix.set(viewMatrix).multLocal(projectionMatrix); viewProjectionMatrix.set(projectionMatrix).multLocal(viewMatrix); } + updateStates(null); } /** 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 0424cea053..23f50d70e6 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Node.java +++ b/jme3-core/src/main/java/com/jme3/scene/Node.java @@ -58,6 +58,9 @@ * @author Joshua Slack */ public class Node extends Spatial { + public enum StateUpdateHints { + INVALIDATE_UPDATE_LIST + } private static final Logger logger = Logger.getLogger(Node.class.getName()); /** * This node's children. @@ -201,6 +204,7 @@ private void addUpdateChildren(SafeArrayList results) { * that would change state. */ void invalidateUpdateList() { + updateStates(StateUpdateHints.INVALIDATE_UPDATE_LIST); updateListValid = false; if (parent != null) { parent.invalidateUpdateList(); 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 eacaadb7af..4faad55bc0 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Spatial.java +++ b/jme3-core/src/main/java/com/jme3/scene/Spatial.java @@ -48,8 +48,10 @@ import com.jme3.renderer.queue.RenderQueue; import com.jme3.renderer.queue.RenderQueue.Bucket; import com.jme3.renderer.queue.RenderQueue.ShadowMode; +import com.jme3.scene.Node.StateUpdateHints; import com.jme3.scene.control.Control; import com.jme3.util.SafeArrayList; +import com.jme3.util.StatefulObject; import com.jme3.util.TempVars; import com.jme3.util.clone.Cloner; import com.jme3.util.clone.IdentityCloneFunction; @@ -68,8 +70,8 @@ * @author Joshua Slack * @version $Revision: 4075 $, $Data$ */ -public abstract class Spatial implements Savable, Cloneable, Collidable, - CloneableSmartAsset, JmeCloneable, HasLocalTransform { +public abstract class Spatial extends StatefulObject implements Savable, Cloneable, Collidable, CloneableSmartAsset, JmeCloneable, HasLocalTransform { + private static final Logger logger = Logger.getLogger(Spatial.class.getName()); /** @@ -775,6 +777,7 @@ public void addControl(Control control) { boolean before = requiresUpdates(); controls.add(control); control.setSpatial(this); + updateStates(StateUpdateHints.INVALIDATE_UPDATE_LIST); boolean after = requiresUpdates(); // If the requirement to be updated has changed, // then we need to let the parent node know, so it @@ -799,6 +802,7 @@ public void removeControl(Class controlType) { break; // added to match the javadoc -pspeed } } + updateStates(StateUpdateHints.INVALIDATE_UPDATE_LIST); boolean after = requiresUpdates(); // If the requirement to be updated has changed, // then we need to let the parent node know, so it @@ -824,13 +828,15 @@ public boolean removeControl(Control control) { control.setSpatial(null); } - boolean after = requiresUpdates(); + updateStates(StateUpdateHints.INVALIDATE_UPDATE_LIST); + boolean after = requiresUpdates(); // If the requirement to be updated has changed, // then we need to let the parent node know, so it // can rebuild its update list. if (parent != null && before != after) { parent.invalidateUpdateList(); } + return result; } @@ -906,6 +912,7 @@ public void updateGeometricState() { // assume that this Spatial is a leaf, a proper implementation // for this method should be provided by Node. + int refreshFlagsCp = refreshFlags; // NOTE: Update world transforms first because // bound transform depends on them. if ((refreshFlags & RF_LIGHTLIST) != 0) { @@ -921,6 +928,8 @@ public void updateGeometricState() { updateMatParamOverrides(); } assert refreshFlags == 0; + + updateStates(refreshFlagsCp); } /** diff --git a/jme3-core/src/main/java/com/jme3/system/NanoTimer.java b/jme3-core/src/main/java/com/jme3/system/NanoTimer.java index 0964857416..fefc4248d7 100644 --- a/jme3-core/src/main/java/com/jme3/system/NanoTimer.java +++ b/jme3-core/src/main/java/com/jme3/system/NanoTimer.java @@ -87,11 +87,13 @@ public void update() { tpf = (getTime() - previousTime) * (1.0f / TIMER_RESOLUTION); fps = 1.0f / tpf; previousTime = getTime(); + updateStates(null); } @Override public void reset() { startTime = System.nanoTime(); previousTime = getTime(); + updateStates(null); } } diff --git a/jme3-core/src/main/java/com/jme3/system/Timer.java b/jme3-core/src/main/java/com/jme3/system/Timer.java index 554399522c..172a14da62 100644 --- a/jme3-core/src/main/java/com/jme3/system/Timer.java +++ b/jme3-core/src/main/java/com/jme3/system/Timer.java @@ -31,6 +31,8 @@ */ package com.jme3.system; +import com.jme3.util.StatefulObject; + /** * Timer is the base class for a high resolution timer. It is * created from getTimer("display system") @@ -38,7 +40,7 @@ * @author Mark Powell * @version $Id: Timer.java,v 1.18 2007/03/09 10:19:34 rherlitz Exp $ */ -public abstract class Timer { +public abstract class Timer extends StatefulObject{ /** * Returns the current time in ticks. A tick is an arbitrary measure of time diff --git a/jme3-core/src/main/java/com/jme3/util/NativeObject.java b/jme3-core/src/main/java/com/jme3/util/NativeObject.java index c7ff725294..cdf044889a 100644 --- a/jme3-core/src/main/java/com/jme3/util/NativeObject.java +++ b/jme3-core/src/main/java/com/jme3/util/NativeObject.java @@ -42,7 +42,7 @@ * collected by the garbage collector, and then invoke the proper destructor * on the OpenGL library to delete it from memory. */ -public abstract class NativeObject implements Cloneable { +public abstract class NativeObject extends StatefulObject implements Cloneable { public static final int INVALID_ID = -1; @@ -130,6 +130,7 @@ public int getId(){ * and its state needs to be updated. */ public void setUpdateNeeded(){ + updateStates(null); updateNeeded = true; } diff --git a/jme3-core/src/main/java/com/jme3/util/StatefulObject.java b/jme3-core/src/main/java/com/jme3/util/StatefulObject.java new file mode 100644 index 0000000000..54f4683c73 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/util/StatefulObject.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2009-2021 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.util; + +import java.util.Collections; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +import com.jme3.util.functional.NoArgFunction; +import com.jme3.util.functional.VoidFunction; + + +/** + * StatefulObject is an object that stores one or more states + * + * @author Riccardo Balbo + */ +public class StatefulObject implements Cloneable{ + private static AtomicLong globalId = new AtomicLong(Long.MIN_VALUE); + + private static long getNewSnapshotID(){ + return globalId.getAndUpdate(n->{ + long nn=n+1; + if(nn==-1||n==0)nn=1; + return nn; + }); + } + + /** + * A state that can be updated. + */ + public static abstract class State { + private volatile long snapshotID = 0; + + /** + * Get unique ID of the snapshot of the current state. + * The ID changes after every update. + * @return + */ + public long getSnapshotID() { + return snapshotID; + } + + /** + * Called when the state needs to be updated + * @param hint Hint on what need to be updated (may be ignored) + */ + public abstract void updateState(S forObject, Object hint); + + private void _updateState(StatefulObject forObject, Object hint) { + this.updateState((S)forObject, hint); + } + + /** + * Called after the update is completed. + * Updates the snapshot ID so that objects monitoring it can know that + * the state has new data. + */ + public void commitUpdate() { + snapshotID = getNewSnapshotID(); // update the snapshot id + } + + public abstract State cloneStateFor(S obj); + + private State _cloneStateFor(StatefulObject obj) { + return cloneStateFor((S) obj); + } + + /** + * Called after the state is attached for the first time + */ + public abstract void attachedTo(S obj); + + private void _attachedTo(StatefulObject obj) { + attachedTo((S) obj); + } + } + + + + private transient Map>> states; + + /** + * Get registered states + * + * @return Map containing all the registered states and their keys + */ + protected Map>> getStates() { + if (states == null) { + states = (Map>>) Collections.synchronizedMap(new WeakHashMap>>()); + } + return states; + } + /** + * Get or attach a state + * @param + * @param key Something that is unique and represent the state (can be any object) + * @param constructor constructor used to create a new state if it doesn't exist + * @return + */ + public > T getState(Object key, Object type, NoArgFunction constructor) { + Map> stateMap = getStates().computeIfAbsent(key,(k)->new ConcurrentHashMap>()); + State state = stateMap.computeIfAbsent(type, (k) -> { + if (constructor == null) return null; + State s = constructor.eval(); + s._attachedTo(this); + return s; + }); + return (T)state; + } + + /** + * Detach a state + * @param key Key representing the state + * @return + */ + public Object removeState(Object key, Object type) { + Map> stateMap = getStates().get(key); + return stateMap.remove(type); + } + + /** + * Mark states for update + * @param hint Suggest what to update (can be ignored) + */ + protected void updateStates(Object hint) { + getStates().forEach((k, v) -> { + v.forEach((k2, v2) -> { + v2._updateState(this, hint); + }); + }); + } + + + + @Override + protected StatefulObject clone() throws CloneNotSupportedException { + StatefulObject clone=(StatefulObject)super.clone(); + clone.states = null; + Map>> statesMap = getStates(); + Map>> clonedStatesMap = clone.getStates(); + assert states != clonedStatesMap; + statesMap.forEach((key,v)->{ + v.forEach((type, s) -> { + clone.getState(key, type, () -> s._cloneStateFor(clone)); + }); + }); + return clone; + } + + +} \ No newline at end of file