com.jme3.scene.Spatial Maven / Gradle / Ivy
Show all versions of jme3-core Show documentation
/*
* Copyright (c) 2009-2023 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.scene;
import com.jme3.anim.util.HasLocalTransform;
import com.jme3.asset.AssetKey;
import com.jme3.asset.CloneableSmartAsset;
import com.jme3.bounding.BoundingVolume;
import com.jme3.collision.Collidable;
import com.jme3.export.*;
import com.jme3.light.Light;
import com.jme3.light.LightList;
import com.jme3.material.MatParamOverride;
import com.jme3.material.Material;
import com.jme3.math.*;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.control.Control;
import com.jme3.util.SafeArrayList;
import com.jme3.util.TempVars;
import com.jme3.util.clone.Cloner;
import com.jme3.util.clone.IdentityCloneFunction;
import com.jme3.util.clone.JmeCloneable;
import java.io.IOException;
import java.util.*;
import java.util.logging.Logger;
/**
* Spatial
defines the base class for scene graph nodes. It
* maintains a link to a parent, its local transforms and the world's
* transforms. All other scene graph elements, such as {@link Node} and
* {@link Geometry} are subclasses of Spatial
.
*
* @author Mark Powell
* @author Joshua Slack
* @version $Revision: 4075 $, $Data$
*/
public abstract class Spatial implements Savable, Cloneable, Collidable,
CloneableSmartAsset, JmeCloneable, HasLocalTransform {
private static final Logger logger = Logger.getLogger(Spatial.class.getName());
/**
* Specifies how frustum culling should be handled by
* this spatial.
*/
public enum CullHint {
/**
* Do whatever our parent does. If no parent, default to {@link #Dynamic}.
*/
Inherit,
/**
* Do not draw if we are not at least partially within the view frustum
* of the camera. The defined
* Camera planes determine whether this Spatial should be culled.
*/
Dynamic,
/**
* Always cull this from the view, throwing away this object
* and any children from rendering commands.
*/
Always,
/**
* Never cull this from view, always draw it.
* Note that we will still get culled if our parent is culled.
*/
Never;
}
/**
* Specifies if this spatial should be batched
*/
public enum BatchHint {
/**
* Do whatever our parent does. If no parent, default to {@link #Always}.
*/
Inherit,
/**
* This spatial will always be batched when attached to a BatchNode.
*/
Always,
/**
* This spatial will never be batched when attached to a BatchNode.
*/
Never;
}
/**
* Refresh flag types
*/
protected static final int
RF_TRANSFORM = 0x01, // need light resort + combine transforms
RF_BOUND = 0x02,
RF_LIGHTLIST = 0x04, // changes in light lists
RF_CHILD_LIGHTLIST = 0x08, // some child need geometry update
RF_MATPARAM_OVERRIDE = 0x10;
protected CullHint cullHint = CullHint.Inherit;
protected BatchHint batchHint = BatchHint.Inherit;
/**
* Spatial's bounding volume relative to the world.
*/
protected BoundingVolume worldBound;
/**
* LightList
*/
protected LightList localLights;
protected transient LightList worldLights;
protected SafeArrayList localOverrides;
protected SafeArrayList worldOverrides;
/**
* This spatial's name.
*/
protected String name;
// scale values
protected transient Camera.FrustumIntersect frustrumIntersects
= Camera.FrustumIntersect.Intersects;
protected RenderQueue.Bucket queueBucket = RenderQueue.Bucket.Inherit;
protected ShadowMode shadowMode = RenderQueue.ShadowMode.Inherit;
public transient float queueDistance = Float.NEGATIVE_INFINITY;
protected Transform localTransform;
protected Transform worldTransform;
protected SafeArrayList controls = new SafeArrayList<>(Control.class);
protected HashMap userData = null;
/**
* Used for smart asset caching
*
* @see com.jme3.asset.AssetKey#getCacheType()
*/
protected AssetKey key;
/**
* Spatial's parent, or null if it has none.
*/
protected transient Node parent;
/**
* Refresh flags. Indicate what data of the spatial need to be
* updated to reflect the correct state.
*/
protected transient int refreshFlags = 0;
/**
* Set to true if a subclass requires updateLogicalState() even
* if it doesn't have any controls. Defaults to true thus implementing
* the legacy behavior for any subclasses not specifically turning it
* off.
* This flag should be set during construction and never changed
* as it's supposed to be class-specific and not runtime state.
*/
private boolean requiresUpdates = true;
/**
* Serialization only. Do not use.
* Not really. This class is never instantiated directly but the
* subclasses like to use the no-arg constructor for their own
* no-arg constructor... which is technically weaker than
* forward supplying defaults.
*/
protected Spatial() {
this(null);
}
/**
* Constructor instantiates a new Spatial
object, setting the
* rotation, translation, and scale values to their defaults.
*
* @param name
* the name of the scene element. This is required for
* identification and comparison purposes.
*/
protected Spatial(String name) {
this.name = name;
localTransform = new Transform();
worldTransform = new Transform();
localLights = new LightList(this);
worldLights = new LightList(this);
localOverrides = new SafeArrayList<>(MatParamOverride.class);
worldOverrides = new SafeArrayList<>(MatParamOverride.class);
refreshFlags |= RF_BOUND;
}
@Override
public void setKey(AssetKey key) {
this.key = key;
}
@Override
public AssetKey getKey() {
return key;
}
/**
* Returns true if this spatial requires updateLogicalState() to
* be called, either because setRequiresUpdate(true) has been called
* or because the spatial has controls. This is package private to
* avoid exposing it to the public API since it is only used by Node.
*/
boolean requiresUpdates() {
return requiresUpdates || !controls.isEmpty();
}
/**
* Subclasses can call this with true to denote that they require
* updateLogicalState() to be called even if they contain no controls.
* Setting this to false reverts to the default behavior of only
* updating if the spatial has controls. This is not meant to
* indicate dynamic state in any way and must be called while
* unattached or an IllegalStateException is thrown. It is designed
* to be called during object construction and then never changed, ie:
* it's meant to be subclass specific state and not runtime state.
* Subclasses of Node or Geometry that do not set this will get the
* old default behavior as if this was set to true. Subclasses should
* call setRequiresUpdate(false) in their constructors to receive
* optimal behavior if they don't require updateLogicalState() to be
* called even if there are no controls.
*
* @param f true→require updates, false→don't require updates
*/
protected void setRequiresUpdates(boolean f) {
// Note to explorers, the reason this was done as a protected setter
// instead of passed on construction is because it frees all subclasses
// from having to make sure to always pass the value up in case they
// are subclassed.
// The reason that requiresUpdates() isn't just a protected method to
// override (which would be more correct) is because the flag provides
// some flexibility in how we break subclasses. A protected method
// would require that all subclasses that required updates need implement
// this method, or they would silently stop processing updates. A flag
// lets us set a default when a subclass is detected that is different
// from the internal "more efficient" default.
// Spatial's default is 'true' for this flag requiring subclasses to
// override it for more optimal behavior. Node and Geometry will override
// it to false if the class is Node.class or Geometry.class.
// This means that all subclasses will default to the old behavior
// unless they opt in.
if (parent != null) {
throw new IllegalStateException("setRequiresUpdates() cannot be called once attached.");
}
this.requiresUpdates = f;
}
/**
* Indicate that the transform of this spatial has changed and that
* a refresh is required.
*/
protected void setTransformRefresh() {
refreshFlags |= RF_TRANSFORM;
setBoundRefresh();
}
protected void setLightListRefresh() {
refreshFlags |= RF_LIGHTLIST;
// Make sure next updateGeometricState() visits this branch
// to update lights.
Spatial p = parent;
while (p != null) {
if ((p.refreshFlags & RF_CHILD_LIGHTLIST) != 0) {
// The parent already has this flag,
// so must all ancestors.
return;
}
p.refreshFlags |= RF_CHILD_LIGHTLIST;
p = p.parent;
}
}
protected void setMatParamOverrideRefresh() {
refreshFlags |= RF_MATPARAM_OVERRIDE;
Spatial p = parent;
while (p != null) {
if ((p.refreshFlags & RF_MATPARAM_OVERRIDE) != 0) {
return;
}
p.refreshFlags |= RF_MATPARAM_OVERRIDE;
p = p.parent;
}
}
/**
* Indicate that the bounding of this spatial has changed and that
* a refresh is required.
*/
protected void setBoundRefresh() {
refreshFlags |= RF_BOUND;
Spatial p = parent;
while (p != null) {
if ((p.refreshFlags & RF_BOUND) != 0) {
return;
}
p.refreshFlags |= RF_BOUND;
p = p.parent;
}
}
/**
* (Internal use only) Forces a refresh of the given types of data.
*
* @param transforms Refresh world transform based on parents'
* @param bounds Refresh bounding volume data based on child nodes
* @param lights Refresh light list based on parents'
*/
public void forceRefresh(boolean transforms, boolean bounds, boolean lights) {
if (transforms) {
setTransformRefresh();
}
if (bounds) {
setBoundRefresh();
}
if (lights) {
setLightListRefresh();
}
}
/**
* checkCulling
checks the spatial with the camera to see if it
* should be culled.
*
* This method is called by the renderer. Usually it should not be called
* directly.
*
* @param cam The camera to check against.
* @return true if inside or intersecting camera frustum
* (should be rendered), false if outside.
*/
public boolean checkCulling(Camera cam) {
if (refreshFlags != 0) {
throw new IllegalStateException("Scene graph is not properly updated for rendering.\n"
+ "State was changed after rootNode.updateGeometricState() call. \n"
+ "Make sure you do not modify the scene from another thread!\n"
+ "Problem spatial name: " + getName());
}
CullHint cm = getCullHint();
assert cm != CullHint.Inherit : "CullHint should never be inherit. Problem spatial name: " + getName();
if (cm == Spatial.CullHint.Always) {
setLastFrustumIntersection(Camera.FrustumIntersect.Outside);
return false;
} else if (cm == Spatial.CullHint.Never) {
setLastFrustumIntersection(Camera.FrustumIntersect.Intersects);
return true;
}
// check to see if we can cull this node
frustrumIntersects = (parent != null ? parent.frustrumIntersects
: Camera.FrustumIntersect.Intersects);
if (frustrumIntersects == Camera.FrustumIntersect.Intersects) {
if (getQueueBucket() == Bucket.Gui) {
return cam.containsGui(getWorldBound());
} else {
frustrumIntersects = cam.contains(getWorldBound());
}
}
return frustrumIntersects != Camera.FrustumIntersect.Outside;
}
/**
* Sets the name of this spatial.
*
* @param name
* The spatial's new name.
*/
public void setName(String name) {
this.name = name;
}
/**
* Returns the name of this spatial.
*
* @return This spatial's name.
*/
public String getName() {
return name;
}
/**
* Returns the local {@link LightList}, which are the lights
* that were directly attached to this Spatial
through the
* {@link #addLight(com.jme3.light.Light) } and
* {@link #removeLight(com.jme3.light.Light) } methods.
*
* @return The local light list
*/
public LightList getLocalLightList() {
return localLights;
}
/**
* Returns the world {@link LightList}, containing the lights
* combined from all this Spatial's
parents up to and including
* this Spatial
's lights.
*
* @return The combined world light list
*/
public LightList getWorldLightList() {
return worldLights;
}
/**
* Get the local material parameter overrides.
*
* @return The list of local material parameter overrides.
*/
public SafeArrayList getLocalMatParamOverrides() {
return localOverrides;
}
/**
* Get the world material parameter overrides.
*
* Note that this list is only updated on a call to
* {@link #updateGeometricState()}. After update, the world overrides list
* will contain the {@link #getParent() parent's} world overrides combined
* with this spatial's {@link #getLocalMatParamOverrides() local overrides}.
*
* @return The list of world material parameter overrides.
*/
public SafeArrayList getWorldMatParamOverrides() {
return worldOverrides;
}
/**
* getWorldRotation
retrieves the absolute rotation of the
* Spatial.
*
* @return the Spatial's world rotation quaternion.
*/
public Quaternion getWorldRotation() {
checkDoTransformUpdate();
return worldTransform.getRotation();
}
/**
* getWorldTranslation
retrieves the absolute translation of
* the spatial.
*
* @return the Spatial's world translation vector.
*/
public Vector3f getWorldTranslation() {
checkDoTransformUpdate();
return worldTransform.getTranslation();
}
/**
* getWorldScale
retrieves the absolute scale factor of the
* spatial.
*
* @return the Spatial's world scale factor.
*/
public Vector3f getWorldScale() {
checkDoTransformUpdate();
return worldTransform.getScale();
}
/**
* getWorldTransform
retrieves the world transformation
* of the spatial.
*
* @return the world transform.
*/
public Transform getWorldTransform() {
checkDoTransformUpdate();
return worldTransform;
}
/**
* rotateUpTo
is a utility function that alters the
* local rotation to point the Y axis in the direction given by newUp.
*
* @param newUp
* the up vector to use - assumed to be a unit vector.
*/
public void rotateUpTo(Vector3f newUp) {
TempVars vars = TempVars.get();
Vector3f compVecA = vars.vect1;
Quaternion q = vars.quat1;
// First figure out the current up vector.
Vector3f upY = compVecA.set(Vector3f.UNIT_Y);
Quaternion rot = localTransform.getRotation();
rot.multLocal(upY);
// get angle between vectors
float angle = upY.angleBetween(newUp);
// figure out rotation axis by taking cross product
Vector3f rotAxis = upY.crossLocal(newUp).normalizeLocal();
// Build a rotation quat and apply current local rotation.
q.fromAngleNormalAxis(angle, rotAxis);
q.mult(rot, rot);
vars.release();
setTransformRefresh();
}
/**
* lookAt
is a convenience method for auto-setting the local
* rotation based on a position in world space and an up vector. It computes the rotation
* to transform the z-axis to point onto 'position' and the y-axis to 'up'.
* Unlike {@link Quaternion#lookAt(com.jme3.math.Vector3f, com.jme3.math.Vector3f) }
* this method takes a world position to look at and not a relative direction.
*
* Note : 28/01/2013 this method has been fixed as it was not taking into account the parent rotation.
* This was resulting in improper rotation when the spatial had rotated parent nodes.
* This method is intended to work in world space, so no matter what parent graph the
* spatial has, it will look at the given position in world space.
*
* @param position
* where to look at in terms of world coordinates
* @param upVector
* a vector indicating the (local) up direction. (typically {0,
* 1, 0} in jME.)
*/
public void lookAt(Vector3f position, Vector3f upVector) {
Vector3f worldTranslation = getWorldTranslation();
TempVars vars = TempVars.get();
Vector3f compVecA = vars.vect4;
compVecA.set(position).subtractLocal(worldTranslation);
getLocalRotation().lookAt(compVecA, upVector);
if (getParent() != null) {
Quaternion rot = vars.quat1;
rot = rot.set(parent.getWorldRotation()).inverseLocal().multLocal(getLocalRotation());
rot.normalizeLocal();
setLocalRotation(rot);
}
vars.release();
setTransformRefresh();
}
/**
* Should be overridden by Node and Geometry.
*/
protected void updateWorldBound() {
// the world bound of a leaf is the same as its model bound
// for a node, the world bound is a combination of all its children
// bounds
// -> handled by subclass
refreshFlags &= ~RF_BOUND;
}
protected void updateWorldLightList() {
if (parent == null) {
worldLights.update(localLights, null);
refreshFlags &= ~RF_LIGHTLIST;
} else {
assert (parent.refreshFlags & RF_LIGHTLIST) == 0 : "Illegal light list update. Problem spatial name: " + getName();
worldLights.update(localLights, parent.worldLights);
refreshFlags &= ~RF_LIGHTLIST;
}
}
protected void updateMatParamOverrides() {
refreshFlags &= ~RF_MATPARAM_OVERRIDE;
worldOverrides.clear();
if (parent == null) {
worldOverrides.addAll(localOverrides);
} else {
assert (parent.refreshFlags & RF_MATPARAM_OVERRIDE) == 0 : "Illegal mat param update. Problem spatial name: " + getName();
worldOverrides.addAll(parent.worldOverrides);
worldOverrides.addAll(localOverrides);
}
}
/**
* Adds a local material parameter override.
*
* @param override The override to add.
* @see MatParamOverride
*/
public void addMatParamOverride(MatParamOverride override) {
if (override == null) {
throw new IllegalArgumentException("override cannot be null");
}
localOverrides.add(override);
setMatParamOverrideRefresh();
}
/**
* Remove a local material parameter override if it exists.
*
* @param override The override to remove.
* @see MatParamOverride
*/
public void removeMatParamOverride(MatParamOverride override) {
if (localOverrides.remove(override)) {
setMatParamOverrideRefresh();
}
}
/**
* Remove all local material parameter overrides.
*
* @see #addMatParamOverride(com.jme3.material.MatParamOverride)
*/
public void clearMatParamOverrides() {
if (!localOverrides.isEmpty()) {
setMatParamOverrideRefresh();
}
localOverrides.clear();
}
/**
* Should only be called from updateGeometricState().
* In most cases should not be subclassed.
*/
protected void updateWorldTransforms() {
if (parent == null) {
worldTransform.set(localTransform);
refreshFlags &= ~RF_TRANSFORM;
} else {
// check if transform for parent is updated
assert ((parent.refreshFlags & RF_TRANSFORM) == 0) : "Illegal rf transform update. Problem spatial name: " + getName();
worldTransform.set(localTransform);
worldTransform.combineWithParent(parent.worldTransform);
refreshFlags &= ~RF_TRANSFORM;
}
}
/**
* Computes the world transform of this Spatial in the most
* efficient manner possible.
*/
void checkDoTransformUpdate() {
if ((refreshFlags & RF_TRANSFORM) == 0) {
return;
}
if (parent == null) {
worldTransform.set(localTransform);
refreshFlags &= ~RF_TRANSFORM;
} else {
TempVars vars = TempVars.get();
Spatial[] stack = vars.spatialStack;
Spatial rootNode = this;
int i = 0;
while (true) {
Spatial hisParent = rootNode.parent;
if (hisParent == null) {
rootNode.worldTransform.set(rootNode.localTransform);
rootNode.refreshFlags &= ~RF_TRANSFORM;
i--;
break;
}
stack[i] = rootNode;
if ((hisParent.refreshFlags & RF_TRANSFORM) == 0) {
break;
}
rootNode = hisParent;
i++;
}
vars.release();
for (int j = i; j >= 0; j--) {
rootNode = stack[j];
//rootNode.worldTransform.set(rootNode.localTransform);
//rootNode.worldTransform.combineWithParent(rootNode.parent.worldTransform);
//rootNode.refreshFlags &= ~RF_TRANSFORM;
rootNode.updateWorldTransforms();
}
}
}
/**
* Computes this Spatial's world bounding volume in the most efficient
* manner possible.
*/
void checkDoBoundUpdate() {
if ((refreshFlags & RF_BOUND) == 0) {
return;
}
checkDoTransformUpdate();
// Go to children recursively and update their bound
if (this instanceof Node) {
Node node = (Node) this;
int len = node.getQuantity();
for (int i = 0; i < len; i++) {
Spatial child = node.getChild(i);
child.checkDoBoundUpdate();
}
}
// All children's bounds have been updated. Update my own now.
updateWorldBound();
}
private void runControlUpdate(float tpf) {
if (controls.isEmpty()) {
return;
}
for (Control c : controls.getArray()) {
c.update(tpf);
}
}
/**
* Called when the Spatial is about to be rendered, to notify
* controls attached to this Spatial using the Control.render() method.
*
* @param rm The RenderManager rendering the Spatial.
* @param vp The ViewPort to which the Spatial is being rendered to.
*
* @see Spatial#addControl(com.jme3.scene.control.Control)
* @see Spatial#getControl(java.lang.Class)
*/
public void runControlRender(RenderManager rm, ViewPort vp) {
if (controls.isEmpty()) {
return;
}
for (Control c : controls.getArray()) {
c.render(rm, vp);
}
}
/**
* Add a control to the list of controls.
*
* @param control The control to add.
*
* @see Spatial#removeControl(java.lang.Class)
*/
public void addControl(Control control) {
boolean before = requiresUpdates();
controls.add(control);
control.setSpatial(this);
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();
}
}
/**
* Adds the specified control to the list, at the specified index. Any
* controls with indices greater than or equal to the specified index will
* have their indices increased by one.
*
* @param index the index at which to add the control (0→first, ≥0)
* @param control the control to add (not null)
* @throws IllegalStateException if the control is already added here
*/
@SuppressWarnings("unchecked")
public void addControlAt(int index, Control control) {
if (control == null) {
throw new IllegalArgumentException("null control");
}
int numControls = getNumControls();
if (index < 0 || index > numControls) {
throw new IndexOutOfBoundsException(
"index=" + index + " for numControls=" + numControls);
}
if (controls.contains(control)) {
throw new IllegalStateException("Control is already added here.");
}
addControl(control); // takes care of the bookkeeping
if (index < numControls) { // re-arrange the list directly
boolean success = controls.remove(control);
assert success : "Surprising control remove failure. " + control.getClass().getSimpleName() + " from spatial " + getName();
controls.add(index, control);
}
}
/**
* Removes the first control that is an instance of the given class.
*
* @param controlType the type of Control to remove
* @see Spatial#addControl(com.jme3.scene.control.Control)
*/
public void removeControl(Class extends Control> controlType) {
boolean before = requiresUpdates();
for (int i = 0; i < controls.size(); i++) {
if (controlType.isAssignableFrom(controls.get(i).getClass())) {
Control control = controls.remove(i);
control.setSpatial(null);
break; // added to match the javadoc -pspeed
}
}
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();
}
}
/**
* Removes the given control from this spatial's controls.
*
* @param control The control to remove
* @return True if the control was successfully removed. False if the
* control is not assigned to this spatial.
*
* @see Spatial#addControl(com.jme3.scene.control.Control)
*/
public boolean removeControl(Control control) {
boolean before = requiresUpdates();
boolean result = controls.remove(control);
if (result) {
control.setSpatial(null);
}
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;
}
/**
* Returns the first control that is an instance of the given class,
* or null if no such control exists.
*
* @param the type of control to look for
* @param controlType The superclass of the control to look for.
* @return The first instance in the list of the controlType class, or null.
*
* @see Spatial#addControl(com.jme3.scene.control.Control)
*/
@SuppressWarnings("unchecked")
public T getControl(Class controlType) {
for (Control c : controls.getArray()) {
if (controlType.isAssignableFrom(c.getClass())) {
return (T) c;
}
}
return null;
}
/**
* Returns the control at the given index in the list.
*
* @param index The index of the control in the list to find.
* @return The control at the given index.
*
* @throws IndexOutOfBoundsException
* If the index is outside the range [0, getNumControls()-1]
*
* @see Spatial#addControl(com.jme3.scene.control.Control)
*/
public Control getControl(int index) {
return controls.get(index);
}
/**
* @return The number of controls attached to this Spatial.
* @see Spatial#addControl(com.jme3.scene.control.Control)
* @see Spatial#removeControl(java.lang.Class)
*/
public int getNumControls() {
return controls.size();
}
/**
* updateLogicalState
calls the update()
method
* for all controls attached to this Spatial.
*
* @param tpf Time per frame.
*
* @see Spatial#addControl(com.jme3.scene.control.Control)
*/
public void updateLogicalState(float tpf) {
runControlUpdate(tpf);
}
/**
* updateGeometricState
updates the light list,
* computes the world transforms, and computes the world bounds
* for this Spatial.
* Calling this when the Spatial is attached to a node
* will cause undefined results. User code should only call this
* method on Spatials having no parent.
*
* @see Spatial#getWorldLightList()
* @see Spatial#getWorldTransform()
* @see Spatial#getWorldBound()
*/
public void updateGeometricState() {
// assume that this Spatial is a leaf, a proper implementation
// for this method should be provided by Node.
// NOTE: Update world transforms first because
// bound transform depends on them.
if ((refreshFlags & RF_LIGHTLIST) != 0) {
updateWorldLightList();
}
if ((refreshFlags & RF_TRANSFORM) != 0) {
updateWorldTransforms();
}
if ((refreshFlags & RF_BOUND) != 0) {
updateWorldBound();
}
if ((refreshFlags & RF_MATPARAM_OVERRIDE) != 0) {
updateMatParamOverrides();
}
assert refreshFlags == 0 : "Illegal refresh flags state: " + refreshFlags + " for spatial " + getName();
}
/**
* Convert a vector (in) from this spatial's local coordinate space to world
* coordinate space.
*
* @param in
* vector to read from
* @param store
* where to write the result (null to create a new vector, may be
* same as in)
* @return the result (store)
*/
public Vector3f localToWorld(final Vector3f in, Vector3f store) {
checkDoTransformUpdate();
return worldTransform.transformVector(in, store);
}
/**
* Convert a vector (in) from world coordinate space to this spatial's local
* coordinate space.
*
* @param in
* vector to read from
* @param store
* where to write the result
* @return the result (store)
*/
public Vector3f worldToLocal(final Vector3f in, final Vector3f store) {
checkDoTransformUpdate();
return worldTransform.transformInverseVector(in, store);
}
/**
* getParent
retrieves this node's parent. If the parent is
* null this is the root node.
*
* @return the parent of this node.
*/
public Node getParent() {
return parent;
}
/**
* Called by {@link Node#attachChild(Spatial)} and
* {@link Node#detachChild(Spatial)} - don't call directly.
* setParent
sets the parent of this node.
*
* @param parent
* the parent of this node.
*/
protected void setParent(Node parent) {
this.parent = parent;
}
/**
* removeFromParent
removes this Spatial from its parent.
*
* @return true if it has a parent and performed the remove.
*/
public boolean removeFromParent() {
if (parent != null) {
parent.detachChild(this);
return true;
}
return false;
}
/**
* determines if the provided Node is the parent, or parent's parent, etc. of this Spatial.
*
* @param ancestor
* the ancestor object to look for.
* @return true if the ancestor is found, false otherwise.
*/
public boolean hasAncestor(Node ancestor) {
if (parent == null) {
return false;
} else if (parent.equals(ancestor)) {
return true;
} else {
return parent.hasAncestor(ancestor);
}
}
/**
* getLocalRotation
retrieves the local rotation of this
* node.
*
* @return the local rotation of this node.
*/
public Quaternion getLocalRotation() {
return localTransform.getRotation();
}
/**
* setLocalRotation
sets the local rotation of this node
* by using a {@link Matrix3f}.
*
* @param rotation
* the new local rotation.
*/
public void setLocalRotation(Matrix3f rotation) {
localTransform.getRotation().fromRotationMatrix(rotation);
setTransformRefresh();
}
/**
* setLocalRotation
sets the local rotation of this node.
*
* @param quaternion the new local rotation (not null,
* {@code quaternion.norm()} approximately equal to 1, unaffected)
*/
public void setLocalRotation(Quaternion quaternion) {
localTransform.setRotation(quaternion);
setTransformRefresh();
}
/**
* getLocalScale
retrieves the local scale of this node.
*
* @return the local scale of this node.
*/
public Vector3f getLocalScale() {
return localTransform.getScale();
}
/**
* setLocalScale
sets the local scale of this node.
*
* @param localScale
* the new local scale, applied to x, y and z
*/
public void setLocalScale(float localScale) {
localTransform.setScale(localScale);
setTransformRefresh();
}
/**
* setLocalScale
sets the local scale of this node.
*
* @param x the desired scale factor for the X axis
* @param y the desired scale factor for the Y axis
* @param z the desired scale factor for the Z axis
*/
public void setLocalScale(float x, float y, float z) {
localTransform.setScale(x, y, z);
setTransformRefresh();
}
/**
* setLocalScale
sets the local scale of this node.
*
* @param localScale
* the new local scale.
*/
public void setLocalScale(Vector3f localScale) {
localTransform.setScale(localScale);
setTransformRefresh();
}
/**
* getLocalTranslation
retrieves the local translation of
* this node.
*
* @return the local translation of this node.
*/
public Vector3f getLocalTranslation() {
return localTransform.getTranslation();
}
/**
* setLocalTranslation
sets the local translation of this
* spatial.
*
* @param localTranslation
* the local translation of this spatial.
*/
public void setLocalTranslation(Vector3f localTranslation) {
this.localTransform.setTranslation(localTranslation);
setTransformRefresh();
}
/**
* setLocalTranslation
sets the local translation of this
* spatial.
*
* @param x the desired offset in the +X direction
* @param y the desired offset in the +Y direction
* @param z the desired offset in the +Z direction
*/
public void setLocalTranslation(float x, float y, float z) {
this.localTransform.setTranslation(x, y, z);
setTransformRefresh();
}
/**
* setLocalTransform
sets the local transform of this
* spatial.
*
* @param t the desired local transform (not null, {@code t.rot.norm()}
* approximately equal to 1, unaffected)
*/
@Override
public void setLocalTransform(Transform t) {
this.localTransform.set(t);
setTransformRefresh();
}
/**
* getLocalTransform
retrieves the local transform of
* this spatial.
*
* @return the local transform of this spatial.
*/
@Override
public Transform getLocalTransform() {
return localTransform;
}
/**
* Applies the given material to the Spatial, this will propagate the
* material down to the geometries in the scene graph.
*
* @param material The material to set.
*/
public void setMaterial(Material material) {
}
/**
* addLight
adds the given light to the Spatial; causing all
* child Spatials to be affected by it.
*
* @param light The light to add.
*/
public void addLight(Light light) {
localLights.add(light);
setLightListRefresh();
}
/**
* removeLight
removes the given light from the Spatial.
*
* @param light The light to remove.
* @see Spatial#addLight(com.jme3.light.Light)
*/
public void removeLight(Light light) {
localLights.remove(light);
setLightListRefresh();
}
/**
* Translates the spatial by the given translation vector.
*
* @param x the offset to apply in the +X direction
* @param y the offset to apply in the +Y direction
* @param z the offset to apply in the +Z direction
* @return The spatial on which this method is called, e.g this
.
*/
public Spatial move(float x, float y, float z) {
this.localTransform.getTranslation().addLocal(x, y, z);
setTransformRefresh();
return this;
}
/**
* Translates the spatial by the given translation vector.
*
* @param offset the desired offset (not null, unaffected)
* @return The spatial on which this method is called, e.g this
.
*/
public Spatial move(Vector3f offset) {
this.localTransform.getTranslation().addLocal(offset);
setTransformRefresh();
return this;
}
/**
* Scales the spatial by the given value
*
* @param s the scaling factor to apply to all axes
* @return The spatial on which this method is called, e.g this
.
*/
public Spatial scale(float s) {
return scale(s, s, s);
}
/**
* Scales the spatial by the given scale vector.
*
* @param x the scaling factor to apply to the X axis
* @param y the scaling factor to apply to the Y axis
* @param z the scaling factor to apply to the Z axis
* @return The spatial on which this method is called, e.g this
.
*/
public Spatial scale(float x, float y, float z) {
this.localTransform.getScale().multLocal(x, y, z);
setTransformRefresh();
return this;
}
/**
* Rotates the spatial by the given rotation.
*
* @param rot the intrinsic rotation to apply (not null, unaffected)
* @return The spatial on which this method is called, e.g this
.
*/
public Spatial rotate(Quaternion rot) {
this.localTransform.getRotation().multLocal(rot);
setTransformRefresh();
return this;
}
/**
* Rotates the spatial by the xAngle, yAngle and zAngle angles (in radians),
* (aka pitch, yaw, roll) in the local coordinate space.
*
* @param xAngle the angle of rotation around the +X axis (in radians)
* @param yAngle the angle of rotation around the +Y axis (in radians)
* @param zAngle the angle of rotation around the +Z axis (in radians)
* @return The spatial on which this method is called, e.g this
.
*/
public Spatial rotate(float xAngle, float yAngle, float zAngle) {
TempVars vars = TempVars.get();
Quaternion q = vars.quat1;
q.fromAngles(xAngle, yAngle, zAngle);
rotate(q);
vars.release();
return this;
}
/**
* Centers the spatial in the origin of the world bound.
*
* @return The spatial on which this method is called, e.g this
.
*/
public Spatial center() {
Vector3f worldTrans = getWorldTranslation();
Vector3f worldCenter = getWorldBound().getCenter();
Vector3f absTrans = worldTrans.subtract(worldCenter);
setLocalTranslation(absTrans);
return this;
}
/**
* @see #setCullHint(CullHint)
* @return the cull mode of this spatial, or if set to CullHint.Inherit, the
* cull mode of its parent.
*/
public CullHint getCullHint() {
if (cullHint != CullHint.Inherit) {
return cullHint;
} else if (parent != null) {
return parent.getCullHint();
} else {
return CullHint.Dynamic;
}
}
public BatchHint getBatchHint() {
if (batchHint != BatchHint.Inherit) {
return batchHint;
} else if (parent != null) {
return parent.getBatchHint();
} else {
return BatchHint.Always;
}
}
/**
* Returns this spatial's render-queue bucket. If the mode is set to inherit,
* then the spatial gets its render-queue bucket from its parent.
*
* @return The spatial's current render-queue bucket.
*/
public RenderQueue.Bucket getQueueBucket() {
if (queueBucket != RenderQueue.Bucket.Inherit) {
return queueBucket;
} else if (parent != null) {
return parent.getQueueBucket();
} else {
return RenderQueue.Bucket.Opaque;
}
}
/**
* @return The shadow mode of this spatial, if the local shadow
* mode is set to inherit, then the parent's shadow mode is returned.
*
* @see Spatial#setShadowMode(com.jme3.renderer.queue.RenderQueue.ShadowMode)
* @see ShadowMode
*/
public RenderQueue.ShadowMode getShadowMode() {
if (shadowMode != RenderQueue.ShadowMode.Inherit) {
return shadowMode;
} else if (parent != null) {
return parent.getShadowMode();
} else {
return ShadowMode.Off;
}
}
/**
* Sets the level of detail to use when rendering this Spatial,
* this call propagates to all geometries under this Spatial.
*
* @param lod The lod level to set.
*/
public void setLodLevel(int lod) {
}
/**
* updateModelBound
recalculates the bounding object for this
* Spatial.
*/
public abstract void updateModelBound();
/**
* setModelBound
sets the bounding object for this Spatial.
*
* @param modelBound
* the bounding object for this spatial.
*/
public abstract void setModelBound(BoundingVolume modelBound);
/**
* @return The sum of all vertices under this Spatial.
*/
public abstract int getVertexCount();
/**
* @return The sum of all triangles under this Spatial.
*/
public abstract int getTriangleCount();
/**
* @return A clone of this Spatial, the scene graph in its entirety
* is cloned and can be altered independently of the original scene graph.
*
* Note that meshes of geometries are not cloned explicitly, they
* are shared if static, or specially cloned if animated.
*
* @param cloneMaterial true to clone materials, false to share them
* @see Mesh#cloneForAnim()
*/
public Spatial clone(boolean cloneMaterial) {
// Set up the cloner for the type of cloning we want to do.
Cloner cloner = new Cloner();
// First, we definitely do not want to clone our own parent
cloner.setClonedValue(parent, null);
// If we aren't cloning materials then we will make sure those
// aren't cloned also
if (!cloneMaterial) {
cloner.setCloneFunction(Material.class, new IdentityCloneFunction());
}
// By default, the meshes are not cloned. The geometry
// may choose to selectively force them to be cloned, but
// normally they will be shared.
cloner.setCloneFunction(Mesh.class, new IdentityCloneFunction());
// Clone it!
Spatial clone = cloner.clone(this);
// Because we've nulled the parent out we need to make sure
// the transforms and stuff get refreshed.
clone.setTransformRefresh();
clone.setLightListRefresh();
clone.setMatParamOverrideRefresh();
return clone;
}
/**
* The old clone() method that did not use the new Cloner utility.
*
* @param cloneMaterial ignored
* @return never
*/
@Deprecated
public Spatial oldClone(boolean cloneMaterial) {
throw new UnsupportedOperationException();
}
/**
* @return A clone of this Spatial, the scene graph in its entirety
* is cloned and can be altered independently of the original scene graph.
*
* Note that meshes of geometries are not cloned explicitly, they
* are shared if static, or specially cloned if animated.
*
* @see Mesh#cloneForAnim()
*/
@Override
public Spatial clone() {
return clone(true);
}
/**
* @return Similar to Spatial.clone() except will create a deep clone of all
* geometries' meshes. Normally this method shouldn't be used. Instead, use
* Spatial.clone()
*
* @see Spatial#clone()
*/
public Spatial deepClone() {
// Set up the cloner for the type of cloning we want to do.
Cloner cloner = new Cloner();
// First, we definitely do not want to clone our own parent
cloner.setClonedValue(parent, null);
Spatial clone = cloner.clone(this);
// Because we've nulled the parent out we need to make sure
// the transforms and stuff get refreshed.
clone.setTransformRefresh();
clone.setLightListRefresh();
clone.setMatParamOverrideRefresh();
return clone;
}
/**
* Called internally by com.jme3.util.clone.Cloner. Do not call directly.
*/
@Override
public Spatial jmeClone() {
try {
Spatial clone = (Spatial) super.clone();
return clone;
} catch (CloneNotSupportedException ex) {
throw new AssertionError();
}
}
/**
* Called internally by com.jme3.util.clone.Cloner. Do not call directly.
*/
@Override
@SuppressWarnings("unchecked")
public void cloneFields(Cloner cloner, Object original) {
// Clone all of the fields that need fix-ups and/or potential
// sharing.
this.parent = cloner.clone(parent);
this.worldBound = cloner.clone(worldBound);
this.worldLights = cloner.clone(worldLights);
this.localLights = cloner.clone(localLights);
this.worldTransform = cloner.clone(worldTransform);
this.localTransform = cloner.clone(localTransform);
this.worldOverrides = cloner.clone(worldOverrides);
this.localOverrides = cloner.clone(localOverrides);
this.controls = cloner.clone(controls);
// Cloner doesn't handle maps on its own just yet.
// Note: this is more advanced cloning than the old clone() method
// did because it just shallow cloned the map. In this case, we want
// to avoid all of the nasty cloneForSpatial() fixup style code that
// used to inject stuff into the clone's user data. By using cloner
// to clone the user data we get this automatically.
if (userData != null) {
userData = (HashMap) userData.clone();
for (Map.Entry e : userData.entrySet()) {
Savable value = e.getValue();
if (value instanceof Cloneable) {
// Note: all JmeCloneable objects are also Cloneable so this
// catches both cases.
e.setValue(cloner.clone(value));
}
}
}
}
public void setUserData(String key, Object data) {
if (data == null) {
if (userData != null) {
userData.remove(key);
if (userData.isEmpty()) {
userData = null;
}
}
} else {
if (userData == null) {
userData = new HashMap();
}
if (data instanceof Savable) {
userData.put(key, (Savable) data);
} else {
userData.put(key, new UserData(UserData.getObjectType(data), data));
}
}
}
@SuppressWarnings("unchecked")
public T getUserData(String key) {
if (userData == null) {
return null;
}
Savable s = userData.get(key);
if (s instanceof UserData) {
return (T) ((UserData) s).getValue();
} else {
return (T) s;
}
}
@SuppressWarnings("unchecked")
public Collection getUserDataKeys() {
if (userData != null) {
return userData.keySet();
}
return Collections.EMPTY_SET;
}
/**
* Note that we are matching the pattern, therefore the pattern
* must match the entire pattern (i.e. it behaves as if it is sandwiched
* between "^" and "$").
* You can set regex modes, like case insensitivity, by using the (?X)
* or (?X:Y) constructs.
*
* @param spatialSubclass Subclass which this must implement.
* Null causes all Spatials to qualify.
* @param nameRegex Regular expression to match this name against.
* Null causes all Names to qualify.
* @return true if this implements the specified class and this's name
* matches the specified pattern.
*
* @see java.util.regex.Pattern
*/
public boolean matches(Class extends Spatial> spatialSubclass,
String nameRegex) {
if (spatialSubclass != null && !spatialSubclass.isInstance(this)) {
return false;
}
if (nameRegex != null && (name == null || !name.matches(nameRegex))) {
return false;
}
return true;
}
@Override
@SuppressWarnings("unchecked")
public void write(JmeExporter ex) throws IOException {
OutputCapsule capsule = ex.getCapsule(this);
capsule.write(name, "name", null);
capsule.write(worldBound, "world_bound", null);
capsule.write(cullHint, "cull_mode", CullHint.Inherit);
capsule.write(batchHint, "batch_hint", BatchHint.Inherit);
capsule.write(queueBucket, "queue", RenderQueue.Bucket.Inherit);
capsule.write(shadowMode, "shadow_mode", ShadowMode.Inherit);
capsule.write(localTransform, "transform", Transform.IDENTITY);
capsule.write(localLights, "lights", null);
capsule.writeSavableArrayList(new ArrayList(localOverrides), "overrides", null);
// Shallow clone the controls array to convert its type.
capsule.writeSavableArrayList(new ArrayList(controls), "controlsList", null);
capsule.writeStringSavableMap(userData, "user_data", null);
}
@Override
@SuppressWarnings("unchecked")
public void read(JmeImporter im) throws IOException {
InputCapsule ic = im.getCapsule(this);
name = ic.readString("name", null);
worldBound = (BoundingVolume) ic.readSavable("world_bound", null);
cullHint = ic.readEnum("cull_mode", CullHint.class, CullHint.Inherit);
batchHint = ic.readEnum("batch_hint", BatchHint.class, BatchHint.Inherit);
queueBucket = ic.readEnum("queue", RenderQueue.Bucket.class,
RenderQueue.Bucket.Inherit);
shadowMode = ic.readEnum("shadow_mode", ShadowMode.class,
ShadowMode.Inherit);
localTransform = (Transform) ic.readSavable("transform", Transform.IDENTITY);
localLights = (LightList) ic.readSavable("lights", null);
localLights.setOwner(this);
ArrayList localOverridesList = ic.readSavableArrayList("overrides", null);
if (localOverridesList == null) {
localOverrides = new SafeArrayList<>(MatParamOverride.class);
} else {
localOverrides = new SafeArrayList(MatParamOverride.class, localOverridesList);
}
worldOverrides = new SafeArrayList<>(MatParamOverride.class);
//changed for backward compatibility with j3o files
//generated before the AnimControl/SkeletonControl split
//the AnimControl creates the SkeletonControl for old files and add it to the spatial.
// The SkeletonControl must be the last in the stack,
// so we add the list of all other controls before it.
//When backward compatibility won't be needed anymore this can be replaced by :
//controls = ic.readSavableArrayList("controlsList", null));
controls.addAll(0, ic.readSavableArrayList("controlsList", null));
userData = (HashMap) ic.readStringSavableMap("user_data", null);
}
/**
* getWorldBound
retrieves the world bound at this node
* level.
*
* @return the world bound at this level.
*/
public BoundingVolume getWorldBound() {
checkDoBoundUpdate();
return worldBound;
}
/**
* setCullHint
alters how view frustum culling will treat this
* spatial.
*
* @param hint one of: CullHint.Dynamic
,
* CullHint.Always
, CullHint.Inherit
, or
* CullHint.Never
*
* The effect of the default value (CullHint.Inherit) may change if the
* spatial gets re-parented.
*/
public void setCullHint(CullHint hint) {
cullHint = hint;
}
/**
* setBatchHint
alters how batching will treat this spatial.
*
* @param hint one of: BatchHint.Never
,
* BatchHint.Always
, or BatchHint.Inherit
*
* The effect of the default value (BatchHint.Inherit) may change if the
* spatial gets re-parented.
*/
public void setBatchHint(BatchHint hint) {
batchHint = hint;
}
/**
* @return the cull mode of this Spatial
*/
public CullHint getLocalCullHint() {
return cullHint;
}
/**
* @return the batch hint for this Spatial
*/
public BatchHint getLocalBatchHint() {
return batchHint;
}
/**
* setQueueBucket
determines at what phase of the
* rendering process this Spatial will rendered. See the
* {@link Bucket} enum for an explanation of the various
* render queue buckets.
*
* @param queueBucket
* The bucket to use for this Spatial.
*/
public void setQueueBucket(RenderQueue.Bucket queueBucket) {
this.queueBucket = queueBucket;
}
/**
* Sets the shadow mode of the spatial
* The shadow mode determines how the spatial should be shadowed,
* when a shadowing technique is used. See the
* documentation for the class {@link ShadowMode} for more information.
*
* @see ShadowMode
*
* @param shadowMode The local shadow mode to set.
*/
public void setShadowMode(RenderQueue.ShadowMode shadowMode) {
this.shadowMode = shadowMode;
}
/**
* @return The locally set queue bucket mode
*
* @see Spatial#setQueueBucket(com.jme3.renderer.queue.RenderQueue.Bucket)
*/
public RenderQueue.Bucket getLocalQueueBucket() {
return queueBucket;
}
/**
* @return The locally set shadow mode
*
* @see Spatial#setShadowMode(com.jme3.renderer.queue.RenderQueue.ShadowMode)
*/
public RenderQueue.ShadowMode getLocalShadowMode() {
return shadowMode;
}
/**
* Returns this spatial's last frustum intersection result. This int is set
* when a check is made to determine if the bounds of the object fall inside
* a camera's frustum. If a parent is found to fall outside the frustum, the
* value for this spatial will not be updated.
*
* @return The spatial's last frustum intersection result.
*/
public Camera.FrustumIntersect getLastFrustumIntersection() {
return frustrumIntersects;
}
/**
* Overrides the last intersection result. This is useful for operations
* that want to start rendering at the middle of a scene tree and don't want
* the parent of that node to influence culling.
*
* @param intersects
* the new value
*/
public void setLastFrustumIntersection(Camera.FrustumIntersect intersects) {
frustrumIntersects = intersects;
}
/**
* Returns the Spatial's name followed by the class of the spatial
* Example: "MyNode (com.jme3.scene.Spatial)"
*
* @return Spatial's name followed by the class of the Spatial
*/
@Override
public String toString() {
return name + " (" + this.getClass().getSimpleName() + ')';
}
/**
* Creates a transform matrix that will convert from this spatials'
* local coordinate space to the world coordinate space
* based on the world transform.
*
* @param store Matrix where to store the result, if null, a new one
* will be created and returned.
*
* @return store if not null, otherwise, a new matrix containing the result.
*
* @see Spatial#getWorldTransform()
*/
public Matrix4f getLocalToWorldMatrix(Matrix4f store) {
if (store == null) {
store = new Matrix4f();
} else {
store.loadIdentity();
}
// multiply with scale first, then rotate, finally translate (cf.
// Eberly)
store.scale(getWorldScale());
store.multLocal(getWorldRotation());
store.setTranslation(getWorldTranslation());
return store;
}
/**
* Visit each scene graph element ordered by DFS with the default post order mode.
*
* @param visitor the action to take for each visited Spatial
* @see #depthFirstTraversal(com.jme3.scene.SceneGraphVisitor, com.jme3.scene.Spatial.DFSMode)
*/
public void depthFirstTraversal(SceneGraphVisitor visitor) {
depthFirstTraversal(visitor, DFSMode.POST_ORDER);
}
/**
* Specifies the mode of the depth first search.
*/
public static enum DFSMode {
/**
* Pre order: the current spatial is visited first, then its children.
*/
PRE_ORDER,
/**
* Post order: the children are visited first, then the parent.
*/
POST_ORDER;
}
/**
* Visit each scene graph element ordered by DFS.
* There are two modes: pre order and post order.
*
* @param visitor the action to take for each visited Spatial
* @param mode the traversal mode: pre order or post order
*/
public abstract void depthFirstTraversal(SceneGraphVisitor visitor, DFSMode mode);
/**
* Visit each scene graph element ordered by BFS
*
* @param visitor the action to take for each visited Spatial
*/
public void breadthFirstTraversal(SceneGraphVisitor visitor) {
Queue queue = new LinkedList<>();
queue.add(this);
while (!queue.isEmpty()) {
Spatial s = queue.poll();
visitor.visit(s);
s.breadthFirstTraversal(visitor, queue);
}
}
protected abstract void breadthFirstTraversal(SceneGraphVisitor visitor, Queue queue);
}