Sfoglia il codice sorgente

Merge branch 'jMonkeyEngine:master' into master

bob0bob 1 anno fa
parent
commit
7dd86939f1
61 ha cambiato i file con 2615 aggiunte e 555 eliminazioni
  1. 1 1
      .github/workflows/format.yml
  2. 2 1
      .gitignore
  3. 6 0
      .vscode/extensions.json
  4. 9 1
      .vscode/settings.json
  5. 1 1
      common.gradle
  6. BIN
      gradle/wrapper/gradle-wrapper.jar
  7. 1 1
      gradle/wrapper/gradle-wrapper.properties
  8. 9 2
      jme3-core/src/main/java/com/jme3/export/FormatVersion.java
  9. 19 4
      jme3-core/src/main/java/com/jme3/input/FlyByCamera.java
  10. 29 8
      jme3-core/src/main/java/com/jme3/material/Material.java
  11. 75 0
      jme3-core/src/main/java/com/jme3/material/RenderState.java
  12. 1 11
      jme3-core/src/main/java/com/jme3/material/TechniqueDef.java
  13. 2 2
      jme3-core/src/main/java/com/jme3/post/Filter.java
  14. 9 1
      jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java
  15. 0 8
      jme3-core/src/main/java/com/jme3/renderer/RenderManager.java
  16. 28 15
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java
  17. 4 0
      jme3-core/src/main/java/com/jme3/renderer/queue/RenderQueue.java
  18. 12 0
      jme3-core/src/main/java/com/jme3/scene/Geometry.java
  19. 46 29
      jme3-core/src/main/java/com/jme3/scene/Mesh.java
  20. 159 0
      jme3-core/src/main/java/com/jme3/scene/SceneGraphIterator.java
  21. 1 1
      jme3-core/src/main/java/com/jme3/scene/UserData.java
  22. 30 21
      jme3-core/src/main/java/com/jme3/shader/DefineList.java
  23. 5 0
      jme3-core/src/main/java/com/jme3/shadow/ShadowUtil.java
  24. 5 5
      jme3-core/src/main/java/com/jme3/texture/Texture.java
  25. 2 0
      jme3-core/src/main/java/com/jme3/util/SkyFactory.java
  26. 1 0
      jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow.vert
  27. 1 1
      jme3-core/src/main/resources/Common/MatDefs/Shadow/PreShadow.vert
  28. 5 5
      jme3-core/src/test/java/com/jme3/renderer/OpaqueComparatorTest.java
  29. 57 0
      jme3-core/src/test/java/com/jme3/scene/mesh/MeshTest.java
  30. 50 31
      jme3-core/src/test/java/com/jme3/shader/DefineListTest.java
  31. 141 95
      jme3-examples/src/main/java/jme3test/TestChooser.java
  32. 34 6
      jme3-examples/src/main/java/jme3test/export/TestIssue2068.java
  33. 2 2
      jme3-examples/src/main/java/jme3test/light/TestSpotLightShadows.java
  34. 7 6
      jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java
  35. 95 0
      jme3-examples/src/main/java/jme3test/model/TestGltfNaming.java
  36. 73 0
      jme3-examples/src/main/java/jme3test/model/TestGltfVertexColor.java
  37. 145 0
      jme3-examples/src/main/java/jme3test/model/anim/TestGltfMorph.java
  38. 127 0
      jme3-examples/src/main/java/jme3test/scene/TestSceneIteration.java
  39. BIN
      jme3-examples/src/main/resources/jme3test/gltfnaming/multi.bin
  40. 202 0
      jme3-examples/src/main/resources/jme3test/gltfnaming/multi.gltf
  41. BIN
      jme3-examples/src/main/resources/jme3test/gltfnaming/parent.bin
  42. 193 0
      jme3-examples/src/main/resources/jme3test/gltfnaming/parent.gltf
  43. BIN
      jme3-examples/src/main/resources/jme3test/gltfnaming/single.bin
  44. 121 0
      jme3-examples/src/main/resources/jme3test/gltfnaming/single.gltf
  45. BIN
      jme3-examples/src/main/resources/jme3test/gltfnaming/untitled.bin
  46. 121 0
      jme3-examples/src/main/resources/jme3test/gltfnaming/untitled.gltf
  47. BIN
      jme3-examples/src/main/resources/jme3test/gltfvertexcolor/VertexColorTest.glb
  48. BIN
      jme3-examples/src/main/resources/jme3test/morph/MorphStressTest.glb
  49. 4 1
      jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglAbstractDisplay.java
  50. 31 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/AnimationList.java
  51. 24 0
      jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/SceneKey.java
  52. 1 0
      jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/CustomContentManager.java
  53. 185 160
      jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java
  54. 30 0
      jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfModelKey.java
  55. 63 0
      jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/PBREmissiveStrengthExtensionLoader.java
  56. 46 0
      jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/PBREmissiveStrengthMaterialAdapter.java
  57. 20 12
      jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/PBRMaterialAdapter.java
  58. 146 4
      jme3-plugins/src/test/java/com/jme3/export/JmeExporterTest.java
  59. 61 59
      jme3-plugins/src/xml/java/com/jme3/export/xml/DOMInputCapsule.java
  60. 63 61
      jme3-plugins/src/xml/java/com/jme3/export/xml/DOMOutputCapsule.java
  61. 110 0
      jme3-plugins/src/xml/java/com/jme3/export/xml/XMLUtils.java

+ 1 - 1
.github/workflows/format.yml

@@ -5,7 +5,7 @@ on:
 jobs:
   format:
     runs-on: ubuntu-latest
-    if: github.repository != 'jMonkeyEngine/jmonkeyengine'
+    if: ${{ false }} 
     steps:
       - name: Checkout
         uses: actions/checkout@v4

+ 2 - 1
.gitignore

@@ -48,4 +48,5 @@ appveyor.yml
 javadoc_deploy
 javadoc_deploy.pub
 !.vscode/settings.json
-!.vscode/JME_style.xml
+!.vscode/JME_style.xml
+!.vscode/extensions.json

+ 6 - 0
.vscode/extensions.json

@@ -0,0 +1,6 @@
+{
+	"recommendations": [
+                "vscjava.vscode-java-pack",
+                "slevesque.shader"
+	]
+}

+ 9 - 1
.vscode/settings.json

@@ -1,7 +1,15 @@
 {
     "java.configuration.updateBuildConfiguration": "automatic",
+    "java.compile.nullAnalysis.mode": "automatic",
     "java.refactor.renameFromFileExplorer": "prompt",
     "java.format.settings.url": "./.vscode/JME_style.xml",
     "editor.formatOnPaste": true,
-    "editor.formatOnType": true
+    "editor.formatOnType": false,
+    "editor.formatOnSave": true,
+    "editor.formatOnSaveMode": "modifications" ,
+
+    "prettier.tabWidth": 4,
+    "prettier.printWidth": 110,
+    "prettier.enable": true,
+    "prettier.resolveGlobalModules": true
 }

+ 1 - 1
common.gradle

@@ -28,7 +28,7 @@ tasks.withType(JavaCompile) { // compile-time options:
 }
 
 ext {
-    lwjgl3Version = '3.3.2' // used in both the jme3-lwjgl3 and jme3-vr build scripts
+    lwjgl3Version = '3.3.3' // used in both the jme3-lwjgl3 and jme3-vr build scripts
     niftyVersion = '1.4.3' // used in both the jme3-niftygui and jme3-examples build scripts
 }
 

BIN
gradle/wrapper/gradle-wrapper.jar


+ 1 - 1
gradle/wrapper/gradle-wrapper.properties

@@ -1,6 +1,6 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-bin.zip
 networkTimeout=10000
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists

+ 9 - 2
jme3-core/src/main/java/com/jme3/export/FormatVersion.java

@@ -39,9 +39,16 @@ package com.jme3.export;
 public final class FormatVersion {
     
     /**
-     * Version number of the format
+     * Version number of the format.
+     * <p>
+     * Changes for each version:
+     * <ol>
+     *   <li>Undocumented
+     *   <li>Undocumented
+     *   <li>XML prefixes "jme-" to all key names
+     * </ol>
      */
-    public static final int VERSION = 2;
+    public static final int VERSION = 3;
 
     /**
      * Signature of the format: currently, "JME3" as ASCII.

+ 19 - 4
jme3-core/src/main/java/com/jme3/input/FlyByCamera.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2023 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -383,9 +383,24 @@ public class FlyByCamera implements AnalogListener, ActionListener {
      * @param value zoom amount
      */
     protected void zoomCamera(float value) {
-        float newFov = cam.getFov() + value * 0.1F * zoomSpeed;
-        if (newFov > 0) {
-            cam.setFov(newFov);
+        if (cam.isParallelProjection()) {
+            float zoomFactor = 1.0F + value * 0.01F * zoomSpeed;
+            if (zoomFactor > 0F) {
+                float left = zoomFactor * cam.getFrustumLeft();
+                float right = zoomFactor * cam.getFrustumRight();
+                float top = zoomFactor * cam.getFrustumTop();
+                float bottom = zoomFactor * cam.getFrustumBottom();
+
+                float near = cam.getFrustumNear();
+                float far = cam.getFrustumFar();
+                cam.setFrustum(near, far, left, right, top, bottom);
+            }
+
+        } else { // perspective projection
+            float newFov = cam.getFov() + value * 0.1F * zoomSpeed;
+            if (newFov > 0) {
+                cam.setFov(newFov);
+            }
         }
     }
 

+ 29 - 8
jme3-core/src/main/java/com/jme3/material/Material.java

@@ -890,16 +890,37 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         return type == VarType.BufferObject;
     }
 
-    private void updateRenderState(RenderManager renderManager, Renderer renderer, TechniqueDef techniqueDef) {
+    private void updateRenderState(Geometry geometry, RenderManager renderManager, Renderer renderer, TechniqueDef techniqueDef) {
         if (renderManager.getForcedRenderState() != null) {
-            renderer.applyRenderState(renderManager.getForcedRenderState());
+            mergedRenderState.copyFrom(renderManager.getForcedRenderState());
+        } else if (techniqueDef.getRenderState() != null) {
+            mergedRenderState.copyFrom(RenderState.DEFAULT);
+            techniqueDef.getRenderState().copyMergedTo(additionalState, mergedRenderState);
         } else {
-            if (techniqueDef.getRenderState() != null) {
-                renderer.applyRenderState(techniqueDef.getRenderState().copyMergedTo(additionalState, mergedRenderState));
-            } else {
-                renderer.applyRenderState(RenderState.DEFAULT.copyMergedTo(additionalState, mergedRenderState));
-            }
+            mergedRenderState.copyFrom(RenderState.DEFAULT);
+            RenderState.DEFAULT.copyMergedTo(additionalState, mergedRenderState);
         }
+        // test if the face cull mode should be flipped before render
+        if (mergedRenderState.isFaceCullFlippable() && isNormalsBackward(geometry.getWorldScale())) {
+            mergedRenderState.flipFaceCull();
+        }
+        renderer.applyRenderState(mergedRenderState);
+    }
+    
+    /**
+     * Returns true if the geometry world scale indicates that normals will be backward.
+     * @param scalar geometry world scale
+     * @return 
+     */
+    private boolean isNormalsBackward(Vector3f scalar) {
+        // count number of negative scalar vector components
+        int n = 0;
+        if (scalar.x < 0) n++;
+        if (scalar.y < 0) n++;
+        if (scalar.z < 0) n++;
+        // An odd number of negative components means the normal vectors
+        // are backward to what they should be.
+        return n == 1 || n == 3;
     }
     
     /**
@@ -1028,7 +1049,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         }
 
         // Apply render state
-        updateRenderState(renderManager, renderer, techniqueDef);
+        updateRenderState(geometry, renderManager, renderer, techniqueDef);
 
         // Get world overrides
         SafeArrayList<MatParamOverride> overrides = geometry.getWorldMatParamOverrides();

+ 75 - 0
jme3-core/src/main/java/com/jme3/material/RenderState.java

@@ -1685,6 +1685,56 @@ public class RenderState implements Cloneable, Savable {
         sfactorAlpha = state.sfactorAlpha;
         dfactorAlpha = state.dfactorAlpha;
     }
+    
+    /**
+     * Copy all values from the given state to this state.
+     * <p>
+     * This method is more precise than {@link #set(com.jme3.material.RenderState)}.
+     * @param state state to copy from
+     */
+    public void copyFrom(RenderState state) {
+        this.applyBlendMode = state.applyBlendMode;
+        this.applyColorWrite = state.applyColorWrite;
+        this.applyCullMode = state.applyCullMode;
+        this.applyDepthFunc = state.applyDepthFunc;
+        this.applyDepthTest = state.applyDepthTest;
+        this.applyDepthWrite = state.applyDepthWrite;
+        this.applyLineWidth = state.applyLineWidth;
+        this.applyPolyOffset = state.applyPolyOffset;
+        this.applyStencilTest = state.applyStencilTest;
+        this.applyWireFrame = state.applyWireFrame;
+        this.backStencilDepthFailOperation = state.backStencilDepthFailOperation;
+        this.backStencilDepthPassOperation = state.backStencilDepthPassOperation;
+        this.backStencilFunction = state.backStencilFunction;
+        this.backStencilMask = state.backStencilMask;
+        this.backStencilReference = state.backStencilReference;
+        this.backStencilStencilFailOperation = state.backStencilStencilFailOperation;
+        this.blendEquation = state.blendEquation;
+        this.blendEquationAlpha = state.blendEquationAlpha;
+        this.blendMode = state.blendMode;
+        this.cachedHashCode = state.cachedHashCode;
+        this.colorWrite = state.colorWrite;
+        this.cullMode = state.cullMode;
+        this.depthFunc = state.depthFunc;
+        this.depthTest = state.depthTest;
+        this.depthWrite = state.depthWrite;
+        this.dfactorAlpha = state.dfactorAlpha;
+        this.dfactorRGB = state.dfactorRGB;
+        this.frontStencilDepthFailOperation = state.frontStencilDepthFailOperation;
+        this.frontStencilDepthPassOperation = state.frontStencilDepthPassOperation;
+        this.frontStencilFunction = state.frontStencilFunction;
+        this.frontStencilMask = state.frontStencilMask;
+        this.frontStencilReference = state.frontStencilReference;
+        this.frontStencilStencilFailOperation = state.frontStencilStencilFailOperation;
+        this.lineWidth = state.lineWidth;
+        this.offsetEnabled = state.offsetEnabled;
+        this.offsetFactor = state.offsetFactor;
+        this.offsetUnits = state.offsetUnits;
+        this.sfactorAlpha = state.sfactorAlpha;
+        this.sfactorRGB = state.sfactorRGB;
+        this.stencilTest = state.stencilTest;
+        this.wireframe = state.wireframe;
+    }
 
     @Override
     public String toString() {
@@ -1711,4 +1761,29 @@ public class RenderState implements Cloneable, Savable {
                 + (blendMode.equals(BlendMode.Custom)? "\ncustomBlendFactors=("+sfactorRGB+", "+dfactorRGB+", "+sfactorAlpha+", "+dfactorAlpha+")":"")
                 +"\n]";
     }
+    
+    /**
+     * Flips the given face cull mode so that {@code Back} becomes
+     * {@code Front} and {@code Front} becomes {@code Back}.
+     * <p>{@code FrontAndBack} and {@code Off} are unaffected. This is important
+     * for flipping the cull mode when normal vectors are found to be backward.
+     * @param cull
+     * @return flipped cull mode
+     */
+    public void flipFaceCull() {
+        switch (cullMode) {
+            case Back:  cullMode = FaceCullMode.Front; break;
+            case Front: cullMode = FaceCullMode.Back;  break;
+        }
+    }
+    
+    /**
+     * Checks if the face cull mode is "flippable".
+     * <p>The cull mode is flippable when it is either {@code Front} or {@code Back}.
+     * @return 
+     */
+    public boolean isFaceCullFlippable() {
+        return cullMode == FaceCullMode.Front || cullMode == FaceCullMode.Back;
+    }
+    
 }

+ 1 - 11
jme3-core/src/main/java/com/jme3/material/TechniqueDef.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2023 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -422,11 +422,6 @@ public class TechniqueDef implements Savable, Cloneable {
     public void addShaderParamDefine(String paramName, VarType paramType, String defineName) {
         int defineId = defineNames.size();
 
-        if (defineId >= DefineList.MAX_DEFINES) {
-            throw new IllegalStateException("Cannot have more than " +
-                    DefineList.MAX_DEFINES + " defines on a technique.");
-        }
-
         paramToDefineId.put(paramName, defineId);
         defineNames.add(defineName);
         defineTypes.add(paramType);
@@ -445,11 +440,6 @@ public class TechniqueDef implements Savable, Cloneable {
     public int addShaderUnmappedDefine(String defineName, VarType defineType) {
         int defineId = defineNames.size();
 
-        if (defineId >= DefineList.MAX_DEFINES) {
-            throw new IllegalStateException("Cannot have more than " +
-                    DefineList.MAX_DEFINES + " defines on a technique.");
-        }
-
         defineNames.add(defineName);
         defineTypes.add(defineType);
         return defineId;

+ 2 - 2
jme3-core/src/main/java/com/jme3/post/Filter.java

@@ -108,7 +108,7 @@ public abstract class Filter implements Savable {
         public void init(Renderer renderer, int width, int height, Format textureFormat, Format depthBufferFormat, int numSamples, boolean renderDepth) {
             Collection<Caps> caps = renderer.getCaps();
             if (numSamples > 1 && caps.contains(Caps.FrameBufferMultisample) && caps.contains(Caps.OpenGL31)) {
-                renderFrameBuffer = new FrameBuffer(width, height, numSamples);                
+                renderFrameBuffer = new FrameBuffer(width, height, numSamples);
                 renderedTexture = new Texture2D(width, height, numSamples, textureFormat);
                 renderFrameBuffer.setDepthTarget(FrameBufferTarget.newTarget(depthBufferFormat));
                 if (renderDepth) {
@@ -126,7 +126,7 @@ public abstract class Filter implements Savable {
             }
 
             renderFrameBuffer.addColorTarget(FrameBufferTarget.newTarget(renderedTexture));
-
+            renderFrameBuffer.setName(getClass().getSimpleName());      
 
         }
 

+ 9 - 1
jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java

@@ -295,7 +295,7 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
                 if (msDepth && filter.isRequiresDepthTexture()) {
                     mat.setInt("NumSamplesDepth", depthTexture.getImage().getMultiSamples());
                 }
-
+                
                 if (filter.isRequiresSceneTexture()) {
                     mat.setTexture("Texture", tex);
                     if (tex.getImage().getMultiSamples() > 1) {
@@ -508,6 +508,14 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
             renderFrameBuffer.addColorTarget(FrameBufferTarget.newTarget(filterTexture));
         }
 
+        if (renderFrameBufferMS != null) {
+            renderFrameBufferMS.setName("FilterPostProcessor MS");
+        }
+
+        if (renderFrameBuffer != null) {
+            renderFrameBuffer.setName("FilterPostProcessor");
+        }
+
         for (Filter filter : filters.getArray()) {
             initFilter(filter, vp);
         }

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

@@ -967,9 +967,7 @@ public class RenderManager {
         if (prof != null) {
             prof.vpStep(VpStep.RenderBucket, vp, Bucket.Opaque);
         }
-        this.renderer.pushDebugGroup(Bucket.Opaque.name());
         rq.renderQueue(Bucket.Opaque, this, cam, flush);
-        this.renderer.popDebugGroup();
 
         // render the sky, with depth range set to the farthest
         if (!rq.isQueueEmpty(Bucket.Sky)) {
@@ -977,9 +975,7 @@ public class RenderManager {
                 prof.vpStep(VpStep.RenderBucket, vp, Bucket.Sky);
             }
             renderer.setDepthRange(1, 1);
-            this.renderer.pushDebugGroup(Bucket.Sky.name());
             rq.renderQueue(Bucket.Sky, this, cam, flush);
-            this.renderer.popDebugGroup();
             depthRangeChanged = true;
         }
 
@@ -995,9 +991,7 @@ public class RenderManager {
                 renderer.setDepthRange(0, 1);
                 depthRangeChanged = false;
             }
-            this.renderer.pushDebugGroup(Bucket.Transparent.name());
             rq.renderQueue(Bucket.Transparent, this, cam, flush);
-            this.renderer.popDebugGroup();
         }
 
         if (!rq.isQueueEmpty(Bucket.Gui)) {
@@ -1006,9 +1000,7 @@ public class RenderManager {
             }
             renderer.setDepthRange(0, 0);
             setCamera(cam, true);
-            this.renderer.pushDebugGroup(Bucket.Gui.name());
             rq.renderQueue(Bucket.Gui, this, cam, flush);
-            this.renderer.popDebugGroup();
             setCamera(cam, false);
             depthRangeChanged = true;
         }

+ 28 - 15
jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java

@@ -616,7 +616,7 @@ public final class GLRenderer implements Renderer {
             caps.add(Caps.UnpackRowLength);
         }
 
-        if (caps.contains(Caps.OpenGL43) || hasExtension("GL_KHR_debug")) {
+        if (caps.contains(Caps.OpenGL43) || hasExtension("GL_KHR_debug") || caps.contains(Caps.WebGL)) {
             caps.add(Caps.GLDebug);
         }
 
@@ -1337,32 +1337,33 @@ public final class GLRenderer implements Renderer {
         switch (uniform.getVarType()) {
             case Float:
                 Float f = (Float) uniform.getValue();
-                assert isValidNumber(f) : "Invalid float value " + f;
+                assert isValidNumber(f) : "Invalid float value " + f + " for " + uniform.getBinding();
                 gl.glUniform1f(loc, f.floatValue());
                 break;
             case Vector2:
                 Vector2f v2 = (Vector2f) uniform.getValue();
-                assert isValidNumber(v2) : "Invalid Vector2f value " + v2;
+                assert isValidNumber(v2) : "Invalid Vector2f value " + v2 + " for " + uniform.getBinding();
                 gl.glUniform2f(loc, v2.getX(), v2.getY());
                 break;
             case Vector3:
                 Vector3f v3 = (Vector3f) uniform.getValue();
-                assert isValidNumber(v3) : "Invalid Vector3f value " + v3;
+                assert isValidNumber(v3) : "Invalid Vector3f value " + v3 + " for " + uniform.getBinding();
                 gl.glUniform3f(loc, v3.getX(), v3.getY(), v3.getZ());
                 break;
             case Vector4:
                 Object val = uniform.getValue();
                 if (val instanceof ColorRGBA) {
                     ColorRGBA c = (ColorRGBA) val;
-                    assert isValidNumber(c) : "Invalid ColorRGBA value " + c;
+                    assert isValidNumber(c) : "Invalid ColorRGBA value " + c + " for " + uniform.getBinding();
                     gl.glUniform4f(loc, c.r, c.g, c.b, c.a);
                 } else if (val instanceof Vector4f) {
                     Vector4f c = (Vector4f) val;
-                    assert isValidNumber(c) : "Invalid Vector4f value " + c;
+                    assert isValidNumber(c) : "Invalid Vector4f value " + c + " for " + uniform.getBinding();
                     gl.glUniform4f(loc, c.x, c.y, c.z, c.w);
                 } else {
                     Quaternion c = (Quaternion) uniform.getValue();
-                    assert isValidNumber(c) : "Invalid Quaternion value " + c;
+                    assert isValidNumber(c) : "Invalid Quaternion value " + c + " for "
+                            + uniform.getBinding();
                     gl.glUniform4f(loc, c.getX(), c.getY(), c.getZ(), c.getW());
                 }
                 break;
@@ -1372,13 +1373,15 @@ public final class GLRenderer implements Renderer {
                 break;
             case Matrix3:
                 fb = uniform.getMultiData();
-                assert isValidNumber(fb) : "Invalid Matrix3f value " + uniform.getValue();
+                assert isValidNumber(fb) : "Invalid Matrix3f value " + uniform.getValue() + " for "
+                        + uniform.getBinding();
                 assert fb.remaining() == 9;
                 gl.glUniformMatrix3(loc, false, fb);
                 break;
             case Matrix4:
                 fb = uniform.getMultiData();
-                assert isValidNumber(fb) : "Invalid Matrix4f value " + uniform.getValue();
+                assert isValidNumber(fb) : "Invalid Matrix4f value " + uniform.getValue() + " for "
+                        + uniform.getBinding();
                 assert fb.remaining() == 16;
                 gl.glUniformMatrix4(loc, false, fb);
                 break;
@@ -1388,27 +1391,36 @@ public final class GLRenderer implements Renderer {
                 break;
             case FloatArray:
                 fb = uniform.getMultiData();
-                assert isValidNumber(fb) : "Invalid float array value " + Arrays.asList((float[]) uniform.getValue());
+                assert isValidNumber(fb) : "Invalid float array value "
+                        + Arrays.asList((float[]) uniform.getValue()) + " for " + uniform.getBinding();
                 gl.glUniform1(loc, fb);
                 break;
             case Vector2Array:
                 fb = uniform.getMultiData();
-                assert isValidNumber(fb) : "Invalid Vector2f array value " + Arrays.deepToString((Vector2f[]) uniform.getValue());
+                assert isValidNumber(fb) : "Invalid Vector2f array value "
+                        + Arrays.deepToString((Vector2f[]) uniform.getValue()) + " for "
+                        + uniform.getBinding();
                 gl.glUniform2(loc, fb);
                 break;
             case Vector3Array:
                 fb = uniform.getMultiData();
-                assert isValidNumber(fb) : "Invalid Vector3f array value " + Arrays.deepToString((Vector3f[]) uniform.getValue());
+                assert isValidNumber(fb) : "Invalid Vector3f array value "
+                        + Arrays.deepToString((Vector3f[]) uniform.getValue()) + " for "
+                        + uniform.getBinding();
                 gl.glUniform3(loc, fb);
                 break;
             case Vector4Array:
                 fb = uniform.getMultiData();
-                assert isValidNumber(fb) : "Invalid Vector4f array value " + Arrays.deepToString((Vector4f[]) uniform.getValue());
+                assert isValidNumber(fb) : "Invalid Vector4f array value "
+                        + Arrays.deepToString((Vector4f[]) uniform.getValue()) + " for "
+                        + uniform.getBinding();
                 gl.glUniform4(loc, fb);
                 break;
             case Matrix4Array:
                 fb = uniform.getMultiData();
-                assert isValidNumber(fb) : "Invalid Matrix4f array value " + Arrays.deepToString((Matrix4f[]) uniform.getValue());
+                assert isValidNumber(fb) : "Invalid Matrix4f array value "
+                        + Arrays.deepToString((Matrix4f[]) uniform.getValue()) + " for "
+                        + uniform.getBinding();
                 gl.glUniformMatrix4(loc, false, fb);
                 break;
             case Int:
@@ -1416,7 +1428,8 @@ public final class GLRenderer implements Renderer {
                 gl.glUniform1i(loc, i.intValue());
                 break;
             default:
-                throw new UnsupportedOperationException("Unsupported uniform type: " + uniform.getVarType());
+                throw new UnsupportedOperationException(
+                        "Unsupported uniform type: " + uniform.getVarType() + " for " + uniform.getBinding());
         }
     }
 

+ 4 - 0
jme3-core/src/main/java/com/jme3/renderer/queue/RenderQueue.java

@@ -279,7 +279,9 @@ public class RenderQueue {
     }
 
     public void renderShadowQueue(GeometryList list, RenderManager rm, Camera cam, boolean clear) {
+        rm.getRenderer().pushDebugGroup("ShadowQueue");
         renderGeometryList(list, rm, cam, clear);
+        rm.getRenderer().popDebugGroup();
     }
 
     public boolean isQueueEmpty(Bucket bucket) {
@@ -304,6 +306,7 @@ public class RenderQueue {
     }
 
     public void renderQueue(Bucket bucket, RenderManager rm, Camera cam, boolean clear) {
+        rm.getRenderer().pushDebugGroup(bucket.name());
         switch (bucket) {
             case Gui:
                 renderGeometryList(guiList, rm, cam, clear);
@@ -324,6 +327,7 @@ public class RenderQueue {
             default:
                 throw new UnsupportedOperationException("Unsupported bucket type: " + bucket);
         }
+        rm.getRenderer().popDebugGroup();
     }
 
     public void clear() {

+ 12 - 0
jme3-core/src/main/java/com/jme3/scene/Geometry.java

@@ -136,6 +136,18 @@ public class Geometry extends Spatial {
         this.mesh = mesh;
     }
 
+    /**
+     * Create a geometry node with mesh data and material.
+     *
+     * @param name The name of this geometry
+     * @param mesh The mesh data for this geometry
+     * @param material The material for this geometry
+     */
+    public Geometry(String name, Mesh mesh, Material material) {
+        this(name, mesh);
+        setMaterial(material);
+    }
+
     @Override
     public boolean checkCulling(Camera cam) {
         if (isGrouped()) {

+ 46 - 29
jme3-core/src/main/java/com/jme3/scene/Mesh.java

@@ -129,6 +129,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
          * for each patch (default is 3 for triangle tessellation)
          */
         Patch(true);
+
         private boolean listMode = false;
 
         private Mode(boolean listMode) {
@@ -148,28 +149,44 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
             return listMode;
         }
     }
+    
+    /**
+     * Default Variables
+     */
+    private static final int DEFAULT_VERTEX_ARRAY_ID = -1;
+    private static final CollisionData DEFAULT_COLLISION_TREE = null;
+
+    private static final float DEFAULT_POINT_SIZE = 1.0f;
+    private static final float DEFAULT_LINE_WIDTH = 1.0f;
 
+    private static final int DEFAULT_VERT_COUNT = -1;
+    private static final int DEFAULT_ELEMENT_COUNT = -1;
+    private static final int DEFAULT_INSTANCE_COUNT = -1;
+    private static final int DEFAULT_PATCH_VERTEX_COUNT = 3;
+    private static final int DEFAULT_MAX_NUM_WEIGHTS = -1;
+    
     /**
      * The bounding volume that contains the mesh entirely.
      * By default a BoundingBox (AABB).
      */
     private BoundingVolume meshBound = new BoundingBox();
 
-    private CollisionData collisionTree = null;
+    private CollisionData collisionTree = DEFAULT_COLLISION_TREE;
 
     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;
+    
+    private float pointSize = DEFAULT_POINT_SIZE;
+    private float lineWidth = DEFAULT_LINE_WIDTH;
 
-    private transient int vertexArrayID = -1;
+    private transient int vertexArrayID = DEFAULT_VERTEX_ARRAY_ID;
 
-    private int vertCount = -1;
-    private int elementCount = -1;
-    private int instanceCount = -1;
-    private int patchVertexCount = 3; //only used for tessellation
-    private int maxNumWeights = -1; // only if using skeletal animation
+    private int vertCount = DEFAULT_VERT_COUNT;
+    private int elementCount = DEFAULT_ELEMENT_COUNT;
+    private int instanceCount = DEFAULT_INSTANCE_COUNT;
+    private int patchVertexCount = DEFAULT_PATCH_VERTEX_COUNT; //only used for tessellation
+    private int maxNumWeights = DEFAULT_MAX_NUM_WEIGHTS; // only if using skeletal animation
 
     private int[] elementLengths;
     private int[] modeStart;
@@ -199,7 +216,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
             clone.collisionTree = collisionTree != null ? collisionTree : null;
             clone.buffers = buffers.clone();
             clone.buffersList = new SafeArrayList<>(VertexBuffer.class, buffersList);
-            clone.vertexArrayID = -1;
+            clone.vertexArrayID = DEFAULT_VERTEX_ARRAY_ID;
             if (elementLengths != null) {
                 clone.elementLengths = elementLengths.clone();
             }
@@ -226,7 +243,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
 
             // TODO: Collision tree cloning
             //clone.collisionTree = collisionTree != null ? collisionTree : null;
-            clone.collisionTree = null; // it will get re-generated in any case
+            clone.collisionTree = DEFAULT_COLLISION_TREE; // it will get re-generated in any case
 
             clone.buffers = new IntMap<>();
             clone.buffersList = new SafeArrayList<>(VertexBuffer.class);
@@ -236,7 +253,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
                 clone.buffersList.add(bufClone);
             }
 
-            clone.vertexArrayID = -1;
+            clone.vertexArrayID = DEFAULT_VERTEX_ARRAY_ID;
             clone.vertCount = vertCount;
             clone.elementCount = elementCount;
             clone.instanceCount = instanceCount;
@@ -296,7 +313,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
     public Mesh jmeClone() {
         try {
             Mesh clone = (Mesh) super.clone();
-            clone.vertexArrayID = -1;
+            clone.vertexArrayID = DEFAULT_VERTEX_ARRAY_ID;
             return clone;
         } catch (CloneNotSupportedException ex) {
             throw new AssertionError();
@@ -309,7 +326,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
     @Override
     public void cloneFields(Cloner cloner, Object original) {
         // Probably could clone this now but it will get regenerated anyway.
-        this.collisionTree = null;
+        this.collisionTree = DEFAULT_COLLISION_TREE;
 
         this.meshBound = cloner.clone(meshBound);
         this.buffersList = cloner.clone(buffersList);
@@ -616,7 +633,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
      */
     @Deprecated
     public float getPointSize() {
-        return 1.0f;
+        return DEFAULT_POINT_SIZE;
     }
 
     /**
@@ -969,7 +986,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
      * @param id the array ID
      */
     public void setId(int id) {
-        if (vertexArrayID != -1) {
+        if (vertexArrayID != DEFAULT_VERTEX_ARRAY_ID) {
             throw new IllegalStateException("ID has already been set.");
         }
 
@@ -995,7 +1012,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
      * generated BIHTree.
      */
     public void clearCollisionData() {
-        collisionTree = null;
+        collisionTree = DEFAULT_COLLISION_TREE;
     }
 
     /**
@@ -1620,15 +1637,15 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
         OutputCapsule out = ex.getCapsule(this);
 
         out.write(meshBound, "modelBound", null);
-        out.write(vertCount, "vertCount", -1);
-        out.write(elementCount, "elementCount", -1);
-        out.write(instanceCount, "instanceCount", -1);
-        out.write(maxNumWeights, "max_num_weights", -1);
+        out.write(vertCount, "vertCount", DEFAULT_VERT_COUNT);
+        out.write(elementCount, "elementCount", DEFAULT_ELEMENT_COUNT);
+        out.write(instanceCount, "instanceCount", DEFAULT_INSTANCE_COUNT);
+        out.write(maxNumWeights, "max_num_weights", DEFAULT_MAX_NUM_WEIGHTS);
         out.write(mode, "mode", Mode.Triangles);
-        out.write(collisionTree, "collisionTree", null);
+        out.write(collisionTree, "collisionTree", DEFAULT_COLLISION_TREE);
         out.write(elementLengths, "elementLengths", null);
         out.write(modeStart, "modeStart", null);
-        out.write(pointSize, "pointSize", 1f);
+        out.write(pointSize, "pointSize", DEFAULT_POINT_SIZE);
 
         //Removing HW skinning buffers to not save them
         VertexBuffer hwBoneIndex = null;
@@ -1663,17 +1680,17 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
     public void read(JmeImporter im) throws IOException {
         InputCapsule in = im.getCapsule(this);
         meshBound = (BoundingVolume) in.readSavable("modelBound", null);
-        vertCount = in.readInt("vertCount", -1);
-        elementCount = in.readInt("elementCount", -1);
-        instanceCount = in.readInt("instanceCount", -1);
-        maxNumWeights = in.readInt("max_num_weights", -1);
+        vertCount = in.readInt("vertCount", DEFAULT_VERT_COUNT);
+        elementCount = in.readInt("elementCount", DEFAULT_ELEMENT_COUNT);
+        instanceCount = in.readInt("instanceCount", DEFAULT_INSTANCE_COUNT);
+        maxNumWeights = in.readInt("max_num_weights", DEFAULT_MAX_NUM_WEIGHTS);
         mode = in.readEnum("mode", Mode.class, Mode.Triangles);
         elementLengths = in.readIntArray("elementLengths", null);
         modeStart = in.readIntArray("modeStart", null);
-        collisionTree = (BIHTree) in.readSavable("collisionTree", null);
+        collisionTree = (BIHTree) in.readSavable("collisionTree", DEFAULT_COLLISION_TREE);
         elementLengths = in.readIntArray("elementLengths", null);
         modeStart = in.readIntArray("modeStart", null);
-        pointSize = in.readFloat("pointSize", 1f);
+        pointSize = in.readFloat("pointSize", DEFAULT_POINT_SIZE);
 
 //        in.readStringSavableMap("buffers", null);
         buffers = (IntMap<VertexBuffer>) in.readIntSavableMap("buffers", null);

+ 159 - 0
jme3-core/src/main/java/com/jme3/scene/SceneGraphIterator.java

@@ -0,0 +1,159 @@
+/*
+ * Copyright (c) 2023 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+
+/**
+ * Iterates over the scene graph with the depth-first traversal method.
+ * <p>
+ * This method of scene traversal allows for more control of the iteration
+ * process than {@link Spatial#depthFirstTraversal(com.jme3.scene.SceneGraphVisitor)}
+ * because it implements {@link Iterator} (enabling it to be used in for-loops).
+ * 
+ * @author codex
+ */
+public class SceneGraphIterator implements Iterable<Spatial>, Iterator<Spatial> {
+    
+    private Spatial current;
+    private Spatial main;
+    private int depth = 0;
+    private final LinkedList<PathNode> path = new LinkedList<>();
+    
+    /**
+     * Instantiates a new {@code SceneGraphIterator} instance that
+     * starts iterating at the given main spatial.
+     * 
+     * @param main the main spatial to start iteration from
+     */
+    public SceneGraphIterator(Spatial main) {
+        if (main instanceof Node) {
+            path.add(new PathNode((Node)main));
+            depth++;
+        }
+        this.main = main;
+    }
+
+    @Override
+    public Iterator<Spatial> iterator() {
+        return this;
+    }
+    
+    @Override
+    public boolean hasNext() {
+        if (main != null) {
+            return true;
+        }
+        trim();
+        return !path.isEmpty();
+    }
+    
+    @Override
+    public Spatial next() {
+        if (main != null) {
+            current = main;
+            main = null;
+        } else {
+            current = path.getLast().iterator.next();
+            if (current instanceof Node) {
+                Node n = (Node)current;
+                if (!n.getChildren().isEmpty()) {
+                    path.addLast(new PathNode(n));
+                    depth++;
+                }
+            }
+        }
+        return current;
+    }
+    
+    /**
+     * Gets the spatial the iterator is currently on.
+     * 
+     * @return current spatial
+     */
+    public Spatial current() {
+        return current;
+    }
+    
+    /**
+     * Makes this iterator ignore all children of the current spatial.
+     * The children of the current spatial will not be iterated through.
+     */
+    public void ignoreChildren() {
+        if (current instanceof Node) {
+            path.removeLast();
+            depth--;
+        }
+    }
+    
+    /**
+     * Gets the current depth of the iterator.
+     * <p>
+     * The depth is how far away from the main spatial the
+     * current spatial is. So, the main spatial's depth is 0,
+     * all its children's depths is 1, and all <em>their</em>
+     * children's depths is 2, etc.
+     * 
+     * @return current depth, or distance from the main spatial.
+     */
+    public int getDepth() {
+        // The depth field is not an accurate indicator of depth.
+        // Whenever the current spatial is an iterable node, the depth
+        // value is exactly 1 greater than it should be.
+        return !path.isEmpty() && current == path.getLast().node ? depth-1 : depth;
+    }
+    
+    /**
+     * Trims the path to the first unexhausted node.
+     */
+    private void trim() {
+        if (!path.isEmpty() && !path.getLast().iterator.hasNext()) {
+            path.removeLast();
+            depth--;
+            trim();
+        }
+    }
+    
+    private static class PathNode {
+
+        Node node;
+        Iterator<Spatial> iterator;
+
+        PathNode(Node node) {
+            this.node = node;
+            iterator = this.node.getChildren().iterator();
+        }
+        
+    }
+    
+}

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

@@ -138,7 +138,7 @@ public final class UserData implements Savable {
     public void write(JmeExporter ex) throws IOException {
         OutputCapsule oc = ex.getCapsule(this);
         oc.write(type, "type", (byte) 0);
-
+        
         switch (type) {
             case TYPE_INTEGER:
                 int i = (Integer) value;

+ 30 - 21
jme3-core/src/main/java/com/jme3/shader/DefineList.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2015 jMonkeyEngine
+ * Copyright (c) 2009-2023 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -32,6 +32,7 @@
 package com.jme3.shader;
 
 import java.util.Arrays;
+import java.util.BitSet;
 import java.util.List;
 
 /**
@@ -41,20 +42,19 @@ import java.util.List;
  */
 public final class DefineList {
 
-    public static final int MAX_DEFINES = 64;
-
-    private long isSet;
+    private final BitSet isSet;
     private final int[] values;
 
     public DefineList(int numValues) {
-        if (numValues < 0 || numValues > MAX_DEFINES) {
-            throw new IllegalArgumentException("numValues must be between 0 and 64");
+        if (numValues < 0) {
+            throw new IllegalArgumentException("numValues must be >= 0");
         }
         values = new int[numValues];
+        isSet = new BitSet(numValues);
     }
 
     private DefineList(DefineList original) {
-        this.isSet = original.isSet;
+        this.isSet = (BitSet) original.isSet.clone();
         this.values = new int[original.values.length];
         System.arraycopy(original.values, 0, values, 0, values.length);
     }
@@ -65,18 +65,18 @@ public final class DefineList {
 
     public boolean isSet(int id) {
         rangeCheck(id);
-        return (isSet & (1L << id)) != 0;
+        return isSet.get(id);
     }
 
     public void unset(int id) {
         rangeCheck(id);
-        isSet &= ~(1L << id);
+        isSet.clear(id);
         values[id] = 0;
     }
 
     public void set(int id, int val) {
         rangeCheck(id);
-        isSet |= (1L << id);
+        isSet.set(id, true);
         values[id] = val;
     }
 
@@ -124,7 +124,7 @@ public final class DefineList {
     }
 
     public void clear() {
-        isSet = 0;
+        isSet.clear();
         Arrays.fill(values, 0);
     }
 
@@ -142,21 +142,30 @@ public final class DefineList {
 
     @Override
     public int hashCode() {
-        return (int) ((isSet >> 32) ^ isSet);
+        return isSet.hashCode();
     }
 
     @Override
-    public boolean equals(Object other) {
-        DefineList o = (DefineList) other;
-        if (isSet == o.isSet) {
-            for (int i = 0; i < values.length; i++) {
-                if (values[i] != o.values[i]) {
-                    return false;
-                }
-            }
+    public boolean equals(Object object) {
+        if (this == object) {
             return true;
         }
-        return false;
+        if (object == null || object.getClass() != getClass()) {
+            return false;
+        }
+        DefineList otherDefineList = (DefineList) object;
+        if (values.length != otherDefineList.values.length) {
+            return false;
+        }
+        if (!isSet.equals(otherDefineList.isSet)) {
+            return false;
+        }
+        for (int i = 0; i < values.length; i++) {
+            if (values[i] != otherDefineList.values[i]) {
+                return false;
+            }
+        }
+        return true;
     }
 
     public DefineList deepClone() {

+ 5 - 0
jme3-core/src/main/java/com/jme3/shadow/ShadowUtil.java

@@ -522,6 +522,11 @@ public class ShadowUtil {
         }
         casterCount = occExt.casterCount;
 
+        if (casterCount == 0) {
+            vars.release();
+            return;
+        }
+
         //Nehon 08/18/2010 this is to avoid shadow bleeding when the ground is set to only receive shadows
         if (casterCount != receiverCount) {
             casterBB.setXExtent(casterBB.getXExtent() + 2.0f);

+ 5 - 5
jme3-core/src/main/java/com/jme3/texture/Texture.java

@@ -637,11 +637,11 @@ public abstract class Texture implements CloneableSmartAsset, Savable, Cloneable
             }
         }
 
-        anisotropicFilter = capsule.readInt("anisotropicFilter", 1);
-        minificationFilter = capsule.readEnum("minificationFilter",
+        setAnisotropicFilter(capsule.readInt("anisotropicFilter", 1));
+        setMinFilter(capsule.readEnum("minificationFilter",
                 MinFilter.class,
-                MinFilter.BilinearNoMipMaps);
-        magnificationFilter = capsule.readEnum("magnificationFilter",
-                MagFilter.class, MagFilter.Bilinear);
+                MinFilter.BilinearNoMipMaps));
+        setMagFilter(capsule.readEnum("magnificationFilter",
+                MagFilter.class, MagFilter.Bilinear));
     }
 }

+ 2 - 0
jme3-core/src/main/java/com/jme3/util/SkyFactory.java

@@ -37,6 +37,7 @@ import com.jme3.bounding.BoundingSphere;
 import com.jme3.material.Material;
 import com.jme3.math.Vector3f;
 import com.jme3.renderer.queue.RenderQueue.Bucket;
+import com.jme3.renderer.queue.RenderQueue.ShadowMode;
 import com.jme3.scene.Geometry;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.shape.Sphere;
@@ -194,6 +195,7 @@ public class SkyFactory {
         sky.setQueueBucket(Bucket.Sky);
         sky.setCullHint(Spatial.CullHint.Never);
         sky.setModelBound(new BoundingSphere(Float.POSITIVE_INFINITY, Vector3f.ZERO));
+        sky.setShadowMode(ShadowMode.Off);
 
         Material skyMat;
         switch (envMapType) {

+ 1 - 0
jme3-core/src/main/resources/Common/MatDefs/Shadow/PostShadow.vert

@@ -1,6 +1,7 @@
 #import "Common/ShaderLib/GLSLCompat.glsllib"
 #import "Common/ShaderLib/Instancing.glsllib"
 #import "Common/ShaderLib/Skinning.glsllib"
+#import "Common/ShaderLib/MorphAnim.glsllib"
 
 uniform mat4 m_LightViewProjectionMatrix0;
 uniform mat4 m_LightViewProjectionMatrix1;

+ 1 - 1
jme3-core/src/main/resources/Common/MatDefs/Shadow/PreShadow.vert

@@ -12,7 +12,7 @@ void main(){
     vec4 modelSpacePos = vec4(inPosition, 1.0);
 
    #ifdef NUM_MORPH_TARGETS
-           Morph_Compute(modelSpacePos, modelSpaceNorm);
+           Morph_Compute(modelSpacePos);
    #endif
 
    #ifdef NUM_BONES

+ 5 - 5
jme3-core/src/test/java/com/jme3/renderer/OpaqueComparatorTest.java

@@ -252,8 +252,8 @@ public class OpaqueComparatorTest {
         lightingMatTCVColorLight.setBoolean("VertexLighting", true);
         lightingMatTCVColorLight.setBoolean("SeparateTexCoord", true);
         
-        testSort(lightingMat, lightingMatVColor, lightingMatVLight,
-                 lightingMatVColorLight, lightingMatTC, lightingMatTCVColorLight);
+        testSort(lightingMatVColor, lightingMat, lightingMatVColorLight,
+                lightingMatVLight, lightingMatTC, lightingMatTCVColorLight);
     }
     
     @Test
@@ -332,8 +332,8 @@ public class OpaqueComparatorTest {
         Material mat2000 = matBase2.clone();
         mat2000.setName("2000");
         
-        testSort(mat1100, mat1101, mat1102, mat1110, 
-                 mat1120, mat1121, mat1122, mat1140, 
-                 mat1200, mat1210, mat1220, mat2000);
+        testSort(mat1110, mat1100, mat1101, mat1102, 
+                 mat1140, mat1120, mat1121, mat1122, 
+                 mat1220, mat1210, mat1200, mat2000);
     }
 }

+ 57 - 0
jme3-core/src/test/java/com/jme3/scene/mesh/MeshTest.java

@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2023 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.scene.mesh;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+import com.jme3.scene.Mesh;
+
+/**
+ * Tests selected methods of the Mesh class.
+ *
+ * @author Melvyn Linke
+ */
+public class MeshTest {
+
+    /**
+     * Tests getVertexCount() on a empty Mesh.
+     */
+    @Test
+    public void testVertexCountOfEmptyMesh() {
+        final Mesh mesh = new Mesh();
+
+        assertEquals(-1, mesh.getVertexCount());
+    }
+}

+ 50 - 31
jme3-core/src/test/java/com/jme3/shader/DefineListTest.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2023 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -91,7 +91,9 @@ public class DefineListTest {
     @Test
     public void testSourceInitial() {
         DefineList dl = new DefineList(NUM_DEFINES);
-        assert dl.hashCode() == 0;
+        for (int id = 0; id < NUM_DEFINES; ++id) {
+            assert !dl.isSet(id);
+        }
         assert generateSource(dl).equals("");
     }
 
@@ -100,19 +102,29 @@ public class DefineListTest {
         DefineList dl = new DefineList(NUM_DEFINES);
 
         dl.set(BOOL_VAR, true);
-        assert dl.hashCode() == 1;
+        for (int id = 0; id < NUM_DEFINES; ++id) {
+            boolean isBoolVar = (id == BOOL_VAR);
+            assert dl.isSet(id) == isBoolVar;
+        }
         assert generateSource(dl).equals("#define BOOL_VAR 1\n");
 
         dl.set(BOOL_VAR, false);
-        assert dl.hashCode() == 0;
+        for (int id = 0; id < NUM_DEFINES; ++id) {
+            assert !dl.isSet(id);
+        }
         assert generateSource(dl).equals("");
 
         dl.set(BOOL_VAR, true);
-        assert dl.hashCode() == 1;
+        for (int id = 0; id < NUM_DEFINES; ++id) {
+            boolean isBoolVar = (id == BOOL_VAR);
+            assert dl.isSet(id) == isBoolVar;
+        }
         assert generateSource(dl).equals("#define BOOL_VAR 1\n");
 
         dl.unset(BOOL_VAR);
-        assert dl.hashCode() == 0;
+        for (int id = 0; id < NUM_DEFINES; ++id) {
+            assert !dl.isSet(id);
+        }
         assert generateSource(dl).equals("");
     }
 
@@ -120,26 +132,38 @@ public class DefineListTest {
     public void testSourceIntDefine() {
         DefineList dl = new DefineList(NUM_DEFINES);
 
-        int hashCodeWithInt = 1 << INT_VAR;
-
         dl.set(INT_VAR, 123);
-        assert dl.hashCode() == hashCodeWithInt;
+        for (int id = 0; id < NUM_DEFINES; ++id) {
+            boolean isIntVar = (id == INT_VAR);
+            assert dl.isSet(id) == isIntVar;
+        }
         assert generateSource(dl).equals("#define INT_VAR 123\n");
 
         dl.set(INT_VAR, 0);
-        assert dl.hashCode() == hashCodeWithInt;
+        for (int id = 0; id < NUM_DEFINES; ++id) {
+            boolean isIntVar = (id == INT_VAR);
+            assert dl.isSet(id) == isIntVar;
+        }
         assert generateSource(dl).equals("#define INT_VAR 0\n");
 
         dl.set(INT_VAR, -99);
-        assert dl.hashCode() == hashCodeWithInt;
+        for (int id = 0; id < NUM_DEFINES; ++id) {
+            boolean isIntVar = (id == INT_VAR);
+            assert dl.isSet(id) == isIntVar;
+        }
         assert generateSource(dl).equals("#define INT_VAR -99\n");
 
         dl.set(INT_VAR, Integer.MAX_VALUE);
-        assert dl.hashCode() == hashCodeWithInt;
+        for (int id = 0; id < NUM_DEFINES; ++id) {
+            boolean isIntVar = (id == INT_VAR);
+            assert dl.isSet(id) == isIntVar;
+        }
         assert generateSource(dl).equals("#define INT_VAR 2147483647\n");
 
         dl.unset(INT_VAR);
-        assert dl.hashCode() == 0;
+        for (int id = 0; id < NUM_DEFINES; ++id) {
+            assert !dl.isSet(id);
+        }
         assert generateSource(dl).equals("");
     }
 
@@ -148,11 +172,17 @@ public class DefineListTest {
         DefineList dl = new DefineList(NUM_DEFINES);
 
         dl.set(FLOAT_VAR, 1f);
-        assert dl.hashCode() == (1 << FLOAT_VAR);
+        for (int id = 0; id < NUM_DEFINES; ++id) {
+            boolean isFloatVar = (id == FLOAT_VAR);
+            assert dl.isSet(id) == isFloatVar;
+        }
         assert generateSource(dl).equals("#define FLOAT_VAR 1.0\n");
 
         dl.set(FLOAT_VAR, 0f);
-        assert dl.hashCode() == (1 << FLOAT_VAR);
+        for (int id = 0; id < NUM_DEFINES; ++id) {
+            boolean isFloatVar = (id == FLOAT_VAR);
+            assert dl.isSet(id) == isFloatVar;
+        }
         assert generateSource(dl).equals("#define FLOAT_VAR 0.0\n");
 
         dl.set(FLOAT_VAR, -1f);
@@ -191,49 +221,38 @@ public class DefineListTest {
         DefineList dl1 = new DefineList(NUM_DEFINES);
         DefineList dl2 = new DefineList(NUM_DEFINES);
 
-        assertEquals(0, dl1.hashCode());
-        assertEquals(0, dl2.hashCode());
+        assertEquals(dl1.hashCode(), dl2.hashCode());
         assertEquals(dl1, dl2);
 
         dl1.set(BOOL_VAR, true);
 
-        assertEquals(1, dl1.hashCode());
-        assertEquals(0, dl2.hashCode());
         assertNotEquals(dl1, dl2);
 
         dl2.set(BOOL_VAR, true);
 
-        assertEquals(1, dl1.hashCode());
-        assertEquals(1, dl2.hashCode());
+        assertEquals(dl1.hashCode(), dl2.hashCode());
         assertEquals(dl1, dl2);
 
         dl1.set(INT_VAR, 2);
 
-        assertEquals(1 | 2, dl1.hashCode());
-        assertEquals(1, dl2.hashCode());
         assertNotEquals(dl1, dl2);
 
         dl2.set(INT_VAR, 2);
 
-        assertEquals(1 | 2, dl1.hashCode());
-        assertEquals(1 | 2, dl2.hashCode());
+        assertEquals(dl1.hashCode(), dl2.hashCode());
         assertEquals(dl1, dl2);
 
         dl1.set(BOOL_VAR, false);
 
-        assertEquals(2, dl1.hashCode());
-        assertEquals(1 | 2, dl2.hashCode());
         assertNotEquals(dl1, dl2);
 
         dl2.unset(BOOL_VAR);
 
-        assertEquals(2, dl1.hashCode());
-        assertEquals(2, dl2.hashCode());
+        assertEquals(dl1.hashCode(), dl2.hashCode());
         assertEquals(dl1, dl2); // unset is the same as false
 
         dl1.unset(BOOL_VAR);
-        assertEquals(2, dl1.hashCode());
-        assertEquals(2, dl2.hashCode());
+        assertEquals(dl1.hashCode(), dl2.hashCode());
         assertEquals(dl1, dl2);
     }
 

+ 141 - 95
jme3-examples/src/main/java/jme3test/TestChooser.java

@@ -72,14 +72,13 @@ import javax.swing.event.DocumentListener;
 import javax.swing.event.ListSelectionEvent;
 import javax.swing.event.ListSelectionListener;
 
-
 /**
  * Class with a main method that displays a dialog to choose any jME demo to be
  * started.
  */
 public class TestChooser extends JFrame {
-    private static final Logger logger = Logger.getLogger(TestChooser.class
-            .getName());
+
+    private static final Logger logger = Logger.getLogger(TestChooser.class.getName());
 
     private static final long serialVersionUID = 1L;
 
@@ -105,7 +104,7 @@ public class TestChooser extends JFrame {
     @Override
     public void dispose() {
         if (executorService != null) {
-            executorService.shutdown();
+            executorService.shutdownNow();
         }
 
         super.dispose();
@@ -117,9 +116,7 @@ public class TestChooser extends JFrame {
      * @return classes vector, list of all the classes in a given package (must
      *         be found in classpath).
      */
-    private void find(String packageName, boolean recursive,
-            Set<Class<?>> classes) {
-
+    private void find(String packageName, boolean recursive, Set<Class<?>> classes) {
         // Translate the package name into an absolute path
         String name = packageName;
         if (!name.startsWith("/")) {
@@ -148,11 +145,20 @@ public class TestChooser extends JFrame {
 
         try {
             Path directory = Paths.get(uri);
-            logger.log(Level.FINE, "Searching for Demo classes in \"{0}\".", directory.getFileName().toString());
+            logger.log(
+                Level.FINE,
+                "Searching for Demo classes in \"{0}\".",
+                directory.getFileName().toString()
+            );
             addAllFilesInDirectory(directory, classes, packageName, recursive);
         } catch (Exception e) {
-            logger.logp(Level.SEVERE, this.getClass().toString(),
-                    "find(pckgname, recursive, classes)", "Exception", e);
+            logger.logp(
+                Level.SEVERE,
+                this.getClass().toString(),
+                "find(pckgname, recursive, classes)",
+                "Exception",
+                e
+            );
         } finally {
             if (fileSystem != null) {
                 try {
@@ -173,8 +179,7 @@ public class TestChooser extends JFrame {
      *         not contain a main method
      */
     private Class load(String name) {
-        String classname = name.substring(0, name.length()
-                - ".class".length());
+        String classname = name.substring(0, name.length() - ".class".length());
 
         if (classname.startsWith("/")) {
             classname = classname.substring(1);
@@ -183,14 +188,16 @@ public class TestChooser extends JFrame {
 
         try {
             final Class<?> cls = Class.forName(classname);
-            cls.getMethod("main", new Class[]{String[].class});
+            cls.getMethod("main", new Class[] { String[].class });
             if (!getClass().equals(cls)) {
                 return cls;
             }
-        } catch (NoClassDefFoundError // class has unresolved dependencies
-                | ClassNotFoundException // class not in classpath
-                | NoSuchMethodException // class does not have a main method
-                | UnsupportedClassVersionError e) { // unsupported version
+        } catch (
+            NoClassDefFoundError // class has unresolved dependencies
+            | ClassNotFoundException // class not in classpath
+            | NoSuchMethodException // class does not have a main method
+            | UnsupportedClassVersionError e
+        ) { // unsupported version
             return null;
         }
         return null;
@@ -208,12 +215,15 @@ public class TestChooser extends JFrame {
      * @param recursive
      *            true to descend into subdirectories
      */
-    private void addAllFilesInDirectory(final Path directory,
-            final Set<Class<?>> allClasses, final String packageName, final boolean recursive) {
+    private void addAllFilesInDirectory(
+        final Path directory,
+        final Set<Class<?>> allClasses,
+        final String packageName,
+        final boolean recursive
+    ) {
         // Get the list of the files contained in the package
         try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory, getFileFilter())) {
             for (Path file : stream) {
-
                 // we are only interested in .class files
                 if (Files.isDirectory(file)) {
                     if (recursive) {
@@ -243,24 +253,25 @@ public class TestChooser extends JFrame {
      */
     private static DirectoryStream.Filter<Path> getFileFilter() {
         return new DirectoryStream.Filter<Path>() {
-
             @Override
             public boolean accept(Path entry) throws IOException {
                 String fileName = entry.getFileName().toString();
-                return (fileName.endsWith(".class")
-                        && (fileName.contains("Test"))
-                        && !fileName.contains("$"))
-                        || (!fileName.startsWith(".") && Files.isDirectory(entry));
+                return (
+                    (fileName.endsWith(".class") && (fileName.contains("Test")) && !fileName.contains("$")) ||
+                    (!fileName.startsWith(".") && Files.isDirectory(entry))
+                );
             }
         };
     }
 
     private void startApp(final List<Class<?>> appClass) {
         if (appClass == null || appClass.isEmpty()) {
-            JOptionPane.showMessageDialog(rootPane,
-                                          "Please select a test from the list",
-                                          "Error",
-                                          JOptionPane.ERROR_MESSAGE);
+            JOptionPane.showMessageDialog(
+                rootPane,
+                "Please select a test from the list",
+                "Error",
+                JOptionPane.ERROR_MESSAGE
+            );
             return;
         }
 
@@ -276,7 +287,10 @@ public class TestChooser extends JFrame {
                         if (LegacyApplication.class.isAssignableFrom(clazz)) {
                             Object app = clazz.getDeclaredConstructor().newInstance();
                             if (app instanceof SimpleApplication) {
-                                final Method settingMethod = clazz.getMethod("setShowSettings", boolean.class);
+                                final Method settingMethod = clazz.getMethod(
+                                    "setShowSettings",
+                                    boolean.class
+                                );
                                 settingMethod.invoke(app, showSetting);
                             }
                             final Method mainMethod = clazz.getMethod("start");
@@ -296,7 +310,7 @@ public class TestChooser extends JFrame {
                             }
                         } else {
                             final Method mainMethod = clazz.getMethod("main", (new String[0]).getClass());
-                            mainMethod.invoke(clazz, new Object[]{new String[0]});
+                            mainMethod.invoke(clazz, new Object[] { new String[0] });
                         }
                         // wait for destroy
                         System.gc();
@@ -309,7 +323,11 @@ public class TestChooser extends JFrame {
                     } catch (InstantiationException ex) {
                         logger.log(Level.SEVERE, "Failed to create app: " + clazz.getName(), ex);
                     } catch (NoSuchMethodException ex) {
-                        logger.log(Level.SEVERE, "Test class doesn't have main method: " + clazz.getName(), ex);
+                        logger.log(
+                            Level.SEVERE,
+                            "Test class doesn't have main method: " + clazz.getName(),
+                            ex
+                        );
                     } catch (Exception ex) {
                         logger.log(Level.SEVERE, "Cannot start test: " + clazz.getName(), ex);
                         ex.printStackTrace();
@@ -344,31 +362,38 @@ public class TestChooser extends JFrame {
         mainPanel.add(createSearchPanel(list), BorderLayout.NORTH);
         mainPanel.add(new JScrollPane(list), BorderLayout.CENTER);
 
-        list.getSelectionModel().addListSelectionListener(
+        list
+            .getSelectionModel()
+            .addListSelectionListener(
                 new ListSelectionListener() {
                     @Override
                     public void valueChanged(ListSelectionEvent e) {
                         selectedClass = list.getSelectedValuesList();
                     }
-                });
-        list.addMouseListener(new MouseAdapter() {
-            @Override
-            public void mouseClicked(MouseEvent e) {
-                if (e.getClickCount() == 2 && selectedClass != null) {
-                    startApp(selectedClass);
+                }
+            );
+        list.addMouseListener(
+            new MouseAdapter() {
+                @Override
+                public void mouseClicked(MouseEvent e) {
+                    if (e.getClickCount() == 2 && selectedClass != null) {
+                        startApp(selectedClass);
+                    }
                 }
             }
-        });
-        list.addKeyListener(new KeyAdapter() {
-            @Override
-            public void keyTyped(KeyEvent e) {
-                if (e.getKeyCode() == KeyEvent.VK_ENTER) {
-                    startApp(selectedClass);
-                } else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
-                    dispose();
+        );
+        list.addKeyListener(
+            new KeyAdapter() {
+                @Override
+                public void keyTyped(KeyEvent e) {
+                    if (e.getKeyCode() == KeyEvent.VK_ENTER) {
+                        startApp(selectedClass);
+                    } else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
+                        dispose();
+                    }
                 }
             }
-        });
+        );
 
         final JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
         mainPanel.add(buttonPanel, BorderLayout.PAGE_END);
@@ -377,28 +402,33 @@ public class TestChooser extends JFrame {
         okButton.setMnemonic('O');
         buttonPanel.add(okButton);
         getRootPane().setDefaultButton(okButton);
-        okButton.addActionListener(new ActionListener() {
-            @Override
-            public void actionPerformed(ActionEvent e) {
-                startApp(selectedClass);
+        okButton.addActionListener(
+            new ActionListener() {
+                @Override
+                public void actionPerformed(ActionEvent e) {
+                    startApp(selectedClass);
+                }
             }
-        });
+        );
 
         final JButton cancelButton = new JButton("Cancel");
         cancelButton.setMnemonic('C');
         buttonPanel.add(cancelButton);
-        cancelButton.addActionListener(new ActionListener() {
-            @Override
-            public void actionPerformed(ActionEvent e) {
-                dispose();
+        cancelButton.addActionListener(
+            new ActionListener() {
+                @Override
+                public void actionPerformed(ActionEvent e) {
+                    dispose();
+                }
             }
-        });
+        );
 
         pack();
         center();
     }
 
     private class FilteredJList extends JList<Class<?>> {
+
         private static final long serialVersionUID = 1L;
 
         private String filter;
@@ -454,8 +484,10 @@ public class TestChooser extends JFrame {
         if (frameSize.width > screenSize.width) {
             frameSize.width = screenSize.width;
         }
-        this.setLocation((screenSize.width - frameSize.width) / 2,
-                (screenSize.height - frameSize.height) / 2);
+        this.setLocation(
+                (screenSize.width - frameSize.width) / 2,
+                (screenSize.height - frameSize.height) / 2
+            );
     }
 
     /**
@@ -467,18 +499,25 @@ public class TestChooser extends JFrame {
     public static void main(final String[] args) {
         try {
             UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
-        } catch (Exception e) {
-        }
+        } catch (Exception e) {}
         new TestChooser().start(args);
     }
 
     protected void start(String[] args) {
-        executorService = new ThreadPoolExecutor(1, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(), new ThreadFactory() {
-            @Override
-            public Thread newThread(Runnable r) {
-                return new Thread(r, "AppStarter");
-            }
-        });
+        executorService =
+            new ThreadPoolExecutor(
+                1,
+                Integer.MAX_VALUE,
+                60L,
+                TimeUnit.SECONDS,
+                new SynchronousQueue<>(),
+                new ThreadFactory() {
+                    @Override
+                    public Thread newThread(Runnable r) {
+                        return new Thread(r, "AppStarter");
+                    }
+                }
+            );
         final Set<Class<?>> classes = new LinkedHashSet<>();
         logger.fine("Composing Test list...");
         addDisplayedClasses(classes);
@@ -493,40 +532,47 @@ public class TestChooser extends JFrame {
     private JPanel createSearchPanel(final FilteredJList classes) {
         JPanel search = new JPanel();
         search.setLayout(new BorderLayout());
-        search.add(new JLabel("Choose a Demo to start:      Find: "),
-                BorderLayout.WEST);
+        search.add(new JLabel("Choose a Demo to start:      Find: "), BorderLayout.WEST);
         final javax.swing.JTextField jtf = new javax.swing.JTextField();
-        jtf.getDocument().addDocumentListener(new DocumentListener() {
-            @Override
-            public void removeUpdate(DocumentEvent e) {
-                classes.setFilter(jtf.getText());
-            }
+        jtf
+            .getDocument()
+            .addDocumentListener(
+                new DocumentListener() {
+                    @Override
+                    public void removeUpdate(DocumentEvent e) {
+                        classes.setFilter(jtf.getText());
+                    }
 
-            @Override
-            public void insertUpdate(DocumentEvent e) {
-                classes.setFilter(jtf.getText());
-            }
+                    @Override
+                    public void insertUpdate(DocumentEvent e) {
+                        classes.setFilter(jtf.getText());
+                    }
 
-            @Override
-            public void changedUpdate(DocumentEvent e) {
-                classes.setFilter(jtf.getText());
-            }
-        });
-        jtf.addActionListener(new ActionListener() {
-            @Override
-            public void actionPerformed(ActionEvent e) {
-                selectedClass = classes.getSelectedValuesList();
-                startApp(selectedClass);
+                    @Override
+                    public void changedUpdate(DocumentEvent e) {
+                        classes.setFilter(jtf.getText());
+                    }
+                }
+            );
+        jtf.addActionListener(
+            new ActionListener() {
+                @Override
+                public void actionPerformed(ActionEvent e) {
+                    selectedClass = classes.getSelectedValuesList();
+                    startApp(selectedClass);
+                }
             }
-        });
+        );
         final JCheckBox showSettingCheck = new JCheckBox("Show Setting");
         showSettingCheck.setSelected(true);
-        showSettingCheck.addActionListener(new ActionListener() {
-            @Override
-            public void actionPerformed(ActionEvent e) {
-                showSetting = showSettingCheck.isSelected();
+        showSettingCheck.addActionListener(
+            new ActionListener() {
+                @Override
+                public void actionPerformed(ActionEvent e) {
+                    showSetting = showSettingCheck.isSelected();
+                }
             }
-        });
+        );
         jtf.setPreferredSize(new Dimension(100, 25));
         search.add(jtf, BorderLayout.CENTER);
         search.add(showSettingCheck, BorderLayout.EAST);

+ 34 - 6
jme3-examples/src/main/java/jme3test/export/TestIssue2068.java

@@ -32,10 +32,14 @@
 package jme3test.export;
 
 import com.jme3.app.SimpleApplication;
+import com.jme3.asset.plugins.FileLocator;
 import com.jme3.export.JmeExporter;
 import com.jme3.export.xml.XMLExporter;
+import com.jme3.export.xml.XMLImporter;
+import com.jme3.scene.Spatial;
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.logging.Level;
@@ -55,7 +59,7 @@ import java.util.logging.Logger;
 public class TestIssue2068 extends SimpleApplication {
     // *************************************************************************
     // constants and loggers
-
+    
     /**
      * message logger for this class
      */
@@ -79,18 +83,42 @@ public class TestIssue2068 extends SimpleApplication {
      */
     @Override
     public void simpleInitApp() {
+        
+        ArrayList<String> list = new ArrayList<>();
+        list.add("list-value");
+        rootNode.setUserData("list", list);
+        
         Map<String, String> map = new HashMap<>();
-        map.put("key", "value");
+        map.put("map-key", "map-value");
         rootNode.setUserData("map", map);
-
-        String outputFilename = "TestIssue2068.xml";
-        File xmlFile = new File(outputFilename);
+        
+        String[] array = new String[1];
+        array[0] = "array-value";
+        rootNode.setUserData("array", array);
+        
+        // export xml
+        String filename = "TestIssue2068.xml";
+        File xmlFile = new File(filename);
         JmeExporter exporter = XMLExporter.getInstance();
         try {
             exporter.save(rootNode, xmlFile);
         } catch (IOException exception) {
-            logger.log(Level.SEVERE, exception.getMessage(), exception);
+            throw new IllegalStateException(exception);
         }
+        
+        // import binary/xml
+        assetManager.registerLocator("", FileLocator.class);
+        assetManager.registerLoader(XMLImporter.class, "xml");
+        Spatial model = assetManager.loadModel(filename);
+        //Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o");
+        model.depthFirstTraversal((Spatial spatial) -> {
+            System.out.println("UserData for "+spatial);
+            for (String key : spatial.getUserDataKeys()) {
+                System.out.println("  "+key+": "+spatial.getUserData(key));
+            }
+        });
+        
         stop();
+        
     }
 }

+ 2 - 2
jme3-examples/src/main/java/jme3test/light/TestSpotLightShadows.java

@@ -99,7 +99,7 @@ public class TestSpotLightShadows extends SimpleApplication {
 
         final SpotLightShadowRenderer slsr = new SpotLightShadowRenderer(assetManager, 512);
         slsr.setLight(spot);       
-        slsr.setShadowIntensity(0.5f);
+        slsr.setShadowIntensity(.7f);
         slsr.setShadowZExtend(100);
         slsr.setShadowZFadeLength(5);
         slsr.setEdgeFilteringMode(EdgeFilteringMode.PCFPOISSON);   
@@ -107,7 +107,7 @@ public class TestSpotLightShadows extends SimpleApplication {
 
         SpotLightShadowFilter slsf = new SpotLightShadowFilter(assetManager, 512);
         slsf.setLight(spot);    
-        slsf.setShadowIntensity(0.5f);
+        slsf.setShadowIntensity(.7f);
         slsf.setShadowZExtend(100);
         slsf.setShadowZFadeLength(5);
         slsf.setEdgeFilteringMode(EdgeFilteringMode.PCFPOISSON);  

+ 7 - 6
jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java

@@ -31,6 +31,7 @@
  */
 package jme3test.model;
 
+import com.jme3.anim.AnimClip;
 import com.jme3.anim.AnimComposer;
 import com.jme3.anim.SkinningControl;
 import com.jme3.app.*;
@@ -133,7 +134,7 @@ public class TestGltfLoading extends SimpleApplication {
         //loadModel("Models/gltf/manta/scene.gltf", Vector3f.ZERO, 0.2f);
         //loadModel("Models/gltf/bone/scene.gltf", Vector3f.ZERO, 0.1f);
 //        loadModel("Models/gltf/box/box.gltf", Vector3f.ZERO, 1);
-        loadModel("Models/gltf/duck/Duck.gltf", new Vector3f(0, -1, 0), 1);
+        loadModel("Models/gltf/duck/Duck.gltf", new Vector3f(0, 1, 0), 1);
 //        loadModel("Models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf", Vector3f.ZERO, 1);
 //        loadModel("Models/gltf/hornet/scene.gltf", new Vector3f(0, -0.5f, 0), 0.4f);
 ////        loadModel("Models/gltf/adamHead/adamHead.gltf", Vector3f.ZERO, 0.6f);
@@ -151,12 +152,9 @@ public class TestGltfLoading extends SimpleApplication {
 
 //        loadModel("Models/gltf/Corset/glTF/Corset.gltf", new Vector3f(0, -1, 0), 20f);
 //        loadModel("Models/gltf/BoxInterleaved/glTF/BoxInterleaved.gltf", new Vector3f(0, 0, 0), 1f);
-
-
+        
         probeNode.attachChild(assets.get(0));
 
-        // setMorphTarget(morphIndex);
-
         ChaseCameraAppState chaseCam = new ChaseCameraAppState();
         chaseCam.setTarget(probeNode);
         getStateManager().attach(chaseCam);
@@ -228,10 +226,13 @@ public class TestGltfLoading extends SimpleApplication {
     }
 
     private void loadModel(String path, Vector3f offset, float scale) {
+        loadModel(path, offset, new Vector3f(scale, scale, scale));
+    }
+    private void loadModel(String path, Vector3f offset, Vector3f scale) {
         GltfModelKey k = new GltfModelKey(path);
         //k.setKeepSkeletonPose(true);
         Spatial s = assetManager.loadModel(k);
-        s.scale(scale);
+        s.scale(scale.x, scale.y, scale.z);
         s.move(offset);
         assets.add(s);
         if (playAnim) {

+ 95 - 0
jme3-examples/src/main/java/jme3test/model/TestGltfNaming.java

@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2009-2023 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package jme3test.model;
+
+import com.jme3.app.*;
+import com.jme3.scene.*;
+import com.jme3.scene.Spatial.CullHint;
+import com.jme3.scene.plugins.gltf.GltfModelKey;
+
+public class TestGltfNaming extends SimpleApplication {
+    private final static String indentString = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t";
+
+    public static void main(String[] args) {
+        TestGltfNaming app = new TestGltfNaming();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        Node r1 = new Node("test1");
+        Node r2 = new Node("test2");
+        Node r3 = new Node("test3");
+
+        r1.attachChild(loadModel("jme3test/gltfnaming/single.gltf"));
+        r2.attachChild(loadModel("jme3test/gltfnaming/multi.gltf"));
+        r3.attachChild(loadModel("jme3test/gltfnaming/parent.gltf"));
+
+        System.out.println("");
+        System.out.println("");
+
+        System.out.println("Test 1: ");
+        dumpScene(r1, 0);
+
+        System.out.println("");
+        System.out.println("");
+
+        System.out.println("Test 2: ");
+        dumpScene(r2, 0);
+
+        System.out.println("");
+        System.out.println("");
+
+        System.out.println("Test 3: ");
+        dumpScene(r3, 0);
+    }
+
+    private Spatial loadModel(String path) {
+        GltfModelKey k = new GltfModelKey(path);
+        Spatial s = assetManager.loadModel(k);
+        s.setCullHint(CullHint.Always);
+        return s;
+    }
+
+    private void dumpScene(Spatial s, int indent) {
+        System.err.println(indentString.substring(0, indent) + s.getName() + " ("
+                + s.getClass().getSimpleName() + ") / " + s.getLocalTransform().getTranslation().toString()
+                + ", " + s.getLocalTransform().getRotation().toString() + ", "
+                + s.getLocalTransform().getScale().toString());
+        if (s instanceof Node) {
+            Node n = (Node) s;
+            for (Spatial spatial : n.getChildren()) {
+                dumpScene(spatial, indent + 1);
+            }
+        }
+    }
+}

+ 73 - 0
jme3-examples/src/main/java/jme3test/model/TestGltfVertexColor.java

@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2009-2023 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package jme3test.model;
+
+import com.jme3.app.*;
+import com.jme3.math.*;
+import com.jme3.renderer.Limits;
+import com.jme3.renderer.queue.RenderQueue;
+import com.jme3.scene.*;
+import com.jme3.scene.plugins.gltf.GltfModelKey;
+
+public class TestGltfVertexColor extends SimpleApplication {
+    Node probeNode;
+
+    public static void main(String[] args) {
+        TestGltfVertexColor app = new TestGltfVertexColor();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        rootNode.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
+        probeNode = (Node) assetManager.loadModel("Scenes/defaultProbe.j3o");
+        rootNode.attachChild(probeNode);
+
+        cam.setFrustumPerspective(45f, (float) cam.getWidth() / cam.getHeight(), 0.1f, 100f);
+        renderer.setDefaultAnisotropicFilter(Math.min(renderer.getLimits().get(Limits.TextureAnisotropy), 8));
+        setPauseOnLostFocus(false);
+
+        flyCam.setEnabled(false);
+        viewPort.setBackgroundColor(new ColorRGBA().setAsSrgb(0.2f, 0.2f, 0.2f, 1.0f));
+
+        loadModel("jme3test/gltfvertexcolor/VertexColorTest.glb", new Vector3f(0, -1, 0), 1);
+    }
+
+    private void loadModel(String path, Vector3f offset, float scale) {
+        GltfModelKey k = new GltfModelKey(path);
+        Spatial s = assetManager.loadModel(k);
+        s.scale(scale);
+        s.move(offset);
+        probeNode.attachChild(s);
+    }
+
+}

+ 145 - 0
jme3-examples/src/main/java/jme3test/model/anim/TestGltfMorph.java

@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2009-2023 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package jme3test.model.anim;
+
+import com.jme3.anim.AnimComposer;
+import com.jme3.app.*;
+import com.jme3.light.AmbientLight;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.math.*;
+import com.jme3.renderer.Limits;
+import com.jme3.renderer.queue.RenderQueue;
+import com.jme3.scene.*;
+import com.jme3.scene.plugins.gltf.GltfModelKey;
+import com.jme3.scene.shape.Quad;
+import com.jme3.shadow.DirectionalLightShadowRenderer;
+import com.jme3.shadow.EdgeFilteringMode;
+
+public class TestGltfMorph extends SimpleApplication {
+    private Node probeNode;
+    private float t = -1;
+    private int n = 0;
+
+    public static void main(String[] args) {
+        TestGltfMorph app = new TestGltfMorph();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        rootNode.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
+
+        probeNode = (Node) assetManager.loadModel("Scenes/defaultProbe.j3o");
+        rootNode.attachChild(probeNode);
+
+        cam.setFrustumPerspective(45f, (float) cam.getWidth() / cam.getHeight(), 0.1f, 100f);
+        renderer.setDefaultAnisotropicFilter(Math.min(renderer.getLimits().get(Limits.TextureAnisotropy), 8));
+        setPauseOnLostFocus(false);
+
+        flyCam.setMoveSpeed(5);
+        flyCam.setDragToRotate(true);
+        flyCam.setEnabled(true);
+        viewPort.setBackgroundColor(new ColorRGBA().setAsSrgb(0.2f, 0.2f, 0.2f, 1.0f));
+
+        setupFloor();
+
+        Vector3f lightDir = new Vector3f(-1, -1, .5f).normalizeLocal();
+
+        // To make shadows, sun
+        DirectionalLight dl = new DirectionalLight();
+        dl.setDirection(lightDir);
+        dl.setColor(ColorRGBA.White);
+        rootNode.addLight(dl);
+
+        // Add ambient light
+        AmbientLight al = new AmbientLight();
+        al.setColor(ColorRGBA.White.multLocal(0.4f));
+        rootNode.addLight(al);
+
+        final int SHADOWMAP_SIZE = 1024;
+        DirectionalLightShadowRenderer dlsr = new DirectionalLightShadowRenderer(getAssetManager(), SHADOWMAP_SIZE, 3);
+        dlsr.setLight(dl);
+        dlsr.setLambda(0.55f);
+        dlsr.setShadowIntensity(0.6f);
+        dlsr.setEdgeFilteringMode(EdgeFilteringMode.PCF8);
+        getViewPort().addProcessor(dlsr);
+
+        loadModel("jme3test/morph/MorphStressTest.glb", new Vector3f(0, -1, 0), 1);
+    }
+
+    private void setupFloor() {
+        Material floorMaterial = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        floorMaterial.setColor("Diffuse", new ColorRGBA(.9f, .9f, .9f, .9f));
+
+        Node floorGeom = new Node("floorGeom");
+        Quad q = new Quad(20, 20);
+        Geometry g = new Geometry("geom", q);
+        g.setLocalRotation(new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X));
+        g.setShadowMode(RenderQueue.ShadowMode.Receive);
+        floorGeom.attachChild(g);
+
+        floorGeom.setMaterial(floorMaterial);
+
+        floorGeom.move(-10f, -2f, 10f);
+
+        rootNode.attachChild(floorGeom);
+    }
+
+    private void loadModel(String path, Vector3f offset, float scale) {
+        GltfModelKey k = new GltfModelKey(path);
+        Spatial s = assetManager.loadModel(k);
+        s.scale(scale);
+        s.move(offset);
+        probeNode.attachChild(s);
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        super.simpleUpdate(tpf);
+        if (t == -1 || t > 5) {
+            rootNode.depthFirstTraversal(sx -> {
+                AnimComposer composer = sx.getControl(AnimComposer.class);
+                if (composer != null) {
+                    String anims[] = composer.getAnimClipsNames().toArray(new String[0]);
+                    String anim = anims[n++ % anims.length];
+                    System.out.println("Play " + anim);
+                    composer.setCurrentAction(anim);
+                }
+            });
+            t = 0;
+        } else {
+            t += tpf;
+        }
+    }
+
+}

+ 127 - 0
jme3-examples/src/main/java/jme3test/scene/TestSceneIteration.java

@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) 2023 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package jme3test.scene;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.SceneGraphIterator;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.shape.Box;
+
+/**
+ * Test suite for {@link SceneGraphIterator}.
+ * <p>
+ * The test succeeds if the rootNode and all its children,
+ * except all spatials named "XXX", are printed on the console
+ * with indents precisely indicating each spatial's distance
+ * from the rootNode.
+ * <p>
+ * The test fails if
+ * <ul>
+ *   <li>Not all expected children are printed on the console.
+ *   <li>An XXX is printed on the console (indicating faulty {@code ignoreChildren}).
+ *   <li>Indents do not accurately indicate distance from the rootNode.
+ * </ul>
+ * 
+ * @author codex
+ */
+public class TestSceneIteration extends SimpleApplication {
+    
+    /**
+     * Launches the test application.
+     * 
+     * @param args no argument required
+     */
+    public static void main(String[] args) {
+        new TestSceneIteration().start();
+    }
+    
+    @Override
+    public void simpleInitApp() {
+        
+        // setup scene graph
+        Node n1 = new Node("town");
+        rootNode.attachChild(n1);
+            n1.attachChild(new Node("car"));
+            n1.attachChild(new Node("tree"));
+            Node n2 = new Node("house");
+            n1.attachChild(n2);
+                n2.attachChild(new Node("chairs"));
+                n2.attachChild(new Node("tables"));
+                n2.attachChild(createGeometry("house-geometry"));
+        Node n3 = new Node("sky");
+        rootNode.attachChild(n3);
+            n3.attachChild(new Node("airplane"));
+            Node ignore = new Node("cloud");
+            n3.attachChild(ignore);
+                ignore.attachChild(new Node("XXX"));
+                ignore.attachChild(new Node("XXX"));
+                ignore.attachChild(new Node("XXX"));
+            n3.attachChild(new Node("bird"));
+        
+        // iterate
+        SceneGraphIterator iterator = new SceneGraphIterator(rootNode);
+        for (Spatial spatial : iterator) {
+            // create a hierarchy in the console
+            System.out.println(constructTabs(iterator.getDepth()) + spatial.getName());
+            // see if the children of this spatial should be ignored
+            if (spatial == ignore) {
+                // ignore all children of this spatial
+                iterator.ignoreChildren();
+            }
+        }
+        
+        // exit the application
+        stop();
+        
+    }
+    
+    private Geometry createGeometry(String name) {
+        Geometry g = new Geometry(name, new Box(1, 1, 1));
+        Material m = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        m.setColor("Color", ColorRGBA.Blue);
+        g.setMaterial(m);
+        return g;
+    }
+    
+    private String constructTabs(int n) {
+        StringBuilder render = new StringBuilder();
+        for (; n > 0; n--) {
+            render.append(" | ");
+        }
+        return render.toString();
+    }
+    
+}

BIN
jme3-examples/src/main/resources/jme3test/gltfnaming/multi.bin


+ 202 - 0
jme3-examples/src/main/resources/jme3test/gltfnaming/multi.gltf

@@ -0,0 +1,202 @@
+{
+	"asset":{
+		"generator":"Khronos glTF Blender I/O v3.6.27",
+		"version":"2.0"
+	},
+	"scene":0,
+	"scenes":[
+		{
+			"name":"Scene",
+			"nodes":[
+				0
+			]
+		}
+	],
+	"nodes":[
+		{
+			"mesh":0,
+			"name":"Cube"
+		}
+	],
+	"materials":[
+		{
+			"doubleSided":true,
+			"name":"Material",
+			"pbrMetallicRoughness":{
+				"baseColorFactor":[
+					0.800000011920929,
+					0.800000011920929,
+					0.800000011920929,
+					1
+				],
+				"metallicFactor":0,
+				"roughnessFactor":0.5
+			}
+		},
+		{
+			"doubleSided":true,
+			"name":"Material.001",
+			"pbrMetallicRoughness":{
+				"baseColorFactor":[
+					0.8000000715255737,
+					0.005118918139487505,
+					0.12912100553512573,
+					1
+				],
+				"metallicFactor":0,
+				"roughnessFactor":0.5
+			}
+		}
+	],
+	"meshes":[
+		{
+			"name":"CubeMesh",
+			"primitives":[
+				{
+					"attributes":{
+						"POSITION":0,
+						"NORMAL":1,
+						"TEXCOORD_0":2
+					},
+					"indices":3,
+					"material":0
+				},
+				{
+					"attributes":{
+						"POSITION":4,
+						"NORMAL":5,
+						"TEXCOORD_0":6
+					},
+					"indices":7,
+					"material":1
+				}
+			]
+		}
+	],
+	"accessors":[
+		{
+			"bufferView":0,
+			"componentType":5126,
+			"count":20,
+			"max":[
+				1,
+				1,
+				1
+			],
+			"min":[
+				-1,
+				-1,
+				0
+			],
+			"type":"VEC3"
+		},
+		{
+			"bufferView":1,
+			"componentType":5126,
+			"count":20,
+			"type":"VEC3"
+		},
+		{
+			"bufferView":2,
+			"componentType":5126,
+			"count":20,
+			"type":"VEC2"
+		},
+		{
+			"bufferView":3,
+			"componentType":5123,
+			"count":30,
+			"type":"SCALAR"
+		},
+		{
+			"bufferView":4,
+			"componentType":5126,
+			"count":20,
+			"max":[
+				1,
+				1,
+				0
+			],
+			"min":[
+				-1,
+				-1,
+				-1
+			],
+			"type":"VEC3"
+		},
+		{
+			"bufferView":5,
+			"componentType":5126,
+			"count":20,
+			"type":"VEC3"
+		},
+		{
+			"bufferView":6,
+			"componentType":5126,
+			"count":20,
+			"type":"VEC2"
+		},
+		{
+			"bufferView":7,
+			"componentType":5123,
+			"count":30,
+			"type":"SCALAR"
+		}
+	],
+	"bufferViews":[
+		{
+			"buffer":0,
+			"byteLength":240,
+			"byteOffset":0,
+			"target":34962
+		},
+		{
+			"buffer":0,
+			"byteLength":240,
+			"byteOffset":240,
+			"target":34962
+		},
+		{
+			"buffer":0,
+			"byteLength":160,
+			"byteOffset":480,
+			"target":34962
+		},
+		{
+			"buffer":0,
+			"byteLength":60,
+			"byteOffset":640,
+			"target":34963
+		},
+		{
+			"buffer":0,
+			"byteLength":240,
+			"byteOffset":700,
+			"target":34962
+		},
+		{
+			"buffer":0,
+			"byteLength":240,
+			"byteOffset":940,
+			"target":34962
+		},
+		{
+			"buffer":0,
+			"byteLength":160,
+			"byteOffset":1180,
+			"target":34962
+		},
+		{
+			"buffer":0,
+			"byteLength":60,
+			"byteOffset":1340,
+			"target":34963
+		}
+	],
+	"buffers":[
+		{
+			"byteLength":1400,
+			"uri":"multi.bin"
+		}
+	]
+}

BIN
jme3-examples/src/main/resources/jme3test/gltfnaming/parent.bin


+ 193 - 0
jme3-examples/src/main/resources/jme3test/gltfnaming/parent.gltf

@@ -0,0 +1,193 @@
+{
+	"asset":{
+		"generator":"Khronos glTF Blender I/O v3.6.27",
+		"version":"2.0"
+	},
+	"scene":0,
+	"scenes":[
+		{
+			"name":"Scene",
+			"nodes":[
+				1
+			]
+		}
+	],
+	"nodes":[
+		{
+			"mesh":0,
+			"name":"Cube2",
+			"translation":[
+				1.2840238809585571,
+				-2.180809736251831,
+				-0.21893274784088135
+			]
+		},
+		{
+			"children":[
+				0
+			],
+			"mesh":1,
+			"name":"CubeParent"
+		}
+	],
+	"materials":[
+		{
+			"doubleSided":true,
+			"name":"Material",
+			"pbrMetallicRoughness":{
+				"baseColorFactor":[
+					0.800000011920929,
+					0.800000011920929,
+					0.800000011920929,
+					1
+				],
+				"metallicFactor":0,
+				"roughnessFactor":0.5
+			}
+		}
+	],
+	"meshes":[
+		{
+			"name":"CubeMesh2",
+			"primitives":[
+				{
+					"attributes":{
+						"POSITION":0,
+						"NORMAL":1,
+						"TEXCOORD_0":2
+					},
+					"indices":3,
+					"material":0
+				}
+			]
+		},
+		{
+			"name":"CubeMesh",
+			"primitives":[
+				{
+					"attributes":{
+						"POSITION":4,
+						"NORMAL":5,
+						"TEXCOORD_0":6
+					},
+					"indices":3,
+					"material":0
+				}
+			]
+		}
+	],
+	"accessors":[
+		{
+			"bufferView":0,
+			"componentType":5126,
+			"count":24,
+			"max":[
+				1,
+				1,
+				1
+			],
+			"min":[
+				-1,
+				-1,
+				-1
+			],
+			"type":"VEC3"
+		},
+		{
+			"bufferView":1,
+			"componentType":5126,
+			"count":24,
+			"type":"VEC3"
+		},
+		{
+			"bufferView":2,
+			"componentType":5126,
+			"count":24,
+			"type":"VEC2"
+		},
+		{
+			"bufferView":3,
+			"componentType":5123,
+			"count":36,
+			"type":"SCALAR"
+		},
+		{
+			"bufferView":4,
+			"componentType":5126,
+			"count":24,
+			"max":[
+				1,
+				1,
+				1
+			],
+			"min":[
+				-1,
+				-1,
+				-1
+			],
+			"type":"VEC3"
+		},
+		{
+			"bufferView":5,
+			"componentType":5126,
+			"count":24,
+			"type":"VEC3"
+		},
+		{
+			"bufferView":6,
+			"componentType":5126,
+			"count":24,
+			"type":"VEC2"
+		}
+	],
+	"bufferViews":[
+		{
+			"buffer":0,
+			"byteLength":288,
+			"byteOffset":0,
+			"target":34962
+		},
+		{
+			"buffer":0,
+			"byteLength":288,
+			"byteOffset":288,
+			"target":34962
+		},
+		{
+			"buffer":0,
+			"byteLength":192,
+			"byteOffset":576,
+			"target":34962
+		},
+		{
+			"buffer":0,
+			"byteLength":72,
+			"byteOffset":768,
+			"target":34963
+		},
+		{
+			"buffer":0,
+			"byteLength":288,
+			"byteOffset":840,
+			"target":34962
+		},
+		{
+			"buffer":0,
+			"byteLength":288,
+			"byteOffset":1128,
+			"target":34962
+		},
+		{
+			"buffer":0,
+			"byteLength":192,
+			"byteOffset":1416,
+			"target":34962
+		}
+	],
+	"buffers":[
+		{
+			"byteLength":1608,
+			"uri":"parent.bin"
+		}
+	]
+}

BIN
jme3-examples/src/main/resources/jme3test/gltfnaming/single.bin


+ 121 - 0
jme3-examples/src/main/resources/jme3test/gltfnaming/single.gltf

@@ -0,0 +1,121 @@
+{
+	"asset":{
+		"generator":"Khronos glTF Blender I/O v3.6.27",
+		"version":"2.0"
+	},
+	"scene":0,
+	"scenes":[
+		{
+			"name":"Scene",
+			"nodes":[
+				0
+			]
+		}
+	],
+	"nodes":[
+		{
+			"mesh":0,
+			"name":"Cube"
+		}
+	],
+	"materials":[
+		{
+			"doubleSided":true,
+			"name":"Material",
+			"pbrMetallicRoughness":{
+				"baseColorFactor":[
+					0.800000011920929,
+					0.800000011920929,
+					0.800000011920929,
+					1
+				],
+				"metallicFactor":0,
+				"roughnessFactor":0.5
+			}
+		}
+	],
+	"meshes":[
+		{
+			"name":"CubeMesh",
+			"primitives":[
+				{
+					"attributes":{
+						"POSITION":0,
+						"NORMAL":1,
+						"TEXCOORD_0":2
+					},
+					"indices":3,
+					"material":0
+				}
+			]
+		}
+	],
+	"accessors":[
+		{
+			"bufferView":0,
+			"componentType":5126,
+			"count":24,
+			"max":[
+				1,
+				1,
+				1
+			],
+			"min":[
+				-1,
+				-1,
+				-1
+			],
+			"type":"VEC3"
+		},
+		{
+			"bufferView":1,
+			"componentType":5126,
+			"count":24,
+			"type":"VEC3"
+		},
+		{
+			"bufferView":2,
+			"componentType":5126,
+			"count":24,
+			"type":"VEC2"
+		},
+		{
+			"bufferView":3,
+			"componentType":5123,
+			"count":36,
+			"type":"SCALAR"
+		}
+	],
+	"bufferViews":[
+		{
+			"buffer":0,
+			"byteLength":288,
+			"byteOffset":0,
+			"target":34962
+		},
+		{
+			"buffer":0,
+			"byteLength":288,
+			"byteOffset":288,
+			"target":34962
+		},
+		{
+			"buffer":0,
+			"byteLength":192,
+			"byteOffset":576,
+			"target":34962
+		},
+		{
+			"buffer":0,
+			"byteLength":72,
+			"byteOffset":768,
+			"target":34963
+		}
+	],
+	"buffers":[
+		{
+			"byteLength":840,
+			"uri":"single.bin"
+		}
+	]
+}

BIN
jme3-examples/src/main/resources/jme3test/gltfnaming/untitled.bin


+ 121 - 0
jme3-examples/src/main/resources/jme3test/gltfnaming/untitled.gltf

@@ -0,0 +1,121 @@
+{
+	"asset":{
+		"generator":"Khronos glTF Blender I/O v3.6.27",
+		"version":"2.0"
+	},
+	"scene":0,
+	"scenes":[
+		{
+			"name":"Scene",
+			"nodes":[
+				0
+			]
+		}
+	],
+	"nodes":[
+		{
+			"mesh":0,
+			"name":"Cube"
+		}
+	],
+	"materials":[
+		{
+			"doubleSided":true,
+			"name":"Material",
+			"pbrMetallicRoughness":{
+				"baseColorFactor":[
+					0.800000011920929,
+					0.800000011920929,
+					0.800000011920929,
+					1
+				],
+				"metallicFactor":0,
+				"roughnessFactor":0.5
+			}
+		}
+	],
+	"meshes":[
+		{
+			"name":"CubeMesh",
+			"primitives":[
+				{
+					"attributes":{
+						"POSITION":0,
+						"NORMAL":1,
+						"TEXCOORD_0":2
+					},
+					"indices":3,
+					"material":0
+				}
+			]
+		}
+	],
+	"accessors":[
+		{
+			"bufferView":0,
+			"componentType":5126,
+			"count":24,
+			"max":[
+				1,
+				1,
+				1
+			],
+			"min":[
+				-1,
+				-1,
+				-1
+			],
+			"type":"VEC3"
+		},
+		{
+			"bufferView":1,
+			"componentType":5126,
+			"count":24,
+			"type":"VEC3"
+		},
+		{
+			"bufferView":2,
+			"componentType":5126,
+			"count":24,
+			"type":"VEC2"
+		},
+		{
+			"bufferView":3,
+			"componentType":5123,
+			"count":36,
+			"type":"SCALAR"
+		}
+	],
+	"bufferViews":[
+		{
+			"buffer":0,
+			"byteLength":288,
+			"byteOffset":0,
+			"target":34962
+		},
+		{
+			"buffer":0,
+			"byteLength":288,
+			"byteOffset":288,
+			"target":34962
+		},
+		{
+			"buffer":0,
+			"byteLength":192,
+			"byteOffset":576,
+			"target":34962
+		},
+		{
+			"buffer":0,
+			"byteLength":72,
+			"byteOffset":768,
+			"target":34963
+		}
+	],
+	"buffers":[
+		{
+			"byteLength":840,
+			"uri":"untitled.bin"
+		}
+	]
+}

BIN
jme3-examples/src/main/resources/jme3test/gltfvertexcolor/VertexColorTest.glb


BIN
jme3-examples/src/main/resources/jme3test/morph/MorphStressTest.glb


+ 4 - 1
jme3-lwjgl/src/main/java/com/jme3/system/lwjgl/LwjglAbstractDisplay.java

@@ -132,7 +132,10 @@ public abstract class LwjglAbstractDisplay extends LwjglContext implements Runna
             }
 
             listener.handleError("Failed to create display", ex);
-            createdLock.notifyAll(); // Release the lock, so start(true) doesn't deadlock.
+            synchronized (createdLock) {
+                createdLock.notifyAll(); // Release the lock, so start(true) doesn't deadlock.
+            }
+
             return false; // if we failed to create display, do not continue
         }
 

+ 31 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/AnimationList.java

@@ -33,6 +33,7 @@ package com.jme3.scene.plugins.fbx;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * Defines animations set that will be created while loading FBX scene
@@ -72,11 +73,41 @@ public class AnimationList {
         cue.lastFrame = lastFrame;
         list.add(cue);
     }
+    
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final AnimationList other = (AnimationList)obj;
+        return Objects.equals(this.list, other.list);
+    }
+    
+    @Override
+    public int hashCode() {
+        return 119 + Objects.hashCode(this.list);
+    }    
 
     static class AnimInverval {
         String name;
         String layerName;
         int firstFrame;
         int lastFrame;
+        // hashCode generator, for good measure
+        @Override
+        public int hashCode() {
+            int hash = 7;
+            hash = 29 * hash + Objects.hashCode(this.name);
+            hash = 29 * hash + Objects.hashCode(this.layerName);
+            hash = 29 * hash + this.firstFrame;
+            hash = 29 * hash + this.lastFrame;
+            return hash;
+        }
     }
 }

+ 24 - 0
jme3-plugins/src/fbx/java/com/jme3/scene/plugins/fbx/SceneKey.java

@@ -32,6 +32,7 @@
 package com.jme3.scene.plugins.fbx;
 
 import com.jme3.asset.ModelKey;
+import java.util.Objects;
 
 public class SceneKey extends ModelKey {
 
@@ -50,5 +51,28 @@ public class SceneKey extends ModelKey {
     public AnimationList getAnimations() {
         return this.animList;
     }
+    
+    @Override
+    public boolean equals(Object object) {
+        if (object == null) {
+            return false;
+        }
+        if (getClass() != object.getClass()) {
+            return false;
+        }
+        final SceneKey other = (SceneKey)object;
+        if (!super.equals(other)) {
+            return false;
+        }
+        if (!Objects.equals(animList, other.animList)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return 413 + Objects.hashCode(animList);
+    }
 
 }

+ 1 - 0
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/CustomContentManager.java

@@ -58,6 +58,7 @@ public class CustomContentManager {
         defaultExtensionLoaders.put("KHR_lights_punctual", new LightsPunctualExtensionLoader());
         defaultExtensionLoaders.put("KHR_materials_unlit", new UnlitExtensionLoader());
         defaultExtensionLoaders.put("KHR_texture_transform", new TextureTransformExtensionLoader());
+        defaultExtensionLoaders.put("KHR_materials_emissive_strength", new PBREmissiveStrengthExtensionLoader());
     }
 
     void init(GltfLoader gltfLoader) {

+ 185 - 160
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java

@@ -165,7 +165,7 @@ public class GltfLoader implements AssetLoader {
 
             setupControls();
 
-            // only one scene let's not return the root.
+            // only one scene, let's not return the root.
             if (rootNode.getChildren().size() == 1) {
                 Node child = (Node) rootNode.getChild(0);
                 // Migrate lights that were in the parent to the child.
@@ -196,7 +196,7 @@ public class GltfLoader implements AssetLoader {
 
     public void readScenes(JsonPrimitive defaultScene, Node rootNode) throws IOException {
         if (scenes == null) {
-            // no scene... lets handle this later...
+            // no scene... let's handle this later...
             throw new AssetLoadException("Gltf files with no scene is not yet supported");
         }
 
@@ -231,7 +231,7 @@ public class GltfLoader implements AssetLoader {
         Object obj = fetchFromCache("nodes", nodeIndex, Object.class);
         if (obj != null) {
             if (obj instanceof JointWrapper) {
-                // the node can be a previously loaded bone let's return it
+                // the node can be a previously loaded bone, let's return it
                 return obj;
             } else {
                 // If a spatial is referenced several times, it may be attached to different parents,
@@ -250,18 +250,16 @@ public class GltfLoader implements AssetLoader {
             // can split meshes in primitives (some kind of sub meshes),
             // We don't have this in JME, so we have to make one Mesh and one Geometry for each primitive.
             Geometry[] primitives = readMeshPrimitives(meshIndex);
-            if (primitives.length == 1 && children == null) {
-                // only one geometry, let's not wrap it in another node unless the node has children.
-                spatial = primitives[0];
-            } else {
-                // several geometries, let's make a parent Node and attach them to it
-                Node node = new Node();
-                for (Geometry primitive : primitives) {
-                    node.attachChild(primitive);
-                }
-                spatial = node;
+
+            Node node = new Node();
+            for (Geometry primitive : primitives) {
+                node.attachChild(primitive);
             }
-            spatial.setName(readMeshName(meshIndex));
+
+            node.setName(readMeshName(meshIndex));
+            
+            spatial = new Node();
+            ((Node)spatial).attachChild(node);
 
         } else {
             // no mesh, we have a node. Can be a camera node or a regular node.
@@ -332,12 +330,12 @@ public class GltfLoader implements AssetLoader {
             for (int i = 0; i < tmpArray.length; i++) {
                 tmpArray[i] = matrix.get(i).getAsFloat();
             }
-            // creates a row major matrix from color major data
+            // creates a row-major matrix from column-major data
             Matrix4f mat = new Matrix4f(tmpArray);
             transform.fromTransformMatrix(mat);
             return transform;
         }
-        // no matrix transforms: no transforms or transforms givens as translation/rotation/scale
+        // no matrix transforms: no transforms or transforms given as translation/rotation/scale
         JsonArray translation = nodeData.getAsJsonArray("translation");
         if (translation != null) {
             transform.setTranslation(
@@ -366,138 +364,145 @@ public class GltfLoader implements AssetLoader {
 
     public Geometry[] readMeshPrimitives(int meshIndex) throws IOException {
         Geometry[] geomArray = (Geometry[]) fetchFromCache("meshes", meshIndex, Object.class);
-        if (geomArray != null) {
-            // cloning the geoms.
-            Geometry[] geoms = new Geometry[geomArray.length];
-            for (int i = 0; i < geoms.length; i++) {
-                geoms[i] = geomArray[i].clone(false);
-            }
-            return geoms;
-        }
-        JsonObject meshData = meshes.get(meshIndex).getAsJsonObject();
-        JsonArray primitives = meshData.getAsJsonArray("primitives");
-        assertNotNull(primitives, "Can't find any primitives in mesh " + meshIndex);
-        String name = getAsString(meshData, "name");
-
-        geomArray = new Geometry[primitives.size()];
-        int index = 0;
-        for (JsonElement primitive : primitives) {
-            JsonObject meshObject = primitive.getAsJsonObject();
-            Mesh mesh = new Mesh();
-            addToCache("mesh", 0, mesh, 1);
-            Integer mode = getAsInteger(meshObject, "mode");
-            mesh.setMode(getMeshMode(mode));
-            Integer indices = getAsInteger(meshObject, "indices");
-            if (indices != null) {
-                mesh.setBuffer(readAccessorData(indices, new VertexBufferPopulator(VertexBuffer.Type.Index)));
-            }
-            JsonObject attributes = meshObject.getAsJsonObject("attributes");
-            assertNotNull(attributes, "No attributes defined for mesh " + mesh);
-
-            skinBuffers.clear();
-
-            for (Map.Entry<String, JsonElement> entry : attributes.entrySet()) {
-                // special case for joints and weights buffer.
-                // If there are more than 4 bones per vertex, there might be several of them
-                // we need to read them all and to keep only the 4 that have the most weight on the vertex.
-                String bufferType = entry.getKey();
-                if (bufferType.startsWith("JOINTS")) {
-                    SkinBuffers buffs = getSkinBuffers(bufferType);
-                    SkinBuffers buffer
-                            = readAccessorData(entry.getValue().getAsInt(), new JointArrayPopulator());
-                    buffs.joints = buffer.joints;
-                    buffs.componentSize = buffer.componentSize;
-                } else if (bufferType.startsWith("WEIGHTS")) {
-                    SkinBuffers buffs = getSkinBuffers(bufferType);
-                    buffs.weights = readAccessorData(entry.getValue().getAsInt(), new FloatArrayPopulator());
-                } else {
-                    VertexBuffer vb = readAccessorData(entry.getValue().getAsInt(),
-                            new VertexBufferPopulator(getVertexBufferType(bufferType)));
-                    if (vb != null) {
-                        mesh.setBuffer(vb);
+        if (geomArray == null) {                
+            JsonObject meshData = meshes.get(meshIndex).getAsJsonObject();
+            JsonArray primitives = meshData.getAsJsonArray("primitives");
+            assertNotNull(primitives, "Can't find any primitives in mesh " + meshIndex);
+            String name = getAsString(meshData, "name");
+
+            geomArray = new Geometry[primitives.size()];
+            int index = 0;
+            for (JsonElement primitive : primitives) {
+                JsonObject meshObject = primitive.getAsJsonObject();
+                Mesh mesh = new Mesh();
+                addToCache("mesh", 0, mesh, 1);
+                Integer mode = getAsInteger(meshObject, "mode");
+                mesh.setMode(getMeshMode(mode));
+                Integer indices = getAsInteger(meshObject, "indices");
+                if (indices != null) {
+                    mesh.setBuffer(readAccessorData(indices, new VertexBufferPopulator(VertexBuffer.Type.Index)));
+                }
+                JsonObject attributes = meshObject.getAsJsonObject("attributes");
+                assertNotNull(attributes, "No attributes defined for mesh " + mesh);
+
+                boolean useVertexColors = false;
+
+                skinBuffers.clear();
+
+                for (Map.Entry<String, JsonElement> entry : attributes.entrySet()) {
+                    // special case for joints and weights buffer.
+                    // If there are more than 4 bones per vertex, there might be several of them
+                    // we need to read them all and to keep only the 4 that have the most weight on the vertex.
+                    String bufferType = entry.getKey();
+                    if (bufferType.startsWith("JOINTS")) {
+                        SkinBuffers buffs = getSkinBuffers(bufferType);
+                        SkinBuffers buffer
+                                = readAccessorData(entry.getValue().getAsInt(), new JointArrayPopulator());
+                        buffs.joints = buffer.joints;
+                        buffs.componentSize = buffer.componentSize;
+                    } else if (bufferType.startsWith("WEIGHTS")) {
+                        SkinBuffers buffs = getSkinBuffers(bufferType);
+                        buffs.weights = readAccessorData(entry.getValue().getAsInt(), new FloatArrayPopulator());
+                    } else {
+                        VertexBuffer vb = readAccessorData(entry.getValue().getAsInt(),
+                                new VertexBufferPopulator(getVertexBufferType(bufferType)));
+                        if (vb != null) {
+                            mesh.setBuffer(vb);
+                        }
+                    }
+                    // if the color buffer is used, we will need to enable vertex colors on the material
+                    if (bufferType.startsWith("COLOR")) {
+                        useVertexColors = true;
                     }
                 }
-            }
-            handleSkinningBuffers(mesh, skinBuffers);
-
-            if (mesh.getBuffer(VertexBuffer.Type.BoneIndex) != null) {
-                // the mesh has some skinning let's create needed buffers for HW skinning
-                // creating empty buffers for HW skinning
-                // the buffers will be setup if ever used.
-                VertexBuffer weightsHW = new VertexBuffer(VertexBuffer.Type.HWBoneWeight);
-                VertexBuffer indicesHW = new VertexBuffer(VertexBuffer.Type.HWBoneIndex);
-                // setting usage to cpuOnly so that the buffer is not sent empty to the GPU
-                indicesHW.setUsage(VertexBuffer.Usage.CpuOnly);
-                weightsHW.setUsage(VertexBuffer.Usage.CpuOnly);
-                mesh.setBuffer(weightsHW);
-                mesh.setBuffer(indicesHW);
-                mesh.generateBindPose();
-            }
-
-            // Read morph target names
-            LinkedList<String> targetNames = new LinkedList<>();
-            if (meshData.has("extras") && meshData.getAsJsonObject("extras").has("targetNames")) {
-                JsonArray targetNamesJson = meshData.getAsJsonObject("extras").getAsJsonArray("targetNames");
-                for (JsonElement target : targetNamesJson) {
-                    targetNames.add(target.getAsString());
+                handleSkinningBuffers(mesh, skinBuffers);
+
+                if (mesh.getBuffer(VertexBuffer.Type.BoneIndex) != null) {
+                    // the mesh has some skinning, let's create needed buffers for HW skinning
+                    // creating empty buffers for HW skinning
+                    // the buffers will be set up if ever used.
+                    VertexBuffer weightsHW = new VertexBuffer(VertexBuffer.Type.HWBoneWeight);
+                    VertexBuffer indicesHW = new VertexBuffer(VertexBuffer.Type.HWBoneIndex);
+                    // setting usage to cpuOnly so that the buffer is not sent empty to the GPU
+                    indicesHW.setUsage(VertexBuffer.Usage.CpuOnly);
+                    weightsHW.setUsage(VertexBuffer.Usage.CpuOnly);
+                    mesh.setBuffer(weightsHW);
+                    mesh.setBuffer(indicesHW);
+                    mesh.generateBindPose();
                 }
-            }
 
-            // Read morph targets
-            JsonArray targets = meshObject.getAsJsonArray("targets");
-            if (targets != null) {
-                for (JsonElement target : targets) {
-                    MorphTarget morphTarget = new MorphTarget();
-                    if (targetNames.size() > 0) {
-                        morphTarget.setName(targetNames.pop());
+                // Read morph target names
+                LinkedList<String> targetNames = new LinkedList<>();
+                if (meshData.has("extras") && meshData.getAsJsonObject("extras").has("targetNames")) {
+                    JsonArray targetNamesJson = meshData.getAsJsonObject("extras").getAsJsonArray("targetNames");
+                    for (JsonElement target : targetNamesJson) {
+                        targetNames.add(target.getAsString());
                     }
-                    for (Map.Entry<String, JsonElement> entry : target.getAsJsonObject().entrySet()) {
-                        String bufferType = entry.getKey();
-                        VertexBuffer.Type type = getVertexBufferType(bufferType);
-                        VertexBuffer vb = readAccessorData(entry.getValue().getAsInt(),
-                                new VertexBufferPopulator(type));
-                        if (vb != null) {
-                            morphTarget.setBuffer(type, (FloatBuffer) vb.getData());
+                }
+
+                // Read morph targets
+                JsonArray targets = meshObject.getAsJsonArray("targets");
+                if (targets != null) {
+                    for (JsonElement target : targets) {
+                        MorphTarget morphTarget = new MorphTarget();
+                        if (targetNames.size() > 0) {
+                            morphTarget.setName(targetNames.pop());
+                        }
+                        for (Map.Entry<String, JsonElement> entry : target.getAsJsonObject().entrySet()) {
+                            String bufferType = entry.getKey();
+                            VertexBuffer.Type type = getVertexBufferType(bufferType);
+                            VertexBuffer vb = readAccessorData(entry.getValue().getAsInt(),
+                                    new VertexBufferPopulator(type));
+                            if (vb != null) {
+                                morphTarget.setBuffer(type, (FloatBuffer) vb.getData());
+                            }
                         }
+                        mesh.addMorphTarget(morphTarget);
                     }
-                    mesh.addMorphTarget(morphTarget);
                 }
-            }
 
-            // Read mesh extras
-            mesh = customContentManager.readExtensionAndExtras("primitive", meshObject, mesh);
-            Geometry geom = new Geometry(null, mesh);
+                // Read mesh extras
+                mesh = customContentManager.readExtensionAndExtras("primitive", meshObject, mesh);
+                Geometry geom = new Geometry(null, mesh);
 
-            Integer materialIndex = getAsInteger(meshObject, "material");
-            if (materialIndex == null) {
-                geom.setMaterial(defaultMat);
-            } else {
-                useNormalsFlag = false;
-                geom.setMaterial(readMaterial(materialIndex));
-                if (geom.getMaterial().getAdditionalRenderState().getBlendMode()
-                        == RenderState.BlendMode.Alpha) {
-                    // Alpha blending is enabled for this material. Let's place the geom in the transparent bucket.
-                    geom.setQueueBucket(RenderQueue.Bucket.Transparent);
+                Integer materialIndex = getAsInteger(meshObject, "material");
+                if (materialIndex == null) {
+                    geom.setMaterial(defaultMat);
+                } else {
+                    useNormalsFlag = false;
+                    geom.setMaterial(readMaterial(materialIndex));
+                    if (geom.getMaterial().getAdditionalRenderState().getBlendMode()
+                            == RenderState.BlendMode.Alpha) {
+                        // Alpha blending is enabled for this material. Let's place the geom in the transparent bucket.
+                        geom.setQueueBucket(RenderQueue.Bucket.Transparent);
+                    }
+                    if (useNormalsFlag && mesh.getBuffer(VertexBuffer.Type.Tangent) == null) {
+                        // No tangent buffer, but there is a normal map, we have to generate them using MikktSpace
+                        MikktspaceTangentGenerator.generate(geom);
+                    }
                 }
-                if (useNormalsFlag && mesh.getBuffer(VertexBuffer.Type.Tangent) == null) {
-                    // No tangent buffer, but there is a normal map, we have to generate them using MiiktSpace
-                    MikktspaceTangentGenerator.generate(geom);
+
+                if (useVertexColors) {
+                    geom.getMaterial().setBoolean("UseVertexColor", useVertexColors);
                 }
-            }
 
-            if (name != null) {
-                geom.setName(name + (primitives.size() > 1 ? ("_" + index) : ""));
+                geom.setName(name + "_" + index);
+                
+                geom.updateModelBound();
+                geomArray[index] = geom;
+                index++;
             }
 
-            geom.updateModelBound();
-            geomArray[index] = geom;
-            index++;
-        }
+            geomArray = customContentManager.readExtensionAndExtras("mesh", meshData, geomArray);
 
-        geomArray = customContentManager.readExtensionAndExtras("mesh", meshData, geomArray);
-
-        addToCache("meshes", meshIndex, geomArray, meshes.size());
-        return geomArray;
+            addToCache("meshes", meshIndex, geomArray, meshes.size());
+        }
+        // cloning the geoms.
+        Geometry[] geoms = new Geometry[geomArray.length];
+        for (int i = 0; i < geoms.length; i++) {
+            geoms[i] = geomArray[i].clone(false);
+        }
+        return geoms;
     }
 
     private SkinBuffers getSkinBuffers(String bufferType) {
@@ -600,12 +605,12 @@ public class GltfLoader implements AssetLoader {
                 BinDataKey key = new BinDataKey(info.getKey().getFolder() + decoded);
                 InputStream input = (InputStream) info.getManager().loadAsset(key);
                 data = new byte[bufferLength];
-                DataInputStream dataStream = new DataInputStream(input);
-                dataStream.readFully(data);
-                dataStream.close();
+                try (DataInputStream dataStream = new DataInputStream(input)) {
+                    dataStream.readFully(data);
+                }
             }
         } else {
-            // no URI this should not happen in a gltf file, only in glb files.
+            // no URI, this should not happen in a gltf file, only in glb files.
             throw new AssetLoadException("Buffer " + bufferIndex + " has no uri");
         }
         return data;
@@ -614,6 +619,11 @@ public class GltfLoader implements AssetLoader {
     public Material readMaterial(int materialIndex) throws IOException {
         assertNotNull(materials, "There is no material defined yet a mesh references one");
 
+        Material material = fetchFromCache("materials", materialIndex, Material.class);
+        if (material != null) {
+            return material.clone();
+        }
+
         JsonObject matData = materials.get(materialIndex).getAsJsonObject();
         JsonObject pbrMat = matData.getAsJsonObject("pbrMetallicRoughness");
 
@@ -683,7 +693,10 @@ public class GltfLoader implements AssetLoader {
 
         adapter.setParam("emissiveTexture", readTexture(matData.getAsJsonObject("emissiveTexture")));
 
-        return adapter.getMaterial();
+        material = adapter.getMaterial();
+        addToCache("materials", materialIndex, material, materials.size());
+
+        return material;
     }
 
     public void readCameras() throws IOException {
@@ -742,11 +755,16 @@ public class GltfLoader implements AssetLoader {
         assertNotNull(textureIndex, "Texture has no index");
         assertNotNull(textures, "There are no textures, yet one is referenced by a material");
 
+        Texture2D texture2d = fetchFromCache("textures", textureIndex, Texture2D.class);
+        if (texture2d != null) {
+            return texture2d;
+        }
+        
         JsonObject textureData = textures.get(textureIndex).getAsJsonObject();
         Integer sourceIndex = getAsInteger(textureData, "source");
         Integer samplerIndex = getAsInteger(textureData, "sampler");
 
-        Texture2D texture2d = readImage(sourceIndex, flip);
+        texture2d = readImage(sourceIndex, flip);
 
         if (samplerIndex != null) {
             texture2d = readSampler(samplerIndex, texture2d);
@@ -756,6 +774,8 @@ public class GltfLoader implements AssetLoader {
 
         texture2d = customContentManager.readExtensionAndExtras("texture", texture, texture2d);
 
+        addToCache("textures", textureIndex, texture2d, textures.size());
+
         return texture2d;
     }
 
@@ -802,7 +822,7 @@ public class GltfLoader implements AssetLoader {
         String name = getAsString(animation, "name");
         assertNotNull(channels, "No channels for animation " + name);
         assertNotNull(samplers, "No samplers for animation " + name);
-
+        
         // temp data storage of track data
         TrackData[] tracks = new TrackData[nodes.size()];
         boolean hasMorphTrack = false;
@@ -902,17 +922,16 @@ public class GltfLoader implements AssetLoader {
                             trackData.translations, trackData.rotations, trackData.scales);
                     aTracks.add(track);
                 }
-                if (trackData.weights != null && s instanceof Geometry) {
-                    Geometry g = (Geometry) s;
-                    int nbMorph = g.getMesh().getMorphTargets().length;
-//                    for (int k = 0; k < trackData.weights.length; k++) {
-//                        System.err.print(trackData.weights[k] + ",");
-//                        if(k % nbMorph == 0 && k!=0){
-//                            System.err.println(" ");
-//                        }
-//                    }
-                    MorphTrack track = new MorphTrack(g, trackData.times, trackData.weights, nbMorph);
-                    aTracks.add(track);
+                if (trackData.weights != null) {
+                    if (s instanceof Node) {
+                        s.depthFirstTraversal((Spatial spatial) -> {
+                            if (spatial instanceof Geometry) {
+                                aTracks.add(toMorphTrack(trackData, spatial));
+                            }
+                        });
+                    } else if (s instanceof Geometry) {
+                        aTracks.add(toMorphTrack(trackData, s));
+                    }
                 }
             } else if (node instanceof JointWrapper) {
                 JointWrapper jw = (JointWrapper) node;
@@ -938,9 +957,9 @@ public class GltfLoader implements AssetLoader {
             }
         }
 
-        // Check each bone to see if their local pose is different from their bind pose.
+        // Check each bone to see if its local pose is different from its bind pose.
         // If it is, we ensure that the bone has an animation track,
-        // else JME way of applying anim transforms will apply the bind pose to those bones,
+        // else the JME way of applying anim transforms will apply the bind pose to those bones,
         // instead of the local pose that is supposed to be the default
         if (skinIndex != -1) {
             SkinData skin = fetchFromCache("skins", skinIndex, SkinData.class);
@@ -957,7 +976,7 @@ public class GltfLoader implements AssetLoader {
                 }
             }
         }
-
+        
         anim.setTracks(aTracks.toArray(new AnimTrack[aTracks.size()]));
         anim = customContentManager.readExtensionAndExtras("animations", animation, anim);
 
@@ -970,7 +989,7 @@ public class GltfLoader implements AssetLoader {
         if (!spatials.isEmpty()) {
             if (skinIndex != -1) {
                 // there are some spatial or morph tracks in this bone animation... or the other way around.
-                // Let's add the spatials in the skinnedSpatials.
+                // Let's add the spatials to the skinnedSpatials.
                 SkinData skin = fetchFromCache("skins", skinIndex, SkinData.class);
                 List<Spatial> spat = skinnedSpatials.get(skin);
                 spat.addAll(spatials);
@@ -1034,7 +1053,7 @@ public class GltfLoader implements AssetLoader {
             JsonObject skin = skins.get(index).getAsJsonObject();
 
             // Note that the "skeleton" index is intentionally ignored.
-            // It's not mandatory and exporters tends to mix up how it should be used
+            // It's not mandatory and exporters tend to mix up how it should be used
             // because the specs are not clear.
             // Anyway we have other means to detect both armature structures and root bones.
 
@@ -1196,6 +1215,12 @@ public class GltfLoader implements AssetLoader {
         JsonObject meshData = meshes.get(meshIndex).getAsJsonObject();
         return getAsString(meshData, "name");
     }
+    
+    private MorphTrack toMorphTrack(TrackData data, Spatial spatial) {
+        Geometry g = (Geometry) spatial;
+        int nbMorph = g.getMesh().getMorphTargets().length;
+        return new MorphTrack(g, data.times, data.weights, nbMorph);
+    }
 
     public <T> T fetchFromCache(String name, int index, Class<T> type) {
         Object[] data = dataCache.get(name);
@@ -1349,7 +1374,7 @@ public class GltfLoader implements AssetLoader {
             float[] data = new float[dataSize];
 
             if (bufferViewIndex == null) {
-                // no referenced buffer, specs says to pad the data with zeros.
+                // no referenced buffer, specs say to pad the data with zeros.
                 padBuffer(data, dataSize);
             } else {
                 readBuffer(bufferViewIndex, byteOffset, count, data, numComponents,
@@ -1371,7 +1396,7 @@ public class GltfLoader implements AssetLoader {
 //            float[] data = new float[dataSize];
 //
 //            if (bufferViewIndex == null) {
-//                // no referenced buffer, specs says to pad the data with zeros.
+//                // no referenced buffer, specs say to pad the data with zeros.
 //                padBuffer(data, dataSize);
 //            } else {
 //                readBuffer(bufferViewIndex, byteOffset, count, data, numComponents,
@@ -1393,7 +1418,7 @@ public class GltfLoader implements AssetLoader {
             Vector3f[] data = new Vector3f[count];
 
             if (bufferViewIndex == null) {
-                // no referenced buffer, specs says to pad the data with zeros.
+                // no referenced buffer, specs say to pad the data with zeros.
                 padBuffer(data, dataSize);
             } else {
                 readBuffer(bufferViewIndex, byteOffset, count, data, numComponents,
@@ -1413,7 +1438,7 @@ public class GltfLoader implements AssetLoader {
             Quaternion[] data = new Quaternion[count];
 
             if (bufferViewIndex == null) {
-                // no referenced buffer, specs says to pad the data with zeros.
+                // no referenced buffer, specs say to pad the data with zeros.
                 padBuffer(data, dataSize);
             } else {
                 readBuffer(bufferViewIndex, byteOffset, count, data, numComponents,
@@ -1462,7 +1487,7 @@ public class GltfLoader implements AssetLoader {
             short[] data = new short[dataSize];
 
             if (bufferViewIndex == null) {
-                // no referenced buffer, specs says to pad the data with zeros.
+                // no referenced buffer, specs say to pad the data with zeros.
                 padBuffer(data, dataSize);
             } else {
                 readBuffer(bufferViewIndex, byteOffset, count, data, numComponents, format);

+ 30 - 0
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfModelKey.java

@@ -35,6 +35,7 @@ import com.jme3.asset.ModelKey;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Objects;
 
 /**
  * An optional key to use when loading a glTF file
@@ -114,4 +115,33 @@ public class GltfModelKey extends ModelKey {
     public void setExtrasLoader(ExtrasLoader extrasLoader) {
         this.extrasLoader = extrasLoader;
     }
+    
+    @Override
+    public boolean equals(Object object) {
+        if (object == null) {
+            return false;
+        }
+        if (getClass() != object.getClass()) {
+            return false;
+        }
+        final GltfModelKey other = (GltfModelKey)object;
+        if (!super.equals(other)) {
+            return false;
+        }
+        if (!Objects.equals(materialAdapters, other.materialAdapters)
+                || !Objects.equals(extrasLoader, other.extrasLoader)) {
+            return false;
+        }
+        return keepSkeletonPose == other.keepSkeletonPose;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 5;
+        hash = 37 * hash + materialAdapters.hashCode();
+        hash = 37 * hash + Objects.hashCode(this.extrasLoader);
+        hash = 37 * hash + (this.keepSkeletonPose ? 1 : 0);
+        return hash;
+    }
+    
 }

+ 63 - 0
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/PBREmissiveStrengthExtensionLoader.java

@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2023 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.gltf;
+
+import com.jme3.asset.AssetKey;
+import com.jme3.plugins.json.JsonElement;
+import java.io.IOException;
+
+/**
+ * Extension loader for "KHR_materials_emissive_strength".
+ * 
+ * @author codex
+ */
+public class PBREmissiveStrengthExtensionLoader implements ExtensionLoader {
+    
+    private PBREmissiveStrengthMaterialAdapter materialAdapter = new PBREmissiveStrengthMaterialAdapter();
+    
+    @Override
+    public Object handleExtension(GltfLoader loader, String parentName, JsonElement parent, JsonElement extension, Object input) throws IOException {
+        MaterialAdapter adapter = materialAdapter;
+        AssetKey key = loader.getInfo().getKey();
+        //check for a custom adapter for emissive strength
+        if (key instanceof GltfModelKey) {
+            MaterialAdapter custom = ((GltfModelKey)key).getAdapterForMaterial("pbrEmissiveStrength");
+            if (custom != null) {
+                adapter = custom;
+            }
+        }        
+        adapter.init(loader.getInfo().getManager());
+        adapter.setParam("emissiveStrength", GltfUtils.getAsFloat(extension.getAsJsonObject(), "emissiveStrength"));
+        return adapter;
+    }
+    
+}

+ 46 - 0
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/PBREmissiveStrengthMaterialAdapter.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2023 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene.plugins.gltf;
+
+/**
+ * Adapter for converting GLTF emissive strength to JME emissive intensity.
+ * 
+ * @author codex
+ */
+public class PBREmissiveStrengthMaterialAdapter extends PBRMaterialAdapter {
+    
+    public PBREmissiveStrengthMaterialAdapter() {
+        super();
+        addParamMapping("emissiveStrength", "EmissiveIntensity");
+    }
+    
+}

+ 20 - 12
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/PBRMaterialAdapter.java

@@ -34,10 +34,17 @@ package com.jme3.scene.plugins.gltf;
 import com.jme3.material.*;
 
 /**
- * Created by Nehon on 08/08/2017.
+ * Adapts GLTF PBR materials to JME PBR materials.
+ * 
+ * @author Nehon
  */
 public abstract class PBRMaterialAdapter extends MaterialAdapter {
-
+    
+    /**
+     * The default alpha discard threshold for "MASK" blend mode.
+     */
+    public static final float MASK_ALPHA_DISCARD = 0.5f;
+    
     public PBRMaterialAdapter() {
         addParamMapping("normalTexture", "NormalMap");
         addParamMapping("normalScale", "NormalScale");
@@ -60,15 +67,16 @@ public abstract class PBRMaterialAdapter extends MaterialAdapter {
         if (param.getName().equals("alpha")) {
             String alphaMode = (String) param.getValue();
             switch (alphaMode) {
-                case "MASK": // fallthrough
+                case "MASK":
+                    // "MASK" -> BlendMode.Off
+                    getMaterial().setFloat("AlphaDiscardThreshold", MASK_ALPHA_DISCARD);
+                    break;
                 case "BLEND":
                     getMaterial().getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
-                    break;
+                    // Alpha is a RenderState not a Material Parameter, so return null
+                    return null;
             }
-            // Alpha is a RenderState not a Material Parameter, so return null
-            return null;
-        }
-        if (param.getName().equals("doubleSided")) {
+        } else if (param.getName().equals("doubleSided")) {
             boolean doubleSided = (boolean) param.getValue();
             if (doubleSided) {
                 //Note that this is not completely right as normals on the back side will be in the wrong direction.
@@ -76,14 +84,14 @@ public abstract class PBRMaterialAdapter extends MaterialAdapter {
             }
             // FaceCulling is a RenderState not a Material Parameter, so return null
             return null;
-        }
-        if (param.getName().equals("NormalMap")) {
+        } else if (param.getName().equals("NormalMap")) {
             //Set the normal map type to OpenGl
             getMaterial().setFloat("NormalType", 1.0f);
-        }
-        if (param.getName().equals("LightMap")) {
+        } else if (param.getName().equals("LightMap")) {
             //Gltf only supports AO maps (gray scales and only the r channel must be read)
             getMaterial().setBoolean("LightMapAsAOMap", true);
+        } else if (param.getName().equals("alphaCutoff")) {
+            getMaterial().setFloat("AlphaDiscardThreshold", (float)param.getValue());
         }
 
         return param;

+ 146 - 4
jme3-plugins/src/test/java/com/jme3/export/JmeExporterTest.java

@@ -31,11 +31,18 @@
  */
 package com.jme3.export;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
 import org.junit.Assert;
 import org.junit.BeforeClass;
 import org.junit.Rule;
@@ -43,12 +50,18 @@ import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
+
+import com.jme3.asset.AssetInfo;
 import com.jme3.asset.AssetManager;
 import com.jme3.asset.DesktopAssetManager;
+import com.jme3.asset.ModelKey;
 import com.jme3.export.binary.BinaryExporter;
+import com.jme3.export.binary.BinaryImporter;
 import com.jme3.export.xml.XMLExporter;
+import com.jme3.export.xml.XMLImporter;
 import com.jme3.material.Material;
 import com.jme3.material.plugin.export.material.J3MExporter;
+import com.jme3.scene.Node;
 
 /**
  * Tests the methods on classes that implements the JmeExporter interface.
@@ -64,7 +77,6 @@ public class JmeExporterTest {
     @Rule
     public TemporaryFolder folder = new TemporaryFolder();
 
-
     @BeforeClass
     public static void beforeClass() {
         AssetManager assetManager = new DesktopAssetManager(true);
@@ -116,10 +128,140 @@ public class JmeExporterTest {
     public void testSaveWithNullParent() throws IOException {
         File file = new File("someFile.txt");
         try {
-        	exporter.save(material, file);
-        	Assert.assertTrue(file.exists());
+            exporter.save(material, file);
+            Assert.assertTrue(file.exists());
         } finally {
-        	file.delete();
+            file.delete();
+        }
+    }
+
+    @Test
+    public void testExporterConsistency() {
+        //
+        final boolean testXML = true;
+        final boolean testLists = false;
+        final boolean testMaps = true;
+        final boolean printXML = false;
+
+        // initialize data
+        AssetManager assetManager = new DesktopAssetManager(true);
+        ArrayList<JmeExporter> exporters = new ArrayList<JmeExporter>();
+        ArrayList<JmeImporter> importers = new ArrayList<JmeImporter>();
+
+        BinaryExporter be = new BinaryExporter();
+        BinaryImporter bi = new BinaryImporter();
+        exporters.add(be);
+        importers.add(bi);
+
+        if (testXML) {
+            XMLExporter xe = new XMLExporter();
+            XMLImporter xi = new XMLImporter();
+            exporters.add(xe);
+            importers.add(xi);
+        }
+
+        Node origin = new Node("origin");
+
+        origin.setUserData("testInt", 10);
+        origin.setUserData("testString", "ABC");
+        origin.setUserData("testBoolean", true);
+        origin.setUserData("testFloat", 1.5f);
+        origin.setUserData("1", "test");
+        if (testLists) {
+            origin.setUserData("string-list", Arrays.asList("abc"));
+            origin.setUserData("int-list", Arrays.asList(1, 2, 3));
+            origin.setUserData("float-list", Arrays.asList(1f, 2f, 3f));
         }
+
+        if (testMaps) {
+            Map<String, Object> map = new HashMap<>();
+            map.put("int", 1);
+            map.put("string", "abc");
+            map.put("float", 1f);
+            origin.setUserData("map", map);
+        }
+
+        // export
+        ByteArrayOutputStream outs[] = new ByteArrayOutputStream[exporters.size()];
+        for (int i = 0; i < exporters.size(); i++) {
+            JmeExporter exporter = exporters.get(i);
+            outs[i] = new ByteArrayOutputStream();
+            try {
+                exporter.save(origin, outs[i]);
+            } catch (IOException ex) {
+                Assert.fail(ex.getMessage());
+            }
+        }
+
+        // print
+        if (printXML) {
+            for (int i = 0; i < exporters.size(); i++) {
+                ByteArrayOutputStream out = outs[i];
+                if (exporters.get(i) instanceof XMLExporter) {
+                    System.out.println("XML: \n" + new String(out.toByteArray()) + "\n\n");
+                } else if (exporters.get(i) instanceof BinaryExporter) {
+                    System.out.println("Binary: " + out.size() + " bytes");
+                } else {
+                    System.out.println("Unknown exporter: " + exporters.get(i).getClass().getName());
+                }
+            }
+        }
+
+        // import
+        Node nodes[] = new Node[importers.size() + 1];
+        nodes[0] = origin;
+        for (int i = 0; i < importers.size(); i++) {
+            JmeImporter importer = importers.get(i);
+            ByteArrayOutputStream out = outs[i];
+            try {
+                AssetInfo info = new AssetInfo(assetManager, new ModelKey("origin")) {
+                    @Override
+                    public InputStream openStream() {
+                        return new ByteArrayInputStream(out.toByteArray());
+                    }
+                };
+                nodes[i + 1] = (Node) importer.load(info);
+            } catch (IOException ex) {
+                Assert.fail(ex.getMessage());
+            }
+        }
+
+        // compare
+        Map<String, Object> userData[] = new Map[nodes.length];
+        for (int i = 0; i < nodes.length; i++) {
+            Node n = nodes[i];
+            userData[i] = new HashMap<String, Object>();
+            for (String k : n.getUserDataKeys()) {
+                userData[i].put(k, n.getUserData(k));
+            }
+        }
+        compareMaps(userData);
+    }
+
+    private static final void compareMaps(Map<String, Object>[] maps) {
+        String[] keys = maps[0].keySet().toArray(new String[0]);
+        // check if all maps have the same keys and values for those keys
+        for (int i = 1; i < maps.length; i++) {
+            Map<String, Object> map = maps[i];
+            Assert.assertEquals("Map " + i + " keys do not match", keys.length, map.size());
+            for (String key : keys) {
+                Assert.assertTrue("Missing key " + key + " in map " + i, map.containsKey(key));
+                Object v1 = maps[0].get(key);
+                Object v2 = map.get(key);
+                if (v1.getClass().isArray()) {
+                    boolean c = Arrays.equals((Object[]) v1, (Object[]) v2);
+                    if (c) System.out.println(key + " match");
+                    Assert.assertTrue("Value does not match in map " + i + " for key " + key + " expected "
+                            + Arrays.deepToString((Object[]) v1) + " but got "
+                            + Arrays.deepToString((Object[]) v2), c);
+                } else {
+                    boolean c = v1.equals(v2);
+                    if (c) System.out.println(key + " match");
+                    Assert.assertTrue("Value does not match in map " + i + " for key " + key + " expected "
+                            + v1 + " but got " + v2, c);
+                }
+            }
+        }
+
     }
 }

+ 61 - 59
jme3-plugins/src/xml/java/com/jme3/export/xml/DOMInputCapsule.java

@@ -72,6 +72,7 @@ public class DOMInputCapsule implements InputCapsule {
         this.importer = importer;
         currentElem = doc.getDocumentElement();
         
+        // file version is always unprefixed for backwards compatibility
         String version = currentElem.getAttribute("format_version");
         importer.formatVersion = version.equals("") ? 0 : Integer.parseInt(version);
     }
@@ -126,7 +127,7 @@ public class DOMInputCapsule implements InputCapsule {
 
     @Override
     public byte readByte(String name, byte defVal) throws IOException {
-        String tmpString = currentElem.getAttribute(name);
+        String tmpString = XMLUtils.getAttribute(importer.getFormatVersion(), currentElem, name);
         if (tmpString == null || tmpString.length() < 1) return defVal;
         try {
             return Byte.parseByte(tmpString);
@@ -149,8 +150,8 @@ public class DOMInputCapsule implements InputCapsule {
             if (tmpEl == null) {
                 return defVal;
             }
-            String sizeString = tmpEl.getAttribute("size");
-            String[] strings = parseTokens(tmpEl.getAttribute("data"));
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
+            String[] strings = parseTokens(XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "data"));
             if (sizeString.length() > 0) {
                 int requiredSize = Integer.parseInt(sizeString);
                 if (strings.length != requiredSize)
@@ -186,7 +187,7 @@ public class DOMInputCapsule implements InputCapsule {
                 return defVal;
             }
 
-            String sizeString = tmpEl.getAttribute("size");
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
             NodeList nodes = currentElem.getChildNodes();
             List<byte[]> byteArrays = new ArrayList<>();
 
@@ -218,7 +219,7 @@ public class DOMInputCapsule implements InputCapsule {
 
     @Override
     public int readInt(String name, int defVal) throws IOException {
-        String tmpString = currentElem.getAttribute(name);
+        String tmpString = XMLUtils.getAttribute(importer.getFormatVersion(), currentElem, name);
         if (tmpString == null || tmpString.length() < 1) return defVal;
         try {
             return Integer.parseInt(tmpString);
@@ -241,8 +242,8 @@ public class DOMInputCapsule implements InputCapsule {
             if (tmpEl == null) {
                 return defVal;
             }
-            String sizeString = tmpEl.getAttribute("size");
-            String[] strings = parseTokens(tmpEl.getAttribute("data"));
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
+            String[] strings = parseTokens(XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "data"));
             if (sizeString.length() > 0) {
                 int requiredSize = Integer.parseInt(sizeString);
                 if (strings.length != requiredSize)
@@ -276,7 +277,7 @@ public class DOMInputCapsule implements InputCapsule {
             if (tmpEl == null) {
                 return defVal;
             }
-            String sizeString = tmpEl.getAttribute("size");
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
 
 
 
@@ -312,7 +313,7 @@ public class DOMInputCapsule implements InputCapsule {
 
     @Override
     public float readFloat(String name, float defVal) throws IOException {
-        String tmpString = currentElem.getAttribute(name);
+        String tmpString = XMLUtils.getAttribute(importer.getFormatVersion(), currentElem, name);
         if (tmpString == null || tmpString.length() < 1) return defVal;
         try {
             return Float.parseFloat(tmpString);
@@ -335,8 +336,8 @@ public class DOMInputCapsule implements InputCapsule {
             if (tmpEl == null) {
                 return defVal;
             }
-            String sizeString = tmpEl.getAttribute("size");
-            String[] strings = parseTokens(tmpEl.getAttribute("data"));
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
+            String[] strings = parseTokens(XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "data"));
             if (sizeString.length() > 0) {
                 int requiredSize = Integer.parseInt(sizeString);
                 if (strings.length != requiredSize)
@@ -371,12 +372,12 @@ public class DOMInputCapsule implements InputCapsule {
             if (tmpEl == null) {
                 return defVal;
             }
-            int size_outer = Integer.parseInt(tmpEl.getAttribute("size_outer"));
-            int size_inner = Integer.parseInt(tmpEl.getAttribute("size_outer"));
+            int size_outer = Integer.parseInt(XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size_outer"));
+            int size_inner = Integer.parseInt(XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size_outer"));
 
             float[][] tmp = new float[size_outer][size_inner];
 
-            String[] strings = parseTokens(tmpEl.getAttribute("data"));
+            String[] strings = parseTokens(XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "data"));
             for (int i = 0; i < size_outer; i++) {
                 tmp[i] = new float[size_inner];
                 for (int k = 0; k < size_inner; k++) {
@@ -393,7 +394,7 @@ public class DOMInputCapsule implements InputCapsule {
 
     @Override
     public double readDouble(String name, double defVal) throws IOException {
-        String tmpString = currentElem.getAttribute(name);
+        String tmpString = XMLUtils.getAttribute(importer.getFormatVersion(), currentElem, name);
         if (tmpString == null || tmpString.length() < 1) return defVal;
         try {
             return Double.parseDouble(tmpString);
@@ -416,8 +417,8 @@ public class DOMInputCapsule implements InputCapsule {
             if (tmpEl == null) {
                 return defVal;
             }
-            String sizeString = tmpEl.getAttribute("size");
-            String[] strings = parseTokens(tmpEl.getAttribute("data"));
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
+            String[] strings = parseTokens(XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "data"));
             if (sizeString.length() > 0) {
                 int requiredSize = Integer.parseInt(sizeString);
                 if (strings.length != requiredSize)
@@ -451,7 +452,7 @@ public class DOMInputCapsule implements InputCapsule {
             if (tmpEl == null) {
                 return defVal;
             }
-            String sizeString = tmpEl.getAttribute("size");
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
             NodeList nodes = currentElem.getChildNodes();
             List<double[]> doubleArrays = new ArrayList<>();
 
@@ -483,7 +484,7 @@ public class DOMInputCapsule implements InputCapsule {
 
     @Override
     public long readLong(String name, long defVal) throws IOException {
-        String tmpString = currentElem.getAttribute(name);
+        String tmpString = XMLUtils.getAttribute(importer.getFormatVersion(), currentElem, name);
         if (tmpString == null || tmpString.length() < 1) return defVal;
         try {
             return Long.parseLong(tmpString);
@@ -506,8 +507,8 @@ public class DOMInputCapsule implements InputCapsule {
             if (tmpEl == null) {
                 return defVal;
             }
-            String sizeString = tmpEl.getAttribute("size");
-            String[] strings = parseTokens(tmpEl.getAttribute("data"));
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
+            String[] strings = parseTokens(XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "data"));
             if (sizeString.length() > 0) {
                 int requiredSize = Integer.parseInt(sizeString);
                 if (strings.length != requiredSize)
@@ -541,7 +542,7 @@ public class DOMInputCapsule implements InputCapsule {
             if (tmpEl == null) {
                 return defVal;
             }
-            String sizeString = tmpEl.getAttribute("size");
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
             NodeList nodes = currentElem.getChildNodes();
             List<long[]> longArrays = new ArrayList<>();
 
@@ -573,7 +574,7 @@ public class DOMInputCapsule implements InputCapsule {
 
     @Override
     public short readShort(String name, short defVal) throws IOException {
-        String tmpString = currentElem.getAttribute(name);
+        String tmpString = XMLUtils.getAttribute(importer.getFormatVersion(), currentElem, name);
         if (tmpString == null || tmpString.length() < 1) return defVal;
         try {
             return Short.parseShort(tmpString);
@@ -596,8 +597,8 @@ public class DOMInputCapsule implements InputCapsule {
              if (tmpEl == null) {
                  return defVal;
              }
-            String sizeString = tmpEl.getAttribute("size");
-            String[] strings = parseTokens(tmpEl.getAttribute("data"));
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
+            String[] strings = parseTokens(XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "data"));
             if (sizeString.length() > 0) {
                 int requiredSize = Integer.parseInt(sizeString);
                 if (strings.length != requiredSize)
@@ -632,7 +633,7 @@ public class DOMInputCapsule implements InputCapsule {
                 return defVal;
             }
 
-            String sizeString = tmpEl.getAttribute("size");
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
             NodeList nodes = currentElem.getChildNodes();
             List<short[]> shortArrays = new ArrayList<>();
 
@@ -664,7 +665,7 @@ public class DOMInputCapsule implements InputCapsule {
 
     @Override
     public boolean readBoolean(String name, boolean defVal) throws IOException {
-        String tmpString = currentElem.getAttribute(name);
+        String tmpString = XMLUtils.getAttribute(importer.getFormatVersion(), currentElem, name);
         if (tmpString == null || tmpString.length() < 1) return defVal;
         try {
             return Boolean.parseBoolean(tmpString);
@@ -687,8 +688,8 @@ public class DOMInputCapsule implements InputCapsule {
             if (tmpEl == null) {
                 return defVal;
             }
-            String sizeString = tmpEl.getAttribute("size");
-            String[] strings = parseTokens(tmpEl.getAttribute("data"));
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
+            String[] strings = parseTokens(XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "data"));
             if (sizeString.length() > 0) {
                 int requiredSize = Integer.parseInt(sizeString);
                 if (strings.length != requiredSize)
@@ -722,7 +723,7 @@ public class DOMInputCapsule implements InputCapsule {
             if (tmpEl == null) {
                 return defVal;
             }
-            String sizeString = tmpEl.getAttribute("size");
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
             NodeList nodes = currentElem.getChildNodes();
             List<boolean[]> booleanArrays = new ArrayList<>();
 
@@ -754,7 +755,7 @@ public class DOMInputCapsule implements InputCapsule {
 
     @Override
     public String readString(String name, String defVal) throws IOException {
-        String tmpString = currentElem.getAttribute(name);
+        String tmpString = XMLUtils.getAttribute(importer.getFormatVersion(), currentElem, name);
         if (tmpString == null || tmpString.length() < 1) return defVal;
         try {
             return decodeString(tmpString);
@@ -777,7 +778,7 @@ public class DOMInputCapsule implements InputCapsule {
              if (tmpEl == null) {
                  return defVal;
              }
-            String sizeString = tmpEl.getAttribute("size");
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
             NodeList nodes = tmpEl.getChildNodes();
             List<String> strings = new ArrayList<>();
 
@@ -818,7 +819,7 @@ public class DOMInputCapsule implements InputCapsule {
             if (tmpEl == null) {
                 return defVal;
             }
-            String sizeString = tmpEl.getAttribute("size");
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
             NodeList nodes = currentElem.getChildNodes();
             List<String[]> stringArrays = new ArrayList<>();
 
@@ -850,7 +851,7 @@ public class DOMInputCapsule implements InputCapsule {
 
     @Override
     public BitSet readBitSet(String name, BitSet defVal) throws IOException {
-        String tmpString = currentElem.getAttribute(name);
+        String tmpString = XMLUtils.getAttribute(importer.getFormatVersion(), currentElem, name);
         if (tmpString == null || tmpString.length() < 1) return defVal;
         try {
             BitSet set = new BitSet();
@@ -914,19 +915,19 @@ public class DOMInputCapsule implements InputCapsule {
         if (currentElem == null || currentElem.getNodeName().equals("null")) {
             return null;
         }
-        String reference = currentElem.getAttribute("ref");
+        String reference = XMLUtils.getAttribute(importer.getFormatVersion(), currentElem, "ref");
         if (reference.length() > 0) {
             ret = referencedSavables.get(reference);
         } else {
             String className = currentElem.getNodeName();
-            if (currentElem.hasAttribute("class")) {
-                className = currentElem.getAttribute("class");
+            if (XMLUtils.hasAttribute(importer.getFormatVersion(), currentElem, "class")) {
+                className = XMLUtils.getAttribute(importer.getFormatVersion(), currentElem, "class");
             } else if (defVal != null) {
                 className = defVal.getClass().getName();
             }
             tmp = SavableClassUtil.fromName(className);
             
-            String versionsStr = currentElem.getAttribute("savable_versions");
+            String versionsStr = XMLUtils.getAttribute(importer.getFormatVersion(), currentElem, "savable_versions");
             if (versionsStr != null && !versionsStr.equals("")){
                 String[] versionStr = versionsStr.split(",");
                 classHierarchyVersions = new int[versionStr.length];
@@ -937,8 +938,8 @@ public class DOMInputCapsule implements InputCapsule {
                 classHierarchyVersions = null;
             }
             
-            String refID = currentElem.getAttribute("reference_ID");
-            if (refID.length() < 1) refID = currentElem.getAttribute("id");
+            String refID = XMLUtils.getAttribute(importer.getFormatVersion(), currentElem, "reference_ID");
+            if (refID.length() < 1) refID = XMLUtils.getAttribute(importer.getFormatVersion(), currentElem, "id");
             if (refID.length() > 0) referencedSavables.put(refID, tmp);
             if (tmp != null) {
                 // Allows reading versions from this savable
@@ -959,7 +960,7 @@ public class DOMInputCapsule implements InputCapsule {
                 return defVal;
             }
 
-            String sizeString = tmpEl.getAttribute("size");
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
             List<Savable> savables = new ArrayList<>();
             for (currentElem = findFirstChildElement(tmpEl);
                     currentElem != null;
@@ -994,8 +995,8 @@ public class DOMInputCapsule implements InputCapsule {
                 return defVal;
             }
 
-            int size_outer = Integer.parseInt(tmpEl.getAttribute("size_outer"));
-            int size_inner = Integer.parseInt(tmpEl.getAttribute("size_outer"));
+            int size_outer = Integer.parseInt(XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size_outer"));
+            int size_inner = Integer.parseInt(XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size_outer"));
 
             Savable[][] tmp = new Savable[size_outer][size_inner];
             currentElem = findFirstChildElement(tmpEl);
@@ -1029,7 +1030,7 @@ public class DOMInputCapsule implements InputCapsule {
                 return defVal;
             }
 
-            String sizeString = tmpEl.getAttribute("size");
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
             ArrayList<Savable> savables = new ArrayList<>();
             for (currentElem = findFirstChildElement(tmpEl);
                     currentElem != null;
@@ -1066,7 +1067,7 @@ public class DOMInputCapsule implements InputCapsule {
             }
             currentElem = tmpEl;
 
-            String sizeString = tmpEl.getAttribute("size");
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
             int requiredSize = (sizeString.length() > 0)
                              ? Integer.parseInt(sizeString)
                              : -1;
@@ -1107,7 +1108,7 @@ public class DOMInputCapsule implements InputCapsule {
                 return defVal;
             }
             currentElem = tmpEl;
-            String sizeString = tmpEl.getAttribute("size");
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
 
             ArrayList<Savable>[] arr;
             List<ArrayList<Savable>[]> sall = new ArrayList<>();
@@ -1142,7 +1143,7 @@ public class DOMInputCapsule implements InputCapsule {
                 return defVal;
             }
 
-            String sizeString = tmpEl.getAttribute("size");
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
             ArrayList<FloatBuffer> tmp = new ArrayList<>();
             for (currentElem = findFirstChildElement(tmpEl);
                     currentElem != null;
@@ -1214,7 +1215,7 @@ public class DOMInputCapsule implements InputCapsule {
                                 if (n instanceof Element && n.getNodeName().equals("MapEntry")) {
                                         Element elem = (Element) n;
                                         currentElem = elem;
-                                        String key = currentElem.getAttribute("key");
+                                        String key = XMLUtils.getAttribute(importer.getFormatVersion(), currentElem, "key");
                                         Savable val = readSavable("Savable", null);
                                         ret.put(key, val);
                                 }
@@ -1245,7 +1246,7 @@ public class DOMInputCapsule implements InputCapsule {
                                 if (n instanceof Element && n.getNodeName().equals("MapEntry")) {
                                         Element elem = (Element) n;
                                         currentElem = elem;
-                                        int key = Integer.parseInt(currentElem.getAttribute("key"));
+                                        int key = Integer.parseInt(XMLUtils.getAttribute(importer.getFormatVersion(), currentElem, "key"));
                                         Savable val = readSavable("Savable", null);
                                         ret.put(key, val);
                                 }
@@ -1272,8 +1273,8 @@ public class DOMInputCapsule implements InputCapsule {
             if (tmpEl == null) {
                 return defVal;
             }
-            String sizeString = tmpEl.getAttribute("size");
-            String[] strings = parseTokens(tmpEl.getAttribute("data"));
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
+            String[] strings = parseTokens(XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "data"));
             if (sizeString.length() > 0) {
                 int requiredSize = Integer.parseInt(sizeString);
                 if (strings.length != requiredSize)
@@ -1302,8 +1303,8 @@ public class DOMInputCapsule implements InputCapsule {
                 return defVal;
             }
 
-            String sizeString = tmpEl.getAttribute("size");
-            String[] strings = parseTokens(tmpEl.getAttribute("data"));
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
+            String[] strings = parseTokens(XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "data"));
             if (sizeString.length() > 0) {
                 int requiredSize = Integer.parseInt(sizeString);
                 if (strings.length != requiredSize)
@@ -1332,8 +1333,8 @@ public class DOMInputCapsule implements InputCapsule {
                 return defVal;
             }
 
-            String sizeString = tmpEl.getAttribute("size");
-            String[] strings = parseTokens(tmpEl.getAttribute("data"));
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
+            String[] strings = parseTokens(XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "data"));
             if (sizeString.length() > 0) {
                 int requiredSize = Integer.parseInt(sizeString);
                 if (strings.length != requiredSize)
@@ -1362,8 +1363,8 @@ public class DOMInputCapsule implements InputCapsule {
                 return defVal;
             }
 
-            String sizeString = tmpEl.getAttribute("size");
-            String[] strings = parseTokens(tmpEl.getAttribute("data"));
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
+            String[] strings = parseTokens(XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "data"));
             if (sizeString.length() > 0) {
                 int requiredSize = Integer.parseInt(sizeString);
                 if (strings.length != requiredSize)
@@ -1392,7 +1393,7 @@ public class DOMInputCapsule implements InputCapsule {
                 return defVal;
             }
 
-            String sizeString = tmpEl.getAttribute("size");
+            String sizeString = XMLUtils.getAttribute(importer.getFormatVersion(), tmpEl, "size");
             ArrayList<ByteBuffer> tmp = new ArrayList<>();
             for (currentElem = findFirstChildElement(tmpEl);
                     currentElem != null;
@@ -1422,7 +1423,7 @@ public class DOMInputCapsule implements InputCapsule {
                         T defVal) throws IOException {
         T ret = defVal;
         try {
-            String eVal = currentElem.getAttribute(name);
+            String eVal = XMLUtils.getAttribute(importer.getFormatVersion(), currentElem, name);
             if (eVal != null && eVal.length() > 0) {
                 ret = Enum.valueOf(enumType, eVal);
             }
@@ -1442,4 +1443,5 @@ public class DOMInputCapsule implements InputCapsule {
                ? zeroStrings
                : outStrings;
     }
+    
 }

+ 63 - 61
jme3-plugins/src/xml/java/com/jme3/export/xml/DOMOutputCapsule.java

@@ -56,7 +56,7 @@ import org.w3c.dom.Element;
  * @author Doug Daniels (dougnukem) - adjustments for jME 2.0 and Java 1.5
  */
 public class DOMOutputCapsule implements OutputCapsule {
-
+    
     private static final String dataAttributeName = "data";
     private Document doc;
     private Element currentElement;
@@ -80,6 +80,7 @@ public class DOMOutputCapsule implements OutputCapsule {
     private Element appendElement(String name) {
         Element ret = doc.createElement(name);
         if (currentElement == null) {
+            // file version is always unprefixed for backwards compatibility
             ret.setAttribute("format_version", Integer.toString(FormatVersion.VERSION));
             doc.appendChild(ret);
         } else {
@@ -104,7 +105,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         if (value == defVal) {
             return;
         }
-        currentElement.setAttribute(name, String.valueOf(value));
+        XMLUtils.setAttribute(currentElement, name, String.valueOf(value));
     }
 
     @Override
@@ -124,8 +125,8 @@ public class DOMOutputCapsule implements OutputCapsule {
         }
 
         Element el = appendElement(name);
-        el.setAttribute("size", String.valueOf(value.length));
-        el.setAttribute(dataAttributeName, buf.toString());
+        XMLUtils.setAttribute(el, "size", String.valueOf(value.length));
+        XMLUtils.setAttribute(el, dataAttributeName, buf.toString());
         currentElement = (Element) currentElement.getParentNode();
     }
 
@@ -151,9 +152,9 @@ public class DOMOutputCapsule implements OutputCapsule {
         }
 
         Element el = appendElement(name);
-        el.setAttribute("size_outer", String.valueOf(value.length));
-        el.setAttribute("size_inner", String.valueOf(value[0].length));
-        el.setAttribute(dataAttributeName, buf.toString());
+        XMLUtils.setAttribute(el, "size_outer", String.valueOf(value.length));
+        XMLUtils.setAttribute(el, "size_inner", String.valueOf(value[0].length));
+        XMLUtils.setAttribute(el, dataAttributeName, buf.toString());
         currentElement = (Element) currentElement.getParentNode();
     }
 
@@ -162,7 +163,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         if (value == defVal) {
             return;
         }
-        currentElement.setAttribute(name, String.valueOf(value));
+        XMLUtils.setAttribute(currentElement, name, String.valueOf(value));
     }
 
     @Override
@@ -182,8 +183,8 @@ public class DOMOutputCapsule implements OutputCapsule {
         }
 
         Element el = appendElement(name);
-        el.setAttribute("size", String.valueOf(value.length));
-        el.setAttribute(dataAttributeName, buf.toString());
+        XMLUtils.setAttribute(el, "size", String.valueOf(value.length));
+        XMLUtils.setAttribute(el, dataAttributeName, buf.toString());
         currentElement = (Element) currentElement.getParentNode();
     }
 
@@ -193,7 +194,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         if(Arrays.deepEquals(value, defVal)) return;
 
         Element el = appendElement(name);
-        el.setAttribute("size", String.valueOf(value.length));
+        XMLUtils.setAttribute(el, "size", String.valueOf(value.length));
 
         for (int i=0; i<value.length; i++) {
                 int[] array = value[i];
@@ -207,7 +208,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         if (value == defVal) {
             return;
         }
-        currentElement.setAttribute(name, String.valueOf(value));
+        XMLUtils.setAttribute(currentElement, name, String.valueOf(value));
     }
 
     @Override
@@ -229,8 +230,8 @@ public class DOMOutputCapsule implements OutputCapsule {
         }
 
         Element el = appendElement(name);
-        el.setAttribute("size", value == null ? "0" : String.valueOf(value.length));
-        el.setAttribute(dataAttributeName, buf.toString());
+        XMLUtils.setAttribute(el, "size", value == null ? "0" : String.valueOf(value.length));
+        XMLUtils.setAttribute(el, dataAttributeName, buf.toString());
         currentElement = (Element) currentElement.getParentNode();
     }
 
@@ -253,9 +254,9 @@ public class DOMOutputCapsule implements OutputCapsule {
         }
 
         Element el = appendElement(name);
-        el.setAttribute("size_outer", String.valueOf(value.length));
-        el.setAttribute("size_inner", String.valueOf(value[0].length));
-        el.setAttribute(dataAttributeName, buf.toString());
+        XMLUtils.setAttribute(el, "size_outer", String.valueOf(value.length));
+        XMLUtils.setAttribute(el, "size_inner", String.valueOf(value[0].length));
+        XMLUtils.setAttribute(el, dataAttributeName, buf.toString());
         currentElement = (Element) currentElement.getParentNode();
     }
 
@@ -264,7 +265,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         if (value == defVal) {
             return;
         }
-        currentElement.setAttribute(name, String.valueOf(value));
+        XMLUtils.setAttribute(currentElement, name, String.valueOf(value));
     }
 
     @Override
@@ -284,8 +285,8 @@ public class DOMOutputCapsule implements OutputCapsule {
         }
 
         Element el = appendElement(name);
-        el.setAttribute("size", String.valueOf(value.length));
-        el.setAttribute(dataAttributeName, buf.toString());
+        XMLUtils.setAttribute(el, "size", String.valueOf(value.length));
+        XMLUtils.setAttribute(el, dataAttributeName, buf.toString());
         currentElement = (Element) currentElement.getParentNode();
     }
 
@@ -295,7 +296,7 @@ public class DOMOutputCapsule implements OutputCapsule {
             if(Arrays.deepEquals(value, defVal)) return;
 
             Element el = appendElement(name);
-            el.setAttribute("size", String.valueOf(value.length));
+            XMLUtils.setAttribute(el, "size", String.valueOf(value.length));
 
             for (int i=0; i<value.length; i++) {
                 double[] array = value[i];
@@ -309,7 +310,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         if (value == defVal) {
             return;
         }
-        currentElement.setAttribute(name, String.valueOf(value));
+        XMLUtils.setAttribute(currentElement, name, String.valueOf(value));
     }
 
     @Override
@@ -329,8 +330,8 @@ public class DOMOutputCapsule implements OutputCapsule {
         }
 
         Element el = appendElement(name);
-        el.setAttribute("size", String.valueOf(value.length));
-        el.setAttribute(dataAttributeName, buf.toString());
+        XMLUtils.setAttribute(el, "size", String.valueOf(value.length));
+        XMLUtils.setAttribute(el, dataAttributeName, buf.toString());
         currentElement = (Element) currentElement.getParentNode();
     }
 
@@ -340,7 +341,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         if(Arrays.deepEquals(value, defVal)) return;
 
         Element el = appendElement(name);
-        el.setAttribute("size", String.valueOf(value.length));
+        XMLUtils.setAttribute(el, "size", String.valueOf(value.length));
 
         for (int i=0; i<value.length; i++) {
                 long[] array = value[i];
@@ -354,7 +355,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         if (value == defVal) {
             return;
         }
-        currentElement.setAttribute(name, String.valueOf(value));
+        XMLUtils.setAttribute(currentElement, name, String.valueOf(value));
     }
 
     @Override
@@ -374,8 +375,8 @@ public class DOMOutputCapsule implements OutputCapsule {
         }
 
         Element el = appendElement(name);
-        el.setAttribute("size", String.valueOf(value.length));
-        el.setAttribute(dataAttributeName, buf.toString());
+        XMLUtils.setAttribute(el, "size", String.valueOf(value.length));
+        XMLUtils.setAttribute(el, dataAttributeName, buf.toString());
         currentElement = (Element) currentElement.getParentNode();
     }
 
@@ -385,7 +386,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         if(Arrays.deepEquals(value, defVal)) return;
 
         Element el = appendElement(name);
-        el.setAttribute("size", String.valueOf(value.length));
+        XMLUtils.setAttribute(el, "size", String.valueOf(value.length));
 
         for (int i=0; i<value.length; i++) {
                 short[] array = value[i];
@@ -399,7 +400,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         if (value == defVal) {
             return;
         }
-        currentElement.setAttribute(name, String.valueOf(value));
+        XMLUtils.setAttribute(currentElement, name, String.valueOf(value));
     }
 
     @Override
@@ -419,8 +420,8 @@ public class DOMOutputCapsule implements OutputCapsule {
         }
 
         Element el = appendElement(name);
-        el.setAttribute("size", String.valueOf(value.length));
-        el.setAttribute(dataAttributeName, buf.toString());
+        XMLUtils.setAttribute(el, "size", String.valueOf(value.length));
+        XMLUtils.setAttribute(el, dataAttributeName, buf.toString());
         currentElement = (Element) currentElement.getParentNode();
     }
 
@@ -430,7 +431,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         if(Arrays.deepEquals(value, defVal)) return;
 
         Element el = appendElement(name);
-        el.setAttribute("size", String.valueOf(value.length));
+        XMLUtils.setAttribute(el, "size", String.valueOf(value.length));
 
         for (int i=0; i<value.length; i++) {
                 boolean[] array = value[i];
@@ -444,7 +445,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         if (value == null || value.equals(defVal)) {
             return;
         }
-        currentElement.setAttribute(name, encodeString(value));
+        XMLUtils.setAttribute(currentElement, name, encodeString(value));
     }
 
     @Override
@@ -455,13 +456,13 @@ public class DOMOutputCapsule implements OutputCapsule {
             value = defVal;
         }
 
-        el.setAttribute("size", String.valueOf(value.length));
+        XMLUtils.setAttribute(el, "size", String.valueOf(value.length));
 
         for (int i=0; i<value.length; i++) {
                 String b = value[i];
                 appendElement("String_"+i);
             String val = encodeString(b);
-            currentElement.setAttribute("value", val);
+            XMLUtils.setAttribute(currentElement, "value", val);
             currentElement = el;
         }
         currentElement = (Element) currentElement.getParentNode();
@@ -473,7 +474,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         if(Arrays.deepEquals(value, defVal)) return;
 
         Element el = appendElement(name);
-        el.setAttribute("size", String.valueOf(value.length));
+        XMLUtils.setAttribute(el, "size", String.valueOf(value.length));
 
         for (int i=0; i<value.length; i++) {
                 String[] array = value[i];
@@ -498,7 +499,7 @@ public class DOMOutputCapsule implements OutputCapsule {
             buf.setLength(buf.length() - 1);
         }
         
-        currentElement.setAttribute(name, buf.toString());
+        XMLUtils.setAttribute(currentElement, name, buf.toString());
 
     }
 
@@ -533,10 +534,10 @@ public class DOMOutputCapsule implements OutputCapsule {
             String refID = el.getAttribute("reference_ID");
             if (refID.length() == 0) {
                 refID = object.getClass().getName() + "@" + object.hashCode();
-                el.setAttribute("reference_ID", refID);
+                XMLUtils.setAttribute(el, "reference_ID", refID);
             }
             el = appendElement(name);
-            el.setAttribute("ref", refID);
+            XMLUtils.setAttribute(el, "ref", refID);
         } else {
             el = appendElement(name);
             
@@ -549,13 +550,13 @@ public class DOMOutputCapsule implements OutputCapsule {
                     sb.append(", ");
                 }
             }
-            el.setAttribute("savable_versions", sb.toString());
+            XMLUtils.setAttribute(el, "savable_versions", sb.toString());
             
             writtenSavables.put(object, el);
             object.write(exporter);
         }
         if(className != null){
-            el.setAttribute("class", className);
+            XMLUtils.setAttribute(el, "class", className);
         }
 
         currentElement = old;
@@ -572,7 +573,7 @@ public class DOMOutputCapsule implements OutputCapsule {
 
         Element old = currentElement;
         Element el = appendElement(name);
-        el.setAttribute("size", String.valueOf(objects.length));
+        XMLUtils.setAttribute(el, "size", String.valueOf(objects.length));
         for (int i = 0; i < objects.length; i++) {
             Savable o = objects[i];
             if(o == null){
@@ -595,8 +596,8 @@ public class DOMOutputCapsule implements OutputCapsule {
         if(Arrays.deepEquals(value, defVal)) return;
 
         Element el = appendElement(name);
-        el.setAttribute("size_outer", String.valueOf(value.length));
-        el.setAttribute("size_inner", String.valueOf(value[0].length));
+        XMLUtils.setAttribute(el, "size_outer", String.valueOf(value.length));
+        XMLUtils.setAttribute(el, "size_inner", String.valueOf(value[0].length));
         for (Savable[] bs : value) {
             for(Savable b : bs){
                 write(b, b.getClass().getSimpleName(), null);
@@ -616,7 +617,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         Element old = currentElement;
         Element el = appendElement(name);
         currentElement = el;
-        el.setAttribute(XMLExporter.ATTRIBUTE_SIZE, String.valueOf(array.size()));
+        XMLUtils.setAttribute(el, XMLExporter.ATTRIBUTE_SIZE, String.valueOf(array.size()));
         for (Object o : array) {
                 if(o == null) {
                         continue;
@@ -638,7 +639,7 @@ public class DOMOutputCapsule implements OutputCapsule {
 
         Element old = currentElement;
         Element el = appendElement(name);
-        el.setAttribute(XMLExporter.ATTRIBUTE_SIZE, String.valueOf(objects.length));
+        XMLUtils.setAttribute(el, XMLExporter.ATTRIBUTE_SIZE, String.valueOf(objects.length));
         for (int i = 0; i < objects.length; i++) {
             ArrayList o = objects[i];
             if(o == null){
@@ -661,7 +662,7 @@ public class DOMOutputCapsule implements OutputCapsule {
 
         Element el = appendElement(name);
         int size = value.length;
-        el.setAttribute(XMLExporter.ATTRIBUTE_SIZE, String.valueOf(size));
+        XMLUtils.setAttribute(el, XMLExporter.ATTRIBUTE_SIZE, String.valueOf(size));
 
         for (int i=0; i< size; i++) {
             ArrayList[] vi = value[i];
@@ -679,7 +680,7 @@ public class DOMOutputCapsule implements OutputCapsule {
             return;
         }
         Element el = appendElement(name);
-        el.setAttribute(XMLExporter.ATTRIBUTE_SIZE, String.valueOf(array.size()));
+        XMLUtils.setAttribute(el, XMLExporter.ATTRIBUTE_SIZE, String.valueOf(array.size()));
         for (FloatBuffer o : array) {
             write(o, XMLExporter.ELEMENT_FLOATBUFFER, null);
         }
@@ -723,7 +724,7 @@ public class DOMOutputCapsule implements OutputCapsule {
                 while(keyIterator.hasNext()) {
                         String key = keyIterator.next();
                         Element mapEntry = appendElement("MapEntry");
-                        mapEntry.setAttribute("key", key);
+                        XMLUtils.setAttribute(mapEntry, "key", key);
                         Savable s = map.get(key);
                         write(s, "Savable", null);
                         currentElement = stringMap;
@@ -745,7 +746,7 @@ public class DOMOutputCapsule implements OutputCapsule {
                 for(Entry<? extends Savable> entry : map) {
                         int key = entry.getKey();
                         Element mapEntry = appendElement("MapEntry");
-                        mapEntry.setAttribute("key", Integer.toString(key));
+                        XMLUtils.setAttribute(mapEntry, "key", Integer.toString(key));
                         Savable s = entry.getValue();
                         write(s, "Savable", null);
                         currentElement = stringMap;
@@ -761,7 +762,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         }
 
         Element el = appendElement(name);
-        el.setAttribute("size", String.valueOf(value.limit()));
+        XMLUtils.setAttribute(el, "size", String.valueOf(value.limit()));
         StringBuilder buf = new StringBuilder();
         int pos = value.position();
         value.rewind();
@@ -784,7 +785,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         }
         
         value.position(pos);
-        el.setAttribute(dataAttributeName, buf.toString());
+        XMLUtils.setAttribute(el, dataAttributeName, buf.toString());
         currentElement = (Element) el.getParentNode();
     }
 
@@ -798,7 +799,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         }
 
         Element el = appendElement(name);
-        el.setAttribute("size", String.valueOf(value.limit()));
+        XMLUtils.setAttribute(el, "size", String.valueOf(value.limit()));
         StringBuilder buf = new StringBuilder();
         int pos = value.position();
         value.rewind();
@@ -820,7 +821,7 @@ public class DOMOutputCapsule implements OutputCapsule {
             buf.setLength(buf.length() - 1);
         }
         value.position(pos);
-        el.setAttribute(dataAttributeName, buf.toString());
+        XMLUtils.setAttribute(el, dataAttributeName, buf.toString());
         currentElement = (Element) el.getParentNode();
     }
 
@@ -830,7 +831,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         if (value.equals(defVal)) return;
 
         Element el = appendElement(name);
-        el.setAttribute("size", String.valueOf(value.limit()));
+        XMLUtils.setAttribute(el, "size", String.valueOf(value.limit()));
         StringBuilder buf = new StringBuilder();
         int pos = value.position();
         value.rewind();
@@ -853,7 +854,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         }
         
         value.position(pos);
-        el.setAttribute(dataAttributeName, buf.toString());
+        XMLUtils.setAttribute(el, dataAttributeName, buf.toString());
         currentElement = (Element) el.getParentNode();
     }
 
@@ -867,7 +868,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         }
 
         Element el = appendElement(name);
-        el.setAttribute("size", String.valueOf(value.limit()));
+        XMLUtils.setAttribute(el, "size", String.valueOf(value.limit()));
         StringBuilder buf = new StringBuilder();
         int pos = value.position();
         value.rewind();
@@ -890,7 +891,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         }
         
         value.position(pos);
-        el.setAttribute(dataAttributeName, buf.toString());
+        XMLUtils.setAttribute(el, dataAttributeName, buf.toString());
         currentElement = (Element) el.getParentNode();
     }
 
@@ -899,7 +900,7 @@ public class DOMOutputCapsule implements OutputCapsule {
         if (value == defVal) {
             return;
         }
-        currentElement.setAttribute(name, String.valueOf(value));
+        XMLUtils.setAttribute(currentElement, name, String.valueOf(value));
 
         }
 
@@ -913,11 +914,12 @@ public class DOMOutputCapsule implements OutputCapsule {
             return;
         }
         Element el = appendElement(name);
-        el.setAttribute("size", String.valueOf(array.size()));
+        XMLUtils.setAttribute(el, "size", String.valueOf(array.size()));
         for (ByteBuffer o : array) {
             write(o, "ByteBuffer", null);
         }
         currentElement = (Element) el.getParentNode();
 
         }
+    
 }

+ 110 - 0
jme3-plugins/src/xml/java/com/jme3/export/xml/XMLUtils.java

@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2009-2021 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.export.xml;
+
+import org.w3c.dom.Element;
+
+/**
+ * Utilities for reading and writing XML files.
+ * 
+ * @author codex
+ */
+public class XMLUtils {
+    
+    /**
+     * Prefix for every jme xml attribute for format versions 3 and up.
+     * <p>
+     * This prefix should be appended at the beginning of every xml
+     * attribute name. For format versions 3 and up, every name to
+     * access an attribute must append this prefix first.
+     */
+    public static final String PREFIX = "jme-";
+    
+    /**
+     * Sets the attribute in the element under the name.
+     * <p>
+     * Automatically appends {@link #PREFIX} to the beginning of the name
+     * before assigning the attribute to the element.
+     * 
+     * @param element element to set the attribute in
+     * @param name name of the attribute (without prefix)
+     * @param attribute attribute to save
+     */
+    public static void setAttribute(Element element, String name, String attribute) {
+        element.setAttribute(PREFIX+name, attribute);
+    }
+    
+    /**
+     * Fetches the named attribute from the element.
+     * <p>
+     * Automatically appends {@link #PREFIX} to the beginning
+     * of the name before looking up the attribute for format versions 3 and up.
+     * 
+     * @param version format version of the xml
+     * @param element XML element to get the attribute from
+     * @param name name of the attribute (without prefix)
+     * @return named attribute
+     */
+    public static String getAttribute(int version, Element element, String name) {
+        if (version >= 3) {
+            return element.getAttribute(PREFIX+name);
+        } else {
+            return element.getAttribute(name);
+        }
+    }
+    
+    /**
+     * Tests if the element contains the named attribute.
+     * <p>
+     * Automatically appends {@link #PREFIX} to the beginning
+     * of the name before looking up the attribute for format versions 3 and up.
+     * 
+     * @param version format version of the xml
+     * @param element element to test
+     * @param name name of the attribute (without prefix)
+     * @return true if the element has the named attribute
+     */
+    public static boolean hasAttribute(int version, Element element, String name) {
+        if (version >= 3) {
+            return element.hasAttribute(PREFIX+name);
+        } else {
+            return element.hasAttribute(name);
+        }
+    }
+    
+    /**
+     * Denies instantiation of this class.
+     */
+    private XMLUtils() {
+    }
+    
+}