Browse Source

Merge branch 'master' into capdevon-SimpleApplication

codex 2 months ago
parent
commit
bd1032df24
100 changed files with 10356 additions and 2282 deletions
  1. 1 0
      .github/.well-known/funding-manifest-urls
  2. 68 0
      .github/actions/tools/uploadToCentral.sh
  3. 135 60
      .github/workflows/main.yml
  4. 3 1
      .github/workflows/screenshot-test-comment.yml
  5. 1 1
      README.md
  6. 20 12
      common.gradle
  7. 97 6
      jme3-android-native/openalsoft.gradle
  8. 3 1
      jme3-android-native/src/native/jme_bufferallocator/Application.mk
  9. 3 1
      jme3-android-native/src/native/jme_decode/Application.mk
  10. 1 0
      jme3-android-native/src/native/jme_decode/com_jme3_audio_plugins_NativeVorbisFile.c
  11. 45 94
      jme3-android-native/src/native/jme_openalsoft/Android.mk
  12. 2 1
      jme3-android-native/src/native/jme_openalsoft/Application.mk
  13. 148 83
      jme3-awt-dialogs/src/main/java/com/jme3/awt/AWTSettingsDialog.java
  14. 54 7
      jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java
  15. 181 150
      jme3-core/src/main/java/com/jme3/anim/SkinningControl.java
  16. 3 1
      jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java
  17. 14 1
      jme3-core/src/main/java/com/jme3/app/SimpleApplication.java
  18. 75 25
      jme3-core/src/main/java/com/jme3/app/state/ConstantVerifierState.java
  19. 5 1
      jme3-core/src/main/java/com/jme3/audio/AudioNode.java
  20. 17 19
      jme3-core/src/main/java/com/jme3/cinematic/events/MotionEvent.java
  21. 5 3
      jme3-core/src/main/java/com/jme3/effect/influencers/NewtonianParticleInfluencer.java
  22. 92 39
      jme3-core/src/main/java/com/jme3/effect/shapes/EmitterMeshFaceShape.java
  23. 167 0
      jme3-core/src/main/java/com/jme3/environment/util/Circle.java
  24. 359 86
      jme3-core/src/main/java/com/jme3/environment/util/LightsDebugState.java
  25. 140 57
      jme3-core/src/main/java/com/jme3/font/BitmapFont.java
  26. 179 98
      jme3-core/src/main/java/com/jme3/font/BitmapText.java
  27. 136 123
      jme3-core/src/main/java/com/jme3/input/FlyByCamera.java
  28. 16 16
      jme3-core/src/main/java/com/jme3/input/JoystickButton.java
  29. 10 1
      jme3-core/src/main/java/com/jme3/light/AmbientLight.java
  30. 11 6
      jme3-core/src/main/java/com/jme3/light/DirectionalLight.java
  31. 13 13
      jme3-core/src/main/java/com/jme3/light/LightProbe.java
  32. 12 1
      jme3-core/src/main/java/com/jme3/light/PointLight.java
  33. 16 5
      jme3-core/src/main/java/com/jme3/light/SpotLight.java
  34. 9 5
      jme3-core/src/main/java/com/jme3/material/Materials.java
  35. 4 4
      jme3-core/src/main/java/com/jme3/material/RenderState.java
  36. 109 104
      jme3-core/src/main/java/com/jme3/math/ColorRGBA.java
  37. 249 119
      jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java
  38. 120 130
      jme3-core/src/main/java/com/jme3/renderer/RenderManager.java
  39. 3 1
      jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java
  40. 4 0
      jme3-core/src/main/java/com/jme3/scene/Geometry.java
  41. 9 0
      jme3-core/src/main/java/com/jme3/scene/Node.java
  42. 37 1
      jme3-core/src/main/java/com/jme3/scene/Spatial.java
  43. 22 5
      jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java
  44. 157 60
      jme3-core/src/main/java/com/jme3/scene/control/LightControl.java
  45. 113 27
      jme3-core/src/main/java/com/jme3/scene/debug/WireFrustum.java
  46. 345 111
      jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java
  47. 136 50
      jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java
  48. 26 10
      jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureInterJointsWire.java
  49. 215 57
      jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java
  50. 7 0
      jme3-core/src/main/java/com/jme3/scene/threadwarden/IllegalThreadSceneGraphMutation.java
  51. 160 0
      jme3-core/src/main/java/com/jme3/scene/threadwarden/SceneGraphThreadWarden.java
  52. 52 6
      jme3-core/src/main/java/com/jme3/shader/VarType.java
  53. 18 30
      jme3-core/src/main/java/com/jme3/shadow/AbstractShadowFilter.java
  54. 221 145
      jme3-core/src/main/java/com/jme3/shadow/AbstractShadowRenderer.java
  55. 41 44
      jme3-core/src/main/java/com/jme3/shadow/PointLightShadowRenderer.java
  56. 144 39
      jme3-core/src/main/java/com/jme3/system/AppSettings.java
  57. 72 0
      jme3-core/src/main/java/com/jme3/util/BufferInputStream.java
  58. 38 0
      jme3-core/src/main/resources/Common/MatDefs/Misc/Dashed.j3md
  59. 23 0
      jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/DashedPattern.j3sn
  60. 9 0
      jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/DashedPattern100.frag
  61. BIN
      jme3-core/src/main/resources/Common/Textures/lightbulb32.png
  62. 153 72
      jme3-core/src/plugins/java/com/jme3/audio/plugins/WAVLoader.java
  63. 46 45
      jme3-core/src/plugins/java/com/jme3/export/binary/BinaryExporter.java
  64. 7 9
      jme3-core/src/plugins/java/com/jme3/material/plugins/ShaderNodeDefinitionLoader.java
  65. 30 0
      jme3-core/src/test/java/com/jme3/math/ColorRGBATest.java
  66. 32 0
      jme3-core/src/test/java/com/jme3/scene/SpatialTest.java
  67. 207 0
      jme3-core/src/test/java/com/jme3/scene/threadwarden/SceneGraphThreadWardenGeometryExtendedTest.java
  68. 203 0
      jme3-core/src/test/java/com/jme3/scene/threadwarden/SceneGraphThreadWardenNodeExtendedTest.java
  69. 316 0
      jme3-core/src/test/java/com/jme3/scene/threadwarden/SceneGraphThreadWardenTest.java
  70. 79 69
      jme3-effects/src/main/java/com/jme3/post/ssao/SSAOFilter.java
  71. 63 0
      jme3-effects/src/test/java/com/jme3/post/filters/SSAOFilterTest.java
  72. 79 23
      jme3-examples/src/main/java/jme3test/export/TestOgreConvert.java
  73. 13 2
      jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java
  74. 85 43
      jme3-examples/src/main/java/jme3test/stress/TestLodGeneration.java
  75. 35 0
      jme3-ios-native/build.gradle
  76. 11 0
      jme3-ios-native/export.sh
  77. 416 0
      jme3-ios-native/jme3-ios-native.xcodeproj/project.pbxproj
  78. 22 0
      jme3-ios-native/src/Info.plist
  79. 130 0
      jme3-ios-native/src/JmeAppHarness.java
  80. 60 0
      jme3-ios-native/src/JmeAppHarness.m
  81. 2392 0
      jme3-ios-native/src/JmeIosGLES.m
  82. 138 0
      jme3-ios-native/src/com_jme3_audio_ios_IosAL.c
  83. 173 0
      jme3-ios-native/src/com_jme3_audio_ios_IosAL.h
  84. 178 0
      jme3-ios-native/src/com_jme3_audio_ios_IosALC.c
  85. 77 0
      jme3-ios-native/src/com_jme3_audio_ios_IosALC.h
  86. 79 0
      jme3-ios-native/src/com_jme3_audio_ios_IosEFX.c
  87. 101 0
      jme3-ios-native/src/com_jme3_audio_ios_IosEFX.h
  88. 94 0
      jme3-ios-native/src/com_jme3_util_IosNativeBufferAllocator.c
  89. 29 0
      jme3-ios-native/src/com_jme3_util_IosNativeBufferAllocator.h
  90. 192 0
      jme3-ios-native/src/jme-ios.m
  91. 18 0
      jme3-ios-native/src/jme3_ios_native.h
  92. 9 0
      jme3-ios-native/template/META-INF/robovm/ios/robovm.xml
  93. 11 0
      jme3-ios/src/main/java/com/jme3/system/ios/IGLESContext.java
  94. 14 1
      jme3-ios/src/main/java/com/jme3/system/ios/JmeIosSystem.java
  95. 71 0
      jme3-ios/src/main/java/com/jme3/util/IosNativeBufferAllocator.java
  96. 23 0
      jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java
  97. 9 7
      jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GlbLoader.java
  98. 77 19
      jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java
  99. 308 132
      jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java
  100. 1 0
      jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java

+ 1 - 0
.github/.well-known/funding-manifest-urls

@@ -0,0 +1 @@
+https://jmonkeyengine.org/funding.json

+ 68 - 0
.github/actions/tools/uploadToCentral.sh

@@ -0,0 +1,68 @@
+#! /bin/bash
+set -euo pipefail
+
+## Upload a deployment
+## from the "org.jmonkeyengine" namespace in Sonatype's OSSRH staging area
+## to Sonatype's Central Publisher Portal
+## so the deployment can be tested and then published or dropped.
+
+## IMPORTANT:  The upload request must originate
+## from the IP address used to stage the deployment to the staging area!
+
+# The required -p and -u flags on the command line
+# specify the password and username components of a "user token"
+# generated using the web interface at https://central.sonatype.com/account
+
+while getopts p:u: flag
+do
+    case "${flag}" in
+        p) centralPassword=${OPTARG};;
+        u) centralUsername=${OPTARG};;
+    esac
+done
+
+# Combine both components into a base64 "user token"
+# suitable for the Authorization header of a POST request:
+
+token=$(printf %s:%s "${centralUsername}" "${centralPassword}" | base64)
+
+# Send a POST request to upload the deployment:
+
+server='ossrh-staging-api.central.sonatype.com'
+endpoint='/manual/upload/defaultRepository/org.jmonkeyengine'
+url="https://${server}${endpoint}"
+
+statusCode=$(curl "${url}" \
+  --no-progress-meter \
+  --output postData1.txt \
+  --write-out '%{response_code}' \
+  --request POST \
+  --header 'accept: */*' \
+  --header "Authorization: Bearer ${token}" \
+  --data '')
+
+echo "Status code = ${statusCode}"
+echo 'Received data:'
+cat postData1.txt
+echo '[EOF]'
+
+# Retry if the default repo isn't found (status=400).
+
+if [ "${statusCode}" == "400" ]; then
+  echo "Will retry after 30 seconds."
+  sleep 30
+
+  statusCode2=$(curl "${url}" \
+    --no-progress-meter \
+    --output postData2.txt \
+    --write-out '%{response_code}' \
+    --request POST \
+    --header 'accept: */*' \
+    --header "Authorization: Bearer ${token}" \
+    --data '')
+
+  echo "Status code = ${statusCode2}"
+  echo 'Received data:'
+  cat postData2.txt
+  echo '[EOF]'
+fi

+ 135 - 60
.github/workflows/main.yml

@@ -16,8 +16,8 @@
 # >> Configure MINIO NATIVES SNAPSHOT
 #     OBJECTS_KEY=XXXXXX
 # >> Configure SONATYPE RELEASE
-#     OSSRH_PASSWORD=XXXXXX
-#     OSSRH_USERNAME=XXXXXX
+#     CENTRAL_PASSWORD=XXXXXX
+#     CENTRAL_USERNAME=XXXXXX
 # >> Configure SIGNING
 #     SIGNING_KEY=XXXXXX
 #     SIGNING_PASSWORD=XXXXXX
@@ -51,6 +51,7 @@ on:
       - v3.5
       - v3.4
       - v3.3
+      - ios-2024_2
   pull_request:
   release:
     types: [published]
@@ -59,62 +60,115 @@ jobs:
   ScreenshotTests:
     name: Run Screenshot Tests
     runs-on: ubuntu-latest
+    container:
+      image: ghcr.io/onemillionworlds/opengl-docker-image:v1
     permissions:
       contents: read
     steps:
-    - uses: actions/checkout@v4
-    - name: Set up JDK 17
-      uses: actions/setup-java@v4
-      with:
-        java-version: '17'
-        distribution: 'temurin'
-    - name: Install Mesa3D
-      run: |
-        sudo apt-get update
-        sudo apt-get install -y mesa-utils libgl1-mesa-dri libgl1 libglx-mesa0 xvfb
-    - name: Set environment variables for Mesa3D
-      run: |
-        echo "LIBGL_ALWAYS_SOFTWARE=1" >> $GITHUB_ENV
-        echo "MESA_LOADER_DRIVER_OVERRIDE=llvmpipe" >> $GITHUB_ENV
-    - name: Start xvfb
-      run: |
-        sudo Xvfb :99 -ac -screen 0 1024x768x16 &
-        export DISPLAY=:99
-        echo "DISPLAY=:99" >> $GITHUB_ENV
-    - name: Verify Mesa3D Installation
-      run: |
-        glxinfo | grep "OpenGL"
-    - name: Validate the Gradle wrapper
-      uses: gradle/actions/wrapper-validation@v3
-    - name: Test with Gradle Wrapper
-      run: |
-        ./gradlew :jme3-screenshot-test:screenshotTest
-    - name: Upload Test Reports
-      uses: actions/upload-artifact@master
-      if: always()
-      with:
-        name: screenshot-test-report
-        retention-days: 30
-        path: |
-          **/build/reports/**
-          **/build/changed-images/**
-          **/build/test-results/**
+      - uses: actions/checkout@v4
+      - name: Start xvfb
+        run: |
+          Xvfb :99 -ac -screen 0 1024x768x16 &
+          export DISPLAY=:99
+          echo "DISPLAY=:99" >> $GITHUB_ENV
+      - name: Report GL/Vulkan
+        run: |
+          set -x
+          echo "DISPLAY=$DISPLAY"
+          glxinfo | grep -E "OpenGL version|OpenGL renderer|OpenGL vendor" || true
+          vulkaninfo --summary || true
+          echo "VK_ICD_FILENAMES=$VK_ICD_FILENAMES"
+          echo "MESA_LOADER_DRIVER_OVERRIDE=$MESA_LOADER_DRIVER_OVERRIDE"
+          echo "GALLIUM_DRIVER=$GALLIUM_DRIVER"
+      - name: Validate the Gradle wrapper
+        uses: gradle/actions/wrapper-validation@v3
+      - name: Test with Gradle Wrapper
+        run: |
+          ./gradlew :jme3-screenshot-test:screenshotTest
+      - name: Upload Test Reports
+        uses: actions/upload-artifact@master
+        if: always()
+        with:
+          name: screenshot-test-report
+          retention-days: 30
+          path: |
+            **/build/reports/**
+            **/build/changed-images/**
+            **/build/test-results/**
+
+  # Build iOS natives
+  BuildIosNatives:
+    name: Build natives for iOS
+    runs-on: macOS-14
+
+    steps:
+      - name: Check default JAVAs 
+        run: echo $JAVA_HOME --- $JAVA_HOME_8_X64 --- $JAVA_HOME_11_X64 --- $JAVA_HOME_17_X64 --- $JAVA_HOME_21_X64 ---
+
+      - name: Setup the java environment
+        uses: actions/setup-java@v4
+        with:
+          distribution: 'temurin'
+          java-version: '11.0.26+4'
+
+      - name: Setup the XCode version to 15.1.0 
+        uses: maxim-lobanov/setup-xcode@v1
+        with:
+          xcode-version: '15.1.0'
+
+      - name: Clone the repo
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 1
+
+      - name: Validate the Gradle wrapper
+        uses: gradle/actions/wrapper-validation@v3
+
+      - name: Build
+        run: |
+          ./gradlew -PuseCommitHashAsVersionName=true --no-daemon -PbuildNativeProjects=true \
+          :jme3-ios-native:build
+
+      - name: Upload natives
+        uses: actions/upload-artifact@master
+        with:
+          name: ios-natives
+          path: jme3-ios-native/template/META-INF/robovm/ios/libs/jme3-ios-native.xcframework
+
   # Build the natives on android
   BuildAndroidNatives:
     name: Build natives for android
     runs-on: ubuntu-latest
     container:
-      image: jmonkeyengine/buildenv-jme3:android
+      image: ghcr.io/cirruslabs/android-sdk:36-ndk
 
     steps:
       - name: Clone the repo
         uses: actions/checkout@v4
         with:
           fetch-depth: 1
+
+      - name: Setup Java 11
+        uses: actions/setup-java@v4
+        with:
+          distribution: temurin
+          java-version: '11'
+
+      - name: Check java version
+        run: java -version
+
+      - name: Install CMake
+        run: |
+          apt-get update
+          apt-get install -y cmake
+          cmake --version
+
       - name: Validate the Gradle wrapper
         uses: gradle/actions/wrapper-validation@v3
+
       - name: Build
         run: |
+          export ANDROID_NDK="$ANDROID_SDK_ROOT/ndk/$ANDROID_NDK_VERSION"
           ./gradlew -PuseCommitHashAsVersionName=true --no-daemon -PbuildNativeProjects=true \
           :jme3-android-native:assemble
 
@@ -126,7 +180,7 @@ jobs:
 
   # Build the engine, we only deploy from ubuntu-latest jdk21
   BuildJMonkey:
-    needs: [BuildAndroidNatives]
+    needs: [BuildAndroidNatives, BuildIosNatives]
     name: Build on ${{ matrix.osName }} jdk${{ matrix.jdk }}
     runs-on: ${{ matrix.os }}
     strategy:
@@ -167,6 +221,12 @@ jobs:
           name: android-natives
           path: build/native
 
+      - name: Download natives for iOS
+        uses: actions/download-artifact@master
+        with:
+          name: ios-natives
+          path: jme3-ios-native/template/META-INF/robovm/ios/libs/jme3-ios-native.xcframework
+
       - name: Validate the Gradle wrapper
         uses: gradle/actions/wrapper-validation@v3
       - name: Build Engine
@@ -359,16 +419,22 @@ jobs:
           name: android-natives
           path: build/native
 
-      - name: Rebuild the maven artifacts and deploy them to the Sonatype repository
+      - name: Download natives for iOS
+        uses: actions/download-artifact@master
+        with:
+          name: ios-natives
+          path: jme3-ios-native/template/META-INF/robovm/ios/libs/jme3-ios-native.xcframework
+
+      - name: Rebuild the maven artifacts and upload them to Sonatype's maven-snapshots repo
         run: |
-          if [ "${{ secrets.OSSRH_PASSWORD }}" = "" ];
+          if [ "${{ secrets.CENTRAL_PASSWORD }}" = "" ];
           then
-            echo "Configure the following secrets to enable deployment to Sonatype:"
-            echo "OSSRH_PASSWORD, OSSRH_USERNAME, SIGNING_KEY, SIGNING_PASSWORD"
+            echo "Configure the following secrets to enable uploading to Sonatype:"
+            echo "CENTRAL_PASSWORD, CENTRAL_USERNAME, SIGNING_KEY, SIGNING_PASSWORD"
           else
             ./gradlew publishMavenPublicationToSNAPSHOTRepository \
-            -PossrhPassword=${{ secrets.OSSRH_PASSWORD }} \
-            -PossrhUsername=${{ secrets.OSSRH_USERNAME }} \
+            -PcentralPassword=${{ secrets.CENTRAL_PASSWORD }} \
+            -PcentralUsername=${{ secrets.CENTRAL_USERNAME }} \
             -PsigningKey='${{ secrets.SIGNING_KEY }}' \
             -PsigningPassword='${{ secrets.SIGNING_PASSWORD }}' \
             -PuseCommitHashAsVersionName=true \
@@ -384,13 +450,13 @@ jobs:
     if: github.event_name == 'release'
     steps:
 
-      # We need to clone everything again for uploadToMaven.sh ...
+      # We need to clone everything again for uploadToCentral.sh ...
       - name: Clone the repo
         uses: actions/checkout@v4
         with:
           fetch-depth: 1
 
-      # Setup jdk 21 used for building Sonatype OSSRH artifacts
+      # Setup jdk 21 used for building Sonatype artifacts
       - name: Setup the java environment
         uses: actions/setup-java@v4
         with:
@@ -416,20 +482,29 @@ jobs:
           name: android-natives
           path: build/native
 
-      - name: Rebuild the maven artifacts and deploy them to Sonatype OSSRH
+      - name: Download natives for iOS
+        uses: actions/download-artifact@master
+        with:
+          name: ios-natives
+          path: jme3-ios-native/template/META-INF/robovm/ios/libs/jme3-ios-native.xcframework
+
+      - name: Rebuild the maven artifacts and upload them to Sonatype's Central Publisher Portal
         run: |
-          if [ "${{ secrets.OSSRH_PASSWORD }}" = "" ];
+          if [ "${{ secrets.CENTRAL_PASSWORD }}" = "" ];
           then
-            echo "Configure the following secrets to enable deployment to Sonatype:"
-            echo "OSSRH_PASSWORD, OSSRH_USERNAME, SIGNING_KEY, SIGNING_PASSWORD"
+            echo "Configure the following secrets to enable uploading to Sonatype:"
+            echo "CENTRAL_PASSWORD, CENTRAL_USERNAME, SIGNING_KEY, SIGNING_PASSWORD"
           else
-            ./gradlew publishMavenPublicationToOSSRHRepository \
-            -PossrhPassword=${{ secrets.OSSRH_PASSWORD }} \
-            -PossrhUsername=${{ secrets.OSSRH_USERNAME }} \
-            -PsigningKey='${{ secrets.SIGNING_KEY }}' \
-            -PsigningPassword='${{ secrets.SIGNING_PASSWORD }}' \
-            -PuseCommitHashAsVersionName=true \
-            --console=plain --stacktrace
+            ./gradlew publishMavenPublicationToCentralRepository \
+              -PcentralPassword=${{ secrets.CENTRAL_PASSWORD }} \
+              -PcentralUsername=${{ secrets.CENTRAL_USERNAME }} \
+              -PsigningKey='${{ secrets.SIGNING_KEY }}' \
+              -PsigningPassword='${{ secrets.SIGNING_PASSWORD }}' \
+              -PuseCommitHashAsVersionName=true \
+              --console=plain --stacktrace
+            .github/actions/tools/uploadToCentral.sh \
+              -p '${{ secrets.CENTRAL_PASSWORD }}' \
+              -u '${{ secrets.CENTRAL_USERNAME }}'
           fi
 
       - name: Deploy to GitHub Releases

+ 3 - 1
.github/workflows/screenshot-test-comment.yml

@@ -21,7 +21,7 @@ jobs:
       contents: read
     steps:
       - name: Wait for GitHub to register the workflow run
-        run: sleep 15
+        run: sleep 120
 
       - name: Wait for Screenshot Tests to complete
         uses: lewagon/[email protected]
@@ -113,5 +113,7 @@ jobs:
             **Note;**  it is very important that the committed reference images are created on the build pipeline, locally created images are not reliable. Similarly tests will fail locally but you can look at the report to check they are "visually similar".
 
             See https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-screenshot-tests/README.md for more information
+            
+            Contact @richardTingle (aka richtea) for guidance if required
           edit-mode: replace
           comment-id: ${{ steps.existingCommentId.outputs.comment-id }}

+ 1 - 1
README.md

@@ -4,7 +4,7 @@ jMonkeyEngine
 [![Build Status](https://github.com/jMonkeyEngine/jmonkeyengine/workflows/Build%20jMonkeyEngine/badge.svg)](https://github.com/jMonkeyEngine/jmonkeyengine/actions)
 
 jMonkeyEngine is a 3-D game engine for adventurous Java developers. It’s open-source, cross-platform, and cutting-edge.
-v3.7.0 is the latest stable version of the engine.
+v3.8.0 is the latest stable version of the engine.
 
 The engine is used by several commercial game studios and computer-science courses. Here's a taste:
 

+ 20 - 12
common.gradle

@@ -157,27 +157,35 @@ publishing {
             version project.version
         }
     }
+
     repositories {
         maven {
             name = 'Dist'
             url = gradle.rootProject.projectDir.absolutePath + '/dist/maven'
         }
+
+        // Uploading to Sonatype relies on the existence of 2 properties
+        // (centralUsername and centralPassword)
+        // which should be set using -P options on the command line.
+
+        maven {
+            // for uploading release builds to the default repo in Sonatype's OSSRH staging area
+            credentials {
+                username = gradle.rootProject.hasProperty('centralUsername') ? centralUsername : 'Unknown user'
+                password = gradle.rootProject.hasProperty('centralPassword') ? centralPassword : 'Unknown password'
+            }
+            name = 'Central'
+            url = 'https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/'
+        }
         maven {
+            // for uploading snapshot builds to Sonatype's maven-snapshots repo
             credentials {
-                username = gradle.rootProject.hasProperty('ossrhUsername') ? ossrhUsername : 'Unknown user'
-                password = gradle.rootProject.hasProperty('ossrhPassword') ? ossrhPassword : 'Unknown password'
+                username = gradle.rootProject.hasProperty('centralUsername') ? centralUsername : 'Unknown user'
+                password = gradle.rootProject.hasProperty('centralPassword') ? centralPassword : 'Unknown password'
             }
-            name = 'OSSRH'
-            url = 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2'
+            name = 'SNAPSHOT'
+            url = 'https://central.sonatype.com/repository/maven-snapshots/'
         }
-	maven {
-	    credentials {
-                username = gradle.rootProject.hasProperty('ossrhUsername') ? ossrhUsername : 'Unknown user'
-                password = gradle.rootProject.hasProperty('ossrhPassword') ? ossrhPassword : 'Unknown password'
-	    }
-	    name = 'SNAPSHOT'
-	    url = 'https://s01.oss.sonatype.org/content/repositories/snapshots/'
-	}
     }
 }
 

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

@@ -1,12 +1,12 @@
 // OpenAL Soft r1.21.1
 // TODO: update URL to jMonkeyEngine fork once it's updated with latest kcat's changes
-String openALSoftUrl = 'https://github.com/kcat/openal-soft/archive/1.21.1.zip'
+String openALSoftUrl = 'https://github.com/kcat/openal-soft/archive/1.24.3.zip'
 String openALSoftZipFile = 'OpenALSoft.zip'
 
 // OpenAL Soft directory the download is extracted into
 // Typically, the downloaded OpenAL Soft zip file will extract to a directory
 // called "openal-soft"
-String openALSoftFolder = 'openal-soft-1.21.1'
+String openALSoftFolder = 'openal-soft-1.24.3'
 
 //Working directories for the ndk build.
 String openalsoftBuildDir = "${buildDir}" + File.separator + 'openalsoft'
@@ -81,13 +81,103 @@ task copyJmeOpenALSoft(type: Copy, dependsOn: [copyOpenALSoft, copyJmeHeadersOpe
     from sourceDir
     into outputDir
 }
+// rootProject.ndkCommandPath must be set to your ndk-build wrapper or full ndk path
+def ndkPath = new File(rootProject.ndkCommandPath).getParent()
+def cmakeToolchain = "${ndkPath}/build/cmake/android.toolchain.cmake"
+
+// 1) list your ABIs here
+def openalAbis = [
+    "armeabi-v7a",
+    "arm64-v8a",
+    "x86",
+    "x86_64"
+]
+
+// 2) for each ABI, register a configure/build pair
+openalAbis.each { abi ->
+
+    // configure task
+    tasks.register("configureOpenAlSoft_${abi}", Exec) {
+        group = "external-native"
+        description = "Generate CMake build files for OpenAL-Soft [$abi]"
+
+        workingDir file("$openalsoftBuildDir/$openALSoftFolder")
+        commandLine = [
+            "cmake",
+            "-S", ".",
+            "-B", "cmake-build-${abi}",
+            "-G", "Unix Makefiles",                         // or Ninja
+            "-DCMAKE_TOOLCHAIN_FILE=${cmakeToolchain}",
+            "-DANDROID_PLATFORM=android-21",
+            "-DANDROID_ABI=${abi}",
+            "-DCMAKE_BUILD_TYPE=Release",
+            "-DALSOFT_UTILS=OFF",
+            "-DALSOFT_EXAMPLES=OFF",
+            "-DALSOFT_TESTS=OFF",
+            "-DALSOFT_BACKEND_OPENSL=ON",
+            '-DALSOFT_SHARED=OFF',
+            '-DBUILD_SHARED_LIBS=OFF',
+            '-DALSOFT_STATIC=ON',
+            '-DLIBTYPE=STATIC',
+            '-DCMAKE_CXX_FLAGS=-stdlib=libc++'
+        ]
+
+        dependsOn copyOpenALSoft
+    }
+
+    // build task
+    tasks.register("buildOpenAlSoft_${abi}", Exec) {
+        group = "external-native"
+        description = "Compile OpenAL-Soft into libopenalsoft.a for [$abi]"
+
+        dependsOn "configureOpenAlSoft_${abi}"
+        workingDir file("$openalsoftBuildDir/$openALSoftFolder")
+        commandLine = [
+            "cmake",
+            "--build", "cmake-build-${abi}",
+            "--config", "Release"
+        ]
+    }
+}
+
+// 3) optional: aggregate tasks
+tasks.register("configureOpenAlSoftAll") {
+    group = "external-native"
+    description = "Configure OpenAL-Soft for all ABIs"
+    dependsOn openalAbis.collect { "configureOpenAlSoft_${it}" }
+}
+
+tasks.register("buildOpenAlSoftAll") {
+    group = "external-native"
+    description = "Build OpenAL-Soft for all ABIs"
+    dependsOn openalAbis.collect { "buildOpenAlSoft_${it}" }
+}
 
-task buildOpenAlSoftNativeLib(type: Exec, dependsOn: copyJmeOpenALSoft) {
-//    println "openalsoft build dir: " + openalsoftBuildDir
-//    println "ndkCommandPath: " + project.ndkCommandPath
+task buildOpenAlSoftNativeLib(type: Exec) {
+    group = "external-native"
+    description = "Runs ndk-build on your JNI code, linking in the prebuilt OpenAL-Soft .a files"
+
+    dependsOn copyJmeOpenALSoft, buildOpenAlSoftAll
+
+    // where your Android.mk lives
     workingDir openalsoftBuildDir
+
+    // call the NDK build script
     executable rootProject.ndkCommandPath
-    args "-j" + Runtime.runtime.availableProcessors()
+
+    // pass in all ABIs (so ndk-build will rebuild your shared .so for each one),
+    // and pass in a custom var OPENALSOFT_BUILD_DIR so your Android.mk can find 
+    // the cmake-build-<ABI> folders.
+    args(
+        // let ndk-build know which ABIs to build for
+        "APP_ABI=armeabi-v7a,arm64-v8a,x86,x86_64",
+
+        // pass in the path to the CMake output root
+        "OPENALSOFT_BUILD_ROOT=${openalsoftBuildDir}/${openALSoftFolder}",
+
+        // parallel jobs
+        "-j${Runtime.runtime.availableProcessors()}"
+    )
 }
 
 task updatePreCompiledOpenAlSoftLibs(type: Copy, dependsOn: buildOpenAlSoftNativeLib) {
@@ -140,3 +230,4 @@ class MyDownload extends DefaultTask {
        ant.get(src: sourceUrl, dest: target)
     }
 }
+

+ 3 - 1
jme3-android-native/src/native/jme_bufferallocator/Application.mk

@@ -36,4 +36,6 @@
 APP_PLATFORM := android-19
 # change this to 'debug' to see android logs
 APP_OPTIM := release
-APP_ABI := all
+APP_ABI := armeabi-v7a,arm64-v8a,x86,x86_64
+APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true
+

+ 3 - 1
jme3-android-native/src/native/jme_decode/Application.mk

@@ -1,3 +1,5 @@
 APP_PLATFORM := android-9
 APP_OPTIM := release
-APP_ABI := all
+APP_ABI := armeabi-v7a,arm64-v8a,x86,x86_64
+APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true
+

+ 1 - 0
jme3-android-native/src/native/jme_decode/com_jme3_audio_plugins_NativeVorbisFile.c

@@ -1,6 +1,7 @@
 #include <unistd.h>
 #include <stdlib.h>
 #include <errno.h>
+#include <string.h>
 
 #include "Tremor/ivorbisfile.h"
 

+ 45 - 94
jme3-android-native/src/native/jme_openalsoft/Android.mk

@@ -1,103 +1,54 @@
-TARGET_PLATFORM := android-19
+# jni/Android.mk
 
 LOCAL_PATH := $(call my-dir)
 
-include $(CLEAR_VARS)
-
-LOCAL_MODULE     := openalsoftjme
-
-LOCAL_C_INCLUDES += $(LOCAL_PATH) $(LOCAL_PATH)/include \
-		    $(LOCAL_PATH)/alc  $(LOCAL_PATH)/common
+# require the path to cmake-build-<ABI>
+ifndef OPENALSOFT_BUILD_ROOT
+$(error OPENALSOFT_BUILD_ROOT not set! pass it via ndk-build OPENALSOFT_BUILD_ROOT=/path/to/cmake-build-root)
+endif
 
-LOCAL_CPP_FEATURES += exceptions
+# assemble the path to this ABI's .a
+OPENAL_PREBUILT_DIR := $(OPENALSOFT_BUILD_ROOT)/cmake-build-$(TARGET_ARCH_ABI)
 
-LOCAL_CFLAGS     := -ffast-math -DAL_BUILD_LIBRARY -DAL_ALEXT_PROTOTYPES -fcommon -O0 -DRESTRICT=""
-LOCAL_LDLIBS     := -lOpenSLES -llog -Wl,-s
+# -----------------------------------------------------------------------------
+# 1) prebuilt static library
+include $(CLEAR_VARS)
+LOCAL_MODULE := openalsoft_prebuilt
+LOCAL_SRC_FILES := $(OPENAL_PREBUILT_DIR)/libopenal.a
+LOCAL_EXPORT_C_INCLUDES := $(OPENALSOFT_BUILD_ROOT)/include
+include $(PREBUILT_STATIC_LIBRARY)
 
-LOCAL_SRC_FILES  :=   al/auxeffectslot.cpp \
-                      al/buffer.cpp \
-                      al/effect.cpp \
-                      al/effects/autowah.cpp \
-                      al/effects/chorus.cpp \
-                      al/effects/compressor.cpp \
-                      al/effects/convolution.cpp \
-                      al/effects/dedicated.cpp \
-                      al/effects/distortion.cpp \
-                      al/effects/echo.cpp \
-                      al/effects/equalizer.cpp \
-                      al/effects/fshifter.cpp \
-                      al/effects/modulator.cpp \
-                      al/effects/null.cpp \
-                      al/effects/pshifter.cpp \
-                      al/effects/reverb.cpp \
-                      al/effects/vmorpher.cpp \
-                      al/error.cpp \
-                      al/event.cpp \
-                      al/extension.cpp \
-                      al/filter.cpp \
-                      al/listener.cpp \
-                      al/source.cpp \
-                      al/state.cpp \
-                      alc/alc.cpp \
-                      alc/alconfig.cpp \
-                      alc/alu.cpp \
-                      alc/backends/base.cpp \
-                      alc/backends/loopback.cpp \
-                      alc/backends/null.cpp \
-                      alc/backends/opensl.cpp \
-                      alc/backends/wave.cpp \
-                      alc/bformatdec.cpp \
-                      alc/buffer_storage.cpp \
-                      alc/converter.cpp \
-                      alc/effects/autowah.cpp \
-                      alc/effects/chorus.cpp \
-                      alc/effects/compressor.cpp \
-                      alc/effects/convolution.cpp \
-                      alc/effects/dedicated.cpp \
-                      alc/effects/distortion.cpp \
-                      alc/effects/echo.cpp \
-                      alc/effects/equalizer.cpp \
-                      alc/effects/fshifter.cpp \
-                      alc/effects/modulator.cpp \
-                      alc/effects/null.cpp \
-                      alc/effects/pshifter.cpp \
-                      alc/effects/reverb.cpp \
-                      alc/effects/vmorpher.cpp \
-                      alc/effectslot.cpp \
-                      alc/helpers.cpp \
-                      alc/hrtf.cpp \
-                      alc/panning.cpp \
-                      alc/uiddefs.cpp \
-                      alc/voice.cpp \
-                      common/alcomplex.cpp \
-                      common/alfstream.cpp \
-                      common/almalloc.cpp \
-                      common/alstring.cpp \
-                      common/dynload.cpp \
-                      common/polyphase_resampler.cpp \
-                      common/ringbuffer.cpp \
-                      common/strutils.cpp \
-                      common/threads.cpp \
-                      core/ambdec.cpp \
-                      core/bs2b.cpp \
-                      core/bsinc_tables.cpp \
-                      core/cpu_caps.cpp \
-                      core/devformat.cpp \
-                      core/except.cpp \
-                      core/filters/biquad.cpp \
-                      core/filters/nfc.cpp \
-                      core/filters/splitter.cpp \
-                      core/fmt_traits.cpp \
-                      core/fpu_ctrl.cpp \
-                      core/logging.cpp \
-                      core/mastering.cpp \
-                      core/mixer/mixer_c.cpp \
-                      core/uhjfilter.cpp \
-                      com_jme3_audio_android_AndroidAL.c \
-                      com_jme3_audio_android_AndroidALC.c \
-                      com_jme3_audio_android_AndroidEFX.c
+# -----------------------------------------------------------------------------
+# 2) your JNI wrapper
+include $(CLEAR_VARS)
+LOCAL_MODULE    := openalsoftjme
+LOCAL_SRC_FILES := \
+    com_jme3_audio_android_AndroidAL.c \
+    com_jme3_audio_android_AndroidALC.c \
+    com_jme3_audio_android_AndroidEFX.c
+
+LOCAL_C_INCLUDES  += \
+    $(LOCAL_PATH) \
+    $(LOCAL_PATH)/include \
+    $(LOCAL_PATH)/alc \
+    $(LOCAL_PATH)/common
+
+LOCAL_CPP_FEATURES          := exceptions rtti
+LOCAL_CFLAGS                := -ffast-math \
+                               -DAL_ALEXT_PROTOTYPES \
+                               -fcommon \
+                               -O0 \
+                               -DRESTRICT=""
+
+LOCAL_LDLIBS                := -lOpenSLES -llog -Wl,-s -lc++_static -lc++abi
+ifeq ($(TARGET_ARCH_ABI),arm64-v8a)
+    LOCAL_LDFLAGS               += "-Wl,-z,max-page-size=16384"
+endif
+ifeq ($(TARGET_ARCH_ABI),x86_64)
+    LOCAL_LDFLAGS               += "-Wl,-z,max-page-size=16384"
+endif
+LOCAL_STATIC_LIBRARIES      := openalsoft_prebuilt
+# (or LOCAL_WHOLE_STATIC_LIBRARIES if you need every object pulled in)
 
 include $(BUILD_SHARED_LIBRARY)
 
-#                      Alc/mixer/hrtf_inc.c \
-

+ 2 - 1
jme3-android-native/src/native/jme_openalsoft/Application.mk

@@ -1,5 +1,6 @@
 APP_PLATFORM := android-19
 APP_OPTIM := release
-APP_ABI := all
+APP_ABI := armeabi-v7a,arm64-v8a,x86,x86_64
 APP_STL := c++_static
+APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true
 

+ 148 - 83
jme3-awt-dialogs/src/main/java/com/jme3/awt/AWTSettingsDialog.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2022 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -57,12 +57,11 @@ import java.util.prefs.BackingStoreException;
 import javax.swing.*;
 
 /**
- * <code>SettingsDialog</code> displays a Swing dialog box to interactively
- * configure the <code>AppSettings</code> of a desktop application before
- * <code>start()</code> is invoked.
- *
- * The <code>AppSettings</code> instance to be configured is passed to the
- * constructor.
+ * `AWTSettingsDialog` displays a Swing dialog box to interactively
+ * configure the `AppSettings` of a desktop application before
+ * `start()` is invoked.
+ * <p>
+ * The `AppSettings` instance to be configured is passed to the constructor.
  *
  * @see AppSettings
  * @author Mark Powell
@@ -71,14 +70,33 @@ import javax.swing.*;
  */
 public final class AWTSettingsDialog extends JFrame {
 
-    public static interface SelectionListener {
-
-        public void onSelection(int selection);
+    /**
+     * Listener interface for handling selection events from the settings dialog.
+     */
+    public interface SelectionListener {
+        /**
+         * Called when a selection is made in the settings dialog (OK or Cancel).
+         *
+         * @param selection The type of selection made: `NO_SELECTION`, `APPROVE_SELECTION`, or `CANCEL_SELECTION`.
+         */
+        void onSelection(int selection);
     }
 
     private static final Logger logger = Logger.getLogger(AWTSettingsDialog.class.getName());
     private static final long serialVersionUID = 1L;
-    public static final int NO_SELECTION = 0, APPROVE_SELECTION = 1, CANCEL_SELECTION = 2;
+
+    /**
+     * Indicates that no selection has been made yet.
+     */
+    public static final int NO_SELECTION = 0;
+    /**
+     * Indicates that the user approved the settings.
+     */
+    public static final int APPROVE_SELECTION = 1;
+    /**
+     * Indicates that the user canceled the settings dialog.
+     */
+    public static final int CANCEL_SELECTION = 2;
 
     // Resource bundle for i18n.
     ResourceBundle resourceBundle = ResourceBundle.getBundle("com.jme3.app/SettingsDialog");
@@ -86,8 +104,12 @@ public final class AWTSettingsDialog extends JFrame {
     // the instance being configured
     private final AppSettings source;
 
-    // Title Image
+    /**
+     * The URL of the image file to be displayed as a title icon in the dialog.
+     * Can be `null` if no image is desired.
+     */
     private URL imageFile = null;
+
     // Array of supported display modes
     private DisplayMode[] modes = null;
     private static final DisplayMode[] windowDefaults = new DisplayMode[] {
@@ -114,10 +136,24 @@ public final class AWTSettingsDialog extends JFrame {
     private int minWidth = 0;
     private int minHeight = 0;
 
+    /**
+     * Displays a settings dialog using the provided `AppSettings` source.
+     * Settings will be loaded from preferences.
+     *
+     * @param sourceSettings The `AppSettings` instance to configure.
+     * @return `true` if the user approved the settings, `false` otherwise.
+     */
     public static boolean showDialog(AppSettings sourceSettings) {
         return showDialog(sourceSettings, true);
     }
 
+    /**
+     * Displays a settings dialog using the provided `AppSettings` source.
+     *
+     * @param sourceSettings The `AppSettings` instance to configure.
+     * @param loadSettings   If `true`, settings will be loaded from preferences; otherwise, they will be merged.
+     * @return `true` if the user approved the settings, `false` otherwise.
+     */
     public static boolean showDialog(AppSettings sourceSettings, boolean loadSettings) {
         String iconPath = sourceSettings.getSettingsDialogImage();
         final URL iconUrl = JmeSystem.class.getResource(iconPath.startsWith("/") ? iconPath : "/" + iconPath);
@@ -127,10 +163,30 @@ public final class AWTSettingsDialog extends JFrame {
         return showDialog(sourceSettings, iconUrl, loadSettings);
     }
 
+    /**
+     * Displays a settings dialog using the provided `AppSettings` source and an image file path.
+     *
+     * @param sourceSettings The `AppSettings` instance to configure.
+     * @param imageFile      The path to the image file to use as the title of the dialog;
+     *                       `null` will result in no image being displayed.
+     * @param loadSettings   If `true`, settings will be loaded from preferences; otherwise, they will be merged.
+     * @return `true` if the user approved the settings, `false` otherwise.
+     */
     public static boolean showDialog(AppSettings sourceSettings, String imageFile, boolean loadSettings) {
         return showDialog(sourceSettings, getURL(imageFile), loadSettings);
     }
 
+    /**
+     * Displays a settings dialog using the provided `AppSettings` source and an image URL.
+     * This method blocks until the dialog is closed.
+     *
+     * @param sourceSettings The `AppSettings` instance to configure (not null).
+     * @param imageFile      The `URL` pointing to the image file to use as the title of the dialog;
+     *                       `null` will result in no image being displayed.
+     * @param loadSettings   If `true`, the dialog will copy settings from preferences. If `false`
+     *                       and preferences exist, they will be merged with the current settings.
+     * @return `true` if the user approved the settings, `false` otherwise (`CANCEL_SELECTION` or dialog close).
+     */
     public static boolean showDialog(AppSettings sourceSettings, URL imageFile, boolean loadSettings) {
         if (SwingUtilities.isEventDispatchThread()) {
             throw new IllegalStateException("Cannot run from EDT");
@@ -166,46 +222,47 @@ public final class AWTSettingsDialog extends JFrame {
         synchronized (lock) {
             while (!done.get()) {
                 try {
+                    // Wait until notified by the selection listener
                     lock.wait();
                 } catch (InterruptedException ex) {
+                    Thread.currentThread().interrupt();
+                    logger.log(Level.WARNING, "Settings dialog thread interrupted while waiting.", ex);
+                    return false; // Treat as cancel if interrupted
                 }
             }
         }
 
-        sourceSettings.copyFrom(settings);
+        // If approved, copy the modified settings back to the original source
+        if (result.get() == APPROVE_SELECTION) {
+            sourceSettings.copyFrom(settings);
+        }
 
-        return result.get() == AWTSettingsDialog.APPROVE_SELECTION;
+        return result.get() == APPROVE_SELECTION;
     }
 
     /**
-     * Instantiate a <code>SettingsDialog</code> for the primary display.
+     * Constructs a `SettingsDialog` for the primary display.
      *
-     * @param source
-     *            the <code>AppSettings</code> (not null)
-     * @param imageFile
-     *            the image file to use as the title of the dialog;
-     *            <code>null</code> will result in to image being displayed
-     * @param loadSettings
-     *            if true, copy the settings, otherwise merge them
-     * @throws IllegalArgumentException
-     *             if the source is <code>null</code>
+     * @param source       The `AppSettings` instance to configure (not null).
+     * @param imageFile    The path to the image file to use as the title of the dialog;
+     *                     `null` will result in no image being displayed.
+     * @param loadSettings If `true`, the dialog will copy settings from preferences. If `false`
+     *                     and preferences exist, they will be merged with the current settings.
+     * @throws IllegalArgumentException if `source` is `null`.
      */
     protected AWTSettingsDialog(AppSettings source, String imageFile, boolean loadSettings) {
         this(source, getURL(imageFile), loadSettings);
     }
 
     /**
-     * /** Instantiate a <code>SettingsDialog</code> for the primary display.
+     * Constructs a `SettingsDialog` for the primary display.
      *
-     * @param source
-     *            the <code>AppSettings</code> object (not null)
-     * @param imageFile
-     *            the image file to use as the title of the dialog;
-     *            <code>null</code> will result in to image being displayed
-     * @param loadSettings
-     *            if true, copy the settings, otherwise merge them
-     * @throws IllegalArgumentException
-     *             if the source is <code>null</code>
+     * @param source       The `AppSettings` instance to configure (not null).
+     * @param imageFile    The `URL` pointing to the image file to use as the title of the dialog;
+     *                     `null` will result in no image being displayed.
+     * @param loadSettings If `true`, the dialog will copy settings from preferences. If `false`
+     *                     and preferences exist, they will be merged with the current settings.
+     * @throws IllegalArgumentException if `source` is `null`.
      */
     protected AWTSettingsDialog(AppSettings source, URL imageFile, boolean loadSettings) {
         if (source == null) {
@@ -232,7 +289,10 @@ public final class AWTSettingsDialog extends JFrame {
         minHeight = source.getMinHeight();
 
         try {
+            logger.log(Level.INFO, "Loading AppSettings from PreferenceKey: {0}", appTitle);
             registrySettings.load(appTitle);
+            AppSettings.printPreferences(appTitle);
+
         } catch (BackingStoreException ex) {
             logger.log(Level.WARNING, "Failed to load settings", ex);
         }
@@ -355,8 +415,6 @@ public final class AWTSettingsDialog extends JFrame {
      * <code>init</code> creates the components to use the dialog.
      */
     private void createUI() {
-        GridBagConstraints gbc;
-
         JPanel mainPanel = new JPanel(new GridBagLayout());
 
         addWindowListener(new WindowAdapter() {
@@ -368,8 +426,9 @@ public final class AWTSettingsDialog extends JFrame {
             }
         });
 
-        if (source.getIcons() != null) {
-            safeSetIconImages(Arrays.asList((BufferedImage[]) source.getIcons()));
+        Object[] sourceIcons = source.getIcons();
+        if (sourceIcons != null && sourceIcons.length > 0) {
+            safeSetIconImages(Arrays.asList((BufferedImage[]) sourceIcons));
         }
 
         setTitle(MessageFormat.format(resourceBundle.getString("frame.title"), source.getTitle()));
@@ -419,7 +478,7 @@ public final class AWTSettingsDialog extends JFrame {
         gammaBox = new JCheckBox(resourceBundle.getString("checkbox.gamma"));
         gammaBox.setSelected(source.isGammaCorrection());
 
-        gbc = new GridBagConstraints();
+        GridBagConstraints gbc = new GridBagConstraints();
         gbc.weightx = 0.5;
         gbc.gridx = 0;
         gbc.gridwidth = 2;
@@ -493,7 +552,6 @@ public final class AWTSettingsDialog extends JFrame {
         // Set the button action listeners. Cancel disposes without saving, OK
         // saves.
         ok.addActionListener(new ActionListener() {
-
             @Override
             public void actionPerformed(ActionEvent e) {
                 if (verifyAndSaveCurrentSelection()) {
@@ -501,12 +559,13 @@ public final class AWTSettingsDialog extends JFrame {
                     dispose();
 
                     // System.gc() should be called to prevent "X Error of
-                    // failed request: RenderBadPicture (invalid Picture
-                    // parameter)"
+                    // failed request: RenderBadPicture (invalid Picture parameter)"
                     // on Linux when using AWT/Swing + GLFW.
                     // For more info see:
                     // https://github.com/LWJGL/lwjgl3/issues/149,
-                    // https://hub.jmonkeyengine.org/t/experimenting-lwjgl3/37275
+
+                    //  intentional double call. see this discussion:
+                    //  https://hub.jmonkeyengine.org/t/experimenting-lwjgl3/37275/12
                     System.gc();
                     System.gc();
                 }
@@ -514,7 +573,6 @@ public final class AWTSettingsDialog extends JFrame {
         });
 
         cancel.addActionListener(new ActionListener() {
-
             @Override
             public void actionPerformed(ActionEvent e) {
                 setUserSelection(CANCEL_SELECTION);
@@ -568,7 +626,6 @@ public final class AWTSettingsDialog extends JFrame {
                 colorDepthCombo.setSelectedItem(source.getBitsPerPixel() + " bpp");
             }
         });
-
     }
 
     /*
@@ -577,10 +634,8 @@ public final class AWTSettingsDialog extends JFrame {
      */
     private void safeSetIconImages(List<? extends Image> icons) {
         try {
-            // Due to Java bug 6445278, we try to set icon on our shared owner
-            // frame first.
-            // Otherwise, our alt-tab icon will be the Java default under
-            // Windows.
+            // Due to Java bug 6445278, we try to set icon on our shared owner frame first.
+            // Otherwise, our alt-tab icon will be the Java default under Windows.
             Window owner = getOwner();
             if (owner != null) {
                 Method setIconImages = owner.getClass().getMethod("setIconImages", List.class);
@@ -608,9 +663,9 @@ public final class AWTSettingsDialog extends JFrame {
         boolean vsync = vsyncBox.isSelected();
         boolean gamma = gammaBox.isSelected();
 
-        int width = Integer.parseInt(display.substring(0, display.indexOf(" x ")));
-        display = display.substring(display.indexOf(" x ") + 3);
-        int height = Integer.parseInt(display);
+        String[] parts = display.split(" x ");
+        int width = Integer.parseInt(parts[0]);
+        int height = Integer.parseInt(parts[1]);
 
         String depthString = (String) colorDepthCombo.getSelectedItem();
         int depth = -1;
@@ -639,21 +694,20 @@ public final class AWTSettingsDialog extends JFrame {
         }
 
         // FIXME: Does not work in Linux
-        /*
-         * if (!fullscreen) { //query the current bit depth of the desktop int
-         * curDepth = GraphicsEnvironment.getLocalGraphicsEnvironment()
-         * .getDefaultScreenDevice().getDisplayMode().getBitDepth(); if (depth >
-         * curDepth) { showError(this,"Cannot choose a higher bit depth in
-         * windowed " + "mode than your current desktop bit depth"); return
-         * false; } }
-         */
-
-        boolean valid = false;
+//        if (!fullscreen) { //query the current bit depth of the desktop int
+//            curDepth = GraphicsEnvironment.getLocalGraphicsEnvironment()
+//                    .getDefaultScreenDevice().getDisplayMode().getBitDepth();
+//            if (depth > curDepth) {
+//                showError(this, "Cannot choose a higher bit depth in
+//                        windowed" + "mode than your current desktop bit depth");
+//                return false;
+//            }
+//        }
+
+        boolean valid = true;
 
         // test valid display mode when going full screen
-        if (!fullscreen) {
-            valid = true;
-        } else {
+        if (fullscreen) {
             GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
             valid = device.isFullScreenSupported();
         }
@@ -673,7 +727,10 @@ public final class AWTSettingsDialog extends JFrame {
             String appTitle = source.getTitle();
 
             try {
+                logger.log(Level.INFO, "Saving AppSettings to PreferencesKey: {0}", appTitle);
                 source.save(appTitle);
+                AppSettings.printPreferences(appTitle);
+
             } catch (BackingStoreException ex) {
                 logger.log(Level.WARNING, "Failed to save setting changes", ex);
             }
@@ -769,7 +826,9 @@ public final class AWTSettingsDialog extends JFrame {
     private void updateAntialiasChoices() {
         // maybe in the future will add support for determining this info
         // through PBuffer
-        String[] choices = new String[] { resourceBundle.getString("antialias.disabled"), "2x", "4x", "6x", "8x", "16x" };
+        String[] choices = new String[] {
+                resourceBundle.getString("antialias.disabled"), "2x", "4x", "6x", "8x", "16x"
+        };
         antialiasCombo.setModel(new DefaultComboBoxModel<>(choices));
         antialiasCombo.setSelectedItem(choices[Math.min(source.getSamples() / 2, 5)]);
     }
@@ -792,6 +851,12 @@ public final class AWTSettingsDialog extends JFrame {
         return url;
     }
 
+    /**
+     * Displays an error message dialog to the user.
+     *
+     * @param parent  The parent `Component` for the dialog.
+     * @param message The message `String` to display.
+     */
     private static void showError(java.awt.Component parent, String message) {
         JOptionPane.showMessageDialog(parent, message, "Error", JOptionPane.ERROR_MESSAGE);
     }
@@ -852,7 +917,7 @@ public final class AWTSettingsDialog extends JFrame {
      * Returns every possible bit depth for the given resolution.
      */
     private static String[] getDepths(String resolution, DisplayMode[] modes) {
-        List<String> depths = new ArrayList<>(4);
+        Set<String> depths = new LinkedHashSet<>(4); // Use LinkedHashSet for uniqueness and order
         for (DisplayMode mode : modes) {
             int bitDepth = mode.getBitDepth();
             if (bitDepth == DisplayMode.BIT_DEPTH_MULTI) {
@@ -865,12 +930,8 @@ public final class AWTSettingsDialog extends JFrame {
                 continue;
             }
             String res = mode.getWidth() + " x " + mode.getHeight();
-            if (!res.equals(resolution)) {
-                continue;
-            }
-            String depth = bitDepth + " bpp";
-            if (!depths.contains(depth)) {
-                depths.add(depth);
+            if (res.equals(resolution)) {
+                depths.add(bitDepth + " bpp");
             }
         }
 
@@ -884,10 +945,15 @@ public final class AWTSettingsDialog extends JFrame {
     }
 
     /**
-     * Returns every possible refresh rate for the given resolution.
+     * Returns every possible unique refresh rate string ("XX Hz" or "???")
+     * for the given resolution from an array of `DisplayMode`s.
+     *
+     * @param resolution The resolution string (e.g., "1280 x 720") to filter by.
+     * @param modes      The array of `DisplayMode`s to process.
+     * @return An array of unique refresh rate strings.
      */
     private static String[] getFrequencies(String resolution, DisplayMode[] modes) {
-        List<String> freqs = new ArrayList<>(4);
+        Set<String> freqs = new LinkedHashSet<>(4); // Use LinkedHashSet for uniqueness and order
         for (DisplayMode mode : modes) {
             String res = mode.getWidth() + " x " + mode.getHeight();
             String freq;
@@ -896,20 +962,19 @@ public final class AWTSettingsDialog extends JFrame {
             } else {
                 freq = mode.getRefreshRate() + " Hz";
             }
-            if (res.equals(resolution) && !freqs.contains(freq)) {
-                freqs.add(freq);
-            }
+            freqs.add(freq);
         }
 
         return freqs.toArray(new String[0]);
     }
 
     /**
-     * Chooses the closest frequency to 60 Hz.
-     * 
-     * @param resolution
-     * @param modes
-     * @return
+     * Chooses the closest known refresh rate to 60 Hz for a given resolution.
+     * If no known refresh rates are found for the resolution, returns `null`.
+     *
+     * @param resolution The resolution string (e.g., "1280 x 720") to find the best frequency for.
+     * @param modes      The array of `DisplayMode`s to search within.
+     * @return The best frequency string (e.g., "60 Hz") or `null` if no suitable frequency is found.
      */
     private static String getBestFrequency(String resolution, DisplayMode[] modes) {
         int closest = Integer.MAX_VALUE;

+ 54 - 7
jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java

@@ -1,3 +1,34 @@
+/*
+ * Copyright (c) 2009-2025 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
 package com.jme3.anim;
 
 import com.jme3.anim.util.JointModelTransform;
@@ -5,21 +36,36 @@ import com.jme3.math.Matrix4f;
 import com.jme3.math.Transform;
 
 /**
- * This JointModelTransform implementation accumulate joints transforms in a Matrix4f to properly
- * support non uniform scaling in an armature hierarchy
+ * An implementation of {@link JointModelTransform} that accumulates joint transformations
+ * into a {@link Matrix4f}. This approach is particularly useful for correctly handling
+ * non-uniform scaling within an armature hierarchy, as {@code Matrix4f} can represent
+ * non-uniform scaling directly, unlike {@link Transform}, which typically handles
+ * uniform scaling.
+ * <p>
+ * This class maintains a single {@link Matrix4f} to represent the accumulated
+ * model-space transform of the joint it's associated with.
  */
 public class MatrixJointModelTransform implements JointModelTransform {
 
-    final private Matrix4f modelTransformMatrix = new Matrix4f();
-    final private Transform modelTransform = new Transform();
+    /**
+     * The model-space transform of the joint represented as a Matrix4f.
+     * This matrix accumulates the local transform of the joint and the model transform
+     * of its parent.
+     */
+    private final Matrix4f modelTransformMatrix = new Matrix4f();
+    /**
+     * A temporary Transform instance used for converting the modelTransformMatrix
+     * to a Transform object when {@link #getModelTransform()} is called.
+     */
+    private final Transform modelTransform = new Transform();
 
     @Override
     public void updateModelTransform(Transform localTransform, Joint parent) {
         localTransform.toTransformMatrix(modelTransformMatrix);
         if (parent != null) {
-            ((MatrixJointModelTransform) parent.getJointModelTransform()).getModelTransformMatrix().mult(modelTransformMatrix, modelTransformMatrix);
+            MatrixJointModelTransform transform = (MatrixJointModelTransform) parent.getJointModelTransform();
+            transform.getModelTransformMatrix().mult(modelTransformMatrix, modelTransformMatrix);
         }
-
     }
 
     @Override
@@ -31,7 +77,8 @@ public class MatrixJointModelTransform implements JointModelTransform {
     public void applyBindPose(Transform localTransform, Matrix4f inverseModelBindMatrix, Joint parent) {
         modelTransformMatrix.set(inverseModelBindMatrix).invertLocal(); // model transform = model bind
         if (parent != null) {
-            ((MatrixJointModelTransform) parent.getJointModelTransform()).getModelTransformMatrix().invert().mult(modelTransformMatrix, modelTransformMatrix);
+            MatrixJointModelTransform transform = (MatrixJointModelTransform) parent.getJointModelTransform();
+            transform.getModelTransformMatrix().invert().mult(modelTransformMatrix, modelTransformMatrix);
         }
         localTransform.fromTransformMatrix(modelTransformMatrix);
     }

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

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2023 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -31,12 +31,21 @@
  */
 package com.jme3.anim;
 
-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.material.MatParamOverride;
 import com.jme3.math.FastMath;
 import com.jme3.math.Matrix4f;
-import com.jme3.renderer.*;
-import com.jme3.scene.*;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.RendererException;
+import com.jme3.renderer.ViewPort;
+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.VertexBuffer.Type;
 import com.jme3.scene.control.AbstractControl;
 import com.jme3.scene.mesh.IndexBuffer;
@@ -53,64 +62,77 @@ import java.util.logging.Level;
 import java.util.logging.Logger;
 
 /**
- * The Skinning control deforms a model according to an armature, It handles the
- * computation of the deformation matrices and performs the transformations on
- * the mesh
+ * The `SkinningControl` deforms a 3D model according to an {@link Armature}. It manages the
+ * computation of deformation matrices and applies these transformations to the mesh,
+ * supporting both software and hardware-accelerated skinning.
+ *
+ * <p>
+ * **Software Skinning:** Performed on the CPU, offering broader compatibility but
+ * potentially lower performance for complex models.
  * <p>
- * It can perform software skinning or Hardware skinning
+ * **Hardware Skinning:** Utilizes the GPU for deformation, providing significantly
+ * better performance but requiring shader support and having a limit on the number
+ * of bones (typically 255 in common shaders).
  *
- * @author Rémy Bouquet Based on SkeletonControl by Kirill Vainer
+ * @author Nehon
  */
-public class SkinningControl extends AbstractControl implements Cloneable, JmeCloneable {
+public class SkinningControl extends AbstractControl implements JmeCloneable {
 
     private static final Logger logger = Logger.getLogger(SkinningControl.class.getName());
 
+    /**
+     * The maximum number of bones supported for hardware skinning in common shaders.
+     */
+    private static final int MAX_BONES_HW_SKINNING_SUPPORT = 255;
+
     /**
      * The armature of the model.
      */
     private Armature armature;
 
     /**
-     * List of geometries affected by this control.
+     * A list of geometries that this control will deform.
      */
     private SafeArrayList<Geometry> targets = new SafeArrayList<>(Geometry.class);
 
     /**
-     * Used to track when a mesh was updated. Meshes are only updated if they
+     * Used to track when a mesh needs to be updated. Meshes are only updated if they
      * are visible in at least one camera.
      */
-    private boolean wasMeshUpdated = false;
+    private boolean meshUpdateRequired = true;
 
     /**
-     * User wishes to use hardware skinning if available.
+     * Indicates whether hardware skinning is preferred. If `true` and the GPU
+     * supports it, hardware skinning will be enabled.
      */
-    private transient boolean hwSkinningDesired = true;
+    private transient boolean hwSkinningPreferred = true;
 
     /**
-     * Hardware skinning is currently being used.
+     * Indicates if hardware skinning is currently active and being used.
      */
     private transient boolean hwSkinningEnabled = false;
 
     /**
-     * Hardware skinning was tested on this GPU, results
-     * are stored in {@link #hwSkinningSupported} variable.
+     * Flag indicating whether hardware skinning compatibility has been tested
+     * on the current GPU. Results are stored in {@link #hwSkinningSupported}.
      */
     private transient boolean hwSkinningTested = false;
 
     /**
-     * If hardware skinning was {@link #hwSkinningTested tested}, then
-     * this variable will be set to true if supported, and false if otherwise.
+     * Stores the result of the hardware skinning compatibility test. `true` if
+     * supported, `false` otherwise. This is only valid after
+     * {@link #hwSkinningTested} is `true`.
      */
     private transient boolean hwSkinningSupported = false;
 
     /**
-     * Bone offset matrices, recreated each frame
+     * Bone offset matrices, computed each frame to deform the mesh based on
+     * the armature's current pose.
      */
-    private transient Matrix4f[] offsetMatrices;
-
+    private transient Matrix4f[] boneOffsetMatrices;
 
-    private MatParamOverride numberOfJointsParam;
-    private MatParamOverride jointMatricesParam;
+    private MatParamOverride numberOfJointsParam = new MatParamOverride(VarType.Int, "NumberOfBones", null);
+    private MatParamOverride jointMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null);
 
     /**
      * Serialization only. Do not use.
@@ -119,26 +141,26 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
     }
 
     /**
-     * Creates an armature control. The list of targets will be acquired
-     * automatically when the control is attached to a node.
+     * Creates a new `SkinningControl` for the given armature.
      *
-     * @param armature the armature
+     * @param armature The armature that drives the deformation (not null).
      */
     public SkinningControl(Armature armature) {
         if (armature == null) {
-            throw new IllegalArgumentException("armature cannot be null");
+            throw new IllegalArgumentException("armature cannot be null.");
         }
         this.armature = armature;
-        this.numberOfJointsParam = new MatParamOverride(VarType.Int, "NumberOfBones", null);
-        this.jointMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null);
     }
 
-
-    private void switchToHardware() {
+    /**
+     * Configures the material parameters and meshes for hardware skinning.
+     */
+    private void enableHardwareSkinning() {
         numberOfJointsParam.setEnabled(true);
         jointMatricesParam.setEnabled(true);
 
-        // Next full 10 bones (e.g. 30 on 24 bones)
+        // Calculate the number of bones rounded up to the nearest multiple of 10.
+        // This is often required by shaders for array uniform declarations.
         int numBones = ((armature.getJointCount() / 10) + 1) * 10;
         numberOfJointsParam.setValue(numBones);
 
@@ -150,7 +172,10 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
         }
     }
 
-    private void switchToSoftware() {
+    /**
+     * Configures the material parameters and meshes for software skinning.
+     */
+    private void enableSoftwareSkinning() {
         numberOfJointsParam.setEnabled(false);
         jointMatricesParam.setEnabled(false);
 
@@ -162,22 +187,34 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
         }
     }
 
-    private boolean testHardwareSupported(RenderManager rm) {
-
-        //Only 255 bones max supported with hardware skinning
-        if (armature.getJointCount() > 255) {
+    /**
+     * Tests if hardware skinning is supported by the GPU for the current spatial.
+     *
+     * @param renderManager the RenderManager instance
+     * @return true if hardware skinning is supported, false otherwise
+     */
+    private boolean testHardwareSupported(RenderManager renderManager) {
+        // Only 255 bones max supported with hardware skinning in common shaders.
+        if (armature.getJointCount() > MAX_BONES_HW_SKINNING_SUPPORT) {
+            logger.log(Level.INFO, "Hardware skinning not supported for {0}: Too many bones ({1} > 255).",
+                    new Object[]{spatial, armature.getJointCount()});
             return false;
         }
 
-        switchToHardware();
+        // Temporarily enable hardware skinning to test shader compilation.
+        enableHardwareSkinning();
+        boolean hwSkinningEngaged = false;
 
         try {
-            rm.preloadScene(spatial);
-            return true;
-        } catch (RendererException e) {
-            logger.log(Level.WARNING, "Could not enable HW skinning due to shader compile error:", e);
-            return false;
+            renderManager.preloadScene(spatial);
+            logger.log(Level.INFO, "Hardware skinning engaged for {0}", spatial);
+            hwSkinningEngaged = true;
+
+        } catch (RendererException ex) {
+            logger.log(Level.WARNING, "Could not enable HW skinning due to shader compile error: ", ex);
         }
+
+        return hwSkinningEngaged;
     }
 
     /**
@@ -190,7 +227,7 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
      * @see #isHardwareSkinningUsed()
      */
     public void setHardwareSkinningPreferred(boolean preferred) {
-        hwSkinningDesired = preferred;
+        hwSkinningPreferred = preferred;
     }
 
     /**
@@ -199,7 +236,7 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
      * @see #setHardwareSkinningPreferred(boolean)
      */
     public boolean isHardwareSkinningPreferred() {
-        return hwSkinningDesired;
+        return hwSkinningPreferred;
     }
 
     /**
@@ -209,25 +246,21 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
         return hwSkinningEnabled;
     }
 
-
     /**
-     * If specified the geometry has an animated mesh, add its mesh and material
-     * to the lists of animation targets.
+     * Recursively finds and adds animated geometries to the targets list.
+     *
+     * @param sp The spatial to search within.
      */
-    private void findTargets(Geometry geometry) {
-        Mesh mesh = geometry.getMesh();
-        if (mesh != null && mesh.isAnimated()) {
-            targets.add(geometry);
-        }
-
-    }
-
-    private void findTargets(Node node) {
-        for (Spatial child : node.getChildren()) {
-            if (child instanceof Geometry) {
-                findTargets((Geometry) child);
-            } else if (child instanceof Node) {
-                findTargets((Node) child);
+    private void collectAnimatedGeometries(Spatial sp) {
+        if (sp instanceof Geometry) {
+            Geometry geo = (Geometry) sp;
+            Mesh mesh = geo.getMesh();
+            if (mesh != null && mesh.isAnimated()) {
+                targets.add(geo);
+            }
+        } else if (sp instanceof Node) {
+            for (Spatial child : ((Node) sp).getChildren()) {
+                collectAnimatedGeometries(child);
             }
         }
     }
@@ -236,65 +269,72 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
     public void setSpatial(Spatial spatial) {
         Spatial oldSpatial = this.spatial;
         super.setSpatial(spatial);
-        updateTargetsAndMaterials(spatial);
+        updateAnimationTargets(spatial);
 
         if (oldSpatial != null) {
+            // Ensure parameters are removed from the old spatial to prevent memory leaks
             oldSpatial.removeMatParamOverride(numberOfJointsParam);
             oldSpatial.removeMatParamOverride(jointMatricesParam);
         }
 
         if (spatial != null) {
-            spatial.removeMatParamOverride(numberOfJointsParam);
-            spatial.removeMatParamOverride(jointMatricesParam);
+            // Add parameters to the new spatial. No need to remove first if they are not already present.
             spatial.addMatParamOverride(numberOfJointsParam);
             spatial.addMatParamOverride(jointMatricesParam);
         }
     }
 
+    /**
+     * Performs software skinning updates.
+     */
     private void controlRenderSoftware() {
         resetToBind(); // reset morph meshes to bind pose
 
-        offsetMatrices = armature.computeSkinningMatrices();
+        boneOffsetMatrices = armature.computeSkinningMatrices();
 
         for (Geometry geometry : targets) {
             Mesh mesh = geometry.getMesh();
-            // NOTE: This assumes code higher up has
-            // already ensured this mesh is animated.
-            // Otherwise a crash will happen in skin update.
-            softwareSkinUpdate(mesh, offsetMatrices);
+            // NOTE: This assumes code higher up has already ensured this mesh is animated.
+            // Otherwise, a crash will happen in skin update.
+            applySoftwareSkinning(mesh, boneOffsetMatrices);
         }
     }
 
+    /**
+     * Prepares parameters for hardware skinning.
+     */
     private void controlRenderHardware() {
-        offsetMatrices = armature.computeSkinningMatrices();
-        jointMatricesParam.setValue(offsetMatrices);
+        boneOffsetMatrices = armature.computeSkinningMatrices();
+        jointMatricesParam.setValue(boneOffsetMatrices);
     }
 
     @Override
     protected void controlRender(RenderManager rm, ViewPort vp) {
-        if (!wasMeshUpdated) {
-            updateTargetsAndMaterials(spatial);
+        if (meshUpdateRequired) {
+            updateAnimationTargets(spatial);
 
             // Prevent illegal cases. These should never happen.
-            assert hwSkinningTested || (!hwSkinningTested && !hwSkinningSupported && !hwSkinningEnabled);
-            assert !hwSkinningEnabled || (hwSkinningEnabled && hwSkinningTested && hwSkinningSupported);
+            assert hwSkinningTested || (!hwSkinningSupported && !hwSkinningEnabled);
+            assert !hwSkinningEnabled || (hwSkinningTested && hwSkinningSupported);
 
-            if (hwSkinningDesired && !hwSkinningTested) {
+            if (hwSkinningPreferred && !hwSkinningTested) {
+                // If hardware skinning is preferred and hasn't been tested yet, test it.
                 hwSkinningTested = true;
                 hwSkinningSupported = testHardwareSupported(rm);
 
                 if (hwSkinningSupported) {
                     hwSkinningEnabled = true;
-
-                    Logger.getLogger(SkinningControl.class.getName()).log(Level.INFO, "Hardware skinning engaged for {0}", spatial);
                 } else {
-                    switchToSoftware();
+                    enableSoftwareSkinning();
                 }
-            } else if (hwSkinningDesired && hwSkinningSupported && !hwSkinningEnabled) {
-                switchToHardware();
+            } else if (hwSkinningPreferred && hwSkinningSupported && !hwSkinningEnabled) {
+                // If hardware skinning is preferred, supported, but not yet enabled, enable it.
+                enableHardwareSkinning();
                 hwSkinningEnabled = true;
-            } else if (!hwSkinningDesired && hwSkinningEnabled) {
-                switchToSoftware();
+
+            } else if (!hwSkinningPreferred && hwSkinningEnabled) {
+                // If hardware skinning is no longer preferred but is enabled, switch to software.
+                enableSoftwareSkinning();
                 hwSkinningEnabled = false;
             }
 
@@ -304,17 +344,22 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
                 controlRenderSoftware();
             }
 
-            wasMeshUpdated = true;
+            meshUpdateRequired = false; // Reset flag after update
         }
     }
 
     @Override
     protected void controlUpdate(float tpf) {
-        wasMeshUpdated = false;
+        meshUpdateRequired = true; // Mark for mesh update on next render pass
         armature.update();
     }
 
-    //only do this for software updates
+    /**
+     * Resets the vertex, normal, and tangent buffers of animated meshes to their
+     * original bind pose. This is crucial for software skinning to ensure
+     * transformations are applied from a consistent base.
+     * This method is only applied when performing software updates.
+     */
     void resetToBind() {
         for (Geometry geometry : targets) {
             Mesh mesh = geometry.getMesh();
@@ -378,51 +423,51 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
     }
 
     /**
-     * Access the attachments node of the named bone. If the bone doesn't
-     * already have an attachments node, create one and attach it to the scene
-     * graph. Models and effects attached to the attachments node will follow
-     * the bone's motions.
+     * Provides access to the attachment node for a specific joint in the armature.
+     * If an attachment node does not already exist for the named joint, one will be
+     * created and attached to the scene graph. Models or effects attached to this
+     * node will follow the motion of the corresponding bone.
      *
      * @param jointName the name of the joint
      * @return the attachments node of the joint
      */
     public Node getAttachmentsNode(String jointName) {
-        Joint b = armature.getJoint(jointName);
-        if (b == null) {
-            throw new IllegalArgumentException("Given bone name does not exist "
-                    + "in the armature.");
+        Joint joint = armature.getJoint(jointName);
+        if (joint == null) {
+            throw new IllegalArgumentException(
+                    "Given joint name '" + jointName + "' does not exist in the armature.");
         }
 
-        updateTargetsAndMaterials(spatial);
-        int boneIndex = armature.getJointIndex(b);
-        Node n = b.getAttachmentsNode(boneIndex, targets);
-        /*
-         * Select a node to parent the attachments node.
-         */
+        updateAnimationTargets(spatial);
+        int jointIndex = armature.getJointIndex(joint);
+        Node attachNode = joint.getAttachmentsNode(jointIndex, targets);
+
+        // Determine the appropriate parent for the attachment node.
         Node parent;
         if (spatial instanceof Node) {
             parent = (Node) spatial; // the usual case
         } else {
             parent = spatial.getParent();
         }
-        parent.attachChild(n);
+        parent.attachChild(attachNode);
 
-        return n;
+        return attachNode;
     }
 
     /**
-     * returns the armature of this control
+     * Returns the armature associated with this skinning control.
      *
-     * @return the pre-existing instance
+     * @return The pre-existing `Armature` instance.
      */
     public Armature getArmature() {
         return armature;
     }
 
     /**
-     * Enumerate the target meshes of this control.
+     * Returns an array containing all the target meshes that this control
+     * is currently affecting.
      *
-     * @return a new array
+     * @return A new array of `Mesh` objects.
      */
     public Mesh[] getTargets() {
         Mesh[] result = new Mesh[targets.size()];
@@ -437,30 +482,31 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
     }
 
     /**
-     * Update the mesh according to the given transformation matrices
+     * Applies software skinning transformations to the given mesh using the
+     * provided bone offset matrices.
      *
-     * @param mesh           then mesh
-     * @param offsetMatrices the transformation matrices to apply
+     * @param mesh           The mesh to deform.
+     * @param offsetMatrices The array of transformation matrices for each bone.
      */
-    private void softwareSkinUpdate(Mesh mesh, Matrix4f[] offsetMatrices) {
+    private void applySoftwareSkinning(Mesh mesh, Matrix4f[] offsetMatrices) {
 
         VertexBuffer tb = mesh.getBuffer(Type.Tangent);
         if (tb == null) {
-            //if there are no tangents use the classic skinning
+            // if there are no tangents use the classic skinning
             applySkinning(mesh, offsetMatrices);
         } else {
-            //if there are tangents use the skinning with tangents
+            // if there are tangents use the skinning with tangents
             applySkinningTangents(mesh, offsetMatrices, tb);
         }
-
-
     }
 
     /**
-     * Method to apply skinning transforms to a mesh's buffers
+     * Applies skinning transformations to a mesh's position and normal buffers.
+     * This method iterates through each vertex, applies the weighted sum of
+     * bone transformations, and updates the vertex buffers.
      *
-     * @param mesh           the mesh
-     * @param offsetMatrices the offset matrices to apply
+     * @param mesh           The mesh to apply skinning to.
+     * @param offsetMatrices The bone offset matrices to use for transformation.
      */
     private void applySkinning(Mesh mesh, Matrix4f[] offsetMatrices) {
         int maxWeightsPerVert = mesh.getMaxNumWeights();
@@ -555,19 +601,16 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
 
         vb.updateData(fvb);
         nb.updateData(fnb);
-
     }
 
     /**
-     * Specific method for skinning with tangents to avoid cluttering the
-     * classic skinning calculation with null checks that would slow down the
-     * process even if tangents don't have to be computed. Also the iteration
-     * has additional indexes since tangent has 4 components instead of 3 for
-     * pos and norm
+     * Applies skinning transformations to a mesh's position, normal, and tangent buffers.
+     * This method is specifically designed for meshes that include tangent data,
+     * ensuring proper deformation of tangents alongside positions and normals.
      *
-     * @param mesh           the mesh
-     * @param offsetMatrices the offsetMatrices to apply
-     * @param tb             the tangent vertexBuffer
+     * @param mesh           The mesh to apply skinning to.
+     * @param offsetMatrices The bone offset matrices to use for transformation.
+     * @param tb             The tangent `VertexBuffer`.
      */
     private void applySkinningTangents(Mesh mesh, Matrix4f[] offsetMatrices, VertexBuffer tb) {
         int maxWeightsPerVert = mesh.getMaxNumWeights();
@@ -594,7 +637,6 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
         FloatBuffer ftb = (FloatBuffer) tb.getData();
         ftb.rewind();
 
-
         // get boneIndexes and weights for mesh
         IndexBuffer ib = IndexBuffer.wrapIndexBuffer(mesh.getBuffer(Type.BoneIndex).getData());
         FloatBuffer wb = (FloatBuffer) mesh.getBuffer(Type.BoneWeight).getData();
@@ -605,8 +647,6 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
         int idxWeights = 0;
 
         TempVars vars = TempVars.get();
-
-
         float[] posBuf = vars.skinPositions;
         float[] normBuf = vars.skinNormals;
         float[] tanBuf = vars.skinTangents;
@@ -723,9 +763,6 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
         super.write(ex);
         OutputCapsule oc = ex.getCapsule(this);
         oc.write(armature, "armature", null);
-
-        oc.write(numberOfJointsParam, "numberOfBonesParam", null);
-        oc.write(jointMatricesParam, "boneMatricesParam", null);
     }
 
     /**
@@ -741,15 +778,13 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
         InputCapsule in = im.getCapsule(this);
         armature = (Armature) in.readSavable("armature", null);
 
-        numberOfJointsParam = (MatParamOverride) in.readSavable("numberOfBonesParam", null);
-        jointMatricesParam = (MatParamOverride) in.readSavable("boneMatricesParam", null);
-
-        if (numberOfJointsParam == null) {
-            numberOfJointsParam = new MatParamOverride(VarType.Int, "NumberOfBones", null);
-            jointMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null);
-            getSpatial().addMatParamOverride(numberOfJointsParam);
-            getSpatial().addMatParamOverride(jointMatricesParam);
+        for (MatParamOverride mpo : spatial.getLocalMatParamOverrides().getArray()) {
+            if (mpo.getName().equals("NumberOfBones") || mpo.getName().equals("BoneMatrices")) {
+                spatial.removeMatParamOverride(mpo);
+            }
         }
+        spatial.addMatParamOverride(numberOfJointsParam);
+        spatial.addMatParamOverride(jointMatricesParam);
     }
 
     /**
@@ -757,13 +792,9 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
      *
      * @param spatial the controlled spatial
      */
-    private void updateTargetsAndMaterials(Spatial spatial) {
+    private void updateAnimationTargets(Spatial spatial) {
         targets.clear();
-
-        if (spatial instanceof Node) {
-            findTargets((Node) spatial);
-        } else if (spatial instanceof Geometry) {
-            findTargets((Geometry) spatial);
-        }
+        collectAnimatedGeometries(spatial);
     }
+
 }

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

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2023 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -250,6 +250,8 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
         resetToBind(); // reset morph meshes to bind pose
 
         offsetMatrices = skeleton.computeSkinningMatrices();
+        numberOfBonesParam.setEnabled(false);
+        boneMatricesParam.setEnabled(false);
 
         for (Geometry geometry : targets) {
             Mesh mesh = geometry.getMesh();

+ 14 - 1
jme3-core/src/main/java/com/jme3/app/SimpleApplication.java

@@ -45,6 +45,7 @@ import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.queue.RenderQueue.Bucket;
 import com.jme3.scene.Node;
 import com.jme3.scene.Spatial.CullHint;
+import com.jme3.scene.threadwarden.SceneGraphThreadWarden;
 import com.jme3.system.AppSettings;
 import com.jme3.system.JmeContext.Type;
 import com.jme3.system.JmeSystem;
@@ -251,7 +252,12 @@ public abstract class SimpleApplication extends LegacyApplication {
     public void initialize() {
         super.initialize();
 
-        // Load the default GUI font. This is essential for rendering text like FPS.
+        //noinspection AssertWithSideEffects
+        assert SceneGraphThreadWarden.setup(rootNode);
+        //noinspection AssertWithSideEffects
+        assert SceneGraphThreadWarden.setup(guiNode);
+
+        // Several things rely on having this
         guiFont = loadGuiFont();
 
         guiNode.setQueueBucket(Bucket.Gui);
@@ -301,6 +307,13 @@ public abstract class SimpleApplication extends LegacyApplication {
         simpleInitApp();
     }
 
+    @Override
+    public void stop(boolean waitFor) {
+        //noinspection AssertWithSideEffects
+        assert SceneGraphThreadWarden.reset();
+        super.stop(waitFor);
+    }
+
     @Override
     public void update() {
         if (prof != null) {

+ 75 - 25
jme3-core/src/main/java/com/jme3/app/state/ConstantVerifierState.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014-2021 jMonkeyEngine
+ * Copyright (c) 2014-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -29,30 +29,39 @@
  * 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.app.state;
 
-import java.util.Arrays;
-import java.util.logging.Logger;
-
 import com.jme3.app.Application;
-import com.jme3.math.*;
+import com.jme3.math.Matrix3f;
+import com.jme3.math.Matrix4f;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.math.Vector4f;
 import com.jme3.util.SafeArrayList;
 
+import java.util.Arrays;
+import java.util.logging.Logger;
+
+import static java.lang.Float.NEGATIVE_INFINITY;
 import static java.lang.Float.NaN;
 import static java.lang.Float.POSITIVE_INFINITY;
-import static java.lang.Float.NEGATIVE_INFINITY;
 
 /**
- *  Checks the various JME 'constants' for drift using either asserts
- *  or straight checks.  The list of constants can also be configured
- *  but defaults to the standard JME Vector3f, Quaternion, etc. constants.
+ * An AppState that periodically checks the values of various JME math constants
+ * (e.g., `Vector3f.ZERO`, `Quaternion.IDENTITY`) against their known good values.
+ * This is useful for detecting accidental modifications or "drift" of these
+ * supposedly immutable constants during application runtime.
+ * <p>
+ * The state can be configured to report discrepancies using asserts,
+ * throwing runtime exceptions, or logging severe messages.
+ * The set of constants to check is configurable.
  *
- *  @author    Paul Speed
+ * @author Paul Speed
  */
 public class ConstantVerifierState extends BaseAppState {
 
-    private static final Logger log = Logger.getLogger(BaseAppState.class.getName());
+    private static final Logger log = Logger.getLogger(ConstantVerifierState.class.getName());
 
     // Note: I've used actual constructed objects for the good values
     //       instead of clone just to better catch cases where the values
@@ -73,7 +82,14 @@ public class ConstantVerifierState extends BaseAppState {
                     new Quaternion().fromAxes(Vector3f.UNIT_X, Vector3f.UNIT_Y, Vector3f.UNIT_Z)),
             new Checker(Quaternion.ZERO, new Quaternion(0, 0, 0, 0)),
             new Checker(Vector2f.ZERO, new Vector2f(0f, 0f)),
+            new Checker(Vector2f.NAN, new Vector2f(NaN, NaN)),
+            new Checker(Vector2f.UNIT_X, new Vector2f(1, 0)),
+            new Checker(Vector2f.UNIT_Y, new Vector2f(0, 1)),
             new Checker(Vector2f.UNIT_XY, new Vector2f(1f, 1f)),
+            new Checker(Vector2f.POSITIVE_INFINITY,
+                    new Vector2f(POSITIVE_INFINITY, POSITIVE_INFINITY)),
+            new Checker(Vector2f.NEGATIVE_INFINITY,
+                    new Vector2f(NEGATIVE_INFINITY, NEGATIVE_INFINITY)),
             new Checker(Vector4f.ZERO, new Vector4f(0, 0, 0, 0)),
             new Checker(Vector4f.NAN, new Vector4f(NaN, NaN, NaN, NaN)),
             new Checker(Vector4f.UNIT_X, new Vector4f(1, 0, 0, 0)),
@@ -91,24 +107,34 @@ public class ConstantVerifierState extends BaseAppState {
             new Checker(Matrix4f.IDENTITY, new Matrix4f())
         };
 
-    public enum ErrorType { Assert, Exception, Log };
+    /**
+     * Defines how constant value discrepancies should be reported.
+     */
+    public enum ErrorType {
+        /** Causes an `assert` failure if the constant has changed. Requires assertions to be enabled. */
+        Assert,
+        /** Throws a `RuntimeException` if the constant has changed. */
+        Exception,
+        /** Logs a severe message if the constant has changed. */
+        Log
+    }
 
-    final private SafeArrayList<Checker> checkers = new SafeArrayList<>(Checker.class);
+    private final SafeArrayList<Checker> checkers = new SafeArrayList<>(Checker.class);
     private ErrorType errorType;
 
     /**
-     *  Creates a verifier app state that will check all of the default
-     *  constant checks using asserts.
+     * Creates a verifier app state that will check all of the default
+     * JME math constants using `ErrorType.Assert`.
      */
     public ConstantVerifierState() {
         this(ErrorType.Assert);
     }
 
     /**
-     *  Creates a verifier app state that will check all of the default
-     *  constant checks using the specified error reporting mechanism.
+     * Creates a verifier app state that will check all of the default
+     * JME math constants using the specified error reporting mechanism.
      *
-     * @param errorType the mechanism to use
+     * @param errorType The mechanism to use when a constant's value drifts.
      */
     public ConstantVerifierState(ErrorType errorType) {
         this(errorType, DEFAULT_CHECKS);
@@ -126,14 +152,32 @@ public class ConstantVerifierState extends BaseAppState {
         this.checkers.addAll(Arrays.asList(checkers));
     }
 
+    /**
+     * Adds a new constant and its expected good value to the list of items to be checked.
+     * The `constant` and `goodValue` should be instances of the same class.
+     *
+     * @param constant The JME constant object to monitor for drift (e.g., `Vector3f.ZERO`).
+     * @param goodValue An independent instance representing the expected correct value of the constant.
+     * This instance should match the initial value of `constant`.
+     */
     public void addChecker(Object constant, Object goodValue) {
         checkers.add(new Checker(constant, goodValue));
     }
 
+    /**
+     * Sets the error reporting mechanism to be used when a constant's value drifts.
+     *
+     * @param errorType The desired error reporting type.
+     */
     public void setErrorType(ErrorType errorType) {
         this.errorType = errorType;
     }
 
+    /**
+     * Returns the currently configured error reporting mechanism.
+     *
+     * @return The current `ErrorType`.
+     */
     public ErrorType getErrorType() {
         return errorType;
     }
@@ -161,21 +205,26 @@ public class ConstantVerifierState extends BaseAppState {
         checkValues();
     }
 
+    /**
+     * Iterates through all registered checkers and verifies the current values
+     * of the constants against their known good values.
+     * Reports any discrepancies based on the configured `ErrorType`.
+     */
     protected void checkValues() {
         for (Checker checker : checkers.getArray()) {
             switch (errorType) {
-                default:
+                default: // Fall through to Assert if somehow null
                 case Assert:
                     assert checker.isValid() : checker.toString();
                     break;
                 case Exception:
                     if (!checker.isValid()) {
-                        throw new RuntimeException("Constant has changed, " + checker.toString());
+                        throw new RuntimeException("JME Constant has changed, " + checker.toString());
                     }
                     break;
                 case Log:
                     if (!checker.isValid()) {
-                        log.severe("Constant has changed, " + checker.toString());
+                        log.severe("JME Constant has changed, " + checker.toString());
                     }
                     break;
             }
@@ -188,8 +237,9 @@ public class ConstantVerifierState extends BaseAppState {
      *  mean anything.
      */
     private static class Checker {
-        private Object constant;
-        private Object goodValue;
+
+        private final Object constant;
+        private final Object goodValue;
 
         public Checker(Object constant, Object goodValue) {
             if (constant == null) {
@@ -197,7 +247,7 @@ public class ConstantVerifierState extends BaseAppState {
             }
             if (!constant.equals(goodValue)) {
                 throw new IllegalArgumentException(
-                        "Constant value:" + constant + " does not match value:" + goodValue);
+                        "Constant value: " + constant + " does not match value: " + goodValue);
             }
             this.constant = constant;
             this.goodValue = goodValue;

+ 5 - 1
jme3-core/src/main/java/com/jme3/audio/AudioNode.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -317,6 +317,10 @@ public class AudioNode extends Node implements AudioSource {
         data = audioData;
         this.audioKey = audioKey;
     }
+    
+    public AudioKey getAudioKey() {
+        return audioKey;
+    }
 
     /**
      * @return The {@link AudioData} set previously with

+ 17 - 19
jme3-core/src/main/java/com/jme3/cinematic/events/MotionEvent.java

@@ -52,8 +52,8 @@ import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 /**
- * A MotionEvent is a control over the spatial that manages the position and direction of the spatial while following a motion Path.
- *
+ * A MotionEvent is a control over the spatial that manages
+ * the position and direction of the spatial while following a motion Path.
  * You must first create a MotionPath and then create a MotionEvent to associate a spatial and the path.
  *
  * @author Nehon
@@ -70,6 +70,7 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
     protected Direction directionType = Direction.None;
     protected MotionPath path;
     private boolean isControl = true;
+    private final Quaternion tempRotation = new Quaternion();
     /**
      * the distance traveled by the spatial on the path
      */
@@ -79,7 +80,6 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
      * Enum for the different type of target direction behavior.
      */
     public enum Direction {
-
         /**
          * The target stays in the starting direction.
          */
@@ -229,13 +229,13 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
     @Override
     public void read(JmeImporter im) throws IOException {
         super.read(im);
-        InputCapsule in = im.getCapsule(this);
-        lookAt = (Vector3f) in.readSavable("lookAt", null);
-        upVector = (Vector3f) in.readSavable("upVector", Vector3f.UNIT_Y);
-        rotation = (Quaternion) in.readSavable("rotation", null);
-        directionType = in.readEnum("directionType", Direction.class, Direction.None);
-        path = (MotionPath) in.readSavable("path", null);
-        spatial = (Spatial) in.readSavable("spatial", null);
+        InputCapsule ic = im.getCapsule(this);
+        lookAt = (Vector3f) ic.readSavable("lookAt", null);
+        upVector = (Vector3f) ic.readSavable("upVector", Vector3f.UNIT_Y);
+        rotation = (Quaternion) ic.readSavable("rotation", null);
+        directionType = ic.readEnum("directionType", Direction.class, Direction.None);
+        path = (MotionPath) ic.readSavable("path", null);
+        spatial = (Spatial) ic.readSavable("spatial", null);
     }
 
     /**
@@ -249,9 +249,8 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
     private void computeTargetDirection() {
         switch (directionType) {
             case Path:
-                Quaternion q = new Quaternion();
-                q.lookAt(direction, upVector);
-                spatial.setLocalRotation(q);
+                tempRotation.lookAt(direction, upVector);
+                spatial.setLocalRotation(tempRotation);
                 break;
             case LookAt:
                 if (lookAt != null) {
@@ -260,10 +259,9 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
                 break;
             case PathAndRotation:
                 if (rotation != null) {
-                    Quaternion q2 = new Quaternion();
-                    q2.lookAt(direction, upVector);
-                    q2.multLocal(rotation);
-                    spatial.setLocalRotation(q2);
+                    tempRotation.lookAt(direction, upVector);
+                    tempRotation.multLocal(rotation);
+                    spatial.setLocalRotation(tempRotation);
                 }
                 break;
             case Rotation:
@@ -272,6 +270,7 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
                 }
                 break;
             case None:
+                // no-op
                 break;
             default:
                 break;
@@ -376,8 +375,7 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
 
     /**
      * Sets the direction of the spatial, using the Y axis as the up vector.
-     * Use MotionEvent#setDirection((Vector3f direction,Vector3f upVector) if
-     * you want a custom up vector.
+     * If a custom up vector is desired, use {@link #setDirection(Vector3f, Vector3f)}.
      * This method is used by the motion path.
      *
      * @param direction the desired forward direction (not null, unaffected)

+ 5 - 3
jme3-core/src/main/java/com/jme3/effect/influencers/NewtonianParticleInfluencer.java

@@ -54,6 +54,8 @@ public class NewtonianParticleInfluencer extends DefaultParticleInfluencer {
     /** Emitters tangent rotation factor. */
     protected float surfaceTangentRotation;
 
+    protected Matrix3f tempMat3 = new Matrix3f();
+
     /**
      * Constructor. Sets velocity variation to 0.0f.
      */
@@ -71,15 +73,15 @@ public class NewtonianParticleInfluencer extends DefaultParticleInfluencer {
             // calculating surface tangent (velocity contains the 'normal' value)
             temp.set(particle.velocity.z * surfaceTangentFactor, particle.velocity.y * surfaceTangentFactor, -particle.velocity.x * surfaceTangentFactor);
             if (surfaceTangentRotation != 0.0f) {// rotating the tangent
-                Matrix3f m = new Matrix3f();
-                m.fromAngleNormalAxis(FastMath.PI * surfaceTangentRotation, particle.velocity);
-                temp = m.multLocal(temp);
+                tempMat3.fromAngleNormalAxis(FastMath.PI * surfaceTangentRotation, particle.velocity);
+                temp = tempMat3.multLocal(temp);
             }
             // applying normal factor (this must be done first)
             particle.velocity.multLocal(normalVelocity);
             // adding tangent vector
             particle.velocity.addLocal(temp);
         }
+        particle.velocity.addLocal(initialVelocity);
         if (velocityVariation != 0.0f) {
             this.applyVelocityVariation(particle);
         }

+ 92 - 39
jme3-core/src/main/java/com/jme3/effect/shapes/EmitterMeshFaceShape.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -36,6 +36,7 @@ import com.jme3.math.Vector3f;
 import com.jme3.scene.Mesh;
 import com.jme3.scene.VertexBuffer.Type;
 import com.jme3.util.BufferUtils;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -52,79 +53,131 @@ public class EmitterMeshFaceShape extends EmitterMeshVertexShape {
     }
 
     /**
-     * Constructor. It stores a copy of vertex list of all meshes.
-     * @param meshes
-     *        a list of meshes that will form the emitter's shape
+     * Constructor. Initializes the emitter shape with a list of meshes.
+     * The vertices and normals for all triangles of these meshes are
+     * extracted and stored internally.
+     *
+     * @param meshes a list of {@link Mesh} objects that will define the
+     * shape from which particles are emitted.
      */
     public EmitterMeshFaceShape(List<Mesh> meshes) {
         super(meshes);
     }
 
+    /**
+     * Sets the meshes for this emitter shape. This method extracts all
+     * triangle vertices and computes their normals, storing them internally
+     * for subsequent particle emission.
+     *
+     * @param meshes a list of {@link Mesh} objects to set as the emitter's shape.
+     */
     @Override
     public void setMeshes(List<Mesh> meshes) {
         this.vertices = new ArrayList<List<Vector3f>>(meshes.size());
         this.normals = new ArrayList<List<Vector3f>>(meshes.size());
+
         for (Mesh mesh : meshes) {
             Vector3f[] vertexTable = BufferUtils.getVector3Array(mesh.getFloatBuffer(Type.Position));
             int[] indices = new int[3];
-            List<Vector3f> vertices = new ArrayList<>(mesh.getTriangleCount() * 3);
-            List<Vector3f> normals = new ArrayList<>(mesh.getTriangleCount());
+            List<Vector3f> meshVertices = new ArrayList<>(mesh.getTriangleCount() * 3);
+            List<Vector3f> meshNormals = new ArrayList<>(mesh.getTriangleCount());
+
             for (int i = 0; i < mesh.getTriangleCount(); ++i) {
                 mesh.getTriangle(i, indices);
-                vertices.add(vertexTable[indices[0]]);
-                vertices.add(vertexTable[indices[1]]);
-                vertices.add(vertexTable[indices[2]]);
-                normals.add(FastMath.computeNormal(vertexTable[indices[0]], vertexTable[indices[1]], vertexTable[indices[2]]));
+
+                Vector3f v1 = vertexTable[indices[0]];
+                Vector3f v2 = vertexTable[indices[1]];
+                Vector3f v3 = vertexTable[indices[2]];
+
+                // Add all three vertices of the triangle
+                meshVertices.add(v1);
+                meshVertices.add(v2);
+                meshVertices.add(v3);
+
+                // Compute and add the normal for the current triangle face
+                meshNormals.add(FastMath.computeNormal(v1, v2, v3));
             }
-            this.vertices.add(vertices);
-            this.normals.add(normals);
+            this.vertices.add(meshVertices);
+            this.normals.add(meshNormals);
         }
     }
 
     /**
-     * Randomly selects a point on a random face.
+     * Randomly selects a point on a random face of one of the stored meshes.
+     * The point is generated using barycentric coordinates to ensure uniform
+     * distribution within the selected triangle.
      *
-     * @param store
-     *        storage for the coordinates of the selected point
+     * @param store a {@link Vector3f} object where the coordinates of the
+     *              selected point will be stored.
      */
     @Override
     public void getRandomPoint(Vector3f store) {
         int meshIndex = FastMath.nextRandomInt(0, vertices.size() - 1);
+        List<Vector3f> currVertices = vertices.get(meshIndex);
+        int numVertices = currVertices.size();
+
         // the index of the first vertex of a face (must be dividable by 3)
-        int vertIndex = FastMath.nextRandomInt(0, vertices.get(meshIndex).size() / 3 - 1) * 3;
-        // put the point somewhere between the first and the second vertex of a face
-        float moveFactor = FastMath.nextRandomFloat();
-        store.set(Vector3f.ZERO);
-        store.addLocal(vertices.get(meshIndex).get(vertIndex));
-        store.addLocal((vertices.get(meshIndex).get(vertIndex + 1).x - vertices.get(meshIndex).get(vertIndex).x) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 1).y - vertices.get(meshIndex).get(vertIndex).y) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 1).z - vertices.get(meshIndex).get(vertIndex).z) * moveFactor);
-        // move the result towards the last face vertex
-        moveFactor = FastMath.nextRandomFloat();
-        store.addLocal((vertices.get(meshIndex).get(vertIndex + 2).x - store.x) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 2).y - store.y) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 2).z - store.z) * moveFactor);
+        int faceIndex = FastMath.nextRandomInt(0, numVertices / 3 - 1);
+        int vertIndex = faceIndex * 3;
+
+        // Generate the random point on the triangle
+        generateRandomPointOnTriangle(currVertices, vertIndex, store);
     }
 
     /**
-     * Randomly selects a point on a random face.
-     * The {@code normal} argument is set to the normal of the selected face.
+     * Randomly selects a point on a random face of one of the stored meshes,
+     * and also sets the normal of that selected face.
+     * The point is generated using barycentric coordinates for uniform distribution.
      *
-     * @param store
-     *        storage for the coordinates of the selected point
-     * @param normal
-     *        storage for the normal of the selected face
+     * @param store  a {@link Vector3f} object where the coordinates of the
+     *               selected point will be stored.
+     * @param normal a {@link Vector3f} object where the normal of the
+     *               selected face will be stored.
      */
     @Override
     public void getRandomPointAndNormal(Vector3f store, Vector3f normal) {
         int meshIndex = FastMath.nextRandomInt(0, vertices.size() - 1);
+        List<Vector3f> currVertices = vertices.get(meshIndex);
+        int numVertices = currVertices.size();
+
         // the index of the first vertex of a face (must be dividable by 3)
-        int faceIndex = FastMath.nextRandomInt(0, vertices.get(meshIndex).size() / 3 - 1);
+        int faceIndex = FastMath.nextRandomInt(0, numVertices / 3 - 1);
         int vertIndex = faceIndex * 3;
-        // put the point somewhere between the first and the second vertex of a face
-        float moveFactor = FastMath.nextRandomFloat();
-        store.set(Vector3f.ZERO);
-        store.addLocal(vertices.get(meshIndex).get(vertIndex));
-        store.addLocal((vertices.get(meshIndex).get(vertIndex + 1).x - vertices.get(meshIndex).get(vertIndex).x) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 1).y - vertices.get(meshIndex).get(vertIndex).y) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 1).z - vertices.get(meshIndex).get(vertIndex).z) * moveFactor);
-        // move the result towards the last face vertex
-        moveFactor = FastMath.nextRandomFloat();
-        store.addLocal((vertices.get(meshIndex).get(vertIndex + 2).x - store.x) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 2).y - store.y) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 2).z - store.z) * moveFactor);
+
+        // Generate the random point on the triangle
+        generateRandomPointOnTriangle(currVertices, vertIndex, store);
+        // Set the normal from the pre-computed normals list for the selected face
         normal.set(normals.get(meshIndex).get(faceIndex));
     }
+
+    /**
+     * Internal method to generate a random point within a specific triangle
+     * using barycentric coordinates.
+     *
+     * @param currVertices The list of vertices for the current mesh.
+     * @param vertIndex    The starting index of the triangle's first vertex
+     *                     within the {@code currVertices} list.
+     * @param store        A {@link Vector3f} object where the calculated point will be stored.
+     */
+    private void generateRandomPointOnTriangle(List<Vector3f> currVertices, int vertIndex, Vector3f store) {
+
+        Vector3f v1 = currVertices.get(vertIndex);
+        Vector3f v2 = currVertices.get(vertIndex + 1);
+        Vector3f v3 = currVertices.get(vertIndex + 2);
+
+        // Generate random barycentric coordinates
+        float u = FastMath.nextRandomFloat();
+        float v = FastMath.nextRandomFloat();
+
+        if ((u + v) > 1) {
+            u = 1 - u;
+            v = 1 - v;
+        }
+
+        // P = v1 + u * (v2 - v1) + v * (v3 - v1)
+        store.x = v1.x + u * (v2.x - v1.x) + v * (v3.x - v1.x);
+        store.y = v1.y + u * (v2.y - v1.y) + v * (v3.y - v1.y);
+        store.z = v1.z + u * (v2.z - v1.z) + v * (v3.z - v1.z);
+    }
+
 }

+ 167 - 0
jme3-core/src/main/java/com/jme3/environment/util/Circle.java

@@ -0,0 +1,167 @@
+ /*
+  * Copyright (c) 2009-2025 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.environment.util;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.material.Material;
+import com.jme3.material.RenderState;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.renderer.queue.RenderQueue;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.util.BufferUtils;
+
+import java.io.IOException;
+import java.nio.FloatBuffer;
+import java.nio.ShortBuffer;
+
+/**
+ * <p>A `Circle` is a 2D mesh representing a circular outline (wireframe).
+ * It's defined by a specified number of radial samples, which determine its smoothness.</p>
+ *
+ * <p>The circle is centered at (0,0,0) in its local coordinate space and has a radius of 1.0.</p>
+ *
+ * @author capdevon
+ */
+public class Circle extends Mesh {
+
+    // The number of segments used to approximate the circle.
+    protected int radialSamples = 256;
+
+    /**
+     * Creates a new `Circle` mesh.
+     */
+    public Circle() {
+        setGeometryData();
+        setIndexData();
+    }
+
+    /**
+     * Initializes the vertex buffers for the circle mesh.
+     */
+    private void setGeometryData() {
+
+        int numVertices = radialSamples + 1;
+
+        FloatBuffer posBuf = BufferUtils.createVector3Buffer(numVertices);
+        FloatBuffer colBuf = BufferUtils.createFloatBuffer(numVertices * 4);
+        FloatBuffer texBuf = BufferUtils.createVector2Buffer(numVertices);
+
+        // --- Generate Geometry Data ---
+        float angleStep = FastMath.TWO_PI / radialSamples;
+
+        // Define the color for the entire circle.
+        ColorRGBA color = ColorRGBA.Orange;
+
+        // Populate the position, color, and texture coordinate buffers.
+        for (int i = 0; i < numVertices; i++) {
+            float angle = angleStep * i;
+            float cos = FastMath.cos(angle);
+            float sin = FastMath.sin(angle);
+
+            posBuf.put(cos).put(sin).put(0);
+            colBuf.put(color.r).put(color.g).put(color.b).put(color.a);
+            texBuf.put(i % 2f).put(i % 2f);
+        }
+
+        setBuffer(Type.Position, 3, posBuf);
+        setBuffer(Type.Color, 4, colBuf);
+        setBuffer(Type.TexCoord, 2, texBuf);
+
+        setMode(Mode.Lines);
+        updateBound();
+        setStatic();
+    }
+
+    /**
+     * Initializes the index buffer for the circle mesh.
+     */
+    private void setIndexData() {
+        // allocate connectivity
+        int numIndices = radialSamples * 2;
+
+        ShortBuffer idxBuf = BufferUtils.createShortBuffer(numIndices);
+        setBuffer(Type.Index, 2, idxBuf);
+
+        // --- Generate Index Data ---
+        for (int i = 0; i < radialSamples; i++) {
+            idxBuf.put((short) i);         // Start of segment
+            idxBuf.put((short) (i + 1));   // End of segment
+        }
+    }
+
+    /**
+     * Creates a {@link Geometry} object representing a dashed wireframe circle.
+     *
+     * @param assetManager The application's AssetManager to load materials.
+     * @param name         The desired name for the Geometry.
+     * @return A new Geometry instance with a `Circle` mesh.
+     */
+    public static Geometry createShape(AssetManager assetManager, String name) {
+        Circle mesh = new Circle();
+        Geometry geom = new Geometry(name, mesh);
+        geom.setQueueBucket(RenderQueue.Bucket.Transparent);
+
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Dashed.j3md");
+        mat.getAdditionalRenderState().setWireframe(true);
+        mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
+        mat.getAdditionalRenderState().setDepthWrite(false);
+        mat.getAdditionalRenderState().setDepthTest(false);
+        mat.getAdditionalRenderState().setLineWidth(2f);
+        mat.setColor("Color", ColorRGBA.Orange);
+        mat.setFloat("DashSize", 0.5f);
+        geom.setMaterial(mat);
+
+        return geom;
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(radialSamples, "radialSamples", 256);
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+        radialSamples = ic.readInt("radialSamples", 256);
+    }
+
+}

+ 359 - 86
jme3-core/src/main/java/com/jme3/environment/util/LightsDebugState.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -33,154 +33,427 @@ package com.jme3.environment.util;
 
 import com.jme3.app.Application;
 import com.jme3.app.state.BaseAppState;
-import com.jme3.light.*;
+import com.jme3.asset.AssetManager;
+import com.jme3.light.DirectionalLight;
+import com.jme3.light.Light;
+import com.jme3.light.LightProbe;
+import com.jme3.light.PointLight;
+import com.jme3.light.SpotLight;
 import com.jme3.material.Material;
+import com.jme3.material.RenderState;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
 import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.renderer.queue.RenderQueue;
 import com.jme3.scene.Geometry;
 import com.jme3.scene.Node;
 import com.jme3.scene.Spatial;
+import com.jme3.scene.control.BillboardControl;
+import com.jme3.scene.debug.Arrow;
+import com.jme3.scene.shape.Quad;
 import com.jme3.scene.shape.Sphere;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
+import com.jme3.texture.Texture;
+
+import java.util.ArrayDeque;
+import java.util.Iterator;
 import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.function.Predicate;
 
 /**
- * A debug state that will display Light gizmos on screen.
- * Still a wip and for now it only displays light probes.
+ * A debug state that visualizes different types of lights in the scene with gizmos.
+ * This state is useful for debugging light positions, ranges, and other properties.
  *
  * @author nehon
+ * @author capdevon
  */
 public class LightsDebugState extends BaseAppState {
 
-    private Node debugNode;
-    private final Map<LightProbe, Node> probeMapping = new HashMap<>();
-    private final List<LightProbe> garbage = new ArrayList<>();
-    private Geometry debugGeom;
-    private Geometry debugBounds;
+    private static final String PROBE_GEOMETRY_NAME = "DebugProbeGeometry";
+    private static final String PROBE_BOUNDS_NAME = "DebugProbeBounds";
+    private static final String SPOT_LIGHT_INNER_RADIUS_NAME = "SpotLightInnerRadius";
+    private static final String SPOT_LIGHT_OUTER_RADIUS_NAME = "SpotLightOuterRadius";
+    private static final String SPOT_LIGHT_RADIUS_NAME = "RadiusNode";
+    private static final String POINT_LIGHT_RADIUS_NAME = "PointLightRadius";
+    private static final String LIGHT_DIR_ARROW_NAME = "LightDirection";
+
+    private final Map<Light, Spatial> lightGizmoMap = new WeakHashMap<>();
+    private final ArrayDeque<Light> lightDeque = new ArrayDeque<>();
+    private Predicate<Light> lightFilter = x -> true; // Identity Function
+
+    private ViewPort viewPort;
+    private AssetManager assetManager;
     private Material debugMaterial;
-    private float probeScale = 1.0f;
-    private Spatial scene = null;
-    private final List<LightProbe> probes = new ArrayList<>();
+    private Node debugNode;
+    private Spatial scene; // The scene whose lights will be debugged
+
+    private boolean showOnTop = true;
+    private float lightProbeScale = 1.0f;
+    private final ColorRGBA debugColor = ColorRGBA.DarkGray;
+    private final Quaternion tempRotation = new Quaternion();
 
     @Override
     protected void initialize(Application app) {
-        debugNode = new Node("Environment debug Node");
-        Sphere s = new Sphere(16, 16, 0.15f);
-        debugGeom = new Geometry("debugEnvProbe", s);
-        debugMaterial = new Material(app.getAssetManager(), "Common/MatDefs/Misc/reflect.j3md");
-        debugGeom.setMaterial(debugMaterial);
-        debugBounds = BoundingSphereDebug.createDebugSphere(app.getAssetManager());
+
+        viewPort = app.getRenderManager().createMainView("LightsDebugView", app.getCamera());
+        viewPort.setClearFlags(false, showOnTop, true);
+
+        assetManager = app.getAssetManager();
+        debugNode = new Node("LightsDebugNode");
+
+        debugMaterial = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        debugMaterial.setColor("Color", debugColor);
+        debugMaterial.getAdditionalRenderState().setWireframe(true);
+
         if (scene == null) {
             scene = app.getViewPort().getScenes().get(0);
         }
     }
 
+    private Spatial createBulb() {
+        Quad q = new Quad(0.5f, 0.5f);
+        Geometry lightBulb = new Geometry("LightBulb", q);
+        lightBulb.move(-q.getHeight() / 2f, -q.getWidth() / 2f, 0);
+
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        Texture tex = assetManager.loadTexture("Common/Textures/lightbulb32.png");
+        mat.setTexture("ColorMap", tex);
+        mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
+        lightBulb.setMaterial(mat);
+        lightBulb.setQueueBucket(RenderQueue.Bucket.Transparent);
+
+        Node billboard = new Node("Billboard");
+        billboard.addControl(new BillboardControl());
+        billboard.attachChild(lightBulb);
+
+        return billboard;
+    }
+
+    private Geometry createRadiusShape(String name, float dashSize) {
+        Geometry radius = Circle.createShape(assetManager, name);
+        Material mat = radius.getMaterial();
+        mat.setColor("Color", debugColor);
+        mat.setFloat("DashSize", dashSize);
+        return radius;
+    }
+
+    private Spatial createPointGizmo() {
+        Node gizmo = new Node("PointLightNode");
+        gizmo.attachChild(createBulb());
+
+        Geometry radius = new Geometry(POINT_LIGHT_RADIUS_NAME, new BoundingSphereDebug());
+        radius.setMaterial(debugMaterial);
+        gizmo.attachChild(radius);
+
+        return gizmo;
+    }
+
+    private Spatial createDirectionalGizmo() {
+        Node gizmo = new Node("DirectionalLightNode");
+        gizmo.move(0, 5, 0);
+        gizmo.attachChild(createBulb());
+
+        Geometry arrow = new Geometry(LIGHT_DIR_ARROW_NAME, new Arrow(Vector3f.UNIT_Z.mult(5f)));
+        arrow.setMaterial(debugMaterial);
+        gizmo.attachChild(arrow);
+
+        return gizmo;
+    }
+
+    private Spatial createSpotGizmo() {
+        Node gizmo = new Node("SpotLightNode");
+        gizmo.attachChild(createBulb());
+
+        Node radiusNode = new Node(SPOT_LIGHT_RADIUS_NAME);
+        gizmo.attachChild(radiusNode);
+
+        Geometry inRadius = createRadiusShape(SPOT_LIGHT_INNER_RADIUS_NAME, 0.725f);
+        radiusNode.attachChild(inRadius);
+
+        Geometry outRadius = createRadiusShape(SPOT_LIGHT_OUTER_RADIUS_NAME, 0.325f);
+        radiusNode.attachChild(outRadius);
+
+        Geometry arrow = new Geometry(LIGHT_DIR_ARROW_NAME, new Arrow(Vector3f.UNIT_Z));
+        arrow.setMaterial(debugMaterial);
+        gizmo.attachChild(arrow);
+
+        return gizmo;
+    }
+
+    private Spatial createLightProbeGizmo() {
+        Node gizmo = new Node("LightProbeNode");
+
+        Sphere sphere = new Sphere(32, 32, lightProbeScale);
+        Geometry probeGeom = new Geometry(PROBE_GEOMETRY_NAME, sphere);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/reflect.j3md");
+        probeGeom.setMaterial(mat);
+        gizmo.attachChild(probeGeom);
+
+        Geometry probeBounds = BoundingSphereDebug.createDebugSphere(assetManager);
+        probeBounds.setName(PROBE_BOUNDS_NAME);
+        gizmo.attachChild(probeBounds);
+
+        return gizmo;
+    }
+
+    /**
+     * Updates the light gizmos based on the current state of lights in the scene.
+     * This method is called every frame when the state is enabled.
+     *
+     * @param tpf The time per frame.
+     */
     @Override
     public void update(float tpf) {
-        if (!isEnabled()) {
-            return;
-        }
-        updateLights(scene);
+        updateLightGizmos(scene);
         debugNode.updateLogicalState(tpf);
+        cleanUpRemovedLights();
+    }
+
+    /**
+     * Renders the debug gizmos onto the screen.
+     *
+     * @param rm The render manager.
+     */
+    @Override
+    public void render(RenderManager rm) {
         debugNode.updateGeometricState();
-        cleanProbes();
-    }
-
-    public void updateLights(Spatial scene) {
-        for (Light light : scene.getWorldLightList()) {
-            switch (light.getType()) {
-
-                case Probe:
-                    LightProbe probe = (LightProbe) light;
-                    probes.add(probe);
-                    Node n = probeMapping.get(probe);
-                    if (n == null) {
-                        n = new Node("DebugProbe");
-                        n.attachChild(debugGeom.clone(true));
-                        n.attachChild(debugBounds.clone(false));
-                        debugNode.attachChild(n);
-                        probeMapping.put(probe, n);
-                    }
-                    Geometry probeGeom = ((Geometry) n.getChild(0));
-                    Material m = probeGeom.getMaterial();
-                    probeGeom.setLocalScale(probeScale);
-                    if (probe.isReady()) {
-                        m.setTexture("CubeMap", probe.getPrefilteredEnvMap());
-                    }
-                    n.setLocalTranslation(probe.getPosition());
-                    n.getChild(1).setLocalScale(probe.getArea().getRadius());
-                    break;
-                default:
-                    break;
+    }
+
+    /**
+     * Recursively traverses the scene graph to find and update light gizmos.
+     * New gizmos are created for new lights, and existing gizmos are updated.
+     *
+     * @param spatial The current spatial to process for lights.
+     */
+    private void updateLightGizmos(Spatial spatial) {
+        // Add or update gizmos for lights attached to the current spatial
+        for (Light light : spatial.getLocalLightList()) {
+            if (!lightFilter.test(light)) {
+                continue;
+            }
+
+            lightDeque.add(light);
+            Spatial gizmo = lightGizmoMap.get(light);
+
+            if (gizmo == null) {
+                gizmo = createLightGizmo(light);
+                if (gizmo != null) {
+                    debugNode.attachChild(gizmo);
+                    lightGizmoMap.put(light, gizmo);
+                    updateGizmoProperties(light, gizmo); // Set initial properties
+                }
+            } else {
+                updateGizmoProperties(light, gizmo);
             }
         }
-        if (scene instanceof Node) {
-            Node n = (Node)scene;
-            for (Spatial spatial : n.getChildren()) {
-                updateLights(spatial);
+
+        // Recursively call for children if it's a Node
+        if (spatial instanceof Node) {
+            Node node = (Node) spatial;
+            for (Spatial child : node.getChildren()) {
+                updateLightGizmos(child);
             }
         }
     }
 
     /**
-     * Set the scenes for which to render light gizmos.
+     * Creates a new gizmo spatial for a given light based on its type.
      *
-     * @param scene the root of the desired scene (alias created)
+     * @param light The light for which to create a gizmo.
+     * @return A spatial representing the gizmo, or null if the light type is not supported.
      */
-    public void setScene(Spatial scene) {
-        this.scene = scene;
+    private Spatial createLightGizmo(Light light) {
+        switch (light.getType()) {
+            case Probe:
+                return createLightProbeGizmo();
+            case Point:
+                return createPointGizmo();
+            case Directional:
+                return createDirectionalGizmo();
+            case Spot:
+                return createSpotGizmo();
+            default:
+                // Unsupported light type
+                return null;
+        }
     }
 
-    private void cleanProbes() {
-        if (probes.size() != probeMapping.size()) {
-            for (LightProbe probe : probeMapping.keySet()) {
-                if (!probes.contains(probe)) {
-                    garbage.add(probe);
+    /**
+     * Updates the visual properties and position of a light gizmo based on its corresponding light.
+     *
+     * @param light The light whose properties are used for updating the gizmo.
+     * @param gizmo The spatial representing the light gizmo.
+     */
+    private void updateGizmoProperties(Light light, Spatial gizmo) {
+        Node lightNode = (Node) gizmo;
+
+        switch (light.getType()) {
+            case Probe:
+                LightProbe probe = (LightProbe) light;
+                Geometry probeGeom = (Geometry) lightNode.getChild(PROBE_GEOMETRY_NAME);
+                Geometry probeBounds = (Geometry) lightNode.getChild(PROBE_BOUNDS_NAME);
+
+                // Update texture if probe is ready
+                if (probe.isReady()) {
+                    Material mat = probeGeom.getMaterial();
+                    if (mat.getTextureParam("CubeMap") == null) {
+                        mat.setTexture("CubeMap", probe.getPrefilteredEnvMap());
+                    }
                 }
-            }
-            for (LightProbe probe : garbage) {
-                probeMapping.remove(probe);
-            }
-            garbage.clear();
-            probes.clear();
+                probeGeom.setLocalScale(lightProbeScale);
+                probeBounds.setLocalScale(probe.getArea().getRadius());
+                gizmo.setLocalTranslation(probe.getPosition());
+                break;
+
+            case Point:
+                PointLight pl = (PointLight) light;
+                Geometry radius = (Geometry) lightNode.getChild(POINT_LIGHT_RADIUS_NAME);
+                radius.setLocalScale(pl.getRadius());
+                gizmo.setLocalTranslation(pl.getPosition());
+                break;
+
+            case Spot:
+                SpotLight sl = (SpotLight) light;
+                gizmo.setLocalTranslation(sl.getPosition());
+
+                tempRotation.lookAt(sl.getDirection(), Vector3f.UNIT_Y);
+                gizmo.setLocalRotation(tempRotation);
+
+                float spotRange = sl.getSpotRange();
+                float innerAngle = sl.getSpotInnerAngle();
+                float outerAngle = sl.getSpotOuterAngle();
+                float innerRadius = spotRange * FastMath.tan(innerAngle);
+                float outerRadius = spotRange * FastMath.tan(outerAngle);
+
+                lightNode.getChild(SPOT_LIGHT_INNER_RADIUS_NAME).setLocalScale(innerRadius);
+                lightNode.getChild(SPOT_LIGHT_OUTER_RADIUS_NAME).setLocalScale(outerRadius);
+                lightNode.getChild(SPOT_LIGHT_RADIUS_NAME).setLocalTranslation(0, 0, spotRange);
+                lightNode.getChild(LIGHT_DIR_ARROW_NAME).setLocalScale(spotRange);
+                break;
+
+            case Directional:
+                DirectionalLight dl = (DirectionalLight) light;
+                tempRotation.lookAt(dl.getDirection(), Vector3f.UNIT_Y);
+                gizmo.setLocalRotation(tempRotation);
+                break;
+
+            default:
+                // Unsupported light type
+                break;
         }
     }
 
-    @Override
-    public void render(RenderManager rm) {
-        if (!isEnabled()) {
-            return;
+    /**
+     * Cleans up gizmos for lights that have been removed from the scene.
+     */
+    private void cleanUpRemovedLights() {
+
+        Iterator<Map.Entry<Light, Spatial>> iterator = lightGizmoMap.entrySet().iterator();
+
+        while (iterator.hasNext()) {
+            Map.Entry<Light, Spatial> entry = iterator.next();
+            Light light = entry.getKey();
+
+            if (!lightDeque.contains(light)) {
+                Spatial gizmo = entry.getValue();
+                gizmo.removeFromParent();
+                iterator.remove();
+            }
         }
-        rm.renderScene(debugNode, getApplication().getViewPort());
+
+        lightDeque.clear();
     }
 
     /**
-     * returns the scale of the probe's debug sphere
-     * @return the scale factor
+     * Sets the scene for which to render light gizmos.
+     * If no scene is set, it defaults to the first scene in the viewport.
+     *
+     * @param scene The root of the desired scene.
+     */
+    public void setScene(Spatial scene) {
+        this.scene = scene;
+        // Clear existing gizmos when the scene changes to avoid displaying gizmos from the old scene
+        debugNode.detachAllChildren();
+        lightGizmoMap.clear();
+        lightDeque.clear();
+    }
+
+    /**
+     * Returns the current scale of the light probe's debug sphere.
+     *
+     * @return The scale factor.
+     */
+    public float getLightProbeScale() {
+        return lightProbeScale;
+    }
+
+    /**
+     * Sets the scale of the light probe's debug sphere.
+     *
+     * @param scale The scale factor (default is 1.0).
+     */
+    public void setLightProbeScale(float scale) {
+        this.lightProbeScale = scale;
+    }
+
+    /**
+     * Checks if the light debug gizmos are set to always
+     * render on top of other scene geometry.
+     *
+     * @return true if gizmos always render on top, false otherwise.
+     */
+    public boolean isShowOnTop() {
+        return showOnTop;
+    }
+
+    /**
+     * Sets whether light debug gizmos should always
+     * render on top of other scene geometry.
+     *
+     * @param showOnTop true to always show gizmos on top, false to respect depth.
      */
-    public float getProbeScale() {
-        return probeScale;
+    public void setShowOnTop(boolean showOnTop) {
+        this.showOnTop = showOnTop;
+        if (viewPort != null) {
+            viewPort.setClearDepth(showOnTop);
+        }
     }
 
     /**
-     * sets the scale of the probe's debug sphere
+     * Sets a filter to control which lights are displayed by the debug state.
+     * By default, no filter is applied, meaning all lights are displayed.
      *
-     * @param probeScale the scale factor (default=1)
+     * @param lightFilter A {@link Predicate} that tests a {@link Light} object.
      */
-    public void setProbeScale(float probeScale) {
-        this.probeScale = probeScale;
+    public void setLightFilter(Predicate<Light> lightFilter) {
+        this.lightFilter = lightFilter;
     }
 
+    /**
+     * Cleans up resources when the app state is detached.
+     *
+     * @param app The application instance.
+     */
     @Override
     protected void cleanup(Application app) {
+        debugNode.detachAllChildren();
+        lightGizmoMap.clear();
+        lightDeque.clear();
+        debugMaterial = null;
+        app.getRenderManager().removeMainView(viewPort);
     }
 
     @Override
     protected void onEnable() {
+        viewPort.attachScene(debugNode);
     }
 
     @Override
     protected void onDisable() {
+        viewPort.detachScene(debugNode);
     }
+
 }

+ 140 - 57
jme3-core/src/main/java/com/jme3/font/BitmapFont.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -31,14 +31,22 @@
  */
 package com.jme3.font;
 
-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.export.Savable;
 import com.jme3.material.Material;
 
 import java.io.IOException;
 
 /**
- * Represents a font within jME that is generated with the AngelCode Bitmap Font Generator
+ * Represents a font loaded from a bitmap font definition
+ * (e.g., generated by <a href="https://libgdx.com/wiki/tools/hiero">AngelCode Bitmap Font Generator</a>).
+ * It manages character sets, font pages (textures), and provides utilities for text measurement and rendering.
+ *
  * @author dhdd
+ * @author Yonghoon
  */
 public class BitmapFont implements Savable {
 
@@ -87,33 +95,30 @@ public class BitmapFont implements Savable {
         Bottom
     }
 
+    // The character set containing definitions for each character (glyph) in the font.
     private BitmapCharacterSet charSet;
+    // An array of materials, where each material corresponds to a font page (texture).
     private Material[] pages;
+    // Indicates whether this font is designed for right-to-left (RTL) text rendering.
     private boolean rightToLeft = false;
     // For cursive bitmap fonts in which letter shape is determined by the adjacent glyphs.
     private GlyphParser glyphParser;
 
     /**
-     * @return true, if this is a right-to-left font, otherwise it will return false.
+     * Creates a new instance of `BitmapFont`.
+     * This constructor is primarily used for deserialization.
      */
-    public boolean isRightToLeft() {
-        return rightToLeft;
+    public BitmapFont() {
     }
 
     /**
-     * Specify if this is a right-to-left font. By default it is set to false.
-     * This can be "overwritten" in the BitmapText constructor.
+     * Creates a new {@link BitmapText} instance initialized with this font.
+     * The label's size will be set to the font's rendered size, and its text content
+     * will be set to the provided string.
      *
-     * @param rightToLeft true &rarr; right-to-left, false &rarr; left-to-right
-     *     (default=false)
+     * @param content The initial text content for the label.
+     * @return A new {@link BitmapText} instance.
      */
-    public void setRightToLeft(boolean rightToLeft) {
-        this.rightToLeft = rightToLeft;
-    }
-
-    public BitmapFont() {
-    }
-
     public BitmapText createLabel(String content) {
         BitmapText label = new BitmapText(this);
         label.setSize(getCharSet().getRenderedSize());
@@ -121,27 +126,81 @@ public class BitmapFont implements Savable {
         return label;
     }
 
+    /**
+     * Checks if this font is configured for right-to-left (RTL) text rendering.
+     *
+     * @return true if this is a right-to-left font, otherwise false (default is left-to-right).
+     */
+    public boolean isRightToLeft() {
+        return rightToLeft;
+    }
+
+    /**
+     * Specifies whether this font should be rendered as right-to-left (RTL).
+     * By default, it is set to false (left-to-right).
+     *
+     * @param rightToLeft true to enable right-to-left rendering; false for left-to-right.
+     */
+    public void setRightToLeft(boolean rightToLeft) {
+        this.rightToLeft = rightToLeft;
+    }
+
+    /**
+     * Returns the preferred size of the font, which is typically its rendered size.
+     *
+     * @return The preferred size of the font in font units.
+     */
     public float getPreferredSize() {
         return getCharSet().getRenderedSize();
     }
 
+    /**
+     * Sets the character set for this font. The character set contains
+     * information about individual glyphs, their positions, and kerning data.
+     *
+     * @param charSet The {@link BitmapCharacterSet} to associate with this font.
+     */
     public void setCharSet(BitmapCharacterSet charSet) {
         this.charSet = charSet;
     }
 
+    /**
+     * Sets the array of materials (font pages) for this font. Each material
+     * corresponds to a texture page containing character bitmaps.
+     * The character set's page size is also updated based on the number of pages.
+     *
+     * @param pages An array of {@link Material} objects representing the font pages.
+     */
     public void setPages(Material[] pages) {
         this.pages = pages;
         charSet.setPageSize(pages.length);
     }
 
+    /**
+     * Retrieves a specific font page material by its index.
+     *
+     * @param index The index of the font page to retrieve.
+     * @return The {@link Material} for the specified font page.
+     * @throws IndexOutOfBoundsException if the index is out of bounds.
+     */
     public Material getPage(int index) {
         return pages[index];
     }
 
+    /**
+     * Returns the total number of font pages (materials) associated with this font.
+     *
+     * @return The number of font pages.
+     */
     public int getPageSize() {
         return pages.length;
     }
 
+    /**
+     * Retrieves the character set associated with this font.
+     *
+     * @return The {@link BitmapCharacterSet} of this font.
+     */
     public BitmapCharacterSet getCharSet() {
         return charSet;
     }
@@ -192,26 +251,19 @@ public class BitmapFont implements Savable {
         return c.getKerning(nextChar);
     }
 
-    @Override
-    public void write(JmeExporter ex) throws IOException {
-        OutputCapsule oc = ex.getCapsule(this);
-        oc.write(charSet, "charSet", null);
-        oc.write(pages, "pages", null);
-        oc.write(rightToLeft, "rightToLeft", false);
-        oc.write(glyphParser, "glyphParser", null);
-    }
-
-    @Override
-    public void read(JmeImporter im) throws IOException {
-        InputCapsule ic = im.getCapsule(this);
-        charSet = (BitmapCharacterSet) ic.readSavable("charSet", null);
-        Savable[] pagesSavable = ic.readSavableArray("pages", null);
-        pages = new Material[pagesSavable.length];
-        System.arraycopy(pagesSavable, 0, pages, 0, pages.length);
-        rightToLeft = ic.readBoolean("rightToLeft", false);
-        glyphParser = (GlyphParser) ic.readSavable("glyphParser", null);
-    }
-
+    /**
+     * Calculates the width of the given text in font units.
+     * This method accounts for character advances, kerning, and line breaks.
+     * It also attempts to skip custom color tags (e.g., "\#RRGGBB#" or "\#RRGGBBAA#")
+     * based on a specific format.
+     * <p>
+     * Note: This method calculates width in "font units" where the font's
+     * {@link BitmapCharacterSet#getRenderedSize() rendered size} is the base.
+     * Actual pixel scaling for display is typically handled by {@link BitmapText}.
+     *
+     * @param text The text to measure.
+     * @return The maximum line width of the text in font units.
+     */
     public float getLineWidth(CharSequence text) {
         // This method will probably always be a bit of a maintenance
         // nightmare since it bases its calculation on a different
@@ -252,29 +304,36 @@ public class BitmapFont implements Savable {
         boolean firstCharOfLine = true;
 //        float sizeScale = (float) block.getSize() / charSet.getRenderedSize();
         float sizeScale = 1f;
-        CharSequence characters = glyphParser != null ? glyphParser.parse(text) : text;
 
-        for (int i = 0; i < characters.length(); i++) {
-            char theChar = characters.charAt(i);
-            if (theChar == '\n') {
+        // Use GlyphParser if available for complex script shaping (e.g., cursive fonts).
+        CharSequence processedText = glyphParser != null ? glyphParser.parse(text) : text;
+
+        for (int i = 0; i < processedText.length(); i++) {
+            char currChar = processedText.charAt(i);
+            if (currChar == '\n') {
                 maxLineWidth = Math.max(maxLineWidth, lineWidth);
                 lineWidth = 0f;
                 firstCharOfLine = true;
                 continue;
             }
-            BitmapCharacter c = charSet.getCharacter(theChar);
+            BitmapCharacter c = charSet.getCharacter(currChar);
             if (c != null) {
-                if (theChar == '\\' && i < characters.length() - 1 && characters.charAt(i + 1) == '#') {
-                    if (i + 5 < characters.length() && characters.charAt(i + 5) == '#') {
+                // Custom color tag skipping logic:
+                // Assumes tags are of the form `\#RRGGBB#` (9 chars total) or `\#RRGGBBAA#` (12 chars total).
+                if (currChar == '\\' && i < processedText.length() - 1 && processedText.charAt(i + 1) == '#') {
+                    // Check for `\#XXXXX#` (6 chars after '\', including final '#')
+                    if (i + 5 < processedText.length() && processedText.charAt(i + 5) == '#') {
                         i += 5;
                         continue;
-                    } else if (i + 8 < characters.length() && characters.charAt(i + 8) == '#') {
+                    }
+                    // Check for `\#XXXXXXXX#` (9 chars after '\', including final '#')
+                    else if (i + 8 < processedText.length() && processedText.charAt(i + 8) == '#') {
                         i += 8;
                         continue;
                     }
                 }
                 if (!firstCharOfLine) {
-                    lineWidth += findKerningAmount(lastChar, theChar) * sizeScale;
+                    lineWidth += findKerningAmount(lastChar, currChar) * sizeScale;
                 } else {
                     if (rightToLeft) {
                         // Ignore offset, so it will be compatible with BitmapText.getLineWidth().
@@ -292,7 +351,7 @@ public class BitmapFont implements Savable {
                 // If this is the last character of a line, then we really should
                 // have only added its width. The advance may include extra spacing
                 // that we don't care about.
-                if (i == characters.length() - 1 || characters.charAt(i + 1) == '\n') {
+                if (i == processedText.length() - 1 || processedText.charAt(i + 1) == '\n') {
                     if (rightToLeft) {
                         // In RTL text we move the letter x0 by its xAdvance, so
                         // we should add it to lineWidth.
@@ -315,30 +374,54 @@ public class BitmapFont implements Savable {
         return Math.max(maxLineWidth, lineWidth);
     }
 
-
     /**
-     * Merge two fonts.
-     * If two font have the same style, merge will fail.
-     * @param newFont Style must be assigned to this.
-     * author: Yonghoon
+     * Merges another {@link BitmapFont} into this one.
+     * This operation combines the character sets and font pages.
+     * If both fonts contain the same style, the merge will fail and throw a RuntimeException.
+     *
+     * @param newFont The {@link BitmapFont} to merge into this one. It must have a style assigned.
      */
     public void merge(BitmapFont newFont) {
         charSet.merge(newFont.charSet);
         final int size1 = this.pages.length;
         final int size2 = newFont.pages.length;
 
-        Material[] tmp = new Material[size1+size2];
+        Material[] tmp = new Material[size1 + size2];
         System.arraycopy(this.pages, 0, tmp, 0, size1);
         System.arraycopy(newFont.pages, 0, tmp, size1, size2);
 
         this.pages = tmp;
-
-//        this.pages = Arrays.copyOf(this.pages, size1+size2);
-//        System.arraycopy(newFont.pages, 0, this.pages, size1, size2);
     }
 
+    /**
+     * Sets the style for the font's character set.
+     * This method is typically used when a font file contains only one style
+     * but needs to be assigned a specific style identifier for merging
+     * with other multi-style fonts.
+     *
+     * @param style The integer style identifier to set.
+     */
     public void setStyle(int style) {
         charSet.setStyle(style);
     }
 
-}
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(charSet, "charSet", null);
+        oc.write(pages, "pages", null);
+        oc.write(rightToLeft, "rightToLeft", false);
+        oc.write(glyphParser, "glyphParser", null);
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule ic = im.getCapsule(this);
+        charSet = (BitmapCharacterSet) ic.readSavable("charSet", null);
+        Savable[] pagesSavable = ic.readSavableArray("pages", null);
+        pages = new Material[pagesSavable.length];
+        System.arraycopy(pagesSavable, 0, pages, 0, pages.length);
+        rightToLeft = ic.readBoolean("rightToLeft", false);
+        glyphParser = (GlyphParser) ic.readSavable("glyphParser", null);
+    }
+}

+ 179 - 98
jme3-core/src/main/java/com/jme3/font/BitmapText.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -38,20 +38,38 @@ import com.jme3.math.ColorRGBA;
 import com.jme3.renderer.RenderManager;
 import com.jme3.scene.Node;
 import com.jme3.util.clone.Cloner;
+
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 /**
+ * `BitmapText` is a spatial node that displays text using a {@link BitmapFont}.
+ * It handles text layout, alignment, wrapping, coloring, and styling based on
+ * the properties set via its methods. The text is rendered as a series of
+ * quads (rectangles) with character textures from the font's pages.
+ *
  * @author YongHoon
  */
 public class BitmapText extends Node {
 
+    // The font used to render this text.
     private BitmapFont font;
+    // Stores the text content and its layout properties (size, box, alignment, etc.).
     private StringBlock block;
+    // A flag indicating whether the text needs to be re-assembled
     private boolean needRefresh = true;
+    // An array of `BitmapTextPage` instances, each corresponding to a font page.
     private BitmapTextPage[] textPages;
+    // Manages the individual letter quads, their positions, colors, and styles.
     private Letters letters;
 
+    /**
+     * Creates a new `BitmapText` instance using the specified font.
+     * The text will be rendered left-to-right by default, unless the font itself
+     * is configured for right-to-left rendering.
+     *
+     * @param font The {@link BitmapFont} to use for rendering the text (not null).
+     */
     public BitmapText(BitmapFont font) {
         this(font, font.isRightToLeft(), false);
     }
@@ -69,6 +87,15 @@ public class BitmapText extends Node {
         this(font, rightToLeft, false);
     }
 
+    /**
+     * Creates a new `BitmapText` instance with the specified font, text direction,
+     * and a flag for array-based rendering.
+     *
+     * @param font The {@link BitmapFont} to use for rendering the text (not null).
+     * @param rightToLeft true for right-to-left text rendering, false for left-to-right.
+     * @param arrayBased If true, the internal text pages will use array-based buffers for rendering.
+     * This might affect performance or compatibility depending on the renderer.
+     */
     public BitmapText(BitmapFont font, boolean rightToLeft, boolean arrayBased) {
         textPages = new BitmapTextPage[font.getPageSize()];
         for (int page = 0; page < textPages.length; page++) {
@@ -84,7 +111,7 @@ public class BitmapText extends Node {
 
     @Override
     public BitmapText clone() {
-        return (BitmapText)super.clone(false);
+        return (BitmapText) super.clone(false);
     }
 
     /**
@@ -114,13 +141,19 @@ public class BitmapText extends Node {
         // so I guess cloning doesn't come up that often.
     }
 
+    /**
+     * Returns the {@link BitmapFont} currently used by this `BitmapText` instance.
+     *
+     * @return The {@link BitmapFont} object.
+     */
     public BitmapFont getFont() {
         return font;
     }
 
     /**
-     * Changes text size
-     * @param size text size
+     * Sets the size of the text. This value scales the font's base character sizes.
+     *
+     * @param size The desired text size (e.g., in world units or pixels).
      */
     public void setSize(float size) {
         block.setSize(size);
@@ -128,13 +161,20 @@ public class BitmapText extends Node {
         letters.invalidate();
     }
 
+    /**
+     * Returns the current size of the text.
+     *
+     * @return The text size.
+     */
     public float getSize() {
         return block.getSize();
     }
 
     /**
+     * Sets the text content to be displayed.
      *
-     * @param text charsequence to change text to
+     * @param text The `CharSequence` (e.g., `String` or `StringBuilder`) to display.
+     * If null, the text will be set to an empty string.
      */
     public void setText(CharSequence text) {
         // note: text.toString() is free if text is already a java.lang.String.
@@ -142,72 +182,50 @@ public class BitmapText extends Node {
     }
 
     /**
+     * Sets the text content to be displayed.
+     * If the new text is the same as the current text, no update occurs.
+     * Otherwise, the internal `StringBlock` and `Letters` objects are updated,
+     * and a refresh is flagged to re-layout the text.
      *
-     * @param text String to change text to
+     * @param text The `String` to display. If null, the text will be set to an empty string.
      */
     public void setText(String text) {
         text = text == null ? "" : text;
-        if (text == block.getText() || block.getText().equals(text)) {
+        if (block.getText().equals(text)) {
             return;
         }
 
-        /*
-        The problem with the below block is that StringBlock carries
-        pretty much all of the text-related state of the BitmapText such
-        as size, text box, alignment, etc.
-
-        I'm not sure why this change was needed and the commit message was
-        not entirely helpful because it purports to fix a problem that I've
-        never encountered.
-
-        If block.setText("") doesn't do the right thing then that's where
-        the fix should go because StringBlock carries too much information to
-        be blown away every time.  -pspeed
-
-        Change was made:
-        http://code.google.com/p/jmonkeyengine/source/detail?spec=svn9389&r=9389
-        Diff:
-        http://code.google.com/p/jmonkeyengine/source/diff?path=/trunk/engine/src/core/com/jme3/font/BitmapText.java&format=side&r=9389&old_path=/trunk/engine/src/core/com/jme3/font/BitmapText.java&old=8843
-
-        // If the text is empty, reset
-        if (text.isEmpty()) {
-            detachAllChildren();
-
-            for (int page = 0; page < textPages.length; page++) {
-                textPages[page] = new BitmapTextPage(font, true, page);
-                attachChild(textPages[page]);
-            }
-
-            block = new StringBlock();
-            letters = new Letters(font, block, letters.getQuad().isRightToLeft());
-        }
-        */
-
         // Update the text content
         block.setText(text);
         letters.setText(text);
-
-        // Flag for refresh
         needRefresh = true;
     }
 
     /**
-     * @return returns text
+     * Returns the current text content displayed by this `BitmapText` instance.
+     *
+     * @return The text content as a `String`.
      */
     public String getText() {
         return block.getText();
     }
 
     /**
-     * @return color of the text
+     * Returns the base color applied to the entire text.
+     * Note: Substring colors set via `setColor(int, int, ColorRGBA)` or
+     * `setColor(String, ColorRGBA)` will override this base color for their respective ranges.
+     *
+     * @return The base {@link ColorRGBA} of the text.
      */
     public ColorRGBA getColor() {
         return letters.getBaseColor();
     }
 
     /**
-     * changes text color. all substring colors are deleted.
-     * @param color new color of text
+     * Sets the base color for the entire text.
+     * This operation will clear any previously set substring colors.
+     *
+     * @param color The new base {@link ColorRGBA} for the text.
      */
     public void setColor(ColorRGBA color) {
         letters.setColor(color);
@@ -216,26 +234,34 @@ public class BitmapText extends Node {
     }
 
     /**
-     *  Sets an overall alpha that will be applied to all
-     *  letters.  If the alpha passed is -1 then alpha reverts
-     *  to default... which will be 1 for anything unspecified
-     *  and color tags will be reset to 1 or their encoded
-     *  alpha.
+     * Sets an overall alpha (transparency) value that will be applied to all
+     * letters in the text.
+     * If the alpha passed is -1, the alpha reverts to its default behavior:
+     * 1.0 for unspecified parts, and the encoded alpha from any color tags.
      *
-     * @param alpha the desired alpha, or -1 to revert to the default
+     * @param alpha The desired alpha value (0.0 for fully transparent, 1.0 for fully opaque),
+     * or -1 to revert to default alpha behavior.
      */
     public void setAlpha(float alpha) {
         letters.setBaseAlpha(alpha);
         needRefresh = true;
     }
 
+    /**
+     * Returns the current base alpha value applied to the text.
+     *
+     * @return The base alpha value, or -1 if default alpha behavior is active.
+     */
     public float getAlpha() {
         return letters.getBaseAlpha();
     }
 
     /**
-     * Define the area where the BitmapText will be rendered.
-     * @param rect position and size box where text is rendered
+     * Defines a rectangular bounding box within which the text will be rendered.
+     * This box is used for text wrapping and alignment.
+     *
+     * @param rect The {@link Rectangle} defining the position (x, y) and size (width, height)
+     * of the text rendering area.
      */
     public void setBox(Rectangle rect) {
         block.setTextBox(rect);
@@ -244,14 +270,19 @@ public class BitmapText extends Node {
     }
 
     /**
-     * @return height of the line
+     * Returns the height of a single line of text, scaled by the current text size.
+     *
+     * @return The calculated line height.
      */
     public float getLineHeight() {
         return font.getLineHeight(block);
     }
 
     /**
-     * @return height of whole text block
+     * Calculates and returns the total height of the entire text block,
+     * considering all lines and the defined text box (if any).
+     *
+     * @return The total height of the text block.
      */
     public float getHeight() {
         if (needRefresh) {
@@ -266,7 +297,9 @@ public class BitmapText extends Node {
     }
 
     /**
-     * @return width of line
+     * Calculates and returns the maximum width of any line in the text block.
+     *
+     * @return The maximum line width of the text.
      */
     public float getLineWidth() {
         if (needRefresh) {
@@ -282,7 +315,9 @@ public class BitmapText extends Node {
     }
 
     /**
-     * @return line count
+     * Returns the number of lines the text currently occupies.
+     *
+     * @return The total number of lines.
      */
     public int getLineCount() {
         if (needRefresh) {
@@ -291,14 +326,21 @@ public class BitmapText extends Node {
         return block.getLineCount();
     }
 
+    /**
+     * Returns the current line wrapping mode set for this text.
+     *
+     * @return The {@link LineWrapMode} enum value.
+     */
     public LineWrapMode getLineWrapMode() {
         return block.getLineWrapMode();
     }
 
     /**
-     * Set horizontal alignment. Applicable only when text bound is set.
+     * Sets the horizontal alignment for the text within its bounding box.
+     * This is only applicable if a text bounding box has been set using {@link #setBox(Rectangle)}.
      *
-     * @param align the desired alignment (such as Align.Left)
+     * @param align The desired horizontal alignment (e.g., {@link Align#Left}, {@link Align#Center}, {@link Align#Right}).
+     * @throws RuntimeException If a bounding box is not set and `align` is not `Align.Left`.
      */
     public void setAlignment(BitmapFont.Align align) {
         if (block.getTextBox() == null && align != Align.Left) {
@@ -310,9 +352,11 @@ public class BitmapText extends Node {
     }
 
     /**
-     * Set vertical alignment. Applicable only when text bound is set.
+     * Sets the vertical alignment for the text within its bounding box.
+     * This is only applicable if a text bounding box has been set using {@link #setBox(Rectangle)}.
      *
-     * @param align the desired alignment (such as Align.Top)
+     * @param align The desired vertical alignment (e.g., {@link VAlign#Top}, {@link VAlign#Center}, {@link VAlign#Bottom}).
+     * @throws RuntimeException If a bounding box is not set and `align` is not `VAlign.Top`.
      */
     public void setVerticalAlignment(BitmapFont.VAlign align) {
         if (block.getTextBox() == null && align != VAlign.Top) {
@@ -323,28 +367,42 @@ public class BitmapText extends Node {
         needRefresh = true;
     }
 
+    /**
+     * Returns the current horizontal alignment set for the text.
+     *
+     * @return The current {@link Align} value.
+     */
     public BitmapFont.Align getAlignment() {
         return block.getAlignment();
     }
 
+    /**
+     * Returns the current vertical alignment set for the text.
+     *
+     * @return The current {@link VAlign} value.
+     */
     public BitmapFont.VAlign getVerticalAlignment() {
         return block.getVerticalAlignment();
     }
 
     /**
-     * Set the font style of substring. If font doesn't contain style, default style is used
-     * @param start start index to set style. inclusive.
-     * @param end   end index to set style. EXCLUSIVE.
-     * @param style the style to apply
+     * Sets the font style for a specific substring of the text.
+     * If the font does not contain the specified style, the default style will be used.
+     *
+     * @param start The starting index of the substring (inclusive).
+     * @param end   The ending index of the substring (exclusive).
+     * @param style The integer style identifier to apply.
      */
     public void setStyle(int start, int end, int style) {
         letters.setStyle(start, end, style);
     }
 
     /**
-     * Set the font style of substring. If font doesn't contain style, default style is applied
-     * @param regexp regular expression
-     * @param style  the style to apply
+     * Sets the font style for all substrings matching a given regular expression.
+     * If the font does not contain the specified style, the default style will be used.
+     *
+     * @param regexp The regular expression string to match against the text.
+     * @param style  The integer style identifier to apply.
      */
     public void setStyle(String regexp, int style) {
         Pattern p = Pattern.compile(regexp);
@@ -355,10 +413,11 @@ public class BitmapText extends Node {
     }
 
     /**
-     * Set the color of substring.
-     * @param start start index to set style. inclusive.
-     * @param end   end index to set style. EXCLUSIVE.
-     * @param color the desired color
+     * Sets the color for a specific substring of the text.
+     *
+     * @param start The starting index of the substring (inclusive).
+     * @param end   The ending index of the substring (exclusive).
+     * @param color The desired {@link ColorRGBA} to apply to the substring.
      */
     public void setColor(int start, int end, ColorRGBA color) {
         letters.setColor(start, end, color);
@@ -367,9 +426,10 @@ public class BitmapText extends Node {
     }
 
     /**
-     * Set the color of substring.
-     * @param regexp regular expression
-     * @param color  the desired color
+     * Sets the color for all substrings matching a given regular expression.
+     *
+     * @param regexp The regular expression string to match against the text.
+     * @param color  The desired {@link ColorRGBA} to apply.
      */
     public void setColor(String regexp, ColorRGBA color) {
         Pattern p = Pattern.compile(regexp);
@@ -382,7 +442,10 @@ public class BitmapText extends Node {
     }
 
     /**
-     * @param tabs tab positions
+     * Sets custom tab stop positions for the text.
+     * Tab characters (`\t`) will align to these specified positions.
+     *
+     * @param tabs An array of float values representing the horizontal tab stop positions.
      */
     public void setTabPosition(float... tabs) {
         block.setTabPosition(tabs);
@@ -391,8 +454,10 @@ public class BitmapText extends Node {
     }
 
     /**
-     * used for the tabs over the last tab position.
-     * @param width tab size
+     * Sets the default width for tabs that extend beyond the last defined tab position.
+     * This value is used if a tab character is encountered after all custom tab stops have been passed.
+     *
+     * @param width The default width for tabs in font units.
      */
     public void setTabWidth(float width) {
         block.setTabWidth(width);
@@ -401,10 +466,10 @@ public class BitmapText extends Node {
     }
 
     /**
-     * for setLineWrapType(LineWrapType.NoWrap),
-     * set the last character when the text exceeds the bound.
+     * When {@link LineWrapMode#NoWrap} is used and the text exceeds the bounding box,
+     * this character will be appended to indicate truncation (e.g., '...').
      *
-     * @param c the character to indicate truncated text
+     * @param c The character to use as the ellipsis.
      */
     public void setEllipsisChar(char c) {
         block.setEllipsisChar(c);
@@ -413,12 +478,18 @@ public class BitmapText extends Node {
     }
 
     /**
-     * Available only when bounding is set. <code>setBox()</code> method call is needed in advance.
-     * true when
-     * @param wrap NoWrap   : Letters over the text bound is not shown. the last character is set to '...'(0x2026)
-     *             Character: Character is split at the end of the line.
-     *             Word     : Word is split at the end of the line.
-     *             Clip     : The text is hard-clipped at the border including showing only a partial letter if it goes beyond the text bound.
+     * Sets the line wrapping mode for the text. This is only applicable when
+     * a text bounding box has been set using {@link #setBox(Rectangle)}.
+     *
+     * @param wrap The desired {@link LineWrapMode}:
+     * <ul>
+     * <li>{@link LineWrapMode#NoWrap}: Letters exceeding the text bound are not shown.
+     * The last visible character might be replaced by an ellipsis character
+     * (set via {@link #setEllipsisChar(char)}).</li>
+     * <li>{@link LineWrapMode#Character}: Text is split at the end of the line, even in the middle of a word.</li>
+     * <li>{@link LineWrapMode#Word}: Words are split at the end of the line.</li>
+     * <li>{@link LineWrapMode#Clip}: The text is hard-clipped at the border, potentially showing only a partial letter.</li>
+     * </ul>
      */
     public void setLineWrapMode(LineWrapMode wrap) {
         if (block.getLineWrapMode() != wrap) {
@@ -436,28 +507,38 @@ public class BitmapText extends Node {
         }
     }
 
+    /**
+     * Assembles the text by generating the quad list (character positions and sizes)
+     * and then populating the vertex buffers of each `BitmapTextPage`.
+     * This method is called internally when `needRefresh` is true.
+     */
     private void assemble() {
-        // first generate quad list
+        // First, generate or update the list of letter quads
+        // based on current text and layout properties.
         letters.update();
-        for (int i = 0; i < textPages.length; i++) {
-            textPages[i].assemble(letters);
+        // Then, for each font page, assemble its mesh data from the generated quads.
+        for (BitmapTextPage textPage : textPages) {
+            textPage.assemble(letters);
         }
         needRefresh = false;
     }
 
+    /**
+     * Renders the `BitmapText` spatial. This method iterates through each
+     * `BitmapTextPage`, sets its texture, and renders it using the provided
+     * `RenderManager`.
+     *
+     * @param rm The `RenderManager` responsible for drawing.
+     * @param color The base color to apply during rendering. Note that colors
+     * set per-substring will override this for those parts.
+     */
     public void render(RenderManager rm, ColorRGBA color) {
         for (BitmapTextPage page : textPages) {
             Material mat = page.getMaterial();
             mat.setTexture("ColorMap", page.getTexture());
-            //ColorRGBA original = getColor(mat, "Color");
-            //mat.setColor("Color", color);
+            // mat.setColor("Color", color); // If the material supports a "Color" parameter
             mat.render(page, rm);
-
-            //if( original == null ) {
-            //    mat.clearParam("Color");
-            //} else {
-            //    mat.setColor("Color", original);
-            //}
         }
     }
+
 }

+ 136 - 123
jme3-core/src/main/java/com/jme3/input/FlyByCamera.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2023 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -44,10 +44,10 @@ import com.jme3.renderer.Camera;
 
 /**
  * A first-person camera controller.
- *
+ * <p>
  * After creation, you (or FlyCamAppState) must register the controller using
  * {@link #registerWithInput(com.jme3.input.InputManager)}.
- *
+ * <p>
  * Controls:
  *  - Move (or, in drag-to-rotate mode, drag) the mouse to rotate the camera
  *  - Mouse wheel for zooming in or out
@@ -57,24 +57,24 @@ import com.jme3.renderer.Camera;
 public class FlyByCamera implements AnalogListener, ActionListener {
 
     private static final String[] mappings = new String[]{
-        CameraInput.FLYCAM_LEFT,
-        CameraInput.FLYCAM_RIGHT,
-        CameraInput.FLYCAM_UP,
-        CameraInput.FLYCAM_DOWN,
+            CameraInput.FLYCAM_LEFT,
+            CameraInput.FLYCAM_RIGHT,
+            CameraInput.FLYCAM_UP,
+            CameraInput.FLYCAM_DOWN,
 
-        CameraInput.FLYCAM_STRAFELEFT,
-        CameraInput.FLYCAM_STRAFERIGHT,
-        CameraInput.FLYCAM_FORWARD,
-        CameraInput.FLYCAM_BACKWARD,
+            CameraInput.FLYCAM_STRAFELEFT,
+            CameraInput.FLYCAM_STRAFERIGHT,
+            CameraInput.FLYCAM_FORWARD,
+            CameraInput.FLYCAM_BACKWARD,
 
-        CameraInput.FLYCAM_ZOOMIN,
-        CameraInput.FLYCAM_ZOOMOUT,
-        CameraInput.FLYCAM_ROTATEDRAG,
+            CameraInput.FLYCAM_ZOOMIN,
+            CameraInput.FLYCAM_ZOOMOUT,
+            CameraInput.FLYCAM_ROTATEDRAG,
 
-        CameraInput.FLYCAM_RISE,
-        CameraInput.FLYCAM_LOWER,
+            CameraInput.FLYCAM_RISE,
+            CameraInput.FLYCAM_LOWER,
 
-        CameraInput.FLYCAM_INVERTY
+            CameraInput.FLYCAM_INVERTY
     };
     /**
      * camera controlled by this controller (not null)
@@ -83,7 +83,7 @@ public class FlyByCamera implements AnalogListener, ActionListener {
     /**
      * normalized "up" direction (a unit vector)
      */
-    protected Vector3f initialUpVec;
+    protected Vector3f initialUpVec = new Vector3f();
     /**
      * rotation-rate multiplier (1=default)
      */
@@ -109,6 +109,15 @@ public class FlyByCamera implements AnalogListener, ActionListener {
     protected boolean invertY = false;
     protected InputManager inputManager;
 
+    // Reusable temporary objects to reduce allocations during updates
+    private final Matrix3f tempMat = new Matrix3f();
+    private final Quaternion tempQuat = new Quaternion();
+    private final Vector3f tempUp = new Vector3f();
+    private final Vector3f tempLeft = new Vector3f();
+    private final Vector3f tempDir = new Vector3f();
+    private final Vector3f tempVel = new Vector3f();
+    private final Vector3f tempPos = new Vector3f();
+
     /**
      * Creates a new FlyByCamera to control the specified camera.
      *
@@ -116,7 +125,7 @@ public class FlyByCamera implements AnalogListener, ActionListener {
      */
     public FlyByCamera(Camera cam) {
         this.cam = cam;
-        initialUpVec = cam.getUp().clone();
+        cam.getUp(initialUpVec);
     }
 
     /**
@@ -128,63 +137,61 @@ public class FlyByCamera implements AnalogListener, ActionListener {
         initialUpVec.set(upVec);
     }
 
-    public void setMotionAllowedListener(MotionAllowedListener listener){
+    public void setMotionAllowedListener(MotionAllowedListener listener) {
         this.motionAllowed = listener;
     }
 
     /**
-     * Set the translation speed.
+     * Sets the translation speed of the camera.
      *
-     * @param moveSpeed new speed (in world units per second)
+     * @param moveSpeed The new translation speed in world units per second. Must be non-negative.
      */
-    public void setMoveSpeed(float moveSpeed){
+    public void setMoveSpeed(float moveSpeed) {
         this.moveSpeed = moveSpeed;
     }
 
     /**
-     * Read the translation speed.
+     * Retrieves the current translation speed of the camera.
      *
-     * @return current speed (in world units per second)
+     * @return The current speed in world units per second.
      */
-    public float getMoveSpeed(){
+    public float getMoveSpeed() {
         return moveSpeed;
     }
 
     /**
-     * Set the rotation-rate multiplier. The bigger the multiplier, the more
-     * rotation for a given movement of the mouse.
+     * Sets the rotation-rate multiplier for mouse input. A higher value
+     * means the camera rotates more for a given mouse movement.
      *
-     * @param rotationSpeed new rate multiplier (1=default)
+     * @param rotationSpeed The new rate multiplier (1.0 is default). Must be non-negative.
      */
-    public void setRotationSpeed(float rotationSpeed){
+    public void setRotationSpeed(float rotationSpeed) {
         this.rotationSpeed = rotationSpeed;
     }
 
     /**
-     * Read the rotation-rate multiplier. The bigger the multiplier, the more
-     * rotation for a given movement of the mouse.
+     * Retrieves the current rotation-rate multiplier.
      *
-     * @return current rate multiplier (1=default)
+     * @return The current rate multiplier.
      */
-    public float getRotationSpeed(){
+    public float getRotationSpeed() {
         return rotationSpeed;
     }
 
     /**
-     * Set the zoom-rate multiplier. The bigger the multiplier, the more zoom
-     * for a given movement of the mouse wheel.
+     * Sets the zoom-rate multiplier for mouse wheel input. A higher value
+     * means the camera zooms more for a given mouse wheel scroll.
      *
-     * @param zoomSpeed new rate multiplier (1=default)
+     * @param zoomSpeed The new rate multiplier (1.0 is default). Must be non-negative.
      */
     public void setZoomSpeed(float zoomSpeed) {
         this.zoomSpeed = zoomSpeed;
     }
 
     /**
-     * Read the zoom-rate multiplier. The bigger the multiplier, the more zoom
-     * for a given movement of the mouse wheel.
+     * Retrieves the current zoom-rate multiplier.
      *
-     * @return current rate multiplier (1=default)
+     * @return The current rate multiplier.
      */
     public float getZoomSpeed() {
         return zoomSpeed;
@@ -196,9 +203,9 @@ public class FlyByCamera implements AnalogListener, ActionListener {
      *
      * @param enable true to enable, false to disable
      */
-    public void setEnabled(boolean enable){
-        if (enabled && !enable){
-            if (inputManager!= null && (!dragToRotate || (dragToRotate && canRotate))){
+    public void setEnabled(boolean enable) {
+        if (enabled && !enable) {
+            if (inputManager != null && (!dragToRotate || (dragToRotate && canRotate))) {
                 inputManager.setCursorVisible(true);
             }
         }
@@ -206,20 +213,19 @@ public class FlyByCamera implements AnalogListener, ActionListener {
     }
 
     /**
-     * Test whether this controller is enabled.
+     * Checks whether this camera controller is currently enabled.
      *
-     * @return true if enabled, otherwise false
+     * @return {@code true} if enabled, {@code false} otherwise.
      * @see #setEnabled(boolean)
      */
-    public boolean isEnabled(){
+    public boolean isEnabled() {
         return enabled;
     }
 
     /**
-     * Test whether drag-to-rotate mode is enabled.
-     *
-     * @return If drag to rotate feature is enabled.
+     * Checks whether drag-to-rotate mode is currently enabled.
      *
+     * @return {@code true} if drag-to-rotate is enabled, {@code false} otherwise.
      * @see #setDragToRotate(boolean)
      */
     public boolean isDragToRotate() {
@@ -245,15 +251,16 @@ public class FlyByCamera implements AnalogListener, ActionListener {
     }
 
     /**
-     * Register this controller to receive input events from the specified input
-     * manager.
+     * Registers this controller to receive input events from the specified
+     * {@link InputManager}. This method sets up all the necessary input mappings
+     * for mouse, keyboard, and joysticks.
      *
-     * @param inputManager (not null, alias created)
+     * @param inputManager The InputManager instance to register with (must not be null).
      */
-    public void registerWithInput(InputManager inputManager){
+    public void registerWithInput(InputManager inputManager) {
         this.inputManager = inputManager;
 
-        // both mouse and button - rotation of cam
+        // Mouse and Keyboard Mappings for Rotation
         inputManager.addMapping(CameraInput.FLYCAM_LEFT, new MouseAxisTrigger(MouseInput.AXIS_X, true),
                 new KeyTrigger(KeyInput.KEY_LEFT));
 
@@ -266,7 +273,7 @@ public class FlyByCamera implements AnalogListener, ActionListener {
         inputManager.addMapping(CameraInput.FLYCAM_DOWN, new MouseAxisTrigger(MouseInput.AXIS_Y, true),
                 new KeyTrigger(KeyInput.KEY_DOWN));
 
-        // mouse only - zoom in/out with wheel, and rotate drag
+        // Mouse Mappings for Zoom and Drag-to-Rotate
         inputManager.addMapping(CameraInput.FLYCAM_ZOOMIN, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
         inputManager.addMapping(CameraInput.FLYCAM_ZOOMOUT, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
         inputManager.addMapping(CameraInput.FLYCAM_ROTATEDRAG, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
@@ -283,13 +290,19 @@ public class FlyByCamera implements AnalogListener, ActionListener {
         inputManager.setCursorVisible(dragToRotate || !isEnabled());
 
         Joystick[] joysticks = inputManager.getJoysticks();
-        if (joysticks != null && joysticks.length > 0){
+        if (joysticks != null && joysticks.length > 0) {
             for (Joystick j : joysticks) {
                 mapJoystick(j);
             }
         }
     }
 
+    /**
+     * Configures joystick input mappings for the camera controller. This method
+     * attempts to map joystick axes and buttons to camera actions.
+     *
+     * @param joystick The {@link Joystick} to map (not null).
+     */
     protected void mapJoystick(Joystick joystick) {
         // Map it differently if there are Z axis
         if (joystick.getAxis(JoystickAxis.Z_ROTATION) != null
@@ -308,7 +321,7 @@ public class FlyByCamera implements AnalogListener, ActionListener {
             // And let the dpad be up and down
             joystick.getPovYAxis().assignAxis(CameraInput.FLYCAM_RISE, CameraInput.FLYCAM_LOWER);
 
-            if( joystick.getButton("Button 8") != null) {
+            if (joystick.getButton("Button 8") != null) {
                 // Let the standard select button be the y invert toggle
                 joystick.getButton("Button 8").assignButton(CameraInput.FLYCAM_INVERTY);
             }
@@ -322,7 +335,7 @@ public class FlyByCamera implements AnalogListener, ActionListener {
     }
 
     /**
-     * Unregister this controller from its input manager.
+     * Unregisters this controller from its currently associated {@link InputManager}.
      */
     public void unregisterInput() {
         if (inputManager == null) {
@@ -338,112 +351,112 @@ public class FlyByCamera implements AnalogListener, ActionListener {
         inputManager.removeListener(this);
         inputManager.setCursorVisible(!dragToRotate);
 
-        Joystick[] joysticks = inputManager.getJoysticks();
-        if (joysticks != null && joysticks.length > 0) {
-            // No way to unassign axis
-        }
+        // Joysticks cannot be "unassigned" in the same way, but mappings are removed with listener.
+        // Joystick-specific mapping might persist but won't trigger this listener.
+        inputManager = null; // Clear reference
     }
 
     /**
-     * Rotate the camera by the specified amount around the specified axis.
+     * Rotates the camera by the specified amount around the given axis.
      *
-     * @param value rotation amount
-     * @param axis direction of rotation (a unit vector)
+     * @param value The amount of rotation.
+     * @param axis  The axis around which to rotate (a unit vector, unaffected).
      */
     protected void rotateCamera(float value, Vector3f axis) {
-        if (dragToRotate) {
-            if (canRotate) {
-//                value = -value;
-            } else {
-                return;
-            }
+        if (dragToRotate && !canRotate) {
+            return; // In drag-to-rotate mode, only rotate if canRotate is true.
         }
 
-        Matrix3f mat = new Matrix3f();
-        mat.fromAngleNormalAxis(rotationSpeed * value, axis);
+        tempMat.fromAngleNormalAxis(rotationSpeed * value, axis);
 
-        Vector3f up = cam.getUp();
-        Vector3f left = cam.getLeft();
-        Vector3f dir = cam.getDirection();
+        // Get current camera axes into temporary vectors
+        cam.getUp(tempUp);
+        cam.getLeft(tempLeft);
+        cam.getDirection(tempDir);
 
-        mat.mult(up, up);
-        mat.mult(left, left);
-        mat.mult(dir, dir);
+        // Apply rotation to the camera's axes
+        tempMat.mult(tempUp, tempUp);
+        tempMat.mult(tempLeft, tempLeft);
+        tempMat.mult(tempDir, tempDir);
 
-        Quaternion q = new Quaternion();
-        q.fromAxes(left, up, dir);
-        q.normalizeLocal();
+        // Set camera axes using a temporary Quaternion
+        tempQuat.fromAxes(tempLeft, tempUp, tempDir);
+        tempQuat.normalizeLocal(); // Ensure quaternion is normalized
 
-        cam.setAxes(q);
+        cam.setAxes(tempQuat);
     }
 
     /**
-     * Zoom the camera by the specified amount.
+     * Zooms the camera by the specified amount. This method handles both
+     * perspective and parallel projections.
      *
-     * @param value zoom amount
+     * @param value The amount to zoom. Positive values typically zoom in, negative out.
      */
     protected void zoomCamera(float value) {
         if (cam.isParallelProjection()) {
             float zoomFactor = 1.0F + value * 0.01F * zoomSpeed;
             if (zoomFactor > 0F) {
-                float left = zoomFactor * cam.getFrustumLeft();
-                float right = zoomFactor * cam.getFrustumRight();
-                float top = zoomFactor * cam.getFrustumTop();
+                float left   = zoomFactor * cam.getFrustumLeft();
+                float right  = zoomFactor * cam.getFrustumRight();
+                float top    = zoomFactor * cam.getFrustumTop();
                 float bottom = zoomFactor * cam.getFrustumBottom();
-
-                float near = cam.getFrustumNear();
-                float far = cam.getFrustumFar();
+                float near   = cam.getFrustumNear();
+                float far    = cam.getFrustumFar();
                 cam.setFrustum(near, far, left, right, top, bottom);
             }
 
         } else { // perspective projection
             float newFov = cam.getFov() + value * 0.1F * zoomSpeed;
-            if (newFov > 0) {
+            // Use a small epsilon to prevent near-zero FoV issues
+            if (newFov > 0.01f) {
                 cam.setFov(newFov);
             }
         }
     }
 
     /**
-     * Translate the camera upward by the specified amount.
+     * Translates the camera vertically (up or down) by the specified amount,
+     * considering the {@code initialUpVec}.
      *
-     * @param value translation amount
+     * @param value The translation amount. Positive values move the camera up, negative down.
      */
     protected void riseCamera(float value) {
-        Vector3f vel = initialUpVec.mult(value * moveSpeed);
-        Vector3f pos = cam.getLocation().clone();
+        tempVel.set(initialUpVec).multLocal(value * moveSpeed);
+        tempPos.set(cam.getLocation());
 
-        if (motionAllowed != null)
-            motionAllowed.checkMotionAllowed(pos, vel);
-        else
-            pos.addLocal(vel);
+        if (motionAllowed != null) {
+            motionAllowed.checkMotionAllowed(tempPos.clone(), tempVel.clone());
+        } else {
+            tempPos.addLocal(tempVel);
+        }
 
-        cam.setLocation(pos);
+        cam.setLocation(tempPos);
     }
 
     /**
-     * Translate the camera left or forward by the specified amount.
+     * Translates the camera left/right or forward/backward by the specified amount.
      *
-     * @param value translation amount
-     * @param sideways true&rarr;left, false&rarr;forward
+     * @param value    The translation amount. Positive values move in the primary
+     *                 direction (right/forward), negative in the opposite.
+     * @param sideways If {@code true}, the camera moves left/right (strafes).
+     *                 If {@code false}, the camera moves forward/backward.
      */
     protected void moveCamera(float value, boolean sideways) {
-        Vector3f vel = new Vector3f();
-        Vector3f pos = cam.getLocation().clone();
-
-        if (sideways){
-            cam.getLeft(vel);
+        if (sideways) {
+            cam.getLeft(tempVel);
         } else {
-            cam.getDirection(vel);
+            cam.getDirection(tempVel);
         }
-        vel.multLocal(value * moveSpeed);
+        tempVel.multLocal(value * moveSpeed);
+        tempPos.set(cam.getLocation());
 
-        if (motionAllowed != null)
-            motionAllowed.checkMotionAllowed(pos, vel);
-        else
-            pos.addLocal(vel);
+        if (motionAllowed != null) {
+            motionAllowed.checkMotionAllowed(tempPos.clone(), tempVel.clone());
+        } else {
+            tempPos.addLocal(tempVel);
+        }
 
-        cam.setLocation(pos);
+        cam.setLocation(tempPos);
     }
 
     /**
@@ -463,9 +476,9 @@ public class FlyByCamera implements AnalogListener, ActionListener {
         } else if (name.equals(CameraInput.FLYCAM_RIGHT)) {
             rotateCamera(-value, initialUpVec);
         } else if (name.equals(CameraInput.FLYCAM_UP)) {
-            rotateCamera(-value * (invertY ? -1 : 1), cam.getLeft());
+            rotateCamera(-value * (invertY ? -1 : 1), cam.getLeft(tempLeft));
         } else if (name.equals(CameraInput.FLYCAM_DOWN)) {
-            rotateCamera(value * (invertY ? -1 : 1), cam.getLeft());
+            rotateCamera(value * (invertY ? -1 : 1), cam.getLeft(tempLeft));
         } else if (name.equals(CameraInput.FLYCAM_FORWARD)) {
             moveCamera(value, false);
         } else if (name.equals(CameraInput.FLYCAM_BACKWARD)) {
@@ -489,20 +502,20 @@ public class FlyByCamera implements AnalogListener, ActionListener {
      * Callback to notify this controller of an action input event.
      *
      * @param name name of the input event
-     * @param value true if the action is "pressed", false otherwise
+     * @param isPressed true if the action is "pressed", false otherwise
      * @param tpf time per frame (in seconds)
      */
     @Override
-    public void onAction(String name, boolean value, float tpf) {
+    public void onAction(String name, boolean isPressed, float tpf) {
         if (!enabled)
             return;
 
         if (name.equals(CameraInput.FLYCAM_ROTATEDRAG) && dragToRotate) {
-            canRotate = value;
-            inputManager.setCursorVisible(!value);
+            canRotate = isPressed;
+            inputManager.setCursorVisible(!isPressed);
         } else if (name.equals(CameraInput.FLYCAM_INVERTY)) {
             // Invert the "up" direction.
-            if (!value) {
+            if (!isPressed) {
                 invertY = !invertY;
             }
         }

+ 16 - 16
jme3-core/src/main/java/com/jme3/input/JoystickButton.java

@@ -38,22 +38,22 @@ package com.jme3.input;
  */
 public interface JoystickButton {
 
-    public final String BUTTON_0 = "0";
-    public final String BUTTON_1 = "1";
-    public final String BUTTON_2 = "2";
-    public final String BUTTON_3 = "3";
-    public final String BUTTON_4 = "4";
-    public final String BUTTON_5 = "5";
-    public final String BUTTON_6 = "6";
-    public final String BUTTON_7 = "7";
-    public final String BUTTON_8 = "8";
-    public final String BUTTON_9 = "9";
-    public final String BUTTON_10 = "10";
-    public final String BUTTON_11 = "11";
-    public final String BUTTON_12 = "12";
-    public final String BUTTON_13 = "13";
-    public final String BUTTON_14 = "14";
-    public final String BUTTON_15 = "15";
+    public static final String BUTTON_0 = "0";
+    public static final String BUTTON_1 = "1";
+    public static final String BUTTON_2 = "2";
+    public static final String BUTTON_3 = "3";
+    public static final String BUTTON_4 = "4";
+    public static final String BUTTON_5 = "5";
+    public static final String BUTTON_6 = "6";
+    public static final String BUTTON_7 = "7";
+    public static final String BUTTON_8 = "8";
+    public static final String BUTTON_9 = "9";
+    public static final String BUTTON_10 = "10";
+    public static final String BUTTON_11 = "11";
+    public static final String BUTTON_12 = "12";
+    public static final String BUTTON_13 = "13";
+    public static final String BUTTON_14 = "14";
+    public static final String BUTTON_15 = "15";
 
     /**
      * Assign the mapping name to receive events from the given button index

+ 10 - 1
jme3-core/src/main/java/com/jme3/light/AmbientLight.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012, 2015 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -83,4 +83,13 @@ public class AmbientLight extends Light {
         return Type.Ambient;
     }
 
+    @Override
+    public String toString() {
+        return getClass().getSimpleName()
+                + "[name=" + name
+                + ", color=" + color
+                + ", enabled=" + enabled
+                + "]";
+    }
+
 }

+ 11 - 6
jme3-core/src/main/java/com/jme3/light/DirectionalLight.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012, 2015-2016 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -132,11 +132,6 @@ public class DirectionalLight extends Light {
         return Type.Directional;
     }
 
-    @Override
-    public String toString() {
-        return getClass().getSimpleName() + "[name=" + name + ", direction=" + direction + ", color=" + color + ", enabled=" + enabled + "]";
-    }
-
     @Override
     public void write(JmeExporter ex) throws IOException {
         super.write(ex);
@@ -157,4 +152,14 @@ public class DirectionalLight extends Light {
         l.direction = direction.clone();
         return l;
     }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName()
+                + "[name=" + name
+                + ", direction=" + direction
+                + ", color=" + color
+                + ", enabled=" + enabled
+                + "]";
+    }
 }

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

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -48,7 +48,7 @@ import java.util.logging.Logger;
 /**
  * A LightProbe is not exactly a light. It holds environment map information used for Image Based Lighting.
  * This is used for indirect lighting in the Physically Based Rendering pipeline.
- *
+ * <p>
  * A light probe has a position in world space. This is the position from where the Environment Map are rendered.
  * There are two environment data structure  held by the LightProbe :
  * - The irradiance spherical harmonics factors (used for indirect diffuse lighting in the PBR pipeline).
@@ -57,9 +57,9 @@ import java.util.logging.Logger;
  * To compute them see
  * {@link com.jme3.environment.LightProbeFactory#makeProbe(com.jme3.environment.EnvironmentCamera, com.jme3.scene.Spatial)}
  * and {@link EnvironmentCamera}.
- *
+ * <p>
  * The light probe has an area of effect centered on its position. It can have a Spherical area or an Oriented Box area
- *
+ * <p>
  * A LightProbe will only be taken into account when it's marked as ready and enabled.
  * A light probe is ready when it has valid environment map data set.
  * Note that you should never call setReady yourself.
@@ -71,7 +71,8 @@ import java.util.logging.Logger;
 public class LightProbe extends Light implements Savable {
 
     private static final Logger logger = Logger.getLogger(LightProbe.class.getName());
-    public static final Matrix4f FALLBACK_MATRIX = new Matrix4f(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1);
+    public static final Matrix4f FALLBACK_MATRIX = new Matrix4f(
+            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1);
 
     private Vector3f[] shCoefficients;
     private TextureCubeMap prefilteredEnvMap;
@@ -149,16 +150,12 @@ public class LightProbe extends Light implements Savable {
      * @return the pre-existing matrix
      */
     public Matrix4f getUniformMatrix(){
-
         Matrix4f mat = area.getUniformMatrix();
-
         // setting the (sp) entry of the matrix
         mat.m33 = nbMipMaps + 1f / area.getRadius();
-
         return mat;
     }
 
-
     @Override
     public void write(JmeExporter ex) throws IOException {
         super.write(ex);
@@ -180,7 +177,7 @@ public class LightProbe extends Light implements Savable {
         position = (Vector3f) ic.readSavable("position", null);
         area = (ProbeArea)ic.readSavable("area", null);
         if(area == null) {
-            // retro compat
+            // retro compatibility
             BoundingSphere bounds = (BoundingSphere) ic.readSavable("bounds", new BoundingSphere(1.0f, Vector3f.ZERO));
             area = new SphereProbeArea(bounds.getCenter(), bounds.getRadius());
         }
@@ -200,7 +197,6 @@ public class LightProbe extends Light implements Savable {
         }
     }
 
-
     /**
      * returns the bounding volume of this LightProbe
      * @return a bounding volume.
@@ -318,8 +314,12 @@ public class LightProbe extends Light implements Savable {
 
     @Override
     public String toString() {
-        return "Light Probe : " + name + " at " + position + " / " + area;
+        return getClass().getSimpleName()
+                + "[name=" + name
+                + ", position=" + position
+                + ", area=" + area
+                + ", enabled=" + enabled
+                + "]";
     }
 
-
 }

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

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012, 2015-2016, 2018 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -247,4 +247,15 @@ public class PointLight extends Light {
         p.position = position.clone();
         return p;
     }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName()
+                + "[name=" + name
+                + ", position=" + position
+                + ", radius=" + radius
+                + ", color=" + color
+                + ", enabled=" + enabled
+                + "]";
+    }
 }

+ 16 - 5
jme3-core/src/main/java/com/jme3/light/SpotLight.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -118,8 +118,7 @@ public class SpotLight extends Light {
         setPosition(position);
         setDirection(direction);
     }
-    
-    
+
     /**
      * Creates a SpotLight at the given position, with the given direction,
      * the given range and the given color.
@@ -160,7 +159,6 @@ public class SpotLight extends Light {
         setDirection(direction);
         setSpotRange(range);
     }  
-    
 
     private void computeAngleParameters() {
         float innerCos = FastMath.cos(spotInnerAngle);
@@ -458,5 +456,18 @@ public class SpotLight extends Light {
         s.position = position.clone();
         return s;
     }
-}
 
+    @Override
+    public String toString() {
+        return getClass().getSimpleName()
+                + "[name=" + name
+                + ", direction=" + direction
+                + ", position=" + position
+                + ", range=" + spotRange
+                + ", innerAngle=" + spotInnerAngle
+                + ", outerAngle=" + spotOuterAngle
+                + ", color=" + color
+                + ", enabled=" + enabled
+                + "]";
+    }
+}

+ 9 - 5
jme3-core/src/main/java/com/jme3/material/Materials.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -38,13 +38,17 @@ package com.jme3.material;
  */
 public class Materials {
 
-    public static final String UNSHADED = "Common/MatDefs/Misc/Unshaded.j3md";
-    public static final String LIGHTING = "Common/MatDefs/Light/Lighting.j3md";
-    public static final String PBR = "Common/MatDefs/Light/PBRLighting.j3md";
+    public static final String SHOW_NORMALS = "Common/MatDefs/Misc/ShowNormals.j3md";
+    public static final String UNSHADED     = "Common/MatDefs/Misc/Unshaded.j3md";
+    public static final String LIGHTING     = "Common/MatDefs/Light/Lighting.j3md";
+    public static final String PBR          = "Common/MatDefs/Light/PBRLighting.j3md";
+    public static final String PARTICLE     = "Common/MatDefs/Misc/Particle.j3md";
+    public static final String BILLBOARD    = "Common/MatDefs/Misc/Billboard.j3md";
+    public static final String GUI          = "Common/MatDefs/Gui/Gui.j3md";
 
     /**
      * A private constructor to inhibit instantiation of this class.
      */
     private Materials() {
     }
-}
+}

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

@@ -1629,9 +1629,9 @@ public class RenderState implements Cloneable, Savable {
             state.backStencilFunction = additionalState.backStencilFunction;
 
             state.frontStencilMask = additionalState.frontStencilMask;
-            state.frontStencilReference = additionalState.frontStencilMask;
+            state.frontStencilReference = additionalState.frontStencilReference;
             state.backStencilMask = additionalState.backStencilMask;
-            state.backStencilReference = additionalState.backStencilMask;
+            state.backStencilReference = additionalState.backStencilReference;
         } else {
             state.stencilTest = stencilTest;
 
@@ -1647,9 +1647,9 @@ public class RenderState implements Cloneable, Savable {
             state.backStencilFunction = backStencilFunction;
 
             state.frontStencilMask = frontStencilMask;
-            state.frontStencilReference = frontStencilMask;
+            state.frontStencilReference = frontStencilReference;
             state.backStencilMask = backStencilMask;
-            state.backStencilReference = backStencilMask;
+            state.backStencilReference = backStencilReference;
         }
         if (additionalState.applyLineWidth) {
             state.lineWidth = additionalState.lineWidth;

+ 109 - 104
jme3-core/src/main/java/com/jme3/math/ColorRGBA.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -30,18 +30,23 @@
  */
 package com.jme3.math;
 
-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.export.Savable;
+
 import java.io.IOException;
 
 /**
  * <code>ColorRGBA</code> defines a color made from a collection of red, green
- * and blue values stored in Linear color space. An alpha value determines is
+ * and blue values stored in Linear color space. An alpha value determines its
  * transparency.
  *
  * @author Mark Powell
- * @version $Id: ColorRGBA.java,v 1.29 2007/09/09 18:25:14 irrisor Exp $
  */
 public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable {
+
     static final float GAMMA = 2.2f;
 
     static final long serialVersionUID = 1;
@@ -153,15 +158,15 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
      * Copy constructor creates a new <code>ColorRGBA</code> object, based on
      * a provided color.
      *
-     * @param rgba The <code>ColorRGBA</code> object to copy.
+     * @param color The <code>ColorRGBA</code> object to copy.
      */
-    public ColorRGBA(ColorRGBA rgba) {
-        this.a = rgba.a;
-        this.r = rgba.r;
-        this.g = rgba.g;
-        this.b = rgba.b;
+    public ColorRGBA(ColorRGBA color) {
+        this.a = color.a;
+        this.r = color.r;
+        this.g = color.g;
+        this.b = color.b;
     }
-    
+
     /**
      * Constructor creates a new <code>ColorRGBA</code> object, based on
      * a provided Vector4f.
@@ -170,12 +175,9 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
      * values copied to this color's r, g, b, and a values respectively.
      */
     public ColorRGBA(Vector4f vec4) {
-        this.a = vec4.w;
-        this.r = vec4.x;
-        this.g = vec4.y;
-        this.b = vec4.z;
-    }    
-    
+        set(vec4);
+    }
+
     /**
      * Constructor creates a new <code>ColorRGBA</code> object, based on
      * a provided Vector3f, at full opacity with a 1.0 alpha value by default
@@ -185,10 +187,8 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
      */
     public ColorRGBA(Vector3f vec3) {
         this.a = 1.0f;
-        this.r = vec3.x;
-        this.g = vec3.y;
-        this.b = vec3.z;
-    }    
+        set(vec3);
+    }
 
     /**
      * <code>set</code> sets the RGBA values of this <code>ColorRGBA</code>.
@@ -214,24 +214,24 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
      * <code>set</code> sets the values of this <code>ColorRGBA</code> to those
      * set by a parameter color.
      *
-     * @param rgba The color to set this <code>ColorRGBA</code> to.
+     * @param color The color to set this <code>ColorRGBA</code> to.
      * @return this
      */
-    public ColorRGBA set(ColorRGBA rgba) {
-        if (rgba == null) {
+    public ColorRGBA set(ColorRGBA color) {
+        if (color == null) {
             r = 0;
             g = 0;
             b = 0;
             a = 0;
         } else {
-            r = rgba.r;
-            g = rgba.g;
-            b = rgba.b;
-            a = rgba.a;
+            r = color.r;
+            g = color.g;
+            b = color.b;
+            a = color.a;
         }
         return this;
     }
-    
+
     /**
      * <code>set</code> sets the values of this <code>ColorRGBA</code> to those
      * set by a parameter Vector4f.
@@ -254,8 +254,8 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
             a = vec4.w;
         }
         return this;
-    }    
-    
+    }
+
     /**
      * <code>set</code> sets the values of this <code>ColorRGBA</code> to those
      * set by a parameter Vector3f.
@@ -276,7 +276,7 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
             b = vec3.z;
         }
         return this;
-    }       
+    }
 
     /**
      * Sets the red color to the specified value.
@@ -336,22 +336,18 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
      * @return The <code>float</code> array that contains the color components.
      */
     public float[] getColorArray() {
-        return new float[]{r, g, b, a};
+        return getColorArray(null);
     }
 
     /**
      * Stores the current r,g,b,a values into the given array.  The given array must have a
      * length of 4 or greater, or an array index out of bounds exception will be thrown.
      *
-     * @param store The <code>float</code> array to store the values into.
+     * @param store The <code>float</code> array to store the values into. If null, a new array is created.
      * @return The <code>float</code> array after storage.
      */
     public float[] getColorArray(float[] store) {
-        store[0] = r;
-        store[1] = g;
-        store[2] = b;
-        store[3] = a;
-        return store;
+        return toArray(store);
     }
 
     /**
@@ -401,11 +397,7 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
      * @return this ColorRGBA
      */
     public ColorRGBA interpolateLocal(ColorRGBA finalColor, float changeAmount) {
-        this.r = (1 - changeAmount) * this.r + changeAmount * finalColor.r;
-        this.g = (1 - changeAmount) * this.g + changeAmount * finalColor.g;
-        this.b = (1 - changeAmount) * this.b + changeAmount * finalColor.b;
-        this.a = (1 - changeAmount) * this.a + changeAmount * finalColor.a;
-        return this;
+        return interpolateLocal(this, finalColor, changeAmount);
     }
 
     /**
@@ -416,7 +408,7 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
      * @param beginColor The beginning color (changeAmount=0).
      * @param finalColor The final color to interpolate towards (changeAmount=1).
      * @param changeAmount An amount between 0.0 - 1.0 representing a percentage
-     *  change from beginColor towards finalColor.
+     * change from beginColor towards finalColor.
      * @return this ColorRGBA
      */
     public ColorRGBA interpolateLocal(ColorRGBA beginColor, ColorRGBA finalColor, float changeAmount) {
@@ -434,11 +426,11 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
      * @return a random <code>ColorRGBA</code> with an alpha set to 1.
      */
     public static ColorRGBA randomColor() {
-        ColorRGBA rVal = new ColorRGBA(0, 0, 0, 1);
-        rVal.r = FastMath.nextRandomFloat();
-        rVal.g = FastMath.nextRandomFloat();
-        rVal.b = FastMath.nextRandomFloat();
-        return rVal;
+        float r = FastMath.nextRandomFloat();
+        float g = FastMath.nextRandomFloat();
+        float b = FastMath.nextRandomFloat();
+        float a = 1.0f;
+        return new ColorRGBA(r, g, b, a);
     }
 
     /**
@@ -535,19 +527,19 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
     /**
      * Saves this <code>ColorRGBA</code> into the given <code>float</code> array.
      *
-     * @param floats The <code>float</code> array to take this <code>ColorRGBA</code>.
+     * @param store The <code>float</code> array to take this <code>ColorRGBA</code>.
      * If null, a new <code>float[4]</code> is created.
      * @return The array, with r,g,b,a float values in that order.
      */
-    public float[] toArray(float[] floats) {
-        if (floats == null) {
-            floats = new float[4];
+    public float[] toArray(float[] store) {
+        if (store == null) {
+            store = new float[4];
         }
-        floats[0] = r;
-        floats[1] = g;
-        floats[2] = b;
-        floats[3] = a;
-        return floats;
+        store[0] = r;
+        store[1] = g;
+        store[2] = b;
+        store[3] = a;
+        return store;
     }
 
     /**
@@ -605,32 +597,32 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
      * Serialize this color to the specified exporter, for example when
      * saving to a J3O file.
      *
-     * @param e (not null)
+     * @param ex (not null)
      * @throws IOException from the exporter
      */
     @Override
-    public void write(JmeExporter e) throws IOException {
-        OutputCapsule capsule = e.getCapsule(this);
-        capsule.write(r, "r", 0);
-        capsule.write(g, "g", 0);
-        capsule.write(b, "b", 0);
-        capsule.write(a, "a", 0);
+    public void write(JmeExporter ex) throws IOException {
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(r, "r", 0);
+        oc.write(g, "g", 0);
+        oc.write(b, "b", 0);
+        oc.write(a, "a", 0);
     }
 
     /**
      * De-serialize this color from the specified importer, for example when
      * loading from a J3O file.
      *
-     * @param importer (not null)
+     * @param im (not null)
      * @throws IOException from the importer
      */
     @Override
-    public void read(JmeImporter importer) throws IOException {
-        InputCapsule capsule = importer.getCapsule(this);
-        r = capsule.readFloat("r", 0);
-        g = capsule.readFloat("g", 0);
-        b = capsule.readFloat("b", 0);
-        a = capsule.readFloat("a", 0);
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule ic = im.getCapsule(this);
+        r = ic.readFloat("r", 0);
+        g = ic.readFloat("g", 0);
+        b = ic.readFloat("b", 0);
+        a = ic.readFloat("a", 0);
     }
 
     /**
@@ -641,10 +633,10 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
      */
     public byte[] asBytesRGBA() {
         byte[] store = new byte[4];
-        store[0] = (byte) ((int) (r * 255) & 0xFF);
-        store[1] = (byte) ((int) (g * 255) & 0xFF);
-        store[2] = (byte) ((int) (b * 255) & 0xFF);
-        store[3] = (byte) ((int) (a * 255) & 0xFF);
+        store[0] = toByte(r);
+        store[1] = toByte(g);
+        store[2] = toByte(b);
+        store[3] = toByte(a);
         return store;
     }
 
@@ -656,11 +648,7 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
      * @return The integer representation of this <code>ColorRGBA</code> in a,r,g,b order.
      */
     public int asIntARGB() {
-        int argb = (((int) (a * 255) & 0xFF) << 24)
-                | (((int) (r * 255) & 0xFF) << 16)
-                | (((int) (g * 255) & 0xFF) << 8)
-                | (((int) (b * 255) & 0xFF));
-        return argb;
+        return toInt(a, r, g, b);
     }
 
     /**
@@ -671,11 +659,7 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
      * @return The integer representation of this <code>ColorRGBA</code> in r,g,b,a order.
      */
     public int asIntRGBA() {
-        int rgba = (((int) (r * 255) & 0xFF) << 24)
-                | (((int) (g * 255) & 0xFF) << 16)
-                | (((int) (b * 255) & 0xFF) << 8)
-                | (((int) (a * 255) & 0xFF));
-        return rgba;
+        return toInt(r, g, b, a);
     }
 
     /**
@@ -686,11 +670,7 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
      * @return The integer representation of this <code>ColorRGBA</code> in a,b,g,r order.
      */
     public int asIntABGR() {
-        int abgr = (((int) (a * 255) & 0xFF) << 24)
-                | (((int) (b * 255) & 0xFF) << 16)
-                | (((int) (g * 255) & 0xFF) << 8)
-                | (((int) (r * 255) & 0xFF));
-        return abgr;
+        return toInt(a, b, g, r);
     }
 
     /**
@@ -702,10 +682,10 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
      * @return this
      */
     public ColorRGBA fromIntARGB(int color) {
-        a = ((byte) (color >> 24) & 0xFF) / 255f;
-        r = ((byte) (color >> 16) & 0xFF) / 255f;
-        g = ((byte) (color >> 8) & 0xFF) / 255f;
-        b = ((byte) (color) & 0xFF) / 255f;
+        a = fromByte(color >> 24);
+        r = fromByte(color >> 16);
+        g = fromByte(color >> 8);
+        b = fromByte(color);
         return this;
     }
 
@@ -717,10 +697,10 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
      * @return this
      */
     public ColorRGBA fromIntRGBA(int color) {
-        r = ((byte) (color >> 24) & 0xFF) / 255f;
-        g = ((byte) (color >> 16) & 0xFF) / 255f;
-        b = ((byte) (color >> 8) & 0xFF) / 255f;
-        a = ((byte) (color) & 0xFF) / 255f;
+        r = fromByte(color >> 24);
+        g = fromByte(color >> 16);
+        b = fromByte(color >> 8);
+        a = fromByte(color);
         return this;
     }
 
@@ -732,10 +712,10 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
      * @return this
      */
     public ColorRGBA fromIntABGR(int color) {
-        a = ((byte) (color >> 24) & 0xFF) / 255f;
-        b = ((byte) (color >> 16) & 0xFF) / 255f;
-        g = ((byte) (color >> 8) & 0xFF) / 255f;
-        r = ((byte) (color) & 0xFF) / 255f;
+        a = fromByte(color >> 24);
+        b = fromByte(color >> 16);
+        g = fromByte(color >> 8);
+        r = fromByte(color);
         return this;
     }
 
@@ -825,4 +805,29 @@ public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable
         srgb.a = a;
         return srgb;
     }
+
+    /**
+     * Helper method to convert a float (0-1) to a byte (0-255).
+     */
+    private byte toByte(float channel) {
+        return (byte) ((int) (channel * 255) & 0xFF);
+    }
+
+    /**
+     * Helper method to convert an int (shifted byte) to a float (0-1).
+     */
+    private float fromByte(int channelByte) {
+        return ((byte) (channelByte) & 0xFF) / 255f;
+    }
+
+    /**
+     * Helper method to combine four float channels into an int.
+     */
+    private int toInt(float c1, float c2, float c3, float c4) {
+        int r = ((int) (c1 * 255) & 0xFF);
+        int g = ((int) (c2 * 255) & 0xFF);
+        int b = ((int) (c3 * 255) & 0xFF);
+        int a = ((int) (c4 * 255) & 0xFF);
+        return (r << 24) | (g << 16) | (b << 8) | a;
+    }
 }

+ 249 - 119
jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2022 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -32,18 +32,28 @@
 package com.jme3.post;
 
 import com.jme3.asset.AssetManager;
-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.export.Savable;
 import com.jme3.material.Material;
-import com.jme3.profile.*;
-import com.jme3.renderer.*;
+import com.jme3.profile.AppProfiler;
+import com.jme3.profile.SpStep;
+import com.jme3.renderer.Camera;
+import com.jme3.renderer.Caps;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.Renderer;
+import com.jme3.renderer.ViewPort;
 import com.jme3.renderer.queue.RenderQueue;
 import com.jme3.texture.FrameBuffer;
+import com.jme3.texture.FrameBuffer.FrameBufferTarget;
 import com.jme3.texture.Image.Format;
 import com.jme3.texture.Texture;
 import com.jme3.texture.Texture2D;
-import com.jme3.texture.FrameBuffer.FrameBufferTarget;
 import com.jme3.ui.Picture;
 import com.jme3.util.SafeArrayList;
+
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -52,13 +62,22 @@ import java.util.Iterator;
 import java.util.List;
 
 /**
- * A FilterPostProcessor is a processor that can apply several {@link Filter}s to a rendered scene<br>
- * It manages a list of filters that will be applied in the order in which they've been added to the list
- * @author Rémy Bouquet aka Nehon
+ * A `FilterPostProcessor` is a {@link SceneProcessor} that can apply several
+ * {@link Filter}s to a rendered scene. It manages a list of filters that will be
+ * applied in the order in which they have been added. This processor handles
+ * rendering the main scene to an offscreen framebuffer, then applying each enabled
+ * filter sequentially, optionally with anti-aliasing (multisampling) and depth texture
+ * support.
+ *
+ * @author Nehon
  */
 public class FilterPostProcessor implements SceneProcessor, Savable {
 
+    /**
+     * The simple name of this class, used for profiling.
+     */
     public static final String FPP = FilterPostProcessor.class.getSimpleName();
+
     private RenderManager renderManager;
     private Renderer renderer;
     private ViewPort viewPort;
@@ -89,23 +108,28 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
     private Format depthFormat = Format.Depth;
 
     /**
-     * Create a FilterProcessor
-     * @param assetManager the assetManager
+     * Creates a new `FilterPostProcessor`.
+     *
+     * @param assetManager The asset manager to be used by filters for loading assets.
      */
     public FilterPostProcessor(AssetManager assetManager) {
         this.assetManager = assetManager;
     }
 
     /**
-     * Don't use this constructor, use {@link #FilterPostProcessor(AssetManager assetManager)}<br>
-     * This constructor is used for serialization only
+     * Serialization-only constructor. Do not use this constructor directly;
+     * use {@link #FilterPostProcessor(AssetManager)}.
      */
     protected FilterPostProcessor() {
     }
 
     /**
-     * Adds a filter to the filters list<br>
-     * @param filter the filter to add
+     * Adds a filter to the list of filters to be applied. Filters are applied
+     * in the order they are added. If the processor is already initialized,
+     * the filter is immediately initialized as well.
+     *
+     * @param filter The filter to add (not null).
+     * @throws IllegalArgumentException If the provided filter is null.
      */
     public void addFilter(Filter filter) {
         if (filter == null) {
@@ -118,13 +142,14 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
         }
 
         setFilterState(filter, filter.isEnabled());
-
     }
 
     /**
-     * removes this filters from the filters list
+     * Removes a specific filter from the list. The filter's `cleanup` method
+     * is called upon removal.
      *
-     * @param filter the Filter to remove (not null)
+     * @param filter The filter to remove (not null).
+     * @throws IllegalArgumentException If the provided filter is null.
      */
     public void removeFilter(Filter filter) {
         if (filter == null) {
@@ -135,10 +160,22 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
         updateLastFilterIndex();
     }
 
+    /**
+     * Returns an iterator over the filters currently managed by this processor.
+     *
+     * @return An `Iterator` of {@link Filter} objects.
+     */
     public Iterator<Filter> getFilterIterator() {
         return filters.iterator();
     }
 
+    /**
+     * Initializes the `FilterPostProcessor`. This method is called by the
+     * `RenderManager` when the processor is added to a viewport.
+     *
+     * @param rm The `RenderManager` instance.
+     * @param vp The `ViewPort` this processor is attached to.
+     */
     @Override
     public void initialize(RenderManager rm, ViewPort vp) {
         renderManager = rm;
@@ -148,10 +185,11 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
         fsQuad.setWidth(1);
         fsQuad.setHeight(1);
 
+        // Determine optimal framebuffer format based on renderer capabilities
         if (!renderer.getCaps().contains(Caps.PackedFloatTexture)) {
-            if(renderer.getCaps().contains(Caps.FloatColorBufferRGB)){
+            if (renderer.getCaps().contains(Caps.FloatColorBufferRGB)) {
                 fbFormat = Format.RGB16F;
-            } else if(renderer.getCaps().contains(Caps.FloatColorBufferRGBA)){
+            } else if (renderer.getCaps().contains(Caps.FloatColorBufferRGBA)) {
                 fbFormat = Format.RGBA16F;
             } else {
                 fbFormat = Format.RGB8;
@@ -160,34 +198,47 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
 
         Camera cam = vp.getCamera();
 
-        //save view port dimensions
+        // Save original viewport dimensions
         left = cam.getViewPortLeft();
         right = cam.getViewPortRight();
         top = cam.getViewPortTop();
         bottom = cam.getViewPortBottom();
         originalWidth = cam.getWidth();
         originalHeight = cam.getHeight();
-        //first call to reshape
+
+        // First call to reshape to set up internal framebuffers and textures
         reshape(vp, cam.getWidth(), cam.getHeight());
     }
 
+    /**
+     * Returns the default color buffer format used for the internal rendering
+     * passes of the filters. This format is determined during initialization
+     * based on the renderer's capabilities.
+     *
+     * @return The default `Format` for the filter pass textures.
+     */
     public Format getDefaultPassTextureFormat() {
         return fbFormat;
     }
 
     /**
-     * init the given filter
-     * @param filter
-     * @param vp
+     * Initializes a single filter. This method is called when a filter is added
+     * or when the post-processor is initialized/reshaped. It sets the processor
+     * for the filter, handles depth texture requirements, and calls the filter's
+     * `init` method.
+     *
+     * @param filter The {@link Filter} to initialize.
+     * @param vp The `ViewPort` associated with this processor.
      */
     private void initFilter(Filter filter, ViewPort vp) {
         filter.setProcessor(this);
         if (filter.isRequiresDepthTexture()) {
             if (!computeDepth && renderFrameBuffer != null) {
+                // If depth texture is required and not yet created, create it
                 depthTexture = new Texture2D(width, height, depthFormat);
                 renderFrameBuffer.setDepthTarget(FrameBufferTarget.newTarget(depthTexture));
             }
-            computeDepth = true;
+            computeDepth = true; // Mark that depth texture is needed
             filter.init(assetManager, renderManager, vp, width, height);
             filter.setDepthTexture(depthTexture);
         } else {
@@ -196,45 +247,52 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
     }
 
     /**
-     * renders a filter on a fullscreen quad
-     * @param r
-     * @param buff
-     * @param mat
+     * Renders a filter's material onto a full-screen quad. This method
+     * handles setting up the rendering context (framebuffer, camera, material)
+     * for a filter pass. It correctly resizes the camera and adjusts material
+     * states based on whether the target buffer is the final output buffer or an
+     * intermediate filter buffer.
+     *
+     * @param r The `Renderer` instance.
+     * @param buff The `FrameBuffer` to render to.
+     * @param mat The `Material` to use for rendering the filter.
      */
     private void renderProcessing(Renderer r, FrameBuffer buff, Material mat) {
+        // Adjust camera and viewport based on target framebuffer
         if (buff == outputBuffer) {
             viewPort.getCamera().resize(originalWidth, originalHeight, false);
             viewPort.getCamera().setViewPort(left, right, bottom, top);
-            // update is redundant because resize and setViewPort will both
-            // run the appropriate (and same) onXXXChange methods.
-            // Also, update() updates some things that don't need to be updated.
-            //viewPort.getCamera().update();
-            renderManager.setCamera( viewPort.getCamera(), false);
+            // viewPort.getCamera().update(); // Redundant as resize and setViewPort call onXXXChange
+            renderManager.setCamera(viewPort.getCamera(), false);
+            // Disable depth test/write for final pass to prevent artifacts
             if (mat.getAdditionalRenderState().isDepthWrite()) {
                 mat.getAdditionalRenderState().setDepthTest(false);
                 mat.getAdditionalRenderState().setDepthWrite(false);
             }
         } else {
+            // Rendering to an intermediate framebuffer for a filter pass
             viewPort.getCamera().resize(buff.getWidth(), buff.getHeight(), false);
             viewPort.getCamera().setViewPort(0, 1, 0, 1);
-            // update is redundant because resize and setViewPort will both
-            // run the appropriate (and same) onXXXChange methods.
-            // Also, update() updates some things that don't need to be updated.
-            //viewPort.getCamera().update();
-            renderManager.setCamera( viewPort.getCamera(), false);
+            // viewPort.getCamera().update(); // Redundant as resize and setViewPort call onXXXChange
+            renderManager.setCamera(viewPort.getCamera(), false);
+            // Enable depth test/write for intermediate passes if material needs it
             mat.getAdditionalRenderState().setDepthTest(true);
             mat.getAdditionalRenderState().setDepthWrite(true);
         }
 
-
         fsQuad.setMaterial(mat);
         fsQuad.updateGeometricState();
 
         r.setFrameBuffer(buff);
-        r.clearBuffers(true, true, true);
+        r.clearBuffers(true, true, true); // Clear color, depth, and stencil buffers
         renderManager.renderGeometry(fsQuad);
     }
 
+    /**
+     * Checks if the `FilterPostProcessor` has been initialized.
+     *
+     * @return True if initialized, false otherwise.
+     */
     @Override
     public boolean isInitialized() {
         return viewPort != null;
@@ -244,30 +302,44 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
     public void postQueue(RenderQueue rq) {
         for (Filter filter : filters.getArray()) {
             if (filter.isEnabled()) {
-                if (prof != null) prof.spStep(SpStep.ProcPostQueue, FPP, filter.getName());
+                if (prof != null) {
+                    prof.spStep(SpStep.ProcPostQueue, FPP, filter.getName());
+                }
                 filter.postQueue(rq);
             }
         }
     }
 
     /**
-     * iterate through the filter list and renders filters
-     * @param r
-     * @param sceneFb
+     * Renders the chain of filters. This method is the core of the post-processing.
+     * It iterates through each enabled filter, handling pre-filter passes,
+     * setting up textures (scene, depth), performing the main filter rendering,
+     * and managing intermediate framebuffers.
+     *
+     * @param r The `Renderer` instance.
+     * @param sceneFb The framebuffer containing the rendered scene (either MS or single-sample).
      */
     private void renderFilterChain(Renderer r, FrameBuffer sceneFb) {
         Texture2D tex = filterTexture;
         FrameBuffer buff = sceneFb;
         boolean msDepth = depthTexture != null && depthTexture.getImage().getMultiSamples() > 1;
+
         for (int i = 0; i < filters.size(); i++) {
             Filter filter = filters.get(i);
-            if (prof != null) prof.spStep(SpStep.ProcPostFrame, FPP, filter.getName());
+            if (prof != null) {
+                prof.spStep(SpStep.ProcPostFrame, FPP, filter.getName());
+            }
+
             if (filter.isEnabled()) {
+                // Handle additional passes a filter might have (e.g., blur passes)
                 if (filter.getPostRenderPasses() != null) {
-                    for (Iterator<Filter.Pass> it1 = filter.getPostRenderPasses().iterator(); it1.hasNext();) {
-                        Filter.Pass pass = it1.next();
-                        if (prof != null) prof.spStep(SpStep.ProcPostFrame, FPP, filter.getName(), pass.toString());
+                    for (Filter.Pass pass : filter.getPostRenderPasses()) {
+                        if (prof != null) {
+                            prof.spStep(SpStep.ProcPostFrame, FPP, filter.getName(), pass.toString());
+                        }
                         pass.beforeRender();
+
+                        // Set scene texture if required by the pass
                         if (pass.requiresSceneAsTexture()) {
                             pass.getPassMaterial().setTexture("Texture", tex);
                             if (tex.getImage().getMultiSamples() > 1) {
@@ -277,6 +349,8 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
 
                             }
                         }
+
+                        // Set depth texture if required by the pass
                         if (pass.requiresDepthAsTexture()) {
                             pass.getPassMaterial().setTexture("DepthTexture", depthTexture);
                             if (msDepth) {
@@ -288,7 +362,9 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
                         renderProcessing(r, pass.getRenderFrameBuffer(), pass.getPassMaterial());
                     }
                 }
-                if (prof != null) prof.spStep(SpStep.ProcPostFrame, FPP, filter.getName(), "postFrame");
+                if (prof != null) {
+                    prof.spStep(SpStep.ProcPostFrame, FPP, filter.getName(), "postFrame");
+                }
                 filter.postFrame(renderManager, viewPort, buff, sceneFb);
 
                 Material mat = filter.getMaterial();
@@ -305,23 +381,31 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
                     }
                 }
 
+                // Apply bilinear filtering if requested by the filter
                 boolean wantsBilinear = filter.isRequiresBilinear();
                 if (wantsBilinear) {
                     tex.setMagFilter(Texture.MagFilter.Bilinear);
                     tex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
                 }
 
+                // Determine target framebuffer and source texture for the next pass
                 buff = outputBuffer;
                 if (i != lastFilterIndex) {
                     buff = filter.getRenderFrameBuffer();
                     tex = filter.getRenderedTexture();
-
                 }
-                if (prof != null) prof.spStep(SpStep.ProcPostFrame, FPP, filter.getName(), "render");
+                if (prof != null) {
+                    prof.spStep(SpStep.ProcPostFrame, FPP, filter.getName(), "render");
+                }
+                // Render the main filter pass
                 renderProcessing(r, buff, mat);
-                if (prof != null) prof.spStep(SpStep.ProcPostFrame, FPP, filter.getName(), "postFilter");
+                if (prof != null) {
+                    prof.spStep(SpStep.ProcPostFrame, FPP, filter.getName(), "postFilter");
+                }
+                // Call filter's postFilter for final adjustments
                 filter.postFilter(r, buff);
 
+                // Revert texture filtering if it was changed
                 if (wantsBilinear) {
                     tex.setMagFilter(Texture.MagFilter.Nearest);
                     tex.setMinFilter(Texture.MinFilter.NearestNoMipMaps);
@@ -339,10 +423,14 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
         } else if (renderFrameBufferMS != null) {
             sceneBuffer = renderFrameBufferMS;
         }
+
+        // Execute the filter chain
         renderFilterChain(renderer, sceneBuffer);
+
+        // Restore the original output framebuffer for the viewport
         renderer.setFrameBuffer(outputBuffer);
 
-        //viewport can be null if no filters are enabled
+        // viewport can be null if no filters are enabled
         if (viewPort != null) {
             renderManager.setCamera(viewPort.getCamera(), false);
         }
@@ -351,40 +439,44 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
     @Override
     public void preFrame(float tpf) {
         if (filters.isEmpty() || lastFilterIndex == -1) {
-            //If the camera is initialized and there are no filter to render, the camera viewport is restored as it was
+            // If no filters are enabled, restore the camera's original viewport
+            // and output framebuffer to bypass the post-processor.
             if (cameraInit) {
                 viewPort.getCamera().resize(originalWidth, originalHeight, true);
                 viewPort.getCamera().setViewPort(left, right, bottom, top);
                 viewPort.setOutputFrameBuffer(outputBuffer);
                 cameraInit = false;
             }
-
         } else {
-           setupViewPortFrameBuffer();
-           //if we are in a multiview situation we need to resize the camera
-           //to the viewport size so that the back buffer is rendered correctly
-           if (multiView) {
+            setupViewPortFrameBuffer();
+            // If in a multi-view situation, resize the camera to the viewport size
+            // so that the back buffer is rendered correctly for filtering.
+            if (multiView) {
                 viewPort.getCamera().resize(width, height, false);
                 viewPort.getCamera().setViewPort(0, 1, 0, 1);
                 viewPort.getCamera().update();
                 renderManager.setCamera(viewPort.getCamera(), false);
-           }
+            }
         }
 
+        // Call preFrame on all enabled filters
         for (Filter filter : filters.getArray()) {
             if (filter.isEnabled()) {
-                if (prof != null) prof.spStep(SpStep.ProcPreFrame, FPP, filter.getName());
+                if (prof != null) {
+                    prof.spStep(SpStep.ProcPreFrame, FPP, filter.getName());
+                }
                 filter.preFrame(tpf);
             }
         }
-
     }
 
     /**
-     * sets the filter to enabled or disabled
+     * Sets the enabled state of a specific filter. If the filter is part of
+     * this processor's list, its `enabled` flag is updated, and the
+     * `lastFilterIndex` is recomputed.
      *
-     * @param filter the Filter to modify (not null)
-     * @param enabled true to enable, false to disable
+     * @param filter The {@link Filter} to modify (not null).
+     * @param enabled True to enable the filter, false to disable it.
      */
     protected void setFilterState(Filter filter, boolean enabled) {
         if (filters.contains(filter)) {
@@ -394,26 +486,27 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
     }
 
     /**
-     * compute the index of the last filter to render
+     * Computes the index of the last enabled filter in the list. This is used
+     * to determine which filter should render to the final output framebuffer
+     * and which should render to intermediate framebuffers. If no filters are
+     * enabled, the viewport's output framebuffer is restored to its original.
      */
     private void updateLastFilterIndex() {
         lastFilterIndex = -1;
         for (int i = filters.size() - 1; i >= 0 && lastFilterIndex == -1; i--) {
             if (filters.get(i).isEnabled()) {
                 lastFilterIndex = i;
-                // The FPP is initialized, but the viewport framebuffer is the
-                // original out framebuffer, so we must recover from a situation
-                // where no filter was enabled. So we set the correct framebuffer
-                // on the viewport.
-                if(isInitialized() && viewPort.getOutputFrameBuffer()==outputBuffer){
+                // If the FPP is initialized but the viewport framebuffer is the
+                // original output framebuffer (meaning no filter was enabled
+                // previously), then redirect it to the FPP's internal framebuffer.
+                if (isInitialized() && viewPort.getOutputFrameBuffer() == outputBuffer) {
                     setupViewPortFrameBuffer();
                 }
                 return;
             }
         }
+        // If no filters are enabled, restore the original framebuffer to the viewport.
         if (isInitialized() && lastFilterIndex == -1) {
-            //There is no enabled filter, we restore the original framebuffer
-            //to the viewport to bypass the fpp.
             viewPort.setOutputFrameBuffer(outputBuffer);
         }
     }
@@ -421,40 +514,56 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
     @Override
     public void cleanup() {
         if (viewPort != null) {
-            //reset the viewport camera viewport to its initial value
+            // Reset the viewport camera and output framebuffer to their initial values
             viewPort.getCamera().resize(originalWidth, originalHeight, true);
             viewPort.getCamera().setViewPort(left, right, bottom, top);
             viewPort.setOutputFrameBuffer(outputBuffer);
             viewPort = null;
 
-            if(renderFrameBuffer != null){
+            // Dispose of internal framebuffers and textures
+            if (renderFrameBuffer != null) {
                 renderFrameBuffer.dispose();
             }
-            if(depthTexture!=null){
-               depthTexture.getImage().dispose();
+            if (depthTexture != null) {
+                depthTexture.getImage().dispose();
             }
             filterTexture.getImage().dispose();
-            if(renderFrameBufferMS != null){
-               renderFrameBufferMS.dispose();
+            if (renderFrameBufferMS != null) {
+                renderFrameBufferMS.dispose();
             }
             for (Filter filter : filters.getArray()) {
                 filter.cleanup(renderer);
             }
         }
-
     }
 
+    /**
+     * Sets the profiler instance for this processor.
+     *
+     * @param profiler The `AppProfiler` instance to use for performance monitoring.
+     */
     @Override
     public void setProfiler(AppProfiler profiler) {
         this.prof = profiler;
     }
 
+    /**
+     * Reshapes the `FilterPostProcessor` when the viewport or canvas size changes.
+     * This method recalculates internal framebuffer dimensions, creates new
+     * framebuffers and textures if necessary (e.g., for anti-aliasing), and
+     * reinitializes all filters with the new dimensions. It also detects
+     * multi-view scenarios.
+     *
+     * @param vp The `ViewPort` being reshaped.
+     * @param w The new width of the viewport's canvas.
+     * @param h The new height of the viewport's canvas.
+     */
     @Override
     public void reshape(ViewPort vp, int w, int h) {
         Camera cam = vp.getCamera();
-        //this has no effect at first init but is useful when resizing the canvas with multi views
+        // This sets the camera viewport to its full extent (0-1) for rendering to the FPP's internal buffer.
         cam.setViewPort(left, right, bottom, top);
-        //resizing the camera to fit the new viewport and saving original dimensions
+        // Resizing the camera to fit the new viewport and saving original dimensions
         cam.resize(w, h, true);
         left = cam.getViewPortLeft();
         right = cam.getViewPortRight();
@@ -463,16 +572,16 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
         originalWidth = w;
         originalHeight = h;
 
-        //computing real dimension of the viewport and resizing the camera
+        // Computing real dimension of the viewport based on its relative size within the canvas
         width = (int) (w * (Math.abs(right - left)));
         height = (int) (h * (Math.abs(bottom - top)));
         width = Math.max(1, width);
         height = Math.max(1, height);
 
-        //Testing original versus actual viewport dimension.
-        //If they are different we are in a multiview situation and
-        //camera must be handled differently
-        if(originalWidth!=width || originalHeight!=height){
+        // Test if original dimensions differ from actual viewport dimensions.
+        // If they are different, we are in a multiview situation, and the
+        // camera must be handled differently (e.g., resized to the sub-viewport).
+        if (originalWidth != width || originalHeight != height) {
             multiView = true;
         }
 
@@ -485,9 +594,11 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
 
         Collection<Caps> caps = renderer.getCaps();
 
-        //antialiasing on filters only supported in opengl 3 due to depth read problem
+        // antialiasing on filters only supported in opengl 3 due to depth read problem
         if (numSamples > 1 && caps.contains(Caps.FrameBufferMultisample)) {
             renderFrameBufferMS = new FrameBuffer(width, height, numSamples);
+
+            // If OpenGL 3.2+ is supported, multisampled textures can be attached directly
             if (caps.contains(Caps.OpenGL32)) {
                 Texture2D msColor = new Texture2D(width, height, numSamples, fbFormat);
                 Texture2D msDepth = new Texture2D(width, height, numSamples, depthFormat);
@@ -496,18 +607,22 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
                 filterTexture = msColor;
                 depthTexture = msDepth;
             } else {
+                // Otherwise, multisampled framebuffer must use internal texture, which cannot be directly read
                 renderFrameBufferMS.setDepthTarget(FrameBufferTarget.newTarget(depthFormat));
                 renderFrameBufferMS.addColorTarget(FrameBufferTarget.newTarget(fbFormat));
             }
         }
 
-        if (numSamples <= 1 || !caps.contains(Caps.OpenGL32)) {
+        // Setup single-sampled framebuffer if no multisampling, or if OpenGL 3.2+ is not supported
+        // (because for non-GL32, a single-sampled buffer is still needed to copy MS content into).
+        if (numSamples <= 1 || !caps.contains(Caps.OpenGL32) || !caps.contains(Caps.FrameBufferMultisample)) {
             renderFrameBuffer = new FrameBuffer(width, height, 1);
             renderFrameBuffer.setDepthTarget(FrameBufferTarget.newTarget(depthFormat));
             filterTexture = new Texture2D(width, height, fbFormat);
             renderFrameBuffer.addColorTarget(FrameBufferTarget.newTarget(filterTexture));
         }
 
+        // Set names for debugging
         if (renderFrameBufferMS != null) {
             renderFrameBufferMS.setName("FilterPostProcessor MS");
         }
@@ -516,6 +631,7 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
             renderFrameBuffer.setName("FilterPostProcessor");
         }
 
+        // Initialize all existing filters with the new dimensions
         for (Filter filter : filters.getArray()) {
             initFilter(filter, vp);
         }
@@ -523,16 +639,16 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
     }
 
     /**
-     * return the number of samples for antialiasing
-     * @return numSamples
+     * Returns the number of samples used for anti-aliasing.
+     *
+     * @return The number of samples.
      */
     public int getNumSamples() {
         return numSamples;
     }
 
     /**
-     *
-     * Removes all the filters from this processor
+     * Removes all filters currently added to this processor.
      */
     public void removeAllFilters() {
         filters.clear();
@@ -540,8 +656,12 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
     }
 
     /**
-     * Sets the number of samples for antialiasing
-     * @param numSamples the number of Samples
+     * Sets the number of samples for anti-aliasing. A value of 1 means no
+     * anti-aliasing. This method should generally be called before the
+     * processor is initialized to have an effect.
+     *
+     * @param numSamples The number of samples. Must be greater than 0.
+     * @throws IllegalArgumentException If `numSamples` is less than or equal to 0.
      */
     public void setNumSamples(int numSamples) {
         if (numSamples <= 0) {
@@ -561,27 +681,30 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
     }
 
     /**
-     * Sets the format to be used for the internal frame buffer's color buffer
+     * Sets the preferred `Image.Format` to be used for the internal frame buffer's
+     * color buffer.
      *
-     * @param fbFormat the format
+     * @param fbFormat The desired `Format` for the color buffer.
      */
     public void setFrameBufferFormat(Format fbFormat) {
         this.fbFormat = fbFormat;
     }
 
     /**
-     * Sets the format to be used for the internal frame buffer's depth buffer
+     * Sets the preferred `Image.Format` to be used for the internal frame buffer's
+     * depth buffer.
      *
-     * @param depthFormat the format
+     * @param depthFormat The desired `Format` for the depth buffer.
      */
     public void setFrameBufferDepthFormat(Format depthFormat) {
         this.depthFormat = depthFormat;
     }
 
     /**
-     * Returns the depth format currently used for the internal frame buffer's depth buffer
-     * 
-     * @return the depth format
+     * Returns the `Image.Format` currently used for the internal frame buffer's
+     * depth buffer.
+     *
+     * @return The current depth `Format`.
      */
     public Format getFrameBufferDepthFormat() {
         return depthFormat;
@@ -609,43 +732,50 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
     }
 
     /**
-     * For internal use only<br>
-     * returns the depth texture of the scene
-     * @return the depth texture
+     * For internal use only.
+     * Returns the depth texture generated from the scene's depth buffer.
+     * This texture is available if any filter requires a depth texture.
+     *
+     * @return The `Texture2D` containing the scene's depth information, or null if not computed.
      */
     public Texture2D getDepthTexture() {
         return depthTexture;
     }
 
     /**
-     * For internal use only<br>
-     * returns the rendered texture of the scene
-     * @return the filter texture
+     * For internal use only.
+     * Returns the color texture that contains the rendered scene or the output
+     * of the previous filter in the chain. This texture serves as input for subsequent filters.
+     *
+     * @return The `Texture2D` containing the scene's color information or the intermediate filter output.
      */
     public Texture2D getFilterTexture() {
         return filterTexture;
     }
 
     /**
-     * returns the first filter in the list assignable from the given type
+     * Returns the first filter in the managed list that is assignable from the
+     * given filter type. Useful for retrieving specific filters to modify their properties.
      *
-     * @param <T> the filter type
-     * @param filterType the filter type
-     * @return a filter assignable from the given type
+     * @param <T> The type of the filter to retrieve.
+     * @param filterType The `Class` object representing the filter type.
+     * @return A filter instance assignable from `filterType`, or null if no such filter is found.
      */
     @SuppressWarnings("unchecked")
     public <T extends Filter> T getFilter(Class<T> filterType) {
-        for (Filter c : filters.getArray()) {
-            if (filterType.isAssignableFrom(c.getClass())) {
-                return (T) c;
+        for (Filter f : filters.getArray()) {
+            if (filterType.isAssignableFrom(f.getClass())) {
+                return (T) f;
             }
         }
         return null;
     }
 
     /**
-     * returns an unmodifiable version of the filter list.
-     * @return the filters list
+     * Returns an unmodifiable version of the list of filters currently
+     * managed by this processor.
+     *
+     * @return An unmodifiable `List` of {@link Filter} objects.
      */
     public List<Filter> getFilterList(){
         return Collections.unmodifiableList(filters);
@@ -658,4 +788,4 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
             viewPort.setOutputFrameBuffer(renderFrameBuffer);
         }
     }
-    }
+}

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

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2024 jMonkeyEngine
+ * Copyright (c) 2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -77,37 +77,30 @@ import java.util.function.Supplier;
 import java.util.logging.Logger;
 
 /**
- * A high-level rendering interface that is
- * above the Renderer implementation. RenderManager takes care
- * of rendering the scene graphs attached to each viewport and
- * handling SceneProcessors.
- *
- * @see SceneProcessor
- * @see ViewPort
- * @see Spatial
+ * The `RenderManager` is a high-level rendering interface that manages
+ * {@link ViewPort}s, {@link SceneProcessor}s, and the overall rendering pipeline.
+ * It is responsible for orchestrating the rendering of scenes into various
+ * viewports.
  */
 public class RenderManager {
 
     private static final Logger logger = Logger.getLogger(RenderManager.class.getName());
+
     private final Renderer renderer;
     private final UniformBindingManager uniformBindingManager = new UniformBindingManager();
     private final ArrayList<ViewPort> preViewPorts = new ArrayList<>();
     private final ArrayList<ViewPort> viewPorts = new ArrayList<>();
     private final ArrayList<ViewPort> postViewPorts = new ArrayList<>();
-    private final HashMap<Class, PipelineContext> contexts = new HashMap<>();
+    private final HashMap<Class<? extends PipelineContext>, PipelineContext> contexts = new HashMap<>();
     private final LinkedList<PipelineContext> usedContexts = new LinkedList<>();
-    private final LinkedList<RenderPipeline> usedPipelines = new LinkedList<>();
-    private RenderPipeline defaultPipeline = new ForwardPipeline();
+    private final LinkedList<RenderPipeline<? extends PipelineContext>> usedPipelines = new LinkedList<>();
+    private RenderPipeline<? extends PipelineContext> defaultPipeline = new ForwardPipeline();
     private Camera prevCam = null;
     private Material forcedMaterial = null;
     private String forcedTechnique = null;
     private RenderState forcedRenderState = null;
-    private final SafeArrayList<MatParamOverride> forcedOverrides
-            = new SafeArrayList<>(MatParamOverride.class);
-    private int viewX;
-    private int viewY;
-    private int viewWidth;
-    private int viewHeight;
+    private final SafeArrayList<MatParamOverride> forcedOverrides = new SafeArrayList<>(MatParamOverride.class);
+
     private final Matrix4f orthoMatrix = new Matrix4f();
     private final LightList filteredLightList = new LightList(null);
     private boolean handleTranslucentBucket = true;
@@ -115,7 +108,7 @@ public class RenderManager {
     private LightFilter lightFilter = new DefaultLightFilter();
     private TechniqueDef.LightMode preferredLightMode = TechniqueDef.LightMode.MultiPass;
     private int singlePassLightBatchSize = 1;
-    private MatParamOverride boundDrawBufferId=new MatParamOverride(VarType.Int, "BoundDrawBuffer", 0);
+    private final MatParamOverride boundDrawBufferId = new MatParamOverride(VarType.Int, "BoundDrawBuffer", 0);
     private Predicate<Geometry> renderFilter;
 
 
@@ -123,7 +116,7 @@ public class RenderManager {
      * Creates a high-level rendering interface over the
      * low-level rendering interface.
      *
-     * @param renderer (alias created)
+     * @param renderer The low-level renderer implementation.
      */
     public RenderManager(Renderer renderer) {
         this.renderer = renderer;
@@ -131,59 +124,61 @@ public class RenderManager {
         // register default pipeline context
         contexts.put(PipelineContext.class, new DefaultPipelineContext());
     }
-    
+
     /**
      * Gets the default pipeline used when a ViewPort does not have a
      * pipeline already assigned to it.
-     * 
-     * @return 
+     *
+     * @return The default {@link RenderPipeline}, which is {@link ForwardPipeline} by default.
      */
-    public RenderPipeline getPipeline() {
+    public RenderPipeline<? extends PipelineContext> getPipeline() {
         return defaultPipeline;
     }
-    
+
     /**
      * Sets the default pipeline used when a ViewPort does not have a
      * pipeline already assigned to it.
      * <p>
      * default={@link ForwardPipeline}
-     * 
-     * @param pipeline default pipeline (not null)
+     *
+     * @param pipeline The default rendering pipeline (not null).
      */
-    public void setPipeline(RenderPipeline pipeline) {
+    public void setPipeline(RenderPipeline<? extends PipelineContext> pipeline) {
         assert pipeline != null;
         this.defaultPipeline = pipeline;
     }
-    
+
     /**
      * Gets the default pipeline context registered under
-     * {@link PipelineContext#getClass()}.
-     * 
-     * @return 
+     * {@link PipelineContext}.
+     *
+     * @return The default {@link PipelineContext}.
      */
     public PipelineContext getDefaultContext() {
         return getContext(PipelineContext.class);
     }
-    
+
     /**
-     * Gets the pipeline context registered under the class.
-     * 
-     * @param <T>
-     * @param type
-     * @return registered context or null
+     * Gets the pipeline context registered under the given class type.
+     *
+     * @param type The class type of the context to retrieve.
+     * @param <T>  The type of the {@link PipelineContext}.
+     * @return The registered context instance, or null if not found.
      */
+    @SuppressWarnings("unchecked")
     public <T extends PipelineContext> T getContext(Class<T> type) {
-        return (T)contexts.get(type);
+        return (T) contexts.get(type);
     }
-    
+
     /**
      * Gets the pipeline context registered under the class or creates
      * and registers a new context from the supplier.
-     * 
-     * @param <T>
-     * @param type
-     * @param supplier interface for creating a new context if necessary
-     * @return registered or newly created context
+     *
+     * @param <T>      The type of the {@link PipelineContext}.
+     * @param type     The class type under which the context is registered.
+     * @param supplier A function interface for creating a new context
+     *                 if one is not already registered under the given type.
+     * @return The registered or newly created context.
      */
     public <T extends PipelineContext> T getOrCreateContext(Class<T> type, Supplier<T> supplier) {
         T c = getContext(type);
@@ -193,15 +188,16 @@ public class RenderManager {
         }
         return c;
     }
-    
+
     /**
      * Gets the pipeline context registered under the class or creates
      * and registers a new context from the function.
-     * 
-     * @param <T>
-     * @param type
-     * @param function interface for creating a new context if necessary
-     * @return registered or newly created context
+     *
+     * @param <T>      The type of the {@link PipelineContext}.
+     * @param type     The class type under which the context is registered.
+     * @param function A function interface for creating a new context, taking the {@code RenderManager} as an argument,
+     *                 if one is not already registered under the given type.
+     * @return The registered or newly created context.
      */
     public <T extends PipelineContext> T getOrCreateContext(Class<T> type, Function<RenderManager, T> function) {
         T c = getContext(type);
@@ -211,16 +207,16 @@ public class RenderManager {
         }
         return c;
     }
-    
+
     /**
-     * Registers the pipeline context under the class.
+     * Registers a pipeline context under the given class type.
      * <p>
      * If another context is already registered under the class, that
      * context will be replaced by the given context.
-     * 
-     * @param <T>
-     * @param type class type to register the context under (not null)
-     * @param context context to register (not null)
+     *
+     * @param type    The class type under which the context is registered.
+     * @param context The context instance to register.
+     * @param <T>     The type of the {@link PipelineContext}.
      */
     public <T extends PipelineContext> void registerContext(Class<T> type, T context) {
         assert type != null;
@@ -229,11 +225,11 @@ public class RenderManager {
         }
         contexts.put(type, context);
     }
-    
+
     /**
      * Gets the application profiler.
-     * 
-     * @return 
+     *
+     * @return The {@link AppProfiler} instance, or null if none is set.
      */
     public AppProfiler getProfiler() {
         return prof;
@@ -522,7 +518,7 @@ public class RenderManager {
         for (ViewPort vp : preViewPorts) {
             notifyRescale(vp, x, y);
         }
-        for (ViewPort vp : viewPorts) {      
+        for (ViewPort vp : viewPorts) {
             notifyRescale(vp, x, y);
         }
         for (ViewPort vp : postViewPorts) {
@@ -531,22 +527,19 @@ public class RenderManager {
     }
 
     /**
-     * Sets the material to use to render all future objects.
-     * This overrides the material set on the geometry and renders
-     * with the provided material instead.
-     * Use null to clear the material and return renderer to normal
-     * functionality.
+     * Sets a material that will be forced on all rendered geometries.
+     * This can be used for debugging (e.g., solid color) or special effects.
      *
-     * @param mat The forced material to set, or null to return to normal
+     * @param forcedMaterial The material to force, or null to disable forcing.
      */
-    public void setForcedMaterial(Material mat) {
-        forcedMaterial = mat;
+    public void setForcedMaterial(Material forcedMaterial) {
+        this.forcedMaterial = forcedMaterial;
     }
-    
+
     /**
-     * Gets the forced material.
-     * 
-     * @return 
+     * Gets the forced material that overrides materials set on geometries.
+     *
+     * @return The forced {@link Material}, or null if no material is forced.
      */
     public Material getForcedMaterial() {
         return forcedMaterial;
@@ -597,10 +590,9 @@ public class RenderManager {
     }
 
     /**
-     * Returns the forced technique name set.
-     *
-     * @return the forced technique name set.
+     * Returns the name of the forced technique.
      *
+     * @return The name of the forced technique, or null if none is forced.
      * @see #setForcedTechnique(java.lang.String)
      */
     public String getForcedTechnique() {
@@ -616,9 +608,7 @@ public class RenderManager {
      * If a forced material is not set and the forced technique name cannot
      * be found on the material, the geometry will <em>not</em> be rendered.
      *
-     * @param forcedTechnique The forced technique name to use, set to null
-     *     to return to normal functionality.
-     *
+     * @param forcedTechnique The technique to force, or null to disable forcing.
      * @see #renderGeometry(com.jme3.scene.Geometry)
      */
     public void setForcedTechnique(String forcedTechnique) {
@@ -627,13 +617,12 @@ public class RenderManager {
 
     /**
      * Adds a forced material parameter to use when rendering geometries.
-     *
-     * <p>The provided parameter takes precedence over parameters set on the
+     * <p>
+     * The provided parameter takes precedence over parameters set on the
      * material or any overrides that exist in the scene graph that have the
      * same name.
      *
-     * @param override The override to add
-     * @see MatParamOverride
+     * @param override The material parameter override to add.
      * @see #removeForcedMatParam(com.jme3.material.MatParamOverride)
      */
     public void addForcedMatParam(MatParamOverride override) {
@@ -641,9 +630,9 @@ public class RenderManager {
     }
 
     /**
-     * Removes a forced material parameter previously added.
+     * Removes a material parameter override.
      *
-     * @param override The override to remove.
+     * @param override The material parameter override to remove.
      * @see #addForcedMatParam(com.jme3.material.MatParamOverride)
      */
     public void removeForcedMatParam(MatParamOverride override) {
@@ -757,33 +746,35 @@ public class RenderManager {
      * @see com.jme3.material.Material#render(com.jme3.scene.Geometry, com.jme3.renderer.RenderManager)
      */
     public void renderGeometry(Geometry geom) {
-        
+
         if (renderFilter != null && !renderFilter.test(geom)) {
             return;
         }
-        
+
         LightList lightList = geom.getWorldLightList();
         if (lightFilter != null) {
             filteredLightList.clear();
             lightFilter.filterLights(geom, filteredLightList);
             lightList = filteredLightList;
         }
-        
+
         renderGeometry(geom, lightList);
-        
     }
-    
+
     /**
-     * 
-     * @param geom
-     * @param lightList 
+     * Renders a single {@link Geometry} with a specific list of lights.
+     * This method applies the world transform, handles forced materials and techniques,
+     * and manages the `BoundDrawBuffer` parameter for multi-target frame buffers.
+     *
+     * @param geom The {@link Geometry} to render.
+     * @param lightList The {@link LightList} containing the lights that affect this geometry.
      */
     public void renderGeometry(Geometry geom, LightList lightList) {
-        
+
         if (renderFilter != null && !renderFilter.test(geom)) {
             return;
         }
-        
+
         this.renderer.pushDebugGroup(geom.getName());
         if (geom.isIgnoreTransform()) {
             setWorldMatrix(Matrix4f.IDENTITY);
@@ -818,8 +809,7 @@ public class RenderManager {
                 RenderState tmpRs = forcedRenderState;
                 if (geom.getMaterial().getActiveTechnique().getDef().getForcedRenderState() != null) {
                     //forcing forced technique renderState
-                    forcedRenderState
-                            = geom.getMaterial().getActiveTechnique().getDef().getForcedRenderState();
+                    forcedRenderState = geom.getMaterial().getActiveTechnique().getDef().getForcedRenderState();
                 }
                 // use geometry's material
                 material.render(geom, lightList, this);
@@ -902,7 +892,7 @@ public class RenderManager {
             }
         }
     }
-    
+
     /**
      * Flattens the given scene graph into the ViewPort's RenderQueue,
      * checking for culling as the call goes down the graph recursively.
@@ -1079,10 +1069,9 @@ public class RenderManager {
      */
     public void setSinglePassLightBatchSize(int singlePassLightBatchSize) {
         // Ensure the batch size is no less than 1
-        this.singlePassLightBatchSize = singlePassLightBatchSize < 1 ? 1 : singlePassLightBatchSize;
+        this.singlePassLightBatchSize = Math.max(singlePassLightBatchSize, 1);
     }
 
-
     /**
      * Renders the given viewport queues.
      *
@@ -1126,7 +1115,6 @@ public class RenderManager {
             depthRangeChanged = true;
         }
 
-
         // transparent objects are last because they require blending with the
         // rest of the scene's objects. Consequently, they are sorted
         // back-to-front.
@@ -1184,12 +1172,12 @@ public class RenderManager {
     private void setViewPort(Camera cam) {
         // this will make sure to clearReservations viewport only if needed
         if (cam != prevCam || cam.isViewportChanged()) {
-            viewX      = (int) (cam.getViewPortLeft() * cam.getWidth());
-            viewY      = (int) (cam.getViewPortBottom() * cam.getHeight());
+            int viewX  = (int) (cam.getViewPortLeft() * cam.getWidth());
+            int viewY  = (int) (cam.getViewPortBottom() * cam.getHeight());
             int viewX2 = (int) (cam.getViewPortRight() * cam.getWidth());
             int viewY2 = (int) (cam.getViewPortTop() * cam.getHeight());
-            viewWidth  = viewX2 - viewX;
-            viewHeight = viewY2 - viewY;
+            int viewWidth  = viewX2 - viewX;
+            int viewHeight = viewY2 - viewY;
             uniformBindingManager.setViewPort(viewX, viewY, viewWidth, viewHeight);
             renderer.setViewPort(viewX, viewY, viewWidth, viewHeight);
             renderer.setClipRect(viewX, viewY, viewWidth, viewHeight);
@@ -1266,8 +1254,8 @@ public class RenderManager {
     /**
      * Applies the ViewPort's Camera and FrameBuffer in preparation
      * for rendering.
-     * 
-     * @param vp 
+     *
+     * @param vp The ViewPort to apply.
      */
     public void applyViewPort(ViewPort vp) {
         renderer.setFrameBuffer(vp.getOutputFrameBuffer());
@@ -1279,7 +1267,7 @@ public class RenderManager {
             renderer.clearBuffers(vp.isClearColor(), vp.isClearDepth(), vp.isClearStencil());
         }
     }
-    
+
     /**
      * Renders the {@link ViewPort} using the ViewPort's {@link RenderPipeline}.
      * <p>
@@ -1294,11 +1282,12 @@ public class RenderManager {
     public void renderViewPort(ViewPort vp, float tpf) {
         if (!vp.isEnabled()) {
             return;
-        }        
+        }
         RenderPipeline pipeline = vp.getPipeline();
         if (pipeline == null) {
             pipeline = defaultPipeline;
         }
+
         PipelineContext context = pipeline.fetchPipelineContext(this);
         if (context == null) {
             throw new NullPointerException("Failed to fetch pipeline context.");
@@ -1310,6 +1299,7 @@ public class RenderManager {
             usedPipelines.add(pipeline);
             pipeline.startRenderFrame(this);
         }
+
         pipeline.pipelineRender(this, context, vp, tpf);
         context.endViewPortRender(this, vp);
     }
@@ -1333,7 +1323,7 @@ public class RenderManager {
         if (renderer instanceof NullRenderer) {
             return;
         }
-        
+
         uniformBindingManager.newFrame();
 
         if (prof != null) {
@@ -1365,17 +1355,16 @@ public class RenderManager {
                 renderViewPort(vp, tpf);
             }
         }
-        
+
         // cleanup for used render pipelines and pipeline contexts only
-        for (PipelineContext c : usedContexts) {
-            c.endContextRenderFrame(this);
+        for (int i = 0; i < usedContexts.size(); i++) {
+            usedContexts.get(i).endContextRenderFrame(this);
         }
-        for (RenderPipeline p : usedPipelines) {
+        for (RenderPipeline<?> p : usedPipelines) {
             p.endRenderFrame(this);
         }
         usedContexts.clear();
         usedPipelines.clear();
-        
     }
 
     /**
@@ -1384,41 +1373,42 @@ public class RenderManager {
      * @return True if the draw buffer target id is passed to the shaders.
      */
     public boolean getPassDrawBufferTargetIdToShaders() {
-        return this.forcedOverrides.contains(boundDrawBufferId);
+        return forcedOverrides.contains(boundDrawBufferId);
     }
 
     /**
      * Enable or disable passing the draw buffer target id to the shaders. This
      * is needed to handle FrameBuffer.setTargetIndex correctly in some
-     * backends.
+     * backends. When enabled, a material parameter named "BoundDrawBuffer" of
+     * type Int will be added to forced material parameters.
      *
-     * @param v
-     *            True to enable, false to disable (default is true)
+     * @param enable True to enable, false to disable (default is true)
      */
-    public void setPassDrawBufferTargetIdToShaders(boolean v) {
-        if (v) {
-            if (!this.forcedOverrides.contains(boundDrawBufferId)) {
-                this.forcedOverrides.add(boundDrawBufferId);
+    public void setPassDrawBufferTargetIdToShaders(boolean enable) {
+        if (enable) {
+            if (!forcedOverrides.contains(boundDrawBufferId)) {
+                forcedOverrides.add(boundDrawBufferId);
             }
         } else {
-            this.forcedOverrides.remove(boundDrawBufferId);
+            forcedOverrides.remove(boundDrawBufferId);
         }
     }
-    
+
     /**
      * Set a render filter. Every geometry will be tested against this filter
      * before rendering and will only be rendered if the filter returns true.
-     * 
-     * @param filter the render filter
+     * This allows for custom culling or selective rendering based on geometry properties.
+     *
+     * @param filter The render filter to apply, or null to remove any existing filter.
      */
     public void setRenderFilter(Predicate<Geometry> filter) {
         renderFilter = filter;
     }
 
     /**
-     * Returns the render filter that the RenderManager is currently using
-     * 
-     * @return the render filter 
+     * Returns the render filter that the RenderManager is currently using.
+     *
+     * @return The currently active render filter, or null if no filter is set.
      */
     public Predicate<Geometry> getRenderFilter() {
         return renderFilter;

+ 3 - 1
jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java

@@ -516,7 +516,9 @@ public final class GLRenderer implements Renderer {
                 limits.put(Limits.FrameBufferSamples, getInteger(GLExt.GL_MAX_SAMPLES_EXT));
             }
 
-            if (hasExtension("GL_ARB_texture_multisample") || caps.contains(Caps.OpenGLES31)) { // GLES31 does not fully support it
+            if (hasExtension("GL_ARB_texture_multisample") || caps.contains(Caps.OpenGLES31)
+                    || (JmeSystem.getPlatform().getOs() == Platform.Os.MacOS
+                            && caps.contains(Caps.OpenGL32))) { // GLES31 does not fully support it
                 caps.add(Caps.TextureMultisample);
                 limits.put(Limits.ColorTextureSamples, getInteger(GLExt.GL_MAX_COLOR_TEXTURE_SAMPLES));
                 limits.put(Limits.DepthTextureSamples, getInteger(GLExt.GL_MAX_DEPTH_TEXTURE_SAMPLES));

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

@@ -44,6 +44,7 @@ import com.jme3.math.Matrix4f;
 import com.jme3.renderer.Camera;
 import com.jme3.scene.VertexBuffer.Type;
 import com.jme3.scene.mesh.MorphTarget;
+import com.jme3.scene.threadwarden.SceneGraphThreadWarden;
 import com.jme3.util.TempVars;
 import com.jme3.util.clone.Cloner;
 import com.jme3.util.clone.IdentityCloneFunction;
@@ -183,6 +184,7 @@ public class Geometry extends Spatial {
      */
     @Override
     public void setLodLevel(int lod) {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         if (mesh.getNumLodLevels() == 0) {
             throw new IllegalStateException("LOD levels are not set on this mesh");
         }
@@ -239,6 +241,7 @@ public class Geometry extends Spatial {
      * @throws IllegalArgumentException If mesh is null
      */
     public void setMesh(Mesh mesh) {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         if (mesh == null) {
             throw new IllegalArgumentException();
         }
@@ -269,6 +272,7 @@ public class Geometry extends Spatial {
      */
     @Override
     public void setMaterial(Material material) {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         this.material = material;
         nbSimultaneousGPUMorph = -1;
         if (isGrouped()) {

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

@@ -38,6 +38,7 @@ import com.jme3.collision.CollisionResults;
 import com.jme3.export.JmeExporter;
 import com.jme3.export.JmeImporter;
 import com.jme3.material.Material;
+import com.jme3.scene.threadwarden.SceneGraphThreadWarden;
 import com.jme3.util.SafeArrayList;
 import com.jme3.util.clone.Cloner;
 import java.io.IOException;
@@ -201,6 +202,7 @@ public class Node extends Spatial {
      *  that would change state.
      */
     void invalidateUpdateList() {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         updateListValid = false;
         if (parent != null) {
             parent.invalidateUpdateList();
@@ -344,6 +346,7 @@ public class Node extends Spatial {
      * @throws IllegalArgumentException if child is null or this
      */
     public int attachChildAt(Spatial child, int index) {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         if (child == null) {
             throw new IllegalArgumentException("child cannot be null");
         }
@@ -428,6 +431,7 @@ public class Node extends Spatial {
      * @return the child at the supplied index.
      */
     public Spatial detachChildAt(int index) {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         Spatial child = children.remove(index);
         if (child != null) {
             child.setParent(null);
@@ -455,6 +459,7 @@ public class Node extends Spatial {
      * node.
      */
     public void detachAllChildren() {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         // Note: this could be a bit more efficient if it delegated
         // to a private method that avoided setBoundRefresh(), etc.
         // for every child and instead did one in here at the end.
@@ -483,6 +488,7 @@ public class Node extends Spatial {
      * @param index2 The index of the second child to swap
      */
     public void swapChildren(int index1, int index2) {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         Spatial c2 = children.get(index2);
         Spatial c1 = children.remove(index1);
         children.add(index1, c2);
@@ -562,6 +568,7 @@ public class Node extends Spatial {
 
     @Override
     public void setMaterial(Material mat) {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         for (int i = 0; i < children.size(); i++) {
             children.get(i).setMaterial(mat);
         }
@@ -778,6 +785,7 @@ public class Node extends Spatial {
 
     @Override
     public void setModelBound(BoundingVolume modelBound) {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         if (children != null) {
             for (Spatial child : children.getArray()) {
                 child.setModelBound(modelBound != null ? modelBound.clone(null) : null);
@@ -787,6 +795,7 @@ public class Node extends Spatial {
 
     @Override
     public void updateModelBound() {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         if (children != null) {
             for (Spatial child : children.getArray()) {
                 child.updateModelBound();

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

@@ -49,6 +49,7 @@ import com.jme3.renderer.queue.RenderQueue;
 import com.jme3.renderer.queue.RenderQueue.Bucket;
 import com.jme3.renderer.queue.RenderQueue.ShadowMode;
 import com.jme3.scene.control.Control;
+import com.jme3.scene.threadwarden.SceneGraphThreadWarden;
 import com.jme3.util.SafeArrayList;
 import com.jme3.util.TempVars;
 import com.jme3.util.clone.Cloner;
@@ -278,11 +279,13 @@ public abstract class Spatial implements Savable, Cloneable, Collidable,
      * a refresh is required.
      */
     protected void setTransformRefresh() {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         refreshFlags |= RF_TRANSFORM;
         setBoundRefresh();
     }
 
     protected void setLightListRefresh() {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         refreshFlags |= RF_LIGHTLIST;
         // Make sure next updateGeometricState() visits this branch
         // to update lights.
@@ -299,6 +302,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable,
     }
 
     protected void setMatParamOverrideRefresh() {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         refreshFlags |= RF_MATPARAM_OVERRIDE;
         Spatial p = parent;
         while (p != null) {
@@ -316,6 +320,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable,
      * a refresh is required.
      */
     protected void setBoundRefresh() {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         refreshFlags |= RF_BOUND;
 
         Spatial p = parent;
@@ -364,7 +369,8 @@ public abstract class Spatial implements Savable, Cloneable, Collidable,
             throw new IllegalStateException("Scene graph is not properly updated for rendering.\n"
                     + "State was changed after rootNode.updateGeometricState() call. \n"
                     + "Make sure you do not modify the scene from another thread!\n"
-                    + "Problem spatial name: " + getName());
+                    + "Problem spatial name: " + getName() + "\n" +
+                    SceneGraphThreadWarden.getTurnOnAssertsPrompt());
         }
 
         CullHint cm = getCullHint();
@@ -612,6 +618,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable,
      * @see MatParamOverride
      */
     public void addMatParamOverride(MatParamOverride override) {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         if (override == null) {
             throw new IllegalArgumentException("override cannot be null");
         }
@@ -626,6 +633,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable,
      * @see MatParamOverride
      */
     public void removeMatParamOverride(MatParamOverride override) {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         if (localOverrides.remove(override)) {
             setMatParamOverrideRefresh();
         }
@@ -637,6 +645,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable,
      * @see #addMatParamOverride(com.jme3.material.MatParamOverride)
      */
     public void clearMatParamOverrides() {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         if (!localOverrides.isEmpty()) {
             setMatParamOverrideRefresh();
         }
@@ -772,6 +781,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable,
      * @see Spatial#removeControl(java.lang.Class)
      */
     public void addControl(Control control) {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         boolean before = requiresUpdates();
         controls.add(control);
         control.setSpatial(this);
@@ -823,6 +833,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable,
      * @see Spatial#addControl(com.jme3.scene.control.Control)
      */
     public void removeControl(Class<? extends Control> controlType) {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         boolean before = requiresUpdates();
         for (int i = 0; i < controls.size(); i++) {
             if (controlType.isAssignableFrom(controls.get(i).getClass())) {
@@ -850,6 +861,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable,
      * @see Spatial#addControl(com.jme3.scene.control.Control)
      */
     public boolean removeControl(Control control) {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
         boolean before = requiresUpdates();
         boolean result = controls.remove(control);
         if (result) {
@@ -986,6 +998,28 @@ public abstract class Spatial implements Savable, Cloneable, Collidable,
         return worldTransform.transformInverseVector(in, store);
     }
 
+    /**
+     * Transforms the given quaternion from world space to local space relative to this object's transform.
+     *
+     * @param in the input quaternion in world space that needs to be transformed
+     * @param store an optional Quaternion to store the result; if null, a new Quaternion will be created
+     * @return the transformed quaternion in local space, either stored in the provided Quaternion or a new one
+     */
+    public Quaternion worldToLocal(final Quaternion in, Quaternion store){
+        checkDoTransformUpdate();
+        if(store == null){
+            store=new Quaternion(in);
+        }else{
+            store.set(in);
+        }
+        TempVars tempVars = TempVars.get();
+        Quaternion worldRotation = tempVars.quat1.set(getWorldRotation());
+        worldRotation.inverseLocal();
+        store.multLocal(worldRotation);
+        tempVars.release();
+        return store;
+    }
+
     /**
      * <code>getParent</code> retrieves this node's parent. If the parent is
      * null this is the root node.
@@ -1005,6 +1039,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable,
      *            the parent of this node.
      */
     protected void setParent(Node parent) {
+        assert SceneGraphThreadWarden.updateRequirement(this, parent);
         this.parent = parent;
     }
 
@@ -1369,6 +1404,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable,
      * @param lod The lod level to set.
      */
     public void setLodLevel(int lod) {
+        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
     }
 
     /**

+ 22 - 5
jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2023 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -57,7 +57,7 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable {
     /**
      * Type of buffer. Specifies the actual attribute it defines.
      */
-    public static enum Type {
+    public enum Type {
         /**
          * Position of the vertex (3 floats)
          */
@@ -233,7 +233,7 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable {
      * is used. This can determine if a vertex buffer is placed in VRAM
      * or held in video memory, but no guarantees are made- it's only a hint.
      */
-    public static enum Usage {
+    public enum Usage {
         /**
          * Mesh data is sent once and very rarely updated.
          */
@@ -261,7 +261,7 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable {
      * For the {@link Format#Half} type, {@link ByteBuffer}s should
      * be used.
      */
-    public static enum Format {
+    public enum Format {
         /**
          * Half precision floating point. 2 bytes, signed.
          */
@@ -1130,6 +1130,7 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable {
         oc.write(offset, "offset", 0);
         oc.write(stride, "stride", 0);
         oc.write(instanceSpan, "instanceSpan", 0);
+        oc.write(name, "name", null);
 
         String dataName = "data" + format.name();
         Buffer roData = getDataReadOnly();
@@ -1166,6 +1167,8 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable {
         offset = ic.readInt("offset", 0);
         stride = ic.readInt("stride", 0);
         instanceSpan = ic.readInt("instanceSpan", 0);
+        name = ic.readString("name", null);
+
         componentsLength = components * format.getComponentSize();
 
         String dataName = "data" + format.name();
@@ -1191,14 +1194,28 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable {
         }
     }
 
+    /**
+     * Returns the name of this `VertexBuffer`. If no name has been explicitly
+     * set, a default name is generated based on its class name and buffer type
+     * (e.g., "VertexBuffer(Position)").
+     *
+     * @return The name of the `VertexBuffer`.
+     */
     public String getName() {
         if (name == null) {
-            name = getClass().getSimpleName() + "(" + getBufferType().name() + ")";
+            return String.format("%s(%s)", getClass().getSimpleName(), getBufferType().name());
         }
         return name;
     }
 
+    /**
+     * Sets a custom name for this `VertexBuffer`.
+     *
+     * @param name The new name for the `VertexBuffer`. Can be null to revert
+     * to the default generated name.
+     */
     public void setName(String name) {
         this.name = name;
     }
+    
 }

+ 157 - 60
jme3-core/src/main/java/com/jme3/scene/control/LightControl.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2023 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -49,52 +49,77 @@ import com.jme3.util.clone.Cloner;
 import java.io.IOException;
 
 /**
- * This Control maintains a reference to a Light,
- * which will be synched with the position (worldTranslation)
- * of the current spatial.
+ * `LightControl` synchronizes the world transformation (position and/or
+ * direction) of a `Light` with its attached `Spatial`. This control allows
+ * a light to follow a spatial or vice-versa, depending on the chosen
+ * {@link ControlDirection}.
+ * <p>
+ * This is particularly useful for attaching lights to animated characters,
+ * moving vehicles, or dynamically controlled objects.
+ * </p>
  *
- * @author tim
+ * @author Tim
+ * @author Markil 3
+ * @author capdevon
  */
 public class LightControl extends AbstractControl {
 
-    private static final String CONTROL_DIR_NAME = "controlDir";
-    private static final String LIGHT_NAME = "light";
-
+    /**
+     * Defines the direction of synchronization between the light and the spatial.
+     */
     public enum ControlDirection {
-
         /**
-         * Means, that the Light's transform is "copied"
-         * to the Transform of the Spatial.
+         * The light's transform is copied to the spatial's transform.
          */
         LightToSpatial,
         /**
-         * Means, that the Spatial's transform is "copied"
-         * to the Transform of the light.
+         * The spatial's transform is copied to the light's transform.
          */
         SpatialToLight
     }
 
+    /**
+     * Represents the local axis of the spatial (X, Y, or Z) to be used
+     * for determining the light's direction when `ControlDirection` is
+     * `SpatialToLight`.
+     */
+    public enum Axis {
+        X, Y, Z
+    }
+
     private Light light;
     private ControlDirection controlDir = ControlDirection.SpatialToLight;
+    private Axis axisRotation = Axis.Z;
+    private boolean invertAxisDirection = false;
 
     /**
-     * Constructor used for Serialization.
+     * For serialization only. Do not use.
      */
     public LightControl() {
     }
 
     /**
+     * Creates a new `LightControl` that synchronizes the light's transform to the spatial.
+     *
      * @param light The light to be synced.
+     * @throws IllegalArgumentException if the light type is not supported
+     * (only Point, Directional, and Spot lights are supported).
      */
     public LightControl(Light light) {
+        validateSupportedLightType(light);
         this.light = light;
     }
 
     /**
+     * Creates a new `LightControl` with a specified synchronization direction.
+     *
      * @param light The light to be synced.
-     * @param controlDir SpatialToLight or LightToSpatial
+     * @param controlDir The direction of synchronization (SpatialToLight or LightToSpatial).
+     * @throws IllegalArgumentException if the light type is not supported
+     * (only Point, Directional, and Spot lights are supported).
      */
     public LightControl(Light light, ControlDirection controlDir) {
+        validateSupportedLightType(light);
         this.light = light;
         this.controlDir = controlDir;
     }
@@ -104,6 +129,7 @@ public class LightControl extends AbstractControl {
     }
 
     public void setLight(Light light) {
+        validateSupportedLightType(light);
         this.light = light;
     }
 
@@ -115,86 +141,141 @@ public class LightControl extends AbstractControl {
         this.controlDir = controlDir;
     }
 
-    // fields used when inverting ControlDirection:
+    public Axis getAxisRotation() {
+        return axisRotation;
+    }
+
+    public void setAxisRotation(Axis axisRotation) {
+        this.axisRotation = axisRotation;
+    }
+
+    public boolean isInvertAxisDirection() {
+        return invertAxisDirection;
+    }
+
+    public void setInvertAxisDirection(boolean invertAxisDirection) {
+        this.invertAxisDirection = invertAxisDirection;
+    }
+
+    private void validateSupportedLightType(Light light) {
+        if (light == null) {
+            return;
+        }
+
+        switch (light.getType()) {
+            case Point:
+            case Directional:
+            case Spot:
+                // These types are supported, validation passes.
+                break;
+            default:
+                throw new IllegalArgumentException(
+                        "Unsupported Light type: " + light.getType());
+        }
+    }
+
     @Override
     protected void controlUpdate(float tpf) {
-        if (spatial != null && light != null) {
-            switch (controlDir) {
-                case SpatialToLight:
-                    spatialToLight(light);
-                    break;
-                case LightToSpatial:
-                    lightToSpatial(light);
-                    break;
-            }
+        if (light == null) {
+            return;
+        }
+
+        switch (controlDir) {
+            case SpatialToLight:
+                spatialToLight(light);
+                break;
+            case LightToSpatial:
+                lightToSpatial(light);
+                break;
         }
     }
 
     /**
-     * Sets the light to adopt the spatial's world transformations.
+     * Updates the light's position and/or direction to match the spatial's
+     * world transformation.
      *
-     * @author Markil 3
-     * @author pspeed42
+     * @param light The light whose properties will be set.
      */
     private void spatialToLight(Light light) {
         TempVars vars = TempVars.get();
 
-        final Vector3f worldTranslation = vars.vect1;
-        worldTranslation.set(spatial.getWorldTranslation());
-        final Vector3f worldDirection = vars.vect2;
-        spatial.getWorldRotation().mult(Vector3f.UNIT_Z, worldDirection).negateLocal();
+        final Vector3f worldPosition = vars.vect1;
+        worldPosition.set(spatial.getWorldTranslation());
+
+        final Vector3f lightDirection = vars.vect2;
+        spatial.getWorldRotation().getRotationColumn(axisRotation.ordinal(), lightDirection);
+        if (invertAxisDirection) {
+            lightDirection.negateLocal();
+        }
 
         if (light instanceof PointLight) {
-            ((PointLight) light).setPosition(worldTranslation);
+            ((PointLight) light).setPosition(worldPosition);
+
         } else if (light instanceof DirectionalLight) {
-            ((DirectionalLight) light).setDirection(worldDirection);
+            ((DirectionalLight) light).setDirection(lightDirection);
+
         } else if (light instanceof SpotLight) {
-            final SpotLight spotLight = (SpotLight) light;
-            spotLight.setPosition(worldTranslation);
-            spotLight.setDirection(worldDirection);
+            SpotLight sl = (SpotLight) light;
+            sl.setPosition(worldPosition);
+            sl.setDirection(lightDirection);
         }
         vars.release();
     }
 
     /**
-     * Sets the spatial to adopt the light's world transformations.
+     * Updates the spatial's local transformation (position and/or rotation)
+     * to match the light's world transformation.
      *
-     * @author Markil 3
+     * @param light The light from which properties will be read.
      */
     private void lightToSpatial(Light light) {
         TempVars vars = TempVars.get();
-        Vector3f translation = vars.vect1;
-        Vector3f direction = vars.vect2;
+        Vector3f lightPosition = vars.vect1;
+        Vector3f lightDirection = vars.vect2;
         Quaternion rotation = vars.quat1;
-        boolean rotateSpatial = false, translateSpatial = false;
+        boolean rotateSpatial = false;
+        boolean translateSpatial = false;
 
         if (light instanceof PointLight) {
-            PointLight pLight = (PointLight) light;
-            translation.set(pLight.getPosition());
+            PointLight pl = (PointLight) light;
+            lightPosition.set(pl.getPosition());
             translateSpatial = true;
+
         } else if (light instanceof DirectionalLight) {
-            DirectionalLight dLight = (DirectionalLight) light;
-            direction.set(dLight.getDirection()).negateLocal();
+            DirectionalLight dl = (DirectionalLight) light;
+            lightDirection.set(dl.getDirection());
+            if (invertAxisDirection) {
+                lightDirection.negateLocal();
+            }
             rotateSpatial = true;
+
         } else if (light instanceof SpotLight) {
-            SpotLight sLight = (SpotLight) light;
-            translation.set(sLight.getPosition());
-            direction.set(sLight.getDirection()).negateLocal();
-            translateSpatial = rotateSpatial = true;
+            SpotLight sl = (SpotLight) light;
+            lightPosition.set(sl.getPosition());
+            lightDirection.set(sl.getDirection());
+            if (invertAxisDirection) {
+                lightDirection.negateLocal();
+            }
+            translateSpatial = true;
+            rotateSpatial = true;
         }
+
+        // Transform light's world properties to spatial's parent's local space
         if (spatial.getParent() != null) {
+            // Get inverse of parent's world matrix
             spatial.getParent().getLocalToWorldMatrix(vars.tempMat4).invertLocal();
-            vars.tempMat4.rotateVect(translation);
-            vars.tempMat4.translateVect(translation);
-            vars.tempMat4.rotateVect(direction);
+            vars.tempMat4.rotateVect(lightPosition);
+            vars.tempMat4.translateVect(lightPosition);
+            vars.tempMat4.rotateVect(lightDirection);
         }
 
+        // Apply transformed properties to spatial's local transformation
         if (rotateSpatial) {
-            rotation.lookAt(direction, Vector3f.UNIT_Y).normalizeLocal();
+            rotation.lookAt(lightDirection, Vector3f.UNIT_Y).normalizeLocal();
             spatial.setLocalRotation(rotation);
         }
         if (translateSpatial) {
-            spatial.setLocalTranslation(translation);
+            spatial.setLocalTranslation(lightPosition);
         }
         vars.release();
     }
@@ -214,15 +295,31 @@ public class LightControl extends AbstractControl {
     public void read(JmeImporter im) throws IOException {
         super.read(im);
         InputCapsule ic = im.getCapsule(this);
-        controlDir = ic.readEnum(CONTROL_DIR_NAME, ControlDirection.class, ControlDirection.SpatialToLight);
-        light = (Light) ic.readSavable(LIGHT_NAME, null);
+        light = (Light) ic.readSavable("light", null);
+        controlDir = ic.readEnum("controlDir", ControlDirection.class, ControlDirection.SpatialToLight);
+        axisRotation = ic.readEnum("axisRotation", Axis.class, Axis.Z);
+        invertAxisDirection = ic.readBoolean("invertAxisDirection", false);
     }
 
     @Override
     public void write(JmeExporter ex) throws IOException {
         super.write(ex);
         OutputCapsule oc = ex.getCapsule(this);
-        oc.write(controlDir, CONTROL_DIR_NAME, ControlDirection.SpatialToLight);
-        oc.write(light, LIGHT_NAME, null);
+        oc.write(light, "light", null);
+        oc.write(controlDir, "controlDir", ControlDirection.SpatialToLight);
+        oc.write(axisRotation, "axisRotation", Axis.Z);
+        oc.write(invertAxisDirection, "invertAxisDirection", false);
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() +
+                "[light=" + (light != null ? light.getType() : null) +
+                ", controlDir=" + controlDir +
+                ", axisRotation=" + axisRotation +
+                ", invertAxisDirection=" + invertAxisDirection +
+                ", enabled=" + enabled +
+                ", spatial=" + spatial +
+                "]";
     }
-}
+}

+ 113 - 27
jme3-core/src/main/java/com/jme3/scene/debug/WireFrustum.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -32,74 +32,160 @@
 package com.jme3.scene.debug;
 
 import com.jme3.math.Vector3f;
+import com.jme3.renderer.Camera;
+import com.jme3.scene.Geometry;
 import com.jme3.scene.Mesh;
 import com.jme3.scene.VertexBuffer;
 import com.jme3.scene.VertexBuffer.Type;
 import com.jme3.scene.VertexBuffer.Usage;
+import com.jme3.shadow.ShadowUtil;
 import com.jme3.util.BufferUtils;
 import java.nio.FloatBuffer;
 
+/**
+ * A specialized Mesh that renders a camera frustum as a wireframe.
+ * This class extends jME3's Mesh and is designed to visually represent
+ * the viewing volume of a camera, which can be useful for debugging
+ * or visualization purposes.
+ * <p>
+ * The frustum is defined by eight points: four for the near plane
+ * and four for the far plane. These points are connected by lines
+ * to form a wireframe cube-like structure.
+ */
 public class WireFrustum extends Mesh {
 
     /**
-     * This constructor is for serialization only. Do not use.
+     * For Serialization only. Do not use.
      */
     protected WireFrustum() {
     }
 
-    public WireFrustum(Vector3f[] points){
-        initGeom(this, points);
-    }
-
-    public static Mesh makeFrustum(Vector3f[] points){
-        Mesh m = new Mesh();
-        initGeom(m, points);
-        return m;
+    /**
+     * Constructs a new `WireFrustum` mesh using the specified frustum corner points.
+     * The points should represent the 8 corners of the frustum.
+     * The expected order of points is typically:
+     * 0-3: Near plane (e.g., bottom-left, bottom-right, top-right, top-left)
+     * 4-7: Far plane (e.g., bottom-left, bottom-right, top-right, top-left)
+     *
+     * @param points An array of 8 `Vector3f` objects representing the frustum's corners.
+     * If the array is null or does not contain 8 points, an
+     * `IllegalArgumentException` will be thrown.
+     */
+    public WireFrustum(Vector3f[] points) {
+        if (points == null || points.length != 8) {
+            throw new IllegalArgumentException("Frustum points array must not be null and must contain 8 points.");
+        }
+        setGeometryData(points);
     }
 
-    private static void initGeom(Mesh m, Vector3f[] points) {
-        if (points != null)
-            m.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(points));
+    /**
+     * Initializes the mesh's geometric data, setting up the vertex positions and indices.
+     * This method is called during the construction of the `WireFrustum`.
+     *
+     * @param points The 8 `Vector3f` points defining the frustum's corners.
+     */
+    private void setGeometryData(Vector3f[] points) {
+        // Set vertex positions
+        setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(points));
 
-        m.setBuffer(Type.Index, 2,
+        // Set indices to draw lines connecting the frustum corners
+        // The indices define 12 lines: 4 for near plane, 4 for far plane, and 4 connecting near to far.
+        setBuffer(Type.Index, 2,
                 new short[]{
+                        // Near plane
                         0, 1,
                         1, 2,
                         2, 3,
                         3, 0,
 
+                        // Far plane
                         4, 5,
                         5, 6,
                         6, 7,
                         7, 4,
 
+                        // Connecting lines (near to far)
                         0, 4,
                         1, 5,
                         2, 6,
                         3, 7,
                 }
         );
-        m.getBuffer(Type.Index).setUsage(Usage.Static);
-        m.setMode(Mode.Lines);
+        getBuffer(Type.Index).setUsage(Usage.Static);
+        setMode(Mode.Lines);
+        updateBound();
     }
 
-    public void update(Vector3f[] points){
+    /**
+     * Updates the vertex positions of the existing `WireFrustum` mesh.
+     * This is more efficient than creating a new `WireFrustum` instance
+     * if only the frustum's position or orientation changes.
+     *
+     * @param points An array of 8 `Vector3f` objects representing the new frustum's corners.
+     * If the array is null or does not contain 8 points, an
+     * `IllegalArgumentException` will be thrown.
+     */
+    public void update(Vector3f[] points) {
+        if (points == null || points.length != 8) {
+            throw new IllegalArgumentException("Frustum points array must not be null and must contain 8 points.");
+        }
+
         VertexBuffer vb = getBuffer(Type.Position);
-        if (vb == null){
-            setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(points));
+        if (vb == null) {
+            // If for some reason the position buffer is missing, re-create it.
+            // This case should ideally not happen if the object is constructed properly.
+            setGeometryData(points);
             return;
         }
 
-        FloatBuffer b = BufferUtils.createFloatBuffer(points);
-        FloatBuffer a = (FloatBuffer) vb.getData();
-        b.rewind();
-        a.rewind();
-        a.put(b);
-        a.rewind();
+        // Create a new FloatBuffer from the updated points
+        FloatBuffer newBuff = BufferUtils.createFloatBuffer(points);
+        // Get the existing FloatBuffer from the VertexBuffer
+        FloatBuffer currBuff = (FloatBuffer) vb.getData();
 
-        vb.updateData(a);
-        
+        currBuff.clear();       // Clear
+        currBuff.put(newBuff);  // Copy
+        currBuff.rewind();      // Rewind
+
+        // Update the VertexBuffer with the modified FloatBuffer data
+        vb.updateData(currBuff);
+
+        // Update the mesh's bounding volume to reflect the new vertex positions
         updateBound();
     }
 
+    /**
+     * A static factory method to create a new `WireFrustum` mesh.
+     * This method provides a cleaner way to instantiate a `WireFrustum`.
+     *
+     * @param points An array of 8 `Vector3f` objects representing the frustum's corners.
+     * @return A new `WireFrustum` instance.
+     */
+    public static Mesh makeFrustum(Vector3f[] points) {
+        return new WireFrustum(points);
+    }
+
+    /**
+     * Creates a `Geometry` object representing the wireframe frustum of a given camera.
+     * The frustum points are calculated based on the camera's current view settings.
+     * The returned `Geometry` can be directly attached to a scene graph.
+     *
+     * @param camera The `Camera` whose frustum is to be visualized.
+     * @return A `Geometry` object containing the `WireFrustum` mesh.
+     */
+    public static Geometry makeGeometry(Camera camera) {
+        Vector3f[] frustumCorners = new Vector3f[8];
+        for (int i = 0; i < 8; i++) {
+            frustumCorners[i] = new Vector3f();
+        }
+
+        Camera tempCam = camera.clone();
+        tempCam.setLocation(new Vector3f(0, 0, 0));
+        tempCam.lookAt(Vector3f.UNIT_Z, Vector3f.UNIT_Y);
+        ShadowUtil.updateFrustumPoints2(tempCam, frustumCorners);
+
+        WireFrustum mesh = new WireFrustum(frustumCorners);
+        return new Geometry("Viewing Frustum", mesh);
+    }
+
 }

+ 345 - 111
jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -31,59 +31,115 @@
  */
 package com.jme3.scene.debug.custom;
 
-import com.jme3.anim.*;
+import com.jme3.anim.Armature;
+import com.jme3.anim.Joint;
+import com.jme3.anim.SkinningControl;
 import com.jme3.app.Application;
 import com.jme3.app.state.BaseAppState;
+import com.jme3.asset.AssetManager;
 import com.jme3.collision.CollisionResults;
+import com.jme3.input.InputManager;
 import com.jme3.input.KeyInput;
 import com.jme3.input.MouseInput;
-import com.jme3.input.controls.*;
-import com.jme3.light.DirectionalLight;
-import com.jme3.math.*;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.input.controls.MouseButtonTrigger;
+import com.jme3.math.Ray;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
 import com.jme3.renderer.Camera;
+import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.ViewPort;
-import com.jme3.scene.*;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.SceneGraphVisitorAdapter;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.control.AbstractControl;
+import com.jme3.util.TempVars;
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 /**
+ * A debug application state for visualizing and interacting with JME3 armatures (skeletons).
+ * This state allows users to see the joints of an armature, select individual joints
+ * by clicking on them, and view their local and model transforms.
+ * It also provides a toggle to display non-deforming joints.
+ * <p>
+ * This debug state operates on its own `ViewPort` and `debugNode` to prevent
+ * interference with the main scene's rendering.
+ *
  * @author Nehon
+ * @author capdevon
  */
 public class ArmatureDebugAppState extends BaseAppState {
 
+    private static final Logger logger = Logger.getLogger(ArmatureDebugAppState.class.getName());
+
+    private static final String PICK_JOINT = "ArmatureDebugAppState_PickJoint";
+    private static final String TOGGLE_JOINTS = "ArmatureDebugAppState_DisplayAllJoints";
+
+    /**
+     * The maximum delay for a mouse click to be registered as a single click.
+     */
     public static final float CLICK_MAX_DELAY = 0.2f;
-    private Node debugNode = new Node("debugNode");
-    private Map<Armature, ArmatureDebugger> armatures = new HashMap<>();
-    private Map<Armature, Joint> selectedBones = new HashMap<>();
-    private Application app;
-    private boolean displayAllJoints = false;
+
+    private Node debugNode = new Node("ArmaturesDebugNode");
+    private final Map<Armature, ArmatureDebugger> armatures = new HashMap<>();
+    private final List<Consumer<Joint>> selectionListeners = new ArrayList<>();
+    private boolean displayNonDeformingJoints = false;
     private float clickDelay = -1;
-    Vector3f tmp = new Vector3f();
-    Vector3f tmp2 = new Vector3f();
-    ViewPort vp;
+    private ViewPort vp;
+    private Camera cam;
+    private InputManager inputManager;
+    private boolean showOnTop = true;
+    private boolean enableJointInfoLogging = true;
 
     @Override
     protected void initialize(Application app) {
-        vp = app.getRenderManager().createMainView("debug", app.getCamera());
+
+        inputManager = app.getInputManager();
+        cam = app.getCamera();
+
+        vp = app.getRenderManager().createMainView("ArmatureDebugView", cam);
         vp.attachScene(debugNode);
-        vp.setClearDepth(true);
-        this.app = app;
-        for (ArmatureDebugger armatureDebugger : armatures.values()) {
-            armatureDebugger.initialize(app.getAssetManager(), app.getCamera());
-        }
-        app.getInputManager().addListener(actionListener, "shoot", "toggleJoints");
-        app.getInputManager().addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT), new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
-        app.getInputManager().addMapping("toggleJoints", new KeyTrigger(KeyInput.KEY_F10));
+        vp.setClearDepth(showOnTop);
 
-        debugNode.addLight(new DirectionalLight(new Vector3f(-1f, -1f, -1f).normalizeLocal()));
+        for (ArmatureDebugger debugger : armatures.values()) {
+            debugger.initialize(app.getAssetManager(), cam);
+        }
 
-        debugNode.addLight(new DirectionalLight(new Vector3f(1f, 1f, 1f).normalizeLocal(), new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f)));
+        // Initially disable the viewport until the state is enabled
         vp.setEnabled(false);
+
+        registerInput();
+    }
+
+    private void registerInput() {
+        inputManager.addMapping(PICK_JOINT, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
+        inputManager.addMapping(TOGGLE_JOINTS, new KeyTrigger(KeyInput.KEY_F10));
+        inputManager.addListener(actionListener, PICK_JOINT, TOGGLE_JOINTS);
+    }
+
+    private void unregisterInput() {
+        inputManager.deleteMapping(PICK_JOINT);
+        inputManager.deleteMapping(TOGGLE_JOINTS);
+        inputManager.removeListener(actionListener);
     }
 
     @Override
     protected void cleanup(Application app) {
-
+        unregisterInput();
+        app.getRenderManager().removeMainView(vp);
+        // Clear maps to release references
+        armatures.clear();
+        selectionListeners.clear();
+        debugNode.detachAllChildren();
     }
 
     @Override
@@ -102,139 +158,317 @@ public class ArmatureDebugAppState extends BaseAppState {
             clickDelay += tpf;
         }
         debugNode.updateLogicalState(tpf);
-        debugNode.updateGeometricState();
+    }
 
+    @Override
+    public void render(RenderManager rm) {
+        debugNode.updateGeometricState();
     }
 
-    public ArmatureDebugger addArmatureFrom(SkinningControl skinningControl) {
-        Armature armature = skinningControl.getArmature();
-        Spatial forSpatial = skinningControl.getSpatial();
-        return addArmatureFrom(armature, forSpatial);
+    /**
+     * Adds an ArmatureDebugger for the armature associated with a given SkinningControl.
+     *
+     * @param skControl The SkinningControl whose armature needs to be debugged.
+     * @return The newly created or existing ArmatureDebugger for the given armature.
+     */
+    public ArmatureDebugger addArmatureFrom(SkinningControl skControl) {
+        return addArmatureFrom(skControl.getArmature(), skControl.getSpatial());
     }
 
-    public ArmatureDebugger addArmatureFrom(Armature armature, Spatial forSpatial) {
+    /**
+     * Adds an ArmatureDebugger for a specific Armature, associating it with a Spatial.
+     * If an ArmatureDebugger for this armature already exists, it is returned.
+     * Otherwise, a new ArmatureDebugger is created, initialized, and attached to the debug node.
+     *
+     * @param armature The Armature to debug.
+     * @param sp The Spatial associated with this armature (used for determining world transform and deforming joints).
+     * @return The newly created or existing ArmatureDebugger for the given armature.
+     */
+    public ArmatureDebugger addArmatureFrom(Armature armature, Spatial sp) {
 
-        ArmatureDebugger ad = armatures.get(armature);
-        if(ad != null){
-            return ad;
+        ArmatureDebugger debugger = armatures.get(armature);
+        if (debugger != null) {
+            return debugger;
         }
 
-        JointInfoVisitor visitor = new JointInfoVisitor(armature);
-        forSpatial.depthFirstTraversal(visitor);
+        // Use a visitor to find joints that actually deform the mesh
+        JointInfoVisitor jointVisitor = new JointInfoVisitor(armature);
+        sp.depthFirstTraversal(jointVisitor);
+
+        Spatial target = sp;
 
-        ad = new ArmatureDebugger(forSpatial.getName() + "_Armature", armature, visitor.deformingJoints);
-        ad.setLocalTransform(forSpatial.getWorldTransform());
-        if (forSpatial instanceof Node) {
+        if (sp instanceof Node) {
             List<Geometry> geoms = new ArrayList<>();
-            findGeoms((Node) forSpatial, geoms);
+            collectGeometries((Node) sp, geoms);
             if (geoms.size() == 1) {
-                ad.setLocalTransform(geoms.get(0).getWorldTransform());
+                target = geoms.get(0);
             }
         }
-        armatures.put(armature, ad);
-        debugNode.attachChild(ad);
+
+        // Create a new ArmatureDebugger
+        debugger = new ArmatureDebugger(sp.getName() + "_ArmatureDebugger", armature, jointVisitor.deformingJoints);
+        debugger.addControl(new ArmatureDebuggerLink(target));
+
+        // Store and attach the new debugger
+        armatures.put(armature, debugger);
+        debugNode.attachChild(debugger);
+
+        // If the AppState is already initialized, initialize the new ArmatureDebugger immediately
         if (isInitialized()) {
-            ad.initialize(app.getAssetManager(), app.getCamera());
+            AssetManager assetManager = getApplication().getAssetManager();
+            debugger.initialize(assetManager, cam);
         }
-        return ad;
+        return debugger;
     }
 
-    private void findGeoms(Node node, List<Geometry> geoms) {
-        for (Spatial spatial : node.getChildren()) {
-            if (spatial instanceof Geometry) {
-                geoms.add((Geometry) spatial);
-            } else if (spatial instanceof Node) {
-                findGeoms((Node) spatial, geoms);
+    /**
+     * Recursively finds all `Geometry` instances within a given `Node` and its children.
+     *
+     * @param node The starting `Node` to search from.
+     * @param geometries The list to which found `Geometry` instances will be added.
+     */
+    private void collectGeometries(Node node, List<Geometry> geometries) {
+        for (Spatial s : node.getChildren()) {
+            if (s instanceof Geometry) {
+                geometries.add((Geometry) s);
+            } else if (s instanceof Node) {
+                collectGeometries((Node) s, geometries);
             }
         }
     }
 
-    final private ActionListener actionListener = new ActionListener() {
+    /**
+     * The ActionListener implementation to handle input events.
+     * Specifically, it processes mouse clicks for joint selection and
+     * the F10 key press for toggling display of all joints.
+     */
+    private final ActionListener actionListener = new ActionListener() {
+
+        private final CollisionResults results = new CollisionResults();
+
         @Override
         public void onAction(String name, boolean isPressed, float tpf) {
-            if (name.equals("shoot") && isPressed) {
-                clickDelay = 0;
+            if (!isEnabled()) {
+                return;
             }
-            if (name.equals("shoot") && !isPressed && clickDelay < CLICK_MAX_DELAY) {
-                Vector2f click2d = app.getInputManager().getCursorPosition();
-                CollisionResults results = new CollisionResults();
-
-                Camera camera = app.getCamera();
-                Vector3f click3d = camera.getWorldCoordinates(click2d, 0f, tmp);
-                Vector3f dir = camera.getWorldCoordinates(click2d, 1f, tmp2)
-                        .subtractLocal(click3d)
-                        .normalizeLocal();
-                Ray ray = new Ray(click3d, dir);
-                debugNode.collideWith(ray, results);
-
-                if (results.size() == 0) {
-                    for (ArmatureDebugger ad : armatures.values()) {
-                        ad.select(null);
-                    }
-                    return;
-                }
-                
-                // The closest result is the target that the player picked:
-                Geometry target = results.getClosestCollision().getGeometry();
-                for (ArmatureDebugger ad : armatures.values()) {
-                    Joint selectedjoint = ad.select(target);
-                    if (selectedjoint != null) {
-                        selectedBones.put(ad.getArmature(), selectedjoint);
-                        System.err.println("-----------------------");
-                        System.err.println("Selected Joint : " + selectedjoint.getName() + " in armature " + ad.getName());
-                        System.err.println("Root Bone : " + (selectedjoint.getParent() == null));
-                        System.err.println("-----------------------");
-                        System.err.println("Local translation: " + selectedjoint.getLocalTranslation());
-                        System.err.println("Local rotation: " + selectedjoint.getLocalRotation());
-                        System.err.println("Local scale: " + selectedjoint.getLocalScale());
-                        System.err.println("---");
-                        System.err.println("Model translation: " + selectedjoint.getModelTransform().getTranslation());
-                        System.err.println("Model rotation: " + selectedjoint.getModelTransform().getRotation());
-                        System.err.println("Model scale: " + selectedjoint.getModelTransform().getScale());
-                        System.err.println("---");
-                        System.err.println("Bind inverse Transform: ");
-                        System.err.println(selectedjoint.getInverseModelBindMatrix());
-                        return;
+
+            if (name.equals(PICK_JOINT)) {
+                if (isPressed) {
+                    // Start counting click delay on mouse press
+                    clickDelay = 0;
+
+                } else if (clickDelay < CLICK_MAX_DELAY) {
+                    // Process click only if it's a quick release (not a hold)
+                    Ray ray = screenPointToRay(cam, inputManager.getCursorPosition());
+                    results.clear();
+                    debugNode.collideWith(ray, results);
+
+                    if (results.size() == 0) {
+                        // If no collision, deselect all joints in all armatures
+                        for (ArmatureDebugger ad : armatures.values()) {
+                            ad.select(null);
+                        }
+                    } else {
+                        // Get the closest geometry hit by the ray
+                        Geometry target = results.getClosestCollision().getGeometry();
+                        logger.log(Level.INFO, "Pick: {0}", target);
+
+                        for (ArmatureDebugger ad : armatures.values()) {
+                            Joint selectedjoint = ad.select(target);
+
+                            if (selectedjoint != null) {
+                                // If a joint was selected, notify it and print its properties
+                                notifySelectionListeners(selectedjoint);
+                                printJointInfo(selectedjoint, ad);
+                                break;
+                            }
+                        }
                     }
                 }
             }
-            if (name.equals("toggleJoints") && isPressed) {
-                displayAllJoints = !displayAllJoints;
+            else if (name.equals(TOGGLE_JOINTS) && isPressed) {
+                displayNonDeformingJoints = !displayNonDeformingJoints;
                 for (ArmatureDebugger ad : armatures.values()) {
-                    ad.displayNonDeformingJoint(displayAllJoints);
+                    ad.displayNonDeformingJoint(displayNonDeformingJoints);
                 }
             }
         }
+
+        private void printJointInfo(Joint selectedJoint, ArmatureDebugger ad) {
+            if (enableJointInfoLogging) {
+                String info = "\n-----------------------\n" +
+                        "Selected Joint : " + selectedJoint.getName() + " in armature " + ad.getName() + "\n" +
+                        "Root Bone : " + (selectedJoint.getParent() == null) + "\n" +
+                        "-----------------------\n" +
+                        "Local translation: " + selectedJoint.getLocalTranslation() + "\n" +
+                        "Local rotation: " + selectedJoint.getLocalRotation() + "\n" +
+                        "Local scale: " + selectedJoint.getLocalScale() + "\n" +
+                        "---\n" +
+                        "Model translation: " + selectedJoint.getModelTransform().getTranslation() + "\n" +
+                        "Model rotation: " + selectedJoint.getModelTransform().getRotation() + "\n" +
+                        "Model scale: " + selectedJoint.getModelTransform().getScale() + "\n" +
+                        "---\n" +
+                        "Bind inverse Transform: \n" +
+                        selectedJoint.getInverseModelBindMatrix();
+
+                logger.log(Level.INFO, info);
+            }
+        }
+
+        /**
+         * Creates a `Ray` from a 2D screen point (e.g., mouse cursor position).
+         *
+         * @param cam The camera to use for ray projection.
+         * @param screenPoint The 2D screen coordinates.
+         * @return A `Ray` originating from the near plane and extending into the scene.
+         */
+        private Ray screenPointToRay(Camera cam, Vector2f screenPoint) {
+            TempVars vars = TempVars.get();
+            Vector3f nearPoint = vars.vect1;
+            Vector3f farPoint = vars.vect2;
+
+            // Get the world coordinates for the near and far points
+            cam.getWorldCoordinates(screenPoint, 0, nearPoint);
+            cam.getWorldCoordinates(screenPoint, 1, farPoint);
+
+            // Calculate direction and normalize
+            Vector3f direction = farPoint.subtractLocal(nearPoint).normalizeLocal();
+            Ray ray = new Ray(nearPoint, direction);
+
+            vars.release();
+            return ray;
+        }
     };
 
-//    public Map<Skeleton, Bone> getSelectedBones() {
-//        return selectedBones;
-//    }
+    /**
+     * Notifies all registered {@code Consumer<Joint>} listeners about the selected joint.
+     *
+     * @param selectedJoint The joint that was selected.
+     */
+    private void notifySelectionListeners(Joint selectedJoint) {
+        for (Consumer<Joint> listener : selectionListeners) {
+            listener.accept(selectedJoint);
+        }
+    }
+
+    /**
+     * Adds a listener that will be notified when a joint is selected.
+     *
+     * @param listener The {@code Consumer<Joint>} listener to add.
+     */
+    public void addSelectionListener(Consumer<Joint> listener) {
+        selectionListeners.add(listener);
+    }
+
+    /**
+     * Removes a previously added selection listener.
+     *
+     * @param listener The {@code Consumer<Joint>} listener to remove.
+     */
+    public void removeSelectionListener(Consumer<Joint> listener) {
+        selectionListeners.remove(listener);
+    }
 
-    public Node getDebugNode() {
-        return debugNode;
+    /**
+     * Clears all registered selection listeners.
+     */
+    public void clearSelectionListeners() {
+        selectionListeners.clear();
     }
 
-    public void setDebugNode(Node debugNode) {
-        this.debugNode = debugNode;
+    /**
+     * Checks if the armature debug gizmos are set to always
+     * render on top of other scene geometry.
+     *
+     * @return true if gizmos always render on top, false otherwise.
+     */
+    public boolean isShowOnTop() {
+        return showOnTop;
     }
 
-    private class JointInfoVisitor extends SceneGraphVisitorAdapter {
+    /**
+     * Sets whether armature debug gizmos should always
+     * render on top of other scene geometry.
+     *
+     * @param showOnTop true to always show gizmos on top, false to respect depth.
+     */
+    public void setShowOnTop(boolean showOnTop) {
+        this.showOnTop = showOnTop;
+        if (vp != null) {
+            vp.setClearDepth(showOnTop);
+        }
+    }
 
-        List<Joint> deformingJoints = new ArrayList<>();
-        Armature armature;
+    /**
+     * Returns whether logging of detailed joint information to `System.err` is currently enabled.
+     *
+     * @return true if logging is enabled, false otherwise.
+     */
+    public boolean isJointInfoLoggingEnabled() {
+        return enableJointInfoLogging;
+    }
 
+    /**
+     * Sets whether logging of detailed joint information to `System.err` should be enabled.
+     *
+     * @param enableJointInfoLogging true to enable logging, false to disable.
+     */
+    public void setJointInfoLoggingEnabled(boolean enableJointInfoLogging) {
+        this.enableJointInfoLogging = enableJointInfoLogging;
+    }
+
+    /**
+     * A utility visitor class to traverse the scene graph and identify
+     * which joints in a given armature are actually deforming a mesh.
+     */
+    private static class JointInfoVisitor extends SceneGraphVisitorAdapter {
+
+        private final List<Joint> deformingJoints = new ArrayList<>();
+        private final Armature armature;
+
+        /**
+         * Constructs a JointInfoVisitor for a specific armature.
+         *
+         * @param armature The armature whose deforming joints are to be identified.
+         */
         public JointInfoVisitor(Armature armature) {
             this.armature = armature;
         }
 
+        /**
+         * Visits a Geometry node in the scene graph.
+         * For each Geometry, it checks all joints in the associated armature
+         * to see if they influence this mesh.
+         *
+         * @param geo The Geometry node being visited.
+         */
         @Override
-        public void visit(Geometry g) {
+        public void visit(Geometry geo) {
             for (Joint joint : armature.getJointList()) {
-                if (g.getMesh().isAnimatedByJoint(armature.getJointIndex(joint))) {
+                int index = armature.getJointIndex(joint);
+                if (geo.getMesh().isAnimatedByJoint(index)) {
                     deformingJoints.add(joint);
                 }
             }
         }
+
+    }
+
+    private static class ArmatureDebuggerLink extends AbstractControl {
+
+        private final Spatial target;
+
+        public ArmatureDebuggerLink(Spatial target) {
+            this.target = target;
+        }
+
+        @Override
+        protected void controlUpdate(float tpf) {
+            spatial.setLocalTransform(target.getWorldTransform());
+        }
+
+        @Override
+        protected void controlRender(RenderManager rm, ViewPort vp) {
+        }
     }
 }

+ 136 - 50
jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugger.java

@@ -1,7 +1,5 @@
-package com.jme3.scene.debug.custom;
-
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -31,9 +29,11 @@ package com.jme3.scene.debug.custom;
  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
+package com.jme3.scene.debug.custom;
 
 import com.jme3.anim.Armature;
 import com.jme3.anim.Joint;
+import com.jme3.anim.SkinningControl;
 import com.jme3.asset.AssetManager;
 import com.jme3.collision.Collidable;
 import com.jme3.collision.CollisionResults;
@@ -55,104 +55,176 @@ import java.util.List;
 public class ArmatureDebugger extends Node {
 
     /**
-     * The lines of the bones or the wires between their heads.
+     * The node responsible for rendering the bones/wires and their outlines.
      */
     private ArmatureNode armatureNode;
-
+    /**
+     * The {@link Armature} instance being debugged.
+     */
     private Armature armature;
-
+    /**
+     * A node containing all {@link Geometry} objects representing the joint points.
+     */
     private Node joints;
+    /**
+     * A node containing all {@link Geometry} objects representing the bone outlines.
+     */
     private Node outlines;
+    /**
+     * A node containing all {@link Geometry} objects representing the bone wires/lines.
+     */
     private Node wires;
     /**
      * The dotted lines between a bone's tail and the had of its children. Not
      * available if the length data was not provided.
      */
     private ArmatureInterJointsWire interJointWires;
-
+    /**
+     * Default constructor for `ArmatureDebugger`.
+     * Use {@link #ArmatureDebugger(String, Armature, List)} for a functional instance.
+     */
     public ArmatureDebugger() {
     }
 
     /**
-     * Creates a debugger with no length data. The wires will be a connection
-     * between the bones' heads only. The points will show the bones' heads only
-     * and no dotted line of inter bones connection will be visible.
+     * Convenience constructor that creates an {@code ArmatureDebugger} and immediately
+     * initializes its materials based on the provided {@code AssetManager}
+     * and {@code SkinningControl}.
+     *
+     * @param assetManager The {@link AssetManager} used to load textures and materials
+     *                     for the debug visualization.
+     * @param skControl    The {@link SkinningControl} from which to extract the
+     *                     {@link Armature} and its associated joints.
+     */
+    public ArmatureDebugger(AssetManager assetManager, SkinningControl skControl) {
+        this(null, skControl.getArmature(), skControl.getArmature().getJointList());
+        initialize(assetManager, null);
+    }
+
+    /**
+     * Creates an `ArmatureDebugger` instance without explicit bone length data.
+     * In this configuration, the visual representation will consist of wires
+     * connecting the bone heads, and points representing the bone heads.
+     * No dotted lines for inter-bone connections will be visible.
      *
-     * @param name     the name of the debugger's node
-     * @param armature the armature that will be shown
-     * @param deformingJoints a list of joints
+     * @param name            The name of this debugger's root node.
+     * @param armature        The {@link Armature} to be visualized.
+     * @param deformingJoints A {@link List} of {@link Joint} objects that are
+     *                        considered deforming joints.
      */
     public ArmatureDebugger(String name, Armature armature, List<Joint> deformingJoints) {
         super(name);
         this.armature = armature;
+        // Ensure the armature's world transforms are up-to-date before visualization.
         armature.update();
 
+        // Initialize the main container nodes for different visual elements.
         joints = new Node("joints");
         outlines = new Node("outlines");
         wires = new Node("bones");
         this.attachChild(joints);
         this.attachChild(outlines);
         this.attachChild(wires);
-        Node ndJoints = new Node("non deforming Joints");
-        Node ndOutlines = new Node("non deforming Joints outlines");
-        Node ndWires = new Node("non deforming Joints wires");
-        joints.attachChild(ndJoints);
-        outlines.attachChild(ndOutlines);
-        wires.attachChild(ndWires);
-        Node outlineDashed = new Node("Outlines Dashed");
-        Node wiresDashed = new Node("Wires Dashed");
-        wiresDashed.attachChild(new Node("dashed non defrom"));
-        outlineDashed.attachChild(new Node("dashed non defrom"));
+
+        // Create child nodes specifically for non-deforming joints' visualization
+        joints.attachChild(new Node("NonDeformingJoints"));
+        outlines.attachChild(new Node("NonDeformingOutlines"));
+        wires.attachChild(new Node("NonDeformingWires"));
+
+        Node outlineDashed = new Node("DashedOutlines");
+        outlineDashed.attachChild(new Node("DashedNonDeformingOutlines"));
         outlines.attachChild(outlineDashed);
+
+        Node wiresDashed = new Node("DashedWires");
+        wiresDashed.attachChild(new Node("DashedNonDeformingWires"));
         wires.attachChild(wiresDashed);
 
+        // Initialize the core ArmatureNode which handles the actual mesh generation.
         armatureNode = new ArmatureNode(armature, joints, wires, outlines, deformingJoints);
-
         this.attachChild(armatureNode);
 
+        // By default, non-deforming joints are hidden.
         displayNonDeformingJoint(false);
     }
 
+    /**
+     * Sets the visibility of non-deforming joints and their associated outlines and wires.
+     *
+     * @param display `true` to make non-deforming joints visible, `false` to hide them.
+     */
     public void displayNonDeformingJoint(boolean display) {
-        joints.getChild(0).setCullHint(display ? CullHint.Dynamic : CullHint.Always);
-        outlines.getChild(0).setCullHint(display ? CullHint.Dynamic : CullHint.Always);
-        wires.getChild(0).setCullHint(display ? CullHint.Dynamic : CullHint.Always);
-        ((Node) outlines.getChild(1)).getChild(0).setCullHint(display ? CullHint.Dynamic : CullHint.Always);
-        ((Node) wires.getChild(1)).getChild(0).setCullHint(display ? CullHint.Dynamic : CullHint.Always);
+        CullHint cullHint = display ? CullHint.Dynamic : CullHint.Always;
+
+        joints.getChild(0).setCullHint(cullHint);
+        outlines.getChild(0).setCullHint(cullHint);
+        wires.getChild(0).setCullHint(cullHint);
+
+        ((Node) outlines.getChild(1)).getChild(0).setCullHint(cullHint);
+        ((Node) wires.getChild(1)).getChild(0).setCullHint(cullHint);
     }
 
+    /**
+     * Initializes the materials and camera for the debugger's visual components.
+     * This method should be called after the `ArmatureDebugger` is added to a scene graph
+     * and an {@link AssetManager} and {@link Camera} are available.
+     *
+     * @param assetManager The {@link AssetManager} to load textures and materials.
+     * @param camera       The scene's primary {@link Camera}, used by the `ArmatureNode`
+     * for billboard rendering of joint points.
+     */
     public void initialize(AssetManager assetManager, Camera camera) {
 
         armatureNode.setCamera(camera);
 
-        Material matJoints = new Material(assetManager, "Common/MatDefs/Misc/Billboard.j3md");
-        Texture t = assetManager.loadTexture("Common/Textures/dot.png");
-        matJoints.setTexture("Texture", t);
-        matJoints.getAdditionalRenderState().setDepthTest(false);
-        matJoints.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
+        // Material for joint points (billboard dots).
+        Material matJoints = getJointMaterial(assetManager);
         joints.setQueueBucket(RenderQueue.Bucket.Translucent);
         joints.setMaterial(matJoints);
 
-        Material matWires = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
-        matWires.setBoolean("VertexColor", true);
-        matWires.getAdditionalRenderState().setLineWidth(1f);
+        // Material for bone wires/lines (unshaded, vertex colored).
+        Material matWires = getUnshadedMaterial(assetManager);
         wires.setMaterial(matWires);
 
-        Material matOutline = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
-        matOutline.setBoolean("VertexColor", true);
-        matOutline.getAdditionalRenderState().setLineWidth(1f);
+        // Material for dashed wires ("DashedLine.j3md" shader).
+        Material matWires2 = getDashedMaterial(assetManager);
+        wires.getChild(1).setMaterial(matWires2);
+
+        // Material for bone outlines (unshaded, vertex colored).
+        Material matOutline = getUnshadedMaterial(assetManager);
         outlines.setMaterial(matOutline);
 
-        Material matOutline2 = new Material(assetManager, "Common/MatDefs/Misc/DashedLine.j3md");
-        matOutline2.getAdditionalRenderState().setLineWidth(1);
+        // Material for dashed outlines ("DashedLine.j3md" shader).
+        Material matOutline2 = getDashedMaterial(assetManager);
         outlines.getChild(1).setMaterial(matOutline2);
+    }
 
-        Material matWires2 = new Material(assetManager, "Common/MatDefs/Misc/DashedLine.j3md");
-        matWires2.getAdditionalRenderState().setLineWidth(1);
-        wires.getChild(1).setMaterial(matWires2);
+    private Material getJointMaterial(AssetManager asm) {
+        Material mat = new Material(asm, "Common/MatDefs/Misc/Billboard.j3md");
+        Texture tex = asm.loadTexture("Common/Textures/dot.png");
+        mat.setTexture("Texture", tex);
+        mat.getAdditionalRenderState().setDepthTest(false);
+        mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
+        return mat;
+    }
+
+    private Material getUnshadedMaterial(AssetManager asm) {
+        Material mat = new Material(asm, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat.setBoolean("VertexColor", true);
+        mat.getAdditionalRenderState().setDepthTest(false);
+        return mat;
+    }
 
+    private Material getDashedMaterial(AssetManager asm) {
+        Material mat = new Material(asm, "Common/MatDefs/Misc/DashedLine.j3md");
+        mat.getAdditionalRenderState().setDepthTest(false);
+        return mat;
     }
 
+    /**
+     * Returns the {@link Armature} instance associated with this debugger.
+     *
+     * @return The {@link Armature} being debugged.
+     */
     public Armature getArmature() {
         return armature;
     }
@@ -168,21 +240,35 @@ public class ArmatureDebugger extends Node {
         return armatureNode.collideWith(other, results);
     }
 
-    protected Joint select(Geometry g) {
-        return armatureNode.select(g);
+    /**
+     * Selects and returns the {@link Joint} associated with a given {@link Geometry}.
+     * This is an internal helper method, likely used for picking operations.
+     *
+     * @param geo The {@link Geometry} representing a part of a joint.
+     * @return The {@link Joint} corresponding to the geometry, or `null` if not found.
+     */
+    protected Joint select(Geometry geo) {
+        return armatureNode.select(geo);
     }
 
     /**
-     * @return the armature wires
+     * Returns the {@link ArmatureNode} which is responsible for generating and
+     * managing the visual mesh of the bones and wires.
+     *
+     * @return The {@link ArmatureNode} instance.
      */
     public ArmatureNode getBoneShapes() {
         return armatureNode;
     }
 
     /**
-     * @return the dotted line between bones (can be null)
+     * Returns the {@link ArmatureInterJointsWire} instance, which represents the
+     * dotted lines connecting a bone's tail to the head of its children.
+     * This will be `null` if the debugger was created without bone length data.
+     *
+     * @return The {@link ArmatureInterJointsWire} instance, or `null` if not present.
      */
     public ArmatureInterJointsWire getInterJointWires() {
         return interJointWires;
     }
-}
+}

+ 26 - 10
jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureInterJointsWire.java

@@ -1,7 +1,5 @@
-package com.jme3.scene.debug.custom;
-
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -31,7 +29,7 @@ package com.jme3.scene.debug.custom;
  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
-
+package com.jme3.scene.debug.custom;
 
 import com.jme3.math.Vector3f;
 import com.jme3.scene.Mesh;
@@ -46,20 +44,38 @@ import java.nio.FloatBuffer;
  * @author Marcin Roguski (Kaelthas)
  */
 public class ArmatureInterJointsWire extends Mesh {
-    private final Vector3f tmp = new Vector3f();
 
+    /**
+     * A temporary {@link Vector3f} used for calculations to avoid object allocation.
+     */
+    private final Vector3f tempVec = new Vector3f();
+
+    /**
+     * For serialization only. Do not use.
+     */
+    protected ArmatureInterJointsWire() {
+    }
 
+    /**
+     * Creates a new {@code ArmatureInterJointsWire} mesh.
+     * The mesh will be set up to draw lines from the {@code start} vector to each of the {@code ends} vectors.
+     *
+     * @param start The starting point of the lines (e.g., the bone tail's position). Not null.
+     * @param ends An array of ending points for the lines (e.g., the children's head positions). Not null.
+     */
     public ArmatureInterJointsWire(Vector3f start, Vector3f[] ends) {
         setMode(Mode.Lines);
         updateGeometry(start, ends);
     }
 
     /**
-     * For serialization only. Do not use.
+     * Updates the geometry of this mesh based on the provided start and end points.
+     * This method re-generates the position, texture coordinate, normal, and index buffers
+     * for the mesh.
+     *
+     * @param start The new starting point for the lines. Not null.
+     * @param ends An array of new ending points for the lines. Not null.
      */
-    protected ArmatureInterJointsWire() {
-    }
-
     protected void updateGeometry(Vector3f start, Vector3f[] ends) {
         float[] pos = new float[ends.length * 3 + 3];
         pos[0] = start.x;
@@ -78,7 +94,7 @@ public class ArmatureInterJointsWire extends Mesh {
         texCoord[0] = 0;
         texCoord[1] = 0;
         for (int i = 0; i < ends.length * 2; i++) {
-            texCoord[i + 2] = tmp.set(start).subtractLocal(ends[i / 2]).length();
+            texCoord[i + 2] = tempVec.set(start).subtractLocal(ends[i / 2]).length();
         }
         setBuffer(Type.TexCoord, 2, texCoord);
 

+ 215 - 57
jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureNode.java

@@ -1,7 +1,5 @@
-package com.jme3.scene.debug.custom;
-
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -31,43 +29,73 @@ package com.jme3.scene.debug.custom;
  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
+package com.jme3.scene.debug.custom;
 
 import com.jme3.anim.Armature;
 import com.jme3.anim.Joint;
-import com.jme3.collision.*;
-import com.jme3.math.*;
+import com.jme3.collision.Collidable;
+import com.jme3.collision.CollisionResult;
+import com.jme3.collision.CollisionResults;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.MathUtils;
+import com.jme3.math.Ray;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
 import com.jme3.renderer.Camera;
 import com.jme3.renderer.queue.RenderQueue;
-import com.jme3.scene.*;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.VertexBuffer;
 import com.jme3.scene.shape.Line;
 
 import java.nio.FloatBuffer;
-import java.util.*;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 /**
- * The class that displays either wires between the bones' heads if no length
- * data is supplied and full bones' shapes otherwise.
+ * Renders an {@link Armature} for debugging purposes. It can display either
+ * wires connecting the heads of bones (if no length data is available) or
+ * full bone shapes (from head to tail) when length data is supplied.
  */
 public class ArmatureNode extends Node {
 
+    /**
+     * The size of the picking box in pixels for joint selection.
+     */
     public static final float PIXEL_BOX = 10f;
     /**
      * The armature to be displayed.
      */
     private final Armature armature;
     /**
-     * The map between the bone index and its length.
+     * Maps a {@link Joint} to its corresponding {@link Geometry} array.
+     * The array typically contains [jointGeometry, boneWireGeometry, boneOutlineGeometry].
      */
     private final Map<Joint, Geometry[]> jointToGeoms = new HashMap<>();
+    /**
+     * Maps a {@link Geometry} to its associated {@link Joint}. Used for picking.
+     */
     private final Map<Geometry, Joint> geomToJoint = new HashMap<>();
+    /**
+     * The currently selected joint.
+     */
     private Joint selectedJoint = null;
-    private final Vector3f tmp = new Vector3f();
-    private final Vector2f tmpv2 = new Vector2f();
+
+    // Temporary vectors for calculations to avoid repeated allocations
+    private final Vector3f tempVec3f = new Vector3f();
+    private final Vector2f tempVec2f = new Vector2f();
+
+    // Color constants for rendering
     private static final ColorRGBA selectedColor = ColorRGBA.Orange;
-    private static final ColorRGBA selectedColorJ = ColorRGBA.Yellow;
+    private static final ColorRGBA selectedColorJoint = ColorRGBA.Yellow;
     private static final ColorRGBA outlineColor = ColorRGBA.LightGray;
     private static final ColorRGBA baseColor = new ColorRGBA(0.05f, 0.05f, 0.05f, 1f);
 
+    /**
+     * The camera used for 2D picking calculations.
+     */
     private Camera camera;
 
 
@@ -88,27 +116,36 @@ public class ArmatureNode extends Node {
         setColor(origin, ColorRGBA.Green);
         attach(joints, true, origin);
 
+        // Recursively create geometries for all joints and bones in the armature
         for (Joint joint : armature.getRoots()) {
             createSkeletonGeoms(joint, joints, wires, outlines, deformingJoints);
         }
         this.updateModelBound();
-
     }
 
+    /**
+     * Recursively creates the geometries for a given joint and its children.
+     *
+     * @param joint           The current joint for which to create geometries.
+     * @param joints          The node for joint geometries.
+     * @param wires           The node for bone wire geometries.
+     * @param outlines        The node for bone outline geometries.
+     * @param deformingJoints A list of deforming joints.
+     */
     protected final void createSkeletonGeoms(Joint joint, Node joints, Node wires, Node outlines, List<Joint> deformingJoints) {
         Vector3f start = joint.getModelTransform().getTranslation().clone();
 
         Vector3f[] ends = null;
         if (!joint.getChildren().isEmpty()) {
             ends = new Vector3f[joint.getChildren().size()];
-        }
-
-        for (int i = 0; i < joint.getChildren().size(); i++) {
-            ends[i] = joint.getChildren().get(i).getModelTransform().getTranslation().clone();
+            for (int i = 0; i < ends.length; i++) {
+                ends[i] = joint.getChildren().get(i).getModelTransform().getTranslation().clone();
+            }
         }
 
         boolean deforms = deformingJoints.contains(joint);
 
+        // Create geometry for the joint head
         Geometry jGeom = new Geometry(joint.getName() + "Joint", new JointShape());
         jGeom.setLocalTranslation(start);
         attach(joints, deforms, jGeom);
@@ -134,8 +171,8 @@ public class ArmatureNode extends Node {
             setColor(bGeom, outlinesAttach == null ? outlineColor : baseColor);
             geomToJoint.put(bGeom, joint);
             bGeom.setUserData("start", getWorldTransform().transformVector(start, start));
-            for (int i = 0; i < ends.length; i++) {
-                getWorldTransform().transformVector(ends[i], ends[i]);
+            for (Vector3f end : ends) {
+                getWorldTransform().transformVector(end, end);
             }
             bGeom.setUserData("end", ends);
             bGeom.setQueueBucket(RenderQueue.Bucket.Transparent);
@@ -148,11 +185,17 @@ public class ArmatureNode extends Node {
         }
         jointToGeoms.put(joint, new Geometry[]{jGeom, bGeom, bGeomO});
 
+        // Recursively call for children
         for (Joint child : joint.getChildren()) {
             createSkeletonGeoms(child, joints, wires, outlines, deformingJoints);
         }
     }
 
+    /**
+     * Sets the camera to be used for 2D picking calculations.
+     *
+     * @param camera The camera to set.
+     */
     public void setCamera(Camera camera) {
         this.camera = camera;
     }
@@ -165,53 +208,83 @@ public class ArmatureNode extends Node {
         }
     }
 
-    protected Joint select(Geometry g) {
-        if (g == null) {
+    /**
+     * Selects a joint based on its associated geometry.
+     * If the selected geometry is already the current selection, no change occurs.
+     * Resets the selection if {@code geometry} is null.
+     *
+     * @param geo The geometry representing the joint or bone to select.
+     * @return The newly selected {@link Joint}, or null if no joint was selected or the selection was reset.
+     */
+    protected Joint select(Geometry geo) {
+        if (geo == null) {
             resetSelection();
             return null;
         }
-        Joint j = geomToJoint.get(g);
-        if (j != null) {
-            if (selectedJoint == j) {
+        Joint jointToSelect = geomToJoint.get(geo);
+        if (jointToSelect != null) {
+            if (selectedJoint == jointToSelect) {
                 return null;
             }
             resetSelection();
-            selectedJoint = j;
+            selectedJoint = jointToSelect;
             Geometry[] geomArray = jointToGeoms.get(selectedJoint);
-            setColor(geomArray[0], selectedColorJ);
+            // Color the joint head
+            setColor(geomArray[0], selectedColorJoint);
 
+            // Color the bone wire
             if (geomArray[1] != null) {
                 setColor(geomArray[1], selectedColor);
             }
 
+            // Restore outline color if present (as it's often the base color when bone is selected)
             if (geomArray[2] != null) {
                 setColor(geomArray[2], baseColor);
             }
-            return j;
+            return jointToSelect;
         }
         return null;
     }
 
+    /**
+     * Resets the color of the currently selected joint and bone geometries to their default colors
+     * and clears the {@code selectedJoint}.
+     */
     private void resetSelection() {
         if (selectedJoint == null) {
             return;
         }
         Geometry[] geoms = jointToGeoms.get(selectedJoint);
+        // Reset joint head color
         setColor(geoms[0], ColorRGBA.White);
+
+        // Reset bone wire color (depends on whether it has an outline)
         if (geoms[1] != null) {
             setColor(geoms[1], geoms[2] == null ? outlineColor : baseColor);
         }
+
+        // Reset bone outline color
         if (geoms[2] != null) {
             setColor(geoms[2], outlineColor);
         }
         selectedJoint = null;
     }
 
+    /**
+     * Returns the currently selected joint.
+     *
+     * @return The {@link Joint} that is currently selected, or null if no joint is selected.
+     */
     protected Joint getSelectedJoint() {
         return selectedJoint;
     }
 
-
+    /**
+     * Updates the geometries associated with a given joint and its children to reflect their
+     * current model transforms. This method is called recursively.
+     *
+     * @param joint The joint to update.
+     */
     protected final void updateSkeletonGeoms(Joint joint) {
         Geometry[] geoms = jointToGeoms.get(joint);
         if (geoms != null) {
@@ -232,70 +305,142 @@ public class ArmatureNode extends Node {
                         updateBoneMesh(bGeomO, start, ends);
                     }
                     bGeom.setUserData("start", getWorldTransform().transformVector(start, start));
-                    for (int i = 0; i < ends.length; i++) {
-                        getWorldTransform().transformVector(ends[i], ends[i]);
+                    for (Vector3f end : ends) {
+                        getWorldTransform().transformVector(end, end);
                     }
                     bGeom.setUserData("end", ends);
-
                 }
             }
         }
 
+        // Recursively update children
         for (Joint child : joint.getChildren()) {
             updateSkeletonGeoms(child);
         }
     }
 
+    /**
+     * Sets the color of the head geometry for a specific joint.
+     *
+     * @param joint The joint whose head color is to be set.
+     * @param color The new color for the joint head.
+     */
+    public void setHeadColor(Joint joint, ColorRGBA color) {
+        Geometry[] geomArray = jointToGeoms.get(joint);
+        setColor(geomArray[0], color);
+    }
+
+    /**
+     * Sets the color of all joint head geometries.
+     *
+     * @param color The new color for all joint heads.
+     */
+    public void setHeadColor(ColorRGBA color) {
+        for (Geometry[] geomArray : jointToGeoms.values()) {
+            setColor(geomArray[0], color);
+        }
+    }
+
+    /**
+     * Sets the color of all bone line geometries.
+     *
+     * @param color The new color for all bone lines.
+     */
+    public void setLineColor(ColorRGBA color) {
+        for (Geometry[] geomArray : jointToGeoms.values()) {
+            if (geomArray[1] != null) {
+                setColor(geomArray[1], color);
+            }
+        }
+    }
+
+    /**
+     * Performs a 2D pick operation to find joints or bones near the given cursor position.
+     * This method primarily checks for joint heads within a {@link #PIXEL_BOX} box
+     * around the cursor, and then checks for bone wires.
+     *
+     * @param cursor  The 2D screen coordinates of the pick ray origin.
+     * @param results The {@link CollisionResults} to store the pick results.
+     * @return The number of collisions found.
+     */
     public int pick(Vector2f cursor, CollisionResults results) {
+        if (camera == null) {
+            return 0;
+        }
 
-        for (Geometry g : geomToJoint.keySet()) {
-            if (g.getMesh() instanceof JointShape) {
-                camera.getScreenCoordinates(g.getWorldTranslation(), tmp);
-                if (cursor.x <= tmp.x + PIXEL_BOX && cursor.x >= tmp.x - PIXEL_BOX
-                        && cursor.y <= tmp.y + PIXEL_BOX && cursor.y >= tmp.y - PIXEL_BOX) {
+        int collisions = 0;
+        for (Geometry geo : geomToJoint.keySet()) {
+            if (geo.getMesh() instanceof JointShape) {
+                camera.getScreenCoordinates(geo.getWorldTranslation(), tempVec3f);
+                if (cursor.x <= tempVec3f.x + PIXEL_BOX && cursor.x >= tempVec3f.x - PIXEL_BOX
+                        && cursor.y <= tempVec3f.y + PIXEL_BOX && cursor.y >= tempVec3f.y - PIXEL_BOX) {
                     CollisionResult res = new CollisionResult();
-                    res.setGeometry(g);
+                    res.setGeometry(geo);
                     results.addCollision(res);
+                    collisions++;
                 }
             }
         }
-        return 0;
+        return collisions;
     }
 
+    /**
+     * Collides this {@code ArmatureNode} with a {@link Collidable} object, typically a {@link Ray}.
+     * It prioritizes 2D picking for joint heads and then performs a distance-based check for bone wires.
+     *
+     * @param other   The {@link Collidable} object to collide with.
+     * @param results The {@link CollisionResults} to store the collision information.
+     * @return The number of collisions found.
+     */
     @Override
     public int collideWith(Collidable other, CollisionResults results) {
-        if (!(other instanceof Ray)) {
+        if (!(other instanceof Ray) || camera == null) {
             return 0;
         }
 
-        // first try a 2D pick;
-        camera.getScreenCoordinates(((Ray)other).getOrigin(),tmp);
-        tmpv2.x = tmp.x;
-        tmpv2.y = tmp.y;
-        int nbHit = pick(tmpv2, results);
-        if (nbHit > 0) {
-            return nbHit;
+        // First, try a 2D pick for joint heads
+        camera.getScreenCoordinates(((Ray) other).getOrigin(), tempVec3f);
+        tempVec2f.x = tempVec3f.x;
+        tempVec2f.y = tempVec3f.y;
+        int hitCount = pick(tempVec2f, results);
+
+        // If 2D pick found hits, return them. Otherwise, proceed with bone wire collision.
+        if (hitCount > 0) {
+            return hitCount;
         }
 
+        // Check for bone wire collisions
         for (Geometry g : geomToJoint.keySet()) {
             if (g.getMesh() instanceof JointShape) {
+                // Skip joint heads, already handled by 2D pick
                 continue;
             }
+
             Vector3f start = g.getUserData("start");
             Vector3f[] ends = g.getUserData("end");
-            for (int i = 0; i < ends.length; i++) {
-                float len = MathUtils.raySegmentShortestDistance((Ray) other, start, ends[i], camera);
-                if (len > 0 && len < PIXEL_BOX) {
+
+            for (Vector3f end : ends) {
+                // Calculate the shortest distance from ray to bone segment
+                float dist = MathUtils.raySegmentShortestDistance((Ray) other, start, end, camera);
+                if (dist > 0 && dist < PIXEL_BOX) {
                     CollisionResult res = new CollisionResult();
                     res.setGeometry(g);
                     results.addCollision(res);
-                    nbHit++;
+                    hitCount++;
                 }
             }
         }
-        return nbHit;
+        return hitCount;
     }
 
+    /**
+     * Updates the mesh of a bone geometry (either {@link ArmatureInterJointsWire} or {@link Line})
+     * with new start and end points.
+     *
+     * @param geom  The bone geometry whose mesh needs updating.
+     * @param start The new starting point of the bone.
+     * @param ends  The new ending points of the bone (can be multiple for {@link ArmatureInterJointsWire}).
+     */
     private void updateBoneMesh(Geometry geom, Vector3f start, Vector3f[] ends) {
         if (geom.getMesh() instanceof ArmatureInterJointsWire) {
             ((ArmatureInterJointsWire) geom.getMesh()).updatePoints(start, ends);
@@ -305,18 +450,31 @@ public class ArmatureNode extends Node {
         geom.updateModelBound();
     }
 
-    private void setColor(Geometry g, ColorRGBA color) {
-        float[] colors = new float[g.getMesh().getVertexCount() * 4];
-        for (int i = 0; i < g.getMesh().getVertexCount() * 4; i += 4) {
+    /**
+     * Sets the color of a given geometry's vertex buffer.
+     * This method creates a new color buffer or updates an existing one with the specified color.
+     *
+     * @param geo   The geometry whose color is to be set.
+     * @param color The {@link ColorRGBA} to apply.
+     */
+    private void setColor(Geometry geo, ColorRGBA color) {
+        Mesh mesh = geo.getMesh();
+        int vertexCount = mesh.getVertexCount();
+
+        float[] colors = new float[vertexCount * 4];
+        for (int i = 0; i < colors.length; i += 4) {
             colors[i] = color.r;
             colors[i + 1] = color.g;
             colors[i + 2] = color.b;
             colors[i + 3] = color.a;
         }
-        VertexBuffer colorBuff = g.getMesh().getBuffer(VertexBuffer.Type.Color);
+
+        VertexBuffer colorBuff = geo.getMesh().getBuffer(VertexBuffer.Type.Color);
         if (colorBuff == null) {
-            g.getMesh().setBuffer(VertexBuffer.Type.Color, 4, colors);
+            // If no color buffer exists, create a new one
+            geo.getMesh().setBuffer(VertexBuffer.Type.Color, 4, colors);
         } else {
+            // If a color buffer exists, update its data
             FloatBuffer cBuff = (FloatBuffer) colorBuff.getData();
             cBuff.rewind();
             cBuff.put(colors);

+ 7 - 0
jme3-core/src/main/java/com/jme3/scene/threadwarden/IllegalThreadSceneGraphMutation.java

@@ -0,0 +1,7 @@
+package com.jme3.scene.threadwarden;
+
+public class IllegalThreadSceneGraphMutation extends IllegalStateException{
+    public IllegalThreadSceneGraphMutation(String message){
+        super(message);
+    }
+}

+ 160 - 0
jme3-core/src/main/java/com/jme3/scene/threadwarden/SceneGraphThreadWarden.java

@@ -0,0 +1,160 @@
+package com.jme3.scene.threadwarden;
+
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Thread warden keeps track of mutations to the scene graph and ensures that they are only done on the main thread.
+ * IF the parent node is marked as being reserved for the main thread (which basically means it's connected to the
+ * root node)
+ * <p>
+ *     Only has an effect if asserts are on
+ * </p>
+ */
+public class SceneGraphThreadWarden {
+
+    private static final Logger logger = Logger.getLogger(SceneGraphThreadWarden.class.getName());
+    
+    /**
+     * If THREAD_WARDEN_ENABLED is true AND asserts are on the checks are made.
+     * This parameter is here to allow asserts to run without thread warden checks (by setting this parameter to false)
+     */
+    public static boolean THREAD_WARDEN_ENABLED = !Boolean.getBoolean("nothreadwarden");
+
+    public static boolean ASSERTS_ENABLED = false;
+
+    static{
+        //noinspection AssertWithSideEffects
+        assert ASSERTS_ENABLED = true;
+    }
+
+    public static Thread mainThread;
+    public static final Set<Spatial> spatialsThatAreMainThreadReserved = Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));
+
+    /**
+     * Marks the given node as being reserved for the main thread.
+     * Additionally, sets the current thread as the main thread (if it hasn't already been set)
+     * @param rootNode the root node of the scene graph. This is used to determine if a spatial is a child of the root node.
+     *                 (Can add multiple "root" nodes, e.g. gui nodes or overlay nodes)
+     */
+    public static boolean setup(Node rootNode){
+        if(checksDisabled()){
+            return true;
+        }
+        Thread thisThread = Thread.currentThread();
+        if(mainThread != null && mainThread != thisThread ){
+            throw new IllegalStateException("The main thread has already been set to " + mainThread.getName() + " but now it's being set to " + Thread.currentThread().getName());
+        }
+        mainThread = thisThread;
+        setTreeRestricted(rootNode);
+
+        return true; // return true so can be a "side effect" of an assert
+    }
+
+    /**
+     * Disables the thread warden checks (even when other asserts are on).
+     * <p>
+     * Alternatively can be disabled by adding the -Dnothreadwarden=true parameter
+     * </p>
+     */
+    public static void disableChecks(){
+        THREAD_WARDEN_ENABLED = false;
+    }
+
+    /**
+     * Runs through the entire tree and sets the restriction state of all nodes below the given node
+     * @param spatial the node (and children) to set the restriction state of
+     */
+    private static void setTreeRestricted(Spatial spatial){
+        spatialsThatAreMainThreadReserved.add(spatial);
+        if(spatial instanceof Node){
+            for(Spatial child : ((Node) spatial).getChildren()){
+                setTreeRestricted(child);
+            }
+        }
+    }
+
+    /**
+     * Releases this tree from being only allowed to be mutated on the main thread
+     * @param spatial the node (and children) to release the restriction state of.
+     */
+    private static void setTreeNotRestricted(Spatial spatial){
+        spatialsThatAreMainThreadReserved.remove(spatial);
+        if(spatial instanceof Node){
+            for(Spatial child : ((Node) spatial).getChildren()){
+                setTreeNotRestricted(child);
+            }
+        }
+    }
+
+    @SuppressWarnings("SameReturnValue")
+    public static boolean updateRequirement(Spatial spatial, Node newParent){
+        if(checksDisabled()){
+            return true;
+        }
+
+        boolean shouldNowBeRestricted = newParent !=null && spatialsThatAreMainThreadReserved.contains(newParent);
+        boolean wasPreviouslyRestricted = spatialsThatAreMainThreadReserved.contains(spatial);
+
+        if(shouldNowBeRestricted || wasPreviouslyRestricted ){
+            assertOnCorrectThread(spatial);
+        }
+
+        if(shouldNowBeRestricted == wasPreviouslyRestricted){
+            return true;
+        }
+        if(shouldNowBeRestricted){
+            setTreeRestricted(spatial);
+        }else{
+            setTreeNotRestricted(spatial);
+        }
+
+        return true; // return true so can be a "side effect" of an assert
+    }
+
+    public static boolean reset(){
+        spatialsThatAreMainThreadReserved.clear();
+        mainThread = null;
+        THREAD_WARDEN_ENABLED = !Boolean.getBoolean("nothreadwarden");
+        return true; // return true so can be a "side effect" of an assert
+    }
+
+    private static boolean checksDisabled(){
+       return !THREAD_WARDEN_ENABLED || !ASSERTS_ENABLED;
+    }
+
+    @SuppressWarnings("SameReturnValue")
+    public static boolean assertOnCorrectThread(Spatial spatial){
+        if(checksDisabled()){
+            return true;
+        }
+        if(spatialsThatAreMainThreadReserved.contains(spatial)){
+            if(Thread.currentThread() != mainThread){
+                // log as well as throw an exception because we are running in a thread, if we are in an executor service the exception
+                // might not make itself known until `get` is called on the future (and JME might crash before that happens).
+                String message = "The spatial " + spatial + " was mutated on a thread other than the main thread, was mutated on " + Thread.currentThread().getName();
+                IllegalThreadSceneGraphMutation ex = new IllegalThreadSceneGraphMutation(message);
+                logger.log(Level.WARNING, message, ex);
+
+                throw ex;
+            }
+        }
+        return true; // return true so can be a "side effect" of an assert
+    }
+
+    public static String getTurnOnAssertsPrompt(){
+        if(ASSERTS_ENABLED){
+            return "";
+        } else{
+            return "To get more accurate debug consider turning on asserts. This will allow JME to do additional checks which *may* find the source of the problem. To do so, add -ea to the JVM arguments.";
+        }
+    }
+
+}
+

+ 52 - 6
jme3-core/src/main/java/com/jme3/shader/VarType.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2024 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -45,6 +45,9 @@ import com.jme3.texture.TextureArray;
 import com.jme3.texture.TextureCubeMap;
 import com.jme3.texture.TextureImage;
 
+/**
+ * Enum representing various GLSL variable types and their corresponding Java types.
+ */
 public enum VarType {
     
     Float("float", float.class, Float.class),
@@ -59,7 +62,7 @@ public enum VarType {
     Vector4Array(true, false, "vec4", Vector4f[].class),
     
     Int("int", int.class, Integer.class),
-    Boolean("bool", Boolean.class, boolean.class),
+    Boolean("bool", boolean.class, Boolean.class),
 
     Matrix3(true, false, "mat3", Matrix3f.class),
     Matrix4(true, false, "mat4", Matrix4f.class),
@@ -83,8 +86,14 @@ public enum VarType {
     private boolean textureType = false;
     private boolean imageType = false;
     private final String glslType;
-    private Class<?>[] javaTypes;
+    private final Class<?>[] javaTypes;
 
+    /**
+     * Constructs a VarType with the specified GLSL type and corresponding Java types.
+     *
+     * @param glslType  the GLSL type name(s)
+     * @param javaTypes the Java classes mapped to this GLSL type
+     */
     VarType(String glslType, Class<?>... javaTypes) {
         this.glslType = glslType;
         if (javaTypes != null) {
@@ -94,6 +103,14 @@ public enum VarType {
         }
     }
 
+    /**
+     * Constructs a VarType with additional flags for multi-data and texture types.
+     *
+     * @param multiData   true if this type uses multiple data elements (e.g. arrays, matrices)
+     * @param textureType true if this type represents a texture sampler
+     * @param glslType    the GLSL type name(s)
+     * @param javaTypes   the Java classes mapped to this GLSL type
+     */
     VarType(boolean multiData, boolean textureType, String glslType, Class<?>... javaTypes) {
         this.usesMultiData = multiData;
         this.textureType = textureType;
@@ -104,7 +121,16 @@ public enum VarType {
             this.javaTypes = new Class<?>[0];
         }
     }
-    
+
+    /**
+     * Constructs a VarType with flags for multi-data, texture, and image types.
+     *
+     * @param multiData   true if this type uses multiple data elements
+     * @param textureType true if this type represents a texture sampler
+     * @param imageType   true if this type represents an image
+     * @param glslType    the GLSL type name(s)
+     * @param javaTypes   the Java classes mapped to this GLSL type
+     */
     VarType(boolean multiData, boolean textureType, boolean imageType, String glslType, Class<?>... javaTypes) {
         this(multiData, textureType, glslType, javaTypes);
         this.imageType = imageType;
@@ -127,25 +153,45 @@ public enum VarType {
 
     /**
      * Get the java types mapped to this VarType
-     * 
+     *
      * @return an array of classes mapped to this VarType
      */
     public Class<?>[] getJavaType() {
         return javaTypes;
     }
 
+    /**
+     * Returns whether this VarType represents a texture sampler type.
+     *
+     * @return true if this is a texture type, false otherwise
+     */
     public boolean isTextureType() {
         return textureType;
     }
-    
+
+    /**
+     * Returns whether this VarType represents an image type.
+     *
+     * @return true if this is an image type, false otherwise
+     */
     public boolean isImageType() {
         return imageType;
     }
 
+    /**
+     * Returns whether this VarType uses multiple data elements (e.g. arrays or matrices).
+     *
+     * @return true if this type uses multiple data elements, false otherwise
+     */
     public boolean usesMultiData() {
         return usesMultiData;
     }
 
+    /**
+     * Returns the GLSL type name(s) associated with this VarType.
+     *
+     * @return the GLSL type string (e.g. "float", "vec3", "sampler2D")
+     */
     public String getGlslType() {
         return glslType;
     }

+ 18 - 30
jme3-core/src/main/java/com/jme3/shadow/AbstractShadowFilter.java

@@ -32,10 +32,6 @@
 package com.jme3.shadow;
 
 import com.jme3.asset.AssetManager;
-import com.jme3.export.InputCapsule;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.OutputCapsule;
 import com.jme3.material.Material;
 import com.jme3.material.RenderState;
 import com.jme3.math.Matrix4f;
@@ -45,38 +41,38 @@ import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.ViewPort;
 import com.jme3.renderer.queue.RenderQueue;
 import com.jme3.texture.FrameBuffer;
+import com.jme3.util.TempVars;
 import com.jme3.util.clone.Cloner;
 import com.jme3.util.clone.JmeCloneable;
 
-import java.io.IOException;
-
 /**
  * Generic abstract filter that holds common implementations for the different
  * shadow filters
  *
  * @author Rémy Bouquet aka Nehon
  */
-public abstract class AbstractShadowFilter<T extends AbstractShadowRenderer> extends Filter implements Cloneable, JmeCloneable {
+public abstract class AbstractShadowFilter<T extends AbstractShadowRenderer> extends Filter implements JmeCloneable {
 
     protected T shadowRenderer;
     protected ViewPort viewPort;
 
+    private final Vector4f tempVec4 = new Vector4f();
+    private final Matrix4f tempMat4 = new Matrix4f();
+
     /**
      * For serialization only. Do not use.
      */
     protected AbstractShadowFilter() {
     }
-    
+
     /**
-     * Abstract class constructor
+     * Creates an AbstractShadowFilter. Subclasses invoke this constructor.
      *
-     * @param manager the application asset manager
-     * @param shadowMapSize the size of the rendered shadowmaps (512,1024,2048,
-     * etc...)
-     * @param shadowRenderer the shadowRenderer to use for this Filter
+     * @param assetManager The application's asset manager.
+     * @param shadowMapSize The size of the rendered shadow maps (e.g., 512, 1024, 2048).
+     * @param shadowRenderer The shadowRenderer to use for this Filter
      */
-    @SuppressWarnings("all")
-    protected AbstractShadowFilter(AssetManager manager, int shadowMapSize, T shadowRenderer) {
+    protected AbstractShadowFilter(AssetManager assetManager, int shadowMapSize, T shadowRenderer) {
         super("Post Shadow");
         this.shadowRenderer = shadowRenderer;
         // this is legacy setting for shadows with backface shadows
@@ -93,18 +89,21 @@ public abstract class AbstractShadowFilter<T extends AbstractShadowRenderer> ext
         return true;
     }
 
+    /**
+     * @deprecated Use {@link #getMaterial()} instead.
+     * @return The Material used by this filter.
+     */
+    @Deprecated
     public Material getShadowMaterial() {       
         return material;
     }
 
-    Vector4f tmpv = new Vector4f();
-
     @Override
     protected void preFrame(float tpf) {
         shadowRenderer.preFrame(tpf);
-        material.setMatrix4("ViewProjectionMatrixInverse", viewPort.getCamera().getViewProjectionMatrix().invert());
         Matrix4f m = viewPort.getCamera().getViewProjectionMatrix();
-        material.setVector4("ViewProjectionMatrixRow2", tmpv.set(m.m20, m.m21, m.m22, m.m23));
+        material.setMatrix4("ViewProjectionMatrixInverse", tempMat4.set(m).invertLocal());
+        material.setVector4("ViewProjectionMatrixRow2", tempVec4.set(m.m20, m.m21, m.m22, m.m23));
     }
 
     @Override
@@ -337,15 +336,4 @@ public abstract class AbstractShadowFilter<T extends AbstractShadowRenderer> ext
         shadowRenderer.setPostShadowMaterial(material);
     }
 
-    @Override
-    public void write(JmeExporter ex) throws IOException {
-        super.write(ex);
-        OutputCapsule oc = ex.getCapsule(this);
-    }
-
-    @Override
-    public void read(JmeImporter im) throws IOException {
-        super.read(im);
-        InputCapsule ic = im.getCapsule(this);
-    }
 }

+ 221 - 145
jme3-core/src/main/java/com/jme3/shadow/AbstractShadowRenderer.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2024 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -32,7 +32,11 @@
 package com.jme3.shadow;
 
 import com.jme3.asset.AssetManager;
-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.export.Savable;
 import com.jme3.light.LightFilter;
 import com.jme3.light.NullLightFilter;
 import com.jme3.material.Material;
@@ -55,12 +59,12 @@ import com.jme3.scene.Geometry;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.debug.WireFrustum;
 import com.jme3.texture.FrameBuffer;
+import com.jme3.texture.FrameBuffer.FrameBufferTarget;
 import com.jme3.texture.Image.Format;
 import com.jme3.texture.Texture.MagFilter;
 import com.jme3.texture.Texture.MinFilter;
 import com.jme3.texture.Texture.ShadowCompareMode;
 import com.jme3.texture.Texture2D;
-import com.jme3.texture.FrameBuffer.FrameBufferTarget;
 import com.jme3.ui.Picture;
 import com.jme3.util.clone.Cloner;
 import com.jme3.util.clone.JmeCloneable;
@@ -71,83 +75,101 @@ import java.util.List;
 import java.util.logging.Logger;
 
 /**
- * abstract shadow renderer that holds commons feature to have for a shadow
- * renderer
+ * An abstract shadow renderer that provides common features for shadow rendering.
  *
  * @author Rémy Bouquet aka Nehon
  */
-public abstract class AbstractShadowRenderer implements SceneProcessor, Savable, JmeCloneable, Cloneable {
+public abstract class AbstractShadowRenderer implements SceneProcessor, Savable, JmeCloneable {
 
     protected static final Logger logger = Logger.getLogger(AbstractShadowRenderer.class.getName());
     private static final LightFilter NULL_LIGHT_FILTER = new NullLightFilter();
+
+    // The number of shadow maps to render.
     protected int nbShadowMaps = 1;
+    // The resolution (width and height) of each shadow map.
     protected float shadowMapSize;
+    // The intensity of the shadows, ranging from 0.0 (fully transparent) to 1.0 (fully opaque).
     protected float shadowIntensity = 0.7f;
+    // The RenderManager instance used for rendering operations.
     protected RenderManager renderManager;
+    // The ViewPort associated with this shadow renderer.
     protected ViewPort viewPort;
+    // Array of frame buffers used for rendering shadow maps.
     protected FrameBuffer[] shadowFB;
+    // Array of 2D textures representing the generated shadow maps.
     protected Texture2D[] shadowMaps;
+    // A dummy texture used to prevent read-buffer crashes on certain platforms (e.g., OSX).
     protected Texture2D dummyTex;
+    // Material used for the pre-shadow pass (rendering occluders into the shadow map).
     protected Material preshadowMat;
+    // Material used for the post-shadow pass (applying shadows to the scene).
     protected Material postshadowMat;
+    // Array of light view projection matrices for each shadow map.
     protected Matrix4f[] lightViewProjectionsMatrices;
+    // The AssetManager instance used to load assets.
     protected AssetManager assetManager;
+    // Flag indicating whether debug visualizations (e.g., shadow maps) should be displayed.
     protected boolean debug = false;
+    // The thickness of shadow edges, influencing PCF (Percentage-Closer Filtering). Value is in tenths of a pixel.
     protected float edgesThickness = 1.0f;
+    // The filtering mode applied to shadow edges.
     protected EdgeFilteringMode edgeFilteringMode = EdgeFilteringMode.Bilinear;
+    // The shadow comparison mode (hardware or software).
     protected CompareMode shadowCompareMode = CompareMode.Hardware;
+    // Array of Picture objects used for debugging to display shadow maps.
     protected Picture[] dispPic;
+    // Forced RenderState used during the pre-shadow pass to render occluders.
     protected RenderState forcedRenderState = new RenderState();
+    // Flag indicating whether back faces should cast shadows.
     protected boolean renderBackFacesShadows = true;
+    // The application profiler for performance monitoring.
     protected AppProfiler prof;
-
-    /**
-     * true if the fallback material should be used, otherwise false
-     */
+    // Flag indicating whether shadow frustums should be displayed for debugging.
+    protected boolean debugfrustums = false;
+    // True if a fallback material should be used for post-shadow rendering, otherwise false.
+    // This occurs if some scene materials do not support the post-shadow technique.
     protected boolean needsfallBackMaterial = false;
-    /**
-     * name of the post material technique
-     */
+    // The name of the technique to use for the post-shadow material.
     protected String postTechniqueName = "PostShadow";
-    /**
-     * list of materials for post shadow queue geometries
-     */
+    // A cache of materials found on geometries in the post-shadow queue.
     protected List<Material> matCache = new ArrayList<>();
+    // List of geometries that receive shadows.
     protected GeometryList lightReceivers = new GeometryList(new OpaqueComparator());
+    // List of geometries that cast shadows (occluders).
     protected GeometryList shadowMapOccluders = new GeometryList(new OpaqueComparator());
+    // Internal cache for shadow map uniform names (e.g., "ShadowMap0", "ShadowMap1").
     private String[] shadowMapStringCache;
+    // nternal cache for light view projection matrix uniform names (e.g., "LightViewProjectionMatrix0").
     private String[] lightViewStringCache;
-    /**
-     * fade shadows at distance
-     */
+    // The distance at which shadows start to fade out. A value of 0 means no override.
     protected float zFarOverride = 0;
+    // Vector containing information about shadow fading (start distance, inverse fade length).
     protected Vector2f fadeInfo;
+    // The length over which shadows fade out.
     protected float fadeLength;
+    // A camera used to define the frustum for shadow rendering, especially when `zFarOverride` is used.
     protected Camera frustumCam;
-    /**
-     * true to skip the post pass when there are no shadow casters
-     */
+    // True to skip the post pass when there are no shadow casters.
     protected boolean skipPostPass;
 
     /**
-     * used for serialization
+     * For serialization only. Do not use.
      */
     protected AbstractShadowRenderer() {
     }
 
     /**
-     * Create an abstract shadow renderer. Subclasses invoke this constructor.
+     * Creates an  AbstractShadowRenderer. Subclasses invoke this constructor.
      *
-     * @param assetManager the application asset manager
-     * @param shadowMapSize the size of the rendered shadow maps (512,1024,2048,
-     * etc...)
-     * @param nbShadowMaps the number of shadow maps rendered (the more shadow
-     * maps the more quality, the fewer fps).
+     * @param assetManager The application's asset manager.
+     * @param shadowMapSize The size of the rendered shadow maps (e.g., 512, 1024, 2048).
+     * @param nbShadowMaps The number of shadow maps to render (1 to 4). More maps
+     * improve quality but can reduce performance.
      */
     protected AbstractShadowRenderer(AssetManager assetManager, int shadowMapSize, int nbShadowMaps) {
         this.assetManager = assetManager;
-        this.nbShadowMaps = nbShadowMaps;
         this.shadowMapSize = shadowMapSize;
+        this.nbShadowMaps = nbShadowMaps;
         init(assetManager, nbShadowMaps, shadowMapSize);
     }
 
@@ -200,9 +222,10 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
     }
 
     /**
-     * set the post shadow material for this renderer
+     * Sets the post-shadow material for this renderer. This material is used to apply
+     * the shadows to the main scene.
      *
-     * @param postShadowMat the desired Material (alias created)
+     * @param postShadowMat The desired Material instance to use (alias created).
      */
     protected final void setPostShadowMaterial(Material postShadowMat) {
         this.postshadowMat = postShadowMat;
@@ -216,10 +239,11 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
     }
 
     /**
-     * Sets the filtering mode for shadow edges. See {@link EdgeFilteringMode}
-     * for more info.
+     * Sets the filtering mode for shadow edges. This affects the smoothness of
+     * shadow boundaries.
      *
-     * @param filterMode the desired filtering mode (not null)
+     * @param filterMode The desired filtering mode (cannot be null). See {@link EdgeFilteringMode}
+     * for available options.
      */
     final public void setEdgeFilteringMode(EdgeFilteringMode filterMode) {
         if (filterMode == null) {
@@ -243,19 +267,21 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
     }
 
     /**
-     * returns the edge filtering mode
+     * Returns the currently edge filtering mode for shadows.
      *
+     * @return The current {@link EdgeFilteringMode} enum value.
      * @see EdgeFilteringMode
-     * @return the enum value
      */
     public EdgeFilteringMode getEdgeFilteringMode() {
         return edgeFilteringMode;
     }
 
     /**
-     * Sets the shadow compare mode. See {@link CompareMode} for more info.
+     * Sets the shadow comparison mode. This determines how shadow map values are
+     * compared to generate shadows.
      *
-     * @param compareMode the desired compare mode (not null)
+     * @param compareMode The desired compare mode (cannot be null). See {@link CompareMode}
+     * for available options.
      */
     final public void setShadowCompareMode(CompareMode compareMode) {
         if (compareMode == null) {
@@ -283,50 +309,52 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
     }
 
     /**
-     * returns the shadow compare mode
+     * Returns the currently shadow comparison mode.
      *
+     * @return The current {@link CompareMode} enum value.
      * @see CompareMode
-     * @return the shadowCompareMode
      */
     public CompareMode getShadowCompareMode() {
         return shadowCompareMode;
     }
 
     /**
-     * debug function to create a visible frustum
+     * Debug function to create a visible wireframe frustum. This is useful for
+     * visualizing the shadow camera's view.
      *
-     * @param pts optional storage for vertex positions (may be null)
-     * @param i the index of the desired wire color (default=White)
-     * @return a new Geometry
+     * @param pts Optional storage for vertex positions. If null, a new array will be created.
+     * @param i The index, used to assign a color to the frustum for differentiation (e.g., for multiple shadow maps).
+     * @return A new {@link Geometry} representing the wireframe frustum.
      */
     protected Geometry createFrustum(Vector3f[] pts, int i) {
         WireFrustum frustum = new WireFrustum(pts);
-        Geometry frustumMdl = new Geometry("f", frustum);
-        frustumMdl.setCullHint(Spatial.CullHint.Never);
-        frustumMdl.setShadowMode(ShadowMode.Off);
+        Geometry geo = new Geometry("WireFrustum" + i, frustum);
         Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
         mat.getAdditionalRenderState().setWireframe(true);
-        frustumMdl.setMaterial(mat);
+        geo.setMaterial(mat);
+        geo.setCullHint(Spatial.CullHint.Never);
+        geo.setShadowMode(ShadowMode.Off);
+
         switch (i) {
             case 0:
-                frustumMdl.getMaterial().setColor("Color", ColorRGBA.Pink);
+                mat.setColor("Color", ColorRGBA.Pink);
                 break;
             case 1:
-                frustumMdl.getMaterial().setColor("Color", ColorRGBA.Red);
+                mat.setColor("Color", ColorRGBA.Red);
                 break;
             case 2:
-                frustumMdl.getMaterial().setColor("Color", ColorRGBA.Green);
+                mat.setColor("Color", ColorRGBA.Green);
                 break;
             case 3:
-                frustumMdl.getMaterial().setColor("Color", ColorRGBA.Blue);
+                mat.setColor("Color", ColorRGBA.Blue);
                 break;
             default:
-                frustumMdl.getMaterial().setColor("Color", ColorRGBA.White);
+                mat.setColor("Color", ColorRGBA.White);
                 break;
         }
 
-        frustumMdl.updateGeometricState();
-        return frustumMdl;
+        geo.updateGeometricState();
+        return geo;
     }
 
     /**
@@ -340,13 +368,14 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
         renderManager = rm;
         viewPort = vp;
         postTechniqueName = "PostShadow";
-        if(zFarOverride>0 && frustumCam == null){
+        if (zFarOverride > 0 && frustumCam == null) {
             initFrustumCam();
         }
     }
 
     /**
-     * delegates the initialization of the frustum cam to child renderers
+     * Delegates the initialization of the frustum camera to child renderers.
+     * This camera defines the view for calculating shadow frustums.
      */
     protected abstract void initFrustumCam();
 
@@ -361,42 +390,43 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
     }
 
     /**
-     * Invoked once per frame to update the shadow cams according to the light
-     * view.
+     * Invoked once per frame to update the shadow cameras according to the light view.
+     * Subclasses must implement this method to define how shadow cameras are positioned
+     * and oriented.
      *
-     * @param viewCam the scene cam
+     * @param viewCam The main scene camera.
      */
     protected abstract void updateShadowCams(Camera viewCam);
 
     /**
-     * Returns a subclass-specific geometryList containing the occluders to be
-     * rendered in the shadow map
+     * Returns a subclass-specific {@link GeometryList} containing the occluders
+     * that should be rendered into the shadow map.
      *
-     * @param shadowMapIndex the index of the shadow map being rendered
-     * @param shadowMapOccluders the list of occluders
-     * @return the geometryList
+     * @param shadowMapIndex The index of the shadow map being rendered.
+     * @param shadowMapOccluders An existing {@link GeometryList} that can be reused or populated.
+     * @return A {@link GeometryList} containing the geometries that cast shadows for the given map.
      */
     protected abstract GeometryList getOccludersToRender(int shadowMapIndex, GeometryList shadowMapOccluders);
 
     /**
-     * return the shadow camera to use for rendering the shadow map according
-     * the given index
+     * Returns the shadow camera to use for rendering the shadow map according to the given index.
+     * Subclasses must implement this to provide the correct camera for each shadow map.
      *
-     * @param shadowMapIndex the index of the shadow map being rendered
-     * @return the shadowCam
+     * @param shadowMapIndex The index of the shadow map being rendered.
+     * @return The {@link Camera} instance representing the shadow's viewpoint.
      */
     protected abstract Camera getShadowCam(int shadowMapIndex);
 
     /**
-     * responsible for displaying the frustum of the shadow cam for debug
-     * purpose
+     * Responsible for displaying the frustum of the shadow camera for debugging purposes.
+     * Subclasses can override this method to provide specific debug visualizations.
      *
-     * @param shadowMapIndex the index of the shadow map
+     * @param shadowMapIndex The index of the shadow map for which to display the frustum.
      */
     protected void doDisplayFrustumDebug(int shadowMapIndex) {
+        // Default implementation does nothing.
     }
 
-    @SuppressWarnings("fallthrough")
     @Override
     public void postQueue(RenderQueue rq) {
         lightReceivers.clear();
@@ -413,13 +443,11 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
         renderManager.setForcedTechnique("PreShadow");
 
         for (int shadowMapIndex = 0; shadowMapIndex < nbShadowMaps; shadowMapIndex++) {
-
-                if (debugfrustums) {
-                    doDisplayFrustumDebug(shadowMapIndex);
-                }
-                renderShadowMap(shadowMapIndex);
-
+            if (debugfrustums) {
+                doDisplayFrustumDebug(shadowMapIndex);
             }
+            renderShadowMap(shadowMapIndex);
+        }
 
         debugfrustums = false;
 
@@ -442,7 +470,7 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
         renderManager.getRenderer().clearBuffers(true, true, true);
         renderManager.setForcedRenderState(forcedRenderState);
 
-        // render shadow casters to shadow map and disables the lightfilter
+        // render shadow casters to shadow map and disables the light filter
         LightFilter tmpLightFilter = renderManager.getLightFilter();
         renderManager.setLightFilter(NULL_LIGHT_FILTER);
         viewPort.getQueue().renderShadowQueue(shadowMapOccluders, renderManager, shadowCam, true);
@@ -450,16 +478,18 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
         renderManager.setForcedRenderState(null);
     }
 
-    boolean debugfrustums = false;
-
+    /**
+     * Enables debugging of shadow frustums, making them visible in the scene.
+     * Call this before {@link #postQueue(RenderQueue)} to see the frustums.
+     */
     public void displayFrustum() {
         debugfrustums = true;
     }
 
     /**
-     * For debugging purposes, display depth shadow maps.
+     * For debugging purposes, displays the depth shadow maps on screen as Picture quads.
      *
-     * @param r ignored
+     * @param r The current {@link Renderer} (ignored).
      */
     protected void displayShadowMap(Renderer r) {
         Camera cam = viewPort.getCamera();
@@ -476,12 +506,19 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
     }
 
     /**
-     * For debugging purposes, "snapshot" the current frustum to the scene.
+     * For debugging purposes, "snapshots" the current state of the shadow maps
+     * and displays them on screen.
      */
     public void displayDebug() {
         debug = true;
     }
 
+    /**
+     * Populates the provided {@link GeometryList} with geometries that are considered
+     * shadow receivers. Subclasses must implement this method.
+     *
+     * @param lightReceivers The {@link GeometryList} to populate with shadow-receiving geometries.
+     */
     protected abstract void getReceivers(GeometryList lightReceivers);
 
     @Override
@@ -523,13 +560,19 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
 
     /**
      * This method is called once per frame and is responsible for clearing any
-     * material parameters that subclasses may need to clear on the post material.
+     * material parameters that subclasses may have set on the post-shadow material.
+     * This ensures that parameters from previous frames or other renderers do not
+     * interfere.
      *
-     * @param material the material that was used for the post shadow pass
+     * @param material The material that was used for the post-shadow pass.
      */
     protected abstract void clearMaterialParameters(Material material);
 
-    private void clearMatParams(){
+    /**
+     * Clears common material parameters set by this renderer on materials in the cache.
+     * This is done to avoid interference with other shadow renderers or subsequent frames.
+     */
+    private void clearMatParams() {
         for (Material mat : matCache) {
 
             //clearing only necessary params, the others may be set by other
@@ -556,14 +599,19 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
      */
     protected abstract void setMaterialParameters(Material material);
 
-    private void setMatParams(GeometryList l) {
+    /**
+     * Iterates through the given {@link GeometryList} to gather unique materials
+     * and sets common shadow-related parameters on them.
+     *
+     * @param list The {@link GeometryList} containing geometries whose materials need parameters set.
+     */
+    private void setMatParams(GeometryList list) {
         //iterate through all the geometries of the list to gather the materials
 
-        buildMatCache(l);
+        buildMatCache(list);
 
         //iterating through the mat cache and setting the parameters
         for (Material mat : matCache) {
-
             mat.setFloat("ShadowMapSize", shadowMapSize);
 
             for (int j = 0; j < nbShadowMaps; j++) {
@@ -581,7 +629,7 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
             mat.setBoolean("BackfaceShadows", renderBackFacesShadows);
 
             if (fadeInfo != null) {
-               mat.setVector2("FadeInfo", fadeInfo);
+                mat.setVector2("FadeInfo", fadeInfo);
             }
 
             setMaterialParameters(mat);
@@ -592,13 +640,19 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
         if (needsfallBackMaterial) {
             setPostShadowParams();
         }
-
     }
 
-    private void buildMatCache(GeometryList l) {
+    /**
+     * Builds a cache of unique materials from the provided {@link GeometryList}
+     * that support the post-shadow technique. If any material does not support
+     * it, the `needsfallBackMaterial` flag is set.
+     *
+     * @param list The {@link GeometryList} to extract materials from.
+     */
+    private void buildMatCache(GeometryList list) {
         matCache.clear();
-        for (int i = 0; i < l.size(); i++) {
-            Material mat = l.get(i).getMaterial();
+        for (int i = 0; i < list.size(); i++) {
+            Material mat = list.get(i).getMaterial();
             //checking if the material has the post technique and adding it to the material cache
             if (mat.getMaterialDef().getTechniqueDefs(postTechniqueName) != null) {
                 if (!matCache.contains(mat)) {
@@ -611,7 +665,8 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
     }
 
     /**
-     * for internal use only
+     * For internal use only. Sets the common shadow parameters on the internal
+     * post-shadow material. This is used when a fallback material is needed.
      */
     protected void setPostShadowParams() {
         setMaterialParameters(postshadowMat);
@@ -626,43 +681,45 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
     }
 
     /**
-     * How far the shadows are rendered in the view
+     * Returns the maximum distance from the eye where shadows are rendered.
+     * A value of 0 indicates that the distance is dynamically computed based on scene bounds.
      *
+     * @return The shadow Z-extend distance in world units.
      * @see #setShadowZExtend(float zFar)
-     * @return shadowZExtend
      */
     public float getShadowZExtend() {
         return zFarOverride;
     }
 
     /**
-     * Set the distance from the eye where the shadows will be rendered default
-     * value is dynamically computed to the shadow casters/receivers union bound
-     * zFar, capped to view frustum far value.
+     * Sets the distance from the camera where shadows will be rendered.
+     * By default (0), this value is dynamically computed based on the union bound
+     * of shadow casters and receivers, capped by the view frustum's far value.
+     * Setting a positive value overrides this dynamic computation.
      *
-     * @param zFar the zFar values that override the computed one
+     * @param zFar The zFar value that overrides the computed one. Set to 0 to use dynamic computation.
      */
     public void setShadowZExtend(float zFar) {
         this.zFarOverride = zFar;
-        if(zFarOverride == 0){
+        if (zFarOverride == 0) {
             fadeInfo = null;
             frustumCam = null;
-        }else{
+        } else {
             if (fadeInfo != null) {
                 fadeInfo.set(zFarOverride - fadeLength, 1f / fadeLength);
             }
-            if(frustumCam == null && viewPort != null){
+            if (frustumCam == null && viewPort != null) {
                 initFrustumCam();
             }
         }
     }
 
     /**
-     * Define the length over which the shadow will fade out when using a
-     * shadowZextend This is useful to make dynamic shadows fade into baked
-     * shadows in the distance.
+     * Defines the length over which the shadow will fade out when using a
+     * custom `shadowZextend`. This is useful for smoothly transitioning
+     * dynamic shadows into baked shadows or for preventing abrupt shadow cut-offs.
      *
-     * @param length the fade length in world units
+     * @param length The fade length in world units. Set to 0 to disable fading.
      */
     public void setShadowZFadeLength(float length) {
         if (length == 0) {
@@ -681,10 +738,10 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
     }
 
     /**
-     * get the length over which the shadow will fade out when using a
-     * shadowZextend
+     * Returns the length over which the shadow will fade out when using a
+     * custom `shadowZextend`.
      *
-     * @return the fade length in world units
+     * @return The fade length in world units. Returns 0 if no fading is applied.
      */
     public float getShadowZFadeLength() {
         if (fadeInfo != null) {
@@ -694,39 +751,45 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
     }
 
     /**
-     * @param viewCam a Camera to define the view frustum
-     * @return true if the light source bounding box is in the view frustum
+     * Abstract method to check if the light source's bounding box is within the view frustum
+     * of the given camera. This is used for culling to avoid unnecessary shadow computations.
+     *
+     * @param viewCam A {@link Camera} to define the view frustum against which to check.
+     * @return True if the light source's bounding box is in the view frustum, otherwise false.
      */
     protected abstract boolean checkCulling(Camera viewCam);
 
     @Override
     public void preFrame(float tpf) {
+        // no-op
     }
 
     @Override
     public void cleanup() {
+        // no-op
     }
 
     @Override
     public void reshape(ViewPort vp, int w, int h) {
+        // no-op
     }
 
     /**
-     * Returns the shadow intensity.
+     * Returns the current shadow intensity.
      *
+     * @return The shadow intensity value, ranging from 0.0 to 1.0.
      * @see #setShadowIntensity(float shadowIntensity)
-     * @return shadowIntensity
      */
     public float getShadowIntensity() {
         return shadowIntensity;
     }
 
     /**
-     * Set the shadowIntensity. The value should be between 0 and 1. A 0 value
-     * gives a bright and invisible shadow, a 1 value gives a pitch black
-     * shadow. The default is 0.7
+     * Sets the shadow intensity. This value controls the darkness of the shadows.
+     * A value of 0.0 results in bright, almost invisible shadows, while 1.0 creates
+     * pitch-black shadows. The default value is 0.7.
      *
-     * @param shadowIntensity the darkness of the shadow
+     * @param shadowIntensity The desired darkness of the shadow, a float between 0.0 and 1.0.
      */
     final public void setShadowIntensity(float shadowIntensity) {
         this.shadowIntensity = shadowIntensity;
@@ -734,38 +797,41 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
     }
 
     /**
-     * returns the edges thickness
+     * Returns the configured shadow edges thickness. The value is returned
+     * as an integer representing tenths of a pixel (e.g., 10 for 1.0 pixel).
      *
+     * @return The edges thickness in tenths of a pixel.
      * @see #setEdgesThickness(int edgesThickness)
-     * @return edgesThickness
      */
     public int getEdgesThickness() {
         return (int) (edgesThickness * 10);
     }
 
     /**
-     * Read the number of shadow maps rendered by this renderer.
+     * Returns the number of shadow maps currently rendered by this processor.
      *
-     * @return count
+     * @return The count of shadow maps.
      */
     public int getNumShadowMaps() {
         return nbShadowMaps;
     }
 
     /**
-     * Read the size of each shadow map rendered by this renderer.
+     * Returns the size (width and height) of each shadow map rendered by this processor.
      *
-     * @return a map's height (which is also its width, in pixels)
+     * @return The resolution of a single shadow map in pixels.
      */
     public int getShadowMapSize() {
         return (int) shadowMapSize;
     }
 
     /**
-     * Sets the shadow edges thickness. Default is 10. Setting it to lower values
-     * can help reduce the jagged effect of shadow edges.
+     * Sets the shadow edges thickness. This parameter influences the
+     * smoothness of shadow edges, particularly with PCF (Percentage-Closer Filtering).
+     * Setting lower values can help reduce jagged artifacts.
      *
-     * @param edgesThickness the desired thickness (in tenths of a pixel, default=10)
+     * @param edgesThickness The desired thickness in tenths of a pixel (e.g., 10 for 1.0 pixel).
+     * The value is clamped between 1 and 10. Default is 10.
      */
     public void setEdgesThickness(int edgesThickness) {
         this.edgesThickness = Math.max(1, Math.min(edgesThickness, 10));
@@ -779,29 +845,39 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
      * @return false
      */
     @Deprecated
-    public boolean isFlushQueues() { return false; }
+    public boolean isFlushQueues() {
+        return false;
+    }
 
     /**
-     * returns the pre shadows pass render state.
-     * use it to adjust the RenderState parameters of the pre shadow pass.
-     * Note that this will be overridden if the preShadow technique in the material has a ForcedRenderState
-     * @return the pre shadow render state.
+     * Returns the {@link RenderState} that is forced during the pre-shadow pass.
+     * You can use this to adjust the rendering parameters for geometries that cast shadows.
+     * Note that this will be overridden if the "PreShadow" technique in the material definition
+     * has its own `ForcedRenderState`.
+     *
+     * @return The {@link RenderState} applied to the pre-shadow pass.
      */
     public RenderState getPreShadowForcedRenderState() {
         return forcedRenderState;
     }
 
     /**
-     * Set to true if you want back faces shadows on geometries.
-     * Note that back faces shadows will be blended over dark lighten areas and may produce overly dark lighting.
+     * Sets whether back faces of geometries should cast shadows.
+     * When enabled, shadows cast by the back side of an object can appear.
+     * Be aware that back face shadows can sometimes lead to overly dark lighting
+     * when blended with existing dark areas.
      *
-     * Also note that setting this parameter will override this parameter for ALL materials in the scene.
-     * You can alternatively change this parameter on a single material using {@link Material#setBoolean(String, boolean)}
+     * <p>Setting this parameter will globally override this setting for ALL materials
+     * in the scene for the shadow pass. Alternatively, you can control this on
+     * individual materials using {@link Material#setBoolean(String, boolean)}
+     * with the "BackfaceShadows" parameter.
      *
-     * This also will automatically adjust the faceCullMode and the PolyOffset of the pre shadow pass.
-     * You can modify them by using {@link #getPreShadowForcedRenderState()}
+     * <p>This method also automatically adjusts the {@link RenderState.FaceCullMode}
+     * and {@link RenderState#setPolyOffset(float, float)} of the pre-shadow pass
+     * to accommodate back face rendering. You can further modify these
+     * using {@link #getPreShadowForcedRenderState()}.
      *
-     * @param renderBackFacesShadows true or false.
+     * @param renderBackFacesShadows True to enable back face shadows, false to disable.
      */
     public void setRenderBackFacesShadows(boolean renderBackFacesShadows) {
         this.renderBackFacesShadows = renderBackFacesShadows;
@@ -815,9 +891,9 @@ public abstract class AbstractShadowRenderer implements SceneProcessor, Savable,
     }
 
     /**
-     * if this processor renders back faces shadows
+     * Checks if this shadow processor is configured to render shadows from back faces.
      *
-     * @return true if this processor renders back faces shadows
+     * @return True if back face shadows are enabled, false otherwise.
      */
     public boolean isRenderBackFacesShadows() {
         return renderBackFacesShadows;

+ 41 - 44
jme3-core/src/main/java/com/jme3/shadow/PointLightShadowRenderer.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -51,33 +51,39 @@ import com.jme3.util.clone.Cloner;
 import java.io.IOException;
 
 /**
- * PointLightShadowRenderer renders shadows for a point light
+ * Renders shadows for a {@link PointLight}. This renderer uses six cameras,
+ * one for each face of a cube map, to capture shadows from the point light's
+ * perspective.
  *
  * @author Rémy Bouquet aka Nehon
  */
 public class PointLightShadowRenderer extends AbstractShadowRenderer {
 
+    /**
+     * The fixed number of cameras used for rendering point light shadows (6 for a cube map).
+     */
     public static final int CAM_NUMBER = 6;
+
     protected PointLight light;
     protected Camera[] shadowCams;
-    private Geometry[] frustums = null;
+    protected Geometry[] frustums = null;
+    protected final Vector3f X_NEG = Vector3f.UNIT_X.mult(-1f);
+    protected final Vector3f Y_NEG = Vector3f.UNIT_Y.mult(-1f);
+    protected final Vector3f Z_NEG = Vector3f.UNIT_Z.mult(-1f);
 
     /**
-     * Used for serialization.
-     * Use PointLightShadowRenderer#PointLightShadowRenderer(AssetManager
-     * assetManager, int shadowMapSize)
-     * instead.
+     * For serialization only. Do not use.
      */
     protected PointLightShadowRenderer() {
         super();
     }
 
     /**
-     * Creates a PointLightShadowRenderer
+     * Creates a new {@code PointLightShadowRenderer} instance.
      *
-     * @param assetManager the application asset manager
-     * @param shadowMapSize the size of the rendered shadowmaps (512,1024,2048,
-     * etc...)
+     * @param assetManager The application's asset manager.
+     * @param shadowMapSize The size of the rendered shadow maps (e.g., 512, 1024, 2048).
+     * Higher values produce better quality shadows but may impact performance.
      */
     public PointLightShadowRenderer(AssetManager assetManager, int shadowMapSize) {
         super(assetManager, shadowMapSize, CAM_NUMBER);
@@ -86,7 +92,7 @@ public class PointLightShadowRenderer extends AbstractShadowRenderer {
 
     private void init(int shadowMapSize) {
         shadowCams = new Camera[CAM_NUMBER];
-        for (int i = 0; i < CAM_NUMBER; i++) {
+        for (int i = 0; i < shadowCams.length; i++) {
             shadowCams[i] = new Camera(shadowMapSize, shadowMapSize);
         }
     }
@@ -95,9 +101,9 @@ public class PointLightShadowRenderer extends AbstractShadowRenderer {
     protected void initFrustumCam() {
         Camera viewCam = viewPort.getCamera();
         frustumCam = viewCam.clone();
-        frustumCam.setFrustum(viewCam.getFrustumNear(), zFarOverride, viewCam.getFrustumLeft(), viewCam.getFrustumRight(), viewCam.getFrustumTop(), viewCam.getFrustumBottom());
+        frustumCam.setFrustum(viewCam.getFrustumNear(), zFarOverride,
+                viewCam.getFrustumLeft(), viewCam.getFrustumRight(), viewCam.getFrustumTop(), viewCam.getFrustumBottom());
     }
-    
 
     @Override
     protected void updateShadowCams(Camera viewCam) {
@@ -107,31 +113,21 @@ public class PointLightShadowRenderer extends AbstractShadowRenderer {
             return;
         }
 
-        //bottom
-        shadowCams[0].setAxes(Vector3f.UNIT_X.mult(-1f), Vector3f.UNIT_Z.mult(-1f), Vector3f.UNIT_Y.mult(-1f));
-
-        //top
-        shadowCams[1].setAxes(Vector3f.UNIT_X.mult(-1f), Vector3f.UNIT_Z, Vector3f.UNIT_Y);
-
-        //forward
-        shadowCams[2].setAxes(Vector3f.UNIT_X.mult(-1f), Vector3f.UNIT_Y, Vector3f.UNIT_Z.mult(-1f));
-
-        //backward
-        shadowCams[3].setAxes(Vector3f.UNIT_X, Vector3f.UNIT_Y, Vector3f.UNIT_Z);
-
-        //left
-        shadowCams[4].setAxes(Vector3f.UNIT_Z, Vector3f.UNIT_Y, Vector3f.UNIT_X.mult(-1f));
-
-        //right
-        shadowCams[5].setAxes(Vector3f.UNIT_Z.mult(-1f), Vector3f.UNIT_Y, Vector3f.UNIT_X);
-
-        for (int i = 0; i < CAM_NUMBER; i++) {
-            shadowCams[i].setFrustumPerspective(90f, 1f, 0.1f, light.getRadius());
-            shadowCams[i].setLocation(light.getPosition());
-            shadowCams[i].update();
-            shadowCams[i].updateViewProjection();
+        // Configure axes for each of the six cube map cameras (positive/negative X, Y, Z)
+        shadowCams[0].setAxes(X_NEG, Z_NEG, Y_NEG);                                 // -Y (bottom)
+        shadowCams[1].setAxes(X_NEG, Vector3f.UNIT_Z, Vector3f.UNIT_Y);             // +Y (top)
+        shadowCams[2].setAxes(X_NEG, Vector3f.UNIT_Y, Z_NEG);                       // +Z (forward)
+        shadowCams[3].setAxes(Vector3f.UNIT_X, Vector3f.UNIT_Y, Vector3f.UNIT_Z);   // -Z (backward)
+        shadowCams[4].setAxes(Vector3f.UNIT_Z, Vector3f.UNIT_Y, X_NEG);             // -X (left)
+        shadowCams[5].setAxes(Z_NEG, Vector3f.UNIT_Y, Vector3f.UNIT_X);             // +X (right)
+
+        // Set perspective and location for all shadow cameras
+        for (Camera shadowCam : shadowCams) {
+            shadowCam.setFrustumPerspective(90f, 1f, 0.1f, light.getRadius());
+            shadowCam.setLocation(light.getPosition());
+            shadowCam.update();
+            shadowCam.updateViewProjection();
         }
-
     }
 
     @Override
@@ -160,7 +156,7 @@ public class PointLightShadowRenderer extends AbstractShadowRenderer {
         if (frustums == null) {
             frustums = new Geometry[CAM_NUMBER];
             Vector3f[] points = new Vector3f[8];
-            for (int i = 0; i < 8; i++) {
+            for (int i = 0; i < points.length; i++) {
                 points[i] = new Vector3f();
             }
             for (int i = 0; i < CAM_NUMBER; i++) {
@@ -168,8 +164,9 @@ public class PointLightShadowRenderer extends AbstractShadowRenderer {
                 frustums[i] = createFrustum(points, i);
             }
         }
-        if (frustums[shadowMapIndex].getParent() == null) {
-            ((Node) viewPort.getScenes().get(0)).attachChild(frustums[shadowMapIndex]);
+        Geometry geo = frustums[shadowMapIndex];
+        if (geo.getParent() == null) {
+            ((Node) viewPort.getScenes().get(0)).attachChild(geo);
         }
     }
 
@@ -237,13 +234,13 @@ public class PointLightShadowRenderer extends AbstractShadowRenderer {
         }
 
         Camera cam = viewCam;
-        if(frustumCam != null){
-            cam = frustumCam;            
+        if (frustumCam != null) {
+            cam = frustumCam;
             cam.setLocation(viewCam.getLocation());
             cam.setRotation(viewCam.getRotation());
         }
         TempVars vars = TempVars.get();
-        boolean intersects = light.intersectsFrustum(cam,vars);
+        boolean intersects = light.intersectsFrustum(cam, vars);
         vars.release();
         return intersects;
     }

+ 144 - 39
jme3-core/src/main/java/com/jme3/system/AppSettings.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2022 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -39,6 +39,8 @@ import java.io.OutputStream;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Properties;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 import java.util.prefs.BackingStoreException;
 import java.util.prefs.Preferences;
 
@@ -58,6 +60,8 @@ public final class AppSettings extends HashMap<String, Object> {
 
     private static final long serialVersionUID = 1L;
 
+    private static final Logger logger = Logger.getLogger(AppSettings.class.getName());
+
     private static final AppSettings defaults = new AppSettings(false);
 
     /**
@@ -295,6 +299,7 @@ public final class AppSettings extends HashMap<String, Object> {
         defaults.put("UseRetinaFrameBuffer", false);
         defaults.put("WindowYPosition", 0);
         defaults.put("WindowXPosition", 0);
+        defaults.put("X11PlatformPreferred", false);
         //  defaults.put("Icons", null);
     }
 
@@ -507,12 +512,25 @@ public final class AppSettings extends HashMap<String, Object> {
      * @return the corresponding value, or 0 if not set
      */
     public int getInteger(String key) {
-        Integer i = (Integer) get(key);
-        if (i == null) {
-            return 0;
-        }
+        return getInteger(key, 0);
+    }
 
-        return i.intValue();
+    /**
+     * Get an integer from the settings.
+     * <p>
+     * If the key is not set, or the stored value is not an Integer, then the
+     * provided default value is returned.
+     *
+     * @param key the key of an integer setting
+     * @param defaultValue the value to return if the key is not found or the
+     * value is not an integer
+     */
+    public int getInteger(String key, int defaultValue) {
+        Object val = get(key);
+        if (val == null) {
+            return defaultValue;
+        }
+        return (Integer) val;
     }
 
     /**
@@ -524,12 +542,25 @@ public final class AppSettings extends HashMap<String, Object> {
      * @return the corresponding value, or false if not set
      */
     public boolean getBoolean(String key) {
-        Boolean b = (Boolean) get(key);
-        if (b == null) {
-            return false;
-        }
+        return getBoolean(key, false);
+    }
 
-        return b.booleanValue();
+    /**
+     * Get a boolean from the settings.
+     * <p>
+     * If the key is not set, or the stored value is not a Boolean, then the
+     * provided default value is returned.
+     *
+     * @param key the key of a boolean setting
+     * @param defaultValue the value to return if the key is not found or the
+     * value is not a boolean
+     */
+    public boolean getBoolean(String key, boolean defaultValue) {
+        Object val = get(key);
+        if (val == null) {
+            return defaultValue;
+        }
+        return (Boolean) val;
     }
 
     /**
@@ -541,12 +572,25 @@ public final class AppSettings extends HashMap<String, Object> {
      * @return the corresponding value, or null if not set
      */
     public String getString(String key) {
-        String s = (String) get(key);
-        if (s == null) {
-            return null;
-        }
+        return getString(key, null);
+    }
 
-        return s;
+    /**
+     * Get a string from the settings.
+     * <p>
+     * If the key is not set, or the stored value is not a String, then the
+     * provided default value is returned.
+     *
+     * @param key the key of a string setting
+     * @param defaultValue the value to return if the key is not found or the
+     * value is not a string
+     */
+    public String getString(String key, String defaultValue) {
+        Object val = get(key);
+        if (val == null) {
+            return defaultValue;
+        }
+        return (String) val;
     }
 
     /**
@@ -558,12 +602,25 @@ public final class AppSettings extends HashMap<String, Object> {
      * @return the corresponding value, or 0 if not set
      */
     public float getFloat(String key) {
-        Float f = (Float) get(key);
-        if (f == null) {
-            return 0f;
-        }
+        return getFloat(key, 0f);
+    }
 
-        return f.floatValue();
+    /**
+     * Get a float from the settings.
+     * <p>
+     * If the key is not set, or the stored value is not a Float, then the
+     * provided default value is returned.
+     *
+     * @param key the key of a float setting
+     * @param defaultValue the value to return if the key is not found or the
+     * value is not a float
+     */
+    public float getFloat(String key, float defaultValue) {
+        Object val = get(key);
+        if (val == null) {
+            return defaultValue;
+        }
+        return (Float) val;
     }
 
     /**
@@ -573,7 +630,7 @@ public final class AppSettings extends HashMap<String, Object> {
      * @param value the desired integer value
      */
     public void putInteger(String key, int value) {
-        put(key, Integer.valueOf(value));
+        put(key, value);
     }
 
     /**
@@ -583,7 +640,7 @@ public final class AppSettings extends HashMap<String, Object> {
      * @param value the desired boolean value
      */
     public void putBoolean(String key, boolean value) {
-        put(key, Boolean.valueOf(value));
+        put(key, value);
     }
 
     /**
@@ -603,7 +660,7 @@ public final class AppSettings extends HashMap<String, Object> {
      * @param value the desired float value
      */
     public void putFloat(String key, float value) {
-        put(key, Float.valueOf(value));
+        put(key, value);
     }
 
     /**
@@ -698,9 +755,9 @@ public final class AppSettings extends HashMap<String, Object> {
     /**
      * Set the graphics renderer to use, one of:<br>
      * <ul>
-     * <li>AppSettings.LWJGL_OPENGL1 - Force OpenGL1.1 compatability</li>
-     * <li>AppSettings.LWJGL_OPENGL2 - Force OpenGL2 compatability</li>
-     * <li>AppSettings.LWJGL_OPENGL3 - Force OpenGL3.3 compatability</li>
+     * <li>AppSettings.LWJGL_OPENGL1 - Force OpenGL1.1 compatibility</li>
+     * <li>AppSettings.LWJGL_OPENGL2 - Force OpenGL2 compatibility</li>
+     * <li>AppSettings.LWJGL_OPENGL3 - Force OpenGL3.3 compatibility</li>
      * <li>AppSettings.LWJGL_OPENGL_ANY - Choose an appropriate
      * OpenGL version based on system capabilities</li>
      * <li>AppSettings.JOGL_OPENGL_BACKWARD_COMPATIBLE</li>
@@ -739,7 +796,7 @@ public final class AppSettings extends HashMap<String, Object> {
     }
 
     /**
-     * @param value the width for the default framebuffer.
+     * @param value the width for the default frame buffer.
      * (Default: 640)
      */
     public void setWidth(int value) {
@@ -747,7 +804,7 @@ public final class AppSettings extends HashMap<String, Object> {
     }
 
     /**
-     * @param value the height for the default framebuffer.
+     * @param value the height for the default frame buffer.
      * (Default: 480)
      */
     public void setHeight(int value) {
@@ -755,7 +812,7 @@ public final class AppSettings extends HashMap<String, Object> {
     }
 
     /**
-     * Set the resolution for the default framebuffer
+     * Set the resolution for the default frame buffer
      * Use {@link #setWindowSize(int, int)} instead, for HiDPI display support.
      * @param width The width
      * @param height The height
@@ -769,8 +826,8 @@ public final class AppSettings extends HashMap<String, Object> {
     /**
      * Set the size of the window
      *
-     * @param width The width in pixels (default = width of the default framebuffer)
-     * @param height The height in pixels (default = height of the default framebuffer)
+     * @param width The width in pixels (default = width of the default frame buffer)
+     * @param height The height in pixels (default = height of the default frame buffer)
      */
     public void setWindowSize(int width, int height) {
         putInteger("WindowWidth", width);
@@ -960,7 +1017,7 @@ public final class AppSettings extends HashMap<String, Object> {
     /**
      * Enable or disable gamma correction. If enabled, the main framebuffer will
      * be configured for sRGB colors, and sRGB images will be linearized.
-     *
+     * <p>
      * Gamma correction requires a GPU that supports GL_ARB_framebuffer_sRGB;
      * otherwise this setting will be ignored.
      *
@@ -971,7 +1028,7 @@ public final class AppSettings extends HashMap<String, Object> {
     }
 
     /**
-     * Get the framerate.
+     * Get the frame rate.
      *
      * @return the maximum rate (in frames per second), or -1 for unlimited
      * @see #setFrameRate(int)
@@ -1004,7 +1061,7 @@ public final class AppSettings extends HashMap<String, Object> {
     /**
      * Get the width
      *
-     * @return the width of the default framebuffer (in pixels)
+     * @return the width of the default frame buffer (in pixels)
      * @see #setWidth(int)
      */
     public int getWidth() {
@@ -1014,7 +1071,7 @@ public final class AppSettings extends HashMap<String, Object> {
     /**
      * Get the height
      *
-     * @return the height of the default framebuffer (in pixels)
+     * @return the height of the default frame buffer (in pixels)
      * @see #setHeight(int)
      */
     public int getHeight() {
@@ -1215,7 +1272,7 @@ public final class AppSettings extends HashMap<String, Object> {
 
     /**
      * Allows the display window to be resized by dragging its edges.
-     *
+     * <p>
      * Only supported for {@link JmeContext.Type#Display} contexts which
      * are in windowed mode, ignored for other types.
      * The default value is <code>false</code>.
@@ -1240,7 +1297,7 @@ public final class AppSettings extends HashMap<String, Object> {
 
     /**
      * When enabled the display context will swap buffers every frame.
-     *
+     * <p>
      * This may need to be disabled when integrating with an external
      * library that handles buffer swapping on its own, e.g. Oculus Rift.
      * When disabled, the engine will process window messages
@@ -1282,7 +1339,7 @@ public final class AppSettings extends HashMap<String, Object> {
     /**
      * Sets a custom platform chooser. This chooser specifies which platform and
      * which devices are used for the OpenCL context.
-     *
+     * <p>
      * Default: an implementation defined one.
      *
      * @param chooser the class of the chooser, must have a default constructor
@@ -1507,4 +1564,52 @@ public final class AppSettings extends HashMap<String, Object> {
     public void setDisplay(int mon) {
         putInteger("Display", mon);
     }
+
+    /**
+     * Prints all key-value pairs stored under a given preferences key
+     * in the Java Preferences API to standard output.
+     *
+     * @param preferencesKey The preferences key (node path) to inspect.
+     * @throws BackingStoreException If an exception occurs while accessing the preferences.
+     */
+    public static void printPreferences(String preferencesKey) throws BackingStoreException {
+        Preferences prefs = Preferences.userRoot().node(preferencesKey);
+        String[] keys = prefs.keys();
+
+        if (keys == null || keys.length == 0) {
+            logger.log(Level.WARNING, "No Preferences found under key: {0}", preferencesKey);
+        } else {
+            StringBuilder sb = new StringBuilder();
+            sb.append("Preferences for key: ").append(preferencesKey);
+            for (String key : keys) {
+                // Retrieve the value as a String (default fallback for Preferences API)
+                String value = prefs.get(key, "[Value Not Found]");
+                sb.append("\n * ").append(key).append(" = ").append(value);
+            }
+            logger.log(Level.INFO, sb.toString());
+        }
+    }
+    /**
+     * Sets the preferred native platform for creating the GL context on Linux distributions.
+     * <p>
+     * This setting is relevant for Linux distributions or derivatives that utilize a Wayland session alongside an X11 via the XWayland bridge.
+     * Enabling this option allows the use of GLX for window positioning and/or icon configuration.
+     *
+     * @param preferred true to prefer GLX (native X11) for the GL context, false to prefer EGL (native Wayland).
+     */
+    public void setX11PlatformPreferred(boolean preferred) {
+        putBoolean("X11PlatformPreferred", preferred);
+    }
+    
+    /**
+     * Determines which native platform is preferred for GL context creation on Linux distributions.
+     * <p>
+     * This setting is only valid on Linux distributions or derivatives that support Wayland,
+     * and it indicates whether GLX (native X11) or EGL (native Wayland) is enabled for the GL context.
+     *
+     * @return true if GLX is preferred, otherwise false if EGL is preferred (native Wayland).
+     */
+    public boolean isX11PlatformPreferred() {
+        return getBoolean("X11PlatformPreferred");
+    }
 }

+ 72 - 0
jme3-core/src/main/java/com/jme3/util/BufferInputStream.java

@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2009-2025 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.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+public class BufferInputStream extends InputStream {
+
+    ByteBuffer input;
+
+    public BufferInputStream(ByteBuffer input) {
+        this.input = input;
+    }
+
+    @Override
+    public int read() throws IOException {
+        if (input.remaining() == 0) return -1; else return input.get() & 0xff;
+    }
+
+    @Override
+    public int read(byte[] b) {
+        return read(b, 0, b.length);
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) {
+        if (b == null) throw new NullPointerException("b == null");
+        if (off < 0 || len < 0 || len > b.length - off) throw new IndexOutOfBoundsException();
+        if (len == 0) return 0;
+        if (!input.hasRemaining()) return -1;
+
+        int toRead = Math.min(len, input.remaining());
+        input.get(b, off, toRead);
+        return toRead;
+    }
+
+    @Override
+    public int available() {
+        return input.remaining();
+    }
+}

+ 38 - 0
jme3-core/src/main/resources/Common/MatDefs/Misc/Dashed.j3md

@@ -0,0 +1,38 @@
+MaterialDef Dashed {
+    MaterialParameters {
+        Float DashSize
+        Vector4 Color
+    }
+    Technique {
+        WorldParameters {
+            WorldViewProjectionMatrix
+        }
+        VertexShaderNodes {
+            ShaderNode CommonVert {
+                Definition : CommonVert : Common/MatDefs/ShaderNodes/Common/CommonVert.j3sn
+                InputMappings {
+                    worldViewProjectionMatrix = WorldParam.WorldViewProjectionMatrix
+                    modelPosition = Global.position.xyz
+                    texCoord1 = Attr.inTexCoord
+                    vertColor = Attr.inColor
+                }
+                OutputMappings {
+                    Global.position = projPosition
+                }
+            }
+        }
+        FragmentShaderNodes {
+            ShaderNode Dashed {
+                Definition : Dashed : Common/MatDefs/ShaderNodes/Common/DashedPattern.j3sn
+                InputMappings {
+                    texCoord = CommonVert.texCoord1
+                    inColor = MatParam.Color
+                    size = MatParam.DashSize
+                }
+                OutputMappings {
+                    Global.color = outColor
+                }
+            }
+        }
+    }
+}

+ 23 - 0
jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/DashedPattern.j3sn

@@ -0,0 +1,23 @@
+ShaderNodeDefinitions{ 
+    ShaderNodeDefinition Dashed {      
+        Type: Fragment
+
+        Shader GLSL100: Common/MatDefs/ShaderNodes/Common/DashedPattern100.frag
+        
+        Documentation{
+            Renders dashed lines            
+            @input vec2 texCoord The texture coordinates
+            @input float size the size of the dashes
+            @input vec4 inColor the color of the fragment so far
+            @outColor vec4 color the output color
+        }
+        Input {
+            vec2 texCoord
+            vec4 inColor
+            float size
+        }
+        Output {
+            vec4 outColor
+        }
+    }
+}

+ 9 - 0
jme3-core/src/main/resources/Common/MatDefs/ShaderNodes/Common/DashedPattern100.frag

@@ -0,0 +1,9 @@
+void main(){
+    //@input vec2 texCoord The texture coordinates
+    //@input float size the size of the dashes
+    //@output vec4 color the output color
+
+    //insert glsl code here
+    outColor = inColor;
+    outColor.a = step(1.0 - size, texCoord.x);
+}

BIN
jme3-core/src/main/resources/Common/Textures/lightbulb32.png


+ 153 - 72
jme3-core/src/plugins/java/com/jme3/audio/plugins/WAVLoader.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -47,39 +47,62 @@ import java.nio.ByteBuffer;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+/**
+ * An {@code AssetLoader} for loading WAV audio files.
+ * This loader supports PCM (Pulse Code Modulation) WAV files,
+ * both as in-memory {@link AudioBuffer}s and streaming {@link AudioStream}s.
+ * It handles 8-bit and 16-bit audio formats.
+ *
+ * <p>The WAV file format consists of chunks. This loader specifically parses
+ * the 'RIFF', 'WAVE', 'fmt ', and 'data' chunks.
+ */
 public class WAVLoader implements AssetLoader {
 
     private static final Logger logger = Logger.getLogger(WAVLoader.class.getName());
 
-    // all these are in big endian
+    // RIFF chunk identifiers (Big-Endian representation of ASCII characters)
     private static final int i_RIFF = 0x46464952;
     private static final int i_WAVE = 0x45564157;
     private static final int i_fmt  = 0x20746D66;
     private static final int i_data = 0x61746164;
 
-    private boolean readStream = false;
-
-    private AudioBuffer audioBuffer;
-    private AudioStream audioStream;
-    private AudioData audioData;
+    /**
+     * The number of bytes per second for the audio data, calculated from the WAV header.
+     * Used to determine the duration of the audio.
+     */
     private int bytesPerSec;
+    /**
+     * The duration of the audio in seconds.
+     */
     private float duration;
-
+    /**
+     * The input stream for reading the WAV file data.
+     */
     private ResettableInputStream in;
-    private int inOffset = 0;
-    
+
+    /**
+     * A custom {@link InputStream} extension that handles little-endian byte
+     * reading and provides seek capabilities for streaming audio by reopening
+     * and skipping the input stream.
+     */
     private static class ResettableInputStream extends LittleEndien implements SeekableStream {
         
-        final private AssetInfo info;
+        private final AssetInfo info;
         private int resetOffset = 0;
         
         public ResettableInputStream(AssetInfo info, InputStream in) {
             super(in);
             this.info = info;
         }
-        
-        public void setResetOffset(int resetOffset) {
-            this.resetOffset = resetOffset;
+
+        /**
+         * Sets the offset from the beginning of the file to reset the stream to.
+         * This is typically the start of the audio data chunk.
+         *
+         * @param offset The byte offset to reset to.
+         */
+        public void setResetOffset(int offset) {
+            this.resetOffset = offset;
         }
 
         @Override
@@ -95,122 +118,170 @@ public class WAVLoader implements AssetLoader {
                 // Resource could have gotten lost, etc.
                 try {
                     newStream.close();
-                } catch (IOException ex2) {
+                } catch (IOException ignored) {
                 }
                 throw new RuntimeException(ex);
             }
         }
     }
 
-    private void readFormatChunk(int size) throws IOException{
+    /**
+     * Reads and parses the 'fmt ' (format) chunk of the WAV file.
+     * This chunk contains information about the audio format such as
+     * compression, channels, sample rate, bits per sample, etc.
+     *
+     * @param chunkSize The size of the 'fmt ' chunk in bytes.
+     * @param audioData The {@link AudioData} object to set the format information on.
+     * @throws IOException if the file is not a supported PCM WAV, or if format
+     * parameters are invalid.
+     */
+    private void readFormatChunk(int chunkSize, AudioData audioData) throws IOException {
         // if other compressions are supported, size doesn't have to be 16
 //        if (size != 16)
 //            logger.warning("Expected size of format chunk to be 16");
 
         int compression = in.readShort();
-        if (compression != 1){
+        if (compression != 1) { // 1 = PCM (Pulse Code Modulation)
             throw new IOException("WAV Loader only supports PCM wave files");
         }
 
-        int channels = in.readShort();
+        int numChannels = in.readShort();
         int sampleRate = in.readInt();
+        bytesPerSec = in.readInt(); // Average bytes per second
 
-        bytesPerSec = in.readInt(); // used to calculate duration
-
-        int bytesPerSample = in.readShort();
+        int bytesPerSample = in.readShort(); // Bytes per sample block (channels * bytesPerSample)
         int bitsPerSample = in.readShort();
 
-        int expectedBytesPerSec = (bitsPerSample * channels * sampleRate) / 8;
-        if (expectedBytesPerSec != bytesPerSec){
+        int expectedBytesPerSec = (bitsPerSample * numChannels * sampleRate) / 8;
+        if (expectedBytesPerSec != bytesPerSec) {
             logger.log(Level.WARNING, "Expected {0} bytes per second, got {1}",
                     new Object[]{expectedBytesPerSec, bytesPerSec});
         }
-        
+
         if (bitsPerSample != 8 && bitsPerSample != 16)
             throw new IOException("Only 8 and 16 bits per sample are supported!");
 
-        if ( (bitsPerSample / 8) * channels != bytesPerSample)
+        if ((bitsPerSample / 8) * numChannels != bytesPerSample)
             throw new IOException("Invalid bytes per sample value");
 
         if (bytesPerSample * sampleRate != bytesPerSec)
             throw new IOException("Invalid bytes per second value");
 
-        audioData.setupFormat(channels, bitsPerSample, sampleRate);
+        audioData.setupFormat(numChannels, bitsPerSample, sampleRate);
 
-        int remaining = size - 16;
-        if (remaining > 0){
-            in.skipBytes(remaining);
+        // Skip any extra parameters in the format chunk (e.g., for non-PCM formats)
+        int remainingChunkBytes = chunkSize - 16;
+        if (remainingChunkBytes > 0) {
+            in.skipBytes(remainingChunkBytes);
         }
     }
 
-    private void readDataChunkForBuffer(int len) throws IOException {
-        ByteBuffer data = BufferUtils.createByteBuffer(len);
-        byte[] buf = new byte[512];
+    /**
+     * Reads the 'data' chunk for an {@link AudioBuffer}. This involves loading
+     * the entire audio data into a {@link ByteBuffer} in memory.
+     *
+     * @param dataChunkSize The size of the 'data' chunk in bytes.
+     * @param audioBuffer   The {@link AudioBuffer} to update with the loaded data.
+     * @throws IOException if an error occurs while reading the data.
+     */
+    private void readDataChunkForBuffer(int dataChunkSize, AudioBuffer audioBuffer) throws IOException {
+        ByteBuffer data = BufferUtils.createByteBuffer(dataChunkSize);
+        byte[] buf = new byte[1024]; // Use a larger buffer for efficiency
         int read = 0;
-        while ( (read = in.read(buf)) > 0){
-            data.put(buf, 0, Math.min(read, data.remaining()) );
+        while ((read = in.read(buf)) > 0) {
+            data.put(buf, 0, Math.min(read, data.remaining()));
         }
         data.flip();
         audioBuffer.updateData(data);
         in.close();
     }
 
-    private void readDataChunkForStream(int offset, int len) throws IOException {
-        in.setResetOffset(offset);
+    /**
+     * Configures the {@link AudioStream} to stream data from the 'data' chunk.
+     * This involves setting the reset offset for seeking and passing the
+     * input stream and duration to the {@link AudioStream}.
+     *
+     * @param dataChunkOffset The byte offset from the start of the file where the 'data' chunk begins.
+     * @param dataChunkSize   The size of the 'data' chunk in bytes.
+     * @param audioStream     The {@link AudioStream} to configure.
+     */
+    private void readDataChunkForStream(int dataChunkOffset, int dataChunkSize, AudioStream audioStream) {
+        in.setResetOffset(dataChunkOffset);
         audioStream.updateData(in, duration);
     }
 
-    private AudioData load(AssetInfo info, InputStream inputStream, boolean stream) throws IOException{
+    /**
+     * Main loading logic for WAV files. This method parses the RIFF, WAVE, fmt,
+     * and data chunks to extract audio information and data.
+     *
+     * @param info        The {@link AssetInfo} for the WAV file.
+     * @param inputStream The initial {@link InputStream} opened for the asset.
+     * @param stream      A boolean indicating whether the audio should be loaded
+     *                    as a stream (true) or an in-memory buffer (false).
+     * @return The loaded {@link AudioData} (either {@link AudioBuffer} or {@link AudioStream}).
+     * @throws IOException if the file is not a valid WAV, or if any I/O error occurs.
+     */
+    private AudioData load(AssetInfo info, InputStream inputStream, boolean stream) throws IOException {
         this.in = new ResettableInputStream(info, inputStream);
-        inOffset = 0;
-        
-        int sig = in.readInt();
-        if (sig != i_RIFF)
+        int inOffset = 0;
+
+        // Read RIFF chunk
+        int riffId = in.readInt();
+        if (riffId != i_RIFF) {
             throw new IOException("File is not a WAVE file");
-        
-        // skip size
+        }
+
+        // Skip RIFF chunk size
         in.readInt();
-        if (in.readInt() != i_WAVE)
+
+        int waveId = in.readInt();
+        if (waveId != i_WAVE)
             throw new IOException("WAVE File does not contain audio");
 
-        inOffset += 4 * 3;
-        
-        readStream = stream;
-        if (readStream){
+        inOffset += 4 * 3; // RIFF_ID + ChunkSize + WAVE_ID
+
+        AudioData audioData;
+        AudioBuffer audioBuffer = null;
+        AudioStream audioStream = null;
+
+        if (stream) {
             audioStream = new AudioStream();
             audioData = audioStream;
-        }else{
+        } else {
             audioBuffer = new AudioBuffer();
             audioData = audioBuffer;
         }
 
         while (true) {
-            int type = in.readInt();
-            int len = in.readInt();
-            
-            inOffset += 4 * 2;
+            int chunkType = in.readInt();
+            int chunkSize = in.readInt();
+
+            inOffset += 4 * 2; // ChunkType + ChunkSize
 
-            switch (type) {
+            switch (chunkType) {
                 case i_fmt:
-                    readFormatChunk(len);
-                    inOffset += len;
+                    readFormatChunk(chunkSize, audioData);
+                    inOffset += chunkSize;
                     break;
                 case i_data:
                     // Compute duration based on data chunk size
-                    duration = len / bytesPerSec;
+                    duration = (float) (chunkSize / bytesPerSec);
 
-                    if (readStream) {
-                        readDataChunkForStream(inOffset, len);
+                    if (stream) {
+                        readDataChunkForStream(inOffset, chunkSize, audioStream);
                     } else {
-                        readDataChunkForBuffer(len);
+                        readDataChunkForBuffer(chunkSize, audioBuffer);
                     }
                     return audioData;
                 default:
-                    int skipped = in.skipBytes(len);
-                    if (skipped <= 0) {
+                    // Skip unknown chunks
+                    int skippedBytes = in.skipBytes(chunkSize);
+                    if (skippedBytes <= 0) {
+                        logger.log(Level.WARNING, "Reached end of stream prematurely while skipping unknown chunk of size {0}. Asset: {1}",
+                                new Object[]{chunkSize, info.getKey().getName()});
                         return null;
                     }
-                    inOffset += skipped;
+                    inOffset += skippedBytes;
                     break;
             }
         }
@@ -218,18 +289,28 @@ public class WAVLoader implements AssetLoader {
     
     @Override
     public Object load(AssetInfo info) throws IOException {
-        AudioData data;
-        InputStream inputStream = null;
+        InputStream is = null;
         try {
-            inputStream = info.openStream();
-            data = load(info, inputStream, ((AudioKey)info.getKey()).isStream());
-            if (data instanceof AudioStream){
-                inputStream = null;
+            is = info.openStream();
+            boolean streamAudio = ((AudioKey) info.getKey()).isStream();
+            AudioData loadedData = load(info, is, streamAudio);
+
+            // If it's an AudioStream, the internal inputStream is managed by the stream itself
+            // and should not be closed here.
+            if (loadedData instanceof AudioStream) {
+                // Prevent closing in finally block
+                is = null;
             }
-            return data;
+            return loadedData;
         } finally {
-            if (inputStream != null){
-                inputStream.close();
+            // Nullify/reset instance variables to ensure the loader instance is clean
+            // for the next load operation.
+            in = null;
+            bytesPerSec = 0;
+            duration = 0.0f;
+
+            if (is != null) {
+                is.close();
             }
         }
     }

+ 46 - 45
jme3-core/src/plugins/java/com/jme3/export/binary/BinaryExporter.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -38,7 +38,13 @@ import com.jme3.export.OutputCapsule;
 import com.jme3.export.Savable;
 import com.jme3.export.SavableClassUtil;
 import com.jme3.math.FastMath;
-import java.io.*;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.IdentityHashMap;
@@ -116,39 +122,40 @@ import java.util.logging.Logger;
  *
  * @author Joshua Slack
  */
-
 public class BinaryExporter implements JmeExporter {
-    private static final Logger logger = Logger.getLogger(BinaryExporter.class
-            .getName());
+
+    private static final Logger logger = Logger.getLogger(BinaryExporter.class.getName());
 
     protected int aliasCount = 1;
     protected int idCount = 1;
 
-    private final IdentityHashMap<Savable, BinaryIdContentPair> contentTable
-             = new IdentityHashMap<>();
-
-    protected HashMap<Integer, Integer> locationTable
-             = new HashMap<>();
+    private final IdentityHashMap<Savable, BinaryIdContentPair> contentTable = new IdentityHashMap<>();
+    protected HashMap<Integer, Integer> locationTable = new HashMap<>();
 
     // key - class name, value = bco
-    private final HashMap<String, BinaryClassObject> classes
-             = new HashMap<>();
-
+    private final HashMap<String, BinaryClassObject> classes = new HashMap<>();
     private final ArrayList<Savable> contentKeys = new ArrayList<>();
 
     public static boolean debug = false;
     public static boolean useFastBufs = true;
 
+    /**
+     * Constructs a new {@code BinaryExporter}.
+     */
     public BinaryExporter() {
     }
 
+    /**
+     * Returns a new instance of {@code BinaryExporter}.
+     *
+     * @return A new {@code BinaryExporter} instance.
+     */
     public static BinaryExporter getInstance() {
         return new BinaryExporter();
     }
 
     /**
      * Saves the object into memory then loads it from memory.
-     *
      * Used by tests to check if the persistence system is working.
      *
      * @param <T> The type of savable.
@@ -163,9 +170,11 @@ public class BinaryExporter implements JmeExporter {
         try {
             BinaryExporter exporter = new BinaryExporter();
             exporter.save(object, baos);
+
             BinaryImporter importer = new BinaryImporter();
             importer.setAssetManager(assetManager);
             return (T) importer.load(baos.toByteArray());
+
         } catch (IOException ex) {
             // Should never happen.
             throw new AssertionError(ex);
@@ -191,28 +200,25 @@ public class BinaryExporter implements JmeExporter {
         // write out tag table
         int classTableSize = 0;
         int classNum = classes.keySet().size();
-        int aliasSize = ((int) FastMath.log(classNum, 256) + 1); // make all
-                                                                  // aliases a
-                                                                  // fixed width
+        int aliasSize = ((int) FastMath.log(classNum, 256) + 1); // make all aliases a fixed width
 
         os.write(ByteUtils.convertToBytes(classNum)); // 3. "number of classes"
         for (String key : classes.keySet()) {
             BinaryClassObject bco = classes.get(key);
 
             // write alias
-            byte[] aliasBytes = fixClassAlias(bco.alias,
-                    aliasSize);
+            byte[] aliasBytes = fixClassAlias(bco.alias, aliasSize);
             os.write(aliasBytes);                     // 4. "class alias"
             classTableSize += aliasSize;
 
             // jME3 NEW: Write class hierarchy version numbers
-            os.write( bco.classHierarchyVersions.length );
-            for (int version : bco.classHierarchyVersions){
+            os.write(bco.classHierarchyVersions.length);
+            for (int version : bco.classHierarchyVersions) {
                 os.write(ByteUtils.convertToBytes(version));
             }
             classTableSize += 1 + bco.classHierarchyVersions.length * 4;
 
-            // write classname size & classname
+            // write class name size & class name
             byte[] classBytes = key.getBytes();
             os.write(ByteUtils.convertToBytes(classBytes.length)); // 5. "full class-name size"
             os.write(classBytes);                                  // 6. "full class name"
@@ -236,14 +242,12 @@ public class BinaryExporter implements JmeExporter {
         // write out data to a separate stream
         int location = 0;
         // keep track of location for each piece
-        HashMap<String, ArrayList<BinaryIdContentPair>> alreadySaved = new HashMap<>(
-                contentTable.size());
+        HashMap<String, ArrayList<BinaryIdContentPair>> alreadySaved = new HashMap<>(contentTable.size());
         for (Savable savable : contentKeys) {
             // look back at previous written data for matches
             String savableName = savable.getClass().getName();
             BinaryIdContentPair pair = contentTable.get(savable);
-            ArrayList<BinaryIdContentPair> bucket = alreadySaved
-                    .get(savableName + getChunk(pair));
+            ArrayList<BinaryIdContentPair> bucket = alreadySaved.get(savableName + getChunk(pair));
             int prevLoc = findPrevMatch(pair, bucket);
             if (prevLoc != -1) {
                 locationTable.put(pair.getId(), prevLoc);
@@ -286,17 +290,14 @@ public class BinaryExporter implements JmeExporter {
         // append stream to the output stream
         out.writeTo(os);
 
-
-        out = null;
-        os = null;
-
         if (debug) {
-            logger.fine("Stats:");
-            logger.log(Level.FINE, "classes: {0}", classNum);
-            logger.log(Level.FINE, "class table: {0} bytes", classTableSize);
-            logger.log(Level.FINE, "objects: {0}", numLocations);
-            logger.log(Level.FINE, "location table: {0} bytes", locationTableSize);
-            logger.log(Level.FINE, "data: {0} bytes", location);
+            logger.log(Level.INFO, "BinaryExporter Stats:"
+                    + "\n * Classes: {0}"
+                    + "\n * Class Table: {1} bytes"
+                    + "\n * Objects: {2}"
+                    + "\n * Location Table: {3} bytes"
+                    + "\n * Data: {4} bytes",
+                    new Object[] {classNum, classTableSize, numLocations, locationTableSize, location});
         }
     }
 
@@ -305,14 +306,15 @@ public class BinaryExporter implements JmeExporter {
                 .getContent().bytes.length));
     }
 
-    private int findPrevMatch(BinaryIdContentPair oldPair,
-            ArrayList<BinaryIdContentPair> bucket) {
-        if (bucket == null)
+    private int findPrevMatch(BinaryIdContentPair oldPair, ArrayList<BinaryIdContentPair> bucket) {
+        if (bucket == null) {
             return -1;
+        }
         for (int x = bucket.size(); --x >= 0;) {
             BinaryIdContentPair pair = bucket.get(x);
-            if (pair.getContent().equals(oldPair.getContent()))
+            if (pair.getContent().equals(oldPair.getContent())) {
                 return locationTable.get(pair.getId());
+            }
         }
         return -1;
     }
@@ -345,7 +347,7 @@ public class BinaryExporter implements JmeExporter {
         return contentTable.get(object).getContent();
     }
 
-    private BinaryClassObject createClassObject(Class<? extends Savable> clazz) throws IOException{
+    private BinaryClassObject createClassObject(Class<? extends Savable> clazz) throws IOException {
         BinaryClassObject bco = new BinaryClassObject();
         bco.alias = generateTag();
         bco.nameFields = new HashMap<>();
@@ -361,10 +363,10 @@ public class BinaryExporter implements JmeExporter {
             return -1;
         }
         Class<? extends Savable> clazz = object.getClass();
-        BinaryClassObject bco = classes.get(object.getClass().getName());
+        BinaryClassObject bco = classes.get(clazz.getName());
         // is this class been looked at before? in tagTable?
         if (bco == null) {
-            bco = createClassObject(object.getClass());
+            bco = createClassObject(clazz);
         }
 
         // is object in contentTable?
@@ -379,7 +381,6 @@ public class BinaryExporter implements JmeExporter {
         object.write(this);
         newPair.getContent().finish();
         return newPair.getId();
-
     }
 
     protected byte[] generateTag() {
@@ -401,4 +402,4 @@ public class BinaryExporter implements JmeExporter {
                 new BinaryOutputCapsule(this, bco));
         return pair;
     }
-}
+}

+ 7 - 9
jme3-core/src/plugins/java/com/jme3/material/plugins/ShaderNodeDefinitionLoader.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2018 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -38,13 +38,14 @@ import com.jme3.asset.AssetLoader;
 import com.jme3.asset.ShaderNodeDefinitionKey;
 import com.jme3.util.blockparser.BlockLanguageParser;
 import com.jme3.util.blockparser.Statement;
+
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.List;
 
 /**
  * ShaderNodeDefinition file loader (.j3sn)
- *
+ * <p>
  * a j3sn file is a block style file like j3md or j3m. It must contain one
  * ShaderNodeDefinition{} block that contains several ShaderNodeDefinition{}
  * blocks
@@ -53,16 +54,14 @@ import java.util.List;
  */
 public class ShaderNodeDefinitionLoader implements AssetLoader {
 
-    private ShaderNodeLoaderDelegate loaderDelegate;
-
     @Override
     public Object load(AssetInfo assetInfo) throws IOException {
-        AssetKey k = assetInfo.getKey();
-        if (!(k instanceof ShaderNodeDefinitionKey)) {
+        AssetKey<?> assetKey = assetInfo.getKey();
+        if (!(assetKey instanceof ShaderNodeDefinitionKey)) {
             throw new IOException("ShaderNodeDefinition file must be loaded via ShaderNodeDefinitionKey");
         }
-        ShaderNodeDefinitionKey key = (ShaderNodeDefinitionKey) k;
-        loaderDelegate = new ShaderNodeLoaderDelegate();
+        ShaderNodeDefinitionKey key = (ShaderNodeDefinitionKey) assetKey;
+        ShaderNodeLoaderDelegate loaderDelegate = new ShaderNodeLoaderDelegate();
 
         InputStream in = assetInfo.openStream();
         List<Statement> roots = BlockLanguageParser.parse(in);
@@ -80,6 +79,5 @@ public class ShaderNodeDefinitionLoader implements AssetLoader {
         }
 
         return loaderDelegate.readNodesDefinitions(roots.get(0).getContents(), key);
-
     }
 }

+ 30 - 0
jme3-core/src/test/java/com/jme3/math/ColorRGBATest.java

@@ -0,0 +1,30 @@
+package com.jme3.math;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * @author capdevon
+ */
+public class ColorRGBATest {
+
+    @Test
+    public void testIntColor() {
+        ColorRGBA color = new ColorRGBA(1.0f, 0.2f, 0.6f, 0.8f);
+
+        int rgba = color.asIntRGBA();
+        int abgr = color.asIntABGR();
+        int argb = color.asIntARGB();
+
+        Assert.assertEquals(-13395508, rgba);
+        Assert.assertEquals(-862374913, abgr);
+        Assert.assertEquals(-855690343, argb);
+
+        ColorRGBA copy = new ColorRGBA();
+
+        Assert.assertEquals(color, copy.fromIntRGBA(rgba));
+        Assert.assertEquals(color, copy.fromIntABGR(abgr));
+        Assert.assertEquals(color, copy.fromIntARGB(argb));
+    }
+
+}

+ 32 - 0
jme3-core/src/test/java/com/jme3/scene/SpatialTest.java

@@ -31,6 +31,9 @@
  */
 package com.jme3.scene;
 
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
 import com.jme3.scene.control.UpdateControl;
 import org.junit.Assert;
 import org.junit.Test;
@@ -119,4 +122,33 @@ public class SpatialTest {
         Assert.assertEquals(testSpatial, control1.getSpatial());
         Assert.assertEquals(testSpatial, control2.getSpatial());
     }
+
+    @Test
+    public void testTransferToOtherNode(){
+        Node nodeA = new Node("nodeA");
+        Node nodeB = new Node("nodeB");
+        Node testNode=new Node("testNode");
+        nodeA.setLocalTranslation(-1,0,0);
+        nodeB.setLocalTranslation(1,0,0);
+        nodeB.rotate(0,90* FastMath.DEG_TO_RAD,0);
+        testNode.setLocalTranslation(1,0,0);
+        nodeA.attachChild(testNode);
+        Vector3f worldTranslation = testNode.getWorldTranslation().clone();
+        Quaternion worldRotation = testNode.getWorldRotation().clone();
+
+        Assert.assertTrue(worldTranslation.isSimilar(testNode.getWorldTranslation(),1e-6f));
+        Assert.assertTrue(worldRotation.isSimilar(testNode.getWorldRotation(),1e-6f));
+
+        nodeB.attachChild(testNode);
+
+        Assert.assertFalse(worldTranslation.isSimilar(testNode.getWorldTranslation(),1e-6f));
+        Assert.assertFalse(worldRotation.isSimilar(testNode.getWorldRotation(),1e-6f));
+
+        testNode.setLocalTranslation(nodeB.worldToLocal(worldTranslation,null));
+        Assert.assertTrue(worldTranslation.isSimilar(testNode.getWorldTranslation(),1e-6f));
+
+        testNode.setLocalRotation(nodeB.worldToLocal(worldRotation,null));
+        System.out.println(testNode.getWorldRotation());
+        Assert.assertTrue(worldRotation.isSimilar(testNode.getWorldRotation(),1e-6f));
+    }
 }

+ 207 - 0
jme3-core/src/test/java/com/jme3/scene/threadwarden/SceneGraphThreadWardenGeometryExtendedTest.java

@@ -0,0 +1,207 @@
+package com.jme3.scene.threadwarden;
+
+import com.jme3.material.Material;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.shape.Box;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+import java.util.function.Consumer;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Parameterized tests for SceneGraphThreadWarden class with Geometry objects.
+ * These tests verify that various scene graph mutations are properly checked for thread safety.
+ */
+@RunWith(Parameterized.class)
+public class SceneGraphThreadWardenGeometryExtendedTest {
+
+    private static ExecutorService executorService;
+
+    private final String testName;
+    private final Consumer<Geometry> action;
+
+    @SuppressWarnings({"ReassignedVariable", "AssertWithSideEffects"})
+    @BeforeClass
+    public static void setupClass() {
+        // Make sure assertions are enabled
+        boolean assertsEnabled = false;
+        assert assertsEnabled = true;
+        if (!assertsEnabled) {
+            throw new RuntimeException("WARNING: Assertions are not enabled! Tests may not work correctly.");
+        }
+    }
+
+    @Before
+    public void setup() {
+        executorService = newSingleThreadDaemonExecutor();
+    }
+
+    @After
+    public void tearDown() {
+        executorService.shutdown();
+        SceneGraphThreadWarden.reset();
+    }
+
+    /**
+     * Constructor for the parameterized test.
+     * 
+     * @param testName A descriptive name for the test
+     * @param action The action to perform on the spatial
+     */
+    public SceneGraphThreadWardenGeometryExtendedTest(String testName, Consumer<Geometry> action) {
+        this.testName = testName;
+        this.action = action;
+    }
+
+    /**
+     * Define the parameters for the test.
+     * Each parameter is a pair of (test name, action to perform on spatial).
+     */
+    @Parameterized.Parameters(name = "{0}")
+    public static Collection<Object[]> data() {
+        Material mockMaterial = Mockito.mock(Material.class);
+        Box box = new Box(1, 1, 1);
+
+        return Arrays.asList(new Object[][] {
+            { 
+                "setMaterial", 
+                (Consumer<Geometry>) spatial -> spatial.setMaterial(mockMaterial)
+            },
+            { 
+                "setMesh", 
+                (Consumer<Geometry>) spatial -> spatial.setMesh(box)
+            },
+            { 
+                "setLodLevel", 
+                (Consumer<Geometry>) spatial -> {
+                    // Need to set a mesh with LOD levels first
+                    Mesh mesh = new Box(1, 1, 1);
+                    mesh.setLodLevels(new com.jme3.scene.VertexBuffer[]{
+                        mesh.getBuffer(com.jme3.scene.VertexBuffer.Type.Index)
+                    });
+                    spatial.setMesh(mesh);
+                    spatial.setLodLevel(0);
+                }
+            },
+            { 
+                "removeFromParent", 
+                (Consumer<Geometry>) Geometry::removeFromParent
+            }
+        });
+    }
+
+    /**
+     * Test that scene graph mutation is fine on the main thread when the object is attached to the root.
+     */
+    @Test
+    public void testMutationOnMainThreadOnAttachedObject() {
+        Node rootNode = new Node("root");
+        SceneGraphThreadWarden.setup(rootNode);
+
+        // Create a geometry and attach it to the root node
+        Geometry geometry = new Geometry("geometry", new Box(1, 1, 1));
+        rootNode.attachChild(geometry);
+
+        // This should work fine since we're on the main thread
+        action.accept(geometry);
+    }
+
+    /**
+     * Test that scene graph mutation is fine on the main thread when the object is not attached to the root.
+     */
+    @Test
+    public void testMutationOnMainThreadOnDetachedObject() {
+        Node rootNode = new Node("root");
+        SceneGraphThreadWarden.setup(rootNode);
+
+        // Create a geometry but don't attach it to the root node
+        Geometry geometry = new Geometry("geometry", new Box(1, 1, 1));
+
+        // This should work fine since we're on the main thread
+        action.accept(geometry);
+    }
+
+    /**
+     * Test that scene graph mutation is fine on a non-main thread when the object is not attached to the root.
+     */
+    @Test
+    public void testMutationOnNonMainThreadOnDetachedObject() throws ExecutionException, InterruptedException {
+        Node rootNode = new Node("root");
+        SceneGraphThreadWarden.setup(rootNode);
+
+        // Create a geometry but don't attach it to the root node
+        Geometry geometry = new Geometry("geometry", new Box(1, 1, 1));
+
+        Future<Void> future = executorService.submit(() -> {
+            // This should work fine since the geometry is not connected to the root node
+            action.accept(geometry);
+            return null;
+        });
+
+        // This should complete without exceptions
+        future.get();
+    }
+
+    /**
+     * Test that scene graph mutation is not allowed on a non-main thread when the object is attached to the root.
+     */
+    @Test
+    public void testMutationOnNonMainThreadOnAttachedObject() throws InterruptedException {
+        Node rootNode = new Node("root");
+        SceneGraphThreadWarden.setup(rootNode);
+
+        // Create a geometry and attach it to the root node
+        Geometry geometry = new Geometry("geometry", new Box(1, 1, 1));
+        rootNode.attachChild(geometry);
+
+        Future<Void> future = executorService.submit(() -> {
+            // This should fail because we're trying to modify a geometry that's connected to the scene graph
+            action.accept(geometry);
+            return null;
+        });
+
+        try {
+            future.get();
+            fail("Expected an IllegalThreadSceneGraphMutation exception");
+        } catch (ExecutionException e) {
+            // This is expected - verify it's the right exception type
+            assertTrue("Expected IllegalThreadSceneGraphMutation, got: " + e.getCause().getClass().getName(),
+                    e.getCause() instanceof IllegalThreadSceneGraphMutation);
+        }
+    }
+
+    /**
+     * Creates a single-threaded executor service with daemon threads.
+     */
+    private static ExecutorService newSingleThreadDaemonExecutor() {
+        return Executors.newSingleThreadExecutor(daemonThreadFactory());
+    }
+
+    /**
+     * Creates a thread factory that produces daemon threads.
+     */
+    private static ThreadFactory daemonThreadFactory() {
+        return r -> {
+            Thread t = Executors.defaultThreadFactory().newThread(r);
+            t.setDaemon(true);
+            return t;
+        };
+    }
+}

+ 203 - 0
jme3-core/src/test/java/com/jme3/scene/threadwarden/SceneGraphThreadWardenNodeExtendedTest.java

@@ -0,0 +1,203 @@
+package com.jme3.scene.threadwarden;
+
+import com.jme3.material.Material;
+import com.jme3.material.MatParamOverride;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.shader.VarType;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+import java.util.function.Consumer;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Parameterized tests for SceneGraphThreadWarden class.
+ * These tests verify that various scene graph mutations are properly checked for thread safety.
+ */
+@RunWith(Parameterized.class)
+public class SceneGraphThreadWardenNodeExtendedTest {
+
+    private static ExecutorService executorService;
+
+    private final String testName;
+    private final Consumer<Node> action;
+
+    @SuppressWarnings({"ReassignedVariable", "AssertWithSideEffects"})
+    @BeforeClass
+    public static void setupClass() {
+        // Make sure assertions are enabled
+        boolean assertsEnabled = false;
+        assert assertsEnabled = true;
+        if (!assertsEnabled) {
+            throw new RuntimeException("WARNING: Assertions are not enabled! Tests may not work correctly.");
+        }
+    }
+
+    @Before
+    public void setup() {
+        executorService = newSingleThreadDaemonExecutor();
+    }
+
+    @After
+    public void tearDown() {
+        executorService.shutdown();
+        SceneGraphThreadWarden.reset();
+    }
+
+    /**
+     * Constructor for the parameterized test.
+     * 
+     * @param testName A descriptive name for the test
+     * @param action The action to perform on the spatial
+     */
+    public SceneGraphThreadWardenNodeExtendedTest(String testName, Consumer<Node> action) {
+        this.testName = testName;
+        this.action = action;
+    }
+
+    /**
+     * Define the parameters for the test.
+     * Each parameter is a pair of (test name, action to perform on spatial).
+     */
+    @Parameterized.Parameters(name = "{0}")
+    public static Collection<Object[]> data() {
+        Material mockMaterial = Mockito.mock(Material.class);
+        MatParamOverride override = new MatParamOverride(VarType.Float, "TestParam", 1.0f);
+
+        return Arrays.asList(new Object[][] {
+            { 
+                "setMaterial", 
+                (Consumer<Node>) spatial -> spatial.setMaterial(mockMaterial)
+            },
+            { 
+                "setLodLevel", 
+                (Consumer<Node>) spatial -> spatial.setLodLevel(1)
+            },
+            { 
+                "addMatParamOverride", 
+                (Consumer<Node>) spatial -> spatial.addMatParamOverride(override)
+            },
+            { 
+                "removeMatParamOverride", 
+                (Consumer<Node>) spatial -> spatial.removeMatParamOverride(override)
+            },
+            { 
+                "clearMatParamOverrides", 
+                (Consumer<Node>) Spatial::clearMatParamOverrides
+            }
+        });
+    }
+
+    /**
+     * Test that scene graph mutation is fine on the main thread when the object is attached to the root.
+     */
+    @Test
+    public void testMutationOnMainThreadOnAttachedObject() {
+        Node rootNode = new Node("root");
+        SceneGraphThreadWarden.setup(rootNode);
+
+        // Create a child node and attach it to the root node
+        Node child = new Node("child");
+        rootNode.attachChild(child);
+
+        // This should work fine since we're on the main thread
+        action.accept(child);
+    }
+
+    /**
+     * Test that scene graph mutation is fine on the main thread when the object is not attached to the root.
+     */
+    @Test
+    public void testMutationOnMainThreadOnDetachedObject() {
+        Node rootNode = new Node("root");
+        SceneGraphThreadWarden.setup(rootNode);
+
+        // Create a child node but don't attach it to the root node
+        Node child = new Node("child");
+
+        // This should work fine since we're on the main thread
+        action.accept(child);
+    }
+
+    /**
+     * Test that scene graph mutation is fine on a non-main thread when the object is not attached to the root.
+     */
+    @Test
+    public void testMutationOnNonMainThreadOnDetachedObject() throws ExecutionException, InterruptedException {
+        Node rootNode = new Node("root");
+        SceneGraphThreadWarden.setup(rootNode);
+
+        // Create a child node but don't attach it to the root node
+        Node child = new Node("child");
+
+        Future<Void> future = executorService.submit(() -> {
+            // This should work fine since the node is not connected to the root node
+            action.accept(child);
+            return null;
+        });
+
+        // This should complete without exceptions
+        future.get();
+    }
+
+    /**
+     * Test that scene graph mutation is not allowed on a non-main thread when the object is attached to the root.
+     */
+    @Test
+    public void testMutationOnNonMainThreadOnAttachedObject() throws InterruptedException {
+        Node rootNode = new Node("root");
+        SceneGraphThreadWarden.setup(rootNode);
+
+        // Create a child node and attach it to the root node
+        Node child = new Node("child");
+        rootNode.attachChild(child);
+
+        Future<Void> future = executorService.submit(() -> {
+            // This should fail because we're trying to modify a node that's connected to the scene graph
+            action.accept(child);
+            return null;
+        });
+
+        try {
+            future.get();
+            fail("Expected an IllegalThreadSceneGraphMutation exception");
+        } catch (ExecutionException e) {
+            // This is expected - verify it's the right exception type
+            assertTrue("Expected IllegalThreadSceneGraphMutation, got: " + e.getCause().getClass().getName(),
+                    e.getCause() instanceof IllegalThreadSceneGraphMutation);
+        }
+    }
+
+    /**
+     * Creates a single-threaded executor service with daemon threads.
+     */
+    private static ExecutorService newSingleThreadDaemonExecutor() {
+        return Executors.newSingleThreadExecutor(daemonThreadFactory());
+    }
+
+    /**
+     * Creates a thread factory that produces daemon threads.
+     */
+    private static ThreadFactory daemonThreadFactory() {
+        return r -> {
+            Thread t = Executors.defaultThreadFactory().newThread(r);
+            t.setDaemon(true);
+            return t;
+        };
+    }
+}

+ 316 - 0
jme3-core/src/test/java/com/jme3/scene/threadwarden/SceneGraphThreadWardenTest.java

@@ -0,0 +1,316 @@
+package com.jme3.scene.threadwarden;
+
+import com.jme3.scene.Node;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests for SceneGraphThreadWarden class.
+ * These tests verify that:
+ * - Normal node mutation is fine on the main thread
+ * - Node mutation on nodes not connected to the root node is fine even on a non main thread
+ * - Adding a node to the scene graph (indirectly) connected to the root node isn't fine on a non main thread
+ * - Adding a node currently attached to a root node to a different node isn't fine on a non main thread
+ */
+public class SceneGraphThreadWardenTest {
+
+    private static ExecutorService executorService;
+
+    @SuppressWarnings({"ReassignedVariable", "AssertWithSideEffects"})
+    @BeforeClass
+    public static void setupClass() {
+        // Make sure assertions are enabled
+        boolean assertsEnabled = false;
+        assert assertsEnabled = true;
+        //noinspection ConstantValue
+        if (!assertsEnabled) {
+            throw new RuntimeException("WARNING: Assertions are not enabled! Tests may not work correctly.");
+        }
+    }
+
+    @Before
+    public void setup() {
+        executorService = newSingleThreadDaemonExecutor();
+    }
+
+    @After
+    public void tearDown() {
+        executorService.shutdown();
+        SceneGraphThreadWarden.reset();
+    }
+
+    /**
+     * Test that normal node mutation is fine on the main thread.
+     */
+    @Test
+    public void testNormalNodeMutationOnMainThread() {
+        Node rootNode = new Node("root");
+        SceneGraphThreadWarden.setup(rootNode);
+
+        // This should work fine since we're on the main thread
+        Node child = new Node("child");
+        rootNode.attachChild(child);
+
+        // Add another level of children
+        Node grandchild = new Node("grandchild");
+        child.attachChild(grandchild);
+
+        // Detach should also work fine
+        child.detachChild(grandchild);
+        rootNode.detachChild(child);
+    }
+
+    /**
+     * Test that node mutation on nodes not connected to the root node is fine even on a non main thread.
+     * <p>
+     *     This is a use case where a thread is preparing things for later attachment to the scene graph.
+     * </p>
+     */
+    @Test
+    public void testNodeMutationOnNonConnectedNodesOnNonMainThread() throws ExecutionException, InterruptedException {
+        Node rootNode = new Node("root");
+        SceneGraphThreadWarden.setup(rootNode);
+
+        Future<Node> nonConnectedNodeFuture = executorService.submit(() -> {
+            // This should work fine since these nodes are not connected to the root node
+            Node parent = new Node("parent");
+            Node child = new Node("child");
+            parent.attachChild(child);
+
+            // Add another level of children
+            Node grandchild = new Node("grandchild");
+            child.attachChild(grandchild);
+
+            return parent;
+        });
+
+        // Get the result to ensure the task completed without exceptions
+        Node nonConnectedNode = nonConnectedNodeFuture.get();
+
+        // Now we can attach it to the root node on the main thread
+        rootNode.attachChild(nonConnectedNode);
+    }
+
+    /**
+     * Test that adding a node to the scene graph connected to the root node in a non main thread leads to an
+     * exception.
+     */
+    @Test
+    public void testAddingNodeToSceneGraphOnNonMainThread() throws InterruptedException {
+        Node rootNode = new Node("root");
+        SceneGraphThreadWarden.setup(rootNode);
+
+        // Create a child node and attach it to the root node
+        Node child = new Node("child");
+        rootNode.attachChild(child);
+
+        Future<Void> illegalMutationFuture = executorService.submit(() -> {
+            // This should fail because we're trying to add a node to a node that's connected to the scene graph
+            Node grandchild = new Node("grandchild");
+            child.attachChild(grandchild);
+            return null;
+        });
+
+        try {
+            illegalMutationFuture.get();
+            fail("Expected an IllegalThreadSceneGraphMutation exception");
+        } catch (ExecutionException e) {
+            // This is expected - verify it's the right exception type
+            assertTrue("Expected IllegalThreadSceneGraphMutation, got: " + e.getCause().getClass().getName(),
+                    e.getCause() instanceof IllegalThreadSceneGraphMutation);
+        }
+    }
+
+    /**
+     * Test that adding a node currently attached to a root node to a different node leads to an exception.
+     * <p>
+     *     This is testing an edge case where you think you'd working with non-connected nodes, but in reality
+     *     one of your nodes is already attached to the scene graph (and you're attaching it to a different node which will
+     *     detach it from the scene graph).
+     * </p>
+     */
+    @Test
+    public void testMovingNodeAttachedToRootOnNonMainThread() throws InterruptedException {
+        Node rootNode = new Node("root");
+        SceneGraphThreadWarden.setup(rootNode);
+
+        // Create two child nodes and attach them to the root node
+        Node child1 = new Node("child1");
+        Node child2 = new Node("child2");
+
+        rootNode.attachChild(child2);
+
+        Future<Void> illegalMutationFuture = executorService.submit(() -> {
+            // This should fail because we're trying to move a node that's connected to the root node
+            child1.attachChild(child2); // This implicitly detaches child2 from rootNode
+            return null;
+        });
+
+        try {
+            illegalMutationFuture.get();
+            fail("Expected an IllegalThreadSceneGraphMutation exception");
+        } catch (ExecutionException e) {
+            // This is expected - verify it's the right exception type
+            assertTrue("Expected IllegalThreadSceneGraphMutation, got: " + e.getCause().getClass().getName(),
+                    e.getCause() instanceof IllegalThreadSceneGraphMutation);
+        }
+    }
+
+    /**
+     * Test that detaching a node releases it from thread protection.
+     */
+    @Test
+    public void testDetachmentReleasesProtection() throws ExecutionException, InterruptedException {
+        Node rootNode = new Node("root");
+        SceneGraphThreadWarden.setup(rootNode);
+
+        // Create a child node and attach it to the root node
+        Node child = new Node("child");
+        rootNode.attachChild(child);
+
+        // Now detach it from the root node
+        child.removeFromParent();
+
+        // Now we should be able to modify it on another thread
+        Future<Void> legalMutationFuture = executorService.submit(() -> {
+            Node grandchild = new Node("grandchild");
+            child.attachChild(grandchild);
+            return null;
+        });
+
+        // This should complete without exceptions
+        legalMutationFuture.get();
+    }
+
+    /**
+     * Test that adding a child to the root node also restricts the grandchild.
+     * This test will add a grandchild to a child BEFORE adding the child to the root,
+     * then try (and fail) to make an illegal on-thread change to the grandchild.
+     */
+    @Test
+    public void testAddingAChildToTheRootNodeAlsoRestrictsTheGrandChild() throws InterruptedException {
+        Node rootNode = new Node("root");
+        SceneGraphThreadWarden.setup(rootNode);
+
+        // Create a child node and a grandchild node
+        Node child = new Node("child");
+        Node grandchild = new Node("grandchild");
+
+        // Attach the grandchild to the child BEFORE adding the child to the root
+        child.attachChild(grandchild);
+
+        // Now attach the child to the root node
+        rootNode.attachChild(child);
+
+        // Try to make an illegal on-thread change to the grandchild
+        Future<Void> illegalMutationFuture = executorService.submit(() -> {
+            // This should fail because the grandchild is now restricted
+            Node greatGrandchild = new Node("greatGrandchild");
+            grandchild.attachChild(greatGrandchild);
+            return null;
+        });
+
+        try {
+            illegalMutationFuture.get();
+            fail("Expected an IllegalThreadSceneGraphMutation exception");
+        } catch (ExecutionException e) {
+            // This is expected - verify it's the right exception type
+            assertTrue("Expected IllegalThreadSceneGraphMutation, got: " + e.getCause().getClass().getName(),
+                    e.getCause() instanceof IllegalThreadSceneGraphMutation);
+        }
+    }
+
+    /**
+     * Test that removing a child from the root node also unrestricts the grandchild.
+     * This test will add a child with a grandchild to the root node, then remove the child
+     * and verify that the grandchild can be modified on a non-main thread.
+     */
+    @Test
+    public void testRemovingAChildFromTheRootNodeAlsoUnrestrictsTheGrandChild() throws ExecutionException, InterruptedException {
+        Node rootNode = new Node("root");
+        SceneGraphThreadWarden.setup(rootNode);
+
+        // Create a child node and a grandchild node
+        Node child = new Node("child");
+        Node grandchild = new Node("grandchild");
+
+        // Attach the grandchild to the child
+        child.attachChild(grandchild);
+
+        // Attach the child to the root node
+        rootNode.attachChild(child);
+
+        // Now remove the child from the root node
+        child.removeFromParent();
+
+        // Try to make a change to the grandchild on a non-main thread
+        Future<Void> legalMutationFuture = executorService.submit(() -> {
+            // This should succeed because the grandchild is no longer restricted
+            Node greatGrandchild = new Node("greatGrandchild");
+            grandchild.attachChild(greatGrandchild);
+            return null;
+        });
+
+        // This should complete without exceptions
+        legalMutationFuture.get();
+    }
+
+    /**
+     * Test that an otherwise illegal scene graph mutation won't throw an exception
+     * if the checks have been disabled by calling disableChecks().
+     */
+    @Test
+    public void testDisableChecksAllowsIllegalMutation() throws ExecutionException, InterruptedException {
+        Node rootNode = new Node("root");
+        SceneGraphThreadWarden.setup(rootNode);
+
+        // Create a child node and attach it to the root node
+        Node child = new Node("child");
+        rootNode.attachChild(child);
+
+        // Disable the thread warden checks
+        SceneGraphThreadWarden.disableChecks();
+
+        // Try to make a change to the child on a non-main thread
+        // This would normally be illegal, but should succeed because checks are disabled
+        Future<Void> mutationFuture = executorService.submit(() -> {
+            Node grandchild = new Node("grandchild");
+            child.attachChild(grandchild);
+            return null;
+        });
+
+        // This should complete without exceptions
+        mutationFuture.get();
+    }
+
+
+
+    /**
+     * Creates a single-threaded executor service with daemon threads.
+     */
+    private static ExecutorService newSingleThreadDaemonExecutor() {
+        return Executors.newSingleThreadExecutor(daemonThreadFactory());
+    }
+
+    /**
+     * Creates a thread factory that produces daemon threads.
+     */
+    private static ThreadFactory daemonThreadFactory() {
+        return r -> {
+            Thread t = Executors.defaultThreadFactory().newThread(r);
+            t.setDaemon(true);
+            return t;
+        };
+    }
+}

+ 79 - 69
jme3-effects/src/main/java/com/jme3/post/ssao/SSAOFilter.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -40,7 +40,6 @@ import com.jme3.material.Material;
 import com.jme3.math.Vector2f;
 import com.jme3.math.Vector3f;
 import com.jme3.post.Filter;
-import com.jme3.post.Filter.Pass;
 import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.Renderer;
 import com.jme3.renderer.ViewPort;
@@ -62,23 +61,22 @@ import java.util.ArrayList;
 public class SSAOFilter extends Filter {
 
     private Pass normalPass;
-    private Vector3f frustumCorner;
-    private Vector2f frustumNearFar;
-    private Vector2f[] samples = {new Vector2f(1.0f, 0.0f), new Vector2f(-1.0f, 0.0f), new Vector2f(0.0f, 1.0f), new Vector2f(0.0f, -1.0f)};
+    private final Vector2f[] samples = {
+            new Vector2f(1.0f, 0.0f),
+            new Vector2f(-1.0f, 0.0f),
+            new Vector2f(0.0f, 1.0f),
+            new Vector2f(0.0f, -1.0f)
+    };
     private float sampleRadius = 5.1f;
     private float intensity = 1.5f;
     private float scale = 0.2f;
     private float bias = 0.1f;
+    private boolean approximateNormals = false;
     private boolean useOnlyAo = false;
     private boolean useAo = true;
     private Material ssaoMat;
-    private Pass ssaoPass;
-//    private Material downSampleMat;
-//    private Pass downSamplePass;
-    private float downSampleFactor = 1f;
     private RenderManager renderManager;
     private ViewPort viewPort;
-    private boolean approximateNormals = false;
 
     /**
      * Create a Screen Space Ambient Occlusion Filter
@@ -89,10 +87,11 @@ public class SSAOFilter extends Filter {
 
     /**
      * Create a Screen Space Ambient Occlusion Filter
+     *
      * @param sampleRadius The radius of the area where random samples will be picked. default 5.1f
-     * @param intensity intensity of the resulting AO. default 1.2f
-     * @param scale distance between occluders and occludee. default 0.2f
-     * @param bias the width of the occlusion cone considered by the occludee. default 0.1f
+     * @param intensity    intensity of the resulting AO. default 1.5f
+     * @param scale        distance between occluders and occludee. default 0.2f
+     * @param bias         the width of the occlusion cone considered by the occludee. default 0.1f
      */
     public SSAOFilter(float sampleRadius, float intensity, float scale, float bias) {
         this();
@@ -126,38 +125,32 @@ public class SSAOFilter extends Filter {
     }
 
     @Override
-    protected void initFilter(AssetManager manager, RenderManager renderManager, ViewPort vp, int w, int h) {
+    protected void initFilter(AssetManager assetManager, RenderManager renderManager, ViewPort vp, int w, int h) {
         this.renderManager = renderManager;
         this.viewPort = vp;
         int screenWidth = w;
         int screenHeight = h;
+        float downSampleFactor = 1f;
         postRenderPasses = new ArrayList<Pass>();
 
         normalPass = new Pass();
         normalPass.init(renderManager.getRenderer(), (int) (screenWidth / downSampleFactor), (int) (screenHeight / downSampleFactor), Format.RGBA8, Format.Depth);
 
-
-        frustumNearFar = new Vector2f();
-
+        Vector2f frustumNearFar = new Vector2f();
         float farY = (vp.getCamera().getFrustumTop() / vp.getCamera().getFrustumNear()) * vp.getCamera().getFrustumFar();
         float farX = farY * (screenWidth / (float) screenHeight);
-        frustumCorner = new Vector3f(farX, farY, vp.getCamera().getFrustumFar());
+        Vector3f frustumCorner = new Vector3f(farX, farY, vp.getCamera().getFrustumFar());
         frustumNearFar.x = vp.getCamera().getFrustumNear();
         frustumNearFar.y = vp.getCamera().getFrustumFar();
 
-
-
-
-
         //ssao Pass
-        ssaoMat = new Material(manager, "Common/MatDefs/SSAO/ssao.j3md");
+        ssaoMat = new Material(assetManager, "Common/MatDefs/SSAO/ssao.j3md");
         ssaoMat.setTexture("Normals", normalPass.getRenderedTexture());
-        Texture random = manager.loadTexture("Common/MatDefs/SSAO/Textures/random.png");
+        Texture random = assetManager.loadTexture("Common/MatDefs/SSAO/Textures/random.png");
         random.setWrap(Texture.WrapMode.Repeat);
         ssaoMat.setTexture("RandomMap", random);
 
-        ssaoPass = new Pass("SSAO pass") {
-
+        Pass ssaoPass = new Pass("SSAO pass") {
             @Override
             public boolean requiresDepthAsTexture() {
                 return true;
@@ -168,18 +161,18 @@ public class SSAOFilter extends Filter {
 //        ssaoPass.getRenderedTexture().setMinFilter(Texture.MinFilter.Trilinear);
 //        ssaoPass.getRenderedTexture().setMagFilter(Texture.MagFilter.Bilinear);
         postRenderPasses.add(ssaoPass);
-        material = new Material(manager, "Common/MatDefs/SSAO/ssaoBlur.j3md");
+        material = new Material(assetManager, "Common/MatDefs/SSAO/ssaoBlur.j3md");
         material.setTexture("SSAOMap", ssaoPass.getRenderedTexture());
+        material.setVector2("FrustumNearFar", frustumNearFar);
+        material.setBoolean("UseAo", useAo);
+        material.setBoolean("UseOnlyAo", useOnlyAo);
 
         ssaoMat.setVector3("FrustumCorner", frustumCorner);
         ssaoMat.setFloat("SampleRadius", sampleRadius);
         ssaoMat.setFloat("Intensity", intensity);
         ssaoMat.setFloat("Scale", scale);
         ssaoMat.setFloat("Bias", bias);
-        material.setBoolean("UseAo", useAo);
-        material.setBoolean("UseOnlyAo", useOnlyAo);
         ssaoMat.setVector2("FrustumNearFar", frustumNearFar);
-        material.setVector2("FrustumNearFar", frustumNearFar);
         ssaoMat.setParam("Samples", VarType.Vector2Array, samples);
         ssaoMat.setBoolean("ApproximateNormals", approximateNormals);
 
@@ -189,7 +182,6 @@ public class SSAOFilter extends Filter {
         float blurScale = 2f;
         material.setFloat("XScale", blurScale * xScale);
         material.setFloat("YScale", blurScale * yScale);
-
     }
 
     @Override
@@ -198,18 +190,20 @@ public class SSAOFilter extends Filter {
     }    
     
     /**
-     * Return the bias<br>
-     * see {@link  #setBias(float bias)}
-     * @return  bias
+     * Returns the bias value used in the SSAO calculation.
+     *
+     * @return The bias value.
+     * @see #setBias(float)
      */
     public float getBias() {
         return bias;
     }
 
     /**
-     * Sets the width of the occlusion cone considered by the occludee default is 0.1f
+     * Sets the width of the occlusion cone considered by the occludee.
+     * A higher bias means a wider cone, resulting in less self-occlusion.
      *
-     * @param bias the desired width (default=0.1)
+     * @param bias The desired bias value (default: 0.1f).
      */
     public void setBias(float bias) {
         this.bias = bias;
@@ -219,62 +213,65 @@ public class SSAOFilter extends Filter {
     }
 
     /**
-     * returns the ambient occlusion intensity
-     * @return intensity
+     * Returns the ambient occlusion intensity.
+     *
+     * @return The intensity value.
      */
     public float getIntensity() {
         return intensity;
     }
 
     /**
-     * Sets the Ambient occlusion intensity default is 1.5
+     * Sets the ambient occlusion intensity. A higher intensity makes the ambient
+     * occlusion effect more pronounced.
      *
-     * @param intensity the desired intensity (default=1.5)
+     * @param intensity The desired intensity (default: 1.5f).
      */
     public void setIntensity(float intensity) {
         this.intensity = intensity;
         if (ssaoMat != null) {
             ssaoMat.setFloat("Intensity", intensity);
         }
-
     }
 
     /**
-     * returns the sample radius<br>
-     * see {link setSampleRadius(float sampleRadius)}
-     * @return the sample radius
+     * Returns the sample radius used in the SSAO calculation.
+     *
+     * @return The sample radius.
+     * @see #setSampleRadius(float)
      */
     public float getSampleRadius() {
         return sampleRadius;
     }
 
     /**
-     * Sets the radius of the area where random samples will be picked default 5.1f 
+     * Sets the radius of the area where random samples will be picked for SSAO.
+     * A larger radius considers more distant occluders.
      *
-     * @param sampleRadius the desired radius (default=5.1)
+     * @param sampleRadius The desired radius (default: 5.1f).
      */
     public void setSampleRadius(float sampleRadius) {
         this.sampleRadius = sampleRadius;
         if (ssaoMat != null) {
             ssaoMat.setFloat("SampleRadius", sampleRadius);
         }
-
     }
 
     /**
-     * returns the scale<br>
-     * see {@link #setScale(float scale)}
-     * @return scale
+     * Returns the scale value used in the SSAO calculation.
+     *
+     * @return The scale value.
+     * @see #setScale(float)
      */
     public float getScale() {
         return scale;
     }
 
     /**
-     * 
-     * Returns the distance between occluders and occludee. default 0.2f
+     * Sets the distance between occluders and occludee for SSAO.
+     * This essentially controls the "thickness" of the ambient occlusion.
      *
-     * @param scale the desired distance (default=0.2)
+     * @param scale The desired distance (default: 0.2f).
      */
     public void setScale(float scale) {
         this.scale = scale;
@@ -284,7 +281,30 @@ public class SSAOFilter extends Filter {
     }
 
     /**
-     * debugging only , will be removed
+     * Sets whether to use approximate normals for the SSAO calculation.
+     * If `true`, normals are derived from the depth buffer. If `false`, a separate
+     * normal pass is rendered.
+     *
+     * @param approximateNormals `true` to use approximate normals, `false` to use a normal pass.
+     */
+    public void setApproximateNormals(boolean approximateNormals) {
+        this.approximateNormals = approximateNormals;
+        if (ssaoMat != null) {
+            ssaoMat.setBoolean("ApproximateNormals", approximateNormals);
+        }
+    }
+
+    /**
+     * Checks if approximate normals are being used for SSAO calculation.
+     *
+     * @return `true` if approximate normals are used, `false` otherwise.
+     */
+    public boolean isApproximateNormals() {
+        return approximateNormals;
+    }
+
+    /**
+     * debugging only, will be removed
      * @return true if using ambient occlusion
      */
     public boolean isUseAo() {
@@ -292,7 +312,7 @@ public class SSAOFilter extends Filter {
     }
 
     /**
-     * debugging only , will be removed
+     * debugging only, will be removed
      *
      * @param useAo true to enable, false to disable (default=true)
      */
@@ -301,22 +321,10 @@ public class SSAOFilter extends Filter {
         if (material != null) {
             material.setBoolean("UseAo", useAo);
         }
-
-    }
-
-    public void setApproximateNormals(boolean approximateNormals) {
-        this.approximateNormals = approximateNormals;
-        if (ssaoMat != null) {
-            ssaoMat.setBoolean("ApproximateNormals", approximateNormals);
-        }
-    }
-
-    public boolean isApproximateNormals() {
-        return approximateNormals;
     }
 
     /**
-     * debugging only , will be removed
+     * debugging only, will be removed
      * @return useOnlyAo
      */
     public boolean isUseOnlyAo() {
@@ -324,7 +332,7 @@ public class SSAOFilter extends Filter {
     }
 
     /**
-     * debugging only , will be removed
+     * debugging only, will be removed
      *
      * @param useOnlyAo true to enable, false to disable (default=false)
      */
@@ -343,6 +351,7 @@ public class SSAOFilter extends Filter {
         oc.write(intensity, "intensity", 1.5f);
         oc.write(scale, "scale", 0.2f);
         oc.write(bias, "bias", 0.1f);
+        oc.write(approximateNormals, "approximateNormals", false);
     }
 
     @Override
@@ -353,5 +362,6 @@ public class SSAOFilter extends Filter {
         intensity = ic.readFloat("intensity", 1.5f);
         scale = ic.readFloat("scale", 0.2f);
         bias = ic.readFloat("bias", 0.1f);
+        approximateNormals = ic.readBoolean("approximateNormals", false);
     }
 }

+ 63 - 0
jme3-effects/src/test/java/com/jme3/post/filters/SSAOFilterTest.java

@@ -0,0 +1,63 @@
+package com.jme3.post.filters;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.asset.DesktopAssetManager;
+import com.jme3.export.binary.BinaryExporter;
+import com.jme3.post.ssao.SSAOFilter;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Automated tests for the {@code SSAOFilter} class.
+ *
+ * @author capdevon
+ */
+public class SSAOFilterTest {
+
+    /**
+     * Tests serialization and de-serialization of an {@code SSAOFilter}.
+     */
+    @Test
+    public void testSaveAndLoad() {
+        SSAOFilter filter = new SSAOFilter();
+
+        // Verify the default parameter values:
+        verifyDefaults(filter);
+
+        // Set parameters to new values:
+        filter.setEnabled(false);
+        filter.setSampleRadius(4.5f);
+        filter.setIntensity(1.8f);
+        filter.setScale(0.4f);
+        filter.setBias(0.5f);
+        filter.setApproximateNormals(true);
+
+        // Create a duplicate filter using serialization:
+        AssetManager assetManager = new DesktopAssetManager();
+        SSAOFilter copy = BinaryExporter.saveAndLoad(assetManager, filter);
+
+        // Verify the parameter values of the copy:
+        Assert.assertEquals("SSAOFilter", copy.getName());
+        Assert.assertEquals(4.5f, copy.getSampleRadius(), 0f);
+        Assert.assertEquals(1.8f, copy.getIntensity(), 0f);
+        Assert.assertEquals(0.4f, copy.getScale(), 0f);
+        Assert.assertEquals(0.5f, copy.getBias(), 0f);
+        Assert.assertTrue(copy.isApproximateNormals());
+        Assert.assertFalse(copy.isEnabled());
+    }
+
+    /**
+     * Verify some default values of a newly instantiated {@code SSAOFilter}.
+     *
+     * @param filter (not null, unaffected)
+     */
+    private void verifyDefaults(SSAOFilter filter) {
+        Assert.assertEquals("SSAOFilter", filter.getName());
+        Assert.assertEquals(5.1f, filter.getSampleRadius(), 0f);
+        Assert.assertEquals(1.5f, filter.getIntensity(), 0f);
+        Assert.assertEquals(0.2f, filter.getScale(), 0f);
+        Assert.assertEquals(0.1f, filter.getBias(), 0f);
+        Assert.assertFalse(filter.isApproximateNormals());
+        Assert.assertTrue(filter.isEnabled());
+    }
+}

+ 79 - 23
jme3-examples/src/main/java/jme3test/export/TestOgreConvert.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -29,53 +29,109 @@
  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
-
 package jme3test.export;
 
 import com.jme3.anim.AnimComposer;
+import com.jme3.anim.SkinningControl;
 import com.jme3.app.SimpleApplication;
 import com.jme3.export.binary.BinaryExporter;
-import com.jme3.export.binary.BinaryImporter;
+import com.jme3.font.BitmapText;
+import com.jme3.light.AmbientLight;
 import com.jme3.light.DirectionalLight;
+import com.jme3.material.MatParamOverride;
 import com.jme3.math.ColorRGBA;
 import com.jme3.math.Vector3f;
-import com.jme3.scene.Node;
 import com.jme3.scene.Spatial;
 
-import java.io.*;
-
+/**
+ * This class is a jMonkeyEngine 3 (jME3) test application designed to verify
+ * the import, export, and runtime behavior of 3D models, particularly those
+ * in or compatible with the Ogre3D format (.mesh.xml).
+ * It loads an Ogre model, saves and reloads it using jME3's binary exporter,
+ * plays an animation, and displays debugging information about its skinning
+ * and material parameters.
+ *
+ * @author capdevon
+ */
 public class TestOgreConvert extends SimpleApplication {
 
-    public static void main(String[] args){
+    public static void main(String[] args) {
         TestOgreConvert app = new TestOgreConvert();
+        app.setPauseOnLostFocus(false);
         app.start();
     }
 
+    private final StringBuilder sb = new StringBuilder();
+    private int frameCount = 0;
+    private BitmapText bmp;
+    private Spatial spCopy;
+    private SkinningControl skinningControl;
+
     @Override
     public void simpleInitApp() {
-        Spatial ogreModel = assetManager.loadModel("Models/Oto/Oto.mesh.xml");
+        configureCamera();
+        setupLights();
+
+        bmp = createLabelText(10, 20, "<placeholder>");
+
+        // Load the Ogre model (Oto.mesh.xml) from the assets
+        Spatial model = assetManager.loadModel("Models/Oto/Oto.mesh.xml");
+        // Save the loaded model to jME3's binary format and then reload it.
+        // This tests the binary serialization/deserialization process.
+        spCopy = BinaryExporter.saveAndLoad(assetManager, model);
+        spCopy.setName("Oto-Copy");
+        rootNode.attachChild(spCopy);
+
+        AnimComposer animComposer = spCopy.getControl(AnimComposer.class);
+        animComposer.setCurrentAction("Walk");
+
+        // Get the SkinningControl from the model to inspect skinning properties
+        skinningControl = spCopy.getControl(SkinningControl.class);
+    }
+
+    private void setupLights() {
+        AmbientLight al = new AmbientLight();
+        rootNode.addLight(al);
 
         DirectionalLight dl = new DirectionalLight();
-        dl.setColor(ColorRGBA.White);
-        dl.setDirection(new Vector3f(0,-1,-1).normalizeLocal());
+        dl.setDirection(new Vector3f(0, -1, -1).normalizeLocal());
         rootNode.addLight(dl);
+    }
 
-        try {
-            ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            BinaryExporter exp = new BinaryExporter();
-            exp.save(ogreModel, baos);
+    private void configureCamera() {
+        flyCam.setDragToRotate(true);
+        flyCam.setMoveSpeed(15f);
 
-            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
-            BinaryImporter imp = new BinaryImporter();
-            imp.setAssetManager(assetManager);
-            Node ogreModelReloaded = (Node) imp.load(bais, null, null);
+        cam.setLocation(new Vector3f(0, 0, 20));
+    }
 
-            AnimComposer composer = ogreModelReloaded.getControl(AnimComposer.class);
-            composer.setCurrentAction("Walk");
+    @Override
+    public void simpleUpdate(float tpf) {
+        frameCount++;
+        if (frameCount == 10) {
+            frameCount = 0;
 
-            rootNode.attachChild(ogreModelReloaded);
-        } catch (IOException ex){
-            ex.printStackTrace();
+            sb.append("HW Skinning Preferred: ").append(skinningControl.isHardwareSkinningPreferred()).append("\n");
+            sb.append("HW Skinning Enabled: ").append(skinningControl.isHardwareSkinningUsed()).append("\n");
+            sb.append("Mesh Targets: ").append(skinningControl.getTargets().length).append("\n");
+
+            for (MatParamOverride mpo : spCopy.getLocalMatParamOverrides()) {
+                sb.append(mpo.getVarType()).append(" ");
+                sb.append(mpo.getName()).append(": ");
+                sb.append(mpo.getValue()).append("\n");
+            }
+
+            bmp.setText(sb.toString());
+            sb.setLength(0);
         }
     }
+
+    private BitmapText createLabelText(int x, int y, String text) {
+        BitmapText bmp = new BitmapText(guiFont);
+        bmp.setText(text);
+        bmp.setLocalTranslation(x, settings.getHeight() - y, 0);
+        bmp.setColor(ColorRGBA.Red);
+        guiNode.attachChild(bmp);
+        return bmp;
+    }
 }

+ 13 - 2
jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java

@@ -31,11 +31,11 @@
  */
 package jme3test.model;
 
-import com.jme3.anim.AnimClip;
 import com.jme3.anim.AnimComposer;
 import com.jme3.anim.SkinningControl;
 import com.jme3.app.*;
 import com.jme3.asset.plugins.FileLocator;
+import com.jme3.asset.plugins.UrlLocator;
 import com.jme3.input.KeyInput;
 import com.jme3.input.controls.ActionListener;
 import com.jme3.input.controls.KeyTrigger;
@@ -82,6 +82,7 @@ public class TestGltfLoading extends SimpleApplication {
 
         String folder = System.getProperty("user.home");
         assetManager.registerLocator(folder, FileLocator.class);
+        assetManager.registerLocator("https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/refs/heads/main/", UrlLocator.class);
 
         // cam.setLocation(new Vector3f(4.0339394f, 2.645184f, 6.4627485f));
         // cam.setRotation(new Quaternion(-0.013950467f, 0.98604023f, -0.119502485f, -0.11510504f));
@@ -152,7 +153,14 @@ public class TestGltfLoading extends SimpleApplication {
 
 //        loadModel("Models/gltf/Corset/glTF/Corset.gltf", new Vector3f(0, -1, 0), 20f);
 //        loadModel("Models/gltf/BoxInterleaved/glTF/BoxInterleaved.gltf", new Vector3f(0, 0, 0), 1f);
-        
+
+        // From url locator
+
+        // loadModel("Models/AnimatedColorsCube/glTF/AnimatedColorsCube.gltf", new Vector3f(0, 0f, 0), 0.1f);
+        // loadModel("Models/AntiqueCamera/glTF/AntiqueCamera.gltf", new Vector3f(0, 0, 0), 0.1f);
+        // loadModel("Models/AnimatedMorphCube/glTF/AnimatedMorphCube.gltf", new Vector3f(0, 0, 0), 0.1f);
+        // loadModel("Models/AnimatedMorphCube/glTF-Binary/AnimatedMorphCube.glb", new Vector3f(0, 0, 0), 0.1f);
+
         probeNode.attachChild(assets.get(0));
 
         ChaseCameraAppState chaseCam = new ChaseCameraAppState();
@@ -231,7 +239,10 @@ public class TestGltfLoading extends SimpleApplication {
     private void loadModel(String path, Vector3f offset, Vector3f scale) {
         GltfModelKey k = new GltfModelKey(path);
         //k.setKeepSkeletonPose(true);
+        long t  = System.currentTimeMillis();        
         Spatial s = assetManager.loadModel(k);
+        System.out.println("Load time : " + (System.currentTimeMillis() - t) + " ms");
+        
         s.scale(scale.x, scale.y, scale.z);
         s.move(offset);
         assets.add(s);

+ 85 - 43
jme3-examples/src/main/java/jme3test/stress/TestLodGeneration.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -44,10 +44,10 @@ import com.jme3.input.ChaseCamera;
 import com.jme3.input.KeyInput;
 import com.jme3.input.controls.ActionListener;
 import com.jme3.input.controls.KeyTrigger;
+import com.jme3.input.controls.Trigger;
 import com.jme3.light.AmbientLight;
 import com.jme3.light.DirectionalLight;
 import com.jme3.material.Material;
-import com.jme3.math.ColorRGBA;
 import com.jme3.math.FastMath;
 import com.jme3.math.Vector3f;
 import com.jme3.scene.Geometry;
@@ -58,44 +58,48 @@ import com.jme3.scene.VertexBuffer;
 
 import jme3tools.optimize.LodGenerator;
 
-public class TestLodGeneration extends SimpleApplication {
+public class TestLodGeneration extends SimpleApplication implements ActionListener {
 
     public static void main(String[] args) {
         TestLodGeneration app = new TestLodGeneration();
         app.start();
     }
 
-    private boolean wireFrame = false;
+    private boolean wireframe = false;
+    // Current reduction value for LOD generation (0.0 to 1.0)
     private float reductionValue = 0.0f;
     private int lodLevel = 0;
     private BitmapText hudText;
-    final private List<Geometry> listGeoms = new ArrayList<>();
-    final private ScheduledThreadPoolExecutor exec = new ScheduledThreadPoolExecutor(5);
+    private final List<Geometry> listGeoms = new ArrayList<>();
+    private final ScheduledThreadPoolExecutor exec = new ScheduledThreadPoolExecutor(5);
 
     @Override
     public void simpleInitApp() {
 
+        // --- Lighting Setup ---
         DirectionalLight dl = new DirectionalLight();
         dl.setDirection(new Vector3f(-1, -1, -1).normalizeLocal());
         rootNode.addLight(dl);
 
         AmbientLight al = new AmbientLight();
-        al.setColor(ColorRGBA.White.mult(0.6f));
         rootNode.addLight(al);
 
+        // --- Model Loading and Setup ---
         // model = (Node) assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml");
         Node model = (Node) assetManager.loadModel("Models/Jaime/Jaime.j3o");
         BoundingBox b = ((BoundingBox) model.getWorldBound());
         model.setLocalScale(1.2f / (b.getYExtent() * 2));
         // model.setLocalTranslation(0,-(b.getCenter().y - b.getYExtent())* model.getLocalScale().y, 0);
+
+        // Iterate through the model's children and collect all Geometry objects
         for (Spatial spatial : model.getChildren()) {
             if (spatial instanceof Geometry) {
                 listGeoms.add((Geometry) spatial);
             }
         }
 
-        ChaseCamera chaseCam = new ChaseCamera(cam, inputManager);
-        model.addControl(chaseCam);
+        // --- Camera Setup ---
+        ChaseCamera chaseCam = new ChaseCamera(cam, model, inputManager);
         chaseCam.setLookAtOffset(b.getCenter());
         chaseCam.setDefaultDistance(5);
         chaseCam.setMinVerticalRotation(-FastMath.HALF_PI + 0.01f);
@@ -103,11 +107,17 @@ public class TestLodGeneration extends SimpleApplication {
 
         SkinningControl skControl = model.getControl(SkinningControl.class);
         if (skControl != null) {
+            // Disable skinning control if found. This is an optimization for static LOD generation
+            // as skinning computation is not needed when generating LODs.
             skControl.setEnabled(false);
         }
 
+        // --- Initial LOD Generation ---
+        // Set initial reduction value and LOD level
         reductionValue = 0.80f;
         lodLevel = 1;
+
+        // Generate LODs for each geometry in the model
         for (final Geometry geom : listGeoms) {
             LodGenerator lodGenerator = new LodGenerator(geom);
             lodGenerator.bakeLods(LodGenerator.TriangleReductionMethod.PROPORTIONAL, reductionValue);
@@ -115,45 +125,49 @@ public class TestLodGeneration extends SimpleApplication {
         }
 
         rootNode.attachChild(model);
+        // Disable the default fly camera as we are using a chase camera
         flyCam.setEnabled(false);
 
-        guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
+        // --- HUD Setup ---
         hudText = new BitmapText(guiFont);
-        hudText.setSize(guiFont.getCharSet().getRenderedSize());
         hudText.setText(computeNbTri() + " tris");
-        hudText.setLocalTranslation(cam.getWidth() / 2, hudText.getLineHeight(), 0);
+        hudText.setLocalTranslation(cam.getWidth() / 2f, hudText.getLineHeight(), 0);
         guiNode.attachChild(hudText);
 
-        inputManager.addListener(new ActionListener() {
-            @Override
-            public void onAction(String name, boolean isPressed, float tpf) {
-                if (isPressed) {
-                    if (name.equals("plus")) {
-                        reductionValue += 0.05f;
-                        updateLod();
-                    }
-                    if (name.equals("minus")) {
-                        reductionValue -= 0.05f;
-                        updateLod();
-                    }
-                    if (name.equals("wireFrame")) {
-                        wireFrame = !wireFrame;
-                        for (Geometry geom : listGeoms) {
-                            Material mat = geom.getMaterial();
-                            mat.getAdditionalRenderState().setWireframe(wireFrame);
-                        }
-                    }
-                }
+        // Register input mappings for user interaction
+        registerInputMappings();
+    }
+
+    @Override
+    public void onAction(String name, boolean isPressed, float tpf) {
+        if (!isPressed) return;
+
+        if (name.equals("plus")) {
+            reductionValue += 0.05f;
+            updateLod();
+
+        } else if (name.equals("minus")) {
+            reductionValue -= 0.05f;
+            updateLod();
+
+        } else if (name.equals("wireframe")) {
+            wireframe = !wireframe;
+            for (Geometry geom : listGeoms) {
+                Material mat = geom.getMaterial();
+                mat.getAdditionalRenderState().setWireframe(wireframe);
             }
-        }, "plus", "minus", "wireFrame");
+        }
+    }
 
-        inputManager.addMapping("plus", new KeyTrigger(KeyInput.KEY_ADD));
-        inputManager.addMapping("minus", new KeyTrigger(KeyInput.KEY_SUBTRACT));
-        inputManager.addMapping("wireFrame", new KeyTrigger(KeyInput.KEY_SPACE));
+    private void registerInputMappings() {
+        addMapping("plus", new KeyTrigger(KeyInput.KEY_P));
+        addMapping("minus", new KeyTrigger(KeyInput.KEY_L));
+        addMapping("wireframe", new KeyTrigger(KeyInput.KEY_SPACE));
     }
 
-    @Override
-    public void simpleUpdate(float tpf) {
+    private void addMapping(String mappingName, Trigger... triggers) {
+        inputManager.addMapping(mappingName, triggers);
+        inputManager.addListener(this, mappingName);
     }
 
     @Override
@@ -163,14 +177,20 @@ public class TestLodGeneration extends SimpleApplication {
     }
 
     private void updateLod() {
+        // Clamp the reduction value between 0.0 and 1.0 to ensure it's within valid range
         reductionValue = FastMath.clamp(reductionValue, 0.0f, 1.0f);
         makeLod(LodGenerator.TriangleReductionMethod.PROPORTIONAL, reductionValue, 1);
     }
 
+    /**
+     * Computes the total number of triangles currently displayed by all geometries.
+     * @return The total number of triangles.
+     */
     private int computeNbTri() {
         int nbTri = 0;
         for (Geometry geom : listGeoms) {
             Mesh mesh = geom.getMesh();
+            // Check if the mesh has LOD levels
             if (mesh.getNumLodLevels() > 0) {
                 nbTri += mesh.getLodLevel(lodLevel).getNumElements();
             } else {
@@ -180,24 +200,46 @@ public class TestLodGeneration extends SimpleApplication {
         return nbTri;
     }
 
-    private void makeLod(final LodGenerator.TriangleReductionMethod method, final float value, final int ll) {
+    /**
+     * Generates and applies LOD levels to the geometries in a background thread.
+     *
+     * @param reductionMethod     The triangle reduction method to use (e.g., PROPORTIONAL).
+     * @param reductionPercentage The percentage of triangles to reduce (0.0 to 1.0).
+     * @param targetLodLevel      The index of the LOD level to set active after generation.
+     */
+    private void makeLod(final LodGenerator.TriangleReductionMethod reductionMethod,
+                         final float reductionPercentage, final int targetLodLevel) {
+
+        // --- Asynchronous LOD Generation ---
+        // Execute the LOD generation process in the background thread pool.
         exec.execute(new Runnable() {
             @Override
             public void run() {
                 for (final Geometry geom : listGeoms) {
                     LodGenerator lodGenerator = new LodGenerator(geom);
-                    final VertexBuffer[] lods = lodGenerator.computeLods(method, value);
+                    final VertexBuffer[] lods = lodGenerator.computeLods(reductionMethod, reductionPercentage);
 
+                    // --- JME Thread Synchronization ---
+                    // Mesh modifications and scene graph updates must be done on the main thread.
                     enqueue(new Callable<Void>() {
                         @Override
                         public Void call() throws Exception {
                             geom.getMesh().setLodLevels(lods);
+
+                            // Reset lodLevel to 0 initially
                             lodLevel = 0;
-                            if (geom.getMesh().getNumLodLevels() > ll) {
-                                lodLevel = ll;
+                            // If the generated LOD levels are more than the target, set to target LOD
+                            if (geom.getMesh().getNumLodLevels() > targetLodLevel) {
+                                lodLevel = targetLodLevel;
                             }
                             geom.setLodLevel(lodLevel);
-                            hudText.setText(computeNbTri() + " tris");
+
+                            int nbTri = computeNbTri();
+                            hudText.setText(nbTri + " tris");
+
+                            // Print debug information to the console
+                            System.out.println(geom + " lodLevel: " + lodLevel + ", numLodLevels: " + geom.getMesh().getNumLodLevels()
+                                    + ", reductionValue: " + reductionValue + ", triangles: " + nbTri);
                             return null;
                         }
                     });

+ 35 - 0
jme3-ios-native/build.gradle

@@ -0,0 +1,35 @@
+import org.apache.tools.ant.taskdefs.condition.Os
+
+task deleteXcframework(type: Delete) {
+    delete 'template/META-INF/robovm/ios/libs/jme3-ios-native.xcframework'
+}
+
+task buildNativeLibIos(type: Exec) {
+    executable "xcodebuild"
+    args 'archive', '-project', 'jme3-ios-native.xcodeproj', '-scheme', 'jme3-ios-native', '-configuration', 'release', '-destination', 'generic/platform=iOS', '-archivePath', 'build/archives/jme3-ios-native_iOS', 'SKIP_INSTALL=NO', 'BUILD_LIBRARY_FOR_DISTRIBUTION=YES'
+}
+
+task buildNativeLibSimulator(type: Exec) {
+    executable "xcodebuild"
+    args 'archive', '-project', 'jme3-ios-native.xcodeproj', '-scheme', 'jme3-ios-native', '-configuration', 'release', '-destination', 'generic/platform=iOS Simulator', '-archivePath', 'build/archives/jme3-ios-native_iOS-Simulator', 'SKIP_INSTALL=NO', 'BUILD_LIBRARY_FOR_DISTRIBUTION=YES'
+}
+
+task buildNativeLib(type: Exec) {
+    dependsOn 'deleteXcframework'
+    dependsOn 'buildNativeLibIos'
+    dependsOn 'buildNativeLibSimulator'
+    executable "xcodebuild"
+    args '-create-xcframework', '-framework', 'build/archives/jme3-ios-native_iOS.xcarchive/Products/Library/Frameworks/jme3_ios_native.framework', '-framework', 'build/archives/jme3-ios-native_iOS-Simulator.xcarchive/Products/Library/Frameworks/jme3_ios_native.framework', '-output', 'template/META-INF/robovm/ios/libs/jme3-ios-native.xcframework'
+}
+
+// buildNativeProjects is a string set to "true"
+if (Os.isFamily(Os.FAMILY_MAC) && buildNativeProjects == "true") {
+    // build native libs and update stored pre-compiled libs to commit
+    compileJava.dependsOn { buildNativeLib }
+} else {
+    // TODO: (like android natives?) use pre-compiled native libs (not building new ones)
+    // compileJava.dependsOn { copyPreCompiledLibs }
+    println "Native build disable or not running on OSX"
+}
+
+jar.into("") { from "template" }

+ 11 - 0
jme3-ios-native/export.sh

@@ -0,0 +1,11 @@
+rm -rf intermediate-builds release template/META-INF/robovm/ios/libs/jme3-ios-native.xcframework
+mkdir intermediate-builds release
+xcodebuild archive -project jme3-ios-native.xcodeproj -scheme jme3-ios-native -destination generic/platform=iOS -archivePath intermediate-builds/jme3-ios-native_iOS SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
+xcodebuild archive -project jme3-ios-native.xcodeproj -scheme jme3-ios-native -destination generic/platform="iOS Simulator" -archivePath intermediate-builds/jme3-ios-native_iOS-Simulator SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
+
+xcodebuild -create-xcframework -framework intermediate-builds/jme3-ios-native_iOS.xcarchive/Products/Library/Frameworks/jme3_ios_native.framework -framework intermediate-builds/jme3-ios-native_iOS-Simulator.xcarchive/Products/Library/Frameworks/jme3_ios_native.framework -output template/META-INF/robovm/ios/libs/jme3-ios-native.xcframework
+
+cd template
+zip -r ../release/jme3-ios-native.jar META-INF
+cd ..
+

+ 416 - 0
jme3-ios-native/jme3-ios-native.xcodeproj/project.pbxproj

@@ -0,0 +1,416 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 52;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		BB0987B82CA2B31900AF4C26 /* com_jme3_util_IosNativeBufferAllocator.c in Sources */ = {isa = PBXBuildFile; fileRef = BB0987B62CA2B31900AF4C26 /* com_jme3_util_IosNativeBufferAllocator.c */; };
+		BB0987B92CA2B31900AF4C26 /* com_jme3_util_IosNativeBufferAllocator.h in Headers */ = {isa = PBXBuildFile; fileRef = BB0987B72CA2B31900AF4C26 /* com_jme3_util_IosNativeBufferAllocator.h */; settings = {ATTRIBUTES = (Public, ); }; };
+		BBAA18642C9CC9B40015DF5E /* jme3_ios_native.h in Headers */ = {isa = PBXBuildFile; fileRef = BBAA18622C9CC9B40015DF5E /* jme3_ios_native.h */; settings = {ATTRIBUTES = (Public, ); }; };
+		BBAA18752C9CCACB0015DF5E /* com_jme3_audio_ios_IosEFX.c in Sources */ = {isa = PBXBuildFile; fileRef = BBAA186A2C9CCACB0015DF5E /* com_jme3_audio_ios_IosEFX.c */; };
+		BBAA18762C9CCACB0015DF5E /* com_jme3_audio_ios_IosALC.c in Sources */ = {isa = PBXBuildFile; fileRef = BBAA186B2C9CCACB0015DF5E /* com_jme3_audio_ios_IosALC.c */; };
+		BBAA18772C9CCACB0015DF5E /* com_jme3_audio_ios_IosEFX.h in Headers */ = {isa = PBXBuildFile; fileRef = BBAA186C2C9CCACB0015DF5E /* com_jme3_audio_ios_IosEFX.h */; settings = {ATTRIBUTES = (Public, ); }; };
+		BBAA18782C9CCACB0015DF5E /* com_jme3_audio_ios_IosALC.h in Headers */ = {isa = PBXBuildFile; fileRef = BBAA186D2C9CCACB0015DF5E /* com_jme3_audio_ios_IosALC.h */; settings = {ATTRIBUTES = (Public, ); }; };
+		BBAA18792C9CCACB0015DF5E /* JmeAppHarness.m in Sources */ = {isa = PBXBuildFile; fileRef = BBAA186E2C9CCACB0015DF5E /* JmeAppHarness.m */; };
+		BBAA187A2C9CCACB0015DF5E /* com_jme3_audio_ios_IosAL.h in Headers */ = {isa = PBXBuildFile; fileRef = BBAA186F2C9CCACB0015DF5E /* com_jme3_audio_ios_IosAL.h */; settings = {ATTRIBUTES = (Public, ); }; };
+		BBAA187B2C9CCACB0015DF5E /* com_jme3_audio_ios_IosAL.c in Sources */ = {isa = PBXBuildFile; fileRef = BBAA18702C9CCACB0015DF5E /* com_jme3_audio_ios_IosAL.c */; };
+		BBAA187C2C9CCACB0015DF5E /* JmeIosGLES.m in Sources */ = {isa = PBXBuildFile; fileRef = BBAA18712C9CCACB0015DF5E /* JmeIosGLES.m */; };
+		BBAA187D2C9CCACB0015DF5E /* JmeAppHarness.java in Sources */ = {isa = PBXBuildFile; fileRef = BBAA18722C9CCACB0015DF5E /* JmeAppHarness.java */; };
+		BBAA187E2C9CCACB0015DF5E /* jme-ios.m in Sources */ = {isa = PBXBuildFile; fileRef = BBAA18732C9CCACB0015DF5E /* jme-ios.m */; };
+		BBAA18822C9CCB720015DF5E /* OpenGLES.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BBAA18812C9CCB720015DF5E /* OpenGLES.framework */; platformFilter = ios; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+		BB0987B62CA2B31900AF4C26 /* com_jme3_util_IosNativeBufferAllocator.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = com_jme3_util_IosNativeBufferAllocator.c; sourceTree = "<group>"; };
+		BB0987B72CA2B31900AF4C26 /* com_jme3_util_IosNativeBufferAllocator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = com_jme3_util_IosNativeBufferAllocator.h; sourceTree = "<group>"; };
+		BBAA185F2C9CC9B40015DF5E /* jme3_ios_native.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = jme3_ios_native.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		BBAA18622C9CC9B40015DF5E /* jme3_ios_native.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = jme3_ios_native.h; sourceTree = "<group>"; };
+		BBAA18632C9CC9B40015DF5E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		BBAA186A2C9CCACB0015DF5E /* com_jme3_audio_ios_IosEFX.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = com_jme3_audio_ios_IosEFX.c; sourceTree = "<group>"; };
+		BBAA186B2C9CCACB0015DF5E /* com_jme3_audio_ios_IosALC.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = com_jme3_audio_ios_IosALC.c; sourceTree = "<group>"; };
+		BBAA186C2C9CCACB0015DF5E /* com_jme3_audio_ios_IosEFX.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = com_jme3_audio_ios_IosEFX.h; sourceTree = "<group>"; };
+		BBAA186D2C9CCACB0015DF5E /* com_jme3_audio_ios_IosALC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = com_jme3_audio_ios_IosALC.h; sourceTree = "<group>"; };
+		BBAA186E2C9CCACB0015DF5E /* JmeAppHarness.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JmeAppHarness.m; sourceTree = "<group>"; };
+		BBAA186F2C9CCACB0015DF5E /* com_jme3_audio_ios_IosAL.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = com_jme3_audio_ios_IosAL.h; sourceTree = "<group>"; };
+		BBAA18702C9CCACB0015DF5E /* com_jme3_audio_ios_IosAL.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = com_jme3_audio_ios_IosAL.c; sourceTree = "<group>"; };
+		BBAA18712C9CCACB0015DF5E /* JmeIosGLES.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JmeIosGLES.m; sourceTree = "<group>"; };
+		BBAA18722C9CCACB0015DF5E /* JmeAppHarness.java */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.java; path = JmeAppHarness.java; sourceTree = "<group>"; };
+		BBAA18732C9CCACB0015DF5E /* jme-ios.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "jme-ios.m"; sourceTree = "<group>"; };
+		BBAA18812C9CCB720015DF5E /* OpenGLES.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGLES.framework; path = System/Library/Frameworks/OpenGLES.framework; sourceTree = SDKROOT; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		BBAA185C2C9CC9B40015DF5E /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				BBAA18822C9CCB720015DF5E /* OpenGLES.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		BBAA18552C9CC9B40015DF5E = {
+			isa = PBXGroup;
+			children = (
+				BBAA18612C9CC9B40015DF5E /* src */,
+				BBAA18602C9CC9B40015DF5E /* Products */,
+				BBAA18802C9CCB710015DF5E /* Frameworks */,
+			);
+			sourceTree = "<group>";
+		};
+		BBAA18602C9CC9B40015DF5E /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				BBAA185F2C9CC9B40015DF5E /* jme3_ios_native.framework */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		BBAA18612C9CC9B40015DF5E /* src */ = {
+			isa = PBXGroup;
+			children = (
+				BB0987B62CA2B31900AF4C26 /* com_jme3_util_IosNativeBufferAllocator.c */,
+				BB0987B72CA2B31900AF4C26 /* com_jme3_util_IosNativeBufferAllocator.h */,
+				BBAA18702C9CCACB0015DF5E /* com_jme3_audio_ios_IosAL.c */,
+				BBAA186F2C9CCACB0015DF5E /* com_jme3_audio_ios_IosAL.h */,
+				BBAA186B2C9CCACB0015DF5E /* com_jme3_audio_ios_IosALC.c */,
+				BBAA186D2C9CCACB0015DF5E /* com_jme3_audio_ios_IosALC.h */,
+				BBAA186A2C9CCACB0015DF5E /* com_jme3_audio_ios_IosEFX.c */,
+				BBAA186C2C9CCACB0015DF5E /* com_jme3_audio_ios_IosEFX.h */,
+				BBAA18732C9CCACB0015DF5E /* jme-ios.m */,
+				BBAA18722C9CCACB0015DF5E /* JmeAppHarness.java */,
+				BBAA186E2C9CCACB0015DF5E /* JmeAppHarness.m */,
+				BBAA18712C9CCACB0015DF5E /* JmeIosGLES.m */,
+				BBAA18622C9CC9B40015DF5E /* jme3_ios_native.h */,
+				BBAA18632C9CC9B40015DF5E /* Info.plist */,
+			);
+			path = src;
+			sourceTree = "<group>";
+		};
+		BBAA18802C9CCB710015DF5E /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				BBAA18812C9CCB720015DF5E /* OpenGLES.framework */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXHeadersBuildPhase section */
+		BBAA185A2C9CC9B40015DF5E /* Headers */ = {
+			isa = PBXHeadersBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				BBAA18772C9CCACB0015DF5E /* com_jme3_audio_ios_IosEFX.h in Headers */,
+				BBAA187A2C9CCACB0015DF5E /* com_jme3_audio_ios_IosAL.h in Headers */,
+				BB0987B92CA2B31900AF4C26 /* com_jme3_util_IosNativeBufferAllocator.h in Headers */,
+				BBAA18782C9CCACB0015DF5E /* com_jme3_audio_ios_IosALC.h in Headers */,
+				BBAA18642C9CC9B40015DF5E /* jme3_ios_native.h in Headers */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXHeadersBuildPhase section */
+
+/* Begin PBXNativeTarget section */
+		BBAA185E2C9CC9B40015DF5E /* jme3-ios-native */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = BBAA18672C9CC9B40015DF5E /* Build configuration list for PBXNativeTarget "jme3-ios-native" */;
+			buildPhases = (
+				BBAA185A2C9CC9B40015DF5E /* Headers */,
+				BBAA185B2C9CC9B40015DF5E /* Sources */,
+				BBAA185C2C9CC9B40015DF5E /* Frameworks */,
+				BBAA185D2C9CC9B40015DF5E /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = "jme3-ios-native";
+			productName = "jme3-ios-native";
+			productReference = BBAA185F2C9CC9B40015DF5E /* jme3_ios_native.framework */;
+			productType = "com.apple.product-type.framework";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		BBAA18562C9CC9B40015DF5E /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastUpgradeCheck = 1130;
+				TargetAttributes = {
+					BBAA185E2C9CC9B40015DF5E = {
+						CreatedOnToolsVersion = 11.3.1;
+					};
+				};
+			};
+			buildConfigurationList = BBAA18592C9CC9B40015DF5E /* Build configuration list for PBXProject "jme3-ios-native" */;
+			compatibilityVersion = "Xcode 9.3";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = BBAA18552C9CC9B40015DF5E;
+			productRefGroup = BBAA18602C9CC9B40015DF5E /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				BBAA185E2C9CC9B40015DF5E /* jme3-ios-native */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		BBAA185D2C9CC9B40015DF5E /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		BBAA185B2C9CC9B40015DF5E /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				BBAA18762C9CCACB0015DF5E /* com_jme3_audio_ios_IosALC.c in Sources */,
+				BBAA187E2C9CCACB0015DF5E /* jme-ios.m in Sources */,
+				BBAA187D2C9CCACB0015DF5E /* JmeAppHarness.java in Sources */,
+				BBAA18752C9CCACB0015DF5E /* com_jme3_audio_ios_IosEFX.c in Sources */,
+				BBAA187B2C9CCACB0015DF5E /* com_jme3_audio_ios_IosAL.c in Sources */,
+				BBAA18792C9CCACB0015DF5E /* JmeAppHarness.m in Sources */,
+				BBAA187C2C9CCACB0015DF5E /* JmeIosGLES.m in Sources */,
+				BB0987B82CA2B31900AF4C26 /* com_jme3_util_IosNativeBufferAllocator.c in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+		BBAA18652C9CC9B40015DF5E /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = NO;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				CURRENT_PROJECT_VERSION = 1;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 10.15;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VERSIONING_SYSTEM = "apple-generic";
+				VERSION_INFO_PREFIX = "";
+			};
+			name = Debug;
+		};
+		BBAA18662C9CC9B40015DF5E /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = NO;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				CURRENT_PROJECT_VERSION = 1;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 10.15;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				MTL_FAST_MATH = YES;
+				SDKROOT = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VERSIONING_SYSTEM = "apple-generic";
+				VERSION_INFO_PREFIX = "";
+			};
+			name = Release;
+		};
+		BBAA18682C9CC9B40015DF5E /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				DEFINES_MODULE = YES;
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				HEADER_SEARCH_PATHS = (
+					/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/JavaVM.framework/Headers/,
+					"/Library/Java/JavaVirtualMachines/jdk-11.0.6.jdk/Contents/Home/include/**",
+					"/Users/runner/hostedtoolcache/Java_Temurin-Hotspot_jdk/11.0.26-4/arm64/Contents/Home/include/**",
+				);
+				INFOPLIST_FILE = src/Info.plist;
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+					"@loader_path/Frameworks",
+				);
+				OTHER_CFLAGS = (
+					"-fno-stack-check",
+					"-fno-stack-protector",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = "jme3.jme3-ios-native";
+				PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+				SKIP_INSTALL = YES;
+				SUPPORTS_MACCATALYST = YES;
+				USER_HEADER_SEARCH_PATHS = (
+					/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/JavaVM.framework/Headers/,
+					"/Library/Java/JavaVirtualMachines/jdk-11.0.6.jdk/Contents/Home/include/**",
+					"/Users/runner/hostedtoolcache/Java_Temurin-Hotspot_jdk/11.0.26-4/arm64/Contents/Home/include/**",
+				);
+			};
+			name = Debug;
+		};
+		BBAA18692C9CC9B40015DF5E /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				DEFINES_MODULE = YES;
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				HEADER_SEARCH_PATHS = (
+					/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/JavaVM.framework/Headers/,
+					"/Library/Java/JavaVirtualMachines/jdk-11.0.6.jdk/Contents/Home/include/**",
+					"/Users/runner/hostedtoolcache/Java_Temurin-Hotspot_jdk/11.0.26-4/arm64/Contents/Home/include/**",
+				);
+				INFOPLIST_FILE = src/Info.plist;
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+					"@loader_path/Frameworks",
+				);
+				OTHER_CFLAGS = (
+					"-fno-stack-check",
+					"-fno-stack-protector",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = "jme3.jme3-ios-native";
+				PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+				SKIP_INSTALL = YES;
+				SUPPORTS_MACCATALYST = YES;
+				USER_HEADER_SEARCH_PATHS = (
+					/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/JavaVM.framework/Headers/,
+					"/Library/Java/JavaVirtualMachines/jdk-11.0.6.jdk/Contents/Home/include/**",
+					"/Users/runner/hostedtoolcache/Java_Temurin-Hotspot_jdk/11.0.26-4/arm64/Contents/Home/include/**",
+				);
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		BBAA18592C9CC9B40015DF5E /* Build configuration list for PBXProject "jme3-ios-native" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				BBAA18652C9CC9B40015DF5E /* Debug */,
+				BBAA18662C9CC9B40015DF5E /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		BBAA18672C9CC9B40015DF5E /* Build configuration list for PBXNativeTarget "jme3-ios-native" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				BBAA18682C9CC9B40015DF5E /* Debug */,
+				BBAA18692C9CC9B40015DF5E /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = BBAA18562C9CC9B40015DF5E /* Project object */;
+}

+ 22 - 0
jme3-ios-native/src/Info.plist

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>$(CURRENT_PROJECT_VERSION)</string>
+</dict>
+</plist>

+ 130 - 0
jme3-ios-native/src/JmeAppHarness.java

@@ -0,0 +1,130 @@
+import com.jme3.system.ios.IosHarness;
+import com.jme3.input.ios.IosInputHandler;
+import com.jme3.math.Vector2f;
+import com.jme3.renderer.opengl.GLRenderer;
+import com.jme3.system.JmeContext;
+import com.jme3.system.AppSettings;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * You can extend this class to perform iOS-only operations from java,
+ * native methods can reside either in .c/.m files in this directory
+ * or in the XCode project itself.
+ * @author normenhansen
+ */
+public class JmeAppHarness extends IosHarness{
+
+    private static final Logger logger = Logger.getLogger(JmeAppHarness.class.getName());
+	protected Renderer renderer;
+	protected IosInputHandler input;
+	protected boolean autoFlush = true;
+	protected Vector2f resizePending = null;
+
+
+    /**
+     * An instance of this object is created when your application
+     * has started on the iOS side.
+     * You can e.g. attach special AppStates or do similar things here. You can
+     * access classes from this source directory as well as your main projects
+     * sources and classpath.
+     */
+    public JmeAppHarness(long id) {
+        super(id);
+        app = new mygame.Main();
+        AppSettings settings = new AppSettings(true);
+        this.app.setSettings(settings);
+        app.start();
+        logger.log(Level.FINE, "JmeAppHarness constructor");
+        app.gainFocus();
+    }
+
+    @Override
+    public void appPaused() {
+        logger.log(Level.FINE, "JmeAppHarness appPaused");
+    }
+
+    @Override
+    public void appReactivated() {
+        logger.log(Level.FINE, "JmeAppHarness appReactivated");
+    }
+
+    @Override
+    public void appClosed() {
+        logger.log(Level.FINE, "JmeAppHarness appClosed");
+        app.stop();
+    }
+
+    @Override
+    public void appUpdate() {
+        logger.log(Level.FINE, "JmeAppHarness appUpdate");
+       //app.update();
+    }
+
+    @Override
+    public void appDraw() {
+        logger.log(Level.FINE, "JmeAppHarness appDraw");
+        if (renderer == null) {
+            JmeContext iosContext = app.getContext();
+            renderer = iosContext.getRenderer();
+            renderer.initialize();
+            input = (IosInputHandler)iosContext.getTouchInput();
+            input.initialize();
+        } else {
+            if(resizePending != null) {
+                appReshape((int)resizePending.x, (int)resizePending.y);
+                resizePending = null;
+            }
+            app.update();
+    	    if (autoFlush) {
+                renderer.postFrame();
+            }
+        }
+    }
+    
+    @Override
+    public void appReshape(int width, int height) {
+        logger.log(Level.FINE, "JmeAppHarness reshape");
+        AppSettings settings = app.getContext().getSettings();
+        settings.setResolution(width, height);
+        if (renderer != null) {
+            app.reshape(width, height);
+            resizePending = null;
+        } else {
+            resizePending = new Vector2f(width, height);
+        }
+
+        if (input != null) {
+            input.loadSettings(settings);
+        }
+    }
+    
+    public void injectTouchBegin(int pointerId, long time, float x, float y) {
+    	if (input != null) {
+        	logger.log(Level.FINE, "JmeAppHarness injectTouchBegin");
+    		input.injectTouchDown(pointerId, time, x, y);
+    	}
+    }
+    
+    public void injectTouchMove(int pointerId, long time, float x, float y) {
+    	if (input != null) {
+        	logger.log(Level.FINE, "JmeAppHarness injectTouchMove");
+    		input.injectTouchMove(pointerId, time, x, y);
+    	}
+    }
+    
+    public void injectTouchEnd(int pointerId, long time, float x, float y) {
+    	if (input != null) {
+        	logger.log(Level.FINE, "JmeAppHarness injectTouchEnd");
+    		input.injectTouchUp(pointerId, time, x, y);
+    	}
+    }
+    
+    /**
+     * Example of a native method calling iOS code.
+     * See the native code in IosHarness.m
+     * @param text The message to display
+     */
+    public native void showDialog(String text);
+
+}

+ 60 - 0
jme3-ios-native/src/JmeAppHarness.m

@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2009-2013 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.
+ */
+#include <jni.h>
+#import <UIKit/UIKit.h>
+
+/**
+ * Author: Normen Hansen
+ */
+
+#ifndef JNIEXPORT
+#define JNIEXPORT __attribute__ ((visibility("default"))) \
+  __attribute__ ((used))
+#endif
+
+#ifndef _Included_JmeAppHarness
+#define _Included_JmeAppHarness
+#endif
+
+JNIEXPORT void JNICALL
+Java_JmeAppHarness_showDialog(JNIEnv* e, jobject c, jstring text) {
+    const char* chars = (*e)->GetStringUTFChars(e, text, 0);
+    NSString* string = [[NSString alloc] initWithUTF8String : chars];
+    (*e)->ReleaseStringUTFChars(e, text, chars);
+    UIAlertView *alert = [[UIAlertView alloc] initWithTitle : @"Message"
+            message : string
+            delegate : nil
+            cancelButtonTitle : @"OK"
+            otherButtonTitles : nil];
+    [alert show];
+    [alert release];
+}

+ 2392 - 0
jme3-ios-native/src/JmeIosGLES.m

@@ -0,0 +1,2392 @@
+#import <stdlib.h>
+#define __LP64__ 1
+#import <jni.h>
+#import <OpenGLES/ES2/gl.h>
+#import <OpenGLES/ES2/glext.h>
+#import <OpenGLES/ES3/gl.h>
+#import <OpenGLES/ES3/glext.h>
+
+/**
+ * Author: Kostyantyn Hushchyn, Jesus Oliver
+ */
+
+#ifndef JNIEXPORT
+#define JNIEXPORT __attribute__ ((visibility("default"))) \
+  __attribute__ ((used))
+#endif
+
+#ifndef _Included_JmeIosGLES
+#define _Included_JmeIosGLES
+#endif
+
+#define glBindVertexArray glBindVertexArrayOES
+
+static int initialized = 0;
+
+static jclass bufferClass = (jclass)0;
+static jclass byteBufferClass = (jclass)0;
+static jclass shortBufferClass = (jclass)0;
+static jclass intBufferClass = (jclass)0;
+static jclass floatBufferClass = (jclass)0;
+static jfieldID positionID;
+static jfieldID limitID;
+
+
+static void
+nativeClassInit(JNIEnv *e);
+
+static int
+allowIndirectBuffers(JNIEnv *e);
+
+static void *
+getDirectBufferPointer(JNIEnv *e, jobject buffer);
+
+static void *
+getPointer(JNIEnv *e, jobject buffer, jarray *array, jint *remaining, jint *offset);
+
+static void
+releasePointer(JNIEnv *e, jarray array, void *data, jboolean commit);
+
+static void
+jniThrowException(JNIEnv *e, const char* type, const char* message);
+
+static jint
+getBufferElementSize(JNIEnv *e, jobject buffer);
+
+static int getNeededCount(GLint pname);
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glActiveTexture(JNIEnv* e, jobject c, jint texture) {
+    glActiveTexture(
+        (GLenum)texture
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glAttachShader(JNIEnv* e, jobject c, jint program, jint shader) {
+    glAttachShader(
+        (GLuint)program,
+        (GLuint)shader
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glBindBuffer(JNIEnv* e, jobject c, jint target, jint buffer) {
+    glBindBuffer(
+        (GLenum)target,
+        (GLuint)buffer
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glBindFramebuffer(JNIEnv* e, jobject c, jint target, jint framebuffer) {
+    glBindFramebuffer(
+        (GLenum)target,
+        (GLuint)framebuffer
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glBindRenderbuffer(JNIEnv* e, jobject c, jint target, jint renderbuffer) {
+    glBindRenderbuffer(
+        (GLenum)target,
+        (GLuint)renderbuffer
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glBindTexture(JNIEnv* e, jobject c, jint target, jint texture) {
+    glBindTexture(
+        (GLenum)target,
+        (GLuint)texture
+    );
+}
+
+ // TODO: Investigate this
+ /*
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glBindVertexArray(JNIEnv* e, jobject c, jint array) {
+	glBindVertexArray(array);
+}
+*/
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glBlendFunc(JNIEnv* e, jobject c, jint sfactor, jint dfactor) {
+    glBlendFunc(
+        (GLenum)sfactor,
+        (GLenum)dfactor
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glBufferData(JNIEnv* e, jobject c, jint target, jint size, jobject data_buf, jint usage) {
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLvoid *data = (GLvoid *) 0;
+
+    if (data_buf) {
+        data = (GLvoid *)getPointer(e, data_buf, &_array, &_remaining, &_bufferOffset);
+        if (_remaining < size) {
+            _exception = 1;
+            _exceptionType = "java/lang/IllegalArgumentException";
+            _exceptionMessage = "remaining() < size < needed";
+            goto exit;
+        }
+    }
+    if (data_buf && data == NULL) {
+        char * _dataBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        data = (GLvoid *) (_dataBase + _bufferOffset);
+    }
+    glBufferData(
+        (GLenum)target,
+        (GLsizeiptr)size,
+        (GLvoid *)data,
+        (GLenum)usage
+    );
+
+exit:
+    if (_array) {
+        releasePointer(e, _array, data, JNI_FALSE);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glBufferData2(JNIEnv* e, jobject c, jint target, jint size, jbyteArray data, jint offset, jint usage) {
+	jbyte *dataNative = (*e)->GetByteArrayElements(e, data, NULL);
+	
+    glBufferData(
+        (GLenum)target,
+        (GLsizeiptr)size,
+        (GLvoid *)dataNative,
+        (GLenum)(usage + offset)
+    );
+	
+	(*e)->ReleaseByteArrayElements(e, data, dataNative, 0);
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glBufferSubData(JNIEnv* e, jobject c, jint target, jint offset, jint size, jobject data_buf) {
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLvoid *data = (GLvoid *) 0;
+
+    data = (GLvoid *)getPointer(e, data_buf, &_array, &_remaining, &_bufferOffset);
+    if (_remaining < size) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "remaining() < size < needed";
+        goto exit;
+    }
+    if (data == NULL) {
+        char * _dataBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        data = (GLvoid *) (_dataBase + _bufferOffset);
+    }
+    glBufferSubData(
+        (GLenum)target,
+        (GLintptr)offset,
+        (GLsizeiptr)size,
+        (GLvoid *)data
+    );
+
+exit:
+    if (_array) {
+        releasePointer(e, _array, data, JNI_FALSE);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glBufferSubData2(JNIEnv* e, jobject c, jint target, jint offset, jint size, jbyteArray data, jint dataoffset) {
+	jbyte *dataNative = (*e)->GetByteArrayElements(e, data, NULL);
+	
+    glBufferSubData(
+        (GLenum)target,
+        (GLintptr)offset,
+        (GLsizeiptr)size,
+        (GLvoid *)(dataNative + dataoffset)
+    );
+	
+	(*e)->ReleaseByteArrayElements(e, data, dataNative, 0);
+}
+
+JNIEXPORT jint JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glCheckFramebufferStatus(JNIEnv* e, jobject c, jint target) {
+    GLenum _returnValue;
+    _returnValue = glCheckFramebufferStatus(
+        (GLenum)target
+    );
+    return (jint)_returnValue;
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glClear(JNIEnv* e, jobject c, jint mask) {
+    glClear(
+        (GLbitfield)mask
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glClearColor(JNIEnv* e, jobject c, jfloat red, jfloat green, jfloat blue, jfloat alpha) {
+    glClearColor(
+        (GLclampf)red,
+        (GLclampf)green,
+        (GLclampf)blue,
+        (GLclampf)alpha
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glColorMask(JNIEnv* e, jobject c, jboolean red, jboolean green, jboolean blue, jboolean alpha) {
+    glColorMask(
+        (GLboolean)red,
+        (GLboolean)green,
+        (GLboolean)blue,
+        (GLboolean)alpha
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glCompileShader(JNIEnv* e, jobject c, jint shader) {
+    glCompileShader(
+        (GLuint)shader
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glCompressedTexImage2D(JNIEnv* e, jobject c, jint target, jint level, jint internalformat, jint width, jint height, jint border, jint imageSize, jobject pixels_buf) {
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLvoid *pixels = (GLvoid *) 0;
+
+    if (pixels_buf) {
+        pixels = (GLvoid *)getPointer(e, pixels_buf, &_array, &_remaining, &_bufferOffset);
+    }
+    if (pixels_buf && pixels == NULL) {
+        char * _pixelsBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        pixels = (GLvoid *) (_pixelsBase + _bufferOffset);
+    }
+    glCompressedTexImage2D(
+        (GLenum)target,
+        (GLint)level,
+        (GLenum)internalformat,
+        (GLsizei)width,
+        (GLsizei)height,
+        (GLint)border,
+        (GLsizei)imageSize,
+        (GLvoid *)pixels
+    );
+    if (_array) {
+        releasePointer(e, _array, pixels, JNI_FALSE);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glCompressedTexSubImage2D(JNIEnv* e, jobject c, jint target, jint level, jint xoffset, jint yoffset, jint width, jint height, jint format, jint imageSize, jobject pixels_buf) {
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLvoid *pixels = (GLvoid *) 0;
+
+    if (pixels_buf) {
+        pixels = (GLvoid *)getPointer(e, pixels_buf, &_array, &_remaining, &_bufferOffset);
+    }
+    if (pixels_buf && pixels == NULL) {
+        char * _pixelsBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        pixels = (GLvoid *) (_pixelsBase + _bufferOffset);
+    }
+    glCompressedTexSubImage2D(
+        (GLenum)target,
+        (GLint)level,
+        (GLint)xoffset,
+        (GLint)yoffset,
+        (GLsizei)width,
+        (GLsizei)height,
+        (GLenum)format,
+        (GLsizei)imageSize,
+        (GLvoid *)pixels
+    );
+    if (_array) {
+        releasePointer(e, _array, pixels, JNI_FALSE);
+    }
+}
+
+JNIEXPORT jint JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glCreateProgram(JNIEnv* e, jobject c) {
+    GLuint _returnValue;
+    _returnValue = glCreateProgram();
+    return (jint)_returnValue;
+}
+
+JNIEXPORT jint JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glCreateShader(JNIEnv* e, jobject c, jint shaderType) {
+    GLuint _returnValue;
+    _returnValue = glCreateShader(
+        (GLenum)shaderType
+    );
+    return (jint)_returnValue;
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glCullFace(JNIEnv* e, jobject c, jint mode) {
+    glCullFace(
+        (GLenum)mode
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glDeleteBuffers(JNIEnv* e, jobject c, jint n, jintArray buffers, jint offset) {
+	jint *buffersNative = (*e)->GetIntArrayElements(e, buffers, NULL);
+	
+    glDeleteBuffers(
+        (GLsizei)n,
+        (GLuint *)buffersNative
+    );
+	
+	(*e)->ReleaseIntArrayElements(e, buffers, buffersNative, 0);
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glDeleteFramebuffers(JNIEnv* e, jobject c, jint n, jintArray framebuffers, jint offset) {
+	jint *buffersNative = (*e)->GetIntArrayElements(e, framebuffers, NULL);
+	
+    glDeleteFramebuffers(
+        (GLsizei)n,
+        (GLuint *)buffersNative
+    );
+	
+	(*e)->ReleaseIntArrayElements(e, framebuffers, buffersNative, 0);
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glDeleteProgram(JNIEnv* e, jobject c, jint program) {
+    glDeleteProgram(
+        (GLuint)program
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glDeleteRenderbuffers(JNIEnv* e, jobject c, jint n, jintArray renderbuffers, jint offset) {
+	jint *buffersNative = (*e)->GetIntArrayElements(e, renderbuffers, NULL);
+	
+    glDeleteRenderbuffers(
+        (GLsizei)n,
+        (GLuint *)buffersNative
+    );
+	
+	(*e)->ReleaseIntArrayElements(e, renderbuffers, buffersNative, 0);
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glDeleteShader(JNIEnv* e, jobject c, jint shader) {
+    glDeleteShader(
+        (GLuint)shader
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glDeleteTextures(JNIEnv* e, jobject c, jint n, jintArray textures_ref, jint offset) {
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    GLuint *textures_base = (GLuint *) 0;
+    jint _remaining;
+    GLuint *textures = (GLuint *) 0;
+
+    if (!textures_ref) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "textures == null";
+        goto exit;
+    }
+    if (offset < 0) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "offset < 0";
+        goto exit;
+    }
+    _remaining = (*e)->GetArrayLength(e, textures_ref) - offset;
+    if (_remaining < n) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "length - offset < n < needed";
+        goto exit;
+    }
+    textures_base = (GLuint *)
+        (*e)->GetPrimitiveArrayCritical(e, textures_ref, (jboolean *)0);
+    textures = textures_base + offset;
+
+    glDeleteTextures(
+        (GLsizei)n,
+        (GLuint *)textures
+    );
+
+exit:
+    if (textures_base) {
+        (*e)->ReleasePrimitiveArrayCritical(e, textures_ref, textures_base,
+            JNI_ABORT);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glDepthFunc(JNIEnv* e, jobject c, jint func) {
+    glDepthFunc(
+        (GLenum)func
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glDepthMask(JNIEnv* e, jobject c, jboolean flag) {
+    glDepthMask(
+        (GLboolean)flag
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glDepthRangef(JNIEnv* e, jobject c, jfloat zNear, jfloat zFar) {
+    glDepthRangef(
+        (GLclampf)zNear,
+        (GLclampf)zFar
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glDetachShader(JNIEnv* e, jobject c, jint program, jint shader) {
+    glDetachShader(
+        (GLuint)program,
+        (GLuint)shader
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glDisable(JNIEnv* e, jobject c, jint cap) {
+    glDisable(
+        (GLenum)cap
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glDisableVertexAttribArray(JNIEnv* e, jobject c, jint index) {
+    glDisableVertexAttribArray(
+        (GLuint)index
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glDrawArrays(JNIEnv* e, jobject c, jint mode, jint first, jint count) {
+    glDrawArrays(
+        (GLenum)mode,
+        (GLint)first,
+        (GLsizei)count
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glDrawElements(JNIEnv* e, jobject c, jint mode, jint count, jint type, jobject indices_buf) {
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLvoid *indices = (GLvoid *) 0;
+
+    indices = (GLvoid *)getPointer(e, indices_buf, &_array, &_remaining, &_bufferOffset);
+    if (_remaining < count) {
+        _exception = 1;
+        _exceptionType = "java/lang/ArrayIndexOutOfBoundsException";
+        _exceptionMessage = "remaining() < count < needed";
+        goto exit;
+    }
+    if (indices == NULL) {
+        char * _indicesBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        indices = (GLvoid *) (_indicesBase + _bufferOffset);
+    }
+    glDrawElements(
+        (GLenum)mode,
+        (GLsizei)count,
+        (GLenum)type,
+        (GLvoid *)indices
+    );
+
+exit:
+    if (_array) {
+        releasePointer(e, _array, indices, JNI_FALSE);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glDrawElements2(JNIEnv* e, jobject c, jint mode, jint count, jint type, jbyteArray indices, jint offset) {
+	jbyte *indicesNative = (*e)->GetByteArrayElements(e, indices, NULL);
+	
+    glDrawElements(
+        (GLenum)mode,
+        (GLsizei)count,
+        (GLenum)type,
+        (GLvoid *)(indicesNative + offset)
+    );
+	
+	(*e)->ReleaseByteArrayElements(e, indices, indicesNative, 0);
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glDrawElementsIndex(JNIEnv* e, jobject c, jint mode, jint count, jint type, jint offset) {
+    glDrawElements(
+        (GLenum)mode,
+        (GLsizei)count,
+        (GLenum)type,
+        (GLvoid *)offset
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glEnable(JNIEnv* e, jobject c, jint cap) {
+    glEnable(
+        (GLenum)cap
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glEnableVertexAttribArray(JNIEnv* e, jobject c, jint index) {
+    glEnableVertexAttribArray(
+        (GLuint)index
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glFramebufferRenderbuffer(JNIEnv* e, jobject c, jint target, jint attachment, jint renderbuffertarget, jint renderbuffer) {
+    glFramebufferRenderbuffer(
+        (GLenum)target,
+        (GLenum)attachment,
+        (GLenum)renderbuffertarget,
+        (GLuint)renderbuffer
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glFramebufferTexture2D(JNIEnv* e, jobject c, jint target, jint attachment, jint textarget, jint texture, jint level) {
+    glFramebufferTexture2D(
+        (GLenum)target,
+        (GLenum)attachment,
+        (GLenum)textarget,
+        (GLuint)texture,
+        (GLint)level
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glGenBuffers(JNIEnv* e, jobject c, jint n, jintArray buffers_ref, jint offset) {
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    GLuint *buffers_base = (GLuint *) 0;
+    jint _remaining;
+    GLuint *buffers = (GLuint *) 0;
+
+    if (!buffers_ref) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "buffers == null";
+        goto exit;
+    }
+    if (offset < 0) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "offset < 0";
+        goto exit;
+    }
+    _remaining = (*e)->GetArrayLength(e, buffers_ref) - offset;
+    if (_remaining < n) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "length - offset < n < needed";
+        goto exit;
+    }
+    buffers_base = (GLuint *)
+        (*e)->GetPrimitiveArrayCritical(e, buffers_ref, (jboolean *)0);
+    buffers = buffers_base + offset;
+
+    glGenBuffers(
+        (GLsizei)n,
+        (GLuint *)buffers
+    );
+
+exit:
+    if (buffers_base) {
+        (*e)->ReleasePrimitiveArrayCritical(e, buffers_ref, buffers_base,
+            _exception ? JNI_ABORT: 0);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glGenFramebuffers(JNIEnv* e, jobject c, jint n, jintArray framebuffers_ref, jint offset) {
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    GLuint *buffers_base = (GLuint *) 0;
+    jint _remaining;
+    GLuint *buffers = (GLuint *) 0;
+
+    if (!framebuffers_ref) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "buffers == null";
+        goto exit;
+    }
+    if (offset < 0) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "offset < 0";
+        goto exit;
+    }
+    _remaining = (*e)->GetArrayLength(e, framebuffers_ref) - offset;
+    if (_remaining < n) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "length - offset < n < needed";
+        goto exit;
+    }
+    buffers_base = (GLuint *)
+        (*e)->GetPrimitiveArrayCritical(e, framebuffers_ref, (jboolean *)0);
+    buffers = buffers_base + offset;
+
+    glGenFramebuffers(
+        (GLsizei)n,
+        (GLuint *)buffers
+    );
+
+exit:
+    if (buffers_base) {
+        (*e)->ReleasePrimitiveArrayCritical(e, framebuffers_ref, buffers_base,
+            _exception ? JNI_ABORT: 0);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glGenRenderbuffers(JNIEnv* e, jobject c, jint n, jintArray renderbuffers_ref, jint offset) {
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    GLuint *buffers_base = (GLuint *) 0;
+    jint _remaining;
+    GLuint *buffers = (GLuint *) 0;
+
+    if (!renderbuffers_ref) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "buffers == null";
+        goto exit;
+    }
+    if (offset < 0) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "offset < 0";
+        goto exit;
+    }
+    _remaining = (*e)->GetArrayLength(e, renderbuffers_ref) - offset;
+    if (_remaining < n) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "length - offset < n < needed";
+        goto exit;
+    }
+    buffers_base = (GLuint *)
+        (*e)->GetPrimitiveArrayCritical(e, renderbuffers_ref, (jboolean *)0);
+    buffers = buffers_base + offset;
+
+    glGenRenderbuffers(
+        (GLsizei)n,
+        (GLuint *)buffers
+    );
+
+exit:
+    if (buffers_base) {
+        (*e)->ReleasePrimitiveArrayCritical(e, renderbuffers_ref, buffers_base,
+            _exception ? JNI_ABORT: 0);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glGenTextures(JNIEnv* e, jobject c, jint n, jintArray textures_ref, jint offset) {
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    GLuint *buffers_base = (GLuint *) 0;
+    jint _remaining;
+    GLuint *buffers = (GLuint *) 0;
+
+    if (!textures_ref) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "buffers == null";
+        goto exit;
+    }
+    if (offset < 0) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "offset < 0";
+        goto exit;
+    }
+    _remaining = (*e)->GetArrayLength(e, textures_ref) - offset;
+    if (_remaining < n) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "length - offset < n < needed";
+        goto exit;
+    }
+    buffers_base = (GLuint *)
+        (*e)->GetPrimitiveArrayCritical(e, textures_ref, (jboolean *)0);
+    buffers = buffers_base + offset;
+
+    glGenTextures(
+        (GLsizei)n,
+        (GLuint *)buffers
+    );
+
+exit:
+    if (buffers_base) {
+        (*e)->ReleasePrimitiveArrayCritical(e, textures_ref, buffers_base,
+            _exception ? JNI_ABORT: 0);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glGenerateMipmap(JNIEnv* e, jobject c, jint target) {
+    glGenerateMipmap(
+        (GLenum)target
+    );
+}
+
+JNIEXPORT jint JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glGetAttribLocation(JNIEnv* e, jobject c, jint program, jstring name) {
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    GLint _returnValue = 0;
+    const char* _nativename = 0;
+
+    if (!name) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "name == null";
+        goto exit;
+    }
+    _nativename = (*e)->GetStringUTFChars(e, name, 0);
+
+    _returnValue = glGetAttribLocation(
+        (GLuint)program,
+        (char *)_nativename
+    );
+
+exit:
+    if (_nativename) {
+        (*e)->ReleaseStringUTFChars(e, name, _nativename);
+    }
+
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+    return (jint)_returnValue;
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glGetBoolean(JNIEnv* e, jobject c, jint pname, jobject params_buf) {
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLvoid *params = (GLvoid *) 0;
+
+    if (params_buf) {
+        params = (GLvoid *)getPointer(e, params_buf, &_array, &_remaining, &_bufferOffset);
+    }
+    if (params_buf && params == NULL) {
+        char * _paramsBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        params = (GLvoid *) (_paramsBase + _bufferOffset);
+    }
+      
+    glGetBooleanv(
+        (GLenum) pname,
+        (GLboolean *) params
+    );
+}
+
+
+JNIEXPORT jint JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glGetError(JNIEnv* e, jobject c) {
+    GLenum _returnValue;
+    _returnValue = glGetError();
+    return (jint)_returnValue;
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glGetFramebufferAttachmentParameteriv(JNIEnv* e, jobject c, jint target, jint attachment, jint pname, jintArray params_ref, jint offset) {
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    GLint *params_base = (GLint *) 0;
+    jint _remaining;
+    GLint *params = (GLint *) 0;
+
+    if (!params_ref) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "params == null";
+        goto exit;
+    }
+    if (offset < 0) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "offset < 0";
+        goto exit;
+    }
+    _remaining = (*e)->GetArrayLength(e, params_ref) - offset;
+    params_base = (GLint *)
+        (*e)->GetPrimitiveArrayCritical(e, params_ref, (jboolean *)0);
+    params = params_base + offset;
+
+    glGetFramebufferAttachmentParameteriv(
+        (GLenum)target,
+        (GLenum)attachment,
+        (GLenum)pname,
+        (GLint *)params
+    );
+
+exit:
+    if (params_base) {
+        (*e)->ReleasePrimitiveArrayCritical(e, params_ref, params_base,
+            _exception ? JNI_ABORT: 0);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glGetIntegerv(JNIEnv* e, jobject c, jint pname, jintArray params_ref, jint offset) {
+    jint _exception = 0;
+    const char * _exceptionType;
+    const char * _exceptionMessage;
+    GLint *params_base = (GLint *) 0;
+    jint _remaining;
+    GLint *params = (GLint *) 0;
+    int _needed = 0;
+
+    if (!params_ref) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "params == null";
+        goto exit;
+    }
+    if (offset < 0) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "offset < 0";
+        goto exit;
+    }
+    _remaining = (*e)->GetArrayLength(e, params_ref) - offset;
+    _needed = getNeededCount(pname);
+    // if we didn't find this pname, we just assume the user passed
+    // an array of the right size -- this might happen with extensions
+    // or if we forget an enum here.
+    if (_remaining < _needed) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "length - offset < needed";
+        goto exit;
+    }
+    params_base = (GLint *)
+        (*e)->GetPrimitiveArrayCritical(e, params_ref, (jboolean *)0);
+    params = params_base + offset;
+
+    glGetIntegerv(
+        (GLenum)pname,
+        (GLint *)params
+    );
+
+exit:
+    if (params_base) {
+        (*e)->ReleasePrimitiveArrayCritical(e, params_ref, params_base,
+            _exception ? JNI_ABORT: 0);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT jstring JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glGetProgramInfoLog(JNIEnv* e, jobject c, jint program) {
+	GLsizei size = 0;
+	glGetProgramiv((GLuint)program, GL_INFO_LOG_LENGTH, &size);
+	
+	GLchar *infoLog;
+
+	if (!size) {
+		return  (*e)->NewStringUTF(e, "");
+	}
+	
+	infoLog = malloc(sizeof(GLchar) * size);
+    if (infoLog == NULL) {
+        jniThrowException(e, "java/lang/IllegalArgumentException", "out of memory");
+        return NULL;
+    }
+	
+	glGetProgramInfoLog((GLuint)program, size, NULL, infoLog);
+	jstring log = (*e)->NewStringUTF(e, infoLog);
+	free(infoLog);
+
+	return log; 
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glGetProgramiv(JNIEnv* e, jobject c, jint program, jint pname, jintArray params_ref, jint offset) {
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    GLint *params_base = (GLint *) 0;
+    jint _remaining;
+    GLint *params = (GLint *) 0;
+
+    if (!params_ref) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "params == null";
+        goto exit;
+    }
+    if (offset < 0) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "offset < 0";
+        goto exit;
+    }
+    _remaining = (*e)->GetArrayLength(e, params_ref) - offset;
+    if (_remaining < 1) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "length - offset < 1 < needed";
+        goto exit;
+    }
+    params_base = (GLint *)
+        (*e)->GetPrimitiveArrayCritical(e, params_ref, (jboolean *)0);
+    params = params_base + offset;
+
+    glGetProgramiv(
+        (GLuint)program,
+        (GLenum)pname,
+        (GLint *)params
+    );
+
+exit:
+    if (params_base) {
+        (*e)->ReleasePrimitiveArrayCritical(e, params_ref, params_base,
+            _exception ? JNI_ABORT: 0);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT jstring JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glGetShaderInfoLog(JNIEnv* e, jobject c, jint shader) {
+	GLsizei size = 0;
+	glGetShaderiv((GLuint)shader, GL_INFO_LOG_LENGTH, &size);
+	
+	GLchar *infoLog;
+
+	if (!size) {
+		return  (*e)->NewStringUTF(e, "");
+	}
+	
+	infoLog = malloc(sizeof(GLchar) * size);
+    if (infoLog == NULL) {
+        jniThrowException(e, "java/lang/IllegalArgumentException", "out of memory");
+        return NULL;
+    }
+	
+	glGetShaderInfoLog((GLuint)shader, size, NULL, infoLog);
+	jstring log = (*e)->NewStringUTF(e, infoLog);
+	free(infoLog);
+
+	return log; 
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glGetShaderiv(JNIEnv* e, jobject c, jint shader, jint pname, jintArray params_ref, jint offset) {
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    GLint *params_base = (GLint *) 0;
+    jint _remaining;
+    GLint *params = (GLint *) 0;
+
+    if (!params_ref) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "params == null";
+        goto exit;
+    }
+    if (offset < 0) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "offset < 0";
+        goto exit;
+    }
+    _remaining = (*e)->GetArrayLength(e, params_ref) - offset;
+    if (_remaining < 1) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "length - offset < 1 < needed";
+        goto exit;
+    }
+    params_base = (GLint *)
+        (*e)->GetPrimitiveArrayCritical(e, params_ref, (jboolean *)0);
+    params = params_base + offset;
+
+    glGetShaderiv(
+        (GLuint)shader,
+        (GLenum)pname,
+        (GLint *)params
+    );
+
+exit:
+    if (params_base) {
+        (*e)->ReleasePrimitiveArrayCritical(e, params_ref, params_base,
+            _exception ? JNI_ABORT: 0);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT jstring JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glGetString(JNIEnv* e, jobject c, jint name) {
+	const GLubyte* value = glGetString((GLenum) name);
+
+	return (*e)->NewStringUTF(e, (const char*)value); 
+}
+
+JNIEXPORT jint JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glGetUniformLocation(JNIEnv* e, jobject c, jint program, jstring name) {
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    GLint _returnValue = 0;
+    const char* _nativename = 0;
+
+    if (!name) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "name == null";
+        goto exit;
+    }
+    _nativename = (*e)->GetStringUTFChars(e, name, 0);
+
+    _returnValue = glGetUniformLocation(
+        (GLuint)program,
+        (char *)_nativename
+    );
+
+exit:
+    if (_nativename) {
+        (*e)->ReleaseStringUTFChars(e, name, _nativename);
+    }
+
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+    return (jint)_returnValue;
+}
+
+JNIEXPORT jboolean JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glIsEnabled(JNIEnv* e, jobject c, jint cap) {
+    GLboolean _returnValue;
+    _returnValue = glIsEnabled(
+        (GLenum)cap
+    );
+    return (jboolean)_returnValue;
+}
+
+JNIEXPORT jboolean JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glIsFramebuffer(JNIEnv* e, jobject c, jint framebuffer) {
+    GLboolean _returnValue;
+    _returnValue = glIsFramebuffer(
+        (GLuint)framebuffer
+    );
+    return (jboolean)_returnValue;
+}
+
+JNIEXPORT jboolean JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glIsRenderbuffer(JNIEnv* e, jobject c, jint renderbuffer) {
+    GLboolean _returnValue;
+    _returnValue = glIsRenderbuffer(
+        (GLuint)renderbuffer
+    );
+    return (jboolean)_returnValue;
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glLineWidth(JNIEnv* e, jobject c, jfloat width) {
+    glLineWidth(
+        (GLfloat)width
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glLinkProgram(JNIEnv* e, jobject c, jint program) {
+    glLinkProgram(
+        (GLuint)program
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glPixelStorei(JNIEnv* e, jobject c, jint pname, jint param) {
+    glPixelStorei(
+        (GLenum)pname,
+        (GLint)param
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glPolygonOffset(JNIEnv* e, jobject c, jfloat factor, jfloat units) {
+    glPolygonOffset(
+        (GLfloat)factor,
+        (GLfloat)units
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glReadPixels(JNIEnv* e, jobject c, jint vpX, jint vpY, jint vpW, jint vpH, jint format, jint type, jobject pixels_buf) {
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLvoid *pixels = (GLvoid *) 0;
+
+    pixels = (GLvoid *)getPointer(e, pixels_buf, &_array, &_remaining, &_bufferOffset);
+    if (pixels == NULL) {
+        char * _pixelsBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        pixels = (GLvoid *) (_pixelsBase + _bufferOffset);
+    }
+    glReadPixels(
+        (GLint)vpX,
+        (GLint)vpY,
+        (GLsizei)vpW,
+        (GLsizei)vpH,
+        (GLenum)format,
+        (GLenum)type,
+        (GLvoid *)pixels
+    );
+    if (_array) {
+        releasePointer(e, _array, pixels, JNI_TRUE);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glReadPixels2(JNIEnv* e, jobject c, jint vpX, jint vpY, jint vpW, jint vpH, jint format, jint type, jintArray pixels, jint offset, jint size) {
+	GLint* bufferNative = malloc(size);
+	
+    glReadPixels(
+        (GLint)vpX,
+        (GLint)vpY,
+        (GLsizei)vpW,
+        (GLsizei)vpH,
+        (GLenum)format,
+        (GLenum)type,
+        (GLvoid *)bufferNative
+    );
+	
+	(*e)->SetIntArrayRegion(e, pixels, offset, size, bufferNative);
+	
+	free(bufferNative);
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glRenderbufferStorage(JNIEnv* e, jobject c, jint target, jint internalformat, jint width, jint height) {
+    glRenderbufferStorage(
+        (GLenum)target,
+        (GLenum)internalformat,
+        (GLsizei)width,
+        (GLsizei)height
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glScissor(JNIEnv* e, jobject c, jint x, jint y, jint width, jint height) {
+    glScissor(
+        (GLint)x,
+        (GLint)y,
+        (GLsizei)width,
+        (GLsizei)height
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glShaderSource(JNIEnv* e, jobject c, jint shader, jstring string) {
+	const char *stringNative = (*e)->GetStringUTFChars(e, string, NULL);
+	glShaderSource(shader, 1, &stringNative, NULL);
+	//jsize stringLen = (*e)->GetStringUTFLength(e, string);
+	//const char** code = { stringNative };
+	//const GLint* length = { stringLen };
+	
+	printf("upload shader source: %s", stringNative);
+
+	//glShaderSource(shader, 1, code, length);
+	
+	(*e)->ReleaseStringUTFChars(e, string, stringNative);
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glStencilFuncSeparate(JNIEnv* e, jobject c, jint face, jint func, jint ref, jint mask) {
+    glStencilFuncSeparate(
+        (GLenum) face,
+        (GLenum) func,
+        (GLint) ref,
+        (GLuint) mask
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glStencilOpSeparate(JNIEnv* e, jobject c, jint face, jint sfail, jint dpfail, jint dppass) {
+    glStencilOpSeparate(
+        (GLenum) face,
+        (GLenum) sfail,
+        (GLenum) dpfail,
+        (GLenum) dppass
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glTexImage2D(JNIEnv* e, jobject c, jint target, jint level, jint internalformat, jint width, jint height, jint border, jint format, jint type, jobject pixels_buf) {
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLvoid *pixels = (GLvoid *) 0;
+
+    if (pixels_buf) {
+        pixels = (GLvoid *)getPointer(e, pixels_buf, &_array, &_remaining, &_bufferOffset);
+    }
+    if (pixels_buf && pixels == NULL) {
+        char * _pixelsBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        pixels = (GLvoid *) (_pixelsBase + _bufferOffset);
+    }
+    glTexImage2D(
+        (GLenum)target,
+        (GLint)level,
+        (GLint)internalformat,
+        (GLsizei)width,
+        (GLsizei)height,
+        (GLint)border,
+        (GLenum)format,
+        (GLenum)type,
+        (GLvoid *)pixels
+    );
+    if (_array) {
+        releasePointer(e, _array, pixels, JNI_FALSE);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glTexParameteri(JNIEnv* e, jobject c, jint target, jint pname, jint param) {
+    glTexParameteri(
+        (GLenum)target,
+        (GLenum)pname,
+        (GLint)param
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glTexParameterf(JNIEnv* e, jobject c, jint target, jint pname, jfloat param) {
+    glTexParameterf(
+        (GLenum)target,
+        (GLenum)pname,
+        (GLfloat)param
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glTexSubImage2D(JNIEnv* e, jobject c, jint target, jint level, jint xoffset, jint yoffset, jint width, jint height, jint format, jint type, jobject pixels_buf) {
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLvoid *pixels = (GLvoid *) 0;
+
+    if (pixels_buf) {
+        pixels = (GLvoid *)getPointer(e, pixels_buf, &_array, &_remaining, &_bufferOffset);
+    }
+    if (pixels_buf && pixels == NULL) {
+        char * _pixelsBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        pixels = (GLvoid *) (_pixelsBase + _bufferOffset);
+    }
+    glTexSubImage2D(
+        (GLenum)target,
+        (GLint)level,
+        (GLint)xoffset,
+        (GLint)yoffset,
+        (GLsizei)width,
+        (GLsizei)height,
+        (GLenum)format,
+        (GLenum)type,
+        (GLvoid *)pixels
+    );
+    if (_array) {
+        releasePointer(e, _array, pixels, JNI_FALSE);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glUniform1f(JNIEnv* e, jobject c, jint location, jfloat x) {
+    glUniform1f(
+        (GLint)location,
+        (GLfloat)x
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glUniform1fv(JNIEnv* e, jobject c, jint location, jint count, jobject v_buf) {
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLfloat *v = (GLfloat *) 0;
+
+    v = (GLfloat *)getPointer(e, v_buf, &_array, &_remaining, &_bufferOffset);
+    if (_remaining < count) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "remaining() < count < needed";
+        goto exit;
+    }
+    if (v == NULL) {
+        char * _vBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        v = (GLfloat *) (_vBase + _bufferOffset);
+    }
+    glUniform1fv(
+        (GLint)location,
+        (GLsizei)count,
+        (GLfloat *)v
+    );
+
+exit:
+    if (_array) {
+        releasePointer(e, _array, v, JNI_FALSE);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glUniform1fv2(JNIEnv* e, jobject c, jint location, jint count, jfloatArray v, jint offset) {
+	jfloat *vNative = (*e)->GetFloatArrayElements(e, v, NULL);
+	
+    glUniform1fv(
+        (GLint)location,
+        (GLsizei)count,
+        (GLfloat *)(vNative + offset * sizeof(GLfloat))
+    );
+	
+	(*e)->ReleaseFloatArrayElements(e, v, vNative, 0);
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glUniform1i(JNIEnv* e, jobject c, jint location, jint x) {
+    glUniform1i(
+        (GLint)location,
+        (GLint)x
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glUniform1iv(JNIEnv* e, jobject c, jint location, jint count, jobject v_buf) {
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLint *v = (GLint *) 0;
+
+    v = (GLint *)getPointer(e, v_buf, &_array, &_remaining, &_bufferOffset);
+    if (_remaining < count) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "remaining() < count < needed";
+        goto exit;
+    }
+    if (v == NULL) {
+        char * _vBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        v = (GLint *) (_vBase + _bufferOffset);
+    }
+    glUniform1iv(
+        (GLint)location,
+        (GLsizei)count,
+        (GLint *)v
+    );
+
+exit:
+    if (_array) {
+        releasePointer(e, _array, v, JNI_FALSE);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glUniform1iv2(JNIEnv* e, jobject c, jint location, jint count, jintArray v, jint offset) {
+	jint *vNative = (*e)->GetIntArrayElements(e, v, NULL);
+	
+    glUniform1iv(
+        (GLint)location,
+        (GLsizei)count,
+        (GLint *)(vNative + offset * sizeof(GLint))
+    );
+	glUniform1iv(location, count, vNative + offset);
+	
+	(*e)->ReleaseIntArrayElements(e, v, vNative, 0);
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glUniform2f(JNIEnv* e, jobject c, jint location, jfloat x, jfloat y) {
+    glUniform2f(
+        (GLint)location,
+        (GLfloat)x,
+        (GLfloat)y
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glUniform2fv(JNIEnv* e, jobject c, jint location, jint count, jobject v_buf) {
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLfloat *v = (GLfloat *) 0;
+
+    v = (GLfloat *)getPointer(e, v_buf, &_array, &_remaining, &_bufferOffset);
+    if (_remaining < count*2) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "remaining() < count*2 < needed";
+        goto exit;
+    }
+    if (v == NULL) {
+        char * _vBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        v = (GLfloat *) (_vBase + _bufferOffset);
+    }
+    glUniform2fv(
+        (GLint)location,
+        (GLsizei)count,
+        (GLfloat *)v
+    );
+
+exit:
+    if (_array) {
+        releasePointer(e, _array, v, JNI_FALSE);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glUniform2fv2(JNIEnv* e, jobject c, jint location, jint count, jfloatArray v, jint offset) {
+	jfloat *vNative = (*e)->GetFloatArrayElements(e, v, NULL);
+	
+    glUniform2fv(
+        (GLint)location,
+        (GLsizei)count,
+        (GLfloat *)(vNative + offset * sizeof(GLfloat))
+    );
+	
+	(*e)->ReleaseFloatArrayElements(e, v, vNative, 0);
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glUniform3f(JNIEnv* e, jobject c, jint location, jfloat x, jfloat y, jfloat z) {
+    glUniform3f(
+        (GLint)location,
+        (GLfloat)x,
+        (GLfloat)y,
+        (GLfloat)z
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glUniform3fv(JNIEnv* e, jobject c, jint location, jint count, jobject v_buf) {
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLfloat *v = (GLfloat *) 0;
+
+    v = (GLfloat *)getPointer(e, v_buf, &_array, &_remaining, &_bufferOffset);
+    if (_remaining < count * 3) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "remaining() < count*3 < needed";
+        goto exit;
+    }
+    if (v == NULL) {
+        char * _vBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        v = (GLfloat *) (_vBase + _bufferOffset);
+    }
+    glUniform3fv(
+        (GLint)location,
+        (GLsizei)count,
+        (GLfloat *)v
+    );
+
+exit:
+    if (_array) {
+        releasePointer(e, _array, v, JNI_FALSE);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glUniform3fv2(JNIEnv* e, jobject c, jint location, jint count, jfloatArray v, jint offset) {
+	jfloat *vNative = (*e)->GetFloatArrayElements(e, v, NULL);
+	
+    glUniform3fv(
+        (GLint)location,
+        (GLsizei)count,
+        (GLfloat *)(vNative + offset * sizeof(GLfloat))
+    );
+	
+	(*e)->ReleaseFloatArrayElements(e, v, vNative, 0);
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glUniform4f(JNIEnv* e, jobject c, jint location, jfloat x, jfloat y, jfloat z, jfloat w) {
+    glUniform4f(
+        (GLint)location,
+        (GLfloat)x,
+        (GLfloat)y,
+        (GLfloat)z,
+        (GLfloat)w
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glUniform4fv(JNIEnv* e, jobject c, jint location, jint count, jobject v_buf) {
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLfloat *v = (GLfloat *) 0;
+
+    v = (GLfloat *)getPointer(e, v_buf, &_array, &_remaining, &_bufferOffset);
+    if (_remaining < count * 4) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "remaining() < count*4 < needed";
+        goto exit;
+    }
+    if (v == NULL) {
+        char * _vBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        v = (GLfloat *) (_vBase + _bufferOffset);
+    }
+    glUniform4fv(
+        (GLint)location,
+        (GLsizei)count,
+        (GLfloat *)v
+    );
+
+exit:
+    if (_array) {
+        releasePointer(e, _array, v, JNI_FALSE);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glUniform4fv2(JNIEnv* e, jobject c, jint location, jint count, jfloatArray v, jint offset) {
+	jfloat *vNative = (*e)->GetFloatArrayElements(e, v, NULL);
+	
+    glUniform4fv(
+        (GLint)location,
+        (GLsizei)count,
+        (GLfloat *)(vNative + offset * sizeof(GLfloat))
+    );
+	
+	(*e)->ReleaseFloatArrayElements(e, v, vNative, 0);
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glUniformMatrix3fv(JNIEnv* e, jobject c, jint location, jint count, jboolean transpose, jobject value_buf) {
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLfloat *value = (GLfloat *) 0;
+
+    value = (GLfloat *)getPointer(e, value_buf, &_array, &_remaining, &_bufferOffset);
+    if (_remaining < count*9) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "remaining() < count*9 < needed";
+        goto exit;
+    }
+    if (value == NULL) {
+        char * _valueBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        value = (GLfloat *) (_valueBase + _bufferOffset);
+    }
+    glUniformMatrix3fv(
+        (GLint)location,
+        (GLsizei)count,
+        (GLboolean)transpose,
+        (GLfloat *)value
+    );
+
+exit:
+    if (_array) {
+        releasePointer(e, _array, value, JNI_FALSE);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glUniformMatrix3fv2(JNIEnv* e, jobject c, jint location, jint count, jboolean transpose, jfloatArray value, jint offset) {
+	jfloat *vNative = (*e)->GetFloatArrayElements(e, value, NULL);
+	
+    glUniformMatrix3fv(
+        (GLint)location,
+        (GLsizei)count,
+        (GLboolean)transpose,
+        (GLfloat *)(vNative + offset * sizeof(GLfloat))
+    );
+	
+	(*e)->ReleaseFloatArrayElements(e, value, vNative, 0);
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glUniformMatrix4fv(JNIEnv* e, jobject c, jint location, jint count, jboolean transpose, jobject value_buf) {
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLfloat *value = (GLfloat *) 0;
+
+    value = (GLfloat *)getPointer(e, value_buf, &_array, &_remaining, &_bufferOffset);
+    if (_remaining < count * 16) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "remaining() < count*16 < needed";
+        goto exit;
+    }
+    if (value == NULL) {
+        char * _valueBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        value = (GLfloat *) (_valueBase + _bufferOffset);
+    }
+    glUniformMatrix4fv(
+        (GLint)location,
+        (GLsizei)count,
+        (GLboolean)transpose,
+        (GLfloat *)value
+    );
+
+exit:
+    if (_array) {
+        releasePointer(e, _array, value, JNI_FALSE);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glUniformMatrix4fv2(JNIEnv* e, jobject c, jint location, jint count, jboolean transpose, jfloatArray value, jint offset) {
+	jfloat *vNative = (*e)->GetFloatArrayElements(e, value, NULL);
+	
+    glUniformMatrix4fv(
+        (GLint)location,
+        (GLsizei)count,
+        (GLboolean)transpose,
+        (GLfloat *)(vNative + offset * sizeof(GLfloat))
+    );
+	
+	(*e)->ReleaseFloatArrayElements(e, value, vNative, 0);
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glUseProgram(JNIEnv* e, jobject c, jint program) {
+    glUseProgram(
+        (GLuint)program
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glVertexAttribPointer(JNIEnv* e, jobject c, jint indx, jint size, jint type, jboolean normalized, jint stride, jobject buffer) {
+    GLvoid *ptr = (GLvoid *) 0;
+
+    if (buffer) {
+        ptr = (GLvoid *) getDirectBufferPointer(e, buffer);
+        if (!ptr) {
+            return;
+        }
+    }
+    glVertexAttribPointer(
+        (GLuint)indx,
+        (GLint)size,
+        (GLenum)type,
+        (GLboolean)normalized,
+        (GLsizei)stride,
+        (GLvoid *)ptr
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glVertexAttribPointer2(JNIEnv* e, jclass c, jint indx, jint size, jint type, jboolean normalized, jint stride, jint offset) {
+	
+    glVertexAttribPointer(
+        (GLuint)indx,
+        (GLint)size,
+        (GLenum)type,
+        (GLboolean)normalized,
+        (GLsizei)stride,
+        (GLvoid *)(offset)
+    );
+}
+
+JNIEXPORT void JNICALL
+Java_com_jme3_renderer_ios_JmeIosGLES_glViewport(JNIEnv* e, jobject c, jint x, jint y, jint width, jint height) {
+    glViewport(
+        (GLint)x,
+        (GLint)y,
+        (GLsizei)width,
+        (GLsizei)height
+    );
+}
+
+JNIEXPORT void JNICALL 
+Java_com_jme3_renderer_ios_JmeIosGLES_glBeginQuery(JNIEnv* e, jobject c, jint target, jint query) {
+    glBeginQuery(
+        (GLint) target,
+        (GLint) query
+    );
+}
+
+JNIEXPORT void JNICALL 
+Java_com_jme3_renderer_ios_JmeIosGLES_glEndQuery(JNIEnv* e, jobject c, jint target)
+{
+    glEndQuery((GLint)target);
+}
+
+JNIEXPORT void JNICALL 
+Java_com_jme3_renderer_ios_JmeIosGLES_glGenQueries(JNIEnv* e, jobject c, jint count, jobject v_buf)
+{
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLint *v = (GLint *) 0;
+
+    v = (GLint *)getPointer(e, v_buf, &_array, &_remaining, &_bufferOffset);
+    if (_remaining < count) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "remaining() < count < needed";
+        goto exit;
+    }
+    if (v == NULL) {
+        char * _vBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        v = (GLint *) (_vBase + _bufferOffset);
+    }
+    glGenQueries(
+        (GLsizei)count,
+        (GLint *)v
+    );
+
+exit:
+    if (_array) {
+        releasePointer(e, _array, v, JNI_FALSE);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT void JNICALL 
+Java_com_jme3_renderer_ios_JmeIosGLES_glGetQueryObjectuiv(JNIEnv* e, jobject c, jint query, jint pname, jintArray params_ref)
+{
+    jint _exception = 0;
+    const char * _exceptionType;
+    const char * _exceptionMessage;
+    GLint *params_base = (GLint *) 0;
+    jint _remaining;
+    GLint *params = (GLint *) 0;
+    int _needed = 0;
+
+    if (!params_ref) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "params == null";
+        goto exit;
+    }
+
+    _remaining = (*e)->GetArrayLength(e, params_ref);
+    _needed = getNeededCount(pname);
+    // if we didn't find this pname, we just assume the user passed
+    // an array of the right size -- this might happen with extensions
+    // or if we forget an enum here.
+    if (_remaining < _needed) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "length < needed";
+        goto exit;
+    }
+    params_base = (GLint *)
+        (*e)->GetPrimitiveArrayCritical(e, params_ref, (jboolean *)0);
+    params = params_base;
+
+    glGetQueryObjectuiv(
+        (GLint)query,
+        (GLenum)pname,
+        (GLint *)params
+    );
+
+exit:
+    if (params_base) {
+        (*e)->ReleasePrimitiveArrayCritical(e, params_ref, params_base,
+            _exception ? JNI_ABORT: 0);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT void JNICALL 
+Java_com_jme3_renderer_ios_JmeIosGLES_glGetQueryiv(JNIEnv* e, jobject c, jint target, jint pname, jintArray params_ref)
+{
+    jint _exception = 0;
+    const char * _exceptionType;
+    const char * _exceptionMessage;
+    GLint *params_base = (GLint *) 0;
+    jint _remaining;
+    GLint *params = (GLint *) 0;
+    int _needed = 0;
+
+    if (!params_ref) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "params == null";
+        goto exit;
+    }
+
+    _remaining = (*e)->GetArrayLength(e, params_ref);
+    _needed = getNeededCount(pname);
+    // if we didn't find this pname, we just assume the user passed
+    // an array of the right size -- this might happen with extensions
+    // or if we forget an enum here.
+    if (_remaining < _needed) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "length < needed";
+        goto exit;
+    }
+    params_base = (GLint *)
+        (*e)->GetPrimitiveArrayCritical(e, params_ref, (jboolean *)0);
+    params = params_base;
+
+    glGetQueryiv(
+        (GLenum)target,
+        (GLenum)pname,
+        (GLint *)params
+    );
+
+exit:
+    if (params_base) {
+        (*e)->ReleasePrimitiveArrayCritical(e, params_ref, params_base,
+            _exception ? JNI_ABORT: 0);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT void JNICALL 
+Java_com_jme3_renderer_ios_JmeIosGLES_glBlitFramebuffer(JNIEnv* e, jobject c, jint srcX0, jint srcY0, jint srcX1, jint srcY1, jint dstX0, jint dstY0, jint dstX1, jint dstY1, jint mask, jint filter)
+{
+    glBlitFramebuffer( 	
+        (GLint) srcX0,
+        (GLint) srcY0,
+        (GLint) srcX1,
+        (GLint) srcY1,
+        (GLint) dstX0,
+        (GLint) dstY0,
+        (GLint) dstX1,
+        (GLint) dstY1,
+        (GLbitfield) mask,
+        (GLenum) filter
+    );
+}
+
+JNIEXPORT void JNICALL 
+Java_com_jme3_renderer_ios_JmeIosGLES_glDrawArraysInstanced(JNIEnv* e, jobject c, jint mode, jint first, jint count, jint primcount)
+{
+    glDrawArraysInstanced(
+        (GLenum) mode,
+        (GLint) first,
+        (GLsizei) count,
+        (GLsizei) primcount
+    );
+}
+
+JNIEXPORT void JNICALL 
+Java_com_jme3_renderer_ios_JmeIosGLES_glDrawBuffers(JNIEnv* e, jobject c, jint count, jobject v_buf)
+{
+    jint _exception = 0;
+    const char * _exceptionType = NULL;
+    const char * _exceptionMessage = NULL;
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLint *v = (GLint *) 0;
+
+    v = (GLint *)getPointer(e, v_buf, &_array, &_remaining, &_bufferOffset);
+    if (_remaining < count) {
+        _exception = 1;
+        _exceptionType = "java/lang/IllegalArgumentException";
+        _exceptionMessage = "remaining() < count < needed";
+        goto exit;
+    }
+    if (v == NULL) {
+        char * _vBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        v = (GLint *) (_vBase + _bufferOffset);
+    }
+    glDrawBuffers(
+        (GLsizei)count,
+        (GLint *)v
+    );
+
+exit:
+    if (_array) {
+        releasePointer(e, _array, v, JNI_FALSE);
+    }
+    if (_exception) {
+        jniThrowException(e, _exceptionType, _exceptionMessage);
+    }
+}
+
+JNIEXPORT void JNICALL 
+Java_com_jme3_renderer_ios_JmeIosGLES_glDrawElementsInstanced(JNIEnv* e, jobject c, jint mode, jint count, jint type, jlong indices, jint primcount)
+{
+    glDrawElementsInstanced(
+        (GLenum) mode,
+        (GLsizei) count,
+        (GLenum) type,
+        (const void *) indices,
+        (GLsizei) primcount
+    );
+}
+
+JNIEXPORT void JNICALL 
+Java_com_jme3_renderer_ios_JmeIosGLES_glVertexAttribDivisor(JNIEnv* e, jobject c, jint index, jint divisor)
+{
+    glVertexAttribDivisor(
+        (GLint) index,
+        (GLint) divisor
+    );
+}
+
+JNIEXPORT void JNICALL 
+Java_com_jme3_renderer_ios_JmeIosGLES_glFramebufferTextureLayer(JNIEnv* e, jobject c, jint target, jint attachment, jint texture, jint level, jint layer)
+{
+    glFramebufferTextureLayer(
+        (GLenum) target,
+        (GLenum) attachment,
+        (GLuint) texture,
+        (GLint) level,
+        (GLint) layer
+    );
+}
+
+JNIEXPORT void JNICALL 
+Java_com_jme3_renderer_ios_JmeIosGLES_glReadBuffer(JNIEnv* e, jobject c, jint src)
+{
+    glReadBuffer((GLenum) src);
+}
+
+JNIEXPORT void JNICALL 
+Java_com_jme3_renderer_ios_JmeIosGLES_glCompressedTexImage3D(JNIEnv* e, jobject c, jint target, jint level, jint internalFormat, jint width, jint height, jint depth, jint border, jint imageSize, jobject pixels_buf)
+{
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLvoid *pixels = (GLvoid *) 0;
+
+    if (pixels_buf) {
+        pixels = (GLvoid *)getPointer(e, pixels_buf, &_array, &_remaining, &_bufferOffset);
+    }
+    if (pixels_buf && pixels == NULL) {
+        char * _pixelsBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        pixels = (GLvoid *) (_pixelsBase + _bufferOffset);
+    }
+        
+    glCompressedTexImage3D(
+        (GLenum) target,
+        (GLint) level,
+        (GLenum) internalFormat,
+        (GLsizei) width,
+        (GLsizei) height,
+        (GLsizei) depth,
+        (GLint) border,
+        (GLsizei) imageSize,
+        (GLvoid *)pixels
+    );
+
+    if (_array) {
+        releasePointer(e, _array, pixels, JNI_FALSE);
+    }
+}
+
+JNIEXPORT void JNICALL 
+Java_com_jme3_renderer_ios_JmeIosGLES_glCompressedTexSubImage3D(JNIEnv* e, jobject c, jint target, jint level, jint xoffset, jint yoffset, jint zoffset, jint width, jint height, jint depth, jint format, jint imageSize, jobject pixels_buf)
+{
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLvoid *pixels = (GLvoid *) 0;
+
+    if (pixels_buf) {
+        pixels = (GLvoid *)getPointer(e, pixels_buf, &_array, &_remaining, &_bufferOffset);
+    }
+    if (pixels_buf && pixels == NULL) {
+        char * _pixelsBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        pixels = (GLvoid *) (_pixelsBase + _bufferOffset);
+    }
+        
+    glCompressedTexSubImage3D(
+        (GLenum) target,
+        (GLint) level,
+        (GLint) xoffset,
+        (GLint) yoffset,
+        (GLint) zoffset,
+        (GLsizei) width,
+        (GLsizei) height,
+        (GLsizei) depth,
+        (GLenum) format,
+        (GLsizei) imageSize,
+        (GLvoid *)pixels
+    );
+
+    if (_array) {
+        releasePointer(e, _array, pixels, JNI_FALSE);
+    }
+}
+
+JNIEXPORT void JNICALL 
+Java_com_jme3_renderer_ios_JmeIosGLES_glTexImage3D(JNIEnv* e, jobject c, jint target, jint level, jint internalFormat, jint width, jint height, jint depth, jint border, jint format, jint type, jobject pixels_buf)
+{
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLvoid *pixels = (GLvoid *) 0;
+
+    if (pixels_buf) {
+        pixels = (GLvoid *)getPointer(e, pixels_buf, &_array, &_remaining, &_bufferOffset);
+    }
+    if (pixels_buf && pixels == NULL) {
+        char * _pixelsBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        pixels = (GLvoid *) (_pixelsBase + _bufferOffset);
+    }
+        
+    glTexImage3D(
+        (GLenum) target,
+        (GLint) level,
+        (GLint) internalFormat,
+        (GLsizei) width,
+        (GLsizei) height,
+        (GLsizei) depth,
+        (GLint) border,
+        (GLenum) format,
+        (GLenum) type,
+        (GLvoid *)pixels
+    );
+
+    if (_array) {
+        releasePointer(e, _array, pixels, JNI_FALSE);
+    }
+}
+
+JNIEXPORT void JNICALL 
+Java_com_jme3_renderer_ios_JmeIosGLES_glTexSubImage3D(JNIEnv* e, jobject c, jint target, jint level, jint xoffset, jint yoffset, jint zoffset, jint width, jint height, jint depth, jint format, jint type, jobject pixels_buf)
+{
+    jarray _array = (jarray) 0;
+    jint _bufferOffset = (jint) 0;
+    jint _remaining;
+    GLvoid *pixels = (GLvoid *) 0;
+
+    if (pixels_buf) {
+        pixels = (GLvoid *)getPointer(e, pixels_buf, &_array, &_remaining, &_bufferOffset);
+    }
+    if (pixels_buf && pixels == NULL) {
+        char * _pixelsBase = (char *)(*e)->GetPrimitiveArrayCritical(e, _array, (jboolean *) 0);
+        pixels = (GLvoid *) (_pixelsBase + _bufferOffset);
+    }
+        
+    glTexSubImage3D(
+        (GLenum) target,
+        (GLint) level,
+        (GLint) xoffset,
+        (GLint) yoffset,
+        (GLint) zoffset,
+        (GLsizei) width,
+        (GLsizei) height,
+        (GLsizei) depth,
+        (GLenum) format,
+        (GLenum) type,
+        (GLvoid *)pixels
+    );
+
+    if (_array) {
+        releasePointer(e, _array, pixels, JNI_FALSE);
+    }
+}
+
+
+static int
+allowIndirectBuffers(JNIEnv *e) {
+    return 0;
+}
+
+static void *
+getDirectBufferPointer(JNIEnv *e, jobject buffer) {
+    if (!buffer) {
+        return NULL;
+    }
+    
+    if (!initialized) {
+    	nativeClassInit(e);
+    }
+
+    void* buf = (*e)->GetDirectBufferAddress(e, buffer);
+    if (buf) {
+        jint position = (*e)->GetIntField(e, buffer, positionID);
+        jint elementSizeShift = getBufferElementSize(e, buffer);
+        buf = ((char*) buf) + (position << elementSizeShift);
+    } else {
+            jniThrowException(e, "java/lang/IllegalArgumentException",
+                              "Must use a native order direct Buffer");
+    }
+    return buf;
+}
+
+static void *
+getPointer(JNIEnv *e, jobject buffer, jarray *array, jint *remaining, jint *offset) {
+    jint position;
+    jint limit;
+    jint elementSizeShift;
+    jlong pointer;
+    
+    if (!buffer) {
+        return NULL;
+    }
+    
+    if (!initialized) {
+    	nativeClassInit(e);
+    }
+
+    position = (*e)->GetIntField(e, buffer, positionID);
+    limit = (*e)->GetIntField(e, buffer, limitID);
+    elementSizeShift = getBufferElementSize(e, buffer);
+    
+    array = (void*) NULL;
+    *remaining = (limit - position) << elementSizeShift; 
+    *offset = position;
+    
+    return getDirectBufferPointer(e, buffer);
+}
+
+
+static void
+nativeClassInit(JNIEnv *e) {
+    if (!byteBufferClass) {
+    	jclass byteBufferClassLocal = (*e)->FindClass(e, "java/nio/ByteBuffer");
+    	byteBufferClass = (jclass) (*e)->NewGlobalRef(e, byteBufferClassLocal);
+    }
+    
+    if (!shortBufferClass) {
+    	jclass shortBufferClassLocal = (*e)->FindClass(e, "java/nio/ShortBuffer");
+    	shortBufferClass = (jclass) (*e)->NewGlobalRef(e, shortBufferClassLocal);
+    }
+    
+    if (!intBufferClass) {
+    	jclass intBufferClassLocal = (*e)->FindClass(e, "java/nio/IntBuffer");
+    	intBufferClass = (jclass) (*e)->NewGlobalRef(e, intBufferClassLocal);
+    }
+    
+    if (!floatBufferClass) {
+    	jclass floatBufferClassLocal = (*e)->FindClass(e, "java/nio/FloatBuffer");
+    	floatBufferClass = (jclass) (*e)->NewGlobalRef(e, floatBufferClassLocal);
+    }
+    
+    if (!bufferClass) {
+        jclass bufferClassLocal = (*e)->FindClass(e, "java/nio/Buffer");
+        bufferClass = (jclass) (*e)->NewGlobalRef(e, bufferClassLocal);
+    }
+
+    if (!positionID && bufferClass) {
+	    positionID = (*e)->GetFieldID(e, bufferClass, "position", "I");
+	}
+
+    if (!limitID && bufferClass) {
+	    limitID = (*e)->GetFieldID(e, bufferClass, "limit", "I");
+	}
+
+	initialized = floatBufferClass && bufferClass && shortBufferClass && byteBufferClass
+			&& intBufferClass && positionID && limitID;
+			
+	printf("Initializion of java.nio.Buffer access functionality %s\n", initialized ? "succeeded" : "failed");
+}
+
+static void
+releasePointer(JNIEnv *e, jarray array, void *data, jboolean commit) {
+    (*e)->ReleasePrimitiveArrayCritical(e, array, data,
+					   commit ? 0 : JNI_ABORT);
+}
+
+static void
+jniThrowException(JNIEnv *e, const char* type, const char* message) {
+	jclass excCls = (*e)->FindClass(e, type);
+	if (excCls != 0) {
+    	(*e)->ThrowNew(e, excCls, message);
+    }
+}
+
+static jint
+getBufferElementSize(JNIEnv *e, jobject buffer) {
+    if (!buffer) {
+        return 0;
+    }
+
+	if ((*e)->IsInstanceOf(e, buffer, floatBufferClass) == JNI_TRUE) {
+		return 2;
+	} else if ((*e)->IsInstanceOf(e, buffer, intBufferClass) == JNI_TRUE) {
+		return 2;
+	} else if ((*e)->IsInstanceOf(e, buffer, shortBufferClass) == JNI_TRUE) {
+		return 1;
+	}
+	
+	//TODO: check other buffer types
+	return 0;
+}
+
+static int getNeededCount(GLint pname) {
+    int needed = 1;
+#ifdef GL_ES_VERSION_2_0
+    // GLES 2.x pnames
+    switch (pname) {
+        case GL_ALIASED_LINE_WIDTH_RANGE:
+        case GL_ALIASED_POINT_SIZE_RANGE:
+            needed = 2;
+            break;
+
+        case GL_BLEND_COLOR:
+        case GL_COLOR_CLEAR_VALUE:
+        case GL_COLOR_WRITEMASK:
+        case GL_SCISSOR_BOX:
+        case GL_VIEWPORT:
+            needed = 4;
+            break;
+
+        case GL_COMPRESSED_TEXTURE_FORMATS:
+            glGetIntegerv(GL_NUM_COMPRESSED_TEXTURE_FORMATS, &needed);
+            break;
+
+        case GL_SHADER_BINARY_FORMATS:
+            glGetIntegerv(GL_NUM_SHADER_BINARY_FORMATS, &needed);
+            break;
+    }
+#endif
+
+#ifdef GL_VERSION_ES_CM_1_1
+    // GLES 1.x pnames
+    switch (pname) {
+        case GL_ALIASED_LINE_WIDTH_RANGE:
+        case GL_ALIASED_POINT_SIZE_RANGE:
+        case GL_DEPTH_RANGE:
+        case GL_SMOOTH_LINE_WIDTH_RANGE:
+        case GL_SMOOTH_POINT_SIZE_RANGE:
+            needed = 2;
+            break;
+
+        case GL_CURRENT_NORMAL:
+        case GL_POINT_DISTANCE_ATTENUATION:
+            needed = 3;
+            break;
+
+        case GL_COLOR_CLEAR_VALUE:
+        case GL_COLOR_WRITEMASK:
+        case GL_CURRENT_COLOR:
+        case GL_CURRENT_TEXTURE_COORDS:
+        case GL_FOG_COLOR:
+        case GL_LIGHT_MODEL_AMBIENT:
+        case GL_SCISSOR_BOX:
+        case GL_VIEWPORT:
+            needed = 4;
+            break;
+
+        case GL_MODELVIEW_MATRIX:
+        case GL_PROJECTION_MATRIX:
+        case GL_TEXTURE_MATRIX:
+            needed = 16;
+            break;
+
+        case GL_COMPRESSED_TEXTURE_FORMATS:
+            glGetIntegerv(GL_NUM_COMPRESSED_TEXTURE_FORMATS, &needed);
+            break;
+    }
+#endif
+    return needed;
+}

+ 138 - 0
jme3-ios-native/src/com_jme3_audio_ios_IosAL.c

@@ -0,0 +1,138 @@
+#include "com_jme3_audio_ios_IosAL.h"
+//#include "AL/al.h"
+//#include "AL/alext.h"
+
+#include "OpenAL/al.h"
+#include "OpenAL/alc.h"
+#include "OpenAL/oalMacOSX_OALExtensions.h"
+
+JNIEXPORT jstring JNICALL Java_com_jme3_audio_ios_IosAL_alGetString
+  (JNIEnv* env, jobject obj, jint param)
+{
+    return (*env)->NewStringUTF(env, alGetString(param));
+}
+
+JNIEXPORT jint JNICALL Java_com_jme3_audio_ios_IosAL_alGenSources
+  (JNIEnv *env, jobject obj)
+{
+    ALuint source;
+    alGenSources(1, &source);
+    return source;
+}
+
+JNIEXPORT jint JNICALL Java_com_jme3_audio_ios_IosAL_alGetError
+  (JNIEnv *env, jobject obj)
+{
+    return alGetError();
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alDeleteSources
+  (JNIEnv* env, jobject obj, jint numSources, jobject intbufSources)
+{
+    ALuint* pIntBufSources = (ALuint*) (*env)->GetDirectBufferAddress(env, intbufSources);
+    alDeleteSources((ALsizei)numSources, pIntBufSources);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alGenBuffers
+  (JNIEnv* env, jobject obj, jint numBuffers, jobject intbufBuffers)
+{
+    ALuint* pIntBufBuffers = (ALuint*) (*env)->GetDirectBufferAddress(env, intbufBuffers);
+    alGenBuffers((ALsizei)numBuffers, pIntBufBuffers);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alDeleteBuffers
+  (JNIEnv* env, jobject obj, jint numBuffers, jobject intbufBuffers)
+{
+    ALuint* pIntBufBuffers = (ALuint*) (*env)->GetDirectBufferAddress(env, intbufBuffers);
+    alDeleteBuffers((ALsizei)numBuffers, pIntBufBuffers);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alSourceStop
+  (JNIEnv *env, jobject obj, jint source)
+{
+    alSourceStop((ALuint)source);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alSourcei
+  (JNIEnv *env, jobject obj, jint source, jint param, jint value)
+{
+    alSourcei((ALuint)source, (ALenum)param, (ALint)value);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alBufferData
+  (JNIEnv* env, jobject obj, jint buffer, jint format, jobject bufferData, jint bufferSize, jint frequency)
+{
+    ALuint* pBufferData = (ALuint*) (*env)->GetDirectBufferAddress(env, bufferData);
+    alBufferData((ALuint)buffer, (ALenum)format, pBufferData, (ALsizei)bufferSize, (ALsizei)frequency);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alSourcePlay
+  (JNIEnv *env, jobject obj, jint source)
+{
+    alSourcePlay((ALuint)source);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alSourcePause
+  (JNIEnv *env, jobject obj, jint source)
+{
+    alSourcePause((ALuint)source);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alSourcef
+  (JNIEnv *env, jobject obj, jint source, jint param, jfloat value)
+{
+    alSourcef((ALuint)source, (ALenum)param, (ALfloat)value);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alSource3f
+  (JNIEnv *env, jobject obj, jint source, jint param, jfloat value1, jfloat value2, jfloat value3)
+{
+    alSource3f((ALuint)source, (ALenum)param, (ALfloat)value1, (ALfloat)value2, (ALfloat)value3);
+}
+
+JNIEXPORT jint JNICALL Java_com_jme3_audio_ios_IosAL_alGetSourcei
+  (JNIEnv *env, jobject obj, jint source, jint param)
+{
+    ALint result;
+    alGetSourcei((ALuint)source, (ALenum)param, &result);
+    return (jint)result;
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alSourceUnqueueBuffers
+  (JNIEnv* env, jobject obj, jint source, jint numBuffers, jobject buffers)
+{
+    ALuint* pBuffers = (ALuint*) (*env)->GetDirectBufferAddress(env, buffers);
+    alSourceUnqueueBuffers((ALuint)source, (ALsizei)numBuffers, pBuffers);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alSourceQueueBuffers
+  (JNIEnv* env, jobject obj, jint source, jint numBuffers, jobject buffers)
+{
+    ALuint* pBuffers = (ALuint*) (*env)->GetDirectBufferAddress(env, buffers);
+    alSourceQueueBuffers((ALuint)source, (ALsizei)numBuffers, pBuffers);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alListener
+  (JNIEnv* env, jobject obj, jint param, jobject bufferData)
+{
+    ALfloat* pBufferData = (ALfloat*) (*env)->GetDirectBufferAddress(env, bufferData);
+    alListenerfv((ALenum)param, pBufferData);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alListenerf
+  (JNIEnv *env, jobject obj, jint param, jfloat value)
+{
+    alListenerf((ALenum)param, (ALfloat)value);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alListener3f
+  (JNIEnv *env, jobject obj, jint param, jfloat value1, jfloat value2, jfloat value3)
+{
+    alListener3f((ALenum)param, (ALfloat)value1, (ALfloat)value2, (ALfloat)value3);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alSource3i
+  (JNIEnv *env, jobject obj, jint source, jint param, jint value1, jint value2, jint value3)
+{
+    alSource3i((ALuint)source, (ALenum)param, (ALint)value1, (ALint)value2, (ALint)value3);
+}

+ 173 - 0
jme3-ios-native/src/com_jme3_audio_ios_IosAL.h

@@ -0,0 +1,173 @@
+/* DO NOT EDIT THIS FILE - it is machine generated */
+#include <jni.h>
+/* Header for class com_jme3_audio_ios_IosAL */
+
+#ifndef _Included_com_jme3_audio_ios_IosAL
+#define _Included_com_jme3_audio_ios_IosAL
+#ifdef __cplusplus
+extern "C" {
+#endif
+/*
+ * Class:     com_jme3_audio_ios_IosAL
+ * Method:    alGetString
+ * Signature: (I)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_com_jme3_audio_ios_IosAL_alGetString
+  (JNIEnv *, jobject, jint);
+
+/*
+ * Class:     com_jme3_audio_ios_IosAL
+ * Method:    alGenSources
+ * Signature: ()I
+ */
+JNIEXPORT jint JNICALL Java_com_jme3_audio_ios_IosAL_alGenSources
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     com_jme3_audio_ios_IosAL
+ * Method:    alGetError
+ * Signature: ()I
+ */
+JNIEXPORT jint JNICALL Java_com_jme3_audio_ios_IosAL_alGetError
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     com_jme3_audio_ios_IosAL
+ * Method:    alDeleteSources
+ * Signature: (ILjava/nio/IntBuffer;)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alDeleteSources
+  (JNIEnv *, jobject, jint, jobject);
+
+/*
+ * Class:     com_jme3_audio_ios_IosAL
+ * Method:    alGenBuffers
+ * Signature: (ILjava/nio/IntBuffer;)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alGenBuffers
+  (JNIEnv *, jobject, jint, jobject);
+
+/*
+ * Class:     com_jme3_audio_ios_IosAL
+ * Method:    alDeleteBuffers
+ * Signature: (ILjava/nio/IntBuffer;)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alDeleteBuffers
+  (JNIEnv *, jobject, jint, jobject);
+
+/*
+ * Class:     com_jme3_audio_ios_IosAL
+ * Method:    alSourceStop
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alSourceStop
+  (JNIEnv *, jobject, jint);
+
+/*
+ * Class:     com_jme3_audio_ios_IosAL
+ * Method:    alSourcei
+ * Signature: (III)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alSourcei
+  (JNIEnv *, jobject, jint, jint, jint);
+
+/*
+ * Class:     com_jme3_audio_ios_IosAL
+ * Method:    alBufferData
+ * Signature: (IILjava/nio/ByteBuffer;II)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alBufferData
+  (JNIEnv *, jobject, jint, jint, jobject, jint, jint);
+
+/*
+ * Class:     com_jme3_audio_ios_IosAL
+ * Method:    alSourcePlay
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alSourcePlay
+  (JNIEnv *, jobject, jint);
+
+/*
+ * Class:     com_jme3_audio_ios_IosAL
+ * Method:    alSourcePause
+ * Signature: (I)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alSourcePause
+  (JNIEnv *, jobject, jint);
+
+/*
+ * Class:     com_jme3_audio_ios_IosAL
+ * Method:    alSourcef
+ * Signature: (IIF)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alSourcef
+  (JNIEnv *, jobject, jint, jint, jfloat);
+
+/*
+ * Class:     com_jme3_audio_ios_IosAL
+ * Method:    alSource3f
+ * Signature: (IIFFF)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alSource3f
+  (JNIEnv *, jobject, jint, jint, jfloat, jfloat, jfloat);
+
+/*
+ * Class:     com_jme3_audio_ios_IosAL
+ * Method:    alGetSourcei
+ * Signature: (II)I
+ */
+JNIEXPORT jint JNICALL Java_com_jme3_audio_ios_IosAL_alGetSourcei
+  (JNIEnv *, jobject, jint, jint);
+
+/*
+ * Class:     com_jme3_audio_ios_IosAL
+ * Method:    alSourceUnqueueBuffers
+ * Signature: (IILjava/nio/IntBuffer;)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alSourceUnqueueBuffers
+  (JNIEnv *, jobject, jint, jint, jobject);
+
+/*
+ * Class:     com_jme3_audio_ios_IosAL
+ * Method:    alSourceQueueBuffers
+ * Signature: (IILjava/nio/IntBuffer;)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alSourceQueueBuffers
+  (JNIEnv *, jobject, jint, jint, jobject);
+
+/*
+ * Class:     com_jme3_audio_ios_IosAL
+ * Method:    alListener
+ * Signature: (ILjava/nio/FloatBuffer;)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alListener
+  (JNIEnv *, jobject, jint, jobject);
+
+/*
+ * Class:     com_jme3_audio_ios_IosAL
+ * Method:    alListenerf
+ * Signature: (IF)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alListenerf
+  (JNIEnv *, jobject, jint, jfloat);
+
+/*
+ * Class:     com_jme3_audio_ios_IosAL
+ * Method:    alListener3f
+ * Signature: (IFFF)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alListener3f
+  (JNIEnv *, jobject, jint, jfloat, jfloat, jfloat);
+
+/*
+ * Class:     com_jme3_audio_ios_IosAL
+ * Method:    alSource3i
+ * Signature: (IIIII)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosAL_alSource3i
+  (JNIEnv *, jobject, jint, jint, jint, jint, jint);
+
+#ifdef __cplusplus
+}
+#endif
+#endif

+ 178 - 0
jme3-ios-native/src/com_jme3_audio_ios_IosALC.c

@@ -0,0 +1,178 @@
+//#include "util.h"
+#include "com_jme3_audio_ios_IosALC.h"
+//#include "AL/alc.h"
+//#include "AL/alext.h"
+
+#include "OpenAL/al.h"
+#include "OpenAL/alc.h"
+#include "OpenAL/oalMacOSX_OALExtensions.h"
+
+static jboolean created = JNI_FALSE;
+
+/* InitAL opens the default device and sets up a context using default
+ * attributes, making the program ready to call OpenAL functions. */
+static int InitAL()
+{
+    ALCdevice *device = NULL;
+    ALCcontext *ctx = NULL;
+
+    /* Open and initialize a device with default settings */
+    device = alcOpenDevice(NULL);
+    
+    if(device == NULL)
+    {
+        fprintf(stderr, "Could not open a device!\n");
+        goto cleanup;
+    }
+
+    ctx = alcCreateContext(device, NULL);
+    
+    if (ctx == NULL)
+    {
+        fprintf(stderr, "Could not create context!\n");
+        goto cleanup;
+    }
+    
+    if (!alcMakeContextCurrent(ctx)) 
+    {
+        fprintf(stderr, "Could not make context current!\n");
+        goto cleanup;
+    }
+
+    return 0;
+    
+cleanup:
+    if (ctx != NULL) alcDestroyContext(ctx);
+    if (device != NULL) alcCloseDevice(device);
+    return 1;
+}
+
+/* CloseAL closes the device belonging to the current context, and destroys the
+ * context. */
+static void CloseAL()
+{
+    ALCdevice *device;
+    ALCcontext *ctx;
+
+    ctx = alcGetCurrentContext();
+    
+    if (ctx == NULL) 
+    {
+        return;
+    }
+
+    device = alcGetContextsDevice(ctx);
+    
+    if (device == NULL) 
+    {
+        return;
+    }
+
+    if(!alcMakeContextCurrent(NULL)) {
+        return;
+    }
+
+    alcDestroyContext(ctx);
+    alcCloseDevice(device);
+}
+
+static ALCdevice* GetALCDevice()
+{
+    ALCdevice *device;
+    ALCcontext *ctx;
+
+    ctx = alcGetCurrentContext();
+    
+    if (ctx != NULL) 
+    {
+        device = alcGetContextsDevice(ctx);
+        
+        if (device != NULL)
+        {
+            return device;
+        }
+    }
+    
+    return NULL;
+}
+
+JNIEXPORT jboolean JNICALL Java_com_jme3_audio_ios_IosALC_isCreated
+  (JNIEnv* env, jobject obj)
+{
+    return created;
+}
+
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosALC_createALC
+  (JNIEnv* env, jobject obj)
+{
+    created = (InitAL() == 0);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosALC_destroyALC
+  (JNIEnv* env, jobject obj)
+{
+    CloseAL();
+    created = JNI_FALSE;
+}
+
+JNIEXPORT jstring JNICALL Java_com_jme3_audio_ios_IosALC_alcGetString
+  (JNIEnv* env, jobject obj, jint param)
+{
+    ALCdevice* device = GetALCDevice();
+    if (device == NULL) return NULL;
+    return (*env)->NewStringUTF(env, alcGetString(device, param));
+}
+
+JNIEXPORT jboolean JNICALL Java_com_jme3_audio_ios_IosALC_alcIsExtensionPresent
+  (JNIEnv* env, jobject obj, jstring extension)
+{
+    ALCdevice* device = GetALCDevice();
+    
+    if (device == NULL) return JNI_FALSE;
+    
+    const char* strExtension = (*env)->GetStringUTFChars(env, extension, NULL);
+    
+    if (strExtension == NULL)
+    {
+        return JNI_FALSE;
+    }
+    
+    jboolean result = alcIsExtensionPresent(device, strExtension);
+    
+    (*env)->ReleaseStringUTFChars(env, extension, strExtension);
+    
+    return result;
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosALC_alcGetInteger
+  (JNIEnv* env, jobject obj, jint param, jobject buffer, jint bufferSize)
+{
+    ALCdevice* device = GetALCDevice();
+    
+    if (device == NULL) return;
+
+    ALCint* pBuffers = (ALCint*) (*env)->GetDirectBufferAddress(env, buffer);
+
+    alcGetIntegerv(device, (ALCenum)param, (ALCsizei)bufferSize, pBuffers);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosALC_alcDevicePauseSOFT
+  (JNIEnv* env, jobject obj)
+{
+    ALCdevice* device = GetALCDevice();
+    
+    if (device == NULL) return;
+    
+//    alcDevicePauseSOFT(device);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosALC_alcDeviceResumeSOFT
+  (JNIEnv* env, jobject obj)
+{
+    ALCdevice* device = GetALCDevice();
+    
+    if (device == NULL) return;
+    
+//    alcDeviceResumeSOFT(device);
+}

+ 77 - 0
jme3-ios-native/src/com_jme3_audio_ios_IosALC.h

@@ -0,0 +1,77 @@
+/* DO NOT EDIT THIS FILE - it is machine generated */
+#include <jni.h>
+/* Header for class com_jme3_audio_ios_IosALC */
+
+#ifndef _Included_com_jme3_audio_ios_IosALC
+#define _Included_com_jme3_audio_ios_IosALC
+#ifdef __cplusplus
+extern "C" {
+#endif
+/*
+ * Class:     com_jme3_audio_ios_IosALC
+ * Method:    createALC
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosALC_createALC
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     com_jme3_audio_ios_IosALC
+ * Method:    destroyALC
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosALC_destroyALC
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     com_jme3_audio_ios_IosALC
+ * Method:    isCreated
+ * Signature: ()Z
+ */
+JNIEXPORT jboolean JNICALL Java_com_jme3_audio_ios_IosALC_isCreated
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     com_jme3_audio_ios_IosALC
+ * Method:    alcGetString
+ * Signature: (I)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_com_jme3_audio_ios_IosALC_alcGetString
+  (JNIEnv *, jobject, jint);
+
+/*
+ * Class:     com_jme3_audio_ios_IosALC
+ * Method:    alcIsExtensionPresent
+ * Signature: (Ljava/lang/String;)Z
+ */
+JNIEXPORT jboolean JNICALL Java_com_jme3_audio_ios_IosALC_alcIsExtensionPresent
+  (JNIEnv *, jobject, jstring);
+
+/*
+ * Class:     com_jme3_audio_ios_IosALC
+ * Method:    alcGetInteger
+ * Signature: (ILjava/nio/IntBuffer;I)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosALC_alcGetInteger
+  (JNIEnv *, jobject, jint, jobject, jint);
+
+/*
+ * Class:     com_jme3_audio_ios_IosALC
+ * Method:    alcDevicePauseSOFT
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosALC_alcDevicePauseSOFT
+  (JNIEnv *, jobject);
+
+/*
+ * Class:     com_jme3_audio_ios_IosALC
+ * Method:    alcDeviceResumeSOFT
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosALC_alcDeviceResumeSOFT
+  (JNIEnv *, jobject);
+
+#ifdef __cplusplus
+}
+#endif
+#endif

+ 79 - 0
jme3-ios-native/src/com_jme3_audio_ios_IosEFX.c

@@ -0,0 +1,79 @@
+//#include "util.h"
+#include "com_jme3_audio_ios_IosEFX.h"
+//#include "AL/alext.h"
+
+#include "OpenAL/al.h"
+#include "OpenAL/alc.h"
+#include "OpenAL/oalMacOSX_OALExtensions.h"
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alGenAuxiliaryEffectSlots
+  (JNIEnv* env, jobject obj, jint numSlots, jobject buffer)
+{
+    ALuint* pBuffers = (ALuint*) (*env)->GetDirectBufferAddress(env, buffer);
+//    alGenAuxiliaryEffectSlots((ALsizei)numSlots, pBuffers);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alGenEffects
+  (JNIEnv* env, jobject obj, jint numEffects, jobject buffer)
+{
+    ALuint* pBuffers = (ALuint*) (*env)->GetDirectBufferAddress(env, buffer);
+//    alGenEffects((ALsizei)numEffects, pBuffers);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alEffecti
+  (JNIEnv* env, jobject obj, jint effect, jint param, jint value)
+{
+//    alEffecti((ALuint)effect, (ALenum)param, (ALint)value);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alAuxiliaryEffectSloti
+  (JNIEnv* env, jobject obj, jint effectSlot, jint param, jint value)
+{
+//    alAuxiliaryEffectSloti((ALuint)effectSlot, (ALenum)param, (ALint)value);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alDeleteEffects
+  (JNIEnv* env, jobject obj, jint numEffects, jobject buffer)
+{
+    ALuint* pBuffers = (ALuint*) (*env)->GetDirectBufferAddress(env, buffer);
+//    alDeleteEffects((ALsizei)numEffects, pBuffers);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alDeleteAuxiliaryEffectSlots
+  (JNIEnv* env, jobject obj, jint numEffectSlots, jobject buffer)
+{
+    ALuint* pBuffers = (ALuint*) (*env)->GetDirectBufferAddress(env, buffer);
+//    alDeleteAuxiliaryEffectSlots((ALsizei)numEffectSlots, pBuffers);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alGenFilters
+  (JNIEnv* env, jobject obj, jint numFilters, jobject buffer)
+{
+    ALuint* pBuffers = (ALuint*) (*env)->GetDirectBufferAddress(env, buffer);
+//    alGenFilters((ALsizei)numFilters, pBuffers);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alFilteri
+  (JNIEnv* env, jobject obj, jint filter, jint param, jint value)
+{
+//    alFilteri((ALuint)filter, (ALenum)param, (ALint)value);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alFilterf
+  (JNIEnv* env, jobject obj, jint filter, jint param, jfloat value)
+{
+//    alFilterf((ALuint)filter, (ALenum)param, (ALfloat)value);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alDeleteFilters
+  (JNIEnv* env, jobject obj, jint numFilters, jobject buffer)
+{
+    ALuint* pBuffers = (ALuint*) (*env)->GetDirectBufferAddress(env, buffer);
+//    alDeleteFilters((ALsizei)numFilters, pBuffers);
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alEffectf
+  (JNIEnv* env, jobject obj, jint effect, jint param, jfloat value)
+{
+//    alEffectf((ALuint)effect, (ALenum)param, (ALfloat)value);
+}

+ 101 - 0
jme3-ios-native/src/com_jme3_audio_ios_IosEFX.h

@@ -0,0 +1,101 @@
+/* DO NOT EDIT THIS FILE - it is machine generated */
+#include <jni.h>
+/* Header for class com_jme3_audio_ios_IosEFX */
+
+#ifndef _Included_com_jme3_audio_ios_IosEFX
+#define _Included_com_jme3_audio_ios_IosEFX
+#ifdef __cplusplus
+extern "C" {
+#endif
+/*
+ * Class:     com_jme3_audio_ios_IosEFX
+ * Method:    alGenAuxiliaryEffectSlots
+ * Signature: (ILjava/nio/IntBuffer;)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alGenAuxiliaryEffectSlots
+  (JNIEnv *, jobject, jint, jobject);
+
+/*
+ * Class:     com_jme3_audio_ios_IosEFX
+ * Method:    alGenEffects
+ * Signature: (ILjava/nio/IntBuffer;)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alGenEffects
+  (JNIEnv *, jobject, jint, jobject);
+
+/*
+ * Class:     com_jme3_audio_ios_IosEFX
+ * Method:    alEffecti
+ * Signature: (III)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alEffecti
+  (JNIEnv *, jobject, jint, jint, jint);
+
+/*
+ * Class:     com_jme3_audio_ios_IosEFX
+ * Method:    alAuxiliaryEffectSloti
+ * Signature: (III)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alAuxiliaryEffectSloti
+  (JNIEnv *, jobject, jint, jint, jint);
+
+/*
+ * Class:     com_jme3_audio_ios_IosEFX
+ * Method:    alDeleteEffects
+ * Signature: (ILjava/nio/IntBuffer;)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alDeleteEffects
+  (JNIEnv *, jobject, jint, jobject);
+
+/*
+ * Class:     com_jme3_audio_ios_IosEFX
+ * Method:    alDeleteAuxiliaryEffectSlots
+ * Signature: (ILjava/nio/IntBuffer;)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alDeleteAuxiliaryEffectSlots
+  (JNIEnv *, jobject, jint, jobject);
+
+/*
+ * Class:     com_jme3_audio_ios_IosEFX
+ * Method:    alGenFilters
+ * Signature: (ILjava/nio/IntBuffer;)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alGenFilters
+  (JNIEnv *, jobject, jint, jobject);
+
+/*
+ * Class:     com_jme3_audio_ios_IosEFX
+ * Method:    alFilteri
+ * Signature: (III)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alFilteri
+  (JNIEnv *, jobject, jint, jint, jint);
+
+/*
+ * Class:     com_jme3_audio_ios_IosEFX
+ * Method:    alFilterf
+ * Signature: (IIF)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alFilterf
+  (JNIEnv *, jobject, jint, jint, jfloat);
+
+/*
+ * Class:     com_jme3_audio_ios_IosEFX
+ * Method:    alDeleteFilters
+ * Signature: (ILjava/nio/IntBuffer;)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alDeleteFilters
+  (JNIEnv *, jobject, jint, jobject);
+
+/*
+ * Class:     com_jme3_audio_ios_IosEFX
+ * Method:    alEffectf
+ * Signature: (IIF)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_audio_ios_IosEFX_alEffectf
+  (JNIEnv *, jobject, jint, jint, jfloat);
+
+#ifdef __cplusplus
+}
+#endif
+#endif

+ 94 - 0
jme3-ios-native/src/com_jme3_util_IosNativeBufferAllocator.c

@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2009-2024 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.
+ */
+
+/**
+ * @file com_jme3_util_IosNativeBufferAllocator.c
+ * @author Jesus Oliver, taken from pavl_g's AndroidNativeBufferAllocator.
+ * @brief Creates and releases direct byte buffers for {com.jme3.util.IosNativeBufferAllocator}.
+ * @date 2024-09-24.
+ * @note
+ * Find more at :
+ * - JNI Direct byte buffers : https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#NewDirectByteBuffer.
+ * - JNI Get Direct byte buffer : https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#GetDirectBufferAddress.
+ * - GNU Basic allocation : https://www.gnu.org/software/libc/manual/html_node/Basic-Allocation.html.
+ * - GNU Allocating Cleared Space : https://www.gnu.org/software/libc/manual/html_node/Allocating-Cleared-Space.html.
+ * - GNU No Memory error : https://www.gnu.org/software/libc/manual/html_node/Error-Codes.html#index-ENOMEM.
+ * - GNU Freeing memory : https://www.gnu.org/software/libc/manual/html_node/Freeing-after-Malloc.html.
+ * - iOS logging : 
+ */
+
+#include "com_jme3_util_IosNativeBufferAllocator.h"
+#include <stdlib.h>
+#include <stdbool.h>
+#include <errno.h>
+
+// TODO: iOS log
+
+bool isDeviceOutOfMemory(void*);
+
+/**
+ * @brief Tests if the device is out of memory.
+ *
+ * @return true if the buffer to allocate is a NULL pointer and the errno is ENOMEM (Error-no-memory).
+ * @return false otherwise.
+ */
+bool isDeviceOutOfMemory(void* buffer) {
+    return buffer == NULL && errno == ENOMEM;
+}
+
+JNIEXPORT void JNICALL Java_com_jme3_util_IosNativeBufferAllocator_releaseDirectByteBuffer
+(JNIEnv * env, jobject object, jobject bufferObject)
+{
+    void* buffer = (*env)->GetDirectBufferAddress(env, bufferObject);
+    // deallocates the buffer pointer
+    free(buffer);
+    // log the destruction by mem address
+    //LOG(ANDROID_LOG_INFO, "Buffer released (mem_address, size) -> (%p, %lu)", buffer, sizeof(buffer));
+    // avoid accessing this memory space by resetting the memory address
+    buffer = NULL;
+    //LOG(ANDROID_LOG_INFO, "Buffer mem_address formatted (mem_address, size) -> (%p, %u)", buffer, sizeof(buffer));
+}
+
+JNIEXPORT jobject JNICALL Java_com_jme3_util_IosNativeBufferAllocator_createDirectByteBuffer
+(JNIEnv * env, jobject object, jlong size)
+{
+    void* buffer = calloc(1, size);
+    if (isDeviceOutOfMemory(buffer)) {
+       //LOG(ANDROID_LOG_FATAL, "Device is out of memory exiting with %u", errno);
+       exit(errno);
+    } else {
+       //LOG(ANDROID_LOG_INFO, "Buffer created successfully (mem_address, size) -> (%p %lli)", buffer, size);
+    }
+    return (*env)->NewDirectByteBuffer(env, buffer, size);
+}
+
+

+ 29 - 0
jme3-ios-native/src/com_jme3_util_IosNativeBufferAllocator.h

@@ -0,0 +1,29 @@
+/* DO NOT EDIT THIS FILE - it is machine generated */
+#include <jni.h>
+/* Header for class com_jme3_util_IosNativeBufferAllocator */
+
+#ifndef _Included_com_jme3_util_IosNativeBufferAllocator
+#define _Included_com_jme3_util_IosNativeBufferAllocator
+#ifdef __cplusplus
+extern "C" {
+#endif
+/*
+ * Class:     com_jme3_util_IosNativeBufferAllocator
+ * Method:    releaseDirectByteBuffer
+ * Signature: (Ljava/nio/Buffer;)V
+ */
+JNIEXPORT void JNICALL Java_com_jme3_util_IosNativeBufferAllocator_releaseDirectByteBuffer
+  (JNIEnv *, jobject, jobject);
+
+/*
+ * Class:     com_jme3_util_IosNativeBufferAllocator
+ * Method:    createDirectByteBuffer
+ * Signature: (J)Ljava/nio/ByteBuffer;
+ */
+JNIEXPORT jobject JNICALL Java_com_jme3_util_IosNativeBufferAllocator_createDirectByteBuffer
+  (JNIEnv *, jobject, jlong);
+
+#ifdef __cplusplus
+}
+#endif
+#endif

+ 192 - 0
jme3-ios-native/src/jme-ios.m

@@ -0,0 +1,192 @@
+/*
+ * Copyright (c) 2009-2013 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.
+ */
+#include <jni.h>
+#import <UIKit/UIKit.h>
+
+/**
+ * Author: Normen Hansen
+ */
+
+#ifndef JNIEXPORT
+#define JNIEXPORT __attribute__ ((visibility("default"))) \
+  __attribute__ ((used))
+#endif
+
+BOOL checkJNIException(JNIEnv *e){
+    if ((*e)->ExceptionCheck(e)) {
+        (*e)->ExceptionDescribe(e);
+        (*e)->ExceptionClear(e);
+        return YES;
+    }
+    return NO;
+}
+
+#ifndef _Included_com_jme3_system_ios_IosImageLoader
+#define _Included_com_jme3_system_ios_IosImageLoader
+#endif
+
+
+static void flipImage(int scanline, int height, char* data)
+{
+    char tmp[scanline];
+    
+    for (int y = 0; y < height / 2; y++)
+    {
+        int oppY = height - y - 1;
+        int yOff  = y * scanline;
+        int oyOff = oppY * scanline;
+        // Copy scanline at Y to tmp
+        memcpy(tmp, &data[yOff], scanline);
+        // Copy data at opposite Y to Y
+        memcpy(&data[yOff], &data[oyOff], scanline);
+        // Copy tmp to opposite Y
+        memcpy(&data[oyOff], tmp, scanline);
+    }
+}
+
+JNIEXPORT jobject JNICALL
+Java_com_jme3_system_ios_IosImageLoader_loadImageData(JNIEnv* e, jclass obj, jobject imageFormat, jboolean flipY, jobject inputStream){
+    // prepare java classes and method pointers
+    jclass imageClass = (*e)->FindClass(e, "com/jme3/texture/Image");
+    jclass imageFormatClass = (*e)->FindClass(e, "com/jme3/texture/Image$Format");
+    jclass inputStreamClass = (*e)->FindClass(e, "java/io/InputStream");
+    jclass bufferUtilsClass = (*e)->FindClass(e, "com/jme3/util/BufferUtils");
+    jclass colorSpaceClass = (*e)->FindClass(e, "com/jme3/texture/image/ColorSpace");
+    
+    jmethodID imageConstructor = (*e)->GetMethodID(e, imageClass, "<init>", "(Lcom/jme3/texture/Image$Format;IILjava/nio/ByteBuffer;Lcom/jme3/texture/image/ColorSpace;)V");
+    jmethodID readMethod = (*e)->GetMethodID(e, inputStreamClass, "read", "([B)I");
+    jmethodID newBufferMethod = (*e)->GetStaticMethodID(e, bufferUtilsClass, "createByteBuffer", "(I)Ljava/nio/ByteBuffer;");
+    jmethodID bitsPerPixel = (*e)->GetMethodID(e, imageFormatClass, "getBitsPerPixel", "()I");
+    jfieldID sRGBFieldID = (*e)->GetStaticFieldID(e, colorSpaceClass, "sRGB", "Lcom/jme3/texture/image/ColorSpace;");
+    jobject sRGBVal = (*e)->GetStaticObjectField(e, colorSpaceClass, sRGBFieldID);
+    
+    if (checkJNIException(e)) {
+        return nil;
+    }
+
+    int bpp = (*e)->CallIntMethod(e, imageFormat, bitsPerPixel);
+    int comps = 4; // Components (Bytes) per Pixel
+    
+    if ((bpp % 8) == 0)
+    {
+        comps = bpp / 8;
+    } else {
+        jclass assetExClazz = (*e)->FindClass(e, "com/jme3/asset/AssetLoadException");
+        (*e)->ThrowNew(e, assetExClazz, "Unsupported ImageFormat: Bits per Pixel is not multiple of 8");
+    }
+    
+    // read data from inputstream via byteArray to NSMutableData
+    jbyteArray tempArray = (*e)->NewByteArray (e, 1000);
+    if (checkJNIException(e)) {
+        return nil;
+    }
+    
+    NSMutableData *inData = [[NSMutableData alloc] init];
+    jint size = (*e)->CallIntMethod(e, inputStream, readMethod, tempArray);
+    if (checkJNIException(e)) {
+        [inData release];
+        return nil;
+    }
+    while (size != -1) {
+        jbyte *data;
+        data = (*e)->GetByteArrayElements(e, tempArray, false);
+        [inData appendBytes:data length:size];
+        (*e)->ReleaseByteArrayElements(e, tempArray, data, JNI_ABORT);
+        size = (*e)->CallIntMethod(e, inputStream, readMethod, tempArray);
+        if (checkJNIException(e)) {
+            [inData release];
+            return nil;
+        }
+    }
+    (*e)->DeleteLocalRef(e, tempArray);
+    if (checkJNIException(e)) {
+        [inData release];
+        return nil;
+    }
+    
+    // decode image data
+    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+    UIImage* inputImage = [UIImage imageWithData:inData];
+    if(inputImage == nil){
+        [inData release];
+        [pool release];
+        return nil;
+    }
+    CGImageRef inImage = [inputImage CGImage];
+    int wdth = CGImageGetWidth(inImage);
+    int ht = CGImageGetHeight(inImage);
+    
+    // NewDirectByteBuffer seems to fail? -> Creating ByteBuffer in java
+    jobject nativeBuffer = (*e)->CallStaticObjectMethod(e, bufferUtilsClass, newBufferMethod, ht*wdth*comps);
+    if (checkJNIException(e)) {
+        [inData release];
+        [pool release];
+        return nil;
+    }
+    void *rawData = (*e)->GetDirectBufferAddress(e, nativeBuffer);
+    NSUInteger bytesPerRowImg = CGImageGetBytesPerRow(inImage);
+    NSUInteger bitsPerComponentImg = CGImageGetBitsPerComponent(inImage);
+    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
+    CGContextRef context = CGBitmapContextCreate(rawData,wdth,ht,bitsPerComponentImg,bytesPerRowImg,colorSpace,kCGImageAlphaPremultipliedLast| kCGBitmapByteOrder32Big);
+    CGColorSpaceRelease(colorSpace);
+    CGContextDrawImage(context,CGRectMake(0,0,wdth,ht), inImage);
+    CGContextRelease(context);
+    [inData release];
+    [pool release];
+    
+    if (flipY) {
+        flipImage(wdth * comps, ht, rawData);
+    }
+    
+    //create image
+    jobject imageObject = (*e)->NewObject(e, imageClass, imageConstructor, imageFormat, wdth, ht, nativeBuffer, sRGBVal);
+    return imageObject;
+}
+
+#ifndef _Included_com_jme3_system_ios_JmeIosSystem
+#define _Included_com_jme3_system_ios_JmeIosSystem
+#endif
+
+JNIEXPORT void JNICALL
+Java_com_jme3_system_ios_JmeIosSystem_showDialog(JNIEnv* e, jobject c, jstring text) {
+    const char* chars = (*e)->GetStringUTFChars(e, text, 0);
+    NSString* string = [[NSString alloc] initWithUTF8String : chars];
+    (*e)->ReleaseStringUTFChars(e, text, chars);
+    UIAlertView *alert = [[UIAlertView alloc] initWithTitle : @"Error"
+                                                    message : string
+                                                   delegate : nil
+                                          cancelButtonTitle : @"OK"
+                                          otherButtonTitles : nil];
+    [alert show];
+    [alert release];
+    [string release];
+}

+ 18 - 0
jme3-ios-native/src/jme3_ios_native.h

@@ -0,0 +1,18 @@
+//
+//  jme3_ios_native.h
+//  jme3-ios-native
+//
+//  Created by joliver82 on 19/09/2024.
+//
+
+#import <Foundation/Foundation.h>
+
+//! Project version number for jme3_ios_native_lib.
+FOUNDATION_EXPORT double jme3_ios_nativeVersionNumber;
+
+//! Project version string for jme3_ios_native_lib.
+FOUNDATION_EXPORT const unsigned char jme3_ios_nativeVersionString[];
+
+// In this header, you should import all the public headers of your framework using statements like #import <jme3_ios_native_lib/PublicHeader.h>
+
+

+ 9 - 0
jme3-ios-native/template/META-INF/robovm/ios/robovm.xml

@@ -0,0 +1,9 @@
+<config>
+    <frameworkPaths>
+        <path>libs</path>
+    </frameworkPaths>
+    <frameworks>
+        <framework>jme3-ios-native</framework>
+    </frameworks>
+</config>
+

+ 11 - 0
jme3-ios/src/main/java/com/jme3/system/ios/IGLESContext.java

@@ -42,6 +42,9 @@ import com.jme3.opencl.Context;
 import com.jme3.renderer.ios.IosGL;
 import com.jme3.renderer.opengl.*;
 import com.jme3.system.*;
+import com.jme3.util.IosNativeBufferAllocator;
+import com.jme3.util.BufferAllocatorFactory;
+
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -64,6 +67,14 @@ public class IGLESContext implements JmeContext {
     protected IosInputHandler input;
     protected int minFrameDuration = 0; // No FPS cap
 
+    static {
+        final String implementation = BufferAllocatorFactory.PROPERTY_BUFFER_ALLOCATOR_IMPLEMENTATION;
+
+        if (System.getProperty(implementation) == null) {
+            System.setProperty(implementation, IosNativeBufferAllocator.class.getName());
+        }
+    }
+
     public IGLESContext() {
         logger.log(Level.FINE, "IGLESContext constructor");
     }

+ 14 - 1
jme3-ios/src/main/java/com/jme3/system/ios/JmeIosSystem.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -35,6 +35,7 @@ import com.jme3.system.AppSettings;
 import com.jme3.system.JmeContext;
 import com.jme3.system.JmeSystemDelegate;
 import com.jme3.system.NullContext;
+import com.jme3.system.Platform;
 import com.jme3.util.res.Resources;
 import com.jme3.audio.AudioRenderer;
 import com.jme3.audio.ios.IosAL;
@@ -108,6 +109,18 @@ public class JmeIosSystem extends JmeSystemDelegate {
 //                throw new UnsupportedOperationException("Not supported yet.");
     }
 
+    @Override
+    public Platform getPlatform() {
+        String arch = System.getProperty("os.arch").toLowerCase();
+        if (arch.contains("arm")) {
+            return Platform.iOS_ARM;
+        } else if (arch.contains("aarch")) {
+            return Platform.iOS_ARM;
+        } else {
+            return Platform.iOS_X86;
+        }
+    }
+
     @Override
     public void showSoftKeyboard(boolean show) {
         throw new UnsupportedOperationException("Not supported yet.");

+ 71 - 0
jme3-ios/src/main/java/com/jme3/util/IosNativeBufferAllocator.java

@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2009-2024 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.util;
+
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+
+/**
+ * Allocates and destroys direct byte buffers using native code.
+ *
+ * @author Jesus Oliver based on pavl_g's AndroidNativeBufferAllocator.
+ */
+public final class IosNativeBufferAllocator implements BufferAllocator {
+
+    @Override
+    public void destroyDirectBuffer(Buffer toBeDestroyed) {
+        releaseDirectByteBuffer(toBeDestroyed);
+    }
+
+    @Override
+    public ByteBuffer allocate(int size) {
+        return createDirectByteBuffer(size);
+    }
+
+    /**
+     * Releases the memory of a direct buffer using a buffer object reference.
+     *
+     * @param buffer the buffer reference to release its memory.
+     * @see IosNativeBufferAllocator#destroyDirectBuffer(Buffer)
+     */
+    private native void releaseDirectByteBuffer(Buffer buffer);
+
+    /**
+     * Creates a new direct byte buffer explicitly with a specific size.
+     *
+     * @param size the byte buffer size used for allocating the buffer.
+     * @return a new direct byte buffer object.
+     * @see IosNativeBufferAllocator#allocate(int)
+     */
+    private native ByteBuffer createDirectByteBuffer(long size);
+}
+

+ 23 - 0
jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java

@@ -274,6 +274,21 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
                 }
         );
 
+        if (glfwPlatformSupported(GLFW_PLATFORM_WAYLAND)) {
+
+            /*
+             * Change the platform GLFW uses to enable GLX on Wayland as long as you 
+             * have XWayland (X11 compatibility)
+             */
+            if (settings.isX11PlatformPreferred() && glfwPlatformSupported(GLFW_PLATFORM_X11)) {
+                glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11);
+            }
+
+            // Disables the libdecor bar when creating a fullscreen context
+            // https://www.glfw.org/docs/latest/intro_guide.html#init_hints_wayland
+            glfwInitHint(GLFW_WAYLAND_LIBDECOR, settings.isFullscreen() ? GLFW_WAYLAND_DISABLE_LIBDECOR : GLFW_WAYLAND_PREFER_LIBDECOR);
+        }
+
         if (!glfwInit()) {
             throw new IllegalStateException("Unable to initialize GLFW");
         }
@@ -397,6 +412,14 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
         );
 
         int platformId = glfwGetPlatform();
+        if (settings.isX11PlatformPreferred()) {
+            if (platformId == GLFW_PLATFORM_X11) {
+                LOGGER.log(Level.INFO, "Active X11 server for GLX management:\n * Platform: GLFW_PLATFORM_X11|XWayland ({0})", platformId);
+            } else {
+                LOGGER.log(Level.WARNING, "Can't change platform to X11 (GLX), check if you have XWayland enabled");
+            }
+        }
+        
         if (platformId != GLFW_PLATFORM_WAYLAND && !settings.isFullscreen()) {
             /*
              * in case the window positioning hints above were ignored, but not

+ 9 - 7
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GlbLoader.java

@@ -32,9 +32,11 @@
 package com.jme3.scene.plugins.gltf;
 
 import com.jme3.asset.AssetInfo;
+import com.jme3.util.BufferUtils;
 import com.jme3.util.LittleEndien;
 
 import java.io.*;
+import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -50,12 +52,12 @@ public class GlbLoader extends GltfLoader {
      */
     private static final Logger logger = Logger.getLogger(GlbLoader.class.getName());
 
-    private ArrayList<byte[]> data = new ArrayList<>();
+    private ArrayList<ByteBuffer> data = new ArrayList<>();
 
     @Override
     public Object load(AssetInfo assetInfo) throws IOException {
         data.clear();
-        LittleEndien stream = new LittleEndien(new DataInputStream(assetInfo.openStream()));
+        LittleEndien stream = new LittleEndien(new BufferedInputStream(assetInfo.openStream()));
         /* magic */ stream.readInt();
 
         int version = stream.readInt();
@@ -76,11 +78,11 @@ public class GlbLoader extends GltfLoader {
             int chunkType = stream.readInt();
             if (chunkType == JSON_TYPE) {
                 json = new byte[chunkLength];
-                stream.read(json);
+                GltfUtils.readToByteArray(stream, json, chunkLength);
             } else {
-                byte[] bin = new byte[chunkLength];
-                stream.read(bin);
-                data.add(bin);
+                ByteBuffer buff = BufferUtils.createByteBuffer(chunkLength);
+                GltfUtils.readToByteBuffer(stream, buff, chunkLength);
+                data.add(buff);
             }
             //8 is the byte size of the 2 ints chunkLength and chunkType.
             length -= chunkLength + 8;
@@ -93,7 +95,7 @@ public class GlbLoader extends GltfLoader {
     }
 
     @Override
-    protected byte[] getBytes(int bufferIndex, String uri, Integer bufferLength) throws IOException {
+    protected ByteBuffer getBytes(int bufferIndex, String uri, Integer bufferLength) throws IOException {
         return data.get(bufferIndex);
     }
 

+ 77 - 19
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java

@@ -48,11 +48,14 @@ import com.jme3.scene.mesh.MorphTarget;
 import static com.jme3.scene.plugins.gltf.GltfUtils.*;
 import com.jme3.texture.Texture;
 import com.jme3.texture.Texture2D;
+import com.jme3.util.BufferInputStream;
+import com.jme3.util.BufferUtils;
 import com.jme3.util.IntMap;
 import com.jme3.util.mikktspace.MikktspaceTangentGenerator;
 import java.io.*;
 import java.net.URLDecoder;
 import java.nio.Buffer;
+import java.nio.ByteBuffer;
 import java.nio.FloatBuffer;
 import java.util.*;
 import java.util.logging.Level;
@@ -109,7 +112,6 @@ public class GltfLoader implements AssetLoader {
 
     protected Object loadFromStream(AssetInfo assetInfo, InputStream stream) throws IOException {
         try {
-            dataCache.clear();
             info = assetInfo;
             skinnedSpatials.clear();
             rootNode = new Node();
@@ -181,6 +183,27 @@ public class GltfLoader implements AssetLoader {
             throw new AssetLoadException("An error occurred loading " + assetInfo.getKey().getName(), e);
         } finally {
             stream.close();
+            dataCache.clear();
+            skinBuffers.clear();
+            skinnedSpatials.clear();
+            info = null;
+            docRoot = null;
+            rootNode = null;
+            defaultMat = null;
+            accessors = null;
+            bufferViews = null;
+            buffers = null;
+            scenes = null;
+            nodes = null;
+            meshes = null;
+            materials = null;
+            textures = null;
+            images = null;
+            samplers = null;
+            animations = null;
+            skins = null;
+            cameras = null;
+            useNormalsFlag = false;
         }
     }
 
@@ -553,11 +576,15 @@ public class GltfLoader implements AssetLoader {
         // Not sure it's useful for us, but I guess it's useful when you map data directly to the GPU.
         // int target = getAsInteger(bufferView, "target", 0);
 
-        byte[] data = readData(bufferIndex);
+        ByteBuffer data = readData(bufferIndex);
         data = customContentManager.readExtensionAndExtras("bufferView", bufferView, data);
 
+        if(!(data instanceof ByteBuffer)){
+            throw new IOException("Buffer data is not a NIO Buffer");
+        }
+
         if (store == null) {
-            store = new byte[byteLength];
+            store = BufferUtils.createByteBuffer(byteLength);
         }
 
         if (count == -1) {
@@ -569,14 +596,40 @@ public class GltfLoader implements AssetLoader {
         return store;
     }
 
-    public byte[] readData(int bufferIndex) throws IOException {
+    public Buffer viewBuffer(Integer bufferViewIndex, int byteOffset, int count,  
+            int numComponents, VertexBuffer.Format originalFormat,  VertexBuffer.Format targetFormat) throws IOException {
+        JsonObject bufferView = bufferViews.get(bufferViewIndex).getAsJsonObject();
+        Integer bufferIndex = getAsInteger(bufferView, "buffer");
+        assertNotNull(bufferIndex, "No buffer defined for bufferView " + bufferViewIndex);
+        int bvByteOffset = getAsInteger(bufferView, "byteOffset", 0);
+        Integer byteLength = getAsInteger(bufferView, "byteLength");
+        assertNotNull(byteLength, "No byte length defined for bufferView " + bufferViewIndex);
+        int byteStride = getAsInteger(bufferView, "byteStride", 0);
+
+        ByteBuffer data = readData(bufferIndex);
+        data = customContentManager.readExtensionAndExtras("bufferView", bufferView, data);
+
+        if(!(data instanceof ByteBuffer)){
+            throw new IOException("Buffer data is not a NIO Buffer");
+        }
+ 
+
+        if (count == -1) {
+            count = byteLength;
+        }
+
+        return GltfUtils.getBufferView(data, byteOffset + bvByteOffset, count, byteStride, numComponents, originalFormat, targetFormat );
+
+    }
+
+    public ByteBuffer readData(int bufferIndex) throws IOException {
         assertNotNull(buffers, "No buffer defined");
 
         JsonObject buffer = buffers.get(bufferIndex).getAsJsonObject();
         String uri = getAsString(buffer, "uri");
         Integer bufferLength = getAsInteger(buffer, "byteLength");
         assertNotNull(bufferLength, "No byteLength defined for buffer " + bufferIndex);
-        byte[] data = (byte[]) fetchFromCache("buffers", bufferIndex, Object.class);
+        ByteBuffer data = (ByteBuffer) fetchFromCache("buffers", bufferIndex, Object.class);
         if (data != null) {
             return data;
         }
@@ -588,12 +641,12 @@ public class GltfLoader implements AssetLoader {
         return data;
     }
 
-    protected byte[] getBytes(int bufferIndex, String uri, Integer bufferLength) throws IOException {
-        byte[] data;
+    protected ByteBuffer getBytes(int bufferIndex, String uri, Integer bufferLength) throws IOException {
+        ByteBuffer data;
         if (uri != null) {
             if (uri.startsWith("data:")) {
                 // base 64 embed data
-                data = Base64.getDecoder().decode(uri.substring(uri.indexOf(",") + 1));
+                data = BufferUtils.createByteBuffer(Base64.getDecoder().decode(uri.substring(uri.indexOf(",") + 1)));
             } else {
                 // external file let's load it
                 String decoded = decodeUri(uri);
@@ -603,11 +656,11 @@ public class GltfLoader implements AssetLoader {
                 }
 
                 BinDataKey key = new BinDataKey(info.getKey().getFolder() + decoded);
-                InputStream input = (InputStream) info.getManager().loadAsset(key);
-                data = new byte[bufferLength];
-                try (DataInputStream dataStream = new DataInputStream(input)) {
-                    dataStream.readFully(data);
+                try(InputStream input = (InputStream) info.getManager().loadAsset(key)){
+                    data = BufferUtils.createByteBuffer(bufferLength);
+                    GltfUtils.readToByteBuffer(input, data, bufferLength);
                 }
+               
             }
         } else {
             // no URI, this should not happen in a gltf file, only in glb files.
@@ -784,19 +837,23 @@ public class GltfLoader implements AssetLoader {
         if (uri == null) {
             assertNotNull(bufferView, "Image " + sourceIndex + " should either have an uri or a bufferView");
             assertNotNull(mimeType, "Image " + sourceIndex + " should have a mimeType");
-            byte[] data = (byte[]) readBuffer(bufferView, 0, -1, null, 1, VertexBuffer.Format.Byte);
+            ByteBuffer data = (ByteBuffer) viewBuffer(bufferView, 0, -1, 1, VertexBuffer.Format.Byte, VertexBuffer.Format.Byte);
+
             String extension = mimeType.split("/")[1];
             TextureKey key = new TextureKey("image" + sourceIndex + "." + extension, flip);
-            result = (Texture2D) info.getManager().loadAssetFromStream(key, new ByteArrayInputStream(data));
-
+            try(BufferedInputStream bis = new BufferedInputStream(new BufferInputStream(data))){
+                result = (Texture2D) info.getManager().loadAssetFromStream(key, bis);
+            }
         } else if (uri.startsWith("data:")) {
             // base64 encoded image
             String[] uriInfo = uri.split(",");
-            byte[] data = Base64.getDecoder().decode(uriInfo[1]);
+            ByteBuffer data = BufferUtils.createByteBuffer(Base64.getDecoder().decode(uriInfo[1]));
             String headerInfo = uriInfo[0].split(";")[0];
             String extension = headerInfo.split("/")[1];
             TextureKey key = new TextureKey("image" + sourceIndex + "." + extension, flip);
-            result = (Texture2D) info.getManager().loadAssetFromStream(key, new ByteArrayInputStream(data));
+            try(BufferedInputStream bis = new BufferedInputStream(new BufferInputStream(data))){
+                result = (Texture2D) info.getManager().loadAssetFromStream(key, bis);
+            }
         } else {
             // external file image
             String decoded = decodeUri(uri);
@@ -1338,13 +1395,14 @@ public class GltfLoader implements AssetLoader {
             }
             int numComponents = getNumberOfComponents(type);
 
-            Buffer buff = VertexBuffer.createBuffer(format, numComponents, count);
             int bufferSize = numComponents * count;
+            Buffer buff;
             if (bufferViewIndex == null) {
+                buff = VertexBuffer.createBuffer(format, numComponents, count);
                 // no referenced buffer, specs says to pad the buffer with zeros.
                 padBuffer(buff, bufferSize);
             } else {
-                readBuffer(bufferViewIndex, byteOffset, count, buff, numComponents, originalFormat);
+                buff = (Buffer) viewBuffer(bufferViewIndex, byteOffset, count, numComponents, originalFormat, format);
             }
 
             if (bufferType == VertexBuffer.Type.Index) {

+ 308 - 132
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java

@@ -44,6 +44,8 @@ import com.jme3.texture.Texture;
 import com.jme3.util.*;
 import java.io.*;
 import java.nio.*;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
 import java.util.*;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -60,7 +62,11 @@ public class GltfUtils {
      */
     private GltfUtils() {
     }
-    
+
+    public static ByteBuffer asReadableByteBuffer(ByteBuffer bbf){
+        return bbf.slice().order(ByteOrder.LITTLE_ENDIAN);
+    }
+
     /**
      * Parse a json input stream and returns a {@link JsonObject}
      * @param stream the stream to parse
@@ -227,62 +233,68 @@ public class GltfUtils {
         }
     }
 
-    public static void padBuffer(Object store, int bufferSize) {
-        if (store instanceof Buffer) {
-            Buffer buffer = (Buffer) store;
-            buffer.clear();
-            if (buffer instanceof IntBuffer) {
-                IntBuffer ib = (IntBuffer) buffer;
-                for (int i = 0; i < bufferSize; i++) {
-                    ib.put(0);
-                }
-            } else if (buffer instanceof FloatBuffer) {
-                FloatBuffer fb = (FloatBuffer) buffer;
-                for (int i = 0; i < bufferSize; i++) {
-                    fb.put(0);
-                }
-            } else if (buffer instanceof ShortBuffer) {
-                ShortBuffer sb = (ShortBuffer) buffer;
-                for (int i = 0; i < bufferSize; i++) {
-                    sb.put((short) 0);
-                }
-            } else if (buffer instanceof ByteBuffer) {
-                ByteBuffer bb = (ByteBuffer) buffer;
-                for (int i = 0; i < bufferSize; i++) {
-                    bb.put((byte) 0);
-                }
-            }
-            buffer.rewind();
-        }
-        if (store instanceof short[]) {
-            short[] array = (short[]) store;
-            for (int i = 0; i < array.length; i++) {
-                array[i] = 0;
-            }
-        } else if (store instanceof float[]) {
-            float[] array = (float[]) store;
-            for (int i = 0; i < array.length; i++) {
-                array[i] = 0;
+    public static void padBuffer(Buffer buffer, int bufferSize) {
+        buffer.clear();
+        if (buffer instanceof IntBuffer) {
+            IntBuffer ib = (IntBuffer) buffer;
+            for (int i = 0; i < bufferSize; i++) {
+                ib.put(0);
             }
-        } else if (store instanceof Vector3f[]) {
-            Vector3f[] array = (Vector3f[]) store;
-            for (int i = 0; i < array.length; i++) {
-                array[i] = new Vector3f();
+        } else if (buffer instanceof FloatBuffer) {
+            FloatBuffer fb = (FloatBuffer) buffer;
+            for (int i = 0; i < bufferSize; i++) {
+                fb.put(0);
             }
-        } else if (store instanceof Quaternion[]) {
-            Quaternion[] array = (Quaternion[]) store;
-            for (int i = 0; i < array.length; i++) {
-                array[i] = new Quaternion();
+        } else if (buffer instanceof ShortBuffer) {
+            ShortBuffer sb = (ShortBuffer) buffer;
+            for (int i = 0; i < bufferSize; i++) {
+                sb.put((short) 0);
             }
-        } else if (store instanceof Matrix4f[]) {
-            Matrix4f[] array = (Matrix4f[]) store;
-            for (int i = 0; i < array.length; i++) {
-                array[i] = new Matrix4f();
+        } else if (buffer instanceof ByteBuffer) {
+            ByteBuffer bb = (ByteBuffer) buffer;
+            for (int i = 0; i < bufferSize; i++) {
+                bb.put((byte) 0);
             }
         }
+        buffer.rewind();
+    }
+
+    public static void padBuffer(float[] array, int bufferSize) {
+        for (int i = 0; i < bufferSize; i++) {
+            array[i] = 0;
+        }
     }
 
-    public static void populateBuffer(Object store, byte[] source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
+    public static void padBuffer(short[] array, int bufferSize) {
+        for (int i = 0; i < bufferSize; i++) {
+            array[i] = 0;
+        }
+    }
+    
+    public static void padBuffer(Vector3f[] array, int bufferSize) {
+        for (int i = 0; i < bufferSize; i++) {
+            array[i] = new Vector3f();
+        }
+    }
+
+    public static void padBuffer(Quaternion[] array, int bufferSize) {
+        for (int i = 0; i < bufferSize; i++) {
+            array[i] = new Quaternion();
+        }      
+    }
+
+    public static void padBuffer(Matrix4f[] array, int bufferSize) {
+        for (int i = 0; i < bufferSize; i++) {
+            array[i] = new Matrix4f();
+        }        
+    }
+
+
+
+
+
+    public static void populateBuffer(Object store, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
+        source = asReadableByteBuffer(source);
 
         if (store instanceof Buffer) {
             Buffer buffer = (Buffer) store;
@@ -291,34 +303,37 @@ public class GltfUtils {
                 populateByteBuffer((ByteBuffer) buffer, source, count, byteOffset, byteStride, numComponents, format);
                 return;
             }
-            LittleEndien stream = getStream(source);
             if (buffer instanceof ShortBuffer) {
-                populateShortBuffer((ShortBuffer) buffer, stream, count, byteOffset, byteStride, numComponents, format);
+                populateShortBuffer((ShortBuffer) buffer, source, count, byteOffset, byteStride, numComponents, format);
             } else if (buffer instanceof IntBuffer) {
-                populateIntBuffer((IntBuffer) buffer, stream, count, byteOffset, byteStride, numComponents, format);
+                populateIntBuffer((IntBuffer) buffer, source, count, byteOffset, byteStride, numComponents, format);
             } else if (buffer instanceof FloatBuffer) {
-                populateFloatBuffer((FloatBuffer) buffer, stream, count, byteOffset, byteStride, numComponents, format);
+                populateFloatBuffer((FloatBuffer) buffer, source, count, byteOffset, byteStride, numComponents, format);
             }
             buffer.rewind();
             return;
         }
-        LittleEndien stream = getStream(source);
+
         if (store instanceof byte[]) {
-            populateByteArray((byte[]) store, stream, count, byteOffset, byteStride, numComponents, format);
+            populateByteArray((byte[]) store, source, count, byteOffset, byteStride, numComponents, format);
         } else if (store instanceof short[]) {
-            populateShortArray((short[]) store, stream, count, byteOffset, byteStride, numComponents, format);
+            populateShortArray((short[]) store, source, count, byteOffset, byteStride, numComponents, format);
         } else if (store instanceof float[]) {
-            populateFloatArray((float[]) store, stream, count, byteOffset, byteStride, numComponents, format);
+            populateFloatArray((float[]) store, source, count, byteOffset, byteStride, numComponents, format);
         } else if (store instanceof Vector3f[]) {
-            populateVector3fArray((Vector3f[]) store, stream, count, byteOffset, byteStride, numComponents, format);
+            populateVector3fArray((Vector3f[]) store, source, count, byteOffset, byteStride, numComponents, format);
         } else if (store instanceof Quaternion[]) {
-            populateQuaternionArray((Quaternion[]) store, stream, count, byteOffset, byteStride, numComponents, format);
+            populateQuaternionArray((Quaternion[]) store, source, count, byteOffset, byteStride, numComponents, format);
         } else if (store instanceof Matrix4f[]) {
-            populateMatrix4fArray((Matrix4f[]) store, stream, count, byteOffset, byteStride, numComponents, format);
+            populateMatrix4fArray((Matrix4f[]) store, source, count, byteOffset, byteStride, numComponents, format);
         }
     }
 
-    private static void populateByteBuffer(ByteBuffer buffer, byte[] source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) {
+    private static void skip(ByteBuffer buff, int n) {
+        buff.position(Math.min(buff.position() + n, buff.limit()));
+    }
+
+    private static void populateByteBuffer(ByteBuffer buffer, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) {
         int componentSize = format.getComponentSize();
         int index = byteOffset;
         int dataLength = componentSize * numComponents;
@@ -326,69 +341,69 @@ public class GltfUtils {
         int end = count * stride + byteOffset;
         while (index < end) {
             for (int i = 0; i < numComponents; i++) {
-                buffer.put(source[index + i]);
+                buffer.put(source.get(index + i));
             }
             index += stride;
         }
     }
 
-    private static void populateShortBuffer(ShortBuffer buffer, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
+    private static void populateShortBuffer(ShortBuffer buffer, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
         int componentSize = format.getComponentSize();
         int index = byteOffset;
         int dataLength = componentSize * numComponents;
         int stride = Math.max(dataLength, byteStride);
-        int end = count * stride + byteOffset;
-        stream.skipBytes(byteOffset);
+        int end = count * stride + byteOffset;        
+        source.position(source.position() + byteOffset);
         while (index < end) {
             for (int i = 0; i < numComponents; i++) {
-                buffer.put(stream.readShort());
+                buffer.put(source.getShort());
             }
 
             if (dataLength < stride) {
-                stream.skipBytes(stride - dataLength);
+                skip(source, stride - dataLength);
             }
             index += stride;
         }
     }
 
 
-    private static void populateIntBuffer(IntBuffer buffer, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
+    private static void populateIntBuffer(IntBuffer buffer, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
         int componentSize = format.getComponentSize();
         int index = byteOffset;
         int dataLength = componentSize * numComponents;
         int stride = Math.max(dataLength, byteStride);
         int end = count * stride + byteOffset;
-        stream.skipBytes(byteOffset);
+        source.position(source.position() + byteOffset);
         while (index < end) {
             for (int i = 0; i < numComponents; i++) {
-                buffer.put(stream.readInt());
+                buffer.put(source.getInt());
             }
             if (dataLength < stride) {
-                stream.skipBytes(stride - dataLength);
+                skip(source, stride - dataLength);
             }
             index += stride;
         }
     }
 
-    private static void populateFloatBuffer(FloatBuffer buffer, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
+    private static void populateFloatBuffer(FloatBuffer buffer, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
         int componentSize = format.getComponentSize();
         int index = byteOffset;
         int dataLength = componentSize * numComponents;
         int stride = Math.max(dataLength, byteStride);
         int end = count * stride + byteOffset;
-        stream.skipBytes(byteOffset);
+        source.position(source.position() + byteOffset);
         while (index < end) {
             for (int i = 0; i < numComponents; i++) {
-                buffer.put(readAsFloat(stream, format));
+                buffer.put(readAsFloat(source, format));
             }
             if (dataLength < stride) {
-                stream.skipBytes(stride - dataLength);
+                skip(source, stride - dataLength);
             }
             index += stride;
         }
     }
 
-    public static float readAsFloat(LittleEndien stream, VertexBuffer.Format format) throws IOException {
+    public static float readAsFloat(ByteBuffer source, VertexBuffer.Format format) throws IOException {
         //We may have packed data so depending on the format, we need to read data differently and unpack it
         // Implementations must use following equations to get corresponding floating-point value f from a normalized integer c and vise-versa:
         // accessor.componentType    int-to-float                float-to-int
@@ -399,34 +414,34 @@ public class GltfUtils {
         int c;
         switch (format) {
             case Byte:
-                c = stream.readByte();
+                c = source.get();
                 return Math.max(c / 127f, -1f);
             case UnsignedByte:
-                c = stream.readUnsignedByte();
+                c = source.get() & 0xFF;
                 return c / 255f;
             case Short:
-                c = stream.readShort();
+                c = source.getShort();
                 return Math.max(c / 32767f, -1f);
-            case UnsignedShort:
-                c = stream.readUnsignedShort();
+            case UnsignedShort:               
+                c = source.get() & 0xff | (source.get() & 0xff) << 8;
                 return c / 65535f;
             default:
                 //we have a regular float
-                return stream.readFloat();
+                return source.getFloat();
         }
 
     }
 
-    private static void populateByteArray(byte[] array, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
+    private static void populateByteArray(byte[] array, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
         int componentSize = format.getComponentSize();
         int index = byteOffset;
         int dataLength = componentSize * numComponents;
         int stride = Math.max(dataLength, byteStride);
         int end = count * stride + byteOffset;
-        stream.skipBytes(byteOffset);
+        source.position(source.position() + byteOffset);
 
         if (dataLength == stride) {
-            read(stream, array, end - index);
+            read(source, array, end - index);
 
             return;
         }
@@ -434,20 +449,21 @@ public class GltfUtils {
         int arrayIndex = 0;
         byte[] buffer = new byte[numComponents];
         while (index < end) {
-            read(stream, buffer, numComponents);
+            read(source, buffer, numComponents);
             System.arraycopy(buffer, 0, array, arrayIndex, numComponents);
             arrayIndex += numComponents;
             if (dataLength < stride) {
-                stream.skipBytes(stride - dataLength);
+                skip(source, stride - dataLength);
             }
             index += stride;
         }
     }
 
-    private static void read(LittleEndien stream, byte[] buffer, int length) throws IOException {
+    private static void read(ByteBuffer source, byte[] buffer, int length) throws IOException {
         int n = 0;
         while (n < length) {
-            int cnt = stream.read(buffer, n, length - n);
+            int cnt = Math.min(source.remaining(), length - n);
+            source.get(buffer, n, cnt);
             if (cnt < 0) {
                 throw new AssetLoadException("Data ended prematurely");
             }
@@ -455,25 +471,25 @@ public class GltfUtils {
         }
     }
 
-    private static void populateShortArray(short[] array, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
+    private static void populateShortArray(short[] array, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
         int componentSize = format.getComponentSize();
         int index = byteOffset;
         int dataLength = componentSize * numComponents;
         int stride = Math.max(dataLength, byteStride);
         int end = count * stride + byteOffset;
-        stream.skipBytes(byteOffset);
+        source.position(source.position() + byteOffset);
         int arrayIndex = 0;
         while (index < end) {
             for (int i = 0; i < numComponents; i++) {
                 if (componentSize == 2) {
-                    array[arrayIndex] = stream.readShort();
+                    array[arrayIndex] = source.getShort();
                 } else {
-                    array[arrayIndex] = stream.readByte();
+                    array[arrayIndex] = source.get();
                 }
                 arrayIndex++;
             }
             if (dataLength < stride) {
-                stream.skipBytes(stride - dataLength);
+                skip(source, stride - dataLength);
             }
             index += stride;
         }
@@ -557,107 +573,106 @@ public class GltfUtils {
         mesh.getBuffer(VertexBuffer.Type.BoneWeight).setUsage(VertexBuffer.Usage.CpuOnly);
     }
 
-    private static void populateFloatArray(float[] array, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
+    private static void populateFloatArray(float[] array, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
         int componentSize = format.getComponentSize();
         int index = byteOffset;
         int dataLength = componentSize * numComponents;
         int stride = Math.max(dataLength, byteStride);
         int end = count * stride + byteOffset;
-        stream.skipBytes(byteOffset);
+        source.position(source.position() + byteOffset);
         int arrayIndex = 0;
         while (index < end) {
             for (int i = 0; i < numComponents; i++) {
-                array[arrayIndex] = readAsFloat(stream, format);
+                array[arrayIndex] = readAsFloat(source, format);
                 arrayIndex++;
             }
             if (dataLength < stride) {
-                stream.skipBytes(stride - dataLength);
+                skip(source, stride - dataLength);
             }
             index += stride;
         }
     }
 
-    private static void populateVector3fArray(Vector3f[] array, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
+    private static void populateVector3fArray(Vector3f[] array, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
         int componentSize = format.getComponentSize();
         int index = byteOffset;
         int dataLength = componentSize * numComponents;
         int stride = Math.max(dataLength, byteStride);
         int end = count * stride + byteOffset;
-        stream.skipBytes(byteOffset);
+        source.position(source.position() + byteOffset);
         int arrayIndex = 0;
         while (index < end) {
             array[arrayIndex] = new Vector3f(
-                    readAsFloat(stream, format),
-                    readAsFloat(stream, format),
-                    readAsFloat(stream, format)
+                    readAsFloat(source, format),
+                    readAsFloat(source, format),
+                    readAsFloat(source, format)
             );
 
             arrayIndex++;
             if (dataLength < stride) {
-                stream.skipBytes(stride - dataLength);
+                skip(source, stride - dataLength);
             }
-
             index += stride;
         }
     }
 
-    private static void populateQuaternionArray(Quaternion[] array, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
+    private static void populateQuaternionArray(Quaternion[] array, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
         int componentSize = format.getComponentSize();
         int index = byteOffset;
         int dataLength = componentSize * numComponents;
         int stride = Math.max(dataLength, byteStride);
         int end = count * stride + byteOffset;
-        stream.skipBytes(byteOffset);
+        source.position(source.position() + byteOffset);
         int arrayIndex = 0;
         while (index < end) {
             array[arrayIndex] = new Quaternion(
-                    readAsFloat(stream, format),
-                    readAsFloat(stream, format),
-                    readAsFloat(stream, format),
-                    readAsFloat(stream, format)
+                    readAsFloat(source, format),
+                    readAsFloat(source, format),
+                    readAsFloat(source, format),
+                    readAsFloat(source, format)
             );
 
             arrayIndex++;
             if (dataLength < stride) {
-                stream.skipBytes(stride - dataLength);
+                skip(source, stride - dataLength);
             }
             index += stride;
         }
     }
 
-    private static void populateMatrix4fArray(Matrix4f[] array, LittleEndien stream, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
+    private static void populateMatrix4fArray(Matrix4f[] array, ByteBuffer source, int count, int byteOffset, int byteStride, int numComponents, VertexBuffer.Format format) throws IOException {
         int componentSize = format.getComponentSize();
         int index = byteOffset;
         int dataLength = componentSize * numComponents;
         int stride = Math.max(dataLength, byteStride);
         int end = count * stride + byteOffset;
-        stream.skipBytes(byteOffset);
+        source.position(source.position() + byteOffset);
         int arrayIndex = 0;
         while (index < end) {
 
             array[arrayIndex] = toRowMajor(
-                    readAsFloat(stream, format),
-                    readAsFloat(stream, format),
-                    readAsFloat(stream, format),
-                    readAsFloat(stream, format),
-                    readAsFloat(stream, format),
-                    readAsFloat(stream, format),
-                    readAsFloat(stream, format),
-                    readAsFloat(stream, format),
-                    readAsFloat(stream, format),
-                    readAsFloat(stream, format),
-                    readAsFloat(stream, format),
-                    readAsFloat(stream, format),
-                    readAsFloat(stream, format),
-                    readAsFloat(stream, format),
-                    readAsFloat(stream, format),
-                    readAsFloat(stream, format)
+                    readAsFloat(source, format),
+                    readAsFloat(source, format),
+                    readAsFloat(source, format),
+                    readAsFloat(source, format),
+                    readAsFloat(source, format),
+                    readAsFloat(source, format),
+                    readAsFloat(source, format),
+                    readAsFloat(source, format),
+                    readAsFloat(source, format),
+                    readAsFloat(source, format),
+                    readAsFloat(source, format),
+                    readAsFloat(source, format),
+                    readAsFloat(source, format),
+                    readAsFloat(source, format),
+                    readAsFloat(source, format),
+                    readAsFloat(source, format)
             );
             //gltf matrix are column major, JME ones are row major.
 
             arrayIndex++;
             if (dataLength < stride) {
-                stream.skipBytes(stride - dataLength);
+                skip(source, stride - dataLength);
             }
 
             index += stride;
@@ -895,4 +910,165 @@ public class GltfUtils {
             System.err.println("\n---------------------------");
         }
     }
+
+    public static void readToByteBuffer(InputStream input, ByteBuffer dst, int bytesToRead) throws IOException {
+        if (bytesToRead <= 0) throw new IOException("bytesToRead must be > 0");
+
+        int startPos = dst.position();
+        int remaining = dst.limit() - startPos;
+        if (remaining < bytesToRead) {
+            throw new IOException("Destination ByteBuffer too small: remaining=" + remaining + " < bytesToRead=" + bytesToRead);
+        }
+    
+        ReadableByteChannel ch = Channels.newChannel(input); 
+        int total = 0;
+        while (total < bytesToRead) {
+            int n = ch.read(dst);
+            if (n == -1) break;
+            total += n;
+        }
+
+        if (total < bytesToRead) {
+            throw new IOException("Data ended prematurely " + total + " < " + bytesToRead);
+        }
+
+        dst.flip();
+    }
+
+    public static void readToByteArray(InputStream input, byte[] dst, int bytesToRead) throws IOException {
+        if (bytesToRead < 0) throw new IllegalArgumentException("bytesToRead < 0");
+        if (bytesToRead > dst.length) {
+            throw new IOException("Destination array too small: length=" + dst.length + " < bytesToRead=" + bytesToRead);
+        }
+
+        int totalRead = 0;
+        while (totalRead < bytesToRead) {
+            int n = input.read(dst, totalRead, bytesToRead - totalRead);
+            if (n == -1) break;
+            totalRead += n;
+        }
+
+        if (totalRead < bytesToRead) {
+            throw new IOException("Data ended prematurely " + totalRead + " < " + bytesToRead);
+        }
+    }
+
+
+    /**
+     * Try to expose a glTF buffer region as a typed NIO view without copying.
+     * Falls back to allocating a destination buffer and populating it when
+     * interleaving, normalization, or format mismatch prevents a pure view.
+     *
+     * @param source         the original ByteBuffer (direct or heap)
+     * @param count          number of elements
+     * @param byteOffset     start offset within source (relative to beginning)
+     * @param byteStride     stride in bytes (0 means tightly packed = element size)
+     * @param numComponents  components per element (e.g. 3 for VEC3)
+     * @param originalFormat the source component type  
+     * @param targetFormat   the desired buffer view type to return
+     */
+    public static Buffer getBufferView(ByteBuffer source, int byteOffset,  int count, int byteStride,
+                                       int numComponents, VertexBuffer.Format originalFormat,
+                                       VertexBuffer.Format targetFormat) throws IOException {
+        // Work in little-endian as per glTF spec
+        source = asReadableByteBuffer(source);  
+
+        // Layout from source format
+        int srcCompSize = originalFormat.getComponentSize();
+        int elemSize = srcCompSize * numComponents;
+        int stride = Math.max(elemSize, byteStride);
+        int start = byteOffset;
+        int bytes = stride * count;
+
+
+        boolean tightlyPacked = (stride == elemSize);
+
+        if (tightlyPacked) {
+            ByteBuffer view = source.duplicate();
+            view.position(start).limit(start + bytes);
+            view = view.slice().order(ByteOrder.LITTLE_ENDIAN);
+
+            // Zero-copy returns only when source/target formats are compatible and aligned
+            switch (targetFormat) {
+                case Byte:
+                case UnsignedByte:
+                    if (srcCompSize == 1 &&
+                        (originalFormat == VertexBuffer.Format.Byte ||
+                         originalFormat == VertexBuffer.Format.UnsignedByte)) {
+                        return view;
+                    }
+                    break;
+
+                case Short:
+                case UnsignedShort:
+                    if (srcCompSize == 2 &&
+                        (originalFormat == VertexBuffer.Format.Short ||
+                         originalFormat == VertexBuffer.Format.UnsignedShort) &&
+                        (start & 1) == 0) {
+                        return view.asShortBuffer();
+                    }
+                    break;
+
+                case Int:
+                case UnsignedInt:
+                    if (srcCompSize == 4 &&
+                        (originalFormat == VertexBuffer.Format.Int ||
+                         originalFormat == VertexBuffer.Format.UnsignedInt) &&
+                        (start & 3) == 0) {
+                        return view.asIntBuffer();
+                    }
+                    break;
+
+                case Float:
+                    if (srcCompSize == 4 &&
+                        originalFormat == VertexBuffer.Format.Float &&
+                        (start & 3) == 0) {
+                        return view.asFloatBuffer();
+                    }
+                    break;
+
+                case Double:
+                    if (srcCompSize == 8 &&
+                        originalFormat == VertexBuffer.Format.Double &&
+                        (start & 7) == 0) {
+                        return view.asDoubleBuffer();
+                    }
+                    break;
+            }
+        }
+
+        // Fallback: allocate destination buffer by desired targetFormat and populate from source
+        int elements = count * numComponents;
+        switch (targetFormat) {
+            case Byte:
+            case UnsignedByte: {
+                ByteBuffer out = BufferUtils.createByteBuffer(elements);
+                populateBuffer(out, source, count, byteOffset, byteStride, numComponents, originalFormat);
+                return out;
+            }
+            case Short:
+            case UnsignedShort: {
+                ShortBuffer out = BufferUtils.createShortBuffer(elements);
+                populateBuffer(out, source, count, byteOffset, byteStride, numComponents, originalFormat);
+                return out;
+            }
+            case Int:
+            case UnsignedInt: {
+                IntBuffer out = BufferUtils.createIntBuffer(elements);
+                populateBuffer(out, source, count, byteOffset, byteStride, numComponents, originalFormat);
+                return out;
+            }
+            case Float: {
+                FloatBuffer out = BufferUtils.createFloatBuffer(elements);
+                populateBuffer(out, source, count, byteOffset, byteStride, numComponents, originalFormat);
+                return out;
+            }
+            case Double:
+                throw new IllegalArgumentException("Double conversion fallback not supported");
+            default:
+                throw new IllegalArgumentException("Unsupported format " + targetFormat);
+        }
+    }
+
+
 }

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

@@ -209,6 +209,7 @@ public class LightsPunctualExtensionLoader implements ExtensionLoader {
             Light light = lightDefinitions.get(lightIndex);
             parent.addLight(light);
             LightControl control = new LightControl(light);
+            control.setInvertAxisDirection(true);
             node.addControl(control);
         } else {
             throw new AssetLoadException("KHR_lights_punctual extension accessed undefined light at index " + lightIndex);

Some files were not shown because too many files changed in this diff