فهرست منبع

Merge branch 'master' into master

Ryan McDonough 8 ماه پیش
والد
کامیت
b2bf1b4470
100فایلهای تغییر یافته به همراه3590 افزوده شده و 534 حذف شده
  1. 62 21
      .github/workflows/main.yml
  2. 1 1
      LICENSE.md
  3. 64 14
      README.md
  4. 13 52
      build.gradle
  5. 12 15
      common.gradle
  6. 1 1
      gradle.properties
  7. 50 0
      gradle/libs.versions.toml
  8. BIN
      gradle/wrapper/gradle-wrapper.jar
  9. 2 1
      gradle/wrapper/gradle-wrapper.properties
  10. 18 13
      gradlew
  11. 10 10
      gradlew.bat
  12. 2 3
      jme3-android-examples/build.gradle
  13. 3 0
      jme3-android-native/bufferallocator.gradle
  14. 0 3
      jme3-android-native/build.gradle
  15. 3 0
      jme3-android-native/decode.gradle
  16. 3 0
      jme3-android-native/openalsoft.gradle
  17. 2 2
      jme3-android/build.gradle
  18. 14 9
      jme3-core/src/main/java/com/jme3/anim/AnimComposer.java
  19. 37 16
      jme3-core/src/main/java/com/jme3/anim/AnimLayer.java
  20. 2 2
      jme3-core/src/main/java/com/jme3/anim/Armature.java
  21. 20 2
      jme3-core/src/main/java/com/jme3/anim/ArmatureMask.java
  22. 9 0
      jme3-core/src/main/java/com/jme3/anim/MorphTrack.java
  23. 218 0
      jme3-core/src/main/java/com/jme3/anim/SingleLayerInfluenceMask.java
  24. 25 15
      jme3-core/src/main/java/com/jme3/anim/SkinningControl.java
  25. 9 0
      jme3-core/src/main/java/com/jme3/anim/TransformTrack.java
  26. 133 21
      jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java
  27. 60 15
      jme3-core/src/main/java/com/jme3/anim/tween/action/BaseAction.java
  28. 50 8
      jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java
  29. 34 2
      jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java
  30. 31 0
      jme3-core/src/main/java/com/jme3/anim/util/HasLocalTransform.java
  31. 31 0
      jme3-core/src/main/java/com/jme3/anim/util/JointModelTransform.java
  32. 31 0
      jme3-core/src/main/java/com/jme3/anim/util/Primitives.java
  33. 31 0
      jme3-core/src/main/java/com/jme3/anim/util/Weighted.java
  34. 25 13
      jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java
  35. 236 0
      jme3-core/src/main/java/com/jme3/app/state/CompositeAppState.java
  36. 1 4
      jme3-core/src/main/java/com/jme3/asset/ImplHandler.java
  37. 1 2
      jme3-core/src/main/java/com/jme3/asset/cache/WeakRefCloneAssetCache.java
  38. 1 1
      jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java
  39. 72 1
      jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java
  40. 64 1
      jme3-core/src/main/java/com/jme3/bounding/BoundingSphere.java
  41. 44 1
      jme3-core/src/main/java/com/jme3/bounding/BoundingVolume.java
  42. 1 1
      jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java
  43. 1 1
      jme3-core/src/main/java/com/jme3/cinematic/events/AnimEvent.java
  44. 14 9
      jme3-core/src/main/java/com/jme3/collision/bih/BIHNode.java
  45. 351 0
      jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java
  46. 122 0
      jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java
  47. 89 0
      jme3-core/src/main/java/com/jme3/environment/baker/EnvBaker.java
  48. 293 0
      jme3-core/src/main/java/com/jme3/environment/baker/GenericEnvBaker.java
  49. 74 0
      jme3-core/src/main/java/com/jme3/environment/baker/IBLEnvBaker.java
  50. 52 0
      jme3-core/src/main/java/com/jme3/environment/baker/IBLEnvBakerLight.java
  51. 296 0
      jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBaker.java
  52. 176 0
      jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java
  53. 209 0
      jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java
  54. 5 17
      jme3-core/src/main/java/com/jme3/environment/util/BoundingSphereDebug.java
  55. 1 1
      jme3-core/src/main/java/com/jme3/export/SavableClassUtil.java
  56. 1 1
      jme3-core/src/main/java/com/jme3/font/BitmapCharacterSet.java
  57. 1 1
      jme3-core/src/main/java/com/jme3/font/ColorTags.java
  58. 1 1
      jme3-core/src/main/java/com/jme3/font/LetterQuad.java
  59. 2 2
      jme3-core/src/main/java/com/jme3/font/Letters.java
  60. 6 6
      jme3-core/src/main/java/com/jme3/input/AbstractJoystick.java
  61. 21 21
      jme3-core/src/main/java/com/jme3/input/CameraInput.java
  62. 7 7
      jme3-core/src/main/java/com/jme3/input/ChaseCamera.java
  63. 7 7
      jme3-core/src/main/java/com/jme3/input/DefaultJoystickAxis.java
  64. 5 5
      jme3-core/src/main/java/com/jme3/input/DefaultJoystickButton.java
  65. 1 1
      jme3-core/src/main/java/com/jme3/input/FlyByCamera.java
  66. 2 2
      jme3-core/src/main/java/com/jme3/input/JoystickCompatibilityMappings.java
  67. 2 2
      jme3-core/src/main/java/com/jme3/input/event/JoyButtonEvent.java
  68. 4 4
      jme3-core/src/main/java/com/jme3/input/event/KeyInputEvent.java
  69. 4 4
      jme3-core/src/main/java/com/jme3/input/event/MouseButtonEvent.java
  70. 1 1
      jme3-core/src/main/java/com/jme3/input/event/MouseMotionEvent.java
  71. 1 1
      jme3-core/src/main/java/com/jme3/light/Light.java
  72. 1 1
      jme3-core/src/main/java/com/jme3/light/SphereProbeArea.java
  73. 1 1
      jme3-core/src/main/java/com/jme3/light/WeightedProbeBlendingStrategy.java
  74. 41 30
      jme3-core/src/main/java/com/jme3/material/MatParam.java
  75. 40 24
      jme3-core/src/main/java/com/jme3/material/MatParamTexture.java
  76. 93 76
      jme3-core/src/main/java/com/jme3/material/Material.java
  77. 20 3
      jme3-core/src/main/java/com/jme3/material/RenderState.java
  78. 4 3
      jme3-core/src/main/java/com/jme3/material/Technique.java
  79. 1 1
      jme3-core/src/main/java/com/jme3/material/TechniqueDef.java
  80. 3 2
      jme3-core/src/main/java/com/jme3/material/logic/DefaultTechniqueDefLogic.java
  81. 3 2
      jme3-core/src/main/java/com/jme3/material/logic/MultiPassLightingLogic.java
  82. 6 6
      jme3-core/src/main/java/com/jme3/material/logic/SinglePassAndImageBasedLightingLogic.java
  83. 3 2
      jme3-core/src/main/java/com/jme3/material/logic/SinglePassLightingLogic.java
  84. 3 2
      jme3-core/src/main/java/com/jme3/material/logic/StaticPassLightingLogic.java
  85. 3 2
      jme3-core/src/main/java/com/jme3/material/logic/TechniqueDefLogic.java
  86. 17 1
      jme3-core/src/main/java/com/jme3/math/AbstractTriangle.java
  87. 10 1
      jme3-core/src/main/java/com/jme3/math/FastMath.java
  88. 17 1
      jme3-core/src/main/java/com/jme3/math/Line.java
  89. 18 1
      jme3-core/src/main/java/com/jme3/math/LineSegment.java
  90. 19 1
      jme3-core/src/main/java/com/jme3/math/Matrix4f.java
  91. 35 5
      jme3-core/src/main/java/com/jme3/math/Quaternion.java
  92. 16 1
      jme3-core/src/main/java/com/jme3/math/Rectangle.java
  93. 1 1
      jme3-core/src/main/java/com/jme3/math/Ring.java
  94. 14 1
      jme3-core/src/main/java/com/jme3/math/Transform.java
  95. 24 0
      jme3-core/src/main/java/com/jme3/math/Vector2f.java
  96. 8 8
      jme3-core/src/main/java/com/jme3/math/Vector3f.java
  97. 9 9
      jme3-core/src/main/java/com/jme3/math/Vector4f.java
  98. 2 2
      jme3-core/src/main/java/com/jme3/opencl/OpenCLObjectManager.java
  99. 1 1
      jme3-core/src/main/java/com/jme3/post/HDRRenderer.java
  100. 2 2
      jme3-core/src/main/java/com/jme3/post/PreDepthProcessor.java

+ 62 - 21
.github/workflows/main.yml

@@ -46,6 +46,7 @@ on:
   push:
     branches:
       - master
+      - v3.7
       - v3.6
       - v3.5
       - v3.4
@@ -55,7 +56,49 @@ on:
     types: [published]
 
 jobs:
-
+  ScreenshotTests:
+    name: Run Screenshot Tests
+    runs-on: ubuntu-latest
+    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/**
   # Build the natives on android
   BuildAndroidNatives:
     name: Build natives for android
@@ -69,7 +112,7 @@ jobs:
         with:
           fetch-depth: 1
       - name: Validate the Gradle wrapper
-        uses: gradle/wrapper-validation-action@v1
+        uses: gradle/actions/wrapper-validation@v3
       - name: Build
         run: |
           ./gradlew -PuseCommitHashAsVersionName=true --no-daemon -PbuildNativeProjects=true \
@@ -81,7 +124,7 @@ jobs:
           name: android-natives
           path: build/native
 
-  # Build the engine, we only deploy from ubuntu-latest jdk17
+  # Build the engine, we only deploy from ubuntu-latest jdk21
   BuildJMonkey:
     needs: [BuildAndroidNatives]
     name: Build on ${{ matrix.osName }} jdk${{ matrix.jdk }}
@@ -89,22 +132,22 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        os: [ubuntu-latest,windows-2019,macOS-latest]
-        jdk: [8, 11, 17]
+        os: [ubuntu-latest,windows-latest,macOS-latest]
+        jdk: [11, 17, 21]
         include:
           - os: ubuntu-latest
             osName: linux
             deploy: true
-          - os: windows-2019
+          - os: windows-latest
             osName: windows
             deploy: false
           - os: macOS-latest
             osName: mac
             deploy: false
-          - jdk: 8
-            deploy: false
           - jdk: 11
             deploy: false
+          - jdk: 17
+            deploy: false
 
     steps:
       - name: Clone the repo
@@ -113,7 +156,7 @@ jobs:
           fetch-depth: 1
 
       - name: Setup the java environment
-        uses: actions/setup-java@v3
+        uses: actions/setup-java@v4
         with:
           distribution: 'temurin'
           java-version: ${{ matrix.jdk }}
@@ -125,12 +168,13 @@ jobs:
           path: build/native
 
       - name: Validate the Gradle wrapper
-        uses: gradle/wrapper-validation-action@v1
+        uses: gradle/actions/wrapper-validation@v3
       - name: Build Engine
         shell: bash
         run: |
-          # Build
-          ./gradlew -PuseCommitHashAsVersionName=true -PskipPrebuildLibraries=true build
+          # Normal build plus ZIP distribution and merged javadoc
+          ./gradlew -PuseCommitHashAsVersionName=true -PskipPrebuildLibraries=true \
+          build createZipDistribution mergedJavadoc
 
           if [ "${{ matrix.deploy }}" = "true" ];
           then
@@ -138,9 +182,6 @@ jobs:
             sudo apt-get update
             sudo apt-get install -y zip
 
-            # Create the zip release and the javadoc
-            ./gradlew -PuseCommitHashAsVersionName=true -PskipPrebuildLibraries=true mergedJavadoc createZipDistribution
-
             # We prepare the release for deploy
             mkdir -p ./dist/release/
             mv build/distributions/*.zip dist/release/
@@ -305,12 +346,12 @@ jobs:
         with:
           fetch-depth: 1
 
-      # Setup jdk 17 used for building Maven-style artifacts
+      # Setup jdk 21 used for building Maven-style artifacts
       - name: Setup the java environment
-        uses: actions/setup-java@v3
+        uses: actions/setup-java@v4
         with:
           distribution: 'temurin'
-          java-version: '17'
+          java-version: '21'
 
       - name: Download natives for android
         uses: actions/download-artifact@master
@@ -349,12 +390,12 @@ jobs:
         with:
           fetch-depth: 1
 
-      # Setup jdk 17 used for building Sonatype OSSRH artifacts
+      # Setup jdk 21 used for building Sonatype OSSRH artifacts
       - name: Setup the java environment
-        uses: actions/setup-java@v3
+        uses: actions/setup-java@v4
         with:
           distribution: 'temurin'
-          java-version: '17'
+          java-version: '21'
 
       # Download all the stuff...
       - name: Download maven artifacts

+ 1 - 1
LICENSE.md

@@ -1,4 +1,4 @@
-Copyright (c) 2009-2023 jMonkeyEngine.
+Copyright (c) 2009-2024 jMonkeyEngine.
 
 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions

+ 64 - 14
README.md

@@ -4,19 +4,19 @@ 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.6.1 is the latest stable version of the engine.
+v3.7.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:
 
 ![jME3 Games Mashup](https://i.imgur.com/nF8WOW6.jpg)
 
- - [jME powered games on IndieDB](http://www.indiedb.com/engines/jmonkeyengine/games)
+ - [jME powered games on IndieDB](https://www.indiedb.com/engines/jmonkeyengine/games)
  - [Boardtastic 2](https://boardtastic-2.fileplanet.com/apk)
  - [Attack of the Gelatinous Blob](https://attack-gelatinous-blob.softwareandgames.com/)
- - [Mythruna](http://mythruna.com/)
+ - [Mythruna](https://mythruna.com/)
  - [PirateHell (on Steam)](https://store.steampowered.com/app/321080/Pirate_Hell/)
- - [3089 (on Steam)](http://store.steampowered.com/app/263360/)
- - [3079 (on Steam)](http://store.steampowered.com/app/259620/)
+ - [3089 (on Steam)](https://store.steampowered.com/app/263360/3089__Futuristic_Action_RPG/)
+ - [3079 (on Steam)](https://store.steampowered.com/app/259620/3079__Block_Action_RPG/)
  - [Lightspeed Frontier (on Steam)](https://store.steampowered.com/app/548650/Lightspeed_Frontier/)
  - [Skullstone](http://www.skullstonegame.com/)
  - [Spoxel (on Steam)](https://store.steampowered.com/app/746880/Spoxel/)
@@ -24,28 +24,34 @@ The engine is used by several commercial game studios and computer-science cours
  - [Leap](https://gamejolt.com/games/leap/313308)
  - [Jumping Jack Flag](http://timealias.bplaced.net/jack/)
  - [PapaSpace Flight Simulation](https://www.papaspace.at/)
- - [Cubic Nightmare](https://jaredbgreat.itch.io/cubic-nightmare)
+ - [Cubic Nightmare (on Itch)](https://jaredbgreat.itch.io/cubic-nightmare)
  - [Chatter Games](https://chatter-games.com)
  - [Exotic Matter](https://exoticmatter.io)
  - [Demon Lord (on Google Play)](https://play.google.com/store/apps/details?id=com.dreiInitiative.demonLord&pli=1)
  - [Marvelous Marbles (on Steam)](https://store.steampowered.com/app/2244540/Marvelous_Marbles/)
  - [Boxer (on Google Play)](https://play.google.com/store/apps/details?id=com.tharg.boxer)
  - [Depthris (on Itch)](https://codewalker.itch.io/depthris)
+ - [Stranded (on Itch)](https://tgiant.itch.io/stranded)
+ - [The Afflicted Forests (Coming Soon to Steam)](https://www.indiedb.com/games/the-afflicted-forests)
+ - [Star Colony: Beyond Horizons (on Google Play)](https://play.google.com/store/apps/details?id=game.colony.ColonyBuilder)
+ - [High Impact (on Steam)](https://store.steampowered.com/app/3059050/High_Impact/)
 
-## Getting started
+## Getting Started
 
 Go to https://github.com/jMonkeyEngine/sdk/releases to download the jMonkeyEngine SDK.
-[Read the wiki](https://jmonkeyengine.github.io/wiki) for a complete install guide. Power up with some SDK Plugins and AssetPacks and you are off to the races. At this point you're gonna want to [join the forum](http://hub.jmonkeyengine.org/) so our tribe can grow stronger.
+Read [the wiki](https://jmonkeyengine.github.io/wiki) for the installation guide and tutorials.
+Join [the discussion forum](https://hub.jmonkeyengine.org/) to participate in our community,
+get your questions answered, and share your projects.
 
-Note: The master branch on GitHub is a development version of the engine and is NOT MEANT TO BE USED IN PRODUCTION, it will break constantly during development of the stable jME versions!
+Note: The master branch on GitHub is a development version of the engine and is NOT MEANT TO BE USED IN PRODUCTION.
 
 ### Technology Stack
 
- - Java
- - NetBeans Platform
- - Gradle
-
-Plus a bunch of awesome libraries & tight integrations like Bullet, NiftyGUI and other goodies.
+ - windowed, multi-platform IDE derived from NetBeans
+ - libraries for GUI, networking, physics, SFX, terrain, importing assets, etc.
+ - platform-neutral core library for scene graph, animation, rendering, math, etc.
+ - LWJGL v2/v3 (to access GLFW, OpenAL, OpenGL, and OpenVR) or Android or iOS
+ - Java Virtual Machine (v8 or higher)
 
 ### Documentation
 
@@ -59,3 +65,47 @@ Read our [contribution guide](https://github.com/jMonkeyEngine/jmonkeyengine/blo
 
 [New BSD (3-clause) License](https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/LICENSE.md)
 
+### How to Build the Engine from Source
+
+1. Install a Java Development Kit (JDK),
+   if you don't already have one.
+2. Point the `JAVA_HOME` environment variable to your JDK installation:
+   (In other words, set it to the path of a directory/folder
+   containing a "bin" that contains a Java executable.
+   That path might look something like
+   "C:\Program Files\Eclipse Adoptium\jdk-17.0.3.7-hotspot"
+   or "/usr/lib/jvm/java-17-openjdk-amd64/" or
+   "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home" .)
+  + using Bash or Zsh: `export JAVA_HOME="` *path to installation* `"`
+  + using Fish: `set -g JAVA_HOME "` *path to installation* `"`
+  + using Windows Command Prompt: `set JAVA_HOME="` *path to installation* `"`
+  + using PowerShell: `$env:JAVA_HOME = '` *path to installation* `'`
+3. Download and extract the engine source code from GitHub:
+  + using Git:
+    + `git clone https://github.com/jMonkeyEngine/jmonkeyengine.git`
+    + `cd jmonkeyengine`
+    + `git checkout -b latest v3.7.0-stable` (unless you plan to do development)
+  + using a web browser:
+    + browse to [the latest release](https://github.com/jMonkeyEngine/jmonkeyengine/releases/latest)
+    + follow the "Source code (zip)" link at the bottom of the page
+    + save the ZIP file
+    + extract the contents of the saved ZIP file
+    + `cd` to the extracted directory/folder
+4. Run the Gradle wrapper:
+  + using Bash or Fish or PowerShell or Zsh: `./gradlew build`
+  + using Windows Command Prompt: `.\gradlew build`
+
+After a successful build,
+fresh JARs will be found in "*/build/libs".
+
+You can install the JARs to your local Maven repository:
++ using Bash or Fish or PowerShell or Zsh: `./gradlew install`
++ using Windows Command Prompt: `.\gradlew install`
+
+You can run the "jme3-examples" app:
++ using Bash or Fish or PowerShell or Zsh: `./gradlew run`
++ using Windows Command Prompt: `.\gradlew run`
+
+You can restore the project to a pristine state:
++ using Bash or Fish or PowerShell or Zsh: `./gradlew clean`
++ using Windows Command Prompt: `.\gradlew clean`

+ 13 - 52
build.gradle

@@ -10,9 +10,9 @@ buildscript {
         }
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:4.2.0'
-        classpath 'me.tatarka:gradle-retrolambda:3.7.1'
-        classpath "gradle.plugin.com.github.spotbugs.snom:spotbugs-gradle-plugin:4.5.1"
+        classpath libs.android.build.gradle
+        classpath libs.gradle.retrolambda
+        classpath libs.spotbugs.gradle.plugin
     }
 }
 
@@ -49,6 +49,7 @@ subprojects {
         // Currently we only warn about issues and try to fix them as we go, but those aren't mission critical.
         spotbugs {
             ignoreFailures = true
+            toolVersion = '4.8.6'
         }
 
         tasks.withType(com.github.spotbugs.snom.SpotBugsTask ) {
@@ -74,23 +75,25 @@ task libDist(dependsOn: subprojects.build, description: 'Builds and copies the e
         subprojects.each {project ->
             if(!project.hasProperty('mainClassName')){
                 project.tasks.withType(Jar).each {archiveTask ->
-                    if(archiveTask.classifier == "sources"){
+                    String classifier = archiveTask.archiveClassifier.get()
+                    String ext = archiveTask.archiveExtension.get()
+                    if (classifier == "sources") {
                         copy {
                             from archiveTask.archivePath
                                 into sourceFolder
-                                rename {project.name + '-' + archiveTask.classifier +'.'+ archiveTask.extension}
+                                rename {project.name + '-' + classifier + '.' + ext}
                         }
-                    } else if(archiveTask.classifier == "javadoc"){
+                    } else if (classifier == "javadoc") {
                         copy {
                             from archiveTask.archivePath
                                 into javadocFolder
-                                rename {project.name + '-' + archiveTask.classifier +'.'+ archiveTask.extension}
+                                rename {project.name + '-' + classifier + '.' + ext}
                         }
                     } else{
                         copy {
                             from archiveTask.archivePath
                                 into libFolder
-                                rename {project.name + '.' + archiveTask.extension}
+                                rename {project.name + '.' + ext}
                         }
                     }
                 }
@@ -114,7 +117,7 @@ task createZipDistribution(type:Zip,dependsOn:["dist","libDist"], description:"P
 task copyLibs(type: Copy){
 //    description 'Copies the engine dependencies to build/libDist'
     from {
-        subprojects*.configurations*.compile*.copyRecursive({ !(it instanceof ProjectDependency); })*.resolve()
+        subprojects*.configurations*.implementation*.copyRecursive({ !(it instanceof ProjectDependency); })*.resolve()
     }
 
     into "$buildDir/libDist/lib-ext" //buildDir.path + '/' + libsDirName + '/lib'
@@ -245,50 +248,8 @@ if (skipPrebuildLibraries != "true" && buildNativeProjects != "true") {
     }
 }
 
-
-//class IncrementalReverseTask extends DefaultTask {
-//    @InputDirectory
-//    def File inputDir
-//
-//    @OutputDirectory
-//    def File outputDir
-//
-//    @Input
-//    def inputProperty
-//
-//    @TaskAction
-//    void execute(IncrementalTaskInputs inputs) {
-//        println inputs.incremental ? "CHANGED inputs considered out of date" : "ALL inputs considered out of date"
-//        inputs.outOfDate { change ->
-//            println "out of date: ${change.file.name}"
-//            def targetFile = new File(outputDir, change.file.name)
-//            targetFile.text = change.file.text.reverse()
-//        }
-//
-//        inputs.removed { change ->
-//            println "removed: ${change.file.name}"
-//            def targetFile = new File(outputDir, change.file.name)
-//            targetFile.delete()
-//        }
-//    }
-//}
-
-//allprojects {
-//    tasks.withType(JavaExec) {
-//        enableAssertions = true // false by default
-//    }
-//    tasks.withType(Test) {
-//        enableAssertions = true // true by default
-//    }
-//}
-
-wrapper {
-    gradleVersion = '7.6.2'
-}
-
-
 retrolambda {
   javaVersion JavaVersion.VERSION_1_7
   incremental true
     jvmArgs '-noverify'
-}
+}

+ 12 - 15
common.gradle

@@ -15,8 +15,10 @@ eclipse.jdt.file.withProperties { props ->
 group = 'org.jmonkeyengine'
 version = jmeFullVersion
 
-sourceCompatibility = JavaVersion.VERSION_1_8
-targetCompatibility = JavaVersion.VERSION_1_8
+java {
+    sourceCompatibility = JavaVersion.VERSION_1_8
+    targetCompatibility = JavaVersion.VERSION_1_8
+}
 
 tasks.withType(JavaCompile) { // compile-time options:
     //options.compilerArgs << '-Xlint:deprecation' // to show deprecation warnings
@@ -27,11 +29,6 @@ tasks.withType(JavaCompile) { // compile-time options:
     }
 }
 
-ext {
-    lwjgl3Version = '3.3.3' // used in both the jme3-lwjgl3 and jme3-vr build scripts
-    niftyVersion = '1.4.3' // used in both the jme3-niftygui and jme3-examples build scripts
-}
-
 repositories {
     mavenCentral()
     flatDir {
@@ -41,9 +38,9 @@ repositories {
 
 dependencies {
     // Adding dependencies here will add the dependencies to each subproject.
-    testImplementation 'junit:junit:4.13.2'
-    testImplementation 'org.mockito:mockito-core:3.12.4'
-    testImplementation 'org.codehaus.groovy:groovy-test:3.0.19'
+    testImplementation libs.junit4
+    testImplementation libs.mokito.core
+    testImplementation libs.groovy.test
 }
 
 // Uncomment if you want to see the status of every test that is run and
@@ -85,12 +82,12 @@ test {
 }
 
 task sourcesJar(type: Jar, dependsOn: classes, description: 'Creates a jar from the source files.') {
-    classifier = 'sources'
+    archiveClassifier = 'sources'
     from sourceSets*.allSource
 }
 
 task javadocJar(type: Jar, dependsOn: javadoc, description: 'Creates a jar from the javadoc files.') {
-    classifier = 'javadoc'
+    archiveClassifier = 'javadoc'
     from javadoc.destinationDir
 }
 
@@ -204,7 +201,7 @@ tasks.withType(Sign) {
 }
 
 checkstyle {
-    toolVersion '9.3'
+    toolVersion libs.versions.checkstyle.get()
     configFile file("${gradle.rootProject.rootDir}/config/checkstyle/checkstyle.xml")
 }
 
@@ -218,8 +215,8 @@ checkstyleTest {
 
 tasks.withType(Checkstyle) {
     reports {
-        xml.enabled false
-        html.enabled true
+        xml.required.set(false)
+        html.required.set(true)
     }
     include("**/com/jme3/renderer/**/*.java")
 }

+ 1 - 1
gradle.properties

@@ -1,5 +1,5 @@
 # Version number: Major.Minor.SubMinor (e.g. 3.3.0)
-jmeVersion = 3.7.0
+jmeVersion = 3.8.0
 
 # Leave empty to autogenerate
 # (use -PjmeVersionName="myVersion" from commandline to specify a custom version name )

+ 50 - 0
gradle/libs.versions.toml

@@ -0,0 +1,50 @@
+## catalog of libraries and plugins used to build the jmonkeyengine project
+
+[versions]
+
+checkstyle = "9.3"
+lwjgl3 = "3.3.3"
+nifty = "1.4.3"
+
+[libraries]
+
+android-build-gradle = "com.android.tools.build:gradle:4.2.0"
+android-support-appcompat = "com.android.support:appcompat-v7:28.0.0"
+androidx-annotation = "androidx.annotation:annotation:1.3.0"
+androidx-lifecycle-common = "androidx.lifecycle:lifecycle-common:2.4.0"
+gradle-git = "org.ajoberstar:gradle-git:1.2.0"
+gradle-retrolambda = "me.tatarka:gradle-retrolambda:3.7.1"
+groovy-test = "org.codehaus.groovy:groovy-test:3.0.22"
+gson = "com.google.code.gson:gson:2.9.1"
+j-ogg-vorbis = "com.github.stephengold:j-ogg-vorbis:1.0.6"
+jbullet = "com.github.stephengold:jbullet:1.0.3"
+jinput = "net.java.jinput:jinput:2.0.9"
+jna = "net.java.dev.jna:jna:5.10.0"
+jnaerator-runtime = "com.nativelibs4java:jnaerator-runtime:0.12"
+junit4 = "junit:junit:4.13.2"
+lwjgl2 = "org.jmonkeyengine:lwjgl:2.9.5"
+lwjgl3-awt = "org.lwjglx:lwjgl3-awt:0.1.8"
+
+lwjgl3-base     = { module = "org.lwjgl:lwjgl",          version.ref = "lwjgl3" }
+lwjgl3-glfw     = { module = "org.lwjgl:lwjgl-glfw",     version.ref = "lwjgl3" }
+lwjgl3-jawt     = { module = "org.lwjgl:lwjgl-jawt",     version.ref = "lwjgl3" }
+lwjgl3-jemalloc = { module = "org.lwjgl:lwjgl-jemalloc", version.ref = "lwjgl3" }
+lwjgl3-openal   = { module = "org.lwjgl:lwjgl-openal",   version.ref = "lwjgl3" }
+lwjgl3-opencl   = { module = "org.lwjgl:lwjgl-opencl",   version.ref = "lwjgl3" }
+lwjgl3-opengl   = { module = "org.lwjgl:lwjgl-opengl",   version.ref = "lwjgl3" }
+lwjgl3-openvr   = { module = "org.lwjgl:lwjgl-openvr",   version.ref = "lwjgl3" }
+lwjgl3-ovr      = { module = "org.lwjgl:lwjgl-ovr",      version.ref = "lwjgl3" }
+
+mokito-core = "org.mockito:mockito-core:3.12.4"
+
+nifty                  = { module = "com.github.nifty-gui:nifty",                  version.ref = "nifty" }
+nifty-default-controls = { module = "com.github.nifty-gui:nifty-default-controls", version.ref = "nifty" }
+nifty-examples         = { module = "com.github.nifty-gui:nifty-examples",         version.ref = "nifty" }
+nifty-style-black      = { module = "com.github.nifty-gui:nifty-style-black",      version.ref = "nifty" }
+
+spotbugs-gradle-plugin = "com.github.spotbugs.snom:spotbugs-gradle-plugin:6.0.18"
+vecmath = "javax.vecmath:vecmath:1.5.2"
+
+[bundles]
+
+[plugins]

BIN
gradle/wrapper/gradle-wrapper.jar


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

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

+ 18 - 13
gradlew

@@ -55,7 +55,7 @@
 #       Darwin, MinGW, and NonStop.
 #
 #   (3) This script is generated from the Groovy template
-#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
 #       within the Gradle project.
 #
 #       You can find Gradle at https://github.com/gradle/gradle/.
@@ -83,10 +83,8 @@ done
 # This is normally unused
 # shellcheck disable=SC2034
 APP_BASE_NAME=${0##*/}
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
 
 # Use the maximum available, or set MAX_FD != -1 to use that value.
 MAX_FD=maximum
@@ -133,10 +131,13 @@ location of your Java installation."
     fi
 else
     JAVACMD=java
-    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
 
 Please set the JAVA_HOME variable in your environment to match the
 location of your Java installation."
+    fi
 fi
 
 # Increase the maximum file descriptors if we can.
@@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
     case $MAX_FD in #(
       max*)
         # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
-        # shellcheck disable=SC3045 
+        # shellcheck disable=SC2039,SC3045
         MAX_FD=$( ulimit -H -n ) ||
             warn "Could not query maximum file descriptor limit"
     esac
@@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
       '' | soft) :;; #(
       *)
         # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
-        # shellcheck disable=SC3045 
+        # shellcheck disable=SC2039,SC3045
         ulimit -n "$MAX_FD" ||
             warn "Could not set maximum file descriptor limit to $MAX_FD"
     esac
@@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then
     done
 fi
 
-# Collect all arguments for the java command;
-#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
-#     shell script including quotes and variable substitutions, so put them in
-#     double quotes to make sure that they get re-expanded; and
-#   * put everything else in single quotes, so that it's not re-expanded.
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
 
 set -- \
         "-Dorg.gradle.appname=$APP_BASE_NAME" \

+ 10 - 10
gradlew.bat

@@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
 %JAVA_EXE% -version >NUL 2>&1
 if %ERRORLEVEL% equ 0 goto execute
 
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
 
 goto fail
 
@@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
 
 if exist "%JAVA_EXE%" goto execute
 
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
 
 goto fail
 

+ 2 - 3
jme3-android-examples/build.gradle

@@ -41,8 +41,8 @@ android {
 
 dependencies {
     implementation fileTree(dir: 'libs', include: ['*.jar'])
-    testImplementation 'junit:junit:4.13.2'
-    implementation 'com.android.support:appcompat-v7:28.0.0'
+    testImplementation libs.junit4
+    implementation libs.android.support.appcompat
 
     implementation project(':jme3-core')
     implementation project(':jme3-android')
@@ -54,5 +54,4 @@ dependencies {
     implementation project(':jme3-plugins')
     implementation project(':jme3-terrain')
     implementation fileTree(dir: '../jme3-examples/build/libs', include: ['*.jar'], exclude: ['*sources*.*'])
-//    compile project(':jme3-examples')
 }

+ 3 - 0
jme3-android-native/bufferallocator.gradle

@@ -44,6 +44,9 @@ task copyPreCompiledLibsBufferAllocator(type: Copy) {
     from file(bufferAllocatorPreCompiledLibsDir)
     into file(bufferAllocatorBuildLibsDir)
 }
+if (skipPrebuildLibraries != "true" && buildNativeProjects != "true") {
+    copyPreCompiledLibsBufferAllocator.dependsOn(rootProject.extractPrebuiltNatives)
+}
 
 // ndkExists is a boolean from the build.gradle in the root project
 // buildNativeProjects is a string set to "true"

+ 0 - 3
jme3-android-native/build.gradle

@@ -28,11 +28,8 @@ ext {
         exclude ".gradle"
     }.asPath
 }
-//println "projectClassPath = " + projectClassPath
 
 // add each native lib build file
 apply from: file('openalsoft.gradle')
-// apply from: file('stb_image.gradle')
-// apply from: file('tremor.gradle')
 apply from: file('decode.gradle')
 apply from: file('bufferallocator.gradle')

+ 3 - 0
jme3-android-native/decode.gradle

@@ -83,6 +83,9 @@ task copyPreCompiledLibs(type: Copy) {
     from sourceDir
     into outputDir
 }
+if (skipPrebuildLibraries != "true" && buildNativeProjects != "true") {
+    copyPreCompiledLibs.dependsOn(rootProject.extractPrebuiltNatives)
+}
 
 // ndkExists is a boolean from the build.gradle in the root project
 // buildNativeProjects is a string set to "true"

+ 3 - 0
jme3-android-native/openalsoft.gradle

@@ -111,6 +111,9 @@ task copyPreCompiledOpenAlSoftLibs(type: Copy) {
     from sourceDir
     into outputDir
 }
+if (skipPrebuildLibraries != "true" && buildNativeProjects != "true") {
+    copyPreCompiledOpenAlSoftLibs.dependsOn(rootProject.extractPrebuiltNatives)
+}
 
 // ndkExists is a boolean from the build.gradle in the root project
 // buildNativeProjects is a string set to "true"

+ 2 - 2
jme3-android/build.gradle

@@ -2,8 +2,8 @@ apply plugin: 'java'
 
 dependencies {
     //added annotations used by JmeSurfaceView.
-    compileOnly 'androidx.annotation:annotation:1.3.0'
-    compileOnly 'androidx.lifecycle:lifecycle-common:2.4.0'
+    compileOnly libs.androidx.annotation
+    compileOnly libs.androidx.lifecycle.common
     api project(':jme3-core')
     compileOnly 'android:android'
 }

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

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2022 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -66,7 +66,7 @@ public class AnimComposer extends AbstractControl {
      * Instantiate a composer with a single layer, no actions, and no clips.
      */
     public AnimComposer() {
-        layers.put(DEFAULT_LAYER, new AnimLayer(this, DEFAULT_LAYER, null));
+        layers.put(DEFAULT_LAYER, new AnimLayer(DEFAULT_LAYER, null));
     }
 
     /**
@@ -310,21 +310,24 @@ public class AnimComposer extends AbstractControl {
     /**
      * Add a layer to this composer.
      *
-     * @param name the desired name for the new layer
-     * @param mask the desired mask for the new layer (alias created)
+     * @param name The desired name for the new layer
+     * @param mask The desired mask for the new layer (alias created)
+     * @return a new layer
      */
-    public void makeLayer(String name, AnimationMask mask) {
-        AnimLayer l = new AnimLayer(this, name, mask);
+    public AnimLayer makeLayer(String name, AnimationMask mask) {
+        AnimLayer l = new AnimLayer(name, mask);
         layers.put(name, l);
+        return l;
     }
 
     /**
      * Remove specified layer. This will stop the current action on this layer.
      *
      * @param name The name of the layer to remove.
+     * @return The removed layer.
      */
-    public void removeLayer(String name) {
-        layers.remove(name);
+    public AnimLayer removeLayer(String name) {
+        return layers.remove(name);
     }
 
     /**
@@ -399,7 +402,7 @@ public class AnimComposer extends AbstractControl {
     @Override
     protected void controlUpdate(float tpf) {
         for (AnimLayer layer : layers.values()) {
-            layer.update(tpf);
+            layer.update(tpf, globalSpeed);
         }
     }
 
@@ -542,6 +545,7 @@ public class AnimComposer extends AbstractControl {
         InputCapsule ic = im.getCapsule(this);
         animClipMap = (Map<String, AnimClip>) ic.readStringSavableMap("animClipMap", new HashMap<String, AnimClip>());
         globalSpeed = ic.readFloat("globalSpeed", 1f);
+        layers = (Map<String, AnimLayer>) ic.readStringSavableMap("layers", new HashMap<String, AnimLayer>());
     }
 
     /**
@@ -557,5 +561,6 @@ public class AnimComposer extends AbstractControl {
         OutputCapsule oc = ex.getCapsule(this);
         oc.writeStringSavableMap(animClipMap, "animClipMap", new HashMap<String, AnimClip>());
         oc.write(globalSpeed, "globalSpeed", 1f);
+        oc.writeStringSavableMap(layers, "layers", new HashMap<String, AnimLayer>());
     }
 }

+ 37 - 16
jme3-core/src/main/java/com/jme3/anim/AnimLayer.java

@@ -32,8 +32,14 @@
 package com.jme3.anim;
 
 import com.jme3.anim.tween.action.Action;
+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.util.clone.Cloner;
 import com.jme3.util.clone.JmeCloneable;
+import java.io.IOException;
 
 /**
  * A named portion of an AnimComposer that can run (at most) one Action at a
@@ -48,7 +54,7 @@ import com.jme3.util.clone.JmeCloneable;
  * <p>Animation time may advance at a different rate from application time,
  * based on speedup factors in the composer and the current Action.
  */
-public class AnimLayer implements JmeCloneable {
+public class AnimLayer implements JmeCloneable, Savable {
     /**
      * The Action currently running on this layer, or null if none.
      */
@@ -57,16 +63,12 @@ public class AnimLayer implements JmeCloneable {
      * The name of Action currently running on this layer, or null if none.
      */
     private String currentActionName;
-    /**
-     * The composer that owns this layer. Were it not for cloning, this field
-     * would be final.
-     */
-    private AnimComposer composer;
+    
     /**
      * Limits the portion of the model animated by this layer. If null, this
      * layer can animate the entire model.
      */
-    private final AnimationMask mask;
+    private AnimationMask mask;
     /**
      * The current animation time, in scaled seconds. Always non-negative.
      */
@@ -79,23 +81,26 @@ public class AnimLayer implements JmeCloneable {
     /**
      * The name of this layer.
      */
-    final private String name;
+    private String name;
 
     private boolean loop = true;
+    
+    /**
+    * For serialization only. Do not use.
+    */
+    protected AnimLayer() {
+        
+    }
 
     /**
      * Instantiates a layer without a manager or a current Action, owned by the
      * specified composer.
      *
-     * @param composer the owner (not null, alias created)
      * @param name the layer name (not null)
      * @param mask the AnimationMask (alias created) or null to allow this layer
      *     to animate the entire model
      */
-    AnimLayer(AnimComposer composer, String name, AnimationMask mask) {
-        assert composer != null;
-        this.composer = composer;
-
+    AnimLayer(String name, AnimationMask mask) {
         assert name != null;
         this.name = name;
 
@@ -248,14 +253,15 @@ public class AnimLayer implements JmeCloneable {
      *
      * @param appDeltaTimeInSeconds the amount application time to advance the
      *     current Action, in seconds
+     * @param globalSpeed the global speed applied to all layers.
      */
-    void update(float appDeltaTimeInSeconds) {
+    void update(float appDeltaTimeInSeconds, float globalSpeed) {
         Action action = currentAction;
         if (action == null) {
             return;
         }
 
-        double speedup = action.getSpeed() * composer.getGlobalSpeed();
+        double speedup = action.getSpeed() * globalSpeed;
         double scaledDeltaTime = speedup * appDeltaTimeInSeconds;
         time += scaledDeltaTime;
 
@@ -292,7 +298,6 @@ public class AnimLayer implements JmeCloneable {
      */
     @Override
     public void cloneFields(Cloner cloner, Object original) {
-        composer = cloner.clone(composer);
         currentAction = null;
         currentActionName = null;
     }
@@ -306,4 +311,20 @@ public class AnimLayer implements JmeCloneable {
             throw new AssertionError();
         }
     }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(name, "name", null);
+        if (mask instanceof Savable) {
+            oc.write((Savable) mask, "mask", null);
+        }
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule ic = im.getCapsule(this);
+        name = ic.readString("name", null);
+        mask = (AnimationMask) ic.readSavable("mask", null);
+    }
 }

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

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -111,7 +111,7 @@ public class Armature implements JmeCloneable, Savable {
 
     /**
      * Sets the JointModelTransform implementation
-     * Default is {@link MatrixJointModelTransform}
+     * Default is {@link SeparateJointModelTransform}
      *
      * @param modelTransformClass which implementation to use
      * @see JointModelTransform

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

@@ -31,14 +31,20 @@
  */
 package com.jme3.anim;
 
+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;
 import java.util.BitSet;
 
 /**
  * An AnimationMask to select joints from a single Armature.
  */
-public class ArmatureMask implements AnimationMask {
+public class ArmatureMask implements AnimationMask, Savable {
 
-    final private BitSet affectedJoints = new BitSet();
+    private BitSet affectedJoints = new BitSet();
 
     /**
      * Instantiate a mask that affects no joints.
@@ -206,4 +212,16 @@ public class ArmatureMask implements AnimationMask {
 
         return this;
     }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(affectedJoints, "affectedJoints", null);
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule ic = im.getCapsule(this);
+        affectedJoints = ic.readBitSet("affectedJoints", null);
+    }
 }

+ 9 - 0
jme3-core/src/main/java/com/jme3/anim/MorphTrack.java

@@ -228,6 +228,15 @@ public class MorphTrack implements AnimTrack<float[]> {
         fi.interpolateWeights(blend, startFrame, weights, nbMorphTargets, store);
     }
 
+    /**
+     * Access the FrameInterpolator.
+     *
+     * @return the pre-existing instance or null
+     */
+    public FrameInterpolator getFrameInterpolator() {
+        return interpolator;
+    }
+
     /**
      * Replace the FrameInterpolator.
      *

+ 218 - 0
jme3-core/src/main/java/com/jme3/anim/SingleLayerInfluenceMask.java

@@ -0,0 +1,218 @@
+/*
+ * Copyright (c) 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.anim;
+
+import com.jme3.scene.Spatial;
+
+/**
+ * Mask that excludes joints from participating in the layer
+ * if a higher layer is using those joints in an animation.
+ * 
+ * @author codex
+ */
+public class SingleLayerInfluenceMask extends ArmatureMask {
+    
+    private final String layer;
+    private final AnimComposer anim;
+    private final SkinningControl skin;
+    private boolean checkUpperLayers = true;
+    
+    /**
+     * @param layer The layer this mask is targeted for. It is important
+     * that this match the name of the layer this mask is (or will be) part of. You
+     * can use {@link makeLayer} to ensure this.
+     * @param spatial Spatial containing necessary controls ({@link AnimComposer} and {@link SkinningControl})
+     */
+    public SingleLayerInfluenceMask(String layer, Spatial spatial) {
+        super();
+        this.layer = layer;
+        anim = spatial.getControl(AnimComposer.class);
+        skin = spatial.getControl(SkinningControl.class);
+    }
+    /**
+     * @param layer The layer this mask is targeted for. It is important
+     * that this match the name of the layer this mask is (or will be) part of. You
+     * can use {@link makeLayer} to ensure this.
+     * @param anim anim composer this mask is assigned to
+     * @param skin skinning control complimenting the anim composer.
+     */
+    public SingleLayerInfluenceMask(String layer, AnimComposer anim, SkinningControl skin) {
+        super();
+        this.layer = layer;
+        this.anim = anim;
+        this.skin = skin;
+    }
+    
+    /**
+     * Makes a layer from this mask.
+     */
+    public void makeLayer() {
+        anim.makeLayer(layer, this);
+    }
+    
+    /**
+     * Adds all joints to this mask.
+     * @return this.instance
+     */
+    public SingleLayerInfluenceMask addAll() {
+        for (Joint j : skin.getArmature().getJointList()) {
+            super.addBones(skin.getArmature(), j.getName());
+        }
+        return this;
+    }
+    
+    /**
+     * Adds the given joint and all its children to this mask.
+     * @param joint
+     * @return this instance
+     */
+    public SingleLayerInfluenceMask addFromJoint(String joint) {
+        super.addFromJoint(skin.getArmature(), joint);
+        return this;
+    }
+    
+    /**
+     * Adds the given joints to this mask.
+     * @param joints
+     * @return this instance
+     */
+    public SingleLayerInfluenceMask addJoints(String... joints) {
+        super.addBones(skin.getArmature(), joints);
+        return this;
+    }
+    
+    /**
+     * Makes this mask check if each joint is being used by a higher layer
+     * before it uses them.
+     * <p>Not checking is more efficient, but checking can avoid some
+     * interpolation issues between layers. Default=true
+     * @param check 
+     * @return this instance
+     */
+    public SingleLayerInfluenceMask setCheckUpperLayers(boolean check) {
+        checkUpperLayers = check;
+        return this;
+    }
+    
+    /**
+     * Get the layer this mask is targeted for.
+     * <p>It is extremely important that this value match the actual layer
+     * this is included in, because checking upper layers may not work if
+     * they are different.
+     * @return target layer
+     */
+    public String getTargetLayer() {
+        return layer;
+    }
+    
+    /**
+     * Get the {@link AnimComposer} this mask is for.
+     * @return anim composer
+     */
+    public AnimComposer getAnimComposer() {
+        return anim;
+    }
+    
+    /**
+     * Get the {@link SkinningControl} this mask is for.
+     * @return skinning control
+     */
+    public SkinningControl getSkinningControl() {
+        return skin;
+    }
+    
+    /**
+     * Returns true if this mask is checking upper layers for joint use.
+     * @return 
+     */
+    public boolean isCheckUpperLayers() {
+        return checkUpperLayers;
+    }
+    
+    @Override
+    public boolean contains(Object target) {
+        return simpleContains(target) && (!checkUpperLayers || !isAffectedByUpperLayers(target));
+    }
+    
+    private boolean simpleContains(Object target) {
+        return super.contains(target);
+    }
+    
+    private boolean isAffectedByUpperLayers(Object target) {
+        boolean higher = false;
+        for (String name : anim.getLayerNames()) {
+            if (name.equals(layer)) {
+                higher = true;
+                continue;
+            }
+            if (!higher) {
+                continue;
+            }
+            AnimLayer lyr = anim.getLayer(name);  
+            // if there is no action playing, no joints are used, so we can skip
+            if (lyr.getCurrentAction() == null) continue;
+            if (lyr.getMask() instanceof SingleLayerInfluenceMask) {
+                // dodge some needless recursion by calling a simpler method
+                if (((SingleLayerInfluenceMask)lyr.getMask()).simpleContains(target)) {
+                    return true;
+                }
+            }
+            else if (lyr.getMask().contains(target)) {
+                return true;
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * Creates an {@code SingleLayerInfluenceMask} for all joints.
+     * @param layer layer the returned mask is, or will be, be assigned to
+     * @param spatial spatial containing anim composer and skinning control
+     * @return new mask
+     */
+    public static SingleLayerInfluenceMask all(String layer, Spatial spatial) {
+        return new SingleLayerInfluenceMask(layer, spatial).addAll();
+    }
+    
+    /**
+     * Creates an {@code SingleLayerInfluenceMask} for all joints.
+     * @param layer layer the returned mask is, or will be, assigned to
+     * @param anim anim composer
+     * @param skin skinning control
+     * @return new mask
+     */
+    public static SingleLayerInfluenceMask all(String layer, AnimComposer anim, SkinningControl skin) {
+        return new SingleLayerInfluenceMask(layer, anim, skin).addAll();
+    }
+    
+}
+

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

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2023 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -327,15 +327,20 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
                 VertexBuffer bindPos = mesh.getBuffer(Type.BindPosePosition);
                 VertexBuffer bindNorm = mesh.getBuffer(Type.BindPoseNormal);
                 VertexBuffer pos = mesh.getBuffer(Type.Position);
-                VertexBuffer norm = mesh.getBuffer(Type.Normal);
                 FloatBuffer pb = (FloatBuffer) pos.getData();
-                FloatBuffer nb = (FloatBuffer) norm.getData();
                 FloatBuffer bpb = (FloatBuffer) bindPos.getData();
-                FloatBuffer bnb = (FloatBuffer) bindNorm.getData();
                 pb.clear();
-                nb.clear();
                 bpb.clear();
-                bnb.clear();
+
+                // reset bind normals if there is a BindPoseNormal buffer
+                if (bindNorm != null) {
+                    VertexBuffer norm = mesh.getBuffer(Type.Normal);
+                    FloatBuffer nb = (FloatBuffer) norm.getData();
+                    FloatBuffer bnb = (FloatBuffer) bindNorm.getData();
+                    nb.clear();
+                    bnb.clear();
+                    nb.put(bnb).clear();
+                }
 
                 //resetting bind tangents if there is a bind tangent buffer
                 VertexBuffer bindTangents = mesh.getBuffer(Type.BindPoseTangent);
@@ -348,9 +353,7 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
                     tb.put(btb).clear();
                 }
 
-
                 pb.put(bpb).clear();
-                nb.put(bnb).clear();
             }
         }
     }
@@ -583,9 +586,10 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
 
         VertexBuffer nb = mesh.getBuffer(Type.Normal);
 
-        FloatBuffer fnb = (FloatBuffer) nb.getData();
-        fnb.rewind();
-
+        FloatBuffer fnb = (nb == null) ? null : (FloatBuffer) nb.getData();
+        if (fnb != null) {
+            fnb.rewind();
+        }
 
         FloatBuffer ftb = (FloatBuffer) tb.getData();
         ftb.rewind();
@@ -615,7 +619,9 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
             bufLength = Math.min(posBuf.length, fvb.remaining());
             tanLength = Math.min(tanBuf.length, ftb.remaining());
             fvb.get(posBuf, 0, bufLength);
-            fnb.get(normBuf, 0, bufLength);
+            if (fnb != null) {
+                fnb.get(normBuf, 0, bufLength);
+            }
             ftb.get(tanBuf, 0, tanLength);
             int verts = bufLength / 3;
             int idxPositions = 0;
@@ -688,8 +694,10 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
 
             fvb.position(fvb.position() - bufLength);
             fvb.put(posBuf, 0, bufLength);
-            fnb.position(fnb.position() - bufLength);
-            fnb.put(normBuf, 0, bufLength);
+            if (fnb != null) {
+                fnb.position(fnb.position() - bufLength);
+                fnb.put(normBuf, 0, bufLength);
+            }
             ftb.position(ftb.position() - tanLength);
             ftb.put(tanBuf, 0, tanLength);
         }
@@ -697,7 +705,9 @@ public class SkinningControl extends AbstractControl implements Cloneable, JmeCl
         vars.release();
 
         vb.updateData(fvb);
-        nb.updateData(fnb);
+        if (nb != null) {
+            nb.updateData(fnb);
+        }
         tb.updateData(ftb);
     }
 

+ 9 - 0
jme3-core/src/main/java/com/jme3/anim/TransformTrack.java

@@ -301,6 +301,15 @@ public class TransformTrack implements AnimTrack<Transform> {
         }
     }
 
+    /**
+     * Access the FrameInterpolator.
+     *
+     * @return the pre-existing instance or null
+     */
+    public FrameInterpolator getFrameInterpolator() {
+        return interpolator;
+    }
+
     /**
      * Replaces the frame interpolator.
      *

+ 133 - 21
jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java

@@ -1,3 +1,34 @@
+/*
+ * Copyright (c) 2009-2023 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
 package com.jme3.anim.tween.action;
 
 import com.jme3.anim.AnimationMask;
@@ -5,14 +36,43 @@ import com.jme3.anim.tween.Tween;
 import com.jme3.util.clone.Cloner;
 import com.jme3.util.clone.JmeCloneable;
 
+/**
+ * Wraps an array of Tween actions into an action object.
+ * 
+ * <p>
+ * Notes :
+ * <li> The sequence of tweens is determined by {@link com.jme3.anim.tween.Tweens} utility class and the {@link BaseAction} interpolates that sequence. </li>
+ * <li> This implementation mimics the {@link com.jme3.anim.tween.AbstractTween}, but it delegates the interpolation method {@link Tween#interpolate(double)}
+ * to the {@link BlendableAction} class. </li>
+ * </p>
+ * 
+ * Created by Nehon.
+ *
+ * @see BlendableAction
+ * @see BaseAction
+ */
 public abstract class Action implements JmeCloneable, Tween {
-
+    
+    /**
+     * A sequence of actions which wraps given tween actions.
+     */
     protected Action[] actions;
     private double length;
     private double speed = 1;
     private AnimationMask mask;
     private boolean forward = true;
-
+    
+    /**
+     * Instantiates an action object that wraps a tween actions array by extracting their actions to the collection {@link Action#actions}.
+     * <p>
+     * Notes :
+     * <li> If intentions are to wrap some tween actions, then subclasses have to call this constructor, examples : {@link BlendableAction} and {@link BlendAction}. </li>
+     * <li> If intentions are to make an implementation of {@link Action} that shouldn't wrap tweens of actions, then subclasses shouldn't call this
+     * constructor, examples : {@link ClipAction} and {@link BaseAction}. </li>
+     * </p> 
+     *
+     * @param tweens the tween actions to be wrapped (not null).
+     */    
     protected Action(Tween... tweens) {
         this.actions = new Action[tweens.length];
         for (int i = 0; i < tweens.length; i++) {
@@ -24,14 +84,19 @@ public abstract class Action implements JmeCloneable, Tween {
             }
         }
     }
-
+    
+    /**
+     * Retrieves the length (the duration) of the current action.
+     *
+     * @return the length of the action in seconds.
+     */
     @Override
     public double getLength() {
         return length;
     }
 
     /**
-     * Alter the length (duration) of this Action.  This can be used to extend
+     * Alters the length (duration) of this Action. This can be used to extend
      * or truncate an Action.
      *
      * @param length the desired length (in unscaled seconds, default=0)
@@ -40,44 +105,66 @@ public abstract class Action implements JmeCloneable, Tween {
         this.length = length;
     }
 
+    /**
+     * Retrieves the speedup factor applied by the layer for this action.
+     *
+     * @see Action#setSpeed(double) for detailed documentation
+     * @return the speed of frames.
+     */
     public double getSpeed() {
         return speed;
     }
 
+    /**
+     * Alters the speedup factor applied by the layer running this action.
+     * <p>
+     * Notes:
+     * <li> This factor controls the animation direction, if the speed is a positive value then the animation will run forward and vice versa. </li>
+     * <li> The speed factor gets applied, inside the {@link com.jme3.anim.AnimLayer}, on each interpolation step by this formula : time += tpf * action.getSpeed() * composer.globalSpeed. </li>
+     * <li> Default speed is 1.0, it plays the animation clips at their normal speed. </li>
+     * <li> Setting the speed factor to Zero will stop the animation, while setting it to a negative number will play the animation in a backward fashion. </li>
+     * </p>
+     * 
+     * @param speed the speed of frames.
+     */
     public void setSpeed(double speed) {
         this.speed = speed;
-        if( speed < 0){
+        if (speed < 0) {
             setForward(false);
         } else {
             setForward(true);
         }
     }
 
+    /**
+     * Retrieves the animation mask for this action.
+     * The animation mask controls which part of the model would be animated. A model part can be
+     * registered using a {@link com.jme3.anim.Joint}.
+     *
+     * @return the animation mask instance, or null if this action will animate the entire model
+     * @see com.jme3.anim.AnimLayer to adjust the animation mask to control which part will be animated
+     */
     public AnimationMask getMask() {
         return mask;
     }
 
+    /**
+     * Meant for internal use only.
+     *
+     * <p> 
+     * Note: This method can be invoked from the user code only if this Action is wrapped by a {@link BaseAction} and
+     * the {@link BaseAction#isMaskPropagationEnabled()} is false.
+     * </p>
+     *
+     * @param mask an animation mask to be applied to this action.
+     * @see com.jme3.anim.AnimLayer to adjust the animation mask to control which part will be animated
+     */
     public void setMask(AnimationMask mask) {
         this.mask = mask;
     }
 
-    protected boolean isForward() {
-        return forward;
-    }
-
-    protected void setForward(boolean forward) {
-        if(this.forward == forward){
-            return;
-        }
-        this.forward = forward;
-        for (Action action : actions) {
-            action.setForward(forward);
-        }
-
-    }
-
     /**
-     * Create a shallow clone for the JME cloner.
+     * Creates a shallow clone for the JME cloner.
      *
      * @return a new action (not null)
      */
@@ -105,4 +192,29 @@ public abstract class Action implements JmeCloneable, Tween {
         actions = cloner.clone(actions);
         mask = cloner.clone(mask);
     }
+    
+    /**
+     * Tests whether the Action is running in the "forward" mode.
+     *
+     * @return true if the animation action is running forward, false otherwise.
+     */
+    protected boolean isForward() {
+        return forward;
+    }
+
+    /**
+     * Adjusts the forward flag which controls the animation action directionality.
+     *
+     * @param forward true to run the animation forward, false otherwise.
+     * @see Action#setSpeed(double) to change the directionality of the tween actions, negative numbers play the animation in a backward fashion
+     */
+    protected void setForward(boolean forward) {
+        if (this.forward == forward) {
+            return;
+        }
+        this.forward = forward;
+        for (Action action : actions) {
+            action.setForward(forward);
+        }
+    }
 }

+ 60 - 15
jme3-core/src/main/java/com/jme3/anim/tween/action/BaseAction.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2022 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -35,14 +35,40 @@ import com.jme3.anim.AnimationMask;
 import com.jme3.anim.tween.ContainsTweens;
 import com.jme3.anim.tween.Tween;
 import com.jme3.util.SafeArrayList;
-
 import java.util.List;
 
+/**
+ * A simple implementation for the abstract class {@link Action} to provide a wrapper for a {@link Tween}.
+ * Internally, it is used as a helper class for {@link Action} to extract and gather actions from a tween and interpolate it.
+ * <p>
+ * An example showing two clip actions running in parallel at 2x of their ordinary speed
+ * by the help of BaseAction on a new Animation Layer :
+ * <pre class="prettyprint">
+ * //create a base action from a tween.
+ * final BaseAction action = new BaseAction(Tweens.parallel(clipAction0, clipAction1));
+ * //set the action properties - utilized within the #{@link Action} class.
+ * baseAction.setSpeed(2f);
+ * //register the action as an observer to the animComposer control.
+ * animComposer.addAction("basicAction", action);
+ * //make a new Layer for a basic armature mask
+ * animComposer.makeLayer(ActionState.class.getSimpleName(), new ArmatureMask());
+ * //run the action within this layer
+ * animComposer.setCurrentAction("basicAction", ActionState.class.getSimpleName());
+ * </pre>
+ * </p>
+ * Created by Nehon.
+ */
 public class BaseAction extends Action {
 
     final private Tween tween;
     private boolean maskPropagationEnabled = true;
 
+    /**
+     * Instantiates an action from a tween by extracting the actions from a tween
+     * to a list of sub-actions to be interpolated later.
+     *
+     * @param tween a tween to extract the actions from (not null).
+     */
     public BaseAction(Tween tween) {
         this.tween = tween;
         setLength(tween.getLength());
@@ -52,33 +78,35 @@ public class BaseAction extends Action {
         subActions.toArray(actions);
     }
 
-    private void gatherActions(Tween tween, List<Action> subActions) {
-        if (tween instanceof Action) {
-            subActions.add((Action) tween);
-        } else if (tween instanceof ContainsTweens) {
-            Tween[] tweens = ((ContainsTweens) tween).getTweens();
-            for (Tween t : tweens) {
-                gatherActions(t, subActions);
-            }
-        }
-    }
-
     /**
-     * @return true if mask propagation to child actions is enabled else returns false
+     * Tests whether the animation mask is applied to the wrapped actions {@link BaseAction#actions}.
+     *
+     * @return true if mask propagation to child actions is enabled else returns false.
      */
     public boolean isMaskPropagationEnabled() {
         return maskPropagationEnabled;
     }
 
     /**
+     * Determines whether to apply the animation mask to the wrapped or child actions {@link BaseAction#actions}.
      *
      * @param maskPropagationEnabled If true, then mask set by AnimLayer will be
-     *                               forwarded to all child actions (Default=true)
+     *                               forwarded to all child actions (Default=true).
      */
     public void setMaskPropagationEnabled(boolean maskPropagationEnabled) {
         this.maskPropagationEnabled = maskPropagationEnabled;
     }
 
+    /**
+     * Sets the animation mask which determines which part of the model will 
+     * be animated by the animation layer. If the {@link BaseAction#isMaskPropagationEnabled()} is false, setting
+     * the mask attribute will not affect the actions under this base action. Setting this to 'null' will animate
+     * the entire model.
+     *
+     * @param mask an animation mask to be applied to this action (nullable).
+     * @see com.jme3.anim.AnimLayer to adjust the animation mask to control which part will be animated
+     * @see BaseAction#setMaskPropagationEnabled(boolean)
+     */
     @Override
     public void setMask(AnimationMask mask) {
         super.setMask(mask);
@@ -94,4 +122,21 @@ public class BaseAction extends Action {
     public boolean interpolate(double t) {
         return tween.interpolate(t);
     }
+    
+    /**
+     * Extracts the actions from a tween into a list.
+     *
+     * @param tween      the tween to extract the actions from (not null).
+     * @param subActions a collection to gather the extracted actions (not null).
+     */
+    private void gatherActions(Tween tween, List<Action> subActions) {
+        if (tween instanceof Action) {
+            subActions.add((Action) tween);
+        } else if (tween instanceof ContainsTweens) {
+            Tween[] tweens = ((ContainsTweens) tween).getTweens();
+            for (Tween t : tweens) {
+                gatherActions(t, subActions);
+            }
+        }
+    }
 }

+ 50 - 8
jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java

@@ -1,15 +1,52 @@
+/*
+ * 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.anim.tween.action;
 
-import com.jme3.anim.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import com.jme3.anim.AnimClip;
+import com.jme3.anim.AnimTrack;
+import com.jme3.anim.MorphTrack;
+import com.jme3.anim.TransformTrack;
+import com.jme3.anim.tween.action.BlendableAction;
 import com.jme3.anim.util.HasLocalTransform;
 import com.jme3.math.Transform;
 import com.jme3.scene.Geometry;
 import com.jme3.util.clone.Cloner;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
 
 public class ClipAction extends BlendableAction {
+    
     private AnimClip clip;
     private Transform transform = new Transform();
 
@@ -59,8 +96,13 @@ public class ClipAction extends BlendableAction {
 //        }
     }
 
-    public void reset() {
-
+    /**
+     * Gets the animation clip associated with this action.
+     * 
+     * @return The animation clip
+     */
+    public AnimClip getAnimClip() {
+        return clip;
     }
 
     @Override
@@ -100,8 +142,8 @@ public class ClipAction extends BlendableAction {
         try {
             ClipAction clone = (ClipAction) super.clone();
             return clone;
-        } catch (CloneNotSupportedException exception) {
-            throw new RuntimeException(exception);
+        } catch (CloneNotSupportedException ex) {
+            throw new RuntimeException(ex);
         }
     }
 

+ 34 - 2
jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java

@@ -1,3 +1,34 @@
+/*
+ * 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.anim.util;
 
 import com.jme3.anim.*;
@@ -10,8 +41,8 @@ import java.util.*;
 
 public class AnimMigrationUtils {
 
-    final private static AnimControlVisitor animControlVisitor = new AnimControlVisitor();
-    final private static SkeletonControlVisitor skeletonControlVisitor = new SkeletonControlVisitor();
+    private static final AnimControlVisitor animControlVisitor = new AnimControlVisitor();
+    private static final SkeletonControlVisitor skeletonControlVisitor = new SkeletonControlVisitor();
 
     /**
      * A private constructor to inhibit instantiation of this class.
@@ -64,6 +95,7 @@ public class AnimMigrationUtils {
 
                 Armature armature = new Armature(joints);
                 armature.saveBindPose();
+                armature.saveInitialPose();
                 skeletonArmatureMap.put(skeleton, armature);
 
                 List<TransformTrack> tracks = new ArrayList<>();

+ 31 - 0
jme3-core/src/main/java/com/jme3/anim/util/HasLocalTransform.java

@@ -1,3 +1,34 @@
+/*
+ * 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.anim.util;
 
 import com.jme3.export.Savable;

+ 31 - 0
jme3-core/src/main/java/com/jme3/anim/util/JointModelTransform.java

@@ -1,3 +1,34 @@
+/*
+ * 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.anim.util;
 
 import com.jme3.anim.Joint;

+ 31 - 0
jme3-core/src/main/java/com/jme3/anim/util/Primitives.java

@@ -1,3 +1,34 @@
+/*
+ * 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.anim.util;
 
 import java.util.Collections;

+ 31 - 0
jme3-core/src/main/java/com/jme3/anim/util/Weighted.java

@@ -1,3 +1,34 @@
+/*
+ * 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.anim.util;
 
 import com.jme3.anim.tween.action.Action;

+ 25 - 13
jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2023 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -321,15 +321,20 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
                 VertexBuffer bindPos = mesh.getBuffer(Type.BindPosePosition);
                 VertexBuffer bindNorm = mesh.getBuffer(Type.BindPoseNormal);
                 VertexBuffer pos = mesh.getBuffer(Type.Position);
-                VertexBuffer norm = mesh.getBuffer(Type.Normal);
                 FloatBuffer pb = (FloatBuffer) pos.getData();
-                FloatBuffer nb = (FloatBuffer) norm.getData();
                 FloatBuffer bpb = (FloatBuffer) bindPos.getData();
-                FloatBuffer bnb = (FloatBuffer) bindNorm.getData();
                 pb.clear();
-                nb.clear();
                 bpb.clear();
-                bnb.clear();
+
+                // reset bind normals if there is a BindPoseNormal buffer
+                if (bindNorm != null) {
+                    VertexBuffer norm = mesh.getBuffer(Type.Normal);
+                    FloatBuffer nb = (FloatBuffer) norm.getData();
+                    FloatBuffer bnb = (FloatBuffer) bindNorm.getData();
+                    nb.clear();
+                    bnb.clear();
+                    nb.put(bnb).clear();
+                }
 
                 //reset bind tangents if there is a bind tangent buffer
                 VertexBuffer bindTangents = mesh.getBuffer(Type.BindPoseTangent);
@@ -343,7 +348,6 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
                 }
 
                 pb.put(bpb).clear();
-                nb.put(bnb).clear();
             }
         }
     }
@@ -574,8 +578,10 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
 
         VertexBuffer nb = mesh.getBuffer(Type.Normal);
 
-        FloatBuffer fnb = (FloatBuffer) nb.getData();
-        fnb.rewind();
+        FloatBuffer fnb = (nb == null) ? null : (FloatBuffer) nb.getData();
+        if (fnb != null) {
+            fnb.rewind();
+        }
 
         FloatBuffer ftb = (FloatBuffer) tb.getData();
         ftb.rewind();
@@ -603,7 +609,9 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
             bufLength = Math.min(posBuf.length, fvb.remaining());
             tanLength = Math.min(tanBuf.length, ftb.remaining());
             fvb.get(posBuf, 0, bufLength);
-            fnb.get(normBuf, 0, bufLength);
+            if (fnb != null) {
+                fnb.get(normBuf, 0, bufLength);
+            }
             ftb.get(tanBuf, 0, tanLength);
             int verts = bufLength / 3;
             int idxPositions = 0;
@@ -676,8 +684,10 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
 
             fvb.position(fvb.position() - bufLength);
             fvb.put(posBuf, 0, bufLength);
-            fnb.position(fnb.position() - bufLength);
-            fnb.put(normBuf, 0, bufLength);
+            if (fnb != null) {
+                fnb.position(fnb.position() - bufLength);
+                fnb.put(normBuf, 0, bufLength);
+            }
             ftb.position(ftb.position() - tanLength);
             ftb.put(tanBuf, 0, tanLength);
         }
@@ -685,7 +695,9 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
         vars.release();
 
         vb.updateData(fvb);
-        nb.updateData(fnb);
+        if (nb != null) {
+            nb.updateData(fnb);
+        }
         tb.updateData(ftb);
     }
 

+ 236 - 0
jme3-core/src/main/java/com/jme3/app/state/CompositeAppState.java

@@ -0,0 +1,236 @@
+/*
+ * 
+ * Copyright (c) 2014-2024 jMonkeyEngine
+ * Copied with Paul Speed's permission from: https://github.com/Simsilica/SiO2
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions 
+ * are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright 
+ *    notice, this list of conditions and the following disclaimer.
+ * 
+ * 2. Redistributions in binary form must reproduce the above copyright 
+ *    notice, this list of conditions and the following disclaimer in 
+ *    the documentation and/or other materials provided with the 
+ *    distribution.
+ * 
+ * 3. Neither the name of the copyright holder nor the names of its 
+ *    contributors may be used to endorse or promote products derived 
+ *    from this software without specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.app.state;
+
+import com.jme3.app.Application;
+import com.jme3.util.SafeArrayList;
+
+/**
+ *  An AppState that manages a set of child app states, making sure
+ *  they are attached/detached and optional enabled/disabled with the
+ *  parent state.
+ *
+ *  @author    Paul Speed
+ */
+public class CompositeAppState extends BaseAppState {
+
+    private final SafeArrayList<AppStateEntry> states = new SafeArrayList<>(AppStateEntry.class);
+    private boolean childrenEnabled;
+    
+    /**
+     *  Since we manage attachmend/detachment possibly before
+     *  initialization, we need to keep track of the stateManager we
+     *  were given in stateAttached() in case we have to attach another
+     *  child prior to initialization (but after we're attached).
+     *  It's possible that we should actually be waiting for initialize
+     *  to add these but I feel like there was some reason I did it this 
+     *  way originally.  Past-me did not leave any clues.
+     */
+    private AppStateManager stateManager;
+    private boolean attached;  
+    
+    public CompositeAppState(AppState... states) {
+        for (AppState a : states) {
+            this.states.add(new AppStateEntry(a, false));
+        }
+    }
+
+    private int indexOf(AppState state) {
+        for (int i = 0; i < states.size(); i++) {
+            AppStateEntry e = states.get(i);
+            if (e.state == state) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    private AppStateEntry entry(AppState state) {
+        for (AppStateEntry e : states.getArray()) {
+            if (e.state == state) {
+                return e;
+            }
+        }
+        return null;
+    }
+
+    protected <T extends AppState> T addChild(T state) {
+        return addChild(state, false);
+    }
+    
+    protected <T extends AppState> T addChild(T state, boolean overrideEnable) {
+        if (indexOf(state) >= 0) {
+            return state;
+        }
+        states.add(new AppStateEntry(state, overrideEnable));
+        if (attached) {
+            stateManager.attach(state);
+        }
+        return state;   
+    }
+ 
+    protected void removeChild( AppState state ) {
+        int index = indexOf(state);
+        if( index < 0 ) {
+            return;
+        }
+        states.remove(index);
+        if( attached ) {
+            stateManager.detach(state);
+        }
+    }
+    
+    protected <T extends AppState> T getChild( Class<T> stateType ) {
+        for( AppStateEntry e : states.getArray() ) {
+            if( stateType.isInstance(e.state) ) {
+                return stateType.cast(e.state);
+            }
+        }
+        return null;
+    }
+
+    protected void clearChildren() {
+        for( AppStateEntry e : states.getArray() ) {
+            removeChild(e.state);
+        }
+    }
+    
+    @Override 
+    public void stateAttached(AppStateManager stateManager) {
+        this.stateManager = stateManager;
+        for (AppStateEntry e : states.getArray()) {
+            stateManager.attach(e.state);
+        }
+        this.attached = true;
+    }
+    
+    @Override
+    public void stateDetached(AppStateManager stateManager) {
+        // Reverse order
+        for (int i = states.size() - 1; i >= 0; i--) {
+            stateManager.detach(states.get(i).state);
+        }
+        this.attached = false;
+        this.stateManager = null;
+    }
+
+    protected void setChildrenEnabled(boolean b) {
+        if(childrenEnabled == b) {
+            return;
+        }
+        childrenEnabled = b;
+        for (AppStateEntry e : states.getArray()) {
+            e.setEnabled(b);
+        }
+    }
+
+    /**
+     *  Overrides the automatic synching of a child's enabled state.
+     *  When override is true, a child will remember its old state when
+     *  the parent's enabled state is false so that when the parent is
+     *  re-enabled the child can resume its previous enabled state.  This
+     *  is useful for the cases where a child may want to be disabled
+     *  independent of the parent... and then not automatically become
+     *  enabled just because the parent does.
+     *  Currently, the parent's disabled state always disables the children,
+     *  too.  Override is about remembering the child's state before that
+     *  happened and restoring it when the 'family' is enabled again as a whole.
+     */
+    public void setOverrideEnabled(AppState state, boolean override) {
+        AppStateEntry e = entry(state);
+        if (e == null) {
+            throw new IllegalArgumentException("State not managed:" + state);
+        }
+        if (override) {
+            e.override = true;
+        } else {
+            e.override = false;
+            e.state.setEnabled(isEnabled());
+        }   
+    }
+
+    @Override
+    protected void initialize(Application app) {
+    }
+
+    @Override
+    protected void cleanup(Application app) {
+    }
+
+    @Override
+    protected void onEnable() {
+        setChildrenEnabled(true);
+    }
+
+    @Override
+    protected void onDisable() {
+        setChildrenEnabled(false);
+    }
+    
+    private class AppStateEntry {
+        AppState state;
+        boolean enabled;
+        boolean override;
+        
+        public AppStateEntry(AppState state, boolean overrideEnable) {
+            this.state = state;
+            this.override = overrideEnable;
+            this.enabled = state.isEnabled();
+        }
+        
+        public void setEnabled(boolean b) {
+ 
+            if (override) {
+                if (b) {
+                    // Set it to whatever its enabled state
+                    // was before going disabled last time.
+                    state.setEnabled(enabled);
+                } else {
+                    // We are going to set enabled to false
+                    // but keep track of what it was before we did
+                    // that
+                    this.enabled = state.isEnabled();
+                    state.setEnabled(false);
+                }               
+            } else {
+                // Just synch it always
+                state.setEnabled(b);
+            }
+        }
+    }
+}
+

+ 1 - 4
jme3-core/src/main/java/com/jme3/asset/ImplHandler.java

@@ -124,10 +124,7 @@ final class ImplHandler {
             } catch (InstantiationException | IllegalAccessException
                     | IllegalArgumentException | InvocationTargetException
                     | NoSuchMethodException | SecurityException ex) {
-                logger.log(Level.SEVERE, "Cannot create locator of type {0}, does"
-                        + " the class have an empty and publicly accessible"
-                        + " constructor?", type.getName());
-                logger.throwing(type.getName(), "<init>", ex);
+                logger.log(Level.SEVERE, "An exception occurred while instantiating asset locator: " + type.getName(), ex);
             }
             return null;
         }

+ 1 - 2
jme3-core/src/main/java/com/jme3/asset/cache/WeakRefCloneAssetCache.java

@@ -60,8 +60,7 @@ public class WeakRefCloneAssetCache implements AssetCache {
      * Maps cloned key to AssetRef which has a weak ref to the original
      * key and a strong ref to the original asset.
      */
-    private final ConcurrentHashMap<AssetKey, AssetRef> smartCache
-            = new ConcurrentHashMap<>();
+    private final ConcurrentHashMap<AssetKey, AssetRef> smartCache = new ConcurrentHashMap<>();
 
     /**
      * Stored in the ReferenceQueue to find out when originalKey is collected

+ 1 - 1
jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java

@@ -55,7 +55,7 @@ public class ALAudioRenderer implements AudioRenderer, Runnable {
     // which is exactly 1 second of audio.
     private static final int BUFFER_SIZE = 35280;
     private static final int STREAMING_BUFFER_COUNT = 5;
-    private final static int MAX_NUM_CHANNELS = 64;
+    private static final int MAX_NUM_CHANNELS = 64;
     private IntBuffer ib = BufferUtils.createIntBuffer(1);
     private final FloatBuffer fb = BufferUtils.createVector3Buffer(2);
     private final ByteBuffer nativeBuf = BufferUtils.createByteBuffer(BUFFER_SIZE);

+ 72 - 1
jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -46,6 +46,7 @@ import com.jme3.util.TempVars;
 import java.io.IOException;
 import java.nio.FloatBuffer;
 //import com.jme.scene.TriMesh;
+import java.util.Objects;
 
 /**
  * <code>BoundingBox</code> describes a bounding volume as an axis-aligned box.
@@ -587,6 +588,76 @@ public class BoundingBox extends BoundingVolume {
         return rVal;
     }
 
+    /**
+     * Tests for exact equality with the argument, distinguishing -0 from 0. If
+     * {@code other} is null, false is returned. Either way, the current
+     * instance is unaffected.
+     *
+     * @param other the object to compare (may be null, unaffected)
+     * @return true if {@code this} and {@code other} have identical values,
+     *     otherwise false
+     */
+    @Override
+    public boolean equals(Object other) {
+        if (!(other instanceof BoundingBox)) {
+            return false;
+        }
+
+        if (this == other) {
+            return true;
+        }
+
+        BoundingBox otherBoundingBox = (BoundingBox) other;
+        if (Float.compare(xExtent, otherBoundingBox.xExtent) != 0) {
+            return false;
+        } else if (Float.compare(yExtent, otherBoundingBox.yExtent) != 0) {
+            return false;
+        } else if (Float.compare(zExtent, otherBoundingBox.zExtent) != 0) {
+            return false;
+        } else {
+            return super.equals(otherBoundingBox);
+        }
+    }
+
+    /**
+     * Returns a hash code. If two bounding boxes have identical values, they
+     * will have the same hash code. The current instance is unaffected.
+     *
+     * @return a 32-bit value for use in hashing
+     */
+    @Override
+    public int hashCode() {
+        int hash = Objects.hash(xExtent, yExtent, zExtent);
+        hash = 59 * hash + super.hashCode();
+
+        return hash;
+    }
+
+    /**
+     * Tests for approximate equality with the specified bounding box, using the
+     * specified tolerance. If {@code other} is null, false is returned. Either
+     * way, the current instance is unaffected.
+     *
+     * @param aabb the bounding box to compare (unaffected) or null for none
+     * @param epsilon the tolerance for each component
+     * @return true if all components are within tolerance, otherwise false
+     */
+    public boolean isSimilar(BoundingBox aabb, float epsilon) {
+        if (aabb == null) {
+            return false;
+        } else if (Float.compare(Math.abs(aabb.xExtent - xExtent), epsilon) > 0) {
+            return false;
+        } else if (Float.compare(Math.abs(aabb.yExtent - yExtent), epsilon) > 0) {
+            return false;
+        } else if (Float.compare(Math.abs(aabb.zExtent - zExtent), epsilon) > 0) {
+            return false;
+        } else if (!center.isSimilar(aabb.getCenter(), epsilon)) {
+            return false;
+        }
+        // The checkPlane field is ignored.
+        return true;
+    }
+
     /**
      * <code>toString</code> returns the string representation of this object.
      * The form is: "[Center: vector xExtent: X.XX yExtent: Y.YY zExtent:

+ 64 - 1
jme3-core/src/main/java/com/jme3/bounding/BoundingSphere.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -43,6 +43,7 @@ import com.jme3.util.BufferUtils;
 import com.jme3.util.TempVars;
 import java.io.IOException;
 import java.nio.FloatBuffer;
+import java.util.Objects;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -651,6 +652,68 @@ public class BoundingSphere extends BoundingVolume {
         return new BoundingSphere(radius, center.clone());
     }
 
+    /**
+     * Tests for exact equality with the argument, distinguishing -0 from 0. If
+     * {@code other} is null, false is returned. Either way, the current
+     * instance is unaffected.
+     *
+     * @param other the object to compare (may be null, unaffected)
+     * @return true if {@code this} and {@code other} have identical values,
+     *     otherwise false
+     */
+    @Override
+    public boolean equals(Object other) {
+        if (!(other instanceof BoundingSphere)) {
+            return false;
+        }
+
+        if (this == other) {
+            return true;
+        }
+
+        BoundingSphere otherBoundingSphere = (BoundingSphere) other;
+        if (Float.compare(radius, otherBoundingSphere.getRadius()) != 0) {
+            return false;
+        } else {
+            return super.equals(otherBoundingSphere);
+        }
+    }
+
+    /**
+     * Returns a hash code. If two bounding boxes have identical values, they
+     * will have the same hash code. The current instance is unaffected.
+     *
+     * @return a 32-bit value for use in hashing
+     */
+    @Override
+    public int hashCode() {
+        int hash = Objects.hash(radius);
+        hash = 59 * hash + super.hashCode();
+
+        return hash;
+    }
+
+    /**
+     * Tests for approximate equality with the specified bounding sphere, using
+     * the specified tolerance. If {@code other} is null, false is returned.
+     * Either way, the current instance is unaffected.
+     *
+     * @param sphere the bounding sphere to compare (unaffected) or null for none
+     * @param epsilon the tolerance for each component
+     * @return true if all components are within tolerance, otherwise false
+     */
+    public boolean isSimilar(BoundingSphere sphere, float epsilon) {
+        if (sphere == null) {
+            return false;
+        } else if (Float.compare(Math.abs(sphere.getRadius() - radius), epsilon) > 0) {
+            return false;
+        } else if (!center.isSimilar(sphere.getCenter(), epsilon)) {
+            return false;
+        }
+        // The checkPlane field is ignored.
+        return true;
+    }
+
     /**
      * <code>toString</code> returns the string representation of this object.
      * The form is: "Radius: RRR.SSSS Center: vector".

+ 44 - 1
jme3-core/src/main/java/com/jme3/bounding/BoundingVolume.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -40,6 +40,7 @@ import com.jme3.math.*;
 import com.jme3.util.TempVars;
 import java.io.IOException;
 import java.nio.FloatBuffer;
+import java.util.Objects;
 
 /**
  * <code>BoundingVolume</code> defines an interface for dealing with
@@ -180,6 +181,48 @@ public abstract class BoundingVolume implements Savable, Cloneable, Collidable {
      */
     public abstract BoundingVolume clone(BoundingVolume store);
 
+    /**
+     * Tests for exact equality with the argument, distinguishing -0 from 0. If
+     * {@code other} is null, false is returned. Either way, the current
+     * instance is unaffected.
+     *
+     * @param other the object to compare (may be null, unaffected)
+     * @return true if {@code this} and {@code other} have identical values,
+     *     otherwise false
+     */
+    @Override
+    public boolean equals(Object other) {
+        if (!(other instanceof BoundingVolume)) {
+            return false;
+        }
+
+        if (this == other) {
+            return true;
+        }
+
+        BoundingVolume otherBoundingVolume = (BoundingVolume) other;
+        if (!center.equals(otherBoundingVolume.getCenter())) {
+            return false;
+        }
+        // The checkPlane field is ignored.
+
+        return true;
+    }
+
+    /**
+     * Returns a hash code. If two bounding volumes have identical values, they
+     * will have the same hash code. The current instance is unaffected.
+     *
+     * @return a 32-bit value for use in hashing
+     */
+    @Override
+    public int hashCode() {
+        int hash = Objects.hash(center);
+        // The checkPlane field is ignored.
+
+        return hash;
+    }
+
     public final Vector3f getCenter() {
         return center;
     }

+ 1 - 1
jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java

@@ -93,7 +93,7 @@ public class Cinematic extends AbstractCinematicEvent implements AppState {
     private Node scene;
     protected TimeLine timeLine = new TimeLine();
     private int lastFetchedKeyFrame = -1;
-    final private List<CinematicEvent> cinematicEvents = new ArrayList<>();
+    private final List<CinematicEvent> cinematicEvents = new ArrayList<>();
     private Map<String, CameraNode> cameras = new HashMap<>();
     private CameraNode currentCam;
     private boolean initialized = false;

+ 1 - 1
jme3-core/src/main/java/com/jme3/cinematic/events/AnimEvent.java

@@ -53,7 +53,7 @@ import java.util.logging.Logger;
  */
 public class AnimEvent extends AbstractCinematicEvent {
 
-    final public static Logger logger
+    public static final Logger logger
             = Logger.getLogger(AnimEvent.class.getName());
 
     /*

+ 14 - 9
jme3-core/src/main/java/com/jme3/collision/bih/BIHNode.java

@@ -411,15 +411,20 @@ public final class BIHNode implements Savable {
                         t = t_world;
                     }
 
-                    Vector3f contactNormal = Triangle.computeTriangleNormal(v1, v2, v3, null);
-                    Vector3f contactPoint = new Vector3f(d).multLocal(t).addLocal(o);
-                    float worldSpaceDist = o.distance(contactPoint);
-
-                    CollisionResult cr = new CollisionResult(contactPoint, worldSpaceDist);
-                    cr.setContactNormal(contactNormal);
-                    cr.setTriangleIndex(tree.getTriangleIndex(i));
-                    results.addCollision(cr);
-                    cols++;
+                    // this second isInfinite test is unlikely to fail but due to numeric precision it might
+                    // be the case that in local coordinates it just hits and in world coordinates it just misses
+                    // this filters those cases out (treating them as misses).
+                    if (!Float.isInfinite(t)){
+                        Vector3f contactNormal = Triangle.computeTriangleNormal(v1, v2, v3, null);
+                        Vector3f contactPoint = new Vector3f(d).multLocal(t).addLocal(o);
+                        float worldSpaceDist = o.distance(contactPoint);
+
+                        CollisionResult cr = new CollisionResult(contactPoint, worldSpaceDist);
+                        cr.setContactNormal(contactNormal);
+                        cr.setTriangleIndex(tree.getTriangleIndex(i));
+                        results.addCollision(cr);
+                        cols++;
+                    }
                 }
             }
         }

+ 351 - 0
jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java

@@ -0,0 +1,351 @@
+/*
+ * Copyright (c) 2009-2023 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.environment;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Predicate;
+import com.jme3.asset.AssetManager;
+import com.jme3.environment.baker.IBLGLEnvBakerLight;
+import com.jme3.environment.baker.IBLHybridEnvBakerLight;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.light.LightProbe;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.control.Control;
+import com.jme3.texture.Image.Format;
+
+/**
+ * A control that automatically handles environment bake and rebake including
+ * only tagged spatials.
+ * 
+ * Simple usage example: <code>
+ * 1. Load a scene
+ *    Node scene=(Node)assetManager.loadModel("Scenes/MyScene.j3o"); 
+ * 2. Add one or more EnvironmentProbeControl to the root of the scene
+ *    EnvironmentProbeControl ec1=new EnvironmentProbeControl(assetManager, 512);
+ *    //  EnvironmentProbeControl ec2=new EnvironmentProbeControl(assetManager, 512);
+ * 2b. (optional) Set the position of the probes
+ *    ec1.setPosition(new Vector3f(0,0,0));
+ *    // ec2.setPosition(new Vector3f(0,0,10));
+ * 3. Tag the spatials that are part of the environment
+ *    scene.deepFirstTraversal(s->{
+ *        if(s.getUserData("isEnvNode")!=null){
+ *          EnvironmentProbeControl.tagGlobal(s);
+ *          // or ec1.tag(s); 
+ *          //    ec2.tag(s);
+ *        }
+ *    });
+ *</code>
+ * 
+ * @author Riccardo Balbo
+ */
+public class EnvironmentProbeControl extends LightProbe implements Control {
+    private static AtomicInteger instanceCounter = new AtomicInteger(0);
+
+    private AssetManager assetManager;
+    private boolean bakeNeeded = true;
+    private int envMapSize = 256;
+    private Spatial spatial;
+    private boolean requiredSavableResults = false;
+    private float frustumNear = 0.001f, frustumFar = 1000f;
+    private String uuid = "none";
+    private boolean enabled = true;
+
+    private Predicate<Geometry> filter = (s) -> {
+        return s.getUserData("tags.env") != null || s.getUserData("tags.env.env" + uuid) != null;
+    };
+
+    protected EnvironmentProbeControl() {
+        super();
+        uuid = System.currentTimeMillis() + "_" + instanceCounter.getAndIncrement();
+        this.setAreaType(AreaType.Spherical);
+        this.getArea().setRadius(Float.MAX_VALUE);
+    }
+
+    /**
+     * Creates a new environment probe control.
+     * 
+     * @param assetManager
+     *            the asset manager used to load the shaders needed for the
+     *            baking
+     * @param size
+     *            the size of side of the resulting cube map (eg. 1024)
+     */
+    public EnvironmentProbeControl(AssetManager assetManager, int size) {
+        this();
+        this.envMapSize = size;
+        this.assetManager = assetManager;        
+    }
+
+    /**
+     * Tags the specified spatial as part of the environment for this EnvironmentProbeControl.
+     * Only tagged spatials will be rendered in the environment map.
+     * 
+     * @param s
+     *            the spatial
+     */
+    public void tag(Spatial s) {
+        if (s instanceof Node) {
+            Node n = (Node) s;
+            for (Spatial sx : n.getChildren()) {
+                tag(sx);
+            }
+        } else if (s instanceof Geometry) {
+            s.setUserData("tags.env.env" + uuid, true);
+        }
+    }
+
+    /**
+     * Untags the specified spatial as part of the environment for this
+     * EnvironmentProbeControl.
+     * 
+     * @param s
+     *            the spatial
+     */
+    public void untag(Spatial s) {
+        if (s instanceof Node) {
+            Node n = (Node) s;
+            for (Spatial sx : n.getChildren()) {
+                untag(sx);
+            }
+        } else if (s instanceof Geometry) {
+            s.setUserData("tags.env.env" + uuid, null);
+        }
+    }
+
+    /**
+     * Tags the specified spatial as part of the environment for every EnvironmentProbeControl.
+     * Only tagged spatials will be rendered in the environment map.
+     * 
+     * @param s
+     *            the spatial
+     */
+    public static void tagGlobal(Spatial s) {
+        if (s instanceof Node) {
+            Node n = (Node) s;
+            for (Spatial sx : n.getChildren()) {
+                tagGlobal(sx);
+            }
+        } else if (s instanceof Geometry) {
+            s.setUserData("tags.env", true);
+        }
+    }
+
+    /**
+     * Untags the specified spatial as part of the environment for every
+     * EnvironmentProbeControl.
+     * 
+     * @param s the spatial
+     */
+    public static void untagGlobal(Spatial s) {
+        if (s instanceof Node) {
+            Node n = (Node) s;
+            for (Spatial sx : n.getChildren()) {
+                untagGlobal(sx);
+            }
+        } else if (s instanceof Geometry) {
+            s.setUserData("tags.env", null);
+        }
+    }
+
+    @Override
+    public Control cloneForSpatial(Spatial spatial) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Requests savable results from the baking process. This will make the
+     * baking process slower and more memory intensive but will allow to
+     * serialize the results with the control.
+     * 
+     * @param v
+     *            true to enable (default: false)
+     */
+    public void setRequiredSavableResults(boolean v) {
+        requiredSavableResults = v;
+    }
+
+    /**
+     * Returns true if savable results are required by this control.
+     * 
+     * @return true if savable results are required.
+     */
+    public boolean isRequiredSavableResults() {
+        return requiredSavableResults;
+    }
+
+    @Override
+    public void setSpatial(Spatial spatial) {
+        if (this.spatial != null && spatial != null && spatial != this.spatial) {
+            throw new IllegalStateException("This control has already been added to a Spatial");
+        }
+        this.spatial = spatial;
+        if (spatial != null) spatial.addLight(this);
+    }
+
+    @Override
+    public void update(float tpf) {
+
+    }
+
+    @Override
+    public void render(RenderManager rm, ViewPort vp) {
+        if (!isEnabled()) return;
+        if (bakeNeeded) {
+            bakeNeeded = false;
+            rebakeNow(rm);
+        }
+    }
+
+    /**
+     * Schedules a rebake of the environment map.
+     */
+    public void rebake() {
+        bakeNeeded = true;
+    }
+
+    /**
+     * Sets the minimum distance to render.
+     * 
+     * @param frustumNear the minimum distance to render
+     */
+    public void setFrustumNear(float frustumNear) {
+        this.frustumNear = frustumNear;
+    }
+
+    /**
+     * Sets the maximum distance to render.
+     * 
+     * @param frustumFar the maximum distance to render
+     */
+    public void setFrustumFar(float frustumFar) {
+        this.frustumFar = frustumFar;
+    }
+
+    /**
+     * Gets the minimum distance to render.
+     * 
+     * @return frustum near
+     */
+    public float getFrustumNear() {
+        return frustumNear;
+    }
+
+    /**
+     * Gets the maximum distance to render.
+     * 
+     * @return frustum far
+     */
+    public float getFrustumFar() {
+        return frustumFar;
+    }
+
+    /**
+     * Sets the asset manager used to load the shaders needed for the baking.
+     * 
+     * @param assetManager the asset manager
+     */
+    public void setAssetManager(AssetManager assetManager) {
+        this.assetManager = assetManager;
+    }
+
+    void rebakeNow(RenderManager renderManager) {
+        IBLHybridEnvBakerLight baker = new IBLGLEnvBakerLight(renderManager, assetManager, Format.RGB16F, Format.Depth,
+                envMapSize, envMapSize);
+                    
+        baker.setTexturePulling(isRequiredSavableResults());
+        baker.bakeEnvironment(spatial, getPosition(), frustumNear, frustumFar, filter);
+        baker.bakeSpecularIBL();
+        baker.bakeSphericalHarmonicsCoefficients();
+
+        setPrefilteredMap(baker.getSpecularIBL());
+
+        int[] mipSizes = getPrefilteredEnvMap().getImage().getMipMapSizes();
+        setNbMipMaps(mipSizes != null ? mipSizes.length : 1);
+
+        setShCoeffs(baker.getSphericalHarmonicsCoefficients());
+        setPosition(Vector3f.ZERO);
+        setReady(true);
+
+        baker.clean();
+    }
+    
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    public Spatial getSpatial() {
+        return spatial;
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(enabled, "enabled", true);
+        oc.write(spatial, "spatial", null);
+        oc.write(envMapSize, "size", 256);
+        oc.write(requiredSavableResults, "requiredSavableResults", false);
+        oc.write(bakeNeeded, "bakeNeeded", true);
+        oc.write(frustumFar, "frustumFar", 1000f);
+        oc.write(frustumNear, "frustumNear", 0.001f);
+        oc.write(uuid, "envProbeControlUUID", "none");
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+        enabled = ic.readBoolean("enabled", true);
+        spatial = (Spatial) ic.readSavable("spatial", null);
+        envMapSize = ic.readInt("size", 256);
+        requiredSavableResults = ic.readBoolean("requiredSavableResults", false);
+        bakeNeeded = ic.readBoolean("bakeNeeded", true);
+        assetManager = im.getAssetManager();
+        frustumFar = ic.readFloat("frustumFar", 1000f);
+        frustumNear = ic.readFloat("frustumNear", 0.001f);
+        uuid = ic.readString("envProbeControlUUID", "none");
+    }
+
+}

+ 122 - 0
jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java

@@ -0,0 +1,122 @@
+/*
+ * Copyright (c) 2009-2023 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.environment;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.environment.baker.IBLGLEnvBakerLight;
+import com.jme3.environment.util.EnvMapUtils;
+import com.jme3.light.LightProbe;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.RenderManager;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.texture.Image.Format;
+
+/**
+ * A faster LightProbeFactory that uses GPU accelerated algorithms.
+ * This is the GPU version of @{link LightProbeFactory} and should be generally preferred.
+ * 
+ * For common use cases where the probe is baking the scene or part of the scene around it, it
+ * is advised to use the @{link EnvironmentProbeControl} instead since it does automatically most of the 
+ * boilerplate work.
+ * 
+ * 
+ * @author Riccardo Balbo
+ */
+public class FastLightProbeFactory {
+
+    /**
+     * Creates a LightProbe with the given EnvironmentCamera in the given scene.
+     * 
+     * @param rm
+     *            The RenderManager
+     * @param am
+     *            The AssetManager
+     * @param size
+     *            The size of the probe
+     * @param pos
+     *            The position of the probe
+     * @param frustumNear
+     *            The near frustum of the probe
+     * @param frustumFar
+     *            The far frustum of the probe
+     * @param scene
+     *            The scene to bake
+     * @return The baked LightProbe
+     */
+    public static LightProbe makeProbe(RenderManager rm, AssetManager am, int size, Vector3f pos, float frustumNear, float frustumFar, Spatial scene) {
+        IBLGLEnvBakerLight baker = new IBLGLEnvBakerLight(rm, am, Format.RGB16F, Format.Depth, size, size);
+
+        baker.setTexturePulling(true);
+        baker.bakeEnvironment(scene, pos, frustumNear, frustumFar, null);
+        baker.bakeSpecularIBL();
+        baker.bakeSphericalHarmonicsCoefficients();
+
+        LightProbe probe = new LightProbe();
+
+        probe.setPosition(pos);
+        probe.setPrefilteredMap(baker.getSpecularIBL());
+
+        int[] mipSizes = probe.getPrefilteredEnvMap().getImage().getMipMapSizes();
+        probe.setNbMipMaps(mipSizes != null ? mipSizes.length : 1);
+
+        probe.setShCoeffs(baker.getSphericalHarmonicsCoefficients());
+        probe.setReady(true);
+
+        baker.clean();
+
+        return probe;
+
+    }
+
+    /**
+     * For debuging purposes only Will return a Node meant to be added to a GUI
+     * presenting the 2 cube maps in a cross pattern with all the mip maps.
+     *
+     * @param manager
+     *            the asset manager
+     * @return a debug node
+     */
+    public static Node getDebugGui(AssetManager manager, LightProbe probe) {
+        if (!probe.isReady()) {
+            throw new UnsupportedOperationException("This EnvProbe is not ready yet, try to test isReady()");
+        }
+
+        Node debugNode = new Node("debug gui probe");
+        Node debugPfemCm = EnvMapUtils.getCubeMapCrossDebugViewWithMipMaps(probe.getPrefilteredEnvMap(), manager);
+        debugNode.attachChild(debugPfemCm);
+        debugPfemCm.setLocalTranslation(520, 0, 0);
+
+        return debugNode;
+    }
+
+}

+ 89 - 0
jme3-core/src/main/java/com/jme3/environment/baker/EnvBaker.java

@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2009-2023 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.environment.baker;
+
+import java.util.function.Predicate;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Spatial;
+import com.jme3.texture.TextureCubeMap;
+
+/**
+ * An environment baker to bake a 3d environment into a cubemap
+ *
+ * @author Riccardo Balbo
+ */
+public interface EnvBaker {
+    /**
+     * Bakes the environment.
+     * 
+     * @param scene
+     *            The scene to bake
+     * @param position
+     *            The position of the camera
+     * @param frustumNear
+     *            The near frustum
+     * @param frustumFar
+     *            The far frustum
+     * @param filter
+     *            A filter to select which geometries to bake
+     */
+    public void bakeEnvironment(Spatial scene, Vector3f position, float frustumNear, float frustumFar, Predicate<Geometry> filter);
+
+    /**
+     * Gets the environment map.
+     * 
+     * @return The environment map
+     */
+    public TextureCubeMap getEnvMap();
+
+    /**
+     * Cleans the environment baker This method should be called when the baker
+     * is no longer needed It will clean up all the resources.
+     */
+    public void clean();
+
+    /**
+     * Specifies whether textures should be pulled from the GPU.
+     * 
+     * @param v
+     */
+    public void setTexturePulling(boolean v);
+
+    /**
+     * Gets if textures should be pulled from the GPU.
+     * 
+     * @return
+     */
+    public boolean isTexturePulling();
+}

+ 293 - 0
jme3-core/src/main/java/com/jme3/environment/baker/GenericEnvBaker.java

@@ -0,0 +1,293 @@
+/*
+ * Copyright (c) 2009-2023 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.environment.baker;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import com.jme3.asset.AssetManager;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.Camera;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Spatial;
+import com.jme3.texture.FrameBuffer;
+import com.jme3.texture.Texture;
+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.WrapMode;
+import com.jme3.texture.TextureCubeMap;
+import com.jme3.texture.image.ColorSpace;
+import com.jme3.util.BufferUtils;
+
+/**
+ * Render the environment into a cubemap
+ *
+ * @author Riccardo Balbo
+ */
+public abstract class GenericEnvBaker implements EnvBaker {
+    private static final Logger LOG = Logger.getLogger(GenericEnvBaker.class.getName());
+
+    protected static Vector3f[] axisX = new Vector3f[6];
+    protected static Vector3f[] axisY = new Vector3f[6];
+    protected static Vector3f[] axisZ = new Vector3f[6];
+    static {
+        // PositiveX axis(left, up, direction)
+        axisX[0] = Vector3f.UNIT_Z.mult(1.0F);
+        axisY[0] = Vector3f.UNIT_Y.mult(-1.0F);
+        axisZ[0] = Vector3f.UNIT_X.mult(1.0F);
+        // NegativeX
+        axisX[1] = Vector3f.UNIT_Z.mult(-1.0F);
+        axisY[1] = Vector3f.UNIT_Y.mult(-1.0F);
+        axisZ[1] = Vector3f.UNIT_X.mult(-1.0F);
+        // PositiveY
+        axisX[2] = Vector3f.UNIT_X.mult(-1.0F);
+        axisY[2] = Vector3f.UNIT_Z.mult(1.0F);
+        axisZ[2] = Vector3f.UNIT_Y.mult(1.0F);
+        // NegativeY
+        axisX[3] = Vector3f.UNIT_X.mult(-1.0F);
+        axisY[3] = Vector3f.UNIT_Z.mult(-1.0F);
+        axisZ[3] = Vector3f.UNIT_Y.mult(-1.0F);
+        // PositiveZ
+        axisX[4] = Vector3f.UNIT_X.mult(-1.0F);
+        axisY[4] = Vector3f.UNIT_Y.mult(-1.0F);
+        axisZ[4] = Vector3f.UNIT_Z;
+        // NegativeZ
+        axisX[5] = Vector3f.UNIT_X.mult(1.0F);
+        axisY[5] = Vector3f.UNIT_Y.mult(-1.0F);
+        axisZ[5] = Vector3f.UNIT_Z.mult(-1.0F);
+    }
+
+    protected TextureCubeMap envMap;
+    protected Format depthFormat;
+
+    protected final RenderManager renderManager;
+    protected final AssetManager assetManager;
+    protected final Camera cam;
+    protected boolean texturePulling = false;
+    protected List<ByteArrayOutputStream> bos = new ArrayList<>();
+
+    protected GenericEnvBaker(RenderManager rm, AssetManager am, Format colorFormat, Format depthFormat, int env_size) {
+        this.depthFormat = depthFormat;
+
+        renderManager = rm;
+        assetManager = am;
+
+        cam = new Camera(128, 128);
+
+        envMap = new TextureCubeMap(env_size, env_size, colorFormat);
+        envMap.setMagFilter(MagFilter.Bilinear);
+        envMap.setMinFilter(MinFilter.BilinearNoMipMaps);
+        envMap.setWrap(WrapMode.EdgeClamp);
+        envMap.getImage().setColorSpace(ColorSpace.Linear);
+    }
+
+    @Override
+    public void setTexturePulling(boolean v) {
+        texturePulling = v;
+    }
+
+    @Override
+    public boolean isTexturePulling() {
+        return texturePulling;
+    }
+
+    public TextureCubeMap getEnvMap() {
+        return envMap;
+    }
+
+    /**
+     * Updates the internal camera to face the given cubemap face
+     * and return it.
+     * 
+     * @param faceId
+     *            the id of the face (0-5)
+     * @param w
+     *            width of the camera
+     * @param h
+     *            height of the camera
+     * @param position
+     *            position of the camera
+     * @param frustumNear
+     *            near frustum
+     * @param frustumFar
+     *            far frustum
+     * @return The updated camera
+     */
+    protected Camera updateAndGetInternalCamera(int faceId, int w, int h, Vector3f position, float frustumNear, float frustumFar) {
+        cam.resize(w, h, false);
+        cam.setLocation(position);
+        cam.setFrustumPerspective(90.0F, 1F, frustumNear, frustumFar);
+        cam.setRotation(new Quaternion().fromAxes(axisX[faceId], axisY[faceId], axisZ[faceId]));
+        return cam;
+    }
+
+    @Override
+    public void clean() {
+
+    }
+
+    @Override
+    public void bakeEnvironment(Spatial scene, Vector3f position, float frustumNear, float frustumFar, Predicate<Geometry> filter) {
+        FrameBuffer envbakers[] = new FrameBuffer[6];
+        for (int i = 0; i < 6; i++) {
+            envbakers[i] = new FrameBuffer(envMap.getImage().getWidth(), envMap.getImage().getHeight(), 1);
+            envbakers[i].setDepthTarget(FrameBufferTarget.newTarget(depthFormat));
+            envbakers[i].setSrgb(false);
+            envbakers[i].addColorTarget(FrameBufferTarget.newTarget(envMap).face(TextureCubeMap.Face.values()[i]));
+        }
+
+        if (isTexturePulling()) {
+            startPulling();
+        }
+
+        for (int i = 0; i < 6; i++) {
+            FrameBuffer envbaker = envbakers[i];
+
+            ViewPort viewPort = new ViewPort("EnvBaker", updateAndGetInternalCamera(i, envbaker.getWidth(), envbaker.getHeight(), position, frustumNear, frustumFar));
+            viewPort.setClearFlags(true, true, true);
+            viewPort.setBackgroundColor(ColorRGBA.Pink);
+
+            viewPort.setOutputFrameBuffer(envbaker);
+            viewPort.clearScenes();
+            viewPort.attachScene(scene);
+
+            scene.updateLogicalState(0);
+            scene.updateGeometricState();
+
+            Predicate<Geometry> ofilter = renderManager.getRenderFilter();
+
+            renderManager.setRenderFilter(filter);
+            renderManager.renderViewPort(viewPort, 0.16f);
+            renderManager.setRenderFilter(ofilter);
+
+            if (isTexturePulling()) {
+                pull(envbaker, envMap, i);
+            }
+
+        }
+
+        if (isTexturePulling()) {
+            endPulling(envMap);
+        }
+
+        envMap.getImage().clearUpdateNeeded();
+
+        for (int i = 0; i < 6; i++) {
+            envbakers[i].dispose();
+        }
+    }
+
+    /**
+     * Starts pulling the data from the framebuffer into the texture.
+     */
+    protected void startPulling() {
+        bos.clear();
+    }
+
+    /**
+     * Pulls the data from the framebuffer into the texture Nb. mipmaps must be
+     * pulled sequentially on the same faceId.
+     * 
+     * @param fb
+     *            the framebuffer to pull from
+     * @param env
+     *            the texture to pull into
+     * @param faceId
+     *            id of face if cubemap or 0 otherwise
+     * @return the ByteBuffer containing the pulled data
+     */
+    protected ByteBuffer pull(FrameBuffer fb, Texture env, int faceId) {
+
+        if (fb.getColorTarget().getFormat() != env.getImage().getFormat())
+            throw new IllegalArgumentException("Format mismatch: " + fb.getColorTarget().getFormat() + "!=" + env.getImage().getFormat());
+
+        ByteBuffer face = BufferUtils.createByteBuffer(fb.getWidth() * fb.getHeight() * (fb.getColorTarget().getFormat().getBitsPerPixel() / 8));
+        renderManager.getRenderer().readFrameBufferWithFormat(fb, face, fb.getColorTarget().getFormat());
+        face.rewind();
+
+        while (bos.size() <= faceId) {
+            bos.add(null);
+        }
+
+        ByteArrayOutputStream bo = bos.get(faceId);
+        if (bo == null) {
+            bos.set(faceId, bo = new ByteArrayOutputStream());
+        }
+        try {
+            byte array[] = new byte[face.limit()];
+            face.get(array);
+            bo.write(array);
+        } catch (Exception ex) {
+            LOG.log(Level.SEVERE, null, ex);
+        }
+        return face;
+    }
+
+    /**
+     * Ends pulling the data into the texture
+     * 
+     * @param tx
+     *            the texture to pull into
+     */
+    protected void endPulling(Texture tx) {
+        for (int i = 0; i < bos.size(); i++) {
+            ByteArrayOutputStream bo = bos.get(i);
+            if (bo != null) {
+                ByteBuffer faceMip = ByteBuffer.wrap(bo.toByteArray());
+                tx.getImage().setData(i, faceMip);
+            } else {
+                LOG.log(Level.SEVERE, "Missing face {0}. Pulling incomplete!", i);
+            }
+        }
+        bos.clear();
+        tx.getImage().clearUpdateNeeded();
+    }
+
+    protected int limitMips(int nbMipMaps, int baseW, int baseH, RenderManager rm) {
+        if (nbMipMaps > 6) {
+            nbMipMaps = 6;
+        }
+        return nbMipMaps;
+    }
+
+}

+ 74 - 0
jme3-core/src/main/java/com/jme3/environment/baker/IBLEnvBaker.java

@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2009-2023 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.environment.baker;
+
+import com.jme3.texture.Texture2D;
+import com.jme3.texture.TextureCubeMap;
+
+/**
+ * An environment baker, but this one is for Imaged Base Lighting.
+ *
+ * @author Riccardo Balbo
+ */
+public interface IBLEnvBaker extends EnvBaker {
+    /**
+     * Generates the BRDF texture.
+     * 
+     * @return The BRDF texture
+     */
+    public Texture2D genBRTF();
+
+    /**
+     * Bakes the irradiance map.
+     */
+    public void bakeIrradiance();
+
+    /**
+     * Bakes the specular IBL map.
+     */
+    public void bakeSpecularIBL();
+
+    /**
+     * Gets the specular IBL map.
+     * 
+     * @return The specular IBL map
+     */
+    public TextureCubeMap getSpecularIBL();
+
+    /**
+     * Gets the irradiance map.
+     * 
+     * @return The irradiance map
+     */
+    public TextureCubeMap getIrradiance();
+}

+ 52 - 0
jme3-core/src/main/java/com/jme3/environment/baker/IBLEnvBakerLight.java

@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2009-2023 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.environment.baker;
+
+import com.jme3.math.Vector3f;
+import com.jme3.texture.TextureCubeMap;
+
+/**
+ * An environment baker for IBL, that uses spherical harmonics for irradiance.
+ * 
+ * @author Riccardo Balbo
+ */
+public interface IBLEnvBakerLight extends EnvBaker {
+
+    public void bakeSpecularIBL();
+
+    public void bakeSphericalHarmonicsCoefficients();
+
+    public TextureCubeMap getSpecularIBL();
+
+    public Vector3f[] getSphericalHarmonicsCoefficients();
+}

+ 296 - 0
jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBaker.java

@@ -0,0 +1,296 @@
+/*
+ * 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.environment.baker;
+
+import java.util.Arrays;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import com.jme3.asset.AssetManager;
+import com.jme3.material.Material;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.Camera;
+import com.jme3.renderer.RenderManager;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Box;
+import com.jme3.texture.FrameBuffer;
+import com.jme3.texture.Image.Format;
+import com.jme3.texture.Texture.MagFilter;
+import com.jme3.texture.Texture.MinFilter;
+import com.jme3.texture.Texture.WrapMode;
+import com.jme3.texture.Texture2D;
+import com.jme3.texture.TextureCubeMap;
+import com.jme3.texture.FrameBuffer.FrameBufferTarget;
+import com.jme3.texture.image.ColorSpace;
+import com.jme3.ui.Picture;
+
+/**
+ * Fully accelerated env baker for IBL that runs entirely on the GPU
+ * 
+ * @author Riccardo Balbo
+ */
+public class IBLGLEnvBaker extends GenericEnvBaker implements IBLEnvBaker {
+    private static final Logger LOGGER = Logger.getLogger(IBLGLEnvBakerLight.class.getName());
+
+    protected Texture2D brtf;
+    protected TextureCubeMap irradiance;
+    protected TextureCubeMap specular;
+
+    /**
+     * Create a new IBL env baker
+     * @param rm The render manager used to render the env scene
+     * @param am The asset manager used to load the baking shaders
+     * @param format  The format of the color buffers
+     * @param depthFormat The format of the depth buffers
+     * @param env_size The size in pixels of the output environment cube map (eg. 1024)
+     * @param specular_size The size in pixels of the output specular cube map (eg. 1024)
+     * @param irradiance_size The size in pixels of the output irradiance cube map (eg. 512)
+     * @param brtf_size The size in pixels of the output brtf map (eg. 512)
+     */
+    public IBLGLEnvBaker(RenderManager rm, AssetManager am, Format format, Format depthFormat, int env_size, int specular_size, int irradiance_size, int brtf_size) {
+        super(rm, am, format, depthFormat, env_size);
+
+        irradiance = new TextureCubeMap(irradiance_size, irradiance_size, format);
+        irradiance.setMagFilter(MagFilter.Bilinear);
+        irradiance.setMinFilter(MinFilter.BilinearNoMipMaps);
+        irradiance.setWrap(WrapMode.EdgeClamp);
+        irradiance.getImage().setColorSpace(ColorSpace.Linear);
+
+        specular = new TextureCubeMap(specular_size, specular_size, format);
+        specular.setMagFilter(MagFilter.Bilinear);
+        specular.setMinFilter(MinFilter.Trilinear);
+        specular.setWrap(WrapMode.EdgeClamp);
+        specular.getImage().setColorSpace(ColorSpace.Linear);
+
+        int nbMipMaps = (int) (Math.log(specular_size) / Math.log(2) + 1);
+        nbMipMaps = limitMips(nbMipMaps, specular.getImage().getWidth(), specular.getImage().getHeight(), rm);
+
+        int[] sizes = new int[nbMipMaps];
+        for (int i = 0; i < nbMipMaps; i++) {
+            int size = (int) FastMath.pow(2, nbMipMaps - 1 - i);
+            sizes[i] = size * size * (specular.getImage().getFormat().getBitsPerPixel() / 8);
+        }
+        specular.getImage().setMipMapSizes(sizes);
+
+        brtf = new Texture2D(brtf_size, brtf_size, format);
+        brtf.setMagFilter(MagFilter.Bilinear);
+        brtf.setMinFilter(MinFilter.BilinearNoMipMaps);
+        brtf.setWrap(WrapMode.EdgeClamp);
+        brtf.getImage().setColorSpace(ColorSpace.Linear);
+    }
+
+    @Override
+    public TextureCubeMap getSpecularIBL() {
+        return specular;
+    }
+
+    @Override
+    public TextureCubeMap getIrradiance() {
+        return irradiance;
+    }
+
+    private void bakeSpecularIBL(int mip, float roughness, Material mat, Geometry screen) throws Exception {
+        mat.setFloat("Roughness", roughness);
+
+        int mipWidth = (int) (specular.getImage().getWidth() * FastMath.pow(0.5f, mip));
+        int mipHeight = (int) (specular.getImage().getHeight() * FastMath.pow(0.5f, mip));
+
+        FrameBuffer specularbakers[] = new FrameBuffer[6];
+        for (int i = 0; i < 6; i++) {
+            specularbakers[i] = new FrameBuffer(mipWidth, mipHeight, 1);
+            specularbakers[i].setSrgb(false);
+            specularbakers[i].addColorTarget(FrameBufferTarget.newTarget(specular).level(mip).face(i));
+            specularbakers[i].setMipMapsGenerationHint(false);
+        }
+
+        for (int i = 0; i < 6; i++) {
+            FrameBuffer specularbaker = specularbakers[i];
+            mat.setInt("FaceId", i);
+
+            screen.updateLogicalState(0);
+            screen.updateGeometricState();
+
+            renderManager.setCamera(updateAndGetInternalCamera(i, specularbaker.getWidth(), specularbaker.getHeight(), Vector3f.ZERO, 1, 1000), false);
+            renderManager.getRenderer().setFrameBuffer(specularbaker);
+            renderManager.renderGeometry(screen);
+
+            if (isTexturePulling()) {
+                pull(specularbaker, specular, i);
+            }
+
+        }
+        for (int i = 0; i < 6; i++) {
+            specularbakers[i].dispose();
+        }
+    }
+
+    @Override
+    public void bakeSpecularIBL() {
+        Box boxm = new Box(1, 1, 1);
+        Geometry screen = new Geometry("BakeBox", boxm);
+
+        Material mat = new Material(assetManager, "Common/IBL/IBLKernels.j3md");
+        mat.setBoolean("UseSpecularIBL", true);
+        mat.setTexture("EnvMap", envMap);
+        screen.setMaterial(mat);
+
+        if (isTexturePulling()) {
+            startPulling();
+        }
+
+        int mip = 0;
+        for (; mip < specular.getImage().getMipMapSizes().length; mip++) {
+            try {
+                float roughness = (float) mip / (float) (specular.getImage().getMipMapSizes().length - 1);
+                bakeSpecularIBL(mip, roughness, mat, screen);
+            } catch (Exception e) {
+                LOGGER.log(Level.WARNING, "Error while computing mip level " + mip, e);
+                break;
+            }
+        }
+
+        if (mip < specular.getImage().getMipMapSizes().length) {
+
+            int[] sizes = specular.getImage().getMipMapSizes();
+            sizes = Arrays.copyOf(sizes, mip);
+            specular.getImage().setMipMapSizes(sizes);
+            specular.getImage().setMipmapsGenerated(true);
+            if (sizes.length <= 1) {
+                try {
+                    LOGGER.log(Level.WARNING, "Workaround driver BUG: only one mip level available, regenerate it with higher roughness (shiny fix)");
+                    bakeSpecularIBL(0, 1f, mat, screen);
+                } catch (Exception e) {
+                    LOGGER.log(Level.FINE, "Error while recomputing mip level 0", e);
+                }
+            }
+        }
+
+        if (isTexturePulling()) {
+            endPulling(specular);
+        }
+        specular.getImage().clearUpdateNeeded();
+
+    }
+
+    @Override
+    public Texture2D genBRTF() {
+
+        Picture screen = new Picture("BakeScreen", true);
+        screen.setWidth(1);
+        screen.setHeight(1);
+
+        FrameBuffer brtfbaker = new FrameBuffer(brtf.getImage().getWidth(), brtf.getImage().getHeight(), 1);
+        brtfbaker.setSrgb(false);
+        brtfbaker.addColorTarget(FrameBufferTarget.newTarget(brtf));
+
+        if (isTexturePulling()) {
+            startPulling();
+        }
+
+        Camera envcam = updateAndGetInternalCamera(0, brtf.getImage().getWidth(), brtf.getImage().getHeight(), Vector3f.ZERO, 1, 1000);
+
+        Material mat = new Material(assetManager, "Common/IBL/IBLKernels.j3md");
+        mat.setBoolean("UseBRDF", true);
+        screen.setMaterial(mat);
+
+        renderManager.getRenderer().setFrameBuffer(brtfbaker);
+        renderManager.setCamera(envcam, false);
+
+        screen.updateLogicalState(0);
+        screen.updateGeometricState();
+        renderManager.renderGeometry(screen);
+
+        if (isTexturePulling()) {
+            pull(brtfbaker, brtf, 0);
+        }
+
+        brtfbaker.dispose();
+
+        if (isTexturePulling()) {
+            endPulling(brtf);
+        }
+        brtf.getImage().clearUpdateNeeded();
+
+        return brtf;
+    }
+
+    @Override
+    public void bakeIrradiance() {
+
+        Box boxm = new Box(1, 1, 1);
+        Geometry screen = new Geometry("BakeBox", boxm);
+
+        FrameBuffer irradiancebaker = new FrameBuffer(irradiance.getImage().getWidth(), irradiance.getImage().getHeight(), 1);
+        irradiancebaker.setSrgb(false);
+
+        if (isTexturePulling()) {
+            startPulling();
+        }
+
+        for (int i = 0; i < 6; i++) {
+            irradiancebaker.addColorTarget(
+                    FrameBufferTarget.newTarget(irradiance).face(TextureCubeMap.Face.values()[i]));
+        }
+
+        Material mat = new Material(assetManager, "Common/IBL/IBLKernels.j3md");
+        mat.setBoolean("UseIrradiance", true);
+        mat.setTexture("EnvMap", envMap);
+        screen.setMaterial(mat);
+
+        for (int i = 0; i < 6; i++) {
+            irradiancebaker.setTargetIndex(i);
+
+            mat.setInt("FaceId", i);
+
+            screen.updateLogicalState(0);
+            screen.updateGeometricState();
+
+            renderManager.setCamera(updateAndGetInternalCamera(i, irradiancebaker.getWidth(), irradiancebaker.getHeight(), Vector3f.ZERO, 1, 1000), false);
+            renderManager.getRenderer().setFrameBuffer(irradiancebaker);
+            renderManager.renderGeometry(screen);
+
+            if (isTexturePulling()) {
+                pull(irradiancebaker, irradiance, i);
+            }
+        }
+
+        irradiancebaker.dispose();
+
+        if (isTexturePulling()) {
+            endPulling(irradiance);
+        }
+        irradiance.getImage().clearUpdateNeeded();
+
+    }
+
+}

+ 176 - 0
jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java

@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2009-2023 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.environment.baker;
+
+import java.nio.ByteBuffer;
+import java.util.logging.Logger;
+import com.jme3.asset.AssetManager;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.Caps;
+import com.jme3.renderer.RenderManager;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Box;
+import com.jme3.texture.FrameBuffer;
+import com.jme3.texture.Image;
+import com.jme3.texture.Texture2D;
+import com.jme3.texture.FrameBuffer.FrameBufferTarget;
+import com.jme3.texture.Image.Format;
+import com.jme3.texture.image.ColorSpace;
+import com.jme3.texture.image.ImageRaster;
+import com.jme3.util.BufferUtils;
+
+/**
+ * Fully accelerated env baker for IBL that bakes the specular map and spherical
+ * harmonics on the GPU.
+ * 
+ * This is lighter on VRAM but it is not as parallelized as IBLGLEnvBaker
+ * 
+ * @author Riccardo Balbo
+ */
+public class IBLGLEnvBakerLight extends IBLHybridEnvBakerLight {
+    private static final int NUM_SH_COEFFICIENT = 9;
+    private static final Logger LOG = Logger.getLogger(IBLGLEnvBakerLight.class.getName());
+
+    /**
+     * Create a new IBL env baker
+     * 
+     * @param rm
+     *            The render manager used to render the env scene
+     * @param am
+     *            The asset manager used to load the baking shaders
+     * @param format
+     *            The format of the color buffers
+     * @param depthFormat
+     *            The format of the depth buffers
+     * @param env_size
+     *            The size in pixels of the output environment cube map (eg.
+     *            1024)
+     * @param specular_size
+     *            The size in pixels of the output specular cube map (eg. 1024)
+     */
+    public IBLGLEnvBakerLight(RenderManager rm, AssetManager am, Format format, Format depthFormat, int env_size, int specular_size) {
+        super(rm, am, format, depthFormat, env_size, specular_size);
+    }
+
+    @Override
+    public boolean isTexturePulling() {
+        return this.texturePulling;
+    }
+
+    @Override
+    public void bakeSphericalHarmonicsCoefficients() {
+        Box boxm = new Box(1, 1, 1);
+        Geometry screen = new Geometry("BakeBox", boxm);
+
+        Material mat = new Material(assetManager, "Common/IBLSphH/IBLSphH.j3md");
+        mat.setTexture("Texture", envMap);
+        mat.setVector2("Resolution", new Vector2f(envMap.getImage().getWidth(), envMap.getImage().getHeight()));
+        screen.setMaterial(mat);
+
+        float remapMaxValue = 0;
+        Format format = Format.RGBA32F;
+        if (!renderManager.getRenderer().getCaps().contains(Caps.FloatColorBufferRGBA)) {
+            LOG.warning("Float textures not supported, using RGB8 instead. This may cause accuracy issues.");
+            format = Format.RGBA8;
+            remapMaxValue = 0.05f;
+        }
+
+        if (remapMaxValue > 0) {
+            mat.setFloat("RemapMaxValue", remapMaxValue);
+        } else {
+            mat.clearParam("RemapMaxValue");
+        }
+
+        Texture2D shCoefTx[] = { new Texture2D(NUM_SH_COEFFICIENT, 1, 1, format), new Texture2D(NUM_SH_COEFFICIENT, 1, 1, format) };
+
+        FrameBuffer shbaker[] = { new FrameBuffer(NUM_SH_COEFFICIENT, 1, 1), new FrameBuffer(NUM_SH_COEFFICIENT, 1, 1) };
+        shbaker[0].setSrgb(false);
+        shbaker[0].addColorTarget(FrameBufferTarget.newTarget(shCoefTx[0]));
+
+        shbaker[1].setSrgb(false);
+        shbaker[1].addColorTarget(FrameBufferTarget.newTarget(shCoefTx[1]));
+
+        int renderOnT = -1;
+
+        for (int faceId = 0; faceId < 6; faceId++) {
+            if (renderOnT != -1) {
+                int s = renderOnT;
+                renderOnT = renderOnT == 0 ? 1 : 0;
+                mat.setTexture("ShCoef", shCoefTx[s]);
+                mat.setInt("FaceId", faceId);
+            } else {
+                renderOnT = 0;
+            }
+
+            screen.updateLogicalState(0);
+            screen.updateGeometricState();
+
+            renderManager.setCamera(updateAndGetInternalCamera(0, shbaker[renderOnT].getWidth(), shbaker[renderOnT].getHeight(), Vector3f.ZERO, 1, 1000), false);
+            renderManager.getRenderer().setFrameBuffer(shbaker[renderOnT]);
+            renderManager.renderGeometry(screen);
+        }
+
+        ByteBuffer shCoefRaw = BufferUtils.createByteBuffer(NUM_SH_COEFFICIENT * 1 * (shbaker[renderOnT].getColorTarget().getFormat().getBitsPerPixel() / 8));
+        renderManager.getRenderer().readFrameBufferWithFormat(shbaker[renderOnT], shCoefRaw, shbaker[renderOnT].getColorTarget().getFormat());
+        shCoefRaw.rewind();
+
+        Image img = new Image(format, NUM_SH_COEFFICIENT, 1, shCoefRaw, ColorSpace.Linear);
+        ImageRaster imgr = ImageRaster.create(img);
+
+        shCoef = new Vector3f[NUM_SH_COEFFICIENT];
+        float weightAccum = 0.0f;
+
+        for (int i = 0; i < shCoef.length; i++) {
+            ColorRGBA c = imgr.getPixel(i, 0);
+            shCoef[i] = new Vector3f(c.r, c.g, c.b);
+            if (weightAccum == 0) weightAccum = c.a;
+            else if (weightAccum != c.a) {
+                LOG.warning("SH weight is not uniform, this may cause issues.");
+            }
+
+        }
+
+        if (remapMaxValue > 0) weightAccum /= remapMaxValue;
+
+        for (int i = 0; i < NUM_SH_COEFFICIENT; ++i) {
+            if (remapMaxValue > 0) shCoef[i].divideLocal(remapMaxValue);
+            shCoef[i].multLocal(4.0f * FastMath.PI / weightAccum);
+        }
+
+        img.dispose();
+
+    }
+}

+ 209 - 0
jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java

@@ -0,0 +1,209 @@
+/*
+ * Copyright (c) 2009-2023 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.environment.baker;
+
+import java.util.Arrays;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import com.jme3.asset.AssetManager;
+import com.jme3.environment.util.EnvMapUtils;
+import com.jme3.material.Material;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.RenderManager;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Box;
+import com.jme3.texture.FrameBuffer;
+import com.jme3.texture.TextureCubeMap;
+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.WrapMode;
+import com.jme3.texture.image.ColorSpace;
+
+/**
+ * An env baker for IBL that bakes the specular map on the GPU and uses
+ * spherical harmonics generated on the CPU for the irradiance map.
+ * 
+ * This is lighter on VRAM but uses the CPU to compute the irradiance map.
+ * 
+ * @author Riccardo Balbo
+ */
+public class IBLHybridEnvBakerLight extends GenericEnvBaker implements IBLEnvBakerLight {
+    private static final Logger LOGGER = Logger.getLogger(IBLHybridEnvBakerLight.class.getName());
+    protected TextureCubeMap specular;
+    protected Vector3f[] shCoef;
+
+    /**
+     * Create a new IBL env baker
+     * 
+     * @param rm
+     *            The render manager used to render the env scene
+     * @param am
+     *            The asset manager used to load the baking shaders
+     * @param format
+     *            The format of the color buffers
+     * @param depthFormat
+     *            The format of the depth buffers
+     * @param env_size
+     *            The size in pixels of the output environment cube map (eg.
+     *            1024)
+     * @param specular_size
+     *            The size in pixels of the output specular cube map (eg. 1024)
+     */
+    public IBLHybridEnvBakerLight(RenderManager rm, AssetManager am, Format format, Format depthFormat, int env_size, int specular_size) {
+        super(rm, am, format, depthFormat, env_size);
+
+        specular = new TextureCubeMap(specular_size, specular_size, format);
+        specular.setWrap(WrapMode.EdgeClamp);
+        specular.setMagFilter(MagFilter.Bilinear);
+        specular.setMinFilter(MinFilter.Trilinear);
+        specular.getImage().setColorSpace(ColorSpace.Linear);
+
+        int nbMipMaps = (int) (Math.log(specular_size) / Math.log(2) + 1);
+        nbMipMaps = limitMips(nbMipMaps, specular.getImage().getWidth(), specular.getImage().getHeight(), rm);
+
+        int[] sizes = new int[nbMipMaps];
+        for (int i = 0; i < nbMipMaps; i++) {
+            int size = (int) FastMath.pow(2, nbMipMaps - 1 - i);
+            sizes[i] = size * size * (specular.getImage().getFormat().getBitsPerPixel() / 8);
+        }
+        specular.getImage().setMipMapSizes(sizes);
+        specular.getImage().setMipmapsGenerated(true);
+
+    }
+
+    @Override
+    public boolean isTexturePulling() { // always pull textures from gpu
+        return true;
+    }
+
+    private void bakeSpecularIBL(int mip, float roughness, Material mat, Geometry screen) throws Exception {
+        mat.setFloat("Roughness", roughness);
+
+        int mipWidth = (int) (specular.getImage().getWidth() * FastMath.pow(0.5f, mip));
+        int mipHeight = (int) (specular.getImage().getHeight() * FastMath.pow(0.5f, mip));
+
+        FrameBuffer specularbakers[] = new FrameBuffer[6];
+        for (int i = 0; i < 6; i++) {
+            specularbakers[i] = new FrameBuffer(mipWidth, mipHeight, 1);
+            specularbakers[i].setSrgb(false);
+            specularbakers[i].addColorTarget(FrameBufferTarget.newTarget(specular).level(mip).face(i));
+            specularbakers[i].setMipMapsGenerationHint(false);
+        }
+
+        for (int i = 0; i < 6; i++) {
+            FrameBuffer specularbaker = specularbakers[i];
+            mat.setInt("FaceId", i);
+
+            screen.updateLogicalState(0);
+            screen.updateGeometricState();
+
+            renderManager.setCamera(updateAndGetInternalCamera(i, specularbaker.getWidth(), specularbaker.getHeight(), Vector3f.ZERO, 1, 1000), false);
+            renderManager.getRenderer().setFrameBuffer(specularbaker);
+            renderManager.renderGeometry(screen);
+
+            if (isTexturePulling()) {
+                pull(specularbaker, specular, i);
+            }
+
+        }
+        for (int i = 0; i < 6; i++) {
+            specularbakers[i].dispose();
+        }
+    }
+
+    @Override
+    public void bakeSpecularIBL() {
+        Box boxm = new Box(1, 1, 1);
+        Geometry screen = new Geometry("BakeBox", boxm);
+
+        Material mat = new Material(assetManager, "Common/IBL/IBLKernels.j3md");
+        mat.setBoolean("UseSpecularIBL", true);
+        mat.setTexture("EnvMap", envMap);
+        screen.setMaterial(mat);
+
+        if (isTexturePulling()) {
+          startPulling();  
+        } 
+
+        int mip = 0;
+        for (; mip < specular.getImage().getMipMapSizes().length; mip++) {
+            try {
+                float roughness = (float) mip / (float) (specular.getImage().getMipMapSizes().length - 1);
+                bakeSpecularIBL(mip, roughness, mat, screen);
+            } catch (Exception e) {
+                LOGGER.log(Level.WARNING, "Error while computing mip level " + mip, e);
+                break;
+            }
+        }
+
+        if (mip < specular.getImage().getMipMapSizes().length) {
+
+            int[] sizes = specular.getImage().getMipMapSizes();
+            sizes = Arrays.copyOf(sizes, mip);
+            specular.getImage().setMipMapSizes(sizes);
+            specular.getImage().setMipmapsGenerated(true);
+            if (sizes.length <= 1) {
+                try {
+                    LOGGER.log(Level.WARNING, "Workaround driver BUG: only one mip level available, regenerate it with higher roughness (shiny fix)");
+                    bakeSpecularIBL(0, 1f, mat, screen);
+                } catch (Exception e) {
+                    LOGGER.log(Level.FINE, "Error while recomputing mip level 0", e);
+                }
+            }
+        }
+
+        if (isTexturePulling()) {
+            endPulling(specular);
+        }
+        specular.getImage().clearUpdateNeeded();
+
+    }
+
+    @Override
+    public TextureCubeMap getSpecularIBL() {
+        return specular;
+    }
+
+    @Override
+    public void bakeSphericalHarmonicsCoefficients() {
+        shCoef = EnvMapUtils.getSphericalHarmonicsCoefficents(getEnvMap());
+    }
+
+    @Override
+    public Vector3f[] getSphericalHarmonicsCoefficients() {
+        return shCoef;
+    }
+}

+ 5 - 17
jme3-core/src/main/java/com/jme3/environment/util/BoundingSphereDebug.java

@@ -1,5 +1,5 @@
  /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -56,14 +56,6 @@ public class BoundingSphereDebug extends Mesh {
     protected int radialSamples = 32;
     protected boolean useEvenSlices;
     protected boolean interior;
-    /**
-     * the distance from the center point each point falls on
-     */
-    public float radius;
-
-    public float getRadius() {
-        return radius;
-    }
 
     public BoundingSphereDebug() {
         setGeometryData();
@@ -151,27 +143,23 @@ public class BoundingSphereDebug extends Mesh {
             if (segDone == radialSamples || segDone == radialSamples * 2) {
                 idx++;
             }
-
         }
-
     }
-
     
     /**
      * Convenience factory method that creates a debug bounding-sphere geometry
+     * 
      * @param assetManager the assetManager
      * @return the bounding sphere debug geometry.
      */
     public static Geometry createDebugSphere(AssetManager assetManager) {
-        BoundingSphereDebug b = new BoundingSphereDebug();
-        Geometry geom = new Geometry("BoundingDebug", b);
-
+        BoundingSphereDebug mesh = new BoundingSphereDebug();
+        Geometry geom = new Geometry("BoundingDebug", mesh);
         Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
         mat.setBoolean("VertexColor", true);
         mat.getAdditionalRenderState().setWireframe(true);
-        
         geom.setMaterial(mat);
         return geom;
-
     }
+    
 }

+ 1 - 1
jme3-core/src/main/java/com/jme3/export/SavableClassUtil.java

@@ -57,7 +57,7 @@ import java.util.logging.Logger;
  */
 public class SavableClassUtil {
 
-    private final static HashMap<String, String> CLASS_REMAPPINGS = new HashMap<>();
+    private static final HashMap<String, String> CLASS_REMAPPINGS = new HashMap<>();
 
     private static void addRemapping(String oldClass, Class<? extends Savable> newClass) {
         CLASS_REMAPPINGS.put(oldClass, newClass.getName());

+ 1 - 1
jme3-core/src/main/java/com/jme3/font/BitmapCharacterSet.java

@@ -43,7 +43,7 @@ public class BitmapCharacterSet implements Savable {
     private int renderedSize;
     private int width;
     private int height;
-    final private IntMap<IntMap<BitmapCharacter>> characters;
+    private final IntMap<IntMap<BitmapCharacter>> characters;
     private int pageSize;
 
     @Override

+ 1 - 1
jme3-core/src/main/java/com/jme3/font/ColorTags.java

@@ -47,7 +47,7 @@ import java.util.regex.Pattern;
 class ColorTags {
     private static final Pattern colorPattern = Pattern.compile("\\\\#([0-9a-fA-F]{8})#|\\\\#([0-9a-fA-F]{6})#|" +
                                                                 "\\\\#([0-9a-fA-F]{4})#|\\\\#([0-9a-fA-F]{3})#");
-    final private LinkedList<Range> colors = new LinkedList<>();
+    private final LinkedList<Range> colors = new LinkedList<>();
     private String text;
     private String original;
     private float baseAlpha = -1;

+ 1 - 1
jme3-core/src/main/java/com/jme3/font/LetterQuad.java

@@ -66,7 +66,7 @@ class LetterQuad {
     private LetterQuad next;
     private int colorInt = 0xFFFFFFFF;
 
-    final private boolean rightToLeft;
+    private final boolean rightToLeft;
     private float alignX;
     private float alignY;
     private float sizeScale = 1;

+ 2 - 2
jme3-core/src/main/java/com/jme3/font/Letters.java

@@ -47,10 +47,10 @@ class Letters {
     private final LetterQuad tail;
     private final BitmapFont font;
     private LetterQuad current;
-    final private StringBlock block;
+    private final StringBlock block;
     private float totalWidth;
     private float totalHeight;
-    final private ColorTags colorTags = new ColorTags();
+    private final ColorTags colorTags = new ColorTags();
     private ColorRGBA baseColor = null;
     private float baseAlpha = -1;
     private String plainText;

+ 6 - 6
jme3-core/src/main/java/com/jme3/input/AbstractJoystick.java

@@ -42,13 +42,13 @@ import java.util.*;
  */
 public abstract class AbstractJoystick implements Joystick {
 
-    final private InputManager inputManager;
-    final private JoyInput joyInput;
-    final private int joyId;
-    final private String name;
+    private final InputManager inputManager;
+    private final JoyInput joyInput;
+    private final int joyId;
+    private final String name;
 
-    final private List<JoystickAxis> axes = new ArrayList<>();
-    final private List<JoystickButton> buttons = new ArrayList<>();
+    private final List<JoystickAxis> axes = new ArrayList<>();
+    private final List<JoystickButton> buttons = new ArrayList<>();
 
     /**
      * Creates a new joystick instance. Only used internally.

+ 21 - 21
jme3-core/src/main/java/com/jme3/input/CameraInput.java

@@ -44,37 +44,37 @@ public class CameraInput {
      * Chase camera mapping for moving down. Default assigned to
      * MouseInput.AXIS_Y direction depending on the invertYaxis configuration
      */
-    public final static String CHASECAM_DOWN = "ChaseCamDown";
+    public static final String CHASECAM_DOWN = "ChaseCamDown";
     /**
      * Chase camera mapping for moving up. Default assigned to MouseInput.AXIS_Y
      * direction depending on the invertYaxis configuration
      */
-    public final static String CHASECAM_UP = "ChaseCamUp";
+    public static final String CHASECAM_UP = "ChaseCamUp";
     /**
      * Chase camera mapping for zooming in. Default assigned to
      * MouseInput.AXIS_WHEEL direction positive
      */
-    public final static String CHASECAM_ZOOMIN = "ChaseCamZoomIn";
+    public static final String CHASECAM_ZOOMIN = "ChaseCamZoomIn";
     /**
      * Chase camera mapping for zooming out. Default assigned to
      * MouseInput.AXIS_WHEEL direction negative
      */
-    public final static String CHASECAM_ZOOMOUT = "ChaseCamZoomOut";
+    public static final String CHASECAM_ZOOMOUT = "ChaseCamZoomOut";
     /**
      * Chase camera mapping for moving left. Default assigned to
      * MouseInput.AXIS_X direction depending on the invertXaxis configuration
      */
-    public final static String CHASECAM_MOVELEFT = "ChaseCamMoveLeft";
+    public static final String CHASECAM_MOVELEFT = "ChaseCamMoveLeft";
     /**
      * Chase camera mapping for moving right. Default assigned to
      * MouseInput.AXIS_X direction depending on the invertXaxis configuration
      */
-    public final static String CHASECAM_MOVERIGHT = "ChaseCamMoveRight";
+    public static final String CHASECAM_MOVERIGHT = "ChaseCamMoveRight";
     /**
      * Chase camera mapping to initiate the rotation of the cam. Default assigned
      * to MouseInput.BUTTON_LEFT and MouseInput.BUTTON_RIGHT
      */
-    public final static String CHASECAM_TOGGLEROTATE = "ChaseCamToggleRotate";
+    public static final String CHASECAM_TOGGLEROTATE = "ChaseCamToggleRotate";
     
         
     
@@ -83,63 +83,63 @@ public class CameraInput {
      * Fly camera mapping to look left. Default assigned to MouseInput.AXIS_X,
      * direction negative
      */
-    public final static String FLYCAM_LEFT = "FLYCAM_Left";
+    public static final String FLYCAM_LEFT = "FLYCAM_Left";
     /**
      * Fly camera mapping to look right. Default assigned to MouseInput.AXIS_X,
      * direction positive
      */
-    public final static String FLYCAM_RIGHT = "FLYCAM_Right";
+    public static final String FLYCAM_RIGHT = "FLYCAM_Right";
     /**
      * Fly camera mapping to look up. Default assigned to MouseInput.AXIS_Y,
      * direction positive
      */
-    public final static String FLYCAM_UP = "FLYCAM_Up";
+    public static final String FLYCAM_UP = "FLYCAM_Up";
     /**
      * Fly camera mapping to look down. Default assigned to MouseInput.AXIS_Y,
      * direction negative
      */
-    public final static String FLYCAM_DOWN = "FLYCAM_Down";
+    public static final String FLYCAM_DOWN = "FLYCAM_Down";
     /**
      * Fly camera mapping to move left. Default assigned to KeyInput.KEY_A   
      */
-    public final static String FLYCAM_STRAFELEFT = "FLYCAM_StrafeLeft";
+    public static final String FLYCAM_STRAFELEFT = "FLYCAM_StrafeLeft";
     /**
      * Fly camera mapping to move right. Default assigned to KeyInput.KEY_D  
      */
-    public final static String FLYCAM_STRAFERIGHT = "FLYCAM_StrafeRight";
+    public static final String FLYCAM_STRAFERIGHT = "FLYCAM_StrafeRight";
     /**
      * Fly camera mapping to move forward. Default assigned to KeyInput.KEY_W   
      */
-    public final static String FLYCAM_FORWARD = "FLYCAM_Forward";
+    public static final String FLYCAM_FORWARD = "FLYCAM_Forward";
     /**
      * Fly camera mapping to move backward. Default assigned to KeyInput.KEY_S   
      */
-    public final static String FLYCAM_BACKWARD = "FLYCAM_Backward";
+    public static final String FLYCAM_BACKWARD = "FLYCAM_Backward";
     /**
      * Fly camera mapping to zoom in. Default assigned to MouseInput.AXIS_WHEEL,
      * direction positive
      */
-    public final static String FLYCAM_ZOOMIN = "FLYCAM_ZoomIn";
+    public static final String FLYCAM_ZOOMIN = "FLYCAM_ZoomIn";
     /**
      * Fly camera mapping to zoom in. Default assigned to MouseInput.AXIS_WHEEL,
      * direction negative
      */
-    public final static String FLYCAM_ZOOMOUT = "FLYCAM_ZoomOut";
+    public static final String FLYCAM_ZOOMOUT = "FLYCAM_ZoomOut";
     /**
      * Fly camera mapping to toggle rotation. Default assigned to 
      * MouseInput.BUTTON_LEFT   
      */
-    public final static String FLYCAM_ROTATEDRAG = "FLYCAM_RotateDrag";
+    public static final String FLYCAM_ROTATEDRAG = "FLYCAM_RotateDrag";
     /**
      * Fly camera mapping to move up. Default assigned to KeyInput.KEY_Q   
      */
-    public final static String FLYCAM_RISE = "FLYCAM_Rise";
+    public static final String FLYCAM_RISE = "FLYCAM_Rise";
     /**
      * Fly camera mapping to move down. Default assigned to KeyInput.KEY_W   
      */
-    public final static String FLYCAM_LOWER = "FLYCAM_Lower";
+    public static final String FLYCAM_LOWER = "FLYCAM_Lower";
     
-    public final static String FLYCAM_INVERTY = "FLYCAM_InvertY";
+    public static final String FLYCAM_INVERTY = "FLYCAM_InvertY";
     
     /**
      * A private constructor to inhibit instantiation of this class.

+ 7 - 7
jme3-core/src/main/java/com/jme3/input/ChaseCamera.java

@@ -105,37 +105,37 @@ public class ChaseCamera implements ActionListener, AnalogListener, Control, Jme
      * @deprecated use {@link CameraInput#CHASECAM_DOWN}
      */
     @Deprecated
-    public final static String ChaseCamDown = "ChaseCamDown";
+    public static final String ChaseCamDown = "ChaseCamDown";
     /**
      * @deprecated use {@link CameraInput#CHASECAM_UP}
      */
     @Deprecated
-    public final static String ChaseCamUp = "ChaseCamUp";
+    public static final String ChaseCamUp = "ChaseCamUp";
     /**
      * @deprecated use {@link CameraInput#CHASECAM_ZOOMIN}
      */
     @Deprecated
-    public final static String ChaseCamZoomIn = "ChaseCamZoomIn";
+    public static final String ChaseCamZoomIn = "ChaseCamZoomIn";
     /**
      * @deprecated use {@link CameraInput#CHASECAM_ZOOMOUT}
      */
     @Deprecated
-    public final static String ChaseCamZoomOut = "ChaseCamZoomOut";
+    public static final String ChaseCamZoomOut = "ChaseCamZoomOut";
     /**
      * @deprecated use {@link CameraInput#CHASECAM_MOVELEFT}
      */
     @Deprecated
-    public final static String ChaseCamMoveLeft = "ChaseCamMoveLeft";
+    public static final String ChaseCamMoveLeft = "ChaseCamMoveLeft";
     /**
      * @deprecated use {@link CameraInput#CHASECAM_MOVERIGHT}
      */
     @Deprecated
-    public final static String ChaseCamMoveRight = "ChaseCamMoveRight";
+    public static final String ChaseCamMoveRight = "ChaseCamMoveRight";
     /**
      * @deprecated use {@link CameraInput#CHASECAM_TOGGLEROTATE}
      */
     @Deprecated
-    public final static String ChaseCamToggleRotate = "ChaseCamToggleRotate";
+    public static final String ChaseCamToggleRotate = "ChaseCamToggleRotate";
 
     protected boolean zoomin;
     protected boolean hideCursorOnRotate = true;

+ 7 - 7
jme3-core/src/main/java/com/jme3/input/DefaultJoystickAxis.java

@@ -40,13 +40,13 @@ import com.jme3.input.controls.JoyAxisTrigger;
  */
 public class DefaultJoystickAxis implements JoystickAxis {
 
-    final private InputManager inputManager;
-    final private Joystick parent;
-    final private int axisIndex;
-    final private String name;
-    final private String logicalId;
-    final private boolean isAnalog;
-    final private boolean isRelative;
+    private final InputManager inputManager;
+    private final Joystick parent;
+    private final int axisIndex;
+    private final String name;
+    private final String logicalId;
+    private final boolean isAnalog;
+    private final boolean isRelative;
     private float deadZone;
 
     /**

+ 5 - 5
jme3-core/src/main/java/com/jme3/input/DefaultJoystickButton.java

@@ -40,11 +40,11 @@ import com.jme3.input.controls.JoyButtonTrigger;
  */
 public class DefaultJoystickButton implements JoystickButton {
 
-    final private InputManager inputManager;
-    final private Joystick parent;
-    final private int buttonIndex;
-    final private String name;
-    final private String logicalId;
+    private final InputManager inputManager;
+    private final Joystick parent;
+    private final int buttonIndex;
+    private final String name;
+    private final String logicalId;
 
     public DefaultJoystickButton(InputManager inputManager, Joystick parent, int buttonIndex,
             String name, String logicalId) {

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

@@ -56,7 +56,7 @@ import com.jme3.renderer.Camera;
  */
 public class FlyByCamera implements AnalogListener, ActionListener {
 
-    final private static String[] mappings = new String[]{
+    private static final String[] mappings = new String[]{
         CameraInput.FLYCAM_LEFT,
         CameraInput.FLYCAM_RIGHT,
         CameraInput.FLYCAM_UP,

+ 2 - 2
jme3-core/src/main/java/com/jme3/input/JoystickCompatibilityMappings.java

@@ -72,8 +72,8 @@ public class JoystickCompatibilityMappings {
     private static Map<String, Map<String, String>> buttonMappings = new HashMap<String, Map<String, String>>();
 
     // Remaps names by regex.
-    final private static Map<Pattern, String> nameRemappings = new HashMap<>();
-    final private static Map<String, String> nameCache = new HashMap<>();
+    private static final Map<Pattern, String> nameRemappings = new HashMap<>();
+    private static final Map<String, String> nameCache = new HashMap<>();
 
     static {
         loadDefaultMappings();

+ 2 - 2
jme3-core/src/main/java/com/jme3/input/event/JoyButtonEvent.java

@@ -41,8 +41,8 @@ import com.jme3.input.JoystickButton;
  */
 public class JoyButtonEvent extends InputEvent {
 
-    final private JoystickButton button;
-    final private boolean pressed;
+    private final JoystickButton button;
+    private final boolean pressed;
 
     public JoyButtonEvent(JoystickButton button, boolean pressed) {
         this.button = button;

+ 4 - 4
jme3-core/src/main/java/com/jme3/input/event/KeyInputEvent.java

@@ -40,10 +40,10 @@ import com.jme3.input.KeyInput;
  */
 public class KeyInputEvent extends InputEvent {
 
-    final private int keyCode;
-    final private char keyChar;
-    final private boolean pressed;
-    final private boolean repeating;
+    private final int keyCode;
+    private final char keyChar;
+    private final boolean pressed;
+    private final boolean repeating;
 
     public KeyInputEvent(int keyCode, char keyChar, boolean pressed, boolean repeating) {
         this.keyCode = keyCode;

+ 4 - 4
jme3-core/src/main/java/com/jme3/input/event/MouseButtonEvent.java

@@ -40,10 +40,10 @@ import com.jme3.input.MouseInput;
  */
 public class MouseButtonEvent extends InputEvent {
 
-    final private int x;
-    final private int y;
-    final private int btnIndex;
-    final private boolean pressed;
+    private final int x;
+    private final int y;
+    private final int btnIndex;
+    private final boolean pressed;
 
     public MouseButtonEvent(int btnIndex, boolean pressed, int x, int y) {
         this.btnIndex = btnIndex;

+ 1 - 1
jme3-core/src/main/java/com/jme3/input/event/MouseMotionEvent.java

@@ -40,7 +40,7 @@ package com.jme3.input.event;
  */
 public class MouseMotionEvent extends InputEvent {
 
-    final private int x, y, dx, dy, wheel, deltaWheel;
+    private final int x, y, dx, dy, wheel, deltaWheel;
 
     public MouseMotionEvent(int x, int y, int dx, int dy, int wheel, int deltaWheel) {
         this.x = x;

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

@@ -87,7 +87,7 @@ public abstract class Light implements Savable, Cloneable {
         Probe(4);
                 
 
-        final private int typeId;
+        private final int typeId;
 
         Type(int type){
             this.typeId = type;

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

@@ -13,7 +13,7 @@ public class SphereProbeArea implements ProbeArea {
 
     private Vector3f center = new Vector3f();
     private float radius = 1;
-    final private Matrix4f uniformMatrix = new Matrix4f();
+    private final Matrix4f uniformMatrix = new Matrix4f();
 
     public SphereProbeArea() {
     }

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

@@ -45,7 +45,7 @@ import java.util.List;
  */
 public class WeightedProbeBlendingStrategy implements LightProbeBlendingStrategy {
 
-    private final static int MAX_PROBES = 3;
+    private static final int MAX_PROBES = 3;
     List<LightProbe> lightProbes = new ArrayList<>();
 
     @Override

+ 41 - 30
jme3-core/src/main/java/com/jme3/material/MatParam.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -31,16 +31,26 @@
  */
 package com.jme3.material;
 
+import java.io.IOException;
+import java.util.Arrays;
+
 import com.jme3.asset.TextureKey;
-import com.jme3.export.*;
-import com.jme3.math.*;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.math.ColorRGBA;
+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.shader.VarType;
 import com.jme3.texture.Texture;
 import com.jme3.texture.Texture.WrapMode;
 
-import java.io.IOException;
-import java.util.Arrays;
-
 /**
  * Describes a material parameter. This is used for both defining a name and type
  * as well as a material parameter value.
@@ -58,8 +68,8 @@ public class MatParam implements Savable, Cloneable {
     /**
      * Create a new material parameter. For internal use only.
      *
-     * @param type the type of the parameter
-     * @param name the desired parameter name
+     * @param type  the type of the parameter
+     * @param name  the desired parameter name
      * @param value the desired parameter value (alias created)
      */
     public MatParam(VarType type, String name, Object value) {
@@ -75,20 +85,19 @@ public class MatParam implements Savable, Cloneable {
     protected MatParam() {
     }
 
-
     public boolean isTypeCheckEnabled() {
         return typeCheck;
     }
 
-
     /**
      * Enable type check for this param.
      * When type check is enabled a RuntimeException is thrown if 
      * an object of the wrong type is passed to setValue.
-     * @param v (default = true)
+     * 
+     * @param typeCheck (default = true)
      */
-    public void setTypeCheckEnabled(boolean v) {
-        typeCheck = v;
+    public void setTypeCheckEnabled(boolean typeCheck) {
+        this.typeCheck = typeCheck;
     }
 
     /**
@@ -102,6 +111,7 @@ public class MatParam implements Savable, Cloneable {
 
     /**
      * Returns the name of the material parameter.
+     * 
      * @return the name of the material parameter.
      */
     public String getName() {
@@ -158,15 +168,16 @@ public class MatParam implements Savable, Cloneable {
                     }
                 }
                 if (!valid) {
-                    throw new RuntimeException("Trying to assign a value of type " + value.getClass() + " to " + this.getName() + " of type " + type.name() + ". Valid types are "
-                            + Arrays.deepToString(type.getJavaType()));
+                    throw new RuntimeException("Trying to assign a value of type " + value.getClass() 
+                            + " to " + this.getName() 
+                            + " of type " + type.name() 
+                            + ". Valid types are " + Arrays.deepToString(type.getJavaType()));
                 }
             }
         }
         this.value = value;
     }
 
-
     /**
      * Returns the material parameter value as it would appear in a J3M
      * file. E.g.
@@ -274,12 +285,12 @@ When arrays can be inserted in J3M files
             case TextureCubeMap:
                 Texture texVal = (Texture) value;
                 TextureKey texKey = (TextureKey) texVal.getKey();
-                if (texKey == null){
-                  //throw new UnsupportedOperationException("The specified MatParam cannot be represented in J3M");
+                if (texKey == null) {
+                    // throw new UnsupportedOperationException("The specified MatParam cannot be represented in J3M");
                     // this is used in toString and the above line causes blender materials to throw this exception.
                     // toStrings should be very robust IMO as even debuggers often invoke toString and logging code
                     // often does as well, even implicitly.
-                    return texVal+":returned null key";
+                    return texVal + ":returned null key";
                 }
 
                 String ret = "";
@@ -287,22 +298,22 @@ When arrays can be inserted in J3M files
                     ret += "Flip ";
                 }
 
-                //Wrap mode
+                // Wrap mode
                 ret += getWrapMode(texVal, Texture.WrapAxis.S);
                 ret += getWrapMode(texVal, Texture.WrapAxis.T);
                 ret += getWrapMode(texVal, Texture.WrapAxis.R);
 
-                //Min and Mag filter
-                Texture.MinFilter def =  Texture.MinFilter.BilinearNoMipMaps;
-                if(texVal.getImage().hasMipmaps() || texKey.isGenerateMips()){
+                // Min and Mag filter
+                Texture.MinFilter def = Texture.MinFilter.BilinearNoMipMaps;
+                if (texVal.getImage().hasMipmaps() || texKey.isGenerateMips()) {
                     def = Texture.MinFilter.Trilinear;
                 }
-                if(texVal.getMinFilter() != def){
-                    ret += "Min" + texVal.getMinFilter().name()+ " ";
+                if (texVal.getMinFilter() != def) {
+                    ret += "Min" + texVal.getMinFilter().name() + " ";
                 }
 
-                if(texVal.getMagFilter() != Texture.MagFilter.Bilinear){
-                    ret += "Mag" + texVal.getMagFilter().name()+ " ";
+                if (texVal.getMagFilter() != Texture.MagFilter.Bilinear) {
+                    ret += "Mag" + texVal.getMagFilter().name() + " ";
                 }
 
                 return ret + "\"" + texKey.getName() + "\"";
@@ -315,12 +326,12 @@ When arrays can be inserted in J3M files
         WrapMode mode = WrapMode.EdgeClamp;
         try {
             mode = texVal.getWrap(axis);
-        } catch (IllegalArgumentException e) {
-            //this axis doesn't exist on the texture
+        } catch (IllegalArgumentException ex) {
+            // this axis doesn't exist on the texture
             return "";
         }
         if (mode != WrapMode.EdgeClamp) {
-            return"Wrap"+ mode.name() + "_" + axis.name() + " ";
+            return "Wrap" + mode.name() + "_" + axis.name() + " ";
         }
         return "";
     }

+ 40 - 24
jme3-core/src/main/java/com/jme3/material/MatParamTexture.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -40,51 +40,67 @@ import com.jme3.texture.Texture;
 import com.jme3.texture.image.ColorSpace;
 import java.io.IOException;
 
+/**
+ * A material parameter that holds a reference to a texture and its required color space.
+ * This class extends {@link MatParam} to provide texture specific functionalities.
+ */
 public class MatParamTexture extends MatParam {
 
-    private Texture texture;
     private ColorSpace colorSpace;
 
+    /**
+     * Constructs a new MatParamTexture instance with the specified type, name,
+     * texture, and color space.
+     *
+     * @param type       the type of the material parameter
+     * @param name       the name of the parameter
+     * @param texture    the texture associated with this parameter
+     * @param colorSpace the required color space for the texture
+     */
     public MatParamTexture(VarType type, String name, Texture texture, ColorSpace colorSpace) {
         super(type, name, texture);
-        this.texture = texture;
         this.colorSpace = colorSpace;
     }
 
+    /**
+     * Serialization only. Do not use.
+     */
     public MatParamTexture() {
     }
 
+    /**
+     * Retrieves the texture associated with this material parameter.
+     *
+     * @return the texture object
+     */
     public Texture getTextureValue() {
-        return texture;
+        return (Texture) getValue();
     }
 
+    /**
+     * Sets the texture associated with this material parameter.
+     *
+     * @param value the texture object to set
+     * @throws RuntimeException if the provided value is not a {@link Texture}
+     */
     public void setTextureValue(Texture value) {
-        this.value = value;
-        this.texture = value;
-    }
-    
-    @Override
-    public void setValue(Object value) {
-        if (!(value instanceof Texture)) {
-            throw new IllegalArgumentException("value must be a texture object");
-        }
-        this.value = value;
-        this.texture = (Texture) value;
+        setValue(value);
     }
 
     /**
+     * Gets the required color space for this texture parameter.
      * 
-     * @return the color space required by this texture param
+     * @return the required color space ({@link ColorSpace})
      */
     public ColorSpace getColorSpace() {
         return colorSpace;
     }
 
     /**
-     * Set to {@link ColorSpace#Linear} if the texture color space has to be forced to linear 
-     * instead of sRGB
+     * Set to {@link ColorSpace#Linear} if the texture color space has to be forced
+     * to linear instead of sRGB.
+     * 
      * @param colorSpace the desired color space
-     * @see ColorSpace
      */
     public void setColorSpace(ColorSpace colorSpace) {
         this.colorSpace = colorSpace;
@@ -94,17 +110,17 @@ public class MatParamTexture extends MatParam {
     public void write(JmeExporter ex) throws IOException {
         super.write(ex);
         OutputCapsule oc = ex.getCapsule(this);
-        oc.write(0, "texture_unit", -1);
-        oc.write(texture, "texture", null); // For backwards compatibility
-
         oc.write(colorSpace, "colorSpace", null);
+        // For backwards compatibility
+        oc.write(0, "texture_unit", -1);
+        oc.write((Texture) value, "texture", null);
     }
 
     @Override
     public void read(JmeImporter im) throws IOException {
         super.read(im);
         InputCapsule ic = im.getCapsule(this);
-        texture = (Texture) value;
         colorSpace = ic.readEnum("colorSpace", ColorSpace.class, null);
     }
-}
+    
+}

+ 93 - 76
jme3-core/src/main/java/com/jme3/material/Material.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -47,6 +47,7 @@ import com.jme3.renderer.TextureUnitException;
 import com.jme3.renderer.queue.RenderQueue.Bucket;
 import com.jme3.scene.Geometry;
 import com.jme3.shader.*;
+import com.jme3.shader.bufferobject.BufferObject;
 import com.jme3.texture.Image;
 import com.jme3.texture.Texture;
 import com.jme3.texture.image.ColorSpace;
@@ -82,11 +83,21 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
     private Technique technique;
     private HashMap<String, Technique> techniques = new HashMap<>();
     private RenderState additionalState = null;
-    final private RenderState mergedRenderState = new RenderState();
+    private final RenderState mergedRenderState = new RenderState();
     private boolean transparent = false;
     private boolean receivesShadows = false;
     private int sortingId = -1;
 
+    /**
+     * Track bind ids for textures and buffers
+     * Used internally 
+     */
+    public static class BindUnits {
+        public int textureUnit = 0;
+        public int bufferUnit = 0;
+    }
+    private BindUnits bindUnits = new BindUnits();
+
     public Material(MaterialDef def) {
         if (def == null) {
             throw new IllegalArgumentException("Material definition cannot be null");
@@ -506,6 +517,18 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         }
     }
 
+    /**
+     * Pass a parameter to the material shader.
+     *
+     * @param name the name of the parameter defined in the material definition (j3md)
+     * @param value the value of the parameter
+     */
+    public void setParam(String name, Object value) {
+        MatParam p = getMaterialDef().getMaterialParam(name);
+        setParam(name, p.getVarType(), value);
+    }
+
+
     /**
      * Clear a parameter from this material. The parameter must exist
      * @param name the name of the parameter to clear
@@ -685,8 +708,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
      * @param value the buffer object.
      */
     public void setUniformBufferObject(final String name, final BufferObject value) {
-        value.setBufferType(BufferObject.BufferType.UniformBufferObject);
-        setParam(name, VarType.BufferObject, value);
+        setParam(name, VarType.UniformBufferObject, value);
     }
 
     /**
@@ -696,8 +718,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
      * @param value the buffer object.
      */
     public void setShaderStorageBufferObject(final String name, final BufferObject value) {
-        value.setBufferType(BufferObject.BufferType.ShaderStorageBufferObject);
-        setParam(name, VarType.BufferObject, value);
+        setParam(name, VarType.ShaderStorageBufferObject, value);
     }
 
     /**
@@ -797,7 +818,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         sortingId = -1;
     }
 
-    private int applyOverrides(Renderer renderer, Shader shader, SafeArrayList<MatParamOverride> overrides, int unit) {
+    private void applyOverrides(Renderer renderer, Shader shader, SafeArrayList<MatParamOverride> overrides, BindUnits bindUnits) {
         for (MatParamOverride override : overrides.getArray()) {
             VarType type = override.getVarType();
 
@@ -810,36 +831,64 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
             Uniform uniform = shader.getUniform(override.getPrefixedName());
 
             if (override.getValue() != null) {
-                if (type.isTextureType()) {
-                    try {
-                        renderer.setTexture(unit, (Texture) override.getValue());
-                    } catch (TextureUnitException exception) {
-                        int numTexParams = unit + 1;
-                        String message = "Too many texture parameters ("
-                                + numTexParams + ") assigned\n to " + toString();
-                        throw new IllegalStateException(message);
-                    }
-                    uniform.setValue(VarType.Int, unit);
-                    unit++;
-                } else {
-                    uniform.setValue(type, override.getValue());
-                }
+                updateShaderMaterialParameter(renderer, type, shader, override, bindUnits, true);
             } else {
                 uniform.clearValue();
             }
         }
-        return unit;
     }
 
-    private int updateShaderMaterialParameters(Renderer renderer, Shader shader,
-                                               SafeArrayList<MatParamOverride> worldOverrides, SafeArrayList<MatParamOverride> forcedOverrides) {
 
-        int unit = 0;
+    private void updateShaderMaterialParameter(Renderer renderer, VarType type, Shader shader, MatParam param, BindUnits unit, boolean override) {
+        if (type == VarType.UniformBufferObject || type == VarType.ShaderStorageBufferObject) {
+            ShaderBufferBlock bufferBlock = shader.getBufferBlock(param.getPrefixedName());
+            BufferObject bufferObject = (BufferObject) param.getValue();
+
+            ShaderBufferBlock.BufferType btype;
+            if (type == VarType.ShaderStorageBufferObject) {
+                btype = ShaderBufferBlock.BufferType.ShaderStorageBufferObject;
+                bufferBlock.setBufferObject(btype, bufferObject);
+                renderer.setShaderStorageBufferObject(unit.bufferUnit, bufferObject); // TODO: probably not needed
+            } else {
+                btype = ShaderBufferBlock.BufferType.UniformBufferObject;
+                bufferBlock.setBufferObject(btype, bufferObject);
+                renderer.setUniformBufferObject(unit.bufferUnit, bufferObject); // TODO: probably not needed
+            }
+            unit.bufferUnit++;
+        } else {
+            Uniform uniform = shader.getUniform(param.getPrefixedName());
+            if (!override && uniform.isSetByCurrentMaterial()) return;
+
+            if (type.isTextureType()) {
+                try {
+                    renderer.setTexture(unit.textureUnit, (Texture) param.getValue());
+                } catch (TextureUnitException exception) {
+                    int numTexParams = unit.textureUnit + 1;
+                    String message = "Too many texture parameters (" + numTexParams + ") assigned\n to " + toString();
+                    throw new IllegalStateException(message);
+                }
+                uniform.setValue(VarType.Int, unit.textureUnit);
+                unit.textureUnit++;
+            } else {
+                uniform.setValue(type, param.getValue());
+            }
+        }
+    }
+
+
+
+
+    private BindUnits updateShaderMaterialParameters(Renderer renderer, Shader shader, SafeArrayList<MatParamOverride> worldOverrides,
+            SafeArrayList<MatParamOverride> forcedOverrides) {
+
+        bindUnits.textureUnit = 0;
+        bindUnits.bufferUnit = 0;
+
         if (worldOverrides != null) {
-            unit = applyOverrides(renderer, shader, worldOverrides, unit);
+            applyOverrides(renderer, shader, worldOverrides, bindUnits);
         }
         if (forcedOverrides != null) {
-            unit = applyOverrides(renderer, shader, forcedOverrides, unit);
+            applyOverrides(renderer, shader, forcedOverrides, bindUnits);
         }
 
         for (int i = 0; i < paramValues.size(); i++) {
@@ -847,66 +896,34 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
             MatParam param = paramValues.getValue(i);
             VarType type = param.getVarType();
 
-            if (isBO(type)) {
-
-                final ShaderBufferBlock bufferBlock = shader.getBufferBlock(param.getPrefixedName());
-                bufferBlock.setBufferObject((BufferObject) param.getValue());
-
-            } else {
-
-                Uniform uniform = shader.getUniform(param.getPrefixedName());
-                if (uniform.isSetByCurrentMaterial()) {
-                    continue;
-                }
-
-                if (type.isTextureType()) {
-                    try {
-                        renderer.setTexture(unit, (Texture) param.getValue());
-                    } catch (TextureUnitException exception) {
-                        int numTexParams = unit + 1;
-                        String message = "Too many texture parameters ("
-                                + numTexParams + ") assigned\n to " + toString();
-                        throw new IllegalStateException(message);
-                    }
-                    uniform.setValue(VarType.Int, unit);
-                    unit++;
-                } else {
-                    uniform.setValue(type, param.getValue());
-                }
-            }
+            updateShaderMaterialParameter(renderer, type, shader, param, bindUnits, false);
         }
 
-        //TODO HACKY HACK remove this when texture unit is handled by the uniform.
-        return unit;
+        // TODO HACKY HACK remove this when texture unit is handled by the
+        // uniform.
+        return bindUnits;
     }
 
-    /**
-     * Returns true if the type is Buffer Object's type.
-     *
-     * @param type the material parameter type.
-     * @return true if the type is Buffer Object's type.
-     */
-    private boolean isBO(final VarType type) {
-        return type == VarType.BufferObject;
-    }
+
 
     private void updateRenderState(Geometry geometry, RenderManager renderManager, Renderer renderer, TechniqueDef techniqueDef) {
+        RenderState finalRenderState;
         if (renderManager.getForcedRenderState() != null) {
-            mergedRenderState.copyFrom(renderManager.getForcedRenderState());
+            finalRenderState = mergedRenderState.copyFrom(renderManager.getForcedRenderState());
         } else if (techniqueDef.getRenderState() != null) {
-            mergedRenderState.copyFrom(RenderState.DEFAULT);
-            techniqueDef.getRenderState().copyMergedTo(additionalState, mergedRenderState);
+            finalRenderState = mergedRenderState.copyFrom(RenderState.DEFAULT);
+            finalRenderState = techniqueDef.getRenderState().copyMergedTo(additionalState, finalRenderState);
         } else {
-            mergedRenderState.copyFrom(RenderState.DEFAULT);
-            RenderState.DEFAULT.copyMergedTo(additionalState, mergedRenderState);
+            finalRenderState = mergedRenderState.copyFrom(RenderState.DEFAULT);
+            finalRenderState = RenderState.DEFAULT.copyMergedTo(additionalState, finalRenderState);
         }
         // test if the face cull mode should be flipped before render
-        if (mergedRenderState.isFaceCullFlippable() && isNormalsBackward(geometry.getWorldScale())) {
-            mergedRenderState.flipFaceCull();
+        if (finalRenderState.isFaceCullFlippable() && isNormalsBackward(geometry.getWorldScale())) {
+            finalRenderState.flipFaceCull();
         }
-        renderer.applyRenderState(mergedRenderState);
+        renderer.applyRenderState(finalRenderState);
     }
-    
+
     /**
      * Returns true if the geometry world scale indicates that normals will be backward.
      * @param scalar geometry world scale
@@ -1064,13 +1081,13 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         renderManager.updateUniformBindings(shader);
         
         // Set material parameters
-        int unit = updateShaderMaterialParameters(renderer, shader, overrides, renderManager.getForcedMatParams());
+        BindUnits units = updateShaderMaterialParameters(renderer, shader, overrides, renderManager.getForcedMatParams());
 
         // Clear any uniforms not changed by material.
         resetUniformsNotSetByCurrent(shader);
         
         // Delegate rendering to the technique
-        technique.render(renderManager, shader, geometry, lights, unit);
+        technique.render(renderManager, shader, geometry, lights, units);
     }
 
     /**

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

@@ -1511,6 +1511,10 @@ public class RenderState implements Cloneable, Savable {
             hash = 79 * hash + (this.backStencilDepthPassOperation != null ? this.backStencilDepthPassOperation.hashCode() : 0);
             hash = 79 * hash + (this.frontStencilFunction != null ? this.frontStencilFunction.hashCode() : 0);
             hash = 79 * hash + (this.backStencilFunction != null ? this.backStencilFunction.hashCode() : 0);
+            hash = 79 * hash + (this.frontStencilMask);
+            hash = 79 * hash + (this.frontStencilReference);
+            hash = 79 * hash + (this.backStencilMask);
+            hash = 79 * hash + (this.backStencilReference);
             hash = 79 * hash + Float.floatToIntBits(this.lineWidth);
             
             hash = 79 * hash + this.sfactorRGB.hashCode();
@@ -1623,6 +1627,11 @@ public class RenderState implements Cloneable, Savable {
 
             state.frontStencilFunction = additionalState.frontStencilFunction;
             state.backStencilFunction = additionalState.backStencilFunction;
+
+            state.frontStencilMask = additionalState.frontStencilMask;
+            state.frontStencilReference = additionalState.frontStencilMask;
+            state.backStencilMask = additionalState.backStencilMask;
+            state.backStencilReference = additionalState.backStencilMask;
         } else {
             state.stencilTest = stencilTest;
 
@@ -1636,6 +1645,11 @@ public class RenderState implements Cloneable, Savable {
 
             state.frontStencilFunction = frontStencilFunction;
             state.backStencilFunction = backStencilFunction;
+
+            state.frontStencilMask = frontStencilMask;
+            state.frontStencilReference = frontStencilMask;
+            state.backStencilMask = backStencilMask;
+            state.backStencilReference = backStencilMask;
         }
         if (additionalState.applyLineWidth) {
             state.lineWidth = additionalState.lineWidth;
@@ -1665,6 +1679,10 @@ public class RenderState implements Cloneable, Savable {
         backStencilDepthPassOperation = state.backStencilDepthPassOperation;
         frontStencilFunction = state.frontStencilFunction;
         backStencilFunction = state.backStencilFunction;
+        frontStencilMask = state.frontStencilMask;
+        frontStencilReference = state.frontStencilReference;
+        backStencilMask = state.backStencilMask;
+        backStencilReference = state.backStencilReference;
         blendEquationAlpha = state.blendEquationAlpha;
         blendEquation = state.blendEquation;
         depthFunc = state.depthFunc;
@@ -1692,7 +1710,7 @@ public class RenderState implements Cloneable, Savable {
      * This method is more precise than {@link #set(com.jme3.material.RenderState)}.
      * @param state state to copy from
      */
-    public void copyFrom(RenderState state) {
+    public RenderState copyFrom(RenderState state) {
         this.applyBlendMode = state.applyBlendMode;
         this.applyColorWrite = state.applyColorWrite;
         this.applyCullMode = state.applyCullMode;
@@ -1734,6 +1752,7 @@ public class RenderState implements Cloneable, Savable {
         this.sfactorRGB = state.sfactorRGB;
         this.stencilTest = state.stencilTest;
         this.wireframe = state.wireframe;
+        return this;
     }
 
     @Override
@@ -1767,8 +1786,6 @@ public class RenderState implements Cloneable, Savable {
      * {@code Front} and {@code Front} becomes {@code Back}.
      * <p>{@code FrontAndBack} and {@code Off} are unaffected. This is important
      * for flipping the cull mode when normal vectors are found to be backward.
-     * @param cull
-     * @return flipped cull mode
      */
     public void flipFaceCull() {
         switch (cullMode) {

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

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -33,6 +33,7 @@ package com.jme3.material;
 
 import com.jme3.asset.AssetManager;
 import com.jme3.light.LightList;
+import com.jme3.material.Material.BindUnits;
 import com.jme3.material.TechniqueDef.LightMode;
 import com.jme3.material.logic.TechniqueDefLogic;
 import com.jme3.renderer.Caps;
@@ -162,9 +163,9 @@ public final class Technique {
      * @param lights Lights which influence the geometry.
      * @param lastTexUnit the index of the most recently used texture unit
      */
-    void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights, int lastTexUnit) {
+    void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights, BindUnits lastBindUnits) {
         TechniqueDefLogic logic = def.getLogic();
-        logic.render(renderManager, shader, geometry, lights, lastTexUnit);
+        logic.render(renderManager, shader, geometry, lights, lastBindUnits);
     }
     
     /**

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

@@ -824,4 +824,4 @@ public class TechniqueDef implements Savable, Cloneable {
 
         return clone;
     }
-}
+}

+ 3 - 2
jme3-core/src/main/java/com/jme3/material/logic/DefaultTechniqueDefLogic.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -34,6 +34,7 @@ package com.jme3.material.logic;
 import com.jme3.asset.AssetManager;
 import com.jme3.light.*;
 import com.jme3.material.TechniqueDef;
+import com.jme3.material.Material.BindUnits;
 import com.jme3.math.ColorRGBA;
 import com.jme3.renderer.Caps;
 import com.jme3.renderer.RenderManager;
@@ -91,7 +92,7 @@ public class DefaultTechniqueDefLogic implements TechniqueDefLogic {
 
 
     @Override
-    public void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights, int lastTexUnit) {
+    public void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights, BindUnits lastBindUnits) {
         Renderer renderer = renderManager.getRenderer();
         renderer.setShader(shader);
         renderMeshFromGeometry(renderer, geometry);

+ 3 - 2
jme3-core/src/main/java/com/jme3/material/logic/MultiPassLightingLogic.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -38,6 +38,7 @@ import com.jme3.light.PointLight;
 import com.jme3.light.SpotLight;
 import com.jme3.material.RenderState;
 import com.jme3.material.TechniqueDef;
+import com.jme3.material.Material.BindUnits;
 import com.jme3.math.ColorRGBA;
 import com.jme3.math.Quaternion;
 import com.jme3.math.Vector3f;
@@ -67,7 +68,7 @@ public final class MultiPassLightingLogic extends DefaultTechniqueDefLogic {
     }
 
     @Override
-    public void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights, int lastTexUnit) {
+    public void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights, BindUnits lastBindUnits) {
         Renderer r = renderManager.getRenderer();
         Uniform lightDir = shader.getUniform("g_LightDirection");
         Uniform lightColor = shader.getUniform("g_LightColor");

+ 6 - 6
jme3-core/src/main/java/com/jme3/material/logic/SinglePassAndImageBasedLightingLogic.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -34,6 +34,7 @@ package com.jme3.material.logic;
 import com.jme3.asset.AssetManager;
 import com.jme3.light.*;
 import com.jme3.material.*;
+import com.jme3.material.Material.BindUnits;
 import com.jme3.material.RenderState.BlendMode;
 import com.jme3.math.*;
 import com.jme3.renderer.*;
@@ -54,7 +55,7 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique
 
     private boolean useAmbientLight;
     private final ColorRGBA ambientLightColor = new ColorRGBA(0, 0, 0, 1);
-    final private List<LightProbe> lightProbes = new ArrayList<>(3);
+    private final List<LightProbe> lightProbes = new ArrayList<>(3);
 
     static {
         ADDITIVE_LIGHT.setBlendMode(BlendMode.AlphaAdditive);
@@ -262,22 +263,21 @@ public final class SinglePassAndImageBasedLightingLogic extends DefaultTechnique
     }
 
     @Override
-    public void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights, int lastTexUnit) {
+    public void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights, BindUnits lastBindUnits) {
         int nbRenderedLights = 0;
         Renderer renderer = renderManager.getRenderer();
         int batchSize = renderManager.getSinglePassLightBatchSize();
         if (lights.size() == 0) {
-            updateLightListUniforms(shader, geometry, lights,batchSize, renderManager, 0, lastTexUnit);
+            updateLightListUniforms(shader, geometry, lights, batchSize, renderManager, 0, lastBindUnits.textureUnit);
             renderer.setShader(shader);
             renderMeshFromGeometry(renderer, geometry);
         } else {
             while (nbRenderedLights < lights.size()) {
-                nbRenderedLights = updateLightListUniforms(shader, geometry, lights, batchSize, renderManager, nbRenderedLights, lastTexUnit);
+                nbRenderedLights = updateLightListUniforms(shader, geometry, lights, batchSize, renderManager, nbRenderedLights, lastBindUnits.textureUnit);
                 renderer.setShader(shader);
                 renderMeshFromGeometry(renderer, geometry);
             }
         }
-        return;
     }
 
     protected void extractIndirectLights(LightList lightList, boolean removeLights) {

+ 3 - 2
jme3-core/src/main/java/com/jme3/material/logic/SinglePassLightingLogic.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -40,6 +40,7 @@ import com.jme3.light.SpotLight;
 import com.jme3.material.RenderState;
 import com.jme3.material.RenderState.BlendMode;
 import com.jme3.material.TechniqueDef;
+import com.jme3.material.Material.BindUnits;
 import com.jme3.math.ColorRGBA;
 import com.jme3.math.Vector3f;
 import com.jme3.math.Vector4f;
@@ -206,7 +207,7 @@ public final class SinglePassLightingLogic extends DefaultTechniqueDefLogic {
     }
 
     @Override
-    public void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights, int lastTexUnit) {
+    public void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights, BindUnits lastBindUnits) {
         int nbRenderedLights = 0;
         Renderer renderer = renderManager.getRenderer();
         int batchSize = renderManager.getSinglePassLightBatchSize();

+ 3 - 2
jme3-core/src/main/java/com/jme3/material/logic/StaticPassLightingLogic.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -38,6 +38,7 @@ import com.jme3.light.LightList;
 import com.jme3.light.PointLight;
 import com.jme3.light.SpotLight;
 import com.jme3.material.TechniqueDef;
+import com.jme3.material.Material.BindUnits;
 import com.jme3.math.ColorRGBA;
 import com.jme3.math.Matrix4f;
 import com.jme3.math.Vector3f;
@@ -171,7 +172,7 @@ public final class StaticPassLightingLogic extends DefaultTechniqueDefLogic {
     }
 
     @Override
-    public void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights, int lastTexUnit) {
+    public void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights, BindUnits lastBindUnits) {
         Renderer renderer = renderManager.getRenderer();
         Matrix4f viewMatrix = renderManager.getCurrentCamera().getViewMatrix();
         updateLightListUniforms(viewMatrix, shader, lights);

+ 3 - 2
jme3-core/src/main/java/com/jme3/material/logic/TechniqueDefLogic.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -33,6 +33,7 @@ package com.jme3.material.logic;
 
 import com.jme3.asset.AssetManager;
 import com.jme3.light.LightList;
+import com.jme3.material.Material.BindUnits;
 import com.jme3.renderer.Caps;
 import com.jme3.renderer.RenderManager;
 import com.jme3.scene.Geometry;
@@ -92,5 +93,5 @@ public interface TechniqueDefLogic {
      * @param lights Lights which influence the geometry.
      * @param lastTexUnit the index of the most recently used texture unit
      */
-    public void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights, int lastTexUnit);
+    public void render(RenderManager renderManager, Shader shader, Geometry geometry, LightList lights, BindUnits lastBindUnits);
 }

+ 17 - 1
jme3-core/src/main/java/com/jme3/math/AbstractTriangle.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2020 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -79,4 +79,20 @@ public abstract class AbstractTriangle implements Collidable {
     public int collideWith(Collidable other, CollisionResults results) {
         return other.collideWith(this, results);
     }
+
+    /**
+     * Returns a string representation of the triangle, which is unaffected. For
+     * example, a {@link com.jme3.math.Triangle} joining (1,0,0) and (0,1,0)
+     * with (0,0,1) is represented by:
+     * <pre>
+     * Triangle [V1: (1.0, 0.0, 0.0)  V2: (0.0, 1.0, 0.0)  V3: (0.0, 0.0, 1.0)]
+     * </pre>
+     *
+     * @return the string representation (not null, not empty)
+     */
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + " [V1: " + get1() + "  V2: "
+                + get2() + "  V3: " + get3() + "]";
+    }
 }

+ 10 - 1
jme3-core/src/main/java/com/jme3/math/FastMath.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -1135,4 +1135,13 @@ final public class FastMath {
     public static float unInterpolateLinear(float value, float min, float max) {
         return (value - min) / (max - min);
     }
+
+    /**
+     * Round n to a multiple of p
+     */
+    public static int toMultipleOf(int n, int p) {
+        return ((n - 1) | (p - 1)) + 1;
+    }
+
+
 }

+ 17 - 1
jme3-core/src/main/java/com/jme3/math/Line.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2020 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -274,4 +274,20 @@ public class Line implements Savable, Cloneable, java.io.Serializable {
             throw new AssertionError();
         }
     }
+
+    /**
+     * Returns a string representation of the Line, which is unaffected. For
+     * example, a line with origin (1,0,0) and direction (0,1,0) is represented
+     * by:
+     * <pre>
+     * Line [Origin: (1.0, 0.0, 0.0)  Direction: (0.0, 1.0, 0.0)]
+     * </pre>
+     *
+     * @return the string representation (not null, not empty)
+     */
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + " [Origin: " + origin
+                + "  Direction: " + direction + "]";
+    }
 }

+ 18 - 1
jme3-core/src/main/java/com/jme3/math/LineSegment.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2020 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -720,6 +720,23 @@ public class LineSegment implements Cloneable, Savable, java.io.Serializable {
         }
     }
 
+    /**
+     * Returns a string representation of the LineSegment, which is unaffected.
+     * For example, a segment extending from (1,0,0) to (1,1,0) is represented
+     * by:
+     * <pre>
+     * LineSegment [Origin: (1.0, 0.0, 0.0)  Direction: (0.0, 1.0, 0.0)  Extent: 1.0]
+     * </pre>
+     *
+     * @return the string representation (not null, not empty)
+     */
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + " [Origin: " + origin
+                + "  Direction: " + direction + "  Extent: " + extent + "]";
+    }
+
+    /**
     /**
      * <p>Evaluates whether a given point is contained within the axis aligned bounding box
      * that contains this LineSegment.</p><p>This function is float error aware.</p>

+ 19 - 1
jme3-core/src/main/java/com/jme3/math/Matrix4f.java

@@ -1802,6 +1802,9 @@ public final class Matrix4f implements Savable, Cloneable, java.io.Serializable
     /**
      * Determine the rotation component of this 3-D coordinate transform.
      *
+     * <p>Assumes (but does not verify) that the transform consists entirely of
+     * translation, rotation, and positive scaling -- no reflection or shear.
+     *
      * @return a new rotation Quaternion
      */
     public Quaternion toRotationQuat() {
@@ -1813,6 +1816,9 @@ public final class Matrix4f implements Savable, Cloneable, java.io.Serializable
     /**
      * Returns the rotation component of the coordinate transform.
      *
+     * <p>Assumes (but does not verify) that the transform consists entirely of
+     * translation, rotation, and positive scaling -- no reflection or shear.
+     *
      * @param q storage for the result (not null, modified)
      * @return the rotation component (in {@code q}) for chaining
      */
@@ -1824,7 +1830,10 @@ public final class Matrix4f implements Savable, Cloneable, java.io.Serializable
     /**
      * Determine the rotation component of this 3-D coordinate transform.
      *
-     * @return a new rotation Matrix3f
+     * <p>If the transform includes scaling or reflection or shear, the result
+     * might not be a valid rotation matrix.
+     *
+     * @return a new Matrix3f
      */
     public Matrix3f toRotationMatrix() {
         return new Matrix3f(m00, m01, m02, m10, m11, m12, m20, m21, m22);
@@ -1833,6 +1842,9 @@ public final class Matrix4f implements Savable, Cloneable, java.io.Serializable
     /**
      * Determines the rotation component of the coordinate transform.
      *
+     * <p>If the transform includes scaling or reflection or shear, the result
+     * might not be a valid rotation matrix.
+     *
      * @param mat storage for the result (not null, modified)
      */
     public void toRotationMatrix(Matrix3f mat) {
@@ -1850,6 +1862,9 @@ public final class Matrix4f implements Savable, Cloneable, java.io.Serializable
     /**
      * Determine the scale component of this 3-D coordinate transform.
      *
+     * <p>All components of the result will be non-negative, even if the
+     * coordinate transform includes reflection.
+     *
      * @return a new Vector3f
      */
     public Vector3f toScaleVector() {
@@ -1861,6 +1876,9 @@ public final class Matrix4f implements Savable, Cloneable, java.io.Serializable
     /**
      * Determines the scale component of the coordinate transform.
      *
+     * <p>All components of the result will be non-negative, even if the
+     * coordinate transform includes reflection.
+     *
      * @param store storage for the result (not null, modified)
      * @return the scale factors (in {@code store}) for chaining
      */

+ 35 - 5
jme3-core/src/main/java/com/jme3/math/Quaternion.java

@@ -356,8 +356,10 @@ public final class Quaternion implements Savable, Cloneable, java.io.Serializabl
     }
 
     /**
-     * Sets the quaternion from the specified rotation matrix. Does not verify
-     * that the argument is a valid rotation matrix.
+     * Sets the quaternion from the specified rotation matrix.
+     *
+     * <p>Does not verify that the argument is a valid rotation matrix.
+     * Positive scaling is compensated for, but not reflection or shear.
      *
      * @param matrix the input matrix (not null, unaffected)
      * @return the (modified) current instance (for chaining)
@@ -369,7 +371,9 @@ public final class Quaternion implements Savable, Cloneable, java.io.Serializabl
 
     /**
      * Sets the quaternion from a rotation matrix with the specified elements.
-     * Does not verify that the arguments form a valid rotation matrix.
+     *
+     * <p>Does not verify that the arguments form a valid rotation matrix.
+     * Positive scaling is compensated for, but not reflection or shear.
      *
      * @param m00 the matrix element in row 0, column 0
      * @param m01 the matrix element in row 0, column 1
@@ -385,7 +389,7 @@ public final class Quaternion implements Savable, Cloneable, java.io.Serializabl
     public Quaternion fromRotationMatrix(float m00, float m01, float m02,
             float m10, float m11, float m12, float m20, float m21, float m22) {
         // first normalize the forward (F), up (U) and side (S) vectors of the rotation matrix
-        // so that the scale does not affect the rotation
+        // so that positive scaling does not affect the rotation
         float lengthSquared = m00 * m00 + m10 * m10 + m20 * m20;
         if (lengthSquared != 1f && lengthSquared != 0f) {
             lengthSquared = 1.0f / FastMath.sqrt(lengthSquared);
@@ -564,7 +568,7 @@ public final class Quaternion implements Savable, Cloneable, java.io.Serializabl
      * current instance is unaffected.
      *
      * <p>Note: preserves the translation and scaling components of
-     * {@code result}.
+     * {@code result} unless {@code result} includes reflection.
      *
      * <p>Note: the result is created from a normalized version of the current
      * instance.
@@ -1013,6 +1017,9 @@ public final class Quaternion implements Savable, Cloneable, java.io.Serializabl
     /**
      * Applies the rotation represented by the argument to the current instance.
      *
+     * <p>Does not verify that {@code matrix} is a valid rotation matrix.
+     * Positive scaling is compensated for, but not reflection or shear.
+     *
      * @param matrix the rotation matrix to apply (not null, unaffected)
      */
     public void apply(Matrix3f matrix) {
@@ -1601,4 +1608,27 @@ public final class Quaternion implements Savable, Cloneable, java.io.Serializabl
             throw new AssertionError(); // can not happen
         }
     }
+
+    /**
+     * Tests whether the argument is a valid quaternion, returning false if it's
+     * null or if any component is NaN or infinite.
+     *
+     * @param quaternion the quaternion to test (unaffected)
+     * @return true if non-null and finite, otherwise false
+     */
+    public static boolean isValidQuaternion(Quaternion quaternion) {
+        if (quaternion == null) {
+            return false;
+        }
+        if (Float.isNaN(quaternion.x)
+                || Float.isNaN(quaternion.y)
+                || Float.isNaN(quaternion.z)
+                || Float.isNaN(quaternion.w)) {
+            return false;
+        }
+        return !Float.isInfinite(quaternion.x)
+                && !Float.isInfinite(quaternion.y)
+                && !Float.isInfinite(quaternion.z)
+                && !Float.isInfinite(quaternion.w);
+    }
 }

+ 16 - 1
jme3-core/src/main/java/com/jme3/math/Rectangle.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2024 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -253,4 +253,19 @@ public final class Rectangle implements Savable, Cloneable, java.io.Serializable
             throw new AssertionError();
         }
     }
+
+    /**
+     * Returns a string representation of the Recatangle, which is unaffected.
+     * For example, a rectangle with vertices at (1,0,0), (2,0,0), (1,2,0), and
+     * (2,2,0) is represented by:
+     * <pre>
+     * Rectangle [A: (1.0, 0.0, 0.0)  B: (2.0, 0.0, 0.0)  C: (1.0, 2.0, 0.0)]
+     * </pre>
+     *
+     * @return the string representation (not null, not empty)
+     */
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + " [A: " + a + "  B: " + b + "  C: " + c + "]";
+    }
 }

+ 1 - 1
jme3-core/src/main/java/com/jme3/math/Ring.java

@@ -47,7 +47,7 @@ public final class Ring implements Savable, Cloneable, java.io.Serializable {
 
     private Vector3f center, up;
     private float innerRadius, outerRadius;
-    private transient static Vector3f b1 = new Vector3f(), b2 = new Vector3f();
+    private static transient Vector3f b1 = new Vector3f(), b2 = new Vector3f();
 
     /**
      * Constructor creates a new <code>Ring</code> lying on the XZ plane,

+ 14 - 1
jme3-core/src/main/java/com/jme3/math/Transform.java

@@ -121,6 +121,7 @@ public final class Transform implements Savable, Cloneable, java.io.Serializable
      * @return the (modified) current instance (for chaining)
      */
     public Transform setRotation(Quaternion rot) {
+        assert Quaternion.isValidQuaternion(rot) : "Invalid rotation " + rot;
         this.rot.set(rot);
         return this;
     }
@@ -132,6 +133,7 @@ public final class Transform implements Savable, Cloneable, java.io.Serializable
      * @return the (modified) current instance (for chaining)
      */
     public Transform setTranslation(Vector3f trans) {
+        assert Vector3f.isValidVector(trans) : "Invalid translation " + trans;
         this.translation.set(trans);
         return this;
     }
@@ -152,6 +154,7 @@ public final class Transform implements Savable, Cloneable, java.io.Serializable
      * @return the (modified) current instance (for chaining)
      */
     public Transform setScale(Vector3f scale) {
+        assert Vector3f.isValidVector(scale) : "Invalid scale " + scale;
         this.scale.set(scale);
         return this;
     }
@@ -163,6 +166,7 @@ public final class Transform implements Savable, Cloneable, java.io.Serializable
      * @return the (modified) current instance (for chaining)
      */
     public Transform setScale(float scale) {
+        assert Float.isFinite(scale) : "Invalid scale " + scale;
         this.scale.set(scale, scale, scale);
         return this;
     }
@@ -286,6 +290,7 @@ public final class Transform implements Savable, Cloneable, java.io.Serializable
      * @return the (modified) current instance (for chaining)
      */
     public Transform setTranslation(float x, float y, float z) {
+        assert Float.isFinite(x) && Float.isFinite(y) && Float.isFinite(z) : "Invalid translation " + x + ", " + y + ", " + z;
         translation.set(x, y, z);
         return this;
     }
@@ -299,6 +304,7 @@ public final class Transform implements Savable, Cloneable, java.io.Serializable
      * @return the (modified) current instance (for chaining)
      */
     public Transform setScale(float x, float y, float z) {
+        assert Float.isFinite(x) && Float.isFinite(y) && Float.isFinite(z) : "Invalid scale " + x + ", " + y + ", " + z;
         scale.set(x, y, z);
         return this;
     }
@@ -389,10 +395,13 @@ public final class Transform implements Savable, Cloneable, java.io.Serializable
     }
 
     /**
-     * Sets the current instance from a transform matrix. Any shear in the
+     * Sets the current instance from a transform matrix. Any reflection or shear in the
      * matrix is lost -- in other words, it may not be possible to recreate the
      * original matrix from the result.
      *
+     * <p>After this method is invoked, all components of {@code scale} will be
+     * non-negative, even if {@code mat} includes reflection.
+     *
      * @param mat the input matrix (not null, unaffected)
      */
     public void fromTransformMatrix(Matrix4f mat) {
@@ -406,6 +415,10 @@ public final class Transform implements Savable, Cloneable, java.io.Serializable
     /**
      * Returns the inverse. The current instance is unaffected.
      *
+     * <p>Assumes (but does not verify) that the scale factors are all positive.
+     * If any component of {@code scale} is negative or zero, the result is
+     * undefined.
+     *
      * @return a new Transform
      */
     public Transform invert() {

+ 24 - 0
jme3-core/src/main/java/com/jme3/math/Vector2f.java

@@ -54,10 +54,34 @@ public final class Vector2f implements Savable, Cloneable, java.io.Serializable
      * Shared instance of the all-zero vector (0,0). Do not modify!
      */
     public static final Vector2f ZERO = new Vector2f(0f, 0f);
+    /**
+     * Shared instance of the all-NaN vector (NaN,NaN). Do not modify!
+     */
+    public static final Vector2f NAN = new Vector2f(Float.NaN, Float.NaN);
+    /**
+     * Shared instance of the +X direction (1,0). Do not modify!
+     */
+    public static final Vector2f UNIT_X = new Vector2f(1, 0);
+    /**
+     * Shared instance of the +Y direction (0,1). Do not modify!
+     */
+    public static final Vector2f UNIT_Y = new Vector2f(0, 1);
     /**
      * Shared instance of the all-ones vector (1,1). Do not modify!
      */
     public static final Vector2f UNIT_XY = new Vector2f(1f, 1f);
+    /**
+     * Shared instance of the all-plus-infinity vector (+Inf,+Inf). Do not modify!
+     */
+    public static final Vector2f POSITIVE_INFINITY = new Vector2f(
+            Float.POSITIVE_INFINITY,
+            Float.POSITIVE_INFINITY);
+    /**
+     * Shared instance of the all-negative-infinity vector (-Inf,-Inf). Do not modify!
+     */
+    public static final Vector2f NEGATIVE_INFINITY = new Vector2f(
+            Float.NEGATIVE_INFINITY,
+            Float.NEGATIVE_INFINITY);
     /**
      * The first (X) component.
      */

+ 8 - 8
jme3-core/src/main/java/com/jme3/math/Vector3f.java

@@ -52,32 +52,32 @@ public final class Vector3f implements Savable, Cloneable, java.io.Serializable
     /**
      * Shared instance of the all-zero vector (0,0,0). Do not modify!
      */
-    public final static Vector3f ZERO = new Vector3f(0, 0, 0);
+    public static final Vector3f ZERO = new Vector3f(0, 0, 0);
     /**
      * Shared instance of the all-NaN vector (NaN,NaN,NaN). Do not modify!
      */
-    public final static Vector3f NAN = new Vector3f(Float.NaN, Float.NaN, Float.NaN);
+    public static final Vector3f NAN = new Vector3f(Float.NaN, Float.NaN, Float.NaN);
     /**
      * Shared instance of the +X direction (1,0,0). Do not modify!
      */
-    public final static Vector3f UNIT_X = new Vector3f(1, 0, 0);
+    public static final Vector3f UNIT_X = new Vector3f(1, 0, 0);
     /**
      * Shared instance of the +Y direction (0,1,0). Do not modify!
      */
-    public final static Vector3f UNIT_Y = new Vector3f(0, 1, 0);
+    public static final Vector3f UNIT_Y = new Vector3f(0, 1, 0);
     /**
      * Shared instance of the +Z direction (0,0,1). Do not modify!
      */
-    public final static Vector3f UNIT_Z = new Vector3f(0, 0, 1);
+    public static final Vector3f UNIT_Z = new Vector3f(0, 0, 1);
     /**
      * Shared instance of the all-ones vector (1,1,1). Do not modify!
      */
-    public final static Vector3f UNIT_XYZ = new Vector3f(1, 1, 1);
+    public static final Vector3f UNIT_XYZ = new Vector3f(1, 1, 1);
     /**
      * Shared instance of the all-plus-infinity vector (+Inf,+Inf,+Inf). Do not
      * modify!
      */
-    public final static Vector3f POSITIVE_INFINITY = new Vector3f(
+    public static final Vector3f POSITIVE_INFINITY = new Vector3f(
             Float.POSITIVE_INFINITY,
             Float.POSITIVE_INFINITY,
             Float.POSITIVE_INFINITY);
@@ -85,7 +85,7 @@ public final class Vector3f implements Savable, Cloneable, java.io.Serializable
      * Shared instance of the all-negative-infinity vector (-Inf,-Inf,-Inf). Do
      * not modify!
      */
-    public final static Vector3f NEGATIVE_INFINITY = new Vector3f(
+    public static final Vector3f NEGATIVE_INFINITY = new Vector3f(
             Float.NEGATIVE_INFINITY,
             Float.NEGATIVE_INFINITY,
             Float.NEGATIVE_INFINITY);

+ 9 - 9
jme3-core/src/main/java/com/jme3/math/Vector4f.java

@@ -51,36 +51,36 @@ public final class Vector4f implements Savable, Cloneable, java.io.Serializable
     /**
      * shared instance of the all-zero vector (0,0,0,0) - Do not modify!
      */
-    public final static Vector4f ZERO = new Vector4f(0, 0, 0, 0);
+    public static final Vector4f ZERO = new Vector4f(0, 0, 0, 0);
     /**
      * shared instance of the all-NaN vector (NaN,NaN,NaN,NaN) - Do not modify!
      */
-    public final static Vector4f NAN = new Vector4f(Float.NaN, Float.NaN, Float.NaN, Float.NaN);
+    public static final Vector4f NAN = new Vector4f(Float.NaN, Float.NaN, Float.NaN, Float.NaN);
     /**
      * shared instance of the +X direction (1,0,0,0) - Do not modify!
      */
-    public final static Vector4f UNIT_X = new Vector4f(1, 0, 0, 0);
+    public static final Vector4f UNIT_X = new Vector4f(1, 0, 0, 0);
     /**
      * shared instance of the +Y direction (0,1,0,0) - Do not modify!
      */
-    public final static Vector4f UNIT_Y = new Vector4f(0, 1, 0, 0);
+    public static final Vector4f UNIT_Y = new Vector4f(0, 1, 0, 0);
     /**
      * shared instance of the +Z direction (0,0,1,0) - Do not modify!
      */
-    public final static Vector4f UNIT_Z = new Vector4f(0, 0, 1, 0);
+    public static final Vector4f UNIT_Z = new Vector4f(0, 0, 1, 0);
     /**
      * shared instance of the +W direction (0,0,0,1) - Do not modify!
      */
-    public final static Vector4f UNIT_W = new Vector4f(0, 0, 0, 1);
+    public static final Vector4f UNIT_W = new Vector4f(0, 0, 0, 1);
     /**
      * shared instance of the all-ones vector (1,1,1,1) - Do not modify!
      */
-    public final static Vector4f UNIT_XYZW = new Vector4f(1, 1, 1, 1);
+    public static final Vector4f UNIT_XYZW = new Vector4f(1, 1, 1, 1);
     /**
      * shared instance of the all-plus-infinity vector (+Inf,+Inf,+Inf,+Inf)
      * - Do not modify!
      */
-    public final static Vector4f POSITIVE_INFINITY = new Vector4f(
+    public static final Vector4f POSITIVE_INFINITY = new Vector4f(
             Float.POSITIVE_INFINITY,
             Float.POSITIVE_INFINITY,
             Float.POSITIVE_INFINITY,
@@ -89,7 +89,7 @@ public final class Vector4f implements Savable, Cloneable, java.io.Serializable
      * shared instance of the all-negative-infinity vector (-Inf,-Inf,-Inf,-Inf)
      * - Do not modify!
      */
-    public final static Vector4f NEGATIVE_INFINITY = new Vector4f(
+    public static final Vector4f NEGATIVE_INFINITY = new Vector4f(
             Float.NEGATIVE_INFINITY,
             Float.NEGATIVE_INFINITY,
             Float.NEGATIVE_INFINITY,

+ 2 - 2
jme3-core/src/main/java/com/jme3/opencl/OpenCLObjectManager.java

@@ -53,8 +53,8 @@ public class OpenCLObjectManager {
         return INSTANCE;
     }
     
-    final private ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
-    final private HashSet<OpenCLObjectRef> activeObjects = new HashSet<>();
+    private final ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
+    private final HashSet<OpenCLObjectRef> activeObjects = new HashSet<>();
     
     private static class OpenCLObjectRef extends PhantomReference<Object> {
         

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

@@ -95,7 +95,7 @@ public class HDRRenderer implements SceneProcessor {
 
     private MinFilter fbMinFilter = MinFilter.BilinearNoMipMaps;
     private MagFilter fbMagFilter = MagFilter.Bilinear;
-    final private AssetManager manager;
+    private final AssetManager manager;
 
     private boolean enabled = true;
 

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

@@ -49,8 +49,8 @@ public class PreDepthProcessor implements SceneProcessor {
 
     private RenderManager rm;
     private ViewPort vp;
-    final private Material preDepth;
-    final private RenderState forcedRS;
+    private final Material preDepth;
+    private final RenderState forcedRS;
 
     public PreDepthProcessor(AssetManager assetManager){
         preDepth = new Material(assetManager, "Common/MatDefs/Shadow/PreShadow.j3md");

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