Kaynağa Gözat

Merge master into branch

Nehon 7 yıl önce
ebeveyn
işleme
54ef6ec280
100 değiştirilmiş dosya ile 7595 ekleme ve 1082 silme
  1. 1 1
      .gitignore
  2. 1 0
      .travis.yml
  3. 1 1
      gradle.properties
  4. 3 3
      jme3-bullet/src/common/java/com/jme3/bullet/BulletAppState.java
  5. 9 19
      jme3-bullet/src/common/java/com/jme3/bullet/control/KinematicRagdollControl.java
  6. 99 0
      jme3-core/src/main/java/com/jme3/anim/AnimClip.java
  7. 250 0
      jme3-core/src/main/java/com/jme3/anim/AnimComposer.java
  8. 12 0
      jme3-core/src/main/java/com/jme3/anim/AnimTrack.java
  9. 12 0
      jme3-core/src/main/java/com/jme3/anim/AnimationMask.java
  10. 300 0
      jme3-core/src/main/java/com/jme3/anim/Armature.java
  11. 62 0
      jme3-core/src/main/java/com/jme3/anim/ArmatureMask.java
  12. 342 0
      jme3-core/src/main/java/com/jme3/anim/Joint.java
  13. 47 0
      jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java
  14. 353 0
      jme3-core/src/main/java/com/jme3/anim/MorphControl.java
  15. 219 0
      jme3-core/src/main/java/com/jme3/anim/MorphTrack.java
  16. 43 0
      jme3-core/src/main/java/com/jme3/anim/SeparateJointModelTransform.java
  17. 744 0
      jme3-core/src/main/java/com/jme3/anim/SkinningControl.java
  18. 315 0
      jme3-core/src/main/java/com/jme3/anim/TransformTrack.java
  19. 95 0
      jme3-core/src/main/java/com/jme3/anim/Weights.java
  20. 13 0
      jme3-core/src/main/java/com/jme3/anim/interpolator/AnimInterpolator.java
  21. 149 0
      jme3-core/src/main/java/com/jme3/anim/interpolator/AnimInterpolators.java
  22. 136 0
      jme3-core/src/main/java/com/jme3/anim/interpolator/FrameInterpolator.java
  23. 97 0
      jme3-core/src/main/java/com/jme3/anim/tween/AbstractTween.java
  24. 6 0
      jme3-core/src/main/java/com/jme3/anim/tween/ContainsTweens.java
  25. 71 0
      jme3-core/src/main/java/com/jme3/anim/tween/Tween.java
  26. 619 0
      jme3-core/src/main/java/com/jme3/anim/tween/Tweens.java
  27. 70 0
      jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java
  28. 38 0
      jme3-core/src/main/java/com/jme3/anim/tween/action/BaseAction.java
  29. 129 0
      jme3-core/src/main/java/com/jme3/anim/tween/action/BlendAction.java
  30. 10 0
      jme3-core/src/main/java/com/jme3/anim/tween/action/BlendSpace.java
  31. 97 0
      jme3-core/src/main/java/com/jme3/anim/tween/action/BlendableAction.java
  32. 94 0
      jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java
  33. 49 0
      jme3-core/src/main/java/com/jme3/anim/tween/action/LinearBlendSpace.java
  34. 200 0
      jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java
  35. 10 0
      jme3-core/src/main/java/com/jme3/anim/util/HasLocalTransform.java
  36. 20 0
      jme3-core/src/main/java/com/jme3/anim/util/JointModelTransform.java
  37. 56 0
      jme3-core/src/main/java/com/jme3/anim/util/Primitives.java
  38. 11 0
      jme3-core/src/main/java/com/jme3/anim/util/Weighted.java
  39. 2 0
      jme3-core/src/main/java/com/jme3/animation/AnimChannel.java
  40. 2 0
      jme3-core/src/main/java/com/jme3/animation/AnimControl.java
  41. 1 0
      jme3-core/src/main/java/com/jme3/animation/AnimEventListener.java
  42. 3 0
      jme3-core/src/main/java/com/jme3/animation/Animation.java
  43. 3 5
      jme3-core/src/main/java/com/jme3/animation/AudioTrack.java
  44. 13 1
      jme3-core/src/main/java/com/jme3/animation/Bone.java
  45. 2 0
      jme3-core/src/main/java/com/jme3/animation/BoneTrack.java
  46. 1 0
      jme3-core/src/main/java/com/jme3/animation/ClonableTrack.java
  47. 5 1
      jme3-core/src/main/java/com/jme3/animation/CompactArray.java
  48. 100 0
      jme3-core/src/main/java/com/jme3/animation/CompactFloatArray.java
  49. 10 5
      jme3-core/src/main/java/com/jme3/animation/EffectTrack.java
  50. 1 0
      jme3-core/src/main/java/com/jme3/animation/LoopMode.java
  51. 2 0
      jme3-core/src/main/java/com/jme3/animation/Pose.java
  52. 5 1
      jme3-core/src/main/java/com/jme3/animation/Skeleton.java
  53. 3 0
      jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java
  54. 3 5
      jme3-core/src/main/java/com/jme3/animation/SpatialTrack.java
  55. 1 0
      jme3-core/src/main/java/com/jme3/animation/Track.java
  56. 3 5
      jme3-core/src/main/java/com/jme3/animation/TrackInfo.java
  57. 10 0
      jme3-core/src/main/java/com/jme3/bounding/Intersection.java
  58. 5 12
      jme3-core/src/main/java/com/jme3/cinematic/events/AnimationEvent.java
  59. 4 9
      jme3-core/src/main/java/com/jme3/environment/EnvironmentCamera.java
  60. 21 0
      jme3-core/src/main/java/com/jme3/environment/LightProbeFactory.java
  61. 22 8
      jme3-core/src/main/java/com/jme3/environment/util/LightsDebugState.java
  62. 8 4
      jme3-core/src/main/java/com/jme3/light/DefaultLightFilter.java
  63. 114 63
      jme3-core/src/main/java/com/jme3/light/LightProbe.java
  64. 0 214
      jme3-core/src/main/java/com/jme3/light/LightProbeBlendingProcessor.java
  65. 254 0
      jme3-core/src/main/java/com/jme3/light/OrientedBoxProbeArea.java
  66. 0 107
      jme3-core/src/main/java/com/jme3/light/PoiLightProbeLightFilter.java
  67. 1 6
      jme3-core/src/main/java/com/jme3/light/PointLight.java
  68. 35 0
      jme3-core/src/main/java/com/jme3/light/ProbeArea.java
  69. 103 0
      jme3-core/src/main/java/com/jme3/light/SphereProbeArea.java
  70. 77 0
      jme3-core/src/main/java/com/jme3/light/WeightedProbeBlendingStrategy.java
  71. 7 0
      jme3-core/src/main/java/com/jme3/material/MatParamOverride.java
  72. 67 16
      jme3-core/src/main/java/com/jme3/material/Material.java
  73. 81 102
      jme3-core/src/main/java/com/jme3/material/RenderState.java
  74. 1 3
      jme3-core/src/main/java/com/jme3/material/logic/MultiPassLightingLogic.java
  75. 40 19
      jme3-core/src/main/java/com/jme3/material/logic/SinglePassAndImageBasedLightingLogic.java
  76. 1 4
      jme3-core/src/main/java/com/jme3/material/logic/SinglePassLightingLogic.java
  77. 13 0
      jme3-core/src/main/java/com/jme3/math/ColorRGBA.java
  78. 13 0
      jme3-core/src/main/java/com/jme3/math/EaseFunction.java
  79. 163 0
      jme3-core/src/main/java/com/jme3/math/Easing.java
  80. 247 0
      jme3-core/src/main/java/com/jme3/math/MathUtils.java
  81. 71 64
      jme3-core/src/main/java/com/jme3/math/Matrix4f.java
  82. 51 8
      jme3-core/src/main/java/com/jme3/math/Quaternion.java
  83. 21 10
      jme3-core/src/main/java/com/jme3/math/Transform.java
  84. 9 1
      jme3-core/src/main/java/com/jme3/renderer/Caps.java
  85. 16 0
      jme3-core/src/main/java/com/jme3/renderer/Limits.java
  86. 29 0
      jme3-core/src/main/java/com/jme3/renderer/RenderContext.java
  87. 1 1
      jme3-core/src/main/java/com/jme3/renderer/RenderManager.java
  88. 15 0
      jme3-core/src/main/java/com/jme3/renderer/Renderer.java
  89. 228 227
      jme3-core/src/main/java/com/jme3/renderer/opengl/GL.java
  90. 83 0
      jme3-core/src/main/java/com/jme3/renderer/opengl/GL3.java
  91. 50 0
      jme3-core/src/main/java/com/jme3/renderer/opengl/GL4.java
  92. 32 0
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLDebugDesktop.java
  93. 5 1
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java
  94. 294 83
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java
  95. 86 1
      jme3-core/src/main/java/com/jme3/scene/Geometry.java
  96. 79 42
      jme3-core/src/main/java/com/jme3/scene/Mesh.java
  97. 3 2
      jme3-core/src/main/java/com/jme3/scene/Spatial.java
  98. 58 12
      jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java
  99. 26 16
      jme3-core/src/main/java/com/jme3/scene/debug/WireFrustum.java
  100. 208 0
      jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java

+ 1 - 1
.gitignore

@@ -2,7 +2,7 @@
 **/.classpath
 **/.settings
 **/.project
-**/out
+**/out/
 /.gradle/
 /.nb-gradle/
 /.idea/

+ 1 - 0
.travis.yml

@@ -6,6 +6,7 @@ branches:
   only:
   - master
   - v3.1
+  - /^v3.2.0-.*$/
 
 matrix:
   include:

+ 1 - 1
gradle.properties

@@ -8,7 +8,7 @@ jmeVersionTag = SNAPSHOT
 jmeVersionTagID = 0
 
 # specify if JavaDoc should be built
-buildJavaDoc = false
+buildJavaDoc = true
 
 # specify if SDK and Native libraries get built
 buildNativeProjects = false

+ 3 - 3
jme3-bullet/src/common/java/com/jme3/bullet/BulletAppState.java

@@ -67,7 +67,7 @@ public class BulletAppState implements AppState, PhysicsTickListener {
 
     /**
      * Creates a new BulletAppState running a PhysicsSpace for physics
-     * simulation, use getStateManager().addState(bulletAppState) to enable
+     * simulation, use getStateManager().attach(bulletAppState) to enable
      * physics for an Application.
      */
     public BulletAppState() {
@@ -75,7 +75,7 @@ public class BulletAppState implements AppState, PhysicsTickListener {
 
     /**
      * Creates a new BulletAppState running a PhysicsSpace for physics
-     * simulation, use getStateManager().addState(bulletAppState) to enable
+     * simulation, use getStateManager().attach(bulletAppState) to enable
      * physics for an Application.
      *
      * @param broadphaseType The type of broadphase collision detection,
@@ -87,7 +87,7 @@ public class BulletAppState implements AppState, PhysicsTickListener {
 
     /**
      * Creates a new BulletAppState running a PhysicsSpace for physics
-     * simulation, use getStateManager().addState(bulletAppState) to enable
+     * simulation, use getStateManager().attach(bulletAppState) to enable
      * physics for an Application. An AxisSweep broadphase is used.
      *
      * @param worldMin The minimum world extent

+ 9 - 19
jme3-bullet/src/common/java/com/jme3/bullet/control/KinematicRagdollControl.java

@@ -31,36 +31,23 @@
  */
 package com.jme3.bullet.control;
 
-import com.jme3.animation.AnimControl;
-import com.jme3.animation.Bone;
-import com.jme3.animation.Skeleton;
-import com.jme3.animation.SkeletonControl;
+import com.jme3.animation.*;
 import com.jme3.bullet.PhysicsSpace;
-import com.jme3.bullet.collision.PhysicsCollisionEvent;
-import com.jme3.bullet.collision.PhysicsCollisionListener;
-import com.jme3.bullet.collision.PhysicsCollisionObject;
-import com.jme3.bullet.collision.RagdollCollisionListener;
+import com.jme3.bullet.collision.*;
 import com.jme3.bullet.collision.shapes.BoxCollisionShape;
 import com.jme3.bullet.collision.shapes.HullCollisionShape;
-import com.jme3.bullet.control.ragdoll.HumanoidRagdollPreset;
-import com.jme3.bullet.control.ragdoll.RagdollPreset;
-import com.jme3.bullet.control.ragdoll.RagdollUtils;
+import com.jme3.bullet.control.ragdoll.*;
 import com.jme3.bullet.joints.SixDofJoint;
 import com.jme3.bullet.objects.PhysicsRigidBody;
-import com.jme3.export.InputCapsule;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.OutputCapsule;
-import com.jme3.export.Savable;
-import com.jme3.math.FastMath;
-import com.jme3.math.Quaternion;
-import com.jme3.math.Vector3f;
+import com.jme3.export.*;
+import com.jme3.math.*;
 import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.ViewPort;
 import com.jme3.scene.Node;
 import com.jme3.scene.Spatial;
 import com.jme3.util.TempVars;
 import com.jme3.util.clone.JmeCloneable;
+
 import java.io.IOException;
 import java.util.*;
 import java.util.logging.Level;
@@ -91,7 +78,10 @@ import java.util.logging.Logger;
  * </ul> </p>
  *
  * @author Normen Hansen and Rémy Bouquet (Nehon)
+ *
+ * TODO this needs to be redone with the new animation system
  */
+@Deprecated
 public class KinematicRagdollControl extends AbstractPhysicsControl implements PhysicsCollisionListener, JmeCloneable {
 
     protected static final Logger logger = Logger.getLogger(KinematicRagdollControl.class.getName());

+ 99 - 0
jme3-core/src/main/java/com/jme3/anim/AnimClip.java

@@ -0,0 +1,99 @@
+package com.jme3.anim;
+
+import com.jme3.anim.tween.Tween;
+import com.jme3.export.*;
+import com.jme3.util.SafeArrayList;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
+
+import java.io.IOException;
+
+/**
+ * Created by Nehon on 20/12/2017.
+ */
+public class AnimClip implements JmeCloneable, Savable {
+
+    private String name;
+    private double length;
+
+    private AnimTrack[] tracks;
+
+    public AnimClip() {
+    }
+
+    public AnimClip(String name) {
+        this.name = name;
+    }
+
+    public void setTracks(AnimTrack[] tracks) {
+        this.tracks = tracks;
+        for (AnimTrack track : tracks) {
+            if (track.getLength() > length) {
+                length = track.getLength();
+            }
+        }
+    }
+
+    public String getName() {
+        return name;
+    }
+
+
+    public double getLength() {
+        return length;
+    }
+
+
+    public AnimTrack[] getTracks() {
+        return tracks;
+    }
+
+    @Override
+    public Object jmeClone() {
+        try {
+            return super.clone();
+        } catch (CloneNotSupportedException e) {
+            throw new RuntimeException("Error cloning", e);
+        }
+    }
+
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        AnimTrack[] newTracks = new AnimTrack[tracks.length];
+        for (int i = 0; i < tracks.length; i++) {
+            newTracks[i] = (cloner.clone(tracks[i]));
+        }
+        this.tracks = newTracks;
+    }
+
+    @Override
+    public String toString() {
+        return "Clip " + name + ", " + length + 's';
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(name, "name", null);
+        oc.write(tracks, "tracks", null);
+
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule ic = im.getCapsule(this);
+        name = ic.readString("name", null);
+        Savable[] arr = ic.readSavableArray("tracks", null);
+        if (arr != null) {
+            tracks = new AnimTrack[arr.length];
+            for (int i = 0; i < arr.length; i++) {
+                AnimTrack t = (AnimTrack) arr[i];
+                tracks[i] = t;
+                if (t.getLength() > length) {
+                    length = t.getLength();
+                }
+            }
+        }
+    }
+
+}

+ 250 - 0
jme3-core/src/main/java/com/jme3/anim/AnimComposer.java

@@ -0,0 +1,250 @@
+package com.jme3.anim;
+
+import com.jme3.anim.tween.Tween;
+import com.jme3.anim.tween.Tweens;
+import com.jme3.anim.tween.action.*;
+import com.jme3.export.*;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.scene.control.AbstractControl;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
+
+import java.io.IOException;
+import java.util.*;
+
+/**
+ * Created by Nehon on 20/12/2017.
+ */
+public class AnimComposer extends AbstractControl {
+
+    public static final String DEFAULT_LAYER = "Default";
+    private Map<String, AnimClip> animClipMap = new HashMap<>();
+
+    private Map<String, Action> actions = new HashMap<>();
+    private float globalSpeed = 1f;
+    private Map<String, Layer> layers = new LinkedHashMap<>();
+
+    public AnimComposer() {
+        layers.put(DEFAULT_LAYER, new Layer());
+    }
+
+    /**
+     * Retrieve an animation from the list of animations.
+     *
+     * @param name The name of the animation to retrieve.
+     * @return The animation corresponding to the given name, or null, if no
+     * such named animation exists.
+     */
+    public AnimClip getAnimClip(String name) {
+        return animClipMap.get(name);
+    }
+
+    /**
+     * Adds an animation to be available for playing to this
+     * <code>AnimControl</code>.
+     *
+     * @param anim The animation to add.
+     */
+    public void addAnimClip(AnimClip anim) {
+        animClipMap.put(anim.getName(), anim);
+    }
+
+    /**
+     * Remove an animation so that it is no longer available for playing.
+     *
+     * @param anim The animation to remove.
+     */
+    public void removeAnimClip(AnimClip anim) {
+        if (!animClipMap.containsKey(anim.getName())) {
+            throw new IllegalArgumentException("Given animation does not exist "
+                    + "in this AnimControl");
+        }
+
+        animClipMap.remove(anim.getName());
+    }
+
+    public Action setCurrentAction(String name) {
+        return setCurrentAction(name, DEFAULT_LAYER);
+    }
+
+    public Action setCurrentAction(String actionName, String layerName) {
+        Layer l = layers.get(layerName);
+        if (l == null) {
+            throw new IllegalArgumentException("Unknown layer " + layerName);
+        }
+        Action currentAction = action(actionName);
+        l.time = 0;
+        l.currentAction = currentAction;
+        return currentAction;
+    }
+
+    public Action action(String name) {
+        Action action = actions.get(name);
+        if (action == null) {
+            action = makeAction(name);
+            actions.put(name, action);
+        }
+        return action;
+    }
+
+    public Action makeAction(String name) {
+        Action action;
+        AnimClip clip = animClipMap.get(name);
+        if (clip == null) {
+            throw new IllegalArgumentException("Cannot find clip named " + name);
+        }
+        action = new ClipAction(clip);
+        return action;
+    }
+
+    public void makeLayer(String name, AnimationMask mask){
+        Layer l = new Layer();
+        l.mask = mask;
+        layers.put(name, l);
+    }
+
+
+    public BaseAction actionSequence(String name, Tween... tweens) {
+        BaseAction action = new BaseAction(Tweens.sequence(tweens));
+        actions.put(name, action);
+        return action;
+    }
+
+    public BlendAction actionBlended(String name, BlendSpace blendSpace, String... clips) {
+        BlendableAction[] acts = new BlendableAction[clips.length];
+        for (int i = 0; i < acts.length; i++) {
+            BlendableAction ba = (BlendableAction) makeAction(clips[i]);
+            acts[i] = ba;
+        }
+        BlendAction action = new BlendAction(blendSpace, acts);
+        actions.put(name, action);
+        return action;
+    }
+
+    public void reset() {
+        for (Layer layer : layers.values()) {
+            layer.currentAction = null;
+            layer.time = 0;
+        }
+    }
+
+    public Collection<AnimClip> getAnimClips() {
+        return Collections.unmodifiableCollection(animClipMap.values());
+    }
+
+    public Collection<String> getAnimClipsNames() {
+        return Collections.unmodifiableCollection(animClipMap.keySet());
+    }
+
+    @Override
+    protected void controlUpdate(float tpf) {
+        for (Layer layer : layers.values()) {
+            Action currentAction = layer.currentAction;
+            if (currentAction == null) {
+                continue;
+            }
+            layer.advance(tpf);
+
+            currentAction.setMask(layer.mask);
+            boolean running = currentAction.interpolate(layer.time);
+            currentAction.setMask(null);
+
+            if (!running) {
+                layer.time = 0;
+            }
+        }
+    }
+
+    @Override
+    protected void controlRender(RenderManager rm, ViewPort vp) {
+
+    }
+
+    public float getGlobalSpeed() {
+        return globalSpeed;
+    }
+
+    public void setGlobalSpeed(float globalSpeed) {
+        this.globalSpeed = globalSpeed;
+    }
+
+    @Override
+    public Object jmeClone() {
+        try {
+            AnimComposer clone = (AnimComposer) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException ex) {
+            throw new AssertionError();
+        }
+    }
+
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        super.cloneFields(cloner, original);
+        Map<String, AnimClip> clips = new HashMap<>();
+        for (String key : animClipMap.keySet()) {
+            clips.put(key, cloner.clone(animClipMap.get(key)));
+        }
+        Map<String, Action> act = new HashMap<>();
+        for (String key : actions.keySet()) {
+            act.put(key, cloner.clone(actions.get(key)));
+        }
+        actions = act;
+        animClipMap = clips;
+
+        Map<String, Layer> newLayers = new LinkedHashMap<>();
+        for (String key : layers.keySet()) {
+            newLayers.put(key, cloner.clone(layers.get(key)));
+        }
+
+        layers = newLayers;
+
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+        animClipMap = (Map<String, AnimClip>) ic.readStringSavableMap("animClipMap", new HashMap<String, AnimClip>());
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.writeStringSavableMap(animClipMap, "animClipMap", new HashMap<String, AnimClip>());
+    }
+
+    private class Layer implements JmeCloneable {
+        private Action currentAction;
+        private AnimationMask mask;
+        private float weight;
+        private double time;
+
+        public void advance(float tpf) {
+            time += tpf * currentAction.getSpeed() * globalSpeed;
+            // make sure negative time is in [0, length] range
+            if (time < 0) {
+                double length = currentAction.getLength();
+                time = (time % length + length) % length;
+            }
+
+        }
+
+        @Override
+        public Object jmeClone() {
+            try {
+                Layer clone = (Layer) super.clone();
+                return clone;
+            } catch (CloneNotSupportedException ex) {
+                throw new AssertionError();
+            }
+        }
+
+        @Override
+        public void cloneFields(Cloner cloner, Object original) {
+            currentAction = null;
+        }
+    }
+}

+ 12 - 0
jme3-core/src/main/java/com/jme3/anim/AnimTrack.java

@@ -0,0 +1,12 @@
+package com.jme3.anim;
+
+import com.jme3.export.Savable;
+import com.jme3.util.clone.JmeCloneable;
+
+public interface AnimTrack<T> extends Savable, JmeCloneable {
+
+    public void getDataAtTime(double time, T store);
+    public double getLength();
+
+
+}

+ 12 - 0
jme3-core/src/main/java/com/jme3/anim/AnimationMask.java

@@ -0,0 +1,12 @@
+package com.jme3.anim;
+
+/**
+ * Created by Nehon
+ * An AnimationMask is defining a subset of elements on which an animation will be applied.
+ * Most used implementation is the ArmatureMask that defines a subset of joints in an Armature.
+ */
+public interface AnimationMask {
+
+    boolean contains(Object target);
+
+}

+ 300 - 0
jme3-core/src/main/java/com/jme3/anim/Armature.java

@@ -0,0 +1,300 @@
+package com.jme3.anim;
+
+import com.jme3.anim.util.JointModelTransform;
+import com.jme3.asset.AssetLoadException;
+import com.jme3.export.*;
+import com.jme3.math.Matrix4f;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
+
+import java.io.IOException;
+import java.util.*;
+
+/**
+ * Created by Nehon on 15/12/2017.
+ */
+public class Armature implements JmeCloneable, Savable {
+
+    private Joint[] rootJoints;
+    private Joint[] jointList;
+
+    /**
+     * Contains the skinning matrices, multiplying it by a vertex effected by a bone
+     * will cause it to go to the animated position.
+     */
+    private transient Matrix4f[] skinningMatrixes;
+    private Class<? extends JointModelTransform> modelTransformClass = SeparateJointModelTransform.class;
+
+    /**
+     * Serialization only
+     */
+    public Armature() {
+    }
+
+    /**
+     * Creates an armature from a joint list.
+     * The root joints are found automatically.
+     * <p>
+     * Note that using this constructor will cause the joints in the list
+     * to have their bind pose recomputed based on their local transforms.
+     *
+     * @param jointList The list of joints to manage by this Armature
+     */
+    public Armature(Joint[] jointList) {
+        this.jointList = jointList;
+
+        List<Joint> rootJointList = new ArrayList<>();
+        for (int i = jointList.length - 1; i >= 0; i--) {
+            Joint joint = jointList[i];
+            joint.setId(i);
+            instanciateJointModelTransform(joint);
+            if (joint.getParent() == null) {
+                rootJointList.add(joint);
+            }
+        }
+        rootJoints = rootJointList.toArray(new Joint[rootJointList.size()]);
+
+        createSkinningMatrices();
+
+        for (int i = rootJoints.length - 1; i >= 0; i--) {
+            Joint rootJoint = rootJoints[i];
+            rootJoint.update();
+        }
+    }
+
+    /**
+     * Update all joints sin this Amature.
+     */
+    public void update() {
+        for (Joint rootJoint : rootJoints) {
+            rootJoint.update();
+        }
+    }
+
+    private void createSkinningMatrices() {
+        skinningMatrixes = new Matrix4f[jointList.length];
+        for (int i = 0; i < skinningMatrixes.length; i++) {
+            skinningMatrixes[i] = new Matrix4f();
+        }
+    }
+
+    /**
+     * Sets the JointModelTransform implementation
+     * Default is {@link MatrixJointModelTransform}
+     *
+     * @param modelTransformClass
+     * @see {@link JointModelTransform},{@link MatrixJointModelTransform},{@link SeparateJointModelTransform},
+     */
+    public void setModelTransformClass(Class<? extends JointModelTransform> modelTransformClass) {
+        this.modelTransformClass = modelTransformClass;
+        if (jointList == null) {
+            return;
+        }
+        for (Joint joint : jointList) {
+            instanciateJointModelTransform(joint);
+        }
+    }
+
+    private void instanciateJointModelTransform(Joint joint) {
+        try {
+            joint.setJointModelTransform(modelTransformClass.newInstance());
+        } catch (InstantiationException | IllegalAccessException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * returns the array of all root joints of this Armature
+     *
+     * @return
+     */
+    public Joint[] getRoots() {
+        return rootJoints;
+    }
+
+    public List<Joint> getJointList() {
+        return Arrays.asList(jointList);
+    }
+
+    /**
+     * return a joint for the given index
+     *
+     * @param index
+     * @return
+     */
+    public Joint getJoint(int index) {
+        return jointList[index];
+    }
+
+    /**
+     * returns the joint with the given name
+     *
+     * @param name
+     * @return
+     */
+    public Joint getJoint(String name) {
+        for (int i = 0; i < jointList.length; i++) {
+            if (jointList[i].getName().equals(name)) {
+                return jointList[i];
+            }
+        }
+        return null;
+    }
+
+    /**
+     * returns the bone index of the given bone
+     *
+     * @param joint
+     * @return
+     */
+    public int getJointIndex(Joint joint) {
+        for (int i = 0; i < jointList.length; i++) {
+            if (jointList[i] == joint) {
+                return i;
+            }
+        }
+
+        return -1;
+    }
+
+    /**
+     * returns the joint index of the joint that has the given name
+     *
+     * @param name
+     * @return
+     */
+    public int getJointIndex(String name) {
+        for (int i = 0; i < jointList.length; i++) {
+            if (jointList[i].getName().equals(name)) {
+                return i;
+            }
+        }
+
+        return -1;
+    }
+
+    /**
+     * Saves the current Armature state as its bind pose.
+     * Note that the bind pose is supposed to be the one where the armature is aligned with the mesh to deform.
+     * Saving this pose will affect how skinning works.
+     */
+    public void saveBindPose() {
+        //make sure all bones are updated
+        update();
+        //Save the current pose as bind pose
+        for (Joint joint : jointList) {
+            joint.saveBindPose();
+        }
+    }
+
+    /**
+     * This methods sets this armature in its bind pose (aligned with the mesh to deform)
+     * Note that this is only useful for debugging purpose.
+     */
+    public void applyBindPose() {
+        for (Joint joint : rootJoints) {
+            joint.applyBindPose();
+        }
+    }
+
+    /**
+     * Saves the current local transform as the initial transform.
+     * Initial transform is the one applied to the armature when loaded.
+     */
+    public void saveInitialPose() {
+        for (Joint joint : jointList) {
+            joint.saveInitialPose();
+        }
+    }
+
+    /**
+     * Applies the initial pose to this armature
+     */
+    public void applyInitialPose() {
+        for (Joint rootJoint : rootJoints) {
+            rootJoint.applyInitialPose();
+        }
+    }
+
+    /**
+     * Compute the skinning matrices for each bone of the armature that would be used to transform vertices of associated meshes
+     *
+     * @return
+     */
+    public Matrix4f[] computeSkinningMatrices() {
+        for (int i = 0; i < jointList.length; i++) {
+            jointList[i].getOffsetTransform(skinningMatrixes[i]);
+        }
+        return skinningMatrixes;
+    }
+
+    /**
+     * returns the number of joints of this armature
+     *
+     * @return
+     */
+    public int getJointCount() {
+        return jointList.length;
+    }
+
+    @Override
+    public Object jmeClone() {
+        try {
+            Armature clone = (Armature) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException ex) {
+            throw new AssertionError();
+        }
+    }
+
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        this.rootJoints = cloner.clone(rootJoints);
+        this.jointList = cloner.clone(jointList);
+        this.skinningMatrixes = cloner.clone(skinningMatrixes);
+        for (Joint joint : jointList) {
+            instanciateJointModelTransform(joint);
+        }
+    }
+
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule input = im.getCapsule(this);
+
+        Savable[] jointRootsAsSavable = input.readSavableArray("rootJoints", null);
+        rootJoints = new Joint[jointRootsAsSavable.length];
+        System.arraycopy(jointRootsAsSavable, 0, rootJoints, 0, jointRootsAsSavable.length);
+
+        Savable[] jointListAsSavable = input.readSavableArray("jointList", null);
+        jointList = new Joint[jointListAsSavable.length];
+        System.arraycopy(jointListAsSavable, 0, jointList, 0, jointListAsSavable.length);
+
+        String className = input.readString("modelTransformClass", MatrixJointModelTransform.class.getCanonicalName());
+        try {
+            modelTransformClass = (Class<? extends JointModelTransform>) Class.forName(className);
+        } catch (ClassNotFoundException e) {
+            throw new AssetLoadException("Cannnot find class for name " + className);
+        }
+
+        int i = 0;
+        for (Joint joint : jointList) {
+            joint.setId(i++);
+            instanciateJointModelTransform(joint);
+        }
+        createSkinningMatrices();
+
+        for (Joint rootJoint : rootJoints) {
+            rootJoint.update();
+        }
+        applyInitialPose();
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        OutputCapsule output = ex.getCapsule(this);
+        output.write(rootJoints, "rootJoints", null);
+        output.write(jointList, "jointList", null);
+        output.write(modelTransformClass.getCanonicalName(), "modelTransformClass", MatrixJointModelTransform.class.getCanonicalName());
+    }
+}

+ 62 - 0
jme3-core/src/main/java/com/jme3/anim/ArmatureMask.java

@@ -0,0 +1,62 @@
+package com.jme3.anim;
+
+import java.util.BitSet;
+
+public class ArmatureMask implements AnimationMask {
+
+    private BitSet affectedJoints = new BitSet();
+
+    @Override
+    public boolean contains(Object target) {
+        return affectedJoints.get(((Joint) target).getId());
+    }
+
+    public static ArmatureMask createMask(Armature armature, String fromJoint) {
+        ArmatureMask mask = new ArmatureMask();
+        mask.addFromJoint(armature, fromJoint);
+        return mask;
+    }
+
+    public static ArmatureMask createMask(Armature armature, String... joints) {
+        ArmatureMask mask = new ArmatureMask();
+        mask.addBones(armature, joints);
+        for (String joint : joints) {
+            mask.affectedJoints.set(armature.getJoint(joint).getId());
+        }
+        return mask;
+    }
+
+    /**
+     * Add joints to be influenced by this animation mask.
+     */
+    public void addBones(Armature armature, String... jointNames) {
+        for (String jointName : jointNames) {
+            Joint joint = findJoint(armature, jointName);
+            affectedJoints.set(joint.getId());
+        }
+    }
+
+    private Joint findJoint(Armature armature, String jointName) {
+        Joint joint = armature.getJoint(jointName);
+        if (joint == null) {
+            throw new IllegalArgumentException("Cannot find joint " + jointName);
+        }
+        return joint;
+    }
+
+    /**
+     * Add a joint and all its sub armature joints to be influenced by this animation mask.
+     */
+    public void addFromJoint(Armature armature, String jointName) {
+        Joint joint = findJoint(armature, jointName);
+        recurseAddJoint(joint);
+    }
+
+    private void recurseAddJoint(Joint joint) {
+        affectedJoints.set(joint.getId());
+        for (Joint j : joint.getChildren()) {
+            recurseAddJoint(j);
+        }
+    }
+
+}

+ 342 - 0
jme3-core/src/main/java/com/jme3/anim/Joint.java

@@ -0,0 +1,342 @@
+package com.jme3.anim;
+
+import com.jme3.anim.util.HasLocalTransform;
+import com.jme3.anim.util.JointModelTransform;
+import com.jme3.export.*;
+import com.jme3.material.MatParamOverride;
+import com.jme3.math.*;
+import com.jme3.scene.*;
+import com.jme3.shader.VarType;
+import com.jme3.util.SafeArrayList;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A Joint is the basic component of an armature designed to perform skeletal animation
+ * Created by Nehon on 15/12/2017.
+ */
+public class Joint implements Savable, JmeCloneable, HasLocalTransform {
+
+    private String name;
+    private int id;
+    private Joint parent;
+    private SafeArrayList<Joint> children = new SafeArrayList<>(Joint.class);
+    private Geometry targetGeometry;
+
+    /**
+     * The attachment node.
+     */
+    private Node attachedNode;
+
+    /**
+     * The transform of the joint in local space. Relative to its parent.
+     * Or relative to the model's origin for the root joint.
+     */
+    private Transform localTransform = new Transform();
+
+    /**
+     * The initial transform of the joint in local space. Relative to its parent.
+     * Or relative to the model's origin for the root joint.
+     * this transform is the transform applied when the armature is loaded.
+     */
+    private Transform initialTransform = new Transform();
+
+    /**
+     * The transform of the joint in model space. Relative to the origin of the model.
+     * this is either a MatrixJointModelTransform or a SeparateJointModelTransform
+     */
+    private JointModelTransform jointModelTransform;
+
+    /**
+     * The matrix used to transform affected vertices position into the joint model space.
+     * Used for skinning.
+     */
+    private Matrix4f inverseModelBindMatrix = new Matrix4f();
+
+
+    public Joint() {
+    }
+
+    public Joint(String name) {
+        this.name = name;
+    }
+
+    /**
+     * Updates world transforms for this bone and it's children.
+     */
+    public final void update() {
+        this.updateModelTransforms();
+
+        for (Joint child : children.getArray()) {
+            child.update();
+        }
+    }
+
+    /**
+     * Updates the model transforms for this bone, and, possibly the attach node
+     * if not null.
+     * <p>
+     * The model transform of this bone is computed by combining the parent's
+     * model transform with this bones' local transform.
+     */
+    public final void updateModelTransforms() {
+        jointModelTransform.updateModelTransform(localTransform, parent);
+        updateAttachNode();
+    }
+
+    /**
+     * Update the local transform of the attachments node.
+     */
+    private void updateAttachNode() {
+        if (attachedNode == null) {
+            return;
+        }
+        Node attachParent = attachedNode.getParent();
+        if (attachParent == null || targetGeometry == null
+                || targetGeometry.getParent() == attachParent
+                && targetGeometry.getLocalTransform().isIdentity()) {
+            /*
+             * The animated meshes are in the same coordinate system as the
+             * attachments node: no further transforms are needed.
+             */
+            attachedNode.setLocalTransform(getModelTransform());
+
+        } else {
+            Spatial loopSpatial = targetGeometry;
+            Transform combined = getModelTransform().clone();
+            /*
+             * Climb the scene graph applying local transforms until the
+             * attachments node's parent is reached.
+             */
+            while (loopSpatial != attachParent && loopSpatial != null) {
+                Transform localTransform = loopSpatial.getLocalTransform();
+                combined.combineWithParent(localTransform);
+                loopSpatial = loopSpatial.getParent();
+            }
+            attachedNode.setLocalTransform(combined);
+        }
+    }
+
+    /**
+     * Stores the skinning transform in the specified Matrix4f.
+     * The skinning transform applies the animation of the bone to a vertex.
+     * <p>
+     * This assumes that the world transforms for the entire bone hierarchy
+     * have already been computed, otherwise this method will return undefined
+     * results.
+     *
+     * @param outTransform
+     */
+    void getOffsetTransform(Matrix4f outTransform) {
+        jointModelTransform.getOffsetTransform(outTransform, inverseModelBindMatrix);
+    }
+
+    /**
+     * Sets the current localTransform as the Bind transform.
+     */
+    protected void saveBindPose() {
+        //Note that the whole Armature must be updated before calling this method.
+        getModelTransform().toTransformMatrix(inverseModelBindMatrix);
+        inverseModelBindMatrix.invertLocal();
+    }
+
+    /**
+     * Sets the current local transforms as the initial transform.
+     */
+    protected void saveInitialPose() {
+        initialTransform.set(localTransform);
+    }
+
+    /**
+     * Sets the local transform with the bind transforms
+     */
+    protected void applyBindPose() {
+        jointModelTransform.applyBindPose(localTransform, inverseModelBindMatrix, parent);
+        updateModelTransforms();
+
+        for (Joint child : children.getArray()) {
+            child.applyBindPose();
+        }
+    }
+
+    /**
+     * Sets the local transform with the initial transform
+     */
+    protected void applyInitialPose() {
+        setLocalTransform(initialTransform);
+        updateModelTransforms();
+
+        for (Joint child : children.getArray()) {
+            child.applyInitialPose();
+        }
+    }
+
+    protected JointModelTransform getJointModelTransform() {
+        return jointModelTransform;
+    }
+
+    protected void setJointModelTransform(JointModelTransform jointModelTransform) {
+        this.jointModelTransform = jointModelTransform;
+    }
+
+    public Vector3f getLocalTranslation() {
+        return localTransform.getTranslation();
+    }
+
+    public Quaternion getLocalRotation() {
+        return localTransform.getRotation();
+    }
+
+    public Vector3f getLocalScale() {
+        return localTransform.getScale();
+    }
+
+    public void setLocalTranslation(Vector3f translation) {
+        localTransform.setTranslation(translation);
+    }
+
+    public void setLocalRotation(Quaternion rotation) {
+        localTransform.setRotation(rotation);
+    }
+
+    public void setLocalScale(Vector3f scale) {
+        localTransform.setScale(scale);
+    }
+
+    public void addChild(Joint child) {
+        children.add(child);
+        child.parent = this;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public void setLocalTransform(Transform localTransform) {
+        this.localTransform.set(localTransform);
+    }
+
+    public void setInverseModelBindMatrix(Matrix4f inverseModelBindMatrix) {
+        this.inverseModelBindMatrix = inverseModelBindMatrix;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public Joint getParent() {
+        return parent;
+    }
+
+    public List<Joint> getChildren() {
+        return children;
+    }
+
+    /**
+     * Access the attachments node of this joint. If this joint doesn't already
+     * have an attachments node, create one. Models and effects attached to the
+     * attachments node will follow this bone's motions.
+     *
+     * @param jointIndex this bone's index in its armature (&ge;0)
+     * @param targets    a list of geometries animated by this bone's skeleton (not
+     *                   null, unaffected)
+     */
+    Node getAttachmentsNode(int jointIndex, SafeArrayList<Geometry> targets) {
+        targetGeometry = null;
+        /*
+         * Search for a geometry animated by this particular bone.
+         */
+        for (Geometry geometry : targets) {
+            Mesh mesh = geometry.getMesh();
+            if (mesh != null && mesh.isAnimatedByJoint(jointIndex)) {
+                targetGeometry = geometry;
+                break;
+            }
+        }
+
+        if (attachedNode == null) {
+            attachedNode = new Node(name + "_attachnode");
+            attachedNode.setUserData("AttachedBone", this);
+            //We don't want the node to have a numBone set by a parent node so we force it to null
+            attachedNode.addMatParamOverride(new MatParamOverride(VarType.Int, "NumberOfBones", null));
+        }
+
+        return attachedNode;
+    }
+
+
+    public Transform getLocalTransform() {
+        return localTransform;
+    }
+
+    public Transform getModelTransform() {
+        return jointModelTransform.getModelTransform();
+    }
+
+    public Matrix4f getInverseModelBindMatrix() {
+        return inverseModelBindMatrix;
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    @Override
+    public Object jmeClone() {
+        try {
+            Joint clone = (Joint) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException ex) {
+            throw new AssertionError();
+        }
+    }
+
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        this.children = cloner.clone(children);
+        this.parent = cloner.clone(parent);
+        this.attachedNode = cloner.clone(attachedNode);
+        this.targetGeometry = cloner.clone(targetGeometry);
+        this.localTransform = cloner.clone(localTransform);
+        this.inverseModelBindMatrix = cloner.clone(inverseModelBindMatrix);
+    }
+
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule input = im.getCapsule(this);
+
+        name = input.readString("name", null);
+        attachedNode = (Node) input.readSavable("attachedNode", null);
+        targetGeometry = (Geometry) input.readSavable("targetGeometry", null);
+        initialTransform = (Transform) input.readSavable("initialTransform", new Transform());
+        inverseModelBindMatrix = (Matrix4f) input.readSavable("inverseModelBindMatrix", inverseModelBindMatrix);
+
+        ArrayList<Joint> childList = input.readSavableArrayList("children", null);
+        for (int i = childList.size() - 1; i >= 0; i--) {
+            this.addChild(childList.get(i));
+        }
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        OutputCapsule output = ex.getCapsule(this);
+
+        output.write(name, "name", null);
+        output.write(attachedNode, "attachedNode", null);
+        output.write(targetGeometry, "targetGeometry", null);
+        output.write(initialTransform, "initialTransform", new Transform());
+        output.write(inverseModelBindMatrix, "inverseModelBindMatrix", new Matrix4f());
+        output.writeSavableArrayList(new ArrayList(children), "children", null);
+    }
+
+}

+ 47 - 0
jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java

@@ -0,0 +1,47 @@
+package com.jme3.anim;
+
+import com.jme3.anim.util.JointModelTransform;
+import com.jme3.math.Matrix4f;
+import com.jme3.math.Transform;
+
+/**
+ * This JointModelTransform implementation accumulate joints transforms in a Matrix4f to properly
+ * support non uniform scaling in an armature hierarchy
+ */
+public class MatrixJointModelTransform implements JointModelTransform {
+
+    private Matrix4f modelTransformMatrix = new Matrix4f();
+    private Transform modelTransform = new Transform();
+
+    @Override
+    public void updateModelTransform(Transform localTransform, Joint parent) {
+        localTransform.toTransformMatrix(modelTransformMatrix);
+        if (parent != null) {
+            ((MatrixJointModelTransform) parent.getJointModelTransform()).getModelTransformMatrix().mult(modelTransformMatrix, modelTransformMatrix);
+        }
+
+    }
+
+    public void getOffsetTransform(Matrix4f outTransform, Matrix4f inverseModelBindMatrix) {
+        modelTransformMatrix.mult(inverseModelBindMatrix, outTransform);
+    }
+
+    @Override
+    public void applyBindPose(Transform localTransform, Matrix4f inverseModelBindMatrix, Joint parent) {
+        modelTransformMatrix.set(inverseModelBindMatrix).invertLocal(); // model transform = model bind
+        if (parent != null) {
+            ((MatrixJointModelTransform) parent.getJointModelTransform()).getModelTransformMatrix().invert().mult(modelTransformMatrix, modelTransformMatrix);
+        }
+        localTransform.fromTransformMatrix(modelTransformMatrix);
+    }
+
+    public Matrix4f getModelTransformMatrix() {
+        return modelTransformMatrix;
+    }
+
+    @Override
+    public Transform getModelTransform() {
+        modelTransform.fromTransformMatrix(modelTransformMatrix);
+        return modelTransform;
+    }
+}

+ 353 - 0
jme3-core/src/main/java/com/jme3/anim/MorphControl.java

@@ -0,0 +1,353 @@
+package com.jme3.anim;
+
+import com.jme3.export.Savable;
+import com.jme3.material.*;
+import com.jme3.renderer.*;
+import com.jme3.scene.*;
+import com.jme3.scene.control.AbstractControl;
+import com.jme3.scene.mesh.MorphTarget;
+import com.jme3.shader.VarType;
+import com.jme3.util.BufferUtils;
+import com.jme3.util.SafeArrayList;
+
+import java.nio.FloatBuffer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A control that handle morph animation for Position, Normal and Tangent buffers.
+ * All stock shaders only support morphing these 3 buffers, but note that MorphTargets can have any type of buffers.
+ * If you want to use other types of buffers you will need a custom MorphControl and a custom shader.
+ *
+ * @author Rémy Bouquet
+ */
+public class MorphControl extends AbstractControl implements Savable {
+
+    private static final Logger logger = Logger.getLogger(MorphControl.class.getName());
+
+    private static final int MAX_MORPH_BUFFERS = 14;
+    private final static float MIN_WEIGHT = 0.005f;
+
+    private SafeArrayList<Geometry> targets = new SafeArrayList<>(Geometry.class);
+    private TargetLocator targetLocator = new TargetLocator();
+
+    private boolean approximateTangents = true;
+    private MatParamOverride nullNumberOfBones = new MatParamOverride(VarType.Int, "NumberOfBones", null);
+
+    private float[] tmpPosArray;
+    private float[] tmpNormArray;
+    private float[] tmpTanArray;
+
+    private static final VertexBuffer.Type bufferTypes[] = VertexBuffer.Type.values();
+
+    @Override
+    protected void controlUpdate(float tpf) {
+        if (!enabled) {
+            return;
+        }
+        // gathering geometries in the sub graph.
+        // This must be done in the update phase as the gathering might add a matparam override
+        targets.clear();
+        this.spatial.depthFirstTraversal(targetLocator);
+    }
+
+    @Override
+    protected void controlRender(RenderManager rm, ViewPort vp) {
+        if (!enabled) {
+            return;
+        }
+        for (Geometry geom : targets) {
+            Mesh mesh = geom.getMesh();
+            if (!geom.isDirtyMorph()) {
+                continue;
+            }
+
+            Material m = geom.getMaterial();
+            float weights[] = geom.getMorphState();
+            MorphTarget morphTargets[] = mesh.getMorphTargets();
+            float matWeights[];
+            //Number of buffer to handle for each morph target
+            int targetNumBuffers = getTargetNumBuffers(morphTargets[0]);
+
+            int maxGPUTargets = getMaxGPUTargets(rm, geom, m, targetNumBuffers);
+
+            MatParam param2 = m.getParam("MorphWeights");
+            matWeights = (float[]) param2.getValue();
+
+            int nbGPUTargets = 0;
+            int lastGpuTargetIndex = 0;
+            int boundBufferIdx = 0;
+            float cpuWeightSum = 0;
+            // binding the morphTargets buffer to the mesh morph buffers
+            for (int i = 0; i < morphTargets.length; i++) {
+                // discard weights below the threshold
+                if (weights[i] < MIN_WEIGHT) {
+                    continue;
+                }
+                if (nbGPUTargets >= maxGPUTargets) {
+                    // we already bound all the available gpu slots we need to merge the remaining morph targets.
+                    cpuWeightSum += weights[i];
+                    continue;
+                }
+                lastGpuTargetIndex = i;
+                // binding the morph target's buffers to the mesh morph buffers.
+                MorphTarget t = morphTargets[i];
+                boundBufferIdx = bindMorphtargetBuffer(mesh, targetNumBuffers, boundBufferIdx, t);
+                // setting the weight in the mat param array
+                matWeights[nbGPUTargets] = weights[i];
+                nbGPUTargets++;
+            }
+
+            if (nbGPUTargets < matWeights.length) {
+                // if we have less simultaneous GPU targets than the length of the weight array, the array is padded with 0
+                for (int i = nbGPUTargets; i < matWeights.length; i++) {
+                    matWeights[i] = 0;
+                }
+            } else if (cpuWeightSum > 0) {
+                // we have more simultaneous morph targets than available gpu slots,
+                // we merge the additional morph targets and bind them to the last gpu slot
+                MorphTarget mt = geom.getFallbackMorphTarget();
+                if (mt == null) {
+                    mt = initCpuMorphTarget(geom);
+                    geom.setFallbackMorphTarget(mt);
+                }
+                // adding the last Gpu target weight
+                cpuWeightSum += matWeights[nbGPUTargets - 1];
+                ensureTmpArraysCapacity(geom.getVertexCount() * 3, targetNumBuffers);
+
+                // merging all remaining targets in tmp arrays
+                for (int i = lastGpuTargetIndex; i < morphTargets.length; i++) {
+                    if (weights[i] < MIN_WEIGHT) {
+                        continue;
+                    }
+                    float weight = weights[i] / cpuWeightSum;
+                    MorphTarget t = geom.getMesh().getMorphTargets()[i];
+                    mergeMorphTargets(targetNumBuffers, weight, t, i == lastGpuTargetIndex);
+                }
+
+                // writing the tmp arrays to the float buffer
+                writeCpuBuffer(targetNumBuffers, mt);
+
+                // binding the merged morph target
+                bindMorphtargetBuffer(mesh, targetNumBuffers, (nbGPUTargets - 1) * targetNumBuffers, mt);
+
+                // setting the eight of the merged targets
+                matWeights[nbGPUTargets - 1] = cpuWeightSum;
+            }
+            geom.setDirtyMorph(false);
+        }
+    }
+
+    private int getMaxGPUTargets(RenderManager rm, Geometry geom, Material mat, int targetNumBuffers) {
+        if (geom.getNbSimultaneousGPUMorph() > -1) {
+            return geom.getNbSimultaneousGPUMorph();
+        }
+
+        // Evaluate the number of CPU slots remaining for morph buffers.
+        int nbMaxBuffers = getRemainingBuffers(geom.getMesh(), rm.getRenderer());
+
+        int realNumTargetsBuffers = geom.getMesh().getMorphTargets().length * targetNumBuffers;
+
+        // compute the max number of targets to send to the GPU
+        int maxGPUTargets = Math.min(realNumTargetsBuffers, Math.min(nbMaxBuffers, MAX_MORPH_BUFFERS)) / targetNumBuffers;
+
+        MatParam param = mat.getParam("MorphWeights");
+        if (param == null) {
+            // init the mat param if it doesn't exists.
+            float[] wts = new float[maxGPUTargets];
+            mat.setParam("MorphWeights", VarType.FloatArray, wts);
+        }
+
+        mat.setInt("NumberOfTargetsBuffers", targetNumBuffers);
+
+        // test compile the shader to find the accurate number of remaining attributes slots
+        boolean compilationOk = false;
+        // Note that if ever the shader has an unrelated issue we want to break at some point, hence the maxGPUTargets > 0
+        while (!compilationOk && maxGPUTargets > 0) {
+            // setting the maximum number as the real number may change every frame and trigger a shader recompilation since it's bound to a define.
+            mat.setInt("NumberOfMorphTargets", maxGPUTargets);
+            try {
+                // preload the spatial. this will trigger a shader compilation that will fail if the number of attributes is over the limit.
+                rm.preloadScene(spatial);
+                compilationOk = true;
+            } catch (RendererException e) {
+                logger.log(Level.FINE, geom.getName() + ": failed at " + maxGPUTargets);
+                // the compilation failed let's decrement the number of targets an try again.
+                maxGPUTargets--;
+            }
+        }
+        logger.log(Level.FINE, geom.getName() + ": " + maxGPUTargets);
+        // set the number of GPU morph on the geom to not have to recompute it next frame.
+        geom.setNbSimultaneousGPUMorph(maxGPUTargets);
+        return maxGPUTargets;
+    }
+
+    private int bindMorphtargetBuffer(Mesh mesh, int targetNumBuffers, int boundBufferIdx, MorphTarget t) {
+        int start = VertexBuffer.Type.MorphTarget0.ordinal();
+        if (targetNumBuffers >= 1) {
+            activateBuffer(mesh, boundBufferIdx, start, t.getBuffer(VertexBuffer.Type.Position));
+            boundBufferIdx++;
+        }
+        if (targetNumBuffers >= 2) {
+            activateBuffer(mesh, boundBufferIdx, start, t.getBuffer(VertexBuffer.Type.Normal));
+            boundBufferIdx++;
+        }
+        if (!approximateTangents && targetNumBuffers == 3) {
+            activateBuffer(mesh, boundBufferIdx, start, t.getBuffer(VertexBuffer.Type.Tangent));
+            boundBufferIdx++;
+        }
+        return boundBufferIdx;
+    }
+
+    private void writeCpuBuffer(int targetNumBuffers, MorphTarget mt) {
+        if (targetNumBuffers >= 1) {
+            FloatBuffer dest = mt.getBuffer(VertexBuffer.Type.Position);
+            dest.rewind();
+            dest.put(tmpPosArray, 0, dest.capacity());
+        }
+        if (targetNumBuffers >= 2) {
+            FloatBuffer dest = mt.getBuffer(VertexBuffer.Type.Normal);
+            dest.rewind();
+            dest.put(tmpNormArray, 0, dest.capacity());
+        }
+        if (!approximateTangents && targetNumBuffers == 3) {
+            FloatBuffer dest = mt.getBuffer(VertexBuffer.Type.Tangent);
+            dest.rewind();
+            dest.put(tmpTanArray, 0, dest.capacity());
+        }
+    }
+
+    private void mergeMorphTargets(int targetNumBuffers, float weight, MorphTarget t, boolean init) {
+        if (targetNumBuffers >= 1) {
+            mergeTargetBuffer(tmpPosArray, weight, t.getBuffer(VertexBuffer.Type.Position), init);
+        }
+        if (targetNumBuffers >= 2) {
+            mergeTargetBuffer(tmpNormArray, weight, t.getBuffer(VertexBuffer.Type.Normal), init);
+        }
+        if (!approximateTangents && targetNumBuffers == 3) {
+            mergeTargetBuffer(tmpTanArray, weight, t.getBuffer(VertexBuffer.Type.Tangent), init);
+        }
+    }
+
+    private void ensureTmpArraysCapacity(int capacity, int targetNumBuffers) {
+        if (targetNumBuffers >= 1) {
+            tmpPosArray = ensureCapacity(tmpPosArray, capacity);
+        }
+        if (targetNumBuffers >= 2) {
+            tmpNormArray = ensureCapacity(tmpNormArray, capacity);
+        }
+        if (!approximateTangents && targetNumBuffers == 3) {
+            tmpTanArray = ensureCapacity(tmpTanArray, capacity);
+        }
+    }
+
+    private void mergeTargetBuffer(float[] array, float weight, FloatBuffer src, boolean init) {
+        src.rewind();
+        for (int j = 0; j < src.capacity(); j++) {
+            if (init) {
+                array[j] = 0;
+            }
+            array[j] += weight * src.get();
+        }
+    }
+
+    private void activateBuffer(Mesh mesh, int idx, int start, FloatBuffer b) {
+        VertexBuffer.Type t = bufferTypes[start + idx];
+        VertexBuffer vb = mesh.getBuffer(t);
+        // only set the buffer if it's different
+        if (vb == null || vb.getData() != b) {
+            mesh.setBuffer(t, 3, b);
+        }
+    }
+
+    private float[] ensureCapacity(float[] tmpArray, int size) {
+        if (tmpArray == null || tmpArray.length < size) {
+            return new float[size];
+        }
+        return tmpArray;
+    }
+
+    private MorphTarget initCpuMorphTarget(Geometry geom) {
+        MorphTarget res = new MorphTarget();
+        MorphTarget mt = geom.getMesh().getMorphTargets()[0];
+        FloatBuffer b = mt.getBuffer(VertexBuffer.Type.Position);
+        if (b != null) {
+            res.setBuffer(VertexBuffer.Type.Position, BufferUtils.createFloatBuffer(b.capacity()));
+        }
+        b = mt.getBuffer(VertexBuffer.Type.Normal);
+        if (b != null) {
+            res.setBuffer(VertexBuffer.Type.Normal, BufferUtils.createFloatBuffer(b.capacity()));
+        }
+        if (!approximateTangents) {
+            b = mt.getBuffer(VertexBuffer.Type.Tangent);
+            if (b != null) {
+                res.setBuffer(VertexBuffer.Type.Tangent, BufferUtils.createFloatBuffer(b.capacity()));
+            }
+        }
+        return res;
+    }
+
+    private int getTargetNumBuffers(MorphTarget morphTarget) {
+        int num = 0;
+        if (morphTarget.getBuffer(VertexBuffer.Type.Position) != null) num++;
+        if (morphTarget.getBuffer(VertexBuffer.Type.Normal) != null) num++;
+
+        // if tangents are not needed we don't count the tangent buffer
+        if (!approximateTangents && morphTarget.getBuffer(VertexBuffer.Type.Tangent) != null) {
+            num++;
+        }
+        return num;
+    }
+
+    /**
+     * Computes the number of remaining buffers on this mesh.
+     * This is supposed to give a hint on how many attributes will be used in the material and computes the remaining available slots for the morph attributes.
+     * However, the shader can declare attributes that are not used and not bound to a real buffer.
+     * That's why we attempt to compile the shader later on to avoid any compilation crash.
+     * This method is here to avoid too much render test iteration.
+     *
+     * @param mesh
+     * @param renderer
+     * @return
+     */
+    private int getRemainingBuffers(Mesh mesh, Renderer renderer) {
+        int nbUsedBuffers = 0;
+        for (VertexBuffer vb : mesh.getBufferList().getArray()) {
+            boolean isMorphBuffer = vb.getBufferType().ordinal() >= VertexBuffer.Type.MorphTarget0.ordinal() && vb.getBufferType().ordinal() <= VertexBuffer.Type.MorphTarget9.ordinal();
+            if (vb.getBufferType() == VertexBuffer.Type.Index || isMorphBuffer) continue;
+            if (vb.getUsage() != VertexBuffer.Usage.CpuOnly) {
+                nbUsedBuffers++;
+            }
+        }
+        return renderer.getLimits().get(Limits.VertexAttributes) - nbUsedBuffers;
+    }
+
+    public void setApproximateTangents(boolean approximateTangents) {
+        this.approximateTangents = approximateTangents;
+    }
+
+    public boolean isApproximateTangents() {
+        return approximateTangents;
+    }
+
+    private class TargetLocator extends SceneGraphVisitorAdapter {
+        @Override
+        public void visit(Geometry geom) {
+            MatParam p = geom.getMaterial().getMaterialDef().getMaterialParam("MorphWeights");
+            if (p == null) {
+                return;
+            }
+            Mesh mesh = geom.getMesh();
+            if (mesh != null && mesh.hasMorphTargets()) {
+                targets.add(geom);
+                // If the mesh is in a subgraph of a node with a SkinningControl it might have hardware skinning activated through mat param override even if it's not skinned.
+                // this code makes sure that if the mesh has no hardware skinning buffers hardware skinning won't be activated.
+                // this is important, because if HW skinning is activated the shader will declare 2 additional useless attributes,
+                // and we desperately need all the attributes we can find for Morph animation.
+                if (mesh.getBuffer(VertexBuffer.Type.HWBoneIndex) == null && !geom.getLocalMatParamOverrides().contains(nullNumberOfBones)) {
+                    geom.addMatParamOverride(nullNumberOfBones);
+                }
+            }
+        }
+    }
+}

+ 219 - 0
jme3-core/src/main/java/com/jme3/anim/MorphTrack.java

@@ -0,0 +1,219 @@
+/*
+ * Copyright (c) 2009-2012 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.anim;
+
+import com.jme3.anim.interpolator.FrameInterpolator;
+import com.jme3.animation.*;
+import com.jme3.export.*;
+import com.jme3.scene.Geometry;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
+
+import java.io.IOException;
+
+/**
+ * Contains a list of weights and times for each keyframe.
+ *
+ * @author Rémy Bouquet
+ */
+public class MorphTrack implements AnimTrack<float[]> {
+
+    private double length;
+    private Geometry target;
+
+    /**
+     * Weights and times for track.
+     */
+    private float[] weights;
+    private FrameInterpolator interpolator = FrameInterpolator.DEFAULT;
+    private float[] times;
+    private int nbMorphTargets;
+
+    /**
+     * Serialization-only. Do not use.
+     */
+    public MorphTrack() {
+    }
+
+    /**
+     * Creates a morph track with the given Geometry as a target
+     *
+     * @param times        a float array with the time of each frame
+     * @param weights       the morphs for each frames
+     */
+    public MorphTrack(Geometry target, float[] times, float[] weights, int nbMorphTargets) {
+        this.target = target;
+        this.nbMorphTargets = nbMorphTargets;
+        this.setKeyframes(times, weights);
+    }
+
+    /**
+     * return the array of weights of this track
+     *
+     * @return
+     */
+    public float[] getWeights() {
+        return weights;
+    }
+
+    /**
+     * returns the arrays of time for this track
+     *
+     * @return
+     */
+    public float[] getTimes() {
+        return times;
+    }
+
+    /**
+     * Sets the keyframes times for this Joint track
+     *
+     * @param times the keyframes times
+     */
+    public void setTimes(float[] times) {
+        if (times.length == 0) {
+            throw new RuntimeException("TransformTrack with no keyframes!");
+        }
+        this.times = times;
+        length = times[times.length - 1] - times[0];
+    }
+
+
+    /**
+     * Set the weight for this morph track
+     *
+     * @param times        a float array with the time of each frame
+     * @param weights      the weights of the morphs for each frame
+
+     */
+    public void setKeyframes(float[] times, float[] weights) {
+        setTimes(times);
+        if (weights != null) {
+            if (times == null) {
+                throw new RuntimeException("MorphTrack doesn't have any time for key frames, please call setTimes first");
+            }
+
+            this.weights = weights;
+
+            assert times != null && times.length == weights.length;
+        }
+    }
+
+    @Override
+    public double getLength() {
+        return length;
+    }
+
+    @Override
+    public void getDataAtTime(double t, float[] store) {
+        float time = (float) t;
+
+        int lastFrame = times.length - 1;
+        if (time < 0 || lastFrame == 0) {
+            if (weights != null) {
+                System.arraycopy(weights,0,store,0, nbMorphTargets);
+            }
+            return;
+        }
+
+        int startFrame = 0;
+        int endFrame = 1;
+        float blend = 0;
+        if (time >= times[lastFrame]) {
+            startFrame = lastFrame;
+
+            time = time - times[startFrame] + times[startFrame - 1];
+            blend = (time - times[startFrame - 1])
+                    / (times[startFrame] - times[startFrame - 1]);
+
+        } else {
+            // use lastFrame so we never overflow the array
+            int i;
+            for (i = 0; i < lastFrame && times[i] < time; i++) {
+                startFrame = i;
+                endFrame = i + 1;
+            }
+            blend = (time - times[startFrame])
+                    / (times[endFrame] - times[startFrame]);
+        }
+
+        interpolator.interpolateWeights(blend, startFrame, weights, nbMorphTargets, store);
+    }
+
+    public void setFrameInterpolator(FrameInterpolator interpolator) {
+        this.interpolator = interpolator;
+    }
+
+    public Geometry getTarget() {
+        return target;
+    }
+
+    public void setTarget(Geometry target) {
+        this.target = target;
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(weights, "weights", null);
+        oc.write(times, "times", null);
+        oc.write(target, "target", null);
+        oc.write(nbMorphTargets, "nbMorphTargets", 0);
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule ic = im.getCapsule(this);
+        weights = ic.readFloatArray("weights", null);
+        times = ic.readFloatArray("times", null);
+        target = (Geometry) ic.readSavable("target", null);
+        nbMorphTargets = ic.readInt("nbMorphTargets", 0);
+        setTimes(times);
+    }
+
+    @Override
+    public Object jmeClone() {
+        try {
+            MorphTrack clone = (MorphTrack) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException ex) {
+            throw new AssertionError();
+        }
+    }
+
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        this.target = cloner.clone(target);
+    }
+
+
+}

+ 43 - 0
jme3-core/src/main/java/com/jme3/anim/SeparateJointModelTransform.java

@@ -0,0 +1,43 @@
+package com.jme3.anim;
+
+import com.jme3.anim.util.JointModelTransform;
+import com.jme3.math.Matrix4f;
+import com.jme3.math.Transform;
+
+/**
+ * This JointModelTransform implementation accumulates model transform in a Transform class
+ * This does NOT support proper non uniform scale in the armature hierarchy.
+ * But the effect might be useful in some circumstances.
+ * Note that this is how the old animation system was working, so you might want to use this
+ * if your model has non uniform scale and was migrated from old j3o model.
+ */
+public class SeparateJointModelTransform implements JointModelTransform {
+
+    private Transform modelTransform = new Transform();
+
+    @Override
+    public void updateModelTransform(Transform localTransform, Joint parent) {
+        modelTransform.set(localTransform);
+        if (parent != null) {
+            modelTransform.combineWithParent(parent.getModelTransform());
+        }
+    }
+
+    public void getOffsetTransform(Matrix4f outTransform, Matrix4f inverseModelBindMatrix) {
+        modelTransform.toTransformMatrix(outTransform).mult(inverseModelBindMatrix, outTransform);
+    }
+
+    @Override
+    public void applyBindPose(Transform localTransform, Matrix4f inverseModelBindMatrix, Joint parent) {
+        localTransform.fromTransformMatrix(inverseModelBindMatrix.invert());
+        if (parent != null) {
+            localTransform.combineWithParent(parent.getModelTransform().invert());
+        }
+    }
+
+    @Override
+    public Transform getModelTransform() {
+        return modelTransform;
+    }
+
+}

+ 744 - 0
jme3-core/src/main/java/com/jme3/anim/SkinningControl.java

@@ -0,0 +1,744 @@
+/*
+ * Copyright (c) 2009-2017 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.anim;
+
+import com.jme3.export.*;
+import com.jme3.material.MatParamOverride;
+import com.jme3.math.FastMath;
+import com.jme3.math.Matrix4f;
+import com.jme3.renderer.*;
+import com.jme3.scene.*;
+import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.scene.control.AbstractControl;
+import com.jme3.scene.mesh.IndexBuffer;
+import com.jme3.shader.VarType;
+import com.jme3.util.SafeArrayList;
+import com.jme3.util.TempVars;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
+
+import java.io.IOException;
+import java.nio.Buffer;
+import java.nio.FloatBuffer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * The Skinning control deforms a model according to an armature, It handles the
+ * computation of the deformation matrices and performs the transformations on
+ * the mesh
+ * <p>
+ * It can perform software skinning or Hardware skinning
+ *
+ * @author Rémy Bouquet Based on SkeletonControl by Kirill Vainer
+ */
+public class SkinningControl extends AbstractControl implements Cloneable, JmeCloneable {
+
+    private static final Logger logger = Logger.getLogger(SkinningControl.class.getName());
+
+    /**
+     * The armature of the model.
+     */
+    private Armature armature;
+
+    /**
+     * List of geometries affected by this control.
+     */
+    private SafeArrayList<Geometry> targets = new SafeArrayList<>(Geometry.class);
+
+    /**
+     * Used to track when a mesh was updated. Meshes are only updated if they
+     * are visible in at least one camera.
+     */
+    private boolean wasMeshUpdated = false;
+
+    /**
+     * User wishes to use hardware skinning if available.
+     */
+    private transient boolean hwSkinningDesired = true;
+
+    /**
+     * Hardware skinning is currently being used.
+     */
+    private transient boolean hwSkinningEnabled = false;
+
+    /**
+     * Hardware skinning was tested on this GPU, results
+     * are stored in {@link #hwSkinningSupported} variable.
+     */
+    private transient boolean hwSkinningTested = false;
+
+    /**
+     * If hardware skinning was {@link #hwSkinningTested tested}, then
+     * this variable will be set to true if supported, and false if otherwise.
+     */
+    private transient boolean hwSkinningSupported = false;
+
+    /**
+     * Bone offset matrices, recreated each frame
+     */
+    private transient Matrix4f[] offsetMatrices;
+
+
+    private MatParamOverride numberOfJointsParam;
+    private MatParamOverride jointMatricesParam;
+
+    /**
+     * Serialization only. Do not use.
+     */
+    public SkinningControl() {
+    }
+
+    /**
+     * Creates a armature control. The list of targets will be acquired
+     * automatically when the control is attached to a node.
+     *
+     * @param armature the armature
+     */
+    public SkinningControl(Armature armature) {
+        if (armature == null) {
+            throw new IllegalArgumentException("armature cannot be null");
+        }
+        this.armature = armature;
+        this.numberOfJointsParam = new MatParamOverride(VarType.Int, "NumberOfBones", null);
+        this.jointMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null);
+    }
+
+
+    private void switchToHardware() {
+        numberOfJointsParam.setEnabled(true);
+        jointMatricesParam.setEnabled(true);
+
+        // Next full 10 bones (e.g. 30 on 24 bones)
+        int numBones = ((armature.getJointCount() / 10) + 1) * 10;
+        numberOfJointsParam.setValue(numBones);
+
+        for (Geometry geometry : targets) {
+            Mesh mesh = geometry.getMesh();
+            if (mesh != null && mesh.isAnimated()) {
+                mesh.prepareForAnim(false);
+            }
+        }
+    }
+
+    private void switchToSoftware() {
+        numberOfJointsParam.setEnabled(false);
+        jointMatricesParam.setEnabled(false);
+
+        for (Geometry geometry : targets) {
+            Mesh mesh = geometry.getMesh();
+            if (mesh != null && mesh.isAnimated()) {
+                mesh.prepareForAnim(true);
+            }
+        }
+    }
+
+    private boolean testHardwareSupported(RenderManager rm) {
+
+        //Only 255 bones max supported with hardware skinning
+        if (armature.getJointCount() > 255) {
+            return false;
+        }
+
+        switchToHardware();
+
+        try {
+            rm.preloadScene(spatial);
+            return true;
+        } catch (RendererException e) {
+            logger.log(Level.WARNING, "Could not enable HW skinning due to shader compile error:", e);
+            return false;
+        }
+    }
+
+    /**
+     * Specifies if hardware skinning is preferred. If it is preferred and
+     * supported by GPU, it shall be enabled, if its not preferred, or not
+     * supported by GPU, then it shall be disabled.
+     *
+     * @param preferred
+     * @see #isHardwareSkinningUsed()
+     */
+    public void setHardwareSkinningPreferred(boolean preferred) {
+        hwSkinningDesired = preferred;
+    }
+
+    /**
+     * @return True if hardware skinning is preferable to software skinning.
+     * Set to false by default.
+     * @see #setHardwareSkinningPreferred(boolean)
+     */
+    public boolean isHardwareSkinningPreferred() {
+        return hwSkinningDesired;
+    }
+
+    /**
+     * @return True is hardware skinning is activated and is currently used, false otherwise.
+     */
+    public boolean isHardwareSkinningUsed() {
+        return hwSkinningEnabled;
+    }
+
+
+    /**
+     * If specified the geometry has an animated mesh, add its mesh and material
+     * to the lists of animation targets.
+     */
+    private void findTargets(Geometry geometry) {
+        Mesh mesh = geometry.getMesh();
+        if (mesh != null && mesh.isAnimated()) {
+            targets.add(geometry);
+        }
+
+    }
+
+    private void findTargets(Node node) {
+        for (Spatial child : node.getChildren()) {
+            if (child instanceof Geometry) {
+                findTargets((Geometry) child);
+            } else if (child instanceof Node) {
+                findTargets((Node) child);
+            }
+        }
+    }
+
+    @Override
+    public void setSpatial(Spatial spatial) {
+        Spatial oldSpatial = this.spatial;
+        super.setSpatial(spatial);
+        updateTargetsAndMaterials(spatial);
+
+        if (oldSpatial != null) {
+            oldSpatial.removeMatParamOverride(numberOfJointsParam);
+            oldSpatial.removeMatParamOverride(jointMatricesParam);
+        }
+
+        if (spatial != null) {
+            spatial.removeMatParamOverride(numberOfJointsParam);
+            spatial.removeMatParamOverride(jointMatricesParam);
+            spatial.addMatParamOverride(numberOfJointsParam);
+            spatial.addMatParamOverride(jointMatricesParam);
+        }
+    }
+
+    private void controlRenderSoftware() {
+        resetToBind(); // reset morph meshes to bind pose
+
+        offsetMatrices = armature.computeSkinningMatrices();
+
+        for (Geometry geometry : targets) {
+            Mesh mesh = geometry.getMesh();
+            // NOTE: This assumes code higher up has
+            // already ensured this mesh is animated.
+            // Otherwise a crash will happen in skin update.
+            softwareSkinUpdate(mesh, offsetMatrices);
+        }
+    }
+
+    private void controlRenderHardware() {
+        offsetMatrices = armature.computeSkinningMatrices();
+        jointMatricesParam.setValue(offsetMatrices);
+    }
+
+    @Override
+    protected void controlRender(RenderManager rm, ViewPort vp) {
+        if (!wasMeshUpdated) {
+            updateTargetsAndMaterials(spatial);
+
+            // Prevent illegal cases. These should never happen.
+            assert hwSkinningTested || (!hwSkinningTested && !hwSkinningSupported && !hwSkinningEnabled);
+            assert !hwSkinningEnabled || (hwSkinningEnabled && hwSkinningTested && hwSkinningSupported);
+
+            if (hwSkinningDesired && !hwSkinningTested) {
+                hwSkinningTested = true;
+                hwSkinningSupported = testHardwareSupported(rm);
+
+                if (hwSkinningSupported) {
+                    hwSkinningEnabled = true;
+
+                    Logger.getLogger(SkinningControl.class.getName()).log(Level.INFO, "Hardware skinning engaged for {0}", spatial);
+                } else {
+                    switchToSoftware();
+                }
+            } else if (hwSkinningDesired && hwSkinningSupported && !hwSkinningEnabled) {
+                switchToHardware();
+                hwSkinningEnabled = true;
+            } else if (!hwSkinningDesired && hwSkinningEnabled) {
+                switchToSoftware();
+                hwSkinningEnabled = false;
+            }
+
+            if (hwSkinningEnabled) {
+                controlRenderHardware();
+            } else {
+                controlRenderSoftware();
+            }
+
+            wasMeshUpdated = true;
+        }
+    }
+
+    @Override
+    protected void controlUpdate(float tpf) {
+        wasMeshUpdated = false;
+        armature.update();
+    }
+
+    //only do this for software updates
+    void resetToBind() {
+        for (Geometry geometry : targets) {
+            Mesh mesh = geometry.getMesh();
+            if (mesh != null && mesh.isAnimated()) {
+                Buffer bwBuff = mesh.getBuffer(Type.BoneWeight).getData();
+                Buffer biBuff = mesh.getBuffer(Type.BoneIndex).getData();
+                if (!biBuff.hasArray() || !bwBuff.hasArray()) {
+                    mesh.prepareForAnim(true); // prepare for software animation
+                }
+                VertexBuffer bindPos = mesh.getBuffer(Type.BindPosePosition);
+                VertexBuffer bindNorm = mesh.getBuffer(Type.BindPoseNormal);
+                VertexBuffer pos = mesh.getBuffer(Type.Position);
+                VertexBuffer norm = mesh.getBuffer(Type.Normal);
+                FloatBuffer pb = (FloatBuffer) pos.getData();
+                FloatBuffer nb = (FloatBuffer) norm.getData();
+                FloatBuffer bpb = (FloatBuffer) bindPos.getData();
+                FloatBuffer bnb = (FloatBuffer) bindNorm.getData();
+                pb.clear();
+                nb.clear();
+                bpb.clear();
+                bnb.clear();
+
+                //reseting bind tangents if there is a bind tangent buffer
+                VertexBuffer bindTangents = mesh.getBuffer(Type.BindPoseTangent);
+                if (bindTangents != null) {
+                    VertexBuffer tangents = mesh.getBuffer(Type.Tangent);
+                    FloatBuffer tb = (FloatBuffer) tangents.getData();
+                    FloatBuffer btb = (FloatBuffer) bindTangents.getData();
+                    tb.clear();
+                    btb.clear();
+                    tb.put(btb).clear();
+                }
+
+
+                pb.put(bpb).clear();
+                nb.put(bnb).clear();
+            }
+        }
+    }
+
+    @Override
+    public Object jmeClone() {
+        return super.jmeClone();
+    }
+
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        super.cloneFields(cloner, original);
+
+        this.armature = cloner.clone(armature);
+
+        // If the targets were cloned then this will clone them.  If the targets
+        // were shared then this will share them.
+        this.targets = cloner.clone(targets);
+
+        this.numberOfJointsParam = cloner.clone(numberOfJointsParam);
+        this.jointMatricesParam = cloner.clone(jointMatricesParam);
+    }
+
+    /**
+     * Access the attachments node of the named bone. If the bone doesn't
+     * already have an attachments node, create one and attach it to the scene
+     * graph. Models and effects attached to the attachments node will follow
+     * the bone's motions.
+     *
+     * @param jointName the name of the joint
+     * @return the attachments node of the joint
+     */
+    public Node getAttachmentsNode(String jointName) {
+        Joint b = armature.getJoint(jointName);
+        if (b == null) {
+            throw new IllegalArgumentException("Given bone name does not exist "
+                    + "in the armature.");
+        }
+
+        updateTargetsAndMaterials(spatial);
+        int boneIndex = armature.getJointIndex(b);
+        Node n = b.getAttachmentsNode(boneIndex, targets);
+        /*
+         * Select a node to parent the attachments node.
+         */
+        Node parent;
+        if (spatial instanceof Node) {
+            parent = (Node) spatial; // the usual case
+        } else {
+            parent = spatial.getParent();
+        }
+        parent.attachChild(n);
+
+        return n;
+    }
+
+    /**
+     * returns the armature of this control
+     *
+     * @return
+     */
+    public Armature getArmature() {
+        return armature;
+    }
+
+    /**
+     * Enumerate the target meshes of this control.
+     *
+     * @return a new array
+     */
+    public Mesh[] getTargets() {
+        Mesh[] result = new Mesh[targets.size()];
+        int i = 0;
+        for (Geometry geometry : targets) {
+            Mesh mesh = geometry.getMesh();
+            result[i] = mesh;
+            i++;
+        }
+
+        return result;
+    }
+
+    /**
+     * Update the mesh according to the given transformation matrices
+     *
+     * @param mesh           then mesh
+     * @param offsetMatrices the transformation matrices to apply
+     */
+    private void softwareSkinUpdate(Mesh mesh, Matrix4f[] offsetMatrices) {
+
+        VertexBuffer tb = mesh.getBuffer(Type.Tangent);
+        if (tb == null) {
+            //if there are no tangents use the classic skinning
+            applySkinning(mesh, offsetMatrices);
+        } else {
+            //if there are tangents use the skinning with tangents
+            applySkinningTangents(mesh, offsetMatrices, tb);
+        }
+
+
+    }
+
+    /**
+     * Method to apply skinning transforms to a mesh's buffers
+     *
+     * @param mesh           the mesh
+     * @param offsetMatrices the offset matices to apply
+     */
+    private void applySkinning(Mesh mesh, Matrix4f[] offsetMatrices) {
+        int maxWeightsPerVert = mesh.getMaxNumWeights();
+        if (maxWeightsPerVert <= 0) {
+            throw new IllegalStateException("Max weights per vert is incorrectly set!");
+        }
+        int fourMinusMaxWeights = 4 - maxWeightsPerVert;
+
+        // NOTE: This code assumes the vertex buffer is in bind pose
+        // resetToBind() has been called this frame
+        VertexBuffer vb = mesh.getBuffer(Type.Position);
+        FloatBuffer fvb = (FloatBuffer) vb.getData();
+        fvb.rewind();
+
+        VertexBuffer nb = mesh.getBuffer(Type.Normal);
+        FloatBuffer fnb = (FloatBuffer) nb.getData();
+        fnb.rewind();
+
+        // get boneIndexes and weights for mesh
+        IndexBuffer ib = IndexBuffer.wrapIndexBuffer(mesh.getBuffer(Type.BoneIndex).getData());
+        FloatBuffer wb = (FloatBuffer) mesh.getBuffer(Type.BoneWeight).getData();
+
+        wb.rewind();
+
+        float[] weights = wb.array();
+        int idxWeights = 0;
+
+        TempVars vars = TempVars.get();
+
+        float[] posBuf = vars.skinPositions;
+        float[] normBuf = vars.skinNormals;
+
+        int iterations = (int) FastMath.ceil(fvb.limit() / ((float) posBuf.length));
+        int bufLength = posBuf.length;
+        for (int i = iterations - 1; i >= 0; i--) {
+            // read next set of positions and normals from native buffer
+            bufLength = Math.min(posBuf.length, fvb.remaining());
+            fvb.get(posBuf, 0, bufLength);
+            fnb.get(normBuf, 0, bufLength);
+            int verts = bufLength / 3;
+            int idxPositions = 0;
+
+            // iterate vertices and apply skinning transform for each effecting bone
+            for (int vert = verts - 1; vert >= 0; vert--) {
+                // Skip this vertex if the first weight is zero.
+                if (weights[idxWeights] == 0) {
+                    idxPositions += 3;
+                    idxWeights += 4;
+                    continue;
+                }
+
+                float nmx = normBuf[idxPositions];
+                float vtx = posBuf[idxPositions++];
+                float nmy = normBuf[idxPositions];
+                float vty = posBuf[idxPositions++];
+                float nmz = normBuf[idxPositions];
+                float vtz = posBuf[idxPositions++];
+
+                float rx = 0, ry = 0, rz = 0, rnx = 0, rny = 0, rnz = 0;
+
+                for (int w = maxWeightsPerVert - 1; w >= 0; w--) {
+                    float weight = weights[idxWeights];
+                    Matrix4f mat = offsetMatrices[ib.get(idxWeights++)];
+
+                    rx += (mat.m00 * vtx + mat.m01 * vty + mat.m02 * vtz + mat.m03) * weight;
+                    ry += (mat.m10 * vtx + mat.m11 * vty + mat.m12 * vtz + mat.m13) * weight;
+                    rz += (mat.m20 * vtx + mat.m21 * vty + mat.m22 * vtz + mat.m23) * weight;
+
+                    rnx += (nmx * mat.m00 + nmy * mat.m01 + nmz * mat.m02) * weight;
+                    rny += (nmx * mat.m10 + nmy * mat.m11 + nmz * mat.m12) * weight;
+                    rnz += (nmx * mat.m20 + nmy * mat.m21 + nmz * mat.m22) * weight;
+                }
+
+                idxWeights += fourMinusMaxWeights;
+
+                idxPositions -= 3;
+                normBuf[idxPositions] = rnx;
+                posBuf[idxPositions++] = rx;
+                normBuf[idxPositions] = rny;
+                posBuf[idxPositions++] = ry;
+                normBuf[idxPositions] = rnz;
+                posBuf[idxPositions++] = rz;
+            }
+
+            fvb.position(fvb.position() - bufLength);
+            fvb.put(posBuf, 0, bufLength);
+            fnb.position(fnb.position() - bufLength);
+            fnb.put(normBuf, 0, bufLength);
+        }
+
+        vars.release();
+
+        vb.updateData(fvb);
+        nb.updateData(fnb);
+
+    }
+
+    /**
+     * Specific method for skinning with tangents to avoid cluttering the
+     * classic skinning calculation with null checks that would slow down the
+     * process even if tangents don't have to be computed. Also the iteration
+     * has additional indexes since tangent has 4 components instead of 3 for
+     * pos and norm
+     *
+     * @param mesh           the mesh
+     * @param offsetMatrices the offsetMatrices to apply
+     * @param tb             the tangent vertexBuffer
+     */
+    private void applySkinningTangents(Mesh mesh, Matrix4f[] offsetMatrices, VertexBuffer tb) {
+        int maxWeightsPerVert = mesh.getMaxNumWeights();
+
+        if (maxWeightsPerVert <= 0) {
+            throw new IllegalStateException("Max weights per vert is incorrectly set!");
+        }
+
+        int fourMinusMaxWeights = 4 - maxWeightsPerVert;
+
+        // NOTE: This code assumes the vertex buffer is in bind pose
+        // resetToBind() has been called this frame
+        VertexBuffer vb = mesh.getBuffer(Type.Position);
+        FloatBuffer fvb = (FloatBuffer) vb.getData();
+        fvb.rewind();
+
+        VertexBuffer nb = mesh.getBuffer(Type.Normal);
+
+        FloatBuffer fnb = (FloatBuffer) nb.getData();
+        fnb.rewind();
+
+
+        FloatBuffer ftb = (FloatBuffer) tb.getData();
+        ftb.rewind();
+
+
+        // get boneIndexes and weights for mesh
+        IndexBuffer ib = IndexBuffer.wrapIndexBuffer(mesh.getBuffer(Type.BoneIndex).getData());
+        FloatBuffer wb = (FloatBuffer) mesh.getBuffer(Type.BoneWeight).getData();
+
+        wb.rewind();
+
+        float[] weights = wb.array();
+        int idxWeights = 0;
+
+        TempVars vars = TempVars.get();
+
+
+        float[] posBuf = vars.skinPositions;
+        float[] normBuf = vars.skinNormals;
+        float[] tanBuf = vars.skinTangents;
+
+        int iterations = (int) FastMath.ceil(fvb.limit() / ((float) posBuf.length));
+        int bufLength = 0;
+        int tanLength = 0;
+        for (int i = iterations - 1; i >= 0; i--) {
+            // read next set of positions and normals from native buffer
+            bufLength = Math.min(posBuf.length, fvb.remaining());
+            tanLength = Math.min(tanBuf.length, ftb.remaining());
+            fvb.get(posBuf, 0, bufLength);
+            fnb.get(normBuf, 0, bufLength);
+            ftb.get(tanBuf, 0, tanLength);
+            int verts = bufLength / 3;
+            int idxPositions = 0;
+            //tangents has their own index because of the 4 components
+            int idxTangents = 0;
+
+            // iterate vertices and apply skinning transform for each effecting bone
+            for (int vert = verts - 1; vert >= 0; vert--) {
+                // Skip this vertex if the first weight is zero.
+                if (weights[idxWeights] == 0) {
+                    idxTangents += 4;
+                    idxPositions += 3;
+                    idxWeights += 4;
+                    continue;
+                }
+
+                float nmx = normBuf[idxPositions];
+                float vtx = posBuf[idxPositions++];
+                float nmy = normBuf[idxPositions];
+                float vty = posBuf[idxPositions++];
+                float nmz = normBuf[idxPositions];
+                float vtz = posBuf[idxPositions++];
+
+                float tnx = tanBuf[idxTangents++];
+                float tny = tanBuf[idxTangents++];
+                float tnz = tanBuf[idxTangents++];
+
+                // skipping the 4th component of the tangent since it doesn't have to be transformed
+                idxTangents++;
+
+                float rx = 0, ry = 0, rz = 0, rnx = 0, rny = 0, rnz = 0, rtx = 0, rty = 0, rtz = 0;
+
+                for (int w = maxWeightsPerVert - 1; w >= 0; w--) {
+                    float weight = weights[idxWeights];
+                    Matrix4f mat = offsetMatrices[ib.get(idxWeights++)];
+
+                    rx += (mat.m00 * vtx + mat.m01 * vty + mat.m02 * vtz + mat.m03) * weight;
+                    ry += (mat.m10 * vtx + mat.m11 * vty + mat.m12 * vtz + mat.m13) * weight;
+                    rz += (mat.m20 * vtx + mat.m21 * vty + mat.m22 * vtz + mat.m23) * weight;
+
+                    rnx += (nmx * mat.m00 + nmy * mat.m01 + nmz * mat.m02) * weight;
+                    rny += (nmx * mat.m10 + nmy * mat.m11 + nmz * mat.m12) * weight;
+                    rnz += (nmx * mat.m20 + nmy * mat.m21 + nmz * mat.m22) * weight;
+
+                    rtx += (tnx * mat.m00 + tny * mat.m01 + tnz * mat.m02) * weight;
+                    rty += (tnx * mat.m10 + tny * mat.m11 + tnz * mat.m12) * weight;
+                    rtz += (tnx * mat.m20 + tny * mat.m21 + tnz * mat.m22) * weight;
+                }
+
+                idxWeights += fourMinusMaxWeights;
+
+                idxPositions -= 3;
+
+                normBuf[idxPositions] = rnx;
+                posBuf[idxPositions++] = rx;
+                normBuf[idxPositions] = rny;
+                posBuf[idxPositions++] = ry;
+                normBuf[idxPositions] = rnz;
+                posBuf[idxPositions++] = rz;
+
+                idxTangents -= 4;
+
+                tanBuf[idxTangents++] = rtx;
+                tanBuf[idxTangents++] = rty;
+                tanBuf[idxTangents++] = rtz;
+
+                //once again skipping the 4th component of the tangent
+                idxTangents++;
+            }
+
+            fvb.position(fvb.position() - bufLength);
+            fvb.put(posBuf, 0, bufLength);
+            fnb.position(fnb.position() - bufLength);
+            fnb.put(normBuf, 0, bufLength);
+            ftb.position(ftb.position() - tanLength);
+            ftb.put(tanBuf, 0, tanLength);
+        }
+
+        vars.release();
+
+        vb.updateData(fvb);
+        nb.updateData(fnb);
+        tb.updateData(ftb);
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(armature, "armature", null);
+
+        oc.write(numberOfJointsParam, "numberOfBonesParam", null);
+        oc.write(jointMatricesParam, "boneMatricesParam", null);
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule in = im.getCapsule(this);
+        armature = (Armature) in.readSavable("armature", null);
+
+        numberOfJointsParam = (MatParamOverride) in.readSavable("numberOfBonesParam", null);
+        jointMatricesParam = (MatParamOverride) in.readSavable("boneMatricesParam", null);
+
+        if (numberOfJointsParam == null) {
+            numberOfJointsParam = new MatParamOverride(VarType.Int, "NumberOfBones", null);
+            jointMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null);
+            getSpatial().addMatParamOverride(numberOfJointsParam);
+            getSpatial().addMatParamOverride(jointMatricesParam);
+        }
+    }
+
+    /**
+     * Update the lists of animation targets.
+     *
+     * @param spatial the controlled spatial
+     */
+    private void updateTargetsAndMaterials(Spatial spatial) {
+        targets.clear();
+
+        if (spatial instanceof Node) {
+            findTargets((Node) spatial);
+        } else if (spatial instanceof Geometry) {
+            findTargets((Geometry) spatial);
+        }
+    }
+}

+ 315 - 0
jme3-core/src/main/java/com/jme3/anim/TransformTrack.java

@@ -0,0 +1,315 @@
+/*
+ * Copyright (c) 2009-2012 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.anim;
+
+import com.jme3.anim.interpolator.FrameInterpolator;
+import com.jme3.anim.tween.Tween;
+import com.jme3.anim.util.HasLocalTransform;
+import com.jme3.animation.CompactQuaternionArray;
+import com.jme3.animation.CompactVector3Array;
+import com.jme3.export.*;
+import com.jme3.math.*;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
+
+import java.io.IOException;
+
+/**
+ * Contains a list of transforms and times for each keyframe.
+ *
+ * @author Rémy Bouquet
+ */
+public class TransformTrack implements AnimTrack<Transform> {
+
+    private double length;
+    private HasLocalTransform target;
+
+    /**
+     * Transforms and times for track.
+     */
+    private CompactVector3Array translations;
+    private CompactQuaternionArray rotations;
+    private CompactVector3Array scales;
+    private FrameInterpolator interpolator = FrameInterpolator.DEFAULT;
+    private float[] times;
+
+    /**
+     * Serialization-only. Do not use.
+     */
+    public TransformTrack() {
+    }
+
+    /**
+     * Creates a transform track for the given bone index
+     *
+     * @param times        a float array with the time of each frame
+     * @param translations the translation of the bone for each frame
+     * @param rotations    the rotation of the bone for each frame
+     * @param scales       the scale of the bone for each frame
+     */
+    public TransformTrack(HasLocalTransform target, float[] times, Vector3f[] translations, Quaternion[] rotations, Vector3f[] scales) {
+        this.target = target;
+        this.setKeyframes(times, translations, rotations, scales);
+    }
+
+    /**
+     * return the array of rotations of this track
+     *
+     * @return
+     */
+    public Quaternion[] getRotations() {
+        return rotations.toObjectArray();
+    }
+
+    /**
+     * returns the array of scales for this track
+     *
+     * @return
+     */
+    public Vector3f[] getScales() {
+        return scales == null ? null : scales.toObjectArray();
+    }
+
+    /**
+     * returns the arrays of time for this track
+     *
+     * @return
+     */
+    public float[] getTimes() {
+        return times;
+    }
+
+    /**
+     * returns the array of translations of this track
+     *
+     * @return
+     */
+    public Vector3f[] getTranslations() {
+        return translations.toObjectArray();
+    }
+
+
+    /**
+     * Sets the keyframes times for this Joint track
+     *
+     * @param times the keyframes times
+     */
+    public void setTimes(float[] times) {
+        if (times.length == 0) {
+            throw new RuntimeException("TransformTrack with no keyframes!");
+        }
+        this.times = times;
+        length = times[times.length - 1] - times[0];
+    }
+
+    /**
+     * Set the translations for this joint track
+     *
+     * @param translations the translation of the bone for each frame
+     */
+    public void setKeyframesTranslation(Vector3f[] translations) {
+        if (times == null) {
+            throw new RuntimeException("TransformTrack doesn't have any time for key frames, please call setTimes first");
+        }
+        if (translations.length == 0) {
+            throw new RuntimeException("TransformTrack with no translation keyframes!");
+        }
+        this.translations = new CompactVector3Array();
+        this.translations.add(translations);
+        this.translations.freeze();
+
+        assert times != null && times.length == translations.length;
+    }
+
+    /**
+     * Set the scales for this joint track
+     *
+     * @param scales the scales of the bone for each frame
+     */
+    public void setKeyframesScale(Vector3f[] scales) {
+        if (times == null) {
+            throw new RuntimeException("TransformTrack doesn't have any time for key frames, please call setTimes first");
+        }
+        if (scales.length == 0) {
+            throw new RuntimeException("TransformTrack with no scale keyframes!");
+        }
+        this.scales = new CompactVector3Array();
+        this.scales.add(scales);
+        this.scales.freeze();
+
+        assert times != null && times.length == scales.length;
+    }
+
+    /**
+     * Set the rotations for this joint track
+     *
+     * @param rotations the rotations of the bone for each frame
+     */
+    public void setKeyframesRotation(Quaternion[] rotations) {
+        if (times == null) {
+            throw new RuntimeException("TransformTrack doesn't have any time for key frames, please call setTimes first");
+        }
+        if (rotations.length == 0) {
+            throw new RuntimeException("TransformTrack with no rotation keyframes!");
+        }
+        this.rotations = new CompactQuaternionArray();
+        this.rotations.add(rotations);
+        this.rotations.freeze();
+
+        assert times != null && times.length == rotations.length;
+    }
+
+
+    /**
+     * Set the translations, rotations and scales for this bone track
+     *
+     * @param times        a float array with the time of each frame
+     * @param translations the translation of the bone for each frame
+     * @param rotations    the rotation of the bone for each frame
+     * @param scales       the scale of the bone for each frame
+     */
+    public void setKeyframes(float[] times, Vector3f[] translations, Quaternion[] rotations, Vector3f[] scales) {
+        setTimes(times);
+        if (translations != null) {
+            setKeyframesTranslation(translations);
+        }
+        if (rotations != null) {
+            setKeyframesRotation(rotations);
+        }
+        if (scales != null) {
+            setKeyframesScale(scales);
+        }
+    }
+
+    public double getLength() {
+        return length;
+    }
+
+    public void getDataAtTime(double t, Transform transform) {
+        float time = (float) t;
+
+        int lastFrame = times.length - 1;
+        if (time < 0 || lastFrame == 0) {
+            if (translations != null) {
+                translations.get(0, transform.getTranslation());
+            }
+            if (rotations != null) {
+                rotations.get(0, transform.getRotation());
+            }
+            if (scales != null) {
+                scales.get(0, transform.getScale());
+            }
+            return;
+        }
+
+        int startFrame = 0;
+        int endFrame = 1;
+        float blend = 0;
+        if (time >= times[lastFrame]) {
+            startFrame = lastFrame;
+
+            time = time - times[startFrame] + times[startFrame - 1];
+            blend = (time - times[startFrame - 1])
+                    / (times[startFrame] - times[startFrame - 1]);
+
+        } else {
+            // use lastFrame so we never overflow the array
+            int i;
+            for (i = 0; i < lastFrame && times[i] < time; i++) {
+                startFrame = i;
+                endFrame = i + 1;
+            }
+            blend = (time - times[startFrame])
+                    / (times[endFrame] - times[startFrame]);
+        }
+
+        Transform interpolated = interpolator.interpolate(blend, startFrame, translations, rotations, scales, times);
+
+        if (translations != null) {
+            transform.setTranslation(interpolated.getTranslation());
+        }
+        if (rotations != null) {
+            transform.setRotation(interpolated.getRotation());
+        }
+        if (scales != null) {
+            transform.setScale(interpolated.getScale());
+        }
+    }
+
+    public void setFrameInterpolator(FrameInterpolator interpolator) {
+        this.interpolator = interpolator;
+    }
+
+    public HasLocalTransform getTarget() {
+        return target;
+    }
+
+    public void setTarget(HasLocalTransform target) {
+        this.target = target;
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(translations, "translations", null);
+        oc.write(rotations, "rotations", null);
+        oc.write(times, "times", null);
+        oc.write(scales, "scales", null);
+        oc.write(target, "target", null);
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule ic = im.getCapsule(this);
+        translations = (CompactVector3Array) ic.readSavable("translations", null);
+        rotations = (CompactQuaternionArray) ic.readSavable("rotations", null);
+        times = ic.readFloatArray("times", null);
+        scales = (CompactVector3Array) ic.readSavable("scales", null);
+        target = (HasLocalTransform) ic.readSavable("target", null);
+        setTimes(times);
+    }
+
+    @Override
+    public Object jmeClone() {
+        try {
+            TransformTrack clone = (TransformTrack) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException ex) {
+            throw new AssertionError();
+        }
+    }
+
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        this.target = cloner.clone(target);
+    }
+}

+ 95 - 0
jme3-core/src/main/java/com/jme3/anim/Weights.java

@@ -0,0 +1,95 @@
+package com.jme3.anim;
+
+import java.util.ArrayList;
+
+public class Weights {//}  extends Savable, JmeCloneable{
+
+
+    private final static float MIN_WEIGHT = 0.005f;
+
+    private int[] indices;
+    private float[] data;
+    private int size;
+
+    public Weights(float[] array, int start, int length) {
+        ArrayList<Float> list = new ArrayList<>();
+        ArrayList<Integer> idx = new ArrayList<>();
+
+        for (int i = start; i < length; i++) {
+            float val = array[i];
+            if (val > MIN_WEIGHT) {
+                list.add(val);
+                idx.add(i);
+            }
+        }
+        size = list.size();
+        data = new float[size];
+        indices = new int[size];
+        for (int i = 0; i < size; i++) {
+            data[i] = list.get(i);
+            indices[i] = idx.get(i);
+        }
+    }
+
+    public int getSize() {
+        return size;
+    }
+
+    //    public Weights(float[] array, int start, int length) {
+//        LinkedList<Float> list = new LinkedList<>();
+//        LinkedList<Integer> idx = new LinkedList<>();
+//        for (int i = start; i < length; i++) {
+//            float val = array[i];
+//            if (val > MIN_WEIGHT) {
+//                int index = insert(list, val);
+//                if (idx.size() < index) {
+//                    idx.add(i);
+//                } else {
+//                    idx.add(index, i);
+//                }
+//            }
+//        }
+//        data = new float[list.size()];
+//        for (int i = 0; i < data.length; i++) {
+//            data[i] = list.get(i);
+//        }
+//
+//        indices = new int[idx.size()];
+//        for (int i = 0; i < indices.length; i++) {
+//            indices[i] = idx.get(i);
+//        }
+//    }
+//
+//    private int insert(LinkedList<Float> list, float value) {
+//        for (int i = 0; i < list.size(); i++) {
+//            float w = list.get(i);
+//            if (value > w) {
+//                list.add(i, value);
+//                return i;
+//            }
+//        }
+//
+//        list.add(value);
+//        return list.size();
+//    }
+
+    @Override
+    public String toString() {
+        StringBuilder b = new StringBuilder();
+        for (int i = 0; i < indices.length; i++) {
+            b.append(indices[i]).append(",");
+        }
+        b.append("\n");
+        for (int i = 0; i < data.length; i++) {
+            b.append(data[i]).append(",");
+        }
+        return b.toString();
+    }
+
+    public static void main(String... args) {
+        // 6 7 4 8
+        float values[] = {0, 0, 0, 0, 0.5f, 0.001f, 0.7f, 0.6f, 0.2f, 0, 0, 0};
+        Weights w = new Weights(values, 0, values.length);
+        System.err.println(w);
+    }
+}

+ 13 - 0
jme3-core/src/main/java/com/jme3/anim/interpolator/AnimInterpolator.java

@@ -0,0 +1,13 @@
+package com.jme3.anim.interpolator;
+
+import static com.jme3.anim.interpolator.FrameInterpolator.TrackDataReader;
+import static com.jme3.anim.interpolator.FrameInterpolator.TrackTimeReader;
+
+/**
+ * Created by nehon on 15/04/17.
+ */
+public abstract class AnimInterpolator<T> {
+
+    public abstract T interpolate(float t, int currentIndex, TrackDataReader<T> data, TrackTimeReader times, T store);
+
+}

+ 149 - 0
jme3-core/src/main/java/com/jme3/anim/interpolator/AnimInterpolators.java

@@ -0,0 +1,149 @@
+package com.jme3.anim.interpolator;
+
+import com.jme3.math.*;
+
+import static com.jme3.anim.interpolator.FrameInterpolator.TrackDataReader;
+import static com.jme3.anim.interpolator.FrameInterpolator.TrackTimeReader;
+
+/**
+ * Created by nehon on 15/04/17.
+ */
+public class AnimInterpolators {
+
+    //Rotation interpolators
+
+    public static final AnimInterpolator<Quaternion> NLerp = new AnimInterpolator<Quaternion>() {
+        private Quaternion next = new Quaternion();
+
+        @Override
+        public Quaternion interpolate(float t, int currentIndex, TrackDataReader<Quaternion> data, TrackTimeReader times, Quaternion store) {
+            data.getEntryClamp(currentIndex, store);
+            data.getEntryClamp(currentIndex + 1, next);
+            store.nlerp(next, t);
+            return store;
+        }
+    };
+
+    public static final AnimInterpolator<Quaternion> SLerp = new AnimInterpolator<Quaternion>() {
+        private Quaternion next = new Quaternion();
+
+        @Override
+        public Quaternion interpolate(float t, int currentIndex, TrackDataReader<Quaternion> data, TrackTimeReader times, Quaternion store) {
+            data.getEntryClamp(currentIndex, store);
+            data.getEntryClamp(currentIndex + 1, next);
+            //MathUtils.slerpNoInvert(store, next, t, store);
+            MathUtils.slerp(store, next, t, store);
+            return store;
+        }
+    };
+
+    public static final AnimInterpolator<Quaternion> SQuad = new AnimInterpolator<Quaternion>() {
+        private Quaternion a = new Quaternion();
+        private Quaternion b = new Quaternion();
+
+        private Quaternion q0 = new Quaternion();
+        private Quaternion q1 = new Quaternion();
+        private Quaternion q2 = new Quaternion();
+        private Quaternion q3 = new Quaternion();
+
+        @Override
+        public Quaternion interpolate(float t, int currentIndex, TrackDataReader<Quaternion> data, TrackTimeReader times, Quaternion store) {
+            data.getEntryModSkip(currentIndex - 1, q0);
+            data.getEntryModSkip(currentIndex, q1);
+            data.getEntryModSkip(currentIndex + 1, q2);
+            data.getEntryModSkip(currentIndex + 2, q3);
+            MathUtils.squad(q0, q1, q2, q3, a, b, t, store);
+            return store;
+        }
+    };
+
+    //Position / Scale interpolators
+    public static final AnimInterpolator<Vector3f> LinearVec3f = new AnimInterpolator<Vector3f>() {
+        private Vector3f next = new Vector3f();
+
+        @Override
+        public Vector3f interpolate(float t, int currentIndex, TrackDataReader<Vector3f> data, TrackTimeReader times, Vector3f store) {
+            data.getEntryClamp(currentIndex, store);
+            data.getEntryClamp(currentIndex + 1, next);
+            store.interpolateLocal(next, t);
+            return store;
+        }
+    };
+    /**
+     * CatmullRom interpolation
+     */
+    public static final CatmullRomInterpolator CatmullRom = new CatmullRomInterpolator();
+
+    public static class CatmullRomInterpolator extends AnimInterpolator<Vector3f> {
+        private Vector3f p0 = new Vector3f();
+        private Vector3f p1 = new Vector3f();
+        private Vector3f p2 = new Vector3f();
+        private Vector3f p3 = new Vector3f();
+        private float tension = 0.7f;
+
+        public CatmullRomInterpolator(float tension) {
+            this.tension = tension;
+        }
+
+        public CatmullRomInterpolator() {
+        }
+
+        @Override
+        public Vector3f interpolate(float t, int currentIndex, TrackDataReader<Vector3f> data, TrackTimeReader times, Vector3f store) {
+            data.getEntryModSkip(currentIndex - 1, p0);
+            data.getEntryModSkip(currentIndex, p1);
+            data.getEntryModSkip(currentIndex + 1, p2);
+            data.getEntryModSkip(currentIndex + 2, p3);
+
+            FastMath.interpolateCatmullRom(t, tension, p0, p1, p2, p3, store);
+            return store;
+        }
+    }
+
+    //Time Interpolators
+
+    public static class TimeInterpolator extends AnimInterpolator<Float> {
+        private EaseFunction ease;
+
+        public TimeInterpolator(EaseFunction ease) {
+            this.ease = ease;
+        }
+
+        @Override
+        public Float interpolate(float t, int currentIndex, TrackDataReader<Float> data, TrackTimeReader times, Float store) {
+            return ease.apply(t);
+        }
+    }
+
+    //in
+    public static final TimeInterpolator easeInQuad = new TimeInterpolator(Easing.inQuad);
+    public static final TimeInterpolator easeInCubic = new TimeInterpolator(Easing.inCubic);
+    public static final TimeInterpolator easeInQuart = new TimeInterpolator(Easing.inQuart);
+    public static final TimeInterpolator easeInQuint = new TimeInterpolator(Easing.inQuint);
+    public static final TimeInterpolator easeInBounce = new TimeInterpolator(Easing.inBounce);
+    public static final TimeInterpolator easeInElastic = new TimeInterpolator(Easing.inElastic);
+
+    //out
+    public static final TimeInterpolator easeOutQuad = new TimeInterpolator(Easing.outQuad);
+    public static final TimeInterpolator easeOutCubic = new TimeInterpolator(Easing.outCubic);
+    public static final TimeInterpolator easeOutQuart = new TimeInterpolator(Easing.outQuart);
+    public static final TimeInterpolator easeOutQuint = new TimeInterpolator(Easing.outQuint);
+    public static final TimeInterpolator easeOutBounce = new TimeInterpolator(Easing.outBounce);
+    public static final TimeInterpolator easeOutElastic = new TimeInterpolator(Easing.outElastic);
+
+    //inout
+    public static final TimeInterpolator easeInOutQuad = new TimeInterpolator(Easing.inOutQuad);
+    public static final TimeInterpolator easeInOutCubic = new TimeInterpolator(Easing.inOutCubic);
+    public static final TimeInterpolator easeInOutQuart = new TimeInterpolator(Easing.inOutQuart);
+    public static final TimeInterpolator easeInOutQuint = new TimeInterpolator(Easing.inOutQuint);
+    public static final TimeInterpolator easeInOutBounce = new TimeInterpolator(Easing.inOutBounce);
+    public static final TimeInterpolator easeInOutElastic = new TimeInterpolator(Easing.inOutElastic);
+
+    //extra
+    public static final TimeInterpolator smoothStep = new TimeInterpolator(Easing.smoothStep);
+    public static final TimeInterpolator smootherStep = new TimeInterpolator(Easing.smootherStep);
+
+    public static final TimeInterpolator constant = new TimeInterpolator(Easing.constant);
+
+
+}

+ 136 - 0
jme3-core/src/main/java/com/jme3/anim/interpolator/FrameInterpolator.java

@@ -0,0 +1,136 @@
+package com.jme3.anim.interpolator;
+
+import com.jme3.animation.*;
+import com.jme3.math.*;
+
+/**
+ * Created by nehon on 15/04/17.
+ */
+public class FrameInterpolator {
+
+    public static final FrameInterpolator DEFAULT = new FrameInterpolator();
+
+    private AnimInterpolator<Float> timeInterpolator;
+    private AnimInterpolator<Vector3f> translationInterpolator = AnimInterpolators.LinearVec3f;
+    private AnimInterpolator<Quaternion> rotationInterpolator = AnimInterpolators.NLerp;
+    private AnimInterpolator<Vector3f> scaleInterpolator = AnimInterpolators.LinearVec3f;
+
+    private TrackDataReader<Vector3f> translationReader = new TrackDataReader<>();
+    private TrackDataReader<Quaternion> rotationReader = new TrackDataReader<>();
+    private TrackDataReader<Vector3f> scaleReader = new TrackDataReader<>();
+    private TrackTimeReader timesReader = new TrackTimeReader();
+
+
+    private Transform transforms = new Transform();
+
+    public Transform interpolate(float t, int currentIndex, CompactVector3Array translations, CompactQuaternionArray rotations, CompactVector3Array scales, float[] times){
+        timesReader.setData(times);
+        if( timeInterpolator != null){
+            t = timeInterpolator.interpolate(t,currentIndex, null, timesReader, null );
+        }
+        if(translations != null) {
+            translationReader.setData(translations);
+            translationInterpolator.interpolate(t, currentIndex, translationReader, timesReader, transforms.getTranslation());
+        }
+        if(rotations != null) {
+            rotationReader.setData(rotations);
+            rotationInterpolator.interpolate(t, currentIndex, rotationReader, timesReader, transforms.getRotation());
+        }
+        if(scales != null){
+            scaleReader.setData(scales);
+            scaleInterpolator.interpolate(t, currentIndex, scaleReader, timesReader, transforms.getScale());
+        }
+        return transforms;
+    }
+
+    public void interpolateWeights(float t, int currentIndex, float[] weights, int nbMorphTargets, float[] store) {
+        int start = currentIndex * nbMorphTargets;
+        for (int i = 0; i < nbMorphTargets; i++) {
+            int current = start + i;
+            int next = current + nbMorphTargets;
+            if (next >= weights.length) {
+                next = current;
+            }
+
+            float val =  FastMath.interpolateLinear(t, weights[current], weights[next]);
+            store[i] = val;
+        }
+    }
+
+    public void setTimeInterpolator(AnimInterpolator<Float> timeInterpolator) {
+        this.timeInterpolator = timeInterpolator;
+    }
+
+    public void setTranslationInterpolator(AnimInterpolator<Vector3f> translationInterpolator) {
+        this.translationInterpolator = translationInterpolator;
+    }
+
+    public void setRotationInterpolator(AnimInterpolator<Quaternion> rotationInterpolator) {
+        this.rotationInterpolator = rotationInterpolator;
+    }
+
+    public void setScaleInterpolator(AnimInterpolator<Vector3f> scaleInterpolator) {
+        this.scaleInterpolator = scaleInterpolator;
+    }
+
+
+    public static class TrackTimeReader {
+        private float[] data;
+
+        protected void setData(float[] data) {
+            this.data = data;
+        }
+
+        public float getEntry(int index) {
+            return data[mod(index, data.length)];
+        }
+
+        public int getLength() {
+            return data.length;
+        }
+    }
+
+    public static class TrackDataReader<T> {
+
+        private CompactArray<T> data;
+
+        protected void setData(CompactArray<T> data) {
+            this.data = data;
+        }
+
+        public T getEntryMod(int index, T store) {
+            return data.get(mod(index, data.getTotalObjectSize()), store);
+        }
+
+        public T getEntryClamp(int index, T store) {
+            index = (int) FastMath.clamp(index, 0, data.getTotalObjectSize() - 1);
+            return data.get(index, store);
+        }
+
+        public T getEntryModSkip(int index, T store) {
+            int total = data.getTotalObjectSize();
+            if (index == -1) {
+                index--;
+            } else if (index >= total) {
+                index++;
+            }
+
+            index = mod(index, total);
+
+
+            return data.get(index, store);
+        }
+    }
+
+    /**
+     * Euclidean modulo (cycle on 0,n instead of -n,0; 0,n)
+     *
+     * @param val
+     * @param n
+     * @return
+     */
+    private static int mod(int val, int n) {
+        return ((val % n) + n) % n;
+    }
+
+}

+ 97 - 0
jme3-core/src/main/java/com/jme3/anim/tween/AbstractTween.java

@@ -0,0 +1,97 @@
+/*
+ * $Id$
+ * 
+ * Copyright (c) 2015, Simsilica, LLC
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions 
+ * are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright 
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ * 2. 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.
+ * 
+ * 3. Neither the name of the copyright holder 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 HOLDER 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.anim.tween;
+
+
+import com.jme3.export.*;
+
+import java.io.IOException;
+
+/**
+ * Base implementation of the Tween interface that provides
+ * default implementations of the getLength() and interopolate()
+ * methods that provide common tween clamping and bounds checking.
+ * Subclasses need only override the doInterpolate() method and
+ * the rest is handled for them.
+ *
+ * @author Paul Speed
+ */
+public abstract class AbstractTween implements Tween {
+
+    private double length;
+
+    protected AbstractTween(double length) {
+        this.length = length;
+    }
+
+    @Override
+    public double getLength() {
+        return length;
+    }
+
+    public void setLength(double length) {
+        this.length = length;
+    }
+
+    /**
+     * Default implementation clamps the time value, converts
+     * it to 0 to 1.0 based on getLength(), and calls doInterpolate().
+     */
+    @Override
+    public boolean interpolate(double t) {
+        if (t < 0) {
+            return true;
+        }
+
+        // Scale t to be between 0 and 1 for our length
+        if (length == 0) {
+            t = 1;
+        } else {
+            t = t / length;
+        }
+
+        boolean done = false;
+        if (t >= 1.0) {
+            t = 1.0;
+            done = true;
+        }
+        doInterpolate(t);
+        return !done;
+    }
+
+    protected abstract void doInterpolate(double t);
+}

+ 6 - 0
jme3-core/src/main/java/com/jme3/anim/tween/ContainsTweens.java

@@ -0,0 +1,6 @@
+package com.jme3.anim.tween;
+
+public interface ContainsTweens {
+
+    public Tween[] getTweens();
+}

+ 71 - 0
jme3-core/src/main/java/com/jme3/anim/tween/Tween.java

@@ -0,0 +1,71 @@
+/*
+ * $Id$
+ * 
+ * Copyright (c) 2015, Simsilica, LLC
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions 
+ * are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright 
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ * 2. 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.
+ * 
+ * 3. Neither the name of the copyright holder 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 HOLDER 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.anim.tween;
+
+
+import com.jme3.export.Savable;
+
+/**
+ * Represents some action that interpolates across input between 0
+ * and some length value.  (For example, movement, rotation, fading.)
+ * It's also possible to have zero length 'instant' tweens.
+ *
+ * @author Paul Speed
+ */
+public interface Tween extends Cloneable {
+
+    /**
+     * Returns the length of the tween.  If 't' represents time in
+     * seconds then this is the notional time in seconds that the tween
+     * will run.  Note: all of the caveats are because tweens may be
+     * externally scaled in such a way that 't' no longer represents
+     * actual time.
+     */
+    public double getLength();
+
+    /**
+     * Sets the implementation specific interpolation to the
+     * specified 'tween' value as a value in the range from 0 to
+     * getLength().  If the value is greater or equal to getLength()
+     * then it is internally clamped and the method returns false.
+     * If 't' is still in the tween's range then this method returns
+     * true.
+     */
+    public boolean interpolate(double t);
+
+}
+

+ 619 - 0
jme3-core/src/main/java/com/jme3/anim/tween/Tweens.java

@@ -0,0 +1,619 @@
+/*
+ * $Id$
+ * 
+ * Copyright (c) 2015, Simsilica, LLC
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions 
+ * are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright 
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ * 2. 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.
+ * 
+ * 3. Neither the name of the copyright holder 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 HOLDER 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.anim.tween;
+
+import com.jme3.anim.util.Primitives;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Static utility methods for creating common generic Tween objects.
+ *
+ * @author Paul Speed
+ */
+public class Tweens {
+
+    static Logger log = Logger.getLogger(Tweens.class.getName());
+
+    private static final CurveFunction SMOOTH = new SmoothStep();
+    private static final CurveFunction SINE = new Sine();
+
+    /**
+     * Creates a tween that will interpolate over an entire sequence
+     * of tweens in order.
+     */
+    public static Tween sequence(Tween... delegates) {
+        return new Sequence(delegates);
+    }
+
+    /**
+     * Creates a tween that will interpolate over an entire list
+     * of tweens in parallel, ie: all tweens will be run at the same
+     * time.
+     */
+    public static Tween parallel(Tween... delegates) {
+        return new Parallel(delegates);
+    }
+
+    /**
+     * Creates a tween that will perform a no-op until the length
+     * has expired.
+     */
+    public static Tween delay(double length) {
+        return new Delay(length);
+    }
+
+    /**
+     * Creates a tween that scales the specified delegate tween or tweens
+     * to the desired length.  If more than one tween is specified then they
+     * are wrapped in a sequence using the sequence() method.
+     */
+    public static Tween stretch(double desiredLength, Tween... delegates) {
+        if (delegates.length == 1) {
+            return new Stretch(delegates[0], desiredLength);
+        }
+        return new Stretch(sequence(delegates), desiredLength);
+    }
+
+    /**
+     * Creates a tween that uses a sine function to smooth step the time value
+     * for the specified delegate tween or tweens.  These 'curved' wrappers
+     * can be used to smooth the interpolation of another tween.
+     */
+    public static Tween sineStep(Tween... delegates) {
+        if (delegates.length == 1) {
+            return new Curve(delegates[0], SINE);
+        }
+        return new Curve(sequence(delegates), SINE);
+    }
+
+    /**
+     * Creates a tween that uses a hermite function to smooth step the time value
+     * for the specified delegate tween or tweens.  This is similar to GLSL's
+     * smoothstep().  These 'curved' wrappers can be used to smooth the interpolation
+     * of another tween.
+     */
+    public static Tween smoothStep(Tween... delegates) {
+        if (delegates.length == 1) {
+            return new Curve(delegates[0], SMOOTH);
+        }
+        return new Curve(sequence(delegates), SMOOTH);
+    }
+
+    /**
+     * Creates a Tween that will call the specified method and optional arguments
+     * whenever supplied a time value greater than or equal to 0.  This creates
+     * an "instant" tween of length 0.
+     */
+    public static Tween callMethod(Object target, String method, Object... args) {
+        return new CallMethod(target, method, args);
+    }
+
+    /**
+     * Creates a Tween that will call the specified method and optional arguments,
+     * including the time value scaled between 0 and 1.  The method must take
+     * a float or double value as its first or last argument, in addition to whatever
+     * optional arguments are specified.
+     * <p>
+     * <p>For example:</p>
+     * <pre>Tweens.callTweenMethod(1, myObject, "foo", "bar")</pre>
+     * <p>Would work for any of the following method signatures:</p>
+     * <pre>
+     *    void foo( float t, String arg )
+     *    void foo( double t, String arg )
+     *    void foo( String arg, float t )
+     *    void foo( String arg, double t )
+     *  </pre>
+     */
+    public static Tween callTweenMethod(double length, Object target, String method, Object... args) {
+        return new CallTweenMethod(length, target, method, args);
+    }
+
+    private static interface CurveFunction {
+        public double curve(double input);
+    }
+
+    /**
+     * Curve function for Hermite interpolation ala GLSL smoothstep().
+     */
+    private static class SmoothStep implements CurveFunction {
+
+        @Override
+        public double curve(double t) {
+            if (t < 0) {
+                return 0;
+            } else if (t > 1) {
+                return 1;
+            }
+            return t * t * (3 - 2 * t);
+        }
+    }
+
+    private static class Sine implements CurveFunction {
+
+        @Override
+        public double curve(double t) {
+            if (t < 0) {
+                return 0;
+            } else if (t > 1) {
+                return 1;
+            }
+            // Sine starting at -90 will go from -1 to 1 through 0 
+            double result = Math.sin(t * Math.PI - Math.PI * 0.5);
+            return (result + 1) * 0.5;
+        }
+    }
+
+    private static class Curve implements Tween {
+        private final Tween delegate;
+        private final CurveFunction func;
+        private final double length;
+
+        public Curve(Tween delegate, CurveFunction func) {
+            this.delegate = delegate;
+            this.func = func;
+            this.length = delegate.getLength();
+        }
+
+        @Override
+        public double getLength() {
+            return length;
+        }
+
+        @Override
+        public boolean interpolate(double t) {
+            // Sanity check the inputs
+            if (t < 0) {
+                return true;
+            }
+
+            if (length == 0) {
+                // Caller did something strange but we'll allow it
+                return delegate.interpolate(t);
+            }
+
+            t = func.curve(t / length);
+            return delegate.interpolate(t * length);
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + "[delegate=" + delegate + ", func=" + func + "]";
+        }
+    }
+
+    private static class Sequence implements Tween, ContainsTweens {
+        private final Tween[] delegates;
+        private int current = 0;
+        private double baseTime;
+        private double length;
+
+        public Sequence(Tween... delegates) {
+            this.delegates = delegates;
+            for (Tween t : delegates) {
+                length += t.getLength();
+            }
+        }
+
+        @Override
+        public double getLength() {
+            return length;
+        }
+
+        @Override
+        public boolean interpolate(double t) {
+
+            // Sanity check the inputs
+            if (t < 0) {
+                return true;
+            }
+
+            if (t < baseTime) {
+                // We've rolled back before the current sequence step
+                // which means we need to reset and start forward
+                // again.  We have no idea how to 'roll back' and
+                // this is the only way to maintain consistency.
+                // The only 'normal' case where this happens is when looping
+                // in which case a full rollback is appropriate.
+                current = 0;
+                baseTime = 0;
+            }
+
+            if (current >= delegates.length) {
+                return false;
+            }
+
+            // Skip any that are done
+            while (!delegates[current].interpolate(t - baseTime)) {
+                // Time to go to the next one
+                baseTime += delegates[current].getLength();
+                current++;
+                if (current >= delegates.length) {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + "[delegates=" + Arrays.asList(delegates) + "]";
+        }
+
+        @Override
+        public Tween[] getTweens() {
+            return delegates;
+        }
+    }
+
+    private static class Parallel implements Tween, ContainsTweens {
+        private final Tween[] delegates;
+        private final boolean[] done;
+        private double length;
+        private double lastTime;
+
+        public Parallel(Tween... delegates) {
+            this.delegates = delegates;
+            done = new boolean[delegates.length];
+
+            for (Tween t : delegates) {
+                if (t.getLength() > length) {
+                    length = t.getLength();
+                }
+            }
+        }
+
+        @Override
+        public double getLength() {
+            return length;
+        }
+
+        protected void reset() {
+            for (int i = 0; i < done.length; i++) {
+                done[i] = false;
+            }
+        }
+
+        @Override
+        public boolean interpolate(double t) {
+            // Sanity check the inputs
+            if (t < 0) {
+                return true;
+            }
+
+            if (t < lastTime) {
+                // We've rolled back before the last time we were given.
+                // This means we may have 'done'ed a few tasks that now
+                // need to be run again.  Better to just reset and start
+                // over.  As mentioned in the Sequence task, the only 'normal'
+                // use-case for time rolling backwards is when looping.  And
+                // in that case, we want to start from the beginning anyway.
+                reset();
+            }
+            lastTime = t;
+
+            int runningCount = delegates.length;
+            for (int i = 0; i < delegates.length; i++) {
+                if (!done[i]) {
+                    done[i] = !delegates[i].interpolate(t);
+                }
+                if (done[i]) {
+                    runningCount--;
+                }
+            }
+            return runningCount > 0;
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + "[delegates=" + Arrays.asList(delegates) + "]";
+        }
+
+        @Override
+        public Tween[] getTweens() {
+            return delegates;
+        }
+    }
+
+    private static class Delay extends AbstractTween {
+
+        public Delay(double length) {
+            super(length);
+        }
+
+        @Override
+        protected void doInterpolate(double t) {
+        }
+    }
+
+    private static class Stretch implements Tween, ContainsTweens {
+
+        private final Tween[] delegate = new Tween[1];
+        private final double length;
+        private final double scale;
+
+        public Stretch(Tween delegate, double length) {
+            this.delegate[0] = delegate;
+
+            this.length = length;
+
+            // Caller desires delegate to be 'length' instead of
+            // it's actual length so we will calculate a time scale
+            // If the desired length is longer than delegate's then
+            // we need to feed time in slower, ie: scale < 1
+            if (length != 0) {
+                this.scale = delegate.getLength() / length;
+            } else {
+                this.scale = 0;
+            }
+        }
+
+        @Override
+        public double getLength() {
+            return length;
+        }
+
+        @Override
+        public Tween[] getTweens() {
+            return delegate;
+        }
+
+        @Override
+        public boolean interpolate(double t) {
+            if (t < 0) {
+                return true;
+            }
+            if (length > 0) {
+                t *= scale;
+            } else {
+                t = length;
+            }
+            return delegate[0].interpolate(t);
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + "[delegate=" + delegate[0] + ", length=" + length + "]";
+        }
+    }
+
+    private static class CallMethod extends AbstractTween {
+
+        private Object target;
+        private Method method;
+        private Object[] args;
+
+        public CallMethod(Object target, String methodName, Object... args) {
+            super(0);
+            if (target == null) {
+                throw new IllegalArgumentException("Target cannot be null.");
+            }
+            this.target = target;
+            this.args = args;
+
+            // Lookup the method
+            if (args == null) {
+                this.method = findMethod(target.getClass(), methodName);
+            } else {
+                this.method = findMethod(target.getClass(), methodName, args);
+            }
+            if (this.method == null) {
+                throw new IllegalArgumentException("Method not found for:" + methodName + " on type:" + target.getClass());
+            }
+            this.method.setAccessible(true);
+        }
+
+        private static Method findMethod(Class type, String name, Object... args) {
+            for (Method m : type.getDeclaredMethods()) {
+                if (!Objects.equals(m.getName(), name)) {
+                    continue;
+                }
+                Class[] paramTypes = m.getParameterTypes();
+                if (paramTypes.length != args.length) {
+                    continue;
+                }
+                int matches = 0;
+                for (int i = 0; i < args.length; i++) {
+                    if (paramTypes[i].isInstance(args[i])
+                            || Primitives.wrap(paramTypes[i]).isInstance(args[i])) {
+                        matches++;
+                    }
+                }
+                if (matches == args.length) {
+                    return m;
+                }
+            }
+            if (type.getSuperclass() != null) {
+                return findMethod(type.getSuperclass(), name, args);
+            }
+            return null;
+        }
+
+        @Override
+        protected void doInterpolate(double t) {
+            try {
+                method.invoke(target, args);
+            } catch (IllegalAccessException e) {
+                throw new RuntimeException("Error running method:" + method + " for object:" + target, e);
+            } catch (InvocationTargetException e) {
+                throw new RuntimeException("Error running method:" + method + " for object:" + target, e);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + "[method=" + method + ", parms=" + Arrays.asList(args) + "]";
+        }
+    }
+
+    private static class CallTweenMethod extends AbstractTween {
+
+        private Object target;
+        private Method method;
+        private Object[] args;
+        private int tIndex = -1;
+        private boolean isFloat = false;
+
+        public CallTweenMethod(double length, Object target, String methodName, Object... args) {
+            super(length);
+            if (target == null) {
+                throw new IllegalArgumentException("Target cannot be null.");
+            }
+            this.target = target;
+
+            // Lookup the method
+            this.method = findMethod(target.getClass(), methodName, args);
+            if (this.method == null) {
+                throw new IllegalArgumentException("Method not found for:" + methodName + " on type:" + target.getClass());
+            }
+            this.method.setAccessible(true);
+
+            // So now setup the real args list
+            this.args = new Object[args.length + 1];
+            if (tIndex == 0) {
+                for (int i = 0; i < args.length; i++) {
+                    this.args[i + 1] = args[i];
+                }
+            } else {
+                for (int i = 0; i < args.length; i++) {
+                    this.args[i] = args[i];
+                }
+            }
+        }
+
+        private static boolean isFloatType(Class type) {
+            return type == Float.TYPE || type == Float.class;
+        }
+
+        private static boolean isDoubleType(Class type) {
+            return type == Double.TYPE || type == Double.class;
+        }
+
+        private Method findMethod(Class type, String name, Object... args) {
+            for (Method m : type.getDeclaredMethods()) {
+                if (!Objects.equals(m.getName(), name)) {
+                    continue;
+                }
+                Class[] paramTypes = m.getParameterTypes();
+                if (paramTypes.length != args.length + 1) {
+                    if (log.isLoggable(Level.FINE)) {
+                        log.log(Level.FINE, "Param lengths of [" + m + "] differ.  method arg count:" + paramTypes.length + "  lookging for:" + (args.length + 1));
+                    }
+                    continue;
+                }
+
+                // We accept the 't' parameter as either first or last
+                // so we'll see which one matches.
+                if (isFloatType(paramTypes[0]) || isDoubleType(paramTypes[0])) {
+                    // Try it as the first parameter 
+                    int matches = 0;
+
+                    for (int i = 1; i < paramTypes.length; i++) {
+                        if (paramTypes[i].isInstance(args[i - 1])) {
+                            matches++;
+                        }
+                    }
+                    if (matches == args.length) {
+                        // Then this is our method and this is how we are configured
+                        tIndex = 0;
+                        isFloat = isFloatType(paramTypes[0]);
+                    } else {
+                        if (log.isLoggable(Level.FINE)) {
+                            log.log(Level.FINE, m + " Leading float check failed because of type mismatches, for:" + m);
+                        }
+                    }
+                }
+                if (tIndex >= 0) {
+                    return m;
+                }
+
+                // Else try it at the end                
+                int last = paramTypes.length - 1;
+                if (isFloatType(paramTypes[last]) || isDoubleType(paramTypes[last])) {
+                    int matches = 0;
+
+                    for (int i = 0; i < last; i++) {
+                        if (paramTypes[i].isInstance(args[i])) {
+                            matches++;
+                        }
+                    }
+                    if (matches == args.length) {
+                        // Then this is our method and this is how we are configured
+                        tIndex = last;
+                        isFloat = isFloatType(paramTypes[last]);
+                        return m;
+                    } else {
+                        if (log.isLoggable(Level.FINE)) {
+                            log.log(Level.FINE, "Trailing float check failed because of type mismatches, for:" + m);
+                        }
+                    }
+                }
+            }
+            if (type.getSuperclass() != null) {
+                return findMethod(type.getSuperclass(), name, args);
+            }
+            return null;
+        }
+
+        @Override
+        protected void doInterpolate(double t) {
+            try {
+                if (isFloat) {
+                    args[tIndex] = (float) t;
+                } else {
+                    args[tIndex] = t;
+                }
+                method.invoke(target, args);
+            } catch (IllegalAccessException e) {
+                throw new RuntimeException("Error running method:" + method + " for object:" + target, e);
+            } catch (InvocationTargetException e) {
+                throw new RuntimeException("Error running method:" + method + " for object:" + target, e);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + "[method=" + method + ", parms=" + Arrays.asList(args) + "]";
+        }
+    }
+}

+ 70 - 0
jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java

@@ -0,0 +1,70 @@
+package com.jme3.anim.tween.action;
+
+import com.jme3.anim.AnimationMask;
+import com.jme3.anim.tween.Tween;
+
+public abstract class Action implements Tween {
+
+    protected Action[] actions;
+    private double length;
+    private double speed = 1;
+    private AnimationMask mask;
+    private boolean forward = true;
+
+    protected Action(Tween... tweens) {
+        this.actions = new Action[tweens.length];
+        for (int i = 0; i < tweens.length; i++) {
+            Tween tween = tweens[i];
+            if (tween instanceof Action) {
+                this.actions[i] = (Action) tween;
+            } else {
+                this.actions[i] = new BaseAction(tween);
+            }
+        }
+    }
+
+    @Override
+    public double getLength() {
+        return length;
+    }
+
+    protected void setLength(double length) {
+        this.length = length;
+    }
+
+    public double getSpeed() {
+        return speed;
+    }
+
+    public void setSpeed(double speed) {
+        this.speed = speed;
+        if( speed < 0){
+            setForward(false);
+        } else {
+            setForward(true);
+        }
+    }
+
+    public AnimationMask getMask() {
+        return mask;
+    }
+
+    public void setMask(AnimationMask mask) {
+        this.mask = mask;
+    }
+
+    protected boolean isForward() {
+        return forward;
+    }
+
+    protected void setForward(boolean forward) {
+        if(this.forward == forward){
+            return;
+        }
+        this.forward = forward;
+        for (Action action : actions) {
+            action.setForward(forward);
+        }
+
+    }
+}

+ 38 - 0
jme3-core/src/main/java/com/jme3/anim/tween/action/BaseAction.java

@@ -0,0 +1,38 @@
+package com.jme3.anim.tween.action;
+
+import com.jme3.anim.tween.ContainsTweens;
+import com.jme3.anim.tween.Tween;
+import com.jme3.util.SafeArrayList;
+
+import java.util.Collections;
+import java.util.List;
+
+public class BaseAction extends Action {
+
+    private Tween tween;
+
+    public BaseAction(Tween tween) {
+        this.tween = tween;
+        setLength(tween.getLength());
+        List<Action> subActions = new SafeArrayList<>(Action.class);
+        gatherActions(tween, subActions);
+        actions = new Action[subActions.size()];
+        subActions.toArray(actions);
+    }
+
+    private void gatherActions(Tween tween, List<Action> subActions) {
+        if (tween instanceof Action) {
+            subActions.add((Action) tween);
+        } else if (tween instanceof ContainsTweens) {
+            Tween[] tweens = ((ContainsTweens) tween).getTweens();
+            for (Tween t : tweens) {
+                gatherActions(t, subActions);
+            }
+        }
+    }
+
+    @Override
+    public boolean interpolate(double t) {
+        return tween.interpolate(t);
+    }
+}

+ 129 - 0
jme3-core/src/main/java/com/jme3/anim/tween/action/BlendAction.java

@@ -0,0 +1,129 @@
+package com.jme3.anim.tween.action;
+
+import com.jme3.anim.util.HasLocalTransform;
+import com.jme3.math.Transform;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+public class BlendAction extends BlendableAction {
+
+    private int firstActiveIndex;
+    private int secondActiveIndex;
+    private BlendSpace blendSpace;
+    private float blendWeight;
+    private double[] timeFactor;
+    private Map<HasLocalTransform, Transform> targetMap = new HashMap<>();
+
+    public BlendAction(BlendSpace blendSpace, BlendableAction... actions) {
+        super(actions);
+        timeFactor = new double[actions.length];
+        this.blendSpace = blendSpace;
+        blendSpace.setBlendAction(this);
+
+        for (BlendableAction action : actions) {
+            if (action.getLength() > getLength()) {
+                setLength(action.getLength());
+            }
+            Collection<HasLocalTransform> targets = action.getTargets();
+            for (HasLocalTransform target : targets) {
+                Transform t = targetMap.get(target);
+                if (t == null) {
+                    t = new Transform();
+                    targetMap.put(target, t);
+                }
+            }
+        }
+
+        //Blending effect maybe unexpected when blended animation don't have the same length
+        //Stretching any action that doesn't have the same length.
+        for (int i = 0; i < this.actions.length; i++) {
+            this.timeFactor[i] = 1;
+            if (this.actions[i].getLength() != getLength()) {
+                double actionLength = this.actions[i].getLength();
+                if (actionLength > 0 && getLength() > 0) {
+                    this.timeFactor[i] = this.actions[i].getLength() / getLength();
+                }
+            }
+        }
+    }
+
+    public void doInterpolate(double t) {
+        blendWeight = blendSpace.getWeight();
+        BlendableAction firstActiveAction = (BlendableAction) actions[firstActiveIndex];
+        BlendableAction secondActiveAction = (BlendableAction) actions[secondActiveIndex];
+        firstActiveAction.setCollectTransformDelegate(this);
+        secondActiveAction.setCollectTransformDelegate(this);
+
+        //only interpolate the first action if the weight if below 1.
+        if (blendWeight < 1f) {
+            firstActiveAction.setWeight(1f);
+            firstActiveAction.interpolate(t * timeFactor[firstActiveIndex]);
+            if (blendWeight == 0) {
+                for (HasLocalTransform target : targetMap.keySet()) {
+                    collect(target, targetMap.get(target));
+                }
+            }
+        }
+
+        //Second action should be interpolated
+        secondActiveAction.setWeight(blendWeight);
+        secondActiveAction.interpolate(t * timeFactor[secondActiveIndex]);
+
+        firstActiveAction.setCollectTransformDelegate(null);
+        secondActiveAction.setCollectTransformDelegate(null);
+
+    }
+
+    protected Action[] getActions() {
+        return actions;
+    }
+
+    public BlendSpace getBlendSpace() {
+        return blendSpace;
+    }
+
+    protected void setFirstActiveIndex(int index) {
+        this.firstActiveIndex = index;
+    }
+
+    protected void setSecondActiveIndex(int index) {
+        this.secondActiveIndex = index;
+    }
+
+    @Override
+    public Collection<HasLocalTransform> getTargets() {
+        return targetMap.keySet();
+    }
+
+    @Override
+    public void collectTransform(HasLocalTransform target, Transform t, float weight, BlendableAction source) {
+
+        Transform tr = targetMap.get(target);
+        if (weight == 1) {
+            tr.set(t);
+        } else if (weight > 0) {
+            tr.interpolateTransforms(tr, t, weight);
+        }
+
+        if (source == actions[secondActiveIndex]) {
+            collect(target, tr);
+        }
+    }
+
+    private void collect(HasLocalTransform target, Transform tr) {
+        if (collectTransformDelegate != null) {
+            collectTransformDelegate.collectTransform(target, tr, this.getWeight(), this);
+        } else {
+            if (getTransitionWeight() == 1) {
+                target.setLocalTransform(tr);
+            } else {
+                Transform trans = target.getLocalTransform();
+                trans.interpolateTransforms(trans, tr, getTransitionWeight());
+                target.setLocalTransform(trans);
+            }
+        }
+    }
+
+}

+ 10 - 0
jme3-core/src/main/java/com/jme3/anim/tween/action/BlendSpace.java

@@ -0,0 +1,10 @@
+package com.jme3.anim.tween.action;
+
+public interface BlendSpace {
+
+    public void setBlendAction(BlendAction action);
+
+    public float getWeight();
+
+    public void setValue(float value);
+}

+ 97 - 0
jme3-core/src/main/java/com/jme3/anim/tween/action/BlendableAction.java

@@ -0,0 +1,97 @@
+package com.jme3.anim.tween.action;
+
+import com.jme3.anim.tween.AbstractTween;
+import com.jme3.anim.tween.Tween;
+import com.jme3.anim.util.HasLocalTransform;
+import com.jme3.math.FastMath;
+import com.jme3.math.Transform;
+
+import java.util.Collection;
+
+public abstract class BlendableAction extends Action {
+
+    protected BlendableAction collectTransformDelegate;
+    private float transitionWeight = 1.0f;
+    private double transitionLength = 0.4f;
+    private float weight = 1f;
+    private TransitionTween transition = new TransitionTween(transitionLength);
+
+    public BlendableAction(Tween... tweens) {
+        super(tweens);
+    }
+
+    public void setCollectTransformDelegate(BlendableAction delegate) {
+        this.collectTransformDelegate = delegate;
+    }
+
+    @Override
+    public boolean interpolate(double t) {
+        // Sanity check the inputs
+        if (t < 0) {
+            return true;
+        }
+
+        if (collectTransformDelegate == null) {
+            if (transition.getLength() > getLength()) {
+                transition.setLength(getLength());
+            }
+            if(isForward()) {
+                transition.interpolate(t);
+            } else {
+                float v = Math.max((float)(getLength() - t), 0f);
+                transition.interpolate(v);
+            }
+        } else {
+            transitionWeight = 1f;
+        }
+
+        if (weight == 0) {
+            //weight is 0 let's not interpolate
+            return t < getLength();
+        }
+
+        doInterpolate(t);
+
+        return t < getLength();
+    }
+
+    public float getWeight() {
+        return weight;
+    }
+
+    public void setWeight(float weight) {
+        this.weight = weight;
+    }
+
+    protected abstract void doInterpolate(double t);
+
+    public abstract Collection<HasLocalTransform> getTargets();
+
+    public abstract void collectTransform(HasLocalTransform target, Transform t, float weight, BlendableAction source);
+
+    public double getTransitionLength() {
+        return transitionLength;
+    }
+
+    public void setTransitionLength(double transitionLength) {
+        this.transitionLength = transitionLength;
+    }
+
+    protected float getTransitionWeight() {
+        return transitionWeight;
+    }
+
+    private class TransitionTween extends AbstractTween {
+
+
+        public TransitionTween(double length) {
+            super(length);
+        }
+
+        @Override
+        protected void doInterpolate(double t) {
+            transitionWeight = (float) t;
+        }
+    }
+
+}

+ 94 - 0
jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java

@@ -0,0 +1,94 @@
+package com.jme3.anim.tween.action;
+
+import com.jme3.anim.*;
+import com.jme3.anim.tween.AbstractTween;
+import com.jme3.anim.util.HasLocalTransform;
+import com.jme3.math.Transform;
+import com.jme3.scene.Geometry;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public class ClipAction extends BlendableAction {
+
+    private AnimClip clip;
+    private Transform transform = new Transform();
+
+    public ClipAction(AnimClip clip) {
+        this.clip = clip;
+        setLength(clip.getLength());
+    }
+
+    @Override
+    public void doInterpolate(double t) {
+        AnimTrack[] tracks = clip.getTracks();
+        for (AnimTrack track : tracks) {
+            if (track instanceof TransformTrack) {
+                TransformTrack tt = (TransformTrack) track;
+                if(getMask() != null && !getMask().contains(tt.getTarget())){
+                    continue;
+                }
+                interpolateTransformTrack(t, tt);
+            } else if (track instanceof MorphTrack) {
+                interpolateMorphTrack(t, (MorphTrack) track);
+            }
+        }
+    }
+
+    private void interpolateTransformTrack(double t, TransformTrack track) {
+        HasLocalTransform target = track.getTarget();
+        transform.set(target.getLocalTransform());
+        track.getDataAtTime(t, transform);
+
+        if (collectTransformDelegate != null) {
+            collectTransformDelegate.collectTransform(target, transform, getWeight(), this);
+        } else {
+            this.collectTransform(target, transform, getTransitionWeight(), this);
+        }
+    }
+    private void interpolateMorphTrack(double t, MorphTrack track) {
+        Geometry target = track.getTarget();
+        float[] weights = target.getMorphState();
+        track.getDataAtTime(t, weights);
+        target.setMorphState(weights);
+
+//        if (collectTransformDelegate != null) {
+//            collectTransformDelegate.collectTransform(target, transform, getWeight(), this);
+//        } else {
+//            this.collectTransform(target, transform, getTransitionWeight(), this);
+//        }
+    }
+
+    public void reset() {
+
+    }
+
+    public String toString() {
+        return clip.toString();
+    }
+
+    @Override
+    public Collection<HasLocalTransform> getTargets() {
+        List<HasLocalTransform> targets = new ArrayList<>(clip.getTracks().length);
+        for (AnimTrack track : clip.getTracks()) {
+            if (track instanceof TransformTrack) {
+                targets.add(((TransformTrack) track).getTarget());
+            }
+        }
+        return targets;
+    }
+
+    @Override
+    public void collectTransform(HasLocalTransform target, Transform t, float weight, BlendableAction source) {
+        if (weight == 1f) {
+            target.setLocalTransform(t);
+        } else {
+            Transform tr = target.getLocalTransform();
+            tr.interpolateTransforms(tr, t, weight);
+            target.setLocalTransform(tr);
+        }
+    }
+
+
+}

+ 49 - 0
jme3-core/src/main/java/com/jme3/anim/tween/action/LinearBlendSpace.java

@@ -0,0 +1,49 @@
+package com.jme3.anim.tween.action;
+
+public class LinearBlendSpace implements BlendSpace {
+
+    private BlendAction action;
+    private float value;
+    private float maxValue;
+    private float minValue;
+    private float step;
+
+    public LinearBlendSpace(float minValue, float maxValue) {
+        this.maxValue = maxValue;
+        this.minValue = minValue;
+    }
+
+    @Override
+    public void setBlendAction(BlendAction action) {
+        this.action = action;
+        Action[] actions = action.getActions();
+        step = (maxValue - minValue) / (float) (actions.length - 1);
+    }
+
+    @Override
+    public float getWeight() {
+        Action[] actions = action.getActions();
+        float lowStep = minValue, highStep = minValue;
+        int lowIndex = 0, highIndex = 0;
+        for (int i = 0; i < actions.length && highStep < value; i++) {
+            lowStep = highStep;
+            lowIndex = i;
+            highStep += step;
+        }
+        highIndex = lowIndex + 1;
+
+        action.setFirstActiveIndex(lowIndex);
+        action.setSecondActiveIndex(highIndex);
+
+        if (highStep == lowStep) {
+            return 0;
+        }
+
+        return (value - lowStep) / (highStep - lowStep);
+    }
+
+    @Override
+    public void setValue(float value) {
+        this.value = value;
+    }
+}

+ 200 - 0
jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java

@@ -0,0 +1,200 @@
+package com.jme3.anim.util;
+
+import com.jme3.anim.*;
+import com.jme3.animation.*;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.*;
+
+import java.util.*;
+
+public class AnimMigrationUtils {
+
+    private static AnimControlVisitor animControlVisitor = new AnimControlVisitor();
+    private static SkeletonControlVisitor skeletonControlVisitor = new SkeletonControlVisitor();
+
+
+    public static Spatial migrate(Spatial source) {
+        Map<Skeleton, Armature> skeletonArmatureMap = new HashMap<>();
+        animControlVisitor.setMappings(skeletonArmatureMap);
+        source.depthFirstTraversal(animControlVisitor);
+        skeletonControlVisitor.setMappings(skeletonArmatureMap);
+        source.depthFirstTraversal(skeletonControlVisitor);
+        return source;
+    }
+
+    private static class AnimControlVisitor implements SceneGraphVisitor {
+
+        Map<Skeleton, Armature> skeletonArmatureMap;
+
+        @Override
+        public void visit(Spatial spatial) {
+            AnimControl control = spatial.getControl(AnimControl.class);
+            if (control != null) {
+                AnimComposer composer = new AnimComposer();
+                Skeleton skeleton = control.getSkeleton();
+                if (skeleton == null) {
+                    //only bone anim for now
+                    return;
+                }
+
+                Joint[] joints = new Joint[skeleton.getBoneCount()];
+                for (int i = 0; i < skeleton.getBoneCount(); i++) {
+                    Bone b = skeleton.getBone(i);
+                    Joint j = joints[i];
+                    if (j == null) {
+                        j = fromBone(b);
+                        joints[i] = j;
+                    }
+                    for (Bone bone : b.getChildren()) {
+                        int index = skeleton.getBoneIndex(bone);
+                        Joint joint = joints[index];
+                        if (joint == null) {
+                            joint = fromBone(bone);
+                        }
+                        j.addChild(joint);
+                        joints[index] = joint;
+                    }
+                }
+
+                Armature armature = new Armature(joints);
+                armature.saveBindPose();
+                skeletonArmatureMap.put(skeleton, armature);
+
+                List<TransformTrack> tracks = new ArrayList<>();
+
+                for (String animName : control.getAnimationNames()) {
+                    tracks.clear();
+                    Animation anim = control.getAnim(animName);
+                    AnimClip clip = new AnimClip(animName);
+                    Joint[] staticJoints = new Joint[joints.length];
+
+                    System.arraycopy(joints, 0, staticJoints, 0, joints.length);
+                    for (Track track : anim.getTracks()) {
+                        if (track instanceof BoneTrack) {
+                            BoneTrack boneTrack = (BoneTrack) track;
+                            int index = boneTrack.getTargetBoneIndex();
+                            Bone bone = skeleton.getBone(index);
+                            Joint joint = joints[index];
+                            TransformTrack jointTrack = fromBoneTrack(boneTrack, bone, joint);
+                            tracks.add(jointTrack);
+                            //this joint is animated let's remove it from the static joints
+                            staticJoints[index] = null;
+                        }
+                        //TODO spatial tracks , Effect tracks, Audio tracks
+                    }
+
+                    for (int i = 0; i < staticJoints.length; i++) {
+                        padJointTracks(tracks, staticJoints[i]);
+                    }
+
+                    clip.setTracks(tracks.toArray(new TransformTrack[tracks.size()]));
+
+                    composer.addAnimClip(clip);
+                }
+                spatial.removeControl(control);
+                spatial.addControl(composer);
+            }
+        }
+
+        public void setMappings(Map<Skeleton, Armature> skeletonArmatureMap) {
+            this.skeletonArmatureMap = skeletonArmatureMap;
+        }
+    }
+
+    public static void padJointTracks(List<TransformTrack> tracks, Joint staticJoint) {
+        Joint j = staticJoint;
+        if (j != null) {
+            // joint has no track , we create one with the default pose
+            float[] times = new float[]{0};
+            Vector3f[] translations = new Vector3f[]{j.getLocalTranslation()};
+            Quaternion[] rotations = new Quaternion[]{j.getLocalRotation()};
+            Vector3f[] scales = new Vector3f[]{j.getLocalScale()};
+            TransformTrack track = new TransformTrack(j, times, translations, rotations, scales);
+            tracks.add(track);
+        }
+    }
+
+    private static class SkeletonControlVisitor implements SceneGraphVisitor {
+
+        Map<Skeleton, Armature> skeletonArmatureMap;
+
+        @Override
+        public void visit(Spatial spatial) {
+            SkeletonControl control = spatial.getControl(SkeletonControl.class);
+            if (control != null) {
+                Armature armature = skeletonArmatureMap.get(control.getSkeleton());
+                SkinningControl skinningControl = new SkinningControl(armature);
+                Map<String, List<Spatial>> attachedSpatials = new HashMap<>();
+                for (int i = 0; i < control.getSkeleton().getBoneCount(); i++) {
+                    Bone b = control.getSkeleton().getBone(i);
+                    Node n = control.getAttachmentsNode(b.getName());
+                    n.removeFromParent();
+                    if (!n.getChildren().isEmpty()) {
+                        attachedSpatials.put(b.getName(), n.getChildren());
+                    }
+                }
+                spatial.removeControl(control);
+                spatial.addControl(skinningControl);
+                for (String name : attachedSpatials.keySet()) {
+                    List<Spatial> spatials = attachedSpatials.get(name);
+                    for (Spatial child : spatials) {
+                        skinningControl.getAttachmentsNode(name).attachChild(child);
+                    }
+                }
+
+            }
+        }
+
+        public void setMappings(Map<Skeleton, Armature> skeletonArmatureMap) {
+            this.skeletonArmatureMap = skeletonArmatureMap;
+        }
+    }
+
+    public static TransformTrack fromBoneTrack(BoneTrack boneTrack, Bone bone, Joint joint) {
+        float[] times = new float[boneTrack.getTimes().length];
+        int length = times.length;
+        System.arraycopy(boneTrack.getTimes(), 0, times, 0, length);
+        //translation
+        Vector3f[] translations = new Vector3f[length];
+        if (boneTrack.getTranslations() != null) {
+            for (int i = 0; i < boneTrack.getTranslations().length; i++) {
+                Vector3f oldTrans = boneTrack.getTranslations()[i];
+                Vector3f newTrans = new Vector3f();
+                newTrans.set(bone.getBindPosition()).addLocal(oldTrans);
+                translations[i] = newTrans;
+            }
+        }
+        //rotation
+        Quaternion[] rotations = new Quaternion[length];
+        if (boneTrack.getRotations() != null) {
+            for (int i = 0; i < boneTrack.getRotations().length; i++) {
+                Quaternion oldRot = boneTrack.getRotations()[i];
+                Quaternion newRot = new Quaternion();
+                newRot.set(bone.getBindRotation()).multLocal(oldRot);
+                rotations[i] = newRot;
+            }
+        }
+        //scale
+        Vector3f[] scales = new Vector3f[length];
+        if (boneTrack.getScales() != null) {
+            for (int i = 0; i < boneTrack.getScales().length; i++) {
+                Vector3f oldScale = boneTrack.getScales()[i];
+                Vector3f newScale = new Vector3f();
+                newScale.set(bone.getBindScale()).multLocal(oldScale);
+                scales[i] = newScale;
+            }
+        }
+        TransformTrack t = new TransformTrack(joint, times, translations, rotations, scales);
+        return t;
+    }
+
+    private static Joint fromBone(Bone b) {
+        Joint j = new Joint(b.getName());
+        j.setLocalTranslation(b.getBindPosition());
+        j.setLocalRotation(b.getBindRotation());
+        j.setLocalScale(b.getBindScale());
+        return j;
+    }
+
+}

+ 10 - 0
jme3-core/src/main/java/com/jme3/anim/util/HasLocalTransform.java

@@ -0,0 +1,10 @@
+package com.jme3.anim.util;
+
+import com.jme3.export.Savable;
+import com.jme3.math.Transform;
+
+public interface HasLocalTransform extends Savable {
+    public void setLocalTransform(Transform transform);
+
+    public Transform getLocalTransform();
+}

+ 20 - 0
jme3-core/src/main/java/com/jme3/anim/util/JointModelTransform.java

@@ -0,0 +1,20 @@
+package com.jme3.anim.util;
+
+import com.jme3.anim.Joint;
+import com.jme3.math.Matrix4f;
+import com.jme3.math.Transform;
+
+/**
+ * Implementations of this interface holds accumulated model transform of a Joint.
+ * Implementation might choose different accumulation strategy.
+ */
+public interface JointModelTransform {
+
+    void updateModelTransform(Transform localTransform, Joint parent);
+
+    void getOffsetTransform(Matrix4f outTransform, Matrix4f inverseModelBindMatrix);
+
+    void applyBindPose(Transform localTransform, Matrix4f inverseModelBindMatrix, Joint parent);
+
+    Transform getModelTransform();
+}

+ 56 - 0
jme3-core/src/main/java/com/jme3/anim/util/Primitives.java

@@ -0,0 +1,56 @@
+package com.jme3.anim.util;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * This is a guava method used in {@link com.jme3.anim.tween.Tweens} class.
+ * Maybe we should just add guava as a dependency in the engine...
+ * //TODO do something about this.
+ */
+public class Primitives {
+
+    /**
+     * A map from primitive types to their corresponding wrapper types.
+     */
+    private static final Map<Class<?>, Class<?>> PRIMITIVE_TO_WRAPPER_TYPE;
+
+    static {
+        Map<Class<?>, Class<?>> primToWrap = new HashMap<>(16);
+
+        primToWrap.put(boolean.class, Boolean.class);
+        primToWrap.put(byte.class, Byte.class);
+        primToWrap.put(char.class, Character.class);
+        primToWrap.put(double.class, Double.class);
+        primToWrap.put(float.class, Float.class);
+        primToWrap.put(int.class, Integer.class);
+        primToWrap.put(long.class, Long.class);
+        primToWrap.put(short.class, Short.class);
+        primToWrap.put(void.class, Void.class);
+
+        PRIMITIVE_TO_WRAPPER_TYPE = Collections.unmodifiableMap(primToWrap);
+    }
+
+    /**
+     * Returns the corresponding wrapper type of {@code type} if it is a primitive type; otherwise
+     * returns {@code type} itself. Idempotent.
+     * <p>
+     * <pre>
+     *     wrap(int.class) == Integer.class
+     *     wrap(Integer.class) == Integer.class
+     *     wrap(String.class) == String.class
+     * </pre>
+     */
+    public static <T> Class<T> wrap(Class<T> type) {
+        if (type == null) {
+            throw new IllegalArgumentException("type is null");
+        }
+
+        // cast is safe: long.class and Long.class are both of type Class<Long>
+        @SuppressWarnings("unchecked")
+        Class<T> wrapped = (Class<T>) PRIMITIVE_TO_WRAPPER_TYPE.get(type);
+        return (wrapped == null) ? type : wrapped;
+    }
+}

+ 11 - 0
jme3-core/src/main/java/com/jme3/anim/util/Weighted.java

@@ -0,0 +1,11 @@
+package com.jme3.anim.util;
+
+import com.jme3.anim.tween.action.Action;
+import com.jme3.math.Transform;
+
+public interface Weighted {
+
+    // public void setWeight(float weight);
+
+    public void setParentAction(Action action);
+}

+ 2 - 0
jme3-core/src/main/java/com/jme3/animation/AnimChannel.java

@@ -33,6 +33,7 @@ package com.jme3.animation;
 
 import com.jme3.math.FastMath;
 import com.jme3.util.TempVars;
+
 import java.util.BitSet;
 
 /**
@@ -46,6 +47,7 @@ import java.util.BitSet;
  * 
  * @author Kirill Vainer
  */
+@Deprecated
 public final class AnimChannel {
 
     private static final float DEFAULT_BLEND_TIME = 0.15f;

+ 2 - 0
jme3-core/src/main/java/com/jme3/animation/AnimControl.java

@@ -64,7 +64,9 @@ import java.util.Map;
  * 1) Morph/Pose animation
  *
  * @author Kirill Vainer
+ * @deprecated use {@link com.jme3.anim.AnimComposer}
  */
+@Deprecated
 public final class AnimControl extends AbstractControl implements Cloneable, JmeCloneable {
 
     /**

+ 1 - 0
jme3-core/src/main/java/com/jme3/animation/AnimEventListener.java

@@ -37,6 +37,7 @@ package com.jme3.animation;
  * 
  * @author Kirill Vainer
  */
+@Deprecated
 public interface AnimEventListener {
 
     /**

+ 3 - 0
jme3-core/src/main/java/com/jme3/animation/Animation.java

@@ -37,13 +37,16 @@ import com.jme3.util.SafeArrayList;
 import com.jme3.util.TempVars;
 import com.jme3.util.clone.Cloner;
 import com.jme3.util.clone.JmeCloneable;
+
 import java.io.IOException;
 
 /**
  * The animation class updates the animation target with the tracks of a given type.
  * 
  * @author Kirill Vainer, Marcin Roguski (Kaelthas)
+ * @deprecated use {@link com.jme3.anim.AnimClip}
  */
+@Deprecated
 public class Animation implements Savable, Cloneable, JmeCloneable {
 
     /** 

+ 3 - 5
jme3-core/src/main/java/com/jme3/animation/AudioTrack.java

@@ -32,15 +32,12 @@
 package com.jme3.animation;
 
 import com.jme3.audio.AudioNode;
-import com.jme3.export.InputCapsule;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.OutputCapsule;
+import com.jme3.export.*;
 import com.jme3.scene.Node;
 import com.jme3.scene.Spatial;
 import com.jme3.util.TempVars;
 import com.jme3.util.clone.Cloner;
-import com.jme3.util.clone.JmeCloneable;
+
 import java.io.IOException;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -62,6 +59,7 @@ import java.util.logging.Logger;
  *
  * @author Nehon
  */
+@Deprecated
 public class AudioTrack implements ClonableTrack {
 
     private static final Logger logger = Logger.getLogger(AudioTrack.class.getName());

+ 13 - 1
jme3-core/src/main/java/com/jme3/animation/Bone.java

@@ -67,7 +67,9 @@ import java.util.ArrayList;
  *
  * @author Kirill Vainer
  * @author Rémy Bouquet
+ * @deprecated use {@link com.jme3.anim.Joint}
  */
+@Deprecated
 public final class Bone implements Savable, JmeCloneable {
 
     // Version #2: Changed naming of transforms as they were misleading
@@ -533,11 +535,21 @@ public final class Bone implements Savable, JmeCloneable {
             attachNode.setLocalRotation(modelRot);
             attachNode.setLocalScale(modelScale);
 
+        } else if (targetGeometry.isIgnoreTransform()) {
+            /*
+             * The animated meshes ignore transforms: match the world transform
+             * of the attachments node to the bone's transform.
+             */
+            attachNode.setLocalTranslation(modelPos);
+            attachNode.setLocalRotation(modelRot);
+            attachNode.setLocalScale(modelScale);
+            attachNode.getLocalTransform().combineWithParent(attachNode.getParent().getWorldTransform().invert());
+
         } else {
             Spatial loopSpatial = targetGeometry;
             Transform combined = new Transform(modelPos, modelRot, modelScale);
             /*
-             * Climb the scene graph applying local transforms until the 
+             * Climb the scene graph applying local transforms until the
              * attachments node's parent is reached.
              */
             while (loopSpatial != attachParent && loopSpatial != null) {

+ 2 - 0
jme3-core/src/main/java/com/jme3/animation/BoneTrack.java

@@ -44,7 +44,9 @@ import java.util.BitSet;
  * Contains a list of transforms and times for each keyframe.
  * 
  * @author Kirill Vainer
+ * @deprecated use {@link com.jme3.anim.JointTrack}
  */
+@Deprecated
 public final class BoneTrack implements JmeCloneable, Track {
 
     /**

+ 1 - 0
jme3-core/src/main/java/com/jme3/animation/ClonableTrack.java

@@ -44,6 +44,7 @@ import com.jme3.util.clone.JmeCloneable;
  *
  * @author Nehon
  */
+@Deprecated
 public interface ClonableTrack extends Track, JmeCloneable {
 
     /**

+ 5 - 1
jme3-core/src/main/java/com/jme3/animation/CompactArray.java

@@ -44,7 +44,7 @@ import java.util.Map;
  */
 public abstract class CompactArray<T> implements JmeCloneable {
 
-    private Map<T, Integer> indexPool = new HashMap<T, Integer>();
+    protected Map<T, Integer> indexPool = new HashMap<T, Integer>();
     protected int[] index;
     protected float[] array;
     private boolean invalid;
@@ -114,6 +114,10 @@ public abstract class CompactArray<T> implements JmeCloneable {
         indexPool.clear();
     }
 
+    protected void setInvalid(boolean invalid) {
+        this.invalid = invalid;
+    }
+
     /**
      * @param index
      * @param value

+ 100 - 0
jme3-core/src/main/java/com/jme3/animation/CompactFloatArray.java

@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2009-2012 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.animation;
+
+import com.jme3.export.*;
+import com.jme3.math.Vector3f;
+
+import java.io.IOException;
+
+/**
+ * Serialize and compress Float by indexing similar values
+ * @author Lim, YongHoon
+ */
+public class CompactFloatArray extends CompactArray<Float> implements Savable {
+
+    /**
+     * Creates a compact vector array
+     */
+    public CompactFloatArray() {
+    }
+
+    /**
+     * creates a compact vector array
+     * @param dataArray the data array
+     * @param index the indices
+     */
+    public CompactFloatArray(float[] dataArray, int[] index) {
+        super(dataArray, index);
+    }
+
+    @Override
+    protected final int getTupleSize() {
+        return 1;
+    }
+
+    @Override
+    protected final Class<Float> getElementClass() {
+        return Float.class;
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        serialize();
+        OutputCapsule out = ex.getCapsule(this);
+        out.write(array, "array", null);
+        out.write(index, "index", null);
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule in = im.getCapsule(this);
+        array = in.readFloatArray("array", null);
+        index = in.readIntArray("index", null);
+    }
+
+    public void fill(int startIndex, float[] store ){
+        for (int i = 0; i < store.length; i++) {
+            store[i] = get(startIndex + i, null);
+        }
+    }
+    
+    @Override
+    protected void serialize(int i, Float data) {
+        array[i] = data;
+    }
+
+    @Override
+    protected Float deserialize(int i, Float store) {
+        return array[i];
+    }
+}

+ 10 - 5
jme3-core/src/main/java/com/jme3/animation/EffectTrack.java

@@ -32,10 +32,7 @@
 package com.jme3.animation;
 
 import com.jme3.effect.ParticleEmitter;
-import com.jme3.export.InputCapsule;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.OutputCapsule;
+import com.jme3.export.*;
 import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.ViewPort;
 import com.jme3.scene.Node;
@@ -67,6 +64,7 @@ import java.util.logging.Logger;
  *
  * @author Nehon
  */
+@Deprecated
 public class EffectTrack implements ClonableTrack {
 
     private static final Logger logger = Logger.getLogger(EffectTrack.class.getName());
@@ -130,15 +128,17 @@ public class EffectTrack implements ClonableTrack {
         @Override
         protected void controlRender(RenderManager rm, ViewPort vp) {
         }
-    };
+    }
 
     //Anim listener that stops the Emmitter when the animation is finished or changed.
     private class OnEndListener implements AnimEventListener {
 
+        @Override
         public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) {
             stop();
         }
 
+        @Override
         public void onAnimChange(AnimControl control, AnimChannel channel, String animName) {
         }
     }
@@ -188,6 +188,7 @@ public class EffectTrack implements ClonableTrack {
      * @see Track#setTime(float, float, com.jme3.animation.AnimControl,
      * com.jme3.animation.AnimChannel, com.jme3.util.TempVars)
      */
+    @Override
     public void setTime(float time, float weight, AnimControl control, AnimChannel channel, TempVars vars) {
 
         if (time >= length) {
@@ -233,6 +234,7 @@ public class EffectTrack implements ClonableTrack {
      *
      * @return length of the track
      */
+    @Override
     public float getLength() {
         return length;
     }
@@ -325,6 +327,7 @@ public class EffectTrack implements ClonableTrack {
         return null;
     }
 
+    @Override
     public void cleanUp() {
         TrackInfo t = (TrackInfo) emitter.getUserData("TrackInfo");
         t.getTracks().remove(this);
@@ -413,6 +416,7 @@ public class EffectTrack implements ClonableTrack {
      * @param ex exporter
      * @throws IOException exception
      */
+    @Override
     public void write(JmeExporter ex) throws IOException {
         OutputCapsule out = ex.getCapsule(this);
         //reset the particle emission rate on the emitter before saving.
@@ -431,6 +435,7 @@ public class EffectTrack implements ClonableTrack {
      * @param im importer
      * @throws IOException Exception
      */
+    @Override
     public void read(JmeImporter im) throws IOException {
         InputCapsule in = im.getCapsule(this);
         this.particlesPerSeconds = in.readFloat("particlesPerSeconds", 0);

+ 1 - 0
jme3-core/src/main/java/com/jme3/animation/LoopMode.java

@@ -35,6 +35,7 @@ package com.jme3.animation;
  * <code>LoopMode</code> determines how animations repeat, or if they
  * do not repeat.
  */
+@Deprecated
 public enum LoopMode {
     /**
      * The animation will play repeatedly, when it reaches the end

+ 2 - 0
jme3-core/src/main/java/com/jme3/animation/Pose.java

@@ -34,12 +34,14 @@ package com.jme3.animation;
 import com.jme3.export.*;
 import com.jme3.math.Vector3f;
 import com.jme3.util.BufferUtils;
+
 import java.io.IOException;
 import java.nio.FloatBuffer;
 
 /**
  * A pose is a list of offsets that say where a mesh vertices should be for this pose.
  */
+@Deprecated
 public final class Pose implements Savable, Cloneable {
 
     private String name;

+ 5 - 1
jme3-core/src/main/java/com/jme3/animation/Skeleton.java

@@ -31,11 +31,13 @@
  */
 package com.jme3.animation;
 
+import com.jme3.anim.Armature;
 import com.jme3.export.*;
 import com.jme3.math.Matrix4f;
 import com.jme3.util.TempVars;
-import com.jme3.util.clone.JmeCloneable;
 import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
+
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -46,7 +48,9 @@ import java.util.List;
  * animated matrixes.
  * 
  * @author Kirill Vainer
+ * @deprecated use {@link Armature}
  */
+@Deprecated
 public final class Skeleton implements Savable, JmeCloneable {
 
     private Bone[] rootBones;

+ 3 - 0
jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java

@@ -31,6 +31,7 @@
  */
 package com.jme3.animation;
 
+import com.jme3.anim.SkinningControl;
 import com.jme3.export.*;
 import com.jme3.material.MatParamOverride;
 import com.jme3.math.FastMath;
@@ -57,7 +58,9 @@ import java.util.logging.Logger;
  * the mesh
  *
  * @author Rémy Bouquet Based on AnimControl by Kirill Vainer
+ * @deprecated use {@link SkinningControl}
  */
+@Deprecated
 public class SkeletonControl extends AbstractControl implements Cloneable, JmeCloneable {
 
     /**

+ 3 - 5
jme3-core/src/main/java/com/jme3/animation/SpatialTrack.java

@@ -31,10 +31,7 @@
  */
 package com.jme3.animation;
 
-import com.jme3.export.InputCapsule;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.OutputCapsule;
+import com.jme3.export.*;
 import com.jme3.math.Quaternion;
 import com.jme3.math.Vector3f;
 import com.jme3.scene.Spatial;
@@ -48,8 +45,9 @@ import java.io.IOException;
  * 
  * @author Marcin Roguski (Kaelthas)
  */
+@Deprecated
 public class SpatialTrack implements JmeCloneable, Track {
-    
+
     /** 
      * Translations of the track. 
      */

+ 1 - 0
jme3-core/src/main/java/com/jme3/animation/Track.java

@@ -34,6 +34,7 @@ package com.jme3.animation;
 import com.jme3.export.Savable;
 import com.jme3.util.TempVars;
 
+@Deprecated
 public interface Track extends Savable, Cloneable {
 
     /**

+ 3 - 5
jme3-core/src/main/java/com/jme3/animation/TrackInfo.java

@@ -31,13 +31,10 @@
  */
 package com.jme3.animation;
 
-import com.jme3.export.InputCapsule;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.OutputCapsule;
-import com.jme3.export.Savable;
+import com.jme3.export.*;
 import com.jme3.util.clone.Cloner;
 import com.jme3.util.clone.JmeCloneable;
+
 import java.io.IOException;
 import java.util.ArrayList;
 
@@ -50,6 +47,7 @@ import java.util.ArrayList;
  *
  * @author Nehon
  */
+@Deprecated
 public class TrackInfo implements Savable, JmeCloneable {
 
     ArrayList<Track> tracks = new ArrayList<Track>();

+ 10 - 0
jme3-core/src/main/java/com/jme3/bounding/Intersection.java

@@ -34,6 +34,7 @@ package com.jme3.bounding;
 import com.jme3.math.FastMath;
 import com.jme3.math.Plane;
 import com.jme3.math.Vector3f;
+import com.jme3.renderer.Camera;
 import com.jme3.util.TempVars;
 import static java.lang.Math.max;
 import static java.lang.Math.min;
@@ -107,6 +108,15 @@ public final class Intersection {
         }
     }
 
+    public static boolean intersect(Camera camera, Vector3f center,float radius){
+        for (int i = 5; i >= 0; i--) {
+            if (camera.getWorldPlane(i).pseudoDistance(center) <= -radius) {
+                return false;
+            }
+        }
+        return true;
+    }
+
 //    private boolean axisTest(float a, float b, float fa, float fb, Vector3f v0, Vector3f v1, )
 //    private boolean axisTestX01(float a, float b, float fa, float fb,
 //                             Vector3f center, Vector3f ext,

+ 5 - 12
jme3-core/src/main/java/com/jme3/cinematic/events/AnimationEvent.java

@@ -31,24 +31,16 @@
  */
 package com.jme3.cinematic.events;
 
-import com.jme3.animation.AnimChannel;
-import com.jme3.animation.AnimControl;
-import com.jme3.animation.LoopMode;
+import com.jme3.animation.*;
 import com.jme3.app.Application;
 import com.jme3.cinematic.Cinematic;
 import com.jme3.cinematic.PlayState;
-import com.jme3.export.InputCapsule;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.OutputCapsule;
+import com.jme3.export.*;
 import com.jme3.scene.Node;
 import com.jme3.scene.Spatial;
-import com.jme3.util.clone.Cloner;
-import com.jme3.util.clone.JmeCloneable;
+
 import java.io.IOException;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.*;
 import java.util.logging.Logger;
 
 /**
@@ -60,6 +52,7 @@ import java.util.logging.Logger;
  *
  * @author Nehon
  */
+@Deprecated
 public class AnimationEvent extends AbstractCinematicEvent {
 
     // Version #2: directly keeping track on the model instead of trying to retrieve 

+ 4 - 9
jme3-core/src/main/java/com/jme3/environment/EnvironmentCamera.java

@@ -38,14 +38,9 @@ import com.jme3.environment.util.EnvMapUtils;
 import com.jme3.light.LightProbe;
 import com.jme3.math.ColorRGBA;
 import com.jme3.math.Vector3f;
-import com.jme3.renderer.Camera;
-import com.jme3.renderer.RenderManager;
-import com.jme3.renderer.ViewPort;
+import com.jme3.renderer.*;
 import com.jme3.scene.Spatial;
-import com.jme3.texture.FrameBuffer;
-import com.jme3.texture.Image;
-import com.jme3.texture.Texture2D;
-import com.jme3.texture.TextureCubeMap;
+import com.jme3.texture.*;
 import com.jme3.texture.image.ColorSpace;
 import com.jme3.util.BufferUtils;
 import com.jme3.util.MipMapGenerator;
@@ -119,7 +114,7 @@ public class EnvironmentCamera extends BaseAppState {
     private final List<SnapshotJob> jobs = new ArrayList<SnapshotJob>();
 
     /**
-     * Creates an EnvironmentCamera with a size of 128
+     * Creates an EnvironmentCamera with a size of 256
      */
     public EnvironmentCamera() {
     }
@@ -322,7 +317,7 @@ public class EnvironmentCamera extends BaseAppState {
         final Camera offCamera = new Camera(mapSize, mapSize);
         offCamera.setLocation(worldPos);
         offCamera.setAxes(axisX, axisY, axisZ);
-        offCamera.setFrustumPerspective(90f, 1f, 1, 1000);
+        offCamera.setFrustumPerspective(90f, 1f, 0.1f, 1000);
         offCamera.setLocation(position);
         return offCamera;
     }

+ 21 - 0
jme3-core/src/main/java/com/jme3/environment/LightProbeFactory.java

@@ -32,6 +32,7 @@
 package com.jme3.environment;
 
 import com.jme3.app.Application;
+import com.jme3.asset.AssetManager;
 import com.jme3.environment.generation.*;
 import com.jme3.environment.util.EnvMapUtils;
 import com.jme3.light.LightProbe;
@@ -204,6 +205,26 @@ public class LightProbeFactory {
         }
     }
 
+    /**
+     * For debuging porpose only
+     * Will return a Node meant to be added to a GUI presenting the 2 cube maps in a cross pattern with all the mip maps.
+     *
+     * @param manager the asset manager
+     * @return a debug node
+     */
+    public static Node getDebugGui(AssetManager manager, LightProbe probe) {
+        if (!probe.isReady()) {
+            throw new UnsupportedOperationException("This EnvProbe is not ready yet, try to test isReady()");
+        }
+
+        Node debugNode = new Node("debug gui probe");
+        Node debugPfemCm = EnvMapUtils.getCubeMapCrossDebugViewWithMipMaps(probe.getPrefilteredEnvMap(), manager);
+        debugNode.attachChild(debugPfemCm);
+        debugPfemCm.setLocalTranslation(520, 0, 0);
+
+        return debugNode;
+    }
+
     /**
      * An inner class to keep the state of a generation process
      */

+ 22 - 8
jme3-core/src/main/java/com/jme3/environment/util/LightsDebugState.java

@@ -34,9 +34,8 @@ package com.jme3.environment.util;
 import com.jme3.app.Application;
 import com.jme3.app.state.BaseAppState;
 import com.jme3.bounding.BoundingSphere;
+import com.jme3.light.*;
 import com.jme3.material.Material;
-import com.jme3.light.LightProbe;
-import com.jme3.light.Light;
 import com.jme3.renderer.RenderManager;
 import com.jme3.scene.Geometry;
 import com.jme3.scene.Node;
@@ -68,7 +67,7 @@ public class LightsDebugState extends BaseAppState {
     @Override
     protected void initialize(Application app) {
         debugNode = new Node("Environment debug Node");
-        Sphere s = new Sphere(16, 16, 1);
+        Sphere s = new Sphere(16, 16, 0.15f);
         debugGeom = new Geometry("debugEnvProbe", s);
         debugMaterial = new Material(app.getAssetManager(), "Common/MatDefs/Misc/reflect.j3md");
         debugGeom.setMaterial(debugMaterial);
@@ -80,6 +79,16 @@ public class LightsDebugState extends BaseAppState {
 
     @Override
     public void update(float tpf) {
+        if(!isEnabled()){
+            return;
+        }
+        updateLights(scene);
+        debugNode.updateLogicalState(tpf);
+        debugNode.updateGeometricState();
+        cleanProbes();
+    }
+
+    public void updateLights(Spatial scene) {
         for (Light light : scene.getWorldLightList()) {
             switch (light.getType()) {
 
@@ -101,16 +110,18 @@ public class LightsDebugState extends BaseAppState {
                         m.setTexture("CubeMap", probe.getPrefilteredEnvMap());
                     }
                     n.setLocalTranslation(probe.getPosition());
-                    n.getChild(1).setLocalScale(((BoundingSphere) probe.getBounds()).getRadius());
+                    n.getChild(1).setLocalScale(probe.getArea().getRadius());
                     break;
                 default:
                     break;
             }
         }
-        debugNode.updateLogicalState(tpf);
-        debugNode.updateGeometricState();
-        cleanProbes();
-
+        if( scene instanceof Node){
+            Node n = (Node)scene;
+            for (Spatial spatial : n.getChildren()) {
+                updateLights(spatial);
+            }
+        }
     }
 
     /**
@@ -138,6 +149,9 @@ public class LightsDebugState extends BaseAppState {
 
     @Override
     public void render(RenderManager rm) {
+        if(!isEnabled()){
+            return;
+        }
         rm.renderScene(debugNode, getApplication().getViewPort());
     }
 

+ 8 - 4
jme3-core/src/main/java/com/jme3/light/DefaultLightFilter.java

@@ -42,11 +42,11 @@ import java.util.HashSet;
 public final class DefaultLightFilter implements LightFilter {
 
     private Camera camera;
-    private final HashSet<Light> processedLights = new HashSet<>();
-    private final LightProbeBlendingStrategy probeBlendStrat;
+    private final HashSet<Light> processedLights = new HashSet<Light>();
+    private LightProbeBlendingStrategy probeBlendStrat;
 
     public DefaultLightFilter() {
-        probeBlendStrat = new BasicProbeBlendingStrategy();
+        probeBlendStrat = new WeightedProbeBlendingStrategy();
     }
 
     public DefaultLightFilter(LightProbeBlendingStrategy probeBlendStrat) {
@@ -114,5 +114,9 @@ public final class DefaultLightFilter implements LightFilter {
             vars.release();
         }
     }
-    
+
+    public void setLightProbeBlendingStrategy(LightProbeBlendingStrategy strategy){
+        probeBlendStrat = strategy;
+    }
+
 }

+ 114 - 63
jme3-core/src/main/java/com/jme3/light/LightProbe.java

@@ -31,24 +31,16 @@
  */
 package com.jme3.light;
 
-import com.jme3.asset.AssetManager;
-import com.jme3.bounding.BoundingBox;
-import com.jme3.bounding.BoundingSphere;
-import com.jme3.bounding.BoundingVolume;
+import com.jme3.bounding.*;
 import com.jme3.environment.EnvironmentCamera;
 import com.jme3.environment.LightProbeFactory;
-import com.jme3.environment.util.EnvMapUtils;
-import com.jme3.export.InputCapsule;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.OutputCapsule;
-import com.jme3.export.Savable;
-import com.jme3.math.Vector3f;
+import com.jme3.export.*;
+import com.jme3.math.*;
 import com.jme3.renderer.Camera;
-import com.jme3.scene.Node;
 import com.jme3.scene.Spatial;
 import com.jme3.texture.TextureCubeMap;
 import com.jme3.util.TempVars;
+
 import java.io.IOException;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -56,7 +48,7 @@ import java.util.logging.Logger;
 /**
  * A LightProbe is not exactly a light. It holds environment map information used for Image Based Lighting.
  * This is used for indirect lighting in the Physically Based Rendering pipeline.
- * 
+ *
  * A light probe has a position in world space. This is the position from where the Environment Map are rendered.
  * There are two environment data structure  held by the LightProbe :
  * - The irradiance spherical harmonics factors (used for indirect diffuse lighting in the PBR pipeline).
@@ -64,10 +56,10 @@ import java.util.logging.Logger;
  * Note that when instantiating the LightProbe, both of those structures are null.
  * To compute them see {@link LightProbeFactory#makeProbe(com.jme3.environment.EnvironmentCamera, com.jme3.scene.Node)}
  * and {@link EnvironmentCamera}.
- * 
- * The light probe has an area of effect that is a bounding volume centered on its position. (for now only Bounding spheres are supported).
- * 
- * A LightProbe will only be taken into account when it's marked as ready. 
+ *
+ * The light probe has an area of effect centered on its position. It can have a Spherical area or an Oriented Box area
+ *
+ * A LightProbe will only be taken into account when it's marked as ready and enabled.
  * A light probe is ready when it has valid environment map data set.
  * Note that you should never call setReady yourself.
  *
@@ -78,20 +70,25 @@ import java.util.logging.Logger;
 public class LightProbe extends Light implements Savable {
 
     private static final Logger logger = Logger.getLogger(LightProbe.class.getName());
+    public static final Matrix4f FALLBACK_MATRIX = new Matrix4f(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1);
 
     private Vector3f[] shCoeffs;
     private TextureCubeMap prefilteredEnvMap;
-    private BoundingVolume bounds = new BoundingSphere(1.0f, Vector3f.ZERO);
+    private ProbeArea area = new SphereProbeArea(Vector3f.ZERO, 1.0f);
     private boolean ready = false;
     private Vector3f position = new Vector3f();
-    private Node debugNode;
     private int nbMipMaps;
 
+    public enum AreaType{
+        Spherical,
+        OrientedBox
+    }
+
     /**
-     * Empty constructor used for serialization. 
+     * Empty constructor used for serialization.
      * You should never call it, use {@link LightProbeFactory#makeProbe(com.jme3.environment.EnvironmentCamera, com.jme3.scene.Node)} instead
      */
-    public LightProbe() {        
+    public LightProbe() {
     }
 
     /**
@@ -104,13 +101,59 @@ public class LightProbe extends Light implements Savable {
     }
 
     /**
-     * Sets the prefiltered environment map 
-     * @param prefileteredEnvMap the prefiltered environment map 
+     * Sets the prefiltered environment map
+     * @param prefileteredEnvMap the prefiltered environment map
      */
     public void setPrefilteredMap(TextureCubeMap prefileteredEnvMap) {
         this.prefilteredEnvMap = prefileteredEnvMap;
     }
 
+    /**
+     * Returns the data to send to the shader.
+     * This is a column major matrix that is not a classic transform matrix, it's laid out in a particular way
+     //   3x3 rot mat|
+     //      0  1  2 |  3
+     // 0 | ax bx cx | px | )
+     // 1 | ay by cy | py | probe position
+     // 2 | az bz cz | pz | )
+     // --|----------|
+     // 3 | sx sy sz   sp | -> 1/probe radius + nbMipMaps
+     //    --scale--
+     * <p>
+     * (ax, ay, az) is the pitch rotation axis
+     * (bx, by, bz) is the yaw rotation axis
+     * (cx, cy, cz) is the roll rotation axis
+     * Like in a standard 3x3 rotation matrix.
+     * It's also the valid rotation matrix of the probe in world space.
+     * Note that for the Spherical Probe area this part is a 3x3 identity matrix.
+     * <p>
+     * (px, py, pz) is the position of the center of the probe in world space
+     * Like in a valid 4x4 transform matrix.
+     * <p>
+     * (sx, sy, sy) is the extent of the probe ( the scale )
+     * In a standard transform matrix the scale is applied to the rotation matrix part.
+     * In the shader we need the rotation and the scale to be separated, doing this avoid to extract
+     * the scale from a classic transform matrix in the shader
+     * <p>
+     * (sp) is a special entry, it contains the packed number of mip maps of the probe and the inverse radius for the probe.
+     * since the inverse radius in lower than 1, it's packed in the decimal part of the float.
+     * The number of mip maps is packed in the integer part of the float.
+     * (ie: for 6 mip maps and a radius of 3, sp= 6.3333333)
+     * <p>
+     * The radius is obvious for a SphereProbeArea,
+     * but in the case of a OrientedBoxProbeArea it's the max of the extent vector's components.
+     */
+    public Matrix4f getUniformMatrix(){
+
+        Matrix4f mat = area.getUniformMatrix();
+
+        // setting the (sp) entry of the matrix
+        mat.m33 = nbMipMaps + 1f / area.getRadius();
+
+        return mat;
+    }
+
+
     @Override
     public void write(JmeExporter ex) throws IOException {
         super.write(ex);
@@ -118,7 +161,7 @@ public class LightProbe extends Light implements Savable {
         oc.write(shCoeffs, "shCoeffs", null);
         oc.write(prefilteredEnvMap, "prefilteredEnvMap", null);
         oc.write(position, "position", null);
-        oc.write(bounds, "bounds", new BoundingSphere(1.0f, Vector3f.ZERO));
+        oc.write(area, "area", new SphereProbeArea(Vector3f.ZERO, 1.0f));
         oc.write(ready, "ready", false);
         oc.write(nbMipMaps, "nbMipMaps", 0);
     }
@@ -127,10 +170,16 @@ public class LightProbe extends Light implements Savable {
     public void read(JmeImporter im) throws IOException {
         super.read(im);
         InputCapsule ic = im.getCapsule(this);
-        
+
         prefilteredEnvMap = (TextureCubeMap) ic.readSavable("prefilteredEnvMap", null);
         position = (Vector3f) ic.readSavable("position", null);
-        bounds = (BoundingVolume) ic.readSavable("bounds", new BoundingSphere(1.0f, Vector3f.ZERO));
+        area = (ProbeArea)ic.readSavable("area", null);
+        if(area == null) {
+            // retro compat
+            BoundingSphere bounds = (BoundingSphere) ic.readSavable("bounds", new BoundingSphere(1.0f, Vector3f.ZERO));
+            area = new SphereProbeArea(bounds.getCenter(), bounds.getRadius());
+        }
+        area.setCenter(position);
         nbMipMaps = ic.readInt("nbMipMaps", 0);
         ready = ic.readBoolean("ready", false);
 
@@ -146,25 +195,49 @@ public class LightProbe extends Light implements Savable {
         }
     }
 
+
     /**
      * returns the bounding volume of this LightProbe
      * @return a bounding volume.
+     * @deprecated use {@link LightProbe#getArea()}
      */
+    @Deprecated
     public BoundingVolume getBounds() {
-        return bounds;
+        return new BoundingSphere(((SphereProbeArea)area).getRadius(), ((SphereProbeArea)area).getCenter());
     }
-    
+
     /**
      * Sets the bounds of this LightProbe
-     * Note that for now only BoundingSphere is supported and this method will 
+     * Note that for now only BoundingSphere is supported and this method will
      * throw an UnsupportedOperationException with any other BoundingVolume type
      * @param bounds the bounds of the LightProbe
+     * @deprecated
      */
+    @Deprecated
     public void setBounds(BoundingVolume bounds) {
-        if( bounds.getType()!= BoundingVolume.Type.Sphere){
-            throw new UnsupportedOperationException("For not only BoundingSphere are suported for LightProbe");
+    }
+
+    public ProbeArea getArea() {
+        return area;
+    }
+
+    public void setAreaType(AreaType type){
+        switch (type){
+            case Spherical:
+                area = new SphereProbeArea(Vector3f.ZERO, 1.0f);
+                break;
+            case OrientedBox:
+                area = new OrientedBoxProbeArea(new Transform());
+                area.setCenter(position);
+                break;
+        }
+    }
+
+    public AreaType getAreaType(){
+        if(area instanceof SphereProbeArea){
+            return AreaType.Spherical;
         }
-        this.bounds = bounds;
+        return AreaType.OrientedBox;
     }
 
     /**
@@ -186,27 +259,6 @@ public class LightProbe extends Light implements Savable {
         this.ready = ready;
     }
 
-    /**
-     * For debuging porpose only
-     * Will return a Node meant to be added to a GUI presenting the 2 cube maps in a cross pattern with all the mip maps.
-     * 
-     * @param manager the asset manager
-     * @return a debug node
-     */
-    public Node getDebugGui(AssetManager manager) {
-        if (!ready) {
-            throw new UnsupportedOperationException("This EnvProbe is not ready yet, try to test isReady()");
-        }
-        if (debugNode == null) {
-            debugNode = new Node("debug gui probe");
-            Node debugPfemCm = EnvMapUtils.getCubeMapCrossDebugViewWithMipMaps(getPrefilteredEnvMap(), manager);
-            debugNode.attachChild(debugPfemCm);
-            debugPfemCm.setLocalTranslation(520, 0, 0);
-        }
-
-        return debugNode;
-    }
-
     public Vector3f[] getShCoeffs() {
         return shCoeffs;
     }
@@ -229,7 +281,7 @@ public class LightProbe extends Light implements Savable {
      */
     public void setPosition(Vector3f position) {
         this.position.set(position);
-        getBounds().setCenter(position);
+        area.setCenter(position);
     }
 
     public int getNbMipMaps() {
@@ -242,12 +294,17 @@ public class LightProbe extends Light implements Savable {
 
     @Override
     public boolean intersectsBox(BoundingBox box, TempVars vars) {
-        return getBounds().intersectsBoundingBox(box);
+        return area.intersectsBox(box, vars);
     }
 
     @Override
     public boolean intersectsFrustum(Camera camera, TempVars vars) {
-        return camera.contains(bounds) != Camera.FrustumIntersect.Outside;
+        return area.intersectsFrustum(camera, vars);
+    }
+
+    @Override
+    public boolean intersectsSphere(BoundingSphere sphere, TempVars vars) {
+        return area.intersectsSphere(sphere, vars);
     }
 
     @Override
@@ -267,14 +324,8 @@ public class LightProbe extends Light implements Savable {
 
     @Override
     public String toString() {
-        return "Light Probe : " + name + " at " + position + " / " + bounds;
+        return "Light Probe : " + name + " at " + position + " / " + area;
     }
 
-    @Override
-    public boolean intersectsSphere(BoundingSphere sphere, TempVars vars) {
-        return getBounds().intersectsSphere(sphere);
-    }
-    
-    
 
 }

+ 0 - 214
jme3-core/src/main/java/com/jme3/light/LightProbeBlendingProcessor.java

@@ -1,214 +0,0 @@
- /*
- * Copyright (c) 2009-2015 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.light;
-
-import com.jme3.bounding.BoundingSphere;
-import com.jme3.post.SceneProcessor;
-import com.jme3.profile.AppProfiler;
-import com.jme3.renderer.RenderManager;
-import com.jme3.renderer.ViewPort;
-import com.jme3.renderer.queue.RenderQueue;
-import com.jme3.scene.Spatial;
-import com.jme3.texture.FrameBuffer;
-import com.jme3.util.TempVars;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * this processor allows to blend several light probes maps together according to a Point of Interest.
- * This is all based on this article by Sebastien lagarde
- * https://seblagarde.wordpress.com/2012/09/29/image-based-lighting-approaches-and-parallax-corrected-cubemap/
- * @author Nehon
- */
-public class LightProbeBlendingProcessor implements SceneProcessor {
-    
-    private ViewPort viewPort;
-    private LightFilter prevFilter;
-    private RenderManager renderManager;
-    private LightProbe probe = new LightProbe();
-    private Spatial poi;
-    private AppProfiler prof;
-
-    public LightProbeBlendingProcessor(Spatial poi) {        
-        this.poi = poi;
-    }
-    
-    @Override
-    public void initialize(RenderManager rm, ViewPort vp) {
-        viewPort = vp;        
-        renderManager = rm;
-        prevFilter = rm.getLightFilter();
-        rm.setLightFilter(new PoiLightProbeLightFilter(this));        
-    }
-
-    @Override
-    public void reshape(ViewPort vp, int w, int h) {
-        
-    }
-
-    @Override
-    public boolean isInitialized() {
-        return viewPort != null;
-    }
-
-    @Override
-    public void preFrame(float tpf) {
-        
-    }
-    
-    /** 1. For POI take a spatial in the constructor and make all calculation against its world pos
-    *      - Alternatively compute an arbitrary POI by casting rays from the camera 
-    *        (one in the center and one for each corner and take the median point)
-    *   2. Take the 4 most weighted probes for default. Maybe allow the user to change this
-    *   3. For the inner influence radius take half of the radius for a start we'll see then how to change this.
-    *   
-    */
-    @Override
-    public void postQueue(RenderQueue rq) {
-        List<BlendFactor> blendFactors = new ArrayList<BlendFactor>();
-        float sumBlendFactors = computeBlendFactors(blendFactors);
-        
-        //Sort blend factors according to their weight
-        Collections.sort(blendFactors);        
-        
-        //normalize blend factors;
-        float normalizer = 1f / sumBlendFactors;
-        for (BlendFactor blendFactor : blendFactors) {
-            blendFactor.ndf *= normalizer;
-           // System.err.println(blendFactor);
-        }
-        
-        
-        //for now just pick the first probe.
-        if(!blendFactors.isEmpty()){
-            probe = blendFactors.get(0).lightProbe;            
-        }else{
-            probe = null;
-        }
-    }
-
-    private float computeBlendFactors(List<BlendFactor> blendFactors) {
-        float sumBlendFactors = 0;
-        for (Spatial scene : viewPort.getScenes()) {
-            for (Light light : scene.getWorldLightList()) {
-                if(light.getType() == Light.Type.Probe){
-                    LightProbe p = (LightProbe)light;
-                    TempVars vars = TempVars.get();
-                    boolean intersect = p.intersectsFrustum(viewPort.getCamera(), vars);
-                    vars.release();
-                    //check if the probe is inside the camera frustum
-                    if(intersect){
-
-                        //is the poi inside the bounds of this probe
-                        if(poi.getWorldBound().intersects(p.getBounds())){
-                            
-                            //computing the distance as we need it to check if th epoi in in the inner radius and later to compute the weight
-                            float outerRadius = ((BoundingSphere)p.getBounds()).getRadius();
-                            float innerRadius = outerRadius * 0.5f;
-                            float distance = p.getBounds().getCenter().distance(poi.getWorldTranslation());
-                            
-                            // if the poi in inside the inner range of this probe, then this probe is the only one that matters.
-                            if( distance < innerRadius ){
-                                blendFactors.clear();
-                                blendFactors.add(new BlendFactor(p, 1.0f));
-                                return 1.0f;
-                            }
-                            //else we need to compute the weight of this probe and collect it for blending
-                            float ndf = (distance - innerRadius) / (outerRadius - innerRadius);
-                            sumBlendFactors += ndf;
-                            blendFactors.add(new BlendFactor(p, ndf));
-                        }
-                    }
-                }
-            }
-        }
-        return sumBlendFactors;
-    }
-
-    @Override
-    public void postFrame(FrameBuffer out) {
-        
-    }
-
-    @Override
-    public void cleanup() {
-        viewPort = null;
-        renderManager.setLightFilter(prevFilter);
-    }
-
-    public void populateProbe(LightList lightList){
-        if(probe != null && probe.isReady()){
-            lightList.add(probe);
-        }
-    }
-
-    public Spatial getPoi() {
-        return poi;
-    }
-
-    public void setPoi(Spatial poi) {
-        this.poi = poi;
-    }
-
-    @Override
-    public void setProfiler(AppProfiler profiler) {
-        this.prof = profiler;
-    }
-
-    private class BlendFactor implements Comparable<BlendFactor>{
-        
-        LightProbe lightProbe;
-        float ndf;       
-
-        public BlendFactor(LightProbe lightProbe, float ndf) {
-            this.lightProbe = lightProbe;
-            this.ndf = ndf;
-        }
-
-        @Override
-        public String toString() {
-            return "BlendFactor{" + "lightProbe=" + lightProbe + ", ndf=" + ndf + '}';
-        }
-        
-        @Override
-        public int compareTo(BlendFactor o) {
-            if(o.ndf > ndf){
-                return -1;
-            }else if(o.ndf < ndf){
-                return 1;
-            }
-            return 0;
-        }
-        
-    }
-}

+ 254 - 0
jme3-core/src/main/java/com/jme3/light/OrientedBoxProbeArea.java

@@ -0,0 +1,254 @@
+package com.jme3.light;
+
+import com.jme3.bounding.BoundingBox;
+import com.jme3.bounding.BoundingSphere;
+import com.jme3.export.*;
+import com.jme3.math.*;
+import com.jme3.renderer.Camera;
+import com.jme3.util.TempVars;
+
+import java.io.IOException;
+
+public class OrientedBoxProbeArea implements ProbeArea {
+    private Transform transform = new Transform();
+
+    /**
+     * @see LightProbe#getUniformMatrix()
+     * for this Area type, the matrix is updated when the probe is transformed,
+     * and its data is used for bound checks in the light culling process.
+     */
+    private Matrix4f uniformMatrix = new Matrix4f();
+
+    public OrientedBoxProbeArea() {
+    }
+
+    public OrientedBoxProbeArea(Transform transform) {
+        transform.set(transform);
+        updateMatrix();
+    }
+
+    @Override
+    public boolean intersectsBox(BoundingBox box, TempVars vars) {
+
+        Vector3f axis1 = getScaledAxis(0, vars.vect1);
+        Vector3f axis2 = getScaledAxis(1, vars.vect2);
+        Vector3f axis3 = getScaledAxis(2, vars.vect3);
+
+        Vector3f tn = vars.vect4;
+        Plane p = vars.plane;
+        Vector3f c = box.getCenter();
+
+        p.setNormal(0, 0, -1);
+        p.setConstant(-(c.z + box.getZExtent()));
+        if (!insidePlane(p, axis1, axis2, axis3, tn)) return false;
+
+        p.setNormal(0, 0, 1);
+        p.setConstant(c.z - box.getZExtent());
+        if (!insidePlane(p, axis1, axis2, axis3, tn)) return false;
+
+
+        p.setNormal(0, -1, 0);
+        p.setConstant(-(c.y + box.getYExtent()));
+        if (!insidePlane(p, axis1, axis2, axis3, tn)) return false;
+
+        p.setNormal(0, 1, 0);
+        p.setConstant(c.y - box.getYExtent());
+        if (!insidePlane(p, axis1, axis2, axis3, tn)) return false;
+
+        p.setNormal(-1, 0, 0);
+        p.setConstant(-(c.x + box.getXExtent()));
+        if (!insidePlane(p, axis1, axis2, axis3, tn)) return false;
+
+        p.setNormal(1, 0, 0);
+        p.setConstant(c.x - box.getXExtent());
+        if (!insidePlane(p, axis1, axis2, axis3, tn)) return false;
+
+        return true;
+
+    }
+
+    @Override
+    public float getRadius() {
+        return Math.max(Math.max(transform.getScale().x, transform.getScale().y), transform.getScale().z);
+    }
+
+    @Override
+    public void setRadius(float radius) {
+        transform.setScale(radius, radius, radius);
+    }
+
+    @Override
+    public boolean intersectsSphere(BoundingSphere sphere, TempVars vars) {
+
+        Vector3f closestPoint = getClosestPoint(vars, sphere.getCenter());
+        // check if the point intersects with the sphere bound
+        if (sphere.intersects(closestPoint)) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean intersectsFrustum(Camera camera, TempVars vars) {
+
+        // extract the scaled axis
+        // this allows a small optimization.
+        Vector3f axis1 = getScaledAxis(0, vars.vect1);
+        Vector3f axis2 = getScaledAxis(1, vars.vect2);
+        Vector3f axis3 = getScaledAxis(2, vars.vect3);
+
+        Vector3f tn = vars.vect4;
+
+        for (int i = 5; i >= 0; i--) {
+            Plane p = camera.getWorldPlane(i);
+            if (!insidePlane(p, axis1, axis2, axis3, tn)) return false;
+        }
+        return true;
+    }
+
+    private Vector3f getScaledAxis(int index, Vector3f store) {
+        Matrix4f u = uniformMatrix;
+        float x = 0, y = 0, z = 0, s = 1;
+        switch (index) {
+            case 0:
+                x = u.m00;
+                y = u.m10;
+                z = u.m20;
+                s = u.m30;
+                break;
+            case 1:
+                x = u.m01;
+                y = u.m11;
+                z = u.m21;
+                s = u.m31;
+                break;
+            case 2:
+                x = u.m02;
+                y = u.m12;
+                z = u.m22;
+                s = u.m32;
+        }
+        return store.set(x, y, z).multLocal(s);
+    }
+
+    private boolean insidePlane(Plane p, Vector3f axis1, Vector3f axis2, Vector3f axis3, Vector3f tn) {
+        // transform the plane normal in the box local space.
+        tn.set(axis1.dot(p.getNormal()), axis2.dot(p.getNormal()), axis3.dot(p.getNormal()));
+
+        // distance check
+        float radius = FastMath.abs(tn.x) +
+                FastMath.abs(tn.y) +
+                FastMath.abs(tn.z);
+
+        float distance = p.pseudoDistance(transform.getTranslation());
+
+        if (distance < -radius) {
+            return false;
+        }
+        return true;
+    }
+
+    private Vector3f getClosestPoint(TempVars vars, Vector3f point) {
+        // non normalized direction
+        Vector3f dir = vars.vect2.set(point).subtractLocal(transform.getTranslation());
+        // initialize the closest point with box center
+        Vector3f closestPoint = vars.vect3.set(transform.getTranslation());
+
+        //store extent in an array
+        float[] r = vars.fWdU;
+        r[0] = transform.getScale().x;
+        r[1] = transform.getScale().y;
+        r[2] = transform.getScale().z;
+
+        // computing closest point to sphere center
+        for (int i = 0; i < 3; i++) {
+            // extract the axis from the 3x3 matrix
+            Vector3f axis = getScaledAxis(i, vars.vect1);
+            // nomalize (here we just divide by the extent
+            axis.divideLocal(r[i]);
+            // distance to the closest point on this axis.
+            float d = FastMath.clamp(dir.dot(axis), -r[i], r[i]);
+            closestPoint.addLocal(vars.vect4.set(axis).multLocal(d));
+        }
+        return closestPoint;
+    }
+
+    private void updateMatrix() {
+        TempVars vars = TempVars.get();
+        Matrix3f r = vars.tempMat3;
+        Matrix4f u = uniformMatrix;
+        transform.getRotation().toRotationMatrix(r);
+
+        u.m00 = r.get(0,0);
+        u.m10 = r.get(1,0);
+        u.m20 = r.get(2,0);
+        u.m01 = r.get(0,1);
+        u.m11 = r.get(1,1);
+        u.m21 = r.get(2,1);
+        u.m02 = r.get(0,2);
+        u.m12 = r.get(1,2);
+        u.m22 = r.get(2,2);
+
+        //scale
+        u.m30 = transform.getScale().x;
+        u.m31 = transform.getScale().y;
+        u.m32 = transform.getScale().z;
+
+        //position
+        u.m03 = transform.getTranslation().x;
+        u.m13 = transform.getTranslation().y;
+        u.m23 = transform.getTranslation().z;
+
+        vars.release();
+    }
+
+    public Matrix4f getUniformMatrix() {
+        return uniformMatrix;
+    }
+
+    public Vector3f getExtent() {
+        return transform.getScale();
+    }
+
+    public void setExtent(Vector3f extent) {
+        transform.setScale(extent);
+        updateMatrix();
+    }
+
+    public Vector3f getCenter() {
+        return transform.getTranslation();
+    }
+
+    public void setCenter(Vector3f center) {
+        transform.setTranslation(center);
+        updateMatrix();
+    }
+
+    public Quaternion getRotation() {
+        return transform.getRotation();
+    }
+
+    public void setRotation(Quaternion rotation) {
+        transform.setRotation(rotation);
+        updateMatrix();
+    }
+
+    @Override
+    protected OrientedBoxProbeArea clone() throws CloneNotSupportedException {
+        return new OrientedBoxProbeArea(transform);
+    }
+
+    @Override
+    public void write(JmeExporter e) throws IOException {
+        OutputCapsule oc = e.getCapsule(this);
+        oc.write(transform, "transform", new Transform());
+    }
+
+    @Override
+    public void read(JmeImporter i) throws IOException {
+        InputCapsule ic = i.getCapsule(this);
+        transform = (Transform) ic.readSavable("transform", new Transform());
+        updateMatrix();
+    }
+
+}

+ 0 - 107
jme3-core/src/main/java/com/jme3/light/PoiLightProbeLightFilter.java

@@ -1,107 +0,0 @@
-/*
- * Copyright (c) 2009-2015 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.light;
-
-import com.jme3.bounding.BoundingBox;
-import com.jme3.bounding.BoundingSphere;
-import com.jme3.bounding.BoundingVolume;
-import com.jme3.renderer.Camera;
-import com.jme3.scene.Geometry;
-import com.jme3.util.TempVars;
-import java.util.HashSet;
-
-public final class PoiLightProbeLightFilter implements LightFilter {
-
-    private Camera camera;
-    private final HashSet<Light> processedLights = new HashSet<Light>();
-    private final LightProbeBlendingProcessor processor;
-
-    public PoiLightProbeLightFilter(LightProbeBlendingProcessor processor) {
-        this.processor = processor;
-    }
-
-    @Override
-    public void setCamera(Camera camera) {
-        this.camera = camera;
-        for (Light light : processedLights) {
-            light.frustumCheckNeeded = true;
-        }
-    }
-
-    @Override
-    public void filterLights(Geometry geometry, LightList filteredLightList) {
-        TempVars vars = TempVars.get();
-        try {
-            LightList worldLights = geometry.getWorldLightList();
-
-            for (int i = 0; i < worldLights.size(); i++) {
-                Light light = worldLights.get(i);
-
-                if (light.getType() == Light.Type.Probe) {
-                    continue;
-                }
-
-                if (light.frustumCheckNeeded) {
-                    processedLights.add(light);
-                    light.frustumCheckNeeded = false;
-                    light.intersectsFrustum = light.intersectsFrustum(camera, vars);
-                }
-
-                if (!light.intersectsFrustum) {
-                    continue;
-                }
-
-                BoundingVolume bv = geometry.getWorldBound();
-
-                if (bv instanceof BoundingBox) {
-                    if (!light.intersectsBox((BoundingBox) bv, vars)) {
-                        continue;
-                    }
-                } else if (bv instanceof BoundingSphere) {
-                    if (!Float.isInfinite(((BoundingSphere) bv).getRadius())) {
-                        if (!light.intersectsSphere((BoundingSphere) bv, vars)) {
-                            continue;
-                        }
-                    }
-                }
-                
-                filteredLightList.add(light);
-            }
-
-            processor.populateProbe(filteredLightList);           
-
-        } finally {
-            vars.release();
-        }
-    }
-
-}

+ 1 - 6
jme3-core/src/main/java/com/jme3/light/PointLight.java

@@ -212,12 +212,7 @@ public class PointLight extends Light {
         if (this.radius == 0) {
             return true;
         } else {
-            for (int i = 5; i >= 0; i--) {
-                if (camera.getWorldPlane(i).pseudoDistance(position) <= -radius) {
-                    return false;
-                }
-            }
-            return true;
+            return Intersection.intersect(camera, position, radius);
         }
     }
     

+ 35 - 0
jme3-core/src/main/java/com/jme3/light/ProbeArea.java

@@ -0,0 +1,35 @@
+package com.jme3.light;
+
+import com.jme3.bounding.BoundingBox;
+import com.jme3.bounding.BoundingSphere;
+import com.jme3.export.Savable;
+import com.jme3.math.Matrix4f;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.Camera;
+import com.jme3.util.TempVars;
+
+public interface ProbeArea extends Savable, Cloneable{
+
+    public void setCenter(Vector3f center);
+
+    public float getRadius();
+
+    public void setRadius(float radius);
+
+    public Matrix4f getUniformMatrix();
+
+    /**
+     * @see Light#intersectsBox(BoundingBox, TempVars)
+     */
+    public boolean intersectsBox(BoundingBox box, TempVars vars);
+
+    /**
+     * @see Light#intersectsSphere(BoundingSphere, TempVars)
+     */
+    public boolean intersectsSphere(BoundingSphere sphere, TempVars vars);
+
+    /**
+     * @see Light#intersectsFrustum(Camera, TempVars)
+     */
+    public abstract boolean intersectsFrustum(Camera camera, TempVars vars);
+}

+ 103 - 0
jme3-core/src/main/java/com/jme3/light/SphereProbeArea.java

@@ -0,0 +1,103 @@
+package com.jme3.light;
+
+import com.jme3.bounding.*;
+import com.jme3.export.*;
+import com.jme3.math.Matrix4f;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.Camera;
+import com.jme3.util.TempVars;
+
+import java.io.IOException;
+import java.util.logging.Level;
+
+public class SphereProbeArea implements ProbeArea {
+
+    private Vector3f center = new Vector3f();
+    private float radius = 1;
+    private Matrix4f uniformMatrix = new Matrix4f();
+
+    public SphereProbeArea() {
+    }
+
+    public SphereProbeArea(Vector3f center, float radius) {
+        this.center.set(center);
+        this.radius = radius;
+        updateMatrix();
+    }
+
+    public Vector3f getCenter() {
+        return center;
+    }
+
+    public void setCenter(Vector3f center) {
+        this.center.set(center);
+        updateMatrix();
+    }
+
+    public float getRadius() {
+        return radius;
+    }
+
+    @Override
+    public void setRadius(float radius) {
+        this.radius = radius;
+        updateMatrix();
+    }
+
+    @Override
+    public Matrix4f getUniformMatrix() {
+        return uniformMatrix;
+    }
+
+    private void updateMatrix(){
+        //position
+        uniformMatrix.m03 = center.x;
+        uniformMatrix.m13 = center.y;
+        uniformMatrix.m23 = center.z;
+
+    }
+
+    @Override
+    public boolean intersectsBox(BoundingBox box, TempVars vars) {
+        return Intersection.intersect(box, center, radius);
+    }
+
+    @Override
+    public boolean intersectsSphere(BoundingSphere sphere, TempVars vars) {
+        return Intersection.intersect(sphere, center, radius);
+    }
+
+    @Override
+    public boolean intersectsFrustum(Camera camera, TempVars vars) {
+        return Intersection.intersect(camera, center, radius);
+    }
+
+    @Override
+    public String toString() {
+        return "SphereProbeArea{" +
+                "center=" + center +
+                ", radius=" + radius +
+                '}';
+    }
+
+    @Override
+    protected SphereProbeArea clone() throws CloneNotSupportedException {
+        return new SphereProbeArea(center, radius);
+    }
+
+    @Override
+    public void write(JmeExporter e) throws IOException {
+        OutputCapsule oc = e.getCapsule(this);
+        oc.write(center, "center", new Vector3f());
+        oc.write(radius, "radius", 1);
+    }
+
+    @Override
+    public void read(JmeImporter i) throws IOException {
+        InputCapsule ic = i.getCapsule(this);
+        center = (Vector3f) ic.readSavable("center", new Vector3f());
+        radius = ic.readFloat("radius", 1);
+        updateMatrix();
+    }
+
+}

+ 77 - 0
jme3-core/src/main/java/com/jme3/light/WeightedProbeBlendingStrategy.java

@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2009-2015 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.light;
+
+import com.jme3.scene.Geometry;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This strategy returns the 3 closest probe from the rendered object.
+ * <p>
+ * Image based lighting will be blended between those probes in the shader according to their distance and range.
+ *
+ * @author Nehon
+ */
+public class WeightedProbeBlendingStrategy implements LightProbeBlendingStrategy {
+
+    private final static int MAX_PROBES = 3;
+    List<LightProbe> lightProbes = new ArrayList<LightProbe>();
+
+    @Override
+    public void registerProbe(LightProbe probe) {
+        lightProbes.add(probe);
+    }
+
+    @Override
+    public void populateProbes(Geometry g, LightList lightList) {
+        if (!lightProbes.isEmpty()) {
+            //The 3 first probes are the closest to the geometry since the
+            //light list is sorted according to the distance to the geom.
+            int addedProbes = 0;
+            for (LightProbe p : lightProbes) {
+                if (p.isReady() && p.isEnabled()) {
+                    lightList.add(p);
+                    addedProbes ++;
+                }
+                if (addedProbes == MAX_PROBES) {
+                    break;
+                }
+            }
+
+            //clearing the list for next pass.
+            lightProbes.clear();
+        }
+    }
+
+}

+ 7 - 0
jme3-core/src/main/java/com/jme3/material/MatParamOverride.java

@@ -140,6 +140,9 @@ public final class MatParamOverride extends MatParam {
         super.write(ex);
         OutputCapsule oc = ex.getCapsule(this);
         oc.write(enabled, "enabled", true);
+        if (value == null) {
+            oc.write(true, "isNull", false);
+        }
     }
 
     @Override
@@ -147,5 +150,9 @@ public final class MatParamOverride extends MatParam {
         super.read(im);
         InputCapsule ic = im.getCapsule(this);
         enabled = ic.readBoolean("enabled", true);
+        boolean isNull = ic.readBoolean("isNull", false);
+        if (isNull) {
+            setValue(null);
+        }
     }
 }

+ 67 - 16
jme3-core/src/main/java/com/jme3/material/Material.java

@@ -46,10 +46,7 @@ import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.Renderer;
 import com.jme3.renderer.queue.RenderQueue.Bucket;
 import com.jme3.scene.Geometry;
-import com.jme3.shader.Shader;
-import com.jme3.shader.Uniform;
-import com.jme3.shader.UniformBindingManager;
-import com.jme3.shader.VarType;
+import com.jme3.shader.*;
 import com.jme3.texture.Image;
 import com.jme3.texture.Texture;
 import com.jme3.texture.image.ColorSpace;
@@ -414,6 +411,17 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         return paramValues.get(name);
     }
 
+    /**
+     * Returns the current parameter's value.
+     *
+     * @param name the parameter name to look up.
+     * @return current value or null if the parameter wasn't set.
+     */
+    public <T> T getParamValue(final String name) {
+        final MatParam param = paramValues.get(name);
+        return param == null ? null : (T) param.getValue();
+    }
+
     /**
      * Returns the texture parameter set on this material with the given name,
      * returns <code>null</code> if the parameter is not set.
@@ -662,6 +670,28 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         setParam(name, VarType.Vector4, value);
     }
 
+    /**
+     * Pass an uniform buffer object to the material shader.
+     *
+     * @param name  the name of the buffer object defined in the material definition (j3md).
+     * @param value the buffer object.
+     */
+    public void setUniformBufferObject(final String name, final BufferObject value) {
+        value.setBufferType(BufferObject.BufferType.UniformBufferObject);
+        setParam(name, VarType.BufferObject, value);
+    }
+
+    /**
+     * Pass a shader storage buffer object to the material shader.
+     *
+     * @param name  the name of the buffer object defined in the material definition (j3md).
+     * @param value the buffer object.
+     */
+    public void setShaderStorageBufferObject(final String name, final BufferObject value) {
+        value.setBufferType(BufferObject.BufferType.ShaderStorageBufferObject);
+        setParam(name, VarType.BufferObject, value);
+    }
+
     /**
      * Pass a Vector2f to the material shader.
      *
@@ -796,20 +826,29 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         }
 
         for (int i = 0; i < paramValues.size(); i++) {
+
             MatParam param = paramValues.getValue(i);
             VarType type = param.getVarType();
-            Uniform uniform = shader.getUniform(param.getPrefixedName());
 
-            if (uniform.isSetByCurrentMaterial()) {
-                continue;
-            }
+            if (isBO(type)) {
+
+                final ShaderBufferBlock bufferBlock = shader.getBufferBlock(param.getPrefixedName());
+                bufferBlock.setBufferObject((BufferObject) param.getValue());
 
-            if (type.isTextureType()) {
-                renderer.setTexture(unit, (Texture) param.getValue());
-                uniform.setValue(VarType.Int, unit);
-                unit++;
             } else {
-                uniform.setValue(type, param.getValue());
+
+                Uniform uniform = shader.getUniform(param.getPrefixedName());
+                if (uniform.isSetByCurrentMaterial()) {
+                    continue;
+                }
+
+                if (type.isTextureType()) {
+                    renderer.setTexture(unit, (Texture) param.getValue());
+                    uniform.setValue(VarType.Int, unit);
+                    unit++;
+                } else {
+                    uniform.setValue(type, param.getValue());
+                }
             }
         }
 
@@ -817,6 +856,16 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         return unit;
     }
 
+    /**
+     * Returns true if the type is Buffer Object's type.
+     *
+     * @param type the material parameter type.
+     * @return true if the type is Buffer Object's type.
+     */
+    private boolean isBO(final VarType type) {
+        return type == VarType.BufferObject;
+    }
+
     private void updateRenderState(RenderManager renderManager, Renderer renderer, TechniqueDef techniqueDef) {
         if (renderManager.getForcedRenderState() != null) {
             if (techniqueDef.getForcedRenderState() != null) {
@@ -845,7 +894,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
      *
      * @param renderManager The render manager to preload for
      */
-    public void preload(RenderManager renderManager) {
+    public void preload(RenderManager renderManager, Geometry geometry) {
         if (technique == null) {
             selectTechnique(TechniqueDef.DEFAULT_TECHNIQUE_NAME, renderManager);
         }
@@ -856,9 +905,11 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         if (techniqueDef.isNoRender()) {
             return;
         }
+        // Get world overrides
+        SafeArrayList<MatParamOverride> overrides = geometry.getWorldMatParamOverrides();
 
-        Shader shader = technique.makeCurrent(renderManager, null, null, null, rendererCaps);
-        updateShaderMaterialParameters(renderer, shader, null, null);
+        Shader shader = technique.makeCurrent(renderManager, geometry, overrides,  renderManager.getForcedMatParams(), rendererCaps);
+        updateShaderMaterialParameters(renderer, shader, overrides,  renderManager.getForcedMatParams());
         renderManager.getRenderer().setShader(shader);
     }
 

+ 81 - 102
jme3-core/src/main/java/com/jme3/material/RenderState.java

@@ -331,9 +331,17 @@ public class RenderState implements Cloneable, Savable {
          */
         Exclusion,
         /**
-         * Allows for custom blending by using glBlendFuncSeparate.
+         * Uses the blend equations and blend factors defined by the render state.
          * <p>
-         * 
+         * These attributes can be set by using the following methods:
+         * <ul>
+         * <li>{@link RenderState#setBlendEquation(BlendEquation)}<br/>
+         * <li>{@link RenderState#setBlendEquationAlpha(BlendEquationAlpha)}<br/>
+         * <li>{@link RenderState#setCustomBlendFactors(BlendFunc, BlendFunc, BlendFunc, BlendFunc)}<br/>
+         * </ul>
+         * <p>
+         * Result.RGB = BlendEquation( sfactorRGB * Source.RGB , dfactorRGB * Destination.RGB )<br/>
+         * Result.A = BlendEquationAlpha( sfactorAlpha * Source.A , dfactorAlpha * Destination.A )
          */
         Custom
     }
@@ -425,8 +433,6 @@ public class RenderState implements Cloneable, Savable {
         ADDITIONAL.applyDepthWrite = false;
         ADDITIONAL.applyDepthTest = false;
         ADDITIONAL.applyColorWrite = false;
-        ADDITIONAL.applyBlendEquation = false;
-        ADDITIONAL.applyBlendEquationAlpha = false;
         ADDITIONAL.applyBlendMode = false;
         ADDITIONAL.applyPolyOffset = false;
         ADDITIONAL.applyStencilTest = false;
@@ -444,9 +450,7 @@ public class RenderState implements Cloneable, Savable {
     boolean colorWrite = true;
     boolean applyColorWrite = true;
     BlendEquation blendEquation = BlendEquation.Add;
-    boolean applyBlendEquation = true;
     BlendEquationAlpha blendEquationAlpha = BlendEquationAlpha.InheritColor;
-    boolean applyBlendEquationAlpha = true;
     BlendMode blendMode = BlendMode.Off;
     boolean applyBlendMode = true;
     float offsetFactor = 0;
@@ -469,10 +473,10 @@ public class RenderState implements Cloneable, Savable {
     TestFunction frontStencilFunction = TestFunction.Always;
     TestFunction backStencilFunction = TestFunction.Always;
     int cachedHashCode = -1;
-    BlendFunc sfactorRGB=BlendFunc.One;
-    BlendFunc dfactorRGB=BlendFunc.Zero;
-    BlendFunc sfactorAlpha=BlendFunc.One;
-    BlendFunc dfactorAlpha=BlendFunc.Zero;
+    BlendFunc sfactorRGB = BlendFunc.One;
+    BlendFunc dfactorRGB = BlendFunc.One;
+    BlendFunc sfactorAlpha = BlendFunc.One;
+    BlendFunc dfactorAlpha = BlendFunc.One;
             
     public void write(JmeExporter ex) throws IOException {
         OutputCapsule oc = ex.getCapsule(this);
@@ -510,8 +514,6 @@ public class RenderState implements Cloneable, Savable {
         oc.write(applyDepthWrite, "applyDepthWrite", true);
         oc.write(applyDepthTest, "applyDepthTest", true);
         oc.write(applyColorWrite, "applyColorWrite", true);
-        oc.write(applyBlendEquation, "applyBlendEquation", true);
-        oc.write(applyBlendEquationAlpha, "applyBlendEquationAlpha", true);
         oc.write(applyBlendMode, "applyBlendMode", true);
         oc.write(applyPolyOffset, "applyPolyOffset", true);
         oc.write(applyDepthFunc, "applyDepthFunc", true);
@@ -544,9 +546,9 @@ public class RenderState implements Cloneable, Savable {
         depthFunc = ic.readEnum("depthFunc", TestFunction.class, TestFunction.LessOrEqual);
         lineWidth = ic.readFloat("lineWidth", 1);
         sfactorRGB = ic.readEnum("sfactorRGB", BlendFunc.class, BlendFunc.One);
-        dfactorAlpha = ic.readEnum("dfactorRGB", BlendFunc.class, BlendFunc.Zero);
+        dfactorAlpha = ic.readEnum("dfactorRGB", BlendFunc.class, BlendFunc.One);
         sfactorRGB = ic.readEnum("sfactorAlpha", BlendFunc.class, BlendFunc.One);
-        dfactorAlpha = ic.readEnum("dfactorAlpha", BlendFunc.class, BlendFunc.Zero);
+        dfactorAlpha = ic.readEnum("dfactorAlpha", BlendFunc.class, BlendFunc.One);
 
 
         applyWireFrame = ic.readBoolean("applyWireFrame", true);
@@ -554,14 +556,11 @@ public class RenderState implements Cloneable, Savable {
         applyDepthWrite = ic.readBoolean("applyDepthWrite", true);
         applyDepthTest = ic.readBoolean("applyDepthTest", true);
         applyColorWrite = ic.readBoolean("applyColorWrite", true);
-        applyBlendEquation = ic.readBoolean("applyBlendEquation", true);
-        applyBlendEquationAlpha = ic.readBoolean("applyBlendEquationAlpha", true);
         applyBlendMode = ic.readBoolean("applyBlendMode", true);
         applyPolyOffset = ic.readBoolean("applyPolyOffset", true);
         applyDepthFunc = ic.readBoolean("applyDepthFunc", true);
         applyLineWidth = ic.readBoolean("applyLineWidth", true);
 
-        
     }
 
     /**
@@ -618,19 +617,32 @@ public class RenderState implements Cloneable, Savable {
             return false;
         }
 
-        if (blendEquation != rs.blendEquation) {
+        if (blendMode != rs.blendMode) {
             return false;
         }
 
-        if (blendEquationAlpha != rs.blendEquationAlpha) {
-            return false;
-        }
+        if (blendMode == BlendMode.Custom) {
+            if (blendEquation != rs.blendEquation) {
+                return false;
+            }
+            if (blendEquationAlpha != rs.blendEquationAlpha) {
+                return false;
+            }
 
-        if (blendMode != rs.blendMode) {
-            return false;
+            if (sfactorRGB != rs.sfactorRGB) {
+                return false;
+            }
+            if (dfactorRGB != rs.dfactorRGB) {
+                return false;
+            }
+            if (sfactorAlpha != rs.sfactorAlpha) {
+                return false;
+            }
+            if (dfactorAlpha != rs.dfactorAlpha) {
+                return false;
+            }
         }
 
-
         if (offsetEnabled != rs.offsetEnabled) {
             return false;
         }
@@ -678,14 +690,6 @@ public class RenderState implements Cloneable, Savable {
         if(lineWidth != rs.lineWidth){
             return false;
         }
-        
-        if (blendMode.equals(BlendMode.Custom)) {
-           return sfactorRGB==rs.getCustomSfactorRGB()
-               && dfactorRGB==rs.getCustomDfactorRGB()
-               && sfactorAlpha==rs.getCustomSfactorAlpha()
-               && dfactorAlpha==rs.getCustomDfactorAlpha();
-           
-        }
 
         return true;
     }
@@ -771,80 +775,68 @@ public class RenderState implements Cloneable, Savable {
     }
 
     /**
-     * Set the blending equation.
+     * Set the blending equation for the color component (RGB).
      * <p>
-     * When blending is enabled, (<code>blendMode</code> is not
-     * {@link BlendMode#Off}) the input pixel will be blended with the pixel
-     * already in the color buffer. The blending equation is determined by the
-     * {@link BlendEquation}. For example, the mode {@link BlendMode#Additive}
-     * and {@link BlendEquation#Add} will add the input pixel's color to the
-     * color already in the color buffer:
+     * The blending equation determines, how the RGB values of the input pixel
+     * will be blended with the RGB values of the pixel already in the color buffer.<br/>
+     * For example, {@link BlendEquation#Add} will add the input pixel's color
+     * to the color already in the color buffer:
      * <br/>
      * <code>Result = Source Color + Destination Color</code>
-     * <br/>
-     * However, the mode {@link BlendMode#Additive}
-     * and {@link BlendEquation#Subtract} will subtract the input pixel's color to the
-     * color already in the color buffer:
-     * <br/>
-     * <code>Result = Source Color - Destination Color</code>
+     * <p>
+     * <b>Note:</b> This gets only used in {@link BlendMode#Custom} mode.
+     * All other blend modes will ignore this setting.
      *
-     * @param blendEquation The blend equation to use. 
+     * @param blendEquation The {@link BlendEquation} to use.
      */
     public void setBlendEquation(BlendEquation blendEquation) {
-        applyBlendEquation = true;
         this.blendEquation = blendEquation;
         cachedHashCode = -1;
     }
-    
+
     /**
      * Set the blending equation for the alpha component.
      * <p>
-     * When blending is enabled, (<code>blendMode</code> is not
-     * {@link BlendMode#Off}) the input pixel will be blended with the pixel
-     * already in the color buffer. The blending equation is determined by the
-     * {@link BlendEquation} and can be overrode for the alpha component using
-     * the {@link BlendEquationAlpha} . For example, the mode
-     * {@link BlendMode#Additive} and {@link BlendEquationAlpha#Add} will add
-     * the input pixel's alpha to the alpha component already in the color
-     * buffer:
+     * The alpha blending equation determines, how the alpha values of the input pixel
+     * will be blended with the alpha values of the pixel already in the color buffer.<br/>
+     * For example, {@link BlendEquationAlpha#Add} will add the input pixel's color
+     * to the color already in the color buffer:
      * <br/>
-     * <code>Result = Source Alpha + Destination Alpha</code>
-     * <br/>
-     * However, the mode {@link BlendMode#Additive} and
-     * {@link BlendEquationAlpha#Subtract} will subtract the input pixel's alpha
-     * to the alpha component already in the color buffer:
-     * <br/>
-     * <code>Result = Source Alpha - Destination Alpha</code>
+     * <code>Result = Source Color + Destination Color</code>
+     * <p>
+     * <b>Note:</b> This gets only used in {@link BlendMode#Custom} mode.
+     * All other blend modes will ignore this setting.
      *
-     * @param blendEquationAlpha The blend equation to use for the alpha
-     *                           component.
+     * @param blendEquationAlpha The {@link BlendEquationAlpha} to use.
      */
     public void setBlendEquationAlpha(BlendEquationAlpha blendEquationAlpha) {
-        applyBlendEquationAlpha = true;
         this.blendEquationAlpha = blendEquationAlpha;
         cachedHashCode = -1;
     }
 
-    
     /**
-     * Sets the custom blend factors for <code>BlendMode.Custom</code> as 
-     * defined by the appropriate <code>BlendFunc</code>.
-     * 
+     * Sets the blend factors used for the source and destination color.
+     * <p>
+     * These factors will be multiplied with the color values of the input pixel
+     * and the pixel already in the color buffer, before both colors gets combined by the {@link BlendEquation}.
+     * <p>
+     * <b>Note:</b> This gets only used in {@link BlendMode#Custom} mode.
+     * All other blend modes will ignore this setting.
+     *
      * @param sfactorRGB   The source blend factor for RGB components.
      * @param dfactorRGB   The destination blend factor for RGB components.
      * @param sfactorAlpha The source blend factor for the alpha component.
      * @param dfactorAlpha The destination blend factor for the alpha component.
      */
-    public void setCustomBlendFactors(BlendFunc sfactorRGB, BlendFunc dfactorRGB, BlendFunc sfactorAlpha, BlendFunc dfactorAlpha)
-    {
+    public void setCustomBlendFactors(BlendFunc sfactorRGB, BlendFunc dfactorRGB, BlendFunc sfactorAlpha, BlendFunc dfactorAlpha) {
        this.sfactorRGB = sfactorRGB;
        this.dfactorRGB = dfactorRGB;
        this.sfactorAlpha = sfactorAlpha;
        this.dfactorAlpha = dfactorAlpha;
        cachedHashCode = -1;
     }
-    
-    
+
+
     /**
      * Enable depth testing.
      *
@@ -1377,14 +1369,6 @@ public class RenderState implements Cloneable, Savable {
         return applyBlendMode;
     }
 
-    public boolean isApplyBlendEquation() {
-        return applyBlendEquation;
-    }
-
-    public boolean isApplyBlendEquationAlpha() {
-        return applyBlendEquationAlpha;
-    }
-
     public boolean isApplyColorWrite() {
         return applyColorWrite;
     }
@@ -1514,27 +1498,26 @@ public class RenderState implements Cloneable, Savable {
         } else {
             state.colorWrite = colorWrite;
         }
-        if (additionalState.applyBlendEquation) {
-            state.blendEquation = additionalState.blendEquation;
-        } else {
-            state.blendEquation = blendEquation;
-        }
-        if (additionalState.applyBlendEquationAlpha) {
-            state.blendEquationAlpha = additionalState.blendEquationAlpha;
-        } else {
-            state.blendEquationAlpha = blendEquationAlpha;
-        }        
         if (additionalState.applyBlendMode) {
             state.blendMode = additionalState.blendMode;
-            if (additionalState.getBlendMode().equals(BlendMode.Custom)) {
-               state.setCustomBlendFactors(
-                additionalState.getCustomSfactorRGB(),
-                additionalState.getCustomDfactorRGB(),
-                additionalState.getCustomSfactorAlpha(),
-                additionalState.getCustomDfactorAlpha());
+            if (additionalState.blendMode == BlendMode.Custom) {
+                state.blendEquation = additionalState.blendEquation;
+                state.blendEquationAlpha = additionalState.blendEquationAlpha;
+                state.sfactorRGB = additionalState.sfactorRGB;
+                state.dfactorRGB = additionalState.dfactorRGB;
+                state.sfactorAlpha = additionalState.sfactorAlpha;
+                state.dfactorAlpha = additionalState.dfactorAlpha;
             }
         } else {
             state.blendMode = blendMode;
+            if (blendMode == BlendMode.Custom) {
+                state.blendEquation = blendEquation;
+                state.blendEquationAlpha = blendEquationAlpha;
+                state.sfactorRGB = sfactorRGB;
+                state.dfactorRGB = dfactorRGB;
+                state.sfactorAlpha = sfactorAlpha;
+                state.dfactorAlpha = dfactorAlpha;
+            }
         }
 
         if (additionalState.applyPolyOffset) {
@@ -1611,8 +1594,6 @@ public class RenderState implements Cloneable, Savable {
         applyDepthWrite =  true;
         applyDepthTest =  true;
         applyColorWrite = true;
-        applyBlendEquation =  true;
-        applyBlendEquationAlpha =  true;
         applyBlendMode = true;
         applyPolyOffset =  true;
         applyDepthFunc = true;
@@ -1639,8 +1620,6 @@ public class RenderState implements Cloneable, Savable {
                 + "\ncolorWrite=" + colorWrite
                 + "\napplyColorWrite=" + applyColorWrite
                 + "\nblendEquation=" + blendEquation
-                + "\napplyBlendEquation=" + applyBlendEquation
-                + "\napplyBlendEquationAlpha=" + applyBlendEquationAlpha
                 + "\nblendMode=" + blendMode
                 + "\napplyBlendMode=" + applyBlendMode
                 + "\noffsetEnabled=" + offsetEnabled

+ 1 - 3
jme3-core/src/main/java/com/jme3/material/logic/MultiPassLightingLogic.java

@@ -82,7 +82,7 @@ public final class MultiPassLightingLogic extends DefaultTechniqueDefLogic {
 
         for (int i = 0; i < lights.size(); i++) {
             Light l = lights.get(i);
-            if (l instanceof AmbientLight) {
+            if (l.getType() == Light.Type.Ambient  || l.getType() == Light.Type.Probe) {
                 continue;
             }
 
@@ -152,8 +152,6 @@ public final class MultiPassLightingLogic extends DefaultTechniqueDefLogic {
 
                     lightDir.setValue(VarType.Vector4, tmpLightDirection);
 
-                    break;
-                case Probe:
                     break;
                 default:
                     throw new UnsupportedOperationException("Unknown type of light: " + l.getType());

+ 40 - 19
jme3-core/src/main/java/com/jme3/material/logic/SinglePassAndImageBasedLightingLogic.java

@@ -48,7 +48,7 @@ import com.jme3.shadow.next.array.DirectionalArrayShadowMap;
 import com.jme3.texture.TextureArray;
 import java.util.Comparator;
 
-import java.util.EnumSet;
+import java.util.*;
 
 public final class SinglePassAndImageBasedLightingLogic extends DefaultTechniqueDefLogic {
 
@@ -60,10 +60,11 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique
     private static final RenderState ADDITIVE_LIGHT = new RenderState();
 
     private final ColorRGBA ambientLightColor = new ColorRGBA(0, 0, 0, 1);
-    private LightProbe lightProbe;
     private TextureArray shadowMapArray;
     private Vector3f pssmSplitsPositions;
     private int numPssmSplits;
+    private static final String DEFINE_NB_PROBES = "NB_PROBES";
+    private List<LightProbe> lightProbes = new ArrayList<>(3);
 
     static {
         ADDITIVE_LIGHT.setBlendMode(BlendMode.AlphaAdditive);
@@ -73,16 +74,16 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique
     private final int singlePassLightingDefineId;
     private final int inPassShadowsDefineId;
     private final int nbLightsDefineId;
-    private final int indirectLightingDefineId;
     private final int numPssmSplitsDefineId;
+    private final int nbProbesDefineId;
 
     public SinglePassAndImageBasedLightingLogic(TechniqueDef techniqueDef) {
         super(techniqueDef);
         numPssmSplitsDefineId = techniqueDef.addShaderUnmappedDefine(DEFINE_NUM_PSSM_SPLITS, VarType.Int);
         singlePassLightingDefineId = techniqueDef.addShaderUnmappedDefine(DEFINE_SINGLE_PASS_LIGHTING, VarType.Boolean);
         nbLightsDefineId = techniqueDef.addShaderUnmappedDefine(DEFINE_NB_LIGHTS, VarType.Int);
-        indirectLightingDefineId = techniqueDef.addShaderUnmappedDefine(DEFINE_INDIRECT_LIGHTING, VarType.Boolean);
         inPassShadowsDefineId = techniqueDef.addShaderUnmappedDefine(DEFINE_IN_PASS_SHADOWS, VarType.Boolean);
+        nbProbesDefineId = techniqueDef.addShaderUnmappedDefine(DEFINE_NB_PROBES, VarType.Int);
     }
 
     @Override
@@ -91,7 +92,7 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique
         
         defines.set(singlePassLightingDefineId, true);
 
-        // TODO: here we have a problem, this is called once before render, 
+        // TODO: here we have a problem, this is called once before render,
         // so the define will be set for all passes (in case we have more than NB_LIGHTS lights)
         // Though the second pass should not render IBL as it is taken care of on 
         // first pass like ambient light in phong lighting.
@@ -100,7 +101,7 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique
         getFilteredLightList(renderManager, geometry);
        
         ambientLightColor.set(0, 0, 0, 1);
-        lightProbe = null;
+        lightProbes.clear();
         pssmSplitsPositions = null;
         numPssmSplits = 0;
         
@@ -110,7 +111,7 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique
                 ambientLightColor.addLocal(light.getColor());
                 filteredLightList.remove(i--);
             } else if (light instanceof LightProbe) {
-                lightProbe = (LightProbe) light;
+                lightProbes.add((LightProbe) light);
                 filteredLightList.remove(i--);
             } else if (light.getShadowMap() != null) {
                 ArrayShadowMap shadowMap = (ArrayShadowMap) light.getShadowMap();
@@ -121,6 +122,7 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique
                 }
             }
         }
+        defines.set(nbProbesDefineId, lightProbes.size());
         ambientLightColor.a = 1.0f;
         
         filteredLightList.sort(new Comparator<Light>() {
@@ -139,7 +141,6 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique
         });
         
         defines.set(nbLightsDefineId, renderManager.getSinglePassLightBatchSize() * 3);
-        defines.set(indirectLightingDefineId, lightProbe != null);
         defines.set(inPassShadowsDefineId, shadowMapArray != null);
         defines.set(numPssmSplitsDefineId, numPssmSplits);
         
@@ -167,12 +168,18 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique
         Uniform lightData = shader.getUniform("g_LightData");
         lightData.setVector4Length(numLights * 3);//8 lights * max 3
         Uniform ambientColor = shader.getUniform("g_AmbientLightColor");
+
+        // Matrix4f
         Uniform lightProbeData = shader.getUniform("g_LightProbeData");
-        lightProbeData.setVector4Length(1);
+        Uniform lightProbeData2 = shader.getUniform("g_LightProbeData2");
+        Uniform lightProbeData3 = shader.getUniform("g_LightProbeData3");
 
-        //TODO These 2 uniforms should be packed in an array, to be able to have several probes and blend between them.
         Uniform shCoeffs = shader.getUniform("g_ShCoeffs");
         Uniform lightProbePemMap = shader.getUniform("g_PrefEnvMap");
+        Uniform shCoeffs2 = shader.getUniform("g_ShCoeffs2");
+        Uniform lightProbePemMap2 = shader.getUniform("g_PrefEnvMap2");
+        Uniform shCoeffs3 = shader.getUniform("g_ShCoeffs3");
+        Uniform lightProbePemMap3 = shader.getUniform("g_PrefEnvMap3");
 
         if (startIndex != 0) {
             // apply additive blending for 2nd and future passes
@@ -183,17 +190,20 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique
         }
 
         //If there is a lightProbe in the list we force its render on the first pass
-        if(lightProbe != null){
-            BoundingSphere s = (BoundingSphere)lightProbe.getBounds();
-            lightProbeData.setVector4InArray(lightProbe.getPosition().x, lightProbe.getPosition().y, lightProbe.getPosition().z, 1f / s.getRadius() + lightProbe.getNbMipMaps(), 0);
-            shCoeffs.setValue(VarType.Vector3Array, lightProbe.getShCoeffs());
-            //assigning new texture indexes
-            int pemUnit = lastTexUnit++;
-            rm.getRenderer().setTexture(pemUnit, lightProbe.getPrefilteredEnvMap());
-            lightProbePemMap.setValue(VarType.Int, pemUnit);
+        if (!lightProbes.isEmpty()) {
+            LightProbe lightProbe = lightProbes.get(0);
+            lastTexUnit = setProbeData(rm, lastTexUnit, lightProbeData, shCoeffs, lightProbePemMap, lightProbe);
+            if (lightProbes.size() > 1) {
+                lightProbe = lightProbes.get(1);
+                lastTexUnit = setProbeData(rm, lastTexUnit, lightProbeData2, shCoeffs2, lightProbePemMap2, lightProbe);
+            }
+            if (lightProbes.size() > 2) {
+                lightProbe = lightProbes.get(2);
+                setProbeData(rm, lastTexUnit, lightProbeData3, shCoeffs3, lightProbePemMap3, lightProbe);
+            }
         } else {
             //Disable IBL for this pass
-            lightProbeData.setVector4InArray(0,0,0,-1, 0);
+            lightProbeData.setValue(VarType.Matrix4, LightProbe.FALLBACK_MATRIX);
         }
 
         Uniform shadowMatricesUniform = shader.getUniform("g_ShadowMatrices");
@@ -290,6 +300,17 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique
         return curIndex;
     }
 
+    private int setProbeData(RenderManager rm, int lastTexUnit, Uniform lightProbeData, Uniform shCoeffs, Uniform lightProbePemMap, LightProbe lightProbe) {
+
+        lightProbeData.setValue(VarType.Matrix4, lightProbe.getUniformMatrix());
+        shCoeffs.setValue(VarType.Vector3Array, lightProbe.getShCoeffs());
+        //assigning new texture indexes
+        int pemUnit = lastTexUnit++;
+        rm.getRenderer().setTexture(pemUnit, lightProbe.getPrefilteredEnvMap());
+        lightProbePemMap.setValue(VarType.Int, pemUnit);
+        return lastTexUnit;
+    }
+
     @Override
     public void render(RenderManager renderManager, Shader shader, Geometry geometry, int lastTexUnit) {
         int nbRenderedLights = 0;

+ 1 - 4
jme3-core/src/main/java/com/jme3/material/logic/SinglePassLightingLogic.java

@@ -112,7 +112,6 @@ public final class SinglePassLightingLogic extends DefaultTechniqueDefLogic {
         lightData.setVector4Length(numLights * 3);//8 lights * max 3
         Uniform ambientColor = shader.getUniform("g_AmbientLightColor");
 
-
         if (startIndex != 0) {
             // apply additive blending for 2nd and future passes
             rm.getRenderer().applyRenderState(ADDITIVE_LIGHT);
@@ -129,7 +128,7 @@ public final class SinglePassLightingLogic extends DefaultTechniqueDefLogic {
         for (curIndex = startIndex; curIndex < endIndex && curIndex < lightList.size(); curIndex++) {
 
             Light l = lightList.get(curIndex);
-            if (l.getType() == Light.Type.Ambient) {
+            if (l.getType() == Light.Type.Ambient || l.getType() == Light.Type.Probe) {
                 endIndex++;
                 continue;
             }
@@ -191,8 +190,6 @@ public final class SinglePassLightingLogic extends DefaultTechniqueDefLogic {
                     lightData.setVector4InArray(tmpVec.getX(), tmpVec.getY(), tmpVec.getZ(), spotAngleCos, lightDataIndex);
                     lightDataIndex++;
                     break;
-                case Probe:
-                    break;
                 default:
                     throw new UnsupportedOperationException("Unknown type of light: " + l.getType());
             }

+ 13 - 0
jme3-core/src/main/java/com/jme3/math/ColorRGBA.java

@@ -558,6 +558,19 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
         a = ((byte) (color) & 0xFF) / 255f;
         return this;
     }
+    /**
+     * Sets the RGBA values of this <code>ColorRGBA</code> with the given combined ABGR value 
+     * Bits 24-31 are alpha, bits 16-23 are blue, bits 8-15 are green, bits 0-7 are red.
+     * @param color The integer ABGR value used to set this object.
+     * @return this
+     */
+    public ColorRGBA fromIntABGR(int color) {
+        a = ((byte) (color >> 24) & 0xFF) / 255f;
+        b = ((byte) (color >> 16) & 0xFF) / 255f;
+        g = ((byte) (color >> 8) & 0xFF) / 255f;
+        r = ((byte) (color) & 0xFF) / 255f;
+        return this;
+    }
 
     /**
      * Transform this <code>ColorRGBA</code> to a <code>Vector3f</code> using

+ 13 - 0
jme3-core/src/main/java/com/jme3/math/EaseFunction.java

@@ -0,0 +1,13 @@
+package com.jme3.math;
+
+/**
+ * Created by Nehon on 26/03/2017.
+ */
+public interface EaseFunction {
+
+    /**
+     * @param value a value from 0 to 1. Passing a value out of this range will have unexpected behavior.
+     * @return
+     */
+    float apply(float value);
+}

+ 163 - 0
jme3-core/src/main/java/com/jme3/math/Easing.java

@@ -0,0 +1,163 @@
+package com.jme3.math;
+
+/**
+ * Expose several Easing function from Robert Penner
+ * Created by Nehon on 26/03/2017.
+ */
+public class Easing {
+
+
+    public static EaseFunction constant = new EaseFunction() {
+        @Override
+        public float apply(float value) {
+            return 0;
+        }
+    };
+    /**
+     * In
+     */
+    public static EaseFunction linear = new EaseFunction() {
+        @Override
+        public float apply(float value) {
+            return value;
+        }
+    };
+
+    public static EaseFunction inQuad = new EaseFunction() {
+        @Override
+        public float apply(float value) {
+            return value * value;
+        }
+    };
+
+    public static EaseFunction inCubic = new EaseFunction() {
+        @Override
+        public float apply(float value) {
+            return value * value * value;
+        }
+    };
+
+    public static EaseFunction inQuart = new EaseFunction() {
+        @Override
+        public float apply(float value) {
+            return value * value * value * value;
+        }
+    };
+
+    public static EaseFunction inQuint = new EaseFunction() {
+        @Override
+        public float apply(float value) {
+            return value * value * value * value * value;
+        }
+    };
+
+
+    /**
+     * Out Elastic and bounce
+     */
+    public static EaseFunction outElastic = new EaseFunction() {
+        @Override
+        public float apply(float value) {
+            return FastMath.pow(2f, -10f * value) * FastMath.sin((value - 0.3f / 4f) * (2f * FastMath.PI) / 0.3f) + 1f;
+        }
+    };
+
+    public static EaseFunction outBounce = new EaseFunction() {
+        @Override
+        public float apply(float value) {
+            if (value < (1f / 2.75f)) {
+                return (7.5625f * value * value);
+            } else if (value < (2f / 2.75f)) {
+                return (7.5625f * (value -= (1.5f / 2.75f)) * value + 0.75f);
+            } else if (value < (2.5 / 2.75)) {
+                return (7.5625f * (value -= (2.25f / 2.75f)) * value + 0.9375f);
+            } else {
+                return (7.5625f * (value -= (2.625f / 2.75f)) * value + 0.984375f);
+            }
+        }
+    };
+
+    /**
+     * In Elastic and bounce
+     */
+    public static EaseFunction inElastic = new Invert(outElastic);
+    public static EaseFunction inBounce = new Invert(outBounce);
+
+    /**
+     * Out
+     */
+    public static EaseFunction outQuad = new Invert(inQuad);
+    public static EaseFunction outCubic = new Invert(inCubic);
+    public static EaseFunction outQuart = new Invert(inQuart);
+    public static EaseFunction outQuint = new Invert(inQuint);
+
+    /**
+     * inOut
+     */
+    public static EaseFunction inOutQuad = new InOut(inQuad, outQuad);
+    public static EaseFunction inOutCubic = new InOut(inCubic, outCubic);
+    public static EaseFunction inOutQuart = new InOut(inQuart, outQuart);
+    public static EaseFunction inOutQuint = new InOut(inQuint, outQuint);
+    public static EaseFunction inOutElastic = new InOut(inElastic, outElastic);
+    public static EaseFunction inOutBounce = new InOut(inBounce, outBounce);
+
+
+    /**
+     * Extra functions
+     */
+
+    public static EaseFunction smoothStep = new EaseFunction() {
+        @Override
+        public float apply(float t) {
+            return t * t * (3f - 2f * t);
+        }
+    };
+
+    public static EaseFunction smootherStep = new EaseFunction() {
+        @Override
+        public float apply(float t) {
+            return t * t * t * (t * (t * 6f - 15f) + 10f);
+        }
+    };
+
+    /**
+     * An Ease function composed of 2 sb function for custom in and out easing
+     */
+    public static class InOut implements EaseFunction {
+
+        private EaseFunction in;
+        private EaseFunction out;
+
+        public InOut(EaseFunction in, EaseFunction out) {
+            this.in = in;
+            this.out = out;
+        }
+
+        @Override
+        public float apply(float value) {
+            if (value < 0.5) {
+                value = value * 2;
+                return inQuad.apply(value) / 2;
+            } else {
+                value = (value - 0.5f) * 2;
+                return outQuad.apply(value) / 2 + 0.5f;
+            }
+        }
+    }
+
+    private static class Invert implements EaseFunction {
+
+        private EaseFunction func;
+
+        public Invert(EaseFunction func) {
+            this.func = func;
+        }
+
+        @Override
+        public float apply(float value) {
+            return 1f - func.apply(1f - value);
+        }
+    }
+
+
+}

+ 247 - 0
jme3-core/src/main/java/com/jme3/math/MathUtils.java

@@ -0,0 +1,247 @@
+package com.jme3.math;
+
+import com.jme3.renderer.Camera;
+import com.jme3.util.TempVars;
+
+/**
+ * Created by Nehon on 23/04/2017.
+ */
+public class MathUtils {
+
+    public static Quaternion log(Quaternion q, Quaternion store) {
+        float a = FastMath.acos(q.w);
+        float sina = FastMath.sin(a);
+
+        store.w = 0;
+        if (sina > 0) {
+            store.x = a * q.x / sina;
+            store.y = a * q.y / sina;
+            store.z = a * q.z / sina;
+        } else {
+            store.x = 0;
+            store.y = 0;
+            store.z = 0;
+        }
+        return store;
+    }
+
+    public static Quaternion exp(Quaternion q, Quaternion store) {
+
+        float len = FastMath.sqrt(q.x * q.x + q.y * q.y + q.z * q.z);
+        float sinLen = FastMath.sin(len);
+        float cosLen = FastMath.cos(len);
+
+        store.w = cosLen;
+        if (len > 0) {
+            store.x = sinLen * q.x / len;
+            store.y = sinLen * q.y / len;
+            store.z = sinLen * q.z / len;
+        } else {
+            store.x = 0;
+            store.y = 0;
+            store.z = 0;
+        }
+        return store;
+    }
+
+    //! This version of slerp, used by squad, does not check for theta > 90.
+    public static Quaternion slerpNoInvert(Quaternion q1, Quaternion q2, float t, Quaternion store) {
+        float dot = q1.dot(q2);
+
+        if (dot > -0.95f && dot < 0.95f) {
+            float angle = FastMath.acos(dot);
+            float sin1 = FastMath.sin(angle * (1 - t));
+            float sin2 = FastMath.sin(angle * t);
+            float sin3 = FastMath.sin(angle);
+            store.x = (q1.x * sin1 + q2.x * sin2) / sin3;
+            store.y = (q1.y * sin1 + q2.y * sin2) / sin3;
+            store.z = (q1.z * sin1 + q2.z * sin2) / sin3;
+            store.w = (q1.w * sin1 + q2.w * sin2) / sin3;
+            System.err.println("real slerp");
+        } else {
+            // if the angle is small, use linear interpolation
+            store.set(q1).nlerp(q2, t);
+            System.err.println("nlerp");
+        }
+        return store;
+    }
+
+    public static Quaternion slerp(Quaternion q1, Quaternion q2, float t, Quaternion store) {
+
+        float dot = (q1.x * q2.x) + (q1.y * q2.y) + (q1.z * q2.z)
+                + (q1.w * q2.w);
+
+        if (dot < 0.0f) {
+            // Negate the second quaternion and the result of the dot product
+            q2.x = -q2.x;
+            q2.y = -q2.y;
+            q2.z = -q2.z;
+            q2.w = -q2.w;
+            dot = -dot;
+        }
+
+        // Set the first and second scale for the interpolation
+        float scale0 = 1 - t;
+        float scale1 = t;
+
+        // Check if the angle between the 2 quaternions was big enough to
+        // warrant such calculations
+        if (dot < 0.9f) {// Get the angle between the 2 quaternions,
+            // and then store the sin() of that angle
+            float theta = FastMath.acos(dot);
+            float invSinTheta = 1f / FastMath.sin(theta);
+
+            // Calculate the scale for q1 and q2, according to the angle and
+            // it's sine value
+            scale0 = FastMath.sin((1 - t) * theta) * invSinTheta;
+            scale1 = FastMath.sin((t * theta)) * invSinTheta;
+
+            // Calculate the x, y, z and w values for the quaternion by using a
+            // special
+            // form of linear interpolation for quaternions.
+            store.x = (scale0 * q1.x) + (scale1 * q2.x);
+            store.y = (scale0 * q1.y) + (scale1 * q2.y);
+            store.z = (scale0 * q1.z) + (scale1 * q2.z);
+            store.w = (scale0 * q1.w) + (scale1 * q2.w);
+        } else {
+            store.x = (scale0 * q1.x) + (scale1 * q2.x);
+            store.y = (scale0 * q1.y) + (scale1 * q2.y);
+            store.z = (scale0 * q1.z) + (scale1 * q2.z);
+            store.w = (scale0 * q1.w) + (scale1 * q2.w);
+            store.normalizeLocal();
+        }
+        // Return the interpolated quaternion
+        return store;
+    }
+
+//    //! Given 3 quaternions, qn-1,qn and qn+1, calculate a control point to be used in spline interpolation
+//    private static Quaternion spline(Quaternion qnm1, Quaternion qn, Quaternion qnp1, Quaternion store, Quaternion tmp) {
+//        store.set(-qn.x, -qn.y, -qn.z, qn.w);
+//        //store.set(qn).inverseLocal();
+//        tmp.set(store);
+//
+//        log(store.multLocal(qnm1), store);
+//        log(tmp.multLocal(qnp1), tmp);
+//        store.addLocal(tmp).multLocal(1f / -4f);
+//        exp(store, tmp);
+//        store.set(tmp).multLocal(qn);
+//
+//        return store.normalizeLocal();
+//        //return qn * (((qni * qnm1).log() + (qni * qnp1).log()) / -4).exp();
+//    }
+
+    //! Given 3 quaternions, qn-1,qn and qn+1, calculate a control point to be used in spline interpolation
+    private static Quaternion spline(Quaternion qnm1, Quaternion qn, Quaternion qnp1, Quaternion store, Quaternion tmp) {
+        Quaternion invQn = new Quaternion(-qn.x, -qn.y, -qn.z, qn.w);
+
+
+        log(invQn.mult(qnp1), tmp);
+        log(invQn.mult(qnm1), store);
+        store.addLocal(tmp).multLocal(-1f / 4f);
+        exp(store, tmp);
+        store.set(qn).multLocal(tmp);
+
+        return store.normalizeLocal();
+        //return qn * (((qni * qnm1).log() + (qni * qnp1).log()) / -4).exp();
+    }
+
+
+    //! spherical cubic interpolation
+    public static Quaternion squad(Quaternion q0, Quaternion q1, Quaternion q2, Quaternion q3, Quaternion a, Quaternion b, float t, Quaternion store) {
+
+        spline(q0, q1, q2, a, store);
+        spline(q1, q2, q3, b, store);
+
+        slerp(a, b, t, store);
+        slerp(q1, q2, t, a);
+        return slerp(a, store, 2 * t * (1 - t), b);
+        //slerpNoInvert(a, b, t, store);
+        //slerpNoInvert(q1, q2, t, a);
+        //return slerpNoInvert(a, store, 2 * t * (1 - t), b);
+
+//        quaternion c = slerpNoInvert(q1, q2, t),
+//                d = slerpNoInvert(a, b, t);
+//        return slerpNoInvert(c, d, 2 * t * (1 - t));
+    }
+
+
+    /**
+     * Returns the shortest distance between a Ray and a segment.
+     * The segment is defined by a start position and an end position in world space
+     * The distance returned will be in world space (world units).
+     * If the camera parameter is not null the distance will be returned in screen space (pixels)
+     *
+     * @param ray      The ray
+     * @param segStart The start position of the segment in world space
+     * @param segEnd   The end position of the segment in world space
+     * @param camera   The renderer camera if the distance is required in screen space. Null if the distance is required in world space
+     * @return the shortest distance between the ray and the segment or -1 if no solution is found.
+     */
+    public static float raySegmentShortestDistance(Ray ray, Vector3f segStart, Vector3f segEnd, Camera camera) {
+        // Algorithm is ported from the C algorithm of
+        // Paul Bourke at http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline3d/
+        TempVars vars = TempVars.get();
+        Vector3f resultSegmentPoint1 = vars.vect1;
+        Vector3f resultSegmentPoint2 = vars.vect2;
+
+        Vector3f p1 = segStart;
+        Vector3f p2 = segEnd;
+        Vector3f p3 = ray.origin;
+        Vector3f p4 = vars.vect3.set(ray.getDirection()).multLocal(Math.min(ray.getLimit(), 1000)).addLocal(ray.getOrigin());
+        Vector3f p13 = vars.vect4.set(p1).subtractLocal(p3);
+        Vector3f p43 = vars.vect5.set(p4).subtractLocal(p3);
+
+        if (p43.lengthSquared() < 0.0001) {
+            vars.release();
+            return -1;
+        }
+        Vector3f p21 = vars.vect6.set(p2).subtractLocal(p1);
+        if (p21.lengthSquared() < 0.0001) {
+            vars.release();
+            return -1;
+        }
+
+        double d1343 = p13.x * (double) p43.x + (double) p13.y * p43.y + (double) p13.z * p43.z;
+        double d4321 = p43.x * (double) p21.x + (double) p43.y * p21.y + (double) p43.z * p21.z;
+        double d1321 = p13.x * (double) p21.x + (double) p13.y * p21.y + (double) p13.z * p21.z;
+        double d4343 = p43.x * (double) p43.x + (double) p43.y * p43.y + (double) p43.z * p43.z;
+        double d2121 = p21.x * (double) p21.x + (double) p21.y * p21.y + (double) p21.z * p21.z;
+
+        double denom = d2121 * d4343 - d4321 * d4321;
+        if (Math.abs(denom) < 0.0001) {
+            vars.release();
+            return -1;
+        }
+        double numer = d1343 * d4321 - d1321 * d4343;
+
+        double mua = numer / denom;
+        double mub = (d1343 + d4321 * (mua)) / d4343;
+
+        resultSegmentPoint1.x = (float) (p1.x + mua * p21.x);
+        resultSegmentPoint1.y = (float) (p1.y + mua * p21.y);
+        resultSegmentPoint1.z = (float) (p1.z + mua * p21.z);
+        resultSegmentPoint2.x = (float) (p3.x + mub * p43.x);
+        resultSegmentPoint2.y = (float) (p3.y + mub * p43.y);
+        resultSegmentPoint2.z = (float) (p3.z + mub * p43.z);
+
+        //check if result 1 is in the segment section.
+        float startToPoint = vars.vect3.set(resultSegmentPoint1).subtractLocal(segStart).lengthSquared();
+        float endToPoint = vars.vect3.set(resultSegmentPoint1).subtractLocal(segEnd).lengthSquared();
+        float segLength = vars.vect3.set(segEnd).subtractLocal(segStart).lengthSquared();
+        if (startToPoint > segLength || endToPoint > segLength) {
+            vars.release();
+            return -1;
+        }
+
+        if (camera != null) {
+            //camera is not null let's convert the points in screen space
+            camera.getScreenCoordinates(resultSegmentPoint1, resultSegmentPoint1);
+            camera.getScreenCoordinates(resultSegmentPoint2, resultSegmentPoint2);
+        }
+
+        float length = resultSegmentPoint1.subtractLocal(resultSegmentPoint2).length();
+        vars.release();
+        return length;
+    }
+
+}

+ 71 - 64
jme3-core/src/main/java/com/jme3/math/Matrix4f.java

@@ -34,6 +34,7 @@ package com.jme3.math;
 import com.jme3.export.*;
 import com.jme3.util.BufferUtils;
 import com.jme3.util.TempVars;
+
 import java.io.IOException;
 import java.nio.FloatBuffer;
 import java.util.logging.Logger;
@@ -1022,96 +1023,95 @@ public final class Matrix4f implements Savable, Cloneable, java.io.Serializable
             store = new Matrix4f();
         }
 
-        float temp00, temp01, temp02, temp03;
-        float temp10, temp11, temp12, temp13;
-        float temp20, temp21, temp22, temp23;
-        float temp30, temp31, temp32, temp33;
+        TempVars v = TempVars.get();
+        float[] m = v.matrixWrite;
 
-        temp00 = m00 * in2.m00
+        m[0] = m00 * in2.m00
                 + m01 * in2.m10
                 + m02 * in2.m20
                 + m03 * in2.m30;
-        temp01 = m00 * in2.m01
+        m[1] = m00 * in2.m01
                 + m01 * in2.m11
                 + m02 * in2.m21
                 + m03 * in2.m31;
-        temp02 = m00 * in2.m02
+        m[2] = m00 * in2.m02
                 + m01 * in2.m12
                 + m02 * in2.m22
                 + m03 * in2.m32;
-        temp03 = m00 * in2.m03
+        m[3] = m00 * in2.m03
                 + m01 * in2.m13
                 + m02 * in2.m23
                 + m03 * in2.m33;
 
-        temp10 = m10 * in2.m00
+        m[4] = m10 * in2.m00
                 + m11 * in2.m10
                 + m12 * in2.m20
                 + m13 * in2.m30;
-        temp11 = m10 * in2.m01
+        m[5] = m10 * in2.m01
                 + m11 * in2.m11
                 + m12 * in2.m21
                 + m13 * in2.m31;
-        temp12 = m10 * in2.m02
+        m[6] = m10 * in2.m02
                 + m11 * in2.m12
                 + m12 * in2.m22
                 + m13 * in2.m32;
-        temp13 = m10 * in2.m03
+        m[7] = m10 * in2.m03
                 + m11 * in2.m13
                 + m12 * in2.m23
                 + m13 * in2.m33;
 
-        temp20 = m20 * in2.m00
+        m[8] = m20 * in2.m00
                 + m21 * in2.m10
                 + m22 * in2.m20
                 + m23 * in2.m30;
-        temp21 = m20 * in2.m01
+        m[9] = m20 * in2.m01
                 + m21 * in2.m11
                 + m22 * in2.m21
                 + m23 * in2.m31;
-        temp22 = m20 * in2.m02
+        m[10] = m20 * in2.m02
                 + m21 * in2.m12
                 + m22 * in2.m22
                 + m23 * in2.m32;
-        temp23 = m20 * in2.m03
+        m[11] = m20 * in2.m03
                 + m21 * in2.m13
                 + m22 * in2.m23
                 + m23 * in2.m33;
 
-        temp30 = m30 * in2.m00
+        m[12] = m30 * in2.m00
                 + m31 * in2.m10
                 + m32 * in2.m20
                 + m33 * in2.m30;
-        temp31 = m30 * in2.m01
+        m[13] = m30 * in2.m01
                 + m31 * in2.m11
                 + m32 * in2.m21
                 + m33 * in2.m31;
-        temp32 = m30 * in2.m02
+        m[14] = m30 * in2.m02
                 + m31 * in2.m12
                 + m32 * in2.m22
                 + m33 * in2.m32;
-        temp33 = m30 * in2.m03
+        m[15] = m30 * in2.m03
                 + m31 * in2.m13
                 + m32 * in2.m23
                 + m33 * in2.m33;
 
-        store.m00 = temp00;
-        store.m01 = temp01;
-        store.m02 = temp02;
-        store.m03 = temp03;
-        store.m10 = temp10;
-        store.m11 = temp11;
-        store.m12 = temp12;
-        store.m13 = temp13;
-        store.m20 = temp20;
-        store.m21 = temp21;
-        store.m22 = temp22;
-        store.m23 = temp23;
-        store.m30 = temp30;
-        store.m31 = temp31;
-        store.m32 = temp32;
-        store.m33 = temp33;
 
+        store.m00 = m[0];
+        store.m01 = m[1];
+        store.m02 = m[2];
+        store.m03 = m[3];
+        store.m10 = m[4];
+        store.m11 = m[5];
+        store.m12 = m[6];
+        store.m13 = m[7];
+        store.m20 = m[8];
+        store.m21 = m[9];
+        store.m22 = m[10];
+        store.m23 = m[11];
+        store.m30 = m[12];
+        store.m31 = m[13];
+        store.m32 = m[14];
+        store.m33 = m[15];
+        v.release();
         return store;
     }
 
@@ -1708,8 +1708,8 @@ public final class Matrix4f implements Savable, Cloneable, java.io.Serializable
         return new Vector3f(m03, m13, m23);
     }
 
-    public void toTranslationVector(Vector3f vector) {
-        vector.set(m03, m13, m23);
+    public Vector3f toTranslationVector(Vector3f vector) {
+        return vector.set(m03, m13, m23);
     }
 
     public Quaternion toRotationQuat() {
@@ -1718,8 +1718,9 @@ public final class Matrix4f implements Savable, Cloneable, java.io.Serializable
         return quat;
     }
 
-    public void toRotationQuat(Quaternion q) {
-        q.fromRotationMatrix(toRotationMatrix());
+    public Quaternion toRotationQuat(Quaternion q) {
+        return q.fromRotationMatrix(m00, m01, m02, m10,
+                m11, m12, m20, m21, m22);
     }
 
     public Matrix3f toRotationMatrix() {
@@ -1752,15 +1753,16 @@ public final class Matrix4f implements Savable, Cloneable, java.io.Serializable
 	/**
 	 * Retreives the scale vector from the matrix and stores it into a given
 	 * vector.
-	 * 
-	 * @param the
-	 *            vector where the scale will be stored
+     *
+     * @param store the vector where the scale will be stored
+     * @return the store vector
 	 */
-	public void toScaleVector(Vector3f vector) {
+    public Vector3f toScaleVector(Vector3f store) {
 		float scaleX = (float) Math.sqrt(m00 * m00 + m10 * m10 + m20 * m20);
 		float scaleY = (float) Math.sqrt(m01 * m01 + m11 * m11 + m21 * m21);
 		float scaleZ = (float) Math.sqrt(m02 * m02 + m12 * m12 + m22 * m22);
-		vector.set(scaleX, scaleY, scaleZ);
+        store.set(scaleX, scaleY, scaleZ);
+        return store;
     }
 
     /**
@@ -1774,25 +1776,30 @@ public final class Matrix4f implements Savable, Cloneable, java.io.Serializable
      *            the Z scale
      */
     public void setScale(float x, float y, float z) {
-        TempVars vars = TempVars.get();
-        vars.vect1.set(m00, m10, m20);
-        vars.vect1.normalizeLocal().multLocal(x);
-        m00 = vars.vect1.x;
-        m10 = vars.vect1.y;
-        m20 = vars.vect1.z;
-
-        vars.vect1.set(m01, m11, m21);
-        vars.vect1.normalizeLocal().multLocal(y);
-        m01 = vars.vect1.x;
-        m11 = vars.vect1.y;
-        m21 = vars.vect1.z;
-
-        vars.vect1.set(m02, m12, m22);
-        vars.vect1.normalizeLocal().multLocal(z);
-        m02 = vars.vect1.x;
-        m12 = vars.vect1.y;
-        m22 = vars.vect1.z;
-        vars.release();
+
+        float length = m00 * m00 + m10 * m10 + m20 * m20;
+        if (length != 0f) {
+            length = length == 1 ? x : (x / FastMath.sqrt(length));
+            m00 *= length;
+            m10 *= length;
+            m20 *= length;
+        }
+
+        length = m01 * m01 + m11 * m11 + m21 * m21;
+        if (length != 0f) {
+            length = length == 1 ? y : (y / FastMath.sqrt(length));
+            m01 *= length;
+            m11 *= length;
+            m21 *= length;
+        }
+
+        length = m02 * m02 + m12 * m12 + m22 * m22;
+        if (length != 0f) {
+            length = length == 1 ? z : (z / FastMath.sqrt(length));
+            m02 *= length;
+            m12 *= length;
+            m22 *= length;
+        }
     }
 
     /**

+ 51 - 8
jme3-core/src/main/java/com/jme3/math/Quaternion.java

@@ -33,10 +33,8 @@ package com.jme3.math;
 
 import com.jme3.export.*;
 import com.jme3.util.TempVars;
-import java.io.Externalizable;
-import java.io.IOException;
-import java.io.ObjectInput;
-import java.io.ObjectOutput;
+
+import java.io.*;
 import java.util.logging.Logger;
 
 /**
@@ -452,11 +450,56 @@ public final class Quaternion implements Savable, Cloneable, java.io.Serializabl
         return result;
     }
 
+    /**
+     * <code>toTransformMatrix</code> converts this quaternion to a transform
+     * matrix. The result is stored in result.
+     * Note this method won't preserve the scale of the given matrix.
+     *
+     * @param store The Matrix3f to store the result in.
+     * @return the transform matrix with the rotation representation of this quaternion.
+     */
+    public Matrix4f toTransformMatrix(Matrix4f store) {
+
+        float norm = norm();
+        // we explicitly test norm against one here, saving a division
+        // at the cost of a test and branch.  Is it worth it?
+        float s = (norm == 1f) ? 2f : (norm > 0f) ? 2f / norm : 0;
+
+        // compute xs/ys/zs first to save 6 multiplications, since xs/ys/zs
+        // will be used 2-4 times each.
+        float xs = x * s;
+        float ys = y * s;
+        float zs = z * s;
+        float xx = x * xs;
+        float xy = x * ys;
+        float xz = x * zs;
+        float xw = w * xs;
+        float yy = y * ys;
+        float yz = y * zs;
+        float yw = w * ys;
+        float zz = z * zs;
+        float zw = w * zs;
+
+        // using s=2/norm (instead of 1/norm) saves 9 multiplications by 2 here
+        store.m00 = 1 - (yy + zz);
+        store.m01 = (xy - zw);
+        store.m02 = (xz + yw);
+        store.m10 = (xy + zw);
+        store.m11 = 1 - (xx + zz);
+        store.m12 = (yz - xw);
+        store.m20 = (xz - yw);
+        store.m21 = (yz + xw);
+        store.m22 = 1 - (xx + yy);
+
+        return store;
+    }
+
     /**
      * <code>toRotationMatrix</code> converts this quaternion to a rotational
      * matrix. The result is stored in result. 4th row and 4th column values are
      * untouched. Note: the result is created from a normalized version of this quat.
-     * 
+     * Note that this method will preserve the scale of the given matrix
+     *
      * @param result
      *            The Matrix4f to store the result in.
      * @return the rotation matrix representation of this quaternion.
@@ -464,7 +507,7 @@ public final class Quaternion implements Savable, Cloneable, java.io.Serializabl
     public Matrix4f toRotationMatrix(Matrix4f result) {
         TempVars tempv = TempVars.get();
         Vector3f originalScale = tempv.vect1;
-        
+
         result.toScaleVector(originalScale);
         result.setScale(1, 1, 1);
         float norm = norm();
@@ -499,9 +542,9 @@ public final class Quaternion implements Savable, Cloneable, java.io.Serializabl
         result.m22 = 1 - (xx + yy);
 
         result.setScale(originalScale);
-        
+
         tempv.release();
-        
+
         return result;
     }
 

+ 21 - 10
jme3-core/src/main/java/com/jme3/math/Transform.java

@@ -32,6 +32,8 @@
 package com.jme3.math;
 
 import com.jme3.export.*;
+import com.jme3.util.TempVars;
+
 import java.io.IOException;
 
 /**
@@ -174,13 +176,14 @@ public final class Transform implements Savable, Cloneable, java.io.Serializable
     }
 
     /**
-     * Sets this matrix to the interpolation between the first matrix and the second by delta amount.
+     * Sets this transform to the interpolation between the first transform and the second by delta amount.
      * @param t1 The beginning transform.
      * @param t2 The ending transform.
      * @param delta An amount between 0 and 1 representing how far to interpolate from t1 to t2.
      */
     public void interpolateTransforms(Transform t1, Transform t2, float delta) {
-        this.rot.slerp(t1.rot,t2.rot,delta);
+        t1.rot.nlerp(t2.rot, delta);
+        this.rot.set(t1.rot);
         this.translation.interpolateLocal(t1.translation,t2.translation,delta);
         this.scale.interpolateLocal(t1.scale,t2.scale,delta);
     }
@@ -257,17 +260,25 @@ public final class Transform implements Savable, Cloneable, java.io.Serializable
     }
 
     public Matrix4f toTransformMatrix() {
-        Matrix4f trans = new Matrix4f();
-        trans.setTranslation(translation);
-        trans.setRotationQuaternion(rot);
-        trans.setScale(scale);
-        return trans;
+        return toTransformMatrix(null);
+    }
+
+    public Matrix4f toTransformMatrix(Matrix4f store) {
+        if (store == null) {
+            store = new Matrix4f();
+        }
+        store.setTranslation(translation);
+        rot.toTransformMatrix(store);
+        store.setScale(scale);
+        return store;
     }
     
     public void fromTransformMatrix(Matrix4f mat) {
-        translation.set(mat.toTranslationVector());
-        rot.set(mat.toRotationQuat());
-        scale.set(mat.toScaleVector());
+        TempVars vars = TempVars.get();
+        translation.set(mat.toTranslationVector(vars.vect1));
+        rot.set(mat.toRotationQuat(vars.quat1));
+        scale.set(mat.toScaleVector(vars.vect2));
+        vars.release();
     }
     
     public Transform invert() {

+ 9 - 1
jme3-core/src/main/java/com/jme3/renderer/Caps.java

@@ -394,7 +394,15 @@ public enum Caps {
     /**
      * GPU can provide and accept binary shaders.
      */
-    BinaryShader;
+    BinaryShader,
+    /**
+     * Supporting working with UniformBufferObject.
+     */
+    UniformBufferObject,
+    /**
+     * Supporting working with ShaderStorageBufferObjects.
+     */
+    ShaderStorageBufferObject;
 
     /**
      * Returns true if given the renderer capabilities, the texture

+ 16 - 0
jme3-core/src/main/java/com/jme3/renderer/Limits.java

@@ -62,4 +62,20 @@ public enum Limits {
     ColorTextureSamples,
     DepthTextureSamples,
     TextureAnisotropy,
+
+    // UBO
+    UniformBufferObjectMaxVertexBlocks,
+    UniformBufferObjectMaxFragmentBlocks,
+    UniformBufferObjectMaxGeometryBlocks,
+    UniformBufferObjectMaxBlockSize,
+
+    // SSBO
+    ShaderStorageBufferObjectMaxBlockSize,
+    ShaderStorageBufferObjectMaxVertexBlocks,
+    ShaderStorageBufferObjectMaxFragmentBlocks,
+    ShaderStorageBufferObjectMaxGeometryBlocks,
+    ShaderStorageBufferObjectMaxTessControlBlocks,
+    ShaderStorageBufferObjectMaxTessEvaluationBlocks,
+    ShaderStorageBufferObjectMaxComputeBlocks,
+    ShaderStorageBufferObjectMaxCombineBlocks,
 }

+ 29 - 0
jme3-core/src/main/java/com/jme3/renderer/RenderContext.java

@@ -32,6 +32,7 @@
 package com.jme3.renderer;
 
 import com.jme3.material.RenderState;
+import com.jme3.material.RenderState.BlendFunc;
 import com.jme3.math.ColorRGBA;
 import com.jme3.scene.Mesh;
 import com.jme3.scene.VertexBuffer;
@@ -110,6 +111,30 @@ public class RenderContext {
      */
     public RenderState.BlendEquationAlpha blendEquationAlpha = RenderState.BlendEquationAlpha.InheritColor;
 
+    /**
+     * @see RenderState#setCustomBlendFactors(com.jme3.material.RenderState.BlendFunc, com.jme3.material.RenderState.BlendFunc,
+     *      com.jme3.material.RenderState.BlendFunc, com.jme3.material.RenderState.BlendFunc)
+     */
+    public RenderState.BlendFunc sfactorRGB = RenderState.BlendFunc.One;
+
+    /**
+     * @see RenderState#setCustomBlendFactors(com.jme3.material.RenderState.BlendFunc, com.jme3.material.RenderState.BlendFunc,
+     *      com.jme3.material.RenderState.BlendFunc, com.jme3.material.RenderState.BlendFunc)
+     */
+    public RenderState.BlendFunc dfactorRGB = RenderState.BlendFunc.One;
+
+    /**
+     * @see RenderState#setCustomBlendFactors(com.jme3.material.RenderState.BlendFunc, com.jme3.material.RenderState.BlendFunc,
+     *      com.jme3.material.RenderState.BlendFunc, com.jme3.material.RenderState.BlendFunc)
+     */
+    public RenderState.BlendFunc sfactorAlpha = RenderState.BlendFunc.One;
+
+    /**
+     * @see RenderState#setCustomBlendFactors(com.jme3.material.RenderState.BlendFunc, com.jme3.material.RenderState.BlendFunc,
+     *      com.jme3.material.RenderState.BlendFunc, com.jme3.material.RenderState.BlendFunc)
+     */
+    public RenderState.BlendFunc dfactorAlpha = RenderState.BlendFunc.One;
+
     /**
      * @see RenderState#setWireframe(boolean) 
      */
@@ -266,6 +291,10 @@ public class RenderContext {
         blendMode = RenderState.BlendMode.Off;
         blendEquation = RenderState.BlendEquation.Add;
         blendEquationAlpha = RenderState.BlendEquationAlpha.InheritColor;
+        sfactorRGB = BlendFunc.One;
+        dfactorRGB = BlendFunc.One;
+        sfactorAlpha = BlendFunc.One;
+        dfactorAlpha = BlendFunc.One;
         wireframe = false;
         boundShaderProgram = 0;
         boundShader = null;

+ 1 - 1
jme3-core/src/main/java/com/jme3/renderer/RenderManager.java

@@ -642,7 +642,7 @@ public class RenderManager {
                 throw new IllegalStateException("No material is set for Geometry: " + gm.getName());
             }
 
-            gm.getMaterial().preload(this);
+            gm.getMaterial().preload(this, gm);
             Mesh mesh = gm.getMesh();
             if (mesh != null
                     && mesh.getVertexCount() != 0

+ 15 - 0
jme3-core/src/main/java/com/jme3/renderer/Renderer.java

@@ -35,6 +35,7 @@ import com.jme3.material.RenderState;
 import com.jme3.math.ColorRGBA;
 import com.jme3.scene.Mesh;
 import com.jme3.scene.VertexBuffer;
+import com.jme3.shader.BufferObject;
 import com.jme3.shader.Shader;
 import com.jme3.shader.Shader.ShaderSource;
 import com.jme3.system.AppSettings;
@@ -267,12 +268,26 @@ public interface Renderer {
      */
     public void updateBufferData(VertexBuffer vb);
 
+    /**
+     * Uploads data of the buffer object on the GPU.
+     *
+     * @param bo the buffer object to upload.
+     */
+    public void updateBufferData(BufferObject bo);
+
     /**
      * Deletes a vertex buffer from the GPU.
      * @param vb The vertex buffer to delete
      */
     public void deleteBuffer(VertexBuffer vb);
 
+    /**
+     * Deletes the buffer object from the GPU.
+     *
+     * @param bo the buffer object to delete.
+     */
+    public void deleteBuffer(BufferObject bo);
+
     /**
      * Renders <code>count</code> meshes, with the geometry data supplied and
      * per-instance data supplied.

Dosya farkı çok büyük olduğundan ihmal edildi
+ 228 - 227
jme3-core/src/main/java/com/jme3/renderer/opengl/GL.java


+ 83 - 0
jme3-core/src/main/java/com/jme3/renderer/opengl/GL3.java

@@ -83,6 +83,46 @@ public interface GL3 extends GL2 {
     public static final int GL_RGB_INTEGER = 36248;
     public static final int GL_RGBA_INTEGER = 36249;
 
+    public static final int GL_UNIFORM_OFFSET = 0x8A3B;
+
+    /**
+     * Accepted by the {@code target} parameters of BindBuffer, BufferData, BufferSubData, MapBuffer, UnmapBuffer, GetBufferSubData, and GetBufferPointerv.
+     */
+    public static final int GL_UNIFORM_BUFFER = 0x8A11;
+
+    /**
+     * Accepted by the {@code pname} parameter of GetActiveUniformBlockiv.
+     */
+    public static final int GL_UNIFORM_BLOCK_BINDING = 0x8A3F;
+    public static final int GL_UNIFORM_BLOCK_DATA_SIZE = 0x8A40;
+    public static final int GL_UNIFORM_BLOCK_NAME_LENGTH = 0x8A41;
+    public static final int GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS = 0x8A42;
+    public static final int GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES = 0x8A43;
+    public static final int GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER = 0x8A44;
+    public static final int GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER = 0x8A45;
+    public static final int GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER = 0x8A46;
+
+    /**
+     *  Accepted by the &lt;pname&gt; parameter of GetBooleanv, GetIntegerv,
+     *  GetFloatv, and GetDoublev:
+     */
+    public static final int GL_MAX_VERTEX_UNIFORM_BLOCKS = 0x8A2B;
+    public static final int GL_MAX_GEOMETRY_UNIFORM_BLOCKS = 0x8A2C;
+    public static final int GL_MAX_FRAGMENT_UNIFORM_BLOCKS = 0x8A2D;
+    public static final int GL_MAX_COMBINED_UNIFORM_BLOCKS = 0x8A2E;
+    public static final int GL_MAX_UNIFORM_BUFFER_BINDINGS = 0x8A2F;
+    public static final int GL_MAX_UNIFORM_BLOCK_SIZE = 0x8A30;
+    public static final int GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS = 0x8A31;
+    public static final int GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS = 0x8A32;
+    public static final int GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS = 0x8A33;
+    public static final int GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT = 0x8A34;
+
+    /**
+     * Accepted by the {@code target} parameters of BindBuffer, BufferData, BufferSubData, MapBuffer, UnmapBuffer, GetBufferSubData, GetBufferPointerv,
+     * BindBufferRange, BindBufferOffset and BindBufferBase.
+     */
+    public static final int GL_TRANSFORM_FEEDBACK_BUFFER = 0x8C8E;
+
     /**
      * <p><a target="_blank" href="http://docs.gl/gl4/glBindFragDataLocation">Reference Page</a></p>
      * <p>
@@ -128,4 +168,47 @@ public interface GL3 extends GL2 {
      * @param index the index of the particular element being queried.
      */
     public String glGetString(int name, int index); /// GL3+
+
+
+    /**
+     * <p><a target="_blank" href="http://docs.gl/gl4/glGetUniformBlockIndex">Reference Page</a></p>
+     *
+     * Retrieves the index of a named uniform block.
+     *
+     * @param program          the name of a program containing the uniform block.
+     * @param uniformBlockName an array of characters to containing the name of the uniform block whose index to retrieve.
+     * @return the block index.
+     */
+    public int glGetUniformBlockIndex(int program, String uniformBlockName);
+
+    /**
+     * <p><a target="_blank" href="http://docs.gl/gl4/glBindBufferBase">Reference Page</a></p>
+     *
+     * Binds a buffer object to an indexed buffer target.
+     *
+     * @param target the target of the bind operation. One of:<br><table><tr><td>{@link #GL_TRANSFORM_FEEDBACK_BUFFER TRANSFORM_FEEDBACK_BUFFER}</td><td>{@link #GL_UNIFORM_BUFFER UNIFORM_BUFFER}</td><td>{@link GL4#GL_ATOMIC_COUNTER_BUFFER ATOMIC_COUNTER_BUFFER}</td><td>{@link GL4#GL_SHADER_STORAGE_BUFFER SHADER_STORAGE_BUFFER}</td></tr></table>
+     * @param index  the index of the binding point within the array specified by {@code target}
+     * @param buffer a buffer object to bind to the specified binding point
+     */
+    public void glBindBufferBase(int target, int index, int buffer);
+
+    /**
+     * Binding points for active uniform blocks are assigned using glUniformBlockBinding. Each of a program's active
+     * uniform blocks has a corresponding uniform buffer binding point. program is the name of a program object for
+     * which the command glLinkProgram has been issued in the past.
+     * <p>
+     * If successful, glUniformBlockBinding specifies that program will use the data store of the buffer object bound
+     * to the binding point uniformBlockBinding to extract the values of the uniforms in the uniform block identified
+     * by uniformBlockIndex.
+     * <p>
+     * When a program object is linked or re-linked, the uniform buffer object binding point assigned to each of its
+     * active uniform blocks is reset to zero.
+     *
+     * @param program             The name of a program object containing the active uniform block whose binding to
+     *                            assign.
+     * @param uniformBlockIndex   The index of the active uniform block within program whose binding to assign.
+     * @param uniformBlockBinding Specifies the binding point to which to bind the uniform block with index
+     *                            uniformBlockIndex within program.
+     */
+    public void glUniformBlockBinding(int program, int uniformBlockIndex, int uniformBlockBinding);
 }

+ 50 - 0
jme3-core/src/main/java/com/jme3/renderer/opengl/GL4.java

@@ -42,6 +42,32 @@ public interface GL4 extends GL3 {
     public static final int GL_TESS_EVALUATION_SHADER = 0x8E87;
     public static final int GL_PATCHES = 0xE;
 
+    /**
+     * Accepted by the {@code target} parameter of BindBufferBase and BindBufferRange.
+     */
+    public static final int GL_ATOMIC_COUNTER_BUFFER = 0x92C0;
+
+    /**
+     * Accepted by the {@code target} parameters of BindBuffer, BufferData, BufferSubData, MapBuffer, UnmapBuffer, GetBufferSubData, and GetBufferPointerv.
+     */
+    public static final int GL_SHADER_STORAGE_BUFFER = 0x90D2;
+    public static final int GL_SHADER_STORAGE_BLOCK = 0x92E6;
+
+    /**
+     *  Accepted by the &lt;pname&gt; parameter of GetIntegerv, GetBooleanv,
+     *  GetInteger64v, GetFloatv, and GetDoublev:
+     */
+    public static final int GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = 0x90D6;
+    public static final int GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS = 0x90D7;
+    public static final int GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS = 0x90D8;
+    public static final int GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS = 0x90D9;
+    public static final int GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS = 0x90DA;
+    public static final int GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS = 0x90DB;
+    public static final int GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS = 0x90DC;
+    public static final int GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS = 0x90DD;
+    public static final int GL_MAX_SHADER_STORAGE_BLOCK_SIZE = 0x90DE;
+    public static final int GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT = 0x90DF;
+
     /**
      * <p><a target="_blank" href="http://docs.gl/gl4/glPatchParameteri">Reference Page</a></p>
      * <p>
@@ -50,4 +76,28 @@ public interface GL4 extends GL3 {
      * @param count the new value for the parameter given by {@code pname}
      */
     public void glPatchParameter(int count);
+
+    /**
+     * Returns the unsigned integer index assigned to a resource named name in the interface type programInterface of
+     * program object program.
+     *
+     * @param program          the name of a program object whose resources to query.
+     * @param programInterface a token identifying the interface within program containing the resource named name.
+     * @param name             the name of the resource to query the index of.
+     * @return the index of a named resource within a program.
+     */
+    public int glGetProgramResourceIndex(int program, int programInterface, String name);
+
+    /**
+     * Cchanges the active shader storage block with an assigned index of storageBlockIndex in program object program.
+     * storageBlockIndex must be an active shader storage block index in program. storageBlockBinding must be less
+     * than the value of {@code #GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS}. If successful, glShaderStorageBlockBinding specifies
+     * that program will use the data store of the buffer object bound to the binding point storageBlockBinding to
+     * read and write the values of the buffer variables in the shader storage block identified by storageBlockIndex.
+     *
+     * @param program             the name of a program object whose resources to query.
+     * @param storageBlockIndex   The index storage block within the program.
+     * @param storageBlockBinding The index storage block binding to associate with the specified storage block.
+     */
+    public void glShaderStorageBlockBinding(int program, int storageBlockIndex, int storageBlockBinding);
 }

+ 32 - 0
jme3-core/src/main/java/com/jme3/renderer/opengl/GLDebugDesktop.java

@@ -83,6 +83,19 @@ public class GLDebugDesktop extends GLDebugES implements GL2, GL3, GL4 {
         return result;
     }
 
+    @Override
+    public int glGetUniformBlockIndex(final int program, final String uniformBlockName) {
+        final int result = gl3.glGetUniformBlockIndex(program, uniformBlockName);
+        checkError();
+        return result;
+    }
+
+    @Override
+    public void glBindBufferBase(final int target, final int index, final int buffer) {
+        gl3.glBindBufferBase(target, index, buffer);
+        checkError();
+    }
+
     @Override
     public void glDeleteVertexArrays(IntBuffer arrays) {
         gl3.glDeleteVertexArrays(arrays);
@@ -95,8 +108,27 @@ public class GLDebugDesktop extends GLDebugES implements GL2, GL3, GL4 {
         checkError();
     }
 
+    @Override
+    public int glGetProgramResourceIndex(int program, int programInterface, String name) {
+        final int result = gl4.glGetProgramResourceIndex(program, programInterface, name);
+        checkError();
+        return result;
+    }
+
+    @Override
+    public void glShaderStorageBlockBinding(int program, int storageBlockIndex, int storageBlockBinding) {
+        gl4.glShaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding);
+        checkError();
+    }
+
     public void glBlendEquationSeparate(int colorMode, int alphaMode) {
         gl.glBlendEquationSeparate(colorMode, alphaMode);
         checkError();
     }
+
+    @Override
+    public void glUniformBlockBinding(final int program, final int uniformBlockIndex, final int uniformBlockBinding) {
+        gl3.glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding);
+        checkError();
+    }
 }

+ 5 - 1
jme3-core/src/main/java/com/jme3/renderer/opengl/GLImageFormats.java

@@ -196,6 +196,10 @@ public final class GLImageFormats {
                 format(formatToGL, Format.Luminance32F,         GLExt.GL_LUMINANCE32F_ARB,       GL.GL_LUMINANCE,       GL.GL_FLOAT);
                 format(formatToGL, Format.Luminance16FAlpha16F, GLExt.GL_LUMINANCE_ALPHA16F_ARB, GL.GL_LUMINANCE_ALPHA, halfFloatFormat);
             }
+            format(formatToGL, Format.R16F,                 GL3.GL_R16F,                     GL3.GL_RED,            halfFloatFormat);
+            format(formatToGL, Format.R32F,                 GL3.GL_R32F,                     GL3.GL_RED,            GL.GL_FLOAT);
+            format(formatToGL, Format.RG16F,                GL3.GL_RG16F,                    GL3.GL_RG,             halfFloatFormat);
+            format(formatToGL, Format.RG32F,                GL3.GL_RG32F,                    GL3.GL_RG,             GL.GL_FLOAT);
             format(formatToGL, Format.RGB16F,               GLExt.GL_RGB16F_ARB,             GL.GL_RGB,             halfFloatFormat);
             format(formatToGL, Format.RGB32F,               GLExt.GL_RGB32F_ARB,             GL.GL_RGB,             GL.GL_FLOAT);
             format(formatToGL, Format.RGBA16F,              GLExt.GL_RGBA16F_ARB,            GL.GL_RGBA,            halfFloatFormat);
@@ -220,7 +224,7 @@ public final class GLImageFormats {
         
         // NOTE: OpenGL ES 2.0 does not support DEPTH_COMPONENT as internal format -- fallback to 16-bit depth.
         if (caps.contains(Caps.OpenGLES20)) {
-            format(formatToGL, Format.Depth, GL.GL_DEPTH_COMPONENT16, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_BYTE);
+            format(formatToGL, Format.Depth, GL.GL_DEPTH_COMPONENT16, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_SHORT);
         } else {
             format(formatToGL, Format.Depth, GL.GL_DEPTH_COMPONENT, GL.GL_DEPTH_COMPONENT, GL.GL_UNSIGNED_BYTE);
         }

+ 294 - 83
jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java

@@ -33,6 +33,7 @@ package com.jme3.renderer.opengl;
 
 import com.jme3.material.RenderState;
 import com.jme3.material.RenderState.BlendFunc;
+import com.jme3.material.RenderState.BlendMode;
 import com.jme3.material.RenderState.StencilOperation;
 import com.jme3.material.RenderState.TestFunction;
 import com.jme3.math.*;
@@ -44,11 +45,9 @@ import com.jme3.scene.VertexBuffer;
 import com.jme3.scene.VertexBuffer.Format;
 import com.jme3.scene.VertexBuffer.Type;
 import com.jme3.scene.VertexBuffer.Usage;
-import com.jme3.shader.Attribute;
-import com.jme3.shader.Shader;
+import com.jme3.shader.*;
 import com.jme3.shader.Shader.ShaderSource;
 import com.jme3.shader.Shader.ShaderType;
-import com.jme3.shader.Uniform;
 import com.jme3.texture.FrameBuffer;
 import com.jme3.texture.FrameBuffer.RenderBuffer;
 import com.jme3.texture.Image;
@@ -60,17 +59,17 @@ import com.jme3.util.BufferUtils;
 import com.jme3.util.ListMap;
 import com.jme3.util.MipMapGenerator;
 import com.jme3.util.NativeObjectManager;
-import java.nio.*;
-import java.util.Arrays;
-import java.util.EnumMap;
-import java.util.EnumSet;
-import java.util.HashSet;
-import java.util.List;
+import jme3tools.shader.ShaderDebug;
+
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.nio.ShortBuffer;
+import java.util.*;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import jme3tools.shader.ShaderDebug;
 
 public final class GLRenderer implements Renderer {
 
@@ -479,6 +478,26 @@ public final class GLRenderer implements Renderer {
             }
         }
 
+        if (hasExtension("GL_ARB_shader_storage_buffer_object")) {
+            caps.add(Caps.ShaderStorageBufferObject);
+            limits.put(Limits.ShaderStorageBufferObjectMaxBlockSize, getInteger(GL4.GL_MAX_SHADER_STORAGE_BLOCK_SIZE));
+            limits.put(Limits.ShaderStorageBufferObjectMaxComputeBlocks, getInteger(GL4.GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS));
+            limits.put(Limits.ShaderStorageBufferObjectMaxGeometryBlocks, getInteger(GL4.GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS));
+            limits.put(Limits.ShaderStorageBufferObjectMaxFragmentBlocks, getInteger(GL4.GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS));
+            limits.put(Limits.ShaderStorageBufferObjectMaxVertexBlocks, getInteger(GL4.GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS));
+            limits.put(Limits.ShaderStorageBufferObjectMaxTessControlBlocks, getInteger(GL4.GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS));
+            limits.put(Limits.ShaderStorageBufferObjectMaxTessEvaluationBlocks, getInteger(GL4.GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS));
+            limits.put(Limits.ShaderStorageBufferObjectMaxCombineBlocks, getInteger(GL4.GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS));
+        }
+
+        if (hasExtension("GL_ARB_uniform_buffer_object")) {
+            caps.add(Caps.UniformBufferObject);
+            limits.put(Limits.UniformBufferObjectMaxBlockSize, getInteger(GL3.GL_MAX_UNIFORM_BLOCK_SIZE));
+            limits.put(Limits.UniformBufferObjectMaxGeometryBlocks, getInteger(GL3.GL_MAX_GEOMETRY_UNIFORM_BLOCKS));
+            limits.put(Limits.UniformBufferObjectMaxFragmentBlocks, getInteger(GL3.GL_MAX_FRAGMENT_UNIFORM_BLOCKS));
+            limits.put(Limits.UniformBufferObjectMaxVertexBlocks, getInteger(GL3.GL_MAX_VERTEX_UNIFORM_BLOCKS));
+        }
+
         // Print context information
         logger.log(Level.INFO, "OpenGL Renderer Information\n" +
                         " * Vendor: {0}\n" +
@@ -743,68 +762,57 @@ public final class GLRenderer implements Renderer {
             context.cullMode = state.getFaceCullMode();
         }
 
-        if (state.getBlendMode() != context.blendMode) {
-            if (state.getBlendMode() == RenderState.BlendMode.Off) {
-                gl.glDisable(GL.GL_BLEND);
-            } else {
-                if (context.blendMode == RenderState.BlendMode.Off) {
-                    gl.glEnable(GL.GL_BLEND);
-                }
-                switch (state.getBlendMode()) {
-                    case Off:
-                        break;
-                    case Additive:
-                        gl.glBlendFunc(GL.GL_ONE, GL.GL_ONE);
-                        break;
-                    case AlphaAdditive:
-                        gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE);
-                        break;
-                    case Alpha:
-                        gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
-                        break;
-                    case PremultAlpha:
-                        gl.glBlendFunc(GL.GL_ONE, GL.GL_ONE_MINUS_SRC_ALPHA);
-                        break;
-                    case Modulate:
-                        gl.glBlendFunc(GL.GL_DST_COLOR, GL.GL_ZERO);
-                        break;
-                    case ModulateX2:
-                        gl.glBlendFunc(GL.GL_DST_COLOR, GL.GL_SRC_COLOR);
-                        break;
-                    case Color:
-                    case Screen:
-                        gl.glBlendFunc(GL.GL_ONE, GL.GL_ONE_MINUS_SRC_COLOR);
-                        break;
-                    case Exclusion:
-                        gl.glBlendFunc(GL.GL_ONE_MINUS_DST_COLOR, GL.GL_ONE_MINUS_SRC_COLOR);
-                        break;
-                    case Custom:
-                        gl.glBlendFuncSeparate(
-                            convertBlendFunc(state.getCustomSfactorRGB()),
-                            convertBlendFunc(state.getCustomDfactorRGB()),
-                            convertBlendFunc(state.getCustomSfactorAlpha()),
-                            convertBlendFunc(state.getCustomDfactorAlpha()));
-                        break;
-                    default:
-                        throw new UnsupportedOperationException("Unrecognized blend mode: "
-                                + state.getBlendMode());
-                }
-                
-                if (state.getBlendEquation() != context.blendEquation || state.getBlendEquationAlpha() != context.blendEquationAlpha) {
-                    int colorMode = convertBlendEquation(state.getBlendEquation());
-                    int alphaMode;
-                    if (state.getBlendEquationAlpha() == RenderState.BlendEquationAlpha.InheritColor) {
-                        alphaMode = colorMode;
-                    } else {
-                        alphaMode = convertBlendEquationAlpha(state.getBlendEquationAlpha());
-                    }
-                    gl.glBlendEquationSeparate(colorMode, alphaMode);
-                    context.blendEquation = state.getBlendEquation();
-                    context.blendEquationAlpha = state.getBlendEquationAlpha();
-                }
+        // Always update the blend equations and factors when using custom blend mode.
+        if (state.getBlendMode() == BlendMode.Custom) {
+            changeBlendMode(BlendMode.Custom);
+
+            blendFuncSeparate(
+                    state.getCustomSfactorRGB(),
+                    state.getCustomDfactorRGB(),
+                    state.getCustomSfactorAlpha(),
+                    state.getCustomDfactorAlpha());
+            blendEquationSeparate(state.getBlendEquation(), state.getBlendEquationAlpha());
+
+        // Update the blend equations and factors only on a mode change for all the other (common) blend modes.
+        } else if (state.getBlendMode() != context.blendMode) {
+            changeBlendMode(state.getBlendMode());
+
+            switch (state.getBlendMode()) {
+                case Off:
+                    break;
+                case Additive:
+                    blendFunc(RenderState.BlendFunc.One, RenderState.BlendFunc.One);
+                    break;
+                case AlphaAdditive:
+                    blendFunc(RenderState.BlendFunc.Src_Alpha, RenderState.BlendFunc.One);
+                    break;
+                case Alpha:
+                    blendFunc(RenderState.BlendFunc.Src_Alpha, RenderState.BlendFunc.One_Minus_Src_Alpha);
+                    break;
+                case PremultAlpha:
+                    blendFunc(RenderState.BlendFunc.One, RenderState.BlendFunc.One_Minus_Src_Alpha);
+                    break;
+                case Modulate:
+                    blendFunc(RenderState.BlendFunc.Dst_Color, RenderState.BlendFunc.Zero);
+                    break;
+                case ModulateX2:
+                    blendFunc(RenderState.BlendFunc.Dst_Color, RenderState.BlendFunc.Src_Color);
+                    break;
+                case Color:
+                case Screen:
+                    blendFunc(RenderState.BlendFunc.One, RenderState.BlendFunc.One_Minus_Src_Color);
+                    break;
+                case Exclusion:
+                    blendFunc(RenderState.BlendFunc.One_Minus_Dst_Color, RenderState.BlendFunc.One_Minus_Src_Color);
+                    break;
+                default:
+                    throw new UnsupportedOperationException("Unrecognized blend mode: "
+                            + state.getBlendMode());
             }
 
-            context.blendMode = state.getBlendMode();
+            // All of the common modes requires the ADD equation.
+            // (This might change in the future?)
+            blendEquationSeparate(RenderState.BlendEquation.Add, RenderState.BlendEquationAlpha.InheritColor);
         }
 
         if (context.stencilTest != state.isStencilTest()
@@ -852,6 +860,65 @@ public final class GLRenderer implements Renderer {
         }
     }
 
+    private void changeBlendMode(RenderState.BlendMode blendMode) {
+        if (blendMode != context.blendMode) {
+            if (blendMode == RenderState.BlendMode.Off) {
+                gl.glDisable(GL.GL_BLEND);
+            } else if (context.blendMode == RenderState.BlendMode.Off) {
+                gl.glEnable(GL.GL_BLEND);
+            }
+
+            context.blendMode = blendMode;
+        }
+    }
+
+    private void blendEquationSeparate(RenderState.BlendEquation blendEquation, RenderState.BlendEquationAlpha blendEquationAlpha) {
+        if (blendEquation != context.blendEquation || blendEquationAlpha != context.blendEquationAlpha) {
+            int glBlendEquation = convertBlendEquation(blendEquation);
+            int glBlendEquationAlpha = blendEquationAlpha == RenderState.BlendEquationAlpha.InheritColor
+                    ? glBlendEquation
+                    : convertBlendEquationAlpha(blendEquationAlpha);
+            gl.glBlendEquationSeparate(glBlendEquation, glBlendEquationAlpha);
+            context.blendEquation = blendEquation;
+            context.blendEquationAlpha = blendEquationAlpha;
+        }
+    }
+
+    private void blendFunc(RenderState.BlendFunc sfactor, RenderState.BlendFunc dfactor) {
+        if (sfactor != context.sfactorRGB
+                || dfactor != context.dfactorRGB
+                || sfactor != context.sfactorAlpha
+                || dfactor != context.dfactorAlpha) {
+
+            gl.glBlendFunc(
+                    convertBlendFunc(sfactor),
+                    convertBlendFunc(dfactor));
+            context.sfactorRGB = sfactor;
+            context.dfactorRGB = dfactor;
+            context.sfactorAlpha = sfactor;
+            context.dfactorAlpha = dfactor;
+        }
+    }
+
+    private void blendFuncSeparate(RenderState.BlendFunc sfactorRGB, RenderState.BlendFunc dfactorRGB,
+            RenderState.BlendFunc sfactorAlpha, RenderState.BlendFunc dfactorAlpha) {
+        if (sfactorRGB != context.sfactorRGB
+                || dfactorRGB != context.dfactorRGB
+                || sfactorAlpha != context.sfactorAlpha
+                || dfactorAlpha != context.dfactorAlpha) {
+
+            gl.glBlendFuncSeparate(
+                    convertBlendFunc(sfactorRGB),
+                    convertBlendFunc(dfactorRGB),
+                    convertBlendFunc(sfactorAlpha),
+                    convertBlendFunc(dfactorAlpha));
+            context.sfactorRGB = sfactorRGB;
+            context.dfactorRGB = dfactorRGB;
+            context.sfactorAlpha = sfactorAlpha;
+            context.dfactorAlpha = dfactorAlpha;
+        }
+    }
+
     private int convertBlendEquation(RenderState.BlendEquation blendEquation) {
         switch (blendEquation) {
             case Add:
@@ -1001,12 +1068,25 @@ public final class GLRenderer implements Renderer {
         }
     }
 
+    @Override
     public void postFrame() {
         objManager.deleteUnused(this);
         OpenCLObjectManager.getInstance().deleteUnusedObjects();
         gl.resetStats();
     }
 
+    protected void bindProgram(Shader shader) {
+        int shaderId = shader.getId();
+        if (context.boundShaderProgram != shaderId) {
+            gl.glUseProgram(shaderId);
+            statistics.onShaderUse(shader, true);
+            context.boundShader = shader;
+            context.boundShaderProgram = shaderId;
+        } else {
+            statistics.onShaderUse(shader, false);
+        }
+    }
+
     /*********************************************************************\
      |* Shaders                                                           *|
      \*********************************************************************/
@@ -1021,18 +1101,6 @@ public final class GLRenderer implements Renderer {
         }
     }
 
-    protected void bindProgram(Shader shader) {
-        int shaderId = shader.getId();
-        if (context.boundShaderProgram != shaderId) {
-            gl.glUseProgram(shaderId);
-            statistics.onShaderUse(shader, true);
-            context.boundShader = shader;
-            context.boundShaderProgram = shaderId;
-        } else {
-            statistics.onShaderUse(shader, false);
-        }
-    }
-
     protected void updateUniform(Shader shader, Uniform uniform) {
         int shaderId = shader.getId();
 
@@ -1138,6 +1206,58 @@ public final class GLRenderer implements Renderer {
         }
     }
 
+    /**
+     * Updates the buffer block for the shader.
+     *
+     * @param shader the shader.
+     * @param bufferBlock the storage block.
+     */
+    protected void updateShaderBufferBlock(final Shader shader, final ShaderBufferBlock bufferBlock) {
+
+        assert bufferBlock.getName() != null;
+        assert shader.getId() > 0;
+
+        final BufferObject bufferObject = bufferBlock.getBufferObject();
+        if (bufferObject.getUniqueId() == -1 || bufferObject.isUpdateNeeded()) {
+            updateBufferData(bufferObject);
+        }
+
+        if (!bufferBlock.isUpdateNeeded()) {
+            return;
+        }
+
+        bindProgram(shader);
+
+        final int shaderId = shader.getId();
+        final BufferObject.BufferType bufferType = bufferObject.getBufferType();
+
+        bindBuffer(bufferBlock, bufferObject, shaderId, bufferType);
+
+        bufferBlock.clearUpdateNeeded();
+    }
+
+    private void bindBuffer(final ShaderBufferBlock bufferBlock, final BufferObject bufferObject, final int shaderId,
+                            final BufferObject.BufferType bufferType) {
+
+        switch (bufferType) {
+            case UniformBufferObject: {
+                final int blockIndex = gl3.glGetUniformBlockIndex(shaderId, bufferBlock.getName());
+                gl3.glBindBufferBase(GL3.GL_UNIFORM_BUFFER, bufferObject.getBinding(), bufferObject.getId());
+                gl3.glUniformBlockBinding(GL3.GL_UNIFORM_BUFFER, blockIndex, bufferObject.getBinding());
+                break;
+            }
+            case ShaderStorageBufferObject: {
+                final int blockIndex = gl4.glGetProgramResourceIndex(shaderId, GL4.GL_SHADER_STORAGE_BLOCK, bufferBlock.getName());
+                gl4.glShaderStorageBlockBinding(shaderId, blockIndex, bufferObject.getBinding());
+                gl4.glBindBufferBase(GL4.GL_SHADER_STORAGE_BUFFER, bufferObject.getBinding(), bufferObject.getId());
+                break;
+            }
+            default: {
+                throw new IllegalArgumentException("Doesn't support binding of " + bufferType);
+            }
+        }
+    }
+
     protected void updateShaderUniforms(Shader shader) {
         ListMap<String, Uniform> uniforms = shader.getUniformMap();
         for (int i = 0; i < uniforms.size(); i++) {
@@ -1148,6 +1268,18 @@ public final class GLRenderer implements Renderer {
         }
     }
 
+    /**
+     * Updates all shader's buffer blocks.
+     *
+     * @param shader the shader.
+     */
+    protected void updateShaderBufferBlocks(final Shader shader) {
+        final ListMap<String, ShaderBufferBlock> bufferBlocks = shader.getBufferBlockMap();
+        for (int i = 0; i < bufferBlocks.size(); i++) {
+            updateShaderBufferBlock(shader, bufferBlocks.getValue(i));
+        }
+    }
+
     protected void resetUniformLocations(Shader shader) {
         ListMap<String, Uniform> uniforms = shader.getUniformMap();
         for (int i = 0; i < uniforms.size(); i++) {
@@ -1195,6 +1327,7 @@ public final class GLRenderer implements Renderer {
                     + "Only GLSL 1.00 shaders are supported.");
         }
 
+        boolean insertPrecision = false;
         // Upload shader source.
         // Merge the defines and source code.
         stringBuf.setLength(0);
@@ -1214,7 +1347,7 @@ public final class GLRenderer implements Renderer {
                     
                     if (source.getType() == ShaderType.Fragment) {
                         // GLES2 requires precision qualifier.
-                        stringBuf.append("precision mediump float;\n");
+                        insertPrecision = true;
                     }
                 } else {
                     // version 100 does not exist in desktop GLSL.
@@ -1233,6 +1366,14 @@ public final class GLRenderer implements Renderer {
         stringBuf.append(source.getDefines());
         stringBuf.append(source.getSource());
 
+        if(insertPrecision){
+            // precision token is not a preprocessor dirrective therefore it must be placed after #extension tokens to avoid
+            // Error P0001: Extension directive must occur before any non-preprocessor tokens
+            int idx = stringBuf.lastIndexOf("#extension");
+            idx = stringBuf.indexOf("\n", idx);
+            stringBuf.insert(idx + 1, "precision mediump float;\n");
+        }
+
         intBuf1.clear();
         intBuf1.put(0, stringBuf.length());
         gl.glShaderSource(id, new String[]{ stringBuf.toString() }, intBuf1);
@@ -1366,6 +1507,7 @@ public final class GLRenderer implements Renderer {
             assert shader.getId() > 0;
 
             updateShaderUniforms(shader);
+            updateShaderBufferBlocks(shader);
             bindProgram(shader);
         }
     }
@@ -2454,6 +2596,58 @@ public final class GLRenderer implements Renderer {
         vb.clearUpdateNeeded();
     }
 
+    @Override
+    public void updateBufferData(final BufferObject bo) {
+
+        int maxSize = Integer.MAX_VALUE;
+
+        final BufferObject.BufferType bufferType = bo.getBufferType();
+
+        if (!caps.contains(bufferType.getRequiredCaps())) {
+            throw new IllegalArgumentException("The current video hardware doesn't support " + bufferType);
+        }
+
+        final ByteBuffer data = bo.computeData(maxSize);
+        if (data == null) {
+            throw new IllegalArgumentException("Can't upload BO without data.");
+        }
+
+        int bufferId = bo.getId();
+        if (bufferId == -1) {
+
+            // create buffer
+            intBuf1.clear();
+            gl.glGenBuffers(intBuf1);
+            bufferId = intBuf1.get(0);
+
+            bo.setId(bufferId);
+
+            objManager.registerObject(bo);
+        }
+
+        data.rewind();
+
+        switch (bufferType) {
+            case UniformBufferObject: {
+                gl3.glBindBuffer(GL3.GL_UNIFORM_BUFFER, bufferId);
+                gl3.glBufferData(GL4.GL_UNIFORM_BUFFER, data, GL3.GL_DYNAMIC_DRAW);
+                gl3.glBindBuffer(GL4.GL_UNIFORM_BUFFER, 0);
+                break;
+            }
+            case ShaderStorageBufferObject: {
+                gl4.glBindBuffer(GL4.GL_SHADER_STORAGE_BUFFER, bufferId);
+                gl4.glBufferData(GL4.GL_SHADER_STORAGE_BUFFER, data, GL4.GL_DYNAMIC_COPY);
+                gl4.glBindBuffer(GL4.GL_SHADER_STORAGE_BUFFER, 0);
+                break;
+            }
+            default: {
+                throw new IllegalArgumentException("Doesn't support binding of " + bufferType);
+            }
+        }
+
+        bo.clearUpdateNeeded();
+    }
+
     public void deleteBuffer(VertexBuffer vb) {
         int bufId = vb.getId();
         if (bufId != -1) {
@@ -2467,6 +2661,23 @@ public final class GLRenderer implements Renderer {
         }
     }
 
+    @Override
+    public void deleteBuffer(final BufferObject bo) {
+
+        int bufferId = bo.getId();
+        if (bufferId == -1) {
+            return;
+        }
+
+        intBuf1.clear();
+        intBuf1.put(bufferId);
+        intBuf1.flip();
+
+        gl.glDeleteBuffers(intBuf1);
+
+        bo.resetObject();
+    }
+
     public void clearVertexAttribs() {
         IDList attribList = context.attribIndexList;
         for (int i = 0; i < attribList.oldLen; i++) {

+ 86 - 1
jme3-core/src/main/java/com/jme3/scene/Geometry.java

@@ -43,6 +43,7 @@ import com.jme3.material.Material;
 import com.jme3.math.Matrix4f;
 import com.jme3.renderer.Camera;
 import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.scene.mesh.MorphTarget;
 import com.jme3.util.TempVars;
 import com.jme3.util.clone.Cloner;
 import com.jme3.util.clone.IdentityCloneFunction;
@@ -86,6 +87,16 @@ public class Geometry extends Spatial {
      */
     protected int startIndex = -1;
 
+    /**
+     * Morph state variable for morph animation
+     */
+    private float[] morphState;
+    private boolean dirtyMorph = true;
+    // a Morph target that will be used to merge all targets that
+    // can't be handled on the cpu on each frame.
+    private MorphTarget fallbackMorphTarget;
+    private int nbSimultaneousGPUMorph = -1;
+
     /**
      * Serialization only. Do not use.
      */
@@ -248,7 +259,7 @@ public class Geometry extends Spatial {
     @Override
     public void setMaterial(Material material) {
         this.material = material;
-
+        nbSimultaneousGPUMorph = -1;
         if (isGrouped()) {
             groupNode.onMaterialChange(this);
         }
@@ -576,6 +587,80 @@ public class Geometry extends Spatial {
         this.material = cloner.clone(material);
     }
 
+    public void setMorphState(float[] state) {
+        if (mesh == null || mesh.getMorphTargets().length == 0){
+            return;
+        }
+
+        int nbMorphTargets = mesh.getMorphTargets().length;
+
+        if (morphState == null) {
+            morphState = new float[nbMorphTargets];
+        }
+        System.arraycopy(state, 0, morphState, 0, morphState.length);
+        this.dirtyMorph = true;
+    }
+
+    /**
+     * returns true if the morph state has changed on the last frame.
+     * @return
+     */
+    public boolean isDirtyMorph() {
+        return dirtyMorph;
+    }
+
+    /**
+     * Seting this to true will stop this geometry morph buffer to be updated,
+     * unless the morph state changes
+     * @param dirtyMorph
+     */
+    public void setDirtyMorph(boolean dirtyMorph) {
+        this.dirtyMorph = dirtyMorph;
+    }
+
+    /**
+     * returns the morph state of this Geometry.
+     * Used internally by the MorphControl.
+     * @return
+     */
+    public float[] getMorphState() {
+        if (morphState == null) {
+            morphState = new float[mesh.getMorphTargets().length];
+        }
+        return morphState;
+    }
+
+    /**
+     * Return the number of morph targets that can be handled on the GPU simultaneously for this geometry.
+     * Note that it depends on the material set on this geometry.
+     * This number is computed and set by the MorphControl, so it might be available only after the first frame.
+     * Else it's set to -1.
+     * @return the number of simultaneous morph targets handled on the GPU
+     */
+    public int getNbSimultaneousGPUMorph() {
+        return nbSimultaneousGPUMorph;
+    }
+
+    /**
+     * Sets the number of morph targets that can be handled on the GPU simultaneously for this geometry.
+     * Note that it depends on the material set on this geometry.
+     * This number is computed and set by the MorphControl, so it might be available only after the first frame.
+     * Else it's set to -1.
+     * WARNING: setting this manually might crash the shader compilation if set too high. Do it at your own risk.
+     * @param nbSimultaneousGPUMorph the number of simultaneous morph targets to be handled on the GPU.
+     */
+    public void setNbSimultaneousGPUMorph(int nbSimultaneousGPUMorph) {
+        this.nbSimultaneousGPUMorph = nbSimultaneousGPUMorph;
+    }
+
+    public MorphTarget getFallbackMorphTarget() {
+        return fallbackMorphTarget;
+    }
+
+    public void setFallbackMorphTarget(MorphTarget fallbackMorphTarget) {
+        this.fallbackMorphTarget = fallbackMorphTarget;
+    }
+
     @Override
     public void write(JmeExporter ex) throws IOException {
         super.write(ex);

+ 79 - 42
jme3-core/src/main/java/com/jme3/scene/Mesh.java

@@ -39,24 +39,18 @@ import com.jme3.collision.bih.BIHTree;
 import com.jme3.export.*;
 import com.jme3.material.Material;
 import com.jme3.material.RenderState;
-import com.jme3.math.Matrix4f;
-import com.jme3.math.Triangle;
-import com.jme3.math.Vector2f;
-import com.jme3.math.Vector3f;
-import com.jme3.scene.VertexBuffer.Format;
-import com.jme3.scene.VertexBuffer.Type;
-import com.jme3.scene.VertexBuffer.Usage;
+import com.jme3.math.*;
+import com.jme3.scene.VertexBuffer.*;
 import com.jme3.scene.mesh.*;
-import com.jme3.util.BufferUtils;
-import com.jme3.util.IntMap;
+import com.jme3.util.*;
 import com.jme3.util.IntMap.Entry;
-import com.jme3.util.SafeArrayList;
 import com.jme3.util.clone.Cloner;
 import com.jme3.util.clone.JmeCloneable;
 
 import java.io.IOException;
 import java.nio.*;
 import java.util.ArrayList;
+import java.util.Arrays;
 
 /**
  * <code>Mesh</code> is used to store rendering data.
@@ -171,8 +165,8 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
 
     private CollisionData collisionTree = null;
 
-    private SafeArrayList<VertexBuffer> buffersList = new SafeArrayList<VertexBuffer>(VertexBuffer.class);
-    private IntMap<VertexBuffer> buffers = new IntMap<VertexBuffer>();
+    private SafeArrayList<VertexBuffer> buffersList = new SafeArrayList<>(VertexBuffer.class);
+    private IntMap<VertexBuffer> buffers = new IntMap<>();
     private VertexBuffer[] lodLevels;
     private float pointSize = 1;
     private float lineWidth = 1;
@@ -190,6 +184,8 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
 
     private Mode mode = Mode.Triangles;
 
+    private SafeArrayList<MorphTarget> morphTargets;
+
     /**
      * Creates a new mesh with no {@link VertexBuffer vertex buffers}.
      */
@@ -210,7 +206,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
             clone.meshBound = meshBound.clone();
             clone.collisionTree = collisionTree != null ? collisionTree : null;
             clone.buffers = buffers.clone();
-            clone.buffersList = new SafeArrayList<VertexBuffer>(VertexBuffer.class,buffersList);
+            clone.buffersList = new SafeArrayList<>(VertexBuffer.class, buffersList);
             clone.vertexArrayID = -1;
             if (elementLengths != null) {
                 clone.elementLengths = elementLengths.clone();
@@ -240,8 +236,8 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
             //clone.collisionTree = collisionTree != null ? collisionTree : null;
             clone.collisionTree = null; // it will get re-generated in any case
 
-            clone.buffers = new IntMap<VertexBuffer>();
-            clone.buffersList = new SafeArrayList<VertexBuffer>(VertexBuffer.class);
+            clone.buffers = new IntMap<>();
+            clone.buffersList = new SafeArrayList<>(VertexBuffer.class);
             for (VertexBuffer vb : buffersList.getArray()){
                 VertexBuffer bufClone = vb.clone();
                 clone.buffers.put(vb.getBufferType().ordinal(), bufClone);
@@ -704,7 +700,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
      */
     @Deprecated
     public void setInterleaved(){
-        ArrayList<VertexBuffer> vbs = new ArrayList<VertexBuffer>();
+        ArrayList<VertexBuffer> vbs = new ArrayList<>();
         vbs.addAll(buffersList);
 
 //        ArrayList<VertexBuffer> vbs = new ArrayList<VertexBuffer>(buffers.values());
@@ -827,8 +823,9 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
      * {@link #setInterleaved() interleaved} format.
      */
     public void updateCounts(){
-        if (getBuffer(Type.InterleavedData) != null)
+        if (getBuffer(Type.InterleavedData) != null) {
             throw new IllegalStateException("Should update counts before interleave");
+        }
 
         VertexBuffer pb = getBuffer(Type.Position);
         VertexBuffer ib = getBuffer(Type.Index);
@@ -851,11 +848,13 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
      */
     public int getTriangleCount(int lod){
         if (lodLevels != null){
-            if (lod < 0)
+            if (lod < 0) {
                 throw new IllegalArgumentException("LOD level cannot be < 0");
+            }
 
-            if (lod >= lodLevels.length)
-                throw new IllegalArgumentException("LOD level "+lod+" does not exist!");
+            if (lod >= lodLevels.length) {
+                throw new IllegalArgumentException("LOD level " + lod + " does not exist!");
+            }
 
             return computeNumElements(lodLevels[lod].getData().limit());
         }else if (lod == 0){
@@ -975,8 +974,9 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
      * Sets the mesh's VAO ID. Internal use only.
      */
     public void setId(int id){
-        if (vertexArrayID != -1)
+        if (vertexArrayID != -1) {
             throw new IllegalStateException("ID has already been set.");
+        }
 
         vertexArrayID = id;
     }
@@ -1044,8 +1044,9 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
      * @throws IllegalArgumentException If the buffer type is already set
      */
     public void setBuffer(VertexBuffer vb){
-        if (buffers.containsKey(vb.getBufferType().ordinal()))
-            throw new IllegalArgumentException("Buffer type already set: "+vb.getBufferType());
+        if (buffers.containsKey(vb.getBufferType().ordinal())) {
+            throw new IllegalArgumentException("Buffer type already set: " + vb.getBufferType());
+        }
 
         buffers.put(vb.getBufferType().ordinal(), vb);
         buffersList.add(vb);
@@ -1158,8 +1159,9 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
      */
     public FloatBuffer getFloatBuffer(Type type) {
         VertexBuffer vb = getBuffer(type);
-        if (vb == null)
+        if (vb == null) {
             return null;
+        }
 
         return (FloatBuffer) vb.getData();
     }
@@ -1173,8 +1175,9 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
      */
     public ShortBuffer getShortBuffer(Type type) {
         VertexBuffer vb = getBuffer(type);
-        if (vb == null)
+        if (vb == null) {
             return null;
+        }
 
         return (ShortBuffer) vb.getData();
     }
@@ -1186,8 +1189,9 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
      * @return A virtual or wrapped index buffer to read the data as a list
      */
     public IndexBuffer getIndicesAsList(){
-        if (mode == Mode.Hybrid)
+        if (mode == Mode.Hybrid) {
             throw new UnsupportedOperationException("Hybrid mode not supported");
+        }
 
         IndexBuffer ib = getIndexBuffer();
         if (ib != null){
@@ -1216,8 +1220,9 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
      */
     public IndexBuffer getIndexBuffer() {
         VertexBuffer vb = getBuffer(Type.Index);
-        if (vb == null)
+        if (vb == null) {
             return null;
+        }
 
         return IndexBuffer.wrapIndexBuffer(vb.getData());
     }
@@ -1240,8 +1245,8 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
         IndexBuffer indexBuf = getIndexBuffer();
         int numIndices = indexBuf.size();
 
-        IntMap<Integer> oldIndicesToNewIndices = new IntMap<Integer>(numIndices);
-        ArrayList<Integer> newIndicesToOldIndices = new ArrayList<Integer>();
+        IntMap<Integer> oldIndicesToNewIndices = new IntMap<>(numIndices);
+        ArrayList<Integer> newIndicesToOldIndices = new ArrayList<>();
         int newIndex = 0;
 
         for (int i = 0; i < numIndices; i++) {
@@ -1352,14 +1357,17 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
      */
     public void scaleTextureCoordinates(Vector2f scaleFactor){
         VertexBuffer tc = getBuffer(Type.TexCoord);
-        if (tc == null)
+        if (tc == null) {
             throw new IllegalStateException("The mesh has no texture coordinates");
+        }
 
-        if (tc.getFormat() != VertexBuffer.Format.Float)
+        if (tc.getFormat() != VertexBuffer.Format.Float) {
             throw new UnsupportedOperationException("Only float texture coord format is supported");
+        }
 
-        if (tc.getNumComponents() != 2)
+        if (tc.getNumComponents() != 2) {
             throw new UnsupportedOperationException("Only 2D texture coords are supported");
+        }
 
         FloatBuffer fb = (FloatBuffer) tc.getData();
         fb.clear();
@@ -1446,13 +1454,23 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
                getBuffer(Type.HWBoneIndex) != null;
     }
 
+    /**
+     * @deprecated use isAnimatedByJoint
+     * @param boneIndex
+     * @return
+     */
+    @Deprecated
+    public boolean isAnimatedByBone(int boneIndex) {
+        return isAnimatedByJoint(boneIndex);
+    }
+
     /**
      * Test whether the specified bone animates this mesh.
      *
-     * @param boneIndex the bone's index in its skeleton
+     * @param jointIndex the bone's index in its skeleton
      * @return true if the specified bone animates this mesh, otherwise false
      */
-    public boolean isAnimatedByBone(int boneIndex) {
+    public boolean isAnimatedByJoint(int jointIndex) {
         VertexBuffer biBuf = getBuffer(VertexBuffer.Type.BoneIndex);
         VertexBuffer wBuf = getBuffer(VertexBuffer.Type.BoneWeight);
         if (biBuf == null || wBuf == null) {
@@ -1472,7 +1490,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
         /*
          * Test each vertex to determine whether the bone affects it.
          */
-        int biByte = boneIndex;
+        int biByte = jointIndex;
         for (int vIndex = 0; vIndex < numVertices; vIndex++) {
             for (int wIndex = 0; wIndex < 4; wIndex++) {
                 int bIndex = boneIndexBuffer.get();
@@ -1501,16 +1519,26 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
         return patchVertexCount;
     }
 
+
+    public void addMorphTarget(MorphTarget target) {
+        if (morphTargets == null) {
+            morphTargets = new SafeArrayList<>(MorphTarget.class);
+        }
+        morphTargets.add(target);
+    }
+
+    public MorphTarget[] getMorphTargets() {
+        return morphTargets.getArray();
+    }
+
+    public boolean hasMorphTargets() {
+        return morphTargets != null && !morphTargets.isEmpty();
+    }
+
+    @Override
     public void write(JmeExporter ex) throws IOException {
         OutputCapsule out = ex.getCapsule(this);
 
-//        HashMap<String, VertexBuffer> map = new HashMap<String, VertexBuffer>();
-//        for (Entry<VertexBuffer> buf : buffers){
-//            if (buf.getValue() != null)
-//                map.put(buf.getKey()+"a", buf.getValue());
-//        }
-//        out.writeStringSavableMap(map, "buffers", null);
-
         out.write(meshBound, "modelBound", null);
         out.write(vertCount, "vertCount", -1);
         out.write(elementCount, "elementCount", -1);
@@ -1545,8 +1573,12 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
         }
 
         out.write(lodLevels, "lodLevels", null);
+        if (morphTargets != null) {
+            out.writeSavableArrayList(new ArrayList(morphTargets), "morphTargets", null);
+        }
     }
 
+    @Override
     public void read(JmeImporter im) throws IOException {
         InputCapsule in = im.getCapsule(this);
         meshBound = (BoundingVolume) in.readSavable("modelBound", null);
@@ -1583,6 +1615,11 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
             lodLevels = new VertexBuffer[lodLevelsSavable.length];
             System.arraycopy( lodLevelsSavable, 0, lodLevels, 0, lodLevels.length);
         }
+
+        ArrayList<Savable> l = in.readSavableArrayList("morphTargets", null);
+        if (l != null) {
+            morphTargets = new SafeArrayList(MorphTarget.class, l);
+        }
     }
 
 }

+ 3 - 2
jme3-core/src/main/java/com/jme3/scene/Spatial.java

@@ -31,6 +31,7 @@
  */
 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;
@@ -67,7 +68,7 @@ import java.util.logging.Logger;
  * @author Joshua Slack
  * @version $Revision: 4075 $, $Data$
  */
-public abstract class Spatial implements Savable, Cloneable, Collidable, CloneableSmartAsset, JmeCloneable {
+public abstract class Spatial implements Savable, Cloneable, Collidable, CloneableSmartAsset, JmeCloneable, HasLocalTransform {
 
     private static final Logger logger = Logger.getLogger(Spatial.class.getName());
 
@@ -1792,4 +1793,4 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
     }
 
     protected abstract void breadthFirstTraversal(SceneGraphVisitor visitor, Queue<Spatial> queue);
-}
+}

+ 58 - 12
jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java

@@ -212,7 +212,42 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable {
          * Format should be {@link Format#Float} and number of components
          * should be 16.
          */
-        InstanceData
+        InstanceData,
+
+        /**
+         * Morph animations targets.
+         * Supports up tp 14 morph target buffers at the same time
+         * Limited due to the limited number of attributes you can bind to a vertex shader usually 16
+         * <p>
+         * MorphTarget buffers are either POSITION, NORMAL or TANGENT buffers.
+         * So we can support up to
+         * 14 simultaneous POSITION targets
+         * 7 simultaneous POSITION and NORMAL targets
+         * 4 simultaneous POSTION, NORMAL and TANGENT targets.
+         * <p>
+         * Note that the MorphControl will find how many buffers can be supported for each mesh/material combination.
+         * Note that all buffers have 3 components (Vector3f) even the Tangent buffer that
+         * does not contain the w (handedness) component that will not be interpolated for morph animation.
+         * <p>
+         * Note that those buffers contain the difference between the base buffer (POSITION, NORMAL or TANGENT) and the target value
+         * So that you can interpolate with a MADD operation in the vertex shader
+         * position = weight * diffPosition + basePosition;
+         */
+        MorphTarget0,
+        MorphTarget1,
+        MorphTarget2,
+        MorphTarget3,
+        MorphTarget4,
+        MorphTarget5,
+        MorphTarget6,
+        MorphTarget7,
+        MorphTarget8,
+        MorphTarget9,
+        MorphTarget10,
+        MorphTarget11,
+        MorphTarget12,
+        MorphTarget13,
+
     }
 
     /**
@@ -241,7 +276,7 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable {
          * Mesh data is <em>not</em> sent to GPU at all. It is only
          * used by the CPU.
          */
-        CpuOnly;
+        CpuOnly
     }
 
     /**
@@ -606,9 +641,13 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable {
      * @return The total number of data elements in the data buffer.
      */
     public int getNumElements(){
+        if( data == null ) {
+            return 0;
+        }
         int elements = data.limit() / components;
-        if (format == Format.Half)
+        if (format == Format.Half) {
             elements /= 2;
+        }
         return elements;
     }
 
@@ -639,14 +678,17 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable {
      * argument.
      */
     public void setupData(Usage usage, int components, Format format, Buffer data){
-        if (id != -1)
+        if (id != -1) {
             throw new UnsupportedOperationException("Data has already been sent. Cannot setupData again.");
+        }
 
-        if (usage == null || format == null || data == null)
+        if (usage == null || format == null || data == null) {
             throw new IllegalArgumentException("None of the arguments can be null");
-            
-        if (data.isReadOnly()) 
-            throw new IllegalArgumentException( "VertexBuffer data cannot be read-only." );
+        }
+
+        if (data.isReadOnly()) {
+            throw new IllegalArgumentException("VertexBuffer data cannot be read-only.");
+        }
 
         if (bufType != Type.InstanceData) {
             if (components < 1 || components > 4) {
@@ -717,11 +759,13 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable {
      * Converts single floating-point data to {@link Format#Half half} floating-point data.
      */
     public void convertToHalf(){
-        if (id != -1)
+        if (id != -1) {
             throw new UnsupportedOperationException("Data has already been sent.");
+        }
 
-        if (format != Format.Float)
+        if (format != Format.Float) {
             throw new IllegalStateException("Format must be float!");
+        }
 
         int numElements = data.limit() / components;
         format = Format.Half;
@@ -910,8 +954,9 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable {
      * match.
      */
     public void copyElements(int inIndex, VertexBuffer outVb, int outIndex, int len){
-        if (outVb.format != format || outVb.components != components)
+        if (outVb.format != format || outVb.components != components) {
             throw new IllegalArgumentException("Buffer format mismatch. Cannot copy");
+        }
 
         int inPos  = inIndex  * components;
         int outPos = outIndex * components;
@@ -978,8 +1023,9 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable {
      * of elements with the given number of components in each element.
      */
     public static Buffer createBuffer(Format format, int components, int numElements){
-        if (components < 1 || components > 4)
+        if (components < 1 || components > 4) {
             throw new IllegalArgumentException("Num components must be between 1 and 4");
+        }
 
         int total = numElements * components;
 

+ 26 - 16
jme3-core/src/main/java/com/jme3/scene/debug/WireFrustum.java

@@ -42,29 +42,39 @@ import java.nio.FloatBuffer;
 public class WireFrustum extends Mesh {
 
     public WireFrustum(Vector3f[] points){
+        initGeom(this, points);
+    }
+
+    public static Mesh makeFrustum(Vector3f[] points){
+        Mesh m = new Mesh();
+        initGeom(m, points);
+        return m;
+    }
+
+    private static void initGeom(Mesh m, Vector3f[] points) {
         if (points != null)
-            setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(points));
+            m.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(points));
 
-        setBuffer(Type.Index, 2,
+        m.setBuffer(Type.Index, 2,
                 new short[]{
-                     0, 1,
-                     1, 2,
-                     2, 3,
-                     3, 0,
+                        0, 1,
+                        1, 2,
+                        2, 3,
+                        3, 0,
 
-                     4, 5,
-                     5, 6,
-                     6, 7,
-                     7, 4,
+                        4, 5,
+                        5, 6,
+                        6, 7,
+                        7, 4,
 
-                     0, 4,
-                     1, 5,
-                     2, 6,
-                     3, 7,
+                        0, 4,
+                        1, 5,
+                        2, 6,
+                        3, 7,
                 }
         );
-        getBuffer(Type.Index).setUsage(Usage.Static);
-        setMode(Mode.Lines);
+        m.getBuffer(Type.Index).setUsage(Usage.Static);
+        m.setMode(Mode.Lines);
     }
 
     public void update(Vector3f[] points){

+ 208 - 0
jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java

@@ -0,0 +1,208 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.jme3.scene.debug.custom;
+
+import com.jme3.anim.*;
+import com.jme3.app.Application;
+import com.jme3.app.state.BaseAppState;
+import com.jme3.collision.CollisionResults;
+import com.jme3.input.KeyInput;
+import com.jme3.input.MouseInput;
+import com.jme3.input.controls.*;
+import com.jme3.light.DirectionalLight;
+import com.jme3.math.*;
+import com.jme3.renderer.ViewPort;
+import com.jme3.scene.*;
+
+import java.util.*;
+
+/**
+ * @author Nehon
+ */
+public class ArmatureDebugAppState extends BaseAppState {
+
+    public static final float CLICK_MAX_DELAY = 0.2f;
+    private Node debugNode = new Node("debugNode");
+    private Map<Armature, ArmatureDebugger> armatures = new HashMap<>();
+    private Map<Armature, Joint> selectedBones = new HashMap<>();
+    private Application app;
+    private boolean displayAllJoints = false;
+    private float clickDelay = -1;
+    Vector3f tmp = new Vector3f();
+    Vector3f tmp2 = new Vector3f();
+    ViewPort vp;
+
+    @Override
+    protected void initialize(Application app) {
+        vp = app.getRenderManager().createMainView("debug", app.getCamera());
+        vp.attachScene(debugNode);
+        vp.setClearDepth(true);
+        this.app = app;
+        for (ArmatureDebugger armatureDebugger : armatures.values()) {
+            armatureDebugger.initialize(app.getAssetManager(), app.getCamera());
+        }
+        app.getInputManager().addListener(actionListener, "shoot", "toggleJoints");
+        app.getInputManager().addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT), new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
+        app.getInputManager().addMapping("toggleJoints", new KeyTrigger(KeyInput.KEY_F10));
+
+        debugNode.addLight(new DirectionalLight(new Vector3f(-1f, -1f, -1f).normalizeLocal()));
+
+        debugNode.addLight(new DirectionalLight(new Vector3f(1f, 1f, 1f).normalizeLocal(), new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f)));
+        vp.setEnabled(false);
+    }
+
+    @Override
+    protected void cleanup(Application app) {
+
+    }
+
+    @Override
+    protected void onEnable() {
+        vp.setEnabled(true);
+    }
+
+    @Override
+    protected void onDisable() {
+        vp.setEnabled(false);
+    }
+
+    @Override
+    public void update(float tpf) {
+        if (clickDelay > -1) {
+            clickDelay += tpf;
+        }
+        debugNode.updateLogicalState(tpf);
+        debugNode.updateGeometricState();
+
+    }
+
+    public ArmatureDebugger addArmatureFrom(SkinningControl skinningControl) {
+        Armature armature = skinningControl.getArmature();
+        Spatial forSpatial = skinningControl.getSpatial();
+        return addArmatureFrom(armature, forSpatial);
+    }
+
+    public ArmatureDebugger addArmatureFrom(Armature armature, Spatial forSpatial) {
+
+        ArmatureDebugger ad = armatures.get(armature);
+        if(ad != null){
+            return ad;
+        }
+
+        JointInfoVisitor visitor = new JointInfoVisitor(armature);
+        forSpatial.depthFirstTraversal(visitor);
+
+        ad = new ArmatureDebugger(forSpatial.getName() + "_Armature", armature, visitor.deformingJoints);
+        ad.setLocalTransform(forSpatial.getWorldTransform());
+        if (forSpatial instanceof Node) {
+            List<Geometry> geoms = new ArrayList<>();
+            findGeoms((Node) forSpatial, geoms);
+            if (geoms.size() == 1) {
+                ad.setLocalTransform(geoms.get(0).getWorldTransform());
+            }
+        }
+        armatures.put(armature, ad);
+        debugNode.attachChild(ad);
+        if (isInitialized()) {
+            ad.initialize(app.getAssetManager(), app.getCamera());
+        }
+        return ad;
+    }
+
+    private void findGeoms(Node node, List<Geometry> geoms) {
+        for (Spatial spatial : node.getChildren()) {
+            if (spatial instanceof Geometry) {
+                geoms.add((Geometry) spatial);
+            } else if (spatial instanceof Node) {
+                findGeoms((Node) spatial, geoms);
+            }
+        }
+    }
+
+    private ActionListener actionListener = new ActionListener() {
+        public void onAction(String name, boolean isPressed, float tpf) {
+            if (name.equals("shoot") && isPressed) {
+                clickDelay = 0;
+            }
+            if (name.equals("shoot") && !isPressed && clickDelay < CLICK_MAX_DELAY) {
+                Vector2f click2d = app.getInputManager().getCursorPosition();
+                CollisionResults results = new CollisionResults();
+
+                Vector3f click3d = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 0f, tmp);
+                Vector3f dir = app.getCamera().getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 1f, tmp2).subtractLocal(click3d);
+                Ray ray = new Ray(click3d, dir);
+                debugNode.collideWith(ray, results);
+
+                if (results.size() == 0) {
+                    for (ArmatureDebugger ad : armatures.values()) {
+                        ad.select(null);
+                    }
+                    return;
+                }
+                
+                // The closest result is the target that the player picked:
+                Geometry target = results.getClosestCollision().getGeometry();
+                for (ArmatureDebugger ad : armatures.values()) {
+                    Joint selectedjoint = ad.select(target);
+                    if (selectedjoint != null) {
+                        selectedBones.put(ad.getArmature(), selectedjoint);
+                        System.err.println("-----------------------");
+                        System.err.println("Selected Joint : " + selectedjoint.getName() + " in armature " + ad.getName());
+                        System.err.println("Root Bone : " + (selectedjoint.getParent() == null));
+                        System.err.println("-----------------------");
+                        System.err.println("Local translation: " + selectedjoint.getLocalTranslation());
+                        System.err.println("Local rotation: " + selectedjoint.getLocalRotation());
+                        System.err.println("Local scale: " + selectedjoint.getLocalScale());
+                        System.err.println("---");
+                        System.err.println("Model translation: " + selectedjoint.getModelTransform().getTranslation());
+                        System.err.println("Model rotation: " + selectedjoint.getModelTransform().getRotation());
+                        System.err.println("Model scale: " + selectedjoint.getModelTransform().getScale());
+                        System.err.println("---");
+                        System.err.println("Bind inverse Transform: ");
+                        System.err.println(selectedjoint.getInverseModelBindMatrix());
+                        return;
+                    }
+                }
+            }
+            if (name.equals("toggleJoints") && isPressed) {
+                displayAllJoints = !displayAllJoints;
+                for (ArmatureDebugger ad : armatures.values()) {
+                    ad.displayNonDeformingJoint(displayAllJoints);
+                }
+            }
+        }
+    };
+
+//    public Map<Skeleton, Bone> getSelectedBones() {
+//        return selectedBones;
+//    }
+
+    public Node getDebugNode() {
+        return debugNode;
+    }
+
+    public void setDebugNode(Node debugNode) {
+        this.debugNode = debugNode;
+    }
+
+    private class JointInfoVisitor extends SceneGraphVisitorAdapter {
+
+        List<Joint> deformingJoints = new ArrayList<>();
+        Armature armature;
+
+        public JointInfoVisitor(Armature armature) {
+            this.armature = armature;
+        }
+
+        @Override
+        public void visit(Geometry g) {
+            for (Joint joint : armature.getJointList()) {
+                if (g.getMesh().isAnimatedByJoint(armature.getJointIndex(joint))) {
+                    deformingJoints.add(joint);
+                }
+            }
+        }
+    }
+}

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor