Explorar el Código

Revert "merging new files."

This reverts commit 1bda3f0cd20e6d052c9ee3d508e644221b53adc8.
KEVIN-DESKTOP\kevinba hace 1 año
padre
commit
d8d827b73d
Se han modificado 100 ficheros con 15266 adiciones y 0 borrados
  1. 66 0
      jme3-core/src/main/java/com/jme3/system/MonitorInfo.java
  2. 105 0
      jme3-core/src/main/java/com/jme3/system/Monitors.java
  3. 54 0
      jme3-examples/src/main/java/jme3test/app/state/RootNodeState.java
  4. 107 0
      jme3-examples/src/main/java/jme3test/app/state/TestAppStates.java
  5. 35 0
      jme3-examples/src/main/java/jme3test/app/state/package-info.java
  6. 230 0
      jme3-examples/src/main/java/jme3test/asset/TestAssetCache.java
  7. 82 0
      jme3-examples/src/main/java/jme3test/asset/TestOnlineJar.java
  8. 80 0
      jme3-examples/src/main/java/jme3test/asset/TestUrlLoading.java
  9. 35 0
      jme3-examples/src/main/java/jme3test/asset/package-info.java
  10. 88 0
      jme3-examples/src/main/java/jme3test/audio/TestAmbient.java
  11. 94 0
      jme3-examples/src/main/java/jme3test/audio/TestDoppler.java
  12. 86 0
      jme3-examples/src/main/java/jme3test/audio/TestIssue1761.java
  13. 117 0
      jme3-examples/src/main/java/jme3test/audio/TestMusicPlayer.form
  14. 311 0
      jme3-examples/src/main/java/jme3test/audio/TestMusicPlayer.java
  15. 60 0
      jme3-examples/src/main/java/jme3test/audio/TestMusicStreaming.java
  16. 70 0
      jme3-examples/src/main/java/jme3test/audio/TestOgg.java
  17. 84 0
      jme3-examples/src/main/java/jme3test/audio/TestReverb.java
  18. 64 0
      jme3-examples/src/main/java/jme3test/audio/TestWav.java
  19. 35 0
      jme3-examples/src/main/java/jme3test/audio/package-info.java
  20. 155 0
      jme3-examples/src/main/java/jme3test/awt/AppHarness.java
  21. 151 0
      jme3-examples/src/main/java/jme3test/awt/TestApplet.java
  22. 152 0
      jme3-examples/src/main/java/jme3test/awt/TestAwtPanels.java
  23. 283 0
      jme3-examples/src/main/java/jme3test/awt/TestCanvas.java
  24. 68 0
      jme3-examples/src/main/java/jme3test/awt/TestSafeCanvas.java
  25. 36 0
      jme3-examples/src/main/java/jme3test/awt/package-info.java
  26. 162 0
      jme3-examples/src/main/java/jme3test/batching/TestBatchNode.java
  27. 362 0
      jme3-examples/src/main/java/jme3test/batching/TestBatchNodeCluster.java
  28. 250 0
      jme3-examples/src/main/java/jme3test/batching/TestBatchNodeTower.java
  29. 36 0
      jme3-examples/src/main/java/jme3test/batching/package-info.java
  30. 65 0
      jme3-examples/src/main/java/jme3test/bounding/TestRayCollision.java
  31. 35 0
      jme3-examples/src/main/java/jme3test/bounding/package-info.java
  32. 221 0
      jme3-examples/src/main/java/jme3test/bullet/BombControl.java
  33. 258 0
      jme3-examples/src/main/java/jme3test/bullet/PhysicsHoverControl.java
  34. 371 0
      jme3-examples/src/main/java/jme3test/bullet/PhysicsTestHelper.java
  35. 300 0
      jme3-examples/src/main/java/jme3test/bullet/TestAttachDriver.java
  36. 129 0
      jme3-examples/src/main/java/jme3test/bullet/TestAttachGhostObject.java
  37. 297 0
      jme3-examples/src/main/java/jme3test/bullet/TestBetterCharacter.java
  38. 244 0
      jme3-examples/src/main/java/jme3test/bullet/TestBoneRagdoll.java
  39. 251 0
      jme3-examples/src/main/java/jme3test/bullet/TestBrickTower.java
  40. 204 0
      jme3-examples/src/main/java/jme3test/bullet/TestBrickWall.java
  41. 153 0
      jme3-examples/src/main/java/jme3test/bullet/TestCcd.java
  42. 107 0
      jme3-examples/src/main/java/jme3test/bullet/TestCollisionGroups.java
  43. 95 0
      jme3-examples/src/main/java/jme3test/bullet/TestCollisionListener.java
  44. 137 0
      jme3-examples/src/main/java/jme3test/bullet/TestCollisionShapeFactory.java
  45. 226 0
      jme3-examples/src/main/java/jme3test/bullet/TestFancyCar.java
  46. 117 0
      jme3-examples/src/main/java/jme3test/bullet/TestGhostObject.java
  47. 304 0
      jme3-examples/src/main/java/jme3test/bullet/TestHoveringTank.java
  48. 214 0
      jme3-examples/src/main/java/jme3test/bullet/TestIssue1120.java
  49. 229 0
      jme3-examples/src/main/java/jme3test/bullet/TestIssue1125.java
  50. 148 0
      jme3-examples/src/main/java/jme3test/bullet/TestIssue877.java
  51. 87 0
      jme3-examples/src/main/java/jme3test/bullet/TestIssue883.java
  52. 107 0
      jme3-examples/src/main/java/jme3test/bullet/TestKinematicAddToPhysicsSpaceIssue.java
  53. 122 0
      jme3-examples/src/main/java/jme3test/bullet/TestLocalPhysics.java
  54. 225 0
      jme3-examples/src/main/java/jme3test/bullet/TestPhysicsCar.java
  55. 212 0
      jme3-examples/src/main/java/jme3test/bullet/TestPhysicsCharacter.java
  56. 110 0
      jme3-examples/src/main/java/jme3test/bullet/TestPhysicsHingeJoint.java
  57. 69 0
      jme3-examples/src/main/java/jme3test/bullet/TestPhysicsRayCast.java
  58. 153 0
      jme3-examples/src/main/java/jme3test/bullet/TestPhysicsReadWrite.java
  59. 183 0
      jme3-examples/src/main/java/jme3test/bullet/TestQ3.java
  60. 152 0
      jme3-examples/src/main/java/jme3test/bullet/TestRagDoll.java
  61. 246 0
      jme3-examples/src/main/java/jme3test/bullet/TestRagdollCharacter.java
  62. 111 0
      jme3-examples/src/main/java/jme3test/bullet/TestSimplePhysics.java
  63. 77 0
      jme3-examples/src/main/java/jme3test/bullet/TestSweepTest.java
  64. 460 0
      jme3-examples/src/main/java/jme3test/bullet/TestWalkingChar.java
  65. 35 0
      jme3-examples/src/main/java/jme3test/bullet/package-info.java
  66. 342 0
      jme3-examples/src/main/java/jme3test/bullet/shape/TestGimpactShape.java
  67. 36 0
      jme3-examples/src/main/java/jme3test/bullet/shape/package-info.java
  68. 101 0
      jme3-examples/src/main/java/jme3test/collision/RayTrace.java
  69. 153 0
      jme3-examples/src/main/java/jme3test/collision/TestMousePick.java
  70. 95 0
      jme3-examples/src/main/java/jme3test/collision/TestRayCasting.java
  71. 127 0
      jme3-examples/src/main/java/jme3test/collision/TestTriangleCollision.java
  72. 36 0
      jme3-examples/src/main/java/jme3test/collision/package-info.java
  73. 93 0
      jme3-examples/src/main/java/jme3test/conversion/TestMipMapGen.java
  74. 35 0
      jme3-examples/src/main/java/jme3test/conversion/package-info.java
  75. 201 0
      jme3-examples/src/main/java/jme3test/effect/TestEverything.java
  76. 282 0
      jme3-examples/src/main/java/jme3test/effect/TestExplosionEffect.java
  77. 303 0
      jme3-examples/src/main/java/jme3test/effect/TestIssue1773.java
  78. 96 0
      jme3-examples/src/main/java/jme3test/effect/TestMovingParticle.java
  79. 74 0
      jme3-examples/src/main/java/jme3test/effect/TestParticleExportingCloning.java
  80. 91 0
      jme3-examples/src/main/java/jme3test/effect/TestPointSprite.java
  81. 184 0
      jme3-examples/src/main/java/jme3test/effect/TestSoftParticles.java
  82. 35 0
      jme3-examples/src/main/java/jme3test/effect/package-info.java
  83. 131 0
      jme3-examples/src/main/java/jme3test/export/TestAssetLinkNode.java
  84. 96 0
      jme3-examples/src/main/java/jme3test/export/TestIssue2068.java
  85. 81 0
      jme3-examples/src/main/java/jme3test/export/TestOgreConvert.java
  86. 35 0
      jme3-examples/src/main/java/jme3test/export/package-info.java
  87. 414 0
      jme3-examples/src/main/java/jme3test/games/CubeField.java
  88. 411 0
      jme3-examples/src/main/java/jme3test/games/RollingTheMonkey.java
  89. 535 0
      jme3-examples/src/main/java/jme3test/games/WorldOfInception.java
  90. 35 0
      jme3-examples/src/main/java/jme3test/games/package-info.java
  91. 134 0
      jme3-examples/src/main/java/jme3test/gui/TestBitmapFont.java
  92. 144 0
      jme3-examples/src/main/java/jme3test/gui/TestBitmapFontAlignment.java
  93. 543 0
      jme3-examples/src/main/java/jme3test/gui/TestBitmapFontLayout.java
  94. 70 0
      jme3-examples/src/main/java/jme3test/gui/TestBitmapText3D.java
  95. 77 0
      jme3-examples/src/main/java/jme3test/gui/TestCursor.java
  96. 58 0
      jme3-examples/src/main/java/jme3test/gui/TestOrtho.java
  97. 70 0
      jme3-examples/src/main/java/jme3test/gui/TestRtlBitmapText.java
  98. 136 0
      jme3-examples/src/main/java/jme3test/gui/TestSoftwareMouse.java
  99. 64 0
      jme3-examples/src/main/java/jme3test/gui/TestZOrder.java
  100. 36 0
      jme3-examples/src/main/java/jme3test/gui/package-info.java

+ 66 - 0
jme3-core/src/main/java/com/jme3/system/MonitorInfo.java

@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2009-2023 jMonkeyEngine All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted
+ * provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice, this list of
+ * conditions and the following disclaimer in the documentation and/or other materials provided with
+ * the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors may be used to endorse or
+ * promote products derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
+ * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.system;
+
+/**
+ * This class holds information about the monitor that was returned by glfwGetMonitors() calls in
+ * the context class
+ * 
+ * @author Kevin Bales
+ */
+public class MonitorInfo {
+
+  /**
+   * monitorID - monitor id that was return from Lwjgl3.
+   */
+  public long monitorID = 0;
+
+  /**
+   * width - width that was return from Lwjgl3.
+   */
+  public int width = 1080;
+
+  /**
+   * height - height that was return from Lwjgl3.
+   */
+  public int height = 1920;
+
+  /**
+   * rate - refresh rate that was return from Lwjgl3.
+   */
+  public int rate = 60;
+
+  /**
+   * primary - indicates if the monitor is the primary monitor.
+   */
+  public boolean primary = false;
+
+  /**
+   * name - monitor name that was return from Lwjgl3.
+   */
+  public String name = "Generic Monitor";
+
+}

+ 105 - 0
jme3-core/src/main/java/com/jme3/system/Monitors.java

@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2009-2023 jMonkeyEngine All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted
+ * provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice, this list of
+ * conditions and the following disclaimer in the documentation and/or other materials provided with
+ * the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors may be used to endorse or
+ * promote products derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
+ * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.system;
+
+import java.util.ArrayList;
+
+/**
+ * This class holds all information about all monitors that where return from the glfwGetMonitors()
+ * call. It stores them into an <ArrayList>
+ * 
+ * @author Kevin Bales
+ */
+public class Monitors {
+
+  private ArrayList<MonitorInfo> monitors = new ArrayList<MonitorInfo>();
+
+  public int addNewMonitor(long monitorID) {
+    MonitorInfo info = new MonitorInfo();
+    info.monitorID = monitorID;
+    monitors.add(info);
+    return monitors.size() - 1;
+  }
+
+  /**
+   * This function returns the size of the monitor ArrayList
+   * 
+   * @return the
+   */
+  public int size() {
+    return monitors.size();
+  }
+
+  /**
+   * Call to get monitor information on a certain monitor.
+   * 
+   * @param pos the position in the arraylist of the monitor information that you want to get.
+   * @return returns the MonitorInfo data for the monitor called for.
+   */
+  public MonitorInfo get(int pos) {
+    if (pos < monitors.size())
+      return monitors.get(pos);
+
+    return null;
+  }
+
+  /**
+   * Set information about this monitor stored in monPos position in the array list.
+   * 
+   * @param monPos arraylist position of monitor to update
+   * @param width the current width the monitor is displaying
+   * @param height the current height the monitor is displaying
+   * @param rate the current refresh rate the monitor is set to
+   */
+  public void setInfo(int monPos, String name, int width, int height, int rate) {
+    if (monPos < monitors.size()) {
+      MonitorInfo info = monitors.get(monPos);
+      if (info != null) {
+        info.width = width;
+        info.height = height;
+        info.rate = rate;
+        info.name = name;
+      }
+    }
+  }
+
+  /**
+   * This function will mark a certain monitor as the primary monitor.
+   * 
+   * @param monPos the position in the arraylist of which monitor is the primary monitor
+   */
+  public void setPrimaryMonitor(int monPos) {
+    if (monPos < monitors.size()) {
+      MonitorInfo info = monitors.get(monPos);
+      if (info != null)
+        info.primary = true;
+    }
+
+  }
+
+
+
+}

+ 54 - 0
jme3-examples/src/main/java/jme3test/app/state/RootNodeState.java

@@ -0,0 +1,54 @@
+/*
+ * 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 jme3test.app.state;
+
+import com.jme3.app.state.AbstractAppState;
+import com.jme3.scene.Node;
+
+public class RootNodeState extends AbstractAppState {
+
+    final private Node rootNode = new Node("Root Node");
+
+    public Node getRootNode(){
+        return rootNode;
+    }
+
+    @Override
+    public void update(float tpf) {
+        super.update(tpf);
+
+        rootNode.updateLogicalState(tpf);
+        rootNode.updateGeometricState();
+    }
+
+}

+ 107 - 0
jme3-examples/src/main/java/jme3test/app/state/TestAppStates.java

@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2009-2022 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.app.state;
+
+import com.jme3.app.LegacyApplication;
+import com.jme3.niftygui.NiftyJmeDisplay;
+import com.jme3.scene.Spatial;
+import com.jme3.system.AppSettings;
+import com.jme3.system.JmeContext;
+import com.jme3.texture.image.ColorSpace;
+import jme3test.niftygui.StartScreenController;
+
+public class TestAppStates extends LegacyApplication {
+
+    public static void main(String[] args){
+        TestAppStates app = new TestAppStates();
+        app.start();
+    }
+
+    @Override
+    public void start(JmeContext.Type contextType){
+        AppSettings settings = new AppSettings(true);
+        settings.setResolution(1024, 768);
+        setSettings(settings);
+
+        super.start(contextType);
+    }
+
+    @Override
+    public void initialize(){
+        super.initialize();
+
+        System.out.println("Initialize");
+
+        RootNodeState state = new RootNodeState();
+        viewPort.attachScene(state.getRootNode());
+        stateManager.attach(state);
+
+        Spatial model = assetManager.loadModel("Models/Teapot/Teapot.obj");
+        model.scale(3);
+        model.setMaterial(assetManager.loadMaterial("Interface/Logo/Logo.j3m"));
+        state.getRootNode().attachChild(model);
+
+        ColorSpace colorSpace = renderer.isMainFrameBufferSrgb()
+                ? ColorSpace.sRGB : ColorSpace.Linear;
+        NiftyJmeDisplay niftyDisplay = new NiftyJmeDisplay(assetManager,
+                inputManager,
+                audioRenderer,
+                guiViewPort,
+                colorSpace);
+        StartScreenController startScreen = new StartScreenController(this);
+        niftyDisplay.getNifty().fromXml("Interface/Nifty/HelloJme.xml", "start",
+                startScreen);
+        guiViewPort.addProcessor(niftyDisplay);
+    }
+
+    @Override
+    public void update(){
+        super.update();
+
+        // do some animation
+        float tpf = timer.getTimePerFrame();
+
+        stateManager.update(tpf);
+        stateManager.render(renderManager);
+
+        // render the viewports
+        renderManager.render(tpf, context.isRenderable());
+    }
+
+    @Override
+    public void destroy(){
+        super.destroy();
+
+        System.out.println("Destroy");
+    }
+}

+ 35 - 0
jme3-examples/src/main/java/jme3test/app/state/package-info.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 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.
+ */
+/**
+ * example apps and non-automated tests for appstates
+ */
+package jme3test.app.state;

+ 230 - 0
jme3-examples/src/main/java/jme3test/asset/TestAssetCache.java

@@ -0,0 +1,230 @@
+/*
+ * 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 jme3test.asset;
+
+import com.jme3.asset.AssetKey;
+import com.jme3.asset.AssetProcessor;
+import com.jme3.asset.CloneableAssetProcessor;
+import com.jme3.asset.CloneableSmartAsset;
+import com.jme3.asset.cache.AssetCache;
+import com.jme3.asset.cache.SimpleAssetCache;
+import com.jme3.asset.cache.WeakRefAssetCache;
+import com.jme3.asset.cache.WeakRefCloneAssetCache;
+import java.util.ArrayList;
+import java.util.List;
+
+public class TestAssetCache {
+   
+    /**
+     * Counter for asset keys
+     */
+    private static int counter = 0;
+    
+    /**
+     * Dummy data is an asset having 10 KB to put a dent in the garbage collector
+     */
+    private static class DummyData implements CloneableSmartAsset {
+
+        private AssetKey key;
+        private byte[] data = new byte[10 * 1024];
+
+        @Override
+        public DummyData clone(){
+            try {
+                DummyData clone = (DummyData) super.clone();
+                clone.data = data.clone();
+                return clone;
+            } catch (CloneNotSupportedException ex) {
+                throw new AssertionError();
+            }
+        }
+        
+        public byte[] getData(){
+            return data;
+        }
+        
+        @Override
+        public AssetKey getKey() {
+            return key;
+        }
+
+        @Override
+        public void setKey(AssetKey key) {
+            this.key = key;
+        }
+    }
+    
+    /**
+     * Dummy key is indexed by a generated ID
+     */
+    private static class DummyKey extends AssetKey<DummyData> implements Cloneable {
+        
+        private int id = 0;
+        
+        public DummyKey(){
+            super(".");
+            id = counter++;
+        }
+        
+        public DummyKey(int id){
+            super(".");
+            this.id = id;
+        }
+        
+        @Override
+        public int hashCode(){
+            return id;
+        }
+        
+        @Override
+        public boolean equals(Object other){
+            return ((DummyKey)other).id == id;
+        }
+        
+        @Override
+        public DummyKey clone(){
+            return new DummyKey(id);
+        }
+        
+        @Override
+        public String toString() {
+            return "ID=" + id;
+        }
+    }
+    
+    private static void runTest(boolean cloneAssets, boolean smartCache, boolean keepRefs, int limit) {
+        counter = 0;
+        List<Object> refs = new ArrayList<>(limit);
+        
+        AssetCache cache;
+        AssetProcessor proc = null;
+        
+        if (cloneAssets) {
+            proc = new CloneableAssetProcessor();
+        }
+        
+        if (smartCache) {
+            if (cloneAssets) {
+                cache = new WeakRefCloneAssetCache();
+            } else {
+                cache = new WeakRefAssetCache();
+            }
+        } else {
+            cache = new SimpleAssetCache();
+        }
+        
+        System.gc();
+        System.gc();
+        System.gc();
+        System.gc();
+        
+        long memory = Runtime.getRuntime().freeMemory();
+        
+        while (counter < limit){
+            // Create a key
+            DummyKey key = new DummyKey();
+            
+            // Create some data
+            DummyData data = new DummyData();
+            
+            // Postprocess the data before placing it in the cache.
+            if (proc != null){
+                data = (DummyData) proc.postProcess(key, data);
+            }
+            
+            if (data.key != null){
+                // Keeping a hard reference to the key in the cache
+                // means the asset will never be collected => bug
+                throw new AssertionError();
+            }
+            
+            cache.addToCache(key, data);
+            
+            // Get the asset from the cache
+            AssetKey<DummyData> keyToGet = key.clone();
+            
+            // NOTE: Commented out because getFromCache leaks the original key
+//            DummyData someLoaded = (DummyData) cache.getFromCache(keyToGet);
+//            if (someLoaded != data){
+//                // Failed to get the same asset from the cache => bug
+//                // Since a hard reference to the key is kept, 
+//                // it cannot be collected at this point.
+//                throw new AssertionError();
+//            }
+            
+            // Clone the asset
+            if (proc != null){
+                // Data is now the clone!
+                data = (DummyData) proc.createClone(data);
+                if (smartCache) {
+                    // Registering a clone is only needed
+                    // if smart cache is used.
+                    cache.registerAssetClone(keyToGet, data);
+                    // The clone of the asset must have the same key as the original
+                    // otherwise => bug
+                    if (data.key != key){
+                        throw new AssertionError();
+                    }
+                }
+            }
+            
+            // Keep references to the asset => *should* prevent
+            // collections of the asset in the cache thus causing
+            // an out of memory error.
+            if (keepRefs){
+                // Prevent the saved references from taking too much memory.
+                if (cloneAssets) {
+                    data.data = null;
+                }
+                refs.add(data);
+            }
+            
+            if ((counter % 1000) == 0){
+                long newMem = Runtime.getRuntime().freeMemory();
+                System.out.println("Allocated objects: " + counter);
+                System.out.println("Allocated memory: " + ((memory - newMem)/(1024*1024)) + " MB" );
+                memory = newMem;
+            }
+        }
+    }
+    
+    public static void main(String[] args){
+        // Test cloneable smart asset
+        System.out.println("====== Running Cloneable Smart Asset Test ======");
+        runTest(true, true, false, 100000);
+        
+        // Test non-cloneable smart asset
+        System.out.println("====== Running Non-cloneable Smart Asset Test ======");
+        runTest(false, true, false, 100000);
+    }
+}

+ 82 - 0
jme3-examples/src/main/java/jme3test/asset/TestOnlineJar.java

@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package jme3test.asset;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.asset.plugins.HttpZipLocator;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Quad;
+import com.jme3.ui.Picture;
+
+/**
+ * This tests loading a file from a jar stored online.
+ * @author Kirill Vainer
+ */
+public class TestOnlineJar extends SimpleApplication {
+
+    public static void main(String[] args){
+        TestOnlineJar app = new TestOnlineJar();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        // create a simple plane/quad
+        Quad quadMesh = new Quad(1, 1);
+        quadMesh.updateGeometry(1, 1, true);
+
+        Geometry quad = new Geometry("Textured Quad", quadMesh);
+        
+        assetManager.registerLocator("https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/jmonkeyengine/town.zip", 
+                                     HttpZipLocator.class);
+        assetManager.registerLocator("https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/jmonkeyengine/wildhouse.zip", 
+                                     HttpZipLocator.class);
+
+        Picture pic1 = new Picture("Picture1");
+        pic1.move(0, 0, -1); 
+        pic1.setPosition(0, 0);
+        pic1.setWidth(128);
+        pic1.setHeight(128);
+        pic1.setImage(assetManager, "grass.jpg", false);
+        guiNode.attachChild(pic1);
+        
+        Picture pic2 = new Picture("Picture1");
+        pic2.move(0, 0, -1); 
+        pic2.setPosition(128, 0);
+        pic2.setWidth(128);
+        pic2.setHeight(128);
+        pic2.setImage(assetManager, "glasstile2.png", false);
+        guiNode.attachChild(pic2);
+    }
+
+}

+ 80 - 0
jme3-examples/src/main/java/jme3test/asset/TestUrlLoading.java

@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package jme3test.asset;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.asset.TextureKey;
+import com.jme3.asset.plugins.UrlLocator;
+import com.jme3.material.Material;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Quad;
+import com.jme3.texture.Texture;
+
+/**
+ * Load an image and display it from the internet using the UrlLocator.
+ * @author Kirill Vainer
+ */
+public class TestUrlLoading extends SimpleApplication {
+
+    public static void main(String[] args){
+        TestUrlLoading app = new TestUrlLoading();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        // create a simple plane/quad
+        Quad quadMesh = new Quad(1, 1);
+        quadMesh.updateGeometry(1, 1, true);
+
+        Geometry quad = new Geometry("Textured Quad", quadMesh);
+
+        assetManager.registerLocator("https://raw.githubusercontent.com/jMonkeyEngine/BookSamples/master/assets/Textures/",
+                                UrlLocator.class);
+        TextureKey key = new TextureKey("mucha-window.png", false);
+        key.setGenerateMips(true);
+        Texture tex = assetManager.loadTexture(key);
+
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat.setTexture("ColorMap", tex);
+        quad.setMaterial(mat);
+
+        float aspect = tex.getImage().getWidth() / (float) tex.getImage().getHeight();
+        quad.setLocalScale(new Vector3f(aspect * 1.5f, 1.5f, 1));
+        quad.center();
+
+        rootNode.attachChild(quad);
+    }
+
+}

+ 35 - 0
jme3-examples/src/main/java/jme3test/asset/package-info.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 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.
+ */
+/**
+ * example apps and non-automated tests for asset loading
+ */
+package jme3test.asset;

+ 88 - 0
jme3-examples/src/main/java/jme3test/audio/TestAmbient.java

@@ -0,0 +1,88 @@
+/*
+ * 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 jme3test.audio;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.audio.AudioData.DataType;
+import com.jme3.audio.AudioNode;
+import com.jme3.audio.Environment;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Box;
+
+public class TestAmbient extends SimpleApplication {
+
+  public static void main(String[] args) {
+    TestAmbient test = new TestAmbient();
+    test.start();
+  }
+
+  @Override
+  public void simpleInitApp() {
+    float[] eax = new float[]{15, 38.0f, 0.300f, -1000, -3300, 0,
+      1.49f, 0.54f, 1.00f, -2560, 0.162f, 0.00f, 0.00f,
+      0.00f, -229, 0.088f, 0.00f, 0.00f, 0.00f, 0.125f, 1.000f,
+      0.250f, 0.000f, -5.0f, 5000.0f, 250.0f, 0.00f, 0x3f};
+    Environment env = new Environment(eax);
+    audioRenderer.setEnvironment(env);
+
+    AudioNode waves = new AudioNode(assetManager,
+            "Sound/Environment/Ocean Waves.ogg", DataType.Buffer);
+    waves.setPositional(true);
+    waves.setLocalTranslation(new Vector3f(0, 0,0));
+    waves.setMaxDistance(100);
+    waves.setRefDistance(5);
+
+    AudioNode nature = new AudioNode(assetManager,
+            "Sound/Environment/Nature.ogg", DataType.Stream);
+    nature.setPositional(false);
+    nature.setVolume(3);
+    
+    waves.playInstance();
+    nature.play();
+    
+    // just a blue box to mark the spot
+    Box box1 = new Box(.5f, .5f, .5f);
+    Geometry player = new Geometry("Player", box1);
+    Material mat1 = new Material(assetManager,
+            "Common/MatDefs/Misc/Unshaded.j3md");
+    mat1.setColor("Color", ColorRGBA.Blue);
+    player.setMaterial(mat1);
+    rootNode.attachChild(player);
+  }
+
+  @Override
+  public void simpleUpdate(float tpf) {
+  }
+}

+ 94 - 0
jme3-examples/src/main/java/jme3test/audio/TestDoppler.java

@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package jme3test.audio;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.audio.AudioData;
+import com.jme3.audio.AudioNode;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Sphere;
+import com.jme3.scene.shape.Torus;
+
+/**
+ * Test Doppler Effect
+ */
+public class TestDoppler extends SimpleApplication {
+
+    private float pos = -5;
+    private float vel = 5;
+    private AudioNode ufoNode;
+
+    public static void main(String[] args){
+        TestDoppler test = new TestDoppler();
+        test.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        flyCam.setMoveSpeed(10);
+
+        Torus torus = new Torus(10, 6, 1, 3);
+        Geometry g = new Geometry("Torus Geom", torus);
+        g.rotate(-FastMath.HALF_PI, 0, 0);
+        g.center();
+
+        g.setMaterial(assetManager.loadMaterial("Common/Materials/RedColor.j3m"));
+//        rootNode.attachChild(g);
+
+        ufoNode = new AudioNode(assetManager, "Sound/Effects/Beep.ogg", AudioData.DataType.Buffer);
+        ufoNode.setLooping(true);
+        ufoNode.setPitch(0.5f);
+        ufoNode.setRefDistance(1);
+        ufoNode.setMaxDistance(100000000);
+        ufoNode.setVelocityFromTranslation(true);
+        ufoNode.play();
+
+        Geometry ball = new Geometry("Beeper", new Sphere(10, 10, 0.1f));
+        ball.setMaterial(assetManager.loadMaterial("Common/Materials/RedColor.j3m"));
+        ufoNode.attachChild(ball);
+
+        rootNode.attachChild(ufoNode);
+    }
+
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        pos += tpf * vel;
+        if (pos < -10 || pos > 10) {
+            vel *= -1;
+        }
+        ufoNode.setLocalTranslation(new Vector3f(pos, 0, 0));
+    }
+}

+ 86 - 0
jme3-examples/src/main/java/jme3test/audio/TestIssue1761.java

@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2009-2022 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.audio;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.asset.plugins.UrlLocator;
+import com.jme3.audio.AudioData;
+import com.jme3.audio.AudioNode;
+import java.util.Random;
+
+/**
+ * Stress test to reproduce JME issue #1761 (AssertionError in ALAudioRenderer).
+ *
+ * <p>After some network delay, a song will play,
+ * albeit slowly and in a broken fashion.
+ * If the issue is solved, the song will play all the way through.
+ * If the issue is present, an AssertionError will be thrown, usually within a
+ * second of the song starting.
+ */
+public class TestIssue1761 extends SimpleApplication {
+
+    private AudioNode audioNode;
+    final private Random random = new Random();
+
+    /**
+     * Main entry point for the TestIssue1761 application.
+     *
+     * @param unused array of command-line arguments
+     */
+    public static void main(String[] unused) {
+        TestIssue1761 test = new TestIssue1761();
+        test.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        assetManager.registerLocator(
+                "https://web.archive.org/web/20170625151521if_/http://www.vorbis.com/music/",
+                UrlLocator.class);
+        audioNode = new AudioNode(assetManager, "Lumme-Badloop.ogg",
+                AudioData.DataType.Stream);
+        audioNode.setPositional(false);
+        audioNode.play();
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        /*
+         * Randomly pause and restart the audio.
+         */
+        if (random.nextInt(2) == 0) {
+            audioNode.pause();
+        } else {
+            audioNode.play();
+        }
+    }
+}

+ 117 - 0
jme3-examples/src/main/java/jme3test/audio/TestMusicPlayer.form

@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<Form version="1.3" maxVersion="1.7" type="org.netbeans.modules.form.forminfo.JFrameFormInfo">
+  <Properties>
+    <Property name="defaultCloseOperation" type="int" value="3"/>
+  </Properties>
+  <SyntheticProperties>
+    <SyntheticProperty name="formSizePolicy" type="int" value="1"/>
+  </SyntheticProperties>
+  <Events>
+    <EventHandler event="windowClosing" listener="java.awt.event.WindowListener" parameters="java.awt.event.WindowEvent" handler="formWindowClosing"/>
+  </Events>
+  <AuxValues>
+    <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/>
+    <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
+    <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
+    <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,0,82,0,0,1,-112"/>
+  </AuxValues>
+
+  <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/>
+  <SubComponents>
+    <Container class="javax.swing.JPanel" name="pnlButtons">
+      <Constraints>
+        <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription">
+          <BorderConstraints direction="Center"/>
+        </Constraint>
+      </Constraints>
+
+      <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBoxLayout"/>
+      <SubComponents>
+        <Component class="javax.swing.JSlider" name="sldVolume">
+          <Properties>
+            <Property name="majorTickSpacing" type="int" value="20"/>
+            <Property name="orientation" type="int" value="1"/>
+            <Property name="paintTicks" type="boolean" value="true"/>
+            <Property name="value" type="int" value="100"/>
+          </Properties>
+          <Events>
+            <EventHandler event="stateChanged" listener="javax.swing.event.ChangeListener" parameters="javax.swing.event.ChangeEvent" handler="sldVolumeStateChanged"/>
+          </Events>
+        </Component>
+        <Component class="javax.swing.JButton" name="btnRewind">
+          <Properties>
+            <Property name="text" type="java.lang.String" value="&lt;&lt;"/>
+          </Properties>
+        </Component>
+        <Component class="javax.swing.JButton" name="btnStop">
+          <Properties>
+            <Property name="text" type="java.lang.String" value="[  ]"/>
+          </Properties>
+          <Events>
+            <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="btnStopActionPerformed"/>
+          </Events>
+        </Component>
+        <Component class="javax.swing.JButton" name="btnPlay">
+          <Properties>
+            <Property name="text" type="java.lang.String" value="II / &gt;"/>
+          </Properties>
+          <Events>
+            <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="btnPlayActionPerformed"/>
+          </Events>
+        </Component>
+        <Component class="javax.swing.JButton" name="btnFF">
+          <Properties>
+            <Property name="text" type="java.lang.String" value="&gt;&gt;"/>
+          </Properties>
+          <Events>
+            <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="btnFFActionPerformed"/>
+          </Events>
+        </Component>
+        <Component class="javax.swing.JButton" name="btnOpen">
+          <Properties>
+            <Property name="text" type="java.lang.String" value="Open ..."/>
+          </Properties>
+          <Events>
+            <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="btnOpenActionPerformed"/>
+          </Events>
+        </Component>
+      </SubComponents>
+    </Container>
+    <Container class="javax.swing.JPanel" name="pnlBar">
+      <Constraints>
+        <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription">
+          <BorderConstraints direction="First"/>
+        </Constraint>
+      </Constraints>
+
+      <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBoxLayout"/>
+      <SubComponents>
+        <Component class="javax.swing.JLabel" name="lblTime">
+          <Properties>
+            <Property name="text" type="java.lang.String" value="0:00-0:00"/>
+            <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor">
+              <Border info="org.netbeans.modules.form.compat2.border.EmptyBorderInfo">
+                <EmptyBorder bottom="3" left="3" right="3" top="3"/>
+              </Border>
+            </Property>
+          </Properties>
+        </Component>
+        <Component class="javax.swing.JSlider" name="sldBar">
+          <Properties>
+            <Property name="value" type="int" value="0"/>
+          </Properties>
+          <Events>
+            <EventHandler event="stateChanged" listener="javax.swing.event.ChangeListener" parameters="javax.swing.event.ChangeEvent" handler="sldBarStateChanged"/>
+          </Events>
+        </Component>
+      </SubComponents>
+    </Container>
+  </SubComponents>
+</Form>

+ 311 - 0
jme3-examples/src/main/java/jme3test/audio/TestMusicPlayer.java

@@ -0,0 +1,311 @@
+/*
+ * 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.audio;
+
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetLoader;
+import com.jme3.audio.*;
+import com.jme3.audio.AudioSource.Status;
+import com.jme3.audio.plugins.OGGLoader;
+import com.jme3.audio.plugins.WAVLoader;
+import com.jme3.system.AppSettings;
+import com.jme3.system.JmeSystem;
+import com.jme3.system.NativeLibraries;
+import com.jme3.system.NativeLibraryLoader;
+
+import java.io.*;
+import javax.swing.JFileChooser;
+
+public class TestMusicPlayer extends javax.swing.JFrame {
+
+    private AudioRenderer ar;
+    private AudioData musicData;
+    private AudioNode musicSource;
+    private float musicLength = 0;
+    private float curTime = 0;
+    final private Listener listener = new Listener();
+
+    static {
+        // Load lwjgl and openal natives if lwjgl version 2 is in classpath.
+        //
+        // In case of lwjgl 2, natives are loaded when LwjglContext is
+        // started, but in this test we do not create a LwjglContext,
+        // so we should handle loading natives ourselves if running
+        // with lwjgl 2.
+        NativeLibraryLoader.loadNativeLibrary(NativeLibraries.Lwjgl.getName(), false);
+        NativeLibraryLoader.loadNativeLibrary(NativeLibraries.OpenAL.getName(), false);
+    }
+
+    public TestMusicPlayer() {
+        initComponents();
+        setLocationRelativeTo(null);
+        initAudioPlayer();
+    }
+
+    private void initAudioPlayer(){
+        AppSettings settings = new AppSettings(true);
+        settings.setRenderer(null); // disable rendering
+        settings.setAudioRenderer("LWJGL");
+        ar = JmeSystem.newAudioRenderer(settings);
+        ar.initialize();
+        ar.setListener(listener);
+        AudioContext.setAudioRenderer(ar);
+    }
+
+    /** This method is called from within the constructor to
+     * initialize the form.
+     * WARNING: Do NOT modify this code. The content of this method is
+     * always regenerated by the Form Editor.
+     */
+    @SuppressWarnings("unchecked")
+    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
+    private void initComponents() {
+
+        pnlButtons = new javax.swing.JPanel();
+        sldVolume = new javax.swing.JSlider();
+        btnRewind = new javax.swing.JButton();
+        btnStop = new javax.swing.JButton();
+        btnPlay = new javax.swing.JButton();
+        btnFF = new javax.swing.JButton();
+        btnOpen = new javax.swing.JButton();
+        pnlBar = new javax.swing.JPanel();
+        lblTime = new javax.swing.JLabel();
+        sldBar = new javax.swing.JSlider();
+
+        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
+        addWindowListener(new java.awt.event.WindowAdapter() {
+            @Override
+            public void windowClosing(java.awt.event.WindowEvent evt) {
+                formWindowClosing(evt);
+            }
+        });
+
+        pnlButtons.setLayout(new javax.swing.BoxLayout(pnlButtons, javax.swing.BoxLayout.LINE_AXIS));
+
+        sldVolume.setMajorTickSpacing(20);
+        sldVolume.setOrientation(javax.swing.JSlider.VERTICAL);
+        sldVolume.setPaintTicks(true);
+        sldVolume.setValue(100);
+        sldVolume.addChangeListener(new javax.swing.event.ChangeListener() {
+            @Override
+            public void stateChanged(javax.swing.event.ChangeEvent evt) {
+                sldVolumeStateChanged(evt);
+            }
+        });
+        pnlButtons.add(sldVolume);
+
+        btnRewind.setText("<<");
+        pnlButtons.add(btnRewind);
+
+        btnStop.setText("[  ]");
+        btnStop.addActionListener(new java.awt.event.ActionListener() {
+            @Override
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                btnStopActionPerformed(evt);
+            }
+        });
+        pnlButtons.add(btnStop);
+
+        btnPlay.setText("II / >");
+        btnPlay.addActionListener(new java.awt.event.ActionListener() {
+            @Override
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                btnPlayActionPerformed(evt);
+            }
+        });
+        pnlButtons.add(btnPlay);
+
+        btnFF.setText(">>");
+        btnFF.addActionListener(new java.awt.event.ActionListener() {
+            @Override
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                btnFFActionPerformed(evt);
+            }
+        });
+        pnlButtons.add(btnFF);
+
+        btnOpen.setText("Open ...");
+        btnOpen.addActionListener(new java.awt.event.ActionListener() {
+            @Override
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                btnOpenActionPerformed(evt);
+            }
+        });
+        pnlButtons.add(btnOpen);
+
+        getContentPane().add(pnlButtons, java.awt.BorderLayout.CENTER);
+
+        pnlBar.setLayout(new javax.swing.BoxLayout(pnlBar, javax.swing.BoxLayout.LINE_AXIS));
+
+        lblTime.setText("0:00-0:00");
+        lblTime.setBorder(javax.swing.BorderFactory.createEmptyBorder(3, 3, 3, 3));
+        pnlBar.add(lblTime);
+
+        sldBar.setValue(0);
+        sldBar.addChangeListener(new javax.swing.event.ChangeListener() {
+            @Override
+            public void stateChanged(javax.swing.event.ChangeEvent evt) {
+                sldBarStateChanged(evt);
+            }
+        });
+        pnlBar.add(sldBar);
+
+        getContentPane().add(pnlBar, java.awt.BorderLayout.PAGE_START);
+
+        pack();
+    }// </editor-fold>//GEN-END:initComponents
+
+    private void btnOpenActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnOpenActionPerformed
+        JFileChooser chooser = new JFileChooser();
+        chooser.setAcceptAllFileFilterUsed(true);
+        chooser.setDialogTitle("Select OGG file");
+        chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
+        chooser.setMultiSelectionEnabled(false);
+        if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION){
+            btnStopActionPerformed(null);
+            
+            final File selected = chooser.getSelectedFile();
+            AssetLoader loader = null;
+            if(selected.getName().endsWith(".wav")){
+                loader = new WAVLoader();
+            }else{
+                loader = new OGGLoader();
+            }
+             
+            AudioKey key = new AudioKey(selected.getName(), true, true);
+            try{
+                musicData = (AudioData) loader.load(new AssetInfo(null, key) {
+                    @Override
+                    public InputStream openStream() {
+                        try{
+                            return new FileInputStream(selected);
+                        }catch (FileNotFoundException ex){
+                            ex.printStackTrace();
+                        }
+                        return null;
+                    }
+                });
+            }catch (IOException ex){
+                ex.printStackTrace();
+            }
+
+            musicSource = new AudioNode(musicData, key);
+            // A positional AudioNode would prohibit stereo sound!
+            musicSource.setPositional(false);
+            musicLength = musicData.getDuration();
+            updateTime();
+        }
+    }//GEN-LAST:event_btnOpenActionPerformed
+
+    private void updateTime(){
+        int max = (int) (musicLength * 100);
+        int pos = (int) (curTime * 100);
+        sldBar.setMaximum(max);
+        sldBar.setValue(pos);
+
+        int minutesTotal = (int) (musicLength / 60);
+        int secondsTotal = (int) (musicLength % 60);
+        int minutesNow = (int) (curTime / 60);
+        int secondsNow = (int) (curTime % 60);
+        String txt = String.format("%01d:%02d-%01d:%02d", minutesNow, secondsNow,
+                                                      minutesTotal, secondsTotal);
+        lblTime.setText(txt);
+    }
+
+    private void btnPlayActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnPlayActionPerformed
+        if (musicSource == null){
+            btnOpenActionPerformed(evt);
+            return;
+        }
+
+        if (musicSource.getStatus() == Status.Playing){
+            musicSource.setPitch(1);
+            ar.pauseSource(musicSource);
+        }else{
+            musicSource.setPitch(1);
+            musicSource.play();
+        }
+    }//GEN-LAST:event_btnPlayActionPerformed
+
+    private void formWindowClosing(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_formWindowClosing
+        ar.cleanup();
+    }//GEN-LAST:event_formWindowClosing
+
+    private void sldVolumeStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_sldVolumeStateChanged
+       listener.setVolume(sldVolume.getValue() / 100f);
+       ar.setListener(listener);
+    }//GEN-LAST:event_sldVolumeStateChanged
+
+    private void btnStopActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnStopActionPerformed
+        if (musicSource != null){
+            musicSource.setPitch(1);
+            ar.stopSource(musicSource);
+        }
+    }//GEN-LAST:event_btnStopActionPerformed
+
+    private void btnFFActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnFFActionPerformed
+        if (musicSource != null && musicSource.getStatus() == Status.Playing) {
+            musicSource.setPitch(2);
+        }
+    }//GEN-LAST:event_btnFFActionPerformed
+
+    private void sldBarStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_sldBarStateChanged
+        // do nothing: OGG/Vorbis supports seeking, but only for time = 0!
+    }//GEN-LAST:event_sldBarStateChanged
+
+    /**
+    * @param args the command line arguments
+    */
+    public static void main(String args[]) {
+        java.awt.EventQueue.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                new TestMusicPlayer().setVisible(true);
+            }
+        });
+    }
+
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    private javax.swing.JButton btnFF;
+    private javax.swing.JButton btnOpen;
+    private javax.swing.JButton btnPlay;
+    private javax.swing.JButton btnRewind;
+    private javax.swing.JButton btnStop;
+    private javax.swing.JLabel lblTime;
+    private javax.swing.JPanel pnlBar;
+    private javax.swing.JPanel pnlButtons;
+    private javax.swing.JSlider sldBar;
+    private javax.swing.JSlider sldVolume;
+    // End of variables declaration//GEN-END:variables
+
+}

+ 60 - 0
jme3-examples/src/main/java/jme3test/audio/TestMusicStreaming.java

@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package jme3test.audio;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.asset.plugins.UrlLocator;
+import com.jme3.audio.AudioData;
+import com.jme3.audio.AudioNode;
+
+public class TestMusicStreaming extends SimpleApplication {
+
+    public static void main(String[] args){
+        TestMusicStreaming test = new TestMusicStreaming();
+        test.start();
+    }
+
+    @Override
+    public void simpleInitApp(){
+        assetManager.registerLocator("https://web.archive.org/web/20170625151521if_/http://www.vorbis.com/music/", UrlLocator.class);
+        AudioNode audioSource = new AudioNode(assetManager, "Lumme-Badloop.ogg",
+                AudioData.DataType.Stream);
+        audioSource.setPositional(false);
+        audioSource.setReverbEnabled(false);
+        audioSource.play();
+    }
+
+    @Override
+    public void simpleUpdate(float tpf){}
+
+}

+ 70 - 0
jme3-examples/src/main/java/jme3test/audio/TestOgg.java

@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package jme3test.audio;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.audio.AudioData.DataType;
+import com.jme3.audio.AudioNode;
+import com.jme3.audio.AudioSource;
+import com.jme3.audio.LowPassFilter;
+
+public class TestOgg extends SimpleApplication {
+
+    private AudioNode audioSource;
+
+    public static void main(String[] args){
+        TestOgg test = new TestOgg();
+        test.start();
+    }
+
+    @Override
+    public void simpleInitApp(){
+        System.out.println("Playing without filter");
+        audioSource = new AudioNode(assetManager, "Sound/Effects/Foot steps.ogg", DataType.Buffer);
+        audioSource.play();
+    }
+
+    @Override
+    public void simpleUpdate(float tpf){
+        if (audioSource.getStatus() != AudioSource.Status.Playing){
+            audioRenderer.deleteAudioData(audioSource.getAudioData());
+
+            System.out.println("Playing with low pass filter");
+            audioSource = new AudioNode(assetManager, "Sound/Effects/Foot steps.ogg", DataType.Buffer);
+            audioSource.setDryFilter(new LowPassFilter(1f, .1f));
+            audioSource.setVolume(3);
+            audioSource.play();
+        }
+    }
+
+}

+ 84 - 0
jme3-examples/src/main/java/jme3test/audio/TestReverb.java

@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package jme3test.audio;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.audio.AudioData;
+import com.jme3.audio.AudioNode;
+import com.jme3.audio.Environment;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+
+public class TestReverb extends SimpleApplication {
+
+  private AudioNode audioSource;
+  private float time = 0;
+  private float nextTime = 1;
+
+  public static void main(String[] args) {
+    TestReverb test = new TestReverb();
+    test.start();
+  }
+
+  @Override
+  public void simpleInitApp() {
+    audioSource = new AudioNode(assetManager, "Sound/Effects/Bang.wav",
+            AudioData.DataType.Buffer);
+
+    float[] eax = new float[]{15, 38.0f, 0.300f, -1000, -3300, 0,
+      1.49f, 0.54f, 1.00f, -2560, 0.162f, 0.00f, 0.00f, 0.00f,
+      -229, 0.088f, 0.00f, 0.00f, 0.00f, 0.125f, 1.000f, 0.250f,
+      0.000f, -5.0f, 5000.0f, 250.0f, 0.00f, 0x3f};
+    audioRenderer.setEnvironment(new Environment(eax));
+    Environment env = Environment.Cavern;
+    audioRenderer.setEnvironment(env);
+  }
+
+  @Override
+  public void simpleUpdate(float tpf) {
+    time += tpf;
+
+    if (time > nextTime) {
+      Vector3f v = new Vector3f();
+      v.setX(FastMath.nextRandomFloat());
+      v.setY(FastMath.nextRandomFloat());
+      v.setZ(FastMath.nextRandomFloat());
+      v.multLocal(40, 2, 40);
+      v.subtractLocal(20, 1, 20);
+
+      audioSource.setLocalTranslation(v);
+      audioSource.playInstance();
+      time = 0;
+      nextTime = FastMath.nextRandomFloat() * 2 + 0.5f;
+    }
+  }
+}

+ 64 - 0
jme3-examples/src/main/java/jme3test/audio/TestWav.java

@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package jme3test.audio;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.audio.AudioData;
+import com.jme3.audio.AudioNode;
+
+public class TestWav extends SimpleApplication {
+
+  private float time = 0;
+  private AudioNode audioSource;
+
+  public static void main(String[] args) {
+    TestWav test = new TestWav();
+    test.start();
+  }
+
+  @Override
+  public void simpleUpdate(float tpf) {
+    time += tpf;
+    if (time > 1f) {
+      audioSource.playInstance();
+      time = 0;
+    }
+
+  }
+
+  @Override
+  public void simpleInitApp() {
+    audioSource = new AudioNode(assetManager, "Sound/Effects/Gun.wav",
+            AudioData.DataType.Buffer);
+    audioSource.setLooping(false);
+  }
+}

+ 35 - 0
jme3-examples/src/main/java/jme3test/audio/package-info.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 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.
+ */
+/**
+ * example apps and non-automated tests for sound effects and music
+ */
+package jme3test.audio;

+ 155 - 0
jme3-examples/src/main/java/jme3test/awt/AppHarness.java

@@ -0,0 +1,155 @@
+/*
+ * 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 jme3test.awt;
+
+import com.jme3.app.LegacyApplication;
+import com.jme3.system.AppSettings;
+import com.jme3.system.JmeCanvasContext;
+import com.jme3.system.JmeSystem;
+import java.applet.Applet;
+import java.awt.Canvas;
+import java.awt.Graphics;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import javax.swing.SwingUtilities;
+
+/**
+ *
+ * @author Kirill
+ */
+public class AppHarness extends Applet {
+
+    private JmeCanvasContext context;
+    private Canvas canvas;
+    private LegacyApplication app;
+
+    private String appClass;
+    private URL appCfg = null;
+
+    @SuppressWarnings("unchecked")
+    private void createCanvas(){
+        AppSettings settings = new AppSettings(true);
+
+        // load app cfg
+        if (appCfg != null){
+            try {
+                InputStream in = appCfg.openStream();
+                settings.load(in);
+                in.close();
+            } catch (IOException ex){
+                ex.printStackTrace();
+            }
+        }
+
+        settings.setWidth(getWidth());
+        settings.setHeight(getHeight());
+        settings.setAudioRenderer(null);
+
+        JmeSystem.setLowPermissions(true);
+
+        try{
+            Class clazz = Class.forName(appClass);
+            app = (LegacyApplication) clazz.getDeclaredConstructor().newInstance();
+        }catch (ClassNotFoundException
+                | InstantiationException
+                | IllegalAccessException
+                | IllegalArgumentException
+                | InvocationTargetException
+                | NoSuchMethodException
+                | SecurityException ex) {
+            ex.printStackTrace();
+        }
+
+        app.setSettings(settings);
+        app.createCanvas();
+
+        context = (JmeCanvasContext) app.getContext();
+        canvas = context.getCanvas();
+        canvas.setSize(getWidth(), getHeight());
+
+        add(canvas);
+        app.startCanvas();
+    }
+
+    @Override
+    public final void update(Graphics g) {
+        canvas.setSize(getWidth(), getHeight());
+    }
+
+    @Override
+    public void init(){
+        appClass = getParameter("AppClass");
+        if (appClass == null)
+            throw new RuntimeException("The required parameter AppClass isn't specified!");
+
+        try {
+            appCfg = new URL(getParameter("AppSettingsURL"));
+        } catch (MalformedURLException ex) {
+            ex.printStackTrace();
+            appCfg = null;
+        }
+
+        createCanvas();
+        System.out.println("applet:init");
+    }
+
+    @Override
+    public void start(){
+        context.setAutoFlushFrames(true);
+        System.out.println("applet:start");
+    }
+
+    @Override
+    public void stop(){
+        context.setAutoFlushFrames(false);
+        System.out.println("applet:stop");
+    }
+
+    @Override
+    public void destroy(){
+        System.out.println("applet:destroyStart");
+        SwingUtilities.invokeLater(new Runnable(){
+            @Override
+            public void run(){
+                removeAll();
+                System.out.println("applet:destroyRemoved");
+            }
+        });
+        app.stop(true);
+        System.out.println("applet:destroyDone");
+    }
+
+}

+ 151 - 0
jme3-examples/src/main/java/jme3test/awt/TestApplet.java

@@ -0,0 +1,151 @@
+/*
+ * 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 jme3test.awt;
+
+import com.jme3.app.LegacyApplication;
+import com.jme3.app.SimpleApplication;
+import com.jme3.system.AppSettings;
+import com.jme3.system.JmeCanvasContext;
+import com.jme3.system.JmeSystem;
+import java.applet.Applet;
+import java.awt.Canvas;
+import java.awt.Graphics;
+import java.lang.reflect.InvocationTargetException;
+import java.util.concurrent.Callable;
+import javax.swing.SwingUtilities;
+
+public class TestApplet extends Applet {
+
+    private static JmeCanvasContext context;
+    private static LegacyApplication app;
+    private static Canvas canvas;
+    private static TestApplet applet;
+
+    public TestApplet(){
+    }
+
+    @SuppressWarnings("unchecked")
+    public static void createCanvas(String appClass){
+        AppSettings settings = new AppSettings(true);
+        settings.setWidth(640);
+        settings.setHeight(480);
+        settings.setRenderer(AppSettings.LWJGL_OPENGL2);
+
+        JmeSystem.setLowPermissions(true);
+
+        try{
+            Class clazz = Class.forName(appClass);
+            app = (LegacyApplication) clazz.getDeclaredConstructor().newInstance();
+        } catch (ClassNotFoundException
+                | InstantiationException
+                | IllegalAccessException
+                | IllegalArgumentException
+                | InvocationTargetException
+                | NoSuchMethodException
+                | SecurityException ex) {
+            ex.printStackTrace();
+        }
+
+        app.setSettings(settings);
+        app.createCanvas();
+
+        context = (JmeCanvasContext) app.getContext();
+        canvas = context.getCanvas();
+        canvas.setSize(settings.getWidth(), settings.getHeight());
+    }
+
+    public static void startApp(){
+        applet.add(canvas);
+        app.startCanvas();
+
+        app.enqueue(new Callable<Void>(){
+            @Override
+            public Void call(){
+                if (app instanceof SimpleApplication){
+                    SimpleApplication simpleApp = (SimpleApplication) app;
+                    simpleApp.getFlyByCamera().setDragToRotate(true);
+                    simpleApp.getInputManager().setCursorVisible(true);
+                }
+                return null;
+            }
+        });
+    }
+
+    public void freezeApp(){
+        remove(canvas);
+    }
+
+    public void unfreezeApp(){
+        add(canvas);
+    }
+
+    @Override
+    public final void update(Graphics g) {
+//        canvas.setSize(getWidth(), getHeight());
+    }
+
+    @Override
+    public void init(){
+        applet = this;
+        createCanvas("jme3test.model.shape.TestBox");
+        startApp();
+        app.setPauseOnLostFocus(false);
+        System.out.println("applet:init");
+    }
+
+    @Override
+    public void start(){
+//        context.setAutoFlushFrames(true);
+        System.out.println("applet:start");
+    }
+
+    @Override
+    public void stop(){
+//        context.setAutoFlushFrames(false);
+        System.out.println("applet:stop");
+    }
+
+    @Override
+    public void destroy(){
+        SwingUtilities.invokeLater(new Runnable(){
+            @Override
+            public void run(){
+                removeAll();
+                System.out.println("applet:destroyStart");
+            }
+        });
+        app.stop(true);
+        System.out.println("applet:destroyEnd");
+    }
+
+}

+ 152 - 0
jme3-examples/src/main/java/jme3test/awt/TestAwtPanels.java

@@ -0,0 +1,152 @@
+/*
+ * 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.awt;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.material.Material;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Box;
+import com.jme3.system.AppSettings;
+import com.jme3.system.awt.AwtPanel;
+import com.jme3.system.awt.AwtPanelsContext;
+import com.jme3.system.awt.PaintMode;
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.Toolkit;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.concurrent.CountDownLatch;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.swing.JFrame;
+import javax.swing.SwingUtilities;
+import javax.swing.UIManager;
+
+public class TestAwtPanels extends SimpleApplication {
+
+    private static final Logger logger = Logger.getLogger(TestAwtPanels.class.getName());
+
+    final private static CountDownLatch panelsAreReady = new CountDownLatch(1);
+    private static TestAwtPanels app;
+    private static AwtPanel panel, panel2;
+    private static int panelsClosed = 0;
+    
+    private static void createWindowForPanel(AwtPanel panel, int location){
+        JFrame frame = new JFrame("Render Display " + location);
+        frame.getContentPane().setLayout(new BorderLayout());
+        frame.getContentPane().add(panel, BorderLayout.CENTER);
+        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+        frame.addWindowListener(new WindowAdapter() {
+            @Override
+            public void windowClosed(WindowEvent e) {
+                if (++panelsClosed == 2){
+                    app.stop();
+                }
+            }
+        });
+        frame.pack();
+        frame.setLocation(location, Toolkit.getDefaultToolkit().getScreenSize().height - 400);
+        frame.setVisible(true);
+    }
+    
+    public static void main(String[] args){
+        Logger.getLogger("com.jme3").setLevel(Level.WARNING);
+
+        try {
+            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+        } catch (Exception e) {
+            logger.warning("Could not set native look and feel.");
+        }
+
+        app = new TestAwtPanels();
+        app.setShowSettings(false);
+        AppSettings settings = new AppSettings(true);
+        settings.setCustomRenderer(AwtPanelsContext.class);
+        settings.setFrameRate(60);
+        app.setSettings(settings);
+        app.start();
+        
+        SwingUtilities.invokeLater(new Runnable(){
+            @Override
+            public void run(){
+                /*
+                 * Sleep 2 seconds to ensure there's no race condition.
+                 * The sleep is not required for correctness.
+                 */
+                try {
+                    Thread.sleep(2000);
+                } catch (InterruptedException exception) {
+                    return;
+                }
+
+                final AwtPanelsContext ctx = (AwtPanelsContext) app.getContext();
+                panel = ctx.createPanel(PaintMode.Accelerated);
+                panel.setPreferredSize(new Dimension(400, 300));
+                ctx.setInputSource(panel);
+                
+                panel2 = ctx.createPanel(PaintMode.Accelerated);
+                panel2.setPreferredSize(new Dimension(400, 300));
+                
+                createWindowForPanel(panel, 300);
+                createWindowForPanel(panel2, 700);
+                /*
+                 * Both panels are ready.
+                 */
+                panelsAreReady.countDown();
+            }
+        });
+    }
+    
+    @Override
+    public void simpleInitApp() {
+        flyCam.setDragToRotate(true);
+        
+        Box b = new Box(1, 1, 1);
+        Geometry geom = new Geometry("Box", b);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg"));
+        geom.setMaterial(mat);
+        rootNode.attachChild(geom);
+        /*
+         * Wait until both AWT panels are ready.
+         */
+        try {
+            panelsAreReady.await();
+        } catch (InterruptedException exception) {
+            throw new RuntimeException("Interrupted while waiting for panels", exception);
+        }
+
+        panel.attachTo(true, viewPort);
+        guiViewPort.setClearFlags(true, true, true);
+        panel2.attachTo(false, guiViewPort);
+    }
+}

+ 283 - 0
jme3-examples/src/main/java/jme3test/awt/TestCanvas.java

@@ -0,0 +1,283 @@
+/*
+ * 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 jme3test.awt;
+
+import com.jme3.app.LegacyApplication;
+import com.jme3.app.SimpleApplication;
+import com.jme3.system.AppSettings;
+import com.jme3.system.JmeCanvasContext;
+import com.jme3.util.JmeFormatter;
+import java.awt.BorderLayout;
+import java.awt.Canvas;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.lang.reflect.InvocationTargetException;
+import java.util.concurrent.Callable;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Handler;
+import java.util.logging.Logger;
+import javax.swing.*;
+
+public class TestCanvas {
+
+    private static JmeCanvasContext context;
+    private static Canvas canvas;
+    private static LegacyApplication app;
+    private static JFrame frame;
+    private static Container canvasPanel1, canvasPanel2;
+    private static Container currentPanel;
+    private static JTabbedPane tabbedPane;
+    private static final String appClass = "jme3test.post.TestRenderToTexture";
+
+    private static void createTabs(){
+        tabbedPane = new JTabbedPane();
+
+        canvasPanel1 = new JPanel();
+        canvasPanel1.setLayout(new BorderLayout());
+        tabbedPane.addTab("jME3 Canvas 1", canvasPanel1);
+
+        canvasPanel2 = new JPanel();
+        canvasPanel2.setLayout(new BorderLayout());
+        tabbedPane.addTab("jME3 Canvas 2", canvasPanel2);
+
+        frame.getContentPane().add(tabbedPane);
+
+        currentPanel = canvasPanel1;
+    }
+
+    private static void createMenu(){
+        JMenuBar menuBar = new JMenuBar();
+        frame.setJMenuBar(menuBar);
+
+        JMenu menuTortureMethods = new JMenu("Canvas Torture Methods");
+        menuBar.add(menuTortureMethods);
+
+        final JMenuItem itemRemoveCanvas = new JMenuItem("Remove Canvas");
+        menuTortureMethods.add(itemRemoveCanvas);
+        itemRemoveCanvas.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                if (itemRemoveCanvas.getText().equals("Remove Canvas")){
+                    currentPanel.remove(canvas);
+
+                    itemRemoveCanvas.setText("Add Canvas");
+                }else if (itemRemoveCanvas.getText().equals("Add Canvas")){
+                    currentPanel.add(canvas, BorderLayout.CENTER);
+
+                    itemRemoveCanvas.setText("Remove Canvas");
+                }
+            }
+        });
+
+        final JMenuItem itemHideCanvas = new JMenuItem("Hide Canvas");
+        menuTortureMethods.add(itemHideCanvas);
+        itemHideCanvas.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                if (itemHideCanvas.getText().equals("Hide Canvas")){
+                    canvas.setVisible(false);
+                    itemHideCanvas.setText("Show Canvas");
+                }else if (itemHideCanvas.getText().equals("Show Canvas")){
+                    canvas.setVisible(true);
+                    itemHideCanvas.setText("Hide Canvas");
+                }
+            }
+        });
+
+        final JMenuItem itemSwitchTab = new JMenuItem("Switch to tab #2");
+        menuTortureMethods.add(itemSwitchTab);
+        itemSwitchTab.addActionListener(new ActionListener(){
+           @Override
+           public void actionPerformed(ActionEvent e){
+               if (itemSwitchTab.getText().equals("Switch to tab #2")){
+                   canvasPanel1.remove(canvas);
+                   canvasPanel2.add(canvas, BorderLayout.CENTER);
+                   currentPanel = canvasPanel2;
+                   itemSwitchTab.setText("Switch to tab #1");
+               }else if (itemSwitchTab.getText().equals("Switch to tab #1")){
+                   canvasPanel2.remove(canvas);
+                   canvasPanel1.add(canvas, BorderLayout.CENTER);
+                   currentPanel = canvasPanel1;
+                   itemSwitchTab.setText("Switch to tab #2");
+               }
+           }
+        });
+
+        JMenuItem itemSwitchLaf = new JMenuItem("Switch Look and Feel");
+        menuTortureMethods.add(itemSwitchLaf);
+        itemSwitchLaf.addActionListener(new ActionListener(){
+            @Override
+            public void actionPerformed(ActionEvent e){
+                try {
+                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+                } catch (Throwable t){
+                    t.printStackTrace();
+                }
+                SwingUtilities.updateComponentTreeUI(frame);
+                frame.pack();
+            }
+        });
+
+        JMenuItem itemSmallSize = new JMenuItem("Set size to (0, 0)");
+        menuTortureMethods.add(itemSmallSize);
+        itemSmallSize.addActionListener(new ActionListener(){
+            @Override
+            public void actionPerformed(ActionEvent e){
+                Dimension preferred = frame.getPreferredSize();
+                frame.setPreferredSize(new Dimension(0, 0));
+                frame.pack();
+                frame.setPreferredSize(preferred);
+            }
+        });
+
+        JMenuItem itemKillCanvas = new JMenuItem("Stop/Start Canvas");
+        menuTortureMethods.add(itemKillCanvas);
+        itemKillCanvas.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                currentPanel.remove(canvas);
+                app.stop(true);
+
+                createCanvas(appClass);
+                currentPanel.add(canvas, BorderLayout.CENTER);
+                frame.pack();
+                startApp();
+            }
+        });
+
+        JMenuItem itemExit = new JMenuItem("Exit");
+        menuTortureMethods.add(itemExit);
+        itemExit.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent ae) {
+                frame.dispose();
+                app.stop();
+            }
+        });
+    }
+
+    private static void createFrame(){
+        frame = new JFrame("Test");
+        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+        frame.addWindowListener(new WindowAdapter(){
+            @Override
+            public void windowClosed(WindowEvent e) {
+                app.stop();
+            }
+        });
+
+        createTabs();
+        createMenu();
+    }
+
+    @SuppressWarnings("unchecked")
+    public static void createCanvas(String appClass){
+        AppSettings settings = new AppSettings(true);
+        settings.setWidth(640);
+        settings.setHeight(480);
+
+        try{
+            Class clazz = Class.forName(appClass);
+            app = (LegacyApplication) clazz.getDeclaredConstructor().newInstance();
+        }catch (ClassNotFoundException
+                | InstantiationException
+                | IllegalAccessException
+                | IllegalArgumentException
+                | InvocationTargetException
+                | NoSuchMethodException
+                | SecurityException ex) {
+            ex.printStackTrace();
+        }
+
+        app.setPauseOnLostFocus(false);
+        app.setSettings(settings);
+        app.createCanvas();
+        app.startCanvas();
+
+        context = (JmeCanvasContext) app.getContext();
+        canvas = context.getCanvas();
+        canvas.setSize(settings.getWidth(), settings.getHeight());
+    }
+
+    public static void startApp(){
+        app.startCanvas();
+        app.enqueue(new Callable<Void>(){
+            @Override
+            public Void call(){
+                if (app instanceof SimpleApplication){
+                    SimpleApplication simpleApp = (SimpleApplication) app;
+                    simpleApp.getFlyByCamera().setDragToRotate(true);
+                }
+                return null;
+            }
+        });
+
+    }
+
+    public static void main(String[] args){
+        JmeFormatter formatter = new JmeFormatter();
+
+        Handler consoleHandler = new ConsoleHandler();
+        consoleHandler.setFormatter(formatter);
+
+        Logger.getLogger("").removeHandler(Logger.getLogger("").getHandlers()[0]);
+        Logger.getLogger("").addHandler(consoleHandler);
+
+        createCanvas(appClass);
+
+        try {
+            Thread.sleep(500);
+        } catch (InterruptedException ex) {
+        }
+
+        SwingUtilities.invokeLater(new Runnable(){
+            @Override
+            public void run(){
+                JPopupMenu.setDefaultLightWeightPopupEnabled(false);
+
+                createFrame();
+
+                currentPanel.add(canvas, BorderLayout.CENTER);
+                frame.pack();
+                startApp();
+                frame.setLocationRelativeTo(null);
+                frame.setVisible(true);
+            }
+        });
+    }
+
+}

+ 68 - 0
jme3-examples/src/main/java/jme3test/awt/TestSafeCanvas.java

@@ -0,0 +1,68 @@
+package jme3test.awt;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.material.Material;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Box;
+import com.jme3.system.AppSettings;
+import com.jme3.system.JmeCanvasContext;
+import java.awt.Canvas;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import javax.swing.JFrame;
+
+public class TestSafeCanvas extends SimpleApplication {
+
+    public static void main(String[] args) throws InterruptedException{
+        AppSettings settings = new AppSettings(true);
+        settings.setWidth(640);
+        settings.setHeight(480);
+
+        final TestSafeCanvas app = new TestSafeCanvas();
+        app.setPauseOnLostFocus(false);
+        app.setSettings(settings);
+        app.createCanvas();
+        app.startCanvas(true);
+
+        JmeCanvasContext context = (JmeCanvasContext) app.getContext();
+        Canvas canvas = context.getCanvas();
+        canvas.setSize(settings.getWidth(), settings.getHeight());
+
+        
+
+        Thread.sleep(3000);
+
+        JFrame frame = new JFrame("Test");
+        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+        frame.addWindowListener(new WindowAdapter() {
+            @Override
+            public void windowClosing(WindowEvent e) {
+                app.stop();
+            }
+        });
+        frame.getContentPane().add(canvas);
+        frame.pack();
+        frame.setLocationRelativeTo(null);
+        frame.setVisible(true);
+
+        Thread.sleep(3000);
+
+        frame.getContentPane().remove(canvas);
+
+        Thread.sleep(3000);
+
+        frame.getContentPane().add(canvas);
+    }
+
+    @Override
+    public void simpleInitApp() {
+        flyCam.setDragToRotate(true);
+
+        Box b = new Box(1, 1, 1);
+        Geometry geom = new Geometry("Box", b);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg"));
+        geom.setMaterial(mat);
+        rootNode.attachChild(geom);
+    }
+}

+ 36 - 0
jme3-examples/src/main/java/jme3test/awt/package-info.java

@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 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.
+ */
+/**
+ * example apps and non-automated tests for interfacing to the Abstract Window
+ * Toolkit (AWT)
+ */
+package jme3test.awt;

+ 162 - 0
jme3-examples/src/main/java/jme3test/batching/TestBatchNode.java

@@ -0,0 +1,162 @@
+/*
+ * 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 jme3test.batching;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bounding.BoundingBox;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.BatchNode;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.debug.WireFrustum;
+import com.jme3.scene.shape.Box;
+import com.jme3.system.NanoTimer;
+import com.jme3.util.TangentBinormalGenerator;
+
+/**
+ * A test to demonstrate the usage and functionality of the {@link BatchNode}
+ * @author Nehon
+ */
+public class TestBatchNode extends SimpleApplication {
+    private BatchNode batch;
+    private WireFrustum frustum;
+    private final Vector3f[] points;
+    private Geometry cube2;
+    private float time = 0;
+    private DirectionalLight dl;
+    private boolean done = false;
+
+    public static void main(String[] args) {
+        TestBatchNode app = new TestBatchNode();
+        app.start();
+    }
+
+    public TestBatchNode() {
+        points = new Vector3f[8];
+        for (int i = 0; i < points.length; i++) {
+            points[i] = new Vector3f();
+        }
+    }
+
+    @Override
+    public void simpleInitApp() {
+        timer = new NanoTimer();
+        batch = new BatchNode("theBatchNode");
+
+        /*
+         * A cube with a color "bleeding" through transparent texture. Uses
+         * Texture from jme3-testdata library!
+         */
+        Box boxShape4 = new Box(1f, 1f, 1f);
+        Geometry cube = new Geometry("cube1", boxShape4);
+        Material mat = assetManager.loadMaterial("Textures/Terrain/Pond/Pond.j3m");
+        cube.setMaterial(mat);
+        //Material mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        //mat.setColor("Diffuse", ColorRGBA.Blue);
+        //mat.setBoolean("UseMaterialColors", true);
+        /*
+         * A cube with a color "bleeding" through transparent texture. Uses
+         * Texture from jme3-testdata library!
+         */
+        Box box = new Box(1f, 1f, 1f);
+        cube2 = new Geometry("cube2", box);
+        cube2.setMaterial(mat);
+
+        TangentBinormalGenerator.generate(cube);
+        TangentBinormalGenerator.generate(cube2);
+
+        batch.attachChild(cube);
+        //  batch.attachChild(cube2);
+        //  batch.setMaterial(mat);
+        batch.batch();
+        rootNode.attachChild(batch);
+        cube.setLocalTranslation(3, 0, 0);
+        cube2.setLocalTranslation(0, 20, 0);
+
+        updateBoundingPoints(points);
+        frustum = new WireFrustum(points);
+        Geometry frustumMdl = new Geometry("f", frustum);
+        frustumMdl.setCullHint(Spatial.CullHint.Never);
+        frustumMdl.setMaterial(new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"));
+        frustumMdl.getMaterial().getAdditionalRenderState().setWireframe(true);
+        frustumMdl.getMaterial().setColor("Color", ColorRGBA.Red);
+        rootNode.attachChild(frustumMdl);
+        dl = new DirectionalLight();
+        dl.setColor(ColorRGBA.White.mult(2));
+        dl.setDirection(new Vector3f(1, -1, -1));
+        rootNode.addLight(dl);
+        flyCam.setMoveSpeed(10);
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        if (!done) {
+            done = true;
+            batch.attachChild(cube2);
+            batch.batch();
+        }
+        updateBoundingPoints(points);
+        frustum.update(points);
+        time += tpf;
+        dl.setDirection(cam.getDirection());
+        cube2.setLocalTranslation(FastMath.sin(-time) * 3, FastMath.cos(time) * 3, 0);
+        cube2.setLocalRotation(new Quaternion().fromAngleAxis(time, Vector3f.UNIT_Z));
+        cube2.setLocalScale(Math.max(FastMath.sin(time), 0.5f));
+
+        // batch.setLocalRotation(new Quaternion().fromAngleAxis(time, Vector3f.UNIT_Z));
+    }
+
+    public void updateBoundingPoints(Vector3f[] points) {
+        BoundingBox bb = (BoundingBox) batch.getWorldBound();
+        float xe = bb.getXExtent();
+        float ye = bb.getYExtent();
+        float ze = bb.getZExtent();
+        float x = bb.getCenter().x;
+        float y = bb.getCenter().y;
+        float z = bb.getCenter().z;
+
+        points[0].set(new Vector3f(x - xe, y - ye, z - ze));
+        points[1].set(new Vector3f(x - xe, y + ye, z - ze));
+        points[2].set(new Vector3f(x + xe, y + ye, z - ze));
+        points[3].set(new Vector3f(x + xe, y - ye, z - ze));
+
+        points[4].set(new Vector3f(x + xe, y - ye, z + ze));
+        points[5].set(new Vector3f(x - xe, y - ye, z + ze));
+        points[6].set(new Vector3f(x - xe, y + ye, z + ze));
+        points[7].set(new Vector3f(x + xe, y + ye, z + ze));
+    }
+}

+ 362 - 0
jme3-examples/src/main/java/jme3test/batching/TestBatchNodeCluster.java

@@ -0,0 +1,362 @@
+/*
+ * 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 jme3test.batching;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.post.FilterPostProcessor;
+import com.jme3.post.filters.BloomFilter;
+import com.jme3.scene.*;
+import com.jme3.scene.debug.Arrow;
+import com.jme3.scene.shape.Box;
+import com.jme3.system.AppSettings;
+import com.jme3.system.NanoTimer;
+import java.util.ArrayList;
+import java.util.Random;
+
+public class TestBatchNodeCluster extends SimpleApplication {
+
+    public static void main(String[] args) {
+        TestBatchNodeCluster app = new TestBatchNodeCluster();
+        settingst = new AppSettings(true);
+        //settingst.setFrameRate(75);
+        settingst.setResolution(640, 480);
+        settingst.setVSync(false);
+        settingst.setFullscreen(false);
+        app.setSettings(settingst);
+        app.setShowSettings(false); 
+        app.start();
+    }
+    final private ActionListener al = new ActionListener() {
+
+        @Override
+        public void onAction(String name, boolean isPressed, float tpf) {
+            if (name.equals("Start Game")) {
+//              randomGenerator();
+            }
+        }
+    };
+    final private Random rand = new Random();
+    final private int maxCubes = 2000;
+    final private int startAt = 0;
+    final private ArrayList<Integer> xPosition = new ArrayList<>();
+    final private ArrayList<Integer> yPosition = new ArrayList<>();
+    final private ArrayList<Integer> zPosition = new ArrayList<>();
+    final private int yLimitf = 60, yLimits = -20;
+    private static AppSettings settingst;
+    final private int lineLength = 50;
+    private BatchNode batchNode;
+    private Material mat1;
+    private Material mat2;
+    private Material mat3;
+    private Material mat4;
+    private Node terrain;
+//    protected Geometry player;
+
+    @Override
+    public void simpleInitApp() {
+        timer = new NanoTimer();
+
+        batchNode = new SimpleBatchNode("BatchNode");
+
+
+        xPosition.add(0);
+        yPosition.add(0);
+        zPosition.add(0);
+
+        mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat1.setColor("Color", ColorRGBA.White);
+        mat1.setColor("GlowColor", ColorRGBA.Blue.mult(10));
+
+        mat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat2.setColor("Color", ColorRGBA.White);
+        mat2.setColor("GlowColor", ColorRGBA.Red.mult(10));
+
+        mat3 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat3.setColor("Color", ColorRGBA.White);
+        mat3.setColor("GlowColor", ColorRGBA.Yellow.mult(10));
+
+        mat4 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat4.setColor("Color", ColorRGBA.White);
+        mat4.setColor("GlowColor", ColorRGBA.Orange.mult(10));
+
+        randomGenerator();
+
+        //rootNode.attachChild(SkyFactory.createSky(
+        //  assetManager, "Textures/SKY02.zip", false));
+        inputManager.addMapping("Start Game", new KeyTrigger(KeyInput.KEY_J));
+        inputManager.addListener(al, new String[]{"Start Game"});
+
+
+        cam.setLocation(new Vector3f(-34.403286f, 126.65158f, 434.791f));
+        cam.setRotation(new Quaternion(0.022630932f, 0.9749435f, -0.18736298f, 0.11776358f));
+
+
+        batchNode.batch();
+
+
+        terrain = new Node("terrain");
+        terrain.setLocalTranslation(50, 0, 50);
+        terrain.attachChild(batchNode);
+
+        flyCam.setMoveSpeed(100);
+        rootNode.attachChild(terrain);
+        Vector3f pos = new Vector3f(-40, 0, -40);
+        batchNode.setLocalTranslation(pos);
+
+
+        Arrow a = new Arrow(new Vector3f(0, 50, 0));
+        Geometry g = new Geometry("a", a);
+        g.setLocalTranslation(terrain.getLocalTranslation());
+        Material m = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        m.setColor("Color", ColorRGBA.Blue);
+        g.setMaterial(m);
+
+
+
+        FilterPostProcessor fpp = new FilterPostProcessor(assetManager);
+        fpp.addFilter(new BloomFilter(BloomFilter.GlowMode.Objects));
+//        SSAOFilter ssao = new SSAOFilter(8.630104f,22.970434f,2.9299977f,0.2999997f);    
+//        fpp.addFilter(ssao);
+        viewPort.addProcessor(fpp);
+        //   viewPort.setBackgroundColor(ColorRGBA.DarkGray);
+    }
+
+    public void randomGenerator() {
+        for (int i = startAt; i < maxCubes - 1; i++) {
+            randomize();
+            Geometry box = new Geometry("Box" + i, new Box(1, 1, 1));
+            box.setLocalTranslation(new Vector3f(xPosition.get(xPosition.size() - 1),
+                    yPosition.get(yPosition.size() - 1),
+                    zPosition.get(zPosition.size() - 1)));
+            batchNode.attachChild(box);
+            if (i < 500) {
+                box.setMaterial(mat1);
+            } else if (i < 1000) {
+
+                box.setMaterial(mat2);
+            } else if (i < 1500) {
+
+                box.setMaterial(mat3);
+            } else {
+
+                box.setMaterial(mat4);
+            }
+
+        }
+    }
+
+//    public BatchNode randomBatch() {
+//
+//        int randomn = rand.nextInt(4);
+//        if (randomn == 0) {
+//            return blue;
+//        } else if (randomn == 1) {
+//            return brown;
+//        } else if (randomn == 2) {
+//            return pink;
+//        } else if (randomn == 3) {
+//            return orange;
+//        }
+//        return null;
+//    }
+    public ColorRGBA randomColor() {
+        ColorRGBA color = ColorRGBA.Black;
+        int randomn = rand.nextInt(4);
+        if (randomn == 0) {
+            color = ColorRGBA.Orange;
+        } else if (randomn == 1) {
+            color = ColorRGBA.Blue;
+        } else if (randomn == 2) {
+            color = ColorRGBA.Brown;
+        } else if (randomn == 3) {
+            color = ColorRGBA.Magenta;
+        }
+        return color;
+    }
+
+    public void randomize() {
+        int xpos = xPosition.get(xPosition.size() - 1);
+        int ypos = yPosition.get(yPosition.size() - 1);
+        int zpos = zPosition.get(zPosition.size() - 1);
+        int x = 0;
+        int y = 0;
+        int z = 0;
+        boolean unTrue = true;
+        while (unTrue) {
+            unTrue = false;
+            boolean xChanged = false;
+            x = 0;
+            y = 0;
+            z = 0;
+            if (xpos >= lineLength * 2) {
+                x = 2;
+                xChanged = true;
+            } else {
+                x = xPosition.get(xPosition.size() - 1) + 2;
+            }
+            if (xChanged) {
+                //y = yPosition.get(yPosition.size() - lineLength) + 2;
+            } else {
+                y = rand.nextInt(3);
+                if (yPosition.size() > lineLength) {
+                    if (yPosition.size() > 51) {
+                        if (y == 0 && ypos < yLimitf && getym(lineLength) > ypos - 2) {
+                            y = ypos + 2;
+                        } else if (y == 1 && ypos > yLimits && getym(lineLength) < ypos + 2) {
+                            y = ypos - 2;
+                        } else if (y == 2 && getym(lineLength) > ypos - 2 && getym(lineLength) < ypos + 2) {
+                            y = ypos;
+                        } else {
+                            if (ypos >= yLimitf) {
+                                y = ypos - 2;
+                            } else if (ypos <= yLimits) {
+                                y = ypos + 2;
+                            } else if (y == 0 && getym(lineLength) >= ypos - 4) {
+                                y = ypos - 2;
+                            } else if (y == 0 && getym(lineLength) >= ypos - 2) {
+                                y = ypos;
+                            } else if (y == 1 && getym(lineLength) >= ypos + 4) {
+                                y = ypos + 2;
+                            } else if (y == 1 && getym(lineLength) >= ypos + 2) {
+                                y = ypos;
+                            } else if (y == 2 && getym(lineLength) <= ypos - 2) {
+                                y = ypos - 2;
+                            } else if (y == 2 && getym(lineLength) >= ypos + 2) {
+                                y = ypos + 2;
+                            } else {
+                                System.out.println("wtf");
+                            }
+                        }
+                    } else if (yPosition.size() == lineLength) {
+                        if (y == 0 && ypos < yLimitf) {
+                            y = getym(lineLength) + 2;
+                        } else if (y == 1 && ypos > yLimits) {
+                            y = getym(lineLength) - 2;
+                        }
+                    }
+                } else {
+                    if (y == 0 && ypos < yLimitf) {
+                        y = ypos + 2;
+                    } else if (y == 1 && ypos > yLimits) {
+                        y = ypos - 2;
+                    } else if (y == 2) {
+                        y = ypos;
+                    } else if (y == 0 && ypos >= yLimitf) {
+                        y = ypos - 2;
+                    } else if (y == 1 && ypos <= yLimits) {
+                        y = ypos + 2;
+                    }
+                }
+            }
+            if (xChanged) {
+                z = zpos + 2;
+            } else {
+                z = zpos;
+            }
+//          for (int i = 0; i < xPosition.size(); i++)
+//          {
+//              if (x - xPosition.get(i) <= 1 && x - xPosition.get(i) >= -1 &&
+//                      y - yPosition.get(i) <= 1 && y - yPosition.get(i) >= -1
+//                      &&z - zPosition.get(i) <= 1 && z - zPosition.get(i) >=
+//                      -1)
+//              {
+//                  unTrue = true;
+//              }
+//          }
+        }
+        xPosition.add(x);
+        yPosition.add(y);
+        zPosition.add(z);
+    }
+
+    public int getxm(int i) {
+        return xPosition.get(xPosition.size() - i);
+    }
+
+    public int getym(int i) {
+        return yPosition.get(yPosition.size() - i);
+    }
+
+    public int getzm(int i) {
+        return zPosition.get(zPosition.size() - i);
+    }
+
+    public int getx(int i) {
+        return xPosition.get(i);
+    }
+
+    public int gety(int i) {
+        return yPosition.get(i);
+    }
+
+    public int getz(int i) {
+        return zPosition.get(i);
+    }
+    private float time = 0;
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        time += tpf;
+        int random = rand.nextInt(2000);
+        float mult1 = 1.0f;
+        float mult2 = 1.0f;
+        if (random < 500) {
+            mult1 = 1.0f;
+            mult2 = 1.0f;
+        } else if (random < 1000) {
+            mult1 = -1.0f;
+            mult2 = 1.0f;
+        } else if (random < 1500) {
+            mult1 = 1.0f;
+            mult2 = -1.0f;
+        } else if (random <= 2000) {
+            mult1 = -1.0f;
+            mult2 = -1.0f;
+        }
+        Spatial box = batchNode.getChild("Box" + random);
+        if (box != null) {
+            Vector3f v = box.getLocalTranslation();
+            box.setLocalTranslation(v.x + FastMath.sin(time * mult1) * 20, v.y + (FastMath.sin(time * mult1) * FastMath.cos(time * mult1) * 20), v.z + FastMath.cos(time * mult2) * 20);
+        }
+        terrain.setLocalRotation(new Quaternion().fromAngleAxis(time, Vector3f.UNIT_Y));
+
+
+    }
+}

+ 250 - 0
jme3-examples/src/main/java/jme3test/batching/TestBatchNodeTower.java

@@ -0,0 +1,250 @@
+/*
+ * 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 jme3test.batching;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.asset.TextureKey;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.SphereCollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.font.BitmapText;
+import com.jme3.input.MouseInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.MouseButtonTrigger;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.post.FilterPostProcessor;
+import com.jme3.renderer.queue.RenderQueue.ShadowMode;
+import com.jme3.scene.BatchNode;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Box;
+import com.jme3.scene.shape.Sphere;
+import com.jme3.scene.shape.Sphere.TextureMode;
+import com.jme3.shadow.CompareMode;
+import com.jme3.shadow.DirectionalLightShadowFilter;
+import com.jme3.shadow.EdgeFilteringMode;
+import com.jme3.system.AppSettings;
+import com.jme3.system.NanoTimer;
+import com.jme3.texture.Texture;
+import com.jme3.texture.Texture.WrapMode;
+import jme3test.bullet.BombControl;
+
+/**
+ *
+ * @author double1984 (tower mod by atom)
+ */
+public class TestBatchNodeTower extends SimpleApplication {
+
+    final private int bricksPerLayer = 8;
+    final private int brickLayers = 30;
+
+    final private static float brickWidth = .75f, brickHeight = .25f, brickDepth = .25f;
+    final private float radius = 3f;
+    private float angle = 0;
+
+
+    private Material mat;
+    private Material mat2;
+    private Material mat3;
+    private Sphere bullet;
+    private Box brick;
+    private SphereCollisionShape bulletCollisionShape;
+
+    private BulletAppState bulletAppState;
+    final private BatchNode batchNode = new BatchNode("batch Node");
+    
+    public static void main(String args[]) {
+        TestBatchNodeTower f = new TestBatchNodeTower();
+        AppSettings s = new AppSettings(true);
+        f.setSettings(s);
+        f.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        timer = new NanoTimer();
+        bulletAppState = new BulletAppState();
+        bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL);
+     //   bulletAppState.setEnabled(false);
+        stateManager.attach(bulletAppState);
+        bullet = new Sphere(32, 32, 0.4f, true, false);
+        bullet.setTextureMode(TextureMode.Projected);
+        bulletCollisionShape = new SphereCollisionShape(0.4f);
+
+        brick = new Box(brickWidth, brickHeight, brickDepth);
+        brick.scaleTextureCoordinates(new Vector2f(1f, .5f));
+        initMaterial();
+        initTower();
+        initFloor();
+        initCrossHairs();
+        this.cam.setLocation(new Vector3f(0, 25f, 8f));
+        cam.lookAt(Vector3f.ZERO, new Vector3f(0, 1, 0));
+        cam.setFrustumFar(80);
+        inputManager.addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
+        inputManager.addListener(actionListener, "shoot");
+        rootNode.setShadowMode(ShadowMode.Off);
+        
+        batchNode.batch();
+        batchNode.setShadowMode(ShadowMode.CastAndReceive);
+        rootNode.attachChild(batchNode);
+
+        DirectionalLightShadowFilter shadowRenderer
+                = new DirectionalLightShadowFilter(assetManager, 1024, 2);
+        DirectionalLight dl = new DirectionalLight();
+        dl.setDirection(new Vector3f(-1, -1, -1).normalizeLocal());
+        shadowRenderer.setLight(dl);
+        shadowRenderer.setLambda(0.55f);
+        shadowRenderer.setShadowIntensity(0.6f);
+        shadowRenderer.setShadowCompareMode(CompareMode.Hardware);
+        shadowRenderer.setEdgeFilteringMode(EdgeFilteringMode.PCF4);
+        FilterPostProcessor fpp = new FilterPostProcessor(assetManager);
+        fpp.addFilter(shadowRenderer);
+        viewPort.addProcessor(fpp);   
+    }
+
+    private PhysicsSpace getPhysicsSpace() {
+        return bulletAppState.getPhysicsSpace();
+    }
+    final private ActionListener actionListener = new ActionListener() {
+
+        @Override
+        public void onAction(String name, boolean keyPressed, float tpf) {
+            if (name.equals("shoot") && !keyPressed) {
+                Geometry bulletGeometry = new Geometry("bullet", bullet);
+                bulletGeometry.setMaterial(mat2);
+                bulletGeometry.setShadowMode(ShadowMode.CastAndReceive);
+                bulletGeometry.setLocalTranslation(cam.getLocation());
+                RigidBodyControl bulletNode = new BombControl(assetManager, bulletCollisionShape, 1);
+//                RigidBodyControl bulletNode = new RigidBodyControl(bulletCollisionShape, 1);
+                bulletNode.setLinearVelocity(cam.getDirection().mult(25));
+                bulletGeometry.addControl(bulletNode);
+                rootNode.attachChild(bulletGeometry);
+                getPhysicsSpace().add(bulletNode);
+            }
+        }
+    };
+
+    public void initTower() {
+        double tempX = 0;
+        double tempY = 0;
+        double tempZ = 0;
+        angle = 0f;
+        for (int i = 0; i < brickLayers; i++){
+            // Increment rows
+            if (i != 0) {
+                tempY += brickHeight * 2;
+            } else {
+                tempY = brickHeight;
+            }
+            // Alternate brick seams
+            angle = 360.0f / bricksPerLayer * i/2f;
+            for (int j = 0; j < bricksPerLayer; j++){
+              tempZ = Math.cos(Math.toRadians(angle))*radius;
+              tempX = Math.sin(Math.toRadians(angle))*radius;
+              System.out.println("x="+((float)(tempX))+" y="+((float)(tempY))+" z="+(float)(tempZ));
+              Vector3f vt = new Vector3f((float)(tempX), (float)(tempY), (float)(tempZ));
+              // Add crenelation
+              if (i==brickLayers-1){
+                if (j%2 == 0){
+                    addBrick(vt);
+                }
+              }
+              // Create main tower
+              else {
+                addBrick(vt);
+              }
+              angle += 360.0/bricksPerLayer;
+            }
+          }
+
+    }
+
+    public void initFloor() {
+        Box floorBox = new Box(10f, 0.1f, 5f);
+        floorBox.scaleTextureCoordinates(new Vector2f(3, 6));
+
+        Geometry floor = new Geometry("floor", floorBox);
+        floor.setMaterial(mat3);
+        floor.setShadowMode(ShadowMode.Receive);
+        floor.setLocalTranslation(0, 0, 0);
+        floor.addControl(new RigidBodyControl(0));
+        this.rootNode.attachChild(floor);
+        this.getPhysicsSpace().add(floor);
+    }
+
+    public void initMaterial() {
+        mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        TextureKey key = new TextureKey("Textures/Terrain/BrickWall/BrickWall.jpg");
+        key.setGenerateMips(true);
+        Texture tex = assetManager.loadTexture(key);
+        mat.setTexture("ColorMap", tex);
+
+        mat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        TextureKey key2 = new TextureKey("Textures/Terrain/Rock/Rock.PNG");
+        key2.setGenerateMips(true);
+        Texture tex2 = assetManager.loadTexture(key2);
+        mat2.setTexture("ColorMap", tex2);
+
+        mat3 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        TextureKey key3 = new TextureKey("Textures/Terrain/Pond/Pond.jpg");
+        key3.setGenerateMips(true);
+        Texture tex3 = assetManager.loadTexture(key3);
+        tex3.setWrap(WrapMode.Repeat);
+        mat3.setTexture("ColorMap", tex3);
+    }
+
+    public void addBrick(Vector3f ori) {
+        Geometry reBoxg = new Geometry("brick", brick);
+        reBoxg.setMaterial(mat);
+        reBoxg.setLocalTranslation(ori);
+        reBoxg.rotate(0f, (float)Math.toRadians(angle) , 0f );
+        reBoxg.addControl(new RigidBodyControl(1.5f));
+        reBoxg.setShadowMode(ShadowMode.CastAndReceive);
+        reBoxg.getControl(RigidBodyControl.class).setFriction(1.6f);
+        this.batchNode.attachChild(reBoxg);
+        this.getPhysicsSpace().add(reBoxg);
+    }
+
+    protected void initCrossHairs() {
+        guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
+        BitmapText ch = new BitmapText(guiFont);
+        ch.setSize(guiFont.getCharSet().getRenderedSize() * 2);
+        ch.setText("+"); // crosshairs
+        ch.setLocalTranslation( // center
+                settings.getWidth() / 2 - guiFont.getCharSet().getRenderedSize() / 3 * 2,
+                settings.getHeight() / 2 + ch.getLineHeight() / 2, 0);
+        guiNode.attachChild(ch);
+    }
+}

+ 36 - 0
jme3-examples/src/main/java/jme3test/batching/package-info.java

@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 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.
+ */
+/**
+ * example apps and non-automated tests for batching multiple geometries into a
+ * single Mesh
+ */
+package jme3test.batching;

+ 65 - 0
jme3-examples/src/main/java/jme3test/bounding/TestRayCollision.java

@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package jme3test.bounding;
+
+import com.jme3.bounding.BoundingBox;
+import com.jme3.collision.CollisionResults;
+import com.jme3.math.Ray;
+import com.jme3.math.Vector3f;
+
+/**
+ * Tests picking/collision between bounds and shapes.
+ */
+public class TestRayCollision {
+
+    public static void main(String[] args){
+        Ray r = new Ray(Vector3f.ZERO, Vector3f.UNIT_X);
+        BoundingBox bbox = new BoundingBox(new Vector3f(5, 0, 0), 1, 1, 1);
+
+        CollisionResults res = new CollisionResults();
+        bbox.collideWith(r, res);
+
+        System.out.println("Bounding:" +bbox);
+        System.out.println("Ray: "+r);
+
+        System.out.println("Num collisions: "+res.size());
+        for (int i = 0; i < res.size(); i++){
+            System.out.println("--- Collision #"+i+" ---");
+            float dist = res.getCollision(i).getDistance();
+            Vector3f pt = res.getCollision(i).getContactPoint();
+            System.out.println("distance: "+dist);
+            System.out.println("point: "+pt);
+        }
+    }
+
+}

+ 35 - 0
jme3-examples/src/main/java/jme3test/bounding/package-info.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 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.
+ */
+/**
+ * example apps and non-automated tests for bounding volumes
+ */
+package jme3test.bounding;

+ 221 - 0
jme3-examples/src/main/java/jme3test/bullet/BombControl.java

@@ -0,0 +1,221 @@
+/*
+ * 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 jme3test.bullet;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.PhysicsTickListener;
+import com.jme3.bullet.collision.PhysicsCollisionEvent;
+import com.jme3.bullet.collision.PhysicsCollisionListener;
+import com.jme3.bullet.collision.PhysicsCollisionObject;
+import com.jme3.bullet.collision.shapes.CollisionShape;
+import com.jme3.bullet.collision.shapes.SphereCollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.bullet.objects.PhysicsGhostObject;
+import com.jme3.bullet.objects.PhysicsRigidBody;
+import com.jme3.effect.ParticleEmitter;
+import com.jme3.effect.ParticleMesh.Type;
+import com.jme3.effect.shapes.EmitterSphereShape;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
+import java.io.IOException;
+import java.util.Iterator;
+
+/**
+ *
+ * @author normenhansen
+ */
+public class BombControl extends RigidBodyControl implements PhysicsCollisionListener, PhysicsTickListener {
+
+    private float explosionRadius = 10;
+    private PhysicsGhostObject ghostObject;
+    final private Vector3f vector = new Vector3f();
+    final private Vector3f vector2 = new Vector3f();
+    private float forceFactor = 1;
+    private ParticleEmitter effect;
+    final private float fxTime = 0.5f;
+    final private float maxTime = 4f;
+    private float curTime = -1.0f;
+    private float timer;
+
+    public BombControl(CollisionShape shape, float mass) {
+        super(shape, mass);
+        createGhostObject();
+    }
+
+    public BombControl(AssetManager manager, CollisionShape shape, float mass) {
+        super(shape, mass);
+        createGhostObject();
+        prepareEffect(manager);
+    }
+
+    @Override
+    public void setPhysicsSpace(PhysicsSpace space) {
+        super.setPhysicsSpace(space);
+        if (space != null) {
+            space.addCollisionListener(this);
+        }
+    }
+
+    private void prepareEffect(AssetManager assetManager) {
+        int COUNT_FACTOR = 1;
+        float COUNT_FACTOR_F = 1f;
+        effect = new ParticleEmitter("Flame", Type.Triangle, 32 * COUNT_FACTOR);
+        effect.setSelectRandomImage(true);
+        effect.setStartColor(new ColorRGBA(1f, 0.4f, 0.05f, (1f / COUNT_FACTOR_F)));
+        effect.setEndColor(new ColorRGBA(.4f, .22f, .12f, 0f));
+        effect.setStartSize(1.3f);
+        effect.setEndSize(2f);
+        effect.setShape(new EmitterSphereShape(Vector3f.ZERO, 1f));
+        effect.setParticlesPerSec(0);
+        effect.setGravity(0, -5f, 0);
+        effect.setLowLife(.4f);
+        effect.setHighLife(.5f);
+        effect.getParticleInfluencer()
+                .setInitialVelocity(new Vector3f(0, 7, 0));
+        effect.getParticleInfluencer().setVelocityVariation(1f);
+        effect.setImagesX(2);
+        effect.setImagesY(2);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
+        mat.setTexture("Texture", assetManager.loadTexture("Effects/Explosion/flame.png"));
+        effect.setMaterial(mat);
+    }
+
+    protected void createGhostObject() {
+        ghostObject = new PhysicsGhostObject(new SphereCollisionShape(explosionRadius));
+    }
+
+    @Override
+    public void collision(PhysicsCollisionEvent event) {
+        if (space == null) {
+            return;
+        }
+        if (event.getObjectA() == this || event.getObjectB() == this) {
+            space.add(ghostObject);
+            ghostObject.setPhysicsLocation(getPhysicsLocation(vector));
+            space.addTickListener(this);
+            if (effect != null && spatial.getParent() != null) {
+                curTime = 0;
+                effect.setLocalTranslation(spatial.getLocalTranslation());
+                spatial.getParent().attachChild(effect);
+                effect.emitAllParticles();
+            }
+            space.remove(this);
+            spatial.removeFromParent();
+        }
+    }
+    
+    @Override
+    public void prePhysicsTick(PhysicsSpace space, float f) {
+        space.removeCollisionListener(this);
+    }
+
+    @Override
+    public void physicsTick(PhysicsSpace space, float f) {
+        //get all overlapping objects and apply impulse to them
+        for (Iterator<PhysicsCollisionObject> it = ghostObject.getOverlappingObjects().iterator(); it.hasNext();) {            
+            PhysicsCollisionObject physicsCollisionObject = it.next();
+            if (physicsCollisionObject instanceof PhysicsRigidBody) {
+                PhysicsRigidBody rBody = (PhysicsRigidBody) physicsCollisionObject;
+                rBody.getPhysicsLocation(vector2);
+                vector2.subtractLocal(vector);
+                float force = explosionRadius - vector2.length();
+                force *= forceFactor;
+                force = force > 0 ? force : 0;
+                vector2.normalizeLocal();
+                vector2.multLocal(force);
+                ((PhysicsRigidBody) physicsCollisionObject).applyImpulse(vector2, Vector3f.ZERO);
+            }
+        }
+        space.removeTickListener(this);
+        space.remove(ghostObject);
+    }
+
+    @Override
+    public void update(float tpf) {
+        super.update(tpf);
+        if(enabled){
+            timer+=tpf;
+            if(timer>maxTime){
+                if(spatial.getParent()!=null){
+                    space.removeCollisionListener(this);
+                    space.remove(this);
+                    spatial.removeFromParent();
+                }
+            }
+        }
+        if (enabled && curTime >= 0) {
+            curTime += tpf;
+            if (curTime > fxTime) {
+                curTime = -1;
+                effect.removeFromParent();
+            }
+        }
+    }
+
+    /**
+     * @return the explosionRadius
+     */
+    public float getExplosionRadius() {
+        return explosionRadius;
+    }
+
+    /**
+     * @param explosionRadius the explosionRadius to set
+     */
+    public void setExplosionRadius(float explosionRadius) {
+        this.explosionRadius = explosionRadius;
+        createGhostObject();
+    }
+
+    public float getForceFactor() {
+        return forceFactor;
+    }
+
+    public void setForceFactor(float forceFactor) {
+        this.forceFactor = forceFactor;
+    }
+    
+    
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        throw new UnsupportedOperationException("Reading not supported.");
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        throw new UnsupportedOperationException("Saving not supported.");
+    }
+}

+ 258 - 0
jme3-examples/src/main/java/jme3test/bullet/PhysicsHoverControl.java

@@ -0,0 +1,258 @@
+/*
+ * 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 jme3test.bullet;
+
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.PhysicsTickListener;
+import com.jme3.bullet.collision.shapes.CollisionShape;
+import com.jme3.bullet.control.PhysicsControl;
+import com.jme3.bullet.objects.PhysicsVehicle;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.control.Control;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
+import java.io.IOException;
+
+/**
+ * PhysicsHoverControl uses a RayCast Vehicle with "slippery wheels" to simulate a hovering tank
+ * @author normenhansen
+ */
+public class PhysicsHoverControl extends PhysicsVehicle implements PhysicsControl, PhysicsTickListener, JmeCloneable {
+
+    protected Spatial spatial;
+    protected boolean enabled = true;
+    protected PhysicsSpace space = null;
+    protected float steeringValue = 0;
+    protected float accelerationValue = 0;
+    protected int xw = 3;
+    protected int zw = 5;
+    protected int yw = 2;
+    protected Vector3f HOVER_HEIGHT_LF_START = new Vector3f(xw, 1, zw);
+    protected Vector3f HOVER_HEIGHT_RF_START = new Vector3f(-xw, 1, zw);
+    protected Vector3f HOVER_HEIGHT_LR_START = new Vector3f(xw, 1, -zw);
+    protected Vector3f HOVER_HEIGHT_RR_START = new Vector3f(-xw, 1, -zw);
+    protected Vector3f HOVER_HEIGHT_LF = new Vector3f(xw, -yw, zw);
+    protected Vector3f HOVER_HEIGHT_RF = new Vector3f(-xw, -yw, zw);
+    protected Vector3f HOVER_HEIGHT_LR = new Vector3f(xw, -yw, -zw);
+    protected Vector3f HOVER_HEIGHT_RR = new Vector3f(-xw, -yw, -zw);
+    protected Vector3f tempVect1 = new Vector3f(0, 0, 0);
+    protected Vector3f tempVect2 = new Vector3f(0, 0, 0);
+    protected Vector3f tempVect3 = new Vector3f(0, 0, 0);
+//    protected float rotationCounterForce = 10000f;
+//    protected float speedCounterMult = 2000f;
+//    protected float multiplier = 1000f;
+
+    public PhysicsHoverControl() {
+    }
+
+    /**
+     * Creates a new PhysicsNode with the supplied collision shape
+     * @param shape the desired collision shape
+     */
+    public PhysicsHoverControl(CollisionShape shape) {
+        super(shape);
+        createWheels();
+    }
+
+    public PhysicsHoverControl(CollisionShape shape, float mass) {
+        super(shape, mass);
+        createWheels();
+    }
+
+    @Deprecated
+    @Override
+    public Control cloneForSpatial(Spatial spatial) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override   
+    public Object jmeClone() {
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }     
+
+    @Override   
+    public void cloneFields( Cloner cloner, Object original ) { 
+        throw new UnsupportedOperationException("Not yet implemented.");
+    }
+         
+    @Override
+    public void setSpatial(Spatial spatial) {
+        this.spatial = spatial;
+        setUserObject(spatial);
+        if (spatial == null) {
+            return;
+        }
+        setPhysicsLocation(spatial.getWorldTranslation());
+        setPhysicsRotation(spatial.getWorldRotation().toRotationMatrix());
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    private void createWheels() {
+        addWheel(HOVER_HEIGHT_LF_START, new Vector3f(0, -1, 0), new Vector3f(-1, 0, 0), yw, yw, false);
+        addWheel(HOVER_HEIGHT_RF_START, new Vector3f(0, -1, 0), new Vector3f(-1, 0, 0), yw, yw, false);
+        addWheel(HOVER_HEIGHT_LR_START, new Vector3f(0, -1, 0), new Vector3f(-1, 0, 0), yw, yw, false);
+        addWheel(HOVER_HEIGHT_RR_START, new Vector3f(0, -1, 0), new Vector3f(-1, 0, 0), yw, yw, false);
+        for (int i = 0; i < 4; i++) {
+            getWheel(i).setFrictionSlip(0.001f);
+        }
+    }
+
+    @Override
+    public void prePhysicsTick(PhysicsSpace space, float f) {
+        Vector3f angVel = getAngularVelocity();
+        float rotationVelocity = angVel.getY();
+        Vector3f dir = getForwardVector(tempVect2).multLocal(1, 0, 1).normalizeLocal();
+        getLinearVelocity(tempVect3);
+        Vector3f linearVelocity = tempVect3.multLocal(1, 0, 1);
+        float groundSpeed = linearVelocity.length();
+
+        if (steeringValue != 0) {
+            if (rotationVelocity < 1 && rotationVelocity > -1) {
+                applyTorque(tempVect1.set(0, steeringValue, 0));
+            }
+        } else {
+            // counter the steering value!
+            if (rotationVelocity > 0.2f) {
+                applyTorque(tempVect1.set(0, -mass * 20, 0));
+            } else if (rotationVelocity < -0.2f) {
+                applyTorque(tempVect1.set(0, mass * 20, 0));
+            }
+        }
+        if (accelerationValue > 0) {
+            // counter force that will adjust velocity
+            // if we are not going where we want to go.
+            // this will prevent "drifting" and thus improve control
+            // of the vehicle
+            if (groundSpeed > FastMath.ZERO_TOLERANCE) {
+                float d = dir.dot(linearVelocity.normalize());
+                Vector3f counter = dir.project(linearVelocity).normalizeLocal().negateLocal().multLocal(1 - d);
+                applyForce(counter.multLocal(mass * 10), Vector3f.ZERO);
+            }
+
+            if (linearVelocity.length() < 30) {
+                applyForce(dir.multLocal(accelerationValue), Vector3f.ZERO);
+            }
+        } else {
+            // counter the acceleration value
+            if (groundSpeed > FastMath.ZERO_TOLERANCE) {
+                linearVelocity.normalizeLocal().negateLocal();
+                applyForce(linearVelocity.mult(mass * 10), Vector3f.ZERO);
+            }
+        }
+    }
+
+    @Override
+    public void physicsTick(PhysicsSpace space, float f) {
+    }
+
+    @Override
+    public void update(float tpf) {
+        if (enabled && spatial != null) {
+            getMotionState().applyTransform(spatial);
+        }
+    }
+
+    @Override
+    public void render(RenderManager rm, ViewPort vp) {
+    }
+
+    @Override
+    public void setPhysicsSpace(PhysicsSpace space) {
+        createVehicle(space);
+        if (space == null) {
+            if (this.space != null) {
+                this.space.removeCollisionObject(this);
+                this.space.removeTickListener(this);
+            }
+            this.space = space;
+        } else {
+            space.addCollisionObject(this);
+            space.addTickListener(this);
+        }
+        this.space = space;
+    }
+
+    @Override
+    public PhysicsSpace getPhysicsSpace() {
+        return space;
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(enabled, "enabled", true);
+        oc.write(spatial, "spatial", null);
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+        enabled = ic.readBoolean("enabled", true);
+        spatial = (Spatial) ic.readSavable("spatial", null);
+    }
+
+    /**
+     * @param steeringValue the steeringValue to set
+     */
+    @Override
+    public void steer(float steeringValue) {
+        this.steeringValue = steeringValue * getMass();
+    }
+
+    /**
+     * @param accelerationValue the accelerationValue to set
+     */
+    @Override
+    public void accelerate(float accelerationValue) {
+        this.accelerationValue = accelerationValue * getMass();
+    }
+
+}

+ 371 - 0
jme3-examples/src/main/java/jme3test/bullet/PhysicsTestHelper.java

@@ -0,0 +1,371 @@
+/*
+ * 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 jme3test.bullet;
+
+import com.jme3.app.Application;
+import com.jme3.asset.AssetManager;
+import com.jme3.asset.TextureKey;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.CollisionShape;
+import com.jme3.bullet.collision.shapes.GImpactCollisionShape;
+import com.jme3.bullet.collision.shapes.MeshCollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.input.MouseInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.MouseButtonTrigger;
+import com.jme3.light.AmbientLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.queue.RenderQueue.ShadowMode;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.scene.shape.Box;
+import com.jme3.scene.shape.Sphere;
+import com.jme3.scene.shape.Sphere.TextureMode;
+import com.jme3.texture.Texture;
+import com.jme3.util.BufferUtils;
+
+/**
+ *
+ * @author normenhansen
+ */
+public class PhysicsTestHelper {
+    /**
+     * A private constructor to inhibit instantiation of this class.
+     */
+    private PhysicsTestHelper() {
+    }
+
+    /**
+     * creates a simple physics test world with a floor, an obstacle and some test boxes
+     *
+     * @param rootNode where lights and geometries should be added
+     * @param assetManager for loading assets
+     * @param space where collision objects should be added
+     */
+    public static void createPhysicsTestWorld(Node rootNode, AssetManager assetManager, PhysicsSpace space) {
+        AmbientLight light = new AmbientLight();
+        light.setColor(ColorRGBA.LightGray);
+        rootNode.addLight(light);
+
+        Material material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        material.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg"));
+
+        Box floorBox = new Box(140, 0.25f, 140);
+        Geometry floorGeometry = new Geometry("Floor", floorBox);
+        floorGeometry.setMaterial(material);
+        floorGeometry.setLocalTranslation(0, -5, 0);
+//        Plane plane = new Plane();
+//        plane.setOriginNormal(new Vector3f(0, 0.25f, 0), Vector3f.UNIT_Y);
+//        floorGeometry.addControl(new RigidBodyControl(new PlaneCollisionShape(plane), 0));
+        floorGeometry.addControl(new RigidBodyControl(0));
+        rootNode.attachChild(floorGeometry);
+        space.add(floorGeometry);
+
+        //movable boxes
+        for (int i = 0; i < 12; i++) {
+            Box box = new Box(0.25f, 0.25f, 0.25f);
+            Geometry boxGeometry = new Geometry("Box", box);
+            boxGeometry.setMaterial(material);
+            boxGeometry.setLocalTranslation(i, 5, -3);
+            //RigidBodyControl automatically uses box collision shapes when attached to single geometry with box mesh
+            boxGeometry.addControl(new RigidBodyControl(2));
+            rootNode.attachChild(boxGeometry);
+            space.add(boxGeometry);
+        }
+
+        //immovable sphere with mesh collision shape
+        Sphere sphere = new Sphere(8, 8, 1);
+        Geometry sphereGeometry = new Geometry("Sphere", sphere);
+        sphereGeometry.setMaterial(material);
+        sphereGeometry.setLocalTranslation(4, -4, 2);
+        sphereGeometry.addControl(new RigidBodyControl(new MeshCollisionShape(sphere), 0));
+        rootNode.attachChild(sphereGeometry);
+        space.add(sphereGeometry);
+
+    }
+
+    public static void createPhysicsTestWorldSoccer(Node rootNode, AssetManager assetManager, PhysicsSpace space) {
+        AmbientLight light = new AmbientLight();
+        light.setColor(ColorRGBA.LightGray);
+        rootNode.addLight(light);
+
+        Material material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        material.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg"));
+
+        Box floorBox = new Box(20, 0.25f, 20);
+        Geometry floorGeometry = new Geometry("Floor", floorBox);
+        floorGeometry.setMaterial(material);
+        floorGeometry.setLocalTranslation(0, -0.25f, 0);
+//        Plane plane = new Plane();
+//        plane.setOriginNormal(new Vector3f(0, 0.25f, 0), Vector3f.UNIT_Y);
+//        floorGeometry.addControl(new RigidBodyControl(new PlaneCollisionShape(plane), 0));
+        floorGeometry.addControl(new RigidBodyControl(0));
+        rootNode.attachChild(floorGeometry);
+        space.add(floorGeometry);
+
+        //movable spheres
+        for (int i = 0; i < 5; i++) {
+            Sphere sphere = new Sphere(16, 16, .5f);
+            Geometry ballGeometry = new Geometry("Soccer ball", sphere);
+            ballGeometry.setMaterial(material);
+            ballGeometry.setLocalTranslation(i, 2, -3);
+            //RigidBodyControl automatically uses Sphere collision shapes when attached to single geometry with sphere mesh
+            ballGeometry.addControl(new RigidBodyControl(.001f));
+            ballGeometry.getControl(RigidBodyControl.class).setRestitution(1);
+            rootNode.attachChild(ballGeometry);
+            space.add(ballGeometry);
+        }
+        {
+            //immovable Box with mesh collision shape
+            Box box = new Box(1, 1, 1);
+            Geometry boxGeometry = new Geometry("Box", box);
+            boxGeometry.setMaterial(material);
+            boxGeometry.setLocalTranslation(4, 1, 2);
+            boxGeometry.addControl(new RigidBodyControl(new MeshCollisionShape(box), 0));
+            rootNode.attachChild(boxGeometry);
+            space.add(boxGeometry);
+        }
+        {
+            //immovable Box with mesh collision shape
+            Box box = new Box(1, 1, 1);
+            Geometry boxGeometry = new Geometry("Box", box);
+            boxGeometry.setMaterial(material);
+            boxGeometry.setLocalTranslation(4, 3, 4);
+            boxGeometry.addControl(new RigidBodyControl(new MeshCollisionShape(box), 0));
+            rootNode.attachChild(boxGeometry);
+            space.add(boxGeometry);
+        }
+    }
+
+    /**
+     * creates a box geometry with a RigidBodyControl
+     *
+     * @param assetManager for loading assets
+     * @return a new Geometry
+     */
+    public static Geometry createPhysicsTestBox(AssetManager assetManager) {
+        Material material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        material.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg"));
+        Box box = new Box(0.25f, 0.25f, 0.25f);
+        Geometry boxGeometry = new Geometry("Box", box);
+        boxGeometry.setMaterial(material);
+        //RigidBodyControl automatically uses box collision shapes when attached to single geometry with box mesh
+        boxGeometry.addControl(new RigidBodyControl(2));
+        return boxGeometry;
+    }
+
+    /**
+     * creates a sphere geometry with a RigidBodyControl
+     *
+     * @param assetManager for loading assets
+     * @return a new Geometry
+     */
+    public static Geometry createPhysicsTestSphere(AssetManager assetManager) {
+        Material material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        material.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg"));
+        Sphere sphere = new Sphere(8, 8, 0.25f);
+        Geometry boxGeometry = new Geometry("Sphere", sphere);
+        boxGeometry.setMaterial(material);
+        //RigidBodyControl automatically uses sphere collision shapes when attached to single geometry with sphere mesh
+        boxGeometry.addControl(new RigidBodyControl(2));
+        return boxGeometry;
+    }
+
+    /**
+     * creates an empty node with a RigidBodyControl
+     *
+     * @param manager for loading assets
+     * @param shape a shape for the collision object
+     * @param mass a mass for rigid body
+     * @return a new Node
+     */
+    public static Node createPhysicsTestNode(AssetManager manager, CollisionShape shape, float mass) {
+        Node node = new Node("PhysicsNode");
+        RigidBodyControl control = new RigidBodyControl(shape, mass);
+        node.addControl(control);
+        return node;
+    }
+
+    /**
+     * creates the necessary input listener and action to shoot balls from the camera
+     *
+     * @param app the application that's running
+     * @param rootNode where ball geometries should be added
+     * @param space where collision objects should be added
+     */
+    public static void createBallShooter(final Application app, final Node rootNode, final PhysicsSpace space) {
+        ActionListener actionListener = new ActionListener() {
+
+            @Override
+            public void onAction(String name, boolean keyPressed, float tpf) {
+                Sphere bullet = new Sphere(32, 32, 0.4f, true, false);
+                bullet.setTextureMode(TextureMode.Projected);
+                Material mat2 = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
+                TextureKey key2 = new TextureKey("Textures/Terrain/Rock/Rock.PNG");
+                key2.setGenerateMips(true);
+                Texture tex2 = app.getAssetManager().loadTexture(key2);
+                mat2.setTexture("ColorMap", tex2);
+                if (name.equals("shoot") && !keyPressed) {
+                    Geometry bulletGeometry = new Geometry("bullet", bullet);
+                    bulletGeometry.setMaterial(mat2);
+                    bulletGeometry.setShadowMode(ShadowMode.CastAndReceive);
+                    bulletGeometry.setLocalTranslation(app.getCamera().getLocation());
+                    RigidBodyControl bulletControl = new RigidBodyControl(10);
+                    bulletGeometry.addControl(bulletControl);
+                    bulletControl.setLinearVelocity(app.getCamera().getDirection().mult(25));
+                    bulletGeometry.addControl(bulletControl);
+                    rootNode.attachChild(bulletGeometry);
+                    space.add(bulletControl);
+                }
+            }
+        };
+        app.getInputManager().addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
+        app.getInputManager().addListener(actionListener, "shoot");
+    }
+
+    /**
+     * Creates a curved "floor" with a GImpactCollisionShape provided as the RigidBodyControl's collision
+     * shape. Surface has four slightly concave corners to allow for multiple tests and minimize falling off
+     * the edge of the floor.
+     *
+     * @param assetManager for loading assets
+     * @param floorDimensions width/depth of the "floor" (X/Z)
+     * @param position sets the floor's local translation
+     * @return a new Geometry
+     */
+    public static Geometry createGImpactTestFloor(AssetManager assetManager, float floorDimensions, Vector3f position) {
+        Geometry floor = createTestFloor(assetManager, floorDimensions, position, ColorRGBA.Red);
+        RigidBodyControl floorControl = new RigidBodyControl(new GImpactCollisionShape(floor.getMesh()), 0);
+        floor.addControl(floorControl);
+        return floor;
+    }
+
+    /**
+     * Creates a curved "floor" with a MeshCollisionShape provided as the RigidBodyControl's collision shape.
+     * Surface has four slightly concave corners to allow for multiple tests and minimize falling off the edge
+     * of the floor.
+     *
+     * @param assetManager for loading assets
+     * @param floorDimensions width/depth of the "floor" (X/Z)
+     * @param position sets the floor's local translation
+     * @return a new Geometry
+     */
+    public static Geometry createMeshTestFloor(AssetManager assetManager, float floorDimensions, Vector3f position) {
+        Geometry floor = createTestFloor(assetManager, floorDimensions, position, new ColorRGBA(0.5f, 0.5f, 0.9f, 1));
+        RigidBodyControl floorControl = new RigidBodyControl(new MeshCollisionShape(floor.getMesh()), 0);
+        floor.addControl(floorControl);
+        return floor;
+    }
+
+    private static Geometry createTestFloor(AssetManager assetManager, float floorDimensions, Vector3f position, ColorRGBA color) {
+        Geometry floor = new Geometry("floor", createFloorMesh(20, floorDimensions));
+        Material material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        material.getAdditionalRenderState().setWireframe(true);
+        material.setColor("Color", color);
+        floor.setMaterial(material);
+        floor.setLocalTranslation(position);
+        return floor;
+    }
+
+    private static Mesh createFloorMesh(int meshDetail, float floorDimensions) {
+        if (meshDetail < 10) {
+            meshDetail = 10;
+        }
+        int numVertices = meshDetail * meshDetail * 2 * 3;//width * depth * two tris * 3 verts per tri
+
+        int[] indexBuf = new int[numVertices];
+        int i = 0;
+        for (int x = 0; x < meshDetail; x++) {
+            for (int z = 0; z < meshDetail; z++) {
+                indexBuf[i] = i++;
+                indexBuf[i] = i++;
+                indexBuf[i] = i++;
+                indexBuf[i] = i++;
+                indexBuf[i] = i++;
+                indexBuf[i] = i++;
+            }
+        }
+
+        float[] vertBuf = new float[numVertices * 3];
+        float xIncrement = floorDimensions / meshDetail;
+        float zIncrement = floorDimensions / meshDetail;
+        int j = 0;
+        for (int x = 0; x < meshDetail; x++) {
+            float xPos = x * xIncrement;
+            for (int z = 0; z < meshDetail; z++) {
+                float zPos = z * zIncrement;
+                //First tri
+                vertBuf[j++] = xPos;
+                vertBuf[j++] = getY(xPos, zPos, floorDimensions);
+                vertBuf[j++] = zPos;
+                vertBuf[j++] = xPos;
+                vertBuf[j++] = getY(xPos, zPos + zIncrement, floorDimensions);
+                vertBuf[j++] = zPos + zIncrement;
+                vertBuf[j++] = xPos + xIncrement;
+                vertBuf[j++] = getY(xPos + xIncrement, zPos, floorDimensions);
+                vertBuf[j++] = zPos;
+                //Second tri
+                vertBuf[j++] = xPos;
+                vertBuf[j++] = getY(xPos, zPos + zIncrement, floorDimensions);
+                vertBuf[j++] = zPos + zIncrement;
+                vertBuf[j++] = xPos + xIncrement;
+                vertBuf[j++] = getY(xPos + xIncrement, zPos + zIncrement, floorDimensions);
+                vertBuf[j++] = zPos + zIncrement;
+                vertBuf[j++] = xPos + xIncrement;
+                vertBuf[j++] = getY(xPos + xIncrement, zPos, floorDimensions);
+                vertBuf[j++] = zPos;
+            }
+        }
+
+        Mesh m = new Mesh();
+        m.setBuffer(VertexBuffer.Type.Index, 1, BufferUtils.createIntBuffer(indexBuf));
+        m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(vertBuf));
+        m.updateBound();
+        return m;
+    }
+
+    private static float getY(float x, float z, float max) {
+        float yMaxHeight = 8;
+        float xv = FastMath.unInterpolateLinear(FastMath.abs(x - (max / 2)), 0, max) * FastMath.TWO_PI;
+        float zv = FastMath.unInterpolateLinear(FastMath.abs(z - (max / 2)), 0, max) * FastMath.TWO_PI;
+
+        float xComp = (FastMath.sin(xv) + 1) * 0.5f;
+        float zComp = (FastMath.sin(zv) + 1) * 0.5f;
+
+        return -yMaxHeight * xComp * zComp;
+    }
+}

+ 300 - 0
jme3-examples/src/main/java/jme3test/bullet/TestAttachDriver.java

@@ -0,0 +1,300 @@
+/*
+ * 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.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.asset.TextureKey;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.BoxCollisionShape;
+import com.jme3.bullet.collision.shapes.CompoundCollisionShape;
+import com.jme3.bullet.collision.shapes.MeshCollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.bullet.control.VehicleControl;
+import com.jme3.bullet.joints.SliderJoint;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.material.Material;
+import com.jme3.math.*;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.shape.Box;
+import com.jme3.scene.shape.Cylinder;
+import com.jme3.texture.Texture;
+
+/**
+ * Tests attaching/detaching nodes via joints
+ * @author normenhansen
+ */
+public class TestAttachDriver extends SimpleApplication implements ActionListener {
+
+    private VehicleControl vehicle;
+    private RigidBodyControl bridge;
+    private SliderJoint slider;
+    private final float accelerationForce = 1000.0f;
+    private final float brakeForce = 100.0f;
+    private float steeringValue = 0;
+    private float accelerationValue = 0;
+    final private Vector3f jumpForce = new Vector3f(0, 3000, 0);
+    private BulletAppState bulletAppState;
+
+    public static void main(String[] args) {
+        TestAttachDriver app = new TestAttachDriver();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        bulletAppState = new BulletAppState();
+        stateManager.attach(bulletAppState);
+        bulletAppState.setDebugEnabled(true);
+        setupKeys();
+        setupFloor();
+        buildPlayer();
+    }
+
+    private PhysicsSpace getPhysicsSpace(){
+        return bulletAppState.getPhysicsSpace();
+    }
+
+    private void setupKeys() {
+        inputManager.addMapping("Lefts", new KeyTrigger(KeyInput.KEY_H));
+        inputManager.addMapping("Rights", new KeyTrigger(KeyInput.KEY_K));
+        inputManager.addMapping("Ups", new KeyTrigger(KeyInput.KEY_U));
+        inputManager.addMapping("Downs", new KeyTrigger(KeyInput.KEY_J));
+        inputManager.addMapping("Space", new KeyTrigger(KeyInput.KEY_SPACE));
+        inputManager.addMapping("Reset", new KeyTrigger(KeyInput.KEY_RETURN));
+        inputManager.addListener(this, "Lefts");
+        inputManager.addListener(this, "Rights");
+        inputManager.addListener(this, "Ups");
+        inputManager.addListener(this, "Downs");
+        inputManager.addListener(this, "Space");
+        inputManager.addListener(this, "Reset");
+    }
+
+    public void setupFloor() {
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        TextureKey key = new TextureKey("Interface/Logo/Monkey.jpg", true);
+        key.setGenerateMips(true);
+        Texture tex = assetManager.loadTexture(key);
+        tex.setMinFilter(Texture.MinFilter.Trilinear);
+        mat.setTexture("ColorMap", tex);
+
+        Box floor = new Box(100, 1f, 100);
+        Geometry floorGeom = new Geometry("Floor", floor);
+        floorGeom.setMaterial(mat);
+        floorGeom.setLocalTranslation(new Vector3f(0f, -3, 0f));
+
+        floorGeom.addControl(new RigidBodyControl(new MeshCollisionShape(floorGeom.getMesh()), 0));
+        rootNode.attachChild(floorGeom);
+        getPhysicsSpace().add(floorGeom);
+    }
+
+    private void buildPlayer() {
+        Material mat = new Material(getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
+        mat.getAdditionalRenderState().setWireframe(true);
+        mat.setColor("Color", ColorRGBA.Red);
+
+        //create a compound shape and attach the BoxCollisionShape for the car body at 0,1,0
+        //this shifts the effective center of mass of the BoxCollisionShape to 0,-1,0
+        CompoundCollisionShape compoundShape = new CompoundCollisionShape();
+        BoxCollisionShape box = new BoxCollisionShape(new Vector3f(1.2f, 0.5f, 2.4f));
+        compoundShape.addChildShape(box, new Vector3f(0, 1, 0));
+
+        //create vehicle node
+        Node vehicleNode=new Node("vehicleNode");
+        vehicle = new VehicleControl(compoundShape, 800);
+        vehicleNode.addControl(vehicle);
+
+        //setting suspension values for wheels, this can be a bit tricky
+        //see also https://docs.google.com/Doc?docid=0AXVUZ5xw6XpKZGNuZG56a3FfMzU0Z2NyZnF4Zmo&hl=en
+        float stiffness = 60.0f;//200=f1 car
+        float compValue = .3f; //(should be lower than damp)
+        float dampValue = .4f;
+        vehicle.setSuspensionCompression(compValue * 2.0f * FastMath.sqrt(stiffness));
+        vehicle.setSuspensionDamping(dampValue * 2.0f * FastMath.sqrt(stiffness));
+        vehicle.setSuspensionStiffness(stiffness);
+        vehicle.setMaxSuspensionForce(10000.0f);
+
+        //Create four wheels and add them at their locations
+        Vector3f wheelDirection = new Vector3f(0, -1, 0); // was 0, -1, 0
+        Vector3f wheelAxle = new Vector3f(-1, 0, 0); // was -1, 0, 0
+        float radius = 0.5f;
+        float restLength = 0.3f;
+        float yOff = 0.5f;
+        float xOff = 1f;
+        float zOff = 2f;
+
+        Cylinder wheelMesh = new Cylinder(16, 16, radius, radius * 0.6f, true);
+
+        Node node1 = new Node("wheel 1 node");
+        Geometry wheels1 = new Geometry("wheel 1", wheelMesh);
+        node1.attachChild(wheels1);
+        wheels1.rotate(0, FastMath.HALF_PI, 0);
+        wheels1.setMaterial(mat);
+        vehicle.addWheel(node1, new Vector3f(-xOff, yOff, zOff),
+                wheelDirection, wheelAxle, restLength, radius, true);
+
+        Node node2 = new Node("wheel 2 node");
+        Geometry wheels2 = new Geometry("wheel 2", wheelMesh);
+        node2.attachChild(wheels2);
+        wheels2.rotate(0, FastMath.HALF_PI, 0);
+        wheels2.setMaterial(mat);
+        vehicle.addWheel(node2, new Vector3f(xOff, yOff, zOff),
+                wheelDirection, wheelAxle, restLength, radius, true);
+
+        Node node3 = new Node("wheel 3 node");
+        Geometry wheels3 = new Geometry("wheel 3", wheelMesh);
+        node3.attachChild(wheels3);
+        wheels3.rotate(0, FastMath.HALF_PI, 0);
+        wheels3.setMaterial(mat);
+        vehicle.addWheel(node3, new Vector3f(-xOff, yOff, -zOff),
+                wheelDirection, wheelAxle, restLength, radius, false);
+
+        Node node4 = new Node("wheel 4 node");
+        Geometry wheels4 = new Geometry("wheel 4", wheelMesh);
+        node4.attachChild(wheels4);
+        wheels4.rotate(0, FastMath.HALF_PI, 0);
+        wheels4.setMaterial(mat);
+        vehicle.addWheel(node4, new Vector3f(xOff, yOff, -zOff),
+                wheelDirection, wheelAxle, restLength, radius, false);
+
+        vehicleNode.attachChild(node1);
+        vehicleNode.attachChild(node2);
+        vehicleNode.attachChild(node3);
+        vehicleNode.attachChild(node4);
+
+        rootNode.attachChild(vehicleNode);
+        getPhysicsSpace().add(vehicle);
+
+        //driver
+        Node driverNode=new Node("driverNode");
+        driverNode.setLocalTranslation(0,2,0);
+        RigidBodyControl driver
+                = new RigidBodyControl(new BoxCollisionShape(new Vector3f(0.2f,.5f,0.2f)));
+        driverNode.addControl(driver);
+
+        rootNode.attachChild(driverNode);
+        getPhysicsSpace().add(driver);
+
+        //joint
+        slider=new SliderJoint(driver, vehicle, Vector3f.UNIT_Y.negate(), Vector3f.UNIT_Y, true);
+        slider.setUpperLinLimit(.1f);
+        slider.setLowerLinLimit(-.1f);
+
+        getPhysicsSpace().add(slider);
+
+        Node pole1Node=new Node("pole1Node");
+        Node pole2Node=new Node("pole1Node");
+        Node bridgeNode=new Node("pole1Node");
+        pole1Node.setLocalTranslation(new Vector3f(-2,-1,4));
+        pole2Node.setLocalTranslation(new Vector3f(2,-1,4));
+        bridgeNode.setLocalTranslation(new Vector3f(0,1.4f,4));
+
+        RigidBodyControl pole1=new RigidBodyControl(new BoxCollisionShape(new Vector3f(0.2f,1.25f,0.2f)),0);
+        pole1Node.addControl(pole1);
+        RigidBodyControl pole2=new RigidBodyControl(new BoxCollisionShape(new Vector3f(0.2f,1.25f,0.2f)),0);
+        pole2Node.addControl(pole2);
+        bridge=new RigidBodyControl(new BoxCollisionShape(new Vector3f(2.5f,0.2f,0.2f)));
+        bridgeNode.addControl(bridge);
+
+        rootNode.attachChild(pole1Node);
+        rootNode.attachChild(pole2Node);
+        rootNode.attachChild(bridgeNode);
+        getPhysicsSpace().add(pole1);
+        getPhysicsSpace().add(pole2);
+        getPhysicsSpace().add(bridge);
+
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        Quaternion quat=new Quaternion();
+        cam.lookAt(vehicle.getPhysicsLocation(), Vector3f.UNIT_Y);
+    }
+
+    @Override
+    public void onAction(String binding, boolean value, float tpf) {
+        if (binding.equals("Lefts")) {
+            if (value) {
+                steeringValue += .5f;
+            } else {
+                steeringValue -= .5f;
+            }
+            vehicle.steer(steeringValue);
+        } else if (binding.equals("Rights")) {
+            if (value) {
+                steeringValue -= .5f;
+            } else {
+                steeringValue += .5f;
+            }
+            vehicle.steer(steeringValue);
+        } else if (binding.equals("Ups")) {
+            if (value) {
+                accelerationValue += accelerationForce;
+            } else {
+                accelerationValue -= accelerationForce;
+            }
+            vehicle.accelerate(accelerationValue);
+        } else if (binding.equals("Downs")) {
+            if (value) {
+                vehicle.brake(brakeForce);
+            } else {
+                vehicle.brake(0f);
+            }
+        } else if (binding.equals("Space")) {
+            if (value) {
+                if (slider != null) {
+                    getPhysicsSpace().remove(slider);
+                    slider.destroy();
+                    slider = null;
+                }
+                vehicle.applyImpulse(jumpForce, Vector3f.ZERO);
+            }
+        } else if (binding.equals("Reset")) {
+            if (value) {
+                System.out.println("Reset");
+                vehicle.setPhysicsLocation(new Vector3f(0, 0, 0));
+                vehicle.setPhysicsRotation(new Matrix3f());
+                vehicle.setLinearVelocity(Vector3f.ZERO);
+                vehicle.setAngularVelocity(Vector3f.ZERO);
+                vehicle.resetSuspension();
+                bridge.setPhysicsLocation(new Vector3f(0,1.4f,4));
+                bridge.setPhysicsRotation(Matrix3f.IDENTITY);
+                bridge.setLinearVelocity(Vector3f.ZERO);
+                bridge.setAngularVelocity(Vector3f.ZERO);
+            }
+        }
+    }
+}

+ 129 - 0
jme3-examples/src/main/java/jme3test/bullet/TestAttachGhostObject.java

@@ -0,0 +1,129 @@
+/*
+ * 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 jme3test.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.BoxCollisionShape;
+import com.jme3.bullet.collision.shapes.SphereCollisionShape;
+import com.jme3.bullet.control.GhostControl;
+import com.jme3.bullet.control.PhysicsControl;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.bullet.joints.HingeJoint;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.AnalogListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Node;
+
+/**
+ * Tests attaching ghost nodes to physics nodes via the scene graph
+ * @author normenhansen
+ */
+public class TestAttachGhostObject extends SimpleApplication implements AnalogListener {
+
+    private HingeJoint joint;
+    private GhostControl ghostControl;
+    private Node collisionNode;
+    private BulletAppState bulletAppState;
+
+    public static void main(String[] args) {
+        TestAttachGhostObject app = new TestAttachGhostObject();
+        app.start();
+    }
+
+    private void setupKeys() {
+        inputManager.addMapping("Lefts", new KeyTrigger(KeyInput.KEY_H));
+        inputManager.addMapping("Rights", new KeyTrigger(KeyInput.KEY_K));
+        inputManager.addMapping("Space", new KeyTrigger(KeyInput.KEY_SPACE));
+        inputManager.addListener(this, "Lefts", "Rights", "Space");
+    }
+
+    @Override
+    public void onAnalog(String binding, float value, float tpf) {
+        if (binding.equals("Lefts")) {
+            joint.enableMotor(true, 1, .1f);
+        } else if (binding.equals("Rights")) {
+            joint.enableMotor(true, -1, .1f);
+        } else if (binding.equals("Space")) {
+            joint.enableMotor(false, 0, 0);
+        }
+    }
+
+    @Override
+    public void simpleInitApp() {
+        bulletAppState = new BulletAppState();
+        stateManager.attach(bulletAppState);
+        bulletAppState.setDebugEnabled(true);
+        setupKeys();
+        setupJoint();
+    }
+
+    private PhysicsSpace getPhysicsSpace() {
+        return bulletAppState.getPhysicsSpace();
+    }
+
+    public void setupJoint() {
+        Node holderNode = PhysicsTestHelper.createPhysicsTestNode(assetManager, new BoxCollisionShape(new Vector3f(.1f, .1f, .1f)), 0);
+        holderNode.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(0f, 0, 0f));
+        rootNode.attachChild(holderNode);
+        getPhysicsSpace().add(holderNode);
+
+        Node hammerNode = PhysicsTestHelper.createPhysicsTestNode(assetManager, new BoxCollisionShape(new Vector3f(.3f, .3f, .3f)), 1);
+        hammerNode.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(0f, -1, 0f));
+        rootNode.attachChild(hammerNode);
+        getPhysicsSpace().add(hammerNode);
+
+        //immovable
+        collisionNode = PhysicsTestHelper.createPhysicsTestNode(assetManager, new BoxCollisionShape(new Vector3f(.3f, .3f, .3f)), 0);
+        collisionNode.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(1.8f, 0, 0f));
+        rootNode.attachChild(collisionNode);
+        getPhysicsSpace().add(collisionNode);
+
+        //ghost node
+        ghostControl = new GhostControl(new SphereCollisionShape(0.7f));
+
+        hammerNode.addControl(ghostControl);
+        getPhysicsSpace().add(ghostControl);
+
+        joint = new HingeJoint(holderNode.getControl(RigidBodyControl.class), hammerNode.getControl(RigidBodyControl.class), Vector3f.ZERO, new Vector3f(0f, -1, 0f), Vector3f.UNIT_Z, Vector3f.UNIT_Z);
+        getPhysicsSpace().add(joint);
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        if (ghostControl.getOverlappingObjects().contains(collisionNode.getControl(PhysicsControl.class))) {
+            fpsText.setText("collide");
+        }
+    }
+}

+ 297 - 0
jme3-examples/src/main/java/jme3test/bullet/TestBetterCharacter.java

@@ -0,0 +1,297 @@
+/*
+ * 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 jme3test.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.MeshCollisionShape;
+import com.jme3.bullet.control.BetterCharacterControl;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.material.Material;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.RenderManager;
+import com.jme3.scene.CameraNode;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.control.CameraControl.ControlDirection;
+import com.jme3.scene.shape.Sphere;
+import com.jme3.system.AppSettings;
+
+/**
+ * A walking physical character followed by a 3rd person camera. (No animation.)
+ *
+ * @author normenhansen, zathras
+ */
+public class TestBetterCharacter extends SimpleApplication implements ActionListener {
+
+    private BulletAppState bulletAppState;
+    private BetterCharacterControl physicsCharacter;
+    private Node characterNode;
+    private CameraNode camNode;
+    final private Vector3f walkDirection = new Vector3f(0, 0, 0);
+    final private Vector3f viewDirection = new Vector3f(0, 0, 1);
+    private boolean leftStrafe = false, rightStrafe = false, forward = false, backward = false,
+            leftRotate = false, rightRotate = false;
+    final private Vector3f normalGravity = new Vector3f(0, -9.81f, 0);
+    private Geometry planet;
+
+    public static void main(String[] args) {
+        TestBetterCharacter app = new TestBetterCharacter();
+        AppSettings settings = new AppSettings(true);
+        settings.setRenderer(AppSettings.LWJGL_OPENGL2);
+        settings.setAudioRenderer(AppSettings.LWJGL_OPENAL);
+        app.setSettings(settings);
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        //setup keyboard mapping
+        setupKeys();
+
+        // activate physics
+        bulletAppState = new BulletAppState() {
+            @Override
+            public void prePhysicsTick(PhysicsSpace space, float tpf) {
+                // Apply radial gravity near the planet, downward gravity elsewhere.
+                checkPlanetGravity();
+            }
+        };
+        stateManager.attach(bulletAppState);
+        bulletAppState.setDebugEnabled(true);
+
+        // init a physics test scene
+        PhysicsTestHelper.createPhysicsTestWorldSoccer(rootNode, assetManager, bulletAppState.getPhysicsSpace());
+        PhysicsTestHelper.createBallShooter(this, rootNode, bulletAppState.getPhysicsSpace());
+        setupPlanet();
+
+        // Create a node for the character model
+        characterNode = new Node("character node");
+        characterNode.setLocalTranslation(new Vector3f(4, 5, 2));
+
+        // Add a character control to the node, so we can add other things and
+        // control the model rotation.
+        physicsCharacter = new BetterCharacterControl(0.3f, 2.5f, 8f);
+        characterNode.addControl(physicsCharacter);
+        getPhysicsSpace().add(physicsCharacter);
+
+        // Load model, attach to character node
+        Node model = (Node) assetManager.loadModel("Models/Jaime/Jaime.j3o");
+        model.setLocalScale(1.50f);
+        characterNode.attachChild(model);
+
+        // Add character node to the rootNode
+        rootNode.attachChild(characterNode);
+
+        cam.setLocation(new Vector3f(10f, 6f, -5f));
+
+        // Set forward camera node that follows the character, only used when
+        // view is "locked"
+        camNode = new CameraNode("CamNode", cam);
+        camNode.setControlDir(ControlDirection.SpatialToCamera);
+        camNode.setLocalTranslation(new Vector3f(0, 2, -6));
+        Quaternion quat = new Quaternion();
+        // These coordinates are local, the camNode is attached to the character node!
+        quat.lookAt(Vector3f.UNIT_Z, Vector3f.UNIT_Y);
+        camNode.setLocalRotation(quat);
+        characterNode.attachChild(camNode);
+        // Disable by default, can be enabled via keyboard shortcut
+        camNode.setEnabled(false);
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        // Get current forward and left vectors of model by using its rotation
+        // to rotate the unit vectors
+        Vector3f modelForwardDir = characterNode.getWorldRotation().mult(Vector3f.UNIT_Z);
+        Vector3f modelLeftDir = characterNode.getWorldRotation().mult(Vector3f.UNIT_X);
+
+        // WalkDirection is global!
+        // You *can* make your character fly with this.
+        walkDirection.set(0, 0, 0);
+        if (leftStrafe) {
+            walkDirection.addLocal(modelLeftDir.mult(3));
+        } else if (rightStrafe) {
+            walkDirection.addLocal(modelLeftDir.negate().multLocal(3));
+        }
+        if (forward) {
+            walkDirection.addLocal(modelForwardDir.mult(3));
+        } else if (backward) {
+            walkDirection.addLocal(modelForwardDir.negate().multLocal(3));
+        }
+        physicsCharacter.setWalkDirection(walkDirection);
+
+        // ViewDirection is local to characters physics system!
+        // The final world rotation depends on the gravity and on the state of
+        // setApplyPhysicsLocal()
+        if (leftRotate) {
+            Quaternion rotateL = new Quaternion().fromAngleAxis(FastMath.PI * tpf, Vector3f.UNIT_Y);
+            rotateL.multLocal(viewDirection);
+        } else if (rightRotate) {
+            Quaternion rotateR = new Quaternion().fromAngleAxis(-FastMath.PI * tpf, Vector3f.UNIT_Y);
+            rotateR.multLocal(viewDirection);
+        }
+        physicsCharacter.setViewDirection(viewDirection);
+        fpsText.setText("Touch da ground = " + physicsCharacter.isOnGround());
+        if (!lockView) {
+            cam.lookAt(characterNode.getWorldTranslation().add(new Vector3f(0, 2, 0)), Vector3f.UNIT_Y);
+        }
+    }
+
+    private void setupPlanet() {
+        Material material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        material.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg"));
+        //immovable sphere with mesh collision shape
+        Sphere sphere = new Sphere(64, 64, 20);
+        planet = new Geometry("Sphere", sphere);
+        planet.setMaterial(material);
+        planet.setLocalTranslation(30, -15, 30);
+        planet.addControl(new RigidBodyControl(new MeshCollisionShape(sphere), 0));
+        rootNode.attachChild(planet);
+        getPhysicsSpace().add(planet);
+    }
+
+    private void checkPlanetGravity() {
+        Vector3f planetDist = planet.getWorldTranslation().subtract(characterNode.getWorldTranslation());
+        if (planetDist.length() < 24) {
+            physicsCharacter.setGravity(planetDist.normalizeLocal().multLocal(9.81f));
+        } else {
+            physicsCharacter.setGravity(normalGravity);
+        }
+    }
+
+    private PhysicsSpace getPhysicsSpace() {
+        return bulletAppState.getPhysicsSpace();
+    }
+
+    @Override
+    public void onAction(String binding, boolean value, float tpf) {
+        if (binding.equals("Strafe Left")) {
+            if (value) {
+                leftStrafe = true;
+            } else {
+                leftStrafe = false;
+            }
+        } else if (binding.equals("Strafe Right")) {
+            if (value) {
+                rightStrafe = true;
+            } else {
+                rightStrafe = false;
+            }
+        } else if (binding.equals("Rotate Left")) {
+            if (value) {
+                leftRotate = true;
+            } else {
+                leftRotate = false;
+            }
+        } else if (binding.equals("Rotate Right")) {
+            if (value) {
+                rightRotate = true;
+            } else {
+                rightRotate = false;
+            }
+        } else if (binding.equals("Walk Forward")) {
+            if (value) {
+                forward = true;
+            } else {
+                forward = false;
+            }
+        } else if (binding.equals("Walk Backward")) {
+            if (value) {
+                backward = true;
+            } else {
+                backward = false;
+            }
+        } else if (binding.equals("Jump")) {
+            physicsCharacter.jump();
+        } else if (binding.equals("Duck")) {
+            if (value) {
+                physicsCharacter.setDucked(true);
+            } else {
+                physicsCharacter.setDucked(false);
+            }
+        } else if (binding.equals("Lock View")) {
+            if (value && lockView) {
+                lockView = false;
+            } else if (value && !lockView) {
+                lockView = true;
+            }
+            flyCam.setEnabled(!lockView);
+            camNode.setEnabled(lockView);
+        }
+    }
+    private boolean lockView = false;
+
+    private void setupKeys() {
+        inputManager.addMapping("Strafe Left",
+                new KeyTrigger(KeyInput.KEY_U),
+                new KeyTrigger(KeyInput.KEY_Z));
+        inputManager.addMapping("Strafe Right",
+                new KeyTrigger(KeyInput.KEY_O),
+                new KeyTrigger(KeyInput.KEY_X));
+        inputManager.addMapping("Rotate Left",
+                new KeyTrigger(KeyInput.KEY_J),
+                new KeyTrigger(KeyInput.KEY_LEFT));
+        inputManager.addMapping("Rotate Right",
+                new KeyTrigger(KeyInput.KEY_L),
+                new KeyTrigger(KeyInput.KEY_RIGHT));
+        inputManager.addMapping("Walk Forward",
+                new KeyTrigger(KeyInput.KEY_I),
+                new KeyTrigger(KeyInput.KEY_UP));
+        inputManager.addMapping("Walk Backward",
+                new KeyTrigger(KeyInput.KEY_K),
+                new KeyTrigger(KeyInput.KEY_DOWN));
+        inputManager.addMapping("Jump",
+                new KeyTrigger(KeyInput.KEY_F),
+                new KeyTrigger(KeyInput.KEY_SPACE));
+        inputManager.addMapping("Duck",
+                new KeyTrigger(KeyInput.KEY_G),
+                new KeyTrigger(KeyInput.KEY_LSHIFT),
+                new KeyTrigger(KeyInput.KEY_RSHIFT));
+        inputManager.addMapping("Lock View",
+                new KeyTrigger(KeyInput.KEY_RETURN));
+        inputManager.addListener(this, "Strafe Left", "Strafe Right");
+        inputManager.addListener(this, "Rotate Left", "Rotate Right");
+        inputManager.addListener(this, "Walk Forward", "Walk Backward");
+        inputManager.addListener(this, "Jump", "Duck", "Lock View");
+    }
+
+    @Override
+    public void simpleRender(RenderManager rm) {
+    }
+}

+ 244 - 0
jme3-examples/src/main/java/jme3test/bullet/TestBoneRagdoll.java

@@ -0,0 +1,244 @@
+/*
+ * 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 jme3test.bullet;
+
+import com.jme3.anim.AnimComposer;
+import com.jme3.anim.tween.Tweens;
+import com.jme3.anim.tween.action.Action;
+import com.jme3.app.SimpleApplication;
+import com.jme3.asset.TextureKey;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.animation.DynamicAnimControl;
+import com.jme3.bullet.animation.PhysicsLink;
+import com.jme3.bullet.animation.RagdollCollisionListener;
+import com.jme3.bullet.collision.PhysicsCollisionEvent;
+import com.jme3.bullet.collision.PhysicsCollisionObject;
+import com.jme3.bullet.collision.shapes.SphereCollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.font.BitmapText;
+import com.jme3.input.KeyInput;
+import com.jme3.input.MouseInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.input.controls.MouseButtonTrigger;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.shape.Sphere;
+import com.jme3.scene.shape.Sphere.TextureMode;
+import com.jme3.texture.Texture;
+
+/**
+ * @author normenhansen
+ */
+public class TestBoneRagdoll
+        extends SimpleApplication
+        implements ActionListener, RagdollCollisionListener {
+
+    private AnimComposer composer;
+    private DynamicAnimControl ragdoll;
+    private float bulletSize = 1f;
+    private Material matBullet;
+    private Node model;
+    private PhysicsSpace physicsSpace;
+    private Sphere bullet;
+    private SphereCollisionShape bulletCollisionShape;
+
+    public static void main(String[] args) {
+        TestBoneRagdoll app = new TestBoneRagdoll();
+        app.start();
+    }
+
+    public void onStandDone() {
+        composer.setCurrentAction("IdleTop");
+    }
+
+    @Override
+    public void onAction(String name, boolean isPressed, float tpf) {
+        if (name.equals("boom") && !isPressed) {
+            Geometry bulletGeometry = new Geometry("bullet", bullet);
+            bulletGeometry.setMaterial(matBullet);
+            bulletGeometry.setLocalTranslation(cam.getLocation());
+            bulletGeometry.setLocalScale(bulletSize);
+            bulletCollisionShape = new SphereCollisionShape(bulletSize);
+            BombControl bulletNode = new BombControl(assetManager, bulletCollisionShape, 1f);
+            bulletNode.setForceFactor(8f);
+            bulletNode.setExplosionRadius(20f);
+            bulletNode.setCcdMotionThreshold(0.001f);
+            bulletNode.setLinearVelocity(cam.getDirection().mult(180f));
+            bulletGeometry.addControl(bulletNode);
+            rootNode.attachChild(bulletGeometry);
+            physicsSpace.add(bulletNode);
+        }
+        if (name.equals("bullet+") && isPressed) {
+            bulletSize += 0.1f;
+        }
+        if (name.equals("bullet-") && isPressed) {
+            bulletSize -= 0.1f;
+        }
+        if (name.equals("shoot") && !isPressed) {
+            Geometry bulletg = new Geometry("bullet", bullet);
+            bulletg.setMaterial(matBullet);
+            bulletg.setLocalTranslation(cam.getLocation());
+            bulletg.setLocalScale(bulletSize);
+            bulletCollisionShape = new SphereCollisionShape(bulletSize);
+            RigidBodyControl bulletNode = new RigidBodyControl(bulletCollisionShape, bulletSize * 10f);
+            bulletNode.setCcdMotionThreshold(0.001f);
+            bulletNode.setLinearVelocity(cam.getDirection().mult(80f));
+            bulletg.addControl(bulletNode);
+            rootNode.attachChild(bulletg);
+            physicsSpace.add(bulletNode);
+        }
+        if (name.equals("stop") && isPressed) {
+            ragdoll.setEnabled(!ragdoll.isEnabled());
+            ragdoll.setRagdollMode();
+        }
+        if (name.equals("toggle") && isPressed) {
+            Vector3f v = new Vector3f(model.getLocalTranslation());
+            v.y = 0f;
+            Quaternion q = new Quaternion();
+            float[] angles = new float[3];
+            model.getLocalRotation().toAngles(angles);
+            q.fromAngleAxis(angles[1], Vector3f.UNIT_Y);
+            Transform endModelTransform
+                    = new Transform(v, q, new Vector3f(1f, 1f, 1f));
+            if (angles[0] < 0f) {
+                composer.setCurrentAction("BackOnce");
+                ragdoll.blendToKinematicMode(0.5f, endModelTransform);
+            } else {
+                composer.setCurrentAction("FrontOnce");
+                ragdoll.blendToKinematicMode(0.5f, endModelTransform);
+            }
+        }
+    }
+
+    @Override
+    public void simpleInitApp() {
+        flyCam.setMoveSpeed(50f);
+        cam.setLocation(new Vector3f(0.3f, 6.7f, 22.3f));
+        cam.setRotation(new Quaternion(-2E-4f, 0.993025f, -0.1179f, -0.0019f));
+
+        initCrossHairs();
+        initMaterial();
+        setupKeys();
+        setupLight();
+
+        BulletAppState bulletAppState = new BulletAppState();
+        stateManager.attach(bulletAppState);
+        //bulletAppState.setDebugEnabled(true);
+        physicsSpace = bulletAppState.getPhysicsSpace();
+
+        bullet = new Sphere(32, 32, 1f, true, false);
+        bullet.setTextureMode(TextureMode.Projected);
+        bulletCollisionShape = new SphereCollisionShape(1f);
+
+        PhysicsTestHelper.createPhysicsTestWorld(rootNode, assetManager,
+                physicsSpace);
+
+        model = (Node) assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml");
+        rootNode.attachChild(model);
+
+        composer = model.getControl(AnimComposer.class);
+        composer.setCurrentAction("Dance");
+
+        Action standUpFront = composer.action("StandUpFront");
+        composer.actionSequence("FrontOnce",
+                standUpFront, Tweens.callMethod(this, "onStandDone"));
+        Action standUpBack = composer.action("StandUpBack");
+        composer.actionSequence("BackOnce",
+                standUpBack, Tweens.callMethod(this, "onStandDone"));
+
+        ragdoll = new DynamicAnimControl();
+        TestRagdollCharacter.setupSinbad(ragdoll);
+        model.addControl(ragdoll);
+        physicsSpace.add(ragdoll);
+        ragdoll.addCollisionListener(this);
+    }
+
+    @Override
+    public void collide(PhysicsLink bone, PhysicsCollisionObject object,
+            PhysicsCollisionEvent event) {
+        if (object.getUserObject() != null
+                && object.getUserObject() instanceof Geometry) {
+            Geometry geom = (Geometry) object.getUserObject();
+            if ("bullet".equals(geom.getName())) {
+                ragdoll.setRagdollMode();
+            }
+        }
+    }
+
+    private void initCrossHairs() {
+        guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
+        BitmapText ch = new BitmapText(guiFont);
+        ch.setSize(guiFont.getCharSet().getRenderedSize() * 2f);
+        ch.setText("+"); // crosshairs
+        ch.setLocalTranslation( // center
+                settings.getWidth() / 2f - guiFont.getCharSet().getRenderedSize() / 3f * 2f,
+                settings.getHeight() / 2f + ch.getLineHeight() / 2f, 0f);
+        guiNode.attachChild(ch);
+    }
+
+    private void initMaterial() {
+        matBullet = new Material(assetManager,
+                "Common/MatDefs/Misc/Unshaded.j3md");
+        TextureKey key2 = new TextureKey("Textures/Terrain/Rock/Rock.PNG");
+        key2.setGenerateMips(true);
+        Texture tex2 = assetManager.loadTexture(key2);
+        matBullet.setTexture("ColorMap", tex2);
+    }
+
+    private void setupKeys() {
+        inputManager.addMapping("boom", new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
+        inputManager.addMapping("bullet+", new KeyTrigger(KeyInput.KEY_PERIOD));
+        inputManager.addMapping("bullet-", new KeyTrigger(KeyInput.KEY_COMMA));
+        inputManager.addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
+        inputManager.addMapping("stop", new KeyTrigger(KeyInput.KEY_H));
+        inputManager.addMapping("toggle", new KeyTrigger(KeyInput.KEY_SPACE));
+
+        inputManager.addListener(this,
+                "boom", "bullet-", "bullet+", "shoot", "stop", "toggle");
+
+    }
+
+    private void setupLight() {
+        DirectionalLight dl = new DirectionalLight();
+        dl.setDirection(new Vector3f(-0.1f, -0.7f, -1f).normalizeLocal());
+        dl.setColor(new ColorRGBA(1f, 1f, 1f, 1f));
+        rootNode.addLight(dl);
+    }
+}

+ 251 - 0
jme3-examples/src/main/java/jme3test/bullet/TestBrickTower.java

@@ -0,0 +1,251 @@
+/*
+ * 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 jme3test.bullet;
+
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.asset.TextureKey;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.SphereCollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.font.BitmapText;
+import com.jme3.input.MouseInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.MouseButtonTrigger;
+import com.jme3.material.Material;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.queue.RenderQueue.ShadowMode;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Box;
+import com.jme3.scene.shape.Sphere;
+import com.jme3.scene.shape.Sphere.TextureMode;
+import com.jme3.texture.Texture;
+import com.jme3.texture.Texture.WrapMode;
+
+/**
+ *
+ * @author double1984 (tower mod by atom)
+ */
+public class TestBrickTower extends SimpleApplication {
+
+    final private int bricksPerLayer = 8;
+    final private int brickLayers = 30;
+
+    final private static float brickWidth = .75f, brickHeight = .25f, brickDepth = .25f;
+    final private float radius = 3f;
+    private float angle = 0;
+
+
+    private Material mat;
+    private Material mat2;
+    private Material mat3;
+    private Sphere bullet;
+    private Box brick;
+    private SphereCollisionShape bulletCollisionShape;
+
+    private BulletAppState bulletAppState;
+
+    public static void main(String args[]) {
+        TestBrickTower f = new TestBrickTower();
+        f.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        bulletAppState = new BulletAppState();
+        bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL);
+     //   bulletAppState.setEnabled(false);
+        stateManager.attach(bulletAppState);
+        bullet = new Sphere(32, 32, 0.4f, true, false);
+        bullet.setTextureMode(TextureMode.Projected);
+        bulletCollisionShape = new SphereCollisionShape(0.4f);
+
+        brick = new Box(brickWidth, brickHeight, brickDepth);
+        brick.scaleTextureCoordinates(new Vector2f(1f, .5f));
+        initMaterial();
+        initTower();
+        initFloor();
+        initCrossHairs();
+        this.cam.setLocation(new Vector3f(0, 25f, 8f));
+        cam.lookAt(Vector3f.ZERO, new Vector3f(0, 1, 0));
+        cam.setFrustumFar(80);
+        inputManager.addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
+        inputManager.addListener(actionListener, "shoot");
+        rootNode.setShadowMode(ShadowMode.Off);
+    }
+
+    private PhysicsSpace getPhysicsSpace() {
+        return bulletAppState.getPhysicsSpace();
+    }
+    final private ActionListener actionListener = new ActionListener() {
+
+        @Override
+        public void onAction(String name, boolean keyPressed, float tpf) {
+            if (name.equals("shoot") && !keyPressed) {
+                Geometry bulletGeometry = new Geometry("bullet", bullet);
+                bulletGeometry.setMaterial(mat2);
+                bulletGeometry.setShadowMode(ShadowMode.CastAndReceive);
+                bulletGeometry.setLocalTranslation(cam.getLocation());
+                RigidBodyControl bulletNode = new BombControl(assetManager, bulletCollisionShape, 1);
+//                RigidBodyControl bulletNode = new RigidBodyControl(bulletCollisionShape, 1);
+                bulletNode.setLinearVelocity(cam.getDirection().mult(25));
+                bulletGeometry.addControl(bulletNode);
+                rootNode.attachChild(bulletGeometry);
+                getPhysicsSpace().add(bulletNode);
+            }
+        }
+    };
+
+    public void initTower() {
+        double tempX = 0;
+        double tempY = 0;
+        double tempZ = 0;
+        angle = 0f;
+        for (int i = 0; i < brickLayers; i++){
+            // Increment rows
+            if(i!=0)
+                tempY+=brickHeight*2;
+            else
+                tempY=brickHeight;
+            // Alternate brick seams
+            angle = 360.0f / bricksPerLayer * i/2f;
+            for (int j = 0; j < bricksPerLayer; j++){
+              tempZ = Math.cos(Math.toRadians(angle))*radius;
+              tempX = Math.sin(Math.toRadians(angle))*radius;
+              System.out.println("x="+((float)(tempX))+" y="+((float)(tempY))+" z="+(float)(tempZ));
+              Vector3f vt = new Vector3f((float)(tempX), (float)(tempY), (float)(tempZ));
+              // Add crenelation
+              if (i==brickLayers-1){
+                if (j%2 == 0){
+                    addBrick(vt);
+                }
+              }
+              // Create main tower
+              else {
+                addBrick(vt);
+              }
+              angle += 360.0/bricksPerLayer;
+            }
+          }
+
+    }
+
+    public void initFloor() {
+        Box floorBox = new Box(10f, 0.1f, 5f);
+        floorBox.scaleTextureCoordinates(new Vector2f(3, 6));
+
+        Geometry floor = new Geometry("floor", floorBox);
+        floor.setMaterial(mat3);
+        floor.setShadowMode(ShadowMode.Receive);
+        floor.setLocalTranslation(0, 0, 0);
+        floor.addControl(new RigidBodyControl(0));
+        this.rootNode.attachChild(floor);
+        this.getPhysicsSpace().add(floor);
+    }
+
+    public void initMaterial() {
+        mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        TextureKey key = new TextureKey("Textures/Terrain/BrickWall/BrickWall.jpg");
+        key.setGenerateMips(true);
+        Texture tex = assetManager.loadTexture(key);
+        mat.setTexture("ColorMap", tex);
+
+        mat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        TextureKey key2 = new TextureKey("Textures/Terrain/Rock/Rock.PNG");
+        key2.setGenerateMips(true);
+        Texture tex2 = assetManager.loadTexture(key2);
+        mat2.setTexture("ColorMap", tex2);
+
+        mat3 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        TextureKey key3 = new TextureKey("Textures/Terrain/Pond/Pond.jpg");
+        key3.setGenerateMips(true);
+        Texture tex3 = assetManager.loadTexture(key3);
+        tex3.setWrap(WrapMode.Repeat);
+        mat3.setTexture("ColorMap", tex3);
+    }
+
+    public void addBrick(Vector3f ori) {
+        Geometry brickGeometry = new Geometry("brick", brick);
+        brickGeometry.setMaterial(mat);
+        brickGeometry.setLocalTranslation(ori);
+        brickGeometry.rotate(0f, (float)Math.toRadians(angle) , 0f );
+        brickGeometry.addControl(new RigidBodyControl(1.5f));
+        brickGeometry.setShadowMode(ShadowMode.CastAndReceive);
+        brickGeometry.getControl(RigidBodyControl.class).setFriction(1.6f);
+        this.rootNode.attachChild(brickGeometry);
+        this.getPhysicsSpace().add(brickGeometry);
+    }
+
+    protected void initCrossHairs() {
+        guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
+        BitmapText ch = new BitmapText(guiFont);
+        ch.setSize(guiFont.getCharSet().getRenderedSize() * 2);
+        ch.setText("+"); // crosshairs
+        ch.setLocalTranslation( // center
+                settings.getWidth() / 2 - guiFont.getCharSet().getRenderedSize() / 3 * 2,
+                settings.getHeight() / 2 + ch.getLineHeight() / 2, 0);
+        guiNode.attachChild(ch);
+    }
+}

+ 204 - 0
jme3-examples/src/main/java/jme3test/bullet/TestBrickWall.java

@@ -0,0 +1,204 @@
+/*
+ * 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 jme3test.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.asset.TextureKey;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.BoxCollisionShape;
+import com.jme3.bullet.collision.shapes.SphereCollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.font.BitmapText;
+import com.jme3.input.KeyInput;
+import com.jme3.input.MouseInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.input.controls.MouseButtonTrigger;
+import com.jme3.material.Material;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.queue.RenderQueue.ShadowMode;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Box;
+import com.jme3.scene.shape.Sphere;
+import com.jme3.scene.shape.Sphere.TextureMode;
+import com.jme3.texture.Texture;
+import com.jme3.texture.Texture.WrapMode;
+
+/**
+ *
+ * @author double1984
+ */
+public class TestBrickWall extends SimpleApplication {
+
+    final private static float bLength = 0.48f;
+    final private static float bWidth = 0.24f;
+    final private static float bHeight = 0.12f;
+    private Material mat;
+    private Material mat2;
+    private Material mat3;
+    private static Sphere bullet;
+    private static Box brick;
+
+    private BulletAppState bulletAppState;
+
+    public static void main(String args[]) {
+        TestBrickWall f = new TestBrickWall();
+        f.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        
+        bulletAppState = new BulletAppState();
+        bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL);
+        stateManager.attach(bulletAppState);
+
+        bullet = new Sphere(32, 32, 0.4f, true, false);
+        bullet.setTextureMode(TextureMode.Projected);
+        brick = new Box(bLength, bHeight, bWidth);
+        brick.scaleTextureCoordinates(new Vector2f(1f, .5f));
+
+        initMaterial();
+        initWall();
+        initFloor();
+        initCrossHairs();
+        this.cam.setLocation(new Vector3f(0, 6f, 6f));
+        cam.lookAt(Vector3f.ZERO, new Vector3f(0, 1, 0));
+        cam.setFrustumFar(15);
+        inputManager.addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
+        inputManager.addListener(actionListener, "shoot");
+        inputManager.addMapping("gc", new KeyTrigger(KeyInput.KEY_X));
+        inputManager.addListener(actionListener, "gc");
+
+        rootNode.setShadowMode(ShadowMode.Off);
+    }
+
+    private PhysicsSpace getPhysicsSpace() {
+        return bulletAppState.getPhysicsSpace();
+    }
+    final private ActionListener actionListener = new ActionListener() {
+
+        @Override
+        public void onAction(String name, boolean keyPressed, float tpf) {
+            if (name.equals("shoot") && !keyPressed) {
+                Geometry bulletGeometry = new Geometry("bullet", bullet);
+                bulletGeometry.setMaterial(mat2);
+                bulletGeometry.setShadowMode(ShadowMode.CastAndReceive);
+                bulletGeometry.setLocalTranslation(cam.getLocation());
+                
+                SphereCollisionShape bulletCollisionShape = new SphereCollisionShape(0.4f);
+                RigidBodyControl bulletNode = new BombControl(assetManager, bulletCollisionShape, 1);
+//                RigidBodyControl bulletNode = new RigidBodyControl(bulletCollisionShape, 1);
+                bulletNode.setLinearVelocity(cam.getDirection().mult(25));
+                bulletGeometry.addControl(bulletNode);
+                rootNode.attachChild(bulletGeometry);
+                getPhysicsSpace().add(bulletNode);
+            }
+            if (name.equals("gc") && !keyPressed) {
+                System.gc();
+            }
+        }
+    };
+
+    public void initWall() {
+        float startX = bLength / 4;
+        float height = 0;
+        for (int j = 0; j < 15; j++) {
+            for (int i = 0; i < 4; i++) {
+                Vector3f vt = new Vector3f(i * bLength * 2 + startX, bHeight + height, 0);
+                addBrick(vt);
+            }
+            startX = -startX;
+            height += 2 * bHeight;
+        }
+    }
+
+    public void initFloor() {
+        Box floorBox = new Box(10f, 0.1f, 5f);
+        floorBox.scaleTextureCoordinates(new Vector2f(3, 6));
+
+        Geometry floor = new Geometry("floor", floorBox);
+        floor.setMaterial(mat3);
+        floor.setShadowMode(ShadowMode.Receive);
+        floor.setLocalTranslation(0, -0.1f, 0);
+        floor.addControl(new RigidBodyControl(new BoxCollisionShape(new Vector3f(10f, 0.1f, 5f)), 0));
+        this.rootNode.attachChild(floor);
+        this.getPhysicsSpace().add(floor);
+    }
+
+    public void initMaterial() {
+        mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        TextureKey key = new TextureKey("Textures/Terrain/BrickWall/BrickWall.jpg");
+        key.setGenerateMips(true);
+        Texture tex = assetManager.loadTexture(key);
+        mat.setTexture("ColorMap", tex);
+
+        mat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        TextureKey key2 = new TextureKey("Textures/Terrain/Rock/Rock.PNG");
+        key2.setGenerateMips(true);
+        Texture tex2 = assetManager.loadTexture(key2);
+        mat2.setTexture("ColorMap", tex2);
+
+        mat3 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        TextureKey key3 = new TextureKey("Textures/Terrain/Pond/Pond.jpg");
+        key3.setGenerateMips(true);
+        Texture tex3 = assetManager.loadTexture(key3);
+        tex3.setWrap(WrapMode.Repeat);
+        mat3.setTexture("ColorMap", tex3);
+    }
+
+    public void addBrick(Vector3f ori) {
+
+        Geometry brickGeometry = new Geometry("brick", brick);
+        brickGeometry.setMaterial(mat);
+        brickGeometry.setLocalTranslation(ori);
+        //for geometry with sphere mesh the physics system automatically uses a sphere collision shape
+        brickGeometry.addControl(new RigidBodyControl(1.5f));
+        brickGeometry.setShadowMode(ShadowMode.CastAndReceive);
+        brickGeometry.getControl(RigidBodyControl.class).setFriction(0.6f);
+        this.rootNode.attachChild(brickGeometry);
+        this.getPhysicsSpace().add(brickGeometry);
+    }
+
+    protected void initCrossHairs() {
+        guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
+        BitmapText ch = new BitmapText(guiFont);
+        ch.setSize(guiFont.getCharSet().getRenderedSize() * 2);
+        ch.setText("+"); // crosshairs
+        ch.setLocalTranslation( // center
+                settings.getWidth() / 2 - guiFont.getCharSet().getRenderedSize() / 3 * 2,
+                settings.getHeight() / 2 + ch.getLineHeight() / 2, 0);
+        guiNode.attachChild(ch);
+    }
+}

+ 153 - 0
jme3-examples/src/main/java/jme3test/bullet/TestCcd.java

@@ -0,0 +1,153 @@
+/*
+ * Copyright (c) 2009-2020 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.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.BoxCollisionShape;
+import com.jme3.bullet.collision.shapes.MeshCollisionShape;
+import com.jme3.bullet.collision.shapes.SphereCollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.input.MouseInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.MouseButtonTrigger;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.queue.RenderQueue.ShadowMode;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.shape.Box;
+import com.jme3.scene.shape.Sphere;
+import com.jme3.scene.shape.Sphere.TextureMode;
+
+/**
+ *
+ * @author normenhansen
+ */
+public class TestCcd extends SimpleApplication implements ActionListener {
+
+    private Material mat;
+    private Material mat2;
+    private Sphere bullet;
+    private SphereCollisionShape bulletCollisionShape;
+    private BulletAppState bulletAppState;
+
+    public static void main(String[] args) {
+        TestCcd app = new TestCcd();
+        app.start();
+    }
+
+    private void setupKeys() {
+        inputManager.addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
+        inputManager.addMapping("shoot2", new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
+        inputManager.addListener(this, "shoot");
+        inputManager.addListener(this, "shoot2");
+    }
+
+    @Override
+    public void simpleInitApp() {
+        bulletAppState = new BulletAppState();
+        stateManager.attach(bulletAppState);
+        bulletAppState.setDebugEnabled(true);
+        bullet = new Sphere(32, 32, 0.4f, true, false);
+        bullet.setTextureMode(TextureMode.Projected);
+        bulletCollisionShape = new SphereCollisionShape(0.1f);
+        setupKeys();
+
+        mat = new Material(getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
+        mat.getAdditionalRenderState().setWireframe(true);
+        mat.setColor("Color", ColorRGBA.Green);
+
+        mat2 = new Material(getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
+        mat2.getAdditionalRenderState().setWireframe(true);
+        mat2.setColor("Color", ColorRGBA.Red);
+
+        // An obstacle mesh, does not move (mass=0)
+        Node node2 = new Node();
+        node2.setName("mesh");
+        node2.setLocalTranslation(new Vector3f(2.5f, 0, 0f));
+        node2.addControl(new RigidBodyControl(new MeshCollisionShape(new Box(4, 4, 0.1f)), 0));
+        rootNode.attachChild(node2);
+        getPhysicsSpace().add(node2);
+
+        // The floor, does not move (mass=0)
+        Node node3 = new Node();
+        node3.setLocalTranslation(new Vector3f(0f, -6, 0f));
+        node3.addControl(new RigidBodyControl(new BoxCollisionShape(new Vector3f(100, 1, 100)), 0));
+        rootNode.attachChild(node3);
+        getPhysicsSpace().add(node3);
+
+    }
+
+    private PhysicsSpace getPhysicsSpace() {
+        return bulletAppState.getPhysicsSpace();
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        //TODO: add update code
+    }
+
+    @Override
+    public void simpleRender(RenderManager rm) {
+        //TODO: add render code
+    }
+
+    @Override
+    public void onAction(String binding, boolean value, float tpf) {
+        if (binding.equals("shoot") && !value) {
+            Geometry bulletGeometry = new Geometry("bullet", bullet);
+            bulletGeometry.setMaterial(mat);
+            bulletGeometry.setName("bullet");
+            bulletGeometry.setLocalTranslation(cam.getLocation());
+            bulletGeometry.setShadowMode(ShadowMode.CastAndReceive);
+            bulletGeometry.addControl(new RigidBodyControl(bulletCollisionShape, 1));
+            bulletGeometry.getControl(RigidBodyControl.class).setCcdMotionThreshold(0.1f);
+            bulletGeometry.getControl(RigidBodyControl.class).setLinearVelocity(cam.getDirection().mult(40));
+            rootNode.attachChild(bulletGeometry);
+            getPhysicsSpace().add(bulletGeometry);
+        } else if (binding.equals("shoot2") && !value) {
+            Geometry bulletGeometry = new Geometry("bullet", bullet);
+            bulletGeometry.setMaterial(mat2);
+            bulletGeometry.setName("bullet");
+            bulletGeometry.setLocalTranslation(cam.getLocation());
+            bulletGeometry.setShadowMode(ShadowMode.CastAndReceive);
+            bulletGeometry.addControl(new RigidBodyControl(bulletCollisionShape, 1));
+            bulletGeometry.getControl(RigidBodyControl.class).setLinearVelocity(cam.getDirection().mult(40));
+            rootNode.attachChild(bulletGeometry);
+            getPhysicsSpace().add(bulletGeometry);
+        }
+    }
+}

+ 107 - 0
jme3-examples/src/main/java/jme3test/bullet/TestCollisionGroups.java

@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package jme3test.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.PhysicsCollisionObject;
+import com.jme3.bullet.collision.shapes.MeshCollisionShape;
+import com.jme3.bullet.collision.shapes.SphereCollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.RenderManager;
+import com.jme3.scene.Node;
+import com.jme3.scene.shape.Box;
+import com.jme3.scene.shape.Sphere;
+
+/**
+ *
+ * @author normenhansen
+ */
+public class TestCollisionGroups extends SimpleApplication {
+
+    private BulletAppState bulletAppState;
+
+    public static void main(String[] args) {
+        TestCollisionGroups app = new TestCollisionGroups();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        bulletAppState = new BulletAppState();
+        stateManager.attach(bulletAppState);
+        bulletAppState.setDebugEnabled(true);
+
+        // Add a physics sphere to the world
+        Node physicsSphere = PhysicsTestHelper.createPhysicsTestNode(assetManager, new SphereCollisionShape(1), 1);
+        physicsSphere.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(3, 6, 0));
+        rootNode.attachChild(physicsSphere);
+        getPhysicsSpace().add(physicsSphere);
+
+        // Add a physics sphere to the world
+        Node physicsSphere2 = PhysicsTestHelper.createPhysicsTestNode(assetManager, new SphereCollisionShape(1), 1);
+        physicsSphere2.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(4, 8, 0));
+        physicsSphere2.getControl(RigidBodyControl.class).addCollideWithGroup(PhysicsCollisionObject.COLLISION_GROUP_02);
+        rootNode.attachChild(physicsSphere2);
+        getPhysicsSpace().add(physicsSphere2);
+
+        // an obstacle mesh, does not move (mass=0)
+        Node node2 = PhysicsTestHelper.createPhysicsTestNode(assetManager, new MeshCollisionShape(new Sphere(16, 16, 1.2f)), 0);
+        node2.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(2.5f, -4, 0f));
+        node2.getControl(RigidBodyControl.class).setCollisionGroup(PhysicsCollisionObject.COLLISION_GROUP_02);
+        node2.getControl(RigidBodyControl.class).setCollideWithGroups(PhysicsCollisionObject.COLLISION_GROUP_02);
+        rootNode.attachChild(node2);
+        getPhysicsSpace().add(node2);
+
+        // the floor, does not move (mass=0)
+        Node node3 = PhysicsTestHelper.createPhysicsTestNode(assetManager, new MeshCollisionShape(new Box(100f, 0.2f, 100f)), 0);
+        node3.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(0f, -6, 0f));
+        rootNode.attachChild(node3);
+        getPhysicsSpace().add(node3);
+    }
+
+    private PhysicsSpace getPhysicsSpace() {
+        return bulletAppState.getPhysicsSpace();
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        //TODO: add update code
+    }
+
+    @Override
+    public void simpleRender(RenderManager rm) {
+        //TODO: add render code
+    }
+}

+ 95 - 0
jme3-examples/src/main/java/jme3test/bullet/TestCollisionListener.java

@@ -0,0 +1,95 @@
+/*
+ * 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 jme3test.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.PhysicsCollisionEvent;
+import com.jme3.bullet.collision.PhysicsCollisionListener;
+import com.jme3.renderer.RenderManager;
+import com.jme3.scene.shape.Sphere;
+import com.jme3.scene.shape.Sphere.TextureMode;
+
+/**
+ *
+ * @author normenhansen
+ */
+public class TestCollisionListener extends SimpleApplication implements PhysicsCollisionListener {
+
+    private BulletAppState bulletAppState;
+
+    public static void main(String[] args) {
+        TestCollisionListener app = new TestCollisionListener();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        bulletAppState = new BulletAppState();
+        stateManager.attach(bulletAppState);
+        bulletAppState.setDebugEnabled(true);
+        Sphere bullet = new Sphere(32, 32, 0.4f, true, false);
+        bullet.setTextureMode(TextureMode.Projected);
+
+        PhysicsTestHelper.createPhysicsTestWorld(rootNode, assetManager, bulletAppState.getPhysicsSpace());
+        PhysicsTestHelper.createBallShooter(this, rootNode, bulletAppState.getPhysicsSpace());
+
+        // add ourselves as collision listener
+        getPhysicsSpace().addCollisionListener(this);
+    }
+
+    private PhysicsSpace getPhysicsSpace(){
+        return bulletAppState.getPhysicsSpace();
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        //TODO: add update code
+    }
+
+    @Override
+    public void simpleRender(RenderManager rm) {
+        //TODO: add render code
+    }
+
+    @Override
+    public void collision(PhysicsCollisionEvent event) {
+        if ("Box".equals(event.getNodeA().getName()) || "Box".equals(event.getNodeB().getName())) {
+            if ("bullet".equals(event.getNodeA().getName()) || "bullet".equals(event.getNodeB().getName())) {
+                fpsText.setText("You hit the box!");
+            }
+        }
+    }
+
+}

+ 137 - 0
jme3-examples/src/main/java/jme3test/bullet/TestCollisionShapeFactory.java

@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package jme3test.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.shape.Box;
+import com.jme3.scene.shape.Cylinder;
+import com.jme3.scene.shape.Torus;
+
+/**
+ * This is a basic Test of jbullet-jme functions
+ *
+ * @author normenhansen
+ */
+public class TestCollisionShapeFactory extends SimpleApplication {
+
+    private BulletAppState bulletAppState;
+    private Material mat1;
+    private Material mat2;
+    private Material mat3;
+
+    public static void main(String[] args) {
+        TestCollisionShapeFactory app = new TestCollisionShapeFactory();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        bulletAppState = new BulletAppState();
+        stateManager.attach(bulletAppState);
+        bulletAppState.setDebugEnabled(true);
+        createMaterial();
+
+        Node node = new Node("node1");
+        attachRandomGeometry(node, mat1);
+        randomizeTransform(node);
+
+        Node node2 = new Node("node2");
+        attachRandomGeometry(node2, mat2);
+        randomizeTransform(node2);
+
+        node.attachChild(node2);
+        rootNode.attachChild(node);
+
+        RigidBodyControl control = new RigidBodyControl(0);
+        node.addControl(control);
+        getPhysicsSpace().add(control);
+
+        //test single geometry too
+        Geometry myGeom = new Geometry("cylinder", new Cylinder(16, 16, 0.5f, 1));
+        myGeom.setMaterial(mat3);
+        randomizeTransform(myGeom);
+        rootNode.attachChild(myGeom);
+        RigidBodyControl control3 = new RigidBodyControl(0);
+        myGeom.addControl(control3);
+        getPhysicsSpace().add(control3);
+    }
+
+    private void attachRandomGeometry(Node node, Material mat) {
+        Box box = new Box(0.25f, 0.25f, 0.25f);
+        Torus torus = new Torus(16, 16, 0.2f, 0.8f);
+        Geometry[] boxes = new Geometry[]{
+            new Geometry("box1", box),
+            new Geometry("box2", box),
+            new Geometry("box3", box),
+            new Geometry("torus1", torus),
+            new Geometry("torus2", torus),
+            new Geometry("torus3", torus)
+        };
+        for (int i = 0; i < boxes.length; i++) {
+            Geometry geometry = boxes[i];
+            geometry.setLocalTranslation((float) Math.random() * 10 -10, (float) Math.random() * 10 -10, (float) Math.random() * 10 -10);
+            geometry.setLocalRotation(new Quaternion().fromAngles((float) Math.random() * FastMath.PI, (float) Math.random() * FastMath.PI, (float) Math.random() * FastMath.PI));
+            geometry.setLocalScale((float) Math.random() * 10 -10, (float) Math.random() * 10 -10, (float) Math.random() * 10 -10);
+            geometry.setMaterial(mat);
+            node.attachChild(geometry);
+        }
+    }
+
+    private void randomizeTransform(Spatial spat){
+        spat.setLocalTranslation((float) Math.random() * 10, (float) Math.random() * 10, (float) Math.random() * 10);
+        spat.setLocalTranslation((float) Math.random() * 10, (float) Math.random() * 10, (float) Math.random() * 10);
+        spat.setLocalScale((float) Math.random() * 2, (float) Math.random() * 2, (float) Math.random() * 2);
+    }
+
+    private void createMaterial() {
+        mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat1.setColor("Color", ColorRGBA.Green);
+        mat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat2.setColor("Color", ColorRGBA.Red);
+        mat3 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat3.setColor("Color", ColorRGBA.Yellow);
+    }
+
+    private PhysicsSpace getPhysicsSpace() {
+        return bulletAppState.getPhysicsSpace();
+    }
+}

+ 226 - 0
jme3-examples/src/main/java/jme3test/bullet/TestFancyCar.java

@@ -0,0 +1,226 @@
+/*
+ * 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 jme3test.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bounding.BoundingBox;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.CollisionShape;
+import com.jme3.bullet.control.VehicleControl;
+import com.jme3.bullet.util.CollisionShapeFactory;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.light.DirectionalLight;
+import com.jme3.math.FastMath;
+import com.jme3.math.Matrix3f;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.queue.RenderQueue.ShadowMode;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+
+public class TestFancyCar extends SimpleApplication implements ActionListener {
+
+    private BulletAppState bulletAppState;
+    private VehicleControl player;
+    private float steeringValue = 0;
+    private float accelerationValue = 0;
+    private Node carNode;
+
+    public static void main(String[] args) {
+        TestFancyCar app = new TestFancyCar();
+        app.start();
+    }
+
+    private void setupKeys() {
+        inputManager.addMapping("Lefts", new KeyTrigger(KeyInput.KEY_H));
+        inputManager.addMapping("Rights", new KeyTrigger(KeyInput.KEY_K));
+        inputManager.addMapping("Ups", new KeyTrigger(KeyInput.KEY_U));
+        inputManager.addMapping("Downs", new KeyTrigger(KeyInput.KEY_J));
+        inputManager.addMapping("Reset", new KeyTrigger(KeyInput.KEY_RETURN));
+        inputManager.addListener(this, "Lefts");
+        inputManager.addListener(this, "Rights");
+        inputManager.addListener(this, "Ups");
+        inputManager.addListener(this, "Downs");
+        inputManager.addListener(this, "Reset");
+    }
+
+    @Override
+    public void simpleInitApp() {
+        bulletAppState = new BulletAppState();
+        stateManager.attach(bulletAppState);
+        cam.setFrustumFar(150f);
+        flyCam.setMoveSpeed(10);
+
+        setupKeys();
+        PhysicsTestHelper.createPhysicsTestWorld(rootNode, assetManager, getPhysicsSpace());
+        buildPlayer();
+
+        DirectionalLight dl = new DirectionalLight();
+        dl.setDirection(new Vector3f(-0.5f, -1f, -0.3f).normalizeLocal());
+        rootNode.addLight(dl);
+    }
+
+    private PhysicsSpace getPhysicsSpace() {
+        return bulletAppState.getPhysicsSpace();
+    }
+
+    private Geometry findGeom(Spatial spatial, String name) {
+        if (spatial instanceof Node) {
+            Node node = (Node) spatial;
+            for (int i = 0; i < node.getQuantity(); i++) {
+                Spatial child = node.getChild(i);
+                Geometry result = findGeom(child, name);
+                if (result != null) {
+                    return result;
+                }
+            }
+        } else if (spatial instanceof Geometry) {
+            if (spatial.getName().startsWith(name)) {
+                return (Geometry) spatial;
+            }
+        }
+        return null;
+    }
+
+    private void buildPlayer() {
+        float stiffness = 120.0f;//200=f1 car
+        float compValue = 0.2f; //(lower than damp!)
+        float dampValue = 0.3f;
+        final float mass = 400;
+
+        // Load model and get chassis Geometry
+        carNode = (Node) assetManager.loadModel("Models/Ferrari/Car.scene");
+        carNode.setShadowMode(ShadowMode.Cast);
+        Geometry chassis = findGeom(carNode, "Car");
+
+        // Create a hull collision shape for the chassis
+        CollisionShape carHull = CollisionShapeFactory.createDynamicMeshShape(chassis);
+
+        // Create a vehicle control
+        player = new VehicleControl(carHull, mass);
+        carNode.addControl(player);
+
+        // Setting default values for wheels
+        player.setSuspensionCompression(compValue * 2.0f * FastMath.sqrt(stiffness));
+        player.setSuspensionDamping(dampValue * 2.0f * FastMath.sqrt(stiffness));
+        player.setSuspensionStiffness(stiffness);
+        player.setMaxSuspensionForce(10000);
+
+        // Create four wheels and add them at their locations.
+        // Note that our fancy car actually goes backward.
+        Vector3f wheelDirection = new Vector3f(0, -1, 0);
+        Vector3f wheelAxle = new Vector3f(-1, 0, 0);
+
+        Geometry wheel_fr = findGeom(carNode, "WheelFrontRight");
+        wheel_fr.center();
+        BoundingBox box = (BoundingBox) wheel_fr.getModelBound();
+        float wheelRadius = box.getYExtent();
+        float back_wheel_h = (wheelRadius * 1.7f) - 1f;
+        float front_wheel_h = (wheelRadius * 1.9f) - 1f;
+        player.addWheel(wheel_fr.getParent(), box.getCenter().add(0, -front_wheel_h, 0),
+                wheelDirection, wheelAxle, 0.2f, wheelRadius, true);
+
+        Geometry wheel_fl = findGeom(carNode, "WheelFrontLeft");
+        wheel_fl.center();
+        box = (BoundingBox) wheel_fl.getModelBound();
+        player.addWheel(wheel_fl.getParent(), box.getCenter().add(0, -front_wheel_h, 0),
+                wheelDirection, wheelAxle, 0.2f, wheelRadius, true);
+
+        Geometry wheel_br = findGeom(carNode, "WheelBackRight");
+        wheel_br.center();
+        box = (BoundingBox) wheel_br.getModelBound();
+        player.addWheel(wheel_br.getParent(), box.getCenter().add(0, -back_wheel_h, 0),
+                wheelDirection, wheelAxle, 0.2f, wheelRadius, false);
+
+        Geometry wheel_bl = findGeom(carNode, "WheelBackLeft");
+        wheel_bl.center();
+        box = (BoundingBox) wheel_bl.getModelBound();
+        player.addWheel(wheel_bl.getParent(), box.getCenter().add(0, -back_wheel_h, 0),
+                wheelDirection, wheelAxle, 0.2f, wheelRadius, false);
+
+        player.getWheel(2).setFrictionSlip(4);
+        player.getWheel(3).setFrictionSlip(4);
+
+        rootNode.attachChild(carNode);
+        getPhysicsSpace().add(player);
+    }
+
+    @Override
+    public void onAction(String binding, boolean value, float tpf) {
+        if (binding.equals("Lefts")) {
+            if (value) {
+                steeringValue += .5f;
+            } else {
+                steeringValue -= .5f;
+            }
+            player.steer(steeringValue);
+        } else if (binding.equals("Rights")) {
+            if (value) {
+                steeringValue -= .5f;
+            } else {
+                steeringValue += .5f;
+            }
+            player.steer(steeringValue);
+        } // Note that our fancy car actually goes backward.
+        else if (binding.equals("Ups")) {
+            if (value) {
+                accelerationValue -= 800;
+            } else {
+                accelerationValue += 800;
+            }
+            player.accelerate(accelerationValue);
+        } else if (binding.equals("Downs")) {
+            if (value) {
+                player.brake(40f);
+            } else {
+                player.brake(0f);
+            }
+        } else if (binding.equals("Reset")) {
+            if (value) {
+                System.out.println("Reset");
+                player.setPhysicsLocation(Vector3f.ZERO);
+                player.setPhysicsRotation(new Matrix3f());
+                player.setLinearVelocity(Vector3f.ZERO);
+                player.setAngularVelocity(Vector3f.ZERO);
+                player.resetSuspension();
+            }
+        }
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        cam.lookAt(carNode.getWorldTranslation(), Vector3f.UNIT_Y);
+    }
+}

+ 117 - 0
jme3-examples/src/main/java/jme3test/bullet/TestGhostObject.java

@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package jme3test.bullet;
+
+import com.jme3.app.Application;
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.BoxCollisionShape;
+import com.jme3.bullet.collision.shapes.CollisionShape;
+import com.jme3.bullet.control.GhostControl;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Node;
+import com.jme3.scene.shape.Box;
+
+/**
+ *
+ * @author tim8dev [at] gmail [dot com]
+ */
+public class TestGhostObject extends SimpleApplication {
+
+    private BulletAppState bulletAppState;
+    private GhostControl ghostControl;
+
+    public static void main(String[] args) {
+        Application app = new TestGhostObject();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        bulletAppState = new BulletAppState();
+        stateManager.attach(bulletAppState);
+        bulletAppState.setDebugEnabled(true);
+
+        // Mesh to be shared across several boxes.
+        Box boxGeom = new Box(1f, 1f, 1f);
+        // CollisionShape to be shared across several boxes.
+        CollisionShape shape = new BoxCollisionShape(new Vector3f(1, 1, 1));
+
+        Node physicsBox = PhysicsTestHelper.createPhysicsTestNode(assetManager, shape, 1);
+        physicsBox.setName("box0");
+        physicsBox.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(.6f, 4, .5f));
+        rootNode.attachChild(physicsBox);
+        getPhysicsSpace().add(physicsBox);
+
+        Node physicsBox1 = PhysicsTestHelper.createPhysicsTestNode(assetManager, shape, 1);
+        physicsBox1.setName("box1");
+        physicsBox1.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(0, 40, 0));
+        rootNode.attachChild(physicsBox1);
+        getPhysicsSpace().add(physicsBox1);
+
+        Node physicsBox2 = PhysicsTestHelper.createPhysicsTestNode(assetManager, new BoxCollisionShape(new Vector3f(1, 1, 1)), 1);
+        physicsBox2.setName("box0");
+        physicsBox2.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(.5f, 80, -.8f));
+        rootNode.attachChild(physicsBox2);
+        getPhysicsSpace().add(physicsBox2);
+
+        // the floor, does not move (mass=0)
+        Node node = PhysicsTestHelper.createPhysicsTestNode(assetManager, new BoxCollisionShape(new Vector3f(100, 1, 100)), 0);
+        node.setName("floor");
+        node.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(0f, -6, 0f));
+        rootNode.attachChild(node);
+        getPhysicsSpace().add(node);
+
+        initGhostObject();
+    }
+
+    private PhysicsSpace getPhysicsSpace(){
+        return bulletAppState.getPhysicsSpace();
+    }
+
+    private void initGhostObject() {
+        Vector3f halfExtents = new Vector3f(3, 4.2f, 1);
+        ghostControl = new GhostControl(new BoxCollisionShape(halfExtents));
+        Node node=new Node("Ghost Object");
+        node.addControl(ghostControl);
+        rootNode.attachChild(node);
+        getPhysicsSpace().add(ghostControl);
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        fpsText.setText("Overlapping objects: " + ghostControl.getOverlappingObjects().toString());
+    }
+}

+ 304 - 0
jme3-examples/src/main/java/jme3test/bullet/TestHoveringTank.java

@@ -0,0 +1,304 @@
+/*
+ * 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 jme3test.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bounding.BoundingBox;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.PhysicsCollisionObject;
+import com.jme3.bullet.collision.shapes.BoxCollisionShape;
+import com.jme3.bullet.collision.shapes.CollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.bullet.util.CollisionShapeFactory;
+import com.jme3.input.ChaseCamera;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.AnalogListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.math.*;
+import com.jme3.renderer.Camera;
+import com.jme3.renderer.queue.RenderQueue.ShadowMode;
+import com.jme3.scene.Spatial;
+import com.jme3.shadow.DirectionalLightShadowRenderer;
+import com.jme3.shadow.EdgeFilteringMode;
+import com.jme3.terrain.geomipmap.TerrainLodControl;
+import com.jme3.terrain.geomipmap.TerrainQuad;
+import com.jme3.terrain.heightmap.AbstractHeightMap;
+import com.jme3.terrain.heightmap.ImageBasedHeightMap;
+import com.jme3.texture.Texture;
+import com.jme3.texture.Texture.WrapMode;
+import com.jme3.util.SkyFactory;
+import com.jme3.util.SkyFactory.EnvMapType;
+import java.util.ArrayList;
+import java.util.List;
+
+public class TestHoveringTank extends SimpleApplication implements AnalogListener,
+        ActionListener {
+
+    private BulletAppState bulletAppState;
+    private PhysicsHoverControl hoverControl;
+    private Spatial spaceCraft;
+    /**
+     * initial location of the tank (in world/physics-space coordinates)
+     */
+    final private Vector3f startLocation = new Vector3f(-140f, 50f, -23f);
+    /**
+     * initial orientation of the tank (in world/physics-space coordinates)
+     */
+    final private Quaternion startOrientation
+            = new Quaternion(new float[]{0f, 0.01f, 0f});
+
+    public static void main(String[] args) {
+        TestHoveringTank app = new TestHoveringTank();
+        app.start();
+    }
+
+    private PhysicsSpace getPhysicsSpace() {
+        return bulletAppState.getPhysicsSpace();
+    }
+
+    private void setupKeys() {
+        inputManager.addMapping("Lefts", new KeyTrigger(KeyInput.KEY_A));
+        inputManager.addMapping("Rights", new KeyTrigger(KeyInput.KEY_D));
+        inputManager.addMapping("Ups", new KeyTrigger(KeyInput.KEY_W));
+        inputManager.addMapping("Downs", new KeyTrigger(KeyInput.KEY_S));
+        inputManager.addMapping("Space", new KeyTrigger(KeyInput.KEY_SPACE));
+        inputManager.addMapping("Reset", new KeyTrigger(KeyInput.KEY_RETURN));
+        inputManager.addListener(this, "Lefts");
+        inputManager.addListener(this, "Rights");
+        inputManager.addListener(this, "Ups");
+        inputManager.addListener(this, "Downs");
+        inputManager.addListener(this, "Space");
+        inputManager.addListener(this, "Reset");
+    }
+
+    @Override
+    public void simpleInitApp() {
+        bulletAppState = new BulletAppState();
+        bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL);
+        stateManager.attach(bulletAppState);
+        bulletAppState.getPhysicsSpace().setAccuracy(1f/30f);
+        rootNode.attachChild(SkyFactory.createSky(assetManager, 
+                "Textures/Sky/Bright/BrightSky.dds", EnvMapType.CubeMap));
+
+        DirectionalLightShadowRenderer dlsr 
+                = new DirectionalLightShadowRenderer(assetManager, 2048, 3);
+        dlsr.setLambda(0.55f);
+        dlsr.setShadowIntensity(0.6f);
+        dlsr.setEdgeFilteringMode(EdgeFilteringMode.Bilinear);
+        viewPort.addProcessor(dlsr);
+
+        setupKeys();
+        createTerrain();
+        buildPlayer();
+
+        DirectionalLight dl = new DirectionalLight();
+        dlsr.setLight(dl);
+        dl.setColor(new ColorRGBA(1.0f, 0.94f, 0.8f, 1f).multLocal(1.3f));
+        dl.setDirection(new Vector3f(-0.5f, -0.3f, -0.3f).normalizeLocal());
+        rootNode.addLight(dl);
+
+        Vector3f lightDir2 = new Vector3f(0.70518064f, 0.5902297f, -0.39287305f);
+        DirectionalLight dl2 = new DirectionalLight();
+        dl2.setColor(new ColorRGBA(0.7f, 0.85f, 1.0f, 1f));
+        dl2.setDirection(lightDir2);
+        rootNode.addLight(dl2);
+    }
+
+    private void buildPlayer() {
+        spaceCraft = assetManager.loadModel("Models/HoverTank/Tank2.mesh.xml");
+        CollisionShape colShape = CollisionShapeFactory.createDynamicMeshShape(spaceCraft);
+        spaceCraft.setShadowMode(ShadowMode.CastAndReceive);
+        spaceCraft.setLocalTranslation(startLocation);
+        spaceCraft.setLocalRotation(startOrientation);
+
+        hoverControl = new PhysicsHoverControl(colShape, 500);
+
+        spaceCraft.addControl(hoverControl);
+
+
+        rootNode.attachChild(spaceCraft);
+        getPhysicsSpace().add(hoverControl);
+        hoverControl.setCollisionGroup(PhysicsCollisionObject.COLLISION_GROUP_02);
+
+        ChaseCamera chaseCam = new ChaseCamera(cam, inputManager);
+        spaceCraft.addControl(chaseCam);
+
+        flyCam.setEnabled(false);
+    }
+
+    public void makeMissile() {
+        Vector3f pos = spaceCraft.getWorldTranslation().clone();
+        Quaternion rot = spaceCraft.getWorldRotation();
+        Vector3f dir = rot.getRotationColumn(2);
+
+        Spatial missile = assetManager.loadModel("Models/SpaceCraft/Rocket.mesh.xml");
+        missile.scale(0.5f);
+        missile.rotate(0, FastMath.PI, 0);
+        missile.updateGeometricState();
+
+        BoundingBox box = (BoundingBox) missile.getWorldBound();
+        final Vector3f extent = box.getExtent(null);
+
+        BoxCollisionShape boxShape = new BoxCollisionShape(extent);
+
+        missile.setName("Missile");
+        missile.rotate(rot);
+        missile.setLocalTranslation(pos.addLocal(0, extent.y * 4.5f, 0));
+        missile.setLocalRotation(hoverControl.getPhysicsRotation());
+        missile.setShadowMode(ShadowMode.Cast);
+        RigidBodyControl control = new BombControl(assetManager, boxShape, 20);
+        control.setLinearVelocity(dir.mult(100));
+        control.setCollisionGroup(PhysicsCollisionObject.COLLISION_GROUP_03);
+        missile.addControl(control);
+
+
+        rootNode.attachChild(missile);
+        getPhysicsSpace().add(missile);
+    }
+
+    @Override
+    public void onAnalog(String binding, float value, float tpf) {
+    }
+
+    @Override
+    public void onAction(String binding, boolean value, float tpf) {
+        if (binding.equals("Lefts")) {
+            hoverControl.steer(value ? 50f : 0);
+        } else if (binding.equals("Rights")) {
+            hoverControl.steer(value ? -50f : 0);
+        } else if (binding.equals("Ups")) {
+            hoverControl.accelerate(value ? 100f : 0);
+        } else if (binding.equals("Downs")) {
+            hoverControl.accelerate(value ? -100f : 0);
+        } else if (binding.equals("Reset")) {
+            if (value) {
+                System.out.println("Reset");
+                hoverControl.setPhysicsLocation(startLocation);
+                hoverControl.setPhysicsRotation(startOrientation);
+                hoverControl.setAngularVelocity(Vector3f.ZERO);
+                hoverControl.setLinearVelocity(Vector3f.ZERO);
+                hoverControl.clearForces();
+            } else {
+            }
+        } else if (binding.equals("Space") && value) {
+            makeMissile();
+        }
+    }
+
+    public void updateCamera() {
+        rootNode.updateGeometricState();
+
+        Vector3f pos = spaceCraft.getWorldTranslation().clone();
+        Quaternion rot = spaceCraft.getWorldRotation();
+        Vector3f dir = rot.getRotationColumn(2);
+
+        // make it XZ only
+        Vector3f camPos = new Vector3f(dir);
+        camPos.setY(0);
+        camPos.normalizeLocal();
+
+        // negate and multiply by distance from object
+        camPos.negateLocal();
+        camPos.multLocal(15);
+
+        // add Y distance
+        camPos.setY(2);
+        camPos.addLocal(pos);
+        cam.setLocation(camPos);
+
+        Vector3f lookAt = new Vector3f(dir);
+        lookAt.multLocal(7); // look at dist
+        lookAt.addLocal(pos);
+        cam.lookAt(lookAt, Vector3f.UNIT_Y);
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+    }
+
+    private void createTerrain() {
+        Material matRock = new Material(assetManager,
+                "Common/MatDefs/Terrain/TerrainLighting.j3md");
+        matRock.setBoolean("useTriPlanarMapping", false);
+        matRock.setBoolean("WardIso", true);
+        matRock.setTexture("AlphaMap", assetManager.loadTexture("Textures/Terrain/splat/alphamap.png"));
+        Texture heightMapImage = assetManager.loadTexture("Textures/Terrain/splat/mountains512.png");
+        Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg");
+        grass.setWrap(WrapMode.Repeat);
+        matRock.setTexture("DiffuseMap", grass);
+        matRock.setFloat("DiffuseMap_0_scale", 64);
+        Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg");
+        dirt.setWrap(WrapMode.Repeat);
+        matRock.setTexture("DiffuseMap_1", dirt);
+        matRock.setFloat("DiffuseMap_1_scale", 16);
+        Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg");
+        rock.setWrap(WrapMode.Repeat);
+        matRock.setTexture("DiffuseMap_2", rock);
+        matRock.setFloat("DiffuseMap_2_scale", 128);
+        Texture normalMap0 = assetManager.loadTexture("Textures/Terrain/splat/grass_normal.jpg");
+        normalMap0.setWrap(WrapMode.Repeat);
+        Texture normalMap1 = assetManager.loadTexture("Textures/Terrain/splat/dirt_normal.png");
+        normalMap1.setWrap(WrapMode.Repeat);
+        Texture normalMap2 = assetManager.loadTexture("Textures/Terrain/splat/road_normal.png");
+        normalMap2.setWrap(WrapMode.Repeat);
+        matRock.setTexture("NormalMap", normalMap0);
+        matRock.setTexture("NormalMap_1", normalMap1);
+        matRock.setTexture("NormalMap_2", normalMap2);
+
+        AbstractHeightMap heightmap = null;
+        try {
+            heightmap = new ImageBasedHeightMap(heightMapImage.getImage(), 0.25f);
+            heightmap.load();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        TerrainQuad terrain
+                = new TerrainQuad("terrain", 65, 513, heightmap.getHeightMap());
+        List<Camera> cameras = new ArrayList<>();
+        cameras.add(getCamera());
+        TerrainLodControl control = new TerrainLodControl(terrain, cameras);
+        terrain.addControl(control);
+        terrain.setMaterial(matRock);
+        terrain.setLocalScale(new Vector3f(2, 2, 2));
+        terrain.setLocked(false); // unlock it so we can edit the height
+
+        terrain.setShadowMode(ShadowMode.CastAndReceive);
+        terrain.addControl(new RigidBodyControl(0));
+        rootNode.attachChild(terrain);
+        getPhysicsSpace().addAll(terrain);
+
+    }
+}

+ 214 - 0
jme3-examples/src/main/java/jme3test/bullet/TestIssue1120.java

@@ -0,0 +1,214 @@
+/*
+ * 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 jme3test.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.GImpactCollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.bullet.debug.BulletDebugAppState;
+import com.jme3.font.BitmapFont;
+import com.jme3.font.BitmapText;
+import com.jme3.input.KeyInput;
+import com.jme3.input.MouseInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.input.controls.MouseButtonTrigger;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.shape.Cylinder;
+import com.jme3.system.AppSettings;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test demonstrating a GImpactCollisionShape falling through a curved mesh, when using JBullet. Bullet native
+ * does not experience this issue at the time this test was created.
+ *
+ * @author lou
+ */
+public class TestIssue1120 extends SimpleApplication {
+
+    private BulletAppState bulletAppState;
+    private final boolean physicsDebug = true;
+    private BitmapText speedText;
+    private final List<Spatial> testObjects = new ArrayList<>();
+    private static final boolean SKIP_SETTINGS = false;//Used for repeated runs of this test during dev
+    private float bulletSpeed = 0.5f;
+
+    public static void main(String[] args) {
+        TestIssue1120 test = new TestIssue1120();
+        test.setSettings(new AppSettings(true));
+        test.settings.setFrameRate(60);
+        if (SKIP_SETTINGS) {
+            test.settings.setWidth(1920);
+            test.settings.setHeight(1150);
+            test.showSettings = !SKIP_SETTINGS;
+        }
+        test.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        cam.setLocation(new Vector3f(-7.285349f, -2.2638104f, 4.954474f));
+        cam.setRotation(new Quaternion(0.07345789f, 0.92521834f, -0.2876841f, 0.23624739f));
+        getFlyByCamera().setMoveSpeed(5);
+
+        DirectionalLight dl = new DirectionalLight();
+        dl.setDirection(new Vector3f(-1, -1, -1).normalizeLocal());
+        dl.setColor(ColorRGBA.Green);
+        rootNode.addLight(dl);
+
+        //Setup interactive test controls
+        inputManager.addMapping("restart", new KeyTrigger(KeyInput.KEY_SPACE));
+        inputManager.addMapping("pause", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
+        inputManager.addMapping("+", new KeyTrigger(KeyInput.KEY_ADD), new KeyTrigger(KeyInput.KEY_EQUALS));
+        inputManager.addMapping("-", new KeyTrigger(KeyInput.KEY_SUBTRACT), new KeyTrigger(KeyInput.KEY_MINUS));
+        inputManager.addListener((ActionListener) (String name, boolean isPressed, float tpf) -> {
+            if (!isPressed) {
+                return;
+            }
+            switch (name) {
+                case "restart":
+                    cleanup();
+                    initializeNewTest();
+                    break;
+                case "pause":
+                    bulletAppState.setSpeed(bulletAppState.getSpeed() > 0.1 ? 0 : bulletSpeed);
+                    break;
+                case "+":
+                    bulletSpeed += 0.1f;
+                    if (bulletSpeed > 1f) {
+                        bulletSpeed = 1f;
+                    }
+                    bulletAppState.setSpeed(bulletSpeed);
+                    break;
+                case "-":
+                    bulletSpeed -= 0.1f;
+                    if (bulletSpeed < 0.1f) {
+                        bulletSpeed = 0.1f;
+                    }
+                    bulletAppState.setSpeed(bulletSpeed);
+                    break;
+            }
+        }, "pause", "restart", "+", "-");
+
+        guiNode = getGuiNode();
+        BitmapFont font = assetManager.loadFont("Interface/Fonts/Default.fnt");
+        BitmapText[] testInfo = new BitmapText[2];
+        testInfo[0] = new BitmapText(font);
+        testInfo[1] = new BitmapText(font);
+        speedText = new BitmapText(font);
+
+        float lineHeight = testInfo[0].getLineHeight();
+        testInfo[0].setText("Camera move: W/A/S/D/Q/Z     +/-: Increase/Decrease Speed");
+        testInfo[0].setLocalTranslation(5, settings.getHeight(), 0);
+        guiNode.attachChild(testInfo[0]);
+        testInfo[1].setText("Left Click: Toggle pause            Space: Restart test");
+        testInfo[1].setLocalTranslation(5, settings.getHeight() - lineHeight, 0);
+        guiNode.attachChild(testInfo[1]);
+
+        speedText.setLocalTranslation(202, lineHeight * 1, 0);
+        guiNode.attachChild(speedText);
+
+        initializeNewTest();
+    }
+
+    private void initializeNewTest() {
+        bulletAppState = new BulletAppState();
+        bulletAppState.setDebugEnabled(physicsDebug);
+        stateManager.attach(bulletAppState);
+
+        bulletAppState.setSpeed(bulletSpeed);
+
+        dropTest();
+
+        Geometry leftFloor = PhysicsTestHelper.createMeshTestFloor(assetManager, 20, new Vector3f(-11, -5, -10));
+        addObject(leftFloor);
+
+        //Hide physics debug visualization for floors
+        if (physicsDebug) {
+            BulletDebugAppState bulletDebugAppState = stateManager.getState(BulletDebugAppState.class);
+            bulletDebugAppState.setFilter((Object obj) -> {
+                return !(obj.equals(leftFloor.getControl(RigidBodyControl.class)));
+            });
+        }
+    }
+
+    private void addObject(Spatial s) {
+        testObjects.add(s);
+        rootNode.attachChild(s);
+        physicsSpace().add(s);
+    }
+
+    private void dropTest() {
+        attachTestObject(new Cylinder(2, 16, 0.2f, 2f, true), new Vector3f(0f, 2f, -5f), 2);
+        attachTestObject(new Cylinder(2, 16, 0.2f, 2f, true), new Vector3f(-1f, 2f, -5f), 2);
+        attachTestObject(new Cylinder(2, 16, 0.2f, 2f, true), new Vector3f(-2f, 2f, -5f), 2);
+        attachTestObject(new Cylinder(2, 16, 0.2f, 2f, true), new Vector3f(-3f, 2f, -5f), 2);
+    }
+
+    private void attachTestObject(Mesh mesh, Vector3f position, float mass) {
+        Material material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        Geometry g = new Geometry("mesh", mesh);
+        g.setLocalTranslation(position);
+        g.setMaterial(material);
+
+        RigidBodyControl control = new RigidBodyControl(new GImpactCollisionShape(mesh), mass);
+        g.addControl(control);
+        addObject(g);
+    }
+
+    private PhysicsSpace physicsSpace() {
+        return bulletAppState.getPhysicsSpace();
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        speedText.setText("Speed: " + String.format("%.1f", bulletSpeed));
+    }
+
+    private void cleanup() {
+        stateManager.detach(bulletAppState);
+        stateManager.detach(stateManager.getState(BulletDebugAppState.class));
+        for (Spatial s : testObjects) {
+            rootNode.detachChild(s);
+        }
+    }
+}

+ 229 - 0
jme3-examples/src/main/java/jme3test/bullet/TestIssue1125.java

@@ -0,0 +1,229 @@
+/*
+ * Copyright (c) 2019 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.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.CollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.bullet.util.CollisionShapeFactory;
+import com.jme3.font.BitmapText;
+import com.jme3.material.Material;
+import com.jme3.material.RenderState;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.terrain.geomipmap.TerrainQuad;
+import java.util.logging.Logger;
+
+/**
+ * Test case for JME issue #1125: heightfield collision shapes don't match
+ * TerrainQuad.
+ * <p>
+ * If successful, just one set of grid diagonals will be visible. If
+ * unsuccessful, you'll see both green diagonals (the TerrainQuad) and
+ * perpendicular blue diagonals (physics debug).
+ * <p>
+ * Use this test with jme3-bullet only; it can yield false success with
+ * jme3-jbullet due to JME issue #1129.
+ *
+ * @author Stephen Gold [email protected]
+ */
+public class TestIssue1125 extends SimpleApplication {
+    // *************************************************************************
+    // constants and loggers
+
+    /**
+     * message logger for this class
+     */
+    final public static Logger logger
+            = Logger.getLogger(TestIssue1125.class.getName());
+    // *************************************************************************
+    // fields
+
+    /**
+     * height array for a small heightfield
+     */
+    final private float[] nineHeights = new float[9];
+    /**
+     * green wireframe material for the TerrainQuad
+     */
+    private Material quadMaterial;
+    /**
+     * space for physics simulation
+     */
+    private PhysicsSpace physicsSpace;
+    // *************************************************************************
+    // new methods exposed
+
+    /**
+     * Main entry point for the TestIssue1125 application.
+     *
+     * @param ignored array of command-line arguments (not null)
+     */
+    public static void main(String[] ignored) {
+        new TestIssue1125().start();
+    }
+    // *************************************************************************
+    // SimpleApplication methods
+
+    /**
+     * Initialize this application.
+     */
+    @Override
+    public void simpleInitApp() {
+        configureCamera();
+        configureMaterials();
+        viewPort.setBackgroundColor(new ColorRGBA(0.5f, 0.2f, 0.2f, 1f));
+        configurePhysics();
+        initializeHeightData();
+        addTerrain();
+        showHints();
+    }
+    // *************************************************************************
+    // private methods
+
+    /**
+     * Add 3x3 terrain to the scene and the PhysicsSpace.
+     */
+    private void addTerrain() {
+        int patchSize = 3;
+        int mapSize = 3;
+        TerrainQuad quad
+                = new TerrainQuad("terrain", patchSize, mapSize, nineHeights);
+        rootNode.attachChild(quad);
+        quad.setMaterial(quadMaterial);
+
+        CollisionShape shape = CollisionShapeFactory.createMeshShape(quad);
+        float massForStatic = 0f;
+        RigidBodyControl rbc = new RigidBodyControl(shape, massForStatic);
+        rbc.setPhysicsSpace(physicsSpace);
+        quad.addControl(rbc);
+    }
+
+    /**
+     * Configure the camera during startup.
+     */
+    private void configureCamera() {
+        float fHeight = cam.getFrustumTop() - cam.getFrustumBottom();
+        float fWidth = cam.getFrustumRight() - cam.getFrustumLeft();
+        float fAspect = fWidth / fHeight;
+        float yDegrees = 45f;
+        float near = 0.02f;
+        float far = 20f;
+        cam.setFrustumPerspective(yDegrees, fAspect, near, far);
+
+        flyCam.setMoveSpeed(5f);
+
+        cam.setLocation(new Vector3f(2f, 4.7f, 0.4f));
+        cam.setRotation(new Quaternion(0.348f, -0.64f, 0.4f, 0.556f));
+    }
+
+    /**
+     * Configure materials during startup.
+     */
+    private void configureMaterials() {
+        quadMaterial = new Material(assetManager,
+                "Common/MatDefs/Misc/Unshaded.j3md");
+        quadMaterial.setColor("Color", ColorRGBA.Green.clone());
+        RenderState ars = quadMaterial.getAdditionalRenderState();
+        ars.setWireframe(true);
+    }
+
+    /**
+     * Configure physics during startup.
+     */
+    private void configurePhysics() {
+        BulletAppState bulletAppState = new BulletAppState();
+        stateManager.attach(bulletAppState);
+        bulletAppState.setDebugEnabled(true);
+        physicsSpace = bulletAppState.getPhysicsSpace();
+    }
+
+    /**
+     * Initialize the height data during startup.
+     */
+    private void initializeHeightData() {
+        nineHeights[0] = 1f;
+        nineHeights[1] = 0f;
+        nineHeights[2] = 1f;
+        nineHeights[3] = 0f;
+        nineHeights[4] = 0.5f;
+        nineHeights[5] = 0f;
+        nineHeights[6] = 1f;
+        nineHeights[7] = 0f;
+        nineHeights[8] = 1f;
+    }
+
+    private void showHints() {
+        guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
+        int numLines = 3;
+        BitmapText lines[] = new BitmapText[numLines];
+        for (int i = 0; i < numLines; ++i) {
+            lines[i] = new BitmapText(guiFont);
+        }
+
+        String p = "Test for jMonkeyEngine issue #1125";
+        if (isNativeBullet()) {
+            lines[0].setText(p + " with native Bullet");
+        } else {
+            lines[0].setText(p + " with JBullet (may yield false success)");
+        }
+        lines[1].setText("Use W/A/S/D/Q/Z/arrow keys to move the camera.");
+        lines[2].setText("F5: render stats, C: camera pos, M: mem stats");
+
+        float textHeight = guiFont.getCharSet().getLineHeight();
+        float viewHeight = cam.getHeight();
+        float viewWidth = cam.getWidth();
+        for (int i = 0; i < numLines; ++i) {
+            float left = Math.round((viewWidth - lines[i].getLineWidth()) / 2f);
+            float top = viewHeight - i * textHeight;
+            lines[i].setLocalTranslation(left, top, 0f);
+            guiNode.attachChild(lines[i]);
+        }
+    }
+
+    /**
+     * Determine which physics library is in use.
+     *
+     * @return true for C++ Bullet, false for JBullet (jme3-jbullet)
+     */
+    private boolean isNativeBullet() {
+        try {
+            Class clazz = Class.forName("com.jme3.bullet.util.NativeMeshUtil");
+            return clazz != null;
+        } catch (ClassNotFoundException exception) {
+            return false;
+        }
+    }
+}

+ 148 - 0
jme3-examples/src/main/java/jme3test/bullet/TestIssue877.java

@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2018-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 jme3test.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.collision.shapes.BoxCollisionShape;
+import com.jme3.bullet.collision.shapes.CollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.bullet.joints.HingeJoint;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Node;
+
+/**
+ * Test case for JME issue #877: multiple hinges. Based on code submitted by
+ * Daniel Martensson.
+ *
+ * If successful, all pendulums will swing at the same frequency, and all the
+ * free-falling objects will fall straight down.
+ */
+public class TestIssue877 extends SimpleApplication {
+
+    final private BulletAppState bulletAppState = new BulletAppState();
+    final private int numPendulums = 6;
+    final private int numFalling = 6;
+    final private Node pivots[] = new Node[numPendulums];
+    final private Node bobs[] = new Node[numPendulums];
+    final private Node falling[] = new Node[numFalling];
+    private float timeToNextPrint = 1f; // in seconds
+
+    public static void main(String[] args) {
+        TestIssue877 app = new TestIssue877();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        flyCam.setEnabled(false);
+        cam.setLocation(new Vector3f(-4.77f, -7.55f, 16.52f));
+        cam.setRotation(new Quaternion(-0.103433f, 0.889420f, 0.368792f, 0.249449f));
+
+        stateManager.attach(bulletAppState);
+        bulletAppState.setDebugEnabled(true);
+
+        float pivotY = 14.6214f;
+        float bobStartY = 3f;
+        float length = pivotY - bobStartY;
+        for (int i = 0; i < numPendulums; i++) {
+            float x = 6f - 2.5f * i;
+            Vector3f pivotLocation = new Vector3f(x, pivotY, 0f);
+            pivots[i] = createTestNode(0f, pivotLocation);
+
+            Vector3f bobLocation = new Vector3f(x, bobStartY, 0f);
+            bobs[i] = createTestNode(1f, bobLocation);
+        }
+
+        for (int i = 0; i < numFalling; i++) {
+            float x = -6f - 2.5f * (i + numPendulums);
+            Vector3f createLocation = new Vector3f(x, bobStartY, 0f);
+            falling[i] = createTestNode(1f, createLocation);
+        }
+
+        for (int i = 0; i < numPendulums; i++) {
+            HingeJoint joint = new HingeJoint(
+                    pivots[i].getControl(RigidBodyControl.class),
+                    bobs[i].getControl(RigidBodyControl.class),
+                    new Vector3f(0f, 0f, 0f),
+                    new Vector3f(length, 0f, 0f),
+                    Vector3f.UNIT_Z.clone(),
+                    Vector3f.UNIT_Z.clone());
+            bulletAppState.getPhysicsSpace().add(joint);
+        }
+    }
+
+    Node createTestNode(float mass, Vector3f location) {
+        float size = 0.1f;
+        Vector3f halfExtents = new Vector3f(size, size, size);
+        CollisionShape shape = new BoxCollisionShape(halfExtents);
+        RigidBodyControl control = new RigidBodyControl(shape, mass);
+        Node node = new Node();
+        node.addControl(control);
+        rootNode.attachChild(node);
+        bulletAppState.getPhysicsSpace().add(node);
+        control.setPhysicsLocation(location);
+
+        return node;
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        if (timeToNextPrint > 0f) {
+            timeToNextPrint -= tpf;
+            return;
+        }
+
+        if (numFalling > 0) {
+            Vector3f fallingLocation = falling[0].getWorldTranslation();
+            System.out.printf("  falling[0] location(x=%f, z=%f)",
+                    fallingLocation.x, fallingLocation.z);
+            /*
+             * If an object is falling vertically, its X- and Z-coordinates 
+             * should not change.
+             */
+        }
+        if (numPendulums > 0) {
+            Vector3f bobLocation = bobs[0].getWorldTranslation();
+            Vector3f pivotLocation = pivots[0].getWorldTranslation();
+            float distance = bobLocation.distance(pivotLocation);
+            System.out.printf("  bob[0] distance=%f", distance);
+            /*
+             * If the hinge is working properly, the distance from the
+             * pivot to the bob should remain roughly constant.
+             */
+        }
+        System.out.println();
+        timeToNextPrint = 1f;
+    }
+}

+ 87 - 0
jme3-examples/src/main/java/jme3test/bullet/TestIssue883.java

@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2018-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 jme3test.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+
+/**
+ * Test case for JME issue #883: extra physicsTicks in ThreadingType.PARALLEL.
+ *
+ * <p>If successful, physics time and frame time will advance at the same rate.
+ */
+public class TestIssue883 extends SimpleApplication {
+
+    private boolean firstPrint = true;
+    private float timeToNextPrint = 1f; // in seconds
+    private double frameTime; // in seconds
+    private double physicsTime; // in seconds
+
+    public static void main(String[] args) {
+        TestIssue883 app = new TestIssue883();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+
+        BulletAppState bulletAppState = new BulletAppState() {
+            @Override
+            public void physicsTick(PhysicsSpace space, float timeStep) {
+                physicsTime += timeStep;
+            }
+        };
+        bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL);
+        stateManager.attach(bulletAppState);
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        frameTime += tpf;
+
+        if (timeToNextPrint > 0f) {
+            timeToNextPrint -= tpf;
+            return;
+        }
+
+        if (firstPrint) { // synchronize
+            frameTime = 0.;
+            physicsTime = 0.;
+            firstPrint = false;
+        }
+
+        System.out.printf(" frameTime= %s   physicsTime= %s%n",
+                frameTime, physicsTime);
+        timeToNextPrint = 1f;
+    }
+}

+ 107 - 0
jme3-examples/src/main/java/jme3test/bullet/TestKinematicAddToPhysicsSpaceIssue.java

@@ -0,0 +1,107 @@
+/*
+ * 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 jme3test.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.MeshCollisionShape;
+import com.jme3.bullet.collision.shapes.PlaneCollisionShape;
+import com.jme3.bullet.collision.shapes.SphereCollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.math.Plane;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Node;
+import com.jme3.scene.shape.Sphere;
+
+/**
+ *
+ * @author Nehon
+ */
+public class TestKinematicAddToPhysicsSpaceIssue extends SimpleApplication {
+
+    public static void main(String[] args) {
+        TestKinematicAddToPhysicsSpaceIssue app = new TestKinematicAddToPhysicsSpaceIssue();
+        app.start();
+    }
+    private BulletAppState bulletAppState;
+
+    @Override
+    public void simpleInitApp() {
+
+        bulletAppState = new BulletAppState();
+        stateManager.attach(bulletAppState);
+        bulletAppState.setDebugEnabled(true);
+        // Add a physics sphere to the world
+        Node physicsSphere = PhysicsTestHelper.createPhysicsTestNode(assetManager, new SphereCollisionShape(1), 1);
+        physicsSphere.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(3, 6, 0));
+        rootNode.attachChild(physicsSphere);
+
+        //Setting the rigidBody to kinematic before adding it to the physics space
+        physicsSphere.getControl(RigidBodyControl.class).setKinematic(true);
+        //adding it to the physics space
+        getPhysicsSpace().add(physicsSphere);
+        //Making it not kinematic again, it should fall under gravity, it doesn't
+        physicsSphere.getControl(RigidBodyControl.class).setKinematic(false);
+
+        // Add a physics sphere to the world using the collision shape from sphere one
+        Node physicsSphere2 = PhysicsTestHelper.createPhysicsTestNode(assetManager, new SphereCollisionShape(1), 1);
+        physicsSphere2.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(5, 6, 0));
+        rootNode.attachChild(physicsSphere2);
+
+        //Adding the rigid body to physics space
+        getPhysicsSpace().add(physicsSphere2);
+        //making it kinematic
+        physicsSphere2.getControl(RigidBodyControl.class).setKinematic(false);
+        //Making it not kinematic again, it works properly, the rigid body is affected by gravity.
+        physicsSphere2.getControl(RigidBodyControl.class).setKinematic(false);
+
+
+
+        // an obstacle mesh, does not move (mass=0)
+        Node node2 = PhysicsTestHelper.createPhysicsTestNode(assetManager, new MeshCollisionShape(new Sphere(16, 16, 1.2f)), 0);
+        node2.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(2.5f, -4, 0f));
+        rootNode.attachChild(node2);
+        getPhysicsSpace().add(node2);
+
+        // the floor mesh, does not move (mass=0)
+        Node node3 = PhysicsTestHelper.createPhysicsTestNode(assetManager, new PlaneCollisionShape(new Plane(new Vector3f(0, 1, 0), 0)), 0);
+        node3.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(0f, -6, 0f));
+        rootNode.attachChild(node3);
+        getPhysicsSpace().add(node3);
+
+    }
+
+    private PhysicsSpace getPhysicsSpace() {
+        return bulletAppState.getPhysicsSpace();
+    }
+}

+ 122 - 0
jme3-examples/src/main/java/jme3test/bullet/TestLocalPhysics.java

@@ -0,0 +1,122 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package jme3test.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.*;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.math.Plane;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Node;
+import com.jme3.scene.shape.Sphere;
+
+/**
+ * This is a basic Test of jbullet-jme functions
+ *
+ * @author normenhansen
+ */
+public class TestLocalPhysics extends SimpleApplication {
+
+    private BulletAppState bulletAppState;
+
+    public static void main(String[] args) {
+        TestLocalPhysics app = new TestLocalPhysics();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        bulletAppState = new BulletAppState();
+        stateManager.attach(bulletAppState);
+        bulletAppState.setDebugEnabled(true);
+
+        // Add a physics sphere to the world
+        Node physicsSphere = PhysicsTestHelper.createPhysicsTestNode(assetManager, new SphereCollisionShape(1), 1);
+        physicsSphere.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(3, 6, 0));
+        physicsSphere.getControl(RigidBodyControl.class).setApplyPhysicsLocal(true);
+        rootNode.attachChild(physicsSphere);
+        getPhysicsSpace().add(physicsSphere);
+
+        // Add a physics sphere to the world using the collision shape from sphere one
+        Node physicsSphere2 = PhysicsTestHelper.createPhysicsTestNode(assetManager, physicsSphere.getControl(RigidBodyControl.class).getCollisionShape(), 1);
+        physicsSphere2.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(4, 8, 0));
+        physicsSphere2.getControl(RigidBodyControl.class).setApplyPhysicsLocal(true);
+        rootNode.attachChild(physicsSphere2);
+        getPhysicsSpace().add(physicsSphere2);
+
+        // Add a physics box to the world
+        Node physicsBox = PhysicsTestHelper.createPhysicsTestNode(assetManager, new BoxCollisionShape(new Vector3f(1, 1, 1)), 1);
+        physicsBox.getControl(RigidBodyControl.class).setFriction(0.1f);
+        physicsBox.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(.6f, 4, .5f));
+        physicsBox.getControl(RigidBodyControl.class).setApplyPhysicsLocal(true);
+        rootNode.attachChild(physicsBox);
+        getPhysicsSpace().add(physicsBox);
+
+        // Add a physics cylinder to the world
+        Node physicsCylinder = PhysicsTestHelper.createPhysicsTestNode(assetManager, new CylinderCollisionShape(new Vector3f(1f, 1f, 1.5f)), 1);
+        physicsCylinder.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(2, 2, 0));
+        physicsCylinder.getControl(RigidBodyControl.class).setApplyPhysicsLocal(true);
+        rootNode.attachChild(physicsCylinder);
+        getPhysicsSpace().add(physicsCylinder);
+
+        // an obstacle mesh, does not move (mass=0)
+        Node node2 = PhysicsTestHelper.createPhysicsTestNode(assetManager, new MeshCollisionShape(new Sphere(16, 16, 1.2f)), 0);
+        node2.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(2.5f, -4, 0f));
+        node2.getControl(RigidBodyControl.class).setApplyPhysicsLocal(true);
+        rootNode.attachChild(node2);
+        getPhysicsSpace().add(node2);
+
+        // the floor mesh, does not move (mass=0)
+        Node node3 = PhysicsTestHelper.createPhysicsTestNode(assetManager, new PlaneCollisionShape(new Plane(new Vector3f(0, 1, 0), 0)), 0);
+        node3.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(0f, -6, 0f));
+        node3.getControl(RigidBodyControl.class).setApplyPhysicsLocal(true);
+        rootNode.attachChild(node3);
+        getPhysicsSpace().add(node3);
+
+        // Join the physics objects with a Point2Point joint
+//        PhysicsPoint2PointJoint joint=new PhysicsPoint2PointJoint(physicsSphere, physicsBox, new Vector3f(-2,0,0), new Vector3f(2,0,0));
+//        PhysicsHingeJoint joint=new PhysicsHingeJoint(physicsSphere, physicsBox, new Vector3f(-2,0,0), new Vector3f(2,0,0), Vector3f.UNIT_Z,Vector3f.UNIT_Z);
+//        getPhysicsSpace().add(joint);
+
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        rootNode.rotate(tpf, 0, 0);
+    }
+
+    private PhysicsSpace getPhysicsSpace() {
+        return bulletAppState.getPhysicsSpace();
+    }
+}

+ 225 - 0
jme3-examples/src/main/java/jme3test/bullet/TestPhysicsCar.java

@@ -0,0 +1,225 @@
+/*
+ * 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 jme3test.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.BoxCollisionShape;
+import com.jme3.bullet.collision.shapes.CompoundCollisionShape;
+import com.jme3.bullet.control.VehicleControl;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Matrix3f;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.shape.Cylinder;
+
+public class TestPhysicsCar extends SimpleApplication implements ActionListener {
+
+    private BulletAppState bulletAppState;
+    private VehicleControl vehicle;
+    private final float accelerationForce = 1000.0f;
+    private final float brakeForce = 100.0f;
+    private float steeringValue = 0;
+    private float accelerationValue = 0;
+    final private Vector3f jumpForce = new Vector3f(0, 3000, 0);
+
+    public static void main(String[] args) {
+        TestPhysicsCar app = new TestPhysicsCar();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        bulletAppState = new BulletAppState();
+        stateManager.attach(bulletAppState);
+        bulletAppState.setDebugEnabled(true);
+        PhysicsTestHelper.createPhysicsTestWorld(rootNode, assetManager, bulletAppState.getPhysicsSpace());
+        setupKeys();
+        buildPlayer();
+    }
+
+    private PhysicsSpace getPhysicsSpace(){
+        return bulletAppState.getPhysicsSpace();
+    }
+
+    private void setupKeys() {
+        inputManager.addMapping("Lefts", new KeyTrigger(KeyInput.KEY_H));
+        inputManager.addMapping("Rights", new KeyTrigger(KeyInput.KEY_K));
+        inputManager.addMapping("Ups", new KeyTrigger(KeyInput.KEY_U));
+        inputManager.addMapping("Downs", new KeyTrigger(KeyInput.KEY_J));
+        inputManager.addMapping("Space", new KeyTrigger(KeyInput.KEY_SPACE));
+        inputManager.addMapping("Reset", new KeyTrigger(KeyInput.KEY_RETURN));
+        inputManager.addListener(this, "Lefts");
+        inputManager.addListener(this, "Rights");
+        inputManager.addListener(this, "Ups");
+        inputManager.addListener(this, "Downs");
+        inputManager.addListener(this, "Space");
+        inputManager.addListener(this, "Reset");
+    }
+
+    private void buildPlayer() {
+        Material mat = new Material(getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
+        mat.getAdditionalRenderState().setWireframe(true);
+        mat.setColor("Color", ColorRGBA.Red);
+
+        //create a compound shape and attach the BoxCollisionShape for the car body at 0,1,0
+        //this shifts the effective center of mass of the BoxCollisionShape to 0,-1,0
+        CompoundCollisionShape compoundShape = new CompoundCollisionShape();
+        BoxCollisionShape box = new BoxCollisionShape(new Vector3f(1.2f, 0.5f, 2.4f));
+        compoundShape.addChildShape(box, new Vector3f(0, 1, 0));
+
+        //create vehicle node
+        Node vehicleNode=new Node("vehicleNode");
+        vehicle = new VehicleControl(compoundShape, 400);
+        vehicleNode.addControl(vehicle);
+
+        //setting suspension values for wheels, this can be a bit tricky
+        //see also https://docs.google.com/Doc?docid=0AXVUZ5xw6XpKZGNuZG56a3FfMzU0Z2NyZnF4Zmo&hl=en
+        float stiffness = 60.0f;//200=f1 car
+        float compValue = .3f; //(should be lower than damp)
+        float dampValue = .4f;
+        vehicle.setSuspensionCompression(compValue * 2.0f * FastMath.sqrt(stiffness));
+        vehicle.setSuspensionDamping(dampValue * 2.0f * FastMath.sqrt(stiffness));
+        vehicle.setSuspensionStiffness(stiffness);
+        vehicle.setMaxSuspensionForce(10000.0f);
+
+        //Create four wheels and add them at their locations
+        Vector3f wheelDirection = new Vector3f(0, -1, 0); // was 0, -1, 0
+        Vector3f wheelAxle = new Vector3f(-1, 0, 0); // was -1, 0, 0
+        float radius = 0.5f;
+        float restLength = 0.3f;
+        float yOff = 0.5f;
+        float xOff = 1f;
+        float zOff = 2f;
+
+        Cylinder wheelMesh = new Cylinder(16, 16, radius, radius * 0.6f, true);
+
+        Node node1 = new Node("wheel 1 node");
+        Geometry wheels1 = new Geometry("wheel 1", wheelMesh);
+        node1.attachChild(wheels1);
+        wheels1.rotate(0, FastMath.HALF_PI, 0);
+        wheels1.setMaterial(mat);
+        vehicle.addWheel(node1, new Vector3f(-xOff, yOff, zOff),
+                wheelDirection, wheelAxle, restLength, radius, true);
+
+        Node node2 = new Node("wheel 2 node");
+        Geometry wheels2 = new Geometry("wheel 2", wheelMesh);
+        node2.attachChild(wheels2);
+        wheels2.rotate(0, FastMath.HALF_PI, 0);
+        wheels2.setMaterial(mat);
+        vehicle.addWheel(node2, new Vector3f(xOff, yOff, zOff),
+                wheelDirection, wheelAxle, restLength, radius, true);
+
+        Node node3 = new Node("wheel 3 node");
+        Geometry wheels3 = new Geometry("wheel 3", wheelMesh);
+        node3.attachChild(wheels3);
+        wheels3.rotate(0, FastMath.HALF_PI, 0);
+        wheels3.setMaterial(mat);
+        vehicle.addWheel(node3, new Vector3f(-xOff, yOff, -zOff),
+                wheelDirection, wheelAxle, restLength, radius, false);
+
+        Node node4 = new Node("wheel 4 node");
+        Geometry wheels4 = new Geometry("wheel 4", wheelMesh);
+        node4.attachChild(wheels4);
+        wheels4.rotate(0, FastMath.HALF_PI, 0);
+        wheels4.setMaterial(mat);
+        vehicle.addWheel(node4, new Vector3f(xOff, yOff, -zOff),
+                wheelDirection, wheelAxle, restLength, radius, false);
+
+        vehicleNode.attachChild(node1);
+        vehicleNode.attachChild(node2);
+        vehicleNode.attachChild(node3);
+        vehicleNode.attachChild(node4);
+        rootNode.attachChild(vehicleNode);
+
+        getPhysicsSpace().add(vehicle);
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        cam.lookAt(vehicle.getPhysicsLocation(), Vector3f.UNIT_Y);
+    }
+
+    @Override
+    public void onAction(String binding, boolean value, float tpf) {
+        if (binding.equals("Lefts")) {
+            if (value) {
+                steeringValue += .5f;
+            } else {
+                steeringValue -= .5f;
+            }
+            vehicle.steer(steeringValue);
+        } else if (binding.equals("Rights")) {
+            if (value) {
+                steeringValue -= .5f;
+            } else {
+                steeringValue += .5f;
+            }
+            vehicle.steer(steeringValue);
+        } else if (binding.equals("Ups")) {
+            if (value) {
+                accelerationValue += accelerationForce;
+            } else {
+                accelerationValue -= accelerationForce;
+            }
+            vehicle.accelerate(accelerationValue);
+        } else if (binding.equals("Downs")) {
+            if (value) {
+                vehicle.brake(brakeForce);
+            } else {
+                vehicle.brake(0f);
+            }
+        } else if (binding.equals("Space")) {
+            if (value) {
+                vehicle.applyImpulse(jumpForce, Vector3f.ZERO);
+            }
+        } else if (binding.equals("Reset")) {
+            if (value) {
+                System.out.println("Reset");
+                vehicle.setPhysicsLocation(Vector3f.ZERO);
+                vehicle.setPhysicsRotation(new Matrix3f());
+                vehicle.setLinearVelocity(Vector3f.ZERO);
+                vehicle.setAngularVelocity(Vector3f.ZERO);
+                vehicle.resetSuspension();
+            } else {
+            }
+        }
+    }
+}

+ 212 - 0
jme3-examples/src/main/java/jme3test/bullet/TestPhysicsCharacter.java

@@ -0,0 +1,212 @@
+/*
+ * 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 jme3test.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
+import com.jme3.bullet.control.CharacterControl;
+import com.jme3.input.KeyInput;
+import com.jme3.input.MouseInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.input.controls.MouseButtonTrigger;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.RenderManager;
+import com.jme3.scene.CameraNode;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.control.CameraControl.ControlDirection;
+
+/**
+ * A walking physical character followed by a 3rd person camera. (No animation.)
+ * @author normenhansen, zathras
+ */
+public class TestPhysicsCharacter extends SimpleApplication implements ActionListener {
+
+  private BulletAppState bulletAppState;
+  private CharacterControl physicsCharacter;
+  final private Vector3f walkDirection = new Vector3f(0,0,0);
+  final private Vector3f viewDirection = new Vector3f(0,0,0);
+  private boolean leftStrafe = false, rightStrafe = false, forward = false, backward = false, 
+          leftRotate = false, rightRotate = false;
+
+  public static void main(String[] args) {
+    TestPhysicsCharacter app = new TestPhysicsCharacter();
+    app.start();
+  }
+
+    private void setupKeys() {
+        inputManager.addMapping("Strafe Left", 
+                new KeyTrigger(KeyInput.KEY_Q), 
+                new KeyTrigger(KeyInput.KEY_Z));
+        inputManager.addMapping("Strafe Right", 
+                new KeyTrigger(KeyInput.KEY_E),
+                new KeyTrigger(KeyInput.KEY_X));
+        inputManager.addMapping("Rotate Left", 
+                new KeyTrigger(KeyInput.KEY_A), 
+                new KeyTrigger(KeyInput.KEY_LEFT));
+        inputManager.addMapping("Rotate Right", 
+                new KeyTrigger(KeyInput.KEY_D), 
+                new KeyTrigger(KeyInput.KEY_RIGHT));
+        inputManager.addMapping("Walk Forward", 
+                new KeyTrigger(KeyInput.KEY_W), 
+                new KeyTrigger(KeyInput.KEY_UP));
+        inputManager.addMapping("Walk Backward", 
+                new KeyTrigger(KeyInput.KEY_S),
+                new KeyTrigger(KeyInput.KEY_DOWN));
+        inputManager.addMapping("Jump", 
+                new KeyTrigger(KeyInput.KEY_SPACE), 
+                new KeyTrigger(KeyInput.KEY_RETURN));
+        inputManager.addMapping("Shoot", 
+                new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
+        inputManager.addListener(this, "Strafe Left", "Strafe Right");
+        inputManager.addListener(this, "Rotate Left", "Rotate Right");
+        inputManager.addListener(this, "Walk Forward", "Walk Backward");
+        inputManager.addListener(this, "Jump", "Shoot");
+    }
+  @Override
+  public void simpleInitApp() {
+    // activate physics
+    bulletAppState = new BulletAppState();
+    stateManager.attach(bulletAppState);
+
+    // init a physical test scene
+    PhysicsTestHelper.createPhysicsTestWorldSoccer(rootNode, assetManager, bulletAppState.getPhysicsSpace());
+    setupKeys();
+
+    // Add a physics character to the world
+    physicsCharacter = new CharacterControl(new CapsuleCollisionShape(0.5f, 1.8f), .1f);
+    physicsCharacter.setPhysicsLocation(new Vector3f(0, 1, 0));
+    Node characterNode = new Node("character node");
+    Spatial model = assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml");
+    model.scale(0.25f);
+    characterNode.addControl(physicsCharacter);
+    getPhysicsSpace().add(physicsCharacter);
+    rootNode.attachChild(characterNode);
+    characterNode.attachChild(model);
+
+    // set forward camera node that follows the character
+    CameraNode camNode = new CameraNode("CamNode", cam);
+    camNode.setControlDir(ControlDirection.SpatialToCamera);
+    camNode.setLocalTranslation(new Vector3f(0, 1, -5));
+    camNode.lookAt(model.getLocalTranslation(), Vector3f.UNIT_Y);
+    characterNode.attachChild(camNode);
+
+    //disable the default 1st-person flyCam (don't forget this!!)
+    flyCam.setEnabled(false);
+
+  }
+
+   @Override
+    public void simpleUpdate(float tpf) {
+        Vector3f camDir = cam.getDirection().mult(0.2f);
+        Vector3f camLeft = cam.getLeft().mult(0.2f);
+        camDir.y = 0;
+        camLeft.y = 0;
+        viewDirection.set(camDir);
+        walkDirection.set(0, 0, 0);
+        if (leftStrafe) {
+            walkDirection.addLocal(camLeft);
+        } else
+        if (rightStrafe) {
+            walkDirection.addLocal(camLeft.negate());
+        }
+        if (leftRotate) {
+            viewDirection.addLocal(camLeft.mult(tpf));
+        } else
+        if (rightRotate) {
+            viewDirection.addLocal(camLeft.mult(tpf).negate());
+        }
+        if (forward) {
+            walkDirection.addLocal(camDir);
+        } else
+        if (backward) {
+            walkDirection.addLocal(camDir.negate());
+        }
+        physicsCharacter.setWalkDirection(walkDirection);
+        physicsCharacter.setViewDirection(viewDirection);
+    }
+
+  @Override
+    public void onAction(String binding, boolean value, float tpf) {
+        if (binding.equals("Strafe Left")) {
+            if (value) {
+                leftStrafe = true;
+            } else {
+                leftStrafe = false;
+            }
+        } else if (binding.equals("Strafe Right")) {
+            if (value) {
+                rightStrafe = true;
+            } else {
+                rightStrafe = false;
+            }
+        } else if (binding.equals("Rotate Left")) {
+            if (value) {
+                leftRotate = true;
+            } else {
+                leftRotate = false;
+            }
+        } else if (binding.equals("Rotate Right")) {
+            if (value) {
+                rightRotate = true;
+            } else {
+                rightRotate = false;
+            }
+        } else if (binding.equals("Walk Forward")) {
+            if (value) {
+                forward = true;
+            } else {
+                forward = false;
+            }
+        } else if (binding.equals("Walk Backward")) {
+            if (value) {
+                backward = true;
+            } else {
+                backward = false;
+            }
+        } else if (binding.equals("Jump")) {
+            physicsCharacter.jump();
+        }
+    }
+
+  private PhysicsSpace getPhysicsSpace() {
+    return bulletAppState.getPhysicsSpace();
+  }
+
+  @Override
+  public void simpleRender(RenderManager rm) {
+    //TODO: add render code
+  }
+}

+ 110 - 0
jme3-examples/src/main/java/jme3test/bullet/TestPhysicsHingeJoint.java

@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2009-2020 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.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.BoxCollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.bullet.joints.HingeJoint;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.AnalogListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Node;
+
+public class TestPhysicsHingeJoint extends SimpleApplication implements AnalogListener {
+    private BulletAppState bulletAppState;
+    private HingeJoint joint;
+
+    public static void main(String[] args) {
+        TestPhysicsHingeJoint app = new TestPhysicsHingeJoint();
+        app.start();
+    }
+
+    private void setupKeys() {
+        inputManager.addMapping("Left", new KeyTrigger(KeyInput.KEY_H));
+        inputManager.addMapping("Right", new KeyTrigger(KeyInput.KEY_K));
+        inputManager.addMapping("Swing", new KeyTrigger(KeyInput.KEY_SPACE));
+        inputManager.addListener(this, "Left", "Right", "Swing");
+    }
+
+    @Override
+    public void onAnalog(String binding, float value, float tpf) {
+        if(binding.equals("Left")){
+            joint.enableMotor(true, 1, .1f);
+        }
+        else if(binding.equals("Right")){
+            joint.enableMotor(true, -1, .1f);
+        }
+        else if(binding.equals("Swing")){
+            joint.enableMotor(false, 0, 0);
+        }
+    }
+
+    @Override
+    public void simpleInitApp() {
+        bulletAppState = new BulletAppState();
+        stateManager.attach(bulletAppState);
+        bulletAppState.setDebugEnabled(true);
+        setupKeys();
+        setupJoint();
+    }
+
+    private PhysicsSpace getPhysicsSpace(){
+        return bulletAppState.getPhysicsSpace();
+    }
+
+    public void setupJoint() {
+        Node holderNode=PhysicsTestHelper.createPhysicsTestNode(assetManager, new BoxCollisionShape(new Vector3f( .1f, .1f, .1f)),0);
+        holderNode.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(0f,0,0f));
+        rootNode.attachChild(holderNode);
+        getPhysicsSpace().add(holderNode);
+
+        Node hammerNode=PhysicsTestHelper.createPhysicsTestNode(assetManager, new BoxCollisionShape(new Vector3f( .3f, .3f, .3f)),1);
+        hammerNode.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(0f,-1,0f));
+        rootNode.attachChild(hammerNode);
+        getPhysicsSpace().add(hammerNode);
+
+        joint=new HingeJoint(holderNode.getControl(RigidBodyControl.class), hammerNode.getControl(RigidBodyControl.class), Vector3f.ZERO, new Vector3f(0f,-1,0f), Vector3f.UNIT_Z, Vector3f.UNIT_Z);
+        getPhysicsSpace().add(joint);
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+
+    }
+
+
+}

+ 69 - 0
jme3-examples/src/main/java/jme3test/bullet/TestPhysicsRayCast.java

@@ -0,0 +1,69 @@
+package jme3test.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.collision.PhysicsCollisionObject;
+import com.jme3.bullet.collision.PhysicsRayTestResult;
+import com.jme3.bullet.collision.shapes.CollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.bullet.util.CollisionShapeFactory;
+import com.jme3.font.BitmapText;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import java.util.List;
+
+/**
+ *
+ * @author wezrule
+ */
+public class TestPhysicsRayCast extends SimpleApplication {
+
+    final private BulletAppState bulletAppState = new BulletAppState();
+
+    public static void main(String[] args) {
+        new TestPhysicsRayCast().start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        stateManager.attach(bulletAppState);
+        initCrossHair();
+
+        Spatial s = assetManager.loadModel("Models/Elephant/Elephant.mesh.xml");
+        s.setLocalScale(0.1f);
+
+        CollisionShape collisionShape = CollisionShapeFactory.createMeshShape(s);
+        Node n = new Node("elephant");
+        n.addControl(new RigidBodyControl(collisionShape, 1));
+        n.getControl(RigidBodyControl.class).setKinematic(true);
+        bulletAppState.getPhysicsSpace().add(n);
+        rootNode.attachChild(n);
+        bulletAppState.setDebugEnabled(true);
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        float rayLength = 50f;
+        Vector3f start = cam.getLocation();
+        Vector3f end = cam.getDirection().scaleAdd(rayLength, start);
+        List<PhysicsRayTestResult> rayTest
+                = bulletAppState.getPhysicsSpace().rayTest(start, end);
+        if (rayTest.size() > 0) {
+            PhysicsRayTestResult get = rayTest.get(0);
+            PhysicsCollisionObject collisionObject = get.getCollisionObject();
+            // Display the name of the 1st object in place of FPS.
+            fpsText.setText(collisionObject.getUserObject().toString());
+        } else {
+            // Provide prompt feedback that no collision object was hit.
+            fpsText.setText("MISSING");
+        }
+    }
+
+    private void initCrossHair() {
+        BitmapText bitmapText = new BitmapText(guiFont);
+        bitmapText.setText("+");
+        bitmapText.setLocalTranslation((settings.getWidth() - bitmapText.getLineWidth())*0.5f, (settings.getHeight() + bitmapText.getLineHeight())*0.5f, 0);
+        guiNode.attachChild(bitmapText);
+    }
+}

+ 153 - 0
jme3-examples/src/main/java/jme3test/bullet/TestPhysicsReadWrite.java

@@ -0,0 +1,153 @@
+/*
+ * 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 jme3test.bullet;
+
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.*;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.bullet.joints.HingeJoint;
+import com.jme3.export.binary.BinaryExporter;
+import com.jme3.export.binary.BinaryImporter;
+import com.jme3.math.Plane;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.RenderManager;
+import com.jme3.scene.Node;
+import com.jme3.scene.shape.Sphere;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This is a basic Test of jbullet-jme functions
+ *
+ * @author normenhansen
+ */
+public class TestPhysicsReadWrite extends SimpleApplication{
+    private BulletAppState bulletAppState;
+
+    public static void main(String[] args){
+        TestPhysicsReadWrite app = new TestPhysicsReadWrite();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        bulletAppState = new BulletAppState();
+        stateManager.attach(bulletAppState);
+        bulletAppState.setDebugEnabled(true);
+        Node physicsRootNode = new Node("PhysicsRootNode");
+        rootNode.attachChild(physicsRootNode);
+
+        // Add a physics sphere to the world
+        Node physicsSphere = PhysicsTestHelper.createPhysicsTestNode(assetManager, new SphereCollisionShape(1), 1);
+        physicsSphere.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(3, 6, 0));
+        rootNode.attachChild(physicsSphere);
+        getPhysicsSpace().add(physicsSphere);
+
+        // Add a physics sphere to the world using the collision shape from sphere one
+        Node physicsSphere2 = PhysicsTestHelper.createPhysicsTestNode(assetManager, physicsSphere.getControl(RigidBodyControl.class).getCollisionShape(), 1);
+        physicsSphere2.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(4, 8, 0));
+        rootNode.attachChild(physicsSphere2);
+        getPhysicsSpace().add(physicsSphere2);
+
+        // Add a physics box to the world
+        Node physicsBox = PhysicsTestHelper.createPhysicsTestNode(assetManager, new BoxCollisionShape(new Vector3f(1, 1, 1)), 1);
+        physicsBox.getControl(RigidBodyControl.class).setFriction(0.1f);
+        physicsBox.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(.6f, 4, .5f));
+        rootNode.attachChild(physicsBox);
+        getPhysicsSpace().add(physicsBox);
+
+        // Add a physics cylinder to the world
+        Node physicsCylinder = PhysicsTestHelper.createPhysicsTestNode(assetManager, new CylinderCollisionShape(new Vector3f(1f, 1f, 1.5f)), 1);
+        physicsCylinder.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(2, 2, 0));
+        rootNode.attachChild(physicsCylinder);
+        getPhysicsSpace().add(physicsCylinder);
+
+        // an obstacle mesh, does not move (mass=0)
+        Node node2 = PhysicsTestHelper.createPhysicsTestNode(assetManager, new MeshCollisionShape(new Sphere(16, 16, 1.2f)), 0);
+        node2.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(2.5f, -4, 0f));
+        rootNode.attachChild(node2);
+        getPhysicsSpace().add(node2);
+
+        // the floor mesh, does not move (mass=0)
+        Node node3 = PhysicsTestHelper.createPhysicsTestNode(assetManager, new PlaneCollisionShape(new Plane(new Vector3f(0, 1, 0), 0)), 0);
+        node3.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(0f, -6, 0f));
+        rootNode.attachChild(node3);
+        getPhysicsSpace().add(node3);
+
+        // Join the physics objects with a Point2Point joint
+        HingeJoint joint=new HingeJoint(physicsSphere.getControl(RigidBodyControl.class), physicsBox.getControl(RigidBodyControl.class), new Vector3f(-2,0,0), new Vector3f(2,0,0), Vector3f.UNIT_Z,Vector3f.UNIT_Z);
+        getPhysicsSpace().add(joint);
+
+        //save and load the physicsRootNode
+        try {
+            //remove all physics objects from physics space
+            getPhysicsSpace().removeAll(physicsRootNode);
+            physicsRootNode.removeFromParent();
+            //export to byte array
+            ByteArrayOutputStream bout=new ByteArrayOutputStream();
+            BinaryExporter.getInstance().save(physicsRootNode, bout);
+            //import from byte array
+            ByteArrayInputStream bin=new ByteArrayInputStream(bout.toByteArray());
+            BinaryImporter imp=BinaryImporter.getInstance();
+            imp.setAssetManager(assetManager);
+            Node newPhysicsRootNode=(Node)imp.load(bin);
+            //add all physics objects to physics space
+            getPhysicsSpace().addAll(newPhysicsRootNode);
+            rootNode.attachChild(newPhysicsRootNode);
+        } catch (IOException ex) {
+            Logger.getLogger(TestPhysicsReadWrite.class.getName()).log(Level.SEVERE, null, ex);
+        }
+
+    }
+
+    private PhysicsSpace getPhysicsSpace(){
+        return bulletAppState.getPhysicsSpace();
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        //TODO: add update code
+    }
+
+    @Override
+    public void simpleRender(RenderManager rm) {
+        //TODO: add render code
+    }
+
+}

+ 183 - 0
jme3-examples/src/main/java/jme3test/bullet/TestQ3.java

@@ -0,0 +1,183 @@
+/*
+ * Copyright (c) 2009-2021 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package jme3test.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.asset.plugins.HttpZipLocator;
+import com.jme3.asset.plugins.ZipLocator;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.SphereCollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.bullet.objects.PhysicsCharacter;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.light.AmbientLight;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.MaterialList;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Node;
+import com.jme3.scene.plugins.ogre.OgreMeshKey;
+import java.io.File;
+
+public class TestQ3 extends SimpleApplication implements ActionListener {
+
+    private BulletAppState bulletAppState;
+    private PhysicsCharacter player;
+    final private Vector3f walkDirection = new Vector3f();
+    private static boolean useHttp = false;
+    private boolean left=false,right=false,up=false,down=false;
+
+    public static void main(String[] args) {        
+        TestQ3 app = new TestQ3();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        File file = new File("quake3level.zip");
+        if (!file.exists()) {
+            useHttp = true;
+        }
+        
+        bulletAppState = new BulletAppState();
+        stateManager.attach(bulletAppState);
+        flyCam.setMoveSpeed(100);
+        setupKeys();
+
+        this.cam.setFrustumFar(2000);
+
+        DirectionalLight dl = new DirectionalLight();
+        dl.setColor(ColorRGBA.White.clone().multLocal(2));
+        dl.setDirection(new Vector3f(-1, -1, -1).normalize());
+        rootNode.addLight(dl);
+
+        AmbientLight am = new AmbientLight();
+        am.setColor(ColorRGBA.White.mult(2));
+        rootNode.addLight(am);
+
+        // load the level from zip or http zip
+        if (useHttp) {
+            assetManager.registerLocator(
+                    "https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/jmonkeyengine/quake3level.zip",
+                    HttpZipLocator.class);
+        } else {
+            assetManager.registerLocator("quake3level.zip", ZipLocator.class);
+        }
+
+        // create the geometry and attach it
+        MaterialList matList = (MaterialList) assetManager.loadAsset("Scene.material");
+        OgreMeshKey key = new OgreMeshKey("main.meshxml", matList);
+        Node gameLevel = (Node) assetManager.loadAsset(key);
+        gameLevel.setLocalScale(0.1f);
+
+        // add a physics control, it will generate a MeshCollisionShape based on the gameLevel
+        gameLevel.addControl(new RigidBodyControl(0));
+
+        player = new PhysicsCharacter(new SphereCollisionShape(5), .01f);
+        player.setJumpSpeed(20);
+        player.setFallSpeed(30);
+        player.setGravity(30);
+
+        player.setPhysicsLocation(new Vector3f(60, 10, -60));
+
+        rootNode.attachChild(gameLevel);
+
+        getPhysicsSpace().addAll(gameLevel);
+        getPhysicsSpace().add(player);
+    }
+
+    private PhysicsSpace getPhysicsSpace(){
+        return bulletAppState.getPhysicsSpace();
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        Vector3f camDir = cam.getDirection().clone().multLocal(0.6f);
+        Vector3f camLeft = cam.getLeft().clone().multLocal(0.4f);
+        walkDirection.set(0,0,0);
+        if(left)
+            walkDirection.addLocal(camLeft);
+        if(right)
+            walkDirection.addLocal(camLeft.negate());
+        if(up)
+            walkDirection.addLocal(camDir);
+        if(down)
+            walkDirection.addLocal(camDir.negate());
+        player.setWalkDirection(walkDirection);
+        cam.setLocation(player.getPhysicsLocation());
+    }
+
+    private void setupKeys() {
+        inputManager.addMapping("Lefts", new KeyTrigger(KeyInput.KEY_A));
+        inputManager.addMapping("Rights", new KeyTrigger(KeyInput.KEY_D));
+        inputManager.addMapping("Ups", new KeyTrigger(KeyInput.KEY_W));
+        inputManager.addMapping("Downs", new KeyTrigger(KeyInput.KEY_S));
+        inputManager.addMapping("Space", new KeyTrigger(KeyInput.KEY_SPACE));
+        inputManager.addListener(this,"Lefts");
+        inputManager.addListener(this,"Rights");
+        inputManager.addListener(this,"Ups");
+        inputManager.addListener(this,"Downs");
+        inputManager.addListener(this,"Space");
+    }
+
+    @Override
+    public void onAction(String binding, boolean value, float tpf) {
+
+        if (binding.equals("Lefts")) {
+            if(value)
+                left=true;
+            else
+                left=false;
+        } else if (binding.equals("Rights")) {
+            if(value)
+                right=true;
+            else
+                right=false;
+        } else if (binding.equals("Ups")) {
+            if(value)
+                up=true;
+            else
+                up=false;
+        } else if (binding.equals("Downs")) {
+            if(value)
+                down=true;
+            else
+                down=false;
+        } else if (binding.equals("Space")) {
+            player.jump();
+        }
+    }
+}

+ 152 - 0
jme3-examples/src/main/java/jme3test/bullet/TestRagDoll.java

@@ -0,0 +1,152 @@
+/*
+ * 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 jme3test.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.bullet.joints.ConeJoint;
+import com.jme3.bullet.joints.PhysicsJoint;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.MouseButtonTrigger;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Node;
+
+/**
+ *
+ * @author normenhansen
+ */
+public class TestRagDoll extends SimpleApplication implements ActionListener {
+
+    private BulletAppState bulletAppState;
+    final private Node ragDoll = new Node();
+    private Node shoulders;
+    final private Vector3f upForce = new Vector3f(0, 200, 0);
+    private boolean applyForce = false;
+
+    public static void main(String[] args) {
+        TestRagDoll app = new TestRagDoll();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        bulletAppState = new BulletAppState();
+        stateManager.attach(bulletAppState);
+        bulletAppState.setDebugEnabled(true);
+        inputManager.addMapping("Pull ragdoll up", new MouseButtonTrigger(0));
+        inputManager.addListener(this, "Pull ragdoll up");
+        PhysicsTestHelper.createPhysicsTestWorld(rootNode, assetManager, bulletAppState.getPhysicsSpace());
+        createRagDoll();
+    }
+
+    private void createRagDoll() {
+        shoulders = createLimb(0.2f, 1.0f, new Vector3f(0.00f, 1.5f, 0), true);
+        Node uArmL = createLimb(0.2f, 0.5f, new Vector3f(-0.75f, 0.8f, 0), false);
+        Node uArmR = createLimb(0.2f, 0.5f, new Vector3f(0.75f, 0.8f, 0), false);
+        Node lArmL = createLimb(0.2f, 0.5f, new Vector3f(-0.75f, -0.2f, 0), false);
+        Node lArmR = createLimb(0.2f, 0.5f, new Vector3f(0.75f, -0.2f, 0), false);
+        Node body = createLimb(0.2f, 1.0f, new Vector3f(0.00f, 0.5f, 0), false);
+        Node hips = createLimb(0.2f, 0.5f, new Vector3f(0.00f, -0.5f, 0), true);
+        Node uLegL = createLimb(0.2f, 0.5f, new Vector3f(-0.25f, -1.2f, 0), false);
+        Node uLegR = createLimb(0.2f, 0.5f, new Vector3f(0.25f, -1.2f, 0), false);
+        Node lLegL = createLimb(0.2f, 0.5f, new Vector3f(-0.25f, -2.2f, 0), false);
+        Node lLegR = createLimb(0.2f, 0.5f, new Vector3f(0.25f, -2.2f, 0), false);
+
+        join(body, shoulders, new Vector3f(0f, 1.4f, 0));
+        join(body, hips, new Vector3f(0f, -0.5f, 0));
+
+        join(uArmL, shoulders, new Vector3f(-0.75f, 1.4f, 0));
+        join(uArmR, shoulders, new Vector3f(0.75f, 1.4f, 0));
+        join(uArmL, lArmL, new Vector3f(-0.75f, .4f, 0));
+        join(uArmR, lArmR, new Vector3f(0.75f, .4f, 0));
+
+        join(uLegL, hips, new Vector3f(-.25f, -0.5f, 0));
+        join(uLegR, hips, new Vector3f(.25f, -0.5f, 0));
+        join(uLegL, lLegL, new Vector3f(-.25f, -1.7f, 0));
+        join(uLegR, lLegR, new Vector3f(.25f, -1.7f, 0));
+
+        ragDoll.attachChild(shoulders);
+        ragDoll.attachChild(body);
+        ragDoll.attachChild(hips);
+        ragDoll.attachChild(uArmL);
+        ragDoll.attachChild(uArmR);
+        ragDoll.attachChild(lArmL);
+        ragDoll.attachChild(lArmR);
+        ragDoll.attachChild(uLegL);
+        ragDoll.attachChild(uLegR);
+        ragDoll.attachChild(lLegL);
+        ragDoll.attachChild(lLegR);
+
+        rootNode.attachChild(ragDoll);
+        bulletAppState.getPhysicsSpace().addAll(ragDoll);
+    }
+
+    private Node createLimb(float width, float height, Vector3f location, boolean rotate) {
+        int axis = rotate ? PhysicsSpace.AXIS_X : PhysicsSpace.AXIS_Y;
+        CapsuleCollisionShape shape = new CapsuleCollisionShape(width, height, axis);
+        Node node = new Node("Limb");
+        RigidBodyControl rigidBodyControl = new RigidBodyControl(shape, 1);
+        node.setLocalTranslation(location);
+        node.addControl(rigidBodyControl);
+        return node;
+    }
+
+    private PhysicsJoint join(Node A, Node B, Vector3f connectionPoint) {
+        Vector3f pivotA = A.worldToLocal(connectionPoint, new Vector3f());
+        Vector3f pivotB = B.worldToLocal(connectionPoint, new Vector3f());
+        ConeJoint joint = new ConeJoint(A.getControl(RigidBodyControl.class), B.getControl(RigidBodyControl.class), pivotA, pivotB);
+        joint.setLimit(1f, 1f, 0);
+        return joint;
+    }
+
+    @Override
+    public void onAction(String string, boolean bln, float tpf) {
+        if ("Pull ragdoll up".equals(string)) {
+            if (bln) {
+                shoulders.getControl(RigidBodyControl.class).activate();
+                applyForce = true;
+            } else {
+                applyForce = false;
+            }
+        }
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        if (applyForce) {
+            shoulders.getControl(RigidBodyControl.class).applyForce(upForce, Vector3f.ZERO);
+        }
+    }
+}

+ 246 - 0
jme3-examples/src/main/java/jme3test/bullet/TestRagdollCharacter.java

@@ -0,0 +1,246 @@
+/*
+ * 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 jme3test.bullet;
+
+import com.jme3.anim.AnimComposer;
+import com.jme3.anim.tween.Tweens;
+import com.jme3.anim.tween.action.Action;
+import com.jme3.app.SimpleApplication;
+import com.jme3.asset.TextureKey;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.animation.DynamicAnimControl;
+import com.jme3.bullet.animation.RangeOfMotion;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.queue.RenderQueue.ShadowMode;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.shape.Box;
+import com.jme3.texture.Texture;
+
+/**
+ * @author normenhansen
+ */
+public class TestRagdollCharacter
+        extends SimpleApplication
+        implements ActionListener {
+
+    private AnimComposer composer;
+    private boolean forward = false, backward = false,
+            leftRotate = false, rightRotate = false;
+    private Node model;
+    private PhysicsSpace physicsSpace;
+
+    public static void main(String[] args) {
+        TestRagdollCharacter app = new TestRagdollCharacter();
+        app.start();
+    }
+
+    public void onSliceDone() {
+        composer.setCurrentAction("IdleTop");
+    }
+
+    static void setupSinbad(DynamicAnimControl ragdoll) {
+        ragdoll.link("Waist", 1f,
+                new RangeOfMotion(1f, -0.4f, 0.8f, -0.8f, 0.4f, -0.4f));
+        ragdoll.link("Chest", 1f, new RangeOfMotion(0.4f, 0f, 0.4f));
+        ragdoll.link("Neck", 1f, new RangeOfMotion(0.5f, 1f, 0.7f));
+
+        ragdoll.link("Clavicle.R", 1f,
+                new RangeOfMotion(0.3f, -0.6f, 0f, 0f, 0.4f, -0.4f));
+        ragdoll.link("Humerus.R", 1f,
+                new RangeOfMotion(1.6f, -0.8f, 1f, -1f, 1.6f, -1f));
+        ragdoll.link("Ulna.R", 1f, new RangeOfMotion(0f, 0f, 1f, -1f, 0f, -2f));
+        ragdoll.link("Hand.R", 1f, new RangeOfMotion(0.8f, 0f, 0.2f));
+
+        ragdoll.link("Clavicle.L", 1f,
+                new RangeOfMotion(0.6f, -0.3f, 0f, 0f, 0.4f, -0.4f));
+        ragdoll.link("Humerus.L",
+                1f, new RangeOfMotion(0.8f, -1.6f, 1f, -1f, 1f, -1.6f));
+        ragdoll.link("Ulna.L", 1f, new RangeOfMotion(0f, 0f, 1f, -1f, 2f, 0f));
+        ragdoll.link("Hand.L", 1f, new RangeOfMotion(0.8f, 0f, 0.2f));
+
+        ragdoll.link("Thigh.R", 1f,
+                new RangeOfMotion(0.4f, -1f, 0.4f, -0.4f, 1f, -0.5f));
+        ragdoll.link("Calf.R", 1f, new RangeOfMotion(2f, 0f, 0f, 0f, 0f, 0f));
+        ragdoll.link("Foot.R", 1f, new RangeOfMotion(0.3f, 0.5f, 0f));
+
+        ragdoll.link("Thigh.L", 1f,
+                new RangeOfMotion(0.4f, -1f, 0.4f, -0.4f, 0.5f, -1f));
+        ragdoll.link("Calf.L", 1f, new RangeOfMotion(2f, 0f, 0f, 0f, 0f, 0f));
+        ragdoll.link("Foot.L", 1f, new RangeOfMotion(0.3f, 0.5f, 0f));
+    }
+
+    @Override
+    public void onAction(String binding, boolean isPressed, float tpf) {
+        if (binding.equals("Rotate Left")) {
+            if (isPressed) {
+                leftRotate = true;
+            } else {
+                leftRotate = false;
+            }
+        } else if (binding.equals("Rotate Right")) {
+            if (isPressed) {
+                rightRotate = true;
+            } else {
+                rightRotate = false;
+            }
+        } else if (binding.equals("Slice")) {
+            if (isPressed) {
+                composer.setCurrentAction("SliceOnce");
+            }
+        } else if (binding.equals("Walk Forward")) {
+            if (isPressed) {
+                forward = true;
+            } else {
+                forward = false;
+            }
+        } else if (binding.equals("Walk Backward")) {
+            if (isPressed) {
+                backward = true;
+            } else {
+                backward = false;
+            }
+        }
+    }
+
+    @Override
+    public void simpleInitApp() {
+        flyCam.setMoveSpeed(50f);
+        cam.setLocation(new Vector3f(-16f, 4.7f, -1.6f));
+        cam.setRotation(new Quaternion(0.0484f, 0.804337f, -0.066f, 0.5885f));
+
+        setupKeys();
+        setupLight();
+
+        BulletAppState bulletAppState = new BulletAppState();
+        stateManager.attach(bulletAppState);
+        //bulletAppState.setDebugEnabled(true);
+        physicsSpace = bulletAppState.getPhysicsSpace();
+
+        PhysicsTestHelper.createPhysicsTestWorld(rootNode, assetManager,
+                physicsSpace);
+        initWall(2f, 1f, 1f);
+
+        model = (Node) assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml");
+        rootNode.attachChild(model);
+        model.lookAt(new Vector3f(0f, 0f, -1f), Vector3f.UNIT_Y);
+        model.setLocalTranslation(4f, 0f, -7f);
+
+        composer = model.getControl(AnimComposer.class);
+        composer.setCurrentAction("IdleTop");
+
+        Action slice = composer.action("SliceHorizontal");
+        composer.actionSequence("SliceOnce",
+                slice, Tweens.callMethod(this, "onSliceDone"));
+
+        DynamicAnimControl ragdoll = new DynamicAnimControl();
+        setupSinbad(ragdoll);
+        model.addControl(ragdoll);
+        physicsSpace.add(ragdoll);
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        if (forward) {
+            model.move(model.getLocalRotation().multLocal(new Vector3f(0f, 0f, tpf)));
+        } else if (backward) {
+            model.move(model.getLocalRotation().multLocal(new Vector3f(0f, 0f, -tpf)));
+        } else if (leftRotate) {
+            model.rotate(0f, tpf, 0f);
+        } else if (rightRotate) {
+            model.rotate(0f, -tpf, 0f);
+        }
+    }
+
+    private void initWall(float bLength, float bWidth, float bHeight) {
+        Box brick = new Box(bLength, bHeight, bWidth);
+        brick.scaleTextureCoordinates(new Vector2f(1f, 0.5f));
+        Material mat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        TextureKey key = new TextureKey("Textures/Terrain/BrickWall/BrickWall.jpg");
+        key.setGenerateMips(true);
+        Texture tex = assetManager.loadTexture(key);
+        mat2.setTexture("ColorMap", tex);
+
+        float startpt = bLength / 4f;
+        float height = -5f;
+        for (int j = 0; j < 15; j++) {
+            for (int i = 0; i < 4; i++) {
+                Vector3f ori = new Vector3f(i * bLength * 2f + startpt, bHeight + height, -10f);
+                Geometry brickGeometry = new Geometry("brick", brick);
+                brickGeometry.setMaterial(mat2);
+                brickGeometry.setLocalTranslation(ori);
+                // for geometry with sphere mesh the physics system automatically uses a sphere collision shape
+                brickGeometry.addControl(new RigidBodyControl(1.5f));
+                brickGeometry.setShadowMode(ShadowMode.CastAndReceive);
+                brickGeometry.getControl(RigidBodyControl.class).setFriction(0.6f);
+                this.rootNode.attachChild(brickGeometry);
+                physicsSpace.add(brickGeometry);
+            }
+            startpt = -startpt;
+            height += 2f * bHeight;
+        }
+    }
+
+    private void setupKeys() {
+        inputManager.addMapping("Rotate Left",
+                new KeyTrigger(KeyInput.KEY_H));
+        inputManager.addMapping("Rotate Right",
+                new KeyTrigger(KeyInput.KEY_K));
+        inputManager.addMapping("Walk Backward",
+                new KeyTrigger(KeyInput.KEY_J));
+        inputManager.addMapping("Walk Forward",
+                new KeyTrigger(KeyInput.KEY_U));
+        inputManager.addMapping("Slice",
+                new KeyTrigger(KeyInput.KEY_SPACE),
+                new KeyTrigger(KeyInput.KEY_RETURN));
+
+        inputManager.addListener(this, "Rotate Left", "Rotate Right", "Slice",
+                "Walk Backward", "Walk Forward");
+    }
+
+    private void setupLight() {
+        DirectionalLight dl = new DirectionalLight();
+        dl.setDirection(new Vector3f(-0.1f, -0.7f, -1f).normalizeLocal());
+        dl.setColor(new ColorRGBA(1f, 1f, 1f, 1f));
+        rootNode.addLight(dl);
+    }
+}

+ 111 - 0
jme3-examples/src/main/java/jme3test/bullet/TestSimplePhysics.java

@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package jme3test.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.*;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.math.Plane;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Node;
+import com.jme3.scene.shape.Sphere;
+
+/**
+ * This is a basic Test of jbullet-jme functions
+ *
+ * @author normenhansen
+ */
+public class TestSimplePhysics extends SimpleApplication {
+
+    private BulletAppState bulletAppState;
+
+    public static void main(String[] args) {
+        TestSimplePhysics app = new TestSimplePhysics();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        bulletAppState = new BulletAppState();
+        stateManager.attach(bulletAppState);
+        bulletAppState.setDebugEnabled(true);
+
+        // Add a physics sphere to the world
+        Node physicsSphere = PhysicsTestHelper.createPhysicsTestNode(assetManager, new SphereCollisionShape(1), 1);
+        physicsSphere.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(3, 6, 0));
+        rootNode.attachChild(physicsSphere);
+        getPhysicsSpace().add(physicsSphere);
+
+        // Add a physics sphere to the world using the collision shape from sphere one
+        Node physicsSphere2 = PhysicsTestHelper.createPhysicsTestNode(assetManager, physicsSphere.getControl(RigidBodyControl.class).getCollisionShape(), 1);
+        physicsSphere2.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(4, 8, 0));
+        rootNode.attachChild(physicsSphere2);
+        getPhysicsSpace().add(physicsSphere2);
+
+        // Add a physics box to the world
+        Node physicsBox = PhysicsTestHelper.createPhysicsTestNode(assetManager, new BoxCollisionShape(new Vector3f(1, 1, 1)), 1);
+        physicsBox.getControl(RigidBodyControl.class).setFriction(0.1f);
+        physicsBox.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(.6f, 4, .5f));
+        rootNode.attachChild(physicsBox);
+        getPhysicsSpace().add(physicsBox);
+
+        // Add a physics cylinder to the world
+        Node physicsCylinder = PhysicsTestHelper.createPhysicsTestNode(assetManager, new CylinderCollisionShape(new Vector3f(1f, 1f, 1.5f)), 1);
+        physicsCylinder.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(2, 2, 0));
+        rootNode.attachChild(physicsCylinder);
+        getPhysicsSpace().add(physicsCylinder);
+
+        // an obstacle mesh, does not move (mass=0)
+        Node node2 = PhysicsTestHelper.createPhysicsTestNode(assetManager, new MeshCollisionShape(new Sphere(16, 16, 1.2f)), 0);
+        node2.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(2.5f, -4, 0f));
+        rootNode.attachChild(node2);
+        getPhysicsSpace().add(node2);
+
+        // the floor mesh, does not move (mass=0)
+        Node node3 = PhysicsTestHelper.createPhysicsTestNode(assetManager, new PlaneCollisionShape(new Plane(new Vector3f(0, 1, 0), 0)), 0);
+        node3.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(0f, -6, 0f));
+        rootNode.attachChild(node3);
+        getPhysicsSpace().add(node3);
+
+        // Join the physics objects with a Point2Point joint
+//        PhysicsPoint2PointJoint joint=new PhysicsPoint2PointJoint(physicsSphere, physicsBox, new Vector3f(-2,0,0), new Vector3f(2,0,0));
+//        PhysicsHingeJoint joint=new PhysicsHingeJoint(physicsSphere, physicsBox, new Vector3f(-2,0,0), new Vector3f(2,0,0), Vector3f.UNIT_Z,Vector3f.UNIT_Z);
+//        getPhysicsSpace().add(joint);
+
+    }
+
+    private PhysicsSpace getPhysicsSpace() {
+        return bulletAppState.getPhysicsSpace();
+    }
+}

+ 77 - 0
jme3-examples/src/main/java/jme3test/bullet/TestSweepTest.java

@@ -0,0 +1,77 @@
+package jme3test.bullet;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.collision.PhysicsCollisionObject;
+import com.jme3.bullet.collision.PhysicsSweepTestResult;
+import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.math.Transform;
+import com.jme3.scene.Node;
+import java.util.List;
+
+/**
+ *
+ * A spatial moves and sweeps its next movement for obstacles before moving
+ * there Run this example with Vsync enabled
+ *
+ * @author wezrule
+ */
+public class TestSweepTest extends SimpleApplication {
+
+    final private BulletAppState bulletAppState = new BulletAppState();
+    private CapsuleCollisionShape capsuleCollisionShape;
+    private Node capsule;
+    final private float dist = .5f;
+
+    public static void main(String[] args) {
+        new TestSweepTest().start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        CapsuleCollisionShape obstacleCollisionShape
+                = new CapsuleCollisionShape(0.3f, 0.5f);
+        capsuleCollisionShape = new CapsuleCollisionShape(1f, 1f);
+
+        stateManager.attach(bulletAppState);
+
+        capsule = new Node("capsule");
+        capsule.move(-2, 0, 0);
+        capsule.addControl(new RigidBodyControl(capsuleCollisionShape, 1));
+        capsule.getControl(RigidBodyControl.class).setKinematic(true);
+        bulletAppState.getPhysicsSpace().add(capsule);
+        rootNode.attachChild(capsule);
+
+        Node obstacle = new Node("obstacle");
+        obstacle.move(2, 0, 0);
+        RigidBodyControl bodyControl = new RigidBodyControl(obstacleCollisionShape, 0);
+        obstacle.addControl(bodyControl);
+        bulletAppState.getPhysicsSpace().add(obstacle);
+        rootNode.attachChild(obstacle);
+
+        bulletAppState.setDebugEnabled(true);
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+
+        float move = tpf * 1;
+        boolean colliding = false;
+
+        List<PhysicsSweepTestResult> sweepTest = bulletAppState.getPhysicsSpace().sweepTest(capsuleCollisionShape, new Transform(capsule.getWorldTranslation()), new Transform(capsule.getWorldTranslation().add(dist, 0, 0)));
+
+        for (PhysicsSweepTestResult result : sweepTest) {
+            if (result.getCollisionObject().getCollisionShape() != capsuleCollisionShape) {
+                PhysicsCollisionObject collisionObject = result.getCollisionObject();
+                fpsText.setText("Almost colliding with " + collisionObject.getUserObject().toString());
+                colliding = true;
+            }
+        }
+
+        if (!colliding) {
+            // if the sweep is clear then move the spatial
+            capsule.move(move, 0, 0);
+        }
+    }
+}

+ 460 - 0
jme3-examples/src/main/java/jme3test/bullet/TestWalkingChar.java

@@ -0,0 +1,460 @@
+/*
+ * 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 jme3test.bullet;
+
+import com.jme3.anim.AnimComposer;
+import com.jme3.anim.Armature;
+import com.jme3.anim.ArmatureMask;
+import com.jme3.anim.SkinningControl;
+import com.jme3.anim.tween.Tween;
+import com.jme3.anim.tween.Tweens;
+import com.jme3.anim.tween.action.Action;
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.PhysicsCollisionEvent;
+import com.jme3.bullet.collision.PhysicsCollisionListener;
+import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
+import com.jme3.bullet.collision.shapes.SphereCollisionShape;
+import com.jme3.bullet.control.CharacterControl;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.bullet.util.CollisionShapeFactory;
+import com.jme3.effect.ParticleEmitter;
+import com.jme3.effect.ParticleMesh.Type;
+import com.jme3.effect.shapes.EmitterSphereShape;
+import com.jme3.input.ChaseCamera;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.math.*;
+import com.jme3.post.FilterPostProcessor;
+import com.jme3.post.filters.BloomFilter;
+import com.jme3.renderer.Camera;
+import com.jme3.renderer.queue.RenderQueue.ShadowMode;
+import com.jme3.scene.*;
+import com.jme3.scene.shape.Box;
+import com.jme3.scene.shape.Sphere;
+import com.jme3.scene.shape.Sphere.TextureMode;
+import com.jme3.terrain.geomipmap.TerrainLodControl;
+import com.jme3.terrain.geomipmap.TerrainQuad;
+import com.jme3.terrain.heightmap.AbstractHeightMap;
+import com.jme3.terrain.heightmap.ImageBasedHeightMap;
+import com.jme3.texture.Texture;
+import com.jme3.texture.Texture.WrapMode;
+import com.jme3.util.SkyFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A walking animated character followed by a 3rd person camera on a terrain with LOD.
+ * @author normenhansen
+ */
+public class TestWalkingChar extends SimpleApplication
+        implements ActionListener, PhysicsCollisionListener {
+
+    private BulletAppState bulletAppState;
+    //character
+    private CharacterControl character;
+    private Node model;
+    //temp vectors
+    final private Vector3f walkDirection = new Vector3f();
+    //Materials
+    private Material matBullet;
+    //animation
+    private Action standAction;
+    private Action walkAction;
+    private AnimComposer composer;
+    private float airTime = 0;
+    //camera
+    private boolean left = false, right = false, up = false, down = false;
+    //bullet
+    private Sphere bullet;
+    private SphereCollisionShape bulletCollisionShape;
+    //explosion
+    private ParticleEmitter effect;
+    //brick wall
+    private Box brick;
+    final private float bLength = 0.8f;
+    final private float bWidth = 0.4f;
+    final private float bHeight = 0.4f;
+
+    public static void main(String[] args) {
+        TestWalkingChar app = new TestWalkingChar();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        bulletAppState = new BulletAppState();
+        bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL);
+        stateManager.attach(bulletAppState);
+        setupKeys();
+        prepareBullet();
+        prepareEffect();
+        createLight();
+        createSky();
+        createTerrain();
+        createWall();
+        createCharacter();
+        setupChaseCamera();
+        setupAnimationController();
+        setupFilter();
+    }
+
+    private void setupFilter() {
+        FilterPostProcessor fpp = new FilterPostProcessor(assetManager);
+        BloomFilter bloom = new BloomFilter(BloomFilter.GlowMode.Objects);
+        fpp.addFilter(bloom);
+        viewPort.addProcessor(fpp);
+    }
+
+    private PhysicsSpace getPhysicsSpace() {
+        return bulletAppState.getPhysicsSpace();
+    }
+
+    private void setupKeys() {
+        inputManager.addMapping("wireframe", new KeyTrigger(KeyInput.KEY_T));
+        inputManager.addListener(this, "wireframe");
+        inputManager.addMapping("CharLeft", new KeyTrigger(KeyInput.KEY_A));
+        inputManager.addMapping("CharRight", new KeyTrigger(KeyInput.KEY_D));
+        inputManager.addMapping("CharUp", new KeyTrigger(KeyInput.KEY_W));
+        inputManager.addMapping("CharDown", new KeyTrigger(KeyInput.KEY_S));
+        inputManager.addMapping("CharSpace", new KeyTrigger(KeyInput.KEY_RETURN));
+        inputManager.addMapping("CharShoot", new KeyTrigger(KeyInput.KEY_SPACE));
+        inputManager.addListener(this, "CharLeft");
+        inputManager.addListener(this, "CharRight");
+        inputManager.addListener(this, "CharUp");
+        inputManager.addListener(this, "CharDown");
+        inputManager.addListener(this, "CharSpace");
+        inputManager.addListener(this, "CharShoot");
+    }
+
+    private void createWall() {
+        float xOff = -144;
+        float zOff = -40;
+        float startpt = bLength / 4 - xOff;
+        float height = 6.1f;
+        brick = new Box(bLength, bHeight, bWidth);
+        brick.scaleTextureCoordinates(new Vector2f(1f, .5f));
+        for (int j = 0; j < 15; j++) {
+            for (int i = 0; i < 4; i++) {
+                Vector3f vt = new Vector3f(i * bLength * 2 + startpt, bHeight + height, zOff);
+                addBrick(vt);
+            }
+            startpt = -startpt;
+            height += 1.01f * bHeight;
+        }
+    }
+
+    private void addBrick(Vector3f ori) {
+        Geometry brickGeometry = new Geometry("brick", brick);
+        brickGeometry.setMaterial(matBullet);
+        brickGeometry.setLocalTranslation(ori);
+        brickGeometry.addControl(new RigidBodyControl(1.5f));
+        brickGeometry.setShadowMode(ShadowMode.CastAndReceive);
+        this.rootNode.attachChild(brickGeometry);
+        this.getPhysicsSpace().add(brickGeometry);
+    }
+
+    private void prepareBullet() {
+        bullet = new Sphere(32, 32, 0.4f, true, false);
+        bullet.setTextureMode(TextureMode.Projected);
+        bulletCollisionShape = new SphereCollisionShape(0.4f);
+        matBullet = new Material(getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
+        matBullet.setColor("Color", ColorRGBA.Green);
+        matBullet.setColor("GlowColor", ColorRGBA.Green);
+        getPhysicsSpace().addCollisionListener(this);
+    }
+
+    private void prepareEffect() {
+        int COUNT_FACTOR = 1;
+        float COUNT_FACTOR_F = 1f;
+        effect = new ParticleEmitter("Flame", Type.Triangle, 32 * COUNT_FACTOR);
+        effect.setSelectRandomImage(true);
+        effect.setStartColor(new ColorRGBA(1f, 0.4f, 0.05f, (1f / COUNT_FACTOR_F)));
+        effect.setEndColor(new ColorRGBA(.4f, .22f, .12f, 0f));
+        effect.setStartSize(1.3f);
+        effect.setEndSize(2f);
+        effect.setShape(new EmitterSphereShape(Vector3f.ZERO, 1f));
+        effect.setParticlesPerSec(0);
+        effect.setGravity(0, -5, 0);
+        effect.setLowLife(.4f);
+        effect.setHighLife(.5f);
+        effect.getParticleInfluencer()
+                .setInitialVelocity(new Vector3f(0, 7, 0));
+        effect.getParticleInfluencer().setVelocityVariation(1f);
+        effect.setImagesX(2);
+        effect.setImagesY(2);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
+        mat.setTexture("Texture", assetManager.loadTexture("Effects/Explosion/flame.png"));
+        effect.setMaterial(mat);
+//        effect.setLocalScale(100);
+        rootNode.attachChild(effect);
+    }
+
+    private void createLight() {
+        Vector3f direction = new Vector3f(-0.1f, -0.7f, -1).normalizeLocal();
+        DirectionalLight dl = new DirectionalLight();
+        dl.setDirection(direction);
+        dl.setColor(new ColorRGBA(1f, 1f, 1f, 1.0f));
+        rootNode.addLight(dl);
+    }
+
+    private void createSky() {
+        rootNode.attachChild(SkyFactory.createSky(assetManager, 
+                "Textures/Sky/Bright/BrightSky.dds", 
+                SkyFactory.EnvMapType.CubeMap));
+    }
+
+    private void createTerrain() {
+        Material matRock = new Material(assetManager, "Common/MatDefs/Terrain/TerrainLighting.j3md");
+        matRock.setBoolean("useTriPlanarMapping", false);
+        matRock.setBoolean("WardIso", true);
+        matRock.setTexture("AlphaMap", assetManager.loadTexture("Textures/Terrain/splat/alphamap.png"));
+        Texture heightMapImage = assetManager.loadTexture("Textures/Terrain/splat/mountains512.png");
+        Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg");
+        grass.setWrap(WrapMode.Repeat);
+        matRock.setTexture("DiffuseMap", grass);
+        matRock.setFloat("DiffuseMap_0_scale", 64);
+        Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg");
+        dirt.setWrap(WrapMode.Repeat);
+        matRock.setTexture("DiffuseMap_1", dirt);
+        matRock.setFloat("DiffuseMap_1_scale", 16);
+        Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg");
+        rock.setWrap(WrapMode.Repeat);
+        matRock.setTexture("DiffuseMap_2", rock);
+        matRock.setFloat("DiffuseMap_2_scale", 128);
+        Texture normalMap0 = assetManager.loadTexture("Textures/Terrain/splat/grass_normal.jpg");
+        normalMap0.setWrap(WrapMode.Repeat);
+        Texture normalMap1 = assetManager.loadTexture("Textures/Terrain/splat/dirt_normal.png");
+        normalMap1.setWrap(WrapMode.Repeat);
+        Texture normalMap2 = assetManager.loadTexture("Textures/Terrain/splat/road_normal.png");
+        normalMap2.setWrap(WrapMode.Repeat);
+        matRock.setTexture("NormalMap", normalMap0);
+        matRock.setTexture("NormalMap_1", normalMap1);
+        matRock.setTexture("NormalMap_2", normalMap2);
+
+        AbstractHeightMap heightmap = null;
+        try {
+            heightmap = new ImageBasedHeightMap(heightMapImage.getImage(), 0.25f);
+            heightmap.load();
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        TerrainQuad terrain
+                = new TerrainQuad("terrain", 65, 513, heightmap.getHeightMap());
+        List<Camera> cameras = new ArrayList<>();
+        cameras.add(getCamera());
+        TerrainLodControl control = new TerrainLodControl(terrain, cameras);
+        terrain.addControl(control);
+        terrain.setMaterial(matRock);
+        terrain.setLocalScale(new Vector3f(2, 2, 2));
+
+        RigidBodyControl terrainPhysicsNode
+                = new RigidBodyControl(CollisionShapeFactory.createMeshShape(terrain), 0);
+        terrain.addControl(terrainPhysicsNode);
+        rootNode.attachChild(terrain);
+        getPhysicsSpace().add(terrainPhysicsNode);
+    }
+
+    private void createCharacter() {
+        CapsuleCollisionShape capsule = new CapsuleCollisionShape(3f, 4f);
+        character = new CharacterControl(capsule, 0.01f);
+        model = (Node) assetManager.loadModel("Models/Oto/Oto.mesh.xml");
+        model.addControl(character);
+        character.setPhysicsLocation(new Vector3f(-140, 40, -10));
+        rootNode.attachChild(model);
+        getPhysicsSpace().add(character);
+    }
+
+    private void setupChaseCamera() {
+        flyCam.setEnabled(false);
+        new ChaseCamera(cam, model, inputManager);
+    }
+
+    private void setupAnimationController() {
+        composer = model.getControl(AnimComposer.class);
+        standAction = composer.action("stand");
+        walkAction = composer.action("Walk");
+        /*
+         * Add a "shootOnce" animation action
+         * that performs the "Dodge" action one time only.
+         */
+        Action dodgeAction = composer.action("Dodge");
+        Tween doneTween = Tweens.callMethod(this, "onShootDone");
+        composer.actionSequence("shootOnce", dodgeAction, doneTween);
+        /*
+         * Define a shooting animation layer
+         * that animates only the joints of the right arm.
+         */
+        SkinningControl skinningControl
+                = model.getControl(SkinningControl.class);
+        Armature armature = skinningControl.getArmature();
+        ArmatureMask shootingMask
+                = ArmatureMask.createMask(armature, "uparm.right");
+        composer.makeLayer("shootingLayer", shootingMask);
+        /*
+         * Define a walking animation layer
+         * that animates all joints except those used for shooting.
+         */
+        ArmatureMask walkingMask = new ArmatureMask();
+        walkingMask.addBones(armature, "head", "spine", "spinehigh");
+        walkingMask.addFromJoint(armature, "hip.left");
+        walkingMask.addFromJoint(armature, "hip.right");
+        walkingMask.addFromJoint(armature, "uparm.left");
+        composer.makeLayer("walkingLayer", walkingMask);
+
+        composer.setCurrentAction("stand", "shootingLayer");
+        composer.setCurrentAction("stand", "walkingLayer");
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        Vector3f camDir = cam.getDirection().clone().multLocal(0.1f);
+        Vector3f camLeft = cam.getLeft().clone().multLocal(0.1f);
+        camDir.y = 0;
+        camLeft.y = 0;
+        walkDirection.set(0, 0, 0);
+        if (left) {
+            walkDirection.addLocal(camLeft);
+        }
+        if (right) {
+            walkDirection.addLocal(camLeft.negate());
+        }
+        if (up) {
+            walkDirection.addLocal(camDir);
+        }
+        if (down) {
+            walkDirection.addLocal(camDir.negate());
+        }
+        if (!character.onGround()) {
+            airTime = airTime + tpf;
+        } else {
+            airTime = 0;
+        }
+
+        Action action = composer.getCurrentAction("walkingLayer");
+        if (walkDirection.length() == 0f) {
+            if (action != standAction) {
+                composer.setCurrentAction("stand", "walkingLayer");
+            }
+        } else {
+            character.setViewDirection(walkDirection);
+            if (airTime > 0.3f) {
+                if (action != standAction) {
+                    composer.setCurrentAction("stand", "walkingLayer");
+                }
+            } else if (action != walkAction) {
+                composer.setCurrentAction("Walk", "walkingLayer");
+            }
+        }
+        character.setWalkDirection(walkDirection);
+    }
+
+    @Override
+    public void onAction(String binding, boolean value, float tpf) {
+        if (binding.equals("CharLeft")) {
+            if (value) {
+                left = true;
+            } else {
+                left = false;
+            }
+        } else if (binding.equals("CharRight")) {
+            if (value) {
+                right = true;
+            } else {
+                right = false;
+            }
+        } else if (binding.equals("CharUp")) {
+            if (value) {
+                up = true;
+            } else {
+                up = false;
+            }
+        } else if (binding.equals("CharDown")) {
+            if (value) {
+                down = true;
+            } else {
+                down = false;
+            }
+        } else if (binding.equals("CharSpace")) {
+            character.jump();
+        } else if (binding.equals("CharShoot") && !value) {
+            bulletControl();
+        }
+    }
+
+    private void bulletControl() {
+        composer.setCurrentAction("shootOnce", "shootingLayer");
+
+        Geometry bulletGeometry = new Geometry("bullet", bullet);
+        bulletGeometry.setMaterial(matBullet);
+        bulletGeometry.setShadowMode(ShadowMode.CastAndReceive);
+        bulletGeometry.setLocalTranslation(character.getPhysicsLocation().add(cam.getDirection().mult(5)));
+        RigidBodyControl bulletControl = new BombControl(bulletCollisionShape, 1);
+        bulletControl.setCcdMotionThreshold(0.1f);
+        bulletControl.setLinearVelocity(cam.getDirection().mult(80));
+        bulletGeometry.addControl(bulletControl);
+        rootNode.attachChild(bulletGeometry);
+        getPhysicsSpace().add(bulletControl);
+    }
+
+    @Override
+    public void collision(PhysicsCollisionEvent event) {
+        if (event.getObjectA() instanceof BombControl) {
+            final Spatial node = event.getNodeA();
+            effect.killAllParticles();
+            effect.setLocalTranslation(node.getLocalTranslation());
+            effect.emitAllParticles();
+        } else if (event.getObjectB() instanceof BombControl) {
+            final Spatial node = event.getNodeB();
+            effect.killAllParticles();
+            effect.setLocalTranslation(node.getLocalTranslation());
+            effect.emitAllParticles();
+        }
+    }
+
+    /**
+     * Callback to indicate that the "shootOnce" animation action has completed.
+     */
+    void onShootDone() {
+        /*
+         * Play the "stand" animation action on the shooting layer.
+         */
+        composer.setCurrentAction("stand", "shootingLayer");
+    }
+}

+ 35 - 0
jme3-examples/src/main/java/jme3test/bullet/package-info.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 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.
+ */
+/**
+ * example apps and non-automated tests for Bullet physics
+ */
+package jme3test.bullet;

+ 342 - 0
jme3-examples/src/main/java/jme3test/bullet/shape/TestGimpactShape.java

@@ -0,0 +1,342 @@
+/*
+ * 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 jme3test.bullet.shape;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.shapes.GImpactCollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.bullet.debug.BulletDebugAppState;
+import com.jme3.font.BitmapFont;
+import com.jme3.font.BitmapText;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.shape.PQTorus;
+import com.jme3.scene.shape.Torus;
+import com.jme3.system.AppSettings;
+import java.util.ArrayList;
+import java.util.List;
+import jme3test.bullet.PhysicsTestHelper;
+
+/**
+ * This test demonstrates various GImpactCollisionShapes colliding against two identical curved surfaces. The
+ * left surface is a MeshCollisionShape, right surface is another GImpactCollisionShape. An ideal result is
+ * for all objects to land and change to a blue colored mesh indicating they are inactive. Falling through the
+ * floor, or never going inactive (bouncing forever) are failure conditions.
+ * <p>
+ * Observations as of June 2019 (JME v3.3.0-alpha2):
+ * <ol>
+ * <li>
+ * With default starting parameters, Native Bullet should pass the test parameters above. JBullet fails due to
+ * the rocket/MeshCollisionShape never going inactive.
+ * </li>
+ * <li>
+ * Native Bullet behaves better than JBullet. JBullet sometimes allows objects to "gain too much energy" after
+ * a collision, such as the rocket or teapot. Native also does this, to a lesser degree. This generally
+ * appears to happen at larger object scales.
+ * </li>
+ * <li>
+ * JBullet allows some objects to get "stuck" inside the floor, which usually results in a fall-through
+ * eventually, generally a larger scales for this test.
+ * </li>
+ * <li>
+ * Some shapes such as PQTorus and signpost never go inactive at larger scales for both Native and JBullet (test
+ * at 1.5 and 1.9 scale)
+ * </li>
+ * </ol>
+ *
+ * @author lou
+ */
+public class TestGimpactShape extends SimpleApplication {
+
+    private static TestGimpactShape test;
+    private BulletAppState bulletAppState;
+    private int solverNumIterations = 10;
+    private BitmapText timeElapsedTxt;
+    private BitmapText solverNumIterationsTxt;
+    private BitmapText testScale;
+    private final List<Spatial> testObjects = new ArrayList<>();
+    private float testTimer = 0;
+    private float scaleMod = 1;
+    private boolean restart = true;
+    private static final boolean SKIP_SETTINGS = false;//Used for repeated runs of this test during dev
+
+    public static void main(String[] args) {
+        test = new TestGimpactShape();
+        test.setSettings(new AppSettings(true));
+        test.settings.setVSync(true);
+        if (SKIP_SETTINGS) {
+            test.settings.setWidth(1920);
+            test.settings.setHeight(1150);
+            test.showSettings = !SKIP_SETTINGS;
+        }
+        test.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        test = this;
+        getCamera().setLocation(new Vector3f(40, 30, 160));
+        getCamera().lookAt(new Vector3f(40, -5, 0), Vector3f.UNIT_Y);
+        getFlyByCamera().setMoveSpeed(25);
+
+        DirectionalLight dl = new DirectionalLight();
+        dl.setDirection(new Vector3f(-1, -1, -1).normalizeLocal());
+        dl.setColor(ColorRGBA.Green);
+        rootNode.addLight(dl);
+
+        //Setup test instructions
+        guiNode = getGuiNode();
+        BitmapFont font = assetManager.loadFont("Interface/Fonts/Default.fnt");
+        BitmapText[] testInfo = new BitmapText[2];
+        testInfo[0] = new BitmapText(font);
+        testInfo[1] = new BitmapText(font);
+        timeElapsedTxt = new BitmapText(font);
+        solverNumIterationsTxt = new BitmapText(font);
+        testScale = new BitmapText(font);
+
+        float lineHeight = testInfo[0].getLineHeight();
+        testInfo[0].setText("Camera move:W/A/S/D/Q/Z     Solver iterations: 1=10, 2=20, 3=30");
+        testInfo[0].setLocalTranslation(5, test.settings.getHeight(), 0);
+        guiNode.attachChild(testInfo[0]);
+        testInfo[1].setText("P: Toggle pause     Inc/Dec object scale: +, -     Space: Restart test");
+        testInfo[1].setLocalTranslation(5, test.settings.getHeight() - lineHeight, 0);
+        guiNode.attachChild(testInfo[1]);
+
+        timeElapsedTxt.setLocalTranslation(202, lineHeight * 1, 0);
+        guiNode.attachChild(timeElapsedTxt);
+        solverNumIterationsTxt.setLocalTranslation(202, lineHeight * 2, 0);
+        guiNode.attachChild(solverNumIterationsTxt);
+        testScale.setLocalTranslation(202, lineHeight * 3, 0);
+        guiNode.attachChild(testScale);
+
+        //Setup interactive test controls
+        inputManager.addMapping("restart", new KeyTrigger(KeyInput.KEY_SPACE));
+        inputManager.addListener((ActionListener) (String name, boolean isPressed, float tpf) -> {
+            restart = true;
+        }, "restart");
+
+        inputManager.addMapping("pause", new KeyTrigger(KeyInput.KEY_P));
+        inputManager.addListener((ActionListener) (String name, boolean isPressed, float tpf) -> {
+            if (!isPressed) {
+                return;
+            }
+            bulletAppState.setSpeed(bulletAppState.getSpeed() > 0.1 ? 0 : 1);
+        }, "pause");
+
+        inputManager.addMapping("1", new KeyTrigger(KeyInput.KEY_1));
+        inputManager.addMapping("2", new KeyTrigger(KeyInput.KEY_2));
+        inputManager.addMapping("3", new KeyTrigger(KeyInput.KEY_3));
+        inputManager.addMapping("+", new KeyTrigger(KeyInput.KEY_ADD), new KeyTrigger(KeyInput.KEY_EQUALS));
+        inputManager.addMapping("-", new KeyTrigger(KeyInput.KEY_SUBTRACT), new KeyTrigger(KeyInput.KEY_MINUS));
+        inputManager.addListener((ActionListener) (String name, boolean isPressed, float tpf) -> {
+            if (!isPressed) {
+                return;
+            }
+            switch (name) {
+                case "1":
+                    solverNumIterations = 10;
+                    break;
+                case "2":
+                    solverNumIterations = 20;
+                    break;
+                case "3":
+                    solverNumIterations = 30;
+                    break;
+                case "+":
+                    scaleMod += scaleMod < 1.9f ? 0.1f : 0;
+                    break;
+                case "-":
+                    scaleMod -= scaleMod > 0.5f ? 0.1f : 0;
+                    break;
+            }
+            restart = true;
+        }, "1", "2", "3", "+", "-");
+
+        initializeNewTest();
+    }
+
+    private void initializeNewTest() {
+        testScale.setText("Object scale: " + String.format("%.1f", scaleMod));
+        solverNumIterationsTxt.setText("Solver Iterations: " + solverNumIterations);
+
+        bulletAppState = new BulletAppState();
+        bulletAppState.setDebugEnabled(true);
+        stateManager.attach(bulletAppState);
+        bulletAppState.getPhysicsSpace().setSolverNumIterations(solverNumIterations);
+
+        float floorSize = 80;
+        //Left side test - GImpact objects collide with MeshCollisionShape floor
+        Vector3f leftFloorPos = new Vector3f(-41, -5, -10);
+        Vector3f leftFloorCenter = leftFloorPos.add(floorSize / 2, 0, floorSize / 2);
+
+        dropTest1(leftFloorCenter);
+        dropTest2(leftFloorCenter);
+        dropPot(leftFloorCenter);
+        dropSword(leftFloorCenter);
+        dropSign(leftFloorCenter);
+        dropRocket(leftFloorCenter);
+
+        Geometry leftFloor = PhysicsTestHelper.createMeshTestFloor(assetManager, floorSize, leftFloorPos);
+        addObject(leftFloor);
+
+        //Right side test - GImpact objects collide with GImpact floor
+        Vector3f rightFloorPos = new Vector3f(41, -5, -10);
+        Vector3f rightFloorCenter = rightFloorPos.add(floorSize / 2, 0, floorSize / 2);
+
+        dropTest1(rightFloorCenter);
+        dropTest2(rightFloorCenter);
+        dropPot(rightFloorCenter);
+        dropSword(rightFloorCenter);
+        dropSign(rightFloorCenter);
+        dropRocket(rightFloorCenter);
+
+        Geometry rightFloor = PhysicsTestHelper.createGImpactTestFloor(assetManager, floorSize, rightFloorPos);
+        addObject(rightFloor);
+
+        //Hide physics debug visualization for floors
+        BulletDebugAppState bulletDebugAppState = stateManager.getState(BulletDebugAppState.class);
+        bulletDebugAppState.setFilter((Object obj) -> {
+            return !(obj.equals(rightFloor.getControl(RigidBodyControl.class))
+                || obj.equals(leftFloor.getControl(RigidBodyControl.class)));
+        });
+    }
+
+    private void addObject(Spatial s) {
+        testObjects.add(s);
+        rootNode.attachChild(s);
+        physicsSpace().add(s);
+    }
+
+    private void dropTest1(Vector3f offset) {
+        offset = offset.add(-18, 6, -18);
+        attachTestObject(new Torus(16, 16, 0.15f, 0.5f), new Vector3f(-12f, 0f, 5f).add(offset), 1);
+        attachTestObject(new PQTorus(2f, 3f, 0.6f, 0.2f, 48, 16), new Vector3f(0, 0, 0).add(offset), 5);
+
+    }
+
+    private void dropTest2(Vector3f offset) {
+        offset = offset.add(18, 6, -18);
+        attachTestObject(new Torus(16, 16, 0.3f, 0.8f), new Vector3f(12f, 0f, 5f).add(offset), 3);
+        attachTestObject(new PQTorus(3f, 5f, 0.8f, 0.2f, 96, 16), new Vector3f(0, 0, 0).add(offset), 10);
+    }
+
+    private void dropPot(Vector3f offset) {
+        drop(offset.add(-12, 7, 15), "Models/Teapot/Teapot.mesh.xml", 1.0f, 2);
+    }
+
+    private void dropSword(Vector3f offset) {
+        drop(offset.add(-10, 5, 3), "Models/Sinbad/Sword.mesh.xml", 1.0f, 2);
+    }
+
+    private void dropSign(Vector3f offset) {
+        drop(offset.add(9, 15, 5), "Models/Sign Post/Sign Post.mesh.xml", 1.0f, 1);
+    }
+
+    private void dropRocket(Vector3f offset) {
+        RigidBodyControl c = drop(offset.add(26, 4, 7), "Models/SpaceCraft/Rocket.mesh.xml", 4.0f, 3);
+        c.setAngularDamping(0.5f);
+        c.setLinearDamping(0.5f);
+    }
+
+    private RigidBodyControl drop(Vector3f offset, String model, float scale, float mass) {
+        scale *= scaleMod;
+        Node n = (Node) assetManager.loadModel(model);
+        n.setLocalTranslation(offset);
+        n.rotate(0, 0, -FastMath.HALF_PI);
+
+        Geometry tp = ((Geometry) n.getChild(0));
+        tp.scale(scale);
+        Material mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        tp.setMaterial(mat);
+
+        Mesh mesh = tp.getMesh();
+        GImpactCollisionShape shape = new GImpactCollisionShape(mesh);
+        shape.setScale(new Vector3f(scale, scale, scale));
+
+        RigidBodyControl control = new RigidBodyControl(shape, mass);
+        n.addControl(control);
+        addObject(n);
+        return control;
+    }
+
+    private void attachTestObject(Mesh mesh, Vector3f position, float mass) {
+        Material material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        material.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg"));
+        Geometry g = new Geometry("mesh", mesh);
+        g.scale(scaleMod);
+        g.setLocalTranslation(position);
+        g.setMaterial(material);
+
+        GImpactCollisionShape shape = new GImpactCollisionShape(mesh);
+        shape.setScale(new Vector3f(scaleMod, scaleMod, scaleMod));
+        RigidBodyControl control = new RigidBodyControl(shape, mass);
+        g.addControl(control);
+        addObject(g);
+    }
+
+    private PhysicsSpace physicsSpace() {
+        return bulletAppState.getPhysicsSpace();
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        testTimer += tpf * bulletAppState.getSpeed();
+
+        if (restart) {
+            cleanup();
+            initializeNewTest();
+            restart = false;
+            testTimer = 0;
+        }
+        timeElapsedTxt.setText("Time Elapsed: " + String.format("%.3f", testTimer));
+    }
+
+    private void cleanup() {
+        stateManager.detach(bulletAppState);
+        stateManager.detach(stateManager.getState(BulletDebugAppState.class));
+        for (Spatial s : testObjects) {
+            rootNode.detachChild(s);
+        }
+    }
+}

+ 36 - 0
jme3-examples/src/main/java/jme3test/bullet/shape/package-info.java

@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 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.
+ */
+/**
+ * example apps and non-automated tests for specific Bullet-physics collision
+ * shapes
+ */
+package jme3test.bullet.shape;

+ 101 - 0
jme3-examples/src/main/java/jme3test/collision/RayTrace.java

@@ -0,0 +1,101 @@
+/*
+ * 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 jme3test.collision;
+
+import com.jme3.collision.CollisionResults;
+import com.jme3.math.Ray;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.Camera;
+import com.jme3.scene.Spatial;
+import java.awt.FlowLayout;
+import java.awt.image.BufferedImage;
+import javax.swing.ImageIcon;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+
+public class RayTrace {
+
+    final private BufferedImage image;
+    final private Camera cam;
+    final private Spatial scene;
+    final private CollisionResults results = new CollisionResults();
+    private JLabel label;
+
+    public RayTrace(Spatial scene, Camera cam, int width, int height){
+        image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+        this.scene = scene;
+        this.cam = cam;
+    }
+
+    public void show(){
+        JFrame frame = new JFrame("HDR View");
+        label = new JLabel(new ImageIcon(image));
+        frame.getContentPane().add(label);
+        frame.setLayout(new FlowLayout());
+        frame.pack();
+        frame.setVisible(true);
+    }
+
+    public void update(){
+        int w = image.getWidth();
+        int h = image.getHeight();
+
+        float wr = (float) cam.getWidth()  / image.getWidth();
+        float hr = (float) cam.getHeight() / image.getHeight();
+
+        scene.updateGeometricState();
+
+        for (int y = 0; y < h; y++){
+            for (int x = 0; x < w; x++){
+                Vector2f v = new Vector2f(x * wr,y * hr);
+                Vector3f pos = cam.getWorldCoordinates(v, 0.0f);
+                Vector3f dir = cam.getWorldCoordinates(v, 0.3f);
+                dir.subtractLocal(pos).normalizeLocal();
+
+                Ray r = new Ray(pos, dir);
+
+                results.clear();
+                scene.collideWith(r, results);
+                if (results.size() > 0){
+                    image.setRGB(x, h - y - 1, 0xFFFFFFFF);
+                }else{
+                    image.setRGB(x, h - y - 1, 0xFF000000);
+                }
+            }
+        }
+
+        label.repaint();
+    }
+
+}

+ 153 - 0
jme3-examples/src/main/java/jme3test/collision/TestMousePick.java

@@ -0,0 +1,153 @@
+/*
+ * 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 jme3test.collision;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.collision.CollisionResult;
+import com.jme3.collision.CollisionResults;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Ray;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.debug.Arrow;
+import com.jme3.scene.shape.Box;
+
+public class TestMousePick extends SimpleApplication {
+
+    public static void main(String[] args) {
+        TestMousePick app = new TestMousePick();
+        app.start();
+    }
+    
+    private Node shootables;
+    private Geometry mark;
+
+    @Override
+    public void simpleInitApp() {
+        flyCam.setEnabled(false);
+        initMark();
+
+        /* Create four colored boxes and a floor to shoot at: */
+        shootables = new Node("Shootables");
+        rootNode.attachChild(shootables);
+        shootables.attachChild(makeCube("a Dragon", -2f, 0f, 1f));
+        shootables.attachChild(makeCube("a tin can", 1f, -2f, 0f));
+        shootables.attachChild(makeCube("the Sheriff", 0f, 1f, -2f));
+        shootables.attachChild(makeCube("the Deputy", 1f, 0f, -4f));
+        shootables.attachChild(makeFloor());
+        shootables.attachChild(makeCharacter());
+    }
+
+    @Override
+    public void simpleUpdate(float tpf){
+        Vector3f origin    = cam.getWorldCoordinates(inputManager.getCursorPosition(), 0.0f);
+        Vector3f direction = cam.getWorldCoordinates(inputManager.getCursorPosition(), 0.3f);
+        direction.subtractLocal(origin).normalizeLocal();
+
+        Ray ray = new Ray(origin, direction);
+        CollisionResults results = new CollisionResults();
+        shootables.collideWith(ray, results);
+//        System.out.println("----- Collisions? " + results.size() + "-----");
+//        for (int i = 0; i < results.size(); i++) {
+//            // For each hit, we know distance, impact point, name of geometry.
+//            float dist = results.getCollision(i).getDistance();
+//            Vector3f pt = results.getCollision(i).getWorldContactPoint();
+//            String hit = results.getCollision(i).getGeometry().getName();
+//            System.out.println("* Collision #" + i);
+//            System.out.println("  You shot " + hit + " at " + pt + ", " + dist + " wu away.");
+//        }
+        if (results.size() > 0) {
+            CollisionResult closest = results.getClosestCollision();
+            mark.setLocalTranslation(closest.getContactPoint());
+
+            Quaternion q = new Quaternion();
+            q.lookAt(closest.getContactNormal(), Vector3f.UNIT_Y);
+            mark.setLocalRotation(q);
+
+            rootNode.attachChild(mark);
+        } else {
+            rootNode.detachChild(mark);
+        }
+    }
+ 
+    /** A cube object for target practice */
+    private Geometry makeCube(String name, float x, float y, float z) {
+        Box box = new Box(1, 1, 1);
+        Geometry cube = new Geometry(name, box);
+        cube.setLocalTranslation(x, y, z);
+        Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat1.setColor("Color", ColorRGBA.randomColor());
+        cube.setMaterial(mat1);
+        return cube;
+    }
+
+    /** A floor to show that the "shot" can go through several objects. */
+    private Geometry makeFloor() {
+        Box box = new Box(15, .2f, 15);
+        Geometry floor = new Geometry("the Floor", box);
+        floor.setLocalTranslation(0, -4, -5);
+        Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat1.setColor("Color", ColorRGBA.Gray);
+        floor.setMaterial(mat1);
+        return floor;
+    }
+
+    /**
+     * A red arrow to mark the spot being picked.
+     */
+    private void initMark() {
+        Arrow arrow = new Arrow(Vector3f.UNIT_Z.mult(2f));
+        mark = new Geometry("BOOM!", arrow);
+        Material mark_mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mark_mat.setColor("Color", ColorRGBA.Red);
+        mark.setMaterial(mark_mat);
+    }
+
+    private Spatial makeCharacter() {
+        // load a character from jme3-testdata
+        Spatial golem = assetManager.loadModel("Models/Oto/Oto.mesh.xml");
+        golem.scale(0.5f);
+        golem.setLocalTranslation(-1.0f, -1.5f, -0.6f);
+
+        // We must add a light to make the model visible
+        DirectionalLight sun = new DirectionalLight();
+        sun.setDirection(new Vector3f(-0.1f, -0.7f, -1.0f).normalizeLocal());
+        golem.addLight(sun);
+        return golem;
+    }
+}

+ 95 - 0
jme3-examples/src/main/java/jme3test/collision/TestRayCasting.java

@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2009-2020 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.collision;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bounding.BoundingSphere;
+import com.jme3.material.Material;
+import com.jme3.math.FastMath;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.VertexBuffer.Type;
+
+public class TestRayCasting extends SimpleApplication {
+
+    private RayTrace tracer;
+    private Spatial teapot;
+
+    public static void main(String[] args){
+        TestRayCasting app = new TestRayCasting();
+        app.setPauseOnLostFocus(false);
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+//        flyCam.setEnabled(false);
+
+        // load material
+        Material mat = assetManager.loadMaterial("Interface/Logo/Logo.j3m");
+
+        Mesh q = new Mesh();
+        q.setBuffer(Type.Position, 3, new float[]
+        {
+            1, 0, 0,
+            0, 1.5f, 0,
+            -1, 0, 0
+        }
+        );
+        q.setBuffer(Type.Index, 3, new int[]{ 0, 1, 2 });
+        q.setBound(new BoundingSphere());
+        q.updateBound();
+//        Geometry teapot = new Geometry("MyGeom", q);
+
+        teapot = assetManager.loadModel("Models/Teapot/Teapot.mesh.xml");
+//        teapot.scale(2f, 2f, 2f);
+//        teapot.move(2f, 2f, -.5f);
+        teapot.rotate(FastMath.HALF_PI, FastMath.HALF_PI, FastMath.HALF_PI);
+        teapot.setMaterial(mat);
+        rootNode.attachChild(teapot);
+
+//        cam.setLocation(cam.getLocation().add(0,1,0));
+//        cam.lookAt(teapot.getWorldBound().getCenter(), Vector3f.UNIT_Y);
+
+        tracer = new RayTrace(rootNode, cam, 160, 128);
+        tracer.show();
+        tracer.update();
+    }
+
+    @Override
+    public void simpleUpdate(float tpf){
+        teapot.rotate(0,tpf,0);
+        tracer.update();
+    }
+
+}

+ 127 - 0
jme3-examples/src/main/java/jme3test/collision/TestTriangleCollision.java

@@ -0,0 +1,127 @@
+/*
+ * 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 jme3test.collision;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bounding.BoundingVolume;
+import com.jme3.collision.CollisionResults;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.AnalogListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.shape.Box;
+
+public class TestTriangleCollision extends SimpleApplication {
+
+    private Geometry geom1;
+
+    private Spatial golem;
+
+    public static void main(String[] args) {
+        TestTriangleCollision app = new TestTriangleCollision();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        // Create two boxes
+        Mesh mesh1 = new Box(0.5f, 0.5f, 0.5f);
+        geom1 = new Geometry("Box", mesh1);
+        geom1.move(2, 2, -.5f);
+        Material m1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        m1.setColor("Color", ColorRGBA.Blue);
+        geom1.setMaterial(m1);
+        rootNode.attachChild(geom1);
+
+        // load a character from jme3-testdata
+        golem = assetManager.loadModel("Models/Oto/Oto.mesh.xml");
+        golem.scale(0.5f);
+        golem.setLocalTranslation(-1.0f, -1.5f, -0.6f);
+
+        // We must add a light to make the model visible
+        DirectionalLight sun = new DirectionalLight();
+        sun.setDirection(new Vector3f(-0.1f, -0.7f, -1.0f).normalizeLocal());
+        golem.addLight(sun);
+        rootNode.attachChild(golem);
+
+        // Create input
+        inputManager.addMapping("MoveRight", new KeyTrigger(KeyInput.KEY_L));
+        inputManager.addMapping("MoveLeft", new KeyTrigger(KeyInput.KEY_J));
+        inputManager.addMapping("MoveUp", new KeyTrigger(KeyInput.KEY_I));
+        inputManager.addMapping("MoveDown", new KeyTrigger(KeyInput.KEY_K));
+
+        inputManager.addListener(analogListener, new String[]{
+                    "MoveRight", "MoveLeft", "MoveUp", "MoveDown"
+                });
+    }
+    final private AnalogListener analogListener = new AnalogListener() {
+
+        @Override
+        public void onAnalog(String name, float value, float tpf) {
+            if (name.equals("MoveRight")) {
+                geom1.move(2 * tpf, 0, 0);
+            }
+
+            if (name.equals("MoveLeft")) {
+                geom1.move(-2 * tpf, 0, 0);
+            }
+
+            if (name.equals("MoveUp")) {
+                geom1.move(0, 2 * tpf, 0);
+            }
+
+            if (name.equals("MoveDown")) {
+                geom1.move(0, -2 * tpf, 0);
+            }
+        }
+    };
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        CollisionResults results = new CollisionResults();
+        BoundingVolume bv = geom1.getWorldBound();
+        golem.collideWith(bv, results);
+
+        if (results.size() > 0) {
+            geom1.getMaterial().setColor("Color", ColorRGBA.Red);
+        }else{
+            geom1.getMaterial().setColor("Color", ColorRGBA.Blue);
+        }
+    }
+}

+ 36 - 0
jme3-examples/src/main/java/jme3test/collision/package-info.java

@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 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.
+ */
+/**
+ * example apps and non-automated tests for picking and non-physics collision
+ * detection
+ */
+package jme3test.collision;

+ 93 - 0
jme3-examples/src/main/java/jme3test/conversion/TestMipMapGen.java

@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2009-2018 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.conversion;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.font.BitmapText;
+import com.jme3.material.Material;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Quad;
+import com.jme3.texture.Image;
+import com.jme3.texture.Texture;
+import com.jme3.util.MipMapGenerator;
+
+public class TestMipMapGen extends SimpleApplication {
+
+    public static void main(String[] args){
+        TestMipMapGen app = new TestMipMapGen();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        BitmapText txt = guiFont.createLabel("Left: HW Mips");
+        txt.setLocalTranslation(0, settings.getHeight() - txt.getLineHeight() * 4, 0);
+        guiNode.attachChild(txt);
+
+        txt = guiFont.createLabel("Right: AWT Mips");
+        txt.setLocalTranslation(0, settings.getHeight() - txt.getLineHeight() * 3, 0);
+        guiNode.attachChild(txt);
+
+        // create a simple plane/quad
+        Quad quadMesh = new Quad(1, 1);
+        quadMesh.updateGeometry(1, 1, false);
+        quadMesh.updateBound();
+
+        Geometry quad1 = new Geometry("Textured Quad", quadMesh);
+        Geometry quad2 = new Geometry("Textured Quad 2", quadMesh);
+
+        Texture tex = assetManager.loadTexture("Interface/Logo/Monkey.png");
+        tex.setMinFilter(Texture.MinFilter.Trilinear);
+
+        Texture texCustomMip = tex.clone();
+        Image imageCustomMip = texCustomMip.getImage().clone();
+        MipMapGenerator.generateMipMaps(imageCustomMip);
+        texCustomMip.setImage(imageCustomMip);
+
+        Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat1.setTexture("ColorMap", tex);
+
+        Material mat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat2.setTexture("ColorMap", texCustomMip);
+
+        quad1.setMaterial(mat1);
+//        quad1.setLocalTranslation(1, 0, 0);
+
+        quad2.setMaterial(mat2);
+        quad2.setLocalTranslation(1, 0, 0);
+
+        rootNode.attachChild(quad1);
+        rootNode.attachChild(quad2);
+    }
+
+}

+ 35 - 0
jme3-examples/src/main/java/jme3test/conversion/package-info.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 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.
+ */
+/**
+ * example apps and non-automated tests for converting textures
+ */
+package jme3test.conversion;

+ 201 - 0
jme3-examples/src/main/java/jme3test/effect/TestEverything.java

@@ -0,0 +1,201 @@
+/*
+ * 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 jme3test.effect;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.math.*;
+import com.jme3.post.FilterPostProcessor;
+import com.jme3.post.filters.ToneMapFilter;
+import com.jme3.renderer.Caps;
+import com.jme3.renderer.queue.RenderQueue.ShadowMode;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.Spatial.CullHint;
+import com.jme3.scene.shape.Box;
+import com.jme3.shadow.DirectionalLightShadowRenderer;
+import com.jme3.texture.Texture;
+import com.jme3.util.SkyFactory;
+import com.jme3.util.TangentBinormalGenerator;
+
+public class TestEverything extends SimpleApplication {
+
+    private DirectionalLightShadowRenderer dlsr;
+    private ToneMapFilter toneMapFilter;
+    final private Vector3f lightDir = new Vector3f(-1, -1, .5f).normalizeLocal();
+
+    public static void main(String[] args){
+        TestEverything app = new TestEverything();
+        app.start();
+    }
+
+    public void setupHdr(){
+        if (renderer.getCaps().contains(Caps.GLSL100)){
+            toneMapFilter = new ToneMapFilter();
+            toneMapFilter.setWhitePoint(new Vector3f(3f, 3f, 3f));
+            FilterPostProcessor fpp = new FilterPostProcessor(assetManager);
+            fpp.addFilter(toneMapFilter);
+            viewPort.addProcessor(fpp);
+
+    //        setPauseOnLostFocus(false);
+        }
+    }
+
+    public void setupBasicShadow(){
+        if (renderer.getCaps().contains(Caps.GLSL100)){
+            dlsr = new DirectionalLightShadowRenderer(assetManager, 1024, 1);
+            viewPort.addProcessor(dlsr);
+        }
+    }
+
+    public void setupSkyBox(){
+        Texture envMap;
+        if (renderer.getCaps().contains(Caps.FloatTexture)){
+            envMap = assetManager.loadTexture("Textures/Sky/St Peters/StPeters.hdr");
+        }else{
+            envMap = assetManager.loadTexture("Textures/Sky/St Peters/StPeters.jpg");
+        }
+        rootNode.attachChild(SkyFactory.createSky(assetManager, envMap, 
+                new Vector3f(-1,-1,-1), SkyFactory.EnvMapType.SphereMap));
+    }
+
+    public void setupLighting(){
+        boolean hdr = false;
+        if (toneMapFilter != null){
+            hdr = toneMapFilter.isEnabled();
+        }
+
+        DirectionalLight dl = new DirectionalLight();
+        if (dlsr != null) {
+            dlsr.setLight(dl);
+        }
+        dl.setDirection(lightDir);
+        if (hdr){
+            dl.setColor(new ColorRGBA(3, 3, 3, 1));
+        }else{
+            dl.setColor(new ColorRGBA(.9f, .9f, .9f, 1));
+        }
+        rootNode.addLight(dl);
+
+        dl = new DirectionalLight();
+        dl.setDirection(new Vector3f(1, 0, -1).normalizeLocal());
+        if (hdr){
+            dl.setColor(new ColorRGBA(1, 1, 1, 1));
+        }else{
+            dl.setColor(new ColorRGBA(.4f, .4f, .4f, 1));
+        }
+        rootNode.addLight(dl);
+    }
+
+    public void setupFloor(){
+        Material mat = assetManager.loadMaterial("Textures/Terrain/BrickWall/BrickWall.j3m");
+        Box floor = new Box(50, 1f, 50);
+        TangentBinormalGenerator.generate(floor);
+        floor.scaleTextureCoordinates(new Vector2f(5, 5));
+        Geometry floorGeom = new Geometry("Floor", floor);
+        floorGeom.setMaterial(mat);
+        floorGeom.setShadowMode(ShadowMode.Receive);
+        rootNode.attachChild(floorGeom);
+    }
+
+//    public void setupTerrain(){
+//        Material mat = manager.loadMaterial("Textures/Terrain/Rock/Rock.j3m");
+//        mat.getTextureParam("DiffuseMap").getValue().setWrap(WrapMode.Repeat);
+//        mat.getTextureParam("NormalMap").getValue().setWrap(WrapMode.Repeat);
+//        try{
+//            Geomap map = GeomapLoader.fromImage(TestEverything.class.getResource("/textures/heightmap.png"));
+//            Mesh m = map.createMesh(new Vector3f(0.35f, 0.0005f, 0.35f), new Vector2f(10, 10), true);
+//            Logger.getLogger(TangentBinormalGenerator.class.getName()).setLevel(Level.SEVERE);
+//            TangentBinormalGenerator.generate(m);
+//            Geometry t = new Geometry("Terrain", m);
+//            t.setLocalTranslation(85, -15, 0);
+//            t.setMaterial(mat);
+//            t.updateModelBound();
+//            t.setShadowMode(ShadowMode.Receive);
+//            rootNode.attachChild(t);
+//        }catch (IOException ex){
+//            ex.printStackTrace();
+//        }
+//
+//    }
+
+    public void setupRobotGuy(){
+        Node model = (Node) assetManager.loadModel("Models/Oto/Oto.mesh.xml");
+        Material mat = assetManager.loadMaterial("Models/Oto/Oto.j3m");
+        model.getChild(0).setMaterial(mat);
+//        model.setAnimation("Walk");
+        model.setLocalTranslation(30, 10.5f, 30);
+        model.setLocalScale(2);
+        model.setShadowMode(ShadowMode.CastAndReceive);
+        rootNode.attachChild(model);
+    }
+
+    public void setupSignpost(){
+        Spatial signpost = assetManager.loadModel("Models/Sign Post/Sign Post.mesh.xml");
+        Material mat = assetManager.loadMaterial("Models/Sign Post/Sign Post.j3m");
+        signpost.setMaterial(mat);
+        signpost.rotate(0, FastMath.HALF_PI, 0);
+        signpost.setLocalTranslation(12, 3.5f, 30);
+        signpost.setLocalScale(4);
+        signpost.setShadowMode(ShadowMode.CastAndReceive);
+        rootNode.attachChild(signpost);
+    }
+
+    @Override
+    public void simpleInitApp() {
+        cam.setLocation(new Vector3f(-32.295086f, 54.80136f, 79.59805f));
+        cam.setRotation(new Quaternion(0.074364014f, 0.92519957f, -0.24794696f, 0.27748522f));
+        cam.update();
+
+        cam.setFrustumFar(300);
+        flyCam.setMoveSpeed(30);
+
+        rootNode.setCullHint(CullHint.Never);
+
+        setupBasicShadow();
+        setupHdr();
+
+        setupLighting();
+        setupSkyBox();
+
+//        setupTerrain();
+        setupFloor();
+//        setupRobotGuy();
+        setupSignpost();
+
+        
+    }
+
+}

+ 282 - 0
jme3-examples/src/main/java/jme3test/effect/TestExplosionEffect.java

@@ -0,0 +1,282 @@
+/*
+ * 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 jme3test.effect;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.effect.ParticleEmitter;
+import com.jme3.effect.ParticleMesh.Type;
+import com.jme3.effect.shapes.EmitterSphereShape;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Node;
+
+public class TestExplosionEffect extends SimpleApplication {
+
+    private float time = 0;
+    private int state = 0;
+    final private Node explosionEffect = new Node("explosionFX");
+    private ParticleEmitter flame, flash, spark, roundspark, smoketrail, debris,
+                            shockwave;
+
+
+    private static final int COUNT_FACTOR = 1;
+    private static final float COUNT_FACTOR_F = 1f;
+
+    private static final boolean POINT_SPRITE = true;
+    private static final Type EMITTER_TYPE = POINT_SPRITE ? Type.Point : Type.Triangle;
+
+    public static void main(String[] args){
+        TestExplosionEffect app = new TestExplosionEffect();
+        app.start();
+    }
+
+    private void createFlame(){
+        flame = new ParticleEmitter("Flame", EMITTER_TYPE, 32 * COUNT_FACTOR);
+        flame.setSelectRandomImage(true);
+        flame.setStartColor(new ColorRGBA(1f, 0.4f, 0.05f, (1f / COUNT_FACTOR_F)));
+        flame.setEndColor(new ColorRGBA(.4f, .22f, .12f, 0f));
+        flame.setStartSize(1.3f);
+        flame.setEndSize(2f);
+        flame.setShape(new EmitterSphereShape(Vector3f.ZERO, 1f));
+        flame.setParticlesPerSec(0);
+        flame.setGravity(0, -5, 0);
+        flame.setLowLife(.4f);
+        flame.setHighLife(.5f);
+        flame.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 7, 0));
+        flame.getParticleInfluencer().setVelocityVariation(1f);
+        flame.setImagesX(2);
+        flame.setImagesY(2);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
+        mat.setTexture("Texture", assetManager.loadTexture("Effects/Explosion/flame.png"));
+        mat.setBoolean("PointSprite", POINT_SPRITE);
+        flame.setMaterial(mat);
+        explosionEffect.attachChild(flame);
+    }
+
+    private void createFlash(){
+        flash = new ParticleEmitter("Flash", EMITTER_TYPE, 24 * COUNT_FACTOR);
+        flash.setSelectRandomImage(true);
+        flash.setStartColor(new ColorRGBA(1f, 0.8f, 0.36f, 1f / COUNT_FACTOR_F));
+        flash.setEndColor(new ColorRGBA(1f, 0.8f, 0.36f, 0f));
+        flash.setStartSize(.1f);
+        flash.setEndSize(3.0f);
+        flash.setShape(new EmitterSphereShape(Vector3f.ZERO, .05f));
+        flash.setParticlesPerSec(0);
+        flash.setGravity(0, 0, 0);
+        flash.setLowLife(.2f);
+        flash.setHighLife(.2f);
+        flash.getParticleInfluencer()
+                .setInitialVelocity(new Vector3f(0, 5f, 0));
+        flash.getParticleInfluencer().setVelocityVariation(1);
+        flash.setImagesX(2);
+        flash.setImagesY(2);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
+        mat.setTexture("Texture", assetManager.loadTexture("Effects/Explosion/flash.png"));
+        mat.setBoolean("PointSprite", POINT_SPRITE);
+        flash.setMaterial(mat);
+        explosionEffect.attachChild(flash);
+    }
+
+    private void createRoundSpark(){
+        roundspark = new ParticleEmitter("RoundSpark", EMITTER_TYPE, 20 * COUNT_FACTOR);
+        roundspark.setStartColor(new ColorRGBA(1f, 0.29f, 0.34f, (float) (1.0 / COUNT_FACTOR_F)));
+        roundspark.setEndColor(new ColorRGBA(0, 0, 0, 0.5f / COUNT_FACTOR_F));
+        roundspark.setStartSize(1.2f);
+        roundspark.setEndSize(1.8f);
+        roundspark.setShape(new EmitterSphereShape(Vector3f.ZERO, 2f));
+        roundspark.setParticlesPerSec(0);
+        roundspark.setGravity(0, -.5f, 0);
+        roundspark.setLowLife(1.8f);
+        roundspark.setHighLife(2f);
+        roundspark.getParticleInfluencer()
+                .setInitialVelocity(new Vector3f(0, 3, 0));
+        roundspark.getParticleInfluencer().setVelocityVariation(.5f);
+        roundspark.setImagesX(1);
+        roundspark.setImagesY(1);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
+        mat.setTexture("Texture", assetManager.loadTexture("Effects/Explosion/roundspark.png"));
+        mat.setBoolean("PointSprite", POINT_SPRITE);
+        roundspark.setMaterial(mat);
+        explosionEffect.attachChild(roundspark);
+    }
+
+    private void createSpark(){
+        spark = new ParticleEmitter("Spark", Type.Triangle, 30 * COUNT_FACTOR);
+        spark.setStartColor(new ColorRGBA(1f, 0.8f, 0.36f, 1.0f / COUNT_FACTOR_F));
+        spark.setEndColor(new ColorRGBA(1f, 0.8f, 0.36f, 0f));
+        spark.setStartSize(.5f);
+        spark.setEndSize(.5f);
+        spark.setFacingVelocity(true);
+        spark.setParticlesPerSec(0);
+        spark.setGravity(0, 5, 0);
+        spark.setLowLife(1.1f);
+        spark.setHighLife(1.5f);
+        spark.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 20, 0));
+        spark.getParticleInfluencer().setVelocityVariation(1);
+        spark.setImagesX(1);
+        spark.setImagesY(1);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
+        mat.setTexture("Texture", assetManager.loadTexture("Effects/Explosion/spark.png"));
+        spark.setMaterial(mat);
+        explosionEffect.attachChild(spark);
+    }
+
+    private void createSmokeTrail(){
+        smoketrail = new ParticleEmitter("SmokeTrail", Type.Triangle, 22 * COUNT_FACTOR);
+        smoketrail.setStartColor(new ColorRGBA(1f, 0.8f, 0.36f, 1.0f / COUNT_FACTOR_F));
+        smoketrail.setEndColor(new ColorRGBA(1f, 0.8f, 0.36f, 0f));
+        smoketrail.setStartSize(.2f);
+        smoketrail.setEndSize(1f);
+
+//        smoketrail.setShape(new EmitterSphereShape(Vector3f.ZERO, 1f));
+        smoketrail.setFacingVelocity(true);
+        smoketrail.setParticlesPerSec(0);
+        smoketrail.setGravity(0, 1, 0);
+        smoketrail.setLowLife(.4f);
+        smoketrail.setHighLife(.5f);
+        smoketrail.getParticleInfluencer()
+                .setInitialVelocity(new Vector3f(0, 12, 0));
+        smoketrail.getParticleInfluencer().setVelocityVariation(1);
+        smoketrail.setImagesX(1);
+        smoketrail.setImagesY(3);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
+        mat.setTexture("Texture", assetManager.loadTexture("Effects/Explosion/smoketrail.png"));
+        smoketrail.setMaterial(mat);
+        explosionEffect.attachChild(smoketrail);
+    }
+
+    private void createDebris(){
+        debris = new ParticleEmitter("Debris", Type.Triangle, 15 * COUNT_FACTOR);
+        debris.setSelectRandomImage(true);
+        debris.setRandomAngle(true);
+        debris.setRotateSpeed(FastMath.TWO_PI * 4);
+        debris.setStartColor(new ColorRGBA(1f, 0.59f, 0.28f, 1.0f / COUNT_FACTOR_F));
+        debris.setEndColor(new ColorRGBA(.5f, 0.5f, 0.5f, 0f));
+        debris.setStartSize(.2f);
+        debris.setEndSize(.2f);
+
+//        debris.setShape(new EmitterSphereShape(Vector3f.ZERO, .05f));
+        debris.setParticlesPerSec(0);
+        debris.setGravity(0, 12f, 0);
+        debris.setLowLife(1.4f);
+        debris.setHighLife(1.5f);
+        debris.getParticleInfluencer()
+                .setInitialVelocity(new Vector3f(0, 15, 0));
+        debris.getParticleInfluencer().setVelocityVariation(.60f);
+        debris.setImagesX(3);
+        debris.setImagesY(3);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
+        mat.setTexture("Texture", assetManager.loadTexture("Effects/Explosion/Debris.png"));
+        debris.setMaterial(mat);
+        explosionEffect.attachChild(debris);
+    }
+
+    private void createShockwave(){
+        shockwave = new ParticleEmitter("Shockwave", Type.Triangle, 1 * COUNT_FACTOR);
+//        shockwave.setRandomAngle(true);
+        shockwave.setFaceNormal(Vector3f.UNIT_Y);
+        shockwave.setStartColor(new ColorRGBA(.48f, 0.17f, 0.01f, .8f / COUNT_FACTOR_F));
+        shockwave.setEndColor(new ColorRGBA(.48f, 0.17f, 0.01f, 0f));
+
+        shockwave.setStartSize(0f);
+        shockwave.setEndSize(7f);
+
+        shockwave.setParticlesPerSec(0);
+        shockwave.setGravity(0, 0, 0);
+        shockwave.setLowLife(0.5f);
+        shockwave.setHighLife(0.5f);
+        shockwave.getParticleInfluencer()
+                .setInitialVelocity(new Vector3f(0, 0, 0));
+        shockwave.getParticleInfluencer().setVelocityVariation(0f);
+        shockwave.setImagesX(1);
+        shockwave.setImagesY(1);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
+        mat.setTexture("Texture", assetManager.loadTexture("Effects/Explosion/shockwave.png"));
+        shockwave.setMaterial(mat);
+        explosionEffect.attachChild(shockwave);
+    }
+
+    @Override
+    public void simpleInitApp() {
+        createFlame();
+        createFlash();
+        createSpark();
+        createRoundSpark();
+        createSmokeTrail();
+        createDebris();
+        createShockwave();
+        explosionEffect.setLocalScale(0.5f);
+        renderManager.preloadScene(explosionEffect);
+
+        cam.setLocation(new Vector3f(0, 3.5135868f, 10));
+        cam.setRotation(new Quaternion(1.5714673E-4f, 0.98696727f, -0.16091813f, 9.6381607E-4f));
+
+        rootNode.attachChild(explosionEffect);
+    }
+
+    @Override
+    public void simpleUpdate(float tpf){  
+        time += tpf / speed;
+        if (time > 1f && state == 0){
+            flash.emitAllParticles();
+            spark.emitAllParticles();
+            smoketrail.emitAllParticles();
+            debris.emitAllParticles();
+            shockwave.emitAllParticles();
+            state++;
+        }
+        if (time > 1f + .05f / speed && state == 1){
+            flame.emitAllParticles();
+            roundspark.emitAllParticles();
+            state++;
+        }
+        
+        // rewind the effect
+        if (time > 5 / speed && state == 2){
+            state = 0;
+            time = 0;
+
+            flash.killAllParticles();
+            spark.killAllParticles();
+            smoketrail.killAllParticles();
+            debris.killAllParticles();
+            flame.killAllParticles();
+            roundspark.killAllParticles();
+            shockwave.killAllParticles();
+        }
+    }
+
+}

+ 303 - 0
jme3-examples/src/main/java/jme3test/effect/TestIssue1773.java

@@ -0,0 +1,303 @@
+/*
+ * 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.effect;
+
+import com.jme3.animation.LoopMode;
+import com.jme3.app.SimpleApplication;
+import com.jme3.cinematic.MotionPath;
+import com.jme3.cinematic.events.MotionEvent;
+import com.jme3.effect.ParticleEmitter;
+import com.jme3.effect.ParticleMesh.Type;
+import com.jme3.effect.shapes.EmitterMeshVertexShape;
+import com.jme3.font.BitmapFont;
+import com.jme3.font.BitmapText;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.input.controls.Trigger;
+import com.jme3.light.AmbientLight;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.material.Materials;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.post.FilterPostProcessor;
+import com.jme3.post.filters.BloomFilter;
+import com.jme3.post.filters.FXAAFilter;
+import com.jme3.renderer.queue.RenderQueue;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.shape.CenterQuad;
+import com.jme3.scene.shape.Torus;
+import com.jme3.shadow.DirectionalLightShadowFilter;
+import com.jme3.system.AppSettings;
+import com.jme3.texture.Texture;
+import java.util.Arrays;
+
+/**
+ * Test case for Issue 1773 (Wrong particle position when using
+ * 'EmitterMeshVertexShape' or 'EmitterMeshFaceShape' and worldSpace
+ * flag equal to true)
+ *
+ * If the test succeeds, the particles will be generated from the vertices
+ * (for EmitterMeshVertexShape) or from the faces (for EmitterMeshFaceShape)
+ * of the torus mesh. If the test fails, the particles will appear in the
+ * center of the torus when worldSpace flag is set to true.
+ *
+ * @author capdevon
+ */
+public class TestIssue1773 extends SimpleApplication implements ActionListener {
+
+    public static void main(String[] args) {
+        TestIssue1773 app = new TestIssue1773();
+        AppSettings settings = new AppSettings(true);
+        settings.setResolution(1280, 720);
+        settings.setRenderer(AppSettings.LWJGL_OPENGL32);
+        app.setSettings(settings);
+        app.setPauseOnLostFocus(false);
+        app.setShowSettings(false);
+        app.start();
+    }
+
+    private ParticleEmitter emit;
+    private Node myModel;
+    private BitmapText emitUI;
+    private MotionEvent motionControl;
+    private boolean playing;
+
+    @Override
+    public void simpleInitApp() {
+
+        BitmapText hud = createTextUI(ColorRGBA.White, 20, 15);
+        hud.setText("Play/Pause Motion: KEY_SPACE, InWorldSpace: KEY_I");
+
+        emitUI = createTextUI(ColorRGBA.Blue, 20, 15 * 2);
+
+        configCamera();
+        setupLights();
+        setupGround();
+        setupCircle();
+        createMotionControl();
+        setupKeys();
+    }
+
+    /**
+     * Crates particle emitter and adds it to root node.
+     */
+    private void setupCircle() {
+        myModel = new Node("FieryCircle");
+
+        Geometry torus = createTorus(1f);
+        myModel.attachChild(torus);
+
+        emit = createParticleEmitter(torus, true);
+        myModel.attachChild(emit);
+
+        rootNode.attachChild(myModel);
+    }
+
+    /**
+     * Creates torus geometry used for the emitter shape.
+     */
+    private Geometry createTorus(float radius) {
+        float s = radius / 8f;
+        Geometry geo = new Geometry("CircleXZ", new Torus(64, 4, s, radius));
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat.setColor("Color", ColorRGBA.Blue);
+        mat.getAdditionalRenderState().setWireframe(true);
+        geo.setMaterial(mat);
+        return geo;
+    }
+
+    /**
+     * Creates a particle emitter that will emit the particles from
+     * the given shape's vertices.
+     */
+    private ParticleEmitter createParticleEmitter(Geometry geo, boolean pointSprite) {
+        Type type = pointSprite ? Type.Point : Type.Triangle;
+        ParticleEmitter emitter = new ParticleEmitter("Emitter", type, 1000);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
+        mat.setTexture("Texture", assetManager.loadTexture("Effects/Smoke/Smoke.png"));
+        mat.setBoolean("PointSprite", pointSprite);
+        emitter.setMaterial(mat);
+        emitter.setLowLife(1);
+        emitter.setHighLife(1);
+        emitter.setImagesX(15);
+        emitter.setStartSize(0.04f);
+        emitter.setEndSize(0.02f);
+        emitter.setStartColor(ColorRGBA.Orange);
+        emitter.setEndColor(ColorRGBA.Red);
+        emitter.setParticlesPerSec(900);
+        emitter.setGravity(0, 0f, 0);
+        //emitter.getParticleInfluencer().setVelocityVariation(1);
+        //emitter.getParticleInfluencer().setInitialVelocity(new Vector3f(0, .5f, 0));
+        emitter.setShape(new EmitterMeshVertexShape(Arrays.asList(geo.getMesh())));
+        //emitter.setShape(new EmitterMeshFaceShape(Arrays.asList(geo.getMesh())));
+        return emitter;
+    }
+
+    /**
+     * Creates a motion control that will move particle emitter in
+     * a circular path.
+     */
+    private void createMotionControl() {
+
+        float radius = 5f;
+        float height = 1.10f;
+
+        MotionPath path = new MotionPath();
+        path.setCycle(true);
+
+        for (int i = 0; i < 8; i++) {
+            float x = FastMath.sin(FastMath.QUARTER_PI * i) * radius;
+            float z = FastMath.cos(FastMath.QUARTER_PI * i) * radius;
+            path.addWayPoint(new Vector3f(x, height, z));
+        }
+        //path.enableDebugShape(assetManager, rootNode);
+
+        motionControl = new MotionEvent(myModel, path);
+        motionControl.setLoopMode(LoopMode.Loop);
+        //motionControl.setInitialDuration(15f);
+        //motionControl.setSpeed(2f);
+        motionControl.setDirectionType(MotionEvent.Direction.Path);
+    }
+
+    /**
+     * Use keyboard space key to toggle emitter motion and I key to
+     * toggle inWorldSpace flag. By default, inWorldSpace flag is on
+     * and emitter motion is off.
+     */
+    private void setupKeys() {
+        addMapping("ToggleMotionEvent", new KeyTrigger(KeyInput.KEY_SPACE));
+        addMapping("InWorldSpace", new KeyTrigger(KeyInput.KEY_I));
+    }
+
+    private void addMapping(String mappingName, Trigger... triggers) {
+        inputManager.addMapping(mappingName, triggers);
+        inputManager.addListener(this, mappingName);
+    }
+
+    @Override
+    public void onAction(String name, boolean isPressed, float tpf) {
+        if (name.equals("InWorldSpace") && isPressed) {
+            boolean worldSpace = emit.isInWorldSpace();
+            emit.setInWorldSpace(!worldSpace);
+
+        } else if (name.equals("ToggleMotionEvent") && isPressed) {
+            if (playing) {
+                playing = false;
+                motionControl.pause();
+            } else {
+                playing = true;
+                motionControl.play();
+            }
+        }
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        emitUI.setText("InWorldSpace: " + emit.isInWorldSpace());
+    }
+
+    private void configCamera() {
+        flyCam.setDragToRotate(true);
+        flyCam.setMoveSpeed(10);
+
+        cam.setLocation(new Vector3f(0, 6f, 9.2f));
+        cam.lookAt(Vector3f.UNIT_Y, Vector3f.UNIT_Y);
+
+        float aspect = (float) cam.getWidth() / cam.getHeight();
+        cam.setFrustumPerspective(45, aspect, 0.1f, 1000f);
+    }
+
+    /**
+     * Adds a ground to the scene
+     */
+    private void setupGround() {
+        CenterQuad quad = new CenterQuad(12, 12);
+        quad.scaleTextureCoordinates(new Vector2f(2, 2));
+        Geometry floor = new Geometry("Floor", quad);
+        Material mat = new Material(assetManager, Materials.LIGHTING);
+        Texture tex = assetManager.loadTexture("Interface/Logo/Monkey.jpg");
+        tex.setWrap(Texture.WrapMode.Repeat);
+        mat.setTexture("DiffuseMap", tex);
+        floor.setMaterial(mat);
+        floor.rotate(-FastMath.HALF_PI, 0, 0);
+        rootNode.attachChild(floor);
+    }
+
+    /**
+     * Adds lights and filters
+     */
+    private void setupLights() {
+        viewPort.setBackgroundColor(ColorRGBA.DarkGray);
+        rootNode.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
+
+        AmbientLight ambient = new AmbientLight();
+        ambient.setColor(ColorRGBA.White);
+        //rootNode.addLight(ambient);
+
+        DirectionalLight sun = new DirectionalLight();
+        sun.setDirection((new Vector3f(-0.5f, -0.5f, -0.5f)).normalizeLocal());
+        sun.setColor(ColorRGBA.White);
+        rootNode.addLight(sun);
+
+        DirectionalLightShadowFilter dlsf = new DirectionalLightShadowFilter(assetManager, 4096, 3);
+        dlsf.setLight(sun);
+        dlsf.setShadowIntensity(0.4f);
+        dlsf.setShadowZExtend(256);
+
+        FXAAFilter fxaa = new FXAAFilter();
+        BloomFilter bloom = new BloomFilter(BloomFilter.GlowMode.Objects);
+
+        FilterPostProcessor fpp = new FilterPostProcessor(assetManager);
+        fpp.addFilter(bloom);
+        fpp.addFilter(dlsf);
+        fpp.addFilter(fxaa);
+        viewPort.addProcessor(fpp);
+    }
+
+    /**
+     * Creates a bitmap test used for displaying debug info.
+     */
+    private BitmapText createTextUI(ColorRGBA color, float xPos, float yPos) {
+        BitmapFont font = assetManager.loadFont("Interface/Fonts/Console.fnt");
+        BitmapText bmp = new BitmapText(font);
+        bmp.setSize(font.getCharSet().getRenderedSize());
+        bmp.setLocalTranslation(xPos, settings.getHeight() - yPos, 0);
+        bmp.setColor(color);
+        guiNode.attachChild(bmp);
+        return bmp;
+    }
+}

+ 96 - 0
jme3-examples/src/main/java/jme3test/effect/TestMovingParticle.java

@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2009-2020 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.effect;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.effect.ParticleEmitter;
+import com.jme3.effect.ParticleMesh.Type;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.material.Material;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+
+/**
+ * Particle that moves in a circle.
+ *
+ * @author Kirill Vainer
+ */
+public class TestMovingParticle extends SimpleApplication {
+    
+    private ParticleEmitter emit;
+    private float angle = 0;
+    
+    public static void main(String[] args) {
+        TestMovingParticle app = new TestMovingParticle();
+        app.start();
+    }
+    
+    @Override
+    public void simpleInitApp() {
+        emit = new ParticleEmitter("Emitter", Type.Triangle, 300);
+        emit.setGravity(0, 0, 0);
+        emit.getParticleInfluencer().setVelocityVariation(1);
+        emit.setLowLife(1);
+        emit.setHighLife(1);
+        emit.getParticleInfluencer()
+                .setInitialVelocity(new Vector3f(0, .5f, 0));
+        emit.setImagesX(15);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
+        mat.setTexture("Texture", assetManager.loadTexture("Effects/Smoke/Smoke.png"));
+        emit.setMaterial(mat);
+        
+        rootNode.attachChild(emit);
+        
+        inputManager.addListener(new ActionListener() {
+            
+            @Override
+            public void onAction(String name, boolean isPressed, float tpf) {
+                if ("setNum".equals(name) && isPressed) {
+                    emit.setNumParticles(1000);
+                }
+            }
+        }, "setNum");
+        
+        inputManager.addMapping("setNum", new KeyTrigger(KeyInput.KEY_SPACE));
+    }
+    
+    @Override
+    public void simpleUpdate(float tpf) {
+        angle += tpf;
+        angle %= FastMath.TWO_PI;
+        float x = FastMath.cos(angle) * 2;
+        float y = FastMath.sin(angle) * 2;
+        emit.setLocalTranslation(x, 0, y);
+    }
+}

+ 74 - 0
jme3-examples/src/main/java/jme3test/effect/TestParticleExportingCloning.java

@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package jme3test.effect;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.effect.ParticleEmitter;
+import com.jme3.effect.ParticleMesh.Type;
+import com.jme3.effect.shapes.EmitterSphereShape;
+import com.jme3.export.binary.BinaryExporter;
+import com.jme3.material.Material;
+import com.jme3.math.Vector3f;
+
+public class TestParticleExportingCloning extends SimpleApplication {
+
+    public static void main(String[] args){
+        TestParticleExportingCloning app = new TestParticleExportingCloning();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        ParticleEmitter emit = new ParticleEmitter("Emitter", Type.Triangle, 200);
+        emit.setShape(new EmitterSphereShape(Vector3f.ZERO, 1f));
+        emit.setGravity(0, 0, 0);
+        emit.setLowLife(5);
+        emit.setHighLife(10);
+        emit.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 0, 0));
+        emit.setImagesX(15);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
+        mat.setTexture("Texture", assetManager.loadTexture("Effects/Smoke/Smoke.png"));
+        emit.setMaterial(mat);
+
+        ParticleEmitter emit2 = emit.clone();
+        emit2.move(3, 0, 0);
+        
+        rootNode.attachChild(emit);
+        rootNode.attachChild(emit2);
+        
+        ParticleEmitter emit3 = BinaryExporter.saveAndLoad(assetManager, emit);
+        emit3.move(-3, 0, 0);
+        rootNode.attachChild(emit3);
+    }
+
+}

+ 91 - 0
jme3-examples/src/main/java/jme3test/effect/TestPointSprite.java

@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2009-2020 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.effect;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.effect.ParticleEmitter;
+import com.jme3.effect.ParticleMesh.Type;
+import com.jme3.effect.shapes.EmitterBoxShape;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
+
+public class TestPointSprite extends SimpleApplication {
+
+    public static void main(String[] args){
+        TestPointSprite app = new TestPointSprite();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        final ParticleEmitter emit = new ParticleEmitter("Emitter", Type.Point, 10000);
+        emit.setShape(new EmitterBoxShape(new Vector3f(-1.8f, -1.8f, -1.8f),
+                                          new Vector3f(1.8f, 1.8f, 1.8f)));
+        emit.setGravity(0, 0, 0);
+        emit.setLowLife(60);
+        emit.setHighLife(60);
+        emit.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 0, 0));
+        emit.setImagesX(15);
+        emit.setStartSize(0.05f);
+        emit.setEndSize(0.05f);
+        emit.setStartColor(ColorRGBA.White);
+        emit.setEndColor(ColorRGBA.White);
+        emit.setSelectRandomImage(true);
+        emit.emitAllParticles();
+        
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
+        mat.setBoolean("PointSprite", true);
+        mat.setTexture("Texture", assetManager.loadTexture("Effects/Smoke/Smoke.png"));
+        emit.setMaterial(mat);
+
+        rootNode.attachChild(emit);
+        inputManager.addListener(new ActionListener() {
+            
+            @Override
+            public void onAction(String name, boolean isPressed, float tpf) {
+                if ("setNum".equals(name) && isPressed) {
+                    emit.setNumParticles(5000);
+                    emit.emitAllParticles();
+                }
+            }
+        }, "setNum");
+        
+        inputManager.addMapping("setNum", new KeyTrigger(KeyInput.KEY_SPACE));
+        
+    }
+
+}

+ 184 - 0
jme3-examples/src/main/java/jme3test/effect/TestSoftParticles.java

@@ -0,0 +1,184 @@
+/*
+ * 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 jme3test.effect;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.effect.ParticleEmitter;
+import com.jme3.effect.ParticleMesh;
+import com.jme3.effect.shapes.EmitterSphereShape;
+import com.jme3.input.KeyInput;
+import com.jme3.input.MouseInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.input.controls.MouseButtonTrigger;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.post.FilterPostProcessor;
+import com.jme3.post.filters.TranslucentBucketFilter;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.shape.Box;
+
+/**
+ *
+ * @author Nehon
+ */
+public class TestSoftParticles extends SimpleApplication {
+
+    private boolean softParticles = true;
+    private FilterPostProcessor fpp;
+    private Node particleNode;
+
+    public static void main(String[] args) {
+        TestSoftParticles app = new TestSoftParticles();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+
+        cam.setLocation(new Vector3f(-7.2221026f, 4.1183004f, 7.759811f));
+        cam.setRotation(new Quaternion(0.06152846f, 0.91236454f, -0.1492115f, 0.37621948f));
+
+        flyCam.setMoveSpeed(10);
+
+
+        // -------- floor
+        Box b = new Box(10, 0.1f, 10);
+        Geometry geom = new Geometry("Box", b);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat.setColor("Color", ColorRGBA.Gray);
+        mat.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg"));
+        geom.setMaterial(mat);
+        rootNode.attachChild(geom);
+
+        Box b2 = new Box(1, 1, 1);
+        Geometry geom2 = new Geometry("Box", b2);
+        Material mat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat2.setColor("Color", ColorRGBA.DarkGray);
+        geom2.setMaterial(mat2);
+        rootNode.attachChild(geom2);
+        geom2.setLocalScale(0.1f, 0.2f, 1);
+
+        fpp = new FilterPostProcessor(assetManager);        
+        TranslucentBucketFilter tbf = new TranslucentBucketFilter(true);
+        fpp.addFilter(tbf);
+        int samples = context.getSettings().getSamples();
+        if (samples > 0) {
+            fpp.setNumSamples(samples);
+        }
+        viewPort.addProcessor(fpp);
+
+        particleNode = new Node("particleNode");
+        rootNode.attachChild(particleNode);
+        
+        createParticles();
+
+        
+        inputManager.addListener(new ActionListener() {
+
+            @Override
+            public void onAction(String name, boolean isPressed, float tpf) {
+                if(isPressed && name.equals("toggle")){
+               //     tbf.setEnabled(!tbf.isEnabled());     
+                    softParticles = !softParticles;
+                    if(softParticles){
+                        viewPort.addProcessor(fpp);
+                    }else{
+                        viewPort.removeProcessor(fpp);
+                    }
+                }
+            }
+        }, "toggle");
+        inputManager.addMapping("toggle", new KeyTrigger(KeyInput.KEY_SPACE));
+        
+        // emit again
+        inputManager.addListener(new ActionListener() {
+            @Override
+            public void onAction(String name, boolean isPressed, float tpf) {
+                if(isPressed && name.equals("refire")) {
+                    //fpp.removeFilter(tbf); // <-- add back in to fix
+                    particleNode.detachAllChildren();
+                    createParticles();
+                    //fpp.addFilter(tbf);
+                }
+            }
+        }, "refire");
+        inputManager.addMapping("refire", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
+    }
+
+    private void createParticles() {
+        
+        Material material = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
+        material.setTexture("Texture", assetManager.loadTexture("Effects/Explosion/flame.png"));
+        material.setFloat("Softness", 3f); // 
+        
+        //Fire
+        ParticleEmitter fire = new ParticleEmitter("Fire", ParticleMesh.Type.Triangle, 30);
+        fire.setMaterial(material);
+        fire.setShape(new EmitterSphereShape(Vector3f.ZERO, 0.1f));
+        fire.setImagesX(2);
+        fire.setImagesY(2); // 2x2 texture animation
+        fire.setEndColor(new ColorRGBA(1f, 0f, 0f, 1f)); // red
+        fire.setStartColor(new ColorRGBA(1f, 1f, 0f, 0.5f)); // yellow
+        fire.setStartSize(0.6f);
+        fire.setEndSize(0.01f);
+        fire.setGravity(0, -0.3f, 0);
+        fire.setLowLife(0.5f);
+        fire.setHighLife(3f);
+        fire.setLocalTranslation(0, 0.2f, 0);
+
+        particleNode.attachChild(fire);
+        
+        
+        ParticleEmitter smoke = new ParticleEmitter("Smoke", ParticleMesh.Type.Triangle, 30);
+        smoke.setMaterial(material);
+        smoke.setShape(new EmitterSphereShape(Vector3f.ZERO, 5));
+        smoke.setImagesX(1);
+        smoke.setImagesY(1); // 2x2 texture animation
+        smoke.setStartColor(new ColorRGBA(0.1f, 0.1f, 0.1f,1f)); // dark gray
+        smoke.setEndColor(new ColorRGBA(0.5f, 0.5f, 0.5f, 0.3f)); // gray      
+        smoke.setStartSize(3f);
+        smoke.setEndSize(5f);
+        smoke.setGravity(0, -0.001f, 0);
+        smoke.setLowLife(100f);
+        smoke.setHighLife(100f);
+        smoke.setLocalTranslation(0, 0.1f, 0);        
+        smoke.emitAllParticles();
+        
+        particleNode.attachChild(smoke);
+    }
+    
+    
+}

+ 35 - 0
jme3-examples/src/main/java/jme3test/effect/package-info.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 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.
+ */
+/**
+ * example apps and non-automated tests for special effects, including particles
+ */
+package jme3test.effect;

+ 131 - 0
jme3-examples/src/main/java/jme3test/export/TestAssetLinkNode.java

@@ -0,0 +1,131 @@
+/*
+ * 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 jme3test.export;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.asset.MaterialKey;
+import com.jme3.asset.ModelKey;
+import com.jme3.export.binary.BinaryExporter;
+import com.jme3.export.binary.BinaryImporter;
+import com.jme3.light.DirectionalLight;
+import com.jme3.light.PointLight;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.AssetLinkNode;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.shape.Sphere;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class TestAssetLinkNode extends SimpleApplication {
+
+    private float angle;
+    private PointLight pl;
+    private Spatial lightMdl;
+
+    public static void main(String[] args){
+        TestAssetLinkNode app = new TestAssetLinkNode();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        AssetLinkNode loaderNode=new AssetLinkNode();
+        loaderNode.addLinkedChild(new ModelKey("Models/MonkeyHead/MonkeyHead.mesh.xml"));
+        //load/attach the children (happens automatically on load)
+//        loaderNode.attachLinkedChildren(assetManager);
+//        rootNode.attachChild(loaderNode);
+
+        //save and load the loaderNode
+        try {
+            //export to byte array
+            ByteArrayOutputStream bout=new ByteArrayOutputStream();
+            BinaryExporter.getInstance().save(loaderNode, bout);
+            //import from byte array, automatically loads the monkey head from file
+            ByteArrayInputStream bin=new ByteArrayInputStream(bout.toByteArray());
+            BinaryImporter imp=BinaryImporter.getInstance();
+            imp.setAssetManager(assetManager);
+            Node newLoaderNode=(Node)imp.load(bin);
+            //attach to rootNode
+            rootNode.attachChild(newLoaderNode);
+        } catch (IOException ex) {
+            Logger.getLogger(TestAssetLinkNode.class.getName()).log(Level.SEVERE, null, ex);
+        }
+
+
+        rootNode.attachChild(loaderNode);
+
+        lightMdl = new Geometry("Light", new Sphere(10, 10, 0.1f));
+        lightMdl.setMaterial(assetManager.loadAsset(new MaterialKey("Common/Materials/RedColor.j3m")));
+        rootNode.attachChild(lightMdl);
+
+        // fluorescent main light
+        pl = new PointLight();
+        pl.setColor(new ColorRGBA(0.88f, 0.92f, 0.95f, 1.0f));
+        rootNode.addLight(pl);
+
+        // sunset light
+        DirectionalLight dl = new DirectionalLight();
+        dl.setDirection(new Vector3f(-0.1f,-0.7f,1).normalizeLocal());
+        dl.setColor(new ColorRGBA(0.44f, 0.30f, 0.20f, 1.0f));
+        rootNode.addLight(dl);
+
+        // skylight
+        dl = new DirectionalLight();
+        dl.setDirection(new Vector3f(-0.6f,-1,-0.6f).normalizeLocal());
+        dl.setColor(new ColorRGBA(0.10f, 0.22f, 0.44f, 1.0f));
+        rootNode.addLight(dl);
+
+        // white ambient light
+        dl = new DirectionalLight();
+        dl.setDirection(new Vector3f(1, -0.5f,-0.1f).normalizeLocal());
+        dl.setColor(new ColorRGBA(0.50f, 0.40f, 0.50f, 1.0f));
+        rootNode.addLight(dl);
+    }
+
+    @Override
+    public void simpleUpdate(float tpf){
+        angle += tpf * 0.25f;
+        angle %= FastMath.TWO_PI;
+
+        pl.setPosition(new Vector3f(FastMath.cos(angle) * 6f, 3f, FastMath.sin(angle) * 6f));
+        lightMdl.setLocalTranslation(pl.getPosition());
+    }
+
+}

+ 96 - 0
jme3-examples/src/main/java/jme3test/export/TestIssue2068.java

@@ -0,0 +1,96 @@
+/*
+ * 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.export;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.xml.XMLExporter;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Test case for JME issue: #2068 exporting a Map to XML results in a
+ * DOMException.
+ *
+ * <p>If the issue is unresolved, the application will exit prematurely with an
+ * uncaught exception.
+ *
+ * <p>If the issue is resolved, the application will complete normally.
+ *
+ * @author Stephen Gold [email protected]
+ */
+public class TestIssue2068 extends SimpleApplication {
+    // *************************************************************************
+    // constants and loggers
+
+    /**
+     * message logger for this class
+     */
+    final public static Logger logger
+            = Logger.getLogger(TestIssue2068.class.getName());
+    // *************************************************************************
+    // new methods exposed
+
+    /**
+     * Main entry point for the TestIssue2068 application.
+     *
+     * @param args array of command-line arguments (not null)
+     */
+    public static void main(String[] args) {
+        TestIssue2068 app = new TestIssue2068();
+        app.start();
+    }
+
+    /**
+     * Initialize the application.
+     */
+    @Override
+    public void simpleInitApp() {
+        Map<String, String> map = new HashMap<>();
+        map.put("key", "value");
+        rootNode.setUserData("map", map);
+
+        String outputFilename = "TestIssue2068.xml";
+        File xmlFile = new File(outputFilename);
+        JmeExporter exporter = XMLExporter.getInstance();
+        try {
+            exporter.save(rootNode, xmlFile);
+        } catch (IOException exception) {
+            logger.log(Level.SEVERE, exception.getMessage(), exception);
+        }
+        stop();
+    }
+}

+ 81 - 0
jme3-examples/src/main/java/jme3test/export/TestOgreConvert.java

@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package jme3test.export;
+
+import com.jme3.anim.AnimComposer;
+import com.jme3.app.SimpleApplication;
+import com.jme3.export.binary.BinaryExporter;
+import com.jme3.export.binary.BinaryImporter;
+import com.jme3.light.DirectionalLight;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+
+import java.io.*;
+
+public class TestOgreConvert extends SimpleApplication {
+
+    public static void main(String[] args){
+        TestOgreConvert app = new TestOgreConvert();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        Spatial ogreModel = assetManager.loadModel("Models/Oto/Oto.mesh.xml");
+
+        DirectionalLight dl = new DirectionalLight();
+        dl.setColor(ColorRGBA.White);
+        dl.setDirection(new Vector3f(0,-1,-1).normalizeLocal());
+        rootNode.addLight(dl);
+
+        try {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            BinaryExporter exp = new BinaryExporter();
+            exp.save(ogreModel, baos);
+
+            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+            BinaryImporter imp = new BinaryImporter();
+            imp.setAssetManager(assetManager);
+            Node ogreModelReloaded = (Node) imp.load(bais, null, null);
+
+            AnimComposer composer = ogreModelReloaded.getControl(AnimComposer.class);
+            composer.setCurrentAction("Walk");
+
+            rootNode.attachChild(ogreModelReloaded);
+        } catch (IOException ex){
+            ex.printStackTrace();
+        }
+    }
+}

+ 35 - 0
jme3-examples/src/main/java/jme3test/export/package-info.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 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.
+ */
+/**
+ * example apps and non-automated tests for asset exporting
+ */
+package jme3test.export;

+ 414 - 0
jme3-examples/src/main/java/jme3test/games/CubeField.java

@@ -0,0 +1,414 @@
+/*
+ * 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 jme3test.games;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bounding.BoundingVolume;
+import com.jme3.font.BitmapFont;
+import com.jme3.font.BitmapText;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.AnalogListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.shape.Box;
+import com.jme3.scene.shape.Dome;
+import java.util.ArrayList;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author Kyle "bonechilla" Williams
+ */
+public class CubeField extends SimpleApplication implements AnalogListener {
+
+    public static void main(String[] args) {
+        CubeField app = new CubeField();
+        app.start();
+    }
+
+    private BitmapFont defaultFont;
+
+    private boolean START;
+    private int difficulty, Score, colorInt, lowCap;
+    private Node player;
+    private Geometry fcube;
+    private ArrayList<Geometry> cubeField;
+    private ArrayList<ColorRGBA> obstacleColors;
+    private float speed, coreTime,coreTime2;
+    private float camAngle = 0;
+    private BitmapText fpsScoreText, pressStart;
+
+    private boolean solidBox = true;
+    private Material playerMaterial;
+    private Material floorMaterial;
+
+    final private float fpsRate = 1000f / 1f;
+
+    /**
+     * Initializes game 
+     */
+    @Override
+    public void simpleInitApp() {
+        Logger.getLogger("com.jme3").setLevel(Level.WARNING);
+
+        flyCam.setEnabled(false);
+        setDisplayStatView(false);
+
+        Keys();
+
+        defaultFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
+        pressStart = new BitmapText(defaultFont);
+        fpsScoreText = new BitmapText(defaultFont);
+
+        loadText(fpsScoreText, "Current Score: 0", defaultFont, 0, 2, 0);
+        loadText(pressStart, "PRESS ENTER", defaultFont, 0, 5, 0);
+        
+        player = createPlayer();
+        rootNode.attachChild(player);
+        cubeField = new ArrayList<Geometry>();
+        obstacleColors = new ArrayList<ColorRGBA>();
+
+        gameReset();
+    }
+    /**
+     * Used to reset cubeField 
+     */
+    private void gameReset(){
+        Score = 0;
+        lowCap = 10;
+        colorInt = 0;
+        difficulty = 40;
+
+        for (Geometry cube : cubeField){
+            cube.removeFromParent();
+        }
+        cubeField.clear();
+
+        if (fcube != null){
+            fcube.removeFromParent();
+        }
+        fcube = createFirstCube();
+
+        obstacleColors.clear();
+        obstacleColors.add(ColorRGBA.Orange);
+        obstacleColors.add(ColorRGBA.Red);
+        obstacleColors.add(ColorRGBA.Yellow);
+        renderer.setBackgroundColor(ColorRGBA.White);
+        speed = lowCap / 400f;
+        coreTime = 20.0f;
+        coreTime2 = 10.0f;
+        player.setLocalTranslation(0,0,0);
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        camTakeOver(tpf);
+        if (START){
+            gameLogic(tpf);
+        }
+        colorLogic();
+    }
+    /**
+     * Forcefully takes over Camera adding functionality and placing it behind the character
+     * @param tpf Ticks Per Frame
+     */
+    private void camTakeOver(float tpf) {
+        cam.setLocation(player.getLocalTranslation().add(-8, 2, 0));
+        cam.lookAt(player.getLocalTranslation(), Vector3f.UNIT_Y);
+        
+        Quaternion rot = new Quaternion();
+        rot.fromAngleNormalAxis(camAngle, Vector3f.UNIT_Z);
+        cam.setRotation(cam.getRotation().mult(rot));
+        camAngle *= FastMath.pow(.99f, fpsRate * tpf);
+    }
+
+    @Override
+    public void requestClose(boolean esc) {
+        if (!esc){
+            System.out.println("The game was quit.");
+        }else{
+            System.out.println("Player has Collided. Final Score is " + Score);
+        }
+        context.destroy(false);
+    }
+    /**
+     * Randomly Places a cube on the map between 30 and 90 paces away from player
+     */
+    private void randomizeCube() {
+        Geometry cube = fcube.clone();
+        int playerX = (int) player.getLocalTranslation().getX();
+        int playerZ = (int) player.getLocalTranslation().getZ();
+//        float x = FastMath.nextRandomInt(playerX + difficulty + 10, playerX + difficulty + 150);
+        float x = FastMath.nextRandomInt(playerX + difficulty + 30, playerX + difficulty + 90);
+        float z = FastMath.nextRandomInt(playerZ - difficulty - 50, playerZ + difficulty + 50);
+        cube.getLocalTranslation().set(x, 0, z);
+
+//        playerX+difficulty+30,playerX+difficulty+90
+
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        if (!solidBox){
+            mat.getAdditionalRenderState().setWireframe(true);
+        }
+        mat.setColor("Color", obstacleColors.get(FastMath.nextRandomInt(0, obstacleColors.size() - 1)));
+        cube.setMaterial(mat);
+
+        rootNode.attachChild(cube);
+        cubeField.add(cube);
+    }
+
+    private Geometry createFirstCube() {
+        Vector3f loc = player.getLocalTranslation();
+        loc.addLocal(4, 0, 0);
+        Box b = new Box(1, 1, 1);
+        Geometry geom = new Geometry("Box", b);
+        geom.setLocalTranslation(loc);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat.setColor("Color", ColorRGBA.Blue);
+        geom.setMaterial(mat);
+
+        return geom;
+    }
+
+    private Node createPlayer() {
+        Dome b = new Dome(Vector3f.ZERO, 10, 100, 1);
+        Geometry playerMesh = new Geometry("Box", b);
+
+        playerMaterial = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        playerMaterial.setColor("Color", ColorRGBA.Red);
+        playerMesh.setMaterial(playerMaterial);
+        playerMesh.setName("player");
+
+        Box floor = new Box(100, 0, 100);
+        
+        Geometry floorMesh = new Geometry("Box", floor);
+
+        Vector3f translation = Vector3f.ZERO.add(playerMesh.getLocalTranslation().getX(),
+                playerMesh.getLocalTranslation().getY() - 1, 0);
+
+        floorMesh.setLocalTranslation(translation);
+
+        floorMaterial = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        floorMaterial.setColor("Color", ColorRGBA.LightGray);
+        floorMesh.setMaterial(floorMaterial);
+        floorMesh.setName("floor");
+
+        Node playerNode = new Node();
+        playerNode.attachChild(playerMesh);
+        playerNode.attachChild(floorMesh);
+
+        return playerNode;
+    }
+
+    /**
+     * If Game is Lost display Score and Reset the Game
+     */
+    private void gameLost(){
+        START = false;
+        loadText(pressStart, "You lost! Press enter to try again.", defaultFont, 0, 5, 0);
+        gameReset();
+    }
+    
+    /**
+     * Core Game Logic
+     */
+    private void gameLogic(float tpf){
+        //Subtract difficulty level in accordance to speed every 10 seconds
+        if(timer.getTimeInSeconds()>=coreTime2){
+            coreTime2=timer.getTimeInSeconds()+10;
+            if(difficulty<=lowCap){
+                difficulty=lowCap;
+            }
+            else if(difficulty>lowCap){
+                difficulty-=5;
+            }
+        }
+        
+        if(speed<.1f){
+            speed+=.000001f*tpf*fpsRate;
+        }
+
+        player.move(speed * tpf * fpsRate, 0, 0);
+        if (cubeField.size() > difficulty){
+            cubeField.remove(0);
+        }else if (cubeField.size() != difficulty){
+            randomizeCube();
+        }
+
+        if (cubeField.isEmpty()){
+            requestClose(false);
+        }else{
+            for (int i = 0; i < cubeField.size(); i++){
+
+                //better way to check collision
+                Geometry playerModel = (Geometry) player.getChild(0);
+                Geometry cubeModel = cubeField.get(i);
+
+                BoundingVolume pVol = playerModel.getWorldBound();
+                BoundingVolume vVol = cubeModel.getWorldBound();
+
+                if (pVol.intersects(vVol)){
+                    gameLost();
+                    return;
+                }
+                //Remove cube if 10 world units behind player
+                if (cubeField.get(i).getLocalTranslation().getX() + 10 < player.getLocalTranslation().getX()){
+                    cubeField.get(i).removeFromParent();
+                    cubeField.remove(cubeField.get(i));
+                }
+
+            }
+        }
+
+        Score += fpsRate * tpf;
+        fpsScoreText.setText("Current Score: "+Score);
+    }
+    /**
+     * Sets up the keyboard bindings
+     */
+    private void Keys() {
+        inputManager.addMapping("START", new KeyTrigger(KeyInput.KEY_RETURN));
+        inputManager.addMapping("Left",  new KeyTrigger(KeyInput.KEY_LEFT));
+        inputManager.addMapping("Right", new KeyTrigger(KeyInput.KEY_RIGHT));
+        inputManager.addListener(this, "START", "Left", "Right");
+    }
+
+    @Override
+    public void onAnalog(String binding, float value, float tpf) {
+        if (binding.equals("START") && !START){
+            START = true;
+            guiNode.detachChild(pressStart);
+            System.out.println("START");
+        }else if (START == true && binding.equals("Left")){
+            player.move(0, 0, -(speed / 2f) * value * fpsRate);
+            camAngle -= value*tpf;
+        }else if (START == true && binding.equals("Right")){
+            player.move(0, 0, (speed / 2f) * value * fpsRate);
+            camAngle += value*tpf;
+        }
+    }
+
+    /**
+     * Determines the colors of the player, floor, obstacle and background
+     */
+    private void colorLogic() {
+        if (timer.getTimeInSeconds() >= coreTime){
+            
+            colorInt++;
+            coreTime = timer.getTimeInSeconds() + 20;
+        
+
+            switch (colorInt){
+                case 1:
+                    obstacleColors.clear();
+                    solidBox = false;
+                    obstacleColors.add(ColorRGBA.Green);
+                    renderer.setBackgroundColor(ColorRGBA.Black);
+                    playerMaterial.setColor("Color", ColorRGBA.White);
+            floorMaterial.setColor("Color", ColorRGBA.Black);
+                    break;
+                case 2:
+                    obstacleColors.set(0, ColorRGBA.Black);
+                    solidBox = true;
+                    renderer.setBackgroundColor(ColorRGBA.White);
+                    playerMaterial.setColor("Color", ColorRGBA.Gray);
+                        floorMaterial.setColor("Color", ColorRGBA.LightGray);
+                    break;
+                case 3:
+                    obstacleColors.set(0, ColorRGBA.Pink);
+                    break;
+                case 4:
+                    obstacleColors.set(0, ColorRGBA.Cyan);
+                    obstacleColors.add(ColorRGBA.Magenta);
+                    renderer.setBackgroundColor(ColorRGBA.Gray);
+                        floorMaterial.setColor("Color", ColorRGBA.Gray);
+                    playerMaterial.setColor("Color", ColorRGBA.White);
+                    break;
+                case 5:
+                    obstacleColors.remove(0);
+                    renderer.setBackgroundColor(ColorRGBA.Pink);
+                    solidBox = false;
+                    playerMaterial.setColor("Color", ColorRGBA.White);
+                    break;
+                case 6:
+                    obstacleColors.set(0, ColorRGBA.White);
+                    solidBox = true;
+                    renderer.setBackgroundColor(ColorRGBA.Black);
+                    playerMaterial.setColor("Color", ColorRGBA.Gray);
+                        floorMaterial.setColor("Color", ColorRGBA.LightGray);
+                    break;
+                case 7:
+                    obstacleColors.set(0, ColorRGBA.Green);
+                    renderer.setBackgroundColor(ColorRGBA.Gray);
+                    playerMaterial.setColor("Color", ColorRGBA.Black);
+                        floorMaterial.setColor("Color", ColorRGBA.Orange);
+                    break;
+                case 8:
+                    obstacleColors.set(0, ColorRGBA.Red);
+                        floorMaterial.setColor("Color", ColorRGBA.Pink);
+                    break;
+                case 9:
+                    obstacleColors.set(0, ColorRGBA.Orange);
+                    obstacleColors.add(ColorRGBA.Red);
+                    obstacleColors.add(ColorRGBA.Yellow);
+                    renderer.setBackgroundColor(ColorRGBA.White);
+                    playerMaterial.setColor("Color", ColorRGBA.Red);
+                    floorMaterial.setColor("Color", ColorRGBA.Gray);
+                    colorInt=0;
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+    /**
+     * Sets up a BitmapText to be displayed
+     * @param txt the Bitmap Text
+     * @param text the 
+     * @param font the font of the text
+     * @param x    
+     * @param y
+     * @param z
+     */
+    private void loadText(BitmapText txt, String text, BitmapFont font, float x, float y, float z) {
+        txt.setSize(font.getCharSet().getRenderedSize());
+        txt.setLocalTranslation(txt.getLineWidth() * x, txt.getLineHeight() * y, z);
+        txt.setText(text);
+        guiNode.attachChild(txt);
+    }
+} 

+ 411 - 0
jme3-examples/src/main/java/jme3test/games/RollingTheMonkey.java

@@ -0,0 +1,411 @@
+/*
+ * 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 jme3test.games;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.PhysicsCollisionEvent;
+import com.jme3.bullet.collision.PhysicsCollisionListener;
+import com.jme3.bullet.collision.shapes.BoxCollisionShape;
+import com.jme3.bullet.collision.shapes.CompoundCollisionShape;
+import com.jme3.bullet.collision.shapes.SphereCollisionShape;
+import com.jme3.bullet.control.GhostControl;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.font.BitmapText;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.post.FilterPostProcessor;
+import com.jme3.renderer.queue.RenderQueue.ShadowMode;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.shape.Box;
+import com.jme3.scene.shape.Sphere;
+import com.jme3.shadow.DirectionalLightShadowFilter;
+import java.util.concurrent.Callable;
+
+/**
+ * Physics based marble game.
+ * 
+ * @author SkidRunner (Mark E. Picknell)
+ */
+public class RollingTheMonkey extends SimpleApplication implements ActionListener, PhysicsCollisionListener {
+    
+    private static final String MESSAGE         = "Thanks for Playing!";
+    private static final String INFO_MESSAGE    = "Collect all the spinning cubes!\nPress the 'R' key any time to reset!";
+    
+    private static final float PLAYER_DENSITY   = 1200;  // OLK(Java LOL) = 1200, STEEL = 8000, RUBBER = 1000
+    private static final float PLAYER_REST      = 0.1f;     // OLK = 0.1f, STEEL = 0.0f, RUBBER = 1.0f I made these up.
+    
+    private static final float PLAYER_RADIUS    = 2.0f;
+    private static final float PLAYER_ACCEL     = 1.0f;
+    
+    private static final float PICKUP_SIZE      = 0.5f;
+    private static final float PICKUP_RADIUS    = 15.0f;
+    private static final int   PICKUP_COUNT     = 16;
+    private static final float PICKUP_SPEED     = 5.0f;
+    
+    private static final float PLAYER_VOLUME    = (FastMath.pow(PLAYER_RADIUS, 3) * FastMath.PI) / 3;   // V = 4/3 * PI * R pow 3
+    private static final float PLAYER_MASS      = PLAYER_DENSITY * PLAYER_VOLUME;
+    private static final float PLAYER_FORCE     = 80000 * PLAYER_ACCEL;  // F = M(4m diameter steel ball) * A
+    private static final Vector3f PLAYER_START  = new Vector3f(0.0f, PLAYER_RADIUS * 2, 0.0f);
+    
+    private static final String INPUT_MAPPING_FORWARD   = "INPUT_MAPPING_FORWARD";
+    private static final String INPUT_MAPPING_BACKWARD  = "INPUT_MAPPING_BACKWARD";
+    private static final String INPUT_MAPPING_LEFT      = "INPUT_MAPPING_LEFT";
+    private static final String INPUT_MAPPING_RIGHT     = "INPUT_MAPPING_RIGHT";
+    private static final String INPUT_MAPPING_RESET     = "INPUT_MAPPING_RESET";
+    
+    public static void main(String[] args) {
+        RollingTheMonkey app = new RollingTheMonkey();
+        app.start();
+    }
+    
+    private boolean keyForward;
+    private boolean keyBackward;
+    private boolean keyLeft;
+    private boolean keyRight;
+    private RigidBodyControl player;
+    private int score;
+    
+    private Node pickUps;
+    private BitmapText scoreText;
+    private BitmapText messageText;
+    
+    @Override
+    public void simpleInitApp() {
+        flyCam.setEnabled(false);
+        cam.setLocation(new Vector3f(0.0f, 12.0f, 21.0f));
+        viewPort.setBackgroundColor(new ColorRGBA(0.2118f, 0.0824f, 0.6549f, 1.0f));
+        
+        // init physics
+        BulletAppState bulletState = new BulletAppState();
+        stateManager.attach(bulletState);
+        PhysicsSpace space = bulletState.getPhysicsSpace();
+        space.addCollisionListener(this);
+        
+        // create light
+        DirectionalLight sun = new DirectionalLight();
+        sun.setDirection((new Vector3f(-0.7f, -0.3f, -0.5f)).normalizeLocal());
+        System.out.println("Here We Go: " + sun.getDirection());
+        sun.setColor(ColorRGBA.White);
+        rootNode.addLight(sun); 
+        
+        // create materials
+        Material materialRed = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        materialRed.setBoolean("UseMaterialColors",true);
+        materialRed.setBoolean("HardwareShadows", true);
+        materialRed.setColor("Diffuse", new ColorRGBA(0.9451f, 0.0078f, 0.0314f, 1.0f));
+        materialRed.setColor("Specular", ColorRGBA.White);
+        materialRed.setFloat("Shininess", 64.0f);
+        
+        Material materialGreen = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        materialGreen.setBoolean("UseMaterialColors",true);
+        materialGreen.setBoolean("HardwareShadows", true);
+        materialGreen.setColor("Diffuse", new ColorRGBA(0.0431f, 0.7725f, 0.0078f, 1.0f));
+        materialGreen.setColor("Specular", ColorRGBA.White);
+        materialGreen.setFloat("Shininess", 64.0f);
+        
+        Material logoMaterial = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        logoMaterial.setBoolean("UseMaterialColors",true);
+        logoMaterial.setBoolean("HardwareShadows", true);
+        logoMaterial.setTexture("DiffuseMap", assetManager.loadTexture("com/jme3/app/Monkey.png"));
+        logoMaterial.setColor("Diffuse", ColorRGBA.White);
+        logoMaterial.setColor("Specular", ColorRGBA.White);
+        logoMaterial.setFloat("Shininess", 32.0f);
+        
+        Material materialYellow = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        materialYellow.setBoolean("UseMaterialColors",true);
+        materialYellow.setBoolean("HardwareShadows", true);
+        materialYellow.setColor("Diffuse", new ColorRGBA(0.9529f, 0.7843f, 0.0078f, 1.0f));
+        materialYellow.setColor("Specular", ColorRGBA.White);
+        materialYellow.setFloat("Shininess", 64.0f);
+        
+        // create level spatial
+        // TODO: create your own level mesh
+        Node level = new Node("level");
+        level.setShadowMode(ShadowMode.CastAndReceive);
+        
+        Geometry floor = new Geometry("floor", new Box(22.0f, 0.5f, 22.0f));
+        floor.setShadowMode(ShadowMode.Receive);
+        floor.setLocalTranslation(0.0f, -0.5f, 0.0f);
+        floor.setMaterial(materialGreen);
+        
+        Geometry wallNorth = new Geometry("wallNorth", new Box(22.0f, 2.0f, 0.5f));
+        wallNorth.setLocalTranslation(0.0f, 2.0f, 21.5f);
+        wallNorth.setMaterial(materialRed);
+        
+        Geometry wallSouth = new Geometry("wallSouth", new Box(22.0f, 2.0f, 0.5f));
+        wallSouth.setLocalTranslation(0.0f, 2.0f, -21.5f);
+        wallSouth.setMaterial(materialRed);
+        
+        Geometry wallEast = new Geometry("wallEast", new Box(0.5f, 2.0f, 21.0f));
+        wallEast.setLocalTranslation(-21.5f, 2.0f, 0.0f);
+        wallEast.setMaterial(materialRed);
+        
+        Geometry wallWest = new Geometry("wallWest", new Box(0.5f, 2.0f, 21.0f));
+        wallWest.setLocalTranslation(21.5f, 2.0f, 0.0f);
+        wallWest.setMaterial(materialRed);
+        
+        level.attachChild(floor);
+        level.attachChild(wallNorth);
+        level.attachChild(wallSouth);
+        level.attachChild(wallEast);
+        level.attachChild(wallWest);
+        
+        // The easy way: level.addControl(new RigidBodyControl(0));
+        
+        // create level Shape
+        CompoundCollisionShape levelShape = new CompoundCollisionShape();
+        BoxCollisionShape floorShape = new BoxCollisionShape(new Vector3f(22.0f, 0.5f, 22.0f));
+        BoxCollisionShape wallNorthShape = new BoxCollisionShape(new Vector3f(22.0f, 2.0f, 0.5f));
+        BoxCollisionShape wallSouthShape = new BoxCollisionShape(new Vector3f(22.0f, 2.0f, 0.5f));
+        BoxCollisionShape wallEastShape = new BoxCollisionShape(new Vector3f(0.5f, 2.0f, 21.0f));
+        BoxCollisionShape wallWestShape = new BoxCollisionShape(new Vector3f(0.5f, 2.0f, 21.0f));
+        
+        levelShape.addChildShape(floorShape, new Vector3f(0.0f, -0.5f, 0.0f));
+        levelShape.addChildShape(wallNorthShape, new Vector3f(0.0f, 2.0f, -21.5f));
+        levelShape.addChildShape(wallSouthShape, new Vector3f(0.0f, 2.0f, 21.5f));
+        levelShape.addChildShape(wallEastShape, new Vector3f(-21.5f, 2.0f, 0.0f));
+        levelShape.addChildShape(wallEastShape, new Vector3f(21.5f, 2.0f, 0.0f));
+        
+        level.addControl(new RigidBodyControl(levelShape, 0));
+        
+        rootNode.attachChild(level);
+        space.addAll(level);
+        
+        // create Pickups
+        // TODO: create your own pickUp mesh
+        //       create single mesh for all pickups
+        // HINT: think particles.
+        pickUps = new Node("pickups");
+        
+        Quaternion rotation = new Quaternion();
+        Vector3f translation = new Vector3f(0.0f, PICKUP_SIZE * 1.5f, -PICKUP_RADIUS);
+        int index = 0;
+        float amount = FastMath.TWO_PI / PICKUP_COUNT;
+        for(float angle = 0; angle < FastMath.TWO_PI; angle += amount) {
+            Geometry pickUp = new Geometry("pickUp" + (index++), new Box(PICKUP_SIZE,PICKUP_SIZE, PICKUP_SIZE));
+            pickUp.setShadowMode(ShadowMode.CastAndReceive);
+            pickUp.setMaterial(materialYellow);
+            pickUp.setLocalRotation(rotation.fromAngles(
+                    FastMath.rand.nextFloat() * FastMath.TWO_PI,
+                    FastMath.rand.nextFloat() * FastMath.TWO_PI,
+                    FastMath.rand.nextFloat() * FastMath.TWO_PI));
+            
+            rotation.fromAngles(0.0f, angle, 0.0f);
+            rotation.mult(translation, pickUp.getLocalTranslation());
+            pickUps.attachChild(pickUp);
+            
+            pickUp.addControl(new GhostControl(new SphereCollisionShape(PICKUP_SIZE)));
+            
+            
+            space.addAll(pickUp);
+            //space.addCollisionListener(pickUpControl);
+        }
+        rootNode.attachChild(pickUps);
+        
+        // Create player
+        // TODO: create your own player mesh
+        Geometry playerGeometry = new Geometry("player", new Sphere(16, 32, PLAYER_RADIUS));
+        playerGeometry.setShadowMode(ShadowMode.CastAndReceive);
+        playerGeometry.setLocalTranslation(PLAYER_START.clone());
+        playerGeometry.setMaterial(logoMaterial);
+        
+        // Store control for applying forces
+        // TODO: create your own player control
+        player = new RigidBodyControl(new SphereCollisionShape(PLAYER_RADIUS), PLAYER_MASS);
+        player.setRestitution(PLAYER_REST);
+        
+        playerGeometry.addControl(player);
+        
+        rootNode.attachChild(playerGeometry);
+        space.addAll(playerGeometry);
+        
+        inputManager.addMapping(INPUT_MAPPING_FORWARD, new KeyTrigger(KeyInput.KEY_UP)
+                , new KeyTrigger(KeyInput.KEY_W));
+        inputManager.addMapping(INPUT_MAPPING_BACKWARD, new KeyTrigger(KeyInput.KEY_DOWN)
+                , new KeyTrigger(KeyInput.KEY_S));
+        inputManager.addMapping(INPUT_MAPPING_LEFT, new KeyTrigger(KeyInput.KEY_LEFT)
+                , new KeyTrigger(KeyInput.KEY_A));
+        inputManager.addMapping(INPUT_MAPPING_RIGHT, new KeyTrigger(KeyInput.KEY_RIGHT)
+                , new KeyTrigger(KeyInput.KEY_D));
+        inputManager.addMapping(INPUT_MAPPING_RESET, new KeyTrigger(KeyInput.KEY_R));
+        inputManager.addListener(this, INPUT_MAPPING_FORWARD, INPUT_MAPPING_BACKWARD
+                , INPUT_MAPPING_LEFT, INPUT_MAPPING_RIGHT, INPUT_MAPPING_RESET);
+        
+        // init UI
+        BitmapText infoText = new BitmapText(guiFont);
+        infoText.setText(INFO_MESSAGE);
+        guiNode.attachChild(infoText);
+        
+        scoreText = new BitmapText(guiFont);
+        scoreText.setText("Score: 0");
+        guiNode.attachChild(scoreText);
+        
+        messageText = new BitmapText(guiFont);
+        messageText.setText(MESSAGE);
+        messageText.setLocalScale(0.0f);
+        guiNode.attachChild(messageText);
+        
+        infoText.setLocalTranslation(0.0f, cam.getHeight(), 0.0f);
+        scoreText.setLocalTranslation((cam.getWidth() - scoreText.getLineWidth()) / 2.0f,
+                scoreText.getLineHeight(), 0.0f);
+        messageText.setLocalTranslation((cam.getWidth() - messageText.getLineWidth()) / 2.0f,
+                (cam.getHeight() - messageText.getLineHeight()) / 2, 0.0f);
+        
+        // init shadows
+        FilterPostProcessor processor = new FilterPostProcessor(assetManager);
+        DirectionalLightShadowFilter filter = new DirectionalLightShadowFilter(assetManager, 2048, 1);
+        filter.setLight(sun);
+        processor.addFilter(filter);
+        viewPort.addProcessor(processor);
+        
+    }
+    
+    @Override
+    public void simpleUpdate(float tpf) {
+        // Update and position the score
+        scoreText.setText("Score: " + score);
+        scoreText.setLocalTranslation((cam.getWidth() - scoreText.getLineWidth()) / 2.0f,
+                scoreText.getLineHeight(), 0.0f);
+        
+        // Rotate all the pickups
+        float pickUpSpeed = PICKUP_SPEED * tpf;
+        for(Spatial pickUp : pickUps.getChildren()) {
+            pickUp.rotate(pickUpSpeed, pickUpSpeed, pickUpSpeed);
+        }
+        
+        Vector3f centralForce = new Vector3f();
+        
+        if(keyForward) centralForce.addLocal(cam.getDirection());
+        if(keyBackward) centralForce.addLocal(cam.getDirection().negate());
+        if(keyLeft) centralForce.addLocal(cam.getLeft());
+        if(keyRight) centralForce.addLocal(cam.getLeft().negate());
+        
+        if(!Vector3f.ZERO.equals(centralForce)) {
+            centralForce.setY(0);                   // stop ball from pushing down or flying up
+            centralForce.normalizeLocal();          // normalize force
+            centralForce.multLocal(PLAYER_FORCE);   // scale vector to force
+
+            player.applyCentralForce(centralForce); // apply force to player
+        }
+        
+        cam.lookAt(player.getPhysicsLocation(), Vector3f.UNIT_Y);
+    }
+
+    @Override
+    public void onAction(String name, boolean isPressed, float tpf) {
+        switch(name) {
+            case INPUT_MAPPING_FORWARD:
+                keyForward = isPressed;
+                break;
+            case INPUT_MAPPING_BACKWARD:
+                keyBackward = isPressed;
+                break;
+            case INPUT_MAPPING_LEFT:
+                keyLeft = isPressed;
+                break;
+            case INPUT_MAPPING_RIGHT:
+                keyRight = isPressed;
+                break;
+            case INPUT_MAPPING_RESET:
+                enqueue(new Callable<Void>() {
+                    @Override
+                    public Void call() {
+                        reset();
+                        return null;
+                    }
+                });
+                break;
+        }
+    }
+    @Override
+    public void collision(PhysicsCollisionEvent event) {
+        Spatial nodeA = event.getNodeA();
+        Spatial nodeB = event.getNodeB();
+        
+        String nameA = nodeA == null ? "" : nodeA.getName();
+        String nameB = nodeB == null ? "" : nodeB.getName();
+        
+        if(nameA.equals("player") && nameB.startsWith("pickUp")) {
+            GhostControl pickUpControl = nodeB.getControl(GhostControl.class);
+            if(pickUpControl != null && pickUpControl.isEnabled()) {
+                pickUpControl.setEnabled(false);
+                nodeB.removeFromParent();
+                nodeB.setLocalScale(0.0f);
+                score += 1;
+                if(score >= PICKUP_COUNT) {
+                    messageText.setLocalScale(1.0f);
+                }
+            }
+        } else if(nameA.startsWith("pickUp") && nameB.equals("player")) {
+            GhostControl pickUpControl = nodeA.getControl(GhostControl.class);
+            if(pickUpControl != null && pickUpControl.isEnabled()) {
+                pickUpControl.setEnabled(false);
+                nodeA.setLocalScale(0.0f);
+                score += 1;
+                if(score >= PICKUP_COUNT) {
+                    messageText.setLocalScale(1.0f);
+                }
+            }
+        }
+    }
+    
+    private void reset() {
+        // Reset the pickups
+        for(Spatial pickUp : pickUps.getChildren()) {
+            GhostControl pickUpControl = pickUp.getControl(GhostControl.class);
+            if(pickUpControl != null) {
+                pickUpControl.setEnabled(true);
+            }
+            pickUp.setLocalScale(1.0f);
+        }
+        // Reset the player
+        player.setPhysicsLocation(PLAYER_START.clone());
+        player.setAngularVelocity(Vector3f.ZERO.clone());
+        player.setLinearVelocity(Vector3f.ZERO.clone());
+        // Reset the score
+        score = 0;
+        // Reset the message
+        messageText.setLocalScale(0.0f);
+    }
+    
+}

+ 535 - 0
jme3-examples/src/main/java/jme3test/games/WorldOfInception.java

@@ -0,0 +1,535 @@
+/*
+ * 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 jme3test.games;
+
+import com.jme3.app.Application;
+import com.jme3.app.SimpleApplication;
+import com.jme3.app.state.AbstractAppState;
+import com.jme3.app.state.AppStateManager;
+import com.jme3.bullet.BulletAppState;
+import com.jme3.bullet.collision.shapes.CollisionShape;
+import com.jme3.bullet.collision.shapes.MeshCollisionShape;
+import com.jme3.bullet.collision.shapes.SphereCollisionShape;
+import com.jme3.bullet.control.RigidBodyControl;
+import com.jme3.bullet.debug.DebugTools;
+import com.jme3.font.BitmapText;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.AnalogListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Ray;
+import com.jme3.math.Vector3f;
+import com.jme3.post.FilterPostProcessor;
+import com.jme3.post.filters.FogFilter;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.shape.Sphere;
+import java.util.Random;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * WorldOfInception - Find the galaxy center ;)
+ *
+ * @author normenhansen
+ */
+public class WorldOfInception extends SimpleApplication implements AnalogListener {
+
+    //Assumptions: POI radius in world == 1, only one player, vector3f hash describes enough worlds
+    private static final Logger logger = Logger.getLogger(WorldOfInception.class.getName());
+    private static final Random random = new Random(System.currentTimeMillis());
+    private static final float scaleDist = 10;
+    private static final float poiRadius = 100;
+    private static final int poiCount = 30;
+    private static Material poiMaterial;
+    private static Mesh poiMesh;
+    private static Material ballMaterial;
+    private static Mesh ballMesh;
+    private static CollisionShape poiHorizonCollisionShape;
+    private static CollisionShape poiCollisionShape;
+    private static CollisionShape ballCollisionShape;
+    private InceptionLevel currentLevel;
+    private final Vector3f walkDirection = new Vector3f();
+    private static DebugTools debugTools;
+
+    public WorldOfInception() {
+        //base level vector position hash == seed
+        super(new InceptionLevel(null, Vector3f.ZERO));
+        currentLevel = super.getStateManager().getState(InceptionLevel.class);
+        currentLevel.takeOverParent();
+        currentLevel.getRootNode().setLocalScale(Vector3f.UNIT_XYZ);
+        currentLevel.getRootNode().setLocalTranslation(Vector3f.ZERO);
+    }
+
+    public static void main(String[] args) {
+        WorldOfInception app = new WorldOfInception();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        //set far frustum only so we see the outer world longer
+        cam.setFrustumFar(10000);
+        cam.setLocation(Vector3f.ZERO);
+        debugTools = new DebugTools(assetManager);
+        rootNode.attachChild(debugTools.debugNode);
+        poiMaterial = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        poiMaterial.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg"));
+        poiMesh = new Sphere(16, 16, 1f);
+
+        ballMaterial = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        ballMaterial.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg"));
+        ballMaterial.setColor("Color", ColorRGBA.Red);
+        ballMesh = new Sphere(16, 16, 1.0f);
+
+        poiHorizonCollisionShape = new MeshCollisionShape(new Sphere(128, 128, poiRadius));
+        poiCollisionShape = new SphereCollisionShape(1f);
+        ballCollisionShape = new SphereCollisionShape(1f);
+        setupKeys();
+        setupDisplay();
+        setupFog();
+    }
+
+    private void setupKeys() {
+        inputManager.addMapping("StrafeLeft", new KeyTrigger(KeyInput.KEY_A));
+        inputManager.addMapping("StrafeRight", new KeyTrigger(KeyInput.KEY_D));
+        inputManager.addMapping("Forward", new KeyTrigger(KeyInput.KEY_W));
+        inputManager.addMapping("Back", new KeyTrigger(KeyInput.KEY_S));
+        inputManager.addMapping("StrafeUp", new KeyTrigger(KeyInput.KEY_Q));
+        inputManager.addMapping("StrafeDown", new KeyTrigger(KeyInput.KEY_Z), new KeyTrigger(KeyInput.KEY_Y));
+        inputManager.addMapping("Space", new KeyTrigger(KeyInput.KEY_SPACE));
+        inputManager.addMapping("Return", new KeyTrigger(KeyInput.KEY_RETURN));
+        inputManager.addMapping("Esc", new KeyTrigger(KeyInput.KEY_ESCAPE));
+        inputManager.addMapping("Up", new KeyTrigger(KeyInput.KEY_UP));
+        inputManager.addMapping("Down", new KeyTrigger(KeyInput.KEY_DOWN));
+        inputManager.addMapping("Left", new KeyTrigger(KeyInput.KEY_LEFT));
+        inputManager.addMapping("Right", new KeyTrigger(KeyInput.KEY_RIGHT));
+        inputManager.addListener(this, "StrafeLeft", "StrafeRight", "Forward", "Back", "StrafeUp", "StrafeDown", "Space", "Reset", "Esc", "Up", "Down", "Left", "Right");
+    }
+
+    private void setupDisplay() {
+        if (fpsText == null) {
+            fpsText = new BitmapText(guiFont);
+        }
+        fpsText.setLocalScale(0.7f, 0.7f, 0.7f);
+        fpsText.setLocalTranslation(0, fpsText.getLineHeight(), 0);
+        fpsText.setText("");
+        fpsText.setCullHint(Spatial.CullHint.Never);
+        guiNode.attachChild(fpsText);
+    }
+
+    private void setupFog() {
+        // use fog to give more sense of depth
+        FilterPostProcessor fpp;
+        FogFilter fog;
+        fpp=new FilterPostProcessor(assetManager);
+        fog=new FogFilter();
+        fog.setFogColor(new ColorRGBA(0.0f, 0.0f, 0.0f, 1.0f));
+        fog.setFogDistance(poiRadius);
+        fog.setFogDensity(2.0f);
+        fpp.addFilter(fog);
+        viewPort.addProcessor(fpp);
+    }
+
+    @Override
+    public void onAnalog(String name, float value, float tpf) {
+        Vector3f left = rootNode.getLocalRotation().mult(Vector3f.UNIT_X.negate());
+        Vector3f forward = rootNode.getLocalRotation().mult(Vector3f.UNIT_Z.negate());
+        Vector3f up = rootNode.getLocalRotation().mult(Vector3f.UNIT_Y);
+        //TODO: properly scale input based on current scaling level
+        tpf = tpf * (10 - (9.0f * currentLevel.getCurrentScaleAmount()));
+        if (name.equals("StrafeLeft") && value > 0) {
+            walkDirection.addLocal(left.mult(tpf));
+        } else if (name.equals("StrafeRight") && value > 0) {
+            walkDirection.addLocal(left.negate().multLocal(tpf));
+        } else if (name.equals("Forward") && value > 0) {
+            walkDirection.addLocal(forward.mult(tpf));
+        } else if (name.equals("Back") && value > 0) {
+            walkDirection.addLocal(forward.negate().multLocal(tpf));
+        } else if (name.equals("StrafeUp") && value > 0) {
+            walkDirection.addLocal(up.mult(tpf));
+        } else if (name.equals("StrafeDown") && value > 0) {
+            walkDirection.addLocal(up.negate().multLocal(tpf));
+        } else if (name.equals("Up") && value > 0) {
+            //TODO: rotate rootNode, needs to be global
+        } else if (name.equals("Down") && value > 0) {
+        } else if (name.equals("Left") && value > 0) {
+        } else if (name.equals("Right") && value > 0) {
+        } else if (name.equals("Esc")) {
+            stop();
+        }
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        currentLevel = currentLevel.getCurrentLevel();
+        currentLevel.move(walkDirection);
+        fpsText.setText("Location: " + currentLevel.getCoordinates());
+        walkDirection.set(Vector3f.ZERO);
+    }
+
+    public static class InceptionLevel extends AbstractAppState {
+
+        private final InceptionLevel parent;
+        private final Vector3f inParentPosition;
+        private SimpleApplication application;
+        private BulletAppState physicsState;
+        private Node rootNode;
+        private Vector3f playerPos;
+        private InceptionLevel currentActiveChild;
+        private InceptionLevel currentReturnLevel;
+        private float curScaleAmount = 0;
+
+        public InceptionLevel(InceptionLevel parent, Vector3f inParentPosition) {
+            this.parent = parent;
+            this.inParentPosition = inParentPosition;
+        }
+
+        @Override
+        public void update(float tpf) {
+            super.update(tpf);
+            if (currentReturnLevel != this) {
+                return;
+            }
+            debugTools.setYellowArrow(new Vector3f(0, 0, -2), playerPos.divide(poiRadius));
+            float curLocalDist = getPlayerPosition().length();
+            // If we are outside the range of one point of interest, move out to
+            // the next upper level
+            if (curLocalDist > poiRadius + FastMath.ZERO_TOLERANCE) { //DAFUQ normalize?
+                if (parent == null) {
+                    //TODO: could add new nodes coming in instead for literally endless space
+                    logger.log(Level.INFO, "Hit event horizon");
+                    currentReturnLevel = this;
+                    return;
+                }
+                //give to parent
+                logger.log(Level.INFO, "give to parent");
+                parent.takeOverChild(inParentPosition.add(playerPos.normalize()));
+                application.getStateManager().attach(parent);
+                currentReturnLevel = parent;
+                return;
+            }
+
+            AppStateManager stateManager = application.getStateManager();
+            // We create child positions based on the parent position hash, so we
+            // should in practice get the same galaxy w/o too many doubles
+            // with each run with the same root vector.
+            Vector3f[] vectors = getPositions(poiCount, inParentPosition.hashCode());
+            for (int i = 0; i < vectors.length; i++) {
+                Vector3f vector3f = vectors[i];
+                //negative rootNode location is our actual player position
+                Vector3f distVect = vector3f.subtract(playerPos);
+                float distance = distVect.length();
+                if (distance <= 1) {
+                    checkActiveChild(vector3f);
+                    float percent = 0;
+                    curScaleAmount = 0;
+                    this.scaleAsParent(percent, playerPos, distVect);
+                    currentActiveChild.scaleAsChild(percent, distVect);
+                    logger.log(Level.INFO, "Give over to child {0}", currentActiveChild);
+                    currentActiveChild.takeOverParent();
+                    stateManager.detach(this);
+                    currentReturnLevel = currentActiveChild;
+                    return;
+                } else if (distance <= 1 + scaleDist) {
+                    debugTools.setRedArrow(Vector3f.ZERO, distVect);
+                    checkActiveChild(vector3f);
+                    //TODO: scale percent nicer for less of an "explosion" effect
+                    float percent = 1 - mapValue(distance - 1, 0, scaleDist, 0, 1);
+                    curScaleAmount = percent;
+                    rootNode.getChild(i).setCullHint(Spatial.CullHint.Always);
+                    this.scaleAsParent(percent, playerPos, distVect);
+                    currentActiveChild.scaleAsChild(percent, distVect);
+                    currentReturnLevel = this;
+                    return;
+                } else if (currentActiveChild != null && currentActiveChild.getPositionInParent().equals(vector3f)) {
+                    //TODO: doing this here causes problems when close to multiple POIs
+                    rootNode.getChild(i).setCullHint(Spatial.CullHint.Inherit);
+                }
+            }
+            checkActiveChild(null);
+            curScaleAmount = 0;
+            rootNode.setLocalScale(1);
+            rootNode.setLocalTranslation(playerPos.negate());
+            debugTools.setRedArrow(Vector3f.ZERO, Vector3f.ZERO);
+            debugTools.setBlueArrow(Vector3f.ZERO, Vector3f.ZERO);
+            debugTools.setGreenArrow(Vector3f.ZERO, Vector3f.ZERO);
+        }
+
+        private void checkActiveChild(Vector3f vector3f) {
+            AppStateManager stateManager = application.getStateManager();
+            if(vector3f == null){
+                if(currentActiveChild != null){
+                    logger.log(Level.INFO, "Detach child {0}", currentActiveChild);
+                    stateManager.detach(currentActiveChild);
+                    currentActiveChild = null;
+                }
+                return;
+            }
+            if (currentActiveChild == null) {
+                currentActiveChild = new InceptionLevel(this, vector3f);
+                stateManager.attach(currentActiveChild);
+                logger.log(Level.INFO, "Attach child {0}", currentActiveChild);
+            } else if (!currentActiveChild.getPositionInParent().equals(vector3f)) {
+                logger.log(Level.INFO, "Switching from child {0}", currentActiveChild);
+                stateManager.detach(currentActiveChild);
+                currentActiveChild = new InceptionLevel(this, vector3f);
+                stateManager.attach(currentActiveChild);
+                logger.log(Level.INFO, "Attach child {0}", currentActiveChild);
+            }
+        }
+
+        private void scaleAsChild(float percent, Vector3f dist) {
+            float childScale = mapValue(percent, 1.0f / poiRadius, 1);
+            Vector3f distToHorizon = dist.normalize();
+            Vector3f scaledDistToHorizon = distToHorizon.mult(childScale * poiRadius);
+            Vector3f rootOff = dist.add(scaledDistToHorizon);
+            debugTools.setBlueArrow(Vector3f.ZERO, rootOff);
+            getRootNode().setLocalScale(childScale);
+            getRootNode().setLocalTranslation(rootOff);
+            //prepare player position already
+            Vector3f playerPosition = dist.normalize().mult(-poiRadius);
+            setPlayerPosition(playerPosition);
+        }
+
+        private void scaleAsParent(float percent, Vector3f playerPos, Vector3f dist) {
+            float scale = mapValue(percent, 1.0f, poiRadius);
+            Vector3f distToHorizon = dist.subtract(dist.normalize());
+            Vector3f offLocation = playerPos.add(distToHorizon);
+            Vector3f rootOff = offLocation.mult(scale).negate();
+            rootOff.addLocal(dist);
+            debugTools.setGreenArrow(Vector3f.ZERO, offLocation);
+            getRootNode().setLocalScale(scale);
+            getRootNode().setLocalTranslation(rootOff);
+        }
+
+        public void takeOverParent() {
+            //got playerPos from scaleAsChild before
+            getPlayerPosition().normalizeLocal().multLocal(poiRadius);
+            currentReturnLevel = this;
+        }
+
+        public void takeOverChild(Vector3f playerPos) {
+            this.playerPos.set(playerPos);
+            currentReturnLevel = this;
+        }
+
+        public InceptionLevel getLastLevel(Ray pickRay) {
+            // TODO: get a level based on positions getting ever more accurate,
+            // from any given position
+            return null;
+        }
+
+        public InceptionLevel getLevel(Vector3f... location) {
+            // TODO: get a level based on positions getting ever more accurate,
+            // from any given position
+            return null;
+        }
+
+        private void initData() {
+            getRootNode();
+            physicsState = new BulletAppState();
+            physicsState.startPhysics();
+            physicsState.getPhysicsSpace().setGravity(Vector3f.ZERO);
+            //horizon
+            physicsState.getPhysicsSpace().add(new RigidBodyControl(poiHorizonCollisionShape, 0));
+            int hashCode = inParentPosition.hashCode();
+            Vector3f[] positions = getPositions(poiCount, hashCode);
+            for (int i = 0; i < positions.length; i++) {
+                Vector3f vector3f = positions[i];
+                Geometry poiGeom = new Geometry("poi", poiMesh);
+                poiGeom.setLocalTranslation(vector3f);
+                poiGeom.setMaterial(poiMaterial);
+                RigidBodyControl control = new RigidBodyControl(poiCollisionShape, 0);
+                //!!! Important
+                control.setApplyPhysicsLocal(true);
+                poiGeom.addControl(control);
+                physicsState.getPhysicsSpace().add(poiGeom);
+                rootNode.attachChild(poiGeom);
+
+            }
+            //add balls after so first 10 geoms == locations
+            for (int i = 0; i < positions.length; i++) {
+                Vector3f vector3f = positions[i];
+                Geometry ball = getRandomBall(vector3f);
+                physicsState.getPhysicsSpace().add(ball);
+                rootNode.attachChild(ball);
+            }
+
+        }
+
+        private Geometry getRandomBall(Vector3f location) {
+            Vector3f localLocation = new Vector3f();
+            localLocation.set(location);
+            localLocation.addLocal(new Vector3f(random.nextFloat() - 0.5f, random.nextFloat() - 0.5f, random.nextFloat() - 0.5f).normalize().mult(3));
+            Geometry poiGeom = new Geometry("ball", ballMesh);
+            poiGeom.setLocalTranslation(localLocation);
+            poiGeom.setMaterial(ballMaterial);
+            RigidBodyControl control = new RigidBodyControl(ballCollisionShape, 1);
+            //!!! Important
+            control.setApplyPhysicsLocal(true);
+            poiGeom.addControl(control);
+            float x = (random.nextFloat() - 0.5f) * 100;
+            float y = (random.nextFloat() - 0.5f) * 100;
+            float z = (random.nextFloat() - 0.5f) * 100;
+            control.setLinearVelocity(new Vector3f(x, y, z));
+            return poiGeom;
+        }
+
+        private void cleanupData() {
+            physicsState.stopPhysics();
+            //TODO: remove all objects?
+            physicsState = null;
+            rootNode = null;
+        }
+
+        @Override
+        public void initialize(AppStateManager stateManager, Application app) {
+            super.initialize(stateManager, app);
+            //only generate data and attach node when we are actually attached (or picking)
+            initData();
+            application = (SimpleApplication) app;
+            application.getRootNode().attachChild(getRootNode());
+            application.getStateManager().attach(physicsState);
+        }
+
+        @Override
+        public void cleanup() {
+            super.cleanup();
+            //detach everything when we are detached
+            application.getRootNode().detachChild(rootNode);
+            application.getStateManager().detach(physicsState);
+            cleanupData();
+        }
+
+        public Node getRootNode() {
+            if (rootNode == null) {
+                rootNode = new Node("ZoomLevel");
+                if (parent != null) {
+                    rootNode.setLocalScale(1.0f / poiRadius);
+                }
+            }
+            return rootNode;
+        }
+
+        public Vector3f getPositionInParent() {
+            return inParentPosition;
+        }
+
+        public Vector3f getPlayerPosition() {
+            if (playerPos == null) {
+                playerPos = new Vector3f();
+            }
+            return playerPos;
+        }
+
+        public void setPlayerPosition(Vector3f vec) {
+            if (playerPos == null) {
+                playerPos = new Vector3f();
+            }
+            playerPos.set(vec);
+        }
+
+        public void move(Vector3f dir) {
+            if (playerPos == null) {
+                playerPos = new Vector3f();
+            }
+            playerPos.addLocal(dir);
+        }
+
+        public float getCurrentScaleAmount() {
+            return curScaleAmount;
+        }
+
+        public InceptionLevel getParent() {
+            return parent;
+        }
+
+        public InceptionLevel getCurrentLevel() {
+            return currentReturnLevel;
+        }
+
+        public String getCoordinates() {
+            InceptionLevel cur = this;
+            StringBuilder stringBuilder = new StringBuilder();
+            stringBuilder.insert(0, this.getPlayerPosition());
+            stringBuilder.insert(0, this.getPositionInParent() + " / ");
+            cur = cur.getParent();
+            while (cur != null) {
+                stringBuilder.insert(0, cur.getPositionInParent() + " / ");
+                cur = cur.getParent();
+            }
+            return stringBuilder.toString();
+        }
+    }
+
+    public static Vector3f[] getPositions(int count, long seed) {
+        Random rnd = new Random(seed);
+        Vector3f[] vectors = new Vector3f[count];
+        for (int i = 0; i < count; i++) {
+            vectors[i] = new Vector3f((rnd.nextFloat() - 0.5f) * poiRadius,
+                    (rnd.nextFloat() - 0.5f) * poiRadius,
+                    (rnd.nextFloat() - 0.5f) * poiRadius);
+        }
+        return vectors;
+    }
+
+    /**
+     * Maps a value from 0-1 to a range from min to max.
+     *
+     * @param x
+     * @param min
+     * @param max
+     * @return the mapped value
+     */
+    private static float mapValue(float x, float min, float max) {
+        return mapValue(x, 0, 1, min, max);
+    }
+
+    /**
+     * Maps a value from inputMin to inputMax to a range from min to max.
+     *
+     * @param x
+     * @param inputMin
+     * @param inputMax
+     * @param min
+     * @param max
+     * @return the mapped value
+     */
+    private static float mapValue(float x, float inputMin, float inputMax, float min, float max) {
+        return (x - inputMin) * (max - min) / (inputMax - inputMin) + min;
+    }
+}

+ 35 - 0
jme3-examples/src/main/java/jme3test/games/package-info.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 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.
+ */
+/**
+ * simple example games
+ */
+package jme3test.games;

+ 134 - 0
jme3-examples/src/main/java/jme3test/gui/TestBitmapFont.java

@@ -0,0 +1,134 @@
+/*
+ * 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 jme3test.gui;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.font.BitmapFont;
+import com.jme3.font.BitmapText;
+import com.jme3.font.LineWrapMode;
+import com.jme3.font.Rectangle;
+import com.jme3.input.KeyInput;
+import com.jme3.input.RawInputListener;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.input.event.*;
+
+public class TestBitmapFont extends SimpleApplication {
+
+    private String txtB =
+    "ABCDEFGHIKLMNOPQRSTUVWXYZ1234567 890`~!@#$%^&*()-=_+[]\\;',./{}|:<>?";
+
+    private BitmapText txt;
+    private BitmapText txt3;
+
+    public static void main(String[] args){
+        TestBitmapFont app = new TestBitmapFont();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        inputManager.addMapping("WordWrap", new KeyTrigger(KeyInput.KEY_TAB));
+        inputManager.addListener(keyListener, "WordWrap");
+        inputManager.addRawInputListener(textListener);
+
+        BitmapFont fnt = assetManager.loadFont("Interface/Fonts/Default.fnt");
+        txt = new BitmapText(fnt);
+        txt.setBox(new Rectangle(0, 0, settings.getWidth(), settings.getHeight()));
+        txt.setSize(fnt.getPreferredSize() * 2f);
+        txt.setText(txtB);
+        txt.setLocalTranslation(0, txt.getHeight(), 0);
+        guiNode.attachChild(txt);
+
+        BitmapText txt2 = new BitmapText(fnt);
+        txt2.setSize(fnt.getPreferredSize() * 1.2f);
+        txt2.setText("Text without restriction. \nText without restriction. Text without restriction. Text without restriction");
+        txt2.setLocalTranslation(0, txt2.getHeight(), 0);
+        guiNode.attachChild(txt2);
+
+        txt3 = new BitmapText(fnt);
+        txt3.setBox(new Rectangle(0, 0, settings.getWidth(), 0));
+        txt3.setText("Press Tab to toggle word-wrap. type text and enter to input text");
+        txt3.setLocalTranslation(0, settings.getHeight()/2, 0);
+        guiNode.attachChild(txt3);
+    }
+
+    private ActionListener keyListener = new ActionListener() {
+        @Override
+        public void onAction(String name, boolean isPressed, float tpf) {
+            if (name.equals("WordWrap") && !isPressed) {
+                txt.setLineWrapMode( txt.getLineWrapMode() == LineWrapMode.Word ?
+                                        LineWrapMode.NoWrap : LineWrapMode.Word );
+            }
+        }
+    };
+
+    final private RawInputListener textListener = new RawInputListener() {
+        final private StringBuilder str = new StringBuilder();
+
+        @Override
+        public void onMouseMotionEvent(MouseMotionEvent evt) { }
+
+        @Override
+        public void onMouseButtonEvent(MouseButtonEvent evt) { }
+
+        @Override
+        public void onKeyEvent(KeyInputEvent evt) {
+            if (evt.isReleased())
+                return;
+            if (evt.getKeyChar() == '\n' || evt.getKeyChar() == '\r') {
+                txt3.setText(str.toString());
+                str.setLength(0);
+            } else {
+                str.append(evt.getKeyChar());
+            }
+        }
+
+        @Override
+        public void onJoyButtonEvent(JoyButtonEvent evt) { }
+
+        @Override
+        public void onJoyAxisEvent(JoyAxisEvent evt) { }
+
+        @Override
+        public void endInput() { }
+
+        @Override
+        public void beginInput() { }
+
+        @Override
+        public void onTouchEvent(TouchEvent evt) { }
+
+    };
+
+}

+ 144 - 0
jme3-examples/src/main/java/jme3test/gui/TestBitmapFontAlignment.java

@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2009-2019 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.gui;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.font.BitmapFont;
+import com.jme3.font.BitmapText;
+import com.jme3.font.Rectangle;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Quad;
+
+public class TestBitmapFontAlignment extends SimpleApplication {
+
+    public static void main(String[] args) {
+        TestBitmapFontAlignment test = new TestBitmapFontAlignment();
+        test.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        int width = getCamera().getWidth();
+        int height = getCamera().getHeight();
+
+        // VAlign.Top
+        BitmapText labelAlignTop = guiFont.createLabel("This text has VAlign.Top.");
+        Rectangle textboxAlignTop = new Rectangle(width * 0.2f, height * 0.7f, 120, 120);
+        labelAlignTop.setBox(textboxAlignTop);
+        labelAlignTop.setVerticalAlignment(BitmapFont.VAlign.Top);
+        getGuiNode().attachChild(labelAlignTop);
+
+        Geometry backgroundBoxAlignTop = new Geometry("", new Quad(textboxAlignTop.width, -textboxAlignTop.height));
+        Material material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        material.setColor("Color", ColorRGBA.Blue);
+        backgroundBoxAlignTop.setMaterial(material);
+        backgroundBoxAlignTop.setLocalTranslation(textboxAlignTop.x, textboxAlignTop.y, -1);
+        getGuiNode().attachChild(backgroundBoxAlignTop);
+
+        // VAlign.Center
+        BitmapText labelAlignCenter = guiFont.createLabel("This text has VAlign.Center");
+        Rectangle textboxAlignCenter = new Rectangle(width * 0.4f, height * 0.7f, 120, 120);
+        labelAlignCenter.setBox(textboxAlignCenter);
+        labelAlignCenter.setVerticalAlignment(BitmapFont.VAlign.Center);
+        getGuiNode().attachChild(labelAlignCenter);
+
+        Geometry backgroundBoxAlignCenter = backgroundBoxAlignTop.clone(false);
+        backgroundBoxAlignCenter.setLocalTranslation(textboxAlignCenter.x, textboxAlignCenter.y, -1);
+        getGuiNode().attachChild(backgroundBoxAlignCenter);
+
+        // VAlign.Bottom
+        BitmapText labelAlignBottom = guiFont.createLabel("This text has VAlign.Bottom");
+        Rectangle textboxAlignBottom = new Rectangle(width * 0.6f, height * 0.7f, 120, 120);
+        labelAlignBottom.setBox(textboxAlignBottom);
+        labelAlignBottom.setVerticalAlignment(BitmapFont.VAlign.Bottom);
+        getGuiNode().attachChild(labelAlignBottom);
+
+        Geometry backgroundBoxAlignBottom = backgroundBoxAlignTop.clone(false);
+        backgroundBoxAlignBottom.setLocalTranslation(textboxAlignBottom.x, textboxAlignBottom.y, -1);
+        getGuiNode().attachChild(backgroundBoxAlignBottom);
+
+        // VAlign.Top + Align.Right
+        BitmapText labelAlignTopRight = guiFont.createLabel("This text has VAlign.Top and Align.Right");
+        Rectangle textboxAlignTopRight = new Rectangle(width * 0.2f, height * 0.3f, 120, 120);
+        labelAlignTopRight.setBox(textboxAlignTopRight);
+        labelAlignTopRight.setVerticalAlignment(BitmapFont.VAlign.Top);
+        labelAlignTopRight.setAlignment(BitmapFont.Align.Right);
+        getGuiNode().attachChild(labelAlignTopRight);
+
+        Geometry backgroundBoxAlignTopRight = backgroundBoxAlignTop.clone(false);
+        backgroundBoxAlignTopRight.setLocalTranslation(textboxAlignTopRight.x, textboxAlignTopRight.y, -1);
+        getGuiNode().attachChild(backgroundBoxAlignTopRight);
+
+        // VAlign.Center + Align.Center
+        BitmapText labelAlignCenterCenter = guiFont.createLabel("This text has VAlign.Center and Align.Center");
+        Rectangle textboxAlignCenterCenter = new Rectangle(width * 0.4f, height * 0.3f, 120, 120);
+        labelAlignCenterCenter.setBox(textboxAlignCenterCenter);
+        labelAlignCenterCenter.setVerticalAlignment(BitmapFont.VAlign.Center);
+        labelAlignCenterCenter.setAlignment(BitmapFont.Align.Center);
+        getGuiNode().attachChild(labelAlignCenterCenter);
+
+        Geometry backgroundBoxAlignCenterCenter = backgroundBoxAlignCenter.clone(false);
+        backgroundBoxAlignCenterCenter.setLocalTranslation(textboxAlignCenterCenter.x, textboxAlignCenterCenter.y, -1);
+        getGuiNode().attachChild(backgroundBoxAlignCenterCenter);
+
+        // VAlign.Bottom + Align.Left
+        BitmapText labelAlignBottomLeft = guiFont.createLabel("This text has VAlign.Bottom and Align.Left");
+        Rectangle textboxAlignBottomLeft = new Rectangle(width * 0.6f, height * 0.3f, 120, 120);
+        labelAlignBottomLeft.setBox(textboxAlignBottomLeft);
+        labelAlignBottomLeft.setVerticalAlignment(BitmapFont.VAlign.Bottom);
+        labelAlignBottomLeft.setAlignment(BitmapFont.Align.Left);
+        getGuiNode().attachChild(labelAlignBottomLeft);
+
+        Geometry backgroundBoxAlignBottomLeft = backgroundBoxAlignBottom.clone(false);
+        backgroundBoxAlignBottomLeft.setLocalTranslation(textboxAlignBottomLeft.x, textboxAlignBottomLeft.y, -1);
+        getGuiNode().attachChild(backgroundBoxAlignBottomLeft);
+
+        // Large quad with VAlign.Center and Align.Center
+        BitmapText label = guiFont.createLabel("This text is centered, both horizontally and vertically.");
+        Rectangle box = new Rectangle(width * 0.05f, height * 0.95f, width * 0.9f, height * 0.1f);
+        label.setBox(box);
+        label.setAlignment(BitmapFont.Align.Center);
+        label.setVerticalAlignment(BitmapFont.VAlign.Center);
+        getGuiNode().attachChild(label);
+
+        Geometry background = new Geometry("background", new Quad(box.width, -box.height));
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat.setColor("Color", ColorRGBA.Green);
+        background.setMaterial(mat);
+        background.setLocalTranslation(box.x, box.y, -1);
+        getGuiNode().attachChild(background);
+    }
+
+}

+ 543 - 0
jme3-examples/src/main/java/jme3test/gui/TestBitmapFontLayout.java

@@ -0,0 +1,543 @@
+/*
+ * Copyright (c) 2018-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:
+ * 
+ * 1. Redistributions of source code must retain the above copyright 
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright 
+ *    notice, this list of conditions and the following disclaimer in 
+ *    the documentation and/or other materials provided with the 
+ *    distribution.
+ * 
+ * 3. Neither the name of the copyright holder nor the names of its 
+ *    contributors may be used to endorse or promote products derived 
+ *    from this software without specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+ 
+package jme3test.gui;
+
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.FontFormatException;
+import java.awt.FontMetrics;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.util.*;
+
+import com.jme3.app.DebugKeysAppState;
+import com.jme3.app.StatsAppState;
+import com.jme3.app.SimpleApplication;
+import com.jme3.app.state.ScreenshotAppState; 
+import com.jme3.bounding.BoundingBox;
+import com.jme3.font.BitmapCharacterSet;
+import com.jme3.font.BitmapFont;
+import com.jme3.font.BitmapText;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.*;
+import com.jme3.scene.debug.WireBox;
+import com.jme3.scene.shape.*;
+import com.jme3.texture.Image;
+import com.jme3.texture.Texture;
+import com.jme3.texture.Texture2D;
+import com.jme3.texture.plugins.AWTLoader;
+
+/**
+ *
+ * @author pspeed42
+ */
+public class TestBitmapFontLayout extends SimpleApplication {
+ 
+    public static final String SCROLL_UP = "scroll up";
+    public static final String SCROLL_DOWN = "scroll down";
+    public static final String SCROLL_LEFT = "scroll left";
+    public static final String SCROLL_RIGHT = "scroll right";
+    public static final String ZOOM_IN = "zoom in";
+    public static final String ZOOM_OUT = "zoom out";
+    public static final String RESET_ZOOM = "reset zoom";
+    public static final String RESET_VIEW = "reset view";
+ 
+    public static final float ZOOM_SPEED = 0.1f;
+    public static final float SCROLL_SPEED = 50;
+ 
+    final private Node testRoot = new Node("test root");
+    final private Node scrollRoot = new Node("scroll root");
+    final private Vector3f scroll = new Vector3f(0, 0, 0);
+    final private Vector3f zoom = new Vector3f(0, 0, 0);
+    
+    public static void main(String[] args){
+        TestBitmapFontLayout app = new TestBitmapFontLayout();
+        app.start();
+    }
+
+    public TestBitmapFontLayout() {
+        super(new StatsAppState(), 
+              new DebugKeysAppState(),
+              new ScreenshotAppState("", System.currentTimeMillis()));
+    }
+
+    public static Font loadTtf( String resource ) {
+        try {
+            return Font.createFont(Font.TRUETYPE_FONT, 
+                                   TestBitmapFontLayout.class.getResourceAsStream(resource));            
+        } catch( FontFormatException | IOException e ) {
+            throw new RuntimeException("Error loading resource:" + resource, e);
+        }
+    }
+    
+    private Texture renderAwtFont( TestConfig test, int width, int height, BitmapFont bitmapFont ) {
+ 
+        BitmapCharacterSet charset = bitmapFont.getCharSet();
+          
+        // Create an image at least as big as our JME text
+        System.out.println("Creating image size:" + width + ", " + height);        
+        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+        Graphics2D g2 = (Graphics2D)image.getGraphics();
+ 
+        g2.setColor(Color.lightGray);
+        g2.fillRect(0, 0, width, height);
+        g2.setColor(Color.cyan);
+        g2.drawRect(0, 0, width, height);
+                
+        g2.setColor(new Color(0, 0, 128));
+        //g2.drawLine(0, 0, 50, 50);
+        //g2.drawLine(xFont, yFont, xFont + 30, yFont);
+        //g2.drawLine(xFont, yFont, xFont, yFont + 30);
+ 
+        //g2.drawString("Testing", 0, 10);
+ 
+        Font font = test.awtFont;
+        System.out.println("Java font:" + font);
+                
+        float size = font.getSize2D();
+        FontMetrics fm = g2.getFontMetrics(font);
+        System.out.println("Java font metrics:" + fm);
+        
+        String[] lines = test.text.split("\n");
+ 
+        g2.setFont(font);
+        g2.setRenderingHint(
+                RenderingHints.KEY_TEXT_ANTIALIASING,
+                RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+        
+        int y = fm.getLeading() + fm.getMaxAscent();
+        for( String s : lines ) {        
+            g2.drawString(s, 0, y);            
+            y += fm.getHeight();
+        }
+        
+        g2.dispose();
+        
+        Image jmeImage = new AWTLoader().load(image, true);
+        return new Texture2D(jmeImage);
+    }
+    
+    private Node createVisual( TestConfig test ) {
+        Node result = new Node(test.name);
+        
+        // For reasons I have trouble articulating, I want the visual's 0,0,0 to be
+        // the same as the JME rendered text.  All other things will then be positioned relative
+        // to that.
+        // JME BitmapText (currently) renders from what it thinks the top of the letter is
+        // down.  The actual bitmap text bounds may extend upwards... so we need to account
+        // for that in any labeling we add above it.
+        // Thus we add and set up the main test text first.
+
+        BitmapFont bitmapFont = assetManager.loadFont(test.jmeFont);
+        BitmapCharacterSet charset = bitmapFont.getCharSet();
+        
+        System.out.println("Test name:" + test.name);
+        System.out.println("Charset line height:" + charset.getLineHeight());
+        System.out.println("    base:" + charset.getBase());
+        System.out.println("    rendered size:" + charset.getRenderedSize());
+        System.out.println("    width:" + charset.getWidth() + " height:" + charset.getHeight());
+        
+        BitmapText bitmapText = new BitmapText(bitmapFont);
+        bitmapText.setText(test.text);
+        bitmapText.setColor(ColorRGBA.Black); 
+        result.attachChild(bitmapText);
+        
+        // And force it to update because BitmapText builds itself lazily.
+        result.updateLogicalState(0.1f);        
+        BoundingBox bb = (BoundingBox)bitmapText.getWorldBound(); 
+               
+        BitmapText label = new BitmapText(assetManager.loadFont("Interface/Fonts/Default.fnt"));
+        label.setText("Test:" + test.name);
+        // Move the label up by its own size plus whatever extra headspace
+        // that the test text might have... plus a couple pixels of padding.
+        float yOffset = Math.max(0, bb.getCenter().y + bb.getYExtent()); 
+        label.move(0, label.getSize() + yOffset + 2, 0);
+        label.setColor(new ColorRGBA(0, 0.2f, 0, 1f));
+        result.attachChild(label);
+        
+
+        // Bitmap text won't update itself automatically... it's lazy.
+        // That means it won't be able to tell us its bounding volume, etc... so
+        // we'll force it to update.
+        result.updateLogicalState(0.1f);        
+ 
+        // Add a bounding box visual
+        WireBox box = new WireBox(bb.getXExtent(), bb.getYExtent(), bb.getZExtent());
+        Geometry geom = new Geometry(test.name + " bounds", box);
+        geom.setMaterial(new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"));
+        geom.getMaterial().setColor("Color", ColorRGBA.Red);
+        geom.setLocalTranslation(bb.getCenter());        
+        result.attachChild(geom);
+ 
+        // Add a box to show 0,0 + font size
+        float size = bitmapText.getLineHeight() * 0.5f; 
+        box = new WireBox(size, size, 0);
+        geom = new Geometry(test.name + " metric", box);
+        geom.setMaterial(new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"));
+        geom.getMaterial().setColor("Color", ColorRGBA.Blue);
+        geom.setLocalTranslation(size, -size, 0);
+        result.attachChild(geom); 
+
+        float yBaseline = -charset.getBase();
+        Line line = new Line(new Vector3f(0, yBaseline, 0), new Vector3f(50, yBaseline, 0));
+        geom = new Geometry(test.name + " base", line);
+        geom.setMaterial(new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"));
+        geom.getMaterial().setColor("Color", ColorRGBA.Green);
+        result.attachChild(geom); 
+         
+        System.out.println("text bb:" + bb);
+        // We want the width and the height to cover the whole potential area
+        // for the font.  So it can't just be the rendered bounds but must encompass
+        // the whole abstract font space... 0, 0, to center + extents.
+        //int width = (int)Math.round(bb.getCenter().x + bb.getXExtent());
+        //int height = (int)Math.round(-bb.getCenter().y + bb.getYExtent());
+        // No, that's not right either because in case like this:
+        // text bb:BoundingBox [Center: (142.0, -15.5, 0.0)  xExtent: 142.0  yExtent: 20.5  zExtent: 0.0]
+        // We get:
+        // Creating image size:284, 36
+        // ...when it should be at least 41 high.
+        float x1 = bb.getCenter().x - bb.getXExtent();
+        float x2 = bb.getCenter().x + bb.getXExtent();
+        float y1 = bb.getCenter().y - bb.getYExtent();
+        float y2 = bb.getCenter().y + bb.getYExtent();
+        System.out.println("xy1:" + x1 + ", " + y1 + "  xy2:" + x2 + ", " + y2);
+        int width = Math.round(x2 - Math.min(0, x1));
+        int height = Math.round(y2 - Math.min(0, y1)); 
+        
+        Texture awtText = renderAwtFont(test, width, height, bitmapFont);
+        Quad quad = new Quad(width, height);
+        geom = new Geometry(test.name + " awt1", quad);
+        geom.setMaterial(new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"));
+        geom.getMaterial().setTexture("ColorMap", awtText);
+        // Quads render from the lower left corner up
+        geom.move(0, -height, 0);
+        
+        // That quad is now positioned directly over where the bitmap text is.
+        // We'll clone it and move one right and one down
+        Geometry right = geom.clone();
+        right.move(width, 0, 0);
+        result.attachChild(right);
+        
+        Geometry down = geom.clone();
+        down.move(0, bb.getCenter().y - bb.getYExtent() - 1, 0);
+        result.attachChild(down);                        
+        
+        return result;
+    }
+
+    @Override
+    public void simpleInitApp() {
+        setPauseOnLostFocus(false);
+        setDisplayStatView(false);
+        setDisplayFps(false);
+        viewPort.setBackgroundColor(ColorRGBA.LightGray);
+ 
+        setupTestScene();
+        setupUserInput();
+ 
+        setupInstructionsNote();
+    }
+    
+    protected void setupInstructionsNote() {
+        // Add some instructional text
+        String instructions = "WASD/Cursor Keys = scroll\n"
+                            + "+/- = zoom\n"
+                            + "space = reset view\n"
+                            + "0 = reset zoom\n";
+        BitmapText note = new BitmapText(guiFont);
+        note.setText(instructions);
+        note.setColor(new ColorRGBA(0, 0.3f, 0, 1));
+        note.updateLogicalState(0.1f);
+        
+        BoundingBox bb = (BoundingBox)note.getWorldBound();        
+        
+        note.setLocalTranslation(cam.getWidth() - bb.getXExtent() * 2 - 20, 
+                                 cam.getHeight() - 20, 10);                
+        
+        guiNode.attachChild(note);
+        
+        BitmapText note2 = note.clone();
+        note2.setColor(ColorRGBA.Black);
+        note2.move(1, -1, -2);
+        guiNode.attachChild(note2);
+
+        BitmapText note3 = note.clone();
+        note3.setColor(ColorRGBA.White);
+        note3.move(-1, 1, -1);
+        guiNode.attachChild(note3);
+        
+    }
+
+    protected void setupTestScene() {  
+        String fox = "The quick brown fox jumps over the lazy dog.";
+        String loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"; 
+        String foxIpsum = fox + "\n" + loremIpsum;
+        
+        List<TestConfig> tests = new ArrayList<>();
+ 
+
+        // Note: for some Java fonts we reduce the point size to more closely
+        // match the pixel size... other than the Java-rendered fonts from Hiero, it will never
+        // be exact because of different font engines. 
+        tests.add(new TestConfig("Hiero Java FreeSerif-16-Italic", 
+                                 foxIpsum,
+                                 "jme3test/font/FreeSerif16I.fnt",
+                                 loadTtf("/jme3test/font/FreeSerif.ttf").deriveFont(Font.ITALIC, 16f)));                
+
+        tests.add(new TestConfig("Hiero FreeType FreeSerif-16-Italic", 
+                                 foxIpsum,
+                                 "jme3test/font/FT-FreeSerif16I.fnt",
+                                 loadTtf("/jme3test/font/FreeSerif.ttf").deriveFont(Font.ITALIC, 14f)));                
+
+        tests.add(new TestConfig("Hiero Native FreeSerif-16-Italic", 
+                                 foxIpsum,
+                                 "jme3test/font/Native-FreeSerif16I.fnt",
+                                 loadTtf("/jme3test/font/FreeSerif.ttf").deriveFont(Font.ITALIC, 15f)));                
+
+        tests.add(new TestConfig("AngelCode FreeSerif-16-Italic", 
+                                 foxIpsum,
+                                 "jme3test/font/BM-FreeSerif16I.fnt",
+                                 loadTtf("/jme3test/font/FreeSerif.ttf").deriveFont(Font.ITALIC, 12f)));
+                                 // It's actually between 12 and 13 but Java rounds up.
+                                                 
+        tests.add(new TestConfig("Hiero Padded FreeSerif-16-Italic", 
+                                 foxIpsum,
+                                 "jme3test/font/FreeSerif16Ipad5555.fnt",
+                                 loadTtf("/jme3test/font/FreeSerif.ttf").deriveFont(Font.ITALIC, 16f)));
+
+        tests.add(new TestConfig("AngelCode Padded FreeSerif-16-Italic", 
+                                 foxIpsum,
+                                 "jme3test/font/BM-FreeSerif16Ipad5555.fnt",
+                                 loadTtf("/jme3test/font/FreeSerif.ttf").deriveFont(Font.ITALIC, 12f)));                
+                                 // It's actually between 12 and 13 but Java rounds up.
+
+        tests.add(new TestConfig("Hiero FreeSerif-32", 
+                                 foxIpsum,
+                                 "jme3test/font/FreeSerif32.fnt",
+                                 loadTtf("/jme3test/font/FreeSerif.ttf").deriveFont(32f)));                
+
+        tests.add(new TestConfig("AngelCode FreeSerif-32", 
+                                 foxIpsum,
+                                 "jme3test/font/BM-FreeSerif32.fnt",
+                                 loadTtf("/jme3test/font/FreeSerif.ttf").deriveFont(25f)));                
+
+        tests.add(new TestConfig("Hiero FreeSerif-64-Italic", 
+                                 foxIpsum,
+                                 "jme3test/font/FreeSerif64I.fnt",
+                                 loadTtf("/jme3test/font/FreeSerif.ttf").deriveFont(Font.ITALIC, 64f)));                
+
+        tests.add(new TestConfig("AngelCode FreeSerif-64-Italic", 
+                                 foxIpsum,
+                                 "jme3test/font/BM-FreeSerif64I.fnt",
+                                 loadTtf("/jme3test/font/FreeSerif.ttf").deriveFont(Font.ITALIC, 50f)));                
+
+
+        /*tests.add(new TestConfig("Japanese", 
+                                 "\u3042\u3047\u3070\u3090\u309E\u3067\u308A\u3089\n"+
+                                 "\u3042\u3047\u3070\u3090\u309E\u3067\u308A\u3089",
+                                 "jme3test/font/DJapaSubset.fnt",
+                                 loadTtf("/jme3test/font/DroidSansFallback.ttf").deriveFont(32f)));*/
+
+        /*tests.add(new TestConfig("DroidSansMono-32",
+                                 "Ă㥹ĔĕĖėχψωӮӯ₴₵₹\n"+
+                                 "Ă㥹ĔĕĖėχψωӮӯ₴₵₹",
+                                 "jme3test/font/DMono32BI.fnt",
+                                 loadTtf("/jme3test/font/DroidSansMono.ttf").deriveFont(32f)));*/
+
+        /*tests.add(new TestConfig("DroidSansMono-32",
+                                 "Ă㥹ĔĕĖėχψωӮӯ\n"+
+                                 "Ă㥹ĔĕĖėχψωӮӯ",
+                                 "jme3test/font/DMono32BI.fnt",
+                                 loadTtf("/jme3test/font/DroidSansMono.ttf").deriveFont(Font.BOLD | Font.ITALIC, 32f)));
+        */                                         
+
+        // Set up the test root node so that y = 0 is the top of the screen
+        testRoot.setLocalTranslation(0, cam.getHeight(), 0);
+        testRoot.attachChild(scrollRoot);
+        guiNode.attachChild(testRoot);
+
+        float y = 0; //cam.getHeight();
+        
+        for( TestConfig test : tests ) {
+            System.out.println("y:" + y);
+                    
+            Node vis = createVisual(test);
+            
+            BoundingBox bb = (BoundingBox)vis.getWorldBound();
+            System.out.println("bb:" + bb); 
+ 
+            // Render it relative to y, projecting down
+            vis.setLocalTranslation(1 + bb.getCenter().x - bb.getXExtent(), 
+                                    y - bb.getCenter().y - bb.getYExtent(), 
+                                    0);
+            //vis.setLocalTranslation(1, y, 0);                                    
+            scrollRoot.attachChild(vis);
+            
+            // Position to render the next one
+            y -= bb.getYExtent() * 2;
+            
+            // plus 5 pixels of padding
+            y -= 5;            
+        }
+    }        
+ 
+    protected void resetZoom() {
+        testRoot.setLocalScale(1, 1, 1);
+    }
+    
+    protected void resetView() {
+        resetZoom();
+        scrollRoot.setLocalTranslation(0, 0, 0); 
+    }
+    
+    protected void setupUserInput() {
+
+        inputManager.addMapping(SCROLL_UP, new KeyTrigger(KeyInput.KEY_UP),
+                                           new KeyTrigger(KeyInput.KEY_W));
+        inputManager.addMapping(SCROLL_DOWN, new KeyTrigger(KeyInput.KEY_DOWN),
+                                           new KeyTrigger(KeyInput.KEY_S));
+        inputManager.addMapping(SCROLL_LEFT, new KeyTrigger(KeyInput.KEY_LEFT),
+                                           new KeyTrigger(KeyInput.KEY_A));
+        inputManager.addMapping(SCROLL_RIGHT, new KeyTrigger(KeyInput.KEY_RIGHT),
+                                           new KeyTrigger(KeyInput.KEY_D));
+        inputManager.addMapping(ZOOM_IN, new KeyTrigger(KeyInput.KEY_ADD), 
+                                         new KeyTrigger(KeyInput.KEY_EQUALS),
+                                         new KeyTrigger(KeyInput.KEY_Q));
+        inputManager.addMapping(ZOOM_OUT, new KeyTrigger(KeyInput.KEY_MINUS),
+                                         new KeyTrigger(KeyInput.KEY_SUBTRACT),
+                                         new KeyTrigger(KeyInput.KEY_Z));
+        inputManager.addMapping(RESET_VIEW, new KeyTrigger(KeyInput.KEY_SPACE));
+        inputManager.addMapping(RESET_ZOOM, new KeyTrigger(KeyInput.KEY_0));
+
+        inputManager.addListener(new KeyStateListener(), 
+                                 RESET_VIEW, RESET_ZOOM,
+                                 SCROLL_UP, SCROLL_DOWN, SCROLL_LEFT, SCROLL_RIGHT,
+                                 ZOOM_IN, ZOOM_OUT);            
+    }
+ 
+    @Override
+    public void simpleUpdate( float tpf ) {
+        if( scroll.lengthSquared() != 0 ) {
+            scrollRoot.move(scroll.mult(tpf));
+        }
+        if( zoom.lengthSquared() != 0 ) {
+            Vector3f current = testRoot.getLocalScale();
+            testRoot.setLocalScale(current.add(zoom.mult(tpf)));
+        }
+    }
+ 
+    private class KeyStateListener implements ActionListener {  
+        @Override
+        public void onAction(String name, boolean value, float tpf) {
+            switch( name ) {
+                case RESET_VIEW:
+                    // Only on the up 
+                    if( !value ) {
+                        resetView();
+                    }                
+                    break;
+                case RESET_ZOOM:
+                    // Only on the up 
+                    if( !value ) {
+                        resetZoom();
+                    }
+                    break;
+                case ZOOM_IN:
+                    if( value ) {
+                        zoom.set(ZOOM_SPEED, ZOOM_SPEED, 0);
+                    } else {
+                        zoom.set(0, 0, 0);
+                    } 
+                    break;
+                case ZOOM_OUT:
+                    if( value ) {
+                        zoom.set(-ZOOM_SPEED, -ZOOM_SPEED, 0);
+                    } else {
+                        zoom.set(0, 0, 0);
+                    } 
+                    break;
+                case SCROLL_UP:
+                    if( value ) {
+                        scroll.set(0, -SCROLL_SPEED, 0);
+                    } else {
+                        scroll.set(0, 0, 0);
+                    } 
+                    break;
+                case SCROLL_DOWN:
+                    if( value ) {
+                        scroll.set(0, SCROLL_SPEED, 0);
+                    } else {
+                        scroll.set(0, 0, 0);
+                    } 
+                    break;
+                case SCROLL_LEFT:
+                    if( value ) {
+                        scroll.set(SCROLL_SPEED, 0, 0);
+                    } else {
+                        scroll.set(0, 0, 0);
+                    } 
+                    break;
+                case SCROLL_RIGHT:
+                    if( value ) {
+                        scroll.set(-SCROLL_SPEED, 0, 0);
+                    } else {
+                        scroll.set(0, 0, 0);
+                    } 
+                    break;
+            }
+        }
+    }
+
+    private class TestConfig {
+        String name;
+        String jmeFont;
+        Font awtFont;
+        String text;
+        
+        public TestConfig( String name, String text, String jmeFont, Font awtFont ) {
+            this.name = name;
+            this.text = text;
+            this.jmeFont = jmeFont;
+            this.awtFont = awtFont;
+        }
+    }
+}

+ 70 - 0
jme3-examples/src/main/java/jme3test/gui/TestBitmapText3D.java

@@ -0,0 +1,70 @@
+/*
+ * 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 jme3test.gui;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.font.BitmapFont;
+import com.jme3.font.BitmapText;
+import com.jme3.font.Rectangle;
+import com.jme3.renderer.queue.RenderQueue.Bucket;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Quad;
+
+public class TestBitmapText3D extends SimpleApplication {
+
+    final private String txtB =
+    "ABCDEFGHIKLMNOPQRSTUVWXYZ1234567890`~!@#$%^&*()-=_+[]\\;',./{}|:<>?";
+
+    public static void main(String[] args){
+        TestBitmapText3D app = new TestBitmapText3D();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        Quad q = new Quad(6, 3);
+        Geometry g = new Geometry("quad", q);
+        g.setLocalTranslation(0, -3, -0.0001f);
+        g.setMaterial(assetManager.loadMaterial("Common/Materials/RedColor.j3m"));
+        rootNode.attachChild(g);
+
+        BitmapFont fnt = assetManager.loadFont("Interface/Fonts/Default.fnt");
+        BitmapText txt = new BitmapText(fnt);
+        txt.setBox(new Rectangle(0, 0, 6, 3));
+        txt.setQueueBucket(Bucket.Transparent);
+        txt.setSize( 0.5f );
+        txt.setText(txtB);
+        rootNode.attachChild(txt);
+    }
+
+}

+ 77 - 0
jme3-examples/src/main/java/jme3test/gui/TestCursor.java

@@ -0,0 +1,77 @@
+package jme3test.gui;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.cursors.plugins.JmeCursor;
+import java.util.ArrayList;
+
+/**
+ * This test class demonstrate how to change cursor in jME3.
+ *
+ * NOTE: This will not work on Android as it does not support cursors.
+ *
+ * Cursor test
+ * @author MadJack
+ */
+public class TestCursor extends SimpleApplication {
+
+    final private ArrayList<JmeCursor> cursors = new ArrayList<>();
+    private long sysTime;
+    private int count = 0;
+
+    public static void main(String[] args){
+        TestCursor app = new TestCursor();
+
+        app.setShowSettings(false);
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        flyCam.setEnabled(false);
+        // We need the cursor to be visible. If it is not visible the cursor
+        // will still be "used" and loaded, you just won't see it on the screen.
+        inputManager.setCursorVisible(true);
+
+        /*
+         * To make jME3 use a custom cursor it is as simple as putting the
+         * .cur/.ico/.ani file in an asset directory. Here we use
+         * "Textures/GUI/Cursors".
+         *
+         * For the purpose of this demonstration we load 3 different cursors and add them
+         * into an array list and switch cursor every 8 seconds.
+         *
+         * The first ico has been made by Sirea and the set can be found here:
+         * http://www.rw-designer.com/icon-set/nyan-cat
+         *
+         * The second cursor has been made by Virum64 and is Public Domain.
+         * http://www.rw-designer.com/cursor-set/memes-faces-v64
+         *
+         * The animated cursor has been made by Pointer Adic and can be found here:
+         * http://www.rw-designer.com/cursor-set/monkey
+         */
+        cursors.add((JmeCursor) assetManager.loadAsset("Textures/Cursors/meme.cur"));
+        cursors.add((JmeCursor) assetManager.loadAsset("Textures/Cursors/nyancat.ico"));
+        cursors.add((JmeCursor) assetManager.loadAsset("Textures/Cursors/monkey.ani"));
+
+        sysTime = System.currentTimeMillis();
+        inputManager.setMouseCursor(cursors.get(count));
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        long currentTime = System.currentTimeMillis();
+
+        if (currentTime - sysTime > 8000) {
+            count++;
+            if (count >= cursors.size()) {
+                count = 0;
+            }
+            sysTime = currentTime;
+            // 8 seconds have passed,
+            // tell jME3 to switch to a different cursor.
+            inputManager.setMouseCursor(cursors.get(count));
+        }
+
+    }
+}
+

+ 58 - 0
jme3-examples/src/main/java/jme3test/gui/TestOrtho.java

@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2009-2020 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.gui;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.ui.Picture;
+
+public class TestOrtho extends SimpleApplication {
+
+    public static void main(String[] args){
+        TestOrtho app = new TestOrtho();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        Picture p = new Picture("Picture");
+        p.move(0, 0, -1); // make it appear behind stats view
+        p.setPosition(0, 0);
+        p.setWidth(settings.getWidth());
+        p.setHeight(settings.getHeight());
+        p.setImage(assetManager, "Interface/Logo/Monkey.png", false);
+
+        // attach geometry to orthoNode
+        guiNode.attachChild(p);
+    }
+
+}

+ 70 - 0
jme3-examples/src/main/java/jme3test/gui/TestRtlBitmapText.java

@@ -0,0 +1,70 @@
+/*
+ * 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 jme3test.gui;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.app.StatsAppState;
+import com.jme3.font.*;
+
+/**
+ * Test case for JME issue #1158: BitmapText right to left line wrapping not work
+ */
+public class TestRtlBitmapText extends SimpleApplication {
+
+    private String text = "This is a test right to left text.";
+    private BitmapFont fnt;
+    private BitmapText txt;
+
+    public static void main(String[] args) {
+        TestRtlBitmapText app = new TestRtlBitmapText();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        float x = 400;
+        float y = 500;
+        getStateManager().detach(stateManager.getState(StatsAppState.class));
+        fnt = assetManager.loadFont("Interface/Fonts/Default.fnt");
+        fnt.setRightToLeft(true);
+
+        // A right to left BitmapText
+        txt = new BitmapText(fnt);
+        txt.setBox(new Rectangle(0, 0, 150, 0));
+        txt.setLineWrapMode(LineWrapMode.Word);
+        txt.setAlignment(BitmapFont.Align.Right);
+        txt.setText(text);
+
+        txt.setLocalTranslation(x, y, 0);
+        guiNode.attachChild(txt);
+    }
+}

+ 136 - 0
jme3-examples/src/main/java/jme3test/gui/TestSoftwareMouse.java

@@ -0,0 +1,136 @@
+/*
+ * 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 jme3test.gui;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.input.RawInputListener;
+import com.jme3.input.event.*;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector2f;
+import com.jme3.system.AppSettings;
+import com.jme3.texture.Texture;
+import com.jme3.texture.Texture2D;
+import com.jme3.ui.Picture;
+
+public class TestSoftwareMouse extends SimpleApplication {
+
+    private Picture cursor;
+
+    final private RawInputListener inputListener = new RawInputListener() {
+
+        @Override
+        public void beginInput() {
+        }
+        @Override
+        public void endInput() {
+        }
+        @Override
+        public void onJoyAxisEvent(JoyAxisEvent evt) {
+        }
+        @Override
+        public void onJoyButtonEvent(JoyButtonEvent evt) {
+        }
+        @Override
+        public void onMouseMotionEvent(MouseMotionEvent evt) {
+            float x = evt.getX();
+            float y = evt.getY();
+
+            // Prevent mouse from leaving screen
+            AppSettings settings = TestSoftwareMouse.this.settings;
+            x = FastMath.clamp(x, 0, settings.getWidth());
+            y = FastMath.clamp(y, 0, settings.getHeight());
+
+            // adjust for hotspot
+            cursor.setPosition(x, y - 64);
+        }
+        @Override
+        public void onMouseButtonEvent(MouseButtonEvent evt) {
+        }
+        @Override
+        public void onKeyEvent(KeyInputEvent evt) {
+        }
+        @Override
+        public void onTouchEvent(TouchEvent evt) {
+        }
+    };
+
+    public static void main(String[] args){
+        TestSoftwareMouse app = new TestSoftwareMouse();
+
+//        AppSettings settings = new AppSettings(true);
+//        settings.setFrameRate(60);
+//        app.setSettings(settings);
+
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        flyCam.setEnabled(false);
+//        inputManager.setCursorVisible(false);
+
+        Texture tex = assetManager.loadTexture("Interface/Logo/Cursor.png");
+
+        cursor = new Picture("cursor");
+        cursor.setTexture(assetManager, (Texture2D) tex, true);
+        cursor.setWidth(64);
+        cursor.setHeight(64);
+        guiNode.attachChild(cursor);
+        /*
+         * Position the software cursor
+         * so that its upper-left corner is at the hotspot.
+         */
+        Vector2f initialPosition = inputManager.getCursorPosition();
+        cursor.setPosition(initialPosition.x, initialPosition.y - 64f);
+
+        inputManager.addRawInputListener(inputListener);
+
+//        Image img = tex.getImage();
+//        ByteBuffer data = img.getData(0);
+//        IntBuffer image = BufferUtils.createIntBuffer(64 * 64);
+//        for (int y = 0; y < 64; y++){
+//            for (int x = 0; x < 64; x++){
+//                int rgba = data.getInt();
+//                image.put(rgba);
+//            }
+//        }
+//        image.clear();
+//
+//        try {
+//            Cursor cur = new Cursor(64, 64, 2, 62, 1, image, null);
+//            Mouse.setNativeCursor(cur);
+//        } catch (LWJGLException ex) {
+//            Logger.getLogger(TestSoftwareMouse.class.getName()).log(Level.SEVERE, null, ex);
+//        }
+    }
+}

+ 64 - 0
jme3-examples/src/main/java/jme3test/gui/TestZOrder.java

@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2009-2020 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.gui;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.ui.Picture;
+
+public class TestZOrder extends SimpleApplication {
+
+    public static void main(String[] args){
+        TestZOrder app = new TestZOrder();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        Picture p = new Picture("Picture1");
+        p.move(0,0,-1);
+        p.setPosition(100, 100);
+        p.setWidth(100);
+        p.setHeight(100);
+        p.setImage(assetManager, "Interface/Logo/Monkey.png", false);
+        guiNode.attachChild(p);
+
+        Picture p2 = new Picture("Picture2");
+        p2.move(0,0,1.001f);
+        p2.setPosition(150, 150);
+        p2.setWidth(100);
+        p2.setHeight(100);
+        p2.setImage(assetManager, "Interface/Logo/Monkey.png", false);
+        guiNode.attachChild(p2);
+    }
+
+}

+ 36 - 0
jme3-examples/src/main/java/jme3test/gui/package-info.java

@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 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.
+ */
+/**
+ * example apps and non-automated tests for native graphical user-interface
+ * (GUI) support
+ */
+package jme3test.gui;

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio