فهرست منبع

Merge pull request #4 from jMonkeyEngine/master

Update fork
joliver82 6 سال پیش
والد
کامیت
05db003ab3
100فایلهای تغییر یافته به همراه5932 افزوده شده و 350 حذف شده
  1. 7 1
      .gitignore
  2. 6 8
      .travis.yml
  3. 3 2
      README.md
  4. 4 1
      appveyor.yml
  5. 4 2
      common.gradle
  6. 5 4
      gradle.properties
  7. 0 6
      jme3-android-examples/src/main/java/jme3test/android/TestAndroidResources.java
  8. 1 5
      jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/JmeFragment.java
  9. 0 1
      jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/MainActivity.java
  10. 1 2
      jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/TestActivity.java
  11. 14 6
      jme3-android-native/decode.gradle
  12. 13 6
      jme3-android-native/openalsoft.gradle
  13. 0 1
      jme3-android/src/main/java/com/jme3/app/AndroidHarness.java
  14. 5 5
      jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java
  15. 5 0
      jme3-android/src/main/java/com/jme3/app/DefaultAndroidProfiler.java
  16. 1 2
      jme3-android/src/main/java/com/jme3/app/state/VideoRecorderAppState.java
  17. 0 1
      jme3-android/src/main/java/com/jme3/asset/plugins/AndroidLocator.java
  18. 0 1
      jme3-android/src/main/java/com/jme3/input/android/AndroidGestureProcessor.java
  19. 0 1
      jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler.java
  20. 0 1
      jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler14.java
  21. 0 1
      jme3-android/src/main/java/com/jme3/input/android/AndroidTouchInput14.java
  22. 2 2
      jme3-android/src/main/java/com/jme3/renderer/android/RendererUtil.java
  23. 5 8
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderContext.java
  24. 8 6
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderLoader.java
  25. 0 2
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/Constraint.java
  26. 3 3
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/ConstraintHelper.java
  27. 5 5
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/BlenderFileException.java
  28. 3 3
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/BlenderInputStream.java
  29. 6 6
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/Field.java
  30. 1 1
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/MaterialContext.java
  31. 3 7
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/math/Matrix.java
  32. 3 3
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/math/Vector3d.java
  33. 1 1
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/Face.java
  34. 4 4
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/MeshHelper.java
  35. 6 8
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/TemporalMesh.java
  36. 1 1
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/UserUVCollection.java
  37. 2 1
      jme3-bullet-native-android/build.gradle
  38. BIN
      jme3-bullet-native-android/libs/arm64-v8a/libbulletjme.so
  39. BIN
      jme3-bullet-native-android/libs/armeabi-v7a/libbulletjme.so
  40. BIN
      jme3-bullet-native-android/libs/armeabi/libbulletjme.so
  41. BIN
      jme3-bullet-native-android/libs/mips/libbulletjme.so
  42. BIN
      jme3-bullet-native-android/libs/mips64/libbulletjme.so
  43. BIN
      jme3-bullet-native-android/libs/x86/libbulletjme.so
  44. BIN
      jme3-bullet-native-android/libs/x86_64/libbulletjme.so
  45. 13 2
      jme3-bullet-native-android/src/native/android/Android.mk
  46. 4 1
      jme3-bullet-native-android/src/native/android/Application.mk
  47. 13 1
      jme3-bullet-native/build.gradle
  48. BIN
      jme3-bullet-native/libs/native/linux/x86/libbulletjme.so
  49. BIN
      jme3-bullet-native/libs/native/linux/x86_64/libbulletjme.so
  50. BIN
      jme3-bullet-native/libs/native/osx/x86/libbulletjme.dylib
  51. BIN
      jme3-bullet-native/libs/native/osx/x86_64/libbulletjme.dylib
  52. BIN
      jme3-bullet-native/libs/native/windows/x86/bulletjme.dll
  53. BIN
      jme3-bullet-native/libs/native/windows/x86_64/bulletjme.dll
  54. 62 57
      jme3-bullet-native/src/native/cpp/com_jme3_bullet_PhysicsSpace.cpp
  55. 37 1
      jme3-bullet-native/src/native/cpp/com_jme3_bullet_collision_PhysicsCollisionObject.cpp
  56. 3 1
      jme3-bullet-native/src/native/cpp/com_jme3_bullet_collision_shapes_HullCollisionShape.cpp
  57. 1 1
      jme3-bullet-native/src/native/cpp/com_jme3_bullet_joints_HingeJoint.cpp
  58. 17 19
      jme3-bullet-native/src/native/cpp/jmePhysicsSpace.cpp
  59. 1 1
      jme3-bullet-native/src/native/cpp/jmePhysicsSpace.h
  60. 15 44
      jme3-bullet/build.gradle
  61. 493 0
      jme3-bullet/src/common/java/com/jme3/bullet/animation/BoneLink.java
  62. 565 0
      jme3-bullet/src/common/java/com/jme3/bullet/animation/DacConfiguration.java
  63. 1108 0
      jme3-bullet/src/common/java/com/jme3/bullet/animation/DacLinks.java
  64. 530 0
      jme3-bullet/src/common/java/com/jme3/bullet/animation/DynamicAnimControl.java
  65. 50 0
      jme3-bullet/src/common/java/com/jme3/bullet/animation/KinematicSubmode.java
  66. 634 0
      jme3-bullet/src/common/java/com/jme3/bullet/animation/PhysicsLink.java
  67. 707 0
      jme3-bullet/src/common/java/com/jme3/bullet/animation/RagUtils.java
  68. 56 0
      jme3-bullet/src/common/java/com/jme3/bullet/animation/RagdollCollisionListener.java
  69. 313 0
      jme3-bullet/src/common/java/com/jme3/bullet/animation/RangeOfMotion.java
  70. 486 0
      jme3-bullet/src/common/java/com/jme3/bullet/animation/TorsoLink.java
  71. 151 0
      jme3-bullet/src/common/java/com/jme3/bullet/animation/VectorSet.java
  72. 35 0
      jme3-bullet/src/common/java/com/jme3/bullet/animation/package-info.java
  73. 7 0
      jme3-bullet/src/common/java/com/jme3/bullet/control/AbstractPhysicsControl.java
  74. 8 0
      jme3-bullet/src/common/java/com/jme3/bullet/control/CharacterControl.java
  75. 7 0
      jme3-bullet/src/common/java/com/jme3/bullet/control/GhostControl.java
  76. 8 0
      jme3-bullet/src/common/java/com/jme3/bullet/control/RigidBodyControl.java
  77. 1 0
      jme3-bullet/src/common/java/com/jme3/bullet/control/VehicleControl.java
  78. 36 6
      jme3-bullet/src/common/java/com/jme3/bullet/control/ragdoll/RagdollUtils.java
  79. 24 7
      jme3-bullet/src/common/java/com/jme3/bullet/debug/BulletCharacterDebugControl.java
  80. 23 9
      jme3-bullet/src/common/java/com/jme3/bullet/debug/BulletGhostObjectDebugControl.java
  81. 22 6
      jme3-bullet/src/common/java/com/jme3/bullet/debug/BulletRigidBodyDebugControl.java
  82. 3 4
      jme3-bullet/src/main/java/com/jme3/bullet/PhysicsSpace.java
  83. 87 0
      jme3-bullet/src/main/java/com/jme3/bullet/collision/CollisionFlag.java
  84. 29 0
      jme3-bullet/src/main/java/com/jme3/bullet/collision/PhysicsCollisionObject.java
  85. 29 3
      jme3-bullet/src/main/java/com/jme3/bullet/collision/shapes/CollisionShape.java
  86. 0 1
      jme3-bullet/src/main/java/com/jme3/bullet/collision/shapes/MeshCollisionShape.java
  87. 39 11
      jme3-bullet/src/main/java/com/jme3/bullet/objects/PhysicsCharacter.java
  88. 23 1
      jme3-bullet/src/main/java/com/jme3/bullet/objects/PhysicsRigidBody.java
  89. 3 9
      jme3-bullet/src/main/java/com/jme3/bullet/util/DebugShapeFactory.java
  90. 2 2
      jme3-core/src/main/java/checkers/quals/SubtypeOf.java
  91. 1 1
      jme3-core/src/main/java/checkers/quals/package-info.java
  92. 0 2
      jme3-core/src/main/java/com/jme3/anim/AnimClip.java
  93. 110 9
      jme3-core/src/main/java/com/jme3/anim/AnimComposer.java
  94. 12 10
      jme3-core/src/main/java/com/jme3/anim/Armature.java
  95. 1 1
      jme3-core/src/main/java/com/jme3/anim/Joint.java
  96. 3 5
      jme3-core/src/main/java/com/jme3/anim/MorphTrack.java
  97. 2 2
      jme3-core/src/main/java/com/jme3/anim/SkinningControl.java
  98. 5 7
      jme3-core/src/main/java/com/jme3/anim/TransformTrack.java
  99. 31 5
      jme3-core/src/main/java/com/jme3/anim/tween/AbstractTween.java
  100. 0 3
      jme3-core/src/main/java/com/jme3/anim/tween/Tween.java

+ 7 - 1
.gitignore

@@ -2,6 +2,7 @@
 **/.classpath
 **/.settings
 **/.project
+**/.vscode
 **/out/
 /.gradle/
 /.nb-gradle/
@@ -18,12 +19,15 @@
 *.jnilib
 *.dylib
 *.iml
+*.class
+*.jtxt
 .gradletasknamecache
 .DS_Store
 /jme3-core/src/main/resources/com/jme3/system/version.properties
 /jme3-*/build/
+/jme3-*/bin/
 /jme3-bullet-native/bullet3.zip
-/jme3-bullet-native/bullet3-2.86.1/
+/jme3-bullet-native/bullet3-*/
 /jme3-bullet-native/src/native/cpp/com_jme3_bullet_*.h
 /jme3-android-native/openal-soft/
 /jme3-android-native/OpenALSoft.zip
@@ -38,9 +42,11 @@
 !/jme3-bullet-native/libs/native/osx/x86_64/libbulletjme.dylib
 !/jme3-bullet-native/libs/native/linux/x86/libbulletjme.so
 !/jme3-bullet-native/libs/native/linux/x86_64/libbulletjme.so
+/jme3-examples/private/
 !/jme3-vr/src/main/resources/**/*.dylib
 !/jme3-vr/src/main/resources/**/*.so
 !/jme3-vr/src/main/resources/**/*.so.dbg
 !/jme3-vr/src/main/resources/**/*.dll
 !/jme3-vr/src/main/resources/**/*.pdb
 /buildMaven.bat
+

+ 6 - 8
.travis.yml

@@ -4,8 +4,9 @@ sudo: true
 branches:
   only:
   - master
-  - v3.1
-  - /^v3.2.0-.*$/
+  - /^v3.3.*$/
+  - v3.2
+  - /^v3.2.*$/
 
 matrix:
   include:
@@ -13,8 +14,8 @@ matrix:
     jdk: oraclejdk8
     env: UPLOAD=true UPLOAD_NATIVE=true
   - os: linux
-    jdk: openjdk7
-    dist: precise
+    jdk: openjdk11
+    dist: xenial
   - os: osx
     osx_image: xcode9.3
     env: UPLOAD_NATIVE=true
@@ -73,7 +74,6 @@ deploy:
     repo: jMonkeyEngine/jmonkeyengine
     tags: true
 
-
 # before_install:
   # required libs for android build tools
   # sudo apt-get update
@@ -82,6 +82,4 @@ deploy:
   # newest Android NDK
   # wget http://dl.google.com/android/ndk/android-ndk-r10c-linux-x86_64.bin -O ndk.bin
   # 7z x ndk.bin -y > /dev/null
-  # export ANDROID_NDK=`pwd`/android-ndk-r10c
-
-
+  # export ANDROID_NDK=`pwd`/android-ndk-r10c

+ 3 - 2
README.md

@@ -3,7 +3,7 @@ jMonkeyEngine
 
 [![Build Status](https://travis-ci.org/jMonkeyEngine/jmonkeyengine.svg?branch=master)](https://travis-ci.org/jMonkeyEngine/jmonkeyengine)
 
-jMonkeyEngine is a 3D game engine for adventurous Java developers. It’s open-source, cross-platform, and cutting-edge. 3.2.0 is the latest stable version of the jMonkeyEngine 3 SDK, a complete game development suite. We'll release 3.2.x updates until the major 3.3 release arrives.
+jMonkeyEngine is a 3D game engine for adventurous Java developers. It’s open-source, cross-platform, and cutting-edge. 3.2.2 is the latest stable version of the jMonkeyEngine 3 SDK, a complete game development suite. We'll release 3.2.x updates until the major 3.3 release arrives.
 
 The engine is used by several commercial game studios and computer-science courses. Here's a taste:
 
@@ -15,12 +15,13 @@ The engine is used by several commercial game studios and computer-science cours
  - [Copod](http://herebeben.com/copod)
  - [Attack of the Gelatinous Blob](http://attackofthegelatinousblob.com/)
  - [Chaos](http://4realms.net/)
- - [Mythruna](https://mythruna.com/)
+ - [Mythruna](http://mythruna.com/)
  - [PirateHell](http://www.desura.com/games/piratehell)
  - [3089 (on steam)](http://store.steampowered.com/app/263360/)
  - [3079 (on steam)](http://store.steampowered.com/app/259620/)
  - [Lightspeed Frontier](http://www.lightspeedfrontier.com/)
  - [Skullstone](http://www.skullstonegame.com/)
+ - [Spoxel](https://store.steampowered.com/app/746880/Spoxel/)
 
 ## Getting started
 

+ 4 - 1
appveyor.yml

@@ -3,10 +3,13 @@ version: 1.0.{build}.{branch}
 branches:
   only:
   - master
+  - v3.2
 
 only_commits:
   files:
     - jme3-bullet-native/
+    - appveyor.yml
+    - gradle.properties
 
 skip_tags: true
 
@@ -34,7 +37,7 @@ build_script:
 cache:
 - C:\Users\appveyor\.gradle\caches
 - C:\Users\appveyor\.gradle\wrapper
-- jme3-bullet-native\bullet3.zip
+- jme3-bullet-native\bullet3.zip -> gradle.properties
 
 test: off
 deploy: off

+ 4 - 2
common.gradle

@@ -8,7 +8,7 @@ apply plugin: 'maven'
 group = 'org.jmonkeyengine'
 version = jmePomVersion
 
-sourceCompatibility = '1.7'
+sourceCompatibility = '1.8'
 [compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
 
 repositories {
@@ -24,7 +24,7 @@ repositories {
 dependencies {
     // Adding dependencies here will add the dependencies to each subproject.
     testCompile group: 'junit', name: 'junit', version: '4.12'
-    testCompile group: 'org.mockito', name: 'mockito-core', version: '2.0.28-beta'
+    testCompile group: 'org.mockito', name: 'mockito-core', version: '1.10.19'
     testCompile group: 'org.easytesting', name: 'fest-assert-core', version: '2.0M10'
 }
 
@@ -43,6 +43,8 @@ javadoc {
     options.header = "<b>jMonkeyEngine ${jmeMainVersion} ${project.name}</b>"
     options.author = "true"
     options.use = "true"
+    options.charSet = "UTF-8"
+    options.encoding = "UTF-8"
     //disable doclint for JDK8, more quiet output
     if (JavaVersion.current().isJava8Compatible()){
         options.addStringOption('Xdoclint:none', '-quiet')

+ 5 - 4
gradle.properties

@@ -5,7 +5,7 @@ jmeMainVersion = 3.3
 # Version addition pre-alpha-svn, Stable, Beta
 jmeVersionTag = SNAPSHOT
 # Increment this each time jmeVersionTag changes but jmeVersion stays the same
-jmeVersionTagID = 0
+jmeVersionTagID = 2
 
 # specify if JavaDoc should be built
 buildJavaDoc = true
@@ -16,11 +16,11 @@ buildAndroidExamples = false
 
 # Path to android NDK for building native libraries
 #ndkPath=/Users/normenhansen/Documents/Code-Import/android-ndk-r7
-ndkPath = /opt/android-ndk-r10c
+ndkPath = /opt/android-ndk-r16b
 
 # Path for downloading native Bullet
-bulletUrl = https://github.com/bulletphysics/bullet3/archive/2.86.1.zip
-bulletFolder = bullet3-2.86.1
+bulletUrl = https://github.com/bulletphysics/bullet3/archive/2.87.zip
+bulletFolder = bullet3-2.87
 bulletZipFile = bullet3.zip
 
 # POM settings
@@ -38,3 +38,4 @@ POM_INCEPTION_YEAR=2009
 # Bintray settings to override in $HOME/.gradle/gradle.properties or ENV or commandline
 bintray_user=
 bintray_api_key=
+

+ 0 - 6
jme3-android-examples/src/main/java/jme3test/android/TestAndroidResources.java

@@ -1,15 +1,9 @@
 package jme3test.android;
 
 import com.jme3.app.SimpleApplication;
-import com.jme3.light.AmbientLight;
-import com.jme3.light.PointLight;
 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;
-import com.jme3.scene.shape.Sphere;
-import com.jme3.util.TangentBinormalGenerator;
 
 /**
  * Test case to look for images stored in the Android drawable and mipmap directories.  Image files are

+ 1 - 5
jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/JmeFragment.java

@@ -1,16 +1,12 @@
 package org.jmonkeyengine.jme3androidexamples;
 
 import android.os.Bundle;
-import android.util.Log;
 import com.jme3.app.AndroidHarnessFragment;
-
 import java.util.logging.Level;
 import java.util.logging.LogManager;
 
-import static org.jmonkeyengine.jme3androidexamples.MainActivity.*;
-
 /**
- * A placeholder fragment containing a the jME GLSurfaceView.
+ * A placeholder fragment containing a jME GLSurfaceView.
  */
 public class JmeFragment extends AndroidHarnessFragment {
 

+ 0 - 1
jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/MainActivity.java

@@ -1,6 +1,5 @@
 package org.jmonkeyengine.jme3androidexamples;
 
-import android.app.Activity;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.os.Bundle;

+ 1 - 2
jme3-android-examples/src/main/java/org/jmonkeyengine/jme3androidexamples/TestActivity.java

@@ -1,9 +1,8 @@
 package org.jmonkeyengine.jme3androidexamples;
 
 import android.app.FragmentTransaction;
-import android.support.v7.app.AppCompatActivity;
 import android.os.Bundle;
-import android.util.Log;
+import android.support.v7.app.AppCompatActivity;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;

+ 14 - 6
jme3-android-native/decode.gradle

@@ -3,6 +3,7 @@ String stbiUrl = 'https://raw.githubusercontent.com/nothings/stb/master/stb_imag
 
 // Working directories for the ndk build.
 String decodeBuildDir = "${buildDir}" + File.separator + 'decode'
+String decodeClassesBuildDir = "${buildDir}" + File.separator + 'decode_classes'
 String decodeBuildJniDir = decodeBuildDir + File.separator + 'jni'
 String decodeBuildLibsDir = decodeBuildDir + File.separator + 'libs'
 
@@ -44,13 +45,20 @@ task copyTremorFiles(type: Copy) {
     into outputDir
 }
 
-// Generate headers via javah
+// Generate headers via javac -h
 task generateJavahHeaders(type: Exec) {
-    executable org.gradle.internal.jvm.Jvm.current().getExecutable('javah')
-    args '-d', decodeSourceDir
-    args '-classpath', project.projectClassPath
-    args "com.jme3.audio.plugins.NativeVorbisFile"
-    args "com.jme3.texture.plugins.AndroidNativeImageLoader"
+    def files0 = fileTree("src/main/java/").filter { it.isFile() && it.getName().endsWith(".java") }.files
+    def files1 = fileTree("src/common/java/").filter { it.isFile() && it.getName().endsWith(".java") }.files
+    def files2 = fileTree("../jme3-core/src/main/java/").filter { it.isFile() && it.getName().endsWith(".java") }.files
+    def files3 = fileTree("../jme3-core/src/plugins/java/").filter { it.isFile() && it.getName().endsWith(".java") }.files
+    def files4 = fileTree("../jme3-core/src/tools/java/").filter { it.isFile() && it.getName().endsWith(".java") }.files
+    def files5 = fileTree("../jme3-terrain/src/main/java/").filter { it.isFile() && it.getName().endsWith(".java") }.files
+    def filesList = "\"" + files0.join("\"\n\"") + "\"\n\"" + files1.join("\"\n\"") + "\"\n\"" + files2.join("\"\n\"") + "\"\n\"" + files3.join("\"\n\"") + "\"\n\"" + files4.join("\"\n\"") + "\"\n\"" + files5.join("\"\n\"") + "\""
+	new File("$projectDir/java_classes.jtxt").text = filesList.replaceAll(java.util.regex.Pattern.quote("\\"), java.util.regex.Matcher.quoteReplacement("/"))
+    executable org.gradle.internal.jvm.Jvm.current().getExecutable('javac')
+    args '-h', decodeSourceDir
+    args "@$projectDir/java_classes.jtxt"
+    args '-d', decodeClassesBuildDir
 }
 
 // Copy jME Android native files to jni directory

+ 13 - 6
jme3-android-native/openalsoft.gradle

@@ -9,6 +9,7 @@ String openALSoftFolder = 'openal-soft-e5016f8'
 
 //Working directories for the ndk build.
 String openalsoftBuildDir = "${buildDir}" + File.separator + 'openalsoft'
+String openalsoftClassesBuildDir = "${buildDir}" + File.separator + 'openalsoft_classes'
 String openalsoftBuildJniDir = openalsoftBuildDir + File.separator + 'jni'
 String openalsoftBuildLibsDir = openalsoftBuildDir + File.separator + 'libs'
 
@@ -73,12 +74,18 @@ task copyJmeOpenALSoft(type: Copy, dependsOn:copyOpenALSoft) {
 }
 
 task generateOpenAlSoftHeaders(type:Exec, dependsOn: copyJmeOpenALSoft) {
-    executable org.gradle.internal.jvm.Jvm.current().getExecutable('javah')
-    args '-d', openalsoftJmeAndroidPath
-    args '-classpath', project.projectClassPath
-    args "com.jme3.audio.android.AndroidAL"
-    args "com.jme3.audio.android.AndroidALC"
-    args "com.jme3.audio.android.AndroidEFX"
+    def files0 = fileTree("src/main/java/").filter { it.isFile() && it.getName().endsWith(".java") }.files
+    def files1 = fileTree("src/common/java/").filter { it.isFile() && it.getName().endsWith(".java") }.files
+    def files2 = fileTree("../jme3-core/src/main/java/").filter { it.isFile() && it.getName().endsWith(".java") }.files
+    def files3 = fileTree("../jme3-core/src/plugins/java/").filter { it.isFile() && it.getName().endsWith(".java") }.files
+    def files4 = fileTree("../jme3-core/src/tools/java/").filter { it.isFile() && it.getName().endsWith(".java") }.files
+    def files5 = fileTree("../jme3-terrain/src/main/java/").filter { it.isFile() && it.getName().endsWith(".java") }.files
+    def filesList = "\"" + files0.join("\"\n\"") + "\"\n\"" + files1.join("\"\n\"") + "\"\n\"" + files2.join("\"\n\"") + "\"\n\"" + files3.join("\"\n\"") + "\"\n\"" + files4.join("\"\n\"") + "\"\n\"" + files5.join("\"\n\"") + "\""
+	new File("$projectDir/java_classes.jtxt").text = filesList.replaceAll(java.util.regex.Pattern.quote("\\"), java.util.regex.Matcher.quoteReplacement("/"))
+    executable org.gradle.internal.jvm.Jvm.current().getExecutable('javac')
+    args '-h', openalsoftJmeAndroidPath
+    args "@$projectDir/java_classes.jtxt"
+    args '-d', openalsoftClassesBuildDir
 }
 
 task buildOpenAlSoftNativeLib(type: Exec, dependsOn: generateOpenAlSoftHeaders) {

+ 0 - 1
jme3-android/src/main/java/com/jme3/app/AndroidHarness.java

@@ -3,7 +3,6 @@ package com.jme3.app;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.DialogInterface;
-import android.content.pm.ActivityInfo;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.NinePatchDrawable;
 import android.opengl.GLSurfaceView;

+ 5 - 5
jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2015 jMonkeyEngine
+ * Copyright (c) 2009-2019 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -125,7 +125,7 @@ public class AndroidHarnessFragment extends Fragment implements
     /**
      * Set the maximum resolution for the surfaceview in either the
      * width or height screen direction depending on the screen size.
-     * If the surfaceview is retangular, the longest side (width or height)
+     * If the surfaceview is rectangular, the longest side (width or height)
      * will have the resolution set to a maximum of maxResolutionDimension.
      * The other direction will be set to a value that maintains the aspect
      * ratio of the surfaceview. </br>
@@ -276,17 +276,17 @@ public class AndroidHarnessFragment extends Fragment implements
     }
 
     /**
-     * Called by the system to create the View hierchy associated with this
+     * Called by the system to create the View hierarchy associated with this
      * Fragment.  For jME, this is a FrameLayout that contains the GLSurfaceView
      * and an overlaying SplashScreen Image (if used).  The View that is returned
-     * will be placed on the screen within the boundries of the View borders defined
+     * will be placed on the screen within the boundaries of the View borders defined
      * by the Activity's layout parameters for this Fragment.  For jME, we also
      * update the application reference to the new view.
      *
      * @param inflater
      * @param container
      * @param savedInstanceState
-     * @return
+     * @return the new view
      */
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

+ 5 - 0
jme3-android/src/main/java/com/jme3/app/DefaultAndroidProfiler.java

@@ -135,6 +135,11 @@ public class DefaultAndroidProfiler implements AppProfiler {
         }
     }
 
+    @Override
+    public void appSubStep(String... additionalInfo) {
+
+    }
+
     public void vpStep(VpStep vpStep, ViewPort vp, RenderQueue.Bucket bucket) {
         if (androidApiLevel >= 18) {
             switch (vpStep) {

+ 1 - 2
jme3-android/src/main/java/com/jme3/app/state/VideoRecorderAppState.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2019 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -123,7 +123,6 @@ public class VideoRecorderAppState extends AbstractAppState {
      * This constructor allows you to specify the output file of the video as well as the quality
      * @param file the video file
      * @param quality the quality of the jpegs in the video stream (0.0 smallest file - 1.0 largest file)
-     * @param framerate the frame rate of the resulting video, the application will be locked to this framerate
      */
     public VideoRecorderAppState(File file, float quality) {
         this.file = file;

+ 0 - 1
jme3-android/src/main/java/com/jme3/asset/plugins/AndroidLocator.java

@@ -6,7 +6,6 @@ import com.jme3.asset.*;
 import com.jme3.system.android.JmeAndroidSystem;
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.logging.Level;
 import java.util.logging.Logger;
 
 public class AndroidLocator implements AssetLocator {

+ 0 - 1
jme3-android/src/main/java/com/jme3/input/android/AndroidGestureProcessor.java

@@ -36,7 +36,6 @@ import android.view.GestureDetector;
 import android.view.MotionEvent;
 import android.view.ScaleGestureDetector;
 import com.jme3.input.event.TouchEvent;
-import java.util.logging.Level;
 import java.util.logging.Logger;
 
 /**

+ 0 - 1
jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler.java

@@ -42,7 +42,6 @@ import android.view.View;
 import com.jme3.input.JoyInput;
 import com.jme3.input.TouchInput;
 import com.jme3.system.AppSettings;
-import java.util.logging.Level;
 import java.util.logging.Logger;
 
 /**

+ 0 - 1
jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler14.java

@@ -37,7 +37,6 @@ import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
-import java.util.logging.Level;
 import java.util.logging.Logger;
 
 /**

+ 0 - 1
jme3-android/src/main/java/com/jme3/input/android/AndroidTouchInput14.java

@@ -36,7 +36,6 @@ import android.view.MotionEvent;
 import com.jme3.input.event.TouchEvent;
 import com.jme3.math.Vector2f;
 import java.util.HashMap;
-import java.util.logging.Level;
 import java.util.logging.Logger;
 
 /**

+ 2 - 2
jme3-android/src/main/java/com/jme3/renderer/android/RendererUtil.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2015 jMonkeyEngine
+ * Copyright (c) 2009-2019 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -38,7 +38,7 @@ import javax.microedition.khronos.egl.EGL10;
 import javax.microedition.khronos.egl.EGL11;
 
 /**
- * Utility class used by the {@link OGLESShaderRenderer renderer} and sister
+ * Utility class used by the OGLESShaderRenderer and sister
  * classes.
  *
  * @author Kirill Vainer

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

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2019 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -304,10 +304,7 @@ public class BlenderContext {
      * 
      * @param oldMemoryAddress
      *            the address of the feature
-     * @param featureName
-     *            the name of the feature
-     * @param structure
-     *            the filled structure of the feature
+     * @param featureDataType
      * @param feature
      *            the feature we want to store
      */
@@ -330,7 +327,7 @@ public class BlenderContext {
      * @param oldMemoryAddress
      *            the address of the feature
      * @param loadedFeatureDataType
-     *            the type of data we want to retreive it can be either filled
+     *            the type of data we want to retrieve it can be either filled
      *            structure or already converted feature
      * @return loaded feature or null if it was not yet loaded
      */
@@ -497,7 +494,7 @@ public class BlenderContext {
     }
 
     /**
-     * This method retreives the structure at the top of the parent's stack but
+     * This method retrieves the structure at the top of the parent's stack but
      * does not remove it.
      * 
      * @return the structure from the top of the stack
@@ -754,7 +751,7 @@ public class BlenderContext {
     }
 
     /**
-     * This enum defines what loaded data type user wants to retreive. It can be
+     * This enum defines what loaded data type user wants to retrieve. It can be
      * either filled structure or already converted data.
      * 
      * @author Marcin Roguski (Kaelthas)

+ 8 - 6
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderLoader.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2019 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -220,7 +220,7 @@ public class BlenderLoader implements AssetLoader {
         } catch (BlenderFileException e) {
             throw new IOException(e.getLocalizedMessage(), e);
         } catch (Exception e) {
-            throw new IOException("Unexpected importer exception occured: " + e.getLocalizedMessage(), e);
+            throw new IOException("Unexpected importer exception occurred: " + e.getLocalizedMessage(), e);
         } finally {
             this.clear(assetInfo);
         }
@@ -245,16 +245,18 @@ public class BlenderLoader implements AssetLoader {
                 Structure objectStructure = pObject.fetchData().get(0);
 
                 Object object = objectHelper.toObject(objectStructure, blenderContext);
-                if (object instanceof LightNode) {
-                    result.addLight(((LightNode) object).getLight());// FIXME: check if this is needed !!!
-                    result.attachChild((LightNode) object);
-                } else if (object instanceof Node) {
+                if (object instanceof Node) {
                     if (LOGGER.isLoggable(Level.FINE)) {
                         LOGGER.log(Level.FINE, "{0}: {1}--> {2}", new Object[] { ((Node) object).getName(), ((Node) object).getLocalTranslation().toString(), ((Node) object).getParent() == null ? "null" : ((Node) object).getParent().getName() });
                     }
+
                     if (((Node) object).getParent() == null) {
                         result.attachChild((Spatial) object);
                     }
+
+                    if(object instanceof LightNode) {
+                        result.addLight(((LightNode) object).getLight());
+                    }
                 }
             }
         }

+ 0 - 2
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/Constraint.java

@@ -48,8 +48,6 @@ public abstract class Constraint {
      *            the constraint's structure (bConstraint clss in blender 2.49).
      * @param ownerOMA
      *            the old memory address of the constraint owner
-     * @param ownerType
-     *            the type of the constraint owner
      * @param influenceIpo
      *            the ipo curve of the influence factor
      * @param blenderContext

+ 3 - 3
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/constraints/ConstraintHelper.java

@@ -153,7 +153,7 @@ public class ConstraintHelper extends AbstractBlenderHelper {
      *            the blender context
      * @return constraint object for the required type
      * @throws BlenderFileException
-     *             thrown when problems with blender file occured
+     *             thrown when problems with blender file occurred
      */
     private Constraint createConstraint(String dataType, Structure constraintStructure, Long ownerOMA, Ipo influenceIpo, BlenderContext blenderContext) throws BlenderFileException {
         if (dataType == null || "Mesh".equalsIgnoreCase(dataType) || "Camera".equalsIgnoreCase(dataType) || "Lamp".equalsIgnoreCase(dataType)) {
@@ -197,7 +197,7 @@ public class ConstraintHelper extends AbstractBlenderHelper {
     }
 
     /**
-     * The method retreives the transform from a feature in a given space.
+     * The method retrieves the transform from a feature in a given space.
      * 
      * @param oma
      *            the OMA of the feature (spatial or armature node)
@@ -205,7 +205,7 @@ public class ConstraintHelper extends AbstractBlenderHelper {
      *            the feature's subtarget (bone in a case of armature's node)
      * @param space
      *            the space the transform is evaluated to
-     * @return thensform of a feature in a given space
+     * @return the transform of a feature in a given space
      */
     public Transform getTransform(Long oma, String subtargetName, Space space) {
         Spatial feature = (Spatial) blenderContext.getLoadedFeature(oma, LoadedDataType.FEATURE);

+ 5 - 5
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/BlenderFileException.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2019 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -49,7 +49,7 @@ public class BlenderFileException extends Exception {
     /**
      * Constructor. Creates an exception containing the given message.
      * @param message
-     *            the message describing the problem that occured
+     *            the message describing the problem that occurred
      */
     public BlenderFileException(String message) {
         super(message);
@@ -58,7 +58,7 @@ public class BlenderFileException extends Exception {
     /**
      * Constructor. Creates an exception that is based upon other thrown object. It contains the whole stacktrace then.
      * @param throwable
-     *            an exception/error that occured
+     *            an exception/error that occurred
      */
     public BlenderFileException(Throwable throwable) {
         super(throwable);
@@ -67,9 +67,9 @@ public class BlenderFileException extends Exception {
     /**
      * Constructor. Creates an exception with both a message and stacktrace.
      * @param message
-     *            the message describing the problem that occured
+     *            the message describing the problem that occurred
      * @param throwable
-     *            an exception/error that occured
+     *            an exception/error that occurred
      */
     public BlenderFileException(String message, Throwable throwable) {
         super(message, throwable);

+ 3 - 3
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/BlenderInputStream.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2019 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -93,7 +93,7 @@ public class BlenderInputStream extends InputStream {
         try {
             this.readStreamToCache(bufferedInputStream);
         } catch (IOException e) {
-            throw new BlenderFileException("Problems occured while caching the file!", e);
+            throw new BlenderFileException("Problems occurred while caching the file!", e);
         } finally {
             try {
                 inputStream.close();
@@ -144,7 +144,7 @@ public class BlenderInputStream extends InputStream {
             gis = new GZIPInputStream(new ByteArrayInputStream(cachedBuffer));
             this.readStreamToCache(gis);
         } catch (IOException e) {
-            throw new IllegalStateException("IO errors occured where they should NOT! " + "The data is already buffered at this point!", e);
+            throw new IllegalStateException("IO errors occurred where they should NOT! " + "The data is already buffered at this point!", e);
         } finally {
             try {
                 if (gis != null) {

+ 6 - 6
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/file/Field.java

@@ -27,7 +27,7 @@ class Field implements Cloneable {
     /** This variable indicates the level of the pointer. */
     public int               pointerLevel;
     /**
-     * This variable determines the sizes of the array. If the value is null the n the field is not an array.
+     * This variable determines the sizes of the array. If the value is null then the field is not an array.
      */
     public int[]             tableSizes;
     /** This variable indicates if the field is a function pointer. */
@@ -52,7 +52,7 @@ class Field implements Cloneable {
 
     /**
      * Copy constructor. Used in clone method. Copying is not full. The value in the new object is not set so that we
-     * have a clead empty copy of the filed to fill with data.
+     * have a clean empty copy of the field to fill with data.
      * @param field
      *            the object that we copy
      */
@@ -73,7 +73,7 @@ class Field implements Cloneable {
     }
 
     /**
-     * This method fills the field wth data read from the input stream.
+     * This method fills the field with data read from the input stream.
      * @param blenderInputStream
      *            the stream we read data from
      * @throws BlenderFileException
@@ -264,9 +264,9 @@ class Field implements Cloneable {
     }
 
     /**
-     * This method removes all whitespaces from the text.
+     * This method removes all whitespace from the text.
      * @param text
-     *            the text we remove whitespaces from
+     *            the text we remove whitespace from
      */
     private void removeWhitespaces(StringBuilder text) {
         for (int i = 0; i < text.length(); ++i) {
@@ -306,7 +306,7 @@ class Field implements Cloneable {
         StringBuilder result = new StringBuilder();
         result.append(this.getFullName());
 
-        // insert appropriate amount of spaces to format the output corrently
+        // insert appropriate number of spaces to format the output corrently
         int nameLength = result.length();
         result.append(' ');// at least one space is a must
         for (int i = 1; i < NAME_LENGTH - nameLength; ++i) {// we start from i=1 because one space is already added

+ 1 - 1
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/materials/MaterialContext.java

@@ -102,7 +102,7 @@ public final class MaterialContext implements Savable {
         long flag = ((Number)structure.getFieldValue("flag")).longValue();
         if((flag & FLAG_TRANSPARENT) != 0) {
             // veryfying if the transparency is present
-            // (in blender transparent mask is 0x10000 but its better to verify it because blender can indicate transparency when
+            // (in blender transparent mask is 0x10000 but it's better to verify it because blender can indicate transparency when
             // it is not required
             boolean transparent = false;
             if (diffuseColor != null) {

+ 3 - 7
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/math/Matrix.java

@@ -186,11 +186,8 @@ public class Matrix extends SimpleMatrix {
     }
     
     /**
-     * Retreives the scale vector from the matrix and stores it into a given
+     * Retrieves the scale vector from the matrix and stores it into a given
      * vector.
-     * 
-     * @param the
-     *            vector where the scale will be stored
      */
     public Vector3d toScaleVector() {
         Vector3d result = new Vector3d();
@@ -199,11 +196,10 @@ public class Matrix extends SimpleMatrix {
     }
     
     /**
-     * Retreives the scale vector from the matrix and stores it into a given
+     * Retrieves the scale vector from the matrix and stores it into a given
      * vector.
      * 
-     * @param the
-     *            vector where the scale will be stored
+     * @param vector the vector where the scale will be stored
      */
     public void toScaleVector(Vector3d vector) {
         double scaleX = Math.sqrt(this.get(0, 0) * this.get(0, 0) + this.get(1, 0) * this.get(1, 0) + this.get(2, 0) * this.get(2, 0));

+ 3 - 3
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/math/Vector3d.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2018 jMonkeyEngine
+ * Copyright (c) 2009-2019 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -116,8 +116,8 @@ public final class Vector3d implements Savable, Cloneable, Serializable {
     /**
      * Constructor instantiates a new <code>Vector3d</code> that is a copy
      * of the provided vector
-     * @param copy
-     *            The Vector3d to copy
+     * @param vector3f
+     *            The Vector3f to copy
      */
     public Vector3d(Vector3f vector3f) {
         this(vector3f.x, vector3f.y, vector3f.z);

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

@@ -306,7 +306,7 @@ public class Face implements Comparator<Integer> {
                 }
             }
         } catch (BlenderFileException e) {
-            LOGGER.log(Level.WARNING, "Errors occured during face triangulation: {0}. The face will be triangulated with the most direct algorithm, but the results might not be identical to blender.", e.getLocalizedMessage());
+            LOGGER.log(Level.WARNING, "Errors occurred during face triangulation: {0}. The face will be triangulated with the most direct algorithm, but the results might not be identical to blender.", e.getLocalizedMessage());
             warning = TriangulationWarning.UNKNOWN;
         }
         if(warning != TriangulationWarning.NONE) {

+ 4 - 4
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/MeshHelper.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2019 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -140,11 +140,11 @@ public class MeshHelper extends AbstractBlenderHelper {
     }
 
     /**
-     * This method returns the vertices.
+     * This method returns the vertices: a list of vertex positions and a list
+     * of vertex normals.
      * 
      * @param meshStructure
      *            the structure containing the mesh data
-     * @return a list of two - element arrays, the first element is the vertex and the second - its normal
      * @throws BlenderFileException
      *             this exception is thrown when the blend file structure is somehow invalid or corrupted
      */
@@ -247,7 +247,7 @@ public class MeshHelper extends AbstractBlenderHelper {
                 }
             }
         } else {
-            // in this case UV's are assigned to faces (the array has the same legnth as the faces count)
+            // in this case UV's are assigned to faces (the array has the same length as the faces count)
             Structure facesData = (Structure) meshStructure.getFieldValue("fdata");
             Pointer pFacesDataLayers = (Pointer) facesData.getFieldValue("layers");
             if (pFacesDataLayers.isNotNull()) {

+ 6 - 8
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/TemporalMesh.java

@@ -198,8 +198,7 @@ public class TemporalMesh extends Geometry {
     }
 
     /**
-     * @param the
-     *            edge of the mesh
+     * @param edge the edge of the mesh
      * @return a list of faces that contain the given edge or an empty list
      */
     public Collection<Face> getAdjacentFaces(Edge edge) {
@@ -212,8 +211,7 @@ public class TemporalMesh extends Geometry {
     }
 
     /**
-     * @param the
-     *            index of the mesh
+     * @param index the index of the mesh
      * @return a list of edges that contain the index
      */
     public Collection<Edge> getAdjacentEdges(Integer index) {
@@ -223,8 +221,7 @@ public class TemporalMesh extends Geometry {
     /**
      * Tells if the given edge is a boundary edge. The boundary edge means that it belongs to a single
      * face or to none.
-     * @param the
-     *            edge of the mesh
+     * @param edge the edge of the mesh
      * @return <b>true</b> if the edge is a boundary one and <b>false</b> otherwise
      */
     public boolean isBoundary(Edge edge) {
@@ -289,8 +286,9 @@ public class TemporalMesh extends Geometry {
     /**
      * The method rebuilds the mappings between faces and edges. Should be called after
      * every major change of the temporal mesh done outside it.
-     * @note I will remove this method soon and make the mappings to be done automatically
-     *       when the mesh is modified.
+     * <p>
+     * Note: I will remove this method soon and cause the mappings to be done
+     * automatically when the mesh is modified.
      */
     public void rebuildIndexesMappings() {
         indexToEdgeMapping.clear();

+ 1 - 1
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/textures/UserUVCollection.java

@@ -62,7 +62,7 @@ public class UserUVCollection {
      *            the name of the UV set
      * @param vertexIndex
      *            the vertex index corresponds to the index in jme mesh and not the original one in blender
-     * @return
+     * @return a pre-existing coordinate vector
      */
     public Vector2f getUVForVertex(String uvSetName, int vertexIndex) {
         return uvsMap.get(uvSetName).get(vertexIndex);

+ 2 - 1
jme3-bullet-native-android/build.gradle

@@ -104,7 +104,8 @@ task copyJmeAndroid(type: Copy) {
     into outputDir
 }
 
-task buildBulletNativeLib(type: Exec, dependsOn: [copyJmeAndroid, copyJmeCpp, copyBullet]) {
+//dependsOn ':jme3-bullet:generateNativeHeaders'
+task buildBulletNativeLib(type: Exec, dependsOn: [copyJmeAndroid, ':jme3-bullet:generateNativeHeaders', copyJmeCpp, copyBullet]) {
 //    args 'TARGET_PLATFORM=android-9'
 //    println "buildBulletNativeLib ndkWorkingPath: " + ndkWorkingPath
 //    println "buildBulletNativeLib rootProject.ndkCommandPath: " + rootProject.ndkCommandPath

BIN
jme3-bullet-native-android/libs/arm64-v8a/libbulletjme.so


BIN
jme3-bullet-native-android/libs/armeabi-v7a/libbulletjme.so


BIN
jme3-bullet-native-android/libs/armeabi/libbulletjme.so


BIN
jme3-bullet-native-android/libs/mips/libbulletjme.so


BIN
jme3-bullet-native-android/libs/mips64/libbulletjme.so


BIN
jme3-bullet-native-android/libs/x86/libbulletjme.so


BIN
jme3-bullet-native-android/libs/x86_64/libbulletjme.so


+ 13 - 2
jme3-bullet-native-android/src/native/android/Android.mk

@@ -54,12 +54,23 @@ LOCAL_C_INCLUDES := $(BULLET_PATH)/\
     $(BULLET_PATH)/vectormath/sse\
     $(BULLET_PATH)/vectormath/neon
 
-LOCAL_CFLAGS := $(LOCAL_C_INCLUDES:%=-I%)
+#ARM mode more performant than thumb for old armeabi
+ifeq ($(TARGET_ARCH_ABI),$(filter $(TARGET_ARCH_ABI), armeabi))
+LOCAL_ARM_MODE := arm
+endif 
+
+#Enable neon for armv7
+ifeq ($(TARGET_ARCH_ABI),$(filter $(TARGET_ARCH_ABI), armeabi-v7a))
+LOCAL_ARM_NEON := true
+endif
+
+LOCAL_CFLAGS := $(LOCAL_C_INCLUDES:%=-I%) 
 LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -ldl -lm -llog
 
 FILE_LIST := $(wildcard $(LOCAL_PATH)/*.cpp)
 FILE_LIST += $(wildcard $(LOCAL_PATH)/**/*.cpp)
 FILE_LIST += $(wildcard $(LOCAL_PATH)/**/**/*.cpp)
+FILE_LIST := $(filter-out $(wildcard $(LOCAL_PATH)/Bullet3OpenCL/**/*.cpp), $(FILE_LIST))
 LOCAL_SRC_FILES := $(FILE_LIST:$(LOCAL_PATH)/%=%)
 
-include $(BUILD_SHARED_LIBRARY)
+include $(BUILD_SHARED_LIBRARY)

+ 4 - 1
jme3-bullet-native-android/src/native/android/Application.mk

@@ -1,4 +1,7 @@
 APP_OPTIM := release
 APP_ABI := all
-#APP_ABI := armeabi-v7a
+APP_STL := stlport_static
+# gnustl_static or stlport_static
 APP_MODULES      := bulletjme
+APP_CFLAGS += -funroll-loops -Ofast
+

+ 13 - 1
jme3-bullet-native/build.gradle

@@ -21,6 +21,10 @@ task cleanHeaders(type: Delete) {
 task cleanUnzipped(type: Delete) {
     delete bulletFolder
 }
+// clean up the downloaded archive
+task cleanZipFile(type: Delete) {
+    delete bulletZipFile
+}
 
 model {
     components {
@@ -37,13 +41,21 @@ model {
                     source {
                         srcDir 'src/native/cpp'
                         srcDir bulletSrcPath
+                        exclude 'Bullet3Collision/**'
+                        exclude 'Bullet3Dynamics/**'
+                        exclude 'Bullet3Geometry/**'
                         exclude 'Bullet3OpenCL/**'
+                        exclude 'Bullet3Serialize/**'
                         include '**/*.cpp'
                     }
                     exportedHeaders {
                         srcDir 'src/native/cpp'
                         srcDir bulletSrcPath
+                        exclude 'Bullet3Collision/**'
+                        exclude 'Bullet3Dynamics/**'
+                        exclude 'Bullet3Geometry/**'
                         exclude 'Bullet3OpenCL/**'
+                        exclude 'Bullet3Serialize/**'
                         include '**/*.h'
                     }
                 }
@@ -208,7 +220,7 @@ task unzipBulletIfNeeded {
 }
 
 unzipBulletIfNeeded.dependsOn {
-    if (buildNativeProjects == "true" && !file(bulletFolder).isDirectory()) {
+    if (buildNativeProjects == "true") {
         unzipBullet
     }
 }

BIN
jme3-bullet-native/libs/native/linux/x86/libbulletjme.so


BIN
jme3-bullet-native/libs/native/linux/x86_64/libbulletjme.so


BIN
jme3-bullet-native/libs/native/osx/x86/libbulletjme.dylib


BIN
jme3-bullet-native/libs/native/osx/x86_64/libbulletjme.dylib


BIN
jme3-bullet-native/libs/native/windows/x86/bulletjme.dll


BIN
jme3-bullet-native/libs/native/windows/x86_64/bulletjme.dll


+ 62 - 57
jme3-bullet-native/src/native/cpp/com_jme3_bullet_PhysicsSpace.cpp

@@ -46,16 +46,21 @@ extern "C" {
      * Signature: (FFFFFFI)J
      */
     JNIEXPORT jlong JNICALL Java_com_jme3_bullet_PhysicsSpace_createPhysicsSpace
-    (JNIEnv * env, jobject object, jfloat minX, jfloat minY, jfloat minZ, jfloat maxX, jfloat maxY, jfloat maxZ, jint broadphase, jboolean threading) {
+    (JNIEnv * env, jobject object, jfloat minX, jfloat minY, jfloat minZ,
+            jfloat maxX, jfloat maxY, jfloat maxZ, jint broadphase,
+            jboolean threading) {
         jmeClasses::initJavaClasses(env);
+
         jmePhysicsSpace* space = new jmePhysicsSpace(env, object);
         if (space == NULL) {
             jclass newExc = env->FindClass("java/lang/NullPointerException");
             env->ThrowNew(newExc, "The physics space has not been created.");
             return 0;
         }
-        space->createPhysicsSpace(minX, minY, minZ, maxX, maxY, maxZ, broadphase, threading);
-        return reinterpret_cast<jlong>(space);
+
+        space->createPhysicsSpace(minX, minY, minZ, maxX, maxY, maxZ,
+                broadphase, threading);
+        return reinterpret_cast<jlong> (space);
     }
 
     /*
@@ -64,8 +69,9 @@ extern "C" {
      * Signature: (JFIF)V
      */
     JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_stepSimulation
-    (JNIEnv * env, jobject object, jlong spaceId, jfloat tpf, jint maxSteps, jfloat accuracy) {
-        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*>(spaceId);
+    (JNIEnv * env, jobject object, jlong spaceId, jfloat tpf, jint maxSteps,
+            jfloat accuracy) {
+        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*> (spaceId);
         if (space == NULL) {
             jclass newExc = env->FindClass("java/lang/NullPointerException");
             env->ThrowNew(newExc, "The physics space does not exist.");
@@ -81,8 +87,8 @@ extern "C" {
      */
     JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_addCollisionObject
     (JNIEnv * env, jobject object, jlong spaceId, jlong objectId) {
-        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*>(spaceId);
-        btCollisionObject* collisionObject = reinterpret_cast<btCollisionObject*>(objectId);
+        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*> (spaceId);
+        btCollisionObject* collisionObject = reinterpret_cast<btCollisionObject*> (objectId);
         if (space == NULL) {
             jclass newExc = env->FindClass("java/lang/NullPointerException");
             env->ThrowNew(newExc, "The physics space does not exist.");
@@ -93,7 +99,7 @@ extern "C" {
             env->ThrowNew(newExc, "The collision object does not exist.");
             return;
         }
-        jmeUserPointer *userPointer = (jmeUserPointer*)collisionObject->getUserPointer();
+        jmeUserPointer *userPointer = (jmeUserPointer*) collisionObject->getUserPointer();
         userPointer -> space = space;
 
         space->getDynamicsWorld()->addCollisionObject(collisionObject);
@@ -106,8 +112,8 @@ extern "C" {
      */
     JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_removeCollisionObject
     (JNIEnv * env, jobject object, jlong spaceId, jlong objectId) {
-        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*>(spaceId);
-        btCollisionObject* collisionObject = reinterpret_cast<btCollisionObject*>(objectId);
+        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*> (spaceId);
+        btCollisionObject* collisionObject = reinterpret_cast<btCollisionObject*> (objectId);
         if (space == NULL) {
             jclass newExc = env->FindClass("java/lang/NullPointerException");
             env->ThrowNew(newExc, "The physics space does not exist.");
@@ -119,7 +125,7 @@ extern "C" {
             return;
         }
         space->getDynamicsWorld()->removeCollisionObject(collisionObject);
-        jmeUserPointer *userPointer = (jmeUserPointer*)collisionObject->getUserPointer();
+        jmeUserPointer *userPointer = (jmeUserPointer*) collisionObject->getUserPointer();
         userPointer -> space = NULL;
     }
 
@@ -130,8 +136,8 @@ extern "C" {
      */
     JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_addRigidBody
     (JNIEnv * env, jobject object, jlong spaceId, jlong rigidBodyId) {
-        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*>(spaceId);
-        btRigidBody* collisionObject = reinterpret_cast<btRigidBody*>(rigidBodyId);
+        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*> (spaceId);
+        btRigidBody* collisionObject = reinterpret_cast<btRigidBody*> (rigidBodyId);
         if (space == NULL) {
             jclass newExc = env->FindClass("java/lang/NullPointerException");
             env->ThrowNew(newExc, "The physics space does not exist.");
@@ -142,7 +148,7 @@ extern "C" {
             env->ThrowNew(newExc, "The collision object does not exist.");
             return;
         }
-        jmeUserPointer *userPointer = (jmeUserPointer*)collisionObject->getUserPointer();
+        jmeUserPointer *userPointer = (jmeUserPointer*) collisionObject->getUserPointer();
         userPointer -> space = space;
         space->getDynamicsWorld()->addRigidBody(collisionObject);
     }
@@ -154,8 +160,8 @@ extern "C" {
      */
     JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_removeRigidBody
     (JNIEnv * env, jobject object, jlong spaceId, jlong rigidBodyId) {
-        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*>(spaceId);
-        btRigidBody* collisionObject = reinterpret_cast<btRigidBody*>(rigidBodyId);
+        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*> (spaceId);
+        btRigidBody* collisionObject = reinterpret_cast<btRigidBody*> (rigidBodyId);
         if (space == NULL) {
             jclass newExc = env->FindClass("java/lang/NullPointerException");
             env->ThrowNew(newExc, "The physics space does not exist.");
@@ -166,7 +172,7 @@ extern "C" {
             env->ThrowNew(newExc, "The collision object does not exist.");
             return;
         }
-        jmeUserPointer *userPointer = (jmeUserPointer*)collisionObject->getUserPointer();
+        jmeUserPointer *userPointer = (jmeUserPointer*) collisionObject->getUserPointer();
         userPointer -> space = NULL;
         space->getDynamicsWorld()->removeRigidBody(collisionObject);
     }
@@ -178,8 +184,8 @@ extern "C" {
      */
     JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_addCharacterObject
     (JNIEnv * env, jobject object, jlong spaceId, jlong objectId) {
-        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*>(spaceId);
-        btCollisionObject* collisionObject = reinterpret_cast<btCollisionObject*>(objectId);
+        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*> (spaceId);
+        btCollisionObject* collisionObject = reinterpret_cast<btCollisionObject*> (objectId);
         if (space == NULL) {
             jclass newExc = env->FindClass("java/lang/NullPointerException");
             env->ThrowNew(newExc, "The physics space does not exist.");
@@ -190,12 +196,12 @@ extern "C" {
             env->ThrowNew(newExc, "The collision object does not exist.");
             return;
         }
-        jmeUserPointer *userPointer = (jmeUserPointer*)collisionObject->getUserPointer();
+        jmeUserPointer *userPointer = (jmeUserPointer*) collisionObject->getUserPointer();
         userPointer -> space = space;
         space->getDynamicsWorld()->addCollisionObject(collisionObject,
                 btBroadphaseProxy::CharacterFilter,
                 btBroadphaseProxy::StaticFilter | btBroadphaseProxy::DefaultFilter
-        );
+                );
     }
 
     /*
@@ -205,8 +211,8 @@ extern "C" {
      */
     JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_removeCharacterObject
     (JNIEnv * env, jobject object, jlong spaceId, jlong objectId) {
-        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*>(spaceId);
-        btCollisionObject* collisionObject = reinterpret_cast<btCollisionObject*>(objectId);
+        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*> (spaceId);
+        btCollisionObject* collisionObject = reinterpret_cast<btCollisionObject*> (objectId);
         if (space == NULL) {
             jclass newExc = env->FindClass("java/lang/NullPointerException");
             env->ThrowNew(newExc, "The physics space does not exist.");
@@ -217,7 +223,7 @@ extern "C" {
             env->ThrowNew(newExc, "The collision object does not exist.");
             return;
         }
-        jmeUserPointer *userPointer = (jmeUserPointer*)collisionObject->getUserPointer();
+        jmeUserPointer *userPointer = (jmeUserPointer*) collisionObject->getUserPointer();
         userPointer -> space = NULL;
         space->getDynamicsWorld()->removeCollisionObject(collisionObject);
     }
@@ -229,8 +235,8 @@ extern "C" {
      */
     JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_addAction
     (JNIEnv * env, jobject object, jlong spaceId, jlong objectId) {
-        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*>(spaceId);
-        btActionInterface* actionObject = reinterpret_cast<btActionInterface*>(objectId);
+        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*> (spaceId);
+        btActionInterface* actionObject = reinterpret_cast<btActionInterface*> (objectId);
         if (space == NULL) {
             jclass newExc = env->FindClass("java/lang/NullPointerException");
             env->ThrowNew(newExc, "The physics space does not exist.");
@@ -251,8 +257,8 @@ extern "C" {
      */
     JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_removeAction
     (JNIEnv * env, jobject object, jlong spaceId, jlong objectId) {
-        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*>(spaceId);
-        btActionInterface* actionObject = reinterpret_cast<btActionInterface*>(objectId);
+        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*> (spaceId);
+        btActionInterface* actionObject = reinterpret_cast<btActionInterface*> (objectId);
         if (space == NULL) {
             jclass newExc = env->FindClass("java/lang/NullPointerException");
             env->ThrowNew(newExc, "The physics space does not exist.");
@@ -273,8 +279,8 @@ extern "C" {
      */
     JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_addVehicle
     (JNIEnv * env, jobject object, jlong spaceId, jlong objectId) {
-        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*>(spaceId);
-        btActionInterface* actionObject = reinterpret_cast<btActionInterface*>(objectId);
+        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*> (spaceId);
+        btActionInterface* actionObject = reinterpret_cast<btActionInterface*> (objectId);
         if (space == NULL) {
             jclass newExc = env->FindClass("java/lang/NullPointerException");
             env->ThrowNew(newExc, "The physics space does not exist.");
@@ -295,8 +301,8 @@ extern "C" {
      */
     JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_removeVehicle
     (JNIEnv * env, jobject object, jlong spaceId, jlong objectId) {
-        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*>(spaceId);
-        btActionInterface* actionObject = reinterpret_cast<btActionInterface*>(objectId);
+        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*> (spaceId);
+        btActionInterface* actionObject = reinterpret_cast<btActionInterface*> (objectId);
         if (space == NULL) {
             jclass newExc = env->FindClass("java/lang/NullPointerException");
             env->ThrowNew(newExc, "The physics space does not exist.");
@@ -317,8 +323,8 @@ extern "C" {
      */
     JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_addConstraint
     (JNIEnv * env, jobject object, jlong spaceId, jlong objectId) {
-        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*>(spaceId);
-        btTypedConstraint* constraint = reinterpret_cast<btTypedConstraint*>(objectId);
+        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*> (spaceId);
+        btTypedConstraint* constraint = reinterpret_cast<btTypedConstraint*> (objectId);
         if (space == NULL) {
             jclass newExc = env->FindClass("java/lang/NullPointerException");
             env->ThrowNew(newExc, "The physics space does not exist.");
@@ -339,8 +345,8 @@ extern "C" {
      */
     JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_addConstraintC
     (JNIEnv * env, jobject object, jlong spaceId, jlong objectId, jboolean collision) {
-        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*>(spaceId);
-        btTypedConstraint* constraint = reinterpret_cast<btTypedConstraint*>(objectId);
+        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*> (spaceId);
+        btTypedConstraint* constraint = reinterpret_cast<btTypedConstraint*> (objectId);
         if (space == NULL) {
             jclass newExc = env->FindClass("java/lang/NullPointerException");
             env->ThrowNew(newExc, "The physics space does not exist.");
@@ -361,8 +367,8 @@ extern "C" {
      */
     JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_removeConstraint
     (JNIEnv * env, jobject object, jlong spaceId, jlong objectId) {
-        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*>(spaceId);
-        btTypedConstraint* constraint = reinterpret_cast<btTypedConstraint*>(objectId);
+        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*> (spaceId);
+        btTypedConstraint* constraint = reinterpret_cast<btTypedConstraint*> (objectId);
         if (space == NULL) {
             jclass newExc = env->FindClass("java/lang/NullPointerException");
             env->ThrowNew(newExc, "The physics space does not exist.");
@@ -383,7 +389,7 @@ extern "C" {
      */
     JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_setGravity
     (JNIEnv * env, jobject object, jlong spaceId, jobject vector) {
-        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*>(spaceId);
+        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*> (spaceId);
         if (space == NULL) {
             jclass newExc = env->FindClass("java/lang/NullPointerException");
             env->ThrowNew(newExc, "The physics space does not exist.");
@@ -391,6 +397,7 @@ extern "C" {
         }
         btVector3 gravity = btVector3();
         jmeBulletUtil::convert(env, vector, &gravity);
+
         space->getDynamicsWorld()->setGravity(gravity);
     }
 
@@ -411,13 +418,13 @@ extern "C" {
      */
     JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_finalizeNative
     (JNIEnv * env, jobject object, jlong spaceId) {
-        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*>(spaceId);
+        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*> (spaceId);
         if (space == NULL) {
             return;
         }
         delete(space);
     }
-    
+
     JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_rayTest_1native
     (JNIEnv * env, jobject object, jobject from, jobject to, jlong spaceId, jobject resultlist, jint flags) {
 
@@ -465,13 +472,12 @@ extern "C" {
         resultCallback.resultlist = resultlist;
         resultCallback.m_flags = flags;
         space->getDynamicsWorld()->rayTest(native_from, native_to, resultCallback);
+
         return;
     }
 
-
-
     JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_sweepTest_1native
-        (JNIEnv * env, jobject object, jlong shapeId, jobject from, jobject to, jlong spaceId, jobject resultlist, jfloat allowedCcdPenetration) {
+    (JNIEnv * env, jobject object, jlong shapeId, jobject from, jobject to, jlong spaceId, jobject resultlist, jfloat allowedCcdPenetration) {
 
         jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*> (spaceId);
         if (space == NULL) {
@@ -489,7 +495,7 @@ extern "C" {
 
         struct AllConvexResultCallback : public btCollisionWorld::ConvexResultCallback {
 
-            AllConvexResultCallback(const btTransform& convexFromWorld, const  btTransform & convexToWorld) : m_convexFromWorld(convexFromWorld), m_convexToWorld(convexToWorld) {
+            AllConvexResultCallback(const btTransform& convexFromWorld, const btTransform & convexToWorld) : m_convexFromWorld(convexFromWorld), m_convexToWorld(convexToWorld) {
             }
             jobject resultlist;
             JNIEnv* env;
@@ -500,17 +506,16 @@ extern "C" {
             btVector3 m_hitPointWorld;
 
             virtual btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult, bool normalInWorldSpace) {
-                    if (normalInWorldSpace) {
-                            m_hitNormalWorld = convexResult.m_hitNormalLocal;
-                    }
-                    else {
-                            m_hitNormalWorld = convexResult.m_hitCollisionObject->getWorldTransform().getBasis() * convexResult.m_hitNormalLocal;
-                    }
-                    m_hitPointWorld.setInterpolate3(m_convexFromWorld.getBasis() * m_convexFromWorld.getOrigin(), m_convexToWorld.getBasis() * m_convexToWorld.getOrigin(), convexResult.m_hitFraction);
+                if (normalInWorldSpace) {
+                    m_hitNormalWorld = convexResult.m_hitNormalLocal;
+                } else {
+                    m_hitNormalWorld = convexResult.m_hitCollisionObject->getWorldTransform().getBasis() * convexResult.m_hitNormalLocal;
+                }
+                m_hitPointWorld.setInterpolate3(m_convexFromWorld.getBasis() * m_convexFromWorld.getOrigin(), m_convexToWorld.getBasis() * m_convexToWorld.getOrigin(), convexResult.m_hitFraction);
 
-                    jmeBulletUtil::addSweepResult(env, resultlist, &m_hitNormalWorld, &m_hitPointWorld, convexResult.m_hitFraction, convexResult.m_hitCollisionObject);
+                jmeBulletUtil::addSweepResult(env, resultlist, &m_hitNormalWorld, &m_hitPointWorld, convexResult.m_hitFraction, convexResult.m_hitCollisionObject);
 
-                    return 1.f;
+                return 1.f;
             }
         };
 
@@ -528,16 +533,16 @@ extern "C" {
         space->getDynamicsWorld()->convexSweepTest((btConvexShape *) shape, native_from, native_to, resultCallback, native_allowed_ccd_penetration);
         return;
     }
-    
+
     JNIEXPORT void JNICALL Java_com_jme3_bullet_PhysicsSpace_setSolverNumIterations
     (JNIEnv *env, jobject object, jlong spaceId, jint value) {
-        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*>(spaceId);
+        jmePhysicsSpace* space = reinterpret_cast<jmePhysicsSpace*> (spaceId);
         if (space == NULL) {
             jclass newExc = env->FindClass("java/lang/NullPointerException");
             env->ThrowNew(newExc, "The physics space does not exist.");
             return;
         }
-        
+
         space->getDynamicsWorld()->getSolverInfo().m_numIterations = value;
     }
 

+ 37 - 1
jme3-bullet-native/src/native/cpp/com_jme3_bullet_collision_PhysicsCollisionObject.cpp

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2018 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -143,6 +143,42 @@ extern "C" {
         }
     }
 
+    /*
+     * Class:     com_jme3_bullet_collision_PhysicsCollisionObject
+     * Method:    getCollisionFlags
+     * Signature: (J)I
+     */
+    JNIEXPORT jint JNICALL Java_com_jme3_bullet_collision_PhysicsCollisionObject_getCollisionFlags
+    (JNIEnv *env, jobject object, jlong objectId) {
+        btCollisionObject* collisionObject = reinterpret_cast<btCollisionObject*> (objectId);
+        if (collisionObject == NULL) {
+            jclass newExc = env->FindClass("java/lang/NullPointerException");
+            env->ThrowNew(newExc, "The native object does not exist.");
+            return 0;
+        }
+
+        jint result = collisionObject->getCollisionFlags();
+        return result;
+    }
+
+    /*
+     * Class:     com_jme3_bullet_collision_PhysicsCollisionObject
+     * Method:    setCollisionFlags
+     * Signature: (JI)V
+     */
+    JNIEXPORT void JNICALL Java_com_jme3_bullet_collision_PhysicsCollisionObject_setCollisionFlags
+    (JNIEnv *env, jobject object, jlong objectId, jint desiredFlags) {
+        btCollisionObject* collisionObject = reinterpret_cast<btCollisionObject*> (objectId);
+        if (collisionObject == NULL) {
+            jclass newExc = env->FindClass("java/lang/NullPointerException");
+            env->ThrowNew(newExc, "The native object does not exist.");
+            return;
+        }
+
+        collisionObject->setCollisionFlags(desiredFlags);
+    }
+
+
 #ifdef __cplusplus
 }
 #endif

+ 3 - 1
jme3-bullet-native/src/native/cpp/com_jme3_bullet_collision_shapes_HullCollisionShape.cpp

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2019 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -60,6 +60,8 @@ extern "C" {
             
             shape->addPoint(vect);
         }
+        
+        shape->optimizeConvexHull();
 
         return reinterpret_cast<jlong>(shape);
     }

+ 1 - 1
jme3-bullet-native/src/native/cpp/com_jme3_bullet_joints_HingeJoint.cpp

@@ -85,7 +85,7 @@ extern "C" {
             env->ThrowNew(newExc, "The native object does not exist.");
             return 0;
         }
-        return joint->getMotorTargetVelosity();
+        return joint->getMotorTargetVelocity();
     }
 
     /*

+ 17 - 19
jme3-bullet-native/src/native/cpp/jmePhysicsSpace.cpp

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2019 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -74,17 +74,16 @@ void jmePhysicsSpace::createPhysicsSpace(jfloat minX, jfloat minY, jfloat minZ,
     btBroadphaseInterface* broadphase;
 
     switch (broadphaseId) {
-        case 0:
+        case 0: // SIMPLE
             broadphase = new btSimpleBroadphase();
             break;
-        case 1:
+        case 1: // AXIS_SWEEP_3
             broadphase = new btAxisSweep3(min, max);
             break;
-        case 2:
-            //TODO: 32bit!
-            broadphase = new btAxisSweep3(min, max);
+        case 2: // AXIS_SWEEP_3_32
+            broadphase = new bt32BitAxisSweep3(min, max);
             break;
-        case 3:
+        case 3: // DBVT
             broadphase = new btDbvtBroadphase();
             break;
     }
@@ -150,8 +149,8 @@ void jmePhysicsSpace::createPhysicsSpace(jfloat minX, jfloat minY, jfloat minZ,
     dynamicsWorld->getPairCache()->setOverlapFilterCallback(new jmeFilterCallback());
     dynamicsWorld->setInternalTickCallback(&jmePhysicsSpace::preTickCallback, static_cast<void *> (this), true);
     dynamicsWorld->setInternalTickCallback(&jmePhysicsSpace::postTickCallback, static_cast<void *> (this));
-    if (gContactProcessedCallback == NULL) {
-        gContactProcessedCallback = &jmePhysicsSpace::contactProcessedCallback;
+    if (gContactStartedCallback == NULL) {
+        gContactStartedCallback = &jmePhysicsSpace::contactStartedCallback;
     }
 }
 
@@ -183,11 +182,10 @@ void jmePhysicsSpace::postTickCallback(btDynamicsWorld *world, btScalar timeStep
     }
 }
 
-bool jmePhysicsSpace::contactProcessedCallback(btManifoldPoint &cp, void *body0, void *body1) {
-    //    printf("contactProcessedCallback %d %dn", body0, body1);
-    btCollisionObject* co0 = (btCollisionObject*) body0;
+void jmePhysicsSpace::contactStartedCallback(btPersistentManifold* const &pm) {
+    const btCollisionObject* co0 = pm->getBody0();
+    const btCollisionObject* co1 = pm->getBody1();
     jmeUserPointer *up0 = (jmeUserPointer*) co0 -> getUserPointer();
-    btCollisionObject* co1 = (btCollisionObject*) body1;
     jmeUserPointer *up1 = (jmeUserPointer*) co1 -> getUserPointer();
     if (up0 != NULL) {
         jmePhysicsSpace *dynamicsWorld = (jmePhysicsSpace *)up0->space;
@@ -197,18 +195,18 @@ bool jmePhysicsSpace::contactProcessedCallback(btManifoldPoint &cp, void *body0,
             if (javaPhysicsSpace != NULL) {
                 jobject javaCollisionObject0 = env->NewLocalRef(up0->javaCollisionObject);
                 jobject javaCollisionObject1 = env->NewLocalRef(up1->javaCollisionObject);
-                env->CallVoidMethod(javaPhysicsSpace, jmeClasses::PhysicsSpace_addCollisionEvent, javaCollisionObject0, javaCollisionObject1, (jlong) & cp);
+                for(int i=0;i<pm->getNumContacts();i++){
+                    env->CallVoidMethod(javaPhysicsSpace, jmeClasses::PhysicsSpace_addCollisionEvent, javaCollisionObject0, javaCollisionObject1, (jlong) & pm->getContactPoint(i));
+                    if (env->ExceptionCheck()) {
+                        env->Throw(env->ExceptionOccurred());
+                    }
+                }
                 env->DeleteLocalRef(javaPhysicsSpace);
                 env->DeleteLocalRef(javaCollisionObject0);
                 env->DeleteLocalRef(javaCollisionObject1);
-                if (env->ExceptionCheck()) {
-                    env->Throw(env->ExceptionOccurred());
-                    return true;
-                }
             }
         }
     }
-    return true;
 }
 
 btDynamicsWorld* jmePhysicsSpace::getDynamicsWorld() {

+ 1 - 1
jme3-bullet-native/src/native/cpp/jmePhysicsSpace.h

@@ -62,5 +62,5 @@ public:
         JNIEnv* getEnv();
         static void preTickCallback(btDynamicsWorld*, btScalar);
         static void postTickCallback(btDynamicsWorld*, btScalar);
-        static bool contactProcessedCallback(btManifoldPoint &, void *, void *);
+        static void contactStartedCallback(btPersistentManifold* const &);
 };

+ 15 - 44
jme3-bullet/build.gradle

@@ -2,6 +2,8 @@ if (!hasProperty('mainClass')) {
     ext.mainClass = ''
 }
 
+String classBuildDir = "${buildDir}" + File.separator + 'classes'
+
 sourceSets {
     main {
         java {
@@ -17,52 +19,21 @@ dependencies {
 }
 
 task generateNativeHeaders(type: Exec, dependsOn: classes) {
-    def classes = " \
-        com.jme3.bullet.PhysicsSpace, \
-        \
-        com.jme3.bullet.collision.PhysicsCollisionEvent, \
-        com.jme3.bullet.collision.PhysicsCollisionObject,\
-        com.jme3.bullet.objects.PhysicsCharacter, \
-        com.jme3.bullet.objects.PhysicsGhostObject, \
-        com.jme3.bullet.objects.PhysicsRigidBody, \
-        com.jme3.bullet.objects.PhysicsVehicle, \
-        com.jme3.bullet.objects.VehicleWheel, \
-        com.jme3.bullet.objects.infos.RigidBodyMotionState, \
-        \
-        com.jme3.bullet.collision.shapes.CollisionShape, \
-        com.jme3.bullet.collision.shapes.BoxCollisionShape, \
-        com.jme3.bullet.collision.shapes.CapsuleCollisionShape, \
-        com.jme3.bullet.collision.shapes.CompoundCollisionShape, \
-        com.jme3.bullet.collision.shapes.ConeCollisionShape, \
-        com.jme3.bullet.collision.shapes.CylinderCollisionShape, \
-        com.jme3.bullet.collision.shapes.GImpactCollisionShape, \
-        com.jme3.bullet.collision.shapes.HeightfieldCollisionShape, \
-        com.jme3.bullet.collision.shapes.HullCollisionShape, \
-        com.jme3.bullet.collision.shapes.MeshCollisionShape, \
-        com.jme3.bullet.collision.shapes.PlaneCollisionShape, \
-        com.jme3.bullet.collision.shapes.SimplexCollisionShape, \
-        com.jme3.bullet.collision.shapes.SphereCollisionShape, \
-        \
-        com.jme3.bullet.joints.PhysicsJoint, \
-        com.jme3.bullet.joints.ConeJoint, \
-        com.jme3.bullet.joints.HingeJoint, \
-        com.jme3.bullet.joints.Point2PointJoint, \
-        com.jme3.bullet.joints.SixDofJoint, \
-        com.jme3.bullet.joints.SixDofSpringJoint, \
-        com.jme3.bullet.joints.SliderJoint, \
-        com.jme3.bullet.joints.motors.RotationalLimitMotor, \
-        com.jme3.bullet.joints.motors.TranslationalLimitMotor, \
-        \
-        com.jme3.bullet.util.NativeMeshUtil, \
-        com.jme3.bullet.util.DebugShapeFactory"
-
+    def files0 = fileTree("src/main/java/").filter { it.isFile() && it.getName().endsWith(".java") }.files
+    def files1 = fileTree("src/common/java/").filter { it.isFile() && it.getName().endsWith(".java") }.files
+    def files2 = fileTree("../jme3-core/src/main/java/").filter { it.isFile() && it.getName().endsWith(".java") }.files
+    def files3 = fileTree("../jme3-core/src/plugins/java/").filter { it.isFile() && it.getName().endsWith(".java") }.files
+    def files4 = fileTree("../jme3-core/src/tools/java/").filter { it.isFile() && it.getName().endsWith(".java") }.files
+    def files5 = fileTree("../jme3-terrain/src/main/java/").filter { it.isFile() && it.getName().endsWith(".java") }.files
     def classpath = sourceSets.main.runtimeClasspath.asPath
     def nativeIncludes = new File(project(":jme3-bullet-native").projectDir, "src/native/cpp")
-
-    executable org.gradle.internal.jvm.Jvm.current().getExecutable('javah')
-    args "-d", nativeIncludes
-    args "-classpath", classpath
-    args classes.split(",").collect { it.trim() }
+	def filesList = "\"" + files0.join("\"\n\"") + "\"\n\"" + files1.join("\"\n\"") + "\"\n\"" + files2.join("\"\n\"") + "\"\n\"" + files3.join("\"\n\"") + "\"\n\"" + files4.join("\"\n\"") + "\"\n\"" + files5.join("\"\n\"") + "\""
+	new File("$projectDir/java_classes.jtxt").text = filesList.replaceAll(java.util.regex.Pattern.quote("\\"), java.util.regex.Matcher.quoteReplacement("/"))
+    executable org.gradle.internal.jvm.Jvm.current().getExecutable('javac')
+    args "-h", nativeIncludes
+    args "@$projectDir/java_classes.jtxt"
+    args '-d', classBuildDir
+    args "-encoding", "UTF-8"
 }
 
 assemble.dependsOn(generateNativeHeaders)

+ 493 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/BoneLink.java

@@ -0,0 +1,493 @@
+/*
+ * Copyright (c) 2018-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 com.jme3.bullet.animation;
+
+import com.jme3.anim.Joint;
+import com.jme3.bullet.collision.shapes.CollisionShape;
+import com.jme3.bullet.joints.SixDofJoint;
+import com.jme3.bullet.objects.PhysicsRigidBody;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.math.Matrix3f;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Spatial;
+import com.jme3.util.clone.Cloner;
+import java.io.IOException;
+import java.util.logging.Logger;
+
+/**
+ * Link an animated bone in a skeleton to a jointed rigid body in a ragdoll.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold [email protected]
+ *
+ * Based on KinematicRagdollControl by Normen Hansen and Rémy Bouquet (Nehon).
+ */
+public class BoneLink extends PhysicsLink {
+    // *************************************************************************
+    // constants and loggers
+
+    /**
+     * message logger for this class
+     */
+    final public static Logger logger2
+            = Logger.getLogger(BoneLink.class.getName());
+    /**
+     * local copy of {@link com.jme3.math.Matrix3f#IDENTITY}
+     */
+    final private static Matrix3f matrixIdentity = new Matrix3f();
+    // *************************************************************************
+    // fields
+
+    /**
+     * bones managed by this link, in a pre-order, depth-first traversal of the
+     * skeleton, starting with the linked bone
+     */
+    private Joint[] managedBones = null;
+    /**
+     * submode when kinematic
+     */
+    private KinematicSubmode submode = KinematicSubmode.Animated;
+    /**
+     * local transform of each managed bone from the previous update
+     */
+    private Transform[] prevBoneTransforms = null;
+    /**
+     * local transform of each managed bone at the start of the most recent
+     * blend interval
+     */
+    private Transform[] startBoneTransforms = null;
+    // *************************************************************************
+    // constructors
+
+    /**
+     * No-argument constructor needed by SavableClassUtil. Do not invoke
+     * directly!
+     */
+    public BoneLink() {
+    }
+
+    /**
+     * Instantiate a purely kinematic link between the named skeleton bone and
+     * the specified rigid body.
+     *
+     * @param control the control that will manage this link (not null, alias
+     * created)
+     * @param bone the linked bone (not null, alias created)
+     * @param collisionShape the desired shape (not null, alias created)
+     * @param linkConfig the link configuration (not null)
+     * @param localOffset the location of the body's center (in the bone's local
+     * coordinates, not null, unaffected)
+     */
+    BoneLink(DacLinks control, Joint bone, CollisionShape collisionShape,
+            float mass, Vector3f localOffset) {
+        super(control, bone, collisionShape, mass, localOffset);
+    }
+    // *************************************************************************
+    // new methods exposed
+
+    /**
+     * Add a physics joint to this link and configure its range of motion. Also
+     * initialize the link's parent and its array of managed bones.
+     *
+     * @param parentLink (not null, alias created)
+     */
+    void addJoint(PhysicsLink parentLink) {
+        assert parentLink != null;
+        assert getJoint() == null;
+
+        setParent(parentLink);
+
+        Transform parentToWorld = parentLink.physicsTransform(null);
+        parentToWorld.setScale(1f);
+        Transform worldToParent = parentToWorld.invert();
+
+        Transform childToWorld = physicsTransform(null);
+        childToWorld.setScale(1f);
+
+        Transform childToParent = childToWorld.clone();
+        childToParent.combineWithParent(worldToParent);
+
+        Spatial transformer = getControl().getTransformer();
+        Vector3f pivotMesh = getBone().getModelTransform().getTranslation();
+        Vector3f pivotWorld = transformer.localToWorld(pivotMesh, null);
+
+        PhysicsRigidBody parentBody = parentLink.getRigidBody();
+        PhysicsRigidBody childBody = getRigidBody();
+        Vector3f pivotParent
+                = parentToWorld.transformInverseVector(pivotWorld, null);
+        Vector3f pivotChild
+                = childToWorld.transformInverseVector(pivotWorld, null);
+        Matrix3f rotParent = childToParent.getRotation().toRotationMatrix();
+        Matrix3f rotChild = matrixIdentity;
+        // TODO try HingeJoint or ConeJoint
+        SixDofJoint joint = new SixDofJoint(parentBody, childBody, pivotParent,
+                pivotChild, rotParent, rotChild, true);
+        super.setJoint(joint);
+
+        String name = boneName();
+        RangeOfMotion rangeOfMotion = getControl().getJointLimits(name);
+        rangeOfMotion.setupJoint(joint);
+
+        joint.setCollisionBetweenLinkedBodys(false);
+
+        assert managedBones == null;
+        managedBones = getControl().listManagedBones(name);
+
+        int numManagedBones = managedBones.length;
+        startBoneTransforms = new Transform[numManagedBones];
+        for (int i = 0; i < numManagedBones; ++i) {
+            startBoneTransforms[i] = new Transform();
+        }
+    }
+
+    /**
+     * Begin blending this link to a purely kinematic mode.
+     *
+     * @param submode enum value (not null)
+     * @param blendInterval the duration of the blend interval (in seconds,
+     * &ge;0)
+     */
+    public void blendToKinematicMode(KinematicSubmode submode,
+            float blendInterval) {
+        super.blendToKinematicMode(blendInterval);
+
+        this.submode = submode;
+        /*
+         * Save initial bone transforms for blending.
+         */
+        int numManagedBones = managedBones.length;
+        for (int mbIndex = 0; mbIndex < numManagedBones; ++mbIndex) {
+            Transform transform;
+            if (prevBoneTransforms == null) { // this link not updated yet
+                Joint managedBone = managedBones[mbIndex];
+                transform = managedBone.getLocalTransform().clone();
+            } else {
+                transform = prevBoneTransforms[mbIndex];
+            }
+            startBoneTransforms[mbIndex].set(transform);
+        }
+    }
+    // *************************************************************************
+    // PhysicsLink methods
+
+    /**
+     * Callback from {@link com.jme3.util.clone.Cloner} to convert this
+     * shallow-cloned link into a deep-cloned one, using the specified cloner
+     * and original to resolve copied fields.
+     *
+     * @param cloner the cloner that's cloning this link (not null)
+     * @param original the instance from which this link was shallow-cloned
+     * (unused)
+     */
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        super.cloneFields(cloner, original);
+
+        managedBones = cloner.clone(managedBones);
+        prevBoneTransforms = cloner.clone(prevBoneTransforms);
+        startBoneTransforms = cloner.clone(startBoneTransforms);
+    }
+
+    /**
+     * Update this link in Dynamic mode, setting the linked bone's transform
+     * based on the transform of the rigid body.
+     */
+    @Override
+    protected void dynamicUpdate() {
+        assert !getRigidBody().isKinematic();
+
+        Transform transform = localBoneTransform(null);
+        getBone().setLocalTransform(transform);
+
+        for (Joint managedBone : managedBones) {
+            managedBone.updateModelTransforms();
+        }
+    }
+
+    /**
+     * Create a shallow clone for the JME cloner.
+     *
+     * @return a new instance
+     */
+    @Override
+    public BoneLink jmeClone() {
+        try {
+            BoneLink clone = (BoneLink) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException exception) {
+            throw new RuntimeException(exception);
+        }
+    }
+
+    /**
+     * Update this link in blended Kinematic mode.
+     *
+     * @param tpf the time interval between frames (in seconds, &ge;0)
+     */
+    @Override
+    protected void kinematicUpdate(float tpf) {
+        assert tpf >= 0f : tpf;
+        assert getRigidBody().isKinematic();
+
+        Transform transform = new Transform();
+        for (int mbIndex = 0; mbIndex < managedBones.length; ++mbIndex) {
+            Joint managedBone = managedBones[mbIndex];
+            switch (submode) {
+                case Animated:
+                    transform.set(managedBone.getLocalTransform());
+                    break;
+                case Frozen:
+                    transform.set(prevBoneTransforms[mbIndex]);
+                    break;
+                default:
+                    throw new IllegalStateException(submode.toString());
+            }
+
+            if (kinematicWeight() < 1f) { // not purely kinematic yet
+                /*
+                 * For a smooth transition, blend the saved bone transform
+                 * (from the start of the blend interval)
+                 * into the goal transform.
+                 */
+                Transform start = startBoneTransforms[mbIndex];
+                Quaternion startQuat = start.getRotation();
+                Quaternion endQuat = transform.getRotation();
+                if (startQuat.dot(endQuat) < 0f) {
+                    endQuat.multLocal(-1f);
+                }
+                transform.interpolateTransforms(
+                        startBoneTransforms[mbIndex].clone(), transform,
+                        kinematicWeight());
+            }
+            /*
+             * Update the managed bone.
+             */
+            managedBone.setLocalTransform(transform);
+            managedBone.updateModelTransforms();
+        }
+
+        super.kinematicUpdate(tpf);
+    }
+
+    /**
+     * Unambiguously identify this link by name, within its DynamicAnimControl.
+     *
+     * @return a brief textual description (not null, not empty)
+     */
+    @Override
+    public String name() {
+        String result = "Bone:" + boneName();
+        return result;
+    }
+
+    /**
+     * Copy animation data from the specified link, which must have the same
+     * name and the same managed bones.
+     *
+     * @param oldLink the link to copy from (not null, unaffected)
+     */
+    void postRebuild(BoneLink oldLink) {
+        int numManagedBones = managedBones.length;
+        assert oldLink.managedBones.length == numManagedBones;
+
+        super.postRebuild(oldLink);
+        if (oldLink.isKinematic()) {
+            submode = oldLink.submode;
+        } else {
+            submode = KinematicSubmode.Frozen;
+        }
+
+        if (prevBoneTransforms == null) {
+            prevBoneTransforms = new Transform[numManagedBones];
+            for (int i = 0; i < numManagedBones; ++i) {
+                prevBoneTransforms[i] = new Transform();
+            }
+        }
+        for (int i = 0; i < numManagedBones; ++i) {
+            prevBoneTransforms[i].set(oldLink.prevBoneTransforms[i]);
+            startBoneTransforms[i].set(oldLink.startBoneTransforms[i]);
+        }
+    }
+
+    /**
+     * De-serialize this link, for example when loading from a J3O file.
+     *
+     * @param im importer (not null)
+     * @throws IOException from importer
+     */
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+
+        Savable[] tmp = ic.readSavableArray("managedBones", null);
+        if (tmp == null) {
+            managedBones = null;
+        } else {
+            managedBones = new Joint[tmp.length];
+            for (int i = 0; i < tmp.length; ++i) {
+                managedBones[i] = (Joint) tmp[i];
+            }
+        }
+
+        submode = ic.readEnum("submode", KinematicSubmode.class,
+                KinematicSubmode.Animated);
+        prevBoneTransforms = RagUtils.readTransformArray(ic,
+                "prevBoneTransforms");
+        startBoneTransforms = RagUtils.readTransformArray(ic,
+                "startBoneTransforms");
+    }
+
+    /**
+     * Immediately put this link into dynamic mode and update the range of
+     * motion of its joint.
+     *
+     * @param uniformAcceleration the uniform acceleration vector (in
+     * physics-space coordinates, not null, unaffected)
+     */
+    @Override
+    public void setDynamic(Vector3f uniformAcceleration) {
+        getControl().verifyReadyForDynamicMode("put link into dynamic mode");
+
+        super.setDynamic(uniformAcceleration);
+
+        String name = boneName();
+        RangeOfMotion preset = getControl().getJointLimits(name);
+        preset.setupJoint((SixDofJoint) getJoint());
+    }
+
+    /**
+     * Internal callback, invoked once per frame during the logical-state
+     * update, provided the control is added to a scene.
+     *
+     * @param tpf the time interval between frames (in seconds, &ge;0)
+     */
+    @Override
+    void update(float tpf) {
+        assert tpf >= 0f : tpf;
+
+        if (prevBoneTransforms == null) {
+            /*
+             * On the first update, allocate and initialize
+             * the array of previous bone transforms, if it wasn't
+             * allocated in blendToKinematicMode().
+             */
+            int numManagedBones = managedBones.length;
+            prevBoneTransforms = new Transform[numManagedBones];
+            for (int mbIndex = 0; mbIndex < numManagedBones; ++mbIndex) {
+                Joint managedBone = managedBones[mbIndex];
+                Transform boneTransform
+                        = managedBone.getLocalTransform().clone();
+                prevBoneTransforms[mbIndex] = boneTransform;
+            }
+        }
+
+        super.update(tpf);
+        /*
+         * Save copies of the latest bone transforms.
+         */
+        for (int mbIndex = 0; mbIndex < managedBones.length; ++mbIndex) {
+            Transform lastTransform = prevBoneTransforms[mbIndex];
+            Joint managedBone = managedBones[mbIndex];
+            lastTransform.set(managedBone.getLocalTransform());
+        }
+    }
+
+    /**
+     * Serialize this link, for example when saving to a J3O file.
+     *
+     * @param ex exporter (not null)
+     * @throws IOException from exporter
+     */
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+
+        oc.write(managedBones, "managedBones", null);
+        oc.write(submode, "submode", KinematicSubmode.Animated);
+        oc.write(prevBoneTransforms, "prevBoneTransforms", new Transform[0]);
+        oc.write(startBoneTransforms, "startBoneTransforms", new Transform[0]);
+    }
+    // *************************************************************************
+    // private methods
+
+    /**
+     * Calculate the local bone transform to match the physics transform of the
+     * rigid body.
+     *
+     * @param storeResult storage for the result (modified if not null)
+     * @return the calculated bone transform (in local coordinates, either
+     * storeResult or a new transform, not null)
+     */
+    private Transform localBoneTransform(Transform storeResult) {
+        Transform result
+                = (storeResult == null) ? new Transform() : storeResult;
+        Vector3f location = result.getTranslation();
+        Quaternion orientation = result.getRotation();
+        Vector3f scale = result.getScale();
+        /*
+         * Start with the rigid body's transform in physics/world coordinates.
+         */
+        PhysicsRigidBody body = getRigidBody();
+        body.getPhysicsLocation(result.getTranslation());
+        body.getPhysicsRotation(result.getRotation());
+        result.setScale(body.getCollisionShape().getScale());
+        /*
+         * Convert to mesh coordinates.
+         */
+        Transform worldToMesh = getControl().meshTransform(null).invert();
+        result.combineWithParent(worldToMesh);
+        /*
+         * Convert to the bone's local coordinate system by factoring out the
+         * parent bone's transform.
+         */
+        Joint parentBone = getBone().getParent();
+        RagUtils.meshToLocal(parentBone, result);
+        /*
+         * Subtract the body's local offset, rotated and scaled.
+         */
+        Vector3f parentOffset = localOffset(null);
+        parentOffset.multLocal(scale);
+        orientation.mult(parentOffset, parentOffset);
+        location.subtractLocal(parentOffset);
+
+        return result;
+    }
+}

+ 565 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/DacConfiguration.java

@@ -0,0 +1,565 @@
+/*
+ * Copyright (c) 2018-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 com.jme3.bullet.animation;
+
+import com.jme3.anim.Armature;
+import com.jme3.anim.Joint;
+import com.jme3.bullet.control.AbstractPhysicsControl;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.scene.Spatial;
+import com.jme3.util.clone.Cloner;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Configure a DynamicAnimControl and access its configuration.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold [email protected]
+ *
+ * Based on KinematicRagdollControl by Normen Hansen and Rémy Bouquet (Nehon).
+ */
+abstract public class DacConfiguration extends AbstractPhysicsControl {
+    // *************************************************************************
+    // constants and loggers
+
+    /**
+     * message logger for this class
+     */
+    final public static Logger logger2
+            = Logger.getLogger(DacConfiguration.class.getName());
+    /**
+     * name for the ragdoll's torso, must not be used for any bone
+     */
+    final public static String torsoName = "";
+    // *************************************************************************
+    // fields
+
+    /**
+     * viscous damping ratio for new rigid bodies (0&rarr;no damping,
+     * 1&rarr;critically damped, default=0.6)
+     */
+    private float damping = 0.6f;
+    /**
+     * minimum applied impulse for a collision event to be dispatched to
+     * listeners (default=0)
+     */
+    private float eventDispatchImpulseThreshold = 0f;
+    /**
+     * mass for the torso
+     */
+    private float torsoMass = 1f;
+    /**
+     * map linked bone names to masses
+     */
+    private Map<String, Float> blConfigMap = new HashMap<>(50);
+    /**
+     * map linked bone names to ranges of motion for createSpatialData()
+     */
+    private Map<String, RangeOfMotion> jointMap = new HashMap<>(50);
+    /**
+     * gravitational acceleration vector for ragdolls (default is 9.8 in the -Y
+     * direction, approximating Earth-normal in MKS units)
+     */
+    private Vector3f gravityVector = new Vector3f(0f, -9.8f, 0f);
+    // *************************************************************************
+    // constructors
+
+    /**
+     * Instantiate an enabled control without any linked bones (torso only).
+     */
+    DacConfiguration() {
+    }
+    // *************************************************************************
+    // new methods exposed
+
+    /**
+     * Count the linked bones.
+     *
+     * @return count (&ge;0)
+     */
+    public int countLinkedBones() {
+        int count = blConfigMap.size();
+
+        assert count == jointMap.size();
+        assert count >= 0 : count;
+        return count;
+    }
+
+    /**
+     * Count the links.
+     *
+     * @return count (&ge;0)
+     */
+    public int countLinks() {
+        int result = countLinkedBones() + 1;
+        return result;
+    }
+
+    /**
+     * Read the damping ratio for new rigid bodies.
+     *
+     * @return the viscous damping ratio (0&rarr;no damping, 1&rarr;critically
+     * damped)
+     */
+    public float damping() {
+        assert damping >= 0f : damping;
+        return damping;
+    }
+
+    /**
+     * Read the event-dispatch impulse threshold of this control.
+     *
+     * @return the threshold value (&ge;0)
+     */
+    public float eventDispatchImpulseThreshold() {
+        assert eventDispatchImpulseThreshold >= 0f;
+        return eventDispatchImpulseThreshold;
+    }
+
+    /**
+     * Access the nominal range of motion for the joint connecting the named
+     * linked bone to its parent in the hierarchy.
+     *
+     * @param boneName the name of the linked bone (not null, not empty)
+     * @return the pre-existing instance (not null)
+     */
+    public RangeOfMotion getJointLimits(String boneName) {
+        if (!hasBoneLink(boneName)) {
+            String msg = "No linked bone named " + boneName;
+            throw new IllegalArgumentException(msg);
+        }
+        RangeOfMotion result = jointMap.get(boneName);
+
+        assert result != null;
+        return result;
+    }
+
+    /**
+     * Copy this control's gravitational acceleration for Ragdoll mode.
+     *
+     * @param storeResult storage for the result (modified if not null)
+     * @return an acceleration vector (in physics-space coordinates, either
+     * storeResult or a new vector, not null)
+     */
+    public Vector3f gravity(Vector3f storeResult) {
+        Vector3f result = (storeResult == null) ? new Vector3f() : storeResult;
+        result.set(gravityVector);
+        return result;
+    }
+
+    /**
+     * Test whether a BoneLink exists for the named bone.
+     *
+     * @param boneName the name of the bone (may be null)
+     * @return true if found, otherwise false
+     */
+    public boolean hasBoneLink(String boneName) {
+        boolean result;
+        if (boneName == null) {
+            result = false;
+        } else {
+            result = blConfigMap.containsKey(boneName);
+        }
+
+        return result;
+    }
+
+    /**
+     * Link the named bone using the specified mass and range of motion.
+     * <p>
+     * Allowed only when the control is NOT added to a spatial.
+     *
+     * @param boneName the name of the bone to link (not null, not empty)
+     * @param mass the desired mass of the bone (&gt;0)
+     * @param rom the desired range of motion (not null)
+     * @see #setJointLimits(java.lang.String,
+     * com.jme3.bullet.animation.RangeOfMotion)
+     */
+    public void link(String boneName, float mass, RangeOfMotion rom) {
+        verifyNotAddedToSpatial("link a bone");
+        if (hasBoneLink(boneName)) {
+            logger2.log(Level.WARNING, "Bone {0} is already linked.", boneName);
+        }
+
+        jointMap.put(boneName, rom);
+        blConfigMap.put(boneName, mass);
+    }
+
+    /**
+     * Enumerate all bones with bone links.
+     *
+     * @return a new array of bone names (not null, may be empty)
+     */
+    public String[] listLinkedBoneNames() {
+        int size = countLinkedBones();
+        String[] result = new String[size];
+        Collection<String> names = blConfigMap.keySet();
+        names.toArray(result);
+
+        return result;
+    }
+
+    /**
+     * Read the mass of the named bone/torso.
+     *
+     * @param boneName the name of the bone/torso (not null)
+     * @return the mass (in physics units, &gt;0)
+     */
+    public float mass(String boneName) {
+        float mass;
+        if (torsoName.equals(boneName)) {
+            mass = torsoMass;
+        } else {
+            mass = blConfigMap.get(boneName);
+        }
+        return mass;
+    }
+
+    /**
+     * Alter the viscous damping ratio for new rigid bodies.
+     *
+     * @param dampingRatio the desired damping ratio (non-negative, 0&rarr;no
+     * damping, 1&rarr;critically damped, default=0.6)
+     */
+    public void setDamping(float dampingRatio) {
+        damping = dampingRatio;
+    }
+
+    /**
+     * Alter the event-dispatch impulse threshold of this control.
+     *
+     * @param threshold the desired threshold (&ge;0)
+     */
+    public void setEventDispatchImpulseThreshold(float threshold) {
+        eventDispatchImpulseThreshold = threshold;
+    }
+
+    /**
+     * Alter this control's gravitational acceleration for Ragdoll mode.
+     *
+     * @param gravity the desired acceleration vector (in physics-space
+     * coordinates, not null, unaffected, default=0,-9.8,0)
+     */
+    public void setGravity(Vector3f gravity) {
+        gravityVector.set(gravity);
+    }
+
+    /**
+     * Alter the range of motion of the joint connecting the named BoneLink to
+     * its parent in the link hierarchy.
+     *
+     * @param boneName the name of the BoneLink (not null, not empty)
+     * @param rom the desired range of motion (not null)
+     */
+    public void setJointLimits(String boneName, RangeOfMotion rom) {
+        if (!hasBoneLink(boneName)) {
+            String msg = "No linked bone named " + boneName;
+            throw new IllegalArgumentException(msg);
+        }
+
+        jointMap.put(boneName, rom);
+    }
+
+    /**
+     * Alter the mass of the named bone/torso.
+     *
+     * @param boneName the name of the bone, or torsoName (not null)
+     * @param mass the desired mass (&gt;0)
+     */
+    public void setMass(String boneName, float mass) {
+        if (torsoName.equals(boneName)) {
+            torsoMass = mass;
+        } else if (hasBoneLink(boneName)) {
+            blConfigMap.put(boneName, mass);
+        } else {
+            String msg = "No bone/torso named " + boneName;
+            throw new IllegalArgumentException(msg);
+        }
+    }
+
+    /**
+     * Calculate the ragdoll's total mass.
+     *
+     * @return the total mass (&gt;0)
+     */
+    public float totalMass() {
+        float totalMass = torsoMass;
+        for (float mass : blConfigMap.values()) {
+            totalMass += mass;
+        }
+
+        return totalMass;
+    }
+
+    /**
+     * Unlink the BoneLink of the named bone.
+     * <p>
+     * Allowed only when the control is NOT added to a spatial.
+     *
+     * @param boneName the name of the bone to unlink (not null, not empty)
+     */
+    public void unlinkBone(String boneName) {
+        if (!hasBoneLink(boneName)) {
+            String msg = "No linked bone named " + boneName;
+            throw new IllegalArgumentException(msg);
+        }
+        verifyNotAddedToSpatial("unlink a bone");
+
+        jointMap.remove(boneName);
+        blConfigMap.remove(boneName);
+    }
+    // *************************************************************************
+    // new protected methods
+
+    /**
+     * Add unlinked descendants of the specified bone to the specified
+     * collection. Note: recursive.
+     *
+     * @param startBone the starting bone (not null, unaffected)
+     * @param addResult the collection of bone names to append to (not null,
+     * modified)
+     */
+    protected void addUnlinkedDescendants(Joint startBone,
+            Collection<Joint> addResult) {
+        for (Joint childBone : startBone.getChildren()) {
+            String childName = childBone.getName();
+            if (!hasBoneLink(childName)) {
+                addResult.add(childBone);
+                addUnlinkedDescendants(childBone, addResult);
+            }
+        }
+    }
+
+    /**
+     * Find the manager of the specified bone.
+     *
+     * @param startBone the bone (not null, unaffected)
+     * @return a bone/torso name (not null)
+     */
+    protected String findManager(Joint startBone) {
+        String managerName;
+        Joint bone = startBone;
+        while (true) {
+            String boneName = bone.getName();
+            if (hasBoneLink(boneName)) {
+                managerName = boneName;
+                break;
+            }
+            bone = bone.getParent();
+            if (bone == null) {
+                managerName = torsoName;
+                break;
+            }
+        }
+
+        assert managerName != null;
+        return managerName;
+    }
+
+    /**
+     * Create a map from bone indices to the names of the bones that manage
+     * them.
+     *
+     * @param skeleton (not null, unaffected)
+     * @return a new array of bone/torso names (not null)
+     */
+    protected String[] managerMap(Armature skeleton) {
+        int numBones = skeleton.getJointCount();
+        String[] managerMap = new String[numBones];
+        for (int boneIndex = 0; boneIndex < numBones; ++boneIndex) {
+            Joint bone = skeleton.getJoint(boneIndex);
+            managerMap[boneIndex] = findManager(bone);
+        }
+
+        return managerMap;
+    }
+    // *************************************************************************
+    // AbstractPhysicsControl methods
+
+    /**
+     * Callback from {@link com.jme3.util.clone.Cloner} to convert this
+     * shallow-cloned control into a deep-cloned one, using the specified cloner
+     * and original to resolve copied fields.
+     *
+     * @param cloner the cloner that's cloning this control (not null, modified)
+     * @param original the control from which this control was shallow-cloned
+     * (not null, unaffected)
+     */
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        super.cloneFields(cloner, original);
+
+        blConfigMap = cloner.clone(blConfigMap);
+        jointMap = cloner.clone(jointMap);
+        gravityVector = cloner.clone(gravityVector);
+    }
+
+    /**
+     * Create a shallow clone for the JME cloner.
+     *
+     * @return a new instance
+     */
+    @Override
+    public DacConfiguration jmeClone() {
+        try {
+            DacConfiguration clone
+                    = (DacConfiguration) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException exception) {
+            throw new RuntimeException(exception);
+        }
+    }
+
+    /**
+     * De-serialize this control, for example when loading from a J3O file.
+     *
+     * @param im importer (not null)
+     * @throws IOException from importer
+     */
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+
+        damping = ic.readFloat("damping", 0.6f);
+        eventDispatchImpulseThreshold
+                = ic.readFloat("eventDispatchImpulseThreshold", 0f);
+
+        jointMap.clear();
+        blConfigMap.clear();
+        String[] linkedBoneNames = ic.readStringArray("linkedBoneNames", null);
+        Savable[] linkedBoneJoints
+                = ic.readSavableArray("linkedBoneJoints", null);
+        float[] blConfigs = ic.readFloatArray("blConfigs", null);
+        for (int i = 0; i < linkedBoneNames.length; ++i) {
+            String boneName = linkedBoneNames[i];
+            RangeOfMotion rom = (RangeOfMotion) linkedBoneJoints[i];
+            jointMap.put(boneName, rom);
+            blConfigMap.put(boneName, blConfigs[i]);
+        }
+
+        torsoMass = ic.readFloat("torsoMass", 1f);
+        gravityVector = (Vector3f) ic.readSavable("gravity", null);
+    }
+
+    /**
+     * Render this control. Invoked once per view port per frame, provided the
+     * control is added to a scene. Should be invoked only by a subclass or by
+     * the RenderManager.
+     *
+     * @param rm the render manager (not null)
+     * @param vp the view port to render (not null)
+     */
+    @Override
+    public void render(RenderManager rm, ViewPort vp) {
+    }
+
+    /**
+     * Alter whether physics-space coordinates should match the spatial's local
+     * coordinates.
+     *
+     * @param applyPhysicsLocal true&rarr;match local coordinates,
+     * false&rarr;match world coordinates (default=false)
+     */
+    @Override
+    public void setApplyPhysicsLocal(boolean applyPhysicsLocal) {
+        if (applyPhysicsLocal) {
+            throw new UnsupportedOperationException(
+                    "DynamicAnimControl does not support local physics.");
+        }
+    }
+
+    /**
+     * Serialize this control, for example when saving to a J3O file.
+     *
+     * @param ex exporter (not null)
+     * @throws IOException from exporter
+     */
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+
+        oc.write(damping, "damping", 0.6f);
+        oc.write(eventDispatchImpulseThreshold, "eventDispatchImpulseThreshold",
+                0f);
+
+        int count = countLinkedBones();
+        String[] linkedBoneNames = new String[count];
+        RangeOfMotion[] roms = new RangeOfMotion[count];
+        float[] blConfigs = new float[count];
+        int i = 0;
+        for (Map.Entry<String, Float> entry : blConfigMap.entrySet()) {
+            linkedBoneNames[i] = entry.getKey();
+            roms[i] = jointMap.get(entry.getKey());
+            blConfigs[i] = entry.getValue();
+            ++i;
+        }
+        oc.write(linkedBoneNames, "linkedBoneNames", null);
+        oc.write(roms, "linkedBoneJoints", null);
+        oc.write(blConfigs, "blConfigs", null);
+
+        oc.write(torsoMass, "torsoMass", 1f);
+        oc.write(gravityVector, "gravity", null);
+    }
+    // *************************************************************************
+    // private methods
+
+    /**
+     * Verify that this control is NOT added to a Spatial.
+     *
+     * @param desiredAction (not null, not empty)
+     */
+    private void verifyNotAddedToSpatial(String desiredAction) {
+        assert desiredAction != null;
+
+        Spatial controlledSpatial = getSpatial();
+        if (controlledSpatial != null) {
+            String message = "Cannot " + desiredAction
+                    + " while the Control is added to a Spatial.";
+            throw new IllegalStateException(message);
+        }
+    }
+}

+ 1108 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/DacLinks.java

@@ -0,0 +1,1108 @@
+/*
+ * Copyright (c) 2018-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 com.jme3.bullet.animation;
+
+import com.jme3.anim.Armature;
+import com.jme3.anim.Joint;
+import com.jme3.anim.SkinningControl;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.PhysicsTickListener;
+import com.jme3.bullet.collision.shapes.CollisionShape;
+import com.jme3.bullet.collision.shapes.HullCollisionShape;
+import com.jme3.bullet.joints.PhysicsJoint;
+import com.jme3.bullet.joints.SixDofJoint;
+import com.jme3.bullet.objects.PhysicsRigidBody;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.util.clone.Cloner;
+import java.io.IOException;
+import java.nio.FloatBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Access a DynamicAnimControl at the PhysicsLink level once it's been added to
+ * a Spatial.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold [email protected]
+ *
+ * Based on KinematicRagdollControl by Normen Hansen and Rémy Bouquet (Nehon).
+ */
+public class DacLinks
+        extends DacConfiguration
+        implements PhysicsTickListener {
+    // *************************************************************************
+    // constants and loggers
+
+    /**
+     * message logger for this class
+     */
+    final public static Logger logger3
+            = Logger.getLogger(DacLinks.class.getName());
+    /**
+     * local copy of {@link com.jme3.math.Quaternion#IDENTITY}
+     */
+    final private static Quaternion rotateIdentity = new Quaternion();
+    /**
+     * local copy of {@link com.jme3.math.Transform#IDENTITY}
+     */
+    final private static Transform transformIdentity = new Transform();
+    /**
+     * local copy of {@link com.jme3.math.Vector3f#ZERO}
+     */
+    final private static Vector3f translateIdentity = new Vector3f(0f, 0f, 0f);
+    // *************************************************************************
+    // fields
+
+    /**
+     * false until the 1st physics tick, true thereafter, indicating that all
+     * links are ready for dynamic mode
+     */
+    private boolean isReady = false;
+    /**
+     * bone links in a pre-order, depth-first traversal of the link hierarchy
+     */
+    private List<BoneLink> boneLinkList = null;
+    /**
+     * map bone names to bone links
+     */
+    private Map<String, BoneLink> boneLinks = new HashMap<>(32);
+    /**
+     * skeleton being controlled
+     */
+    private Armature skeleton = null;
+    /**
+     * spatial that provides the mesh-coordinate transform
+     */
+    private Spatial transformer = null;
+    /**
+     * torso link for this control
+     */
+    private TorsoLink torsoLink = null;
+    // *************************************************************************
+    // constructors
+
+    /**
+     * Instantiate an enabled control without any linked bones (torso only).
+     */
+    DacLinks() {
+    }
+    // *************************************************************************
+    // new methods exposed
+
+    /**
+     * Access the named bone.
+     * <p>
+     * Allowed only when the control IS added to a spatial.
+     *
+     * @param boneName the name of the skeleton bone to access
+     * @return the pre-existing instance, or null if not found
+     */
+    public Joint findBone(String boneName) {
+        verifyAddedToSpatial("access a bone");
+        Joint result = skeleton.getJoint(boneName);
+        return result;
+    }
+
+    /**
+     * Access the BoneLink for the named bone. Returns null if bone is not
+     * linked, or if the control is not added to a spatial.
+     *
+     * @param boneName the name of the bone (not null, not empty)
+     * @return the pre-existing BoneLink, or null if not found
+     */
+    public BoneLink findBoneLink(String boneName) {
+        BoneLink boneLink = boneLinks.get(boneName);
+        return boneLink;
+    }
+
+    /**
+     * Access the named link. Returns null if the name is invalid, or if the
+     * control is not added to a spatial.
+     *
+     * @param linkName the name of the link (not null, not empty)
+     * @return the pre-existing link, or null if not found
+     */
+    public PhysicsLink findLink(String linkName) {
+        PhysicsLink link;
+        if (linkName.startsWith("Bone:")) {
+            String boneName = linkName.substring(5);
+            link = findBoneLink(boneName);
+        } else {
+            assert linkName.equals("Torso:");
+            link = torsoLink;
+        }
+
+        return link;
+    }
+
+    /**
+     * Access the skeleton. Returns null if the control is not added to a
+     * spatial.
+     *
+     * @return the pre-existing skeleton, or null
+     */
+    public Armature getSkeleton() {
+        return skeleton;
+    }
+
+    /**
+     * Access the TorsoLink. Returns null if the control is not added to a
+     * spatial.
+     *
+     * @return the pre-existing TorsoLink, or null
+     */
+    public TorsoLink getTorsoLink() {
+        return torsoLink;
+    }
+
+    /**
+     * Access the spatial with the mesh-coordinate transform. Returns null if
+     * the control is not added to a spatial.
+     *
+     * @return the pre-existing spatial, or null
+     */
+    Spatial getTransformer() {
+        return transformer;
+    }
+
+    /**
+     * Test whether this control is ready for dynamic mode.
+     *
+     * @return true if ready, otherwise false
+     */
+    public boolean isReady() {
+        return isReady;
+    }
+
+    /**
+     * Enumerate all physics links of the specified type managed by this
+     * control.
+     *
+     * @param <T> subclass of PhysicsLink
+     * @param linkType the subclass of PhysicsLink to search for (not null)
+     * @return a new array of links (not null, not empty)
+     */
+    @SuppressWarnings("unchecked")
+    public <T extends PhysicsLink> List<T> listLinks(Class<T> linkType) {
+        int numLinks = countLinks();
+        List<T> result = new ArrayList<>(numLinks);
+
+        if (torsoLink != null
+                && linkType.isAssignableFrom(torsoLink.getClass())) {
+            result.add((T) torsoLink);
+        }
+        for (BoneLink link : boneLinkList) {
+            if (linkType.isAssignableFrom(link.getClass())) {
+                result.add((T) link);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Enumerate all managed bones of the named link, in a pre-order,
+     * depth-first traversal of the skeleton, such that child bones never
+     * precede their ancestors.
+     *
+     * @param managerName the name of the managing link (not null)
+     * @return a new array of managed bones, including the manager if it is not
+     * the torso
+     */
+    Joint[] listManagedBones(String managerName) {
+        List<Joint> list = new ArrayList<>(8);
+
+        if (torsoName.equals(managerName)) {
+            Joint[] roots = skeleton.getRoots();
+            for (Joint rootBone : roots) {
+                list.add(rootBone);
+                addUnlinkedDescendants(rootBone, list);
+            }
+
+        } else {
+            BoneLink manager = findBoneLink(managerName);
+            if (manager == null) {
+                String msg = "No link named " + managerName;
+                throw new IllegalArgumentException(msg);
+            }
+            Joint managerBone = manager.getBone();
+            list.add(managerBone);
+            addUnlinkedDescendants(managerBone, list);
+        }
+        /*
+         * Convert the list to an array.
+         */
+        int numManagedBones = list.size();
+        Joint[] array = new Joint[numManagedBones];
+        list.toArray(array);
+
+        return array;
+    }
+
+    /**
+     * Enumerate all rigid bodies managed by this control.
+     * <p>
+     * Allowed only when the control IS added to a spatial.
+     *
+     * @return a new array of pre-existing rigid bodies (not null, not empty)
+     */
+    public PhysicsRigidBody[] listRigidBodies() {
+        verifyAddedToSpatial("enumerate rigid bodies");
+
+        int numLinks = countLinks();
+        PhysicsRigidBody[] result = new PhysicsRigidBody[numLinks];
+
+        int linkIndex = 0;
+        if (torsoLink != null) {
+            result[0] = torsoLink.getRigidBody();
+            ++linkIndex;
+        }
+        for (BoneLink boneLink : boneLinkList) {
+            result[linkIndex] = boneLink.getRigidBody();
+            ++linkIndex;
+        }
+        assert linkIndex == numLinks;
+
+        return result;
+    }
+
+    /**
+     * Copy the model's mesh-to-world transform.
+     *
+     * @param storeResult storage for the result (modified if not null)
+     * @return the model's mesh transform (in world coordinates, either
+     * storeResult or a new transform, not null)
+     */
+    Transform meshTransform(Transform storeResult) {
+        Transform result = transformer.getWorldTransform().clone();
+        return result;
+    }
+
+    /**
+     * Calculate the physics transform to match the specified skeleton bone.
+     *
+     * @param bone the skeleton bone to match (not null, unaffected)
+     * @param localOffset the location of the body's center (in the bone's local
+     * coordinates, not null, unaffected)
+     * @param storeResult storage for the result (modified if not null)
+     * @return the calculated physics transform (either storeResult or a new
+     * transform, not null)
+     */
+    Transform physicsTransform(Joint bone, Vector3f localOffset,
+            Transform storeResult) {
+        Transform result
+                = (storeResult == null) ? new Transform() : storeResult;
+        /*
+         * Start with the body's transform in the bone's local coordinates.
+         */
+        result.setTranslation(localOffset);
+        result.setRotation(rotateIdentity);
+        result.setScale(1f);
+        /*
+         * Convert to mesh coordinates.
+         */
+        Transform localToMesh = bone.getModelTransform();
+        result.combineWithParent(localToMesh);
+        /*
+         * Convert to world (physics-space) coordinates.
+         */
+        Transform meshToWorld = meshTransform(null);
+        result.combineWithParent(meshToWorld);
+
+        return result;
+    }
+
+    /**
+     * Rebuild the ragdoll. This is useful if you applied scale to the model
+     * after it was initialized.
+     * <p>
+     * Allowed only when the control IS added to a spatial.
+     */
+    public void rebuild() {
+        verifyAddedToSpatial("rebuild the ragdoll");
+
+        Map<String, BoneLink> saveBones = new HashMap<>(boneLinks);
+        TorsoLink saveTorso = torsoLink;
+
+        Spatial controlledSpatial = getSpatial();
+        removeSpatialData(controlledSpatial);
+        createSpatialData(controlledSpatial);
+
+        for (Map.Entry<String, BoneLink> entry : boneLinks.entrySet()) {
+            String name = entry.getKey();
+            BoneLink newLink = entry.getValue();
+            BoneLink oldLink = saveBones.get(name);
+            newLink.postRebuild(oldLink);
+        }
+        if (torsoLink != null) {
+            torsoLink.postRebuild(saveTorso);
+        }
+    }
+
+    /**
+     * Alter the mass of the specified link.
+     *
+     * @param link the link to modify (not null)
+     * @param mass the desired mass (&gt;0)
+     */
+    public void setMass(PhysicsLink link, float mass) {
+        if (link instanceof BoneLink) {
+            String boneName = link.boneName();
+            setMass(boneName, mass);
+        } else {
+            assert link instanceof TorsoLink;
+            setMass(torsoName, mass);
+        }
+    }
+
+    /**
+     * Verify that this control is ready for dynamic mode, which implies that it
+     * is added to a Spatial.
+     *
+     * @param desiredAction (not null, not empty)
+     */
+    public void verifyReadyForDynamicMode(String desiredAction) {
+        assert desiredAction != null;
+
+        verifyAddedToSpatial(desiredAction);
+
+        if (!isReady) {
+            String message = "Cannot " + desiredAction
+                    + " until the physics has been stepped.";
+            throw new IllegalStateException(message);
+        }
+    }
+    // *************************************************************************
+    // new protected methods
+
+    /**
+     * Access the list of bone links in a pre-order, depth-first traversal of
+     * the link hierarchy.
+     *
+     * @return the pre-existing list (not null)
+     */
+    protected List<BoneLink> getBoneLinks() {
+        assert boneLinkList != null;
+        return boneLinkList;
+    }
+
+    /**
+     * Verify that this control is added to a Spatial.
+     *
+     * @param desiredAction (not null, not empty)
+     */
+    protected void verifyAddedToSpatial(String desiredAction) {
+        assert desiredAction != null;
+
+        Spatial controlledSpatial = getSpatial();
+        if (controlledSpatial == null) {
+            String message = "Cannot " + desiredAction
+                    + " unless the Control is added to a Spatial.";
+            throw new IllegalStateException(message);
+        }
+    }
+    // *************************************************************************
+    // DacConfiguration methods
+
+    /**
+     * Add all managed physics objects to the PhysicsSpace.
+     */
+    @Override
+    protected void addPhysics(PhysicsSpace space) {
+        Vector3f gravity = gravity(null);
+
+        PhysicsRigidBody rigidBody;
+        if (torsoLink != null) {
+            rigidBody = torsoLink.getRigidBody();
+            space.add(rigidBody);
+            rigidBody.setGravity(gravity);
+        }
+
+        for (BoneLink boneLink : boneLinkList) {
+            rigidBody = boneLink.getRigidBody();
+            space.add(rigidBody);
+            rigidBody.setGravity(gravity);
+
+            PhysicsJoint joint = boneLink.getJoint();
+            space.add(joint);
+        }
+    }
+
+    /**
+     * Callback from {@link com.jme3.util.clone.Cloner} to convert this
+     * shallow-cloned control into a deep-cloned one, using the specified cloner
+     * and original to resolve copied fields.
+     *
+     * @param cloner the cloner that's cloning this control (not null, modified)
+     * @param original the control from which this control was shallow-cloned
+     * (not null, unaffected)
+     */
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        super.cloneFields(cloner, original);
+        DacLinks originalDac = (DacLinks) original;
+
+        boneLinkList = cloner.clone(boneLinkList);
+
+        boneLinks = new HashMap<>(32);
+        for (Map.Entry<String, BoneLink> entry
+                : originalDac.boneLinks.entrySet()) {
+            String boneName = entry.getKey();
+            BoneLink link = entry.getValue();
+            BoneLink copyLink = cloner.clone(link);
+            boneLinks.put(boneName, copyLink);
+        }
+
+        skeleton = cloner.clone(skeleton);
+        transformer = cloner.clone(transformer);
+        torsoLink = cloner.clone(torsoLink);
+    }
+
+    /**
+     * Create spatial-dependent data. Invoked each time the control is added to
+     * a spatial. Also invoked by {@link #rebuild()}.
+     *
+     * @param spatial the controlled spatial (not null)
+     */
+    @Override
+    protected void createSpatialData(Spatial spatial) {
+        RagUtils.validate(spatial);
+
+        SkinningControl skeletonControl
+                = spatial.getControl(SkinningControl.class);
+        if (skeletonControl == null) {
+            throw new IllegalArgumentException(
+                    "The controlled spatial must have a SkinningControl. "
+                    + "Make sure the control is there and not on a subnode.");
+        }
+        sortControls(skeletonControl);
+        skeletonControl.setHardwareSkinningPreferred(false);
+        /*
+         * Analyze the model's skeleton.
+         */
+        skeleton = skeletonControl.getArmature();
+        validateSkeleton();
+        String[] tempManagerMap = managerMap(skeleton);
+        int numBones = skeleton.getJointCount();
+        /*
+         * Temporarily set all bones' local translations and rotations to bind.
+         */
+        Transform[] savedTransforms = new Transform[numBones];
+        for (int boneIndex = 0; boneIndex < numBones; ++boneIndex) {
+            Joint bone = skeleton.getJoint(boneIndex);
+            savedTransforms[boneIndex] = bone.getLocalTransform().clone();
+            bone.applyBindPose(); // TODO adjust the scale?
+        }
+        skeleton.update();
+        /*
+         * Find the target meshes and choose the transform spatial.
+         */
+        List<Mesh> targetList = RagUtils.listAnimatedMeshes(spatial, null);
+        Mesh[] targets = new Mesh[targetList.size()];
+        targetList.toArray(targets);
+        transformer = RagUtils.findAnimatedGeometry(spatial);
+        if (transformer == null) {
+            transformer = spatial;
+        }
+        /*
+         * Enumerate mesh-vertex coordinates and assign them to managers.
+         */
+        Map<String, VectorSet> coordsMap
+                = RagUtils.coordsMap(targets, tempManagerMap);
+        /*
+         * Create the torso link.
+         */
+        VectorSet vertexLocations = coordsMap.get(torsoName);
+        createTorsoLink(vertexLocations, targets);
+        /*
+         * Create bone links without joints.
+         */
+        String[] linkedBoneNames = listLinkedBoneNames();
+        for (String boneName : linkedBoneNames) {
+            vertexLocations = coordsMap.get(boneName);
+            createBoneLink(boneName, vertexLocations);
+        }
+        int numLinkedBones = countLinkedBones();
+        assert boneLinks.size() == numLinkedBones;
+        /*
+         * Add joints to connect each BoneLink rigid body with its parent in the
+         * link hierarchy.  Also initialize the boneLinkList.
+         */
+        boneLinkList = new ArrayList<>(numLinkedBones);
+        addJoints(torsoLink);
+        assert boneLinkList.size() == numLinkedBones : boneLinkList.size();
+        /*
+         * Restore the skeleton's pose.
+         */
+        for (int boneIndex = 0; boneIndex < numBones; ++boneIndex) {
+            Joint bone = skeleton.getJoint(boneIndex);
+            bone.setLocalTransform(savedTransforms[boneIndex]);
+        }
+        skeleton.update();
+
+        if (added) {
+            addPhysics(space);
+        }
+
+        logger3.log(Level.FINE, "Created ragdoll for skeleton.");
+    }
+
+    /**
+     * Create a shallow clone for the JME cloner.
+     *
+     * @return a new instance
+     */
+    @Override
+    public DacLinks jmeClone() {
+        try {
+            DacLinks clone = (DacLinks) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException exception) {
+            throw new RuntimeException(exception);
+        }
+    }
+
+    /**
+     * Read the mass of the named bone/torso.
+     *
+     * @param boneName the name of the bone/torso (not null)
+     * @return the mass (&gt;0) or NaN if undetermined
+     */
+    @Override
+    public float mass(String boneName) {
+        float mass;
+        if (getSpatial() == null) {
+            mass = super.mass(boneName);
+        } else if (torsoName.equals(boneName)) {
+            PhysicsRigidBody rigidBody = torsoLink.getRigidBody();
+            mass = rigidBody.getMass();
+        } else if (boneLinks.containsKey(boneName)) {
+            BoneLink link = boneLinks.get(boneName);
+            PhysicsRigidBody rigidBody = link.getRigidBody();
+            mass = rigidBody.getMass();
+        } else {
+            String msg = "No bone/torso named " + boneName;
+            throw new IllegalArgumentException(msg);
+        }
+
+        return mass;
+    }
+
+    /**
+     * De-serialize this control, for example when loading from a J3O file.
+     *
+     * @param im importer (not null)
+     * @throws IOException from importer
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+
+        boneLinkList
+                = ic.readSavableArrayList("boneLinkList", null);
+        for (BoneLink link : boneLinkList) {
+            String name = link.boneName();
+            boneLinks.put(name, link);
+        }
+
+        skeleton = (Armature) ic.readSavable("skeleton", null);
+        transformer = (Spatial) ic.readSavable("transformer", null);
+        torsoLink = (TorsoLink) ic.readSavable("torsoLink", null);
+    }
+
+    /**
+     * Remove all managed physics objects from the PhysicsSpace.
+     */
+    @Override
+    protected void removePhysics(PhysicsSpace space) {
+        assert added;
+
+        PhysicsRigidBody rigidBody;
+        if (torsoLink != null) {
+            rigidBody = torsoLink.getRigidBody();
+            space.remove(rigidBody);
+        }
+
+        for (BoneLink boneLink : boneLinks.values()) {
+            rigidBody = boneLink.getRigidBody();
+            space.remove(rigidBody);
+
+            PhysicsJoint joint = boneLink.getJoint();
+            space.remove(joint);
+        }
+    }
+
+    /**
+     * Remove spatial-dependent data. Invoked each time this control is rebuilt
+     * or removed from a spatial.
+     *
+     * @param spat the previously controlled spatial (unused)
+     */
+    @Override
+    protected void removeSpatialData(Spatial spat) {
+        if (added) {
+            removePhysics(space);
+        }
+
+        skeleton = null;
+
+        boneLinks.clear();
+        boneLinkList = null;
+        torsoLink = null;
+        transformer = null;
+    }
+
+    /**
+     * Alter the viscous damping ratio for all rigid bodies, including new ones.
+     *
+     * @param dampingRatio the desired damping ratio (non-negative, 0&rarr;no
+     * damping, 1&rarr;critically damped, default=0.6)
+     */
+    @Override
+    public void setDamping(float dampingRatio) {
+        super.setDamping(dampingRatio);
+
+        if (getSpatial() != null) {
+            PhysicsRigidBody[] bodies = listRigidBodies();
+            for (PhysicsRigidBody rigidBody : bodies) {
+                rigidBody.setDamping(dampingRatio, dampingRatio);
+            }
+        }
+    }
+
+    /**
+     * Alter this control's gravitational acceleration for Ragdoll mode.
+     *
+     * @param gravity the desired acceleration vector (in physics-space
+     * coordinates, not null, unaffected, default=0,-9.8,0)
+     */
+    @Override
+    public void setGravity(Vector3f gravity) {
+        super.setGravity(gravity);
+
+        if (getSpatial() != null) { // TODO make sure it's in ragdoll mode
+            PhysicsRigidBody[] bodies = listRigidBodies();
+            for (PhysicsRigidBody rigidBody : bodies) {
+                rigidBody.setGravity(gravity);
+            }
+        }
+    }
+
+    /**
+     * Alter the range of motion of the joint connecting the named BoneLink to
+     * its parent in the link hierarchy.
+     *
+     * @param boneName the name of the BoneLink (not null, not empty)
+     * @param rom the desired range of motion (not null)
+     */
+    @Override
+    public void setJointLimits(String boneName, RangeOfMotion rom) {
+        if (!hasBoneLink(boneName)) {
+            String msg = "No linked bone named " + boneName;
+            throw new IllegalArgumentException(msg);
+        }
+
+        super.setJointLimits(boneName, rom);
+
+        if (getSpatial() != null) {
+            BoneLink boneLink = findBoneLink(boneName);
+            SixDofJoint joint = (SixDofJoint) boneLink.getJoint();
+            rom.setupJoint(joint);
+        }
+    }
+
+    /**
+     * Alter the mass of the named bone/torso.
+     *
+     * @param boneName the name of the bone, or torsoName (not null)
+     * @param mass the desired mass (&gt;0)
+     */
+    @Override
+    public void setMass(String boneName, float mass) {
+        super.setMass(boneName, mass);
+
+        if (getSpatial() != null) {
+            PhysicsRigidBody rigidBody;
+            if (torsoName.equals(boneName)) {
+                rigidBody = torsoLink.getRigidBody();
+            } else {
+                BoneLink link = findBoneLink(boneName);
+                rigidBody = link.getRigidBody();
+            }
+            rigidBody.setMass(mass);
+        }
+    }
+
+    /**
+     * Translate the torso to the specified location.
+     *
+     * @param vec desired location (not null, unaffected)
+     */
+    @Override
+    protected void setPhysicsLocation(Vector3f vec) {
+        torsoLink.getRigidBody().setPhysicsLocation(vec);
+    }
+
+    /**
+     * Rotate the torso to the specified orientation.
+     *
+     * @param quat desired orientation (not null, unaffected)
+     */
+    @Override
+    protected void setPhysicsRotation(Quaternion quat) {
+        torsoLink.getRigidBody().setPhysicsRotation(quat);
+    }
+
+    /**
+     * Update this control. Invoked once per frame during the logical-state
+     * update, provided the control is added to a scene. Do not invoke directly
+     * from user code.
+     *
+     * @param tpf the time interval between frames (in seconds, &ge;0)
+     */
+    @Override
+    public void update(float tpf) {
+        verifyAddedToSpatial("update the control");
+        if (!isEnabled()) {
+            return;
+        }
+
+        if (torsoLink != null) {
+            torsoLink.update(tpf);
+        }
+        for (BoneLink boneLink : boneLinkList) {
+            boneLink.update(tpf);
+        }
+    }
+
+    /**
+     * Serialize this control, for example when saving to a J3O file.
+     *
+     * @param ex exporter (not null)
+     * @throws IOException from exporter
+     */
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+
+        int count = countLinkedBones();
+        Savable[] savableArray = new Savable[count];
+        boneLinkList.toArray(savableArray);
+        oc.write(savableArray, "boneLinkList", null);
+
+        oc.write(skeleton, "skeleton", null);
+        oc.write(transformer, "transformer", null);
+        oc.write(torsoLink, "torsoLink", null);
+    }
+    // *************************************************************************
+    // PhysicsTickListener methods
+
+    /**
+     * Callback from Bullet, invoked just after the physics has been stepped.
+     * Used to re-activate any deactivated rigid bodies.
+     *
+     * @param space the space that was just stepped (not null)
+     * @param timeStep the time per physics step (in seconds, &ge;0)
+     */
+    @Override
+    public void physicsTick(PhysicsSpace space, float timeStep) {
+        assert space == getPhysicsSpace();
+
+        torsoLink.postTick();
+        for (BoneLink boneLink : boneLinkList) {
+            boneLink.postTick();
+        }
+
+        isReady = true;
+    }
+
+    /**
+     * Callback from Bullet, invoked just before the physics is stepped. A good
+     * time to clear/apply forces.
+     *
+     * @param space the space that is about to be stepped (not null)
+     * @param timeStep the time per physics step (in seconds, &ge;0)
+     */
+    @Override
+    public void prePhysicsTick(PhysicsSpace space, float timeStep) {
+        assert space == getPhysicsSpace();
+
+        torsoLink.preTick(timeStep);
+        for (BoneLink boneLink : boneLinkList) {
+            boneLink.preTick(timeStep);
+        }
+    }
+    // *************************************************************************
+    // private methods
+
+    /**
+     * Add joints to connect the named bone/torso link with each of its
+     * children. Also fill in the boneLinkList. Note: recursive!
+     *
+     * @param parentName the parent bone/torso link (not null)
+     */
+    private void addJoints(PhysicsLink parentLink) {
+        List<String> childNames = childNames(parentLink);
+        for (String childName : childNames) {
+            /*
+             * Add the joint and configure its range of motion.
+             * Also initialize the BoneLink's parent and its array
+             * of managed bones.
+             */
+            BoneLink childLink = findBoneLink(childName);
+            childLink.addJoint(parentLink);
+            /*
+             * Add the BoneLink to the pre-order list.
+             */
+            boneLinkList.add(childLink);
+
+            addJoints(childLink);
+        }
+    }
+
+    /**
+     * Enumerate all immediate child BoneLinks of the specified bone/torso link.
+     *
+     * @param link the bone/torso link (not null)
+     * @return a new list of bone names
+     */
+    private List<String> childNames(PhysicsLink link) {
+        assert link != null;
+
+        String linkName;
+        if (link == torsoLink) {
+            linkName = torsoName;
+        } else {
+            linkName = link.boneName();
+        }
+
+        List<String> result = new ArrayList<>(8);
+        for (String childName : listLinkedBoneNames()) {
+            Joint bone = findBone(childName);
+            Joint parent = bone.getParent();
+            if (parent != null && findManager(parent).equals(linkName)) {
+                result.add(childName);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Create a jointless BoneLink for the named bone, and add it to the
+     * boneLinks map.
+     *
+     * @param boneName the name of the bone to be linked (not null)
+     * @param vertexLocations the set of vertex locations (not null, not empty)
+     */
+    private void createBoneLink(String boneName, VectorSet vertexLocations) {
+        Joint bone = findBone(boneName);
+        Transform boneToMesh = bone.getModelTransform();
+        Transform meshToBone = boneToMesh.invert();
+        //logger3.log(Level.INFO, "meshToBone = {0}", meshToBone);
+        /*
+         * Create the CollisionShape and locate the center of mass.
+         */
+        CollisionShape shape;
+        Vector3f center;
+        if (vertexLocations == null || vertexLocations.numVectors() == 0) {
+            throw new IllegalStateException("no vertex for " + boneName);
+        } else {
+            center = vertexLocations.mean(null);
+            center.subtractLocal(bone.getModelTransform().getTranslation());
+            shape = createShape(meshToBone, center, vertexLocations);
+        }
+
+        meshToBone.getTranslation().zero();
+        float mass = super.mass(boneName);
+        Vector3f offset = meshToBone.transformVector(center, null);
+        BoneLink link = new BoneLink(this, bone, shape, mass, offset);
+        boneLinks.put(boneName, link);
+    }
+
+    /**
+     * Create a CollisionShape for the specified transform, center, and vertex
+     * locations.
+     *
+     * @param vertexToShape the transform from vertex coordinates to de-scaled
+     * shape coordinates (not null, unaffected)
+     * @param center the location of the shape's center, in vertex coordinates
+     * (not null, unaffected)
+     * @param vertexLocations the set of vertex locations (not null, not empty,
+     * TRASHED)
+     * @return a new CollisionShape
+     */
+    private CollisionShape createShape(Transform vertexToShape, Vector3f center,
+            VectorSet vertexLocations) {
+        int numVectors = vertexLocations.numVectors();
+        assert numVectors > 0 : numVectors;
+
+        Vector3f tempLocation = new Vector3f();
+        int numPoints = vertexLocations.numVectors();
+        float points[] = new float[3 * numPoints];
+        FloatBuffer buffer = vertexLocations.toBuffer();
+        buffer.rewind();
+        int floatIndex = 0;
+        while (buffer.hasRemaining()) {
+            tempLocation.x = buffer.get();
+            tempLocation.y = buffer.get();
+            tempLocation.z = buffer.get();
+            /*
+             * Translate so that vertex coordinates are relative to
+             * the shape's center.
+             */
+            tempLocation.subtractLocal(center);
+            /*
+             * Transform vertex coordinates to de-scaled shape coordinates.
+             */
+            vertexToShape.transformVector(tempLocation, tempLocation);
+            points[floatIndex] = tempLocation.x;
+            points[floatIndex + 1] = tempLocation.y;
+            points[floatIndex + 2] = tempLocation.z;
+            floatIndex += 3;
+        }
+
+        CollisionShape result = new HullCollisionShape(points);
+
+        return result;
+    }
+
+    /**
+     * Create the TorsoLink.
+     *
+     * @param vertexLocations the set of vertex locations (not null, not empty)
+     * @param meshes array of animated meshes to use (not null, unaffected)
+     */
+    private void createTorsoLink(VectorSet vertexLocations, Mesh[] meshes) {
+        if (vertexLocations == null || vertexLocations.numVectors() == 0) {
+            throw new IllegalArgumentException(
+                    "No mesh vertices for the torso."
+                    + " Make sure the root bone is not linked.");
+        }
+        /*
+         * Create the CollisionShape.
+         */
+        Joint bone = RagUtils.findMainBone(skeleton, meshes);
+        assert bone.getParent() == null;
+        Transform boneToMesh = bone.getModelTransform();
+        Transform meshToBone = boneToMesh.invert();
+        Vector3f center = vertexLocations.mean(null);
+        center.subtractLocal(boneToMesh.getTranslation());
+        CollisionShape shape = createShape(meshToBone, center, vertexLocations);
+
+        meshToBone.getTranslation().zero();
+        Vector3f offset = meshToBone.transformVector(center, null);
+
+        Transform meshToModel;
+        Spatial cgm = getSpatial();
+        if (cgm instanceof Node) {
+            Transform modelToMesh
+                    = RagUtils.relativeTransform(transformer, (Node) cgm, null);
+            meshToModel = modelToMesh.invert();
+        } else {
+            meshToModel = transformIdentity;
+        }
+
+        float mass = super.mass(torsoName);
+        torsoLink = new TorsoLink(this, bone, shape, mass, meshToModel, offset);
+    }
+
+    /**
+     * Sort the controls of the controlled spatial, such that this control will
+     * come BEFORE the specified SkinningControl.
+     *
+     * @param skinningControl (not null)
+     */
+    private void sortControls(SkinningControl skinningControl) {
+        assert skinningControl != null;
+
+        int dacIndex = RagUtils.findIndex(spatial, this);
+        assert dacIndex != -1;
+        int scIndex = RagUtils.findIndex(spatial, skinningControl);
+        assert scIndex != -1;
+        assert dacIndex != scIndex;
+
+        if (dacIndex > scIndex) {
+            /*
+             * Remove the SkinningControl and re-add it to make sure it will get
+             * updated *after* this control.
+             */
+            spatial.removeControl(skinningControl);
+            spatial.addControl(skinningControl);
+
+            dacIndex = RagUtils.findIndex(spatial, this);
+            assert dacIndex != -1;
+            scIndex = RagUtils.findIndex(spatial, skinningControl);
+            assert scIndex != -1;
+            assert dacIndex < scIndex;
+        }
+    }
+
+    /**
+     * Validate the model's skeleton.
+     */
+    private void validateSkeleton() {
+        RagUtils.validate(skeleton);
+
+        for (String boneName : listLinkedBoneNames()) {
+            Joint bone = findBone(boneName);
+            if (bone == null) {
+                String msg = String.format(
+                        "Linked bone %s not found in skeleton.", boneName);
+                throw new IllegalArgumentException(msg);
+            }
+            if (bone.getParent() == null) {
+                logger3.log(Level.WARNING, "Linked bone {0} is a root bone.",
+                        boneName);
+            }
+        }
+    }
+}

+ 530 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/DynamicAnimControl.java

@@ -0,0 +1,530 @@
+/*
+ * Copyright (c) 2018-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 com.jme3.bullet.animation;
+
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.PhysicsCollisionEvent;
+import com.jme3.bullet.collision.PhysicsCollisionListener;
+import com.jme3.bullet.collision.PhysicsCollisionObject;
+import com.jme3.bullet.objects.PhysicsRigidBody;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.util.SafeArrayList;
+import com.jme3.util.clone.Cloner;
+import java.io.IOException;
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * Before adding this control to a spatial, configure it by invoking
+ * {@link #link(java.lang.String, float, com.jme3.bullet.animation.RangeOfMotion)}
+ * for each bone that should have its own rigid body. Leave unlinked bones near
+ * the root of the skeleton to form the torso of the ragdoll.
+ * <p>
+ * When you add the control to a spatial, it generates a ragdoll consisting of a
+ * rigid body for the torso and another for each linked bone. It also creates a
+ * SixDofJoint connecting each rigid body to its parent in the link hierarchy.
+ * The mass of each rigid body and the range-of-motion of each joint can be
+ * reconfigured on the fly.
+ * <p>
+ * Each link is either dynamic (driven by forces and torques) or kinematic
+ * (unperturbed by forces and torques). Transitions from dynamic to kinematic
+ * can be immediate or gradual.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold [email protected]
+ *
+ * Based on KinematicRagdollControl by Normen Hansen and Rémy Bouquet (Nehon).
+ */
+public class DynamicAnimControl
+        extends DacLinks
+        implements PhysicsCollisionListener {
+    // *************************************************************************
+    // constants and loggers
+
+    /**
+     * message logger for this class
+     */
+    final public static Logger logger35
+            = Logger.getLogger(DynamicAnimControl.class.getName());
+    // *************************************************************************
+    // fields
+
+    /**
+     * calculated total mass
+     */
+    private float ragdollMass = 0f;
+    /**
+     * list of registered collision listeners
+     */
+    private List<RagdollCollisionListener> collisionListeners
+            = new SafeArrayList<>(RagdollCollisionListener.class);
+    /*
+     * center-of-mass actual location (in physics-space coordinates)
+     */
+    private Vector3f centerLocation = new Vector3f();
+    /*
+     * center-of-mass estimated velocity (psu/second in physics-space coordinates)
+     */
+    private Vector3f centerVelocity = new Vector3f();
+    // *************************************************************************
+    // constructors
+
+    /**
+     * Instantiate an enabled control without any linked bones (torso only).
+     */
+    public DynamicAnimControl() {
+    }
+    // *************************************************************************
+    // new methods exposed
+
+    /**
+     * Add a collision listener to this control.
+     *
+     * @param listener (not null, alias created)
+     */
+    public void addCollisionListener(RagdollCollisionListener listener) {
+        collisionListeners.add(listener);
+    }
+
+    /**
+     * Begin blending the specified link and all its descendants to kinematic
+     * animation.
+     *
+     * @param rootLink the root of the subtree to bind (not null)
+     * @param blendInterval the duration of the blend interval (in seconds,
+     * &ge;0)
+     */
+    public void animateSubtree(PhysicsLink rootLink, float blendInterval) {
+        verifyAddedToSpatial("change modes");
+        blendSubtree(rootLink, KinematicSubmode.Animated, blendInterval);
+    }
+
+    /**
+     * Begin blending all links to purely kinematic mode, driven by animation.
+     * TODO callback when the transition completes
+     * <p>
+     * Allowed only when the control IS added to a spatial.
+     *
+     * @param blendInterval the duration of the blend interval (in seconds,
+     * &ge;0)
+     * @param endModelTransform the desired local transform for the controlled
+     * spatial when the transition completes or null for no change to local
+     * transform (unaffected)
+     */
+    public void blendToKinematicMode(float blendInterval,
+            Transform endModelTransform) {
+        verifyAddedToSpatial("change modes");
+
+        getTorsoLink().blendToKinematicMode(KinematicSubmode.Animated, blendInterval,
+                endModelTransform);
+        for (BoneLink boneLink : getBoneLinks()) {
+            boneLink.blendToKinematicMode(KinematicSubmode.Animated,
+                    blendInterval);
+        }
+    }
+
+    /**
+     * Calculate the ragdoll's total mass and center of mass.
+     * <p>
+     * Allowed only when the control IS added to a spatial.
+     *
+     * @param storeLocation storage for the location of the center (in
+     * physics-space coordinates, modified if not null)
+     * @param storeVelocity storage for the velocity of the center (psu/second
+     * in physics-space coordinates, modified if not null)
+     * @return the total mass (&gt;0)
+     */
+    public float centerOfMass(Vector3f storeLocation, Vector3f storeVelocity) {
+        verifyReadyForDynamicMode("calculate the center of mass");
+
+        recalculateCenter();
+        if (storeLocation != null) {
+            storeLocation.set(centerLocation);
+        }
+        if (storeVelocity != null) {
+            storeVelocity.set(centerVelocity);
+        }
+
+        return ragdollMass;
+    }
+
+    /**
+     * Alter the contact-response setting of the specified link and all its
+     * descendants. Note: recursive!
+     * <p>
+     * Allowed only when the control IS added to a spatial.
+     *
+     * @param rootLink the root of the subtree to modify (not null)
+     * @param desiredResponse true for the usual rigid-body response, false for
+     * ghost-like response
+     */
+    public void setContactResponseSubtree(PhysicsLink rootLink,
+            boolean desiredResponse) {
+        verifyAddedToSpatial("change modes");
+
+        PhysicsRigidBody rigidBody = rootLink.getRigidBody();
+        rigidBody.setContactResponse(desiredResponse);
+
+        PhysicsLink[] children = rootLink.listChildren();
+        for (PhysicsLink child : children) {
+            setContactResponseSubtree(child, desiredResponse);
+        }
+    }
+
+    /**
+     * Immediately put the specified link and all its ancestors (excluding the
+     * torso) into dynamic mode. Note: recursive!
+     * <p>
+     * Allowed only when the control IS added to a spatial.
+     *
+     * @param startLink the start of the chain to modify (not null)
+     * @param chainLength the maximum number of links to modify (&ge;0)
+     * @param uniformAcceleration the uniform acceleration vector (in
+     * physics-space coordinates, not null, unaffected)
+     */
+    public void setDynamicChain(PhysicsLink startLink, int chainLength,
+            Vector3f uniformAcceleration) {
+        if (chainLength == 0) {
+            return;
+        }
+        verifyAddedToSpatial("change modes");
+
+        if (startLink instanceof BoneLink) {
+            BoneLink boneLink = (BoneLink) startLink;
+            boneLink.setDynamic(uniformAcceleration);
+        }
+
+        PhysicsLink parent = startLink.getParent();
+        if (parent != null && chainLength > 1) {
+            setDynamicChain(parent, chainLength - 1, uniformAcceleration);
+        }
+    }
+
+    /**
+     * Immediately put the specified link and all its descendants into dynamic
+     * mode. Note: recursive!
+     * <p>
+     * Allowed only when the control IS added to a spatial.
+     *
+     * @param rootLink the root of the subtree to modify (not null)
+     * @param uniformAcceleration the uniform acceleration vector (in
+     * physics-space coordinates, not null, unaffected)
+     */
+    public void setDynamicSubtree(PhysicsLink rootLink,
+            Vector3f uniformAcceleration) {
+        verifyAddedToSpatial("change modes");
+
+        if (rootLink == getTorsoLink()) {
+            getTorsoLink().setDynamic(uniformAcceleration);
+        } else if (rootLink instanceof BoneLink) {
+            BoneLink boneLink = (BoneLink) rootLink;
+            boneLink.setDynamic(uniformAcceleration);
+        }
+
+        PhysicsLink[] children = rootLink.listChildren();
+        for (PhysicsLink child : children) {
+            setDynamicSubtree(child, uniformAcceleration);
+        }
+    }
+
+    /**
+     * Immediately put all links into purely kinematic mode.
+     * <p>
+     * Allowed only when the control IS added to a spatial.
+     */
+    public void setKinematicMode() {
+        verifyAddedToSpatial("set kinematic mode");
+
+        Transform localTransform = getSpatial().getLocalTransform();
+        blendToKinematicMode(0f, localTransform);
+    }
+
+    /**
+     * Immediately put this control into ragdoll mode.
+     * <p>
+     * Allowed only when the control IS added to a spatial and all links are
+     * "ready".
+     */
+    public void setRagdollMode() {
+        verifyReadyForDynamicMode("set ragdoll mode");
+
+        TorsoLink torsoLink = getTorsoLink();
+        Vector3f acceleration = gravity(null);
+        torsoLink.setDynamic(acceleration);
+        for (BoneLink boneLink : getBoneLinks()) {
+            boneLink.setDynamic(acceleration);
+        }
+    }
+    // *************************************************************************
+    // DacPhysicsLinks methods
+
+    /**
+     * Add all managed physics objects to the PhysicsSpace.
+     */
+    @Override
+    protected void addPhysics(PhysicsSpace space) {
+        super.addPhysics(space);
+
+        space.addCollisionListener(this);
+        space.addTickListener(this);
+    }
+
+    /**
+     * Callback from {@link com.jme3.util.clone.Cloner} to convert this
+     * shallow-cloned control into a deep-cloned one, using the specified cloner
+     * and original to resolve copied fields.
+     *
+     * @param cloner the cloner that's cloning this control (not null, modified)
+     * @param original the control from which this control was shallow-cloned
+     * (not null, unaffected)
+     */
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        super.cloneFields(cloner, original);
+
+        collisionListeners = cloner.clone(collisionListeners);
+        centerLocation = cloner.clone(centerLocation);
+        centerVelocity = cloner.clone(centerVelocity);
+    }
+
+    /**
+     * Create a shallow clone for the JME cloner.
+     *
+     * @return a new instance
+     */
+    @Override
+    public DynamicAnimControl jmeClone() {
+        try {
+            DynamicAnimControl clone = (DynamicAnimControl) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException exception) {
+            throw new RuntimeException(exception);
+        }
+    }
+
+    /**
+     * De-serialize this control, for example when loading from a J3O file.
+     *
+     * @param im the importer (not null)
+     * @throws IOException from the importer
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+
+        // isReady and collisionListeners not read
+        ragdollMass = ic.readFloat("ragdollMass", 1f);
+        centerLocation
+                = (Vector3f) ic.readSavable("centerLocation", new Vector3f());
+        centerVelocity
+                = (Vector3f) ic.readSavable("centerVelocity", new Vector3f());
+    }
+
+    /**
+     * Remove all managed physics objects from the PhysicsSpace.
+     */
+    @Override
+    protected void removePhysics(PhysicsSpace space) {
+        super.removePhysics(space);
+
+        space.removeCollisionListener(this);
+        space.removeTickListener(this);
+    }
+
+    /**
+     * Serialize this control, for example when saving to a J3O file.
+     *
+     * @param ex the exporter (not null)
+     * @throws IOException from the exporter
+     */
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+
+        // isReady and collisionListeners not written
+        oc.write(ragdollMass, "ragdollMass", 1f);
+        oc.write(centerLocation, "centerLocation", null);
+        oc.write(centerVelocity, "centerVelocity", null);
+    }
+    // *************************************************************************
+    // PhysicsCollisionListener methods
+
+    /**
+     * For internal use only: callback for collision events.
+     *
+     * @param event (not null)
+     */
+    @Override
+    public void collision(PhysicsCollisionEvent event) {
+        if (event.getNodeA() == null && event.getNodeB() == null) {
+            return;
+        }
+        /*
+         * Determine which bone was involved (if any) and also the
+         * other collision object involved.
+         */
+        boolean isThisControlInvolved = false;
+        PhysicsLink physicsLink = null;
+        PhysicsCollisionObject otherPco = null;
+        PhysicsCollisionObject pcoA = event.getObjectA();
+        PhysicsCollisionObject pcoB = event.getObjectB();
+
+        Object userA = pcoA.getUserObject();
+        Object userB = pcoB.getUserObject();
+        if (userA instanceof PhysicsLink) {
+            physicsLink = (PhysicsLink) userA;
+            DacLinks control = physicsLink.getControl();
+            if (control == this) {
+                isThisControlInvolved = true;
+            }
+            otherPco = pcoB;
+        }
+        if (userB instanceof PhysicsLink) {
+            physicsLink = (PhysicsLink) userB;
+            DacLinks control = physicsLink.getControl();
+            if (control == this) {
+                isThisControlInvolved = true;
+            }
+            otherPco = pcoA;
+        }
+        /*
+         * Discard collisions that don't involve this control.
+         */
+        if (!isThisControlInvolved) {
+            return;
+        }
+        /*
+         * Discard low-impulse collisions.
+         */
+        float impulseThreshold = eventDispatchImpulseThreshold();
+        if (event.getAppliedImpulse() < impulseThreshold) {
+            return;
+        }
+        /*
+         * Dispatch an event.
+         */
+        for (RagdollCollisionListener listener : collisionListeners) {
+            listener.collide(physicsLink, otherPco, event);
+        }
+    }
+    // *************************************************************************
+    // private methods
+
+    /**
+     * Begin blending the descendents of the specified link to the specified
+     * kinematic submode. Note: recursive!
+     *
+     * @param rootLink the root of the subtree to blend (not null)
+     * @param submode an enum value (not null)
+     * @param blendInterval the duration of the blend interval (in seconds,
+     * &ge;0)
+     */
+    private void blendDescendants(PhysicsLink rootLink,
+            KinematicSubmode submode, float blendInterval) {
+        assert rootLink != null;
+        assert submode != null;
+        assert blendInterval >= 0f : blendInterval;
+
+        PhysicsLink[] children = rootLink.listChildren();
+        for (PhysicsLink child : children) {
+            if (child instanceof BoneLink) {
+                BoneLink boneLink = (BoneLink) child;
+                boneLink.blendToKinematicMode(submode, blendInterval);
+            }
+            blendDescendants(child, submode, blendInterval);
+        }
+    }
+
+    /**
+     * Begin blending the specified link and all its descendants to the
+     * specified kinematic submode.
+     *
+     * @param rootLink the root of the subtree to blend (not null)
+     * @param submode the desired submode (not null)
+     * @param blendInterval the duration of the blend interval (in seconds,
+     * &ge;0)
+     */
+    private void blendSubtree(PhysicsLink rootLink, KinematicSubmode submode,
+            float blendInterval) {
+        assert rootLink != null;
+        assert submode != null;
+        assert blendInterval >= 0f : blendInterval;
+
+        blendDescendants(rootLink, submode, blendInterval);
+
+        if (rootLink == getTorsoLink()) {
+            getTorsoLink().blendToKinematicMode(submode, blendInterval, null);
+        } else if (rootLink instanceof BoneLink) {
+            BoneLink boneLink = (BoneLink) rootLink;
+            boneLink.blendToKinematicMode(submode, blendInterval);
+        }
+    }
+
+    /**
+     * Recalculate the total mass of the ragdoll. Also updates the location and
+     * estimated velocity of the center of mass.
+     */
+    private void recalculateCenter() {
+        double massSum = 0.0;
+        Vector3f locationSum = new Vector3f();
+        Vector3f velocitySum = new Vector3f();
+        Vector3f tmpVector = new Vector3f();
+        List<PhysicsLink> links = listLinks(PhysicsLink.class);
+        for (PhysicsLink link : links) {
+            PhysicsRigidBody rigidBody = link.getRigidBody();
+            float mass = rigidBody.getMass();
+            massSum += mass;
+
+            rigidBody.getPhysicsLocation(tmpVector);
+            tmpVector.multLocal(mass);
+            locationSum.addLocal(tmpVector);
+
+            link.velocity(tmpVector);
+            tmpVector.multLocal(mass);
+            velocitySum.addLocal(tmpVector);
+        }
+
+        float invMass = (float) (1.0 / massSum);
+        locationSum.mult(invMass, centerLocation);
+        velocitySum.mult(invMass, centerVelocity);
+        ragdollMass = (float) massSum;
+    }
+}

+ 50 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/KinematicSubmode.java

@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2018-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 com.jme3.bullet.animation;
+
+/**
+ * Enumerate submodes for a link in kinematic mode.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold [email protected]
+ */
+public enum KinematicSubmode {
+    /**
+     * driven by animation (if any)
+     */
+    Animated,
+    /**
+     * frozen in the transform it had when blending started
+     */
+    Frozen;
+}

+ 634 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/PhysicsLink.java

@@ -0,0 +1,634 @@
+/*
+ * Copyright (c) 2018-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 com.jme3.bullet.animation;
+
+import com.jme3.anim.Joint;
+import com.jme3.bullet.collision.shapes.CollisionShape;
+import com.jme3.bullet.joints.PhysicsJoint;
+import com.jme3.bullet.objects.PhysicsRigidBody;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * The abstract base class used by DynamicAnimControl to link pieces of a JME
+ * model to their corresponding collision objects in a ragdoll. Subclasses
+ * include BoneLink and TorsoLink. The links in each DynamicAnimControl form a
+ * hierarchy with the TorsoLink at its root.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold [email protected]
+ *
+ * Based on KinematicRagdollControl by Normen Hansen and Rémy Bouquet (Nehon).
+ */
+abstract public class PhysicsLink
+        implements JmeCloneable, Savable {
+    // *************************************************************************
+    // constants and loggers
+
+    /**
+     * message logger for this class
+     */
+    final public static Logger logger
+            = Logger.getLogger(PhysicsLink.class.getName());
+    // *************************************************************************
+    // fields
+
+    /**
+     * immediate children in the link hierarchy (not null)
+     */
+    private ArrayList<PhysicsLink> children = new ArrayList<>(8);
+    /**
+     * corresponding bone in the skeleton (not null)
+     */
+    private Joint bone;
+    /**
+     * scene-graph control that manages this link (not null)
+     */
+    private DacLinks control;
+    /**
+     * duration of the most recent blend interval (in seconds, &ge;0)
+     */
+    private float blendInterval = 1f;
+    /**
+     * weighting of kinematic movement (&ge;0, &le;1, 0=purely dynamic, 1=purely
+     * kinematic, default=1, progresses from 0 to 1 during the blend interval)
+     */
+    private float kinematicWeight = 1f;
+    /**
+     * joint between the rigid body and the parent's rigid body, or null if not
+     * yet created
+     */
+    private PhysicsJoint joint = null;
+    /**
+     * parent/manager in the link hierarchy, or null if none
+     */
+    private PhysicsLink parent = null;
+    /**
+     * linked rigid body in the ragdoll (not null)
+     */
+    private PhysicsRigidBody rigidBody;
+    /**
+     * transform of the rigid body as of the most recent update (in
+     * physics-space coordinates, updated in kinematic mode only)
+     */
+    private Transform kpTransform = new Transform();
+    /**
+     * estimate of the body's linear velocity as of the most recent update
+     * (psu/sec in physics-space coordinates, kinematic mode only)
+     */
+    private Vector3f kpVelocity = new Vector3f();
+    /**
+     * location of the rigid body's center (in the skeleton bone's local
+     * coordinates)
+     */
+    private Vector3f localOffset;
+    // *************************************************************************
+    // constructors
+
+    /**
+     * No-argument constructor needed by SavableClassUtil. Do not invoke
+     * directly!
+     */
+    public PhysicsLink() {
+    }
+
+    /**
+     * Instantiate a purely kinematic link between the specified skeleton bone
+     * and the specified rigid body.
+     *
+     * @param control the control that will manage this link (not null, alias
+     * created)
+     * @param bone the corresponding bone (not null, alias created)
+     * @param collisionShape the desired shape (not null, alias created)
+     * @param mass the mass (in physics-mass units)
+     * @param localOffset the location of the body's center (in the bone's local
+     * coordinates, not null, unaffected)
+     */
+    PhysicsLink(DacLinks control, Joint bone, CollisionShape collisionShape,
+            float mass, Vector3f localOffset) {
+        assert control != null;
+        assert bone != null;
+        assert collisionShape != null;
+        assert localOffset != null;
+
+        this.control = control;
+        this.bone = bone;
+        rigidBody = createRigidBody(mass, collisionShape);
+
+        logger.log(Level.FINE, "Creating link for bone {0} with mass={1}",
+                new Object[]{bone.getName(), rigidBody.getMass()});
+
+        this.localOffset = localOffset.clone();
+        updateKPTransform();
+    }
+    // *************************************************************************
+    // new methods exposed
+
+    /**
+     * Read the name of the corresponding bone.
+     *
+     * @return the bone name (not null)
+     */
+    public String boneName() {
+        String boneName = bone.getName();
+
+        assert boneName != null;
+        return boneName;
+    }
+
+    /**
+     * Count this link's immediate children in the link hierarchy.
+     *
+     * @return the count (&ge;0)
+     */
+    public int countChildren() {
+        int numChildren = children.size();
+        return numChildren;
+    }
+
+    /**
+     * Access the corresponding bone.
+     *
+     * @return the pre-existing instance (not null)
+     */
+    final public Joint getBone() {
+        assert bone != null;
+        return bone;
+    }
+
+    /**
+     * Access the control that manages this link.
+     *
+     * @return the pre-existing instance (not null)
+     */
+    public DacLinks getControl() {
+        assert control != null;
+        return control;
+    }
+
+    /**
+     * Access the joint between this link's rigid body and that of its parent.
+     *
+     * @return the pre-existing instance, or null for the torso
+     */
+    public PhysicsJoint getJoint() {
+        return joint;
+    }
+
+    /**
+     * Access this link's parent/manager in the link hierarchy.
+     *
+     * @return the link, or null if none
+     */
+    public PhysicsLink getParent() {
+        return parent;
+    }
+
+    /**
+     * Access the linked rigid body.
+     *
+     * @return the pre-existing instance (not null)
+     */
+    public PhysicsRigidBody getRigidBody() {
+        assert rigidBody != null;
+        return rigidBody;
+    }
+
+    /**
+     * Test whether the link is in kinematic mode.
+     *
+     * @return true if kinematic, or false if purely dynamic
+     */
+    public boolean isKinematic() {
+        if (kinematicWeight > 0f) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Read the kinematic weight of this link.
+     *
+     * @return 0 if purely dynamic, 1 if purely kinematic
+     */
+    public float kinematicWeight() {
+        assert kinematicWeight >= 0f : kinematicWeight;
+        assert kinematicWeight <= 1f : kinematicWeight;
+        return kinematicWeight;
+    }
+
+    /**
+     * Enumerate this link's immediate children in the link hierarchy.
+     *
+     * @return a new array (not null)
+     */
+    public PhysicsLink[] listChildren() {
+        int numChildren = children.size();
+        PhysicsLink[] result = new PhysicsLink[numChildren];
+        children.toArray(result);
+
+        return result;
+    }
+
+    /**
+     * Unambiguously identify this link by name, within its DynamicAnimControl.
+     *
+     * @return a text string (not null, not empty)
+     */
+    abstract public String name();
+
+    /**
+     * Calculate a physics transform for the rigid body (to match the skeleton
+     * bone).
+     *
+     * @param storeResult storage for the result (modified if not null)
+     * @return the calculated transform (in physics-space coordinates, either
+     * storeResult or a new transform, not null)
+     */
+    public Transform physicsTransform(Transform storeResult) {
+        Transform result
+                = (storeResult == null) ? new Transform() : storeResult;
+        if (isKinematic()) {
+            result.set(kpTransform);
+        } else {
+            rigidBody.getPhysicsLocation(result.getTranslation());
+            rigidBody.getPhysicsRotation(result.getRotation());
+            result.setScale(rigidBody.getCollisionShape().getScale());
+        }
+
+        return result;
+    }
+
+    /**
+     * Copy animation data from the specified link, which must correspond to the
+     * same bone.
+     *
+     * @param oldLink the link to copy from (not null, unaffected)
+     */
+    void postRebuild(PhysicsLink oldLink) {
+        assert oldLink != null;
+        assert oldLink.bone == bone;
+
+        if (oldLink.isKinematic()) {
+            blendInterval = oldLink.blendInterval;
+            kinematicWeight = oldLink.kinematicWeight;
+        } else {
+            blendInterval = 0f;
+            kinematicWeight = 1f;
+        }
+    }
+
+    /**
+     * Internal callback, invoked just AFTER the physics is stepped.
+     */
+    void postTick() {
+        if (!isKinematic()) {
+            rigidBody.activate();
+        }
+    }
+
+    /**
+     * Internal callback, invoked just BEFORE the physics is stepped.
+     *
+     * @param timeStep the physics time step (in seconds, &ge;0)
+     */
+    void preTick(float timeStep) {
+        if (isKinematic()) {
+            rigidBody.setPhysicsLocation(kpTransform.getTranslation());
+            rigidBody.setPhysicsRotation(kpTransform.getRotation());
+            rigidBody.getCollisionShape().setScale(kpTransform.getScale());
+        }
+    }
+
+    /**
+     * Immediately put this link into dynamic mode. The control must be "ready".
+     *
+     * @param uniformAcceleration the uniform acceleration vector to apply (in
+     * physics-space coordinates, not null, unaffected)
+     */
+    public void setDynamic(Vector3f uniformAcceleration) {
+        control.verifyReadyForDynamicMode("put link into dynamic mode");
+
+        setKinematicWeight(0f);
+        rigidBody.setGravity(uniformAcceleration);
+    }
+
+    /**
+     * Internal callback, invoked once per frame during the logical-state
+     * update, provided the control is added to a scene.
+     *
+     * @param tpf the time interval between frames (in seconds, &ge;0)
+     */
+    void update(float tpf) {
+        assert tpf >= 0f : tpf;
+
+        if (kinematicWeight > 0f) {
+            kinematicUpdate(tpf);
+        } else {
+            dynamicUpdate();
+        }
+    }
+
+    /**
+     * Copy the body's linear velocity, or an estimate thereof.
+     *
+     * @param storeResult (modified if not null)
+     * @return a new velocity vector (psu/sec in physics-space coordinates)
+     */
+    Vector3f velocity(Vector3f storeResult) {
+        Vector3f result = (storeResult == null) ? new Vector3f() : storeResult;
+        if (isKinematic()) {
+            result.set(kpVelocity);
+        } else {
+            assert !rigidBody.isKinematic();
+            rigidBody.getLinearVelocity(result);
+        }
+
+        return result;
+    }
+    // *************************************************************************
+    // new protected methods
+
+    /**
+     * Begin blending this link to a purely kinematic mode.
+     *
+     * @param blendInterval the duration of the blend interval (in seconds,
+     * &ge;0)
+     */
+    protected void blendToKinematicMode(float blendInterval) {
+        assert blendInterval >= 0f : blendInterval;
+
+        this.blendInterval = blendInterval;
+        setKinematicWeight(Float.MIN_VALUE); // non-zero to trigger blending
+    }
+
+    /**
+     * Update this link in Dynamic mode, setting the linked bone's transform
+     * based on the transform of the rigid body.
+     */
+    abstract protected void dynamicUpdate();
+
+    /**
+     * Update this link in blended Kinematic mode.
+     *
+     * @param tpf the time interval between frames (in seconds, &ge;0)
+     */
+    protected void kinematicUpdate(float tpf) {
+        assert tpf >= 0f : tpf;
+        assert rigidBody.isKinematic();
+        /*
+         * If blending, increase the kinematic weight.
+         */
+        if (blendInterval == 0f) {
+            setKinematicWeight(1f); // done blending
+        } else {
+            setKinematicWeight(kinematicWeight + tpf / blendInterval);
+        }
+        /*
+         * If we didn't need kpVelocity, we could defer this
+         * calculation until the preTick().
+         */
+        Vector3f previousLocation = kpTransform.getTranslation(null);
+        updateKPTransform();
+        if (tpf > 0f) {
+            kpTransform.getTranslation().subtract(previousLocation, kpVelocity);
+            kpVelocity.divideLocal(tpf);
+        }
+    }
+
+    /**
+     * Copy the local offset of this link.
+     *
+     * @param storeResult storage for the result (modified if not null)
+     * @return the offset (in bone local coordinates, either storeResult or a
+     * new vector, not null)
+     */
+    protected Vector3f localOffset(Vector3f storeResult) {
+        Vector3f result = (storeResult == null) ? new Vector3f() : storeResult;
+        result.set(localOffset);
+        return result;
+    }
+
+    /**
+     * Assign a physics joint to this link, or cancel the assigned joint.
+     *
+     * @param joint (may be null, alias created)
+     */
+    final protected void setJoint(PhysicsJoint joint) {
+        this.joint = joint;
+    }
+
+    /**
+     * Assign a parent/manager for this link.
+     *
+     * @param parent (not null, alias created)
+     */
+    final protected void setParent(PhysicsLink parent) {
+        assert parent != null;
+        assert this.parent == null;
+        this.parent = parent;
+        parent.children.add(this);
+    }
+
+    /**
+     * Alter the rigid body for this link.
+     *
+     * @param body the desired rigid body (not null, alias created)
+     */
+    protected void setRigidBody(PhysicsRigidBody body) {
+        assert body != null;
+        assert rigidBody != null;
+        rigidBody = body;
+    }
+    // *************************************************************************
+    // JmeCloneable methods
+
+    /**
+     * Callback from {@link com.jme3.util.clone.Cloner} to convert this
+     * shallow-cloned link into a deep-cloned one, using the specified cloner
+     * and original to resolve copied fields.
+     *
+     * @param cloner the cloner that's cloning this link (not null)
+     * @param original the instance from which this link was shallow-cloned
+     * (unused)
+     */
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        bone = cloner.clone(bone);
+        control = cloner.clone(control);
+        children = cloner.clone(children);
+        joint = cloner.clone(joint);
+        parent = cloner.clone(parent);
+        rigidBody = cloner.clone(rigidBody);
+        kpTransform = cloner.clone(kpTransform);
+        kpVelocity = cloner.clone(kpVelocity);
+        localOffset = cloner.clone(localOffset);
+    }
+
+    /**
+     * Create a shallow clone for the JME cloner.
+     *
+     * @return a new instance
+     */
+    @Override
+    public PhysicsLink jmeClone() {
+        try {
+            PhysicsLink clone = (PhysicsLink) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException exception) {
+            throw new RuntimeException(exception);
+        }
+    }
+    // *************************************************************************
+    // Savable methods
+
+    /**
+     * De-serialize this link, for example when loading from a J3O file.
+     *
+     * @param im importer (not null)
+     * @throws IOException from importer
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule ic = im.getCapsule(this);
+
+        children = ic.readSavableArrayList("children", new ArrayList(1));
+        bone = (Joint) ic.readSavable("bone", null);
+        control = (DacLinks) ic.readSavable("control", null);
+        blendInterval = ic.readFloat("blendInterval", 1f);
+        kinematicWeight = ic.readFloat("kinematicWeight", 1f);
+        joint = (PhysicsJoint) ic.readSavable("joint", null);
+        parent = (PhysicsLink) ic.readSavable("parent", null);
+        rigidBody = (PhysicsRigidBody) ic.readSavable("rigidBody", null);
+        kpTransform
+                = (Transform) ic.readSavable("kpTransform", new Transform());
+        kpVelocity = (Vector3f) ic.readSavable("kpVelocity", new Vector3f());
+        localOffset = (Vector3f) ic.readSavable("offset", new Vector3f());
+    }
+
+    /**
+     * Serialize this link, for example when saving to a J3O file.
+     *
+     * @param ex exporter (not null)
+     * @throws IOException from exporter
+     */
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        OutputCapsule oc = ex.getCapsule(this);
+
+        oc.writeSavableArrayList(children, "children", null);
+        oc.write(bone, "bone", null);
+        oc.write(control, "control", null);
+        oc.write(blendInterval, "blendInterval", 1f);
+        oc.write(kinematicWeight, "kinematicWeight", 1f);
+        oc.write(joint, "joint", null);
+        oc.write(parent, "parent", null);
+        oc.write(rigidBody, "rigidBody", null);
+        oc.write(kpTransform, "kpTransform", null);
+        oc.write(kpVelocity, "kpVelocity", null);
+        oc.write(localOffset, "offset", null);
+    }
+    // *************************************************************************
+    // private methods
+
+    /**
+     * Create and configure a rigid body for this link.
+     *
+     * @param linkConfig the link configuration (not null)
+     * @param collisionShape the desired shape (not null, alias created)
+     * @return a new instance, not in any PhysicsSpace
+     */
+    private PhysicsRigidBody createRigidBody(float mass,
+            CollisionShape collisionShape) {
+        assert collisionShape != null;
+
+        PhysicsRigidBody body = new PhysicsRigidBody(collisionShape, mass);
+
+        float viscousDamping = control.damping();
+        body.setDamping(viscousDamping, viscousDamping);
+
+        body.setKinematic(true);
+        body.setUserObject(this);
+
+        return body;
+    }
+
+    /**
+     * Alter the kinematic weight and copy the physics transform and velocity
+     * info as needed.
+     *
+     * @param weight (&ge;0)
+     */
+    private void setKinematicWeight(float weight) {
+        assert weight >= 0f : weight;
+
+        boolean wasKinematic = (kinematicWeight > 0f);
+        kinematicWeight = (weight > 1f) ? 1f : weight;
+        boolean isKinematic = (kinematicWeight > 0f);
+
+        if (wasKinematic && !isKinematic) {
+            rigidBody.setKinematic(false);
+            rigidBody.setPhysicsLocation(kpTransform.getTranslation());
+            rigidBody.setPhysicsRotation(kpTransform.getRotation());
+            rigidBody.getCollisionShape().setScale(kpTransform.getScale());
+            rigidBody.setLinearVelocity(kpVelocity);
+        } else if (isKinematic && !wasKinematic) {
+            rigidBody.getPhysicsLocation(kpTransform.getTranslation());
+            rigidBody.getPhysicsRotation(kpTransform.getRotation());
+            Vector3f scale = rigidBody.getCollisionShape().getScale();
+            kpTransform.setScale(scale);
+            rigidBody.getLinearVelocity(kpVelocity);
+            rigidBody.setKinematic(true);
+        }
+    }
+
+    /**
+     * Update the kinematic physics transform.
+     */
+    private void updateKPTransform() {
+        control.physicsTransform(bone, localOffset, kpTransform);
+    }
+}

+ 707 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/RagUtils.java

@@ -0,0 +1,707 @@
+/*
+ * Copyright (c) 2018-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 com.jme3.bullet.animation;
+
+import com.jme3.anim.Armature;
+import com.jme3.anim.Joint;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Transform;
+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.VertexBuffer;
+import com.jme3.scene.control.Control;
+import java.io.IOException;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.nio.ShortBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.logging.Logger;
+
+/**
+ * Utility methods used by DynamicAnimControl and associated classes.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold [email protected]
+ *
+ * Based on KinematicRagdollControl by Normen Hansen and Rémy Bouquet (Nehon).
+ */
+public class RagUtils {
+    // *************************************************************************
+    // constants and loggers
+
+    /**
+     * message logger for this class
+     */
+    final private static Logger logger
+            = Logger.getLogger(RagUtils.class.getName());
+    // *************************************************************************
+    // constructors
+
+    /**
+     * A private constructor to inhibit instantiation of this class.
+     */
+    private RagUtils() {
+    }
+    // *************************************************************************
+    // new methods exposed
+
+    /**
+     * Assign each mesh vertex to a bone/torso link and add its location (mesh
+     * coordinates in bind pose) to that link's list.
+     *
+     * @param meshes array of animated meshes to use (not null, unaffected)
+     * @param managerMap a map from bone indices to managing link names (not
+     * null, unaffected)
+     * @return a new map from bone/torso names to sets of vertex coordinates
+     */
+    static Map<String, VectorSet> coordsMap(Mesh[] meshes,
+            String[] managerMap) {
+        float[] wArray = new float[4];
+        int[] iArray = new int[4];
+        Vector3f bindPosition = new Vector3f();
+        Map<String, VectorSet> coordsMap = new HashMap<>(32);
+        for (Mesh mesh : meshes) {
+            int numVertices = mesh.getVertexCount();
+            for (int vertexI = 0; vertexI < numVertices; ++vertexI) {
+                String managerName = findManager(mesh, vertexI, iArray, wArray,
+                        managerMap);
+                VectorSet set = coordsMap.get(managerName);
+                if (set == null) {
+                    set = new VectorSet(1);
+                    coordsMap.put(managerName, set);
+                }
+                vertexVector3f(mesh, VertexBuffer.Type.BindPosePosition,
+                        vertexI, bindPosition);
+                set.add(bindPosition);
+            }
+        }
+
+        return coordsMap;
+    }
+
+    /**
+     * Find an animated geometry in the specified subtree of the scene graph.
+     * Note: recursive!
+     *
+     * @param subtree where to search (not null, unaffected)
+     * @return a pre-existing instance, or null if none
+     */
+    static Geometry findAnimatedGeometry(Spatial subtree) {
+        Geometry result = null;
+        if (subtree instanceof Geometry) {
+            Geometry geometry = (Geometry) subtree;
+            Mesh mesh = geometry.getMesh();
+            VertexBuffer indices = mesh.getBuffer(VertexBuffer.Type.BoneIndex);
+            boolean hasIndices = indices != null;
+            VertexBuffer weights = mesh.getBuffer(VertexBuffer.Type.BoneWeight);
+            boolean hasWeights = weights != null;
+            if (hasIndices && hasWeights) {
+                result = geometry;
+            }
+
+        } else if (subtree instanceof Node) {
+            Node node = (Node) subtree;
+            List<Spatial> children = node.getChildren();
+            for (Spatial child : children) {
+                result = findAnimatedGeometry(child);
+                if (result != null) {
+                    break;
+                }
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Find the index of the specified scene-graph control in the specified
+     * spatial.
+     *
+     * @param spatial the spatial to search (not null, unaffected)
+     * @param sgc the control to search for (not null, unaffected)
+     * @return the index (&ge;0) or -1 if not found
+     */
+    static int findIndex(Spatial spatial, Control sgc) {
+        int numControls = spatial.getNumControls();
+        int result = -1;
+        for (int controlIndex = 0; controlIndex < numControls; ++controlIndex) {
+            Control control = spatial.getControl(controlIndex);
+            if (control == sgc) {
+                result = controlIndex;
+                break;
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Find the main root bone of a skeleton, based on its total bone weight.
+     *
+     * @param skeleton the skeleton (not null, unaffected)
+     * @param targetMeshes an array of animated meshes to provide bone weights
+     * (not null)
+     * @return a root bone, or null if none found
+     */
+    static Joint findMainBone(Armature skeleton, Mesh[] targetMeshes) {
+        Joint[] rootBones = skeleton.getRoots();
+
+        Joint result;
+        if (rootBones.length == 1) {
+            result = rootBones[0];
+        } else {
+            result = null;
+            float[] totalWeights = totalWeights(targetMeshes, skeleton);
+            float greatestTotalWeight = Float.NEGATIVE_INFINITY;
+            for (Joint rootBone : rootBones) {
+                int boneIndex = skeleton.getJointIndex(rootBone);
+                float weight = totalWeights[boneIndex];
+                if (weight > greatestTotalWeight) {
+                    result = rootBone;
+                    greatestTotalWeight = weight;
+                }
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Enumerate all animated meshes in the specified subtree of a scene graph.
+     * Note: recursive!
+     *
+     * @param subtree which subtree (aliases created)
+     * @param storeResult (added to if not null)
+     * @return an expanded list (either storeResult or a new instance)
+     */
+    static List<Mesh> listAnimatedMeshes(Spatial subtree,
+            List<Mesh> storeResult) {
+        if (storeResult == null) {
+            storeResult = new ArrayList<>(10);
+        }
+
+        if (subtree instanceof Geometry) {
+            Geometry geometry = (Geometry) subtree;
+            Mesh mesh = geometry.getMesh();
+            VertexBuffer indices = mesh.getBuffer(VertexBuffer.Type.BoneIndex);
+            boolean hasIndices = indices != null;
+            VertexBuffer weights = mesh.getBuffer(VertexBuffer.Type.BoneWeight);
+            boolean hasWeights = weights != null;
+            if (hasIndices && hasWeights && !storeResult.contains(mesh)) {
+                storeResult.add(mesh);
+            }
+
+        } else if (subtree instanceof Node) {
+            Node node = (Node) subtree;
+            List<Spatial> children = node.getChildren();
+            for (Spatial child : children) {
+                listAnimatedMeshes(child, storeResult);
+            }
+        }
+
+        return storeResult;
+    }
+
+    /**
+     * Convert a transform from the mesh coordinate system to the local
+     * coordinate system of the specified bone.
+     *
+     * @param parentBone (not null)
+     * @param transform the transform to convert (not null, modified)
+     */
+    static void meshToLocal(Joint parentBone, Transform transform) {
+        Vector3f location = transform.getTranslation();
+        Quaternion orientation = transform.getRotation();
+        Vector3f scale = transform.getScale();
+
+        Transform pmx = parentBone.getModelTransform();
+        Vector3f pmTranslate = pmx.getTranslation();
+        Quaternion pmRotInv = pmx.getRotation().inverse();
+        Vector3f pmScale = pmx.getScale();
+
+        location.subtractLocal(pmTranslate);
+        location.divideLocal(pmScale);
+        pmRotInv.mult(location, location);
+        scale.divideLocal(pmScale);
+        pmRotInv.mult(orientation, orientation);
+    }
+
+    /**
+     * Read an array of transforms from an input capsule.
+     *
+     * @param capsule the input capsule (not null)
+     * @param fieldName the name of the field to read (not null)
+     * @return a new array or null
+     * @throws IOException from capsule
+     */
+    static Transform[] readTransformArray(InputCapsule capsule,
+            String fieldName) throws IOException {
+        Savable[] tmp = capsule.readSavableArray(fieldName, null);
+        Transform[] result;
+        if (tmp == null) {
+            result = null;
+        } else {
+            result = new Transform[tmp.length];
+            for (int i = 0; i < tmp.length; ++i) {
+                result[i] = (Transform) tmp[i];
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Calculate a coordinate transform for the specified spatial relative to a
+     * specified ancestor node. The result incorporates the transform of the
+     * starting spatial, but not that of the ancestor.
+     *
+     * @param startSpatial the starting spatial (not null, unaffected)
+     * @param ancestorNode the ancestor node (not null, unaffected)
+     * @param storeResult storage for the result (modified if not null)
+     * @return a coordinate transform (either storeResult or a new vector, not
+     * null)
+     */
+    static Transform relativeTransform(Spatial startSpatial,
+            Node ancestorNode, Transform storeResult) {
+        assert startSpatial.hasAncestor(ancestorNode);
+        Transform result
+                = (storeResult == null) ? new Transform() : storeResult;
+
+        result.loadIdentity();
+        Spatial loopSpatial = startSpatial;
+        while (loopSpatial != ancestorNode) {
+            Transform localTransform = loopSpatial.getLocalTransform();
+            result.combineWithParent(localTransform);
+            loopSpatial = loopSpatial.getParent();
+        }
+
+        return result;
+    }
+
+    /**
+     * Validate a skeleton for use with DynamicAnimControl.
+     *
+     * @param skeleton the skeleton to validate (not null, unaffected)
+     */
+    static void validate(Armature skeleton) {
+        int numBones = skeleton.getJointCount();
+        if (numBones < 0) {
+            throw new IllegalArgumentException("Bone count is negative!");
+        }
+
+        Set<String> nameSet = new TreeSet<>();
+        for (int boneIndex = 0; boneIndex < numBones; ++boneIndex) {
+            Joint bone = skeleton.getJoint(boneIndex);
+            if (bone == null) {
+                String msg = String.format("Bone %d in skeleton is null!",
+                        boneIndex);
+                throw new IllegalArgumentException(msg);
+            }
+            String boneName = bone.getName();
+            if (boneName == null) {
+                String msg = String.format("Bone %d in skeleton has null name!",
+                        boneIndex);
+                throw new IllegalArgumentException(msg);
+            } else if (boneName.equals(DynamicAnimControl.torsoName)) {
+                String msg = String.format(
+                        "Bone %d in skeleton has a reserved name!",
+                        boneIndex);
+                throw new IllegalArgumentException(msg);
+            } else if (nameSet.contains(boneName)) {
+                String msg = "Duplicate bone name in skeleton: " + boneName;
+                throw new IllegalArgumentException(msg);
+            }
+            nameSet.add(boneName);
+        }
+    }
+
+    /**
+     * Validate a model for use with DynamicAnimControl.
+     *
+     * @param model the model to validate (not null, unaffected)
+     */
+    static void validate(Spatial model) {
+        List<Geometry> geometries = listGeometries(model, null);
+        if (geometries.isEmpty()) {
+            throw new IllegalArgumentException("No meshes in the model.");
+        }
+        for (Geometry geometry : geometries) {
+            if (geometry.isIgnoreTransform()) {
+                throw new IllegalArgumentException(
+                        "A model geometry ignores transforms.");
+            }
+        }
+    }
+    // *************************************************************************
+    // private methods
+
+    private static void addPreOrderJoints(Joint bone, List<Joint> addResult) {
+        assert bone != null;
+        addResult.add(bone);
+        List<Joint> children = bone.getChildren();
+        for (Joint child : children) {
+            addPreOrderJoints(child, addResult);
+        }
+    }
+
+    /**
+     * Add the vertex weights of each bone in the specified mesh to an array of
+     * total weights.
+     *
+     * @param mesh animated mesh to analyze (not null, unaffected)
+     * @param totalWeights (not null, modified)
+     */
+    private static void addWeights(Mesh mesh, float[] totalWeights) {
+        assert totalWeights != null;
+
+        int maxWeightsPerVert = mesh.getMaxNumWeights();
+        if (maxWeightsPerVert <= 0) {
+            maxWeightsPerVert = 1;
+        }
+        assert maxWeightsPerVert > 0 : maxWeightsPerVert;
+        assert maxWeightsPerVert <= 4 : maxWeightsPerVert;
+
+        VertexBuffer biBuf = mesh.getBuffer(VertexBuffer.Type.BoneIndex);
+        Buffer boneIndexBuffer = biBuf.getDataReadOnly();
+        boneIndexBuffer.rewind();
+        int numBoneIndices = boneIndexBuffer.remaining();
+        assert numBoneIndices % 4 == 0 : numBoneIndices;
+        int numVertices = boneIndexBuffer.remaining() / 4;
+
+        VertexBuffer wBuf = mesh.getBuffer(VertexBuffer.Type.BoneWeight);
+        FloatBuffer weightBuffer = (FloatBuffer) wBuf.getDataReadOnly();
+        weightBuffer.rewind();
+        int numWeights = weightBuffer.remaining();
+        assert numWeights == numVertices * 4 : numWeights;
+
+        for (int vIndex = 0; vIndex < numVertices; ++vIndex) {
+            for (int wIndex = 0; wIndex < 4; ++wIndex) {
+                float weight = weightBuffer.get();
+                int boneIndex = readIndex(boneIndexBuffer);
+                if (wIndex < maxWeightsPerVert) {
+                    totalWeights[boneIndex] += FastMath.abs(weight);
+                }
+            }
+        }
+    }
+
+    /**
+     * Determine which physics link should manage the specified mesh vertex.
+     *
+     * @param mesh the mesh containing the vertex (not null, unaffected)
+     * @param vertexIndex the vertex index in the mesh (&ge;0)
+     * @param iArray temporary storage for bone indices (not null, modified)
+     * @param wArray temporary storage for bone weights (not null, modified)
+     * @param managerMap a map from bone indices to bone/torso names (not null,
+     * unaffected)
+     * @return a bone/torso name
+     */
+    private static String findManager(Mesh mesh, int vertexIndex, int[] iArray,
+            float[] wArray, String[] managerMap) {
+        vertexBoneIndices(mesh, vertexIndex, iArray);
+        vertexBoneWeights(mesh, vertexIndex, wArray);
+        Map<String, Float> weightMap = weightMap(iArray, wArray, managerMap);
+
+        float bestTotalWeight = Float.NEGATIVE_INFINITY;
+        String bestName = null;
+        for (Map.Entry<String, Float> entry : weightMap.entrySet()) {
+            float totalWeight = entry.getValue();
+            if (totalWeight >= bestTotalWeight) {
+                bestTotalWeight = totalWeight;
+                bestName = entry.getKey();
+            }
+        }
+
+        return bestName;
+    }
+
+    /**
+     * Enumerate all geometries in the specified subtree of a scene graph. Note:
+     * recursive!
+     *
+     * @param subtree (not null, aliases created)
+     * @param addResult (added to if not null)
+     * @return an expanded list (either storeResult or a new instance)
+     */
+    private static List<Geometry> listGeometries(Spatial subtree,
+            List<Geometry> addResult) {
+        List<Geometry> result = (addResult == null) ? new ArrayList<Geometry>(50) : addResult;
+
+        if (subtree instanceof Geometry) {
+            Geometry geometry = (Geometry) subtree;
+            if (!result.contains(geometry)) {
+                result.add(geometry);
+            }
+        }
+
+        if (subtree instanceof Node) {
+            Node node = (Node) subtree;
+            List<Spatial> children = node.getChildren();
+            for (Spatial child : children) {
+                listGeometries(child, result);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Enumerate all bones in a pre-order, depth-first traversal of the
+     * skeleton, such that child bones never precede their ancestors.
+     *
+     * @param skeleton the skeleton to traverse (not null, unaffected)
+     * @return a new list of bones
+     */
+    private static List<Joint> preOrderJoints(Armature skeleton) {
+        int numBones = skeleton.getJointCount();
+        List<Joint> result = new ArrayList<>(numBones);
+        Joint[] rootBones = skeleton.getRoots();
+        for (Joint rootBone : rootBones) {
+            addPreOrderJoints(rootBone, result);
+        }
+
+        assert result.size() == numBones : result.size();
+        return result;
+    }
+
+    /**
+     * Read an index from a buffer.
+     *
+     * @param buffer a buffer of bytes or shorts (not null)
+     * @return index (&ge;0)
+     */
+    private static int readIndex(Buffer buffer) {
+        int result;
+        if (buffer instanceof ByteBuffer) {
+            ByteBuffer byteBuffer = (ByteBuffer) buffer;
+            byte b = byteBuffer.get();
+            result = 0xff & b;
+        } else if (buffer instanceof ShortBuffer) {
+            ShortBuffer shortBuffer = (ShortBuffer) buffer;
+            short s = shortBuffer.get();
+            result = 0xffff & s;
+        } else {
+            throw new IllegalArgumentException();
+        }
+
+        assert result >= 0 : result;
+        return result;
+    }
+
+    /**
+     * Calculate the total bone weight animated by each bone in the specified
+     * meshes.
+     *
+     * @param meshes the animated meshes to analyze (not null, unaffected)
+     * @param skeleton (not null, unaffected)
+     * @return a map from bone indices to total bone weight
+     */
+    private static float[] totalWeights(Mesh[] meshes, Armature skeleton) {
+        int numBones = skeleton.getJointCount();
+        float[] result = new float[numBones];
+        for (Mesh mesh : meshes) {
+            RagUtils.addWeights(mesh, result);
+        }
+
+        List<Joint> bones = preOrderJoints(skeleton);
+        Collections.reverse(bones);
+        for (Joint childBone : bones) {
+            int childIndex = skeleton.getJointIndex(childBone);
+            Joint parent = childBone.getParent();
+            if (parent != null) {
+                int parentIndex = skeleton.getJointIndex(parent);
+                result[parentIndex] += result[childIndex];
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Copy the bone indices for the indexed vertex.
+     *
+     * @param mesh subject mesh (not null)
+     * @param vertexIndex index into the mesh's vertices (&ge;0)
+     * @param storeResult (modified if not null)
+     * @return the data vector (either storeResult or a new instance)
+     */
+    private static int[] vertexBoneIndices(Mesh mesh,
+            int vertexIndex, int[] storeResult) {
+        if (storeResult == null) {
+            storeResult = new int[4];
+        } else {
+            assert storeResult.length >= 4 : storeResult.length;
+        }
+
+        int maxWeightsPerVert = mesh.getMaxNumWeights();
+        if (maxWeightsPerVert <= 0) {
+            maxWeightsPerVert = 1;
+        }
+
+        VertexBuffer biBuf = mesh.getBuffer(VertexBuffer.Type.BoneIndex);
+        Buffer boneIndexBuffer = biBuf.getDataReadOnly();
+        boneIndexBuffer.position(4 * vertexIndex);
+        for (int wIndex = 0; wIndex < maxWeightsPerVert; ++wIndex) {
+            int boneIndex = readIndex(boneIndexBuffer);
+            storeResult[wIndex] = boneIndex;
+        }
+        /*
+         * Fill with -1s.
+         */
+        int length = storeResult.length;
+        for (int wIndex = maxWeightsPerVert; wIndex < length; ++wIndex) {
+            storeResult[wIndex] = -1;
+        }
+
+        return storeResult;
+    }
+
+    /**
+     * Copy the bone weights for the indexed vertex.
+     *
+     * @param mesh subject mesh (not null)
+     * @param vertexIndex index into the mesh's vertices (&ge;0)
+     * @param storeResult (modified if not null)
+     * @return the data vector (either storeResult or a new instance)
+     */
+    private static float[] vertexBoneWeights(Mesh mesh,
+            int vertexIndex, float[] storeResult) {
+        if (storeResult == null) {
+            storeResult = new float[4];
+        } else {
+            assert storeResult.length >= 4 : storeResult.length;
+        }
+
+        int maxWeightsPerVert = mesh.getMaxNumWeights();
+        if (maxWeightsPerVert <= 0) {
+            maxWeightsPerVert = 1;
+        }
+
+        VertexBuffer wBuf = mesh.getBuffer(VertexBuffer.Type.BoneWeight);
+        FloatBuffer weightBuffer = (FloatBuffer) wBuf.getDataReadOnly();
+        weightBuffer.position(4 * vertexIndex);
+        for (int wIndex = 0; wIndex < maxWeightsPerVert; ++wIndex) {
+            storeResult[wIndex] = weightBuffer.get();
+        }
+        /*
+         * Fill with 0s.
+         */
+        int length = storeResult.length;
+        for (int wIndex = maxWeightsPerVert; wIndex < length; ++wIndex) {
+            storeResult[wIndex] = 0f;
+        }
+
+        return storeResult;
+    }
+
+    /**
+     * Copy Vector3f data for the indexed vertex from the specified vertex
+     * buffer.
+     * <p>
+     * A software skin update is required BEFORE reading vertex
+     * positions/normals/tangents from an animated mesh
+     *
+     * @param mesh subject mesh (not null)
+     * @param bufferType which buffer to read (5 legal values)
+     * @param vertexIndex index into the mesh's vertices (&ge;0)
+     * @param storeResult (modified if not null)
+     * @return the data vector (either storeResult or a new instance)
+     */
+    private static Vector3f vertexVector3f(Mesh mesh,
+            VertexBuffer.Type bufferType, int vertexIndex,
+            Vector3f storeResult) {
+        assert bufferType == VertexBuffer.Type.BindPoseNormal
+                || bufferType == VertexBuffer.Type.BindPosePosition
+                || bufferType == VertexBuffer.Type.Binormal
+                || bufferType == VertexBuffer.Type.Normal
+                || bufferType == VertexBuffer.Type.Position : bufferType;
+        if (storeResult == null) {
+            storeResult = new Vector3f();
+        }
+
+        VertexBuffer vertexBuffer = mesh.getBuffer(bufferType);
+        FloatBuffer floatBuffer = (FloatBuffer) vertexBuffer.getDataReadOnly();
+        floatBuffer.position(3 * vertexIndex);
+        storeResult.x = floatBuffer.get();
+        storeResult.y = floatBuffer.get();
+        storeResult.z = floatBuffer.get();
+
+        return storeResult;
+    }
+
+    /**
+     * Tabulate the total bone weight associated with each bone/torso link in a
+     * ragdoll.
+     *
+     * @param biArray the array of bone indices (not null, unaffected)
+     * @param bwArray the array of bone weights (not null, unaffected)
+     * @param managerMap a map from bone indices to managing link names (not
+     * null, unaffected)
+     * @return a new map from link names to total weight
+     */
+    private static Map<String, Float> weightMap(int[] biArray,
+            float[] bwArray, String[] managerMap) {
+        assert biArray.length == 4;
+        assert bwArray.length == 4;
+
+        Map<String, Float> weightMap = new HashMap<>(4);
+        for (int j = 0; j < 4; ++j) {
+            int boneIndex = biArray[j];
+            if (boneIndex != -1) {
+                String managerName = managerMap[boneIndex];
+                if (weightMap.containsKey(managerName)) {
+                    float oldWeight = weightMap.get(managerName);
+                    float newWeight = oldWeight + bwArray[j];
+                    weightMap.put(managerName, newWeight);
+                } else {
+                    weightMap.put(managerName, bwArray[j]);
+                }
+            }
+        }
+
+        return weightMap;
+    }
+}

+ 56 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/RagdollCollisionListener.java

@@ -0,0 +1,56 @@
+/*
+ * 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 com.jme3.bullet.animation;
+
+import com.jme3.bullet.collision.PhysicsCollisionEvent;
+import com.jme3.bullet.collision.PhysicsCollisionObject;
+
+/**
+ * Interface to receive notifications whenever a linked rigid body in a
+ * DynamicAnimControl collides with another physics object.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Nehon
+ */
+public interface RagdollCollisionListener {
+
+    /**
+     * Invoked when a collision involving a linked rigid body occurs.
+     *
+     * @param physicsLink the physics link that collided (not null)
+     * @param object the collision object that collided with the bone (not null)
+     * @param event other event details (not null)
+     */
+    void collide(PhysicsLink physicsLink, PhysicsCollisionObject object,
+            PhysicsCollisionEvent event);
+}

+ 313 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/RangeOfMotion.java

@@ -0,0 +1,313 @@
+/*
+ * Copyright (c) 2018-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 com.jme3.bullet.animation;
+
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.joints.SixDofJoint;
+import com.jme3.bullet.joints.motors.RotationalLimitMotor;
+import com.jme3.bullet.joints.motors.TranslationalLimitMotor;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.math.Vector3f;
+import java.io.IOException;
+import java.util.logging.Logger;
+
+/**
+ * Range of motion for a ragdoll joint. Immutable except for
+ * {@link #read(com.jme3.export.JmeImporter)}.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold [email protected]
+ *
+ * Based on RagdollPreset by Rémy Bouquet (Nehon).
+ */
+public class RangeOfMotion implements Savable {
+    // *************************************************************************
+    // constants and loggers
+
+    /**
+     * message logger for this class
+     */
+    final public static Logger logger
+            = Logger.getLogger(RangeOfMotion.class.getName());
+    /**
+     * local copy of {@link com.jme3.math.Vector3f#ZERO}
+     */
+    final private static Vector3f translateIdentity = new Vector3f(0f, 0f, 0f);
+    // *************************************************************************
+    // fields
+
+    /**
+     * maximum rotation angle around the X axis (in radians)
+     */
+    private float maxX = 0f;
+    /**
+     * minimum rotation angle around the X axis (in radians)
+     */
+    private float minX = 0f;
+    /**
+     * maximum rotation angle around the Y axis (in radians)
+     */
+    private float maxY = 0f;
+    /**
+     * minimum rotation angle around the Y axis (in radians)
+     */
+    private float minY = 0f;
+    /**
+     * maximum rotation angle around the Z axis (in radians)
+     */
+    private float maxZ = 0f;
+    /**
+     * minimum rotation angle around the Z axis (in radians)
+     */
+    private float minZ = 0f;
+    // *************************************************************************
+    // constructors
+
+    /**
+     * Instantiate a preset with no motion allowed.
+     */
+    public RangeOfMotion() {
+    }
+
+    /**
+     * Instantiate a preset with the specified range of motion.
+     *
+     * @param maxX the maximum rotation around the X axis (in radians)
+     * @param minX the minimum rotation around the X axis (in radians)
+     * @param maxY the maximum rotation around the Y axis (in radians)
+     * @param minY the minimum rotation around the Y axis (in radians)
+     * @param maxZ the maximum rotation around the Z axis (in radians)
+     * @param minZ the minimum rotation around the Z axis (in radians)
+     */
+    public RangeOfMotion(float maxX, float minX, float maxY, float minY,
+            float maxZ, float minZ) {
+        this.maxX = maxX;
+        this.minX = minX;
+        this.maxY = maxY;
+        this.minY = minY;
+        this.maxZ = maxZ;
+        this.minZ = minZ;
+    }
+
+    /**
+     * Instantiate a preset with the specified symmetric range of motion.
+     *
+     * @param maxX the maximum rotation around the X axis (in radians, &ge;0)
+     * @param maxY the maximum rotation around the Y axis (in radians, &ge;0)
+     * @param maxZ the maximum rotation around the Z axis (in radians, &ge;0)
+     */
+    public RangeOfMotion(float maxX, float maxY, float maxZ) {
+        this.maxX = maxX;
+        this.minX = -maxX;
+        this.maxY = maxY;
+        this.minY = -maxY;
+        this.maxZ = maxZ;
+        this.minZ = -maxZ;
+    }
+
+    /**
+     * Instantiate a preset with the specified symmetric range of motion.
+     *
+     * @param maxAngle the maximum rotation around each axis (in radians, &ge;0)
+     */
+    public RangeOfMotion(float maxAngle) {
+        maxX = maxAngle;
+        minX = -maxAngle;
+        maxY = maxAngle;
+        minY = -maxAngle;
+        maxZ = maxAngle;
+        minZ = -maxAngle;
+    }
+
+    /**
+     * Instantiate a preset for rotation on a single axis.
+     *
+     * @param axisIndex which axis: 0&rarr;X, 1&rarr;Y, 2&rarr;Z
+     */
+    public RangeOfMotion(int axisIndex) {
+        switch (axisIndex) {
+            case PhysicsSpace.AXIS_X:
+                maxX = 1f;
+                minX = -1f;
+                break;
+            case PhysicsSpace.AXIS_Y:
+                maxY = 1f;
+                minY = -1f;
+                break;
+            case PhysicsSpace.AXIS_Z:
+                maxZ = 1f;
+                minZ = -1f;
+                break;
+            default:
+                String msg = String.format("axisIndex=%d", axisIndex);
+                throw new IllegalArgumentException(msg);
+        }
+    }
+    // *************************************************************************
+    // new methods exposed
+
+    /**
+     * Read the maximum rotation around the indexed axis.
+     *
+     * @param axisIndex which axis: 0&rarr;X, 1&rarr;Y, 2&rarr;Z
+     *
+     * @return the rotation angle (in radians)
+     */
+    public float getMaxRotation(int axisIndex) {
+        float result;
+        switch (axisIndex) {
+            case PhysicsSpace.AXIS_X:
+                result = maxX;
+                break;
+            case PhysicsSpace.AXIS_Y:
+                result = maxY;
+                break;
+            case PhysicsSpace.AXIS_Z:
+                result = maxZ;
+                break;
+            default:
+                String msg = String.format("axisIndex=%d", axisIndex);
+                throw new IllegalArgumentException(msg);
+        }
+
+        return result;
+    }
+
+    /**
+     * Read the minimum rotation around the indexed axis.
+     *
+     * @param axisIndex which axis: 0&rarr;X, 1&rarr;Y, 2&rarr;Z
+     *
+     * @return the rotation angle (in radians)
+     */
+    public float getMinRotation(int axisIndex) {
+        float result;
+        switch (axisIndex) {
+            case PhysicsSpace.AXIS_X:
+                result = minX;
+                break;
+            case PhysicsSpace.AXIS_Y:
+                result = minY;
+                break;
+            case PhysicsSpace.AXIS_Z:
+                result = minZ;
+                break;
+            default:
+                String msg = String.format("axisIndex=%d", axisIndex);
+                throw new IllegalArgumentException(msg);
+        }
+
+        return result;
+    }
+
+    /**
+     * Apply this preset to the specified joint.
+     *
+     * @param joint where to apply this preset (not null, modified)
+     */
+    public void setupJoint(SixDofJoint joint) {
+        Vector3f lower = new Vector3f(minX, minY, minZ);
+        Vector3f upper = new Vector3f(maxX, maxY, maxZ);
+
+        RotationalLimitMotor rotX
+                = joint.getRotationalLimitMotor(PhysicsSpace.AXIS_X);
+        rotX.setLoLimit(lower.x);
+        rotX.setHiLimit(upper.x);
+
+        RotationalLimitMotor rotY
+                = joint.getRotationalLimitMotor(PhysicsSpace.AXIS_Y);
+        rotY.setLoLimit(lower.y);
+        rotY.setHiLimit(upper.y);
+
+        RotationalLimitMotor rotZ
+                = joint.getRotationalLimitMotor(PhysicsSpace.AXIS_Z);
+        rotZ.setLoLimit(lower.z);
+        rotZ.setHiLimit(upper.z);
+
+        joint.setAngularLowerLimit(lower);
+        joint.setAngularUpperLimit(upper);
+
+        for (int i = 0; i < 3; ++i) {
+            RotationalLimitMotor rot = joint.getRotationalLimitMotor(i);
+            rot.setMaxMotorForce(1e8f);
+            rot.setMaxLimitForce(1e9f);
+        }
+
+        joint.setLinearLowerLimit(translateIdentity);
+        joint.setLinearUpperLimit(translateIdentity);
+
+        TranslationalLimitMotor tra = joint.getTranslationalLimitMotor();
+        tra.setLowerLimit(translateIdentity);
+        tra.setUpperLimit(translateIdentity);
+    }
+    // *************************************************************************
+    // Savable methods
+
+    /**
+     * De-serialize this preset, for example when loading from a J3O file.
+     *
+     * @param im importer (not null)
+     * @throws IOException from importer
+     */
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule capsule = im.getCapsule(this);
+        maxX = capsule.readFloat("maxX", 0f);
+        minX = capsule.readFloat("minX", 0f);
+        maxY = capsule.readFloat("maxY", 0f);
+        minY = capsule.readFloat("minY", 0f);
+        maxZ = capsule.readFloat("maxZ", 0f);
+        minZ = capsule.readFloat("minZ", 0f);
+    }
+
+    /**
+     * Serialize this preset, for example when saving to a J3O file.
+     *
+     * @param ex exporter (not null)
+     * @throws IOException from exporter
+     */
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        OutputCapsule capsule = ex.getCapsule(this);
+        capsule.write(maxX, "maxX", 0f);
+        capsule.write(minX, "minX", 0f);
+        capsule.write(maxY, "maxY", 0f);
+        capsule.write(minY, "minY", 0f);
+        capsule.write(maxZ, "maxZ", 0f);
+        capsule.write(minZ, "minZ", 0f);
+    }
+}

+ 486 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/TorsoLink.java

@@ -0,0 +1,486 @@
+/*
+ * Copyright (c) 2018-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 com.jme3.bullet.animation;
+
+import com.jme3.anim.Joint;
+import com.jme3.bullet.collision.shapes.CollisionShape;
+import com.jme3.bullet.objects.PhysicsRigidBody;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Node;
+import com.jme3.util.clone.Cloner;
+import java.io.IOException;
+import java.util.logging.Logger;
+
+/**
+ * Link the torso of an animated model to a rigid body in a ragdoll.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold [email protected]
+ *
+ * Based on KinematicRagdollControl by Normen Hansen and Rémy Bouquet (Nehon).
+ */
+public class TorsoLink extends PhysicsLink {
+    // *************************************************************************
+    // constants and loggers
+
+    /**
+     * message logger for this class
+     */
+    final public static Logger logger2
+            = Logger.getLogger(TorsoLink.class.getName());
+    // *************************************************************************
+    // fields
+
+    /**
+     * bones managed by this link, in a pre-order, depth-first traversal of the
+     * skeleton
+     */
+    private Joint[] managedBones = null;
+    /**
+     * submode when kinematic
+     */
+    private KinematicSubmode submode = KinematicSubmode.Animated;
+    /**
+     * local transform for the controlled spatial at the end of this link's most
+     * recent blend interval, or null for no spatial blending
+     */
+    private Transform endModelTransform = null;
+    /**
+     * transform from mesh coordinates to model coordinates
+     */
+    private Transform meshToModel = null;
+    /**
+     * local transform of the controlled spatial at the start of this link's
+     * most recent blend interval
+     */
+    private Transform startModelTransform = new Transform();
+    /**
+     * local transform of each managed bone from the previous update
+     */
+    private Transform[] prevBoneTransforms = null;
+    /**
+     * local transform of each managed bone at the start of the most recent
+     * blend interval
+     */
+    private Transform[] startBoneTransforms = null;
+    // *************************************************************************
+    // constructors
+
+    /**
+     * No-argument constructor needed by SavableClassUtil. Do not invoke
+     * directly!
+     */
+    public TorsoLink() {
+    }
+
+    /**
+     * Instantiate a purely kinematic link between the torso of the specified
+     * control and the specified rigid body.
+     *
+     * @param control the control that will manage this link (not null, alias
+     * created)
+     * @param mainRootBone the root bone with the most animation weight (not
+     * null, alias created)
+     * @param collisionShape the desired shape (not null, alias created)
+     * @param mass the mass (in physics-mass units)
+     * @param meshToModel the transform from mesh coordinates to model
+     * coordinates (not null, unaffected)
+     * @param localOffset the location of the body's center (in the bone's local
+     * coordinates, not null, unaffected)
+     */
+    TorsoLink(DacLinks control, Joint mainRootBone,
+            CollisionShape collisionShape, float mass,
+            Transform meshToModel, Vector3f localOffset) {
+        super(control, mainRootBone, collisionShape, mass, localOffset);
+        this.meshToModel = meshToModel.clone();
+        managedBones = control.listManagedBones(DynamicAnimControl.torsoName);
+
+        int numManagedBones = managedBones.length;
+        startBoneTransforms = new Transform[numManagedBones];
+        for (int i = 0; i < numManagedBones; ++i) {
+            startBoneTransforms[i] = new Transform();
+        }
+    }
+    // *************************************************************************
+    // new methods exposed
+
+    /**
+     * Begin blending this link to a purely kinematic mode.
+     *
+     * @param submode enum value (not null)
+     * @param blendInterval the duration of the blend interval (in seconds,
+     * &ge;0)
+     * @param endModelTransform the desired local transform for the controlled
+     * spatial when the blend completes or null for no change to local transform
+     * (unaffected)
+     */
+    public void blendToKinematicMode(KinematicSubmode submode,
+            float blendInterval, Transform endModelTransform) {
+        super.blendToKinematicMode(blendInterval);
+
+        this.submode = submode;
+        this.endModelTransform = endModelTransform;
+        /*
+         * Save initial transforms for blending.
+         */
+        if (endModelTransform != null) {
+            Transform current = getControl().getSpatial().getLocalTransform();
+            startModelTransform.set(current);
+        }
+        int numManagedBones = managedBones.length;
+        for (int mbIndex = 0; mbIndex < numManagedBones; ++mbIndex) {
+            Transform transform;
+            if (prevBoneTransforms == null) { // this link not updated yet
+                Joint managedBone = managedBones[mbIndex];
+                transform = managedBone.getLocalTransform().clone();
+            } else {
+                transform = prevBoneTransforms[mbIndex];
+            }
+            startBoneTransforms[mbIndex].set(transform);
+        }
+    }
+    // *************************************************************************
+    // PhysicsLink methods
+
+    /**
+     * Callback from {@link com.jme3.util.clone.Cloner} to convert this
+     * shallow-cloned link into a deep-cloned one, using the specified cloner
+     * and original to resolve copied fields.
+     *
+     * @param cloner the cloner that's cloning this link (not null)
+     * @param original the instance from which this link was shallow-cloned
+     * (unused)
+     */
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        super.cloneFields(cloner, original);
+
+        managedBones = cloner.clone(managedBones);
+        endModelTransform = cloner.clone(endModelTransform);
+        meshToModel = cloner.clone(meshToModel);
+        prevBoneTransforms = cloner.clone(prevBoneTransforms);
+        startBoneTransforms = cloner.clone(startBoneTransforms);
+        startModelTransform = cloner.clone(startModelTransform);
+    }
+
+    /**
+     * Update this link in Dynamic mode, setting the local transform of the
+     * model's root spatial based on the transform of the linked rigid body.
+     */
+    @Override
+    protected void dynamicUpdate() {
+        /*
+         * Calculate the inverse world transform of the model's parent node.
+         */
+        Transform worldToParent;
+        Node parent = getControl().getSpatial().getParent();
+        if (parent == null) {
+            worldToParent = new Transform();
+        } else {
+            Transform parentToWorld = parent.getWorldTransform();
+            worldToParent = parentToWorld.invert();
+        }
+
+        Transform transform = meshToModel.clone();
+        Transform shapeToWorld = new Transform();
+        PhysicsRigidBody body = getRigidBody();
+        body.getPhysicsLocation(shapeToWorld.getTranslation());
+        body.getPhysicsRotation(shapeToWorld.getRotation());
+        shapeToWorld.setScale(body.getCollisionShape().getScale());
+
+        transform.combineWithParent(shapeToWorld);
+        transform.combineWithParent(worldToParent);
+        getControl().getSpatial().setLocalTransform(transform);
+
+        localBoneTransform(transform);
+        Joint[] rootBones = getControl().getSkeleton().getRoots();
+        for (Joint rootBone : rootBones) {
+            rootBone.setLocalTransform(transform);
+        }
+
+        for (Joint managedBone : managedBones) {
+            managedBone.updateModelTransforms();
+        }
+    }
+
+    /**
+     * Create a shallow clone for the JME cloner.
+     *
+     * @return a new instance
+     */
+    @Override
+    public TorsoLink jmeClone() {
+        try {
+            TorsoLink clone = (TorsoLink) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException exception) {
+            throw new RuntimeException(exception);
+        }
+    }
+
+    /**
+     * Update this link in blended Kinematic mode.
+     *
+     * @param tpf the time interval between frames (in seconds, &ge;0)
+     */
+    @Override
+    protected void kinematicUpdate(float tpf) {
+        assert tpf >= 0f : tpf;
+        assert getRigidBody().isKinematic();
+
+        Transform transform = new Transform();
+
+        if (endModelTransform != null) {
+            /*
+             * For a smooth transition, blend the saved model transform
+             * (from the start of the blend interval) into the goal transform.
+             */
+            transform.interpolateTransforms(startModelTransform.clone(),
+                    endModelTransform, kinematicWeight());
+            getControl().getSpatial().setLocalTransform(transform);
+        }
+
+        for (int mbIndex = 0; mbIndex < managedBones.length; ++mbIndex) {
+            Joint managedBone = managedBones[mbIndex];
+            switch (submode) {
+                case Animated:
+                    transform.set(managedBone.getLocalTransform());
+                    break;
+                case Frozen:
+                    transform.set(prevBoneTransforms[mbIndex]);
+                    break;
+                default:
+                    throw new IllegalStateException(submode.toString());
+            }
+
+            if (kinematicWeight() < 1f) { // not purely kinematic yet
+                /*
+                 * For a smooth transition, blend the saved bone transform
+                 * (from the start of the blend interval)
+                 * into the goal transform.
+                 */
+                transform.interpolateTransforms(
+                        startBoneTransforms[mbIndex].clone(), transform,
+                        kinematicWeight());
+            }
+            /*
+             * Update the managed bone.
+             */
+            managedBone.setLocalTransform(transform);
+            managedBone.updateModelTransforms();
+        }
+
+        super.kinematicUpdate(tpf);
+    }
+
+    /**
+     * Unambiguously identify this link by name, within its DynamicAnimControl.
+     *
+     * @return a brief textual description (not null, not empty)
+     */
+    @Override
+    public String name() {
+        return "Torso:";
+    }
+
+    /**
+     * Copy animation data from the specified link, which must have the same
+     * main bone.
+     *
+     * @param oldLink the link to copy from (not null, unaffected)
+     */
+    void postRebuild(TorsoLink oldLink) {
+        int numManagedBones = managedBones.length;
+        assert oldLink.managedBones.length == numManagedBones;
+
+        super.postRebuild(oldLink);
+        if (oldLink.isKinematic()) {
+            submode = oldLink.submode;
+        } else {
+            submode = KinematicSubmode.Frozen;
+        }
+
+        Transform emt = oldLink.endModelTransform;
+        endModelTransform = (emt == null) ? null : emt.clone();
+        startModelTransform.set(oldLink.startModelTransform);
+
+        if (prevBoneTransforms == null) {
+            prevBoneTransforms = new Transform[numManagedBones];
+            for (int i = 0; i < numManagedBones; ++i) {
+                prevBoneTransforms[i] = new Transform();
+            }
+        }
+        for (int i = 0; i < numManagedBones; ++i) {
+            prevBoneTransforms[i].set(oldLink.prevBoneTransforms[i]);
+            startBoneTransforms[i].set(oldLink.startBoneTransforms[i]);
+        }
+    }
+
+    /**
+     * De-serialize this link, for example when loading from a J3O file.
+     *
+     * @param im importer (not null)
+     * @throws IOException from importer
+     */
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+
+        Savable[] tmp = ic.readSavableArray("managedBones", null);
+        if (tmp == null) {
+            managedBones = null;
+        } else {
+            managedBones = new Joint[tmp.length];
+            for (int i = 0; i < tmp.length; ++i) {
+                managedBones[i] = (Joint) tmp[i];
+            }
+        }
+
+        submode = ic.readEnum("submode", KinematicSubmode.class,
+                KinematicSubmode.Animated);
+        endModelTransform = (Transform) ic.readSavable("endModelTransform",
+                new Transform());
+        meshToModel
+                = (Transform) ic.readSavable("meshToModel", new Transform());
+        startModelTransform = (Transform) ic.readSavable("startModelTransform",
+                new Transform());
+        prevBoneTransforms = RagUtils.readTransformArray(ic,
+                "prevBoneTransforms");
+        startBoneTransforms = RagUtils.readTransformArray(ic,
+                "startBoneTransforms");
+    }
+
+    /**
+     * Internal callback, invoked once per frame during the logical-state
+     * update, provided the control is added to a scene.
+     *
+     * @param tpf the time interval between frames (in seconds, &ge;0)
+     */
+    @Override
+    void update(float tpf) {
+        assert tpf >= 0f : tpf;
+
+        if (prevBoneTransforms == null) {
+            /*
+             * On the first update, allocate and initialize
+             * the array of previous bone transforms, if it wasn't
+             * allocated in blendToKinematicMode().
+             */
+            int numManagedBones = managedBones.length;
+            prevBoneTransforms = new Transform[numManagedBones];
+            for (int mbIndex = 0; mbIndex < numManagedBones; ++mbIndex) {
+                Joint managedBone = managedBones[mbIndex];
+                Transform boneTransform
+                        = managedBone.getLocalTransform().clone();
+                prevBoneTransforms[mbIndex] = boneTransform;
+            }
+        }
+
+        super.update(tpf);
+        /*
+         * Save copies of the latest bone transforms.
+         */
+        for (int mbIndex = 0; mbIndex < managedBones.length; ++mbIndex) {
+            Transform lastTransform = prevBoneTransforms[mbIndex];
+            Joint managedBone = managedBones[mbIndex];
+            lastTransform.set(managedBone.getLocalTransform());
+        }
+    }
+
+    /**
+     * Serialize this link, for example when saving to a J3O file.
+     *
+     * @param ex exporter (not null)
+     * @throws IOException from exporter
+     */
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+
+        oc.write(managedBones, "managedBones", null);
+        oc.write(submode, "submode", KinematicSubmode.Animated);
+        oc.write(endModelTransform, "endModelTransforms", new Transform());
+        oc.write(meshToModel, "meshToModel", new Transform());
+        oc.write(startModelTransform, "startModelTransforms", new Transform());
+        oc.write(prevBoneTransforms, "prevBoneTransforms", new Transform[0]);
+        oc.write(startBoneTransforms, "startBoneTransforms", new Transform[0]);
+    }
+    // *************************************************************************
+    // private methods
+
+    /**
+     * Calculate the local bone transform to match the physics transform of the
+     * rigid body.
+     *
+     * @param storeResult storage for the result (modified if not null)
+     * @return the calculated bone transform (in local coordinates, either
+     * storeResult or a new transform, not null)
+     */
+    private Transform localBoneTransform(Transform storeResult) {
+        Transform result
+                = (storeResult == null) ? new Transform() : storeResult;
+        Vector3f location = result.getTranslation();
+        Quaternion orientation = result.getRotation();
+        Vector3f scale = result.getScale();
+        /*
+         * Start with the rigid body's transform in physics/world coordinates.
+         */
+        PhysicsRigidBody body = getRigidBody();
+        body.getPhysicsLocation(result.getTranslation());
+        body.getPhysicsRotation(result.getRotation());
+        result.setScale(body.getCollisionShape().getScale());
+        /*
+         * Convert to mesh coordinates.
+         */
+        Transform worldToMesh = getControl().meshTransform(null).invert();
+        result.combineWithParent(worldToMesh);
+        /*
+         * Subtract the body's local offset, rotated and scaled.
+         */
+        Vector3f meshOffset = localOffset(null);
+        meshOffset.multLocal(scale);
+        orientation.mult(meshOffset, meshOffset);
+        location.subtractLocal(meshOffset);
+
+        return result;
+    }
+}

+ 151 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/VectorSet.java

@@ -0,0 +1,151 @@
+/*
+ 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 com.jme3.bullet.animation;
+
+import com.jme3.math.Vector3f;
+import com.jme3.util.BufferUtils;
+import java.nio.FloatBuffer;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.logging.Logger;
+
+/**
+ * A simplified collection of Vector3f values without duplicates, implemented
+ * using a Collection.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold [email protected]
+ */
+public class VectorSet {
+    // *************************************************************************
+    // constants and loggers
+
+    /**
+     * message logger for this class
+     */
+    final private static Logger logger
+            = Logger.getLogger(VectorSet.class.getName());
+    // *************************************************************************
+    // fields
+
+    /**
+     * collection of values
+     */
+    final private Set<Vector3f> set;
+    // *************************************************************************
+    // constructors
+
+    /**
+     * Instantiate an empty set with the specified initial capacity and default
+     * load factor.
+     *
+     * @param numVectors the initial capacity of the hash table (&gt;0)
+     */
+    public VectorSet(int numVectors) {
+        set = new HashSet<>(numVectors);
+    }
+    // *************************************************************************
+    // VectorSet methods
+
+    /**
+     * Add the value of the specified Vector3f to this set.
+     *
+     * @param vector the value to add (not null, unaffected)
+     */
+    public void add(Vector3f vector) {
+        set.add(vector.clone());
+    }
+
+    /**
+     * Test whether this set contains the value of the specified Vector3f.
+     *
+     * @param vector the value to find (not null, unaffected)
+     * @return true if found, otherwise false
+     */
+    public boolean contains(Vector3f vector) {
+        boolean result = set.contains(vector);
+        return result;
+    }
+
+    /**
+     * Calculate the sample mean for each axis over the Vector3f values in this
+     * set.
+     *
+     * @param storeResult (modified if not null)
+     * @return the sample mean for each axis (either storeResult or a new
+     * Vector3f)
+     */
+    public Vector3f mean(Vector3f storeResult) {
+        int numVectors = numVectors();
+        assert numVectors > 0 : numVectors;
+        Vector3f result = (storeResult == null) ? new Vector3f() : storeResult;
+
+        result.zero();
+        for (Vector3f tempVector : set) {
+            result.addLocal(tempVector);
+        }
+        result.divideLocal(numVectors);
+
+        return result;
+    }
+
+    /**
+     * Calculate the number of Vector3f values in this set.
+     *
+     * @return the count (&ge;0)
+     */
+    public int numVectors() {
+        int numVectors = set.size();
+        assert numVectors >= 0 : numVectors;
+        return numVectors;
+    }
+
+    /**
+     * Access the buffer containing all the Vector3f values in this set. No
+     * further add() is allowed.
+     *
+     * @return a new buffer, flipped
+     */
+    public FloatBuffer toBuffer() {
+        int numFloats = 3 * set.size();
+        FloatBuffer buffer = BufferUtils.createFloatBuffer(numFloats);
+        for (Vector3f tempVector : set) {
+            buffer.put(tempVector.x);
+            buffer.put(tempVector.y);
+            buffer.put(tempVector.z);
+        }
+        buffer.flip();
+
+        return buffer;
+    }
+}

+ 35 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/package-info.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 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.
+ */
+/**
+ * A dynamic animation control and some associated classes.
+ */
+package com.jme3.bullet.animation;

+ 7 - 0
jme3-bullet/src/common/java/com/jme3/bullet/control/AbstractPhysicsControl.java

@@ -246,6 +246,13 @@ public abstract class AbstractPhysicsControl implements PhysicsControl, JmeClone
         setPhysicsRotation(getSpatialRotation());
     }
 
+    /**
+     * @return returns the spatial the control is added to, or null if the control is not attached to a spatial yet.
+     */
+    public Spatial getSpatial(){
+        return this.spatial;
+    }
+
     /**
      * Enable or disable this control.
      * <p>

+ 8 - 0
jme3-bullet/src/common/java/com/jme3/bullet/control/CharacterControl.java

@@ -102,6 +102,7 @@ public class CharacterControl extends PhysicsCharacter implements PhysicsControl
         control.setCcdSweptSphereRadius(getCcdSweptSphereRadius());
         control.setCollideWithGroups(getCollideWithGroups());
         control.setCollisionGroup(getCollisionGroup());
+        control.setContactResponse(isContactResponse());
         control.setFallSpeed(getFallSpeed());
         control.setGravity(getGravity());
         control.setJumpSpeed(getJumpSpeed());
@@ -128,6 +129,13 @@ public class CharacterControl extends PhysicsCharacter implements PhysicsControl
         setPhysicsLocation(getSpatialTranslation());
     }
 
+    /**
+     * @return returns the spatial the control is added to, or null if the control is not attached to a spatial yet.
+     */
+    public Spatial getSpatial(){
+        return this.spatial;
+    }
+
     public void setEnabled(boolean enabled) {
         this.enabled = enabled;
         if (space != null) {

+ 7 - 0
jme3-bullet/src/common/java/com/jme3/bullet/control/GhostControl.java

@@ -206,6 +206,13 @@ public class GhostControl extends PhysicsGhostObject implements PhysicsControl,
         setPhysicsRotation(getSpatialRotation());
     }
 
+    /**
+     * @return returns the spatial the control is added to, or null if the control is not attached to a spatial yet.
+     */
+    public Spatial getSpatial(){
+        return this.spatial;
+    }
+
     /**
      * Enable or disable this control.
      * <p>

+ 8 - 0
jme3-bullet/src/common/java/com/jme3/bullet/control/RigidBodyControl.java

@@ -150,6 +150,7 @@ public class RigidBodyControl extends PhysicsRigidBody implements PhysicsControl
         control.setCcdSweptSphereRadius(getCcdSweptSphereRadius());
         control.setCollideWithGroups(getCollideWithGroups());
         control.setCollisionGroup(getCollisionGroup());
+        control.setContactResponse(isContactResponse());
         control.setDamping(getLinearDamping(), getAngularDamping());
         control.setFriction(getFriction());
         control.setGravity(getGravity());
@@ -206,6 +207,13 @@ public class RigidBodyControl extends PhysicsRigidBody implements PhysicsControl
         setPhysicsRotation(getSpatialRotation());
     }
 
+    /**
+     * @return returns the spatial the control is added to, or null if the control is not attached to a spatial yet.
+     */
+    public Spatial getSpatial(){
+        return this.spatial;
+    }
+
     /**
      * Set the collision shape based on the controlled spatial and its
      * descendents.

+ 1 - 0
jme3-bullet/src/common/java/com/jme3/bullet/control/VehicleControl.java

@@ -168,6 +168,7 @@ public class VehicleControl extends PhysicsVehicle implements PhysicsControl, Jm
         control.setCcdSweptSphereRadius(getCcdSweptSphereRadius());
         control.setCollideWithGroups(getCollideWithGroups());
         control.setCollisionGroup(getCollisionGroup());
+        control.setContactResponse(isContactResponse());
         control.setDamping(getLinearDamping(), getAngularDamping());
         control.setFriction(getFriction());
         control.setGravity(getGravity());

+ 36 - 6
jme3-bullet/src/common/java/com/jme3/bullet/control/ragdoll/RagdollUtils.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2018 jMonkeyEngine
+ * Copyright (c) 2009-2019 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -41,9 +41,12 @@ import com.jme3.math.Transform;
 import com.jme3.math.Vector3f;
 import com.jme3.scene.Mesh;
 import com.jme3.scene.Spatial;
+import com.jme3.scene.VertexBuffer;
 import com.jme3.scene.VertexBuffer.Type;
+import java.nio.Buffer;
 import java.nio.ByteBuffer;
 import java.nio.FloatBuffer;
+import java.nio.ShortBuffer;
 import java.util.*;
 
 /**
@@ -254,7 +257,8 @@ public class RagdollUtils {
     private static List<Float> getPoints(Mesh mesh, int boneIndex, Vector3f initialScale, Vector3f offset, float weightThreshold) {
 
         FloatBuffer vertices = mesh.getFloatBuffer(Type.Position);
-        ByteBuffer boneIndices = (ByteBuffer) mesh.getBuffer(Type.BoneIndex).getData();
+        VertexBuffer biBuf = mesh.getBuffer(VertexBuffer.Type.BoneIndex);
+        Buffer boneIndices = biBuf.getDataReadOnly();
         FloatBuffer boneWeight = (FloatBuffer) mesh.getBuffer(Type.BoneWeight).getData();
 
         vertices.rewind();
@@ -270,7 +274,8 @@ public class RagdollUtils {
             boolean add = false;
             int start = i / 3 * 4;
             for (k = start; k < start + 4; k++) {
-                if (boneIndices.get(k) == boneIndex && boneWeight.get(k) >= weightThreshold) {
+                if (readIndex(boneIndices, k) == boneIndex
+                        && boneWeight.get(k) >= weightThreshold) {
                     add = true;
                     break;
                 }
@@ -349,8 +354,8 @@ public class RagdollUtils {
     public static boolean hasVertices(int boneIndex, Mesh[] targets,
             float weightThreshold) {
         for (Mesh mesh : targets) {
-            ByteBuffer boneIndices
-                    = (ByteBuffer) mesh.getBuffer(Type.BoneIndex).getData();
+            VertexBuffer biBuf = mesh.getBuffer(VertexBuffer.Type.BoneIndex);
+            Buffer boneIndices = biBuf.getDataReadOnly();
             FloatBuffer boneWeight
                     = (FloatBuffer) mesh.getBuffer(Type.BoneWeight).getData();
 
@@ -361,7 +366,7 @@ public class RagdollUtils {
             for (int i = 0; i < vertexComponents; i += 3) {
                 int start = i / 3 * 4;
                 for (int k = start; k < start + 4; k++) {
-                    if (boneIndices.get(k) == boneIndex
+                    if (readIndex(boneIndices, k) == boneIndex
                             && boneWeight.get(k) >= weightThreshold) {
                         return true;
                     }
@@ -371,4 +376,29 @@ public class RagdollUtils {
 
         return false;
     }
+
+    /**
+     * Read an index from a buffer.
+     *
+     * @param buffer a buffer of bytes or shorts (not null)
+     * @param k the position from which the index will be read
+     * @return the index value (&ge;0)
+     */
+    public static int readIndex(Buffer buffer, int k) {
+        int result;
+        if (buffer instanceof ByteBuffer) {
+            ByteBuffer byteBuffer = (ByteBuffer) buffer;
+            byte b = byteBuffer.get(k);
+            result = 0xff & b;
+        } else if (buffer instanceof ShortBuffer) {
+            ShortBuffer shortBuffer = (ShortBuffer) buffer;
+            short s = shortBuffer.get(k);
+            result = 0xffff & s;
+        } else {
+            throw new IllegalArgumentException(buffer.getClass().getName());
+        }
+
+        assert result >= 0 : result;
+        return result;
+    }
 }

+ 24 - 7
jme3-bullet/src/common/java/com/jme3/bullet/debug/BulletCharacterDebugControl.java

@@ -67,6 +67,11 @@ public class BulletCharacterDebugControl extends AbstractPhysicsDebugControl {
      * geometry to visualize myShape (not null)
      */
     protected Spatial geom;
+    /**
+     * physics scale for which geom was generated
+     */
+    final private Vector3f oldScale = new Vector3f();
+
     /**
      * Instantiate an enabled control to visualize the specified character.
      *
@@ -78,7 +83,10 @@ public class BulletCharacterDebugControl extends AbstractPhysicsDebugControl {
         super(debugAppState);
         this.body = body;
         myShape = body.getCollisionShape();
-        this.geom = DebugShapeFactory.getDebugShape(body.getCollisionShape());
+        oldScale.set(myShape.getScale());
+
+        this.geom = DebugShapeFactory.getDebugShape(myShape);
+        this.geom.setName(body.toString());
         geom.setMaterial(debugAppState.DEBUG_PINK);
     }
 
@@ -110,15 +118,24 @@ public class BulletCharacterDebugControl extends AbstractPhysicsDebugControl {
      */
     @Override
     protected void controlUpdate(float tpf) {
-        if(myShape != body.getCollisionShape()){
-            Node node = (Node) this.spatial;
+        CollisionShape newShape = body.getCollisionShape();
+        Vector3f newScale = newShape.getScale();
+        if (myShape != newShape || !oldScale.equals(newScale)) {
+            myShape = newShape;
+            oldScale.set(newScale);
+
+            Node node = (Node) spatial;
             node.detachChild(geom);
-            geom = DebugShapeFactory.getDebugShape(body.getCollisionShape());
-            geom.setMaterial(debugAppState.DEBUG_PINK);
+
+            geom = DebugShapeFactory.getDebugShape(myShape);
+            geom.setName(body.toString());
+
             node.attachChild(geom);
         }
-        applyPhysicsTransform(body.getPhysicsLocation(location), Quaternion.IDENTITY);
-        geom.setLocalScale(body.getCollisionShape().getScale());
+        geom.setMaterial(debugAppState.DEBUG_PINK);
+
+        body.getPhysicsLocation(location);
+        applyPhysicsTransform(location, Quaternion.IDENTITY);
     }
 
     /**

+ 23 - 9
jme3-bullet/src/common/java/com/jme3/bullet/debug/BulletGhostObjectDebugControl.java

@@ -70,7 +70,10 @@ public class BulletGhostObjectDebugControl extends AbstractPhysicsDebugControl {
      * geometry to visualize myShape (not null)
      */
     protected Spatial geom;
-
+    /**
+     * physics scale for which geom was generated
+     */
+    final private Vector3f oldScale = new Vector3f();
     /**
      * Instantiate an enabled control to visualize the specified ghost object.
      *
@@ -81,8 +84,9 @@ public class BulletGhostObjectDebugControl extends AbstractPhysicsDebugControl {
         super(debugAppState);
         this.body = body;
         myShape = body.getCollisionShape();
-        this.geom = DebugShapeFactory.getDebugShape(body.getCollisionShape());
-        this.geom.setName(body.toString());
+        oldScale.set(myShape.getScale());
+        
+        this.geom = DebugShapeFactory.getDebugShape(myShape);
         this.geom.setName(body.toString());
         geom.setMaterial(debugAppState.DEBUG_YELLOW);
     }
@@ -115,15 +119,25 @@ public class BulletGhostObjectDebugControl extends AbstractPhysicsDebugControl {
      */
     @Override
     protected void controlUpdate(float tpf) {
-        if (myShape != body.getCollisionShape()) {
-            Node node = (Node) this.spatial;
+        CollisionShape newShape = body.getCollisionShape();
+        Vector3f newScale = newShape.getScale();
+        if (myShape != newShape || !oldScale.equals(newScale)) {
+            myShape = newShape;
+            oldScale.set(newScale);
+
+            Node node = (Node) spatial;
             node.detachChild(geom);
-            geom = DebugShapeFactory.getDebugShape(body.getCollisionShape());
-            geom.setMaterial(debugAppState.DEBUG_YELLOW);
+
+            geom = DebugShapeFactory.getDebugShape(myShape);
+            geom.setName(body.toString());
+
             node.attachChild(geom);
         }
-        applyPhysicsTransform(body.getPhysicsLocation(location), body.getPhysicsRotation(rotation));
-        geom.setLocalScale(body.getCollisionShape().getScale());
+        geom.setMaterial(debugAppState.DEBUG_YELLOW);
+
+        body.getPhysicsLocation(location);
+        body.getPhysicsRotation(rotation);
+        applyPhysicsTransform(location, rotation);
     }
 
     /**

+ 22 - 6
jme3-bullet/src/common/java/com/jme3/bullet/debug/BulletRigidBodyDebugControl.java

@@ -70,6 +70,10 @@ public class BulletRigidBodyDebugControl extends AbstractPhysicsDebugControl {
      * geometry to visualize myShape (not null)
      */
     protected Spatial geom;
+    /**
+     * physics scale for which geom was generated
+     */
+    final private Vector3f oldScale = new Vector3f();
 
     /**
      * Instantiate an enabled control to visualize the specified body.
@@ -81,7 +85,9 @@ public class BulletRigidBodyDebugControl extends AbstractPhysicsDebugControl {
         super(debugAppState);
         this.body = body;
         myShape = body.getCollisionShape();
-        this.geom = DebugShapeFactory.getDebugShape(body.getCollisionShape());
+        oldScale.set(myShape.getScale());
+
+        this.geom = DebugShapeFactory.getDebugShape(myShape);
         this.geom.setName(body.toString());
         geom.setMaterial(debugAppState.DEBUG_BLUE);
     }
@@ -114,10 +120,18 @@ public class BulletRigidBodyDebugControl extends AbstractPhysicsDebugControl {
      */
     @Override
     protected void controlUpdate(float tpf) {
-        if(myShape != body.getCollisionShape()){
-            Node node = (Node) this.spatial;
+        CollisionShape newShape = body.getCollisionShape();
+        Vector3f newScale = newShape.getScale();
+        if (myShape != newShape || !oldScale.equals(newScale)) {
+            myShape = newShape;
+            oldScale.set(newScale);
+
+            Node node = (Node) spatial;
             node.detachChild(geom);
-            geom = DebugShapeFactory.getDebugShape(body.getCollisionShape());
+
+            geom = DebugShapeFactory.getDebugShape(myShape);
+            geom.setName(body.toString());
+
             node.attachChild(geom);
         }
         if(body.isActive()){
@@ -125,8 +139,10 @@ public class BulletRigidBodyDebugControl extends AbstractPhysicsDebugControl {
         }else{
             geom.setMaterial(debugAppState.DEBUG_BLUE);
         }
-        applyPhysicsTransform(body.getPhysicsLocation(location), body.getPhysicsRotation(rotation));
-        geom.setLocalScale(body.getCollisionShape().getScale());
+
+        body.getPhysicsLocation(location);
+        body.getPhysicsRotation(rotation);
+        applyPhysicsTransform(location, rotation);
     }
 
     /**

+ 3 - 4
jme3-bullet/src/main/java/com/jme3/bullet/PhysicsSpace.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2018 jMonkeyEngine
+ * Copyright (c) 2009-2019 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -55,7 +55,6 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Comparator;
-import java.util.Deque;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentLinkedQueue;
@@ -1002,7 +1001,7 @@ public class PhysicsSpace {
      * unaffected)
      * @return a new list of results (not null)
      */
-    public List rayTest(Vector3f from, Vector3f to) {
+    public List<PhysicsRayTestResult> rayTest(Vector3f from, Vector3f to) {
         List<PhysicsRayTestResult> results = new ArrayList<PhysicsRayTestResult>();
         rayTest(from, to, results);
         
@@ -1019,7 +1018,7 @@ public class PhysicsSpace {
      * unaffected)
      * @return a new list of results (not null)
      */
-    public List rayTestRaw(Vector3f from, Vector3f to) {
+    public List<PhysicsRayTestResult> rayTestRaw(Vector3f from, Vector3f to) {
         List<PhysicsRayTestResult> results = new ArrayList<PhysicsRayTestResult>();
         rayTestRaw(from, to, results);
         

+ 87 - 0
jme3-bullet/src/main/java/com/jme3/bullet/collision/CollisionFlag.java

@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 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 com.jme3.bullet.collision;
+
+/**
+ * Named collision flags for a {@link PhysicsCollisionObject}. Values must agree
+ * with those in BulletCollision/CollisionDispatch/btCollisionObject.h
+ *
+ * @author Stephen Gold [email protected]
+ * @see PhysicsCollisionObject#getCollisionFlags(long)
+ */
+public class CollisionFlag {
+    /**
+     * flag for a static object
+     */
+    final public static int STATIC_OBJECT = 0x1;
+    /**
+     * flag for a kinematic object
+     */
+    final public static int KINEMATIC_OBJECT = 0x2;
+    /**
+     * flag for an object with no contact response, such as a PhysicsGhostObject
+     */
+    final public static int NO_CONTACT_RESPONSE = 0x4;
+    /**
+     * flag to enable a custom material callback for per-triangle
+     * friction/restitution (not used by JME)
+     */
+    final public static int CUSTOM_MATERIAL_CALLBACK = 0x8;
+    /**
+     * flag for a character object, such as a PhysicsCharacter
+     */
+    final public static int CHARACTER_OBJECT = 0x10;
+    /**
+     * flag to disable debug visualization (not used by JME)
+     */
+    final public static int DISABLE_VISUALIZE_OBJECT = 0x20;
+    /**
+     * flag to disable parallel/SPU processing (not used by JME)
+     */
+    final public static int DISABLE_SPU_COLLISION_PROCESSING = 0x40;
+    /**
+     * flag not used by JME
+     */
+    final public static int HAS_CONTACT_STIFFNESS_DAMPING = 0x80;
+    /**
+     * flag not used by JME
+     */
+    final public static int HAS_CUSTOM_DEBUG_RENDERING_COLOR = 0x100;
+    /**
+     * flag not used by JME
+     */
+    final public static int HAS_FRICTION_ANCHOR = 0x200;
+    /**
+     * flag not used by JME
+     */
+    final public static int HAS_COLLISION_SOUND_TRIGGER = 0x400;
+}

+ 29 - 0
jme3-bullet/src/main/java/com/jme3/bullet/collision/PhysicsCollisionObject.java

@@ -254,6 +254,17 @@ public abstract class PhysicsCollisionObject implements Savable {
         return userObject;
     }
 
+    /**
+     * Test whether this object responds to contact with other objects.
+     *
+     * @return true if responsive, otherwise false
+     */
+    public boolean isContactResponse() {
+        int flags = getCollisionFlags(objectId);
+        boolean result = (flags & CollisionFlag.NO_CONTACT_RESPONSE) == 0x0;
+        return result;
+    }
+
     /**
      * Associate a user object (such as a Spatial) with this collision object.
      *
@@ -314,6 +325,24 @@ public abstract class PhysicsCollisionObject implements Savable {
         collisionShape = shape;
     }
 
+    /**
+     * Read the collision flags of this object. Subclasses are responsible for
+     * cloning/reading/writing these flags.
+     *
+     * @param objectId the ID of the btCollisionObject (not zero)
+     * @return the collision flags (bit mask)
+     */
+    native protected int getCollisionFlags(long objectId);
+
+    /**
+     * Alter the collision flags of this object. Subclasses are responsible for
+     * cloning/reading/writing these flags.
+     *
+     * @param objectId the ID of the btCollisionObject (not zero)
+     * @param desiredFlags the desired collision flags (bit mask)
+     */
+    native protected void setCollisionFlags(long objectId, int desiredFlags);
+
     /**
      * Finalize this collision object just before it is destroyed. Should be
      * invoked only by a subclass or by the garbage collector.

+ 29 - 3
jme3-bullet/src/main/java/com/jme3/bullet/collision/shapes/CollisionShape.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2018 jMonkeyEngine
+ * Copyright (c) 2009-2019 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -49,6 +49,11 @@ import java.util.logging.Logger;
  */
 public abstract class CollisionShape implements Savable {
 
+    /**
+     * default margin for new non-sphere/non-capsule shapes (in physics-space
+     * units, &gt;0, default=0.04)
+     */
+    private static float defaultMargin = 0.04f;
     /**
      * unique identifier of the btCollisionShape
      * <p>
@@ -63,7 +68,7 @@ public abstract class CollisionShape implements Savable {
     /**
      * copy of collision margin (in physics-space units, &gt;0, default=0)
      */
-    protected float margin = 0.0f;
+    protected float margin = defaultMargin;
 
     public CollisionShape() {
     }
@@ -135,6 +140,27 @@ public abstract class CollisionShape implements Savable {
     
     private native float getMargin(long objectId);
 
+    /**
+     * Alter the default margin for new shapes that are neither capsules nor
+     * spheres.
+     *
+     * @param margin the desired margin distance (in physics-space units, &gt;0,
+     * default=0.04)
+     */
+    public static void setDefaultMargin(float margin) {
+        defaultMargin = margin;
+    }
+
+    /**
+     * Read the default margin for new shapes.
+     *
+     * @return margin the default margin distance (in physics-space units,
+     * &gt;0)
+     */
+    public static float getDefaultMargin() {
+        return defaultMargin;
+    }
+
     /**
      * Alter the collision margin of this shape. CAUTION: Margin is applied
      * differently, depending on the type of shape. Generally the collision
@@ -145,7 +171,7 @@ public abstract class CollisionShape implements Savable {
      * compound shapes) changes can have unintended consequences.
      *
      * @param margin the desired margin distance (in physics-space units, &gt;0,
-     * default=0)
+     * default=0.04)
      */
     public void setMargin(float margin) {
         setMargin(objectId, margin);

+ 0 - 1
jme3-bullet/src/main/java/com/jme3/bullet/collision/shapes/MeshCollisionShape.java

@@ -33,7 +33,6 @@ package com.jme3.bullet.collision.shapes;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
 import java.nio.FloatBuffer;
 import java.util.logging.Level;
 import java.util.logging.Logger;

+ 39 - 11
jme3-bullet/src/main/java/com/jme3/bullet/objects/PhysicsCharacter.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2018 jMonkeyEngine
+ * Copyright (c) 2009-2019 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -31,6 +31,7 @@
  */
 package com.jme3.bullet.objects;
 
+import com.jme3.bullet.collision.CollisionFlag;
 import com.jme3.bullet.collision.PhysicsCollisionObject;
 import com.jme3.bullet.collision.shapes.CollisionShape;
 import com.jme3.export.InputCapsule;
@@ -50,7 +51,6 @@ import java.util.logging.Logger;
  * @author normenhansen
  */
 public class PhysicsCharacter extends PhysicsCollisionObject {
-
    /**
      * Unique identifier of btKinematicCharacterController (as opposed to its
      * collision object, which is a ghost). Constructors are responsible for
@@ -88,6 +88,12 @@ public class PhysicsCharacter extends PhysicsCollisionObject {
 //        }
         this.stepHeight = stepHeight;
         buildObject();
+        /*
+         * The default gravity for a Bullet btKinematicCharacterController
+         * is (0,0,-29.4), which makes no sense for JME.
+         * So override the default.
+         */
+        setGravity(new Vector3f(0f, -29.4f, 0f));
     }
 
     /**
@@ -289,14 +295,14 @@ public class PhysicsCharacter extends PhysicsCollisionObject {
     /**
      * @deprecated Deprecated in bullet 2.86.1. Use setGravity(Vector3f)
      * instead.
-     * @param value the desired upward component of the acceleration (typically
-     * negative)
+     * @param value the desired downward (-Y) component of the acceleration
+     * (typically positive)
      */
     @Deprecated
     public void setGravity(float value) {
-    	setGravity(new Vector3f(0,value,0));
+        setGravity(new Vector3f(0, -value, 0));
     }
-    
+
     /**
      * Alter this character's gravitational acceleration.
      *
@@ -311,11 +317,12 @@ public class PhysicsCharacter extends PhysicsCollisionObject {
     /**
      * @deprecated Deprecated in bullet 2.86.1. Use getGravity(Vector3f)
      * instead.
-     * @return the upward component of the acceleration (typically negative)
+     * @return the downward (-Y) component of the acceleration (typically
+     * positive)
      */
     @Deprecated
     public float getGravity() {
-        return getGravity(null).y;
+        return -getGravity(null).y;
     }
 
     /**
@@ -456,6 +463,22 @@ public class PhysicsCharacter extends PhysicsCollisionObject {
 
     private native float getMaxSlope(long characterId);
 
+    /**
+     * Enable/disable this character's contact response.
+     *
+     * @param responsive true to respond to contacts, false to ignore them
+     * (default=true)
+     */
+    public void setContactResponse(boolean responsive) {
+        int flags = getCollisionFlags(objectId);
+        if (responsive) {
+            flags &= ~CollisionFlag.NO_CONTACT_RESPONSE;
+        } else {
+            flags |= CollisionFlag.NO_CONTACT_RESPONSE;
+        }
+        setCollisionFlags(objectId, flags);
+    }
+
     /**
      * Test whether this character is on the ground.
      *
@@ -472,10 +495,15 @@ public class PhysicsCharacter extends PhysicsCollisionObject {
      */
     @Deprecated
     public void jump() {
-    	jump(Vector3f.UNIT_Y);
+        jump(Vector3f.ZERO);
+        /*
+         * The zero vector is treated as a special case
+         * by Bullet's btKinematicCharacterController::jump(),
+         * causing the character to jump in its "up" direction
+         * with the pre-set speed.
+         */
     }
-    
-    
+
     /**
      * Jump in the specified direction.
      *

+ 23 - 1
jme3-bullet/src/main/java/com/jme3/bullet/objects/PhysicsRigidBody.java

@@ -32,6 +32,7 @@
 package com.jme3.bullet.objects;
 
 import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.CollisionFlag;
 import com.jme3.bullet.collision.PhysicsCollisionObject;
 import com.jme3.bullet.collision.shapes.CollisionShape;
 import com.jme3.bullet.collision.shapes.MeshCollisionShape;
@@ -56,7 +57,6 @@ import java.util.logging.Logger;
  * @author normenhansen
  */
 public class PhysicsRigidBody extends PhysicsCollisionObject {
-
     /**
      * motion state
      */
@@ -370,6 +370,22 @@ public class PhysicsRigidBody extends PhysicsCollisionObject {
         return kinematic;
     }
 
+    /**
+     * Enable/disable this body's contact response.
+     *
+     * @param responsive true to respond to contacts, false to ignore them
+     * (default=true)
+     */
+    public void setContactResponse(boolean responsive) {
+        int flags = getCollisionFlags(objectId);
+        if (responsive) {
+            flags &= ~CollisionFlag.NO_CONTACT_RESPONSE;
+        } else {
+            flags |= CollisionFlag.NO_CONTACT_RESPONSE;
+        }
+        setCollisionFlags(objectId, flags);
+    }
+
     /**
      * Alter the radius of the swept sphere used for continuous collision
      * detection (CCD).
@@ -993,6 +1009,7 @@ public class PhysicsRigidBody extends PhysicsCollisionObject {
         capsule.write(getMass(), "mass", 1.0f);
 
         capsule.write(getGravity(), "gravity", Vector3f.ZERO);
+        capsule.write(isContactResponse(), "contactResponse", true);
         capsule.write(getFriction(), "friction", 0.5f);
         capsule.write(getRestitution(), "restitution", 0);
         Vector3f angularFactor = getAngularFactor(null);
@@ -1014,6 +1031,8 @@ public class PhysicsRigidBody extends PhysicsCollisionObject {
 
         capsule.write(getPhysicsLocation(new Vector3f()), "physicsLocation", new Vector3f());
         capsule.write(getPhysicsRotationMatrix(new Matrix3f()), "physicsRotation", new Matrix3f());
+        capsule.write(getLinearVelocity(), "linearVelocity", null);
+        capsule.write(getAngularVelocity(), "angularVelocity", null);
 
         capsule.writeSavableArrayList(joints, "joints", null);
     }
@@ -1033,6 +1052,7 @@ public class PhysicsRigidBody extends PhysicsCollisionObject {
         this.mass = mass;
         rebuildRigidBody();
         setGravity((Vector3f) capsule.readSavable("gravity", Vector3f.ZERO.clone()));
+        setContactResponse(capsule.readBoolean("contactResponse", true));
         setFriction(capsule.readFloat("friction", 0.5f));
         setKinematic(capsule.readBoolean("kinematic", false));
 
@@ -1051,6 +1071,8 @@ public class PhysicsRigidBody extends PhysicsCollisionObject {
 
         setPhysicsLocation((Vector3f) capsule.readSavable("physicsLocation", new Vector3f()));
         setPhysicsRotation((Matrix3f) capsule.readSavable("physicsRotation", new Matrix3f()));
+        setLinearVelocity((Vector3f) capsule.readSavable("linearVelocity", new Vector3f()));
+        setAngularVelocity((Vector3f) capsule.readSavable("angularVelocity", new Vector3f()));
 
         joints = capsule.readSavableArrayList("joints", null);
     }

+ 3 - 9
jme3-bullet/src/main/java/com/jme3/bullet/util/DebugShapeFactory.java

@@ -35,7 +35,6 @@ import com.jme3.bullet.collision.shapes.CollisionShape;
 import com.jme3.bullet.collision.shapes.CompoundCollisionShape;
 import com.jme3.bullet.collision.shapes.infos.ChildCollisionShape;
 import com.jme3.math.Matrix3f;
-import com.jme3.math.Vector3f;
 import com.jme3.scene.Geometry;
 import com.jme3.scene.Mesh;
 import com.jme3.scene.Node;
@@ -46,6 +45,7 @@ import java.util.Iterator;
 import java.util.List;
 
 /**
+ * A utility class to generate debug spatials from Bullet collision shapes.
  *
  * @author CJ Hare, normenhansen
  */
@@ -128,14 +128,8 @@ public class DebugShapeFactory {
     public static Mesh getDebugMesh(CollisionShape shape) {
         Mesh mesh = new Mesh();
         DebugMeshCallback callback = new DebugMeshCallback();
-        /*
-         * Populate the mesh based on an unscaled shape;
-         * the shape's scale will be applied later, to the geometry.
-         */
-        Vector3f savedScale = shape.getScale().clone();
-        shape.setScale(Vector3f.UNIT_XYZ);
-        getVertices(shape.getObjectId(), callback);
-        shape.setScale(savedScale);
+        long id = shape.getObjectId();
+        getVertices(id, callback);
 
         mesh.setBuffer(Type.Position, 3, callback.getVertices());
         mesh.getFloatBuffer(Type.Position).clear();

+ 2 - 2
jme3-core/src/main/java/checkers/quals/SubtypeOf.java

@@ -6,8 +6,8 @@ import java.lang.annotation.*;
  * A meta-annotation to specify all the qualifiers that the given qualifier
  * is a subtype of.  This provides a declarative way to specify the type
  * qualifier hierarchy.  (Alternatively, the hierarchy can be defined
- * procedurally by subclassing {@link checkers.types.QualifierHierarchy} or
- * {@link checkers.types.TypeHierarchy}.)
+ * procedurally by subclassing checkers.types.QualifierHierarchy or
+ * checkers.types.TypeHierarchy.)
  *
  * <p>
  * Example:

+ 1 - 1
jme3-core/src/main/java/checkers/quals/package-info.java

@@ -5,6 +5,6 @@
  * They may serve as documentation for the type qualifiers, and aid the
  * Checker Framework to infer the relations between the type qualifiers.
  *
- * @checker.framework.manual #writing-a-checker Writing a checker
+ * checker.framework.manual #writing-a-checker Writing a checker
  */
 package checkers.quals;

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

@@ -1,8 +1,6 @@
 package com.jme3.anim;
 
-import com.jme3.anim.tween.Tween;
 import com.jme3.export.*;
-import com.jme3.util.SafeArrayList;
 import com.jme3.util.clone.Cloner;
 import com.jme3.util.clone.JmeCloneable;
 

+ 110 - 9
jme3-core/src/main/java/com/jme3/anim/AnimComposer.java

@@ -3,7 +3,10 @@ package com.jme3.anim;
 import com.jme3.anim.tween.Tween;
 import com.jme3.anim.tween.Tweens;
 import com.jme3.anim.tween.action.*;
-import com.jme3.export.*;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
 import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.ViewPort;
 import com.jme3.scene.control.AbstractControl;
@@ -26,7 +29,7 @@ public class AnimComposer extends AbstractControl {
     private Map<String, Layer> layers = new LinkedHashMap<>();
 
     public AnimComposer() {
-        layers.put(DEFAULT_LAYER, new Layer());
+        layers.put(DEFAULT_LAYER, new Layer(this));
     }
 
     /**
@@ -67,18 +70,47 @@ public class AnimComposer extends AbstractControl {
     public Action setCurrentAction(String name) {
         return setCurrentAction(name, DEFAULT_LAYER);
     }
-
+    
+    /**
+     * Run an action on specified layer.
+     * 
+     * @param actionName The name of the action to run.
+     * @param layerName The layer on which action should run.
+     * @return The action corresponding to the given name.
+     */
     public Action setCurrentAction(String actionName, String layerName) {
         Layer l = layers.get(layerName);
         if (l == null) {
             throw new IllegalArgumentException("Unknown layer " + layerName);
         }
+        
         Action currentAction = action(actionName);
         l.time = 0;
         l.currentAction = currentAction;
         return currentAction;
     }
+    
+    /**
+     * Remove current action on specified layer.
+     *
+     * @param layerName The name of the layer we want to remove its action.
+     */
+    public void removeCurrentAction(String layerName) {
+        Layer l = layers.get(layerName);
+        if (l == null) {
+            throw new IllegalArgumentException("Unknown layer " + layerName);
+        }
+        
+        l.time = 0;
+        l.currentAction = null;
+    }
 
+    /**
+     * 
+     * @param name The name of the action to return.
+     * @return The action registered with specified name. It will make a new action if there isn't any.
+     * @see #makeAction(java.lang.String)
+     */
     public Action action(String name) {
         Action action = actions.get(name);
         if (action == null) {
@@ -87,7 +119,33 @@ public class AnimComposer extends AbstractControl {
         }
         return action;
     }
+    
+    /**
+     * 
+     * @param name The name of the action to return.
+     * @return The action registered with specified name or null if nothing is registered.
+     */
+    public Action getAction(String name){
+        return actions.get(name);
+    }
+    
+    /**
+     * Register given action with specified name.
+     * 
+     * @param name The name of the action.
+     * @param action The action to add.
+     */
+    public void addAction(String name, Action action){
+        actions.put(name, action);
+    }
 
+    /**
+     * Create a new ClipAction with specified clip name.
+     * 
+     * @param name The name of the clip.
+     * @return a new action
+     * @throws IllegalArgumentException if clip with specified name not found.
+     */
     public Action makeAction(String name) {
         Action action;
         AnimClip clip = animClipMap.get(name);
@@ -97,13 +155,35 @@ public class AnimComposer extends AbstractControl {
         action = new ClipAction(clip);
         return action;
     }
+    
+    public boolean hasAction(String name) {
+        return actions.containsKey(name);
+    }
+    
+    /**
+     * Remove specified action.
+     *
+     * @param name The name of the action to remove.
+     * @return The removed action.
+     */
+    public Action removeAction(String name) {
+        return actions.remove(name);
+    }
 
-    public void makeLayer(String name, AnimationMask mask){
-        Layer l = new Layer();
+    public void makeLayer(String name, AnimationMask mask) {
+        Layer l = new Layer(this);
         l.mask = mask;
         layers.put(name, l);
     }
 
+    /**
+     * Remove specified layer. This will stop the current action on this layer.
+     *
+     * @param name The name of the layer to remove.
+     */
+    public void removeLayer(String name) {
+        layers.remove(name);
+    }
 
     public BaseAction actionSequence(String name, Tween... tweens) {
         BaseAction action = new BaseAction(Tweens.sequence(tweens));
@@ -129,12 +209,25 @@ public class AnimComposer extends AbstractControl {
         }
     }
 
+    /**
+     * Returns an unmodifiable collection of all available animations. When an attempt
+     * is made to modify the collection, an UnsupportedOperationException is thrown.
+     *
+     * @return the unmodifiable collection of animations
+     */
     public Collection<AnimClip> getAnimClips() {
         return Collections.unmodifiableCollection(animClipMap.values());
     }
 
-    public Collection<String> getAnimClipsNames() {
-        return Collections.unmodifiableCollection(animClipMap.keySet());
+    /**
+     * Returns an unmodifiable set of all available animation names. When an
+     * attempt is made to modify the set, an UnsupportedOperationException is
+     * thrown.
+     *
+     * @return the unmodifiable set of animation names.
+     */
+    public Set<String> getAnimClipsNames() {
+        return Collections.unmodifiableSet(animClipMap.keySet());
     }
 
     @Override
@@ -207,6 +300,7 @@ public class AnimComposer extends AbstractControl {
         super.read(im);
         InputCapsule ic = im.getCapsule(this);
         animClipMap = (Map<String, AnimClip>) ic.readStringSavableMap("animClipMap", new HashMap<String, AnimClip>());
+        globalSpeed = ic.readFloat("globalSpeed", 1f);
     }
 
     @Override
@@ -214,16 +308,22 @@ public class AnimComposer extends AbstractControl {
         super.write(ex);
         OutputCapsule oc = ex.getCapsule(this);
         oc.writeStringSavableMap(animClipMap, "animClipMap", new HashMap<String, AnimClip>());
+        oc.write(globalSpeed, "globalSpeed", 1f);
     }
 
-    private class Layer implements JmeCloneable {
+    private static class Layer implements JmeCloneable {
+        private AnimComposer ac;
         private Action currentAction;
         private AnimationMask mask;
         private float weight;
         private double time;
 
+        public Layer(AnimComposer ac) {
+            this.ac = ac;
+        }
+        
         public void advance(float tpf) {
-            time += tpf * currentAction.getSpeed() * globalSpeed;
+            time += tpf * currentAction.getSpeed() * ac.globalSpeed;
             // make sure negative time is in [0, length] range
             if (time < 0) {
                 double length = currentAction.getLength();
@@ -244,6 +344,7 @@ public class AnimComposer extends AbstractControl {
 
         @Override
         public void cloneFields(Cloner cloner, Object original) {
+            ac = cloner.clone(ac);
             currentAction = null;
         }
     }

+ 12 - 10
jme3-core/src/main/java/com/jme3/anim/Armature.java

@@ -63,7 +63,7 @@ public class Armature implements JmeCloneable, Savable {
     }
 
     /**
-     * Update all joints sin this Amature.
+     * Update all joints in this Armature.
      */
     public void update() {
         for (Joint rootJoint : rootJoints) {
@@ -83,7 +83,9 @@ public class Armature implements JmeCloneable, Savable {
      * Default is {@link MatrixJointModelTransform}
      *
      * @param modelTransformClass
-     * @see {@link JointModelTransform},{@link MatrixJointModelTransform},{@link SeparateJointModelTransform},
+     * @see JointModelTransform
+     * @see MatrixJointModelTransform
+     * @see SeparateJointModelTransform
      */
     public void setModelTransformClass(Class<? extends JointModelTransform> modelTransformClass) {
         this.modelTransformClass = modelTransformClass;
@@ -106,7 +108,7 @@ public class Armature implements JmeCloneable, Savable {
     /**
      * returns the array of all root joints of this Armature
      *
-     * @return
+     * @return the pre-existing array
      */
     public Joint[] getRoots() {
         return rootJoints;
@@ -120,7 +122,7 @@ public class Armature implements JmeCloneable, Savable {
      * return a joint for the given index
      *
      * @param index
-     * @return
+     * @return the pre-existing instance
      */
     public Joint getJoint(int index) {
         return jointList[index];
@@ -130,7 +132,7 @@ public class Armature implements JmeCloneable, Savable {
      * returns the joint with the given name
      *
      * @param name
-     * @return
+     * @return the pre-existing instance or null if not found
      */
     public Joint getJoint(String name) {
         for (int i = 0; i < jointList.length; i++) {
@@ -145,7 +147,7 @@ public class Armature implements JmeCloneable, Savable {
      * returns the bone index of the given bone
      *
      * @param joint
-     * @return
+     * @return the index (&ge;0) or -1 if not found
      */
     public int getJointIndex(Joint joint) {
         for (int i = 0; i < jointList.length; i++) {
@@ -161,7 +163,7 @@ public class Armature implements JmeCloneable, Savable {
      * returns the joint index of the joint that has the given name
      *
      * @param name
-     * @return
+     * @return the index (&ge;0) or -1 if not found
      */
     public int getJointIndex(String name) {
         for (int i = 0; i < jointList.length; i++) {
@@ -219,7 +221,7 @@ public class Armature implements JmeCloneable, Savable {
     /**
      * Compute the skinning matrices for each bone of the armature that would be used to transform vertices of associated meshes
      *
-     * @return
+     * @return the pre-existing array
      */
     public Matrix4f[] computeSkinningMatrices() {
         for (int i = 0; i < jointList.length; i++) {
@@ -231,7 +233,7 @@ public class Armature implements JmeCloneable, Savable {
     /**
      * returns the number of joints of this armature
      *
-     * @return
+     * @return the count (&ge;0)
      */
     public int getJointCount() {
         return jointList.length;
@@ -274,7 +276,7 @@ public class Armature implements JmeCloneable, Savable {
         try {
             modelTransformClass = (Class<? extends JointModelTransform>) Class.forName(className);
         } catch (ClassNotFoundException e) {
-            throw new AssetLoadException("Cannnot find class for name " + className);
+            throw new AssetLoadException("Cannot find class for name " + className);
         }
 
         int i = 0;

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

@@ -154,7 +154,7 @@ public class Joint implements Savable, JmeCloneable, HasLocalTransform {
     /**
      * Sets the local transform with the bind transforms
      */
-    protected void applyBindPose() {
+    public void applyBindPose() {
         jointModelTransform.applyBindPose(localTransform, inverseModelBindMatrix, parent);
         updateModelTransforms();
 

+ 3 - 5
jme3-core/src/main/java/com/jme3/anim/MorphTrack.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2019 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -32,11 +32,9 @@
 package com.jme3.anim;
 
 import com.jme3.anim.interpolator.FrameInterpolator;
-import com.jme3.animation.*;
 import com.jme3.export.*;
 import com.jme3.scene.Geometry;
 import com.jme3.util.clone.Cloner;
-import com.jme3.util.clone.JmeCloneable;
 
 import java.io.IOException;
 
@@ -79,7 +77,7 @@ public class MorphTrack implements AnimTrack<float[]> {
     /**
      * return the array of weights of this track
      *
-     * @return
+     * @return the pre-existing array
      */
     public float[] getWeights() {
         return weights;
@@ -88,7 +86,7 @@ public class MorphTrack implements AnimTrack<float[]> {
     /**
      * returns the arrays of time for this track
      *
-     * @return
+     * @return the pre-existing array
      */
     public float[] getTimes() {
         return times;

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

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2017 jMonkeyEngine
+ * Copyright (c) 2009-2019 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -409,7 +409,7 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
     /**
      * returns the armature of this control
      *
-     * @return
+     * @return the pre-existing instance
      */
     public Armature getArmature() {
         return armature;

+ 5 - 7
jme3-core/src/main/java/com/jme3/anim/TransformTrack.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2019 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -32,14 +32,12 @@
 package com.jme3.anim;
 
 import com.jme3.anim.interpolator.FrameInterpolator;
-import com.jme3.anim.tween.Tween;
 import com.jme3.anim.util.HasLocalTransform;
 import com.jme3.animation.CompactQuaternionArray;
 import com.jme3.animation.CompactVector3Array;
 import com.jme3.export.*;
 import com.jme3.math.*;
 import com.jme3.util.clone.Cloner;
-import com.jme3.util.clone.JmeCloneable;
 
 import java.io.IOException;
 
@@ -84,7 +82,7 @@ public class TransformTrack implements AnimTrack<Transform> {
     /**
      * return the array of rotations of this track
      *
-     * @return
+     * @return an array
      */
     public Quaternion[] getRotations() {
         return rotations.toObjectArray();
@@ -93,7 +91,7 @@ public class TransformTrack implements AnimTrack<Transform> {
     /**
      * returns the array of scales for this track
      *
-     * @return
+     * @return an array or null
      */
     public Vector3f[] getScales() {
         return scales == null ? null : scales.toObjectArray();
@@ -102,7 +100,7 @@ public class TransformTrack implements AnimTrack<Transform> {
     /**
      * returns the arrays of time for this track
      *
-     * @return
+     * @return the pre-existing array
      */
     public float[] getTimes() {
         return times;
@@ -111,7 +109,7 @@ public class TransformTrack implements AnimTrack<Transform> {
     /**
      * returns the array of translations of this track
      *
-     * @return
+     * @return an array
      */
     public Vector3f[] getTranslations() {
         return translations.toObjectArray();

+ 31 - 5
jme3-core/src/main/java/com/jme3/anim/tween/AbstractTween.java

@@ -36,10 +36,8 @@
 
 package com.jme3.anim.tween;
 
-
-import com.jme3.export.*;
-
-import java.io.IOException;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
 
 /**
  * Base implementation of the Tween interface that provides
@@ -50,7 +48,7 @@ import java.io.IOException;
  *
  * @author Paul Speed
  */
-public abstract class AbstractTween implements Tween {
+public abstract class AbstractTween implements JmeCloneable, Tween {
 
     private double length;
 
@@ -94,4 +92,32 @@ public abstract class AbstractTween implements Tween {
     }
 
     protected abstract void doInterpolate(double t);
+
+    /**
+     * Create a shallow clone for the JME cloner.
+     *
+     * @return a new tween (not null)
+     */
+    @Override
+    public AbstractTween jmeClone() {
+        try {
+            AbstractTween clone = (AbstractTween) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException exception) {
+            throw new RuntimeException(exception);
+        }
+    }
+
+    /**
+     * Callback from {@link com.jme3.util.clone.Cloner} to convert this
+     * shallow-cloned tween into a deep-cloned one, using the specified cloner
+     * and original to resolve copied fields.
+     *
+     * @param cloner the cloner that's cloning this tween (not null)
+     * @param original the tween from which this tween was shallow-cloned
+     * (unused)
+     */
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+    }
 }

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

@@ -36,9 +36,6 @@
 
 package com.jme3.anim.tween;
 
-
-import com.jme3.export.Savable;
-
 /**
  * Represents some action that interpolates across input between 0
  * and some length value.  (For example, movement, rotation, fading.)

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است