浏览代码

Merge branch 'master' into experimental

Conflicts:
	jme3-core/src/main/java/com/jme3/renderer/RenderManager.java
Kirill Vainer 9 年之前
父节点
当前提交
a3638f3e0c
共有 43 个文件被更改,包括 676 次插入283 次删除
  1. 6 5
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderContext.java
  2. 83 19
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Edge.java
  3. 100 63
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Face.java
  4. 3 1
      jme3-bullet/src/common/java/com/jme3/bullet/control/BetterCharacterControl.java
  5. 4 2
      jme3-bullet/src/main/java/com/jme3/bullet/collision/shapes/SphereCollisionShape.java
  6. 31 0
      jme3-core/src/main/java/com/jme3/app/Application.java
  7. 7 4
      jme3-core/src/main/java/com/jme3/collision/bih/BIHTree.java
  8. 5 0
      jme3-core/src/main/java/com/jme3/light/DefaultLightFilter.java
  9. 7 1
      jme3-core/src/main/java/com/jme3/light/DirectionalLight.java
  10. 18 15
      jme3-core/src/main/java/com/jme3/light/Light.java
  11. 8 1
      jme3-core/src/main/java/com/jme3/light/PointLight.java
  12. 9 1
      jme3-core/src/main/java/com/jme3/light/SpotLight.java
  13. 2 0
      jme3-core/src/main/java/com/jme3/material/Material.java
  14. 12 50
      jme3-core/src/main/java/com/jme3/renderer/RenderManager.java
  15. 0 1
      jme3-core/src/main/java/com/jme3/renderer/Statistics.java
  16. 5 0
      jme3-core/src/main/java/com/jme3/scene/Node.java
  17. 19 0
      jme3-core/src/main/java/com/jme3/util/BufferUtils.java
  18. 4 3
      jme3-core/src/main/resources/Common/MatDefs/Misc/Particle.vert
  19. 19 0
      jme3-core/src/main/resources/joystick-mapping.properties
  20. 1 1
      jme3-core/src/tools/java/jme3tools/optimize/TextureAtlas.java
  21. 72 0
      jme3-examples/src/main/java/jme3test/app/TestEnqueueRunnable.java
  22. 22 21
      jme3-examples/src/main/java/jme3test/light/TestManyLightsSingle.java
  23. 1 1
      jme3-jbullet/src/main/java/com/jme3/bullet/objects/PhysicsVehicle.java
  24. 31 22
      jme3-networking/src/main/java/com/jme3/network/base/DefaultClient.java
  25. 1 1
      jme3-networking/src/main/java/com/jme3/network/base/DefaultServer.java
  26. 7 3
      jme3-networking/src/main/java/com/jme3/network/base/KernelAdapter.java
  27. 1 1
      jme3-networking/src/main/java/com/jme3/network/base/MessageProtocol.java
  28. 12 0
      jme3-networking/src/main/java/com/jme3/network/kernel/AbstractKernel.java
  29. 4 9
      jme3-networking/src/main/java/com/jme3/network/kernel/tcp/SelectorKernel.java
  30. 5 10
      jme3-networking/src/main/java/com/jme3/network/kernel/udp/UdpKernel.java
  31. 5 3
      jme3-networking/src/main/java/com/jme3/network/message/SerializerRegistrationsMessage.java
  32. 18 2
      jme3-networking/src/main/java/com/jme3/network/serializing/Serializer.java
  33. 3 2
      jme3-networking/src/main/java/com/jme3/network/serializing/serializers/EnumSerializer.java
  34. 34 5
      jme3-networking/src/main/java/com/jme3/network/serializing/serializers/FieldSerializer.java
  35. 5 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiRegistry.java
  36. 6 1
      jme3-networking/src/main/java/com/jme3/network/service/rpc/RpcConnection.java
  37. 32 6
      jme3-networking/src/main/java/com/jme3/network/service/rpc/msg/RpcResponseMessage.java
  38. 1 1
      jme3-networking/src/main/java/com/jme3/network/service/serializer/ServerSerializerRegistrationsService.java
  39. 2 2
      sdk/jme3-assetpack-support/src/com/jme3/gde/assetpack/browser/Bundle.properties
  40. 2 2
      sdk/jme3-core/src/com/jme3/gde/core/appstates/AppStateExplorerTopComponent.java
  41. 2 2
      sdk/jme3-core/src/com/jme3/gde/core/filters/Bundle.properties
  42. 2 2
      sdk/jme3-core/src/com/jme3/gde/core/sceneexplorer/Bundle.properties
  43. 65 20
      sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/sky/SkyboxWizardPanel2.java

+ 6 - 5
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderContext.java

@@ -59,6 +59,7 @@ import com.jme3.scene.plugins.blender.file.FileBlockHeader;
 import com.jme3.scene.plugins.blender.file.FileBlockHeader.BlockCode;
 import com.jme3.scene.plugins.blender.file.Structure;
 import com.jme3.scene.plugins.blender.materials.MaterialContext;
+import com.jme3.scene.plugins.blender.meshes.TemporalMesh;
 import com.jme3.texture.Texture;
 
 /**
@@ -389,11 +390,11 @@ public class BlenderContext {
                     }
                 }
             } else if("ME".equals(namePrefix)) {
-                List<Node> features = (List<Node>) linkedFeatures.get("meshes");
-                if(features != null) {
-                    for(Node feature : features) {
-                        if(featureName.equals(feature.getName())) {
-                            return feature;
+                List<TemporalMesh> temporalMeshes = (List<TemporalMesh>) linkedFeatures.get("meshes");
+                if(temporalMeshes != null) {
+                    for(TemporalMesh temporalMesh : temporalMeshes) {
+                        if(featureName.equals(temporalMesh.getName())) {
+                            return temporalMesh;
                         }
                     }
                 }

+ 83 - 19
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Edge.java

@@ -10,6 +10,7 @@ import com.jme3.math.Vector3f;
 import com.jme3.scene.plugins.blender.file.BlenderFileException;
 import com.jme3.scene.plugins.blender.file.Pointer;
 import com.jme3.scene.plugins.blender.file.Structure;
+import com.jme3.scene.plugins.blender.math.Vector3d;
 import com.jme3.scene.plugins.blender.meshes.IndexesLoop.IndexPredicate;
 
 /**
@@ -24,6 +25,8 @@ public class Edge {
 
     /** The vertices indexes. */
     private int                 index1, index2;
+    /** The vertices that can be set if we need and abstract edge outside the mesh (for computations). */
+    private Vector3f 			v1, v2;
     /** The weight of the edge. */
     private float               crease;
     /** A variable that indicates if this edge belongs to any face or not. */
@@ -31,6 +34,13 @@ public class Edge {
     /** The mesh that owns the edge. */
     private TemporalMesh        temporalMesh;
 
+    public Edge(Vector3f v1, Vector3f v2) {
+		this.v1 = v1 == null ? new Vector3f() : v1;
+		this.v2 = v2 == null ? new Vector3f() : v2;
+		index1 = 0;
+		index2 = 1;
+	}
+    
     /**
      * This constructor only stores the indexes of the vertices. The position vertices should be stored
      * outside this class.
@@ -74,14 +84,14 @@ public class Edge {
      * @return the first vertex of the edge
      */
     public Vector3f getFirstVertex() {
-        return temporalMesh.getVertices().get(index1);
+        return temporalMesh == null ? v1 : temporalMesh.getVertices().get(index1);
     }
 
     /**
      * @return the second vertex of the edge
      */
     public Vector3f getSecondVertex() {
-        return temporalMesh.getVertices().get(index2);
+        return temporalMesh == null ? v2 : temporalMesh.getVertices().get(index2);
     }
 
     /**
@@ -188,28 +198,82 @@ public class Edge {
      * @return <b>true</b> if the edges cross and false otherwise
      */
     public boolean cross(Edge edge) {
-        Vector3f P1 = this.getFirstVertex();
-        Vector3f P2 = edge.getFirstVertex();
-        Vector3f u = this.getSecondVertex().subtract(P1);
-        Vector3f v = edge.getSecondVertex().subtract(P2);
-        float t2 = (u.x * (P2.y - P1.y) - u.y * (P2.x - P1.x)) / (u.y * v.x - u.x * v.y);
-        float t1 = (P2.x - P1.x + v.x * t2) / u.x;
-        Vector3f p1 = P1.add(u.mult(t1));
-        Vector3f p2 = P2.add(v.mult(t2));
+        return this.getCrossPoint(edge) != null;
+    }
+    
+    /**
+	 * The method computes the crossing pint of this edge and another edge. If
+	 * there is no crossing then null is returned.
+	 * 
+	 * @param edge
+	 *            the edge to compute corss point with
+	 * @return cross point on null if none exist
+	 */
+	public Vector3f getCrossPoint(Edge edge) {
+		return this.getCrossPoint(edge, false, false);
+	}
+    
+	/**
+	 * The method computes the crossing pint of this edge and another edge. If
+	 * there is no crossing then null is returned. This method also allows to
+	 * get the crossing point of the straight lines that contain these edges if
+	 * you set the 'extend' parameter to true.
+	 * 
+	 * @param edge
+	 *            the edge to compute corss point with
+	 * @param extendThisEdge
+	 *            set to <b>true</b> to find a crossing point along the whole
+	 *            straight that contains the current edge
+	 * @param extendSecondEdge
+	 *            set to <b>true</b> to find a crossing point along the whole
+	 *            straight that contains the given edge
+	 * @return cross point on null if none exist
+	 */
+	public Vector3f getCrossPoint(Edge edge, boolean extendThisEdge, boolean extendSecondEdge) {
+		Vector3d P1 = new Vector3d(this.getFirstVertex());
+		Vector3d P2 = new Vector3d(edge.getFirstVertex());
+		Vector3d u = new Vector3d(this.getSecondVertex()).subtract(P1).normalizeLocal();
+		Vector3d v = new Vector3d(edge.getSecondVertex()).subtract(P2).normalizeLocal();
+		
+		double t1 = 0, t2 = 0;
+		if(u.x == 0 && v.x == 0) {
+			t2 = (u.z * (P2.y - P1.y) - u.y * (P2.z - P1.z)) / (u.y * v.z - u.z * v.y);
+	        t1 = (P2.z - P1.z + v.z * t2) / u.z;
+		} else if(u.y == 0 && v.y == 0) {
+			t2 = (u.x * (P2.z - P1.z) - u.z * (P2.x - P1.x)) / (u.z * v.x - u.x * v.z);
+	        t1 = (P2.x - P1.x + v.x * t2) / u.x;
+		} else if(u.z == 0 && v.z == 0) {
+			t2 = (u.x * (P2.y - P1.y) - u.y * (P2.x - P1.x)) / (u.y * v.x - u.x * v.y);
+	        t1 = (P2.x - P1.x + v.x * t2) / u.x;
+		} else {
+			t2 = (P1.y * u.x - P1.x * u.y + P2.x * u.y - P2.y * u.x) / (v.y * u.x - u.y * v.x);
+			t1 = (P2.x - P1.x + v.x * t2) / u.x;
+			if(Math.abs(P1.z - P2.z + u.z * t1 - v.z * t2) > FastMath.FLT_EPSILON) {
+				return null;
+			}
+		}
+		Vector3d p1 = P1.add(u.mult(t1));
+        Vector3d p2 = P2.add(v.mult(t2));
 
-        if (p1.distance(p2) <= FastMath.FLT_EPSILON) {
-            // the lines cross, check if p1 and p2 are within the edges
-            Vector3f p = p1.subtract(P1);
-            float cos = p.dot(u) / (p.length() * u.length());
-            if (cos > 0 && p.length() <= u.length()) {
+		if (p1.distance(p2) <= FastMath.FLT_EPSILON) {
+			if(extendThisEdge && extendSecondEdge) {
+				return p1.toVector3f();
+			}
+			// the lines cross, check if p1 and p2 are within the edges
+            Vector3d p = p1.subtract(P1);
+            double cos = p.dot(u) / p.length();
+            if (extendThisEdge || p.length()<= FastMath.FLT_EPSILON || cos >= 1 - FastMath.FLT_EPSILON && p.length() <= this.getLength()) {
                 // p1 is inside the first edge, lets check the other edge now
                 p = p2.subtract(P2);
-                cos = p.dot(v) / (p.length() * v.length());
-                return cos > 0 && p.length() <= u.length();
+                cos = p.dot(v) / p.length();
+                if(extendSecondEdge || p.length()<= FastMath.FLT_EPSILON || cos >= 1 - FastMath.FLT_EPSILON && p.length() <= edge.getLength()) {
+                	return p1.toVector3f();
+                }
             }
         }
-        return false;
-    }
+		
+		return null;
+	}
 
     @Override
     public String toString() {

+ 100 - 63
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Face.java

@@ -276,30 +276,45 @@ public class Face implements Comparator<Integer> {
             List<Face> facesToTriangulate = new ArrayList<Face>(Arrays.asList(this.clone()));
             while (facesToTriangulate.size() > 0) {
                 Face face = facesToTriangulate.remove(0);
-                int previousIndex1 = -1, previousIndex2 = -1, previousIndex3 = -1;
-                while (face.vertexCount() > 0) {
-                    indexes[0] = face.getIndex(0);
-                    indexes[1] = face.findClosestVertex(indexes[0], -1);
-                    indexes[2] = face.findClosestVertex(indexes[0], indexes[1]);
-
-                    LOGGER.finer("Veryfying improper triangulation of the temporal mesh.");
-                    if (indexes[0] < 0 || indexes[1] < 0 || indexes[2] < 0) {
-                        throw new BlenderFileException("Unable to find two closest vertices while triangulating face in mesh: " + temporalMesh + "Please apply triangulation modifier in blender as a workaround and load again!");
-                    }
-                    if (previousIndex1 == indexes[0] && previousIndex2 == indexes[1] && previousIndex3 == indexes[2]) {
-                        throw new BlenderFileException("Infinite loop detected during triangulation of mesh: " + temporalMesh + "Please apply triangulation modifier in blender as a workaround and load again!");
-                    }
-                    previousIndex1 = indexes[0];
-                    previousIndex2 = indexes[1];
-                    previousIndex3 = indexes[2];
+                // two special cases will improve the computations speed
+                if(face.getIndexes().size() == 3) {
+                	triangulatedFaces.add(face.getIndexes().clone());
+                } else if(face.getIndexes().size() == 4) {
+                	// in case face has 4 verts we use the plain triangulation
+                	indexes[0] = face.getIndex(0);
+                    indexes[1] = face.getIndex(1);
+                    indexes[2] = face.getIndex(2);
+                	triangulatedFaces.add(new IndexesLoop(indexes));
+                	
+                    indexes[1] = face.getIndex(2);
+                    indexes[2] = face.getIndex(3);
+                	triangulatedFaces.add(new IndexesLoop(indexes));
+                } else {
+                	int previousIndex1 = -1, previousIndex2 = -1, previousIndex3 = -1;
+                    while (face.vertexCount() > 0) {
+                        indexes[0] = face.getIndex(0);
+                        indexes[1] = face.findClosestVertex(indexes[0], -1);
+                        indexes[2] = face.findClosestVertex(indexes[0], indexes[1]);
+
+                        LOGGER.finer("Veryfying improper triangulation of the temporal mesh.");
+                        if (indexes[0] < 0 || indexes[1] < 0 || indexes[2] < 0) {
+                            throw new BlenderFileException("Unable to find two closest vertices while triangulating face in mesh: " + temporalMesh + "Please apply triangulation modifier in blender as a workaround and load again!");
+                        }
+                        if (previousIndex1 == indexes[0] && previousIndex2 == indexes[1] && previousIndex3 == indexes[2]) {
+                            throw new BlenderFileException("Infinite loop detected during triangulation of mesh: " + temporalMesh + "Please apply triangulation modifier in blender as a workaround and load again!");
+                        }
+                        previousIndex1 = indexes[0];
+                        previousIndex2 = indexes[1];
+                        previousIndex3 = indexes[2];
 
-                    Arrays.sort(indexes, this);
-                    facesToTriangulate.addAll(face.detachTriangle(indexes));
-                    triangulatedFaces.add(new IndexesLoop(indexes));
+                        Arrays.sort(indexes, this);
+                        facesToTriangulate.addAll(face.detachTriangle(indexes));
+                        triangulatedFaces.add(new IndexesLoop(indexes));
+                    }
                 }
             }
         } catch (BlenderFileException e) {
-            LOGGER.log(Level.WARNING, "Errors occured during face triangulation: {0}. The face will be triangulated with the most direct algorithm, " + "but the results might not be identical to blender.", e.getLocalizedMessage());
+            LOGGER.log(Level.WARNING, "Errors occured during face triangulation: {0}. The face will be triangulated with the most direct algorithm, but the results might not be identical to blender.", e.getLocalizedMessage());
             indexes[0] = this.getIndex(0);
             for (int i = 1; i < this.vertexCount() - 1; ++i) {
                 indexes[1] = this.getIndex(i);
@@ -308,7 +323,7 @@ public class Face implements Comparator<Integer> {
             }
         }
     }
-
+    
     /**
      * @return <b>true</b> if the face is smooth and <b>false</b> otherwise
      */
@@ -335,17 +350,23 @@ public class Face implements Comparator<Integer> {
         return "Face " + indexes;
     }
 
-    /**
-     * The method finds the closest vertex to the one specified by <b>index</b>.
-     * If the vertexToIgnore is positive than it will be ignored in the result.
-     * The closes vertex must be able to create an edge that is fully contained within the face and does not cross
-     * any other edges.
-     * @param index
-     *            the index of the vertex that needs to have found the nearest neighbour
-     * @param indexToIgnore
-     *            the index to ignore in the result (pass -1 if none is to be ignored)
-     * @return the index of the closest vertex to the given one
-     */
+	/**
+	 * The method finds the closest vertex to the one specified by <b>index</b>.
+	 * If the vertexToIgnore is positive than it will be ignored in the result.
+	 * The closest vertex must be able to create an edge that is fully contained
+	 * within the face and does not cross any other edges. Also if the
+	 * vertexToIgnore is not negative then the condition that the edge between
+	 * the found index and the one to ignore is inside the face must also be
+	 * met.
+	 * 
+	 * @param index
+	 *            the index of the vertex that needs to have found the nearest
+	 *            neighbour
+	 * @param indexToIgnore
+	 *            the index to ignore in the result (pass -1 if none is to be
+	 *            ignored)
+	 * @return the index of the closest vertex to the given one
+	 */
     private int findClosestVertex(int index, int indexToIgnore) {
         int result = -1;
         List<Vector3f> vertices = temporalMesh.getVertices();
@@ -355,7 +376,7 @@ public class Face implements Comparator<Integer> {
             if (i != index && i != indexToIgnore) {
                 Vector3f v2 = vertices.get(i);
                 float d = v2.distance(v1);
-                if (d < distance && this.contains(new Edge(index, i, 0, true, temporalMesh))) {
+                if (d < distance && this.contains(new Edge(index, i, 0, true, temporalMesh)) && (indexToIgnore < 0 || this.contains(new Edge(indexToIgnore, i, 0, true, temporalMesh)))) {
                     result = i;
                     distance = d;
                 }
@@ -376,11 +397,9 @@ public class Face implements Comparator<Integer> {
         int index2 = edge.getSecondIndex();
         // check if the line between the vertices is not a border edge of the face
         if (!indexes.areNeighbours(index1, index2)) {
-            List<Vector3f> vertices = temporalMesh.getVertices();
-
             for (int i = 0; i < indexes.size(); ++i) {
-                int i1 = this.getIndex(i);
-                int i2 = this.getIndex(i + 1);
+                int i1 = this.getIndex(i - 1);
+                int i2 = this.getIndex(i);
                 // check if the edges have no common verts (because if they do, they cannot cross)
                 if (i1 != index1 && i1 != index2 && i2 != index1 && i2 != index2) {
                     if (edge.cross(new Edge(i1, i2, 0, false, temporalMesh))) {
@@ -389,35 +408,53 @@ public class Face implements Comparator<Integer> {
                 }
             }
 
-            // the edge does NOT cross any of other edges, so now we need to verify if it is inside the face or outside
-            // we check it by comparing the angle that is created by vertices: [index1 - 1, index1, index1 + 1]
-            // with the one creaded by vertices: [index1 - 1, index1, index2]
-            // if the latter is greater than it means that the edge is outside the face
-            // IMPORTANT: we assume that all vertices are in one plane (this should be ensured before creating the Face)
-            int indexOfIndex1 = indexes.indexOf(index1);
-            int indexMinus1 = this.getIndex(indexOfIndex1 - 1);// indexOfIndex1 == 0 ? indexes.get(indexes.size() - 1) : indexes.get(indexOfIndex1 - 1);
-            int indexPlus1 = this.getIndex(indexOfIndex1 + 1);// indexOfIndex1 == indexes.size() - 1 ? 0 : indexes.get(indexOfIndex1 + 1);
-
-            Vector3f edge1 = vertices.get(indexMinus1).subtract(vertices.get(index1)).normalizeLocal();
-            Vector3f edge2 = vertices.get(indexPlus1).subtract(vertices.get(index1)).normalizeLocal();
-            Vector3f newEdge = vertices.get(index2).subtract(vertices.get(index1)).normalizeLocal();
-
-            // verify f the later computed angle is inside or outside the face
-            Vector3f direction1 = edge1.cross(edge2).normalizeLocal();
-            Vector3f direction2 = edge1.cross(newEdge).normalizeLocal();
-            Vector3f normal = temporalMesh.getNormals().get(index1);
-
-            boolean isAngle1Interior = normal.dot(direction1) < 0;
-            boolean isAngle2Interior = normal.dot(direction2) < 0;
-
-            float angle1 = isAngle1Interior ? edge1.angleBetween(edge2) : FastMath.TWO_PI - edge1.angleBetween(edge2);
-            float angle2 = isAngle2Interior ? edge1.angleBetween(newEdge) : FastMath.TWO_PI - edge1.angleBetween(newEdge);
-
-            return angle1 >= angle2;
+            // computing the edge's middle point
+            Vector3f edgeMiddlePoint = edge.computeCentroid();
+            // computing the edge that is perpendicular to the given edge and has a length of 1 (length actually does not matter)
+            Vector3f edgeVector = edge.getSecondVertex().subtract(edge.getFirstVertex());
+            Vector3f edgeNormal = temporalMesh.getNormals().get(index1).cross(edgeVector).normalizeLocal();
+            Edge e = new Edge(edgeMiddlePoint, edgeNormal.add(edgeMiddlePoint));
+            // compute the vectors from the middle point to the crossing between the extended edge 'e' and other edges of the face
+            List<Vector3f> crossingVectors = new ArrayList<Vector3f>();
+            for (int i = 0; i < indexes.size(); ++i) {
+                int i1 = this.getIndex(i);
+                int i2 = this.getIndex(i + 1);
+            	Vector3f crossPoint = e.getCrossPoint(new Edge(i1, i2, 0, false, temporalMesh), true, false);
+                if(crossPoint != null) {
+                	crossingVectors.add(crossPoint.subtractLocal(edgeMiddlePoint));
+                }
+            }
+            if(crossingVectors.size() == 0) {
+            	return false;// edges do not cross
+            }
+            
+            // use only distinct vertices (doubles may appear if the crossing point is a vertex)
+            List<Vector3f> distinctCrossingVectors = new ArrayList<Vector3f>();
+            for(Vector3f cv : crossingVectors) {
+        		double minDistance = Double.MAX_VALUE;
+        		for(Vector3f dcv : distinctCrossingVectors) {
+        			minDistance = Math.min(minDistance, dcv.distance(cv));
+        		}
+        		if(minDistance > FastMath.FLT_EPSILON) {
+        			distinctCrossingVectors.add(cv);
+        		}
+            }
+            
+            if(distinctCrossingVectors.size() == 0) {
+            	throw new IllegalStateException("There MUST be at least 2 crossing vertices!");
+            }
+            // checking if all crossing vectors point to the same direction (if yes then the edge is outside the face)
+            float direction = Math.signum(distinctCrossingVectors.get(0).dot(edgeNormal));// if at least one vector has different direction that this - it means that the edge is inside the face
+            for(int i=1;i<distinctCrossingVectors.size();++i) {
+            	if(direction != Math.signum(distinctCrossingVectors.get(i).dot(edgeNormal))) {
+            		return true;
+            	}
+            }
+            return false;
         }
         return true;
     }
-
+    
     @Override
     public int hashCode() {
         final int prime = 31;

+ 3 - 1
jme3-bullet/src/common/java/com/jme3/bullet/control/BetterCharacterControl.java

@@ -171,6 +171,8 @@ public class BetterCharacterControl extends AbstractPhysicsControl implements Ph
         }
         TempVars vars = TempVars.get();
 
+        Vector3f currentVelocity = vars.vect2.set(velocity);
+        
         // dampen existing x/z forces
         float existingLeftVelocity = velocity.dot(localLeft);
         float existingForwardVelocity = velocity.dot(localForward);
@@ -194,7 +196,7 @@ public class BetterCharacterControl extends AbstractPhysicsControl implements Ph
             //add resulting vector to existing velocity
             velocity.addLocal(localWalkDirection);
         }
-        rigidBody.setLinearVelocity(velocity);
+        if(currentVelocity.distance(velocity) > FastMath.ZERO_TOLERANCE) rigidBody.setLinearVelocity(velocity);
         if (jump) {
             //TODO: precalculate jump force
             Vector3f rotatedJumpForce = vars.vect1;

+ 4 - 2
jme3-bullet/src/main/java/com/jme3/bullet/collision/shapes/SphereCollisionShape.java

@@ -82,7 +82,9 @@ public class SphereCollisionShape extends CollisionShape {
      */
     @Override
     public void setScale(Vector3f scale) {
-        Logger.getLogger(this.getClass().getName()).log(Level.WARNING, "SphereCollisionShape cannot be scaled");
+        if (!scale.equals(Vector3f.UNIT_XYZ)) {
+            Logger.getLogger(this.getClass().getName()).log(Level.WARNING, "SphereCollisionShape cannot be scaled");
+        }
     }
 
     protected void createShape() {
@@ -91,7 +93,7 @@ public class SphereCollisionShape extends CollisionShape {
 //        new SphereShape(radius);
 //        objectId.setLocalScaling(Converter.convert(getScale()));
 //        objectId.setMargin(margin);
-        setScale(scale);
+        setScale(scale); // Set the scale to 1
         setMargin(margin);
     }
     

+ 31 - 0
jme3-core/src/main/java/com/jme3/app/Application.java

@@ -661,12 +661,28 @@ public class Application implements SystemListener {
      * Callables are executed right at the beginning of the main loop.
      * They are executed even if the application is currently paused
      * or out of focus.
+     * 
+     * @param callable The callable to run in the main jME3 thread
      */
     public <V> Future<V> enqueue(Callable<V> callable) {
         AppTask<V> task = new AppTask<V>(callable);
         taskQueue.add(task);
         return task;
     }
+    
+    /**
+     * Enqueues a runnable object to execute in the jME3
+     * rendering thread.
+     * <p>
+     * Runnables are executed right at the beginning of the main loop.
+     * They are executed even if the application is currently paused
+     * or out of focus.
+     * 
+     * @param runnable The runnable to run in the main jME3 thread
+     */    
+    public void enqueue(Runnable runnable){
+        enqueue(new RunnableWrapper(runnable));
+    }
 
     /**
      * Runs tasks enqueued via {@link #enqueue(Callable)}
@@ -752,4 +768,19 @@ public class Application implements SystemListener {
         return viewPort;
     }
 
+    private class RunnableWrapper implements Callable{
+        private final Runnable runnable;
+
+        public RunnableWrapper(Runnable runnable){
+            this.runnable = runnable;
+        }
+
+        @Override
+        public Object call(){
+            runnable.run();
+            return null;
+        }
+        
+    }
+    
 }

+ 7 - 4
jme3-core/src/main/java/com/jme3/collision/bih/BIHTree.java

@@ -109,8 +109,11 @@ public class BIHTree implements CollisionData {
         this.mesh = mesh;
         this.maxTrisPerNode = maxTrisPerNode;
 
-        if (maxTrisPerNode < 1 || mesh == null) {
-            throw new IllegalArgumentException();
+        if (maxTrisPerNode < 1) {
+            throw new IllegalArgumentException("maxTrisPerNode cannot be less than 1");
+        }
+        if (mesh == null) {
+            throw new IllegalArgumentException("Mesh cannot be null");
         }
 
         bihSwapTmp = new float[9];
@@ -451,7 +454,7 @@ public class BIHTree implements CollisionData {
         } else if (bv instanceof BoundingBox) {
             bbox = new BoundingBox((BoundingBox) bv);
         } else {
-            throw new UnsupportedCollisionException();
+            throw new UnsupportedCollisionException("BoundingVolume:" + bv);
         }
 
         bbox.transform(worldMatrix.invert(), bbox);
@@ -470,7 +473,7 @@ public class BIHTree implements CollisionData {
             BoundingVolume bv = (BoundingVolume) other;
             return collideWithBoundingVolume(bv, worldMatrix, results);
         } else {
-            throw new UnsupportedCollisionException();
+            throw new UnsupportedCollisionException("Collidable:" + other);
         }
     }
 

+ 5 - 0
jme3-core/src/main/java/com/jme3/light/DefaultLightFilter.java

@@ -60,6 +60,11 @@ public final class DefaultLightFilter implements LightFilter {
             for (int i = 0; i < worldLights.size(); i++) {
                 Light light = worldLights.get(i);
 
+                // If this light is not enabled it will be ignored.
+                if (!light.isEnabled()) {
+                    continue;
+                }
+
                 if (light.frustumCheckNeeded) {
                     processedLights.add(light);
                     light.frustumCheckNeeded = false;

+ 7 - 1
jme3-core/src/main/java/com/jme3/light/DirectionalLight.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012, 2015 jMonkeyEngine
+ * Copyright (c) 2009-2012, 2015-2016 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -146,4 +146,10 @@ public class DirectionalLight extends Light {
         direction = (Vector3f) ic.readSavable("direction", null);
     }
 
+    @Override
+    public DirectionalLight clone() {
+        DirectionalLight l = (DirectionalLight)super.clone();
+        l.direction = direction.clone();
+        return l;
+    }
 }

+ 18 - 15
jme3-core/src/main/java/com/jme3/light/Light.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012, 2015 jMonkeyEngine
+ * Copyright (c) 2009-2012, 2015-2016 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -103,9 +103,6 @@ public abstract class Light implements Savable, Cloneable {
      */
     protected transient float lastDistance = -1;
 
-    /**
-     * If light is disabled, it will not have any 
-     */
     protected boolean enabled = true;
 
     /** 
@@ -169,20 +166,24 @@ public abstract class Light implements Savable, Cloneable {
         this.color.set(color);
     }
 
-    
-    /*
-     * Returns true if the light is enabled
-     * 
-     * @return true if the light is enabled
-     * 
-     * @see Light#setEnabled(boolean)
+
+    /**
+     * Returns true if this light is enabled.
+     * @return true if enabled, otherwise false.
      */
-    /*
     public boolean isEnabled() {
         return enabled;
     }
-    */
-    
+
+    /**
+     * Set to false in order to disable a light and have it filtered out from being included in rendering.
+     *
+     * @param enabled true to enable and false to disable the light.
+     */
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+
     /**
      * Determines if the light intersects with the given bounding box.
      * <p>
@@ -227,7 +228,9 @@ public abstract class Light implements Savable, Cloneable {
     @Override
     public Light clone(){
         try {
-            return (Light) super.clone();
+            Light l = (Light) super.clone();
+            l.color = color.clone();
+            return l;
         } catch (CloneNotSupportedException ex) {
             throw new AssertionError();
         }

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

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012, 2015 jMonkeyEngine
+ * Copyright (c) 2009-2012, 2015-2016 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -241,4 +241,11 @@ public class PointLight extends Light {
             this.invRadius = 0;
         }
     }
+
+    @Override
+    public PointLight clone() {
+        PointLight p = (PointLight)super.clone();
+        p.position = position.clone();
+        return p;
+    }
 }

+ 9 - 1
jme3-core/src/main/java/com/jme3/light/SpotLight.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012, 2015 jMonkeyEngine
+ * Copyright (c) 2009-2012, 2015-2016 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -448,5 +448,13 @@ public class SpotLight extends Light {
             this.invSpotRange = 0;
         }
     }
+
+    @Override
+    public SpotLight clone() {
+        SpotLight s = (SpotLight)super.clone();
+        s.direction = direction.clone();
+        s.position = position.clone();
+        return s;
+    }
 }
 

+ 2 - 0
jme3-core/src/main/java/com/jme3/material/Material.java

@@ -976,6 +976,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         oc.write(def.getAssetName(), "material_def", null);
         oc.write(additionalState, "render_state", null);
         oc.write(transparent, "is_transparent", false);
+        oc.write(name, "name", null);
         oc.writeStringSavableMap(paramValues, "parameters", null);
     }
     
@@ -990,6 +991,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
     public void read(JmeImporter im) throws IOException {
         InputCapsule ic = im.getCapsule(this);
 
+        name = ic.readString("name", null);
         additionalState = (RenderState) ic.readSavable("render_state", null);
         transparent = ic.readBoolean("is_transparent", false);
 

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

@@ -49,7 +49,7 @@ import com.jme3.renderer.queue.RenderQueue;
 import com.jme3.renderer.queue.RenderQueue.Bucket;
 import com.jme3.renderer.queue.RenderQueue.ShadowMode;
 import com.jme3.scene.*;
-import com.jme3.shader.Shader;
+import com.jme3.shader.Uniform;
 import com.jme3.shader.UniformBinding;
 import com.jme3.shader.UniformBindingManager;
 import com.jme3.system.NullRenderer;
@@ -483,8 +483,8 @@ public class RenderManager {
      * Updates the given list of uniforms with {@link UniformBinding uniform bindings}
      * based on the current world state.
      */
-    public void updateUniformBindings(Shader shader) {
-        uniformBindingManager.updateUniformBindings(shader);
+    public void updateUniformBindings(List<Uniform> params) {
+        uniformBindingManager.updateUniformBindings(params);
     }
 
     /**
@@ -556,7 +556,7 @@ public class RenderManager {
                 forcedRenderState = tmpRs;
 
                 //Reverted this part from revision 6197
-                //If forcedTechnique does not exists, and frocedMaterial is not set, the geom MUST NOT be rendered
+                //If forcedTechnique does not exists, and forcedMaterial is not set, the geom MUST NOT be rendered
             } else if (forcedMaterial != null) {
                 // use forced material
                 forcedMaterial.render(g, lightList, this);
@@ -585,37 +585,6 @@ public class RenderManager {
             renderGeometry(gl.get(i));
         }
     }
-    
-    public void renderGeometryListNew(GeometryList gl) {
-        int size = gl.size();
-        int pass = 0;
-        
-        // Keep rendering geometries in the list
-        // checking each time if they need more passes.
-        // Geometries which need more passes are added to the beginning
-        // of the list and then another pass is executed.
-        // In the end, all geometries will have their passes rendered.
-        while (true) {
-            int writeIdx = 0;
-            for (int i = 0; i < size; i++) {
-                Geometry obj = gl.get(i);
-                renderGeometry(obj);
-                boolean morePasses = true;
-                if (morePasses) {
-                    // Geometry wants to be rendered again.
-                    // Move it to the beginning of the list.
-                    gl.set(writeIdx++, obj);
-                }
-            }
-            // No geometries were written to the beginning of the list -
-            // all passes are finished.
-            if (writeIdx == 0) {
-                return;
-            }
-            pass++;
-            size = writeIdx;
-        }
-    }
 
     /**
      * Preloads a scene for rendering.
@@ -647,9 +616,7 @@ public class RenderManager {
 
             gm.getMaterial().preload(this);
             Mesh mesh = gm.getMesh();
-            if (mesh != null
-                    && mesh.getVertexCount() != 0
-                    && mesh.getTriangleCount() != 0) {
+            if (mesh != null) {
                 for (VertexBuffer vb : mesh.getBufferList().getArray()) {
                     if (vb.getData() != null && vb.getUsage() != VertexBuffer.Usage.CpuOnly) {
                         renderer.updateBufferData(vb);
@@ -674,10 +641,8 @@ public class RenderManager {
      * <p>
      * In addition to enqueuing the visible geometries, this method
      * also scenes which cast or receive shadows, by putting them into the
-     * RenderQueue's 
-     * {@link RenderQueue#addToShadowQueue(com.jme3.scene.Geometry, com.jme3.renderer.queue.RenderQueue.ShadowMode) 
-     * shadow queue}. Each Spatial which has its 
-     * {@link Spatial#setShadowMode(com.jme3.renderer.queue.RenderQueue.ShadowMode) shadow mode}
+     * RenderQueue's {@link RenderQueue#renderShadowQueue(GeometryList, RenderManager, Camera, boolean) shadow queue}.
+     * Each Spatial which has its {@link Spatial#setShadowMode(com.jme3.renderer.queue.RenderQueue.ShadowMode) shadow mode}
      * set to not off, will be put into the appropriate shadow queue, note that
      * this process does not check for frustum culling on any 
      * {@link ShadowMode#Cast shadow casters}, as they don't have to be
@@ -817,10 +782,8 @@ public class RenderManager {
      * @param singlePassLightBatchSize the number of lights.
      */
     public void setSinglePassLightBatchSize(int singlePassLightBatchSize) {
-        if (singlePassLightBatchSize < 1) {
-            throw new IllegalArgumentException("batch size cannot be less than 1");
-        }
-        this.singlePassLightBatchSize = singlePassLightBatchSize;
+        // Ensure the batch size is no less than 1
+        this.singlePassLightBatchSize = singlePassLightBatchSize < 1 ? 1 : singlePassLightBatchSize;
     }
     
     
@@ -1026,13 +989,12 @@ public class RenderManager {
      * (see {@link #renderTranslucentQueue(com.jme3.renderer.ViewPort) })</li>
      * <li>If any objects remained in the render queue, they are removed
      * from the queue. This is generally objects added to the 
-     * {@link RenderQueue#renderShadowQueue(com.jme3.renderer.queue.RenderQueue.ShadowMode, com.jme3.renderer.RenderManager, com.jme3.renderer.Camera, boolean) 
-     * shadow queue}
+     * {@link RenderQueue#renderShadowQueue(GeometryList, RenderManager, Camera, boolean) shadow queue}
      * which were not rendered because of a missing shadow renderer.</li>
      * </ul>
      * 
-     * @param vp
-     * @param tpf 
+     * @param vp View port to render
+     * @param tpf Time per frame value
      */
     public void renderViewPort(ViewPort vp, float tpf) {
         if (!vp.isEnabled()) {

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

@@ -36,7 +36,6 @@ import com.jme3.shader.Shader;
 import com.jme3.texture.FrameBuffer;
 import com.jme3.texture.Image;
 import com.jme3.util.IntMap;
-import java.util.HashSet;
 
 /**
  * The statistics class allows tracking of real-time rendering statistics.

+ 5 - 0
jme3-core/src/main/java/com/jme3/scene/Node.java

@@ -687,6 +687,11 @@ public class Node extends Spatial {
 //            childClone.parent = nodeClone;
 //            nodeClone.children.add(childClone);
 //        }
+
+        // Reset the fields of the clone that should be in a 'new' state.
+        nodeClone.updateList = null;
+        nodeClone.updateListValid = false; // safe because parent is nulled out in super.clone()
+            
         return nodeClone;
     }
 

+ 19 - 0
jme3-core/src/main/java/com/jme3/util/BufferUtils.java

@@ -401,6 +401,25 @@ public final class BufferUtils {
         vector.z = buf.get(index * 3 + 2);
     }
 
+    /**
+     * Updates the values of the given vector from the specified buffer at the
+     * index provided.
+     * 
+     * @param vector
+     *            the vector to set data on
+     * @param buf
+     *            the buffer to read from
+     * @param index
+     *            the position (in terms of vectors, not floats) to read from
+     *            the buf
+     */
+    public static void populateFromBuffer(Vector4f vector, FloatBuffer buf, int index) {
+        vector.x = buf.get(index * 4);
+        vector.y = buf.get(index * 4 + 1);
+        vector.z = buf.get(index * 4 + 2);
+        vector.w = buf.get(index * 4 + 3);
+    }
+
     /**
      * Generates a Vector3f array from the given FloatBuffer.
      * 

+ 4 - 3
jme3-core/src/main/resources/Common/MatDefs/Misc/Particle.vert

@@ -32,11 +32,12 @@ void main(){
     #ifdef POINT_SPRITE
         vec4 worldPos = g_WorldMatrix * pos;
         float d = distance(g_CameraPosition.xyz, worldPos.xyz);
-        gl_PointSize = max(1.0, (inSize * SIZE_MULTIPLIER * m_Quadratic) / d);
+        float size = (inSize * SIZE_MULTIPLIER * m_Quadratic) / d);
+        gl_PointSize = max(1.0, size);
 
         //vec4 worldViewPos = g_WorldViewMatrix * pos;
         //gl_PointSize = (inSize * SIZE_MULTIPLIER * m_Quadratic)*100.0 / worldViewPos.z;
 
-        color.a *= min(gl_PointSize, 1.0);
+        color.a *= min(size, 1.0);
     #endif
-}
+}

+ 19 - 0
jme3-core/src/main/resources/joystick-mapping.properties

@@ -95,6 +95,25 @@ Gamepad\ F310\ (Controller).ry=rz
 # keeps it from confusing the .rx mapping.
 Gamepad\ F310\ (Controller).z=trigger
 
+# Logitech F310 gamepad with dip switch XInput for Windows 10
+Controller\ (Gamepad\ F310).0=2
+Controller\ (Gamepad\ F310).1=1
+Controller\ (Gamepad\ F310).2=3
+Controller\ (Gamepad\ F310).3=0
+
+Controller\ (Gamepad\ F310).6=8
+Controller\ (Gamepad\ F310).7=9
+
+Controller\ (Gamepad\ F310).8=10
+Controller\ (Gamepad\ F310).9=11
+
+Controller\ (Gamepad\ F310).rx=z
+Controller\ (Gamepad\ F310).ry=rz
+
+# requires custom code to support trigger buttons but this
+# keeps it from confusing the .rx mapping.
+Controller\ (Gamepad\ F310).z=trigger
+
 # Alternate version of the XBOX 360 controller
 XBOX\ 360\ For\ Windows\ (Controller).0=2
 XBOX\ 360\ For\ Windows\ (Controller).1=1

+ 1 - 1
jme3-core/src/tools/java/jme3tools/optimize/TextureAtlas.java

@@ -155,7 +155,7 @@ public class TextureAtlas {
                 return false;
             } else {
                 if (normal != null && normal.getKey() != null) {
-                    addTexture(diffuse, "NormalMap", keyName);
+                    addTexture(normal, "NormalMap", keyName);
                 }
                 if (specular != null && specular.getKey() != null) {
                     addTexture(specular, "SpecularMap", keyName);

+ 72 - 0
jme3-examples/src/main/java/jme3test/app/TestEnqueueRunnable.java

@@ -0,0 +1,72 @@
+package jme3test.app;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Box;
+
+/**
+ * @author john01dav
+ */
+public class TestEnqueueRunnable extends SimpleApplication{
+    private ExampleAsyncTask exampleAsyncTask;
+    
+    public static void main(String[] args){
+        new TestEnqueueRunnable().start();
+    }
+    
+    @Override
+    public void simpleInitApp(){
+        Geometry geom = new Geometry("Box", new Box(1, 1, 1));
+        Material material = new Material(getAssetManager(), "/Common/MatDefs/Misc/Unshaded.j3md");
+        material.setColor("Color", ColorRGBA.Blue); //a color is needed to start with
+        geom.setMaterial(material);
+        getRootNode().attachChild(geom);
+        
+        exampleAsyncTask = new ExampleAsyncTask(material);
+        exampleAsyncTask.getThread().start();
+    }
+
+    @Override
+    public void destroy(){
+        exampleAsyncTask.endTask();
+        super.destroy();
+    }
+    
+    private class ExampleAsyncTask implements Runnable{
+        private final Thread thread;
+        private final Material material;
+        private volatile boolean running = true;
+
+        public ExampleAsyncTask(Material material){
+            this.thread = new Thread(this);
+            this.material = material;
+        }
+
+        public Thread getThread(){
+            return thread;
+        }
+        
+        public void run(){
+            while(running){
+                enqueue(new Runnable(){ //primary usage of this in real applications would use lambda expressions which are unavailable at java 6
+                    public void run(){
+                        material.setColor("Color", ColorRGBA.randomColor());
+                    }
+                });
+                
+                try{
+                    Thread.sleep(1000);
+                }catch(InterruptedException e){}
+            }
+        }
+        
+        public void endTask(){
+            running = false;
+            thread.interrupt();
+        }
+        
+    }
+    
+}

+ 22 - 21
jme3-examples/src/main/java/jme3test/light/TestManyLightsSingle.java

@@ -65,25 +65,23 @@ public class TestManyLightsSingle extends SimpleApplication {
         TestManyLightsSingle app = new TestManyLightsSingle();
         app.start();
     }
-    
+
     /**
      * Switch mode with space bar at run time
      */
     TechniqueDef.LightMode lm = TechniqueDef.LightMode.SinglePass;
-    int lightNum = 6;
 
     @Override
     public void simpleInitApp() {
         renderManager.setPreferredLightMode(lm);
-        renderManager.setSinglePassLightBatchSize(lightNum);
-
+        renderManager.setSinglePassLightBatchSize(6);
 
         flyCam.setMoveSpeed(10);
 
         Node scene = (Node) assetManager.loadModel("Scenes/ManyLights/Main.scene");
         rootNode.attachChild(scene);
         Node n = (Node) rootNode.getChild(0);
-        LightList lightList = n.getWorldLightList();
+        final LightList lightList = n.getWorldLightList();
         final Geometry g = (Geometry) n.getChild("Grid-geom-1");
 
         g.getMaterial().setColor("Ambient", new ColorRGBA(0.2f, 0.2f, 0.2f, 1f));
@@ -152,8 +150,6 @@ public class TestManyLightsSingle extends SimpleApplication {
 //        guiNode.setCullHint(CullHint.Always);
 
 
-
-
         flyCam.setDragToRotate(true);
         flyCam.setMoveSpeed(50);
 
@@ -168,27 +164,35 @@ public class TestManyLightsSingle extends SimpleApplication {
                         helloText.setText("(Multi pass)");
                     } else {
                         lm = TechniqueDef.LightMode.SinglePass;
-                        helloText.setText("(Single pass) nb lights per batch : " + lightNum);
+                        helloText.setText("(Single pass) nb lights per batch : " + renderManager.getSinglePassLightBatchSize());
                     }
                     renderManager.setPreferredLightMode(lm);
-                    reloadScene(g,boxGeo,cubeNodes);
+                    reloadScene(g, boxGeo, cubeNodes);
                 }
                 if (name.equals("lightsUp") && isPressed) {
-                    lightNum++;
-                    renderManager.setSinglePassLightBatchSize(lightNum);
-                    helloText.setText("(Single pass) nb lights per batch : " + lightNum);
+                    renderManager.setSinglePassLightBatchSize(renderManager.getSinglePassLightBatchSize() + 1);
+                    helloText.setText("(Single pass) nb lights per batch : " + renderManager.getSinglePassLightBatchSize());
                 }
                 if (name.equals("lightsDown") && isPressed) {
-                    lightNum--;
-                    renderManager.setSinglePassLightBatchSize(lightNum);
-                    helloText.setText("(Single pass) nb lights per batch : " + lightNum);
+                    renderManager.setSinglePassLightBatchSize(renderManager.getSinglePassLightBatchSize() - 1);
+                    helloText.setText("(Single pass) nb lights per batch : " + renderManager.getSinglePassLightBatchSize());
+                }
+                if (name.equals("toggleOnOff") && isPressed) {
+                    for (final Light light : lightList) {
+                        if (light instanceof AmbientLight) {
+                            continue;
+                        }
+
+                        light.setEnabled(!light.isEnabled());
+                    }
                 }
             }
-        }, "toggle", "lightsUp", "lightsDown");
+        }, "toggle", "lightsUp", "lightsDown", "toggleOnOff");
 
         inputManager.addMapping("toggle", new KeyTrigger(KeyInput.KEY_SPACE));
         inputManager.addMapping("lightsUp", new KeyTrigger(KeyInput.KEY_UP));
         inputManager.addMapping("lightsDown", new KeyTrigger(KeyInput.KEY_DOWN));
+        inputManager.addMapping("toggleOnOff", new KeyTrigger(KeyInput.KEY_L));
 
 
         SpotLight spot = new SpotLight();
@@ -215,12 +219,9 @@ public class TestManyLightsSingle extends SimpleApplication {
         guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
         helloText = new BitmapText(guiFont, false);
         helloText.setSize(guiFont.getCharSet().getRenderedSize());
-        helloText.setText("(Single pass) nb lights per batch : " + lightNum);
+        helloText.setText("(Single pass) nb lights per batch : " + renderManager.getSinglePassLightBatchSize());
         helloText.setLocalTranslation(300, helloText.getLineHeight(), 0);
         guiNode.attachChild(helloText);
-
-
-
     }
 
     protected void reloadScene(Geometry g, Geometry boxGeo, Node cubeNodes) {
@@ -234,7 +235,7 @@ public class TestManyLightsSingle extends SimpleApplication {
             cubeNodes.setMaterial(m);
         }
     }
-    
+
     BitmapText helloText;
     long time;
     long nbFrames;

+ 1 - 1
jme3-jbullet/src/main/java/com/jme3/bullet/objects/PhysicsVehicle.java

@@ -456,7 +456,7 @@ public class PhysicsVehicle extends PhysicsRigidBody {
     /**
      * Get the current forward vector of the vehicle in world coordinates
      * @param vector The object to write the forward vector values to.
-     * Passing null will cause a new {@link Vector3f) to be created.
+     * Passing null will cause a new {@link Vector3f} to be created.
      * @return The forward vector
      */
     public Vector3f getForwardVector(Vector3f vector) {

+ 31 - 22
jme3-networking/src/main/java/com/jme3/network/base/DefaultClient.java

@@ -301,35 +301,38 @@ public class DefaultClient implements Client
 
     protected void closeConnections( DisconnectInfo info )
     {
-        if( !isRunning )
-            return;
+        synchronized(this) {
+            if( !isRunning )
+                return;
 
-        if( services.isStarted() ) {
-            // Let the services get a chance to stop before we
-            // kill the connection.
-            services.stop();
-        }
+            if( services.isStarted() ) {
+                // Let the services get a chance to stop before we
+                // kill the connection.
+                services.stop();
+            }
         
-        // Send a close message
+            // Send a close message
     
-        // Tell the thread it's ok to die
-        for( ConnectorAdapter ca : channels ) {
-            if( ca == null )
-                continue;
-            ca.close();
-        }
+            // Tell the thread it's ok to die
+            for( ConnectorAdapter ca : channels ) {
+                if( ca == null )
+                    continue;
+                ca.close();
+            }
         
-        // Wait for the threads?
+            // Wait for the threads?
 
-        // Just in case we never fully connected
-        connecting.countDown();
+            // Just in case we never fully connected
+            connecting.countDown();
         
-        fireDisconnected(info);
+            isRunning = false;
         
-        isRunning = false;
+            // Terminate the services
+            services.terminate();            
+        }
         
-        // Terminate the services
-        services.terminate();            
+        // Make sure we aren't synched while firing events
+        fireDisconnected(info);        
     }         
 
     @Override
@@ -462,11 +465,17 @@ public class DefaultClient implements Client
                 this.id = (int)crm.getId();
                 log.log( Level.FINE, "Connection established, id:{0}.", this.id );
                 connecting.countDown();
-                fireConnected();
+                //fireConnected();
             } else {
                 // Else it's a message letting us know that the 
                 // hosted services have been started
                 startServices();
+ 
+                // Delay firing 'connected' until the services have all
+                // been started to avoid odd race conditions.  If there is some
+                // need to get some kind of event before the services have been
+                // started then we should create a new event step.               
+                fireConnected();
             }
             return;
         } else if( m instanceof ChannelInfoMessage ) {

+ 1 - 1
jme3-networking/src/main/java/com/jme3/network/base/DefaultServer.java

@@ -608,7 +608,7 @@ public class DefaultServer implements Server
             // should always already be closed through all paths that I
             // can conceive... but it doesn't hurt to be sure. 
             for( Endpoint p : channels ) {
-                if( p == null ) 
+                if( p == null || !p.isConnected() ) 
                     continue;
                 p.close();
             }

+ 7 - 3
jme3-networking/src/main/java/com/jme3/network/base/KernelAdapter.java

@@ -112,6 +112,8 @@ public class KernelAdapter extends Thread
         
         // Kill the kernel
         kernel.terminate();
+        
+        join();
     }
 
     protected void reportError( Endpoint p, Object context, Exception e )
@@ -119,9 +121,11 @@ public class KernelAdapter extends Thread
         // Should really be queued up so the outer thread can
         // retrieve them.  For now we'll just log it.  FIXME
         log.log( Level.SEVERE, "Unhandled error, endpoint:" + p + ", context:" + context, e );
-        
-        // In lieu of other options, at least close the endpoint
-        p.close();
+
+        if( p.isConnected() ) {
+            // In lieu of other options, at least close the endpoint
+            p.close();
+        }
     }                                                      
 
     protected HostedConnection getConnection( Endpoint p )

+ 1 - 1
jme3-networking/src/main/java/com/jme3/network/base/MessageProtocol.java

@@ -181,7 +181,7 @@ public class MessageProtocol
             Message m = (Message)obj;
             messages.add(m);
         } catch( IOException e ) {
-            throw new RuntimeException( "Error deserializing object, clas ID:" + buffer.getShort(0), e );   
+            throw new RuntimeException( "Error deserializing object, class ID:" + buffer.getShort(0), e );   
         }         
     }
 }

+ 12 - 0
jme3-networking/src/main/java/com/jme3/network/kernel/AbstractKernel.java

@@ -76,6 +76,18 @@ public abstract class AbstractKernel implements Kernel
         log.log( Level.SEVERE, "Unhanddled kernel error", e );
     }
 
+    protected void wakeupReader() {
+        // If there are no pending messages then add one so that the
+        // kernel-user knows to wake up if it is only listening for
+        // envelopes.
+        if( !hasEnvelopes() ) {
+            // Note: this is not really a race condition.  At worst, our
+            // event has already been handled by now and it does no harm
+            // to check again.
+            addEnvelope( EVENTS_PENDING );
+        }
+    }
+
     protected long nextEndpointId()
     {
         return nextId.getAndIncrement();

+ 4 - 9
jme3-networking/src/main/java/com/jme3/network/kernel/tcp/SelectorKernel.java

@@ -106,6 +106,9 @@ public class SelectorKernel extends AbstractKernel
         try {
             thread.close();
             thread = null;
+            
+            // Need to let any caller waiting for a read() wakeup 
+            wakeupReader();       
         } catch( IOException e ) {
             throw new KernelException( "Error closing host connection:" + address, e );
         }
@@ -164,15 +167,7 @@ public class SelectorKernel extends AbstractKernel
         // Enqueue an endpoint event for the listeners
         addEvent( EndpointEvent.createRemove( this, p ) );
 
-        // If there are no pending messages then add one so that the
-        // kernel-user knows to wake up if it is only listening for
-        // envelopes.
-        if( !hasEnvelopes() ) {
-            // Note: this is not really a race condition.  At worst, our
-            // event has already been handled by now and it does no harm
-            // to check again.
-            addEnvelope( EVENTS_PENDING );
-        }
+        wakeupReader();
     }
 
     /**

+ 5 - 10
jme3-networking/src/main/java/com/jme3/network/kernel/udp/UdpKernel.java

@@ -110,6 +110,9 @@ public class UdpKernel extends AbstractKernel
             thread.close();
             writer.shutdown();
             thread = null;
+            
+            // Need to let any caller waiting for a read() wakeup 
+            wakeupReader();       
         } catch( IOException e ) {
             throw new KernelException( "Error closing host connection:" + address, e );
         }
@@ -169,16 +172,8 @@ public class UdpKernel extends AbstractKernel
         log.log( Level.FINE, "Socket endpoints size:{0}", socketEndpoints.size() );
 
         addEvent( EndpointEvent.createRemove( this, p ) );
-        
-        // If there are no pending messages then add one so that the
-        // kernel-user knows to wake up if it is only listening for
-        // envelopes.
-        if( !hasEnvelopes() ) {
-            // Note: this is not really a race condition.  At worst, our
-            // event has already been handled by now and it does no harm
-            // to check again.
-            addEnvelope( EVENTS_PENDING );
-        }
+ 
+        wakeupReader();       
     }
 
     protected void newData( DatagramPacket packet )

+ 5 - 3
jme3-networking/src/main/java/com/jme3/network/message/SerializerRegistrationsMessage.java

@@ -146,15 +146,17 @@ public class SerializerRegistrationsMessage extends AbstractMessage {
             // that also run their own servers but realistically they would have
             // to disable the ServerSerializerRegistrationsServer anyway.
             if( compiled != null ) {
-                log.log( Level.INFO, "Skipping registration as registry is locked, presumably by a local server process.");
+                log.log(Level.INFO, "Skipping registration as registry is locked, presumably by a local server process.");
                 return;
             }
         }
         
+        log.log(Level.FINE, "Registering {0} classes...", registrations.length);
         for( Registration reg : registrations ) {
-            log.log( Level.INFO, "Registering:{0}", reg);
+            log.log(Level.INFO, "Registering:{0}", reg);
             reg.register();
         }
+        log.log(Level.FINE, "Done registering serializable classes.");
     }
     
     @Serializable
@@ -187,7 +189,7 @@ public class SerializerRegistrationsMessage extends AbstractMessage {
                     serializer = (Serializer)serializerType.newInstance();                    
                 }
                 SerializerRegistration result = Serializer.registerClassForId(id, type, serializer);
-                log.log( Level.FINE, "   result:{0}", result);                
+                log.log(Level.FINE, "   result:{0}", result);                
             } catch( ClassNotFoundException e ) {
                 throw new RuntimeException( "Class not found attempting to register:" + this, e );
             } catch( InstantiationException e ) {

+ 18 - 2
jme3-networking/src/main/java/com/jme3/network/serializing/Serializer.java

@@ -130,7 +130,7 @@ public abstract class Serializer {
         registerClass(IdentityHashMap.class,            new MapSerializer());
         registerClass(TreeMap.class,                    new MapSerializer());
         registerClass(WeakHashMap.class,                new MapSerializer());
-        
+ 
         registerClass(Enum.class,      new EnumSerializer());
         registerClass(GZIPCompressedMessage.class, new GZIPSerializer());
         registerClass(ZIPCompressedMessage.class, new ZIPSerializer());
@@ -331,7 +331,7 @@ public abstract class Serializer {
     @SuppressWarnings("unchecked")
     public static SerializerRegistration getSerializerRegistration(Class cls, boolean failOnMiss) {
         SerializerRegistration reg = classRegistrations.get(cls);
-
+        
         if (reg != null) return reg;
 
         for (Map.Entry<Class, SerializerRegistration> entry : classRegistrations.entrySet()) {
@@ -425,6 +425,22 @@ public abstract class Serializer {
             return;
         }
         SerializerRegistration reg = writeClass(buffer, object.getClass());
+        
+        // If the caller (or us) has registered a generic base class (like Enum)
+        // that is meant to steer automatic resolution for things like FieldSerializer
+        // that have final classes in fields... then there are cases where the exact 
+        // type isn't known by the outer class.  (Think of a message object
+        // that has an Object field but tries to send an Enum subclass in it.)
+        // In that case, the SerializerRegistration object we get back isn't
+        // really going to be capable of recreating the object on the other
+        // end because it won't know what class to use.  This only comes up
+        // in writeclassAndObejct() because we just wrote an ID to a more generic
+        // class than will be readable on the other end.  The check is simple, though.
+        if( reg.getType() != object.getClass() ) {
+            throw new IllegalArgumentException("Class has not been registered:" 
+                    + object.getClass() + " but resolved to generic serializer for:" + reg.getType());
+        } 
+
         reg.getSerializer().writeObject(buffer, object);
     }
 

+ 3 - 2
jme3-networking/src/main/java/com/jme3/network/serializing/serializers/EnumSerializer.java

@@ -48,8 +48,9 @@ public class EnumSerializer extends Serializer {
 
             if (ordinal == -1) return null;
             T[] enumConstants = c.getEnumConstants();
-            if (enumConstants == null)
-                throw new SerializerException( "Class has no enum constants:" + c );
+            if (enumConstants == null) {
+                throw new SerializerException("Class has no enum constants:" + c + "  Ordinal:" + ordinal);
+            }
             return enumConstants[ordinal];
         } catch (IndexOutOfBoundsException ex) {
             return null;

+ 34 - 5
jme3-networking/src/main/java/com/jme3/network/serializing/serializers/FieldSerializer.java

@@ -34,11 +34,14 @@ package com.jme3.network.serializing.serializers;
 import com.jme3.network.serializing.Serializer;
 import com.jme3.network.serializing.SerializerException;
 import java.io.IOException;
+import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
 import java.nio.BufferOverflowException;
 import java.nio.ByteBuffer;
 import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 /**
  * The field serializer is the default serializer used for custom class.
@@ -46,16 +49,35 @@ import java.util.*;
  * @author Lars Wesselius, Nathan Sweet
  */
 public class FieldSerializer extends Serializer {
+    
+    static final Logger log = Logger.getLogger(FieldSerializer.class.getName());
+
     private static Map<Class, SavedField[]> savedFields = new HashMap<Class, SavedField[]>();
+    private static Map<Class, Constructor> savedCtors = new HashMap<Class, Constructor>();
 
     protected void checkClass(Class clazz) {
     
         // See if the class has a public no-arg constructor
         try {
-            clazz.getConstructor();
+            savedCtors.put(clazz, clazz.getConstructor());
+            return;
+        } catch( NoSuchMethodException e ) {
+            //throw new RuntimeException( "Registration error: no-argument constructor not found on:" + clazz ); 
+        }
+        
+        // See if it has a non-public no-arg constructor
+        try {
+            Constructor ctor = clazz.getDeclaredConstructor();
+            
+            // Make sure we can call it later.
+            ctor.setAccessible(true);
+             
+            savedCtors.put(clazz, ctor);
+            return;
         } catch( NoSuchMethodException e ) {
-            throw new RuntimeException( "Registration error: no-argument constructor not found on:" + clazz ); 
-        } 
+        }
+        
+        throw new RuntimeException( "Registration error: no-argument constructor not found on:" + clazz );  
     }        
     
     public void initialize(Class clazz) {
@@ -121,7 +143,8 @@ public class FieldSerializer extends Serializer {
 
         T object;
         try {
-            object = c.newInstance();
+            Constructor<T> ctor = (Constructor<T>)savedCtors.get(c);
+            object = ctor.newInstance();
         } catch (Exception e) {
             throw new SerializerException( "Error creating object of type:" + c, e );
         }
@@ -129,6 +152,9 @@ public class FieldSerializer extends Serializer {
         for (SavedField savedField : fields) {
             Field field = savedField.field;
             Serializer serializer = savedField.serializer;
+            if( log.isLoggable(Level.FINER) ) {
+                log.log(Level.FINER, "Reading field:{0} using serializer:{1}", new Object[]{field, serializer});
+            }
             Object value;
 
             if (serializer != null) {
@@ -164,9 +190,12 @@ public class FieldSerializer extends Serializer {
             try {
                 val = savedField.field.get(object);
             } catch (IllegalAccessException e) {
-                e.printStackTrace();
+                throw new SerializerException("Unable to access field:" + savedField.field + " on:" + object, e);
             }
             Serializer serializer = savedField.serializer;
+            if( log.isLoggable(Level.FINER) ) {
+                log.log(Level.FINER, "Writing field:{0} using serializer:{1}", new Object[]{savedField.field, serializer});
+            }
 
             try {
                 if (serializer != null) {

+ 5 - 0
jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiRegistry.java

@@ -351,6 +351,11 @@ public class RmiRegistry {
         }
         
         public Object invoke( short procId, Object[] args ) {
+            if( log.isLoggable(Level.FINEST) ) {
+                log.finest("SharedObject->invoking:" + classInfo.getMethod(procId) 
+                           + " on:" + object 
+                           + " with:" + (args == null ? "null" : Arrays.asList(args))); 
+            }
             return classInfo.getMethod(procId).invoke(object, args);
         }
     }

+ 6 - 1
jme3-networking/src/main/java/com/jme3/network/service/rpc/RpcConnection.java

@@ -184,7 +184,7 @@ public class RpcConnection {
     
         if( log.isLoggable(Level.FINEST) ) {
             log.log(Level.FINEST, "handleMessage({0})", msg);
-        }    
+        }
         RpcHandler handler = handlers.get(msg.getObjectId());
         try {
             if( handler == null ) {
@@ -225,6 +225,7 @@ public class RpcConnection {
     private class ResponseHolder {
         private Object response;
         private String error;
+        private Throwable exception;
         private RpcCallMessage msg;
         boolean received = false;
  
@@ -235,6 +236,7 @@ public class RpcConnection {
         public synchronized void setResponse( RpcResponseMessage msg ) {
             this.response = msg.getResult();
             this.error = msg.getError();
+            this.exception = msg.getThrowable();
             this.received = true;
             notifyAll();
         }
@@ -250,6 +252,9 @@ public class RpcConnection {
             if( error != null ) {
                 throw new RuntimeException("Error calling remote procedure:" + msg + "\n" + error);
             }
+            if( exception != null ) {
+                throw new RuntimeException("Error calling remote procedure:" + msg, exception);
+            } 
             return response;              
         }
         

+ 32 - 6
jme3-networking/src/main/java/com/jme3/network/service/rpc/msg/RpcResponseMessage.java

@@ -34,6 +34,7 @@ package com.jme3.network.service.rpc.msg;
 
 import com.jme3.network.AbstractMessage;
 import com.jme3.network.serializing.Serializable;
+import com.jme3.network.serializing.Serializer;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 
@@ -50,6 +51,7 @@ public class RpcResponseMessage extends AbstractMessage {
     private long msgId;
     private Object result;
     private String error;
+    private Object exception; // if it was serializable
 
     public RpcResponseMessage() {
     }
@@ -61,12 +63,31 @@ public class RpcResponseMessage extends AbstractMessage {
 
     public RpcResponseMessage( long msgId, Throwable t ) {
         this.msgId = msgId;
-         
-        StringWriter sOut = new StringWriter();
-        PrintWriter out = new PrintWriter(sOut);
-        t.printStackTrace(out);
-        out.close();
-        this.error = sOut.toString();
+ 
+        // See if the exception is serializable
+        if( isSerializable(t) ) {
+            // Can send the exception itself
+            this.exception = t;
+        } else {
+            // We'll compose all of the info into a string           
+            StringWriter sOut = new StringWriter();
+            PrintWriter out = new PrintWriter(sOut);
+            t.printStackTrace(out);
+            out.close();
+            this.error = sOut.toString();
+        }
+    }
+ 
+    public static boolean isSerializable( Throwable error ) {
+        if( error == null ) {
+            return false;
+        }
+        for( Throwable t = error; t != null; t = t.getCause() ) {
+            if( Serializer.getExactSerializerRegistration(t.getClass()) == null ) {
+                return false;
+            }
+        }
+        return true; 
     }
  
     public long getMessageId() {
@@ -81,10 +102,15 @@ public class RpcResponseMessage extends AbstractMessage {
         return error;
     }
     
+    public Throwable getThrowable() {
+        return (Throwable)exception;
+    }
+    
     @Override
     public String toString() {
         return getClass().getSimpleName() + "[#" + msgId + ", result=" + result
                                           + (error != null ? ", error=" + error : "")
+                                          + (exception != null ? ", exception=" + exception : "")
                                           + "]";
     }
 }

+ 1 - 1
jme3-networking/src/main/java/com/jme3/network/service/serializer/ServerSerializerRegistrationsService.java

@@ -65,7 +65,7 @@ public class ServerSerializerRegistrationsService extends AbstractHostedService
     public void connectionAdded(Server server, HostedConnection hc) {
         // Just in case
         super.connectionAdded(server, hc);
-        
+ 
         // Send the client the registration information
         hc.send(SerializerRegistrationsMessage.INSTANCE);
     }

+ 2 - 2
sdk/jme3-assetpack-support/src/com/jme3/gde/assetpack/browser/Bundle.properties

@@ -1,6 +1,6 @@
 CTL_AssetPackBrowserAction=AssetPackBrowser
-CTL_AssetPackBrowserTopComponent=AssetPackBrowser Window
-HINT_AssetPackBrowserTopComponent=This is a AssetPackBrowser window
+CTL_AssetPackBrowserTopComponent=AssetPackBrowser
+HINT_AssetPackBrowserTopComponent=The AssetPackBrowser allows easy managing of your AssetPacks
 AssetPackBrowserTopComponent.jTextField1.text=search
 AssetPackBrowserTopComponent.jButton1.text=update
 AssetPackBrowserTopComponent.jButton2.text=online assetpacks

+ 2 - 2
sdk/jme3-core/src/com/jme3/gde/core/appstates/AppStateExplorerTopComponent.java

@@ -67,8 +67,8 @@ persistenceType = TopComponent.PERSISTENCE_ALWAYS)
 preferredID = "AppStateExplorerTopComponent")
 @Messages({
     "CTL_AppStateExplorerAction=AppStateExplorer",
-    "CTL_AppStateExplorerTopComponent=AppStateExplorer Window",
-    "HINT_AppStateExplorerTopComponent=This is a AppStateExplorer window"
+    "CTL_AppStateExplorerTopComponent=AppStateExplorer",
+    "HINT_AppStateExplorerTopComponent=The AppStateExplorer provides an Overview over your current AppState"
 })
 public final class AppStateExplorerTopComponent extends TopComponent implements ExplorerManager.Provider {
 

+ 2 - 2
sdk/jme3-core/src/com/jme3/gde/core/filters/Bundle.properties

@@ -1,3 +1,3 @@
 CTL_FilterExplorerAction=FilterExplorer
-CTL_FilterExplorerTopComponent=FilterExplorer Window
-HINT_FilterExplorerTopComponent=This is a FilterExplorer window
+CTL_FilterExplorerTopComponent=FilterExplorer
+HINT_FilterExplorerTopComponent=The FilterExplorer provides an Overview over your current Filter

+ 2 - 2
sdk/jme3-core/src/com/jme3/gde/core/sceneexplorer/Bundle.properties

@@ -1,4 +1,4 @@
 CTL_SceneExplorerAction=SceneExplorer
-CTL_SceneExplorerTopComponent=SceneExplorer Window
-HINT_SceneExplorerTopComponent=This is a SceneExplorer window
+CTL_SceneExplorerTopComponent=SceneExplorer
+HINT_SceneExplorerTopComponent=The SceneExplorer provides an Overview over the SceneGraph of your Scene.
 SceneExplorerTopComponent.jButton1.text=update

+ 65 - 20
sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/sky/SkyboxWizardPanel2.java

@@ -32,14 +32,16 @@
 package com.jme3.gde.terraineditor.sky;
 
 import com.jme3.math.Vector3f;
+import com.jme3.texture.Image;
 import com.jme3.texture.Texture;
 import java.awt.Component;
 import javax.swing.event.ChangeListener;
 import org.openide.WizardDescriptor;
+import org.openide.WizardValidationException;
 import org.openide.util.HelpCtx;
 
 @SuppressWarnings({"unchecked", "rawtypes"})
-public class SkyboxWizardPanel2 implements WizardDescriptor.Panel {
+public class SkyboxWizardPanel2 implements WizardDescriptor.ValidatingPanel<WizardDescriptor> {
 
     /**
      * The visual component that displays this panel. If you need to access the
@@ -76,10 +78,12 @@ public class SkyboxWizardPanel2 implements WizardDescriptor.Panel {
         // fireChangeEvent();
         // and uncomment the complicated stuff below.
     }
-
+    
+    @Override
     public final void addChangeListener(ChangeListener l) {
     }
-
+    
+    @Override
     public final void removeChangeListener(ChangeListener l) {
     }
     /*
@@ -105,14 +109,56 @@ public class SkyboxWizardPanel2 implements WizardDescriptor.Panel {
     }
     }
      */
-
+    
+    @Override
+    public void validate() throws WizardValidationException {
+        SkyboxVisualPanel2 sky = (SkyboxVisualPanel2)component;
+        
+        /* Check if there are empty textures */
+        if (multipleTextures) {
+            if (sky.getEditorNorth().getAsText() == null) { throw new WizardValidationException(null, " Texture North: Missing texture!", null); }
+            if (sky.getEditorSouth().getAsText() == null) { throw new WizardValidationException(null, " Texture South: Missing texture!", null); }
+            if (sky.getEditorWest().getAsText() == null) { throw new WizardValidationException(null, " Texture West: Missing texture!", null); }
+            if (sky.getEditorEast().getAsText() == null) { throw new WizardValidationException(null, " Texture East: Missing texture!", null); }
+            if (sky.getEditorTop().getAsText() == null) { throw new WizardValidationException(null, " Texture Top: Missing texture!", null); }
+            if (sky.getEditorBottom().getAsText() == null) { throw new WizardValidationException(null, " Texture Bottom: Missing texture!", null); }
+            
+            /* Prevent Null-Pointer Exception. If this is triggered, the Texture has no Image or the AssetKey is invalid (which should never happen) */
+            if (sky.getEditorNorth().getValue() == null || ((Texture)sky.getEditorNorth().getValue()).getImage() == null) { throw new WizardValidationException(null, " Texture North: Cannot load texture!", null); }
+            if (sky.getEditorSouth().getValue() == null || ((Texture)sky.getEditorSouth().getValue()).getImage() == null) { throw new WizardValidationException(null, " Texture South: Cannot load texture!", null); }
+            if (sky.getEditorWest().getValue() == null || ((Texture)sky.getEditorWest().getValue()).getImage() == null) { throw new WizardValidationException(null, " Texture West: Cannot load texture!", null); }
+            if (sky.getEditorEast().getValue() == null || ((Texture)sky.getEditorEast().getValue()).getImage() == null) { throw new WizardValidationException(null, " Texture East: Cannot load texture!", null); }
+            if (sky.getEditorTop().getValue() == null || ((Texture)sky.getEditorTop().getValue()).getImage() == null) { throw new WizardValidationException(null, " Texture Top: Cannot load texture!", null); }
+            if (sky.getEditorBottom().getValue() == null || ((Texture)sky.getEditorBottom().getValue()).getImage() == null) { throw new WizardValidationException(null, " Texture Bottom: Cannot load texture!", null); }
+            
+            /* Check for squares */
+            Image I = ((Texture)sky.getEditorNorth().getValue()).getImage();
+            if (I.getWidth() != I.getHeight()) { throw new WizardValidationException(null, " Texture North: Image has to be a square (width == height)!", null); }
+            I = ((Texture)sky.getEditorSouth().getValue()).getImage();
+            if (I.getWidth() != I.getHeight()) { throw new WizardValidationException(null, " Texture South: Image has to be a square (width == height)!", null); }
+            I = ((Texture)sky.getEditorWest().getValue()).getImage();
+            if (I.getWidth() != I.getHeight()) { throw new WizardValidationException(null, " Texture West: Image has to be a square (width == height)!", null); }
+            I = ((Texture)sky.getEditorEast().getValue()).getImage();
+            if (I.getWidth() != I.getHeight()) { throw new WizardValidationException(null, " Texture East: Image has to be a square (width == height)!", null); }
+            I = ((Texture)sky.getEditorTop().getValue()).getImage();
+            if (I.getWidth() != I.getHeight()) { throw new WizardValidationException(null, " Texture Top: Image has to be a square (width == height)!", null); }
+            I = ((Texture)sky.getEditorBottom().getValue()).getImage();
+            if (I.getWidth() != I.getHeight()) { throw new WizardValidationException(null, " Texture Bottom: Image has to be a square (width == height)!", null); }
+        } else {
+            if (sky.getEditorSingle().getAsText() == null){ throw new WizardValidationException(null, " Single Texture: Missing texture!", null); }
+            if (sky.getEditorSingle().getValue() == null || ((Texture)sky.getEditorSingle().getValue()).getImage() == null){ throw new WizardValidationException(null, " Single Texture: Cannot load texture!", null); }
+            Image I = ((Texture)sky.getEditorSingle().getValue()).getImage();
+            if (I.getWidth() != I.getHeight()) { throw new WizardValidationException(null, " Single Texture: Image has to be a square (width == height)!", null); }
+        }
+    }
+    
     // You can use a settings object to keep track of state. Normally the
     // settings object will be the WizardDescriptor, so you can use
     // WizardDescriptor.getProperty & putProperty to store information entered
     // by the user.
-    public void readSettings(Object settings) {
-        WizardDescriptor wiz = (WizardDescriptor) settings;
-        multipleTextures = (Boolean)wiz.getProperty("multipleTextures");
+    @Override
+    public void readSettings(WizardDescriptor settings) {
+        multipleTextures = (Boolean)settings.getProperty("multipleTextures");
         SkyboxVisualPanel2 comp = (SkyboxVisualPanel2) getComponent();
         if (multipleTextures) {
             comp.getMultipleTexturePanel().setVisible(true);
@@ -124,28 +170,27 @@ public class SkyboxWizardPanel2 implements WizardDescriptor.Panel {
     }
 
     @Override
-    public void storeSettings(Object settings) {
-        WizardDescriptor wiz = (WizardDescriptor) settings;
+    public void storeSettings(WizardDescriptor settings) {
         SkyboxVisualPanel2 comp = (SkyboxVisualPanel2) getComponent();
         if (multipleTextures) {
-            wiz.putProperty("textureSouth", (Texture)comp.getEditorSouth().getValue());
-            wiz.putProperty("textureNorth", (Texture)comp.getEditorNorth().getValue());
-            wiz.putProperty("textureEast", (Texture)comp.getEditorEast().getValue());
-            wiz.putProperty("textureWest", (Texture)comp.getEditorWest().getValue());
-            wiz.putProperty("textureTop", (Texture)comp.getEditorTop().getValue());
-            wiz.putProperty("textureBottom", (Texture)comp.getEditorBottom().getValue());
+            settings.putProperty("textureSouth", (Texture)comp.getEditorSouth().getValue());
+            settings.putProperty("textureNorth", (Texture)comp.getEditorNorth().getValue());
+            settings.putProperty("textureEast", (Texture)comp.getEditorEast().getValue());
+            settings.putProperty("textureWest", (Texture)comp.getEditorWest().getValue());
+            settings.putProperty("textureTop", (Texture)comp.getEditorTop().getValue());
+            settings.putProperty("textureBottom", (Texture)comp.getEditorBottom().getValue());
             float x = new Float(comp.getNormal1X().getText());
             float y = new Float(comp.getNormal1Y().getText());
             float z = new Float(comp.getNormal1Z().getText());
-            wiz.putProperty("normalScale", new Vector3f(x,y,z) );
+            settings.putProperty("normalScale", new Vector3f(x,y,z) );
         } else {
-            wiz.putProperty("textureSingle", (Texture)comp.getEditorSingle().getValue());
+            settings.putProperty("textureSingle", (Texture)comp.getEditorSingle().getValue());
             float x = new Float(comp.getNormal2X().getText());
             float y = new Float(comp.getNormal2Y().getText());
             float z = new Float(comp.getNormal2Z().getText());
-            wiz.putProperty("normalScale", new Vector3f(x,y,z) );
-            wiz.putProperty("envMapType", comp.getEnvMapType());         
-            wiz.putProperty("flipY", comp.getFlipYCheckBox().isSelected());
+            settings.putProperty("normalScale", new Vector3f(x,y,z) );
+            settings.putProperty("envMapType", comp.getEnvMapType());         
+            settings.putProperty("flipY", comp.getFlipYCheckBox().isSelected());
         }
     }
 }