diff --git a/.github/.well-known/funding-manifest-urls b/.github/.well-known/funding-manifest-urls
new file mode 100644
index 0000000000..856e91df9f
--- /dev/null
+++ b/.github/.well-known/funding-manifest-urls
@@ -0,0 +1 @@
+https://jmonkeyengine.org/funding.json
\ No newline at end of file
diff --git a/.github/actions/tools/uploadToCentral.sh b/.github/actions/tools/uploadToCentral.sh
new file mode 100755
index 0000000000..12a36ac5e2
--- /dev/null
+++ b/.github/actions/tools/uploadToCentral.sh
@@ -0,0 +1,68 @@
+#! /bin/bash
+set -euo pipefail
+
+## Upload a deployment
+## from the "org.jmonkeyengine" namespace in Sonatype's OSSRH staging area
+## to Sonatype's Central Publisher Portal
+## so the deployment can be tested and then published or dropped.
+
+## IMPORTANT: The upload request must originate
+## from the IP address used to stage the deployment to the staging area!
+
+# The required -p and -u flags on the command line
+# specify the password and username components of a "user token"
+# generated using the web interface at https://central.sonatype.com/account
+
+while getopts p:u: flag
+do
+ case "${flag}" in
+ p) centralPassword=${OPTARG};;
+ u) centralUsername=${OPTARG};;
+ esac
+done
+
+# Combine both components into a base64 "user token"
+# suitable for the Authorization header of a POST request:
+
+token=$(printf %s:%s "${centralUsername}" "${centralPassword}" | base64)
+
+# Send a POST request to upload the deployment:
+
+server='ossrh-staging-api.central.sonatype.com'
+endpoint='/manual/upload/defaultRepository/org.jmonkeyengine'
+url="https://${server}${endpoint}"
+
+statusCode=$(curl "${url}" \
+ --no-progress-meter \
+ --output postData1.txt \
+ --write-out '%{response_code}' \
+ --request POST \
+ --header 'accept: */*' \
+ --header "Authorization: Bearer ${token}" \
+ --data '')
+
+echo "Status code = ${statusCode}"
+echo 'Received data:'
+cat postData1.txt
+echo '[EOF]'
+
+# Retry if the default repo isn't found (status=400).
+
+if [ "${statusCode}" == "400" ]; then
+ echo "Will retry after 30 seconds."
+ sleep 30
+
+ statusCode2=$(curl "${url}" \
+ --no-progress-meter \
+ --output postData2.txt \
+ --write-out '%{response_code}' \
+ --request POST \
+ --header 'accept: */*' \
+ --header "Authorization: Bearer ${token}" \
+ --data '')
+
+ echo "Status code = ${statusCode2}"
+ echo 'Received data:'
+ cat postData2.txt
+ echo '[EOF]'
+fi
diff --git a/.github/workflows/bounty.yml b/.github/workflows/bounty.yml
new file mode 100644
index 0000000000..99689a7d5d
--- /dev/null
+++ b/.github/workflows/bounty.yml
@@ -0,0 +1,93 @@
+name: Bounty detector
+
+on:
+ issues:
+ types: [labeled]
+
+permissions:
+ issues: write
+ pull-requests: read
+
+jobs:
+ notify:
+ runs-on: ubuntu-latest
+ if: startsWith(github.event.label.name, 'diff:')
+ steps:
+ - name: Comment bounty info
+ uses: actions/github-script@v7
+ env:
+ FORUM_URL: "https://hub.jmonkeyengine.org/t/bounty-program-trial-starts-today/49394/"
+ RESERVE_HOURS: "48"
+ TIMER_SVG_BASE: "https://jme-bounty-reservation-indicator.rblb.workers.dev/timer.svg"
+ with:
+ script: |
+ const issue = context.payload.issue;
+ const actor = context.actor;
+ const issueOwner = issue.user?.login;
+ if (!issueOwner) return;
+
+ const forumUrl = process.env.FORUM_URL || "TBD";
+ const reserveHours = Number(process.env.RESERVE_HOURS || "48");
+ const svgBase = process.env.TIMER_SVG_BASE || "";
+
+ // "previous contributor" = has at least one merged PR authored in this repo
+ const repoFull = `${context.repo.owner}/${context.repo.repo}`;
+ const q = `repo:${repoFull} type:pr author:${issueOwner} is:merged`;
+
+ let isPreviousContributor = false;
+ try {
+ const search = await github.rest.search.issuesAndPullRequests({ q, per_page: 1 });
+ isPreviousContributor = (search.data.total_count ?? 0) > 0;
+ } catch (e) {
+ isPreviousContributor = false;
+ }
+
+ // Reserve only if previous contributor AND labeler is NOT the issue owner
+ const shouldReserve = isPreviousContributor && (actor !== issueOwner);
+
+ const lines = [];
+ lines.push(`## 💰 This issue has a bounty`);
+ lines.push(`Resolve it to receive a reward.`);
+ lines.push(`For details (amount, rules, eligibility), see: ${forumUrl}`);
+ lines.push("");
+
+ lines.push(`If you want to start working on this, **comment on this issue** with your intent.`);
+ lines.push(`If accepted by a maintainer, the issue will be **assigned** to you.`);
+ lines.push("");
+
+ if (shouldReserve && svgBase) {
+ const reservedUntil = new Date(Date.now() + reserveHours * 60 * 60 * 1000);
+ const reservedUntilIso = reservedUntil.toISOString();
+
+ const svgUrl =
+ `${svgBase}` +
+ `?until=${encodeURIComponent(reservedUntilIso)}` +
+ `&user=${encodeURIComponent(issueOwner)}` +
+ `&theme=dark`;
+
+ lines.push(``);
+ lines.push("");
+ }
+
+ // Avoid duplicate comments for the same label
+ const comments = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issue.number,
+ per_page: 100,
+ });
+
+ const already = comments.data.some(c =>
+ c.user?.login === "github-actions[bot]" &&
+ typeof c.body === "string" &&
+ c.body.includes("This issue has a bounty")
+ );
+
+ if (already) return;
+
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issue.number,
+ body: lines.join("\n"),
+ });
diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml
new file mode 100644
index 0000000000..6ff2d099fa
--- /dev/null
+++ b/.github/workflows/format.yml
@@ -0,0 +1,21 @@
+name: auto-format
+on:
+ push:
+
+jobs:
+ format:
+ runs-on: ubuntu-latest
+ if: ${{ false }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Prettify code
+ uses: creyD/prettier_action@v4.3
+ with:
+ prettier_options: --tab-width 4 --print-width 110 --write **/**/*.java
+ prettier_version: "2.8.8"
+ only_changed: True
+ commit_message: "auto-format"
+ prettier_plugins: "prettier-plugin-java"
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 23d0923658..a46920fb0b 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -16,8 +16,8 @@
# >> Configure MINIO NATIVES SNAPSHOT
# OBJECTS_KEY=XXXXXX
# >> Configure SONATYPE RELEASE
-# OSSRH_PASSWORD=XXXXXX
-# OSSRH_USERNAME=XXXXXX
+# CENTRAL_PASSWORD=XXXXXX
+# CENTRAL_USERNAME=XXXXXX
# >> Configure SIGNING
# SIGNING_KEY=XXXXXX
# SIGNING_PASSWORD=XXXXXX
@@ -46,32 +46,129 @@ on:
push:
branches:
- master
+ - v3.7
- v3.6
- v3.5
- v3.4
- v3.3
+ - ios-2024_2
pull_request:
release:
types: [published]
jobs:
+ ScreenshotTests:
+ name: Run Screenshot Tests
+ runs-on: ubuntu-latest
+ container:
+ image: ghcr.io/onemillionworlds/opengl-docker-image:v1
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@v4
+ - name: Start xvfb
+ run: |
+ Xvfb :99 -ac -screen 0 1024x768x16 &
+ export DISPLAY=:99
+ echo "DISPLAY=:99" >> $GITHUB_ENV
+ - name: Report GL/Vulkan
+ run: |
+ set -x
+ echo "DISPLAY=$DISPLAY"
+ glxinfo | grep -E "OpenGL version|OpenGL renderer|OpenGL vendor" || true
+ vulkaninfo --summary || true
+ echo "VK_ICD_FILENAMES=$VK_ICD_FILENAMES"
+ echo "MESA_LOADER_DRIVER_OVERRIDE=$MESA_LOADER_DRIVER_OVERRIDE"
+ echo "GALLIUM_DRIVER=$GALLIUM_DRIVER"
+ - name: Validate the Gradle wrapper
+ uses: gradle/actions/wrapper-validation@v3
+ - name: Test with Gradle Wrapper
+ run: |
+ ./gradlew :jme3-screenshot-test:screenshotTest
+ - name: Upload Test Reports
+ uses: actions/upload-artifact@master
+ if: always()
+ with:
+ name: screenshot-test-report
+ retention-days: 30
+ path: |
+ **/build/reports/**
+ **/build/changed-images/**
+ **/build/test-results/**
+
+ # Build iOS natives
+ BuildIosNatives:
+ name: Build natives for iOS
+ runs-on: macOS-14
+
+ steps:
+ - name: Check default JAVAs
+ run: echo $JAVA_HOME --- $JAVA_HOME_8_X64 --- $JAVA_HOME_11_X64 --- $JAVA_HOME_17_X64 --- $JAVA_HOME_21_X64 ---
+
+ - name: Setup the java environment
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11.0.26+4'
+
+ - name: Setup the XCode version to 15.1.0
+ uses: maxim-lobanov/setup-xcode@v1
+ with:
+ xcode-version: '15.1.0'
+
+ - name: Clone the repo
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Validate the Gradle wrapper
+ uses: gradle/actions/wrapper-validation@v3
+
+ - name: Build
+ run: |
+ ./gradlew -PuseCommitHashAsVersionName=true --no-daemon -PbuildNativeProjects=true \
+ :jme3-ios-native:build
+
+ - name: Upload natives
+ uses: actions/upload-artifact@master
+ with:
+ name: ios-natives
+ path: jme3-ios-native/template/META-INF/robovm/ios/libs/jme3-ios-native.xcframework
# Build the natives on android
BuildAndroidNatives:
name: Build natives for android
runs-on: ubuntu-latest
container:
- image: jmonkeyengine/buildenv-jme3:android
+ image: ghcr.io/cirruslabs/android-sdk:36-ndk
steps:
- name: Clone the repo
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 1
+
+ - name: Setup Java 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: '11'
+
+ - name: Check java version
+ run: java -version
+
+ - name: Install CMake
+ run: |
+ apt-get update
+ apt-get install -y cmake
+ cmake --version
+
- name: Validate the Gradle wrapper
- uses: gradle/wrapper-validation-action@v1.0.5
+ uses: gradle/actions/wrapper-validation@v3
+
- name: Build
run: |
+ export ANDROID_NDK="$ANDROID_SDK_ROOT/ndk/$ANDROID_NDK_VERSION"
./gradlew -PuseCommitHashAsVersionName=true --no-daemon -PbuildNativeProjects=true \
:jme3-android-native:assemble
@@ -81,39 +178,39 @@ 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]
+ needs: [BuildAndroidNatives, BuildIosNatives]
name: Build on ${{ matrix.osName }} jdk${{ matrix.jdk }}
runs-on: ${{ matrix.os }}
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
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
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 }}
@@ -124,13 +221,20 @@ jobs:
name: android-natives
path: build/native
+ - name: Download natives for iOS
+ uses: actions/download-artifact@master
+ with:
+ name: ios-natives
+ path: jme3-ios-native/template/META-INF/robovm/ios/libs/jme3-ios-native.xcframework
+
- name: Validate the Gradle wrapper
- uses: gradle/wrapper-validation-action@v1.0.5
+ 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 +242,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/
@@ -301,16 +402,16 @@ jobs:
# We need to clone everything again for uploadToMaven.sh ...
- name: Clone the repo
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
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
@@ -318,16 +419,22 @@ jobs:
name: android-natives
path: build/native
- - name: Rebuild the maven artifacts and deploy them to the Sonatype repository
+ - name: Download natives for iOS
+ uses: actions/download-artifact@master
+ with:
+ name: ios-natives
+ path: jme3-ios-native/template/META-INF/robovm/ios/libs/jme3-ios-native.xcframework
+
+ - name: Rebuild the maven artifacts and upload them to Sonatype's maven-snapshots repo
run: |
- if [ "${{ secrets.OSSRH_PASSWORD }}" = "" ];
+ if [ "${{ secrets.CENTRAL_PASSWORD }}" = "" ];
then
- echo "Configure the following secrets to enable deployment to Sonatype:"
- echo "OSSRH_PASSWORD, OSSRH_USERNAME, SIGNING_KEY, SIGNING_PASSWORD"
+ echo "Configure the following secrets to enable uploading to Sonatype:"
+ echo "CENTRAL_PASSWORD, CENTRAL_USERNAME, SIGNING_KEY, SIGNING_PASSWORD"
else
./gradlew publishMavenPublicationToSNAPSHOTRepository \
- -PossrhPassword=${{ secrets.OSSRH_PASSWORD }} \
- -PossrhUsername=${{ secrets.OSSRH_USERNAME }} \
+ -PcentralPassword=${{ secrets.CENTRAL_PASSWORD }} \
+ -PcentralUsername=${{ secrets.CENTRAL_USERNAME }} \
-PsigningKey='${{ secrets.SIGNING_KEY }}' \
-PsigningPassword='${{ secrets.SIGNING_PASSWORD }}' \
-PuseCommitHashAsVersionName=true \
@@ -343,18 +450,18 @@ jobs:
if: github.event_name == 'release'
steps:
- # We need to clone everything again for uploadToMaven.sh ...
+ # We need to clone everything again for uploadToCentral.sh ...
- name: Clone the repo
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 1
- # Setup jdk 17 used for building Sonatype OSSRH artifacts
+ # Setup jdk 21 used for building Sonatype 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
@@ -375,20 +482,29 @@ jobs:
name: android-natives
path: build/native
- - name: Rebuild the maven artifacts and deploy them to Sonatype OSSRH
+ - name: Download natives for iOS
+ uses: actions/download-artifact@master
+ with:
+ name: ios-natives
+ path: jme3-ios-native/template/META-INF/robovm/ios/libs/jme3-ios-native.xcframework
+
+ - name: Rebuild the maven artifacts and upload them to Sonatype's Central Publisher Portal
run: |
- if [ "${{ secrets.OSSRH_PASSWORD }}" = "" ];
+ if [ "${{ secrets.CENTRAL_PASSWORD }}" = "" ];
then
- echo "Configure the following secrets to enable deployment to Sonatype:"
- echo "OSSRH_PASSWORD, OSSRH_USERNAME, SIGNING_KEY, SIGNING_PASSWORD"
+ echo "Configure the following secrets to enable uploading to Sonatype:"
+ echo "CENTRAL_PASSWORD, CENTRAL_USERNAME, SIGNING_KEY, SIGNING_PASSWORD"
else
- ./gradlew publishMavenPublicationToOSSRHRepository \
- -PossrhPassword=${{ secrets.OSSRH_PASSWORD }} \
- -PossrhUsername=${{ secrets.OSSRH_USERNAME }} \
- -PsigningKey='${{ secrets.SIGNING_KEY }}' \
- -PsigningPassword='${{ secrets.SIGNING_PASSWORD }}' \
- -PuseCommitHashAsVersionName=true \
- --console=plain --stacktrace
+ ./gradlew publishMavenPublicationToCentralRepository \
+ -PcentralPassword=${{ secrets.CENTRAL_PASSWORD }} \
+ -PcentralUsername=${{ secrets.CENTRAL_USERNAME }} \
+ -PsigningKey='${{ secrets.SIGNING_KEY }}' \
+ -PsigningPassword='${{ secrets.SIGNING_PASSWORD }}' \
+ -PuseCommitHashAsVersionName=true \
+ --console=plain --stacktrace
+ .github/actions/tools/uploadToCentral.sh \
+ -p '${{ secrets.CENTRAL_PASSWORD }}' \
+ -u '${{ secrets.CENTRAL_USERNAME }}'
fi
- name: Deploy to GitHub Releases
diff --git a/.github/workflows/screenshot-test-comment.yml b/.github/workflows/screenshot-test-comment.yml
new file mode 100644
index 0000000000..5b4ae992e9
--- /dev/null
+++ b/.github/workflows/screenshot-test-comment.yml
@@ -0,0 +1,119 @@
+name: Screenshot Test PR Comment
+
+# This workflow is designed to safely comment on PRs from forks
+# It uses pull_request_target which has higher permissions than pull_request
+# Security note: This workflow does NOT check out or execute code from the PR
+# It only monitors the status of the ScreenshotTests job and posts comments
+# (If this commenting was done in the main worflow it would not have the permissions
+# to create a comment)
+
+on:
+ pull_request_target:
+ types: [opened, synchronize, reopened]
+
+jobs:
+ monitor-screenshot-tests:
+ name: Monitor Screenshot Tests and Comment
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+ permissions:
+ pull-requests: write
+ contents: read
+ steps:
+ - name: Wait for GitHub to register the workflow run
+ run: sleep 120
+
+ - name: Wait for Screenshot Tests to complete
+ uses: lewagon/wait-on-check-action@v1.3.1
+ with:
+ ref: ${{ github.event.pull_request.head.sha }}
+ check-name: 'Run Screenshot Tests'
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ wait-interval: 10
+ allowed-conclusions: success,skipped,failure
+ - name: Check Screenshot Tests status
+ id: check-status
+ uses: actions/github-script@v6
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { owner, repo } = context.repo;
+ const ref = '${{ github.event.pull_request.head.sha }}';
+
+ // Get workflow runs for the PR
+ const runs = await github.rest.actions.listWorkflowRunsForRepo({
+ owner,
+ repo,
+ head_sha: ref
+ });
+
+ // Find the ScreenshotTests job
+ let screenshotTestRun = null;
+ for (const run of runs.data.workflow_runs) {
+ if (run.name === 'Build jMonkeyEngine') {
+ const jobs = await github.rest.actions.listJobsForWorkflowRun({
+ owner,
+ repo,
+ run_id: run.id
+ });
+
+ for (const job of jobs.data.jobs) {
+ if (job.name === 'Run Screenshot Tests') {
+ screenshotTestRun = job;
+ break;
+ }
+ }
+
+ if (screenshotTestRun) break;
+ }
+ }
+
+ if (!screenshotTestRun) {
+ console.log('Screenshot test job not found');
+ return;
+ }
+
+ // Check if the job failed
+ if (screenshotTestRun.conclusion === 'failure') {
+ core.setOutput('failed', 'true');
+ } else {
+ core.setOutput('failed', 'false');
+ }
+ - name: Find Existing Comment
+ uses: peter-evans/find-comment@v3
+ id: existingCommentId
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ comment-author: 'github-actions[bot]'
+ body-includes: Screenshot tests have failed.
+
+ - name: Comment on PR if tests fail
+ if: steps.check-status.outputs.failed == 'true'
+ uses: peter-evans/create-or-update-comment@v4
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ body: |
+ 🖼️ **Screenshot tests have failed.**
+
+ The purpose of these tests is to ensure that changes introduced in this PR don't break visual features. They are visual unit tests.
+
+ 📄 **Where to find the report:**
+ - Go to the (failed run) > Summary > Artifacts > screenshot-test-report
+ - Download the zip and open jme3-screenshot-tests/build/reports/ScreenshotDiffReport.html
+
+ ⚠️ **If you didn't expect to change anything visual:**
+ Fix your changes so the screenshot tests pass.
+
+ ✅ **If you did mean to change things:**
+ Review the replacement images in jme3-screenshot-tests/build/changed-images to make sure they really are improvements and then replace and commit the replacement images at jme3-screenshot-tests/src/test/resources.
+
+ ✨ **If you are creating entirely new tests:**
+ Find the new images in jme3-screenshot-tests/build/changed-images and commit the new images at jme3-screenshot-tests/src/test/resources.
+
+ **Note;** it is very important that the committed reference images are created on the build pipeline, locally created images are not reliable. Similarly tests will fail locally but you can look at the report to check they are "visually similar".
+
+ See https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-screenshot-tests/README.md for more information
+
+ Contact @richardTingle (aka richtea) for guidance if required
+ edit-mode: replace
+ comment-id: ${{ steps.existingCommentId.outputs.comment-id }}
diff --git a/.gitignore b/.gitignore
index 150b0d492f..121fd33ac3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,4 +48,6 @@ appveyor.yml
javadoc_deploy
javadoc_deploy.pub
!.vscode/settings.json
-!.vscode/JME_style.xml
\ No newline at end of file
+!.vscode/JME_style.xml
+!.vscode/extensions.json
+joysticks-*.txt
\ No newline at end of file
diff --git a/.vscode/JME_style.xml b/.vscode/JME_style.xml
index 7e54d4f76f..b4014b077e 100644
--- a/.vscode/JME_style.xml
+++ b/.vscode/JME_style.xml
@@ -43,7 +43,7 @@
-
+
@@ -89,7 +89,7 @@
-
+
@@ -220,7 +220,7 @@
-
+
@@ -264,7 +264,7 @@
-
+
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000000..a1538208b3
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,6 @@
+{
+ "recommendations": [
+ "vscjava.vscode-java-pack",
+ "slevesque.shader"
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 274005bb2e..89d691dd52 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,7 +1,15 @@
{
"java.configuration.updateBuildConfiguration": "automatic",
+ "java.compile.nullAnalysis.mode": "automatic",
"java.refactor.renameFromFileExplorer": "prompt",
"java.format.settings.url": "./.vscode/JME_style.xml",
"editor.formatOnPaste": true,
- "editor.formatOnType": true
+ "editor.formatOnType": false,
+ "editor.formatOnSave": true,
+ "editor.formatOnSaveMode": "modifications" ,
+
+ "prettier.tabWidth": 4,
+ "prettier.printWidth": 110,
+ "prettier.enable": true,
+ "prettier.resolveGlobalModules": true
}
diff --git a/LICENSE.md b/LICENSE.md
index 0d6db16108..bb12c971e2 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,4 +1,4 @@
-Copyright (c) 2009-2023 jMonkeyEngine.
+Copyright (c) 2009-2025 jMonkeyEngine.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
diff --git a/README.md b/README.md
index 26d0e652f4..923eeb6480 100644
--- a/README.md
+++ b/README.md
@@ -4,19 +4,19 @@ jMonkeyEngine
[](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.0 is the latest stable version of the engine.
+v3.8.0 is the latest stable version of the engine.
The engine is used by several commercial game studios and computer-science courses. Here's a taste:

- - [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,27 +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](https://play.google.com/store/apps/details?id=com.dreiInitiative.demonLord&pli=1)
- - [Wild Magic](http://wildmagicgame.ru/)
+ - [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
@@ -58,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`
diff --git a/build.gradle b/build.gradle
index 2a7b4ed0c5..d4bd67720d 100644
--- a/build.gradle
+++ b/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'
-}
-
-
retrolambda {
javaVersion JavaVersion.VERSION_1_7
incremental true
jvmArgs '-noverify'
-}
\ No newline at end of file
+}
diff --git a/common.gradle b/common.gradle
index e97fe498ad..042d88e3b5 100644
--- a/common.gradle
+++ b/common.gradle
@@ -7,14 +7,18 @@ apply plugin: 'groovy'
apply plugin: 'maven-publish'
apply plugin: 'signing'
apply plugin: 'eclipse'
+apply plugin: 'checkstyle'
+
eclipse.jdt.file.withProperties { props ->
props.setProperty "org.eclipse.jdt.core.circularClasspath", "warning"
}
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
@@ -25,11 +29,6 @@ tasks.withType(JavaCompile) { // compile-time options:
}
}
-ext {
- lwjgl3Version = '3.3.2' // 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 {
@@ -39,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.17'
+ 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
@@ -83,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
}
@@ -158,32 +157,40 @@ publishing {
version project.version
}
}
+
repositories {
maven {
name = 'Dist'
url = gradle.rootProject.projectDir.absolutePath + '/dist/maven'
}
+
+ // Uploading to Sonatype relies on the existence of 2 properties
+ // (centralUsername and centralPassword)
+ // which should be set using -P options on the command line.
+
maven {
+ // for uploading release builds to the default repo in Sonatype's OSSRH staging area
credentials {
- username = gradle.rootProject.hasProperty('ossrhUsername') ? ossrhUsername : 'Unknown user'
- password = gradle.rootProject.hasProperty('ossrhPassword') ? ossrhPassword : 'Unknown password'
+ username = gradle.rootProject.hasProperty('centralUsername') ? centralUsername : 'Unknown user'
+ password = gradle.rootProject.hasProperty('centralPassword') ? centralPassword : 'Unknown password'
}
- name = 'OSSRH'
- url = 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2'
+ name = 'Central'
+ url = 'https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/'
+ }
+ maven {
+ // for uploading snapshot builds to Sonatype's maven-snapshots repo
+ credentials {
+ username = gradle.rootProject.hasProperty('centralUsername') ? centralUsername : 'Unknown user'
+ password = gradle.rootProject.hasProperty('centralPassword') ? centralPassword : 'Unknown password'
+ }
+ name = 'SNAPSHOT'
+ url = 'https://central.sonatype.com/repository/maven-snapshots/'
}
- maven {
- credentials {
- username = gradle.rootProject.hasProperty('ossrhUsername') ? ossrhUsername : 'Unknown user'
- password = gradle.rootProject.hasProperty('ossrhPassword') ? ossrhPassword : 'Unknown password'
- }
- name = 'SNAPSHOT'
- url = 'https://s01.oss.sonatype.org/content/repositories/snapshots/'
- }
}
}
publishToMavenLocal.doLast {
- println 'published ' + project.getName() + "-${jmeFullVersion} to mavenLocal"
+ println 'published ' + project.getName() + "-${jmeFullVersion} to mavenLocal"
}
task('install') {
dependsOn 'publishToMavenLocal'
@@ -200,3 +207,24 @@ signing {
tasks.withType(Sign) {
onlyIf { gradle.rootProject.hasProperty('signingKey') }
}
+
+checkstyle {
+ toolVersion libs.versions.checkstyle.get()
+ configFile file("${gradle.rootProject.rootDir}/config/checkstyle/checkstyle.xml")
+}
+
+checkstyleMain {
+ source ='src/main/java'
+}
+
+checkstyleTest {
+ source ='src/test/java'
+}
+
+tasks.withType(Checkstyle) {
+ reports {
+ xml.required.set(false)
+ html.required.set(true)
+ }
+ include("**/com/jme3/renderer/**/*.java")
+}
\ No newline at end of file
diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml
new file mode 100644
index 0000000000..919d0a916f
--- /dev/null
+++ b/config/checkstyle/checkstyle-suppressions.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml
new file mode 100644
index 0000000000..927c3779c9
--- /dev/null
+++ b/config/checkstyle/checkstyle.xml
@@ -0,0 +1,361 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 3f377cdfb6..42b02b8708 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,5 @@
# Version number: Major.Minor.SubMinor (e.g. 3.3.0)
-jmeVersion = 3.7.0
+jmeVersion = 3.9.0
# Leave empty to autogenerate
# (use -PjmeVersionName="myVersion" from commandline to specify a custom version name )
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000000..1244e46c51
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,49 @@
+## catalog of libraries and plugins used to build the jmonkeyengine project
+
+[versions]
+
+checkstyle = "9.3"
+lwjgl3 = "3.4.0"
+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.24"
+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.2.3"
+
+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-sdl = { module = "org.lwjgl:lwjgl-sdl", 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]
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 943f0cbfa7..e6441136f3 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index f398c33c4b..a4413138c9 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/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-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 65dcd68d65..b740cf1339 100755
--- a/gradlew
+++ b/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" \
diff --git a/gradlew.bat b/gradlew.bat
index 6689b85bee..7101f8e467 100644
--- a/gradlew.bat
+++ b/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
diff --git a/jme3-android-examples/build.gradle b/jme3-android-examples/build.gradle
index 5bc2d1cecd..c17664e1dd 100644
--- a/jme3-android-examples/build.gradle
+++ b/jme3-android-examples/build.gradle
@@ -2,7 +2,7 @@ apply plugin: 'com.android.application'
android {
compileSdkVersion 28
- buildToolsVersion "28.0.3"
+ buildToolsVersion "30.0.2"
lintOptions {
// Fix nifty gui referencing "java.awt" package.
@@ -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')
}
diff --git a/jme3-android-native/bufferallocator.gradle b/jme3-android-native/bufferallocator.gradle
index f03027e275..d10335a7f2 100644
--- a/jme3-android-native/bufferallocator.gradle
+++ b/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"
diff --git a/jme3-android-native/build.gradle b/jme3-android-native/build.gradle
index cc78ab4722..5ec15daea1 100644
--- a/jme3-android-native/build.gradle
+++ b/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')
diff --git a/jme3-android-native/decode.gradle b/jme3-android-native/decode.gradle
index 27dc02dd5b..d6a3842f02 100644
--- a/jme3-android-native/decode.gradle
+++ b/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"
diff --git a/jme3-android-native/openalsoft.gradle b/jme3-android-native/openalsoft.gradle
index 8c224ec457..0a14d4b429 100644
--- a/jme3-android-native/openalsoft.gradle
+++ b/jme3-android-native/openalsoft.gradle
@@ -1,12 +1,12 @@
// OpenAL Soft r1.21.1
// TODO: update URL to jMonkeyEngine fork once it's updated with latest kcat's changes
-String openALSoftUrl = 'https://github.com/kcat/openal-soft/archive/1.21.1.zip'
+String openALSoftUrl = 'https://github.com/kcat/openal-soft/archive/1.24.3.zip'
String openALSoftZipFile = 'OpenALSoft.zip'
// OpenAL Soft directory the download is extracted into
// Typically, the downloaded OpenAL Soft zip file will extract to a directory
// called "openal-soft"
-String openALSoftFolder = 'openal-soft-1.21.1'
+String openALSoftFolder = 'openal-soft-1.24.3'
//Working directories for the ndk build.
String openalsoftBuildDir = "${buildDir}" + File.separator + 'openalsoft'
@@ -81,13 +81,103 @@ task copyJmeOpenALSoft(type: Copy, dependsOn: [copyOpenALSoft, copyJmeHeadersOpe
from sourceDir
into outputDir
}
+// rootProject.ndkCommandPath must be set to your ndk-build wrapper or full ndk path
+def ndkPath = new File(rootProject.ndkCommandPath).getParent()
+def cmakeToolchain = "${ndkPath}/build/cmake/android.toolchain.cmake"
+
+// 1) list your ABIs here
+def openalAbis = [
+ "armeabi-v7a",
+ "arm64-v8a",
+ "x86",
+ "x86_64"
+]
+
+// 2) for each ABI, register a configure/build pair
+openalAbis.each { abi ->
+
+ // configure task
+ tasks.register("configureOpenAlSoft_${abi}", Exec) {
+ group = "external-native"
+ description = "Generate CMake build files for OpenAL-Soft [$abi]"
+
+ workingDir file("$openalsoftBuildDir/$openALSoftFolder")
+ commandLine = [
+ "cmake",
+ "-S", ".",
+ "-B", "cmake-build-${abi}",
+ "-G", "Unix Makefiles", // or Ninja
+ "-DCMAKE_TOOLCHAIN_FILE=${cmakeToolchain}",
+ "-DANDROID_PLATFORM=android-21",
+ "-DANDROID_ABI=${abi}",
+ "-DCMAKE_BUILD_TYPE=Release",
+ "-DALSOFT_UTILS=OFF",
+ "-DALSOFT_EXAMPLES=OFF",
+ "-DALSOFT_TESTS=OFF",
+ "-DALSOFT_BACKEND_OPENSL=ON",
+ '-DALSOFT_SHARED=OFF',
+ '-DBUILD_SHARED_LIBS=OFF',
+ '-DALSOFT_STATIC=ON',
+ '-DLIBTYPE=STATIC',
+ '-DCMAKE_CXX_FLAGS=-stdlib=libc++'
+ ]
+
+ dependsOn copyOpenALSoft
+ }
+
+ // build task
+ tasks.register("buildOpenAlSoft_${abi}", Exec) {
+ group = "external-native"
+ description = "Compile OpenAL-Soft into libopenalsoft.a for [$abi]"
+
+ dependsOn "configureOpenAlSoft_${abi}"
+ workingDir file("$openalsoftBuildDir/$openALSoftFolder")
+ commandLine = [
+ "cmake",
+ "--build", "cmake-build-${abi}",
+ "--config", "Release"
+ ]
+ }
+}
+
+// 3) optional: aggregate tasks
+tasks.register("configureOpenAlSoftAll") {
+ group = "external-native"
+ description = "Configure OpenAL-Soft for all ABIs"
+ dependsOn openalAbis.collect { "configureOpenAlSoft_${it}" }
+}
+
+tasks.register("buildOpenAlSoftAll") {
+ group = "external-native"
+ description = "Build OpenAL-Soft for all ABIs"
+ dependsOn openalAbis.collect { "buildOpenAlSoft_${it}" }
+}
-task buildOpenAlSoftNativeLib(type: Exec, dependsOn: copyJmeOpenALSoft) {
-// println "openalsoft build dir: " + openalsoftBuildDir
-// println "ndkCommandPath: " + project.ndkCommandPath
+task buildOpenAlSoftNativeLib(type: Exec) {
+ group = "external-native"
+ description = "Runs ndk-build on your JNI code, linking in the prebuilt OpenAL-Soft .a files"
+
+ dependsOn copyJmeOpenALSoft, buildOpenAlSoftAll
+
+ // where your Android.mk lives
workingDir openalsoftBuildDir
+
+ // call the NDK build script
executable rootProject.ndkCommandPath
- args "-j" + Runtime.runtime.availableProcessors()
+
+ // pass in all ABIs (so ndk-build will rebuild your shared .so for each one),
+ // and pass in a custom var OPENALSOFT_BUILD_DIR so your Android.mk can find
+ // the cmake-build- folders.
+ args(
+ // let ndk-build know which ABIs to build for
+ "APP_ABI=armeabi-v7a,arm64-v8a,x86,x86_64",
+
+ // pass in the path to the CMake output root
+ "OPENALSOFT_BUILD_ROOT=${openalsoftBuildDir}/${openALSoftFolder}",
+
+ // parallel jobs
+ "-j${Runtime.runtime.availableProcessors()}"
+ )
}
task updatePreCompiledOpenAlSoftLibs(type: Copy, dependsOn: buildOpenAlSoftNativeLib) {
@@ -111,6 +201,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"
@@ -137,3 +230,4 @@ class MyDownload extends DefaultTask {
ant.get(src: sourceUrl, dest: target)
}
}
+
diff --git a/jme3-android-native/src/native/jme_bufferallocator/Application.mk b/jme3-android-native/src/native/jme_bufferallocator/Application.mk
index 4bcc2ef85e..de0aea4497 100644
--- a/jme3-android-native/src/native/jme_bufferallocator/Application.mk
+++ b/jme3-android-native/src/native/jme_bufferallocator/Application.mk
@@ -36,4 +36,6 @@
APP_PLATFORM := android-19
# change this to 'debug' to see android logs
APP_OPTIM := release
-APP_ABI := all
\ No newline at end of file
+APP_ABI := armeabi-v7a,arm64-v8a,x86,x86_64
+APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true
+
diff --git a/jme3-android-native/src/native/jme_decode/Application.mk b/jme3-android-native/src/native/jme_decode/Application.mk
index fbc028f847..9651f8ee88 100644
--- a/jme3-android-native/src/native/jme_decode/Application.mk
+++ b/jme3-android-native/src/native/jme_decode/Application.mk
@@ -1,3 +1,5 @@
APP_PLATFORM := android-9
APP_OPTIM := release
-APP_ABI := all
\ No newline at end of file
+APP_ABI := armeabi-v7a,arm64-v8a,x86,x86_64
+APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true
+
diff --git a/jme3-android-native/src/native/jme_decode/com_jme3_audio_plugins_NativeVorbisFile.c b/jme3-android-native/src/native/jme_decode/com_jme3_audio_plugins_NativeVorbisFile.c
index 25a15f5b8a..b00ff9a7cc 100644
--- a/jme3-android-native/src/native/jme_decode/com_jme3_audio_plugins_NativeVorbisFile.c
+++ b/jme3-android-native/src/native/jme_decode/com_jme3_audio_plugins_NativeVorbisFile.c
@@ -1,6 +1,7 @@
#include
#include
#include
+#include
#include "Tremor/ivorbisfile.h"
diff --git a/jme3-android-native/src/native/jme_openalsoft/Android.mk b/jme3-android-native/src/native/jme_openalsoft/Android.mk
index 13e1547aad..d1f38c1864 100644
--- a/jme3-android-native/src/native/jme_openalsoft/Android.mk
+++ b/jme3-android-native/src/native/jme_openalsoft/Android.mk
@@ -1,103 +1,54 @@
-TARGET_PLATFORM := android-19
+# jni/Android.mk
LOCAL_PATH := $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := openalsoftjme
-
-LOCAL_C_INCLUDES += $(LOCAL_PATH) $(LOCAL_PATH)/include \
- $(LOCAL_PATH)/alc $(LOCAL_PATH)/common
+# require the path to cmake-build-
+ifndef OPENALSOFT_BUILD_ROOT
+$(error OPENALSOFT_BUILD_ROOT not set! pass it via ndk-build OPENALSOFT_BUILD_ROOT=/path/to/cmake-build-root)
+endif
-LOCAL_CPP_FEATURES += exceptions
+# assemble the path to this ABI's .a
+OPENAL_PREBUILT_DIR := $(OPENALSOFT_BUILD_ROOT)/cmake-build-$(TARGET_ARCH_ABI)
-LOCAL_CFLAGS := -ffast-math -DAL_BUILD_LIBRARY -DAL_ALEXT_PROTOTYPES -fcommon -O0 -DRESTRICT=""
-LOCAL_LDLIBS := -lOpenSLES -llog -Wl,-s
+# -----------------------------------------------------------------------------
+# 1) prebuilt static library
+include $(CLEAR_VARS)
+LOCAL_MODULE := openalsoft_prebuilt
+LOCAL_SRC_FILES := $(OPENAL_PREBUILT_DIR)/libopenal.a
+LOCAL_EXPORT_C_INCLUDES := $(OPENALSOFT_BUILD_ROOT)/include
+include $(PREBUILT_STATIC_LIBRARY)
-LOCAL_SRC_FILES := al/auxeffectslot.cpp \
- al/buffer.cpp \
- al/effect.cpp \
- al/effects/autowah.cpp \
- al/effects/chorus.cpp \
- al/effects/compressor.cpp \
- al/effects/convolution.cpp \
- al/effects/dedicated.cpp \
- al/effects/distortion.cpp \
- al/effects/echo.cpp \
- al/effects/equalizer.cpp \
- al/effects/fshifter.cpp \
- al/effects/modulator.cpp \
- al/effects/null.cpp \
- al/effects/pshifter.cpp \
- al/effects/reverb.cpp \
- al/effects/vmorpher.cpp \
- al/error.cpp \
- al/event.cpp \
- al/extension.cpp \
- al/filter.cpp \
- al/listener.cpp \
- al/source.cpp \
- al/state.cpp \
- alc/alc.cpp \
- alc/alconfig.cpp \
- alc/alu.cpp \
- alc/backends/base.cpp \
- alc/backends/loopback.cpp \
- alc/backends/null.cpp \
- alc/backends/opensl.cpp \
- alc/backends/wave.cpp \
- alc/bformatdec.cpp \
- alc/buffer_storage.cpp \
- alc/converter.cpp \
- alc/effects/autowah.cpp \
- alc/effects/chorus.cpp \
- alc/effects/compressor.cpp \
- alc/effects/convolution.cpp \
- alc/effects/dedicated.cpp \
- alc/effects/distortion.cpp \
- alc/effects/echo.cpp \
- alc/effects/equalizer.cpp \
- alc/effects/fshifter.cpp \
- alc/effects/modulator.cpp \
- alc/effects/null.cpp \
- alc/effects/pshifter.cpp \
- alc/effects/reverb.cpp \
- alc/effects/vmorpher.cpp \
- alc/effectslot.cpp \
- alc/helpers.cpp \
- alc/hrtf.cpp \
- alc/panning.cpp \
- alc/uiddefs.cpp \
- alc/voice.cpp \
- common/alcomplex.cpp \
- common/alfstream.cpp \
- common/almalloc.cpp \
- common/alstring.cpp \
- common/dynload.cpp \
- common/polyphase_resampler.cpp \
- common/ringbuffer.cpp \
- common/strutils.cpp \
- common/threads.cpp \
- core/ambdec.cpp \
- core/bs2b.cpp \
- core/bsinc_tables.cpp \
- core/cpu_caps.cpp \
- core/devformat.cpp \
- core/except.cpp \
- core/filters/biquad.cpp \
- core/filters/nfc.cpp \
- core/filters/splitter.cpp \
- core/fmt_traits.cpp \
- core/fpu_ctrl.cpp \
- core/logging.cpp \
- core/mastering.cpp \
- core/mixer/mixer_c.cpp \
- core/uhjfilter.cpp \
- com_jme3_audio_android_AndroidAL.c \
- com_jme3_audio_android_AndroidALC.c \
- com_jme3_audio_android_AndroidEFX.c
+# -----------------------------------------------------------------------------
+# 2) your JNI wrapper
+include $(CLEAR_VARS)
+LOCAL_MODULE := openalsoftjme
+LOCAL_SRC_FILES := \
+ com_jme3_audio_android_AndroidAL.c \
+ com_jme3_audio_android_AndroidALC.c \
+ com_jme3_audio_android_AndroidEFX.c
+
+LOCAL_C_INCLUDES += \
+ $(LOCAL_PATH) \
+ $(LOCAL_PATH)/include \
+ $(LOCAL_PATH)/alc \
+ $(LOCAL_PATH)/common
+
+LOCAL_CPP_FEATURES := exceptions rtti
+LOCAL_CFLAGS := -ffast-math \
+ -DAL_ALEXT_PROTOTYPES \
+ -fcommon \
+ -O0 \
+ -DRESTRICT=""
+
+LOCAL_LDLIBS := -lOpenSLES -llog -Wl,-s -lc++_static -lc++abi
+ifeq ($(TARGET_ARCH_ABI),arm64-v8a)
+ LOCAL_LDFLAGS += "-Wl,-z,max-page-size=16384"
+endif
+ifeq ($(TARGET_ARCH_ABI),x86_64)
+ LOCAL_LDFLAGS += "-Wl,-z,max-page-size=16384"
+endif
+LOCAL_STATIC_LIBRARIES := openalsoft_prebuilt
+# (or LOCAL_WHOLE_STATIC_LIBRARIES if you need every object pulled in)
include $(BUILD_SHARED_LIBRARY)
-# Alc/mixer/hrtf_inc.c \
-
diff --git a/jme3-android-native/src/native/jme_openalsoft/Application.mk b/jme3-android-native/src/native/jme_openalsoft/Application.mk
index d269117ddd..58561d1d14 100644
--- a/jme3-android-native/src/native/jme_openalsoft/Application.mk
+++ b/jme3-android-native/src/native/jme_openalsoft/Application.mk
@@ -1,5 +1,6 @@
APP_PLATFORM := android-19
APP_OPTIM := release
-APP_ABI := all
+APP_ABI := armeabi-v7a,arm64-v8a,x86,x86_64
APP_STL := c++_static
+APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true
diff --git a/jme3-android/build.gradle b/jme3-android/build.gradle
index f0725dfc5e..4fa73b50ca 100644
--- a/jme3-android/build.gradle
+++ b/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'
}
diff --git a/jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java b/jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java
index c9b4dac5e2..701da81518 100644
--- a/jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java
+++ b/jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java
@@ -45,6 +45,7 @@
public class AndroidGL implements GL, GL2, GLES_30, GLExt, GLFbo {
IntBuffer tmpBuff = BufferUtils.createIntBuffer(1);
+ IntBuffer tmpBuff16 = BufferUtils.createIntBuffer(16);
@Override
public void resetStats() {
@@ -162,6 +163,11 @@ public void glGetBufferSubData(int target, long offset, ByteBuffer data) {
throw new UnsupportedOperationException("OpenGL ES 2 does not support glGetBufferSubData");
}
+ @Override
+ public void glGetBufferSubData(int target, long offset, IntBuffer data) {
+ throw new UnsupportedOperationException("OpenGL ES 2 does not support glGetBufferSubData");
+ }
+
@Override
public void glClear(int mask) {
GLES20.glClear(mask);
@@ -694,10 +700,17 @@ public void glPolygonMode(int face, int mode) {
// Wrapper to DrawBuffers as there's no DrawBuffer method in GLES
@Override
public void glDrawBuffer(int mode) {
- tmpBuff.clear();
- tmpBuff.put(0, mode);
- tmpBuff.rewind();
- glDrawBuffers(tmpBuff);
+ int nBuffers = (mode - GLFbo.GL_COLOR_ATTACHMENT0_EXT) + 1;
+ if (nBuffers <= 0 || nBuffers > 16) {
+ throw new IllegalArgumentException("Draw buffer outside range: " + Integer.toHexString(mode));
+ }
+ tmpBuff16.clear();
+ for (int i = 0; i < nBuffers - 1; i++) {
+ tmpBuff16.put(GL.GL_NONE);
+ }
+ tmpBuff16.put(mode);
+ tmpBuff16.flip();
+ glDrawBuffers(tmpBuff16);
}
@Override
@@ -729,5 +742,21 @@ public void glTexSubImage3D(int target, int level, int xoffset, int yoffset, int
GLES30.glTexSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data);
}
+ @Override
+ public void glBindVertexArray(int array) {
+ GLES30.glBindVertexArray(array);
+ }
+
+ @Override
+ public void glDeleteVertexArrays(IntBuffer arrays) {
+ GLES30.glDeleteVertexArrays(arrays.limit(),arrays);
+ }
+
+ @Override
+ public void glGenVertexArrays(IntBuffer arrays) {
+ GLES30.glGenVertexArrays(arrays.limit(),arrays);
+
+ }
+
}
diff --git a/jme3-android/src/main/java/com/jme3/system/android/JmeAndroidSystem.java b/jme3-android/src/main/java/com/jme3/system/android/JmeAndroidSystem.java
index 0d6ee82c6d..dc0cfe5588 100644
--- a/jme3-android/src/main/java/com/jme3/system/android/JmeAndroidSystem.java
+++ b/jme3-android/src/main/java/com/jme3/system/android/JmeAndroidSystem.java
@@ -17,7 +17,7 @@
import com.jme3.system.*;
import com.jme3.system.JmeContext.Type;
import com.jme3.util.AndroidScreenshots;
-import com.jme3.util.functional.VoidFunction;
+import com.jme3.util.res.Resources;
import java.io.File;
import java.io.IOException;
@@ -52,7 +52,7 @@ public JmeAndroidSystem(){
@Override
public URL getPlatformAssetConfigURL() {
- return Thread.currentThread().getContextClassLoader().getResource("com/jme3/asset/Android.cfg");
+ return Resources.getResource("com/jme3/asset/Android.cfg");
}
@Override
diff --git a/jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java b/jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java
index 5757368ef8..70911d855a 100644
--- a/jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java
+++ b/jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2023 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -58,7 +58,7 @@
import com.jme3.renderer.opengl.*;
import com.jme3.system.*;
import com.jme3.util.BufferAllocatorFactory;
-import com.jme3.util.AndroidNativeBufferAllocator;
+import com.jme3.util.PrimitiveAllocator;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -78,19 +78,18 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
protected SystemListener listener;
protected boolean autoFlush = true;
protected AndroidInputHandler androidInput;
- protected long minFrameDuration = 0; // No FPS cap
+ protected long minFrameDuration = 0; // No FPS cap
protected long lastUpdateTime = 0;
static {
final String implementation = BufferAllocatorFactory.PROPERTY_BUFFER_ALLOCATOR_IMPLEMENTATION;
if (System.getProperty(implementation) == null) {
- System.setProperty(implementation, AndroidNativeBufferAllocator.class.getName());
+ System.setProperty(implementation, PrimitiveAllocator.class.getName());
}
}
- public OGLESContext() {
- }
+ public OGLESContext() {}
@Override
public Type getType() {
@@ -114,9 +113,11 @@ public GLSurfaceView createView(Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
// below 4.0, check OpenGL ES 2.0 support.
if (info.reqGlEsVersion < 0x20000) {
- throw new UnsupportedOperationException("OpenGL ES 2.0 or better is not supported on this device");
+ throw new UnsupportedOperationException(
+ "OpenGL ES 2.0 or better is not supported on this device"
+ );
}
- } else if (Build.VERSION.SDK_INT < 9){
+ } else if (Build.VERSION.SDK_INT < 9) {
throw new UnsupportedOperationException("jME3 requires Android 2.3 or later");
}
@@ -126,7 +127,7 @@ public GLSurfaceView createView(Context context) {
if (androidInput == null) {
if (Build.VERSION.SDK_INT >= 14) {
androidInput = new AndroidInputHandler14();
- } else if (Build.VERSION.SDK_INT >= 9){
+ } else if (Build.VERSION.SDK_INT >= 9) {
androidInput = new AndroidInputHandler();
}
}
@@ -136,7 +137,7 @@ public GLSurfaceView createView(Context context) {
// setEGLContextClientVersion must be set before calling setRenderer
// this means it cannot be set in AndroidConfigChooser (too late)
// use proper openGL ES version
- view.setEGLContextClientVersion(info.reqGlEsVersion>>16);
+ view.setEGLContextClientVersion(info.reqGlEsVersion >> 16);
view.setFocusableInTouchMode(true);
view.setFocusable(true);
@@ -199,22 +200,35 @@ protected void initInThread() {
logger.log(Level.FINE, "Running on thread: {0}", Thread.currentThread().getName());
// Setup unhandled Exception Handler
- Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
- @Override
- public void uncaughtException(Thread thread, Throwable thrown) {
- listener.handleError("Exception thrown in " + thread.toString(), thrown);
- }
- });
+ Thread
+ .currentThread()
+ .setUncaughtExceptionHandler(
+ new Thread.UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread thread, Throwable thrown) {
+ listener.handleError("Exception thrown in " + thread.toString(), thrown);
+ }
+ }
+ );
timer = new NanoTimer();
GL gl = new AndroidGL();
if (settings.getBoolean("GraphicsDebug")) {
- gl = (GL) GLDebug.createProxy(gl, gl, GL.class, GL2.class, GLES_30.class, GLFbo.class, GLExt.class);
+ gl =
+ (GL) GLDebug.createProxy(
+ gl,
+ gl,
+ GL.class,
+ GL2.class,
+ GLES_30.class,
+ GLFbo.class,
+ GLExt.class
+ );
}
if (settings.getBoolean("GraphicsTrace")) {
- gl = (GL)GLTracer.createGlesTracer(gl, GL.class, GLES_30.class, GLFbo.class, GLExt.class);
+ gl = (GL) GLTracer.createGlesTracer(gl, GL.class, GLES_30.class, GLFbo.class, GLExt.class);
}
- renderer = new GLRenderer(gl, (GLExt)gl, (GLFbo)gl);
+ renderer = new GLRenderer(gl, (GLExt) gl, (GLFbo) gl);
renderer.initialize();
JmeSystem.setSoftTextDialogInput(this);
@@ -233,10 +247,19 @@ protected void deinitInThread() {
}
listener.destroy();
-
+ // releases the view holder from the Android Input Resources
+ // releasing the view enables the context instance to be
+ // reclaimed by the GC.
+ // if not released; it leads to a weak reference leak
+ // disabling the destruction of the Context View Holder.
+ androidInput.setView(null);
+
+ // nullifying the references
+ // signals their memory to be reclaimed
listener = null;
renderer = null;
timer = null;
+ androidInput = null;
// do android specific cleaning here
logger.fine("Display destroyed.");
@@ -253,7 +276,7 @@ public void setSettings(AppSettings settings) {
}
if (settings.getFrameRate() > 0) {
- minFrameDuration = (long)(1000d / settings.getFrameRate()); // ms
+ minFrameDuration = (long) (1000d / settings.getFrameRate()); // ms
logger.log(Level.FINE, "Setting min tpf: {0}ms", minFrameDuration);
} else {
minFrameDuration = 0;
@@ -311,8 +334,7 @@ public Timer getTimer() {
}
@Override
- public void setTitle(String title) {
- }
+ public void setTitle(String title) {}
@Override
public boolean isCreated() {
@@ -328,7 +350,11 @@ public void setAutoFlushFrames(boolean enabled) {
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
if (logger.isLoggable(Level.FINE)) {
- logger.log(Level.FINE, "GL Surface changed, width: {0} height: {1}", new Object[]{width, height});
+ logger.log(
+ Level.FINE,
+ "GL Surface changed, width: {0} height: {1}",
+ new Object[] { width, height }
+ );
}
// update the application settings with the new resolution
settings.setResolution(width, height);
@@ -371,16 +397,14 @@ public void onDrawFrame(GL10 gl) {
// Enforce a FPS cap
if (updateDelta < minFrameDuration) {
-// logger.log(Level.INFO, "lastUpdateTime: {0}, updateDelta: {1}, minTimePerFrame: {2}",
-// new Object[]{lastUpdateTime, updateDelta, minTimePerFrame});
+ // logger.log(Level.INFO, "lastUpdateTime: {0}, updateDelta: {1}, minTimePerFrame: {2}",
+ // new Object[]{lastUpdateTime, updateDelta, minTimePerFrame});
try {
Thread.sleep(minFrameDuration - updateDelta);
- } catch (InterruptedException e) {
- }
+ } catch (InterruptedException e) {}
}
lastUpdateTime = System.currentTimeMillis();
-
}
}
@@ -401,8 +425,7 @@ public void create() {
}
@Override
- public void restart() {
- }
+ public void restart() {}
@Override
public void destroy(boolean waitFor) {
@@ -420,76 +443,99 @@ protected void waitFor(boolean createdVal) {
while (renderable.get() != createdVal) {
try {
Thread.sleep(10);
- } catch (InterruptedException ex) {
- }
+ } catch (InterruptedException ex) {}
}
}
@Override
- public void requestDialog(final int id, final String title, final String initialValue, final SoftTextDialogInputListener listener) {
+ public void requestDialog(
+ final int id,
+ final String title,
+ final String initialValue,
+ final SoftTextDialogInputListener listener
+ ) {
if (logger.isLoggable(Level.FINE)) {
- logger.log(Level.FINE, "requestDialog: title: {0}, initialValue: {1}",
- new Object[]{title, initialValue});
+ logger.log(
+ Level.FINE,
+ "requestDialog: title: {0}, initialValue: {1}",
+ new Object[] { title, initialValue }
+ );
}
final View view = JmeAndroidSystem.getView();
- view.getHandler().post(new Runnable() {
- @Override
- public void run() {
-
- final FrameLayout layoutTextDialogInput = new FrameLayout(view.getContext());
- final EditText editTextDialogInput = new EditText(view.getContext());
- editTextDialogInput.setWidth(LayoutParams.FILL_PARENT);
- editTextDialogInput.setHeight(LayoutParams.FILL_PARENT);
- editTextDialogInput.setPadding(20, 20, 20, 20);
- editTextDialogInput.setGravity(Gravity.FILL_HORIZONTAL);
- //editTextDialogInput.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
-
- editTextDialogInput.setText(initialValue);
-
- switch (id) {
- case SoftTextDialogInput.TEXT_ENTRY_DIALOG:
-
- editTextDialogInput.setInputType(InputType.TYPE_CLASS_TEXT);
- break;
-
- case SoftTextDialogInput.NUMERIC_ENTRY_DIALOG:
-
- editTextDialogInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL | InputType.TYPE_NUMBER_FLAG_SIGNED);
- break;
-
- case SoftTextDialogInput.NUMERIC_KEYPAD_DIALOG:
-
- editTextDialogInput.setInputType(InputType.TYPE_CLASS_PHONE);
- break;
-
- default:
- break;
+ view
+ .getHandler()
+ .post(
+ new Runnable() {
+ @Override
+ public void run() {
+ final FrameLayout layoutTextDialogInput = new FrameLayout(view.getContext());
+ final EditText editTextDialogInput = new EditText(view.getContext());
+ editTextDialogInput.setWidth(LayoutParams.FILL_PARENT);
+ editTextDialogInput.setHeight(LayoutParams.FILL_PARENT);
+ editTextDialogInput.setPadding(20, 20, 20, 20);
+ editTextDialogInput.setGravity(Gravity.FILL_HORIZONTAL);
+ //editTextDialogInput.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
+
+ editTextDialogInput.setText(initialValue);
+
+ switch (id) {
+ case SoftTextDialogInput.TEXT_ENTRY_DIALOG:
+ editTextDialogInput.setInputType(InputType.TYPE_CLASS_TEXT);
+ break;
+ case SoftTextDialogInput.NUMERIC_ENTRY_DIALOG:
+ editTextDialogInput.setInputType(
+ InputType.TYPE_CLASS_NUMBER |
+ InputType.TYPE_NUMBER_FLAG_DECIMAL |
+ InputType.TYPE_NUMBER_FLAG_SIGNED
+ );
+ break;
+ case SoftTextDialogInput.NUMERIC_KEYPAD_DIALOG:
+ editTextDialogInput.setInputType(InputType.TYPE_CLASS_PHONE);
+ break;
+ default:
+ break;
+ }
+
+ layoutTextDialogInput.addView(editTextDialogInput);
+
+ AlertDialog dialogTextInput = new AlertDialog.Builder(view.getContext())
+ .setTitle(title)
+ .setView(layoutTextDialogInput)
+ .setPositiveButton(
+ "OK",
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ /* User clicked OK, send COMPLETE action
+ * and text */
+ listener.onSoftText(
+ SoftTextDialogInputListener.COMPLETE,
+ editTextDialogInput.getText().toString()
+ );
+ }
+ }
+ )
+ .setNegativeButton(
+ "Cancel",
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ /* User clicked CANCEL, send CANCEL action
+ * and text */
+ listener.onSoftText(
+ SoftTextDialogInputListener.CANCEL,
+ editTextDialogInput.getText().toString()
+ );
+ }
+ }
+ )
+ .create();
+
+ dialogTextInput.show();
+ }
}
-
- layoutTextDialogInput.addView(editTextDialogInput);
-
- AlertDialog dialogTextInput = new AlertDialog.Builder(view.getContext()).setTitle(title).setView(layoutTextDialogInput).setPositiveButton("OK",
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int whichButton) {
- /* User clicked OK, send COMPLETE action
- * and text */
- listener.onSoftText(SoftTextDialogInputListener.COMPLETE, editTextDialogInput.getText().toString());
- }
- }).setNegativeButton("Cancel",
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int whichButton) {
- /* User clicked CANCEL, send CANCEL action
- * and text */
- listener.onSoftText(SoftTextDialogInputListener.CANCEL, editTextDialogInput.getText().toString());
- }
- }).create();
-
- dialogTextInput.show();
- }
- });
+ );
}
@Override
@@ -541,11 +587,11 @@ public int getWindowXPosition() {
public int getWindowYPosition() {
throw new UnsupportedOperationException("not implemented yet");
}
-
+
/**
* Retrieves the dimensions of the input surface. Note: do not modify the
* returned object.
- *
+ *
* @return the dimensions (in pixels, left and top are 0)
*/
private Rect getSurfaceFrame() {
@@ -554,4 +600,16 @@ private Rect getSurfaceFrame() {
Rect result = holder.getSurfaceFrame();
return result;
}
+
+ @Override
+ public Displays getDisplays() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public int getPrimaryDisplay() {
+ // TODO Auto-generated method stub
+ return 0;
+ }
}
diff --git a/jme3-android/src/main/java/com/jme3/view/surfaceview/JmeSurfaceView.java b/jme3-android/src/main/java/com/jme3/view/surfaceview/JmeSurfaceView.java
index d364ab0923..67d73f4b5c 100644
--- a/jme3-android/src/main/java/com/jme3/view/surfaceview/JmeSurfaceView.java
+++ b/jme3-android/src/main/java/com/jme3/view/surfaceview/JmeSurfaceView.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2022 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -239,11 +239,7 @@ public void startRenderer(int delayMillis) {
}
private void removeGLSurfaceView() {
- ((Activity) getContext()).runOnUiThread(() -> {
- if (glSurfaceView != null) {
- JmeSurfaceView.this.removeView(glSurfaceView);
- }
- });
+ ((Activity) getContext()).runOnUiThread(() -> JmeSurfaceView.this.removeView(glSurfaceView));
}
@Override
@@ -265,19 +261,34 @@ public void handleError(String errorMsg, Throwable throwable) {
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
switch (event) {
case ON_DESTROY:
- /*destroy only if the policy flag is enabled*/
- if (destructionPolicy == DestructionPolicy.DESTROY_WHEN_FINISH) {
- legacyApplication.stop(!isGLThreadPaused());
- }
+ // activity is off the foreground stack
+ // activity is being destructed completely as a result of Activity#finish()
+ // this is a killable automata state!
+ jmeSurfaceViewLogger.log(Level.INFO, "Hosting Activity has been destructed.");
break;
case ON_PAUSE:
- loseFocus();
+ // activity is still on the foreground stack but not
+ // on the topmost level or before transition to stopped/hidden or destroyed state
+ // as a result of dispatch to Activity#finish()
+ // activity is no longer visible and is out of foreground
+ if (((Activity) getContext()).isFinishing()) {
+ if (destructionPolicy == DestructionPolicy.DESTROY_WHEN_FINISH) {
+ legacyApplication.stop(!isGLThreadPaused());
+ } else if (destructionPolicy == DestructionPolicy.KEEP_WHEN_FINISH) {
+ jmeSurfaceViewLogger.log(Level.INFO, "Context stops, but game is still running.");
+ }
+ } else {
+ loseFocus();
+ }
break;
case ON_RESUME:
+ // activity is back to the topmost of the
+ // foreground stack
gainFocus();
break;
case ON_STOP:
- jmeSurfaceViewLogger.log(Level.INFO, "Context stops, but game is still running");
+ // activity is out off the foreground stack or being destructed by a finishing dispatch
+ // this is a killable automata state!
break;
}
}
@@ -404,13 +415,13 @@ public void loseFocus() {
@Override
public void destroy() {
- /*skip the destroy block if the invoking instance is null*/
- if (legacyApplication == null) {
- return;
+ if (glSurfaceView != null) {
+ removeGLSurfaceView();
+ }
+ if (legacyApplication != null) {
+ legacyApplication.destroy();
}
- removeGLSurfaceView();
- legacyApplication.destroy();
- /*help the Dalvik Garbage collector to destruct the pointers, by making them nullptr*/
+ /*help the Dalvik Garbage collector to destruct the objects, by releasing their references*/
/*context instances*/
legacyApplication = null;
appSettings = null;
@@ -430,10 +441,10 @@ public void destroy() {
onRendererCompleted = null;
onExceptionThrown = null;
onLayoutDrawn = null;
- /*nullifying the static memory (pushing zero to registers to prepare for a clean use)*/
GameState.setLegacyApplication(null);
GameState.setFirstUpdatePassed(false);
- jmeSurfaceViewLogger.log(Level.INFO, "Context and Game have been destructed");
+ JmeAndroidSystem.setView(null);
+ jmeSurfaceViewLogger.log(Level.INFO, "Context and Game have been destructed.");
}
@Override
@@ -516,11 +527,13 @@ public void bindAppStateToActivityLifeCycle(final boolean condition) {
/*register this Ui Component as an observer to the context of jmeSurfaceView only if this context is a LifeCycleOwner*/
if (getContext() instanceof LifecycleOwner) {
((LifecycleOwner) getContext()).getLifecycle().addObserver(JmeSurfaceView.this);
+ jmeSurfaceViewLogger.log(Level.INFO, "Command binding SurfaceView to the Activity Lifecycle.");
}
} else {
/*un-register this Ui Component as an observer to the context of jmeSurfaceView only if this context is a LifeCycleOwner*/
if (getContext() instanceof LifecycleOwner) {
((LifecycleOwner) getContext()).getLifecycle().removeObserver(JmeSurfaceView.this);
+ jmeSurfaceViewLogger.log(Level.INFO, "Command removing SurfaceView from the Activity Lifecycle.");
}
}
}
@@ -917,7 +930,7 @@ public void setShowErrorDialog(boolean showErrorDialog) {
}
/**
- * Determines whether the app context would be destructed
+ * Determines whether the app context would be destructed as a result of dispatching {@link Activity#finish()}
* with the holder activity context in case of {@link DestructionPolicy#DESTROY_WHEN_FINISH} or be
* spared for a second use in case of {@link DestructionPolicy#KEEP_WHEN_FINISH}.
* Default value is : {@link DestructionPolicy#DESTROY_WHEN_FINISH}.
@@ -926,12 +939,14 @@ public void setShowErrorDialog(boolean showErrorDialog) {
*/
public enum DestructionPolicy {
/**
- * Finishes the game context with the activity context (ignores the static memory {@link GameState#legacyApplication}).
+ * Finishes the game context with the activity context (ignores the static memory {@link GameState#legacyApplication})
+ * as a result of dispatching {@link Activity#finish()}.
*/
DESTROY_WHEN_FINISH,
/**
* Spares the game context inside a static memory {@link GameState#legacyApplication}
- * when the activity context is destroyed, but the app stills in the background.
+ * when the activity context is destroyed dispatching {@link Activity#finish()}, but the {@link android.app.Application}
+ * stills in the background.
*/
KEEP_WHEN_FINISH
}
diff --git a/jme3-awt-dialogs/src/main/java/com/jme3/awt/AWTSettingsDialog.java b/jme3-awt-dialogs/src/main/java/com/jme3/awt/AWTSettingsDialog.java
index 34744b2298..efbb721054 100644
--- a/jme3-awt-dialogs/src/main/java/com/jme3/awt/AWTSettingsDialog.java
+++ b/jme3-awt-dialogs/src/main/java/com/jme3/awt/AWTSettingsDialog.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2022 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -57,12 +57,11 @@
import javax.swing.*;
/**
- * SettingsDialog displays a Swing dialog box to interactively
- * configure the AppSettings of a desktop application before
- * start() is invoked.
- *
- * The AppSettings instance to be configured is passed to the
- * constructor.
+ * `AWTSettingsDialog` displays a Swing dialog box to interactively
+ * configure the `AppSettings` of a desktop application before
+ * `start()` is invoked.
+ *
+ * The `AppSettings` instance to be configured is passed to the constructor.
*
* @see AppSettings
* @author Mark Powell
@@ -71,14 +70,33 @@
*/
public final class AWTSettingsDialog extends JFrame {
- public static interface SelectionListener {
-
- public void onSelection(int selection);
+ /**
+ * Listener interface for handling selection events from the settings dialog.
+ */
+ public interface SelectionListener {
+ /**
+ * Called when a selection is made in the settings dialog (OK or Cancel).
+ *
+ * @param selection The type of selection made: `NO_SELECTION`, `APPROVE_SELECTION`, or `CANCEL_SELECTION`.
+ */
+ void onSelection(int selection);
}
private static final Logger logger = Logger.getLogger(AWTSettingsDialog.class.getName());
private static final long serialVersionUID = 1L;
- public static final int NO_SELECTION = 0, APPROVE_SELECTION = 1, CANCEL_SELECTION = 2;
+
+ /**
+ * Indicates that no selection has been made yet.
+ */
+ public static final int NO_SELECTION = 0;
+ /**
+ * Indicates that the user approved the settings.
+ */
+ public static final int APPROVE_SELECTION = 1;
+ /**
+ * Indicates that the user canceled the settings dialog.
+ */
+ public static final int CANCEL_SELECTION = 2;
// Resource bundle for i18n.
ResourceBundle resourceBundle = ResourceBundle.getBundle("com.jme3.app/SettingsDialog");
@@ -86,8 +104,12 @@ public static interface SelectionListener {
// the instance being configured
private final AppSettings source;
- // Title Image
+ /**
+ * The URL of the image file to be displayed as a title icon in the dialog.
+ * Can be `null` if no image is desired.
+ */
private URL imageFile = null;
+
// Array of supported display modes
private DisplayMode[] modes = null;
private static final DisplayMode[] windowDefaults = new DisplayMode[] {
@@ -114,10 +136,24 @@ public static interface SelectionListener {
private int minWidth = 0;
private int minHeight = 0;
+ /**
+ * Displays a settings dialog using the provided `AppSettings` source.
+ * Settings will be loaded from preferences.
+ *
+ * @param sourceSettings The `AppSettings` instance to configure.
+ * @return `true` if the user approved the settings, `false` otherwise.
+ */
public static boolean showDialog(AppSettings sourceSettings) {
return showDialog(sourceSettings, true);
}
+ /**
+ * Displays a settings dialog using the provided `AppSettings` source.
+ *
+ * @param sourceSettings The `AppSettings` instance to configure.
+ * @param loadSettings If `true`, settings will be loaded from preferences; otherwise, they will be merged.
+ * @return `true` if the user approved the settings, `false` otherwise.
+ */
public static boolean showDialog(AppSettings sourceSettings, boolean loadSettings) {
String iconPath = sourceSettings.getSettingsDialogImage();
final URL iconUrl = JmeSystem.class.getResource(iconPath.startsWith("/") ? iconPath : "/" + iconPath);
@@ -127,10 +163,30 @@ public static boolean showDialog(AppSettings sourceSettings, boolean loadSetting
return showDialog(sourceSettings, iconUrl, loadSettings);
}
+ /**
+ * Displays a settings dialog using the provided `AppSettings` source and an image file path.
+ *
+ * @param sourceSettings The `AppSettings` instance to configure.
+ * @param imageFile The path to the image file to use as the title of the dialog;
+ * `null` will result in no image being displayed.
+ * @param loadSettings If `true`, settings will be loaded from preferences; otherwise, they will be merged.
+ * @return `true` if the user approved the settings, `false` otherwise.
+ */
public static boolean showDialog(AppSettings sourceSettings, String imageFile, boolean loadSettings) {
return showDialog(sourceSettings, getURL(imageFile), loadSettings);
}
+ /**
+ * Displays a settings dialog using the provided `AppSettings` source and an image URL.
+ * This method blocks until the dialog is closed.
+ *
+ * @param sourceSettings The `AppSettings` instance to configure (not null).
+ * @param imageFile The `URL` pointing to the image file to use as the title of the dialog;
+ * `null` will result in no image being displayed.
+ * @param loadSettings If `true`, the dialog will copy settings from preferences. If `false`
+ * and preferences exist, they will be merged with the current settings.
+ * @return `true` if the user approved the settings, `false` otherwise (`CANCEL_SELECTION` or dialog close).
+ */
public static boolean showDialog(AppSettings sourceSettings, URL imageFile, boolean loadSettings) {
if (SwingUtilities.isEventDispatchThread()) {
throw new IllegalStateException("Cannot run from EDT");
@@ -166,46 +222,47 @@ public void onSelection(int selection) {
synchronized (lock) {
while (!done.get()) {
try {
+ // Wait until notified by the selection listener
lock.wait();
} catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ logger.log(Level.WARNING, "Settings dialog thread interrupted while waiting.", ex);
+ return false; // Treat as cancel if interrupted
}
}
}
- sourceSettings.copyFrom(settings);
+ // If approved, copy the modified settings back to the original source
+ if (result.get() == APPROVE_SELECTION) {
+ sourceSettings.copyFrom(settings);
+ }
- return result.get() == AWTSettingsDialog.APPROVE_SELECTION;
+ return result.get() == APPROVE_SELECTION;
}
/**
- * Instantiate a SettingsDialog for the primary display.
+ * Constructs a `SettingsDialog` for the primary display.
*
- * @param source
- * the AppSettings (not null)
- * @param imageFile
- * the image file to use as the title of the dialog;
- * null will result in to image being displayed
- * @param loadSettings
- * if true, copy the settings, otherwise merge them
- * @throws IllegalArgumentException
- * if the source is null
+ * @param source The `AppSettings` instance to configure (not null).
+ * @param imageFile The path to the image file to use as the title of the dialog;
+ * `null` will result in no image being displayed.
+ * @param loadSettings If `true`, the dialog will copy settings from preferences. If `false`
+ * and preferences exist, they will be merged with the current settings.
+ * @throws IllegalArgumentException if `source` is `null`.
*/
protected AWTSettingsDialog(AppSettings source, String imageFile, boolean loadSettings) {
this(source, getURL(imageFile), loadSettings);
}
/**
- * /** Instantiate a SettingsDialog for the primary display.
+ * Constructs a `SettingsDialog` for the primary display.
*
- * @param source
- * the AppSettings object (not null)
- * @param imageFile
- * the image file to use as the title of the dialog;
- * null will result in to image being displayed
- * @param loadSettings
- * if true, copy the settings, otherwise merge them
- * @throws IllegalArgumentException
- * if the source is null
+ * @param source The `AppSettings` instance to configure (not null).
+ * @param imageFile The `URL` pointing to the image file to use as the title of the dialog;
+ * `null` will result in no image being displayed.
+ * @param loadSettings If `true`, the dialog will copy settings from preferences. If `false`
+ * and preferences exist, they will be merged with the current settings.
+ * @throws IllegalArgumentException if `source` is `null`.
*/
protected AWTSettingsDialog(AppSettings source, URL imageFile, boolean loadSettings) {
if (source == null) {
@@ -232,7 +289,10 @@ protected AWTSettingsDialog(AppSettings source, URL imageFile, boolean loadSetti
minHeight = source.getMinHeight();
try {
+ logger.log(Level.INFO, "Loading AppSettings from PreferenceKey: {0}", appTitle);
registrySettings.load(appTitle);
+ AppSettings.printPreferences(appTitle);
+
} catch (BackingStoreException ex) {
logger.log(Level.WARNING, "Failed to load settings", ex);
}
@@ -355,8 +415,6 @@ public void showDialog() {
* init creates the components to use the dialog.
*/
private void createUI() {
- GridBagConstraints gbc;
-
JPanel mainPanel = new JPanel(new GridBagLayout());
addWindowListener(new WindowAdapter() {
@@ -368,8 +426,9 @@ public void windowClosing(WindowEvent e) {
}
});
- if (source.getIcons() != null) {
- safeSetIconImages(Arrays.asList((BufferedImage[]) source.getIcons()));
+ Object[] sourceIcons = source.getIcons();
+ if (sourceIcons != null && sourceIcons.length > 0) {
+ safeSetIconImages(Arrays.asList((BufferedImage[]) sourceIcons));
}
setTitle(MessageFormat.format(resourceBundle.getString("frame.title"), source.getTitle()));
@@ -419,7 +478,7 @@ public void actionPerformed(ActionEvent e) {
gammaBox = new JCheckBox(resourceBundle.getString("checkbox.gamma"));
gammaBox.setSelected(source.isGammaCorrection());
- gbc = new GridBagConstraints();
+ GridBagConstraints gbc = new GridBagConstraints();
gbc.weightx = 0.5;
gbc.gridx = 0;
gbc.gridwidth = 2;
@@ -493,7 +552,6 @@ public void actionPerformed(ActionEvent e) {
// Set the button action listeners. Cancel disposes without saving, OK
// saves.
ok.addActionListener(new ActionListener() {
-
@Override
public void actionPerformed(ActionEvent e) {
if (verifyAndSaveCurrentSelection()) {
@@ -501,12 +559,13 @@ public void actionPerformed(ActionEvent e) {
dispose();
// System.gc() should be called to prevent "X Error of
- // failed request: RenderBadPicture (invalid Picture
- // parameter)"
+ // failed request: RenderBadPicture (invalid Picture parameter)"
// on Linux when using AWT/Swing + GLFW.
// For more info see:
// https://github.com/LWJGL/lwjgl3/issues/149,
- // https://hub.jmonkeyengine.org/t/experimenting-lwjgl3/37275
+
+ // intentional double call. see this discussion:
+ // https://hub.jmonkeyengine.org/t/experimenting-lwjgl3/37275/12
System.gc();
System.gc();
}
@@ -514,7 +573,6 @@ public void actionPerformed(ActionEvent e) {
});
cancel.addActionListener(new ActionListener() {
-
@Override
public void actionPerformed(ActionEvent e) {
setUserSelection(CANCEL_SELECTION);
@@ -568,7 +626,6 @@ public void run() {
colorDepthCombo.setSelectedItem(source.getBitsPerPixel() + " bpp");
}
});
-
}
/*
@@ -577,10 +634,8 @@ public void run() {
*/
private void safeSetIconImages(List extends Image> icons) {
try {
- // Due to Java bug 6445278, we try to set icon on our shared owner
- // frame first.
- // Otherwise, our alt-tab icon will be the Java default under
- // Windows.
+ // Due to Java bug 6445278, we try to set icon on our shared owner frame first.
+ // Otherwise, our alt-tab icon will be the Java default under Windows.
Window owner = getOwner();
if (owner != null) {
Method setIconImages = owner.getClass().getMethod("setIconImages", List.class);
@@ -608,9 +663,9 @@ private boolean verifyAndSaveCurrentSelection() {
boolean vsync = vsyncBox.isSelected();
boolean gamma = gammaBox.isSelected();
- int width = Integer.parseInt(display.substring(0, display.indexOf(" x ")));
- display = display.substring(display.indexOf(" x ") + 3);
- int height = Integer.parseInt(display);
+ String[] parts = display.split(" x ");
+ int width = Integer.parseInt(parts[0]);
+ int height = Integer.parseInt(parts[1]);
String depthString = (String) colorDepthCombo.getSelectedItem();
int depth = -1;
@@ -639,21 +694,20 @@ private boolean verifyAndSaveCurrentSelection() {
}
// FIXME: Does not work in Linux
- /*
- * if (!fullscreen) { //query the current bit depth of the desktop int
- * curDepth = GraphicsEnvironment.getLocalGraphicsEnvironment()
- * .getDefaultScreenDevice().getDisplayMode().getBitDepth(); if (depth >
- * curDepth) { showError(this,"Cannot choose a higher bit depth in
- * windowed " + "mode than your current desktop bit depth"); return
- * false; } }
- */
-
- boolean valid = false;
+// if (!fullscreen) { //query the current bit depth of the desktop int
+// curDepth = GraphicsEnvironment.getLocalGraphicsEnvironment()
+// .getDefaultScreenDevice().getDisplayMode().getBitDepth();
+// if (depth > curDepth) {
+// showError(this, "Cannot choose a higher bit depth in
+// windowed" + "mode than your current desktop bit depth");
+// return false;
+// }
+// }
+
+ boolean valid = true;
// test valid display mode when going full screen
- if (!fullscreen) {
- valid = true;
- } else {
+ if (fullscreen) {
GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
valid = device.isFullScreenSupported();
}
@@ -673,7 +727,10 @@ private boolean verifyAndSaveCurrentSelection() {
String appTitle = source.getTitle();
try {
+ logger.log(Level.INFO, "Saving AppSettings to PreferencesKey: {0}", appTitle);
source.save(appTitle);
+ AppSettings.printPreferences(appTitle);
+
} catch (BackingStoreException ex) {
logger.log(Level.WARNING, "Failed to save setting changes", ex);
}
@@ -769,7 +826,9 @@ private void updateResolutionChoices() {
private void updateAntialiasChoices() {
// maybe in the future will add support for determining this info
// through PBuffer
- String[] choices = new String[] { resourceBundle.getString("antialias.disabled"), "2x", "4x", "6x", "8x", "16x" };
+ String[] choices = new String[] {
+ resourceBundle.getString("antialias.disabled"), "2x", "4x", "6x", "8x", "16x"
+ };
antialiasCombo.setModel(new DefaultComboBoxModel<>(choices));
antialiasCombo.setSelectedItem(choices[Math.min(source.getSamples() / 2, 5)]);
}
@@ -792,6 +851,12 @@ private static URL getURL(String file) {
return url;
}
+ /**
+ * Displays an error message dialog to the user.
+ *
+ * @param parent The parent `Component` for the dialog.
+ * @param message The message `String` to display.
+ */
private static void showError(java.awt.Component parent, String message) {
JOptionPane.showMessageDialog(parent, message, "Error", JOptionPane.ERROR_MESSAGE);
}
@@ -852,7 +917,7 @@ private String[] getWindowedResolutions(DisplayMode[] modes) {
* Returns every possible bit depth for the given resolution.
*/
private static String[] getDepths(String resolution, DisplayMode[] modes) {
- List depths = new ArrayList<>(4);
+ Set depths = new LinkedHashSet<>(4); // Use LinkedHashSet for uniqueness and order
for (DisplayMode mode : modes) {
int bitDepth = mode.getBitDepth();
if (bitDepth == DisplayMode.BIT_DEPTH_MULTI) {
@@ -865,12 +930,8 @@ private static String[] getDepths(String resolution, DisplayMode[] modes) {
continue;
}
String res = mode.getWidth() + " x " + mode.getHeight();
- if (!res.equals(resolution)) {
- continue;
- }
- String depth = bitDepth + " bpp";
- if (!depths.contains(depth)) {
- depths.add(depth);
+ if (res.equals(resolution)) {
+ depths.add(bitDepth + " bpp");
}
}
@@ -884,10 +945,15 @@ private static String[] getDepths(String resolution, DisplayMode[] modes) {
}
/**
- * Returns every possible refresh rate for the given resolution.
+ * Returns every possible unique refresh rate string ("XX Hz" or "???")
+ * for the given resolution from an array of `DisplayMode`s.
+ *
+ * @param resolution The resolution string (e.g., "1280 x 720") to filter by.
+ * @param modes The array of `DisplayMode`s to process.
+ * @return An array of unique refresh rate strings.
*/
private static String[] getFrequencies(String resolution, DisplayMode[] modes) {
- List freqs = new ArrayList<>(4);
+ Set freqs = new LinkedHashSet<>(4); // Use LinkedHashSet for uniqueness and order
for (DisplayMode mode : modes) {
String res = mode.getWidth() + " x " + mode.getHeight();
String freq;
@@ -896,20 +962,19 @@ private static String[] getFrequencies(String resolution, DisplayMode[] modes) {
} else {
freq = mode.getRefreshRate() + " Hz";
}
- if (res.equals(resolution) && !freqs.contains(freq)) {
- freqs.add(freq);
- }
+ freqs.add(freq);
}
return freqs.toArray(new String[0]);
}
/**
- * Chooses the closest frequency to 60 Hz.
- *
- * @param resolution
- * @param modes
- * @return
+ * Chooses the closest known refresh rate to 60 Hz for a given resolution.
+ * If no known refresh rates are found for the resolution, returns `null`.
+ *
+ * @param resolution The resolution string (e.g., "1280 x 720") to find the best frequency for.
+ * @param modes The array of `DisplayMode`s to search within.
+ * @return The best frequency string (e.g., "60 Hz") or `null` if no suitable frequency is found.
*/
private static String getBestFrequency(String resolution, DisplayMode[] modes) {
int closest = Integer.MAX_VALUE;
diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java
index d06b0816cc..4a47008542 100644
--- a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java
+++ b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2022 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -52,6 +52,7 @@
* @author Nehon
*/
public class AnimComposer extends AbstractControl {
+
/**
* The name of the default layer.
*/
@@ -66,7 +67,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));
}
/**
@@ -121,7 +122,7 @@ public void removeAnimClip(AnimClip anim) {
* @return The action corresponding to the given name.
*/
public Action setCurrentAction(String name) {
- return setCurrentAction(name, DEFAULT_LAYER);
+ return setCurrentAction(name, DEFAULT_LAYER, true);
}
/**
@@ -144,9 +145,9 @@ public Action setCurrentAction(String actionName, String layerName) {
* @return The action corresponding to the given name.
*/
public Action setCurrentAction(String actionName, String layerName, boolean loop) {
- AnimLayer l = getLayer(layerName);
+ AnimLayer layer = getLayer(layerName);
Action currentAction = action(actionName);
- l.setCurrentAction(actionName, currentAction, loop);
+ layer.setCurrentAction(actionName, currentAction, loop);
return currentAction;
}
@@ -239,7 +240,8 @@ public void setTime(String layerName, double time) {
/**
*
* @param name The name of the action to return.
- * @return The action registered with specified name. It will make a new action if there isn't any.
+ * @return The action registered with specified name. It will make a new
+ * action if there isn't any.
* @see #makeAction(java.lang.String)
*/
public Action action(String name) {
@@ -254,7 +256,8 @@ public Action action(String name) {
/**
*
* @param name The name of the action to return.
- * @return The action registered with specified name or null if nothing is registered.
+ * @return The action registered with specified name or null if nothing is
+ * registered.
*/
public Action getAction(String name) {
return actions.get(name);
@@ -314,7 +317,7 @@ public Action removeAction(String name) {
* @param mask the desired mask for the new layer (alias created)
*/
public void makeLayer(String name, AnimationMask mask) {
- AnimLayer l = new AnimLayer(this, name, mask);
+ AnimLayer l = new AnimLayer(name, mask);
layers.put(name, l);
}
@@ -328,8 +331,8 @@ public void removeLayer(String name) {
}
/**
- * Creates an action that will interpolate over an entire sequence
- * of tweens in order.
+ * Creates an action that will interpolate over an entire sequence of tweens
+ * in order.
*
* @param name a name for the new Action
* @param tweens the desired sequence of tweens
@@ -371,8 +374,9 @@ public void reset() {
}
/**
- * Returns an unmodifiable collection of all available animations. When an attempt
- * is made to modify the collection, an UnsupportedOperationException is thrown.
+ * Returns an unmodifiable collection of all available animations. When an
+ * attempt is made to modify the collection, an
+ * UnsupportedOperationException is thrown.
*
* @return the unmodifiable collection of animations
*/
@@ -399,7 +403,7 @@ public Set getAnimClipsNames() {
@Override
protected void controlUpdate(float tpf) {
for (AnimLayer layer : layers.values()) {
- layer.update(tpf);
+ layer.update(tpf, globalSpeed);
}
}
@@ -523,9 +527,8 @@ public void cloneFields(Cloner cloner, Object original) {
for (String key : layers.keySet()) {
newLayers.put(key, cloner.clone(layers.get(key)));
}
-
+ newLayers.putIfAbsent(DEFAULT_LAYER, new AnimLayer(DEFAULT_LAYER, null));
layers = newLayers;
-
}
/**
@@ -542,6 +545,8 @@ public void read(JmeImporter im) throws IOException {
InputCapsule ic = im.getCapsule(this);
animClipMap = (Map) ic.readStringSavableMap("animClipMap", new HashMap());
globalSpeed = ic.readFloat("globalSpeed", 1f);
+ layers = (Map) ic.readStringSavableMap("layers", new HashMap());
+ layers.putIfAbsent(DEFAULT_LAYER, new AnimLayer(DEFAULT_LAYER, null));
}
/**
@@ -557,5 +562,6 @@ public void write(JmeExporter ex) throws IOException {
OutputCapsule oc = ex.getCapsule(this);
oc.writeStringSavableMap(animClipMap, "animClipMap", new HashMap());
oc.write(globalSpeed, "globalSpeed", 1f);
+ oc.writeStringSavableMap(layers, "layers", new HashMap());
}
}
diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimLayer.java b/jme3-core/src/main/java/com/jme3/anim/AnimLayer.java
index 0c3c97e998..6cd79e731d 100644
--- a/jme3-core/src/main/java/com/jme3/anim/AnimLayer.java
+++ b/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 @@
*
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 void setLooping(boolean loop) {
*
* @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 @@ void update(float appDeltaTimeInSeconds) {
*/
@Override
public void cloneFields(Cloner cloner, Object original) {
- composer = cloner.clone(composer);
currentAction = null;
currentActionName = null;
}
@@ -306,4 +311,20 @@ public Object jmeClone() {
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);
+ }
}
diff --git a/jme3-core/src/main/java/com/jme3/anim/Armature.java b/jme3-core/src/main/java/com/jme3/anim/Armature.java
index 5058fcec24..197242cfa1 100644
--- a/jme3-core/src/main/java/com/jme3/anim/Armature.java
+++ b/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 @@ private void createSkinningMatrices() {
/**
* Sets the JointModelTransform implementation
- * Default is {@link MatrixJointModelTransform}
+ * Default is {@link SeparateJointModelTransform}
*
* @param modelTransformClass which implementation to use
* @see JointModelTransform
diff --git a/jme3-core/src/main/java/com/jme3/anim/ArmatureMask.java b/jme3-core/src/main/java/com/jme3/anim/ArmatureMask.java
index 3aa2342f28..bf8e10155d 100644
--- a/jme3-core/src/main/java/com/jme3/anim/ArmatureMask.java
+++ b/jme3-core/src/main/java/com/jme3/anim/ArmatureMask.java
@@ -1,13 +1,50 @@
+/*
+ * 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;
+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.
@@ -49,6 +86,9 @@ private BitSet getAffectedJoints() {
* @param armature the Armature containing the joints (not null, unaffected)
* @param jointNames the names of the joints to be removed
* @return this
+ *
+ * @throws IllegalArgumentException if it can not find the joint
+ * with the specified name on the provided armature
*/
public ArmatureMask removeJoints(Armature armature, String... jointNames) {
for (String jointName : jointNames) {
@@ -72,6 +112,9 @@ public boolean contains(Object target) {
* @param armature the Armature containing the joints (not null)
* @param fromJoint the name of the ancestor joint
* @return a new mask
+ *
+ * @throws IllegalArgumentException if it can not find the joint
+ * with the specified name on the provided armature
*/
public static ArmatureMask createMask(Armature armature, String fromJoint) {
ArmatureMask mask = new ArmatureMask();
@@ -85,13 +128,13 @@ public static ArmatureMask createMask(Armature armature, String fromJoint) {
* @param armature the Armature containing the joints (not null)
* @param joints the names of the joints to be included
* @return a new mask
+ *
+ * @throws IllegalArgumentException if it can not find the joint
+ * with the specified name on the provided armature
*/
public static ArmatureMask createMask(Armature armature, String... joints) {
ArmatureMask mask = new ArmatureMask();
mask.addBones(armature, joints);
- for (String joint : joints) {
- mask.affectedJoints.set(armature.getJoint(joint).getId());
- }
return mask;
}
@@ -100,6 +143,9 @@ public static ArmatureMask createMask(Armature armature, String... joints) {
*
* @param armature the Armature containing the joints
* @param jointNames the names of the joints to be influenced
+ *
+ * @throws IllegalArgumentException if it can not find the joint
+ * with the specified name on the provided armature
*/
public void addBones(Armature armature, String... jointNames) {
for (String jointName : jointNames) {
@@ -121,6 +167,9 @@ private Joint findJoint(Armature armature, String jointName) {
*
* @param armature the Armature containing the ancestor joint
* @param jointName the names of the ancestor joint
+ *
+ * @throws IllegalArgumentException if it can not find the joint
+ * with the specified name on the provided armature
*/
public void addFromJoint(Armature armature, String jointName) {
Joint joint = findJoint(armature, jointName);
@@ -163,4 +212,16 @@ public ArmatureMask removeAncestors(Joint start) {
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);
+ }
}
diff --git a/jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java b/jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java
index a87fbc0f74..2d4fd32809 100644
--- a/jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java
+++ b/jme3-core/src/main/java/com/jme3/anim/MatrixJointModelTransform.java
@@ -1,3 +1,34 @@
+/*
+ * Copyright (c) 2009-2025 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
package com.jme3.anim;
import com.jme3.anim.util.JointModelTransform;
@@ -5,21 +36,36 @@
import com.jme3.math.Transform;
/**
- * This JointModelTransform implementation accumulate joints transforms in a Matrix4f to properly
- * support non uniform scaling in an armature hierarchy
+ * An implementation of {@link JointModelTransform} that accumulates joint transformations
+ * into a {@link Matrix4f}. This approach is particularly useful for correctly handling
+ * non-uniform scaling within an armature hierarchy, as {@code Matrix4f} can represent
+ * non-uniform scaling directly, unlike {@link Transform}, which typically handles
+ * uniform scaling.
+ *
+ * This class maintains a single {@link Matrix4f} to represent the accumulated
+ * model-space transform of the joint it's associated with.
*/
public class MatrixJointModelTransform implements JointModelTransform {
- final private Matrix4f modelTransformMatrix = new Matrix4f();
- final private Transform modelTransform = new Transform();
+ /**
+ * The model-space transform of the joint represented as a Matrix4f.
+ * This matrix accumulates the local transform of the joint and the model transform
+ * of its parent.
+ */
+ private final Matrix4f modelTransformMatrix = new Matrix4f();
+ /**
+ * A temporary Transform instance used for converting the modelTransformMatrix
+ * to a Transform object when {@link #getModelTransform()} is called.
+ */
+ private final Transform modelTransform = new Transform();
@Override
public void updateModelTransform(Transform localTransform, Joint parent) {
localTransform.toTransformMatrix(modelTransformMatrix);
if (parent != null) {
- ((MatrixJointModelTransform) parent.getJointModelTransform()).getModelTransformMatrix().mult(modelTransformMatrix, modelTransformMatrix);
+ MatrixJointModelTransform transform = (MatrixJointModelTransform) parent.getJointModelTransform();
+ transform.getModelTransformMatrix().mult(modelTransformMatrix, modelTransformMatrix);
}
-
}
@Override
@@ -31,7 +77,8 @@ public void getOffsetTransform(Matrix4f outTransform, Matrix4f inverseModelBindM
public void applyBindPose(Transform localTransform, Matrix4f inverseModelBindMatrix, Joint parent) {
modelTransformMatrix.set(inverseModelBindMatrix).invertLocal(); // model transform = model bind
if (parent != null) {
- ((MatrixJointModelTransform) parent.getJointModelTransform()).getModelTransformMatrix().invert().mult(modelTransformMatrix, modelTransformMatrix);
+ MatrixJointModelTransform transform = (MatrixJointModelTransform) parent.getJointModelTransform();
+ transform.getModelTransformMatrix().invert().mult(modelTransformMatrix, modelTransformMatrix);
}
localTransform.fromTransformMatrix(modelTransformMatrix);
}
diff --git a/jme3-core/src/main/java/com/jme3/anim/MorphTrack.java b/jme3-core/src/main/java/com/jme3/anim/MorphTrack.java
index ee767d2885..92ff43514a 100644
--- a/jme3-core/src/main/java/com/jme3/anim/MorphTrack.java
+++ b/jme3-core/src/main/java/com/jme3/anim/MorphTrack.java
@@ -228,6 +228,15 @@ public void getDataAtTime(double t, float[] store) {
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.
*
diff --git a/jme3-core/src/main/java/com/jme3/anim/SingleLayerInfluenceMask.java b/jme3-core/src/main/java/com/jme3/anim/SingleLayerInfluenceMask.java
new file mode 100644
index 0000000000..0050b9ba2c
--- /dev/null
+++ b/jme3-core/src/main/java/com/jme3/anim/SingleLayerInfluenceMask.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2025 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.anim;
+
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import java.io.IOException;
+
+/**
+ * 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 String targetLayer;
+ private AnimComposer animComposer;
+
+ /**
+ * For serialization only. Do not use
+ */
+ protected SingleLayerInfluenceMask() {
+ }
+
+ /**
+ * Instantiate a mask that affects all joints in the specified Armature.
+ *
+ * @param targetLayer The layer this mask is targeted for.
+ * @param animComposer The animation composer associated with this mask.
+ * @param armature The Armature containing the joints.
+ */
+ public SingleLayerInfluenceMask(String targetLayer, AnimComposer animComposer, Armature armature) {
+ super(armature);
+ this.targetLayer = targetLayer;
+ this.animComposer = animComposer;
+ }
+
+ /**
+ * Instantiate a mask that affects no joints.
+ *
+ * @param targetLayer The layer this mask is targeted for.
+ * @param animComposer The animation composer associated with this mask.
+ */
+ public SingleLayerInfluenceMask(String targetLayer, AnimComposer animComposer) {
+ this.targetLayer = targetLayer;
+ this.animComposer = animComposer;
+ }
+
+ /**
+ * Get the layer this mask is targeted for.
+ *
+ * @return The target layer
+ */
+ public String getTargetLayer() {
+ return targetLayer;
+ }
+
+ /**
+ * Sets the animation composer for this mask.
+ *
+ * @param animComposer The new animation composer.
+ */
+ public void setAnimComposer(AnimComposer animComposer) {
+ this.animComposer = animComposer;
+ }
+
+ /**
+ * Checks if the specified target is contained within this mask.
+ *
+ * @param target The target to check.
+ * @return True if the target is contained within this mask, false otherwise.
+ */
+ @Override
+ public boolean contains(Object target) {
+ return simpleContains(target) && (animComposer == null || !isAffectedByUpperLayers(target));
+ }
+
+ private boolean simpleContains(Object target) {
+ return super.contains(target);
+ }
+
+ private boolean isAffectedByUpperLayers(Object target) {
+ boolean higher = false;
+ for (String layerName : animComposer.getLayerNames()) {
+ if (layerName.equals(targetLayer)) {
+ higher = true;
+ continue;
+ }
+ if (!higher) {
+ continue;
+ }
+
+ AnimLayer animLayer = animComposer.getLayer(layerName);
+ if (animLayer.getCurrentAction() != null) {
+ AnimationMask mask = animLayer.getMask();
+
+ if (mask instanceof SingleLayerInfluenceMask) {
+ // dodge some needless recursion by calling a simpler method
+ if (((SingleLayerInfluenceMask) mask).simpleContains(target)) {
+ return true;
+ }
+ } else if (mask != null && mask.contains(target)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void write(JmeExporter ex) throws IOException {
+ super.write(ex);
+ OutputCapsule oc = ex.getCapsule(this);
+ oc.write(targetLayer, "targetLayer", null);
+ }
+
+ @Override
+ public void read(JmeImporter im) throws IOException {
+ super.read(im);
+ InputCapsule ic = im.getCapsule(this);
+ targetLayer = ic.readString("targetLayer", null);
+ }
+
+}
\ No newline at end of file
diff --git a/jme3-core/src/main/java/com/jme3/anim/SkinningControl.java b/jme3-core/src/main/java/com/jme3/anim/SkinningControl.java
index 38505f9317..9ceb4b3638 100644
--- a/jme3-core/src/main/java/com/jme3/anim/SkinningControl.java
+++ b/jme3-core/src/main/java/com/jme3/anim/SkinningControl.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -31,12 +31,21 @@
*/
package com.jme3.anim;
-import com.jme3.export.*;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
import com.jme3.material.MatParamOverride;
import com.jme3.math.FastMath;
import com.jme3.math.Matrix4f;
-import com.jme3.renderer.*;
-import com.jme3.scene.*;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.RendererException;
+import com.jme3.renderer.ViewPort;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.VertexBuffer;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.control.AbstractControl;
import com.jme3.scene.mesh.IndexBuffer;
@@ -53,64 +62,77 @@
import java.util.logging.Logger;
/**
- * The Skinning control deforms a model according to an armature, It handles the
- * computation of the deformation matrices and performs the transformations on
- * the mesh
+ * The `SkinningControl` deforms a 3D model according to an {@link Armature}. It manages the
+ * computation of deformation matrices and applies these transformations to the mesh,
+ * supporting both software and hardware-accelerated skinning.
+ *
+ *
+ * **Software Skinning:** Performed on the CPU, offering broader compatibility but
+ * potentially lower performance for complex models.
*
- * It can perform software skinning or Hardware skinning
+ * **Hardware Skinning:** Utilizes the GPU for deformation, providing significantly
+ * better performance but requiring shader support and having a limit on the number
+ * of bones (typically 255 in common shaders).
*
- * @author Rémy Bouquet Based on SkeletonControl by Kirill Vainer
+ * @author Nehon
*/
-public class SkinningControl extends AbstractControl implements Cloneable, JmeCloneable {
+public class SkinningControl extends AbstractControl implements JmeCloneable {
private static final Logger logger = Logger.getLogger(SkinningControl.class.getName());
+ /**
+ * The maximum number of bones supported for hardware skinning in common shaders.
+ */
+ private static final int MAX_BONES_HW_SKINNING_SUPPORT = 255;
+
/**
* The armature of the model.
*/
private Armature armature;
/**
- * List of geometries affected by this control.
+ * A list of geometries that this control will deform.
*/
private SafeArrayList targets = new SafeArrayList<>(Geometry.class);
/**
- * Used to track when a mesh was updated. Meshes are only updated if they
+ * Used to track when a mesh needs to be updated. Meshes are only updated if they
* are visible in at least one camera.
*/
- private boolean wasMeshUpdated = false;
+ private boolean meshUpdateRequired = true;
/**
- * User wishes to use hardware skinning if available.
+ * Indicates whether hardware skinning is preferred. If `true` and the GPU
+ * supports it, hardware skinning will be enabled.
*/
- private transient boolean hwSkinningDesired = true;
+ private transient boolean hwSkinningPreferred = true;
/**
- * Hardware skinning is currently being used.
+ * Indicates if hardware skinning is currently active and being used.
*/
private transient boolean hwSkinningEnabled = false;
/**
- * Hardware skinning was tested on this GPU, results
- * are stored in {@link #hwSkinningSupported} variable.
+ * Flag indicating whether hardware skinning compatibility has been tested
+ * on the current GPU. Results are stored in {@link #hwSkinningSupported}.
*/
private transient boolean hwSkinningTested = false;
/**
- * If hardware skinning was {@link #hwSkinningTested tested}, then
- * this variable will be set to true if supported, and false if otherwise.
+ * Stores the result of the hardware skinning compatibility test. `true` if
+ * supported, `false` otherwise. This is only valid after
+ * {@link #hwSkinningTested} is `true`.
*/
private transient boolean hwSkinningSupported = false;
/**
- * Bone offset matrices, recreated each frame
+ * Bone offset matrices, computed each frame to deform the mesh based on
+ * the armature's current pose.
*/
- private transient Matrix4f[] offsetMatrices;
-
+ private transient Matrix4f[] boneOffsetMatrices;
- private MatParamOverride numberOfJointsParam;
- private MatParamOverride jointMatricesParam;
+ private MatParamOverride numberOfJointsParam = new MatParamOverride(VarType.Int, "NumberOfBones", null);
+ private MatParamOverride jointMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null);
/**
* Serialization only. Do not use.
@@ -119,26 +141,26 @@ protected SkinningControl() {
}
/**
- * Creates an armature control. The list of targets will be acquired
- * automatically when the control is attached to a node.
+ * Creates a new `SkinningControl` for the given armature.
*
- * @param armature the armature
+ * @param armature The armature that drives the deformation (not null).
*/
public SkinningControl(Armature armature) {
if (armature == null) {
- throw new IllegalArgumentException("armature cannot be null");
+ throw new IllegalArgumentException("armature cannot be null.");
}
this.armature = armature;
- this.numberOfJointsParam = new MatParamOverride(VarType.Int, "NumberOfBones", null);
- this.jointMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null);
}
-
- private void switchToHardware() {
+ /**
+ * Configures the material parameters and meshes for hardware skinning.
+ */
+ private void enableHardwareSkinning() {
numberOfJointsParam.setEnabled(true);
jointMatricesParam.setEnabled(true);
- // Next full 10 bones (e.g. 30 on 24 bones)
+ // Calculate the number of bones rounded up to the nearest multiple of 10.
+ // This is often required by shaders for array uniform declarations.
int numBones = ((armature.getJointCount() / 10) + 1) * 10;
numberOfJointsParam.setValue(numBones);
@@ -150,7 +172,10 @@ private void switchToHardware() {
}
}
- private void switchToSoftware() {
+ /**
+ * Configures the material parameters and meshes for software skinning.
+ */
+ private void enableSoftwareSkinning() {
numberOfJointsParam.setEnabled(false);
jointMatricesParam.setEnabled(false);
@@ -162,22 +187,34 @@ private void switchToSoftware() {
}
}
- private boolean testHardwareSupported(RenderManager rm) {
-
- //Only 255 bones max supported with hardware skinning
- if (armature.getJointCount() > 255) {
+ /**
+ * Tests if hardware skinning is supported by the GPU for the current spatial.
+ *
+ * @param renderManager the RenderManager instance
+ * @return true if hardware skinning is supported, false otherwise
+ */
+ private boolean testHardwareSupported(RenderManager renderManager) {
+ // Only 255 bones max supported with hardware skinning in common shaders.
+ if (armature.getJointCount() > MAX_BONES_HW_SKINNING_SUPPORT) {
+ logger.log(Level.INFO, "Hardware skinning not supported for {0}: Too many bones ({1} > 255).",
+ new Object[]{spatial, armature.getJointCount()});
return false;
}
- switchToHardware();
+ // Temporarily enable hardware skinning to test shader compilation.
+ enableHardwareSkinning();
+ boolean hwSkinningEngaged = false;
try {
- rm.preloadScene(spatial);
- return true;
- } catch (RendererException e) {
- logger.log(Level.WARNING, "Could not enable HW skinning due to shader compile error:", e);
- return false;
+ renderManager.preloadScene(spatial);
+ logger.log(Level.INFO, "Hardware skinning engaged for {0}", spatial);
+ hwSkinningEngaged = true;
+
+ } catch (RendererException ex) {
+ logger.log(Level.WARNING, "Could not enable HW skinning due to shader compile error: ", ex);
}
+
+ return hwSkinningEngaged;
}
/**
@@ -190,7 +227,7 @@ private boolean testHardwareSupported(RenderManager rm) {
* @see #isHardwareSkinningUsed()
*/
public void setHardwareSkinningPreferred(boolean preferred) {
- hwSkinningDesired = preferred;
+ hwSkinningPreferred = preferred;
}
/**
@@ -199,7 +236,7 @@ public void setHardwareSkinningPreferred(boolean preferred) {
* @see #setHardwareSkinningPreferred(boolean)
*/
public boolean isHardwareSkinningPreferred() {
- return hwSkinningDesired;
+ return hwSkinningPreferred;
}
/**
@@ -209,25 +246,21 @@ public boolean isHardwareSkinningUsed() {
return hwSkinningEnabled;
}
-
/**
- * If specified the geometry has an animated mesh, add its mesh and material
- * to the lists of animation targets.
+ * Recursively finds and adds animated geometries to the targets list.
+ *
+ * @param sp The spatial to search within.
*/
- private void findTargets(Geometry geometry) {
- Mesh mesh = geometry.getMesh();
- if (mesh != null && mesh.isAnimated()) {
- targets.add(geometry);
- }
-
- }
-
- private void findTargets(Node node) {
- for (Spatial child : node.getChildren()) {
- if (child instanceof Geometry) {
- findTargets((Geometry) child);
- } else if (child instanceof Node) {
- findTargets((Node) child);
+ private void collectAnimatedGeometries(Spatial sp) {
+ if (sp instanceof Geometry) {
+ Geometry geo = (Geometry) sp;
+ Mesh mesh = geo.getMesh();
+ if (mesh != null && mesh.isAnimated()) {
+ targets.add(geo);
+ }
+ } else if (sp instanceof Node) {
+ for (Spatial child : ((Node) sp).getChildren()) {
+ collectAnimatedGeometries(child);
}
}
}
@@ -236,65 +269,72 @@ private void findTargets(Node node) {
public void setSpatial(Spatial spatial) {
Spatial oldSpatial = this.spatial;
super.setSpatial(spatial);
- updateTargetsAndMaterials(spatial);
+ updateAnimationTargets(spatial);
if (oldSpatial != null) {
+ // Ensure parameters are removed from the old spatial to prevent memory leaks
oldSpatial.removeMatParamOverride(numberOfJointsParam);
oldSpatial.removeMatParamOverride(jointMatricesParam);
}
if (spatial != null) {
- spatial.removeMatParamOverride(numberOfJointsParam);
- spatial.removeMatParamOverride(jointMatricesParam);
+ // Add parameters to the new spatial. No need to remove first if they are not already present.
spatial.addMatParamOverride(numberOfJointsParam);
spatial.addMatParamOverride(jointMatricesParam);
}
}
+ /**
+ * Performs software skinning updates.
+ */
private void controlRenderSoftware() {
resetToBind(); // reset morph meshes to bind pose
- offsetMatrices = armature.computeSkinningMatrices();
+ boneOffsetMatrices = armature.computeSkinningMatrices();
for (Geometry geometry : targets) {
Mesh mesh = geometry.getMesh();
- // NOTE: This assumes code higher up has
- // already ensured this mesh is animated.
- // Otherwise a crash will happen in skin update.
- softwareSkinUpdate(mesh, offsetMatrices);
+ // NOTE: This assumes code higher up has already ensured this mesh is animated.
+ // Otherwise, a crash will happen in skin update.
+ applySoftwareSkinning(mesh, boneOffsetMatrices);
}
}
+ /**
+ * Prepares parameters for hardware skinning.
+ */
private void controlRenderHardware() {
- offsetMatrices = armature.computeSkinningMatrices();
- jointMatricesParam.setValue(offsetMatrices);
+ boneOffsetMatrices = armature.computeSkinningMatrices();
+ jointMatricesParam.setValue(boneOffsetMatrices);
}
@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
- if (!wasMeshUpdated) {
- updateTargetsAndMaterials(spatial);
+ if (meshUpdateRequired) {
+ updateAnimationTargets(spatial);
// Prevent illegal cases. These should never happen.
- assert hwSkinningTested || (!hwSkinningTested && !hwSkinningSupported && !hwSkinningEnabled);
- assert !hwSkinningEnabled || (hwSkinningEnabled && hwSkinningTested && hwSkinningSupported);
+ assert hwSkinningTested || (!hwSkinningSupported && !hwSkinningEnabled);
+ assert !hwSkinningEnabled || (hwSkinningTested && hwSkinningSupported);
- if (hwSkinningDesired && !hwSkinningTested) {
+ if (hwSkinningPreferred && !hwSkinningTested) {
+ // If hardware skinning is preferred and hasn't been tested yet, test it.
hwSkinningTested = true;
hwSkinningSupported = testHardwareSupported(rm);
if (hwSkinningSupported) {
hwSkinningEnabled = true;
-
- Logger.getLogger(SkinningControl.class.getName()).log(Level.INFO, "Hardware skinning engaged for {0}", spatial);
} else {
- switchToSoftware();
+ enableSoftwareSkinning();
}
- } else if (hwSkinningDesired && hwSkinningSupported && !hwSkinningEnabled) {
- switchToHardware();
+ } else if (hwSkinningPreferred && hwSkinningSupported && !hwSkinningEnabled) {
+ // If hardware skinning is preferred, supported, but not yet enabled, enable it.
+ enableHardwareSkinning();
hwSkinningEnabled = true;
- } else if (!hwSkinningDesired && hwSkinningEnabled) {
- switchToSoftware();
+
+ } else if (!hwSkinningPreferred && hwSkinningEnabled) {
+ // If hardware skinning is no longer preferred but is enabled, switch to software.
+ enableSoftwareSkinning();
hwSkinningEnabled = false;
}
@@ -304,17 +344,22 @@ protected void controlRender(RenderManager rm, ViewPort vp) {
controlRenderSoftware();
}
- wasMeshUpdated = true;
+ meshUpdateRequired = false; // Reset flag after update
}
}
@Override
protected void controlUpdate(float tpf) {
- wasMeshUpdated = false;
+ meshUpdateRequired = true; // Mark for mesh update on next render pass
armature.update();
}
- //only do this for software updates
+ /**
+ * Resets the vertex, normal, and tangent buffers of animated meshes to their
+ * original bind pose. This is crucial for software skinning to ensure
+ * transformations are applied from a consistent base.
+ * This method is only applied when performing software updates.
+ */
void resetToBind() {
for (Geometry geometry : targets) {
Mesh mesh = geometry.getMesh();
@@ -327,15 +372,20 @@ void resetToBind() {
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 +398,7 @@ void resetToBind() {
tb.put(btb).clear();
}
-
pb.put(bpb).clear();
- nb.put(bnb).clear();
}
}
}
@@ -375,51 +423,51 @@ public void cloneFields(Cloner cloner, Object original) {
}
/**
- * Access the attachments node of the named bone. If the bone doesn't
- * already have an attachments node, create one and attach it to the scene
- * graph. Models and effects attached to the attachments node will follow
- * the bone's motions.
+ * Provides access to the attachment node for a specific joint in the armature.
+ * If an attachment node does not already exist for the named joint, one will be
+ * created and attached to the scene graph. Models or effects attached to this
+ * node will follow the motion of the corresponding bone.
*
* @param jointName the name of the joint
* @return the attachments node of the joint
*/
public Node getAttachmentsNode(String jointName) {
- Joint b = armature.getJoint(jointName);
- if (b == null) {
- throw new IllegalArgumentException("Given bone name does not exist "
- + "in the armature.");
+ Joint joint = armature.getJoint(jointName);
+ if (joint == null) {
+ throw new IllegalArgumentException(
+ "Given joint name '" + jointName + "' does not exist in the armature.");
}
- updateTargetsAndMaterials(spatial);
- int boneIndex = armature.getJointIndex(b);
- Node n = b.getAttachmentsNode(boneIndex, targets);
- /*
- * Select a node to parent the attachments node.
- */
+ updateAnimationTargets(spatial);
+ int jointIndex = armature.getJointIndex(joint);
+ Node attachNode = joint.getAttachmentsNode(jointIndex, targets);
+
+ // Determine the appropriate parent for the attachment node.
Node parent;
if (spatial instanceof Node) {
parent = (Node) spatial; // the usual case
} else {
parent = spatial.getParent();
}
- parent.attachChild(n);
+ parent.attachChild(attachNode);
- return n;
+ return attachNode;
}
/**
- * returns the armature of this control
+ * Returns the armature associated with this skinning control.
*
- * @return the pre-existing instance
+ * @return The pre-existing `Armature` instance.
*/
public Armature getArmature() {
return armature;
}
/**
- * Enumerate the target meshes of this control.
+ * Returns an array containing all the target meshes that this control
+ * is currently affecting.
*
- * @return a new array
+ * @return A new array of `Mesh` objects.
*/
public Mesh[] getTargets() {
Mesh[] result = new Mesh[targets.size()];
@@ -434,30 +482,31 @@ public Mesh[] getTargets() {
}
/**
- * Update the mesh according to the given transformation matrices
+ * Applies software skinning transformations to the given mesh using the
+ * provided bone offset matrices.
*
- * @param mesh then mesh
- * @param offsetMatrices the transformation matrices to apply
+ * @param mesh The mesh to deform.
+ * @param offsetMatrices The array of transformation matrices for each bone.
*/
- private void softwareSkinUpdate(Mesh mesh, Matrix4f[] offsetMatrices) {
+ private void applySoftwareSkinning(Mesh mesh, Matrix4f[] offsetMatrices) {
VertexBuffer tb = mesh.getBuffer(Type.Tangent);
if (tb == null) {
- //if there are no tangents use the classic skinning
+ // if there are no tangents use the classic skinning
applySkinning(mesh, offsetMatrices);
} else {
- //if there are tangents use the skinning with tangents
+ // if there are tangents use the skinning with tangents
applySkinningTangents(mesh, offsetMatrices, tb);
}
-
-
}
/**
- * Method to apply skinning transforms to a mesh's buffers
+ * Applies skinning transformations to a mesh's position and normal buffers.
+ * This method iterates through each vertex, applies the weighted sum of
+ * bone transformations, and updates the vertex buffers.
*
- * @param mesh the mesh
- * @param offsetMatrices the offset matrices to apply
+ * @param mesh The mesh to apply skinning to.
+ * @param offsetMatrices The bone offset matrices to use for transformation.
*/
private void applySkinning(Mesh mesh, Matrix4f[] offsetMatrices) {
int maxWeightsPerVert = mesh.getMaxNumWeights();
@@ -552,19 +601,16 @@ private void applySkinning(Mesh mesh, Matrix4f[] offsetMatrices) {
vb.updateData(fvb);
nb.updateData(fnb);
-
}
/**
- * Specific method for skinning with tangents to avoid cluttering the
- * classic skinning calculation with null checks that would slow down the
- * process even if tangents don't have to be computed. Also the iteration
- * has additional indexes since tangent has 4 components instead of 3 for
- * pos and norm
+ * Applies skinning transformations to a mesh's position, normal, and tangent buffers.
+ * This method is specifically designed for meshes that include tangent data,
+ * ensuring proper deformation of tangents alongside positions and normals.
*
- * @param mesh the mesh
- * @param offsetMatrices the offsetMatrices to apply
- * @param tb the tangent vertexBuffer
+ * @param mesh The mesh to apply skinning to.
+ * @param offsetMatrices The bone offset matrices to use for transformation.
+ * @param tb The tangent `VertexBuffer`.
*/
private void applySkinningTangents(Mesh mesh, Matrix4f[] offsetMatrices, VertexBuffer tb) {
int maxWeightsPerVert = mesh.getMaxNumWeights();
@@ -583,14 +629,14 @@ private void applySkinningTangents(Mesh mesh, Matrix4f[] offsetMatrices, VertexB
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();
-
// get boneIndexes and weights for mesh
IndexBuffer ib = IndexBuffer.wrapIndexBuffer(mesh.getBuffer(Type.BoneIndex).getData());
FloatBuffer wb = (FloatBuffer) mesh.getBuffer(Type.BoneWeight).getData();
@@ -601,8 +647,6 @@ private void applySkinningTangents(Mesh mesh, Matrix4f[] offsetMatrices, VertexB
int idxWeights = 0;
TempVars vars = TempVars.get();
-
-
float[] posBuf = vars.skinPositions;
float[] normBuf = vars.skinNormals;
float[] tanBuf = vars.skinTangents;
@@ -615,7 +659,9 @@ private void applySkinningTangents(Mesh mesh, Matrix4f[] offsetMatrices, VertexB
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 +734,10 @@ private void applySkinningTangents(Mesh mesh, Matrix4f[] offsetMatrices, VertexB
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 +745,9 @@ private void applySkinningTangents(Mesh mesh, Matrix4f[] offsetMatrices, VertexB
vars.release();
vb.updateData(fvb);
- nb.updateData(fnb);
+ if (nb != null) {
+ nb.updateData(fnb);
+ }
tb.updateData(ftb);
}
@@ -713,9 +763,6 @@ public void write(JmeExporter ex) throws IOException {
super.write(ex);
OutputCapsule oc = ex.getCapsule(this);
oc.write(armature, "armature", null);
-
- oc.write(numberOfJointsParam, "numberOfBonesParam", null);
- oc.write(jointMatricesParam, "boneMatricesParam", null);
}
/**
@@ -731,15 +778,13 @@ public void read(JmeImporter im) throws IOException {
InputCapsule in = im.getCapsule(this);
armature = (Armature) in.readSavable("armature", null);
- numberOfJointsParam = (MatParamOverride) in.readSavable("numberOfBonesParam", null);
- jointMatricesParam = (MatParamOverride) in.readSavable("boneMatricesParam", null);
-
- if (numberOfJointsParam == null) {
- numberOfJointsParam = new MatParamOverride(VarType.Int, "NumberOfBones", null);
- jointMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null);
- getSpatial().addMatParamOverride(numberOfJointsParam);
- getSpatial().addMatParamOverride(jointMatricesParam);
+ for (MatParamOverride mpo : spatial.getLocalMatParamOverrides().getArray()) {
+ if (mpo.getName().equals("NumberOfBones") || mpo.getName().equals("BoneMatrices")) {
+ spatial.removeMatParamOverride(mpo);
+ }
}
+ spatial.addMatParamOverride(numberOfJointsParam);
+ spatial.addMatParamOverride(jointMatricesParam);
}
/**
@@ -747,13 +792,9 @@ public void read(JmeImporter im) throws IOException {
*
* @param spatial the controlled spatial
*/
- private void updateTargetsAndMaterials(Spatial spatial) {
+ private void updateAnimationTargets(Spatial spatial) {
targets.clear();
-
- if (spatial instanceof Node) {
- findTargets((Node) spatial);
- } else if (spatial instanceof Geometry) {
- findTargets((Geometry) spatial);
- }
+ collectAnimatedGeometries(spatial);
}
+
}
diff --git a/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java b/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java
index 3492d17eec..39b66d5d36 100644
--- a/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java
+++ b/jme3-core/src/main/java/com/jme3/anim/TransformTrack.java
@@ -301,6 +301,15 @@ public void getDataAtTime(double t, Transform transform) {
}
}
+ /**
+ * Access the FrameInterpolator.
+ *
+ * @return the pre-existing instance or null
+ */
+ public FrameInterpolator getFrameInterpolator() {
+ return interpolator;
+ }
+
/**
* Replaces the frame interpolator.
*
diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/AbstractTween.java b/jme3-core/src/main/java/com/jme3/anim/tween/AbstractTween.java
index 9dd961b530..5216051f64 100644
--- a/jme3-core/src/main/java/com/jme3/anim/tween/AbstractTween.java
+++ b/jme3-core/src/main/java/com/jme3/anim/tween/AbstractTween.java
@@ -57,6 +57,9 @@ public double getLength() {
}
public void setLength(double length) {
+ if (length < 0.0) {
+ throw new IllegalArgumentException("length must be greater than or equal to 0");
+ }
this.length = length;
}
diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java
index 0fb423f336..344a69b216 100644
--- a/jme3-core/src/main/java/com/jme3/anim/tween/action/Action.java
+++ b/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.util.clone.Cloner;
import com.jme3.util.clone.JmeCloneable;
+/**
+ * Wraps an array of Tween actions into an action object.
+ *
+ *
+ * Notes :
+ *
The sequence of tweens is determined by {@link com.jme3.anim.tween.Tweens} utility class and the {@link BaseAction} interpolates that sequence.
+ *
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.
+ *
+ *
+ * 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}.
+ *
+ * Notes :
+ *
If intentions are to wrap some tween actions, then subclasses have to call this constructor, examples : {@link BlendableAction} and {@link BlendAction}.
+ *
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}.
+ *
+ *
+ * @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 @@ protected Action(Tween... tweens) {
}
}
}
-
+
+ /**
+ * 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 void setLength(double length) {
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.
+ *
+ * Notes:
+ *
This factor controls the animation direction, if the speed is a positive value then the animation will run forward and vice versa.
+ *
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.
+ *
Default speed is 1.0, it plays the animation clips at their normal speed.
+ *
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.
+ *
+ *
+ * @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.
+ *
+ *
+ * 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.
+ *
+ *
+ * @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 void cloneFields(Cloner cloner, Object original) {
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);
+ }
+ }
}
diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/BaseAction.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/BaseAction.java
index 99c559fca6..edc819f7ff 100644
--- a/jme3-core/src/main/java/com/jme3/anim/tween/action/BaseAction.java
+++ b/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.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.
+ *
+ * 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 :
+ *
+ * //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());
+ *
+ *
+ * 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 BaseAction(Tween tween) {
subActions.toArray(actions);
}
- private void gatherActions(Tween tween, List 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 void setMask(AnimationMask mask) {
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 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);
+ }
+ }
+ }
}
diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendSpace.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendSpace.java
index a88be7529c..17b66f8b07 100644
--- a/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendSpace.java
+++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendSpace.java
@@ -1,10 +1,95 @@
+/*
+ * Copyright (c) 2009-2025 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
package com.jme3.anim.tween.action;
+/**
+ * A provider interface which provides a value {@link BlendSpace#getWeight()} to control the blending between 2 successive actions in a {@link BlendAction}.
+ * The blending weight is a read-only value, and it can be manipulated using the arbitrary value {@link BlendSpace#setValue(float)} during the application runtime.
+ *
+ *
+ * Notes about the blending action and its relations with the blending weight:
+ *
+ *
Blending is the action of mixing between 2 successive animation {@link BlendableAction}s by interpolating their transforms and
+ * then applying the result on the assigned {@link HasLocalTransform} object, the {@link BlendSpace} provides this blending action with a blend weight value.
+ *
The blend weight is the value for the interpolation for the target transforms.
+ *
The blend weight value must be in this interval [0, 1].
+ *
+ *
+ *
+ *
+ * Different blending weight case scenarios managed by {@link BlendAction} internally:
+ *
+ *
In case of (0 < Blending weight < 1), the blending is executed each update among 2 actions, the first action will use
+ * a blend value of 1 and the second action will use the blend space weight as a value for the interpolation.
+ *
In case of (Blending weight = 0), the blending hasn't started yet, only the first action will be interpolated at (weight = 1).
+ *
In case of (Blending weight = 1), the blending is finished and only the second action will continue to run at (weight = 1).
+ *
+ *
+ *
+ *
+ * Notes about the blending weight value:
+ *
+ *
Negative values and values greater than 1 aren't allowed (i.e., extrapolations aren't allowed).
+ *
For more details, see {@link BlendAction#doInterpolate(double)} and {@link BlendAction#collectTransform(HasLocalTransform, Transform, float, BlendableAction)}.
+ *
+ *
+ *
+ * Created by Nehon.
+ * @see LinearBlendSpace an example of blendspace implementation
+ */
public interface BlendSpace {
+ /**
+ * Adjusts the target blend action instance that will utilize the blend weight value provided by this blend-space implementation.
+ *
+ * @param action the blend action instance that will utilize this blend-space (not null).
+ */
public void setBlendAction(BlendAction action);
+ /**
+ * Provides the blend weight value to the assigned {@link BlendAction} instance,
+ * this value will be used for interpolating a collection of actions' transformations (i.e., keyframes).
+ *
+ * @return the blending weight value in the range from 0 to 1,
+ * negative values and values above 1 aren't allowed.
+ * @see LinearBlendSpace#getWeight()
+ */
public float getWeight();
+ /**
+ * An arbitrary value used for adjusting the blending weight value.
+ *
+ * @param value the value in floats.
+ * @see LinearBlendSpace#setValue(float)
+ */
public void setValue(float value);
}
diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendableAction.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendableAction.java
index c9882529f9..9064fb042e 100644
--- a/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendableAction.java
+++ b/jme3-core/src/main/java/com/jme3/anim/tween/action/BlendableAction.java
@@ -105,6 +105,9 @@ public double getTransitionLength() {
}
public void setTransitionLength(double transitionLength) {
+ if (transitionLength < 0.0) {
+ throw new IllegalArgumentException("transitionLength must be greater than or equal to 0");
+ }
this.transitionLength = transitionLength;
this.transition.setLength(transitionLength);
}
diff --git a/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java b/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java
index 9f5c855521..7c442bd122 100644
--- a/jme3-core/src/main/java/com/jme3/anim/tween/action/ClipAction.java
+++ b/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 @@ private void interpolateMorphTrack(double t, MorphTrack track) {
// }
}
- 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 ClipAction jmeClone() {
try {
ClipAction clone = (ClipAction) super.clone();
return clone;
- } catch (CloneNotSupportedException exception) {
- throw new RuntimeException(exception);
+ } catch (CloneNotSupportedException ex) {
+ throw new RuntimeException(ex);
}
}
diff --git a/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java b/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java
index 3fc65f63a5..6c463f329d 100644
--- a/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java
+++ b/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 @@
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 void visit(Spatial spatial) {
Armature armature = new Armature(joints);
armature.saveBindPose();
+ armature.saveInitialPose();
skeletonArmatureMap.put(skeleton, armature);
List tracks = new ArrayList<>();
diff --git a/jme3-core/src/main/java/com/jme3/anim/util/HasLocalTransform.java b/jme3-core/src/main/java/com/jme3/anim/util/HasLocalTransform.java
index 28f560dfce..f483cce7f0 100644
--- a/jme3-core/src/main/java/com/jme3/anim/util/HasLocalTransform.java
+++ b/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;
diff --git a/jme3-core/src/main/java/com/jme3/anim/util/JointModelTransform.java b/jme3-core/src/main/java/com/jme3/anim/util/JointModelTransform.java
index 837c50ca2e..708d1410dd 100644
--- a/jme3-core/src/main/java/com/jme3/anim/util/JointModelTransform.java
+++ b/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;
diff --git a/jme3-core/src/main/java/com/jme3/anim/util/Primitives.java b/jme3-core/src/main/java/com/jme3/anim/util/Primitives.java
index 56fbbf4788..4e04c49cbb 100644
--- a/jme3-core/src/main/java/com/jme3/anim/util/Primitives.java
+++ b/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;
diff --git a/jme3-core/src/main/java/com/jme3/anim/util/Weighted.java b/jme3-core/src/main/java/com/jme3/anim/util/Weighted.java
index f771d44edd..1a2a690d1b 100644
--- a/jme3-core/src/main/java/com/jme3/anim/util/Weighted.java
+++ b/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;
diff --git a/jme3-core/src/main/java/com/jme3/animation/LoopMode.java b/jme3-core/src/main/java/com/jme3/animation/LoopMode.java
index 9572b87bc5..33a50b96bc 100644
--- a/jme3-core/src/main/java/com/jme3/animation/LoopMode.java
+++ b/jme3-core/src/main/java/com/jme3/animation/LoopMode.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -35,7 +35,6 @@
* LoopMode determines how animations repeat, or if they
* do not repeat.
*/
-@Deprecated
public enum LoopMode {
/**
* The animation will play repeatedly, when it reaches the end
@@ -55,6 +54,6 @@ public enum LoopMode {
* animation will play backwards from the last frame until it reaches
* the first frame.
*/
- Cycle,
+ Cycle
}
diff --git a/jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java b/jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java
index d6e7392cae..34d712f707 100644
--- a/jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java
+++ b/jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -250,6 +250,8 @@ private void controlRenderSoftware() {
resetToBind(); // reset morph meshes to bind pose
offsetMatrices = skeleton.computeSkinningMatrices();
+ numberOfBonesParam.setEnabled(false);
+ boneMatricesParam.setEnabled(false);
for (Geometry geometry : targets) {
Mesh mesh = geometry.getMesh();
@@ -321,15 +323,20 @@ void resetToBind() {
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 +350,6 @@ void resetToBind() {
}
pb.put(bpb).clear();
- nb.put(bnb).clear();
}
}
}
@@ -574,8 +580,10 @@ private void applySkinningTangents(Mesh mesh, Matrix4f[] offsetMatrices, VertexB
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 +611,9 @@ private void applySkinningTangents(Mesh mesh, Matrix4f[] offsetMatrices, VertexB
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 +686,10 @@ private void applySkinningTangents(Mesh mesh, Matrix4f[] offsetMatrices, VertexB
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 +697,9 @@ private void applySkinningTangents(Mesh mesh, Matrix4f[] offsetMatrices, VertexB
vars.release();
vb.updateData(fvb);
- nb.updateData(fnb);
+ if (nb != null) {
+ nb.updateData(fnb);
+ }
tb.updateData(ftb);
}
diff --git a/jme3-core/src/main/java/com/jme3/app/AppTask.java b/jme3-core/src/main/java/com/jme3/app/AppTask.java
index 59e96c4046..a7122765a5 100644
--- a/jme3-core/src/main/java/com/jme3/app/AppTask.java
+++ b/jme3-core/src/main/java/com/jme3/app/AppTask.java
@@ -31,7 +31,12 @@
*/
package com.jme3.app;
-import java.util.concurrent.*;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
@@ -52,7 +57,8 @@ public class AppTask implements Future {
private V result;
private ExecutionException exception;
- private boolean cancelled, finished;
+ private boolean cancelled;
+ private boolean finished;
private final ReentrantLock stateLock = new ReentrantLock();
private final Condition finishedCondition = stateLock.newCondition();
@@ -100,7 +106,8 @@ public V get() throws InterruptedException, ExecutionException {
}
@Override
- public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+ public V get(long timeout, TimeUnit unit)
+ throws InterruptedException, ExecutionException, TimeoutException {
stateLock.lock();
try {
if (!isDone()) {
diff --git a/jme3-core/src/main/java/com/jme3/app/Application.java b/jme3-core/src/main/java/com/jme3/app/Application.java
index 198d730efa..dba60e140c 100644
--- a/jme3-core/src/main/java/com/jme3/app/Application.java
+++ b/jme3-core/src/main/java/com/jme3/app/Application.java
@@ -41,7 +41,9 @@
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.Renderer;
import com.jme3.renderer.ViewPort;
-import com.jme3.system.*;
+import com.jme3.system.AppSettings;
+import com.jme3.system.JmeContext;
+import com.jme3.system.Timer;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
@@ -226,7 +228,7 @@ public interface Application {
* After the application has stopped, it cannot be used anymore.
*
@param waitFor true→wait for the context to be fully destroyed,
- * true→don't wait
+ * false→don't wait
*/
public void stop(boolean waitFor);
diff --git a/jme3-core/src/main/java/com/jme3/app/BasicProfiler.java b/jme3-core/src/main/java/com/jme3/app/BasicProfiler.java
index e7ae0a7884..1c95730bbc 100644
--- a/jme3-core/src/main/java/com/jme3/app/BasicProfiler.java
+++ b/jme3-core/src/main/java/com/jme3/app/BasicProfiler.java
@@ -32,7 +32,11 @@
package com.jme3.app;
-import com.jme3.profile.*;
+
+import com.jme3.profile.AppProfiler;
+import com.jme3.profile.AppStep;
+import com.jme3.profile.SpStep;
+import com.jme3.profile.VpStep;
import com.jme3.renderer.ViewPort;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.scene.Mesh;
diff --git a/jme3-core/src/main/java/com/jme3/app/BasicProfilerState.java b/jme3-core/src/main/java/com/jme3/app/BasicProfilerState.java
index d7b56511a6..68ee1483fb 100644
--- a/jme3-core/src/main/java/com/jme3/app/BasicProfilerState.java
+++ b/jme3-core/src/main/java/com/jme3/app/BasicProfilerState.java
@@ -122,53 +122,52 @@ protected void refreshBackground() {
float frameTime = 1000f / 60;
mesh.setBuffer(Type.Position, 3, new float[] {
- // first quad
- 0, 0, 0,
- size, 0, 0,
- size, frameTime, 0,
- 0, frameTime, 0,
-
- // second quad
- 0, frameTime, 0,
- size, frameTime, 0,
- size, frameTime * 2, 0,
- 0, frameTime * 2, 0,
-
- // A lower dark border just to frame the
- // 'update' stats against bright backgrounds
- 0, -2, 0,
- size, -2, 0,
- size, 0, 0,
- 0, 0, 0
- });
+ // first quad
+ 0, 0, 0,
+ size, 0, 0,
+ size, frameTime, 0,
+ 0, frameTime, 0,
+ // second quad
+ 0, frameTime, 0,
+ size, frameTime, 0,
+ size, frameTime * 2, 0,
+ 0, frameTime * 2, 0,
+
+ // A lower dark border just to frame the
+ // 'update' stats against bright backgrounds
+ 0, -2, 0,
+ size, -2, 0,
+ size, 0, 0,
+ 0, 0, 0
+ });
mesh.setBuffer(Type.Color, 4, new float[] {
// first quad, within normal frame limits
- 0, 1, 0, 0.25f,
- 0, 1, 0, 0.25f,
- 0, 0.25f, 0, 0.25f,
- 0, 0.25f, 0, 0.25f,
-
- // Second quad, dropped frames
- 0.25f, 0, 0, 0.25f,
- 0.25f, 0, 0, 0.25f,
- 1, 0, 0, 0.25f,
- 1, 0, 0, 0.25f,
-
- 0, 0, 0, 0.5f,
- 0, 0, 0, 0.5f,
- 0, 0, 0, 0.5f,
- 0, 0, 0, 0.5f
- });
+ 0, 1, 0, 0.25f,
+ 0, 1, 0, 0.25f,
+ 0, 0.25f, 0, 0.25f,
+ 0, 0.25f, 0, 0.25f,
+
+ // Second quad, dropped frames
+ 0.25f, 0, 0, 0.25f,
+ 0.25f, 0, 0, 0.25f,
+ 1, 0, 0, 0.25f,
+ 1, 0, 0, 0.25f,
+
+ 0, 0, 0, 0.5f,
+ 0, 0, 0, 0.5f,
+ 0, 0, 0, 0.5f,
+ 0, 0, 0, 0.5f
+ });
mesh.setBuffer(Type.Index, 3, new short[] {
- 0, 1, 2,
- 0, 2, 3,
- 4, 5, 6,
- 4, 6, 7,
- 8, 9, 10,
- 8, 10, 11
- });
+ 0, 1, 2,
+ 0, 2, 3,
+ 4, 5, 6,
+ 4, 6, 7,
+ 8, 9, 10,
+ 8, 10, 11
+ });
}
@Override
diff --git a/jme3-core/src/main/java/com/jme3/app/ChaseCameraAppState.java b/jme3-core/src/main/java/com/jme3/app/ChaseCameraAppState.java
index f4beacdf85..9e3740bec9 100644
--- a/jme3-core/src/main/java/com/jme3/app/ChaseCameraAppState.java
+++ b/jme3-core/src/main/java/com/jme3/app/ChaseCameraAppState.java
@@ -82,7 +82,8 @@ public class ChaseCameraAppState extends AbstractAppState implements ActionListe
protected Vector3f leftVector = new Vector3f();
protected Trigger[] zoomOutTrigger = {new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true)};
protected Trigger[] zoomInTrigger = {new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false)};
- protected Trigger[] toggleRotateTrigger = {new MouseButtonTrigger(MouseInput.BUTTON_LEFT), new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)};
+ protected Trigger[] toggleRotateTrigger = {new MouseButtonTrigger(MouseInput.BUTTON_LEFT),
+ new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)};
//
// protected boolean rotating = false;
@@ -209,7 +210,8 @@ public void setTarget(Spatial targetSpatial) {
@Override
public void update(float tpf) {
if (spatial == null) {
- throw new IllegalArgumentException("The spatial to follow is null, please use the setTarget method");
+ throw new IllegalArgumentException(
+ "The spatial to follow is null, please use the setTarget method");
}
target.setLocalTranslation(spatial.getWorldTranslation());
camNode.lookAt(target.getWorldTranslation(), upVector);
diff --git a/jme3-core/src/main/java/com/jme3/app/DebugKeysAppState.java b/jme3-core/src/main/java/com/jme3/app/DebugKeysAppState.java
index 61919d4f86..143e13210a 100644
--- a/jme3-core/src/main/java/com/jme3/app/DebugKeysAppState.java
+++ b/jme3-core/src/main/java/com/jme3/app/DebugKeysAppState.java
@@ -55,7 +55,7 @@ public class DebugKeysAppState extends AbstractAppState {
public static final String INPUT_MAPPING_MEMORY = "SIMPLEAPP_Memory";
private Application app;
- final private DebugKeyListener keyListener = new DebugKeyListener();
+ private final DebugKeyListener keyListener = new DebugKeyListener();
private InputManager inputManager;
public DebugKeysAppState() {
@@ -83,10 +83,12 @@ public void initialize(AppStateManager stateManager, Application app) {
public void cleanup() {
super.cleanup();
- if (inputManager.hasMapping(INPUT_MAPPING_CAMERA_POS))
+ if (inputManager.hasMapping(INPUT_MAPPING_CAMERA_POS)) {
inputManager.deleteMapping(INPUT_MAPPING_CAMERA_POS);
- if (inputManager.hasMapping(INPUT_MAPPING_MEMORY))
+ }
+ if (inputManager.hasMapping(INPUT_MAPPING_MEMORY)) {
inputManager.deleteMapping(INPUT_MAPPING_MEMORY);
+ }
inputManager.removeListener(keyListener);
}
@@ -111,7 +113,8 @@ public void onAction(String name, boolean value, float tpf) {
System.out.println("Camera Direction: " + cam.getDirection());
System.out.println("cam.setLocation(new Vector3f("
+ loc.x + "f, " + loc.y + "f, " + loc.z + "f));");
- System.out.println("cam.setRotation(new Quaternion(" + rot.getX() + "f, " +rot.getY()+ "f, " + rot.getZ() + "f, " + rot.getW() + "f));");
+ System.out.println("cam.setRotation(new Quaternion(" + rot.getX() + "f, " + rot.getY()
+ + "f, " + rot.getZ() + "f, " + rot.getW() + "f));");
}
} else if (name.equals(INPUT_MAPPING_MEMORY)) {
diff --git a/jme3-core/src/main/java/com/jme3/app/DetailedProfiler.java b/jme3-core/src/main/java/com/jme3/app/DetailedProfiler.java
index 13700a870a..2936a30072 100644
--- a/jme3-core/src/main/java/com/jme3/app/DetailedProfiler.java
+++ b/jme3-core/src/main/java/com/jme3/app/DetailedProfiler.java
@@ -43,7 +43,7 @@
*/
public class DetailedProfiler implements AppProfiler {
- private final static int MAX_FRAMES = 100;
+ private static final int MAX_FRAMES = 100;
private Map data;
private Map pool;
private long startFrame;
@@ -59,10 +59,10 @@ public class DetailedProfiler implements AppProfiler {
private String curSpPath = null;
private VpStep lastVpStep = null;
- final private StringBuilder path = new StringBuilder(256);
- final private StringBuilder vpPath = new StringBuilder(256);
+ private final StringBuilder path = new StringBuilder(256);
+ private final StringBuilder vpPath = new StringBuilder(256);
- final private Deque idsPool = new ArrayDeque<>(100);
+ private final Deque idsPool = new ArrayDeque<>(100);
StatLine frameTime;
@@ -152,14 +152,17 @@ public void vpStep(VpStep step, ViewPort vp, RenderQueue.Bucket bucket) {
if (data != null) {
vpPath.setLength(0);
- vpPath.append(vp.getName()).append("/").append((bucket == null ? step.name() : bucket.name() + " Bucket"));
+ vpPath.append(vp.getName()).append("/")
+ .append((bucket == null ? step.name() : bucket.name() + " Bucket"));
path.setLength(0);
if ((lastVpStep == VpStep.PostQueue || lastVpStep == VpStep.PostFrame) && bucket != null) {
- path.append(curAppPath).append("/").append(curVpPath).append(curSpPath).append("/").append(vpPath);
+ path.append(curAppPath).append("/").append(curVpPath).append(curSpPath).append("/")
+ .append(vpPath);
curVpPath = vpPath.toString();
} else {
if (bucket != null) {
- path.append(curAppPath).append("/").append(curVpPath).append("/").append(bucket.name() + " Bucket");
+ path.append(curAppPath).append("/").append(curVpPath).append("/")
+ .append(bucket.name() + " Bucket");
} else {
path.append(curAppPath).append("/").append(vpPath);
curVpPath = vpPath.toString();
@@ -185,7 +188,7 @@ public void spStep(SpStep step, String... additionalInfo) {
public Map getStats() {
if (data != null) {
- return data;//new LinkedHashMap<>(data);
+ return data; //new LinkedHashMap<>(data);
}
return null;
}
@@ -256,8 +259,8 @@ private int getUnusedTaskId() {
}
public static class StatLine {
- final private long[] cpuTimes = new long[MAX_FRAMES];
- final private long[] gpuTimes = new long[MAX_FRAMES];
+ private final long[] cpuTimes = new long[MAX_FRAMES];
+ private final long[] gpuTimes = new long[MAX_FRAMES];
private int startCursor = 0;
private int cpuCursor = 0;
private int gpuCursor = 0;
diff --git a/jme3-core/src/main/java/com/jme3/app/DetailedProfilerState.java b/jme3-core/src/main/java/com/jme3/app/DetailedProfilerState.java
index 1aa244a797..7a3e0393cc 100644
--- a/jme3-core/src/main/java/com/jme3/app/DetailedProfilerState.java
+++ b/jme3-core/src/main/java/com/jme3/app/DetailedProfilerState.java
@@ -60,13 +60,13 @@ public class DetailedProfilerState extends BaseAppState {
private static final String TOGGLE_KEY = "Toggle_Detailed_Profiler";
private static final String CLICK_KEY = "Click_Detailed_Profiler";
private static final String INSIGNIFICANT = "Hide insignificant stat";
- final private DetailedProfiler prof = new DetailedProfiler();
+ private final DetailedProfiler prof = new DetailedProfiler();
private float time = 0;
private BitmapFont font;
private BitmapFont bigFont;
- final private Node ui = new Node("Stats ui");
- final private Map lines = new HashMap<>();
+ private final Node ui = new Node("Stats ui");
+ private final Map lines = new HashMap<>();
private double totalTimeCpu;
private double totalTimeGpu;
private int maxLevel = 0;
@@ -83,14 +83,14 @@ public class DetailedProfilerState extends BaseAppState {
private StatLineView rootLine;
private int height = 0;
- final private DecimalFormat df = new DecimalFormat("##0.00", new DecimalFormatSymbols(Locale.US));
+ private final DecimalFormat df = new DecimalFormat("##0.00", new DecimalFormatSymbols(Locale.US));
- final private ColorRGBA dimmedWhite = ColorRGBA.White.mult(0.7f);
- final private ColorRGBA dimmedGreen = ColorRGBA.Green.mult(0.7f);
- final private ColorRGBA dimmedOrange = ColorRGBA.Orange.mult(0.7f);
- final private ColorRGBA dimmedRed = ColorRGBA.Red.mult(0.7f);
+ private final ColorRGBA dimmedWhite = ColorRGBA.White.mult(0.7f);
+ private final ColorRGBA dimmedGreen = ColorRGBA.Green.mult(0.7f);
+ private final ColorRGBA dimmedOrange = ColorRGBA.Orange.mult(0.7f);
+ private final ColorRGBA dimmedRed = ColorRGBA.Red.mult(0.7f);
- final private ProfilerInputListener inputListener = new ProfilerInputListener();
+ private final ProfilerInputListener inputListener = new ProfilerInputListener();
public DetailedProfilerState() {
@@ -101,7 +101,8 @@ protected void initialize(Application app) {
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(0, 0, 0, 0.5f));
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
- Geometry darkenStats = new Geometry("StatsDarken", new Quad(PANEL_WIDTH, app.getCamera().getHeight()));
+ Geometry darkenStats = new Geometry("StatsDarken", new Quad(PANEL_WIDTH,
+ app.getCamera().getHeight()));
darkenStats.setMaterial(mat);
darkenStats.setLocalTranslation(0, -app.getCamera().getHeight(), -1);
@@ -116,17 +117,20 @@ protected void initialize(Application app) {
BitmapText frameLabel = new BitmapText(bigFont);
frameLabel.setText("Total Frame Time: ");
ui.attachChild(frameLabel);
- frameLabel.setLocalTranslation(new Vector3f(PANEL_WIDTH / 2 - bigFont.getLineWidth(frameLabel.getText()), -PADDING, 0));
+ frameLabel.setLocalTranslation(
+ new Vector3f(PANEL_WIDTH / 2 - bigFont.getLineWidth(frameLabel.getText()), -PADDING, 0));
BitmapText cpuLabel = new BitmapText(bigFont);
cpuLabel.setText("CPU");
ui.attachChild(cpuLabel);
- cpuLabel.setLocalTranslation(PANEL_WIDTH / 4 - bigFont.getLineWidth(cpuLabel.getText()) / 2, -PADDING - 30, 0);
+ cpuLabel.setLocalTranslation(PANEL_WIDTH / 4 - bigFont.getLineWidth(cpuLabel.getText()) / 2,
+ -PADDING - 30, 0);
BitmapText gpuLabel = new BitmapText(bigFont);
gpuLabel.setText("GPU");
ui.attachChild(gpuLabel);
- gpuLabel.setLocalTranslation(3 * PANEL_WIDTH / 4 - bigFont.getLineWidth(gpuLabel.getText()) / 2, -PADDING - 30, 0);
+ gpuLabel.setLocalTranslation(3 * PANEL_WIDTH / 4 - bigFont.getLineWidth(gpuLabel.getText()) / 2,
+ -PADDING - 30, 0);
frameTimeValue = new BitmapText(bigFont);
frameCpuTimeValue = new BitmapText(bigFont);
@@ -221,16 +225,22 @@ private void layout() {
setColor(frameTimeValue, prof.getAverageFrameTime(), totalTimeCpu, false, false);
frameCpuTimeValue.setText(df.format(getMsFromNs(totalTimeCpu)) + "ms");
- frameCpuTimeValue.setLocalTranslation(new Vector3f(PANEL_WIDTH / 4 - bigFont.getLineWidth(frameCpuTimeValue.getText()) / 2, -PADDING - 50, 0));
+ frameCpuTimeValue.setLocalTranslation(
+ new Vector3f(PANEL_WIDTH / 4 - bigFont.getLineWidth(frameCpuTimeValue.getText()) / 2,
+ -PADDING - 50, 0));
setColor(frameCpuTimeValue, totalTimeCpu, totalTimeCpu, false, false);
frameGpuTimeValue.setText(df.format(getMsFromNs(totalTimeGpu)) + "ms");
- frameGpuTimeValue.setLocalTranslation(new Vector3f(3 * PANEL_WIDTH / 4 - bigFont.getLineWidth(frameGpuTimeValue.getText()) / 2, -PADDING - 50, 0));
+ frameGpuTimeValue.setLocalTranslation(
+ new Vector3f(3 * PANEL_WIDTH / 4 - bigFont.getLineWidth(frameGpuTimeValue.getText()) / 2,
+ -PADDING - 50, 0));
setColor(frameGpuTimeValue, totalTimeGpu, totalTimeGpu, false, false);
- selectedField.setText("Selected: " + df.format(getMsFromNs(selectedValueCpu)) + "ms / " + df.format(getMsFromNs(selectedValueGpu)) + "ms");
+ selectedField.setText("Selected: " + df.format(getMsFromNs(selectedValueCpu)) + "ms / "
+ + df.format(getMsFromNs(selectedValueGpu)) + "ms");
- selectedField.setLocalTranslation(3 * PANEL_WIDTH / 4 - font.getLineWidth(selectedField.getText()) / 2, -PADDING - 75, 0);
+ selectedField.setLocalTranslation(
+ 3 * PANEL_WIDTH / 4 - font.getLineWidth(selectedField.getText()) / 2, -PADDING - 75, 0);
}
private StatLineView getStatLineView(String path) {
@@ -285,7 +295,8 @@ protected void onDisable() {
ui.removeFromParent();
}
- public boolean setColor(BitmapText t, double value, double totalTime, boolean isParent, boolean expended) {
+ public boolean setColor(BitmapText t, double value, double totalTime, boolean isParent,
+ boolean expended) {
boolean dimmed = isParent && expended;
boolean insignificant = false;
@@ -413,7 +424,8 @@ public void layout(int indent) {
int y = -(height * LINE_HEIGHT + HEADER_HEIGHT);
label.setLocalTranslation(PADDING + indent * PADDING, y, 0);
- float gpuPos = PANEL_WIDTH - font.getLineWidth(gpuText.getText()) - PADDING * (maxLevel - indent + 1);
+ float gpuPos = PANEL_WIDTH - font.getLineWidth(gpuText.getText())
+ - PADDING * (maxLevel - indent + 1);
cpuText.setLocalTranslation(gpuPos - font.getLineWidth(cpuText.getText()), y, 0);
gpuText.setLocalTranslation(gpuPos, y, 0);
@@ -466,7 +478,8 @@ public void setVisible(boolean visible) {
@Override
public String toString() {
- return label.getText() + " - " + df.format(getMsFromNs(cpuValue)) + "ms / " + df.format(getMsFromNs(gpuValue)) + "ms";
+ return label.getText() + " - " + df.format(getMsFromNs(cpuValue)) + "ms / "
+ + df.format(getMsFromNs(gpuValue)) + "ms";
}
}
diff --git a/jme3-core/src/main/java/com/jme3/app/LegacyApplication.java b/jme3-core/src/main/java/com/jme3/app/LegacyApplication.java
index 626dec4bfd..1e0a75daf7 100644
--- a/jme3-core/src/main/java/com/jme3/app/LegacyApplication.java
+++ b/jme3-core/src/main/java/com/jme3/app/LegacyApplication.java
@@ -37,7 +37,11 @@
import com.jme3.audio.AudioContext;
import com.jme3.audio.AudioRenderer;
import com.jme3.audio.Listener;
-import com.jme3.input.*;
+import com.jme3.input.InputManager;
+import com.jme3.input.JoyInput;
+import com.jme3.input.KeyInput;
+import com.jme3.input.MouseInput;
+import com.jme3.input.TouchInput;
import com.jme3.math.Vector3f;
import com.jme3.profile.AppProfiler;
import com.jme3.profile.AppStep;
@@ -45,8 +49,15 @@
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.Renderer;
import com.jme3.renderer.ViewPort;
-import com.jme3.system.*;
+import com.jme3.system.AppSettings;
+import com.jme3.system.Displays;
+import com.jme3.system.JmeContext;
import com.jme3.system.JmeContext.Type;
+import com.jme3.system.JmeSystem;
+import com.jme3.system.NanoTimer;
+import com.jme3.system.SystemListener;
+import com.jme3.system.Timer;
+import com.jme3.util.res.Resources;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.Callable;
@@ -188,9 +199,9 @@ public void setPauseOnLostFocus(boolean pauseOnLostFocus) {
@Deprecated
public void setAssetManager(AssetManager assetManager) {
- if (this.assetManager != null)
- throw new IllegalStateException("Can only set asset manager"
- + " before initialization.");
+ if (this.assetManager != null) {
+ throw new IllegalStateException("Can only set asset manager" + " before initialization.");
+ }
this.assetManager = assetManager;
}
@@ -204,11 +215,16 @@ private void initAssetManager() {
try {
assetCfgUrl = new URL(assetCfg);
} catch (MalformedURLException ex) {
+ //do nothing, we check assetCfgUrl
}
if (assetCfgUrl == null) {
- assetCfgUrl = LegacyApplication.class.getClassLoader().getResource(assetCfg);
+ assetCfgUrl = Resources.getResource(assetCfg);
if (assetCfgUrl == null) {
- logger.log(Level.SEVERE, "Unable to access AssetConfigURL in asset config:{0}", assetCfg);
+ logger.log(
+ Level.SEVERE,
+ "Unable to access AssetConfigURL in asset config:{0}",
+ assetCfg
+ );
return;
}
}
@@ -332,21 +348,25 @@ private void initCamera() {
*/
private void initInput() {
mouseInput = context.getMouseInput();
- if (mouseInput != null)
+ if (mouseInput != null) {
mouseInput.initialize();
+ }
keyInput = context.getKeyInput();
- if (keyInput != null)
+ if (keyInput != null) {
keyInput.initialize();
+ }
touchInput = context.getTouchInput();
- if (touchInput != null)
+ if (touchInput != null) {
touchInput.initialize();
+ }
if (settings.useJoysticks()) {
joyInput = context.getJoyInput();
- if (joyInput != null)
+ if (joyInput != null) {
joyInput.initialize();
+ }
}
inputManager = new InputManager(mouseInput, keyInput, joyInput, touchInput);
@@ -578,9 +598,8 @@ public void reshape(int w, int h) {
}
}
-
@Override
- public void rescale(float x, float y){
+ public void rescale(float x, float y) {
if (renderManager != null) {
renderManager.notifyRescale(x, y);
}
@@ -618,7 +637,7 @@ public void stop() {
* After the application has stopped, it cannot be used anymore.
*
* @param waitFor true→wait for the context to be fully destroyed,
- * true→don't wait
+ * false→don't wait
*/
@Override
public void stop(boolean waitFor) {
@@ -651,9 +670,8 @@ public void initialize() {
initAudio();
// update timer so that the next delta is not too large
-// timer.update();
+ // timer.update();
timer.reset();
-
// user code here
}
@@ -667,8 +685,12 @@ public void handleError(String errMsg, Throwable t) {
// Display error message on screen if not in headless mode
if (context.getType() != JmeContext.Type.Headless) {
if (t != null) {
- JmeSystem.handleErrorMessage(errMsg + "\n" + t.getClass().getSimpleName()
- + (t.getMessage() != null ? ": " + t.getMessage() : ""));
+ JmeSystem.handleErrorMessage(
+ errMsg +
+ "\n" +
+ t.getClass().getSimpleName() +
+ (t.getMessage() != null ? ": " + t.getMessage() : "")
+ );
} else {
JmeSystem.handleErrorMessage(errMsg);
}
@@ -770,42 +792,49 @@ public void update() {
// Make sure the audio renderer is available to callables
AudioContext.setAudioRenderer(audioRenderer);
- if (prof != null)
+ if (prof != null) {
prof.appStep(AppStep.QueuedTasks);
+ }
runQueuedTasks();
- if (speed == 0 || paused)
+ if (speed == 0 || paused) {
return;
+ }
timer.update();
if (inputEnabled) {
- if (prof != null)
+ if (prof != null) {
prof.appStep(AppStep.ProcessInput);
+ }
inputManager.update(timer.getTimePerFrame());
}
if (audioRenderer != null) {
- if (prof != null)
+ if (prof != null) {
prof.appStep(AppStep.ProcessAudio);
+ }
audioRenderer.update(timer.getTimePerFrame());
}
-
// user code here
}
protected void destroyInput() {
- if (mouseInput != null)
+ if (mouseInput != null) {
mouseInput.destroy();
+ }
- if (keyInput != null)
+ if (keyInput != null) {
keyInput.destroy();
+ }
- if (joyInput != null)
+ if (joyInput != null) {
joyInput.destroy();
+ }
- if (touchInput != null)
+ if (touchInput != null) {
touchInput.destroy();
+ }
inputManager = null;
}
@@ -819,8 +848,9 @@ public void destroy() {
stateManager.cleanup();
destroyInput();
- if (audioRenderer != null)
+ if (audioRenderer != null) {
audioRenderer.cleanup();
+ }
timer.reset();
}
@@ -840,6 +870,7 @@ public ViewPort getViewPort() {
}
private class RunnableWrapper implements Callable {
+
private final Runnable runnable;
public RunnableWrapper(Runnable runnable) {
@@ -852,4 +883,26 @@ public Object call() {
return null;
}
}
+
+ /**
+ * This call will return a list of Monitors that glfwGetMonitors()
+ * returns and information about the monitor, like width, height,
+ * and refresh rate.
+ *
+ * @return returns a list of monitors and their information.
+ */
+ public Displays getDisplays() {
+ return context.getDisplays();
+ }
+
+ /**
+ * Use this to get the positional number of the primary
+ * monitor from the glfwGetMonitors() function call.
+ *
+ * @return the position of the value in the arraylist of
+ * the primary monitor.
+ */
+ public int getPrimaryDisplay() {
+ return context.getPrimaryDisplay();
+ }
}
diff --git a/jme3-core/src/main/java/com/jme3/app/SimpleApplication.java b/jme3-core/src/main/java/com/jme3/app/SimpleApplication.java
index 90e1bb33e0..704d0eb77a 100644
--- a/jme3-core/src/main/java/com/jme3/app/SimpleApplication.java
+++ b/jme3-core/src/main/java/com/jme3/app/SimpleApplication.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2022 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -45,26 +45,47 @@
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial.CullHint;
+import com.jme3.scene.threadwarden.SceneGraphThreadWarden;
import com.jme3.system.AppSettings;
import com.jme3.system.JmeContext.Type;
import com.jme3.system.JmeSystem;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
/**
- * SimpleApplication is the base class for all jME3 Applications.
- * SimpleApplication will display a statistics view
- * using the {@link com.jme3.app.StatsAppState} AppState. It will display
- * the current frames-per-second value on-screen in addition to the statistics.
- * Several keys have special functionality in SimpleApplication:
+ * `SimpleApplication` is the foundational base class for all jMonkeyEngine 3 (jME3) applications.
+ * It provides a streamlined setup for common game development tasks, including scene management,
+ * camera controls, and performance monitoring.
+ *
+ *
By default, `SimpleApplication` attaches several essential {@link com.jme3.app.state.AppState} instances:
+ *
{@link com.jme3.app.FlyCamAppState}: Provides a convenient first-person fly-by camera
+ * controller, allowing easy navigation within the scene.
+ *
{@link com.jme3.audio.AudioListenerState}: Manages the audio listener, essential for 3D sound.
+ *
{@link com.jme3.app.DebugKeysAppState}: Enables debug functionalities like displaying
+ * camera position and memory usage in the console.
+ *
{@link com.jme3.app.state.ConstantVerifierState}: A utility state for verifying constant
+ * values, primarily for internal engine debugging.
+ *
*
- * Esc - Close the application.
- * C - Display the camera position and rotation in the console.
- * M - Display memory usage in the console.
+ *
Default Key Bindings:
+ *
+ *
Esc: Closes and exits the application.
+ *
F5: Toggles the visibility of the statistics view (FPS and debug stats).
+ *
C: Prints the current camera position and rotation to the console.
+ *
M: Prints memory usage statistics to the console.
+ *
*
- * A {@link com.jme3.app.FlyCamAppState} is by default attached as well and can
- * be removed by calling stateManager.detach(stateManager.getState(FlyCamAppState.class));
+ *
Applications extending `SimpleApplication` should implement the
+ * {@link #simpleInitApp()} method to set up their initial scene and game logic.
*/
public abstract class SimpleApplication extends LegacyApplication {
+ protected static final Logger logger = Logger.getLogger(SimpleApplication.class.getName());
+
public static final String INPUT_MAPPING_EXIT = "SIMPLEAPP_Exit";
public static final String INPUT_MAPPING_CAMERA_POS = DebugKeysAppState.INPUT_MAPPING_CAMERA_POS;
public static final String INPUT_MAPPING_MEMORY = DebugKeysAppState.INPUT_MAPPING_MEMORY;
@@ -76,31 +97,48 @@ public abstract class SimpleApplication extends LegacyApplication {
protected BitmapFont guiFont;
protected FlyByCamera flyCam;
protected boolean showSettings = true;
- final private AppActionListener actionListener = new AppActionListener();
+ private final AppActionListener actionListener = new AppActionListener();
private class AppActionListener implements ActionListener {
@Override
- public void onAction(String name, boolean value, float tpf) {
- if (!value) {
+ public void onAction(String name, boolean isPressed, float tpf) {
+ if (!isPressed) {
return;
}
if (name.equals(INPUT_MAPPING_EXIT)) {
stop();
} else if (name.equals(INPUT_MAPPING_HIDE_STATS)) {
- if (stateManager.getState(StatsAppState.class) != null) {
- stateManager.getState(StatsAppState.class).toggleStats();
+ StatsAppState statsState = stateManager.getState(StatsAppState.class);
+ if (statsState != null) {
+ statsState.toggleStats();
}
}
}
}
+ /**
+ * Constructs a `SimpleApplication` with a predefined set of default
+ * {@link com.jme3.app.state.AppState} instances.
+ * These states provide common functionalities like statistics display,
+ * fly camera control, audio listener, debug keys, and constant verification.
+ */
public SimpleApplication() {
- this(new StatsAppState(), new FlyCamAppState(), new AudioListenerState(), new DebugKeysAppState(),
+ this(new StatsAppState(),
+ new FlyCamAppState(),
+ new AudioListenerState(),
+ new DebugKeysAppState(),
new ConstantVerifierState());
}
+ /**
+ * Constructs a `SimpleApplication` with a custom array of initial
+ * {@link com.jme3.app.state.AppState} instances.
+ *
+ * @param initialStates An array of `AppState` instances to be attached
+ * to the `stateManager` upon initialization.
+ */
public SimpleApplication(AppState... initialStates) {
super(initialStates);
}
@@ -111,6 +149,7 @@ public void start() {
// settings dialog is not shown
boolean loadSettings = false;
if (settings == null) {
+ logger.log(Level.INFO, "AppSettings not set, creating default settings.");
setSettings(new AppSettings(true));
loadSettings = true;
}
@@ -127,57 +166,73 @@ public void start() {
}
/**
- * Returns the application's speed.
+ * Returns the current speed multiplier of the application.
+ * This value affects how quickly the game world updates relative to real time.
+ * A value of 1.0f means normal speed, 0.5f means half speed, 2.0f means double speed.
*
- * @return The speed of the application.
+ * @return The current speed of the application.
*/
public float getSpeed() {
return speed;
}
/**
- * Changes the application's speed. 0.0f prevents the application from updating.
- * @param speed The speed to set.
+ * Changes the application's speed multiplier.
+ * A `speed` of 0.0f effectively pauses the application's update cycle.
+ *
+ * @param speed The desired speed multiplier. A value of 1.0f is normal speed.
+ * Must be non-negative.
*/
public void setSpeed(float speed) {
this.speed = speed;
}
/**
- * Retrieves flyCam
- * @return flyCam Camera object
+ * Retrieves the `FlyByCamera` instance associated with this application.
+ * This camera allows free-form navigation within the 3D scene.
*
+ * @return The `FlyByCamera` object, or `null` if `FlyCamAppState` is not attached
+ * or has not yet initialized the camera.
*/
public FlyByCamera getFlyByCamera() {
return flyCam;
}
/**
- * Retrieves guiNode
- * @return guiNode Node object
+ * Retrieves the `Node` dedicated to 2D graphical user interface (GUI) elements.
+ * Objects attached to this node are rendered on top of the 3D scene,
+ * typically without perspective effects, suitable for HUDs and UI.
*
+ * @return The `Node` object representing the GUI root.
*/
public Node getGuiNode() {
return guiNode;
}
/**
- * Retrieves rootNode
- * @return rootNode Node object
+ * Retrieves the root `Node` of the 3D scene graph.
+ * All main 3D spatial objects and models should be attached to this node
+ * to be part of the rendered scene.
*
+ * @return The `Node` object representing the 3D scene root.
*/
public Node getRootNode() {
return rootNode;
}
+ /**
+ * Checks whether the settings dialog is configured to be shown at application startup.
+ *
+ * @return `true` if the settings dialog will be displayed, `false` otherwise.
+ */
public boolean isShowSettings() {
return showSettings;
}
/**
- * Toggles settings window to display at start-up
- * @param showSettings Sets true/false
+ * Sets whether the jME3 settings dialog should be displayed before the application starts.
*
+ * @param showSettings `true` to show the settings dialog, `false` to suppress it.
*/
public void setShowSettings(boolean showSettings) {
this.showSettings = showSettings;
@@ -197,109 +252,169 @@ protected BitmapFont loadGuiFont() {
public void initialize() {
super.initialize();
+ //noinspection AssertWithSideEffects
+ assert SceneGraphThreadWarden.setup(rootNode);
+ //noinspection AssertWithSideEffects
+ assert SceneGraphThreadWarden.setup(guiNode);
+
// Several things rely on having this
guiFont = loadGuiFont();
guiNode.setQueueBucket(Bucket.Gui);
guiNode.setCullHint(CullHint.Never);
+
viewPort.attachScene(rootNode);
guiViewPort.attachScene(guiNode);
if (inputManager != null) {
-
- // We have to special-case the FlyCamAppState because too
- // many SimpleApplication subclasses expect it to exist in
- // simpleInit(). But at least it only gets initialized if
- // the app state is added.
- if (stateManager.getState(FlyCamAppState.class) != null) {
+ // Special handling for FlyCamAppState:
+ // Although FlyCamAppState manages the FlyByCamera, SimpleApplication
+ // historically initializes and configures a default FlyByCamera instance
+ // and sets its initial speed. This allows subclasses to directly access
+ // 'flyCam' early in simpleInitApp().
+
+ FlyCamAppState flyCamState = stateManager.getState(FlyCamAppState.class);
+ if (flyCamState != null) {
flyCam = new FlyByCamera(cam);
- flyCam.setMoveSpeed(1f); // odd to set this here but it did it before
- stateManager.getState(FlyCamAppState.class).setCamera(flyCam);
+ flyCam.setMoveSpeed(1f); // Set a default movement speed for the camera
+ flyCamState.setCamera(flyCam); // Link the FlyCamAppState to this camera instance
}
+ // Register the "Exit" input mapping for the Escape key, but only for Display contexts.
if (context.getType() == Type.Display) {
inputManager.addMapping(INPUT_MAPPING_EXIT, new KeyTrigger(KeyInput.KEY_ESCAPE));
}
- if (stateManager.getState(StatsAppState.class) != null) {
+ // Register the "Hide Stats" input mapping for the F5 key, if StatsAppState is active.
+ StatsAppState statsState = stateManager.getState(StatsAppState.class);
+ if (statsState != null) {
inputManager.addMapping(INPUT_MAPPING_HIDE_STATS, new KeyTrigger(KeyInput.KEY_F5));
inputManager.addListener(actionListener, INPUT_MAPPING_HIDE_STATS);
}
+ // Attach the action listener to the "Exit" mapping.
inputManager.addListener(actionListener, INPUT_MAPPING_EXIT);
}
- if (stateManager.getState(StatsAppState.class) != null) {
- // Some tests rely on having access to fpsText
- // for quick display. Maybe a different way would be better.
- stateManager.getState(StatsAppState.class).setFont(guiFont);
- fpsText = stateManager.getState(StatsAppState.class).getFpsText();
+ // Configure the StatsAppState if it exists.
+ StatsAppState statsState = stateManager.getState(StatsAppState.class);
+ if (statsState != null) {
+ statsState.setFont(guiFont);
+ fpsText = statsState.getFpsText();
}
- // call user code
+ // Call the user's application initialization code.
simpleInitApp();
}
+ @Override
+ public void stop(boolean waitFor) {
+ //noinspection AssertWithSideEffects
+ assert SceneGraphThreadWarden.reset();
+ super.stop(waitFor);
+ }
+
@Override
public void update() {
- if (prof != null)
+ if (prof != null) {
prof.appStep(AppStep.BeginFrame);
+ }
- super.update(); // makes sure to execute AppTasks
+ // Executes AppTasks from the main thread
+ super.update();
+
+ // Skip updates if paused or speed is zero
if (speed == 0 || paused) {
return;
}
float tpf = timer.getTimePerFrame() * speed;
- // update states
- if (prof != null)
+ // Update AppStates
+ if (prof != null) {
prof.appStep(AppStep.StateManagerUpdate);
+ }
stateManager.update(tpf);
- // simple update and root node
+ // Call user's per-frame update method
simpleUpdate(tpf);
- if (prof != null)
+ // Update scene graph nodes (logical and geometric states)
+ if (prof != null) {
prof.appStep(AppStep.SpatialUpdate);
+ }
rootNode.updateLogicalState(tpf);
guiNode.updateLogicalState(tpf);
rootNode.updateGeometricState();
guiNode.updateGeometricState();
- // render states
- if (prof != null)
+ // Render AppStates and the scene
+ if (prof != null) {
prof.appStep(AppStep.StateManagerRender);
+ }
stateManager.render(renderManager);
- if (prof != null)
+ if (prof != null) {
prof.appStep(AppStep.RenderFrame);
+ }
renderManager.render(tpf, context.isRenderable());
+ // Call user's custom render method
simpleRender(renderManager);
stateManager.postRender();
- if (prof != null)
+ if (prof != null) {
prof.appStep(AppStep.EndFrame);
+ }
}
+ /**
+ * Controls the visibility of the frames-per-second (FPS) display on the screen.
+ *
+ * @param show `true` to display the FPS, `false` to hide it.
+ */
public void setDisplayFps(boolean show) {
- if (stateManager.getState(StatsAppState.class) != null) {
- stateManager.getState(StatsAppState.class).setDisplayFps(show);
+ StatsAppState statsState = stateManager.getState(StatsAppState.class);
+ if (statsState != null) {
+ statsState.setDisplayFps(show);
}
}
+ /**
+ * Controls the visibility of the comprehensive statistics view on the screen.
+ * This view typically includes details about memory, triangles, and other performance metrics.
+ *
+ * @param show `true` to display the statistics view, `false` to hide it.
+ */
public void setDisplayStatView(boolean show) {
- if (stateManager.getState(StatsAppState.class) != null) {
- stateManager.getState(StatsAppState.class).setDisplayStatView(show);
+ StatsAppState statsState = stateManager.getState(StatsAppState.class);
+ if (statsState != null) {
+ statsState.setDisplayStatView(show);
}
}
public abstract void simpleInitApp();
+ /**
+ * An optional method that can be overridden by subclasses for per-frame update logic.
+ * This method is called during the application's update loop, after AppStates are updated
+ * and before the scene graph's logical state is updated.
+ *
+ * @param tpf The time per frame (in seconds), adjusted by the application's speed.
+ */
public void simpleUpdate(float tpf) {
+ // Default empty implementation; subclasses can override
}
+ /**
+ * An optional method that can be overridden by subclasses for custom rendering logic.
+ * This method is called during the application's render loop, after the main scene
+ * has been rendered and before post-rendering for states.
+ * Useful for drawing overlays or specific rendering tasks outside the main scene graph.
+ *
+ * @param rm The `RenderManager` instance, which provides access to rendering functionalities.
+ */
public void simpleRender(RenderManager rm) {
+ // Default empty implementation; subclasses can override
}
}
diff --git a/jme3-core/src/main/java/com/jme3/app/StatsView.java b/jme3-core/src/main/java/com/jme3/app/StatsView.java
index f6b28fb642..8e274e97b5 100644
--- a/jme3-core/src/main/java/com/jme3/app/StatsView.java
+++ b/jme3-core/src/main/java/com/jme3/app/StatsView.java
@@ -60,11 +60,11 @@
*
*/
public class StatsView extends Node implements Control, JmeCloneable {
- final private BitmapText statText;
- final private Statistics statistics;
+ private final BitmapText statText;
+ private final Statistics statistics;
- final private String[] statLabels;
- final private int[] statData;
+ private final String[] statLabels;
+ private final int[] statData;
private boolean enabled = true;
@@ -96,8 +96,9 @@ public float getHeight() {
@Override
public void update(float tpf) {
- if (!isEnabled())
+ if (!isEnabled()) {
return;
+ }
statistics.getData(statData);
stringBuilder.setLength(0);
diff --git a/jme3-core/src/main/java/com/jme3/app/state/CompositeAppState.java b/jme3-core/src/main/java/com/jme3/app/state/CompositeAppState.java
new file mode 100644
index 0000000000..c8d2d9acb5
--- /dev/null
+++ b/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 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 addChild(T state) {
+ return addChild(state, false);
+ }
+
+ protected 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 getChild( Class 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);
+ }
+ }
+ }
+}
+
diff --git a/jme3-core/src/main/java/com/jme3/app/state/ConstantVerifierState.java b/jme3-core/src/main/java/com/jme3/app/state/ConstantVerifierState.java
index 956818e3d5..8d42fad624 100644
--- a/jme3-core/src/main/java/com/jme3/app/state/ConstantVerifierState.java
+++ b/jme3-core/src/main/java/com/jme3/app/state/ConstantVerifierState.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2014-2021 jMonkeyEngine
+ * Copyright (c) 2014-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -29,30 +29,39 @@
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-
package com.jme3.app.state;
-import java.util.Arrays;
-import java.util.logging.Logger;
-
import com.jme3.app.Application;
-import com.jme3.math.*;
+import com.jme3.math.Matrix3f;
+import com.jme3.math.Matrix4f;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.math.Vector4f;
import com.jme3.util.SafeArrayList;
+import java.util.Arrays;
+import java.util.logging.Logger;
+
+import static java.lang.Float.NEGATIVE_INFINITY;
import static java.lang.Float.NaN;
import static java.lang.Float.POSITIVE_INFINITY;
-import static java.lang.Float.NEGATIVE_INFINITY;
/**
- * Checks the various JME 'constants' for drift using either asserts
- * or straight checks. The list of constants can also be configured
- * but defaults to the standard JME Vector3f, Quaternion, etc. constants.
+ * An AppState that periodically checks the values of various JME math constants
+ * (e.g., `Vector3f.ZERO`, `Quaternion.IDENTITY`) against their known good values.
+ * This is useful for detecting accidental modifications or "drift" of these
+ * supposedly immutable constants during application runtime.
+ *
+ * The state can be configured to report discrepancies using asserts,
+ * throwing runtime exceptions, or logging severe messages.
+ * The set of constants to check is configurable.
*
- * @author Paul Speed
+ * @author Paul Speed
*/
public class ConstantVerifierState extends BaseAppState {
- private static final Logger log = Logger.getLogger(BaseAppState.class.getName());
+ private static final Logger log = Logger.getLogger(ConstantVerifierState.class.getName());
// Note: I've used actual constructed objects for the good values
// instead of clone just to better catch cases where the values
@@ -73,7 +82,14 @@ public class ConstantVerifierState extends BaseAppState {
new Quaternion().fromAxes(Vector3f.UNIT_X, Vector3f.UNIT_Y, Vector3f.UNIT_Z)),
new Checker(Quaternion.ZERO, new Quaternion(0, 0, 0, 0)),
new Checker(Vector2f.ZERO, new Vector2f(0f, 0f)),
+ new Checker(Vector2f.NAN, new Vector2f(NaN, NaN)),
+ new Checker(Vector2f.UNIT_X, new Vector2f(1, 0)),
+ new Checker(Vector2f.UNIT_Y, new Vector2f(0, 1)),
new Checker(Vector2f.UNIT_XY, new Vector2f(1f, 1f)),
+ new Checker(Vector2f.POSITIVE_INFINITY,
+ new Vector2f(POSITIVE_INFINITY, POSITIVE_INFINITY)),
+ new Checker(Vector2f.NEGATIVE_INFINITY,
+ new Vector2f(NEGATIVE_INFINITY, NEGATIVE_INFINITY)),
new Checker(Vector4f.ZERO, new Vector4f(0, 0, 0, 0)),
new Checker(Vector4f.NAN, new Vector4f(NaN, NaN, NaN, NaN)),
new Checker(Vector4f.UNIT_X, new Vector4f(1, 0, 0, 0)),
@@ -91,24 +107,34 @@ public class ConstantVerifierState extends BaseAppState {
new Checker(Matrix4f.IDENTITY, new Matrix4f())
};
- public enum ErrorType { Assert, Exception, Log };
+ /**
+ * Defines how constant value discrepancies should be reported.
+ */
+ public enum ErrorType {
+ /** Causes an `assert` failure if the constant has changed. Requires assertions to be enabled. */
+ Assert,
+ /** Throws a `RuntimeException` if the constant has changed. */
+ Exception,
+ /** Logs a severe message if the constant has changed. */
+ Log
+ }
- final private SafeArrayList checkers = new SafeArrayList<>(Checker.class);
+ private final SafeArrayList checkers = new SafeArrayList<>(Checker.class);
private ErrorType errorType;
/**
- * Creates a verifier app state that will check all of the default
- * constant checks using asserts.
+ * Creates a verifier app state that will check all of the default
+ * JME math constants using `ErrorType.Assert`.
*/
public ConstantVerifierState() {
this(ErrorType.Assert);
}
/**
- * Creates a verifier app state that will check all of the default
- * constant checks using the specified error reporting mechanism.
+ * Creates a verifier app state that will check all of the default
+ * JME math constants using the specified error reporting mechanism.
*
- * @param errorType the mechanism to use
+ * @param errorType The mechanism to use when a constant's value drifts.
*/
public ConstantVerifierState(ErrorType errorType) {
this(errorType, DEFAULT_CHECKS);
@@ -126,14 +152,32 @@ private ConstantVerifierState(ErrorType errorType, Checker... checkers) {
this.checkers.addAll(Arrays.asList(checkers));
}
+ /**
+ * Adds a new constant and its expected good value to the list of items to be checked.
+ * The `constant` and `goodValue` should be instances of the same class.
+ *
+ * @param constant The JME constant object to monitor for drift (e.g., `Vector3f.ZERO`).
+ * @param goodValue An independent instance representing the expected correct value of the constant.
+ * This instance should match the initial value of `constant`.
+ */
public void addChecker(Object constant, Object goodValue) {
checkers.add(new Checker(constant, goodValue));
}
+ /**
+ * Sets the error reporting mechanism to be used when a constant's value drifts.
+ *
+ * @param errorType The desired error reporting type.
+ */
public void setErrorType(ErrorType errorType) {
this.errorType = errorType;
}
+ /**
+ * Returns the currently configured error reporting mechanism.
+ *
+ * @return The current `ErrorType`.
+ */
public ErrorType getErrorType() {
return errorType;
}
@@ -161,21 +205,26 @@ public void postRender() {
checkValues();
}
+ /**
+ * Iterates through all registered checkers and verifies the current values
+ * of the constants against their known good values.
+ * Reports any discrepancies based on the configured `ErrorType`.
+ */
protected void checkValues() {
for (Checker checker : checkers.getArray()) {
switch (errorType) {
- default:
+ default: // Fall through to Assert if somehow null
case Assert:
assert checker.isValid() : checker.toString();
break;
case Exception:
if (!checker.isValid()) {
- throw new RuntimeException("Constant has changed, " + checker.toString());
+ throw new RuntimeException("JME Constant has changed, " + checker.toString());
}
break;
case Log:
if (!checker.isValid()) {
- log.severe("Constant has changed, " + checker.toString());
+ log.severe("JME Constant has changed, " + checker.toString());
}
break;
}
@@ -188,8 +237,9 @@ protected void checkValues() {
* mean anything.
*/
private static class Checker {
- private Object constant;
- private Object goodValue;
+
+ private final Object constant;
+ private final Object goodValue;
public Checker(Object constant, Object goodValue) {
if (constant == null) {
@@ -197,7 +247,7 @@ public Checker(Object constant, Object goodValue) {
}
if (!constant.equals(goodValue)) {
throw new IllegalArgumentException(
- "Constant value:" + constant + " does not match value:" + goodValue);
+ "Constant value: " + constant + " does not match value: " + goodValue);
}
this.constant = constant;
this.goodValue = goodValue;
diff --git a/jme3-core/src/main/java/com/jme3/asset/AssetConfig.java b/jme3-core/src/main/java/com/jme3/asset/AssetConfig.java
index be2c834ea9..3307cca4cb 100644
--- a/jme3-core/src/main/java/com/jme3/asset/AssetConfig.java
+++ b/jme3-core/src/main/java/com/jme3/asset/AssetConfig.java
@@ -39,6 +39,8 @@
import java.util.logging.Level;
import java.util.logging.Logger;
+import com.jme3.util.res.Resources;
+
/**
* AssetConfig loads a config file to configure the asset manager.
*
@@ -101,7 +103,7 @@ public static void loadText(AssetManager assetManager, URL configUrl) throws IOE
}
} else if (cmd.equals("INCLUDE")) {
String includedCfg = scan.nextLine().trim();
- URL includedCfgUrl = Thread.currentThread().getContextClassLoader().getResource(includedCfg);
+ URL includedCfgUrl = Resources.getResource(includedCfg);
if (includedCfgUrl != null) {
loadText(assetManager, includedCfgUrl);
} else {
diff --git a/jme3-core/src/main/java/com/jme3/asset/AssetManager.java b/jme3-core/src/main/java/com/jme3/asset/AssetManager.java
index cde85ecec1..73762a1234 100644
--- a/jme3-core/src/main/java/com/jme3/asset/AssetManager.java
+++ b/jme3-core/src/main/java/com/jme3/asset/AssetManager.java
@@ -46,6 +46,7 @@
import com.jme3.texture.plugins.TGALoader;
import java.io.IOException;
import java.io.InputStream;
+import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
@@ -86,7 +87,7 @@
* so that modifications to one instance do not leak onto others.
*/
public interface AssetManager {
-
+
/**
* Adds a {@link ClassLoader} that is used to load {@link Class classes}
* that are needed for finding and loading Assets.
@@ -94,23 +95,35 @@ public interface AssetManager {
* use registerLocator for that.
*
* @param loader A ClassLoader that Classes in asset files can be loaded from.
+ * @deprecated use {@link com.jme3.util.res.Resources}
*/
- public void addClassLoader(ClassLoader loader);
+ @Deprecated
+ public default void addClassLoader(ClassLoader loader) {
+
+ }
/**
* Remove a {@link ClassLoader} from the list of registered ClassLoaders
*
* @param loader the ClassLoader to be removed
+ * @deprecated use {@link com.jme3.util.res.Resources}
*/
- public void removeClassLoader(ClassLoader loader);
+ @Deprecated
+ public default void removeClassLoader(ClassLoader loader) {
+
+ }
/**
* Retrieve the list of registered ClassLoaders that are used for loading
* {@link Class classes} from asset files.
*
* @return an unmodifiable list
+ * @deprecated use {@link com.jme3.util.res.Resources}
*/
- public List getClassLoaders();
+ @Deprecated
+ public default List getClassLoaders() {
+ return new ArrayList<>();
+ }
/**
* Register an {@link AssetLoader} by using a class object.
diff --git a/jme3-core/src/main/java/com/jme3/asset/CloneableSmartAsset.java b/jme3-core/src/main/java/com/jme3/asset/CloneableSmartAsset.java
index 74a00dae42..b428fd08d1 100644
--- a/jme3-core/src/main/java/com/jme3/asset/CloneableSmartAsset.java
+++ b/jme3-core/src/main/java/com/jme3/asset/CloneableSmartAsset.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2023 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -65,13 +65,14 @@ public interface CloneableSmartAsset extends Cloneable {
public CloneableSmartAsset clone();
/**
- * Set by the {@link AssetManager} to track this asset.
+ * Assigns the specified AssetKey to the asset.
*
- * Only clones of the asset has this set, the original copy that
- * was loaded has this key set to null so that only the clones are tracked
- * for garbage collection.
+ * This is invoked by the {@link AssetManager}.
+ * Only clones of the asset have non-null keys. The original copy that
+ * was loaded has no key assigned. Only the clones are tracked
+ * for garbage collection.
*
- * @param key The AssetKey to set
+ * @param key The AssetKey to assign
*/
public void setKey(AssetKey key);
diff --git a/jme3-core/src/main/java/com/jme3/asset/DesktopAssetManager.java b/jme3-core/src/main/java/com/jme3/asset/DesktopAssetManager.java
index c81e56fec5..a8ff73f2c4 100644
--- a/jme3-core/src/main/java/com/jme3/asset/DesktopAssetManager.java
+++ b/jme3-core/src/main/java/com/jme3/asset/DesktopAssetManager.java
@@ -73,8 +73,8 @@ public class DesktopAssetManager implements AssetManager {
final private CopyOnWriteArrayList eventListeners =
new CopyOnWriteArrayList<>();
- final private List classLoaders =
- Collections.synchronizedList(new ArrayList<>());
+ @Deprecated
+ final private List classLoaders = Collections.synchronizedList(new ArrayList<>());
public DesktopAssetManager() {
this(null);
@@ -99,21 +99,23 @@ private void loadConfigFile(URL configFile) {
}
}
+ @Deprecated
@Override
public void addClassLoader(ClassLoader loader) {
classLoaders.add(loader);
}
+ @Deprecated
@Override
public void removeClassLoader(ClassLoader loader) {
classLoaders.remove(loader);
}
+ @Deprecated
@Override
public List getClassLoaders() {
return Collections.unmodifiableList(classLoaders);
}
-
@Override
public void addAssetEventListener(AssetEventListener listener) {
eventListeners.add(listener);
diff --git a/jme3-core/src/main/java/com/jme3/asset/ImplHandler.java b/jme3-core/src/main/java/com/jme3/asset/ImplHandler.java
index 1b6275d12d..90af79d803 100644
--- a/jme3-core/src/main/java/com/jme3/asset/ImplHandler.java
+++ b/jme3-core/src/main/java/com/jme3/asset/ImplHandler.java
@@ -124,10 +124,7 @@ protected T initialValue() {
} 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(), "", ex);
+ logger.log(Level.SEVERE, "An exception occurred while instantiating asset locator: " + type.getName(), ex);
}
return null;
}
diff --git a/jme3-core/src/main/java/com/jme3/asset/cache/WeakRefCloneAssetCache.java b/jme3-core/src/main/java/com/jme3/asset/cache/WeakRefCloneAssetCache.java
index 5e0a90c173..722afde411 100644
--- a/jme3-core/src/main/java/com/jme3/asset/cache/WeakRefCloneAssetCache.java
+++ b/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 smartCache
- = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap smartCache = new ConcurrentHashMap<>();
/**
* Stored in the ReferenceQueue to find out when originalKey is collected
diff --git a/jme3-core/src/main/java/com/jme3/audio/AudioBuffer.java b/jme3-core/src/main/java/com/jme3/audio/AudioBuffer.java
index 80f5f63040..249d9601eb 100644
--- a/jme3-core/src/main/java/com/jme3/audio/AudioBuffer.java
+++ b/jme3-core/src/main/java/com/jme3/audio/AudioBuffer.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
@@ -132,6 +132,6 @@ public NativeObject createDestructableClone() {
@Override
public long getUniqueId() {
- return ((long) OBJTYPE_AUDIOBUFFER << 32) | ((long) id);
+ return ((long) OBJTYPE_AUDIOBUFFER << 32) | (0xffffffffL & (long) id);
}
}
diff --git a/jme3-core/src/main/java/com/jme3/audio/AudioNode.java b/jme3-core/src/main/java/com/jme3/audio/AudioNode.java
index 67ac7cba2f..d9b9e8cd2c 100644
--- a/jme3-core/src/main/java/com/jme3/audio/AudioNode.java
+++ b/jme3-core/src/main/java/com/jme3/audio/AudioNode.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -317,6 +317,10 @@ public void setAudioData(AudioData audioData, AudioKey audioKey) {
data = audioData;
this.audioKey = audioKey;
}
+
+ public AudioKey getAudioKey() {
+ return audioKey;
+ }
/**
* @return The {@link AudioData} set previously with
diff --git a/jme3-core/src/main/java/com/jme3/audio/AudioStream.java b/jme3-core/src/main/java/com/jme3/audio/AudioStream.java
index ccc74e181c..15b40c5b43 100644
--- a/jme3-core/src/main/java/com/jme3/audio/AudioStream.java
+++ b/jme3-core/src/main/java/com/jme3/audio/AudioStream.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
@@ -222,6 +222,6 @@ public void setTime(float time) {
@Override
public long getUniqueId() {
- return ((long) OBJTYPE_AUDIOSTREAM << 32) | ((long) ids[0]);
+ return ((long) OBJTYPE_AUDIOSTREAM << 32) | (0xffffffffL & (long) ids[0]);
}
}
diff --git a/jme3-core/src/main/java/com/jme3/audio/BandPassFilter.java b/jme3-core/src/main/java/com/jme3/audio/BandPassFilter.java
new file mode 100644
index 0000000000..950371278c
--- /dev/null
+++ b/jme3-core/src/main/java/com/jme3/audio/BandPassFilter.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2009-2025 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.audio;
+
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.util.NativeObject;
+
+import java.io.IOException;
+
+/**
+ * Represents an OpenAL EFX Band-Pass Filter.
+ */
+public class BandPassFilter extends Filter {
+
+ // Default values based on OpenAL EFX specification defaults
+ protected float volume = 1.0f;
+ protected float highFreqVolume = 1.0f;
+ protected float lowFreqVolume = 1.0f;
+
+ /**
+ * Constructs a band-pass filter with default settings.
+ * Required for jME deserialization
+ */
+ public BandPassFilter() {}
+
+ protected BandPassFilter(int id) {
+ super(id);
+ }
+
+ public BandPassFilter(float volume, float highFreqVolume, float lowFreqVolume) {
+ super();
+ setVolume(volume);
+ setHighFreqVolume(highFreqVolume);
+ setLowFreqVolume(lowFreqVolume);
+ }
+
+ public float getVolume() {
+ return volume;
+ }
+
+ /**
+ * Sets the overall gain of the Band-Pass filter.
+ *
+ * @param volume The gain value (0.0 to 1.0).
+ */
+ public void setVolume(float volume) {
+ if (volume < 0 || volume > 1)
+ throw new IllegalArgumentException("Volume must be between 0 and 1");
+
+ this.volume = volume;
+ this.updateNeeded = true;
+ }
+
+ public float getHighFreqVolume() {
+ return highFreqVolume;
+ }
+
+ /**
+ * Sets the gain at high frequencies for the Band-Pass filter.
+ *
+ * @param highFreqVolume The high-frequency gain value (0.0 to 1.0).
+ */
+ public void setHighFreqVolume(float highFreqVolume) {
+ if (highFreqVolume < 0 || highFreqVolume > 1)
+ throw new IllegalArgumentException("High freq volume must be between 0 and 1");
+
+ this.highFreqVolume = highFreqVolume;
+ this.updateNeeded = true;
+ }
+
+ public float getLowFreqVolume() {
+ return lowFreqVolume;
+ }
+
+ /**
+ * Sets the gain at low frequencies for the Band-Pass filter.
+ *
+ * @param lowFreqVolume The low-frequency gain value (0.0 to 1.0).
+ */
+ public void setLowFreqVolume(float lowFreqVolume) {
+ if (lowFreqVolume < 0 || lowFreqVolume > 1)
+ throw new IllegalArgumentException("Low freq volume must be between 0 and 1");
+
+ this.lowFreqVolume = lowFreqVolume;
+ this.updateNeeded = true;
+ }
+
+ @Override
+ public NativeObject createDestructableClone() {
+ return new BandPassFilter(this.id);
+ }
+
+ /**
+ * Retrieves a unique identifier for this filter. Used internally for native object management.
+ *
+ * @return a unique long identifier.
+ */
+ @Override
+ public long getUniqueId() {
+ return ((long) OBJTYPE_FILTER << 32) | (0xffffffffL & (long) id);
+ }
+
+ @Override
+ public void write(JmeExporter ex) throws IOException {
+ super.write(ex);
+ OutputCapsule oc = ex.getCapsule(this);
+ oc.write(this.volume, "volume", 1f);
+ oc.write(this.lowFreqVolume, "lf_volume", 1f);
+ oc.write(this.highFreqVolume, "hf_volume", 1f);
+ }
+
+ @Override
+ public void read(JmeImporter im) throws IOException {
+ super.read(im);
+ InputCapsule ic = im.getCapsule(this);
+ this.volume = ic.readFloat("volume", 1f);
+ this.lowFreqVolume = ic.readFloat("lf_volume", 1f);
+ this.highFreqVolume = ic.readFloat("hf_volume", 1f);
+ }
+}
diff --git a/jme3-core/src/main/java/com/jme3/audio/Environment.java b/jme3-core/src/main/java/com/jme3/audio/Environment.java
index b974ee25d3..05bec8770c 100644
--- a/jme3-core/src/main/java/com/jme3/audio/Environment.java
+++ b/jme3-core/src/main/java/com/jme3/audio/Environment.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -34,49 +34,80 @@
import com.jme3.math.FastMath;
/**
- * Audio environment, for reverb effects.
+ * Represents an audio environment, primarily used to define reverb effects.
+ * This class provides parameters that correspond to the properties controllable
+ * through the OpenAL EFX (Environmental Effects Extension) library.
+ * By adjusting these parameters, developers can simulate various acoustic spaces
+ * like rooms, caves, and concert halls, adding depth and realism to the audio experience.
+ *
* @author Kirill
*/
public class Environment {
- private float airAbsorbGainHf = 0.99426f;
+ /** High-frequency air absorption gain (0.0f to 1.0f). */
+ private float airAbsorbGainHf = 0.99426f;
+ /** Factor controlling room effect rolloff with distance. */
private float roomRolloffFactor = 0;
-
- private float decayTime = 1.49f;
- private float decayHFRatio = 0.54f;
-
- private float density = 1.0f;
- private float diffusion = 0.3f;
-
- private float gain = 0.316f;
- private float gainHf = 0.022f;
-
- private float lateReverbDelay = 0.088f;
- private float lateReverbGain = 0.768f;
-
- private float reflectDelay = 0.162f;
- private float reflectGain = 0.052f;
-
- private boolean decayHfLimit = true;
-
- public static final Environment Garage, Dungeon, Cavern, AcousticLab, Closet;
-
- static {
- Garage = new Environment(1, 1, 1, 1, .9f, .5f, .751f, .0039f, .661f, .0137f);
- Dungeon = new Environment(.75f, 1, 1, .75f, 1.6f, 1, 0.95f, 0.0026f, 0.93f, 0.0103f);
- Cavern = new Environment(.5f, 1, 1, .5f, 2.25f, 1, .908f, .0103f, .93f, .041f);
- AcousticLab = new Environment(.5f, 1, 1, 1, .28f, 1, .87f, .002f, .81f, .008f);
- Closet = new Environment(1, 1, 1, 1, .15f, 1, .6f, .0025f, .5f, .0006f);
- }
-
+ /** Overall decay time of the reverberation (in seconds). */
+ private float decayTime = 1.49f;
+ /** Ratio of high-frequency decay time to overall decay time (0.0f to 1.0f). */
+ private float decayHFRatio = 0.54f;
+ /** Density of the medium affecting reverb smoothness (0.0f to 1.0f). */
+ private float density = 1.0f;
+ /** Diffusion of reflections affecting echo distinctness (0.0f to 1.0f). */
+ private float diffusion = 0.3f;
+ /** Overall gain of the environment effect (linear scale). */
+ private float gain = 0.316f;
+ /** High-frequency gain of the environment effect (linear scale). */
+ private float gainHf = 0.022f;
+ /** Delay time for late reverberation relative to early reflections (in seconds). */
+ private float lateReverbDelay = 0.088f;
+ /** Gain of the late reverberation (linear scale). */
+ private float lateReverbGain = 0.768f;
+ /** Delay time for the initial reflections (in seconds). */
+ private float reflectDelay = 0.162f;
+ /** Gain of the initial reflections (linear scale). */
+ private float reflectGain = 0.052f;
+ /** Flag limiting high-frequency decay by the overall decay time. */
+ private boolean decayHfLimit = true;
+
+ public static final Environment Garage = new Environment(
+ 1, 1, 1, 1, .9f, .5f, .751f, .0039f, .661f, .0137f);
+ public static final Environment Dungeon = new Environment(
+ .75f, 1, 1, .75f, 1.6f, 1, 0.95f, 0.0026f, 0.93f, 0.0103f);
+ public static final Environment Cavern = new Environment(
+ .5f, 1, 1, .5f, 2.25f, 1, .908f, .0103f, .93f, .041f);
+ public static final Environment AcousticLab = new Environment(
+ .5f, 1, 1, 1, .28f, 1, .87f, .002f, .81f, .008f);
+ public static final Environment Closet = new Environment(
+ 1, 1, 1, 1, .15f, 1, .6f, .0025f, .5f, .0006f);
+
+ /**
+ * Utility method to convert an EAX decibel value to an amplitude factor.
+ * EAX often expresses gain and attenuation in decibels scaled by 1000.
+ * This method performs the reverse of that conversion to obtain a linear
+ * amplitude value suitable for OpenAL.
+ *
+ * @param eaxDb The EAX decibel value (scaled by 1000).
+ * @return The corresponding amplitude factor.
+ */
private static float eaxDbToAmp(float eaxDb) {
float dB = eaxDb / 2000f;
return FastMath.pow(10f, dB);
}
+ /**
+ * Constructs a new, default {@code Environment}. The default values are
+ * typically chosen to represent a neutral or common acoustic space.
+ */
public Environment() {
}
+ /**
+ * Creates a new {@code Environment} as a copy of the provided {@code Environment}.
+ *
+ * @param source The {@code Environment} to copy the settings from.
+ */
public Environment(Environment source) {
this.airAbsorbGainHf = source.airAbsorbGainHf;
this.roomRolloffFactor = source.roomRolloffFactor;
@@ -93,9 +124,24 @@ public Environment(Environment source) {
this.decayHfLimit = source.decayHfLimit;
}
+ /**
+ * Creates a new {@code Environment} with the specified parameters. These parameters
+ * directly influence the properties of the reverb effect as managed by OpenAL EFX.
+ *
+ * @param density The density of the medium.
+ * @param diffusion The diffusion of the reflections.
+ * @param gain Overall gain applied to the environment effect.
+ * @param gainHf High-frequency gain applied to the environment effect.
+ * @param decayTime The overall decay time of the reflected sound.
+ * @param decayHf Ratio of high-frequency decay time to the overall decay time.
+ * @param reflectGain Gain applied to the initial reflections.
+ * @param reflectDelay Delay time for the initial reflections.
+ * @param lateGain Gain applied to the late reverberation.
+ * @param lateDelay Delay time for the late reverberation.
+ */
public Environment(float density, float diffusion, float gain, float gainHf,
- float decayTime, float decayHf, float reflectGain,
- float reflectDelay, float lateGain, float lateDelay) {
+ float decayTime, float decayHf, float reflectGain, float reflectDelay,
+ float lateGain, float lateDelay) {
this.decayTime = decayTime;
this.decayHFRatio = decayHf;
this.density = density;
@@ -108,6 +154,16 @@ public Environment(float density, float diffusion, float gain, float gainHf,
this.reflectGain = reflectGain;
}
+ /**
+ * Creates a new {@code Environment} by interpreting an array of 28 float values
+ * as an EAX preset. This constructor attempts to map the EAX preset values to
+ * the corresponding OpenAL EFX parameters. Note that not all EAX parameters
+ * have a direct equivalent in standard OpenAL EFX, so some values might be
+ * approximated or ignored.
+ *
+ * @param e An array of 28 float values representing an EAX preset.
+ * @throws IllegalArgumentException If the provided array does not have a length of 28.
+ */
public Environment(float[] e) {
if (e.length != 28)
throw new IllegalArgumentException("Not an EAX preset");
@@ -252,4 +308,73 @@ public float getRoomRolloffFactor() {
public void setRoomRolloffFactor(float roomRolloffFactor) {
this.roomRolloffFactor = roomRolloffFactor;
}
+
+ @Override
+ public boolean equals(Object obj) {
+
+ if (!(obj instanceof Environment))
+ return false;
+
+ if (obj == this)
+ return true;
+
+ Environment other = (Environment) obj;
+ float epsilon = 1e-6f;
+
+ float[] thisFloats = {
+ this.airAbsorbGainHf,
+ this.decayHFRatio,
+ this.decayTime,
+ this.density,
+ this.diffusion,
+ this.gain,
+ this.gainHf,
+ this.lateReverbDelay,
+ this.lateReverbGain,
+ this.reflectDelay,
+ this.reflectGain,
+ this.roomRolloffFactor
+ };
+
+ float[] otherFloats = {
+ other.airAbsorbGainHf,
+ other.decayHFRatio,
+ other.decayTime,
+ other.density,
+ other.diffusion,
+ other.gain,
+ other.gainHf,
+ other.lateReverbDelay,
+ other.lateReverbGain,
+ other.reflectDelay,
+ other.reflectGain,
+ other.roomRolloffFactor
+ };
+
+ for (int i = 0; i < thisFloats.length; i++) {
+ if (Math.abs(thisFloats[i] - otherFloats[i]) >= epsilon) {
+ return false;
+ }
+ }
+
+ return this.decayHfLimit == other.decayHfLimit;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (airAbsorbGainHf != +0.0f ? Float.floatToIntBits(airAbsorbGainHf) : 0);
+ result = 31 * result + (roomRolloffFactor != +0.0f ? Float.floatToIntBits(roomRolloffFactor) : 0);
+ result = 31 * result + (decayTime != +0.0f ? Float.floatToIntBits(decayTime) : 0);
+ result = 31 * result + (decayHFRatio != +0.0f ? Float.floatToIntBits(decayHFRatio) : 0);
+ result = 31 * result + (density != +0.0f ? Float.floatToIntBits(density) : 0);
+ result = 31 * result + (diffusion != +0.0f ? Float.floatToIntBits(diffusion) : 0);
+ result = 31 * result + (gain != +0.0f ? Float.floatToIntBits(gain) : 0);
+ result = 31 * result + (gainHf != +0.0f ? Float.floatToIntBits(gainHf) : 0);
+ result = 31 * result + (lateReverbDelay != +0.0f ? Float.floatToIntBits(lateReverbDelay) : 0);
+ result = 31 * result + (lateReverbGain != +0.0f ? Float.floatToIntBits(lateReverbGain) : 0);
+ result = 31 * result + (reflectDelay != +0.0f ? Float.floatToIntBits(reflectDelay) : 0);
+ result = 31 * result + (reflectGain != +0.0f ? Float.floatToIntBits(reflectGain) : 0);
+ result = 31 * result + (decayHfLimit ? 1 : 0);
+ return result;
+ }
}
diff --git a/jme3-core/src/main/java/com/jme3/audio/Filter.java b/jme3-core/src/main/java/com/jme3/audio/Filter.java
index 83bc99e753..86bf987cd6 100644
--- a/jme3-core/src/main/java/com/jme3/audio/Filter.java
+++ b/jme3-core/src/main/java/com/jme3/audio/Filter.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2020 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -35,9 +35,12 @@
import com.jme3.export.JmeImporter;
import com.jme3.export.Savable;
import com.jme3.util.NativeObject;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
+
import java.io.IOException;
-public abstract class Filter extends NativeObject implements Savable {
+public abstract class Filter extends NativeObject implements Savable, JmeCloneable {
public Filter() {
super();
@@ -49,12 +52,28 @@ protected Filter(int id) {
@Override
public void write(JmeExporter ex) throws IOException {
- // nothing to save
+ // no-op
}
@Override
public void read(JmeImporter im) throws IOException {
- // nothing to read
+ // no-op
+ }
+
+ /**
+ * Called internally by com.jme3.util.clone.Cloner. Do not call directly.
+ */
+ @Override
+ public Object jmeClone() {
+ return super.clone();
+ }
+
+ /**
+ * Called internally by com.jme3.util.clone.Cloner. Do not call directly.
+ */
+ @Override
+ public void cloneFields(Cloner cloner, Object original) {
+ // no-op
}
@Override
diff --git a/jme3-core/src/main/java/com/jme3/audio/HighPassFilter.java b/jme3-core/src/main/java/com/jme3/audio/HighPassFilter.java
new file mode 100644
index 0000000000..f3838abc0e
--- /dev/null
+++ b/jme3-core/src/main/java/com/jme3/audio/HighPassFilter.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (c) 2009-2025 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.audio;
+
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.util.NativeObject;
+
+import java.io.IOException;
+
+/**
+ * Represents an OpenAL EFX High-Pass Filter.
+ */
+public class HighPassFilter extends Filter {
+
+ // Default values based on OpenAL EFX specification defaults
+ protected float volume = 1.0f;
+ protected float lowFreqVolume = 1.0f;
+
+ /**
+ * Constructs a high-pass filter with default settings.
+ * Required for jME deserialization
+ */
+ public HighPassFilter(){}
+
+ protected HighPassFilter(int id) {
+ super(id);
+ }
+
+ public HighPassFilter(float volume, float lowFreqVolume) {
+ super();
+ setVolume(volume);
+ setLowFreqVolume(lowFreqVolume);
+ }
+
+ public float getVolume() {
+ return volume;
+ }
+
+ /**
+ * Sets the gain of the High-Pass filter.
+ *
+ * @param volume The gain value (0.0 to 1.0).
+ */
+ public void setVolume(float volume) {
+ if (volume < 0 || volume > 1)
+ throw new IllegalArgumentException("Volume must be between 0 and 1");
+
+ this.volume = volume;
+ this.updateNeeded = true;
+ }
+
+ public float getLowFreqVolume() {
+ return lowFreqVolume;
+ }
+
+ /**
+ * Sets the gain at low frequencies for the High-Pass filter.
+ *
+ * @param lowFreqVolume The low-frequency gain value (0.0 to 1.0).
+ */
+ public void setLowFreqVolume(float lowFreqVolume) {
+ if (lowFreqVolume < 0 || lowFreqVolume > 1)
+ throw new IllegalArgumentException("Low freq volume must be between 0 and 1");
+
+ this.lowFreqVolume = lowFreqVolume;
+ this.updateNeeded = true;
+ }
+
+ @Override
+ public NativeObject createDestructableClone() {
+ return new HighPassFilter(this.id);
+ }
+
+ /**
+ * Retrieves a unique identifier for this filter. Used internally for native object management.
+ *
+ * @return a unique long identifier.
+ */
+ @Override
+ public long getUniqueId() {
+ return ((long) OBJTYPE_FILTER << 32) | (0xffffffffL & (long) id);
+ }
+
+ @Override
+ public void write(JmeExporter ex) throws IOException {
+ super.write(ex);
+ OutputCapsule oc = ex.getCapsule(this);
+ oc.write(this.volume, "volume", 1f);
+ oc.write(this.lowFreqVolume, "lf_volume", 1f);
+ }
+
+ @Override
+ public void read(JmeImporter im) throws IOException {
+ super.read(im);
+ InputCapsule ic = im.getCapsule(this);
+ this.volume = ic.readFloat("volume", 1f);
+ this.lowFreqVolume = ic.readFloat("lf_volume", 1f);
+ }
+}
diff --git a/jme3-core/src/main/java/com/jme3/audio/Listener.java b/jme3-core/src/main/java/com/jme3/audio/Listener.java
index 5ef28b4f09..d582df33cd 100644
--- a/jme3-core/src/main/java/com/jme3/audio/Listener.java
+++ b/jme3-core/src/main/java/com/jme3/audio/Listener.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -34,6 +34,11 @@
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
+/**
+ * Represents the audio listener in the 3D sound scene.
+ * The listener defines the point of view from which sound is heard,
+ * influencing spatial audio effects like panning and Doppler shift.
+ */
public class Listener {
private final Vector3f location;
@@ -42,72 +47,159 @@ public class Listener {
private float volume = 1;
private AudioRenderer renderer;
+ /**
+ * Constructs a new {@code Listener} with default parameters.
+ */
public Listener() {
location = new Vector3f();
velocity = new Vector3f();
rotation = new Quaternion();
}
+ /**
+ * Constructs a new {@code Listener} by copying the properties of another {@code Listener}.
+ *
+ * @param source The {@code Listener} to copy the properties from.
+ */
public Listener(Listener source) {
- location = source.location.clone();
- velocity = source.velocity.clone();
- rotation = source.rotation.clone();
- volume = source.volume;
+ this.location = source.location.clone();
+ this.velocity = source.velocity.clone();
+ this.rotation = source.rotation.clone();
+ this.volume = source.volume;
+ this.renderer = source.renderer; // Note: Renderer is also copied
}
+ /**
+ * Sets the {@link AudioRenderer} associated with this listener.
+ * The renderer is responsible for applying the listener's parameters
+ * to the audio output.
+ *
+ * @param renderer The {@link AudioRenderer} to associate with.
+ */
public void setRenderer(AudioRenderer renderer) {
this.renderer = renderer;
}
+ /**
+ * Gets the current volume of the listener.
+ *
+ * @return The current volume.
+ */
public float getVolume() {
return volume;
}
+ /**
+ * Sets the volume of the listener.
+ * If an {@link AudioRenderer} is set, it will be notified of the volume change.
+ *
+ * @param volume The new volume.
+ */
public void setVolume(float volume) {
this.volume = volume;
- if (renderer != null)
- renderer.updateListenerParam(this, ListenerParam.Volume);
+ updateListenerParam(ListenerParam.Volume);
}
+ /**
+ * Gets the current location of the listener in world space.
+ *
+ * @return The listener's location as a {@link Vector3f}.
+ */
public Vector3f getLocation() {
return location;
}
+ /**
+ * Gets the current rotation of the listener in world space.
+ *
+ * @return The listener's rotation as a {@link Quaternion}.
+ */
public Quaternion getRotation() {
return rotation;
}
+ /**
+ * Gets the current velocity of the listener.
+ * This is used for Doppler effect calculations.
+ *
+ * @return The listener's velocity as a {@link Vector3f}.
+ */
public Vector3f getVelocity() {
return velocity;
}
+ /**
+ * Gets the left direction vector of the listener.
+ * This vector is derived from the listener's rotation.
+ *
+ * @return The listener's left direction as a {@link Vector3f}.
+ */
public Vector3f getLeft() {
return rotation.getRotationColumn(0);
}
+ /**
+ * Gets the up direction vector of the listener.
+ * This vector is derived from the listener's rotation.
+ *
+ * @return The listener's up direction as a {@link Vector3f}.
+ */
public Vector3f getUp() {
return rotation.getRotationColumn(1);
}
+ /**
+ * Gets the forward direction vector of the listener.
+ * This vector is derived from the listener's rotation.
+ *
+ * @return The listener's forward direction.
+ */
public Vector3f getDirection() {
return rotation.getRotationColumn(2);
}
+ /**
+ * Sets the location of the listener in world space.
+ * If an {@link AudioRenderer} is set, it will be notified of the position change.
+ *
+ * @param location The new location of the listener.
+ */
public void setLocation(Vector3f location) {
this.location.set(location);
- if (renderer != null)
- renderer.updateListenerParam(this, ListenerParam.Position);
+ updateListenerParam(ListenerParam.Position);
}
+ /**
+ * Sets the rotation of the listener in world space.
+ * If an {@link AudioRenderer} is set, it will be notified of the rotation change.
+ *
+ * @param rotation The new rotation of the listener.
+ */
public void setRotation(Quaternion rotation) {
this.rotation.set(rotation);
- if (renderer != null)
- renderer.updateListenerParam(this, ListenerParam.Rotation);
+ updateListenerParam(ListenerParam.Rotation);
}
+ /**
+ * Sets the velocity of the listener.
+ * This is used for Doppler effect calculations.
+ * If an {@link AudioRenderer} is set, it will be notified of the velocity change.
+ *
+ * @param velocity The new velocity of the listener.
+ */
public void setVelocity(Vector3f velocity) {
this.velocity.set(velocity);
- if (renderer != null)
- renderer.updateListenerParam(this, ListenerParam.Velocity);
+ updateListenerParam(ListenerParam.Velocity);
+ }
+
+ /**
+ * Updates the associated {@link AudioRenderer} with the specified listener parameter.
+ *
+ * @param param The {@link ListenerParam} to update on the renderer.
+ */
+ private void updateListenerParam(ListenerParam param) {
+ if (renderer != null) {
+ renderer.updateListenerParam(this, param);
+ }
}
}
diff --git a/jme3-core/src/main/java/com/jme3/audio/LowPassFilter.java b/jme3-core/src/main/java/com/jme3/audio/LowPassFilter.java
index aaf1f5b69b..8c70490ba6 100644
--- a/jme3-core/src/main/java/com/jme3/audio/LowPassFilter.java
+++ b/jme3-core/src/main/java/com/jme3/audio/LowPassFilter.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2020 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -36,26 +36,71 @@
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.util.NativeObject;
+
import java.io.IOException;
+/**
+ * A filter that attenuates frequencies above a specified threshold, allowing lower
+ * frequencies to pass through with less attenuation. Commonly used to simulate effects
+ * such as muffling or underwater acoustics.
+ */
public class LowPassFilter extends Filter {
- protected float volume, highFreqVolume;
+ /**
+ * The overall volume scaling of the filtered sound
+ */
+ protected float volume = 1.0f;
+ /**
+ * The volume scaling of the high frequencies allowed to pass through. Valid values range
+ * from 0.0 to 1.0, where 0.0 completely eliminates high frequencies and 1.0 lets them pass
+ * through unchanged.
+ */
+ protected float highFreqVolume = 1.0f;
+
+ /**
+ * Constructs a low-pass filter with default settings.
+ * Required for jME deserialization.
+ */
+ public LowPassFilter() {
+ super();
+ }
+ /**
+ * Constructs a low-pass filter.
+ *
+ * @param volume the overall volume scaling of the filtered sound (0.0 - 1.0).
+ * @param highFreqVolume the volume scaling of high frequencies (0.0 - 1.0).
+ * @throws IllegalArgumentException if {@code volume} or {@code highFreqVolume} is out of range.
+ */
public LowPassFilter(float volume, float highFreqVolume) {
super();
setVolume(volume);
setHighFreqVolume(highFreqVolume);
}
+ /**
+ * For internal cloning
+ * @param id the native object ID
+ */
protected LowPassFilter(int id) {
super(id);
}
+ /**
+ * Retrieves the current volume scaling of high frequencies.
+ *
+ * @return the high-frequency volume scaling.
+ */
public float getHighFreqVolume() {
return highFreqVolume;
}
+ /**
+ * Sets the high-frequency volume.
+ *
+ * @param highFreqVolume the new high-frequency volume scaling (0.0 - 1.0).
+ * @throws IllegalArgumentException if {@code highFreqVolume} is out of range.
+ */
public void setHighFreqVolume(float highFreqVolume) {
if (highFreqVolume < 0 || highFreqVolume > 1)
throw new IllegalArgumentException("High freq volume must be between 0 and 1");
@@ -64,10 +109,21 @@ public void setHighFreqVolume(float highFreqVolume) {
this.updateNeeded = true;
}
+ /**
+ * Retrieves the current overall volume scaling of the filtered sound.
+ *
+ * @return the overall volume scaling.
+ */
public float getVolume() {
return volume;
}
+ /**
+ * Sets the overall volume.
+ *
+ * @param volume the new overall volume scaling (0.0 - 1.0).
+ * @throws IllegalArgumentException if {@code volume} is out of range.
+ */
public void setVolume(float volume) {
if (volume < 0 || volume > 1)
throw new IllegalArgumentException("Volume must be between 0 and 1");
@@ -80,25 +136,35 @@ public void setVolume(float volume) {
public void write(JmeExporter ex) throws IOException {
super.write(ex);
OutputCapsule oc = ex.getCapsule(this);
- oc.write(volume, "volume", 0);
- oc.write(highFreqVolume, "hf_volume", 0);
+ oc.write(volume, "volume", 1f);
+ oc.write(highFreqVolume, "hf_volume", 1f);
}
@Override
public void read(JmeImporter im) throws IOException {
super.read(im);
InputCapsule ic = im.getCapsule(this);
- volume = ic.readFloat("volume", 0);
- highFreqVolume = ic.readFloat("hf_volume", 0);
+ volume = ic.readFloat("volume", 1f);
+ highFreqVolume = ic.readFloat("hf_volume", 1f);
}
+ /**
+ * Creates a native object clone of this filter for internal usage.
+ *
+ * @return a new {@code LowPassFilter} instance with the same native ID.
+ */
@Override
public NativeObject createDestructableClone() {
return new LowPassFilter(id);
}
+ /**
+ * Retrieves a unique identifier for this filter. Used internally for native object management.
+ *
+ * @return a unique long identifier.
+ */
@Override
public long getUniqueId() {
- return ((long) OBJTYPE_FILTER << 32) | ((long) id);
+ return ((long) OBJTYPE_FILTER << 32) | (0xffffffffL & (long) id);
}
}
diff --git a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java
index 834d80eca1..ec89b9df06 100644
--- a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java
+++ b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2022 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -31,19 +31,36 @@
*/
package com.jme3.audio.openal;
-import com.jme3.audio.*;
+import com.jme3.audio.AudioBuffer;
+import com.jme3.audio.AudioData;
+import com.jme3.audio.AudioParam;
+import com.jme3.audio.AudioRenderer;
+import com.jme3.audio.AudioSource;
import com.jme3.audio.AudioSource.Status;
import static com.jme3.audio.openal.AL.*;
+
+import com.jme3.audio.AudioStream;
+import com.jme3.audio.BandPassFilter;
+import com.jme3.audio.Environment;
+import com.jme3.audio.Filter;
+import com.jme3.audio.HighPassFilter;
+import com.jme3.audio.Listener;
+import com.jme3.audio.ListenerParam;
+import com.jme3.audio.LowPassFilter;
import com.jme3.math.Vector3f;
import com.jme3.util.BufferUtils;
import com.jme3.util.NativeObjectManager;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
+/**
+ * ALAudioRenderer is the backend implementation for OpenAL audio rendering.
+ */
public class ALAudioRenderer implements AudioRenderer, Runnable {
private static final Logger logger = Logger.getLogger(ALAudioRenderer.class.getName());
@@ -55,79 +72,97 @@ 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 IntBuffer ib = BufferUtils.createIntBuffer(1);
- private final FloatBuffer fb = BufferUtils.createVector3Buffer(2);
- private final ByteBuffer nativeBuf = BufferUtils.createByteBuffer(BUFFER_SIZE);
- private final byte[] arrayBuf = new byte[BUFFER_SIZE];
- private int[] channels;
- private AudioSource[] channelSources;
- private int nextChan = 0;
- private final ArrayList freeChannels = new ArrayList<>();
+ private static final int MAX_NUM_CHANNELS = 64;
+
+ // Buffers for OpenAL calls
+ private IntBuffer ib = BufferUtils.createIntBuffer(1); // Reused for single int operations
+ private final FloatBuffer fb = BufferUtils.createVector3Buffer(2); // For listener orientation
+ private final ByteBuffer nativeBuf = BufferUtils.createByteBuffer(BUFFER_SIZE); // For streaming data
+ private final byte[] arrayBuf = new byte[BUFFER_SIZE]; // Intermediate array buffer for streaming
+
+ // Channel management
+ private int[] channels; // OpenAL source IDs
+ private AudioSource[] channelSources; // jME source associated with each channel
+ private int nextChannelIndex = 0; // Next available channel index
+ private final ArrayDeque freeChannels = new ArrayDeque<>(); // Pool of freed channels
+
+ // Listener and environment
private Listener listener;
+ private Environment environment;
+ private int reverbFx = -1; // EFX reverb effect ID
+ private int reverbFxSlot = -1; // EFX effect slot ID
+
+ // State and capabilities
private boolean audioDisabled = false;
private boolean supportEfx = false;
private boolean supportPauseDevice = false;
private boolean supportDisconnect = false;
- private int auxSends = 0;
- private int reverbFx = -1;
- private int reverbFxSlot = -1;
- // Fill streaming sources every 50 ms
- private static final float UPDATE_RATE = 0.05f;
+ // Update thread
+ private static final float UPDATE_RATE = 0.05f; // Update streaming sources every 50ms
private final Thread decoderThread = new Thread(this, THREAD_NAME);
- private final Object threadLock = new Object();
+ private final Object threadLock = new Object(); // Lock for thread safety
+ // OpenAL API interfaces
private final AL al;
private final ALC alc;
private final EFX efx;
+ /**
+ * Creates a new ALAudioRenderer instance.
+ *
+ * @param al The OpenAL interface.
+ * @param alc The OpenAL Context interface.
+ * @param efx The OpenAL Effects Extension interface.
+ */
public ALAudioRenderer(AL al, ALC alc, EFX efx) {
this.al = al;
this.alc = alc;
this.efx = efx;
}
+ /**
+ * Initializes the OpenAL and ALC context.
+ */
private void initOpenAL() {
try {
if (!alc.isCreated()) {
alc.createALC();
}
} catch (UnsatisfiedLinkError ex) {
- logger.log(Level.SEVERE, "Failed to load audio library", ex);
+ logger.log(Level.SEVERE, "Failed to load audio library (OpenAL). Audio will be disabled.", ex);
audioDisabled = true;
return;
}
- // Find maximum # of sources supported by this implementation
- ArrayList channelList = new ArrayList<>();
- for (int i = 0; i < MAX_NUM_CHANNELS; i++) {
- int chan = al.alGenSources();
- if (al.alGetError() != 0) {
- break;
- } else {
- channelList.add(chan);
- }
- }
+ enumerateAvailableChannels();
- channels = new int[channelList.size()];
- for (int i = 0; i < channels.length; i++) {
- channels[i] = channelList.get(i);
+ printAudioRendererInfo();
+
+ // Check for specific ALC extensions
+ supportPauseDevice = alc.alcIsExtensionPresent("ALC_SOFT_pause_device");
+ if (!supportPauseDevice) {
+ logger.log(Level.WARNING, "Pausing audio device not supported (ALC_SOFT_pause_device).");
+ }
+ supportDisconnect = alc.alcIsExtensionPresent("ALC_EXT_disconnect");
+ if (!supportDisconnect) {
+ logger.log(Level.INFO, "Device disconnect detection not supported (ALC_EXT_disconnect).");
}
- ib = BufferUtils.createIntBuffer(channels.length);
- channelSources = new AudioSource[channels.length];
+ initEfx();
+ }
+ private void printAudioRendererInfo() {
final String deviceName = alc.alcGetString(ALC.ALC_DEVICE_SPECIFIER);
logger.log(Level.INFO, "Audio Renderer Information\n"
- + " * Device: {0}\n"
- + " * Vendor: {1}\n"
- + " * Renderer: {2}\n"
- + " * Version: {3}\n"
- + " * Supported channels: {4}\n"
- + " * ALC extensions: {5}\n"
- + " * AL extensions: {6}",
+ + " * Device: {0}\n"
+ + " * Vendor: {1}\n"
+ + " * Renderer: {2}\n"
+ + " * Version: {3}\n"
+ + " * Supported channels: {4}\n"
+ + " * ALC extensions: {5}\n"
+ + " * AL extensions: {6}",
new Object[] {
deviceName,
al.alGetString(AL_VENDOR),
@@ -137,58 +172,82 @@ private void initOpenAL() {
alc.alcGetString(ALC.ALC_EXTENSIONS),
al.alGetString(AL_EXTENSIONS)
});
+ }
- // Pause device is a feature used specifically on Android
- // where the application could be closed but still running,
- // thus the audio context remains open but no audio should be playing.
- supportPauseDevice = alc.alcIsExtensionPresent("ALC_SOFT_pause_device");
- if (!supportPauseDevice) {
- logger.log(Level.WARNING, "Pausing audio device not supported.");
+ /**
+ * Generates OpenAL sources to determine the maximum number supported.
+ */
+ private void enumerateAvailableChannels() {
+ // Find maximum # of sources supported by this implementation
+ ArrayList channelList = new ArrayList<>();
+ for (int i = 0; i < MAX_NUM_CHANNELS; i++) {
+ int sourceId = al.alGenSources();
+ if (al.alGetError() != 0) {
+ break;
+ } else {
+ channelList.add(sourceId);
+ }
}
- // Disconnected audio devices (such as USB sound cards, headphones...)
- // never reconnect, the whole context must be re-created
- supportDisconnect = alc.alcIsExtensionPresent("ALC_EXT_disconnect");
+ channels = new int[channelList.size()];
+ for (int i = 0; i < channels.length; i++) {
+ channels[i] = channelList.get(i);
+ }
+
+ ib = BufferUtils.createIntBuffer(channels.length);
+ channelSources = new AudioSource[channels.length];
+ }
+ /**
+ * Initializes the EFX extension if supported.
+ */
+ private void initEfx() {
supportEfx = alc.alcIsExtensionPresent("ALC_EXT_EFX");
if (supportEfx) {
- ib.position(0).limit(1);
+ ib.clear().limit(1);
alc.alcGetInteger(EFX.ALC_EFX_MAJOR_VERSION, ib, 1);
- int major = ib.get(0);
- ib.position(0).limit(1);
+ int majorVersion = ib.get(0);
+
+ ib.clear().limit(1);
alc.alcGetInteger(EFX.ALC_EFX_MINOR_VERSION, ib, 1);
- int minor = ib.get(0);
- logger.log(Level.INFO, "Audio effect extension version: {0}.{1}", new Object[]{major, minor});
+ int minorVersion = ib.get(0);
+ logger.log(Level.INFO, "Audio effect extension version: {0}.{1}", new Object[]{majorVersion, minorVersion});
+ ib.clear().limit(1);
alc.alcGetInteger(EFX.ALC_MAX_AUXILIARY_SENDS, ib, 1);
- auxSends = ib.get(0);
- logger.log(Level.INFO, "Audio max auxiliary sends: {0}", auxSends);
+ int maxAuxSends = ib.get(0);
+ logger.log(Level.INFO, "Audio max auxiliary sends: {0}", maxAuxSends);
- // create slot
- ib.position(0).limit(1);
+ // 1. Create reverb effect slot
+ ib.clear().limit(1);
efx.alGenAuxiliaryEffectSlots(1, ib);
reverbFxSlot = ib.get(0);
- // create effect
- ib.position(0).limit(1);
+ // 2. Create reverb effect
+ ib.clear().limit(1);
efx.alGenEffects(1, ib);
reverbFx = ib.get(0);
+
+ // 3. Configure effect type
efx.alEffecti(reverbFx, EFX.AL_EFFECT_TYPE, EFX.AL_EFFECT_REVERB);
- // attach reverb effect to effect slot
+ // 4. attach reverb effect to effect slot
efx.alAuxiliaryEffectSloti(reverbFxSlot, EFX.AL_EFFECTSLOT_EFFECT, reverbFx);
+
} else {
logger.log(Level.WARNING, "OpenAL EFX not available! Audio effects won't work.");
}
}
+ /**
+ * Destroys the OpenAL context, deleting sources, buffers, filters, and effects.
+ */
private void destroyOpenAL() {
if (audioDisabled) {
- alc.destroyALC();
- return;
+ return; // Nothing to destroy if context wasn't created
}
- // stop any playing channels
+ // Stops channels and detaches buffers/filters
for (int i = 0; i < channelSources.length; i++) {
if (channelSources[i] != null) {
clearChannel(i);
@@ -201,24 +260,39 @@ private void destroyOpenAL() {
ib.flip();
al.alDeleteSources(channels.length, ib);
- // delete audio buffers and filters
+ // Delete audio buffers and filters managed by NativeObjectManager
objManager.deleteAllObjects(this);
+ // Delete EFX objects if they were created
if (supportEfx) {
- ib.position(0).limit(1);
- ib.put(0, reverbFx);
- efx.alDeleteEffects(1, ib);
-
- // If this is not allocated, why is it deleted?
- // Commented out to fix native crash in OpenAL.
- ib.position(0).limit(1);
- ib.put(0, reverbFxSlot);
- efx.alDeleteAuxiliaryEffectSlots(1, ib);
+ if (reverbFx != -1) {
+ ib.clear().limit(1);
+ ib.put(0, reverbFx);
+ efx.alDeleteEffects(1, ib);
+ reverbFx = -1;
+ }
+
+ if (reverbFxSlot != -1) {
+ ib.clear().limit(1);
+ ib.put(0, reverbFxSlot);
+ efx.alDeleteAuxiliaryEffectSlots(1, ib);
+ reverbFxSlot = -1;
+ }
}
+ channels = null; // Force re-enumeration
+ channelSources = null;
+ freeChannels.clear();
+ nextChannelIndex = 0;
+
alc.destroyALC();
+ logger.info("OpenAL context destroyed.");
}
+ /**
+ * Initializes the OpenAL context, enumerates channels, checks capabilities,
+ * and starts the audio decoder thread.
+ */
@Override
public void initialize() {
if (decoderThread.isAlive()) {
@@ -228,6 +302,11 @@ public void initialize() {
// Initialize OpenAL context.
initOpenAL();
+ if (audioDisabled) {
+ logger.warning("Audio Disabled. Cannot start decoder thread.");
+ return;
+ }
+
// Initialize decoder thread.
// Set high priority to avoid buffer starvation.
decoderThread.setDaemon(true);
@@ -235,20 +314,28 @@ public void initialize() {
decoderThread.start();
}
+ /**
+ * Checks if the audio thread has terminated unexpectedly.
+ * @throws IllegalStateException if the decoding thread is terminated.
+ */
private void checkDead() {
if (decoderThread.getState() == Thread.State.TERMINATED) {
throw new IllegalStateException("Decoding thread is terminated");
}
}
+ /**
+ * Main loop for the audio decoder thread. Updates streaming sources.
+ */
@Override
public void run() {
- long updateRateNanos = (long) (UPDATE_RATE * 1000000000);
+ long updateRateNanos = (long) (UPDATE_RATE * 1_000_000_000);
mainloop:
while (true) {
long startTime = System.nanoTime();
if (Thread.interrupted()) {
+ logger.fine("Audio decoder thread interrupted, exiting.");
break;
}
@@ -260,40 +347,52 @@ public void run() {
long endTime = System.nanoTime();
long diffTime = endTime - startTime;
+ // Sleep to maintain the desired update rate
if (diffTime < updateRateNanos) {
long desiredEndTime = startTime + updateRateNanos;
while (System.nanoTime() < desiredEndTime) {
try {
Thread.sleep(1);
} catch (InterruptedException ex) {
+ logger.fine("Audio decoder thread interrupted during sleep, exiting.");
break mainloop;
}
}
}
}
+ logger.fine("Audio decoder thread finished.");
}
+ /**
+ * Shuts down the audio decoder thread and destroys the OpenAL context.
+ */
@Override
public void cleanup() {
// kill audio thread
- if (!decoderThread.isAlive()) {
- return;
- }
-
- decoderThread.interrupt();
- try {
- decoderThread.join();
- } catch (InterruptedException ex) {
+ if (decoderThread.isAlive()) {
+ decoderThread.interrupt();
+ try {
+ decoderThread.join();
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt(); // Re-interrupt thread
+ logger.log(Level.WARNING, "Interrupted while waiting for audio thread to finish.", ex);
+ }
}
- // destroy OpenAL context
+ // Destroy OpenAL context
destroyOpenAL();
}
+ /**
+ * Updates an OpenAL filter object based on the jME Filter properties.
+ * Generates the AL filter ID if necessary.
+ * @param f The Filter object.
+ */
private void updateFilter(Filter f) {
int id = f.getId();
if (id == -1) {
- ib.position(0).limit(1);
+ // Generate OpenAL filter ID
+ ib.clear().limit(1);
efx.alGenFilters(1, ib);
id = ib.get(0);
f.setId(id);
@@ -302,18 +401,37 @@ private void updateFilter(Filter f) {
}
if (f instanceof LowPassFilter) {
- LowPassFilter lpf = (LowPassFilter) f;
+ LowPassFilter lowPass = (LowPassFilter) f;
efx.alFilteri(id, EFX.AL_FILTER_TYPE, EFX.AL_FILTER_LOWPASS);
- efx.alFilterf(id, EFX.AL_LOWPASS_GAIN, lpf.getVolume());
- efx.alFilterf(id, EFX.AL_LOWPASS_GAINHF, lpf.getHighFreqVolume());
+ efx.alFilterf(id, EFX.AL_LOWPASS_GAIN, lowPass.getVolume());
+ efx.alFilterf(id, EFX.AL_LOWPASS_GAINHF, lowPass.getHighFreqVolume());
+
+ } else if (f instanceof HighPassFilter) {
+ HighPassFilter highPass = (HighPassFilter) f;
+ efx.alFilteri(id, EFX.AL_FILTER_TYPE, EFX.AL_FILTER_HIGHPASS);
+ efx.alFilterf(id, EFX.AL_HIGHPASS_GAIN, highPass.getVolume());
+ efx.alFilterf(id, EFX.AL_HIGHPASS_GAINLF, highPass.getLowFreqVolume());
+
+ } else if (f instanceof BandPassFilter) {
+ BandPassFilter bandPass = (BandPassFilter) f;
+ efx.alFilteri(id, EFX.AL_FILTER_TYPE, EFX.AL_FILTER_BANDPASS);
+ efx.alFilterf(id, EFX.AL_BANDPASS_GAIN, bandPass.getVolume());
+ efx.alFilterf(id, EFX.AL_BANDPASS_GAINHF, bandPass.getHighFreqVolume());
+ efx.alFilterf(id, EFX.AL_BANDPASS_GAINLF, bandPass.getLowFreqVolume());
+
} else {
- throw new UnsupportedOperationException("Filter type unsupported: "
- + f.getClass().getName());
+ throw new UnsupportedOperationException("Unsupported filter type: " + f.getClass().getName());
}
f.clearUpdateNeeded();
}
+ /**
+ * Gets the current playback time (in seconds) for a source.
+ * For streams, includes the time represented by already processed buffers.
+ * @param src The audio source.
+ * @return The playback time in seconds, or 0 if not playing or invalid.
+ */
@Override
public float getSourcePlaybackTime(AudioSource src) {
checkDead();
@@ -322,38 +440,27 @@ public float getSourcePlaybackTime(AudioSource src) {
return 0;
}
- // See comment in updateSourceParam().
if (src.getChannel() < 0) {
- return 0;
+ return 0; // Not playing or invalid state
}
- int id = channels[src.getChannel()];
+ int sourceId = channels[src.getChannel()];
AudioData data = src.getAudioData();
+ if (data == null) {
+ return 0; // No audio data
+ }
int playbackOffsetBytes = 0;
+ // For streams, add the bytes from buffers that have already been fully processed and unqueued.
if (data instanceof AudioStream) {
- // Because audio streams are processed in buffer chunks,
- // we have to compute the amount of time the stream was already
- // been playing based on the number of buffers that were processed.
AudioStream stream = (AudioStream) data;
-
- // NOTE: the assumption is that all enqueued buffers are the same size.
- // this is currently enforced by fillBuffer().
-
- // The number of unenqueued bytes that the decoder thread
- // keeps track of.
- int unqueuedBytes = stream.getUnqueuedBufferBytes();
-
- // Additional processed buffers that the decoder thread
- // did not unenqueue yet (it only updates 20 times per second).
- int unqueuedBytesExtra = al.alGetSourcei(id, AL_BUFFERS_PROCESSED) * BUFFER_SIZE;
-
- // Total additional bytes that need to be considered.
- playbackOffsetBytes = unqueuedBytes; // + unqueuedBytesExtra;
+ // This value is updated by the decoder thread when buffers are unqueued.
+ playbackOffsetBytes = stream.getUnqueuedBufferBytes();
}
// Add byte offset from source (for both streams and buffers)
- playbackOffsetBytes += al.alGetSourcei(id, AL_BYTE_OFFSET);
+ int byteOffset = al.alGetSourcei(sourceId, AL_BYTE_OFFSET);
+ playbackOffsetBytes += byteOffset;
// Compute time value from bytes
// E.g. for 44100 source with 2 channels and 16 bits per sample:
@@ -362,10 +469,20 @@ public float getSourcePlaybackTime(AudioSource src) {
* data.getChannels()
* data.getBitsPerSample() / 8);
+ if (bytesPerSecond <= 0) {
+ logger.warning("Invalid bytesPerSecond calculated for source. Cannot get playback time.");
+ return 0; // Avoid division by zero
+ }
+
return (float) playbackOffsetBytes / bytesPerSecond;
}
}
+ /**
+ * Updates a specific parameter for an audio source on its assigned channel.
+ * @param src The audio source.
+ * @param param The parameter to update.
+ */
@Override
public void updateSourceParam(AudioSource src, AudioParam param) {
checkDead();
@@ -374,228 +491,237 @@ public void updateSourceParam(AudioSource src, AudioParam param) {
return;
}
- // There is a race condition in AudioSource that can
- // cause this to be called for a node that has been
- // detached from its channel. For example, setVolume()
- // called from the render thread may see that the AudioSource
- // still has a channel value but the audio thread may
- // clear that channel before setVolume() gets to call
- // updateSourceParam() (because the audio stopped playing
- // on its own right as the volume was set). In this case,
- // it should be safe to just ignore the update.
- if (src.getChannel() < 0) {
+ int channel = src.getChannel();
+ // Parameter updates only make sense if the source is associated with a channel
+ // and hasn't been stopped (which would set channel to -1).
+ if (channel < 0) {
+ // This can happen due to race conditions if a source stops playing
+ // right as a parameter update is requested from another thread.
+ if (logger.isLoggable(Level.FINE)) {
+ logger.log(Level.FINE, "Ignoring parameter update for source {0} as it's not validly associated with channel {1}.",
+ new Object[]{src, channel});
+ }
return;
}
- assert src.getChannel() >= 0;
+ int sourceId = channels[channel];
- int id = channels[src.getChannel()];
switch (param) {
case Position:
- if (!src.isPositional()) {
- return;
+ if (src.isPositional()) {
+ Vector3f pos = src.getPosition();
+ al.alSource3f(sourceId, AL_POSITION, pos.x, pos.y, pos.z);
}
-
- Vector3f pos = src.getPosition();
- al.alSource3f(id, AL_POSITION, pos.x, pos.y, pos.z);
break;
+
case Velocity:
- if (!src.isPositional()) {
- return;
+ if (src.isPositional()) {
+ Vector3f vel = src.getVelocity();
+ al.alSource3f(sourceId, AL_VELOCITY, vel.x, vel.y, vel.z);
}
-
- Vector3f vel = src.getVelocity();
- al.alSource3f(id, AL_VELOCITY, vel.x, vel.y, vel.z);
break;
- case MaxDistance:
- if (!src.isPositional()) {
- return;
- }
- al.alSourcef(id, AL_MAX_DISTANCE, src.getMaxDistance());
- break;
- case RefDistance:
- if (!src.isPositional()) {
- return;
+ case MaxDistance:
+ if (src.isPositional()) {
+ al.alSourcef(sourceId, AL_MAX_DISTANCE, src.getMaxDistance());
}
-
- al.alSourcef(id, AL_REFERENCE_DISTANCE, src.getRefDistance());
break;
- case ReverbFilter:
- if (!supportEfx || !src.isPositional() || !src.isReverbEnabled()) {
- return;
- }
- int filter = EFX.AL_FILTER_NULL;
- if (src.getReverbFilter() != null) {
- Filter f = src.getReverbFilter();
- if (f.isUpdateNeeded()) {
- updateFilter(f);
- }
- filter = f.getId();
+ case RefDistance:
+ if (src.isPositional()) {
+ al.alSourcef(sourceId, AL_REFERENCE_DISTANCE, src.getRefDistance());
}
- al.alSource3i(id, EFX.AL_AUXILIARY_SEND_FILTER, reverbFxSlot, 0, filter);
break;
- case ReverbEnabled:
- if (!supportEfx || !src.isPositional()) {
- return;
- }
- if (src.isReverbEnabled()) {
- updateSourceParam(src, AudioParam.ReverbFilter);
- } else {
- al.alSource3i(id, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL);
- }
- break;
case IsPositional:
- if (!src.isPositional()) {
- // Play in headspace
- al.alSourcei(id, AL_SOURCE_RELATIVE, AL_TRUE);
- al.alSource3f(id, AL_POSITION, 0, 0, 0);
- al.alSource3f(id, AL_VELOCITY, 0, 0, 0);
-
- // Disable reverb
- al.alSource3i(id, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL);
- } else {
- al.alSourcei(id, AL_SOURCE_RELATIVE, AL_FALSE);
- updateSourceParam(src, AudioParam.Position);
- updateSourceParam(src, AudioParam.Velocity);
- updateSourceParam(src, AudioParam.MaxDistance);
- updateSourceParam(src, AudioParam.RefDistance);
- updateSourceParam(src, AudioParam.ReverbEnabled);
- }
+ applySourcePositionalState(sourceId, src);
break;
+
case Direction:
- if (!src.isDirectional()) {
- return;
+ if (src.isDirectional()) {
+ Vector3f dir = src.getDirection();
+ al.alSource3f(sourceId, AL_DIRECTION, dir.x, dir.y, dir.z);
}
-
- Vector3f dir = src.getDirection();
- al.alSource3f(id, AL_DIRECTION, dir.x, dir.y, dir.z);
break;
+
case InnerAngle:
- if (!src.isDirectional()) {
- return;
+ if (src.isDirectional()) {
+ al.alSourcef(sourceId, AL_CONE_INNER_ANGLE, src.getInnerAngle());
}
-
- al.alSourcef(id, AL_CONE_INNER_ANGLE, src.getInnerAngle());
break;
+
case OuterAngle:
- if (!src.isDirectional()) {
- return;
+ if (src.isDirectional()) {
+ al.alSourcef(sourceId, AL_CONE_OUTER_ANGLE, src.getOuterAngle());
}
-
- al.alSourcef(id, AL_CONE_OUTER_ANGLE, src.getOuterAngle());
break;
+
case IsDirectional:
- if (src.isDirectional()) {
- updateSourceParam(src, AudioParam.Direction);
- updateSourceParam(src, AudioParam.InnerAngle);
- updateSourceParam(src, AudioParam.OuterAngle);
- al.alSourcef(id, AL_CONE_OUTER_GAIN, 0);
- } else {
- al.alSourcef(id, AL_CONE_INNER_ANGLE, 360);
- al.alSourcef(id, AL_CONE_OUTER_ANGLE, 360);
- al.alSourcef(id, AL_CONE_OUTER_GAIN, 1f);
- }
+ applySourceDirectionalState(sourceId, src);
break;
+
case DryFilter:
- if (!supportEfx) {
- return;
+ applySourceDryFilter(sourceId, src);
+ break;
+
+ case ReverbFilter:
+ if (src.isPositional()) {
+ applySourceReverbFilter(sourceId, src);
}
- Filter dryFilter = src.getDryFilter();
- int filterId;
- if (dryFilter == null) {
- filterId = EFX.AL_FILTER_NULL;
- } else {
- if (dryFilter.isUpdateNeeded()) {
- updateFilter(dryFilter);
+ break;
+
+ case ReverbEnabled:
+ if (supportEfx && src.isPositional()) {
+ if (!src.isReverbEnabled()) {
+ al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL);
+ } else {
+ applySourceReverbFilter(sourceId, src);
}
- filterId = dryFilter.getId();
}
- // NOTE: must re-attach filter for changes to apply.
- al.alSourcei(id, EFX.AL_DIRECT_FILTER, filterId);
break;
+
case Looping:
- if (src.isLooping() && !(src.getAudioData() instanceof AudioStream)) {
- al.alSourcei(id, AL_LOOPING, AL_TRUE);
- } else {
- al.alSourcei(id, AL_LOOPING, AL_FALSE);
- }
+ applySourceLooping(sourceId, src, false);
break;
+
case Volume:
- al.alSourcef(id, AL_GAIN, src.getVolume());
+ al.alSourcef(sourceId, AL_GAIN, src.getVolume());
break;
+
case Pitch:
- al.alSourcef(id, AL_PITCH, src.getPitch());
+ al.alSourcef(sourceId, AL_PITCH, src.getPitch());
+ break;
+
+ default:
+ logger.log(Level.WARNING, "Unhandled source parameter update: {0}", param);
break;
}
}
}
- private void setSourceParams(int id, AudioSource src, boolean forceNonLoop) {
- if (src.isPositional()) {
- Vector3f pos = src.getPosition();
- Vector3f vel = src.getVelocity();
- al.alSource3f(id, AL_POSITION, pos.x, pos.y, pos.z);
- al.alSource3f(id, AL_VELOCITY, vel.x, vel.y, vel.z);
- al.alSourcef(id, AL_MAX_DISTANCE, src.getMaxDistance());
- al.alSourcef(id, AL_REFERENCE_DISTANCE, src.getRefDistance());
- al.alSourcei(id, AL_SOURCE_RELATIVE, AL_FALSE);
-
- if (src.isReverbEnabled() && supportEfx) {
- int filter = EFX.AL_FILTER_NULL;
- if (src.getReverbFilter() != null) {
- Filter f = src.getReverbFilter();
- if (f.isUpdateNeeded()) {
- updateFilter(f);
- }
- filter = f.getId();
+ /**
+ * Applies all parameters from the AudioSource to the specified OpenAL source ID.
+ * Used when initially playing a source or instance.
+ *
+ * @param sourceId The OpenAL source ID.
+ * @param src The jME AudioSource.
+ * @param forceNonLoop If true, looping will be disabled regardless of source setting (used for instances).
+ */
+ private void setSourceParams(int sourceId, AudioSource src, boolean forceNonLoop) {
+
+ al.alSourcef(sourceId, AL_GAIN, src.getVolume());
+ al.alSourcef(sourceId, AL_PITCH, src.getPitch());
+ al.alSourcef(sourceId, AL_SEC_OFFSET, src.getTimeOffset());
+
+ applySourceLooping(sourceId, src, forceNonLoop);
+ applySourcePositionalState(sourceId, src);
+ applySourceDirectionalState(sourceId, src);
+ applySourceDryFilter(sourceId, src);
+ }
+
+ // --- Source Parameter Helper Methods ---
+
+ private void applySourceDryFilter(int sourceId, AudioSource src) {
+ if (supportEfx) {
+ int filterId = EFX.AL_FILTER_NULL;
+ if (src.getDryFilter() != null) {
+ Filter f = src.getDryFilter();
+ if (f.isUpdateNeeded()) {
+ updateFilter(f);
}
- al.alSource3i(id, EFX.AL_AUXILIARY_SEND_FILTER, reverbFxSlot, 0, filter);
+ filterId = f.getId();
}
- } else {
- // play in headspace
- al.alSourcei(id, AL_SOURCE_RELATIVE, AL_TRUE);
- al.alSource3f(id, AL_POSITION, 0, 0, 0);
- al.alSource3f(id, AL_VELOCITY, 0, 0, 0);
+ // NOTE: must re-attach filter for changes to apply.
+ al.alSourcei(sourceId, EFX.AL_DIRECT_FILTER, filterId);
}
+ }
- if (src.getDryFilter() != null && supportEfx) {
- Filter f = src.getDryFilter();
- if (f.isUpdateNeeded()) {
- updateFilter(f);
-
- // NOTE: must re-attach filter for changes to apply.
- al.alSourcei(id, EFX.AL_DIRECT_FILTER, f.getId());
+ private void applySourceReverbFilter(int sourceId, AudioSource src) {
+ if (supportEfx) {
+ int filterId = EFX.AL_FILTER_NULL;
+ if (src.isReverbEnabled() && src.getReverbFilter() != null) {
+ Filter f = src.getReverbFilter();
+ if (f.isUpdateNeeded()) {
+ updateFilter(f);
+ }
+ filterId = f.getId();
}
+ al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, reverbFxSlot, 0, filterId);
}
+ }
+
+ private void applySourceLooping(int sourceId, AudioSource src, boolean forceNonLoop) {
+ boolean looping = !forceNonLoop && src.isLooping();
+ // Streams handle looping internally by rewinding, not via AL_LOOPING.
+ if (src.getAudioData() instanceof AudioStream) {
+ looping = false;
+ }
+ al.alSourcei(sourceId, AL_LOOPING, looping ? AL_TRUE : AL_FALSE);
+ }
- if (forceNonLoop || src.getAudioData() instanceof AudioStream) {
- al.alSourcei(id, AL_LOOPING, AL_FALSE);
+ /** Sets AL_SOURCE_RELATIVE and applies position/velocity/distance accordingly */
+ private void applySourcePositionalState(int sourceId, AudioSource src) {
+ if (src.isPositional()) {
+ // Play in world space: absolute position/velocity
+ Vector3f pos = src.getPosition();
+ Vector3f vel = src.getVelocity();
+ al.alSource3f(sourceId, AL_POSITION, pos.x, pos.y, pos.z);
+ al.alSource3f(sourceId, AL_VELOCITY, vel.x, vel.y, vel.z);
+ al.alSourcef(sourceId, AL_REFERENCE_DISTANCE, src.getRefDistance());
+ al.alSourcef(sourceId, AL_MAX_DISTANCE, src.getMaxDistance());
+ al.alSourcei(sourceId, AL_SOURCE_RELATIVE, AL_FALSE);
+
+ if (supportEfx) {
+ if (!src.isReverbEnabled()) {
+ al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL);
+ } else {
+ applySourceReverbFilter(sourceId, src);
+ }
+ }
} else {
- al.alSourcei(id, AL_LOOPING, src.isLooping() ? AL_TRUE : AL_FALSE);
+ // Play in headspace: relative to listener, fixed position/velocity
+ al.alSource3f(sourceId, AL_POSITION, 0, 0, 0);
+ al.alSource3f(sourceId, AL_VELOCITY, 0, 0, 0);
+ al.alSourcei(sourceId, AL_SOURCE_RELATIVE, AL_TRUE);
+
+ // Disable reverb send for non-positional sounds
+ if (supportEfx) {
+ al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL);
+ }
}
- al.alSourcef(id, AL_GAIN, src.getVolume());
- al.alSourcef(id, AL_PITCH, src.getPitch());
- al.alSourcef(id, AL_SEC_OFFSET, src.getTimeOffset());
+ }
+ /** Sets cone angles/gain based on whether the source is directional */
+ private void applySourceDirectionalState(int sourceId, AudioSource src) {
if (src.isDirectional()) {
Vector3f dir = src.getDirection();
- al.alSource3f(id, AL_DIRECTION, dir.x, dir.y, dir.z);
- al.alSourcef(id, AL_CONE_INNER_ANGLE, src.getInnerAngle());
- al.alSourcef(id, AL_CONE_OUTER_ANGLE, src.getOuterAngle());
- al.alSourcef(id, AL_CONE_OUTER_GAIN, 0);
+ al.alSource3f(sourceId, AL_DIRECTION, dir.x, dir.y, dir.z);
+ al.alSourcef(sourceId, AL_CONE_INNER_ANGLE, src.getInnerAngle());
+ al.alSourcef(sourceId, AL_CONE_OUTER_ANGLE, src.getOuterAngle());
+ al.alSourcef(sourceId, AL_CONE_OUTER_GAIN, 0);
} else {
- al.alSourcef(id, AL_CONE_INNER_ANGLE, 360);
- al.alSourcef(id, AL_CONE_OUTER_ANGLE, 360);
- al.alSourcef(id, AL_CONE_OUTER_GAIN, 1f);
+ // Omnidirectional: 360 degree cone, full gain
+ al.alSourcef(sourceId, AL_CONE_INNER_ANGLE, 360f);
+ al.alSourcef(sourceId, AL_CONE_OUTER_ANGLE, 360f);
+ al.alSourcef(sourceId, AL_CONE_OUTER_GAIN, 1f);
}
}
+ /**
+ * Updates a specific parameter for the listener.
+ *
+ * @param listener The listener object.
+ * @param param The parameter to update.
+ */
@Override
public void updateListenerParam(Listener listener, ListenerParam param) {
checkDead();
+ // Check if this listener is the active one
+ if (this.listener != listener) {
+ logger.warning("updateListenerParam called on inactive listener.");
+ return;
+ }
+
synchronized (threadLock) {
if (audioDisabled) {
return;
@@ -603,63 +729,84 @@ public void updateListenerParam(Listener listener, ListenerParam param) {
switch (param) {
case Position:
- Vector3f pos = listener.getLocation();
- al.alListener3f(AL_POSITION, pos.x, pos.y, pos.z);
+ applyListenerPosition(listener);
break;
case Rotation:
- Vector3f dir = listener.getDirection();
- Vector3f up = listener.getUp();
- fb.rewind();
- fb.put(dir.x).put(dir.y).put(dir.z);
- fb.put(up.x).put(up.y).put(up.z);
- fb.flip();
- al.alListener(AL_ORIENTATION, fb);
+ applyListenerRotation(listener);
break;
case Velocity:
- Vector3f vel = listener.getVelocity();
- al.alListener3f(AL_VELOCITY, vel.x, vel.y, vel.z);
+ applyListenerVelocity(listener);
break;
case Volume:
- al.alListenerf(AL_GAIN, listener.getVolume());
+ applyListenerVolume(listener);
+ break;
+ default:
+ logger.log(Level.WARNING, "Unhandled listener parameter: {0}", param);
break;
}
}
}
+ /**
+ * Applies all parameters from the listener object to OpenAL.
+ * @param listener The listener object.
+ */
private void setListenerParams(Listener listener) {
+ applyListenerPosition(listener);
+ applyListenerRotation(listener);
+ applyListenerVelocity(listener);
+ applyListenerVolume(listener);
+ }
+
+ // --- Listener Parameter Helper Methods ---
+
+ private void applyListenerPosition(Listener listener) {
Vector3f pos = listener.getLocation();
- Vector3f vel = listener.getVelocity();
+ al.alListener3f(AL_POSITION, pos.x, pos.y, pos.z);
+ }
+
+ private void applyListenerRotation(Listener listener) {
Vector3f dir = listener.getDirection();
Vector3f up = listener.getUp();
-
- al.alListener3f(AL_POSITION, pos.x, pos.y, pos.z);
- al.alListener3f(AL_VELOCITY, vel.x, vel.y, vel.z);
+ // Use the shared FloatBuffer fb
fb.rewind();
fb.put(dir.x).put(dir.y).put(dir.z);
fb.put(up.x).put(up.y).put(up.z);
fb.flip();
al.alListener(AL_ORIENTATION, fb);
+ }
+
+ private void applyListenerVelocity(Listener listener) {
+ Vector3f vel = listener.getVelocity();
+ al.alListener3f(AL_VELOCITY, vel.x, vel.y, vel.z);
+ }
+
+ private void applyListenerVolume(Listener listener) {
al.alListenerf(AL_GAIN, listener.getVolume());
}
private int newChannel() {
- if (freeChannels.size() > 0) {
- return freeChannels.remove(0);
- } else if (nextChan < channels.length) {
- return nextChan++;
+ if (!freeChannels.isEmpty()) {
+ return freeChannels.removeFirst();
+ } else if (nextChannelIndex < channels.length) {
+ return nextChannelIndex++;
} else {
return -1;
}
}
private void freeChannel(int index) {
- if (index == nextChan - 1) {
- nextChan--;
+ if (index == nextChannelIndex - 1) {
+ nextChannelIndex--;
} else {
freeChannels.add(index);
}
}
+ /**
+ * Configures the global reverb effect based on the Environment settings.
+ * @param env The Environment object.
+ */
@Override
public void setEnvironment(Environment env) {
checkDead();
@@ -668,6 +815,7 @@ public void setEnvironment(Environment env) {
return;
}
+ // Apply reverb properties from the Environment object
efx.alEffectf(reverbFx, EFX.AL_REVERB_DENSITY, env.getDensity());
efx.alEffectf(reverbFx, EFX.AL_REVERB_DIFFUSION, env.getDiffusion());
efx.alEffectf(reverbFx, EFX.AL_REVERB_GAIN, env.getGain());
@@ -681,38 +829,60 @@ public void setEnvironment(Environment env) {
efx.alEffectf(reverbFx, EFX.AL_REVERB_AIR_ABSORPTION_GAINHF, env.getAirAbsorbGainHf());
efx.alEffectf(reverbFx, EFX.AL_REVERB_ROOM_ROLLOFF_FACTOR, env.getRoomRolloffFactor());
- // attach effect to slot
+ // (Re)attach the configured reverb effect to the slot
efx.alAuxiliaryEffectSloti(reverbFxSlot, EFX.AL_EFFECTSLOT_EFFECT, reverbFx);
+ this.environment = env;
}
}
- private boolean fillBuffer(AudioStream stream, int id) {
- int size = 0;
- int result;
-
- while (size < arrayBuf.length) {
- result = stream.readSamples(arrayBuf, size, arrayBuf.length - size);
-
- if (result > 0) {
- size += result;
+ /**
+ * Fills a single OpenAL buffer with data from the audio stream.
+ * Uses the shared nativeBuf and arrayBuf.
+ *
+ * @param stream The AudioStream to read from.
+ * @param bufferId The OpenAL buffer ID to fill.
+ * @return True if the buffer was filled with data, false if stream EOF was reached before filling.
+ */
+ private boolean fillBuffer(AudioStream stream, int bufferId) {
+ int totalBytesRead = 0;
+ int bytesRead;
+
+ while (totalBytesRead < arrayBuf.length) {
+ bytesRead = stream.readSamples(arrayBuf, totalBytesRead, arrayBuf.length - totalBytesRead);
+
+ if (bytesRead > 0) {
+ totalBytesRead += bytesRead;
} else {
break;
}
}
- if (size == 0) {
+ if (totalBytesRead == 0) {
return false;
}
+ // Copy data from arrayBuf to nativeBuf
nativeBuf.clear();
- nativeBuf.put(arrayBuf, 0, size);
+ nativeBuf.put(arrayBuf, 0, totalBytesRead);
nativeBuf.flip();
- al.alBufferData(id, convertFormat(stream), nativeBuf, size, stream.getSampleRate());
+ // Upload data to the OpenAL buffer
+ int format = getOpenALFormat(stream);
+ int sampleRate = stream.getSampleRate();
+ al.alBufferData(bufferId, format, nativeBuf, totalBytesRead, sampleRate);
return true;
}
+ /**
+ * Unqueues processed buffers from a streaming source and refills/requeues them.
+ * Updates the stream's internal count of processed bytes.
+ *
+ * @param sourceId The OpenAL source ID.
+ * @param stream The AudioStream.
+ * @param looping Whether the stream should loop internally.
+ * @return True if at least one buffer was successfully refilled and requeued.
+ */
private boolean fillStreamingSource(int sourceId, AudioStream stream, boolean looping) {
boolean success = false;
int processed = al.alGetSourcei(sourceId, AL_BUFFERS_PROCESSED);
@@ -721,7 +891,7 @@ private boolean fillStreamingSource(int sourceId, AudioStream stream, boolean lo
for (int i = 0; i < processed; i++) {
int buffer;
- ib.position(0).limit(1);
+ ib.clear().limit(1);
al.alSourceUnqueueBuffers(sourceId, 1, ib);
buffer = ib.get(0);
@@ -730,23 +900,23 @@ private boolean fillStreamingSource(int sourceId, AudioStream stream, boolean lo
// be the case...
unqueuedBufferBytes += BUFFER_SIZE;
- boolean active = fillBuffer(stream, buffer);
-
- if (!active && !stream.isEOF()) {
+ // Try to refill the buffer
+ boolean filled = fillBuffer(stream, buffer);
+ if (!filled && !stream.isEOF()) {
throw new AssertionError();
}
- if (!active && looping) {
+ if (!filled && looping) {
stream.setTime(0);
- active = fillBuffer(stream, buffer);
- if (!active) {
+ filled = fillBuffer(stream, buffer); // Try filling again
+ if (!filled) {
throw new IllegalStateException("Looping streaming source "
+ "was rewound but could not be filled");
}
}
- if (active) {
- ib.position(0).limit(1);
+ if (filled) {
+ ib.clear().limit(1);
ib.put(0, buffer);
al.alSourceQueueBuffers(sourceId, 1, ib);
// At least one buffer enqueued = success.
@@ -757,6 +927,7 @@ private boolean fillStreamingSource(int sourceId, AudioStream stream, boolean lo
}
}
+ // Update the stream's internal counter for processed bytes
stream.setUnqueuedBufferBytes(stream.getUnqueuedBufferBytes() + unqueuedBufferBytes);
return success;
@@ -765,29 +936,30 @@ private boolean fillStreamingSource(int sourceId, AudioStream stream, boolean lo
private void attachStreamToSource(int sourceId, AudioStream stream, boolean looping) {
boolean success = false;
- // Reset the stream. Typically happens if it finished playing on
- // its own and got reclaimed.
- // Note that AudioNode.stop() already resets the stream
- // since it might not be at the EOF when stopped.
+ // Reset the stream. Typically, happens if it finished playing on its own and got reclaimed.
+ // Note that AudioNode.stop() already resets the stream since it might not be at the EOF when stopped.
if (stream.isEOF()) {
stream.setTime(0);
}
for (int id : stream.getIds()) {
- boolean active = fillBuffer(stream, id);
- if (!active && !stream.isEOF()) {
+ // Try to refill the buffer
+ boolean filled = fillBuffer(stream, id);
+ if (!filled && !stream.isEOF()) {
throw new AssertionError();
}
- if (!active && looping) {
+
+ if (!filled && looping) {
stream.setTime(0);
- active = fillBuffer(stream, id);
- if (!active) {
+ filled = fillBuffer(stream, id);
+ if (!filled) {
throw new IllegalStateException("Looping streaming source "
+ "was rewound but could not be filled");
}
}
- if (active) {
- ib.position(0).limit(1);
+
+ if (filled) {
+ ib.clear().limit(1);
ib.put(id).flip();
al.alSourceQueueBuffers(sourceId, 1, ib);
success = true;
@@ -800,9 +972,8 @@ private void attachStreamToSource(int sourceId, AudioStream stream, boolean loop
}
}
- private boolean attachBufferToSource(int sourceId, AudioBuffer buffer) {
+ private void attachBufferToSource(int sourceId, AudioBuffer buffer) {
al.alSourcei(sourceId, AL_BUFFER, buffer.getId());
- return true;
}
private void attachAudioToSource(int sourceId, AudioData data, boolean looping) {
@@ -815,6 +986,12 @@ private void attachAudioToSource(int sourceId, AudioData data, boolean looping)
}
}
+ /**
+ * Stops the AL source on the channel, detaches buffers and filters,
+ * and clears the jME source association. Does NOT free the channel index itself.
+ *
+ * @param index The channel index to clear.
+ */
private void clearChannel(int index) {
// make room at this channel
if (channelSources[index] != null) {
@@ -826,13 +1003,14 @@ private void clearChannel(int index) {
// For streaming sources, this will clear all queued buffers.
al.alSourcei(sourceId, AL_BUFFER, 0);
- if (src.getDryFilter() != null && supportEfx) {
- // detach filter
- al.alSourcei(sourceId, EFX.AL_DIRECT_FILTER, EFX.AL_FILTER_NULL);
- }
- if (src.isPositional()) {
- AudioSource pas = src;
- if (pas.isReverbEnabled() && supportEfx) {
+ if (supportEfx) {
+ if (src.getDryFilter() != null) {
+ // detach direct filter
+ al.alSourcei(sourceId, EFX.AL_DIRECT_FILTER, EFX.AL_FILTER_NULL);
+ }
+
+ if (src.isPositional() && src.isReverbEnabled()) {
+ // Detach auxiliary send filter (reverb)
al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL);
}
}
@@ -841,8 +1019,8 @@ private void clearChannel(int index) {
}
}
- private AudioSource.Status convertStatus(int oalStatus) {
- switch (oalStatus) {
+ private AudioSource.Status convertStatus(int openALState) {
+ switch (openALState) {
case AL_INITIAL:
case AL_STOPPED:
return Status.Stopped;
@@ -851,7 +1029,7 @@ private AudioSource.Status convertStatus(int oalStatus) {
case AL_PLAYING:
return Status.Playing;
default:
- throw new UnsupportedOperationException("Unrecognized OAL state: " + oalStatus);
+ throw new UnsupportedOperationException("Unrecognized OpenAL state: " + openALState);
}
}
@@ -862,29 +1040,71 @@ public void update(float tpf) {
}
}
+ /**
+ * Checks the device connection status and attempts to restart the renderer if disconnected.
+ * Called periodically from the decoder thread.
+ */
private void checkDevice() {
-
- // If the device is disconnected, pick a new one
- if (isDisconnected()) {
- logger.log(Level.INFO, "Current audio device disconnected.");
+ if (isDeviceDisconnected()) {
+ logger.log(Level.WARNING, "Audio device disconnected! Attempting to restart audio renderer...");
restartAudioRenderer();
}
}
- private boolean isDisconnected() {
- if (!supportDisconnect) {
+ /**
+ * Checks if the audio device has been disconnected.
+ * Requires ALC_EXT_disconnect extension.
+ * @return True if disconnected, false otherwise or if not supported.
+ */
+ private boolean isDeviceDisconnected() {
+ if (audioDisabled || !supportDisconnect) {
return false;
}
+ ib.clear().limit(1);
alc.alcGetInteger(ALC.ALC_CONNECTED, ib, 1);
+ // Returns 1 if connected, 0 if disconnected.
return ib.get(0) == 0;
}
private void restartAudioRenderer() {
+ // Preserve internal state variables
+ Listener currentListener = this.listener;
+ Environment currentEnvironment = this.environment;
+
+ // Destroy existing OpenAL resources
destroyOpenAL();
+
+ // Re-initialize OpenAL
+ // Creates new context, enumerates channels, checks caps, inits EFX
initOpenAL();
+
+ // Restore Listener and Environment (if possible and successful init)
+ if (!audioDisabled) {
+ if (currentListener != null) {
+ setListener(currentListener); // Re-apply listener params
+ }
+ if (currentEnvironment != null) {
+ setEnvironment(currentEnvironment); // Re-apply environment
+ }
+ // TODO: What about existing AudioSource objects?
+ // Their state (Playing/Paused/Stopped) is lost.
+ // Their AudioData (buffers/streams) needs re-uploading/re-preparing.
+ // This requires iterating through all known AudioNodes, which the renderer doesn't track.
+ // The application layer would need to handle re-playing sounds after a device reset.
+ logger.warning("Audio renderer restarted. Application may need to re-play active sounds.");
+
+ } else {
+ logger.severe("Audio remained disabled after attempting restart.");
+ }
}
+ /**
+ * Internal update logic called from the render thread within the lock.
+ * Checks source statuses and reclaims finished channels.
+ *
+ * @param tpf Time per frame (currently unused).
+ */
public void updateInRenderThread(float tpf) {
if (audioDisabled) {
return;
@@ -894,80 +1114,104 @@ public void updateInRenderThread(float tpf) {
AudioSource src = channelSources[i];
if (src == null) {
- continue;
+ continue; // No source on this channel
}
int sourceId = channels[i];
boolean boundSource = i == src.getChannel();
boolean reclaimChannel = false;
- Status oalStatus = convertStatus(al.alGetSourcei(sourceId, AL_SOURCE_STATE));
+ // Get OpenAL status for the source
+ int openALState = al.alGetSourcei(sourceId, AL_SOURCE_STATE);
+ Status openALStatus = convertStatus(openALState);
+ // --- Handle Instanced Playback (Not bound to a specific channel) ---
if (!boundSource) {
- // Rules for instanced playback vary significantly.
- // Handle it here.
- if (oalStatus == Status.Stopped) {
- // Instanced audio stopped playing. Reclaim channel.
- clearChannel(i);
- freeChannel(i);
- } else if (oalStatus == Status.Paused) {
- throw new AssertionError("Instanced audio cannot be paused");
+ if (openALStatus == Status.Stopped) {
+ // Instanced audio (non-looping buffer) finished playing. Reclaim channel.
+ if (logger.isLoggable(Level.FINE)) {
+ logger.log(Level.FINE, "Reclaiming channel {0} from finished instance.", i);
+ }
+ clearChannel(i); // Stop source, detach buffer/filter
+ freeChannel(i); // Add channel back to the free pool
+ } else if (openALStatus == Status.Paused) {
+ throw new AssertionError("Instanced audio source on channel " + i + " cannot be paused.");
}
+ // If Playing, do nothing, let it finish.
continue;
}
+ // --- Handle Bound Playback (Normal play/pause/stop) ---
Status jmeStatus = src.getStatus();
- // Check if we need to sync JME status with OAL status.
- if (oalStatus != jmeStatus) {
- if (oalStatus == Status.Stopped && jmeStatus == Status.Playing) {
- // Maybe we need to reclaim the channel.
+ // Check if we need to sync JME status with OpenAL status.
+ if (openALStatus != jmeStatus) {
+ if (openALStatus == Status.Stopped && jmeStatus == Status.Playing) {
+
+ // Source stopped playing unexpectedly (finished or starved)
if (src.getAudioData() instanceof AudioStream) {
AudioStream stream = (AudioStream) src.getAudioData();
if (stream.isEOF() && !src.isLooping()) {
- // Stream finished playing
+ // Stream reached EOF and is not looping.
+ if (logger.isLoggable(Level.FINE)) {
+ logger.log(Level.FINE, "Stream source on channel {0} finished.", i);
+ }
reclaimChannel = true;
} else {
- // Stream still has data.
- // Buffer starvation occurred.
- // Audio decoder thread will fill the data
- // and start the channel again.
+ // Stream still has data or is looping, but stopped.
+ // This indicates buffer starvation. The decoder thread will handle restarting it.
+ if (logger.isLoggable(Level.FINE)) {
+ logger.log(Level.FINE, "Stream source on channel {0} likely starved.", i);
+ }
+ // Don't reclaim channel here, let decoder thread refill and restart.
}
} else {
// Buffer finished playing.
if (src.isLooping()) {
- // When a device is disconnected, all sources
- // will enter the "stopped" state.
- logger.warning("A looping sound has stopped playing");
+ // This is unexpected for looping buffers unless the device was disconnected/reset.
+ logger.log(Level.WARNING, "Looping buffer source on channel {0} stopped unexpectedly.", i);
+ } else {
+ // Non-looping buffer finished normally.
+ if (logger.isLoggable(Level.FINE)) {
+ logger.log(Level.FINE, "Buffer source on channel {0} finished.", i);
+ }
}
reclaimChannel = true;
}
if (reclaimChannel) {
+ if (logger.isLoggable(Level.FINE)) {
+ logger.log(Level.FINE, "Reclaiming channel {0} from finished source.", i);
+ }
src.setStatus(Status.Stopped);
src.setChannel(-1);
- clearChannel(i);
- freeChannel(i);
+ clearChannel(i); // Stop AL source, detach buffers/filters
+ freeChannel(i); // Add channel back to the free pool
}
} else {
- // jME3 state does not match OAL state.
+ // jME3 state does not match OpenAL state.
// This is only relevant for bound sources.
throw new AssertionError("Unexpected sound status. "
- + "OAL: " + oalStatus
- + ", JME: " + jmeStatus);
+ + "OpenAL: " + openALStatus + ", JME: " + jmeStatus);
}
} else {
// Stopped channel was not cleared correctly.
- if (oalStatus == Status.Stopped) {
+ if (openALStatus == Status.Stopped) {
throw new AssertionError("Channel " + i + " was not reclaimed");
}
}
}
}
+ /**
+ * Internal update logic called from the decoder thread within the lock.
+ * Fills streaming buffers and restarts starved sources. Deletes unused objects.
+ *
+ * @param tpf Time per frame (currently unused).
+ */
public void updateInDecoderThread(float tpf) {
if (audioDisabled) {
return;
@@ -976,6 +1220,7 @@ public void updateInDecoderThread(float tpf) {
for (int i = 0; i < channels.length; i++) {
AudioSource src = channelSources[i];
+ // Only process streaming sources associated with this channel
if (src == null || !(src.getAudioData() instanceof AudioStream)) {
continue;
}
@@ -983,21 +1228,26 @@ public void updateInDecoderThread(float tpf) {
int sourceId = channels[i];
AudioStream stream = (AudioStream) src.getAudioData();
- Status oalStatus = convertStatus(al.alGetSourcei(sourceId, AL_SOURCE_STATE));
+ // Get current AL state, primarily to check if we need to restart playback
+ int openALState = al.alGetSourcei(sourceId, AL_SOURCE_STATE);
+ Status openALStatus = convertStatus(openALState);
Status jmeStatus = src.getStatus();
// Keep filling data (even if we are stopped / paused)
boolean buffersWereFilled = fillStreamingSource(sourceId, stream, src.isLooping());
- if (buffersWereFilled && oalStatus == Status.Stopped && jmeStatus == Status.Playing) {
+ // Check if the source stopped due to buffer starvation while it was supposed to be playing
+ if (buffersWereFilled
+ && openALStatus == Status.Stopped
+ && jmeStatus == Status.Playing) {
// The source got stopped due to buffer starvation.
// Start it again.
- logger.log(Level.WARNING, "Buffer starvation occurred while playing stream");
+ logger.log(Level.WARNING, "Buffer starvation detected for stream on channel {0}. Restarting playback.", i);
al.alSourcePlay(sourceId);
}
}
- // Delete any unused objects.
+ // Delete any unused objects (buffers, filters) that are no longer referenced.
objManager.deleteUnused(this);
}
@@ -1010,35 +1260,60 @@ public void setListener(Listener listener) {
}
if (this.listener != null) {
- // previous listener no longer associated with current
- // renderer
+ // previous listener no longer associated with current renderer
this.listener.setRenderer(null);
}
this.listener = listener;
- this.listener.setRenderer(this);
- setListenerParams(listener);
+
+ if (this.listener != null) {
+ this.listener.setRenderer(this);
+ setListenerParams(listener);
+ } else {
+ logger.info("Listener set to null.");
+ }
}
}
+ /**
+ * Pauses all audio playback by pausing the OpenAL device context.
+ * Requires ALC_SOFT_pause_device extension.
+ * @throws UnsupportedOperationException if the extension is not supported.
+ */
@Override
public void pauseAll() {
if (!supportPauseDevice) {
- throw new UnsupportedOperationException("Pause device is NOT supported!");
+ throw new UnsupportedOperationException(
+ "Pausing the audio device is not supported by the current OpenAL driver" +
+ " (requires ALC_SOFT_pause_device).");
}
alc.alcDevicePauseSOFT();
+ logger.info("Audio device paused.");
}
+ /**
+ * Resumes all audio playback by resuming the OpenAL device context.
+ * Requires ALC_SOFT_pause_device extension.
+ * @throws UnsupportedOperationException if the extension is not supported.
+ */
@Override
public void resumeAll() {
if (!supportPauseDevice) {
- throw new UnsupportedOperationException("Pause device is NOT supported!");
+ throw new UnsupportedOperationException(
+ "Resuming the audio device is not supported by the current OpenAL driver" +
+ " (requires ALC_SOFT_pause_device).");
}
alc.alcDeviceResumeSOFT();
+ logger.info("Audio device resumed.");
}
+ /**
+ * Plays an audio source as a one-shot instance (non-looping buffer).
+ * A free channel is allocated temporarily.
+ * @param src The audio source to play.
+ */
@Override
public void playSourceInstance(AudioSource src) {
checkDead();
@@ -1047,29 +1322,34 @@ public void playSourceInstance(AudioSource src) {
return;
}
- if (src.getAudioData() instanceof AudioStream) {
+ AudioData audioData = src.getAudioData();
+ if (audioData == null) {
+ logger.log(Level.WARNING, "playSourceInstance called on source with null AudioData: {0}", src);
+ return;
+ }
+ if (audioData instanceof AudioStream) {
throw new UnsupportedOperationException(
- "Cannot play instances "
- + "of audio streams. Use play() instead.");
+ "Cannot play instances of audio streams. Use play() instead.");
}
- if (src.getAudioData().isUpdateNeeded()) {
- updateAudioData(src.getAudioData());
+ if (audioData.isUpdateNeeded()) {
+ updateAudioData(audioData);
}
- // create a new index for an audio-channel
+ // Allocate a temporary channel
int index = newChannel();
if (index == -1) {
+ logger.log(Level.WARNING, "No channel available to play instance of {0}", src);
return;
}
+ // Ensure channel is clean before use
int sourceId = channels[index];
-
clearChannel(index);
- // set parameters, like position and max distance
+ // Set parameters for this specific instance (force non-looping)
setSourceParams(sourceId, src, true);
- attachAudioToSource(sourceId, src.getAudioData(), false);
+ attachAudioToSource(sourceId, audioData, false);
channelSources[index] = src;
// play the channel
@@ -1077,6 +1357,11 @@ public void playSourceInstance(AudioSource src) {
}
}
+ /**
+ * Plays an audio source, allocating a persistent channel for it.
+ * Handles both buffers and streams. Can be paused and stopped.
+ * @param src The audio source to play.
+ */
@Override
public void playSource(AudioSource src) {
checkDead();
@@ -1086,36 +1371,51 @@ public void playSource(AudioSource src) {
}
if (src.getStatus() == Status.Playing) {
+ // Already playing, do nothing.
return;
- } else if (src.getStatus() == Status.Stopped) {
- // Assertion removed because it seems it's not possible to have
- // something different from -1 when first playing an AudioNode.
- // assert src.getChannel() != -1;
+ }
+
+ if (src.getStatus() == Status.Stopped) {
- // allocate channel to this source
+ AudioData audioData = src.getAudioData();
+ if (audioData == null) {
+ logger.log(Level.WARNING, "playSource called on source with null AudioData: {0}", src);
+ return;
+ }
+
+ // Allocate a temporary channel
int index = newChannel();
if (index == -1) {
- logger.log(Level.WARNING, "No channel available to play {0}", src);
+ logger.log(Level.WARNING, "No channel available to play instance of {0}", src);
return;
}
+
+ // Ensure channel is clean before use
+ int sourceId = channels[index];
clearChannel(index);
src.setChannel(index);
- AudioData data = src.getAudioData();
- if (data.isUpdateNeeded()) {
- updateAudioData(data);
+ if (audioData.isUpdateNeeded()) {
+ updateAudioData(audioData);
}
+ // Set all source parameters and attach the audio data
channelSources[index] = src;
- setSourceParams(channels[index], src, false);
- attachAudioToSource(channels[index], data, src.isLooping());
+ setSourceParams(sourceId, src, false);
+ attachAudioToSource(sourceId, audioData, src.isLooping());
}
- al.alSourcePlay(channels[src.getChannel()]);
- src.setStatus(Status.Playing);
+ // play the channel
+ int sourceId = channels[src.getChannel()];
+ al.alSourcePlay(sourceId);
+ src.setStatus(Status.Playing); // Update JME status
}
}
+ /**
+ * Pauses a playing audio source.
+ * @param src The audio source to pause.
+ */
@Override
public void pauseSource(AudioSource src) {
checkDead();
@@ -1124,15 +1424,27 @@ public void pauseSource(AudioSource src) {
return;
}
+ AudioData audioData = src.getAudioData();
+ if (audioData == null) {
+ logger.log(Level.WARNING, "pauseSource called on source with null AudioData: {0}", src);
+ return;
+ }
+
if (src.getStatus() == Status.Playing) {
assert src.getChannel() != -1;
- al.alSourcePause(channels[src.getChannel()]);
- src.setStatus(Status.Paused);
+ int sourceId = channels[src.getChannel()];
+ al.alSourcePause(sourceId);
+ src.setStatus(Status.Paused); // Update JME status
}
}
}
+ /**
+ * Stops a playing or paused audio source, releasing its channel.
+ * For streams, resets or closes the stream.
+ * @param src The audio source to stop.
+ */
@Override
public void stopSource(AudioSource src) {
synchronized (threadLock) {
@@ -1140,18 +1452,24 @@ public void stopSource(AudioSource src) {
return;
}
+ AudioData audioData = src.getAudioData();
+ if (audioData == null) {
+ logger.log(Level.WARNING, "stopSource called on source with null AudioData: {0}", src);
+ return;
+ }
+
if (src.getStatus() != Status.Stopped) {
- int chan = src.getChannel();
- assert chan != -1; // if it's not stopped, must have id
+ int channel = src.getChannel();
+ assert channel != -1; // if it's not stopped, must have id
src.setStatus(Status.Stopped);
src.setChannel(-1);
- clearChannel(chan);
- freeChannel(chan);
+ clearChannel(channel);
+ freeChannel(channel);
if (src.getAudioData() instanceof AudioStream) {
- // If the stream is seekable, then rewind it.
- // Otherwise, close it, as it is no longer valid.
+ // If the stream is seekable, rewind it to the beginning.
+ // Otherwise (non-seekable), close it, as it might be invalid now.
AudioStream stream = (AudioStream) src.getAudioData();
if (stream.isSeekable()) {
stream.setTime(0);
@@ -1163,107 +1481,140 @@ public void stopSource(AudioSource src) {
}
}
- private int convertFormat(AudioData ad) {
- switch (ad.getBitsPerSample()) {
- case 8:
- if (ad.getChannels() == 1) {
- return AL_FORMAT_MONO8;
- } else if (ad.getChannels() == 2) {
- return AL_FORMAT_STEREO8;
- }
-
- break;
- case 16:
- if (ad.getChannels() == 1) {
- return AL_FORMAT_MONO16;
- } else {
- return AL_FORMAT_STEREO16;
- }
+ /**
+ * Gets the corresponding OpenAL format enum for the audio data properties.
+ * @param audioData The AudioData.
+ * @return The OpenAL format enum.
+ * @throws UnsupportedOperationException if the format is not supported.
+ */
+ private int getOpenALFormat(AudioData audioData) {
+
+ int channels = audioData.getChannels();
+ int bitsPerSample = audioData.getBitsPerSample();
+
+ if (channels == 1) {
+ if (bitsPerSample == 8) {
+ return AL_FORMAT_MONO8;
+ } else if (bitsPerSample == 16) {
+ return AL_FORMAT_MONO16;
+ }
+ } else if (channels == 2) {
+ if (bitsPerSample == 8) {
+ return AL_FORMAT_STEREO8;
+ } else if (bitsPerSample == 16) {
+ return AL_FORMAT_STEREO16;
+ }
}
- throw new UnsupportedOperationException("Unsupported channels/bits combination: "
- + "bits=" + ad.getBitsPerSample() + ", channels=" + ad.getChannels());
+
+ throw new UnsupportedOperationException("Unsupported audio format: "
+ + channels + " channels, " + bitsPerSample + " bits per sample.");
}
+ /**
+ * Uploads buffer data to OpenAL. Generates buffer ID if needed.
+ * @param ab The AudioBuffer.
+ */
private void updateAudioBuffer(AudioBuffer ab) {
int id = ab.getId();
if (ab.getId() == -1) {
- ib.position(0).limit(1);
+ ib.clear().limit(1);
al.alGenBuffers(1, ib);
id = ib.get(0);
ab.setId(id);
+ // Register for automatic cleanup if unused
objManager.registerObject(ab);
}
- ab.getData().clear();
- al.alBufferData(id, convertFormat(ab), ab.getData(), ab.getData().capacity(), ab.getSampleRate());
+ ByteBuffer data = ab.getData();
+
+ data.clear(); // Ensure buffer is ready for reading
+ int format = getOpenALFormat(ab);
+ int sampleRate = ab.getSampleRate();
+
+ al.alBufferData(id, format, data, data.capacity(), sampleRate);
ab.clearUpdateNeeded();
}
+ /**
+ * Prepares OpenAL buffers for an AudioStream. Generates buffer IDs.
+ * Does not fill buffers with data yet.
+ * @param as The AudioStream.
+ */
private void updateAudioStream(AudioStream as) {
+ // Delete old buffers if they exist (e.g., re-initializing stream)
if (as.getIds() != null) {
deleteAudioData(as);
}
int[] ids = new int[STREAMING_BUFFER_COUNT];
- ib.position(0).limit(STREAMING_BUFFER_COUNT);
+ ib.clear().limit(STREAMING_BUFFER_COUNT);
+
al.alGenBuffers(STREAMING_BUFFER_COUNT, ib);
- ib.position(0).limit(STREAMING_BUFFER_COUNT);
- ib.get(ids);
- // Not registered with object manager.
- // AudioStreams can be handled without object manager
- // since their lifecycle is known to the audio renderer.
+ ib.rewind();
+ ib.get(ids);
+ // Streams are managed directly, not via NativeObjectManager,
+ // because their lifecycle is tied to active playback.
as.setIds(ids);
as.clearUpdateNeeded();
}
- private void updateAudioData(AudioData ad) {
- if (ad instanceof AudioBuffer) {
- updateAudioBuffer((AudioBuffer) ad);
- } else if (ad instanceof AudioStream) {
- updateAudioStream((AudioStream) ad);
+ private void updateAudioData(AudioData audioData) {
+ if (audioData instanceof AudioBuffer) {
+ updateAudioBuffer((AudioBuffer) audioData);
+ } else if (audioData instanceof AudioStream) {
+ updateAudioStream((AudioStream) audioData);
}
}
+ /**
+ * Deletes the OpenAL filter object associated with the Filter.
+ * @param filter The Filter object.
+ */
@Override
public void deleteFilter(Filter filter) {
int id = filter.getId();
if (id != -1) {
- ib.position(0).limit(1);
+ ib.clear().limit(1);
ib.put(id).flip();
efx.alDeleteFilters(1, ib);
filter.resetObject();
}
}
+ /**
+ * Deletes the OpenAL objects associated with the AudioData.
+ * @param audioData The AudioData to delete.
+ */
@Override
- public void deleteAudioData(AudioData ad) {
+ public void deleteAudioData(AudioData audioData) {
synchronized (threadLock) {
if (audioDisabled) {
return;
}
- if (ad instanceof AudioBuffer) {
- AudioBuffer ab = (AudioBuffer) ad;
+ if (audioData instanceof AudioBuffer) {
+ AudioBuffer ab = (AudioBuffer) audioData;
int id = ab.getId();
if (id != -1) {
ib.put(0, id);
- ib.position(0).limit(1);
+ ib.clear().limit(1);
al.alDeleteBuffers(1, ib);
- ab.resetObject();
+ ab.resetObject(); // Mark as deleted on JME side
}
- } else if (ad instanceof AudioStream) {
- AudioStream as = (AudioStream) ad;
+ } else if (audioData instanceof AudioStream) {
+ AudioStream as = (AudioStream) audioData;
int[] ids = as.getIds();
if (ids != null) {
ib.clear();
ib.put(ids).flip();
al.alDeleteBuffers(ids.length, ib);
- as.resetObject();
+ as.resetObject(); // Mark as deleted on JME side
}
}
}
}
+
}
diff --git a/jme3-core/src/main/java/com/jme3/audio/openal/EFX.java b/jme3-core/src/main/java/com/jme3/audio/openal/EFX.java
index 86170c142a..32d572da09 100644
--- a/jme3-core/src/main/java/com/jme3/audio/openal/EFX.java
+++ b/jme3-core/src/main/java/com/jme3/audio/openal/EFX.java
@@ -27,19 +27,19 @@ public interface EFX {
/* Effect properties. */
/* Reverb effect parameters */
- public static final int AL_REVERB_DENSITY = 0x0001;
- public static final int AL_REVERB_DIFFUSION = 0x0002;
- public static final int AL_REVERB_GAIN = 0x0003;
- public static final int AL_REVERB_GAINHF = 0x0004;
- public static final int AL_REVERB_DECAY_TIME = 0x0005;
- public static final int AL_REVERB_DECAY_HFRATIO = 0x0006;
- public static final int AL_REVERB_REFLECTIONS_GAIN = 0x0007;
- public static final int AL_REVERB_REFLECTIONS_DELAY = 0x0008;
- public static final int AL_REVERB_LATE_REVERB_GAIN = 0x0009;
- public static final int AL_REVERB_LATE_REVERB_DELAY = 0x000A;
+ public static final int AL_REVERB_DENSITY = 0x0001;
+ public static final int AL_REVERB_DIFFUSION = 0x0002;
+ public static final int AL_REVERB_GAIN = 0x0003;
+ public static final int AL_REVERB_GAINHF = 0x0004;
+ public static final int AL_REVERB_DECAY_TIME = 0x0005;
+ public static final int AL_REVERB_DECAY_HFRATIO = 0x0006;
+ public static final int AL_REVERB_REFLECTIONS_GAIN = 0x0007;
+ public static final int AL_REVERB_REFLECTIONS_DELAY = 0x0008;
+ public static final int AL_REVERB_LATE_REVERB_GAIN = 0x0009;
+ public static final int AL_REVERB_LATE_REVERB_DELAY = 0x000A;
public static final int AL_REVERB_AIR_ABSORPTION_GAINHF = 0x000B;
- public static final int AL_REVERB_ROOM_ROLLOFF_FACTOR = 0x000C;
- public static final int AL_REVERB_DECAY_HFLIMIT = 0x000D;
+ public static final int AL_REVERB_ROOM_ROLLOFF_FACTOR = 0x000C;
+ public static final int AL_REVERB_DECAY_HFLIMIT = 0x000D;
/* EAX Reverb effect parameters */
//#define AL_EAXREVERB_DENSITY 0x0001
@@ -171,28 +171,28 @@ public interface EFX {
///* Filter properties. */
/* Lowpass filter parameters */
- public static final int AL_LOWPASS_GAIN = 0x0001;
- public static final int AL_LOWPASS_GAINHF = 0x0002;
+ public static final int AL_LOWPASS_GAIN = 0x0001;
+ public static final int AL_LOWPASS_GAINHF = 0x0002;
- ///* Highpass filter parameters */
- //#define AL_HIGHPASS_GAIN 0x0001
- //#define AL_HIGHPASS_GAINLF 0x0002
+ // * Highpass filter parameters */
+ public static final int AL_HIGHPASS_GAIN = 0x0001;
+ public static final int AL_HIGHPASS_GAINLF = 0x0002;
- ///* Bandpass filter parameters */
- //#define AL_BANDPASS_GAIN 0x0001
- //#define AL_BANDPASS_GAINLF 0x0002
- //#define AL_BANDPASS_GAINHF 0x0003
+ // * Bandpass filter parameters */
+ public static final int AL_BANDPASS_GAIN = 0x0001;
+ public static final int AL_BANDPASS_GAINLF = 0x0002;
+ public static final int AL_BANDPASS_GAINHF = 0x0003;
/* Filter type */
//#define AL_FILTER_FIRST_PARAMETER 0x0000
//#define AL_FILTER_LAST_PARAMETER 0x8000
- public static final int AL_FILTER_TYPE = 0x8001;
+ public static final int AL_FILTER_TYPE = 0x8001;
/* Filter types, used with the AL_FILTER_TYPE property */
- public static final int AL_FILTER_NULL = 0x0000;
- public static final int AL_FILTER_LOWPASS = 0x0001;
- public static final int AL_FILTER_HIGHPASS = 0x0002;
- //#define AL_FILTER_BANDPASS 0x0003
+ public static final int AL_FILTER_NULL = 0x0000;
+ public static final int AL_FILTER_LOWPASS = 0x0001;
+ public static final int AL_FILTER_HIGHPASS = 0x0002;
+ public static final int AL_FILTER_BANDPASS = 0x0003;
///* Filter ranges and defaults. */
//
diff --git a/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java b/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java
index b99d4d7351..6b0e023b34 100644
--- a/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java
+++ b/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 java.io.IOException;
import java.nio.FloatBuffer;
//import com.jme.scene.TriMesh;
+import java.util.Objects;
/**
* BoundingBox describes a bounding volume as an axis-aligned box.
@@ -587,6 +588,76 @@ public BoundingVolume clone(BoundingVolume store) {
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;
+ }
+
/**
* toString returns the string representation of this object.
* The form is: "[Center: vector xExtent: X.XX yExtent: Y.YY zExtent:
diff --git a/jme3-core/src/main/java/com/jme3/bounding/BoundingSphere.java b/jme3-core/src/main/java/com/jme3/bounding/BoundingSphere.java
index 5b46846a23..67cf7263f8 100644
--- a/jme3-core/src/main/java/com/jme3/bounding/BoundingSphere.java
+++ b/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.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 BoundingVolume clone(BoundingVolume store) {
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;
+ }
+
/**
* toString returns the string representation of this object.
* The form is: "Radius: RRR.SSSS Center: vector".
diff --git a/jme3-core/src/main/java/com/jme3/bounding/BoundingVolume.java b/jme3-core/src/main/java/com/jme3/bounding/BoundingVolume.java
index 88cd4148ef..3a80764910 100644
--- a/jme3-core/src/main/java/com/jme3/bounding/BoundingVolume.java
+++ b/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.util.TempVars;
import java.io.IOException;
import java.nio.FloatBuffer;
+import java.util.Objects;
/**
* BoundingVolume defines an interface for dealing with
@@ -180,6 +181,48 @@ public final BoundingVolume transform(Transform trans) {
*/
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;
}
diff --git a/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java b/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java
index b03850c255..daca01b843 100644
--- a/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java
+++ b/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 cinematicEvents = new ArrayList<>();
+ private final List cinematicEvents = new ArrayList<>();
private Map cameras = new HashMap<>();
private CameraNode currentCam;
private boolean initialized = false;
diff --git a/jme3-core/src/main/java/com/jme3/cinematic/MotionPath.java b/jme3-core/src/main/java/com/jme3/cinematic/MotionPath.java
index 7a139a43a4..20e3232ad7 100644
--- a/jme3-core/src/main/java/com/jme3/cinematic/MotionPath.java
+++ b/jme3-core/src/main/java/com/jme3/cinematic/MotionPath.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -45,6 +45,8 @@
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Curve;
import com.jme3.util.TempVars;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
@@ -54,7 +56,7 @@
* Motion path is used to create a path between way points.
* @author Nehon
*/
-public class MotionPath implements Savable {
+public class MotionPath implements JmeCloneable, Savable {
private Node debugNode;
private AssetManager assetManager;
@@ -177,6 +179,40 @@ public void read(JmeImporter im) throws IOException {
spline = (Spline) in.readSavable("spline", null);
}
+ /**
+ * Callback from {@link com.jme3.util.clone.Cloner} to convert this
+ * shallow-cloned MotionPath into a deep-cloned one, using the specified
+ * cloner and original to resolve copied fields.
+ *
+ * @param cloner the cloner that's cloning this MotionPath (not null)
+ * @param original the object from which this MotionPath was shallow-cloned
+ * (not null, unaffected)
+ */
+ @Override
+ public void cloneFields(Cloner cloner, Object original) {
+ this.debugNode = cloner.clone(debugNode);
+ this.spline = cloner.clone(spline);
+ /*
+ * The clone will share both the asset manager and the list of listeners
+ * of the original MotionPath.
+ */
+ }
+
+ /**
+ * Creates a shallow clone for the JME cloner.
+ *
+ * @return a new object
+ */
+ @Override
+ public MotionPath jmeClone() {
+ try {
+ MotionPath clone = (MotionPath) clone();
+ return clone;
+ } catch (CloneNotSupportedException exception) {
+ throw new RuntimeException(exception);
+ }
+ }
+
/**
* compute the index of the waypoint and the interpolation value according to a distance
* returns a vector 2 containing the index in the x field and the interpolation value in the y field
diff --git a/jme3-core/src/main/java/com/jme3/cinematic/events/AnimEvent.java b/jme3-core/src/main/java/com/jme3/cinematic/events/AnimEvent.java
index 34c5275d20..7d7721e166 100644
--- a/jme3-core/src/main/java/com/jme3/cinematic/events/AnimEvent.java
+++ b/jme3-core/src/main/java/com/jme3/cinematic/events/AnimEvent.java
@@ -53,7 +53,7 @@
*/
public class AnimEvent extends AbstractCinematicEvent {
- final public static Logger logger
+ public static final Logger logger
= Logger.getLogger(AnimEvent.class.getName());
/*
diff --git a/jme3-core/src/main/java/com/jme3/cinematic/events/CameraEvent.java b/jme3-core/src/main/java/com/jme3/cinematic/events/CameraEvent.java
index 08b2e8fe46..a9dfec3960 100644
--- a/jme3-core/src/main/java/com/jme3/cinematic/events/CameraEvent.java
+++ b/jme3-core/src/main/java/com/jme3/cinematic/events/CameraEvent.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -40,27 +40,37 @@
import java.io.IOException;
/**
+ * A `CameraEvent` is a cinematic event that instantly sets the active camera
+ * within a `Cinematic` sequence.
*
* @author Rickard (neph1 @ github)
*/
public class CameraEvent extends AbstractCinematicEvent {
+ /**
+ * The name of the camera to activate.
+ */
private String cameraName;
+ /**
+ * The `Cinematic` instance to which this event belongs and on which the
+ * camera will be set.
+ */
private Cinematic cinematic;
- public String getCameraName() {
- return cameraName;
- }
-
- public void setCameraName(String cameraName) {
- this.cameraName = cameraName;
- }
-
+ /**
+ * For serialization only. Do not use.
+ */
public CameraEvent() {
}
- public CameraEvent(Cinematic parentEvent, String cameraName) {
- this.cinematic = parentEvent;
+ /**
+ * Constructs a new `CameraEvent` with the specified cinematic and camera name.
+ *
+ * @param cinematic The `Cinematic` instance this event belongs to (cannot be null).
+ * @param cameraName The name of the camera to be activated by this event (cannot be null or empty).
+ */
+ public CameraEvent(Cinematic cinematic, String cameraName) {
+ this.cinematic = cinematic;
this.cameraName = cameraName;
}
@@ -102,33 +112,56 @@ public void setTime(float time) {
play();
}
+ /**
+ * Returns the `Cinematic` instance associated with this event.
+ * @return The `Cinematic` instance.
+ */
public Cinematic getCinematic() {
return cinematic;
}
+ /**
+ * Sets the `Cinematic` instance for this event.
+ * @param cinematic The `Cinematic` instance to set (cannot be null).
+ */
public void setCinematic(Cinematic cinematic) {
this.cinematic = cinematic;
}
/**
- * used internally for serialization
+ * Returns the name of the camera that this event will activate.
+ * @return The camera name.
+ */
+ public String getCameraName() {
+ return cameraName;
+ }
+
+ /**
+ * Sets the name of the camera that this event will activate.
+ * @param cameraName The new camera name (cannot be null or empty).
+ */
+ public void setCameraName(String cameraName) {
+ this.cameraName = cameraName;
+ }
+
+ /**
+ * Used internally for serialization.
*
- * @param ex the exporter (not null)
- * @throws IOException from the exporter
+ * @param ex The exporter (not null).
+ * @throws IOException If an I/O error occurs during serialization.
*/
@Override
public void write(JmeExporter ex) throws IOException {
super.write(ex);
OutputCapsule oc = ex.getCapsule(this);
oc.write(cameraName, "cameraName", null);
-
}
/**
- * used internally for serialization
+ * Used internally for deserialization.
*
- * @param im the importer (not null)
- * @throws IOException from the importer
+ * @param im The importer (not null).
+ * @throws IOException If an I/O error occurs during deserialization.
*/
@Override
public void read(JmeImporter im) throws IOException {
diff --git a/jme3-core/src/main/java/com/jme3/cinematic/events/MotionEvent.java b/jme3-core/src/main/java/com/jme3/cinematic/events/MotionEvent.java
index 2bd4485768..d4751807ef 100644
--- a/jme3-core/src/main/java/com/jme3/cinematic/events/MotionEvent.java
+++ b/jme3-core/src/main/java/com/jme3/cinematic/events/MotionEvent.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -52,8 +52,8 @@
import java.io.IOException;
/**
- * A MotionEvent is a control over the spatial that manages the position and direction of the spatial while following a motion Path.
- *
+ * A MotionEvent is a control over the spatial that manages
+ * the position and direction of the spatial while following a motion Path.
* You must first create a MotionPath and then create a MotionEvent to associate a spatial and the path.
*
* @author Nehon
@@ -70,6 +70,7 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
protected Direction directionType = Direction.None;
protected MotionPath path;
private boolean isControl = true;
+ private final Quaternion tempRotation = new Quaternion();
/**
* the distance traveled by the spatial on the path
*/
@@ -79,7 +80,6 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
* Enum for the different type of target direction behavior.
*/
public enum Direction {
-
/**
* The target stays in the starting direction.
*/
@@ -229,13 +229,13 @@ public void write(JmeExporter ex) throws IOException {
@Override
public void read(JmeImporter im) throws IOException {
super.read(im);
- InputCapsule in = im.getCapsule(this);
- lookAt = (Vector3f) in.readSavable("lookAt", null);
- upVector = (Vector3f) in.readSavable("upVector", Vector3f.UNIT_Y);
- rotation = (Quaternion) in.readSavable("rotation", null);
- directionType = in.readEnum("directionType", Direction.class, Direction.None);
- path = (MotionPath) in.readSavable("path", null);
- spatial = (Spatial) in.readSavable("spatial", null);
+ InputCapsule ic = im.getCapsule(this);
+ lookAt = (Vector3f) ic.readSavable("lookAt", null);
+ upVector = (Vector3f) ic.readSavable("upVector", Vector3f.UNIT_Y);
+ rotation = (Quaternion) ic.readSavable("rotation", null);
+ directionType = ic.readEnum("directionType", Direction.class, Direction.None);
+ path = (MotionPath) ic.readSavable("path", null);
+ spatial = (Spatial) ic.readSavable("spatial", null);
}
/**
@@ -249,9 +249,8 @@ public boolean needsDirection() {
private void computeTargetDirection() {
switch (directionType) {
case Path:
- Quaternion q = new Quaternion();
- q.lookAt(direction, upVector);
- spatial.setLocalRotation(q);
+ tempRotation.lookAt(direction, upVector);
+ spatial.setLocalRotation(tempRotation);
break;
case LookAt:
if (lookAt != null) {
@@ -260,10 +259,9 @@ private void computeTargetDirection() {
break;
case PathAndRotation:
if (rotation != null) {
- Quaternion q2 = new Quaternion();
- q2.lookAt(direction, upVector);
- q2.multLocal(rotation);
- spatial.setLocalRotation(q2);
+ tempRotation.lookAt(direction, upVector);
+ tempRotation.multLocal(rotation);
+ spatial.setLocalRotation(tempRotation);
}
break;
case Rotation:
@@ -272,6 +270,7 @@ private void computeTargetDirection() {
}
break;
case None:
+ // no-op
break;
default:
break;
@@ -312,6 +311,9 @@ public Object jmeClone() {
@Override
public void cloneFields(Cloner cloner, Object original) {
+ this.lookAt = cloner.clone(lookAt);
+ this.path = cloner.clone(path);
+ this.rotation = cloner.clone(rotation);
this.spatial = cloner.clone(spatial);
}
@@ -373,8 +375,7 @@ public Vector3f getDirection() {
/**
* Sets the direction of the spatial, using the Y axis as the up vector.
- * Use MotionEvent#setDirection((Vector3f direction,Vector3f upVector) if
- * you want a custom up vector.
+ * If a custom up vector is desired, use {@link #setDirection(Vector3f, Vector3f)}.
* This method is used by the motion path.
*
* @param direction the desired forward direction (not null, unaffected)
diff --git a/jme3-core/src/main/java/com/jme3/collision/bih/BIHNode.java b/jme3-core/src/main/java/com/jme3/collision/bih/BIHNode.java
index 6f422baed3..12bf88b23a 100644
--- a/jme3-core/src/main/java/com/jme3/collision/bih/BIHNode.java
+++ b/jme3-core/src/main/java/com/jme3/collision/bih/BIHNode.java
@@ -411,15 +411,20 @@ public final int intersectWhere(Ray r,
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++;
+ }
}
}
}
diff --git a/jme3-core/src/main/java/com/jme3/effect/ParticleEmitter.java b/jme3-core/src/main/java/com/jme3/effect/ParticleEmitter.java
index a2d481f36b..338894bbdf 100644
--- a/jme3-core/src/main/java/com/jme3/effect/ParticleEmitter.java
+++ b/jme3-core/src/main/java/com/jme3/effect/ParticleEmitter.java
@@ -1124,7 +1124,7 @@ private void updateParticleState(float tpf) {
lastPos.set(getWorldTranslation());
//This check avoids a NaN bounds when all the particles are dead during the first update.
- if (!min.equals(Vector3f.POSITIVE_INFINITY) && !max.equals(Vector3f.NEGATIVE_INFINITY)) {
+ if (Vector3f.isValidVector(min) && Vector3f.isValidVector(max)) {
BoundingBox bbox = (BoundingBox) this.getMesh().getBound();
bbox.setMinMax(min, max);
this.setBoundRefresh();
diff --git a/jme3-core/src/main/java/com/jme3/effect/influencers/DefaultParticleInfluencer.java b/jme3-core/src/main/java/com/jme3/effect/influencers/DefaultParticleInfluencer.java
index d6d2c5ecf2..ba12a90e44 100644
--- a/jme3-core/src/main/java/com/jme3/effect/influencers/DefaultParticleInfluencer.java
+++ b/jme3-core/src/main/java/com/jme3/effect/influencers/DefaultParticleInfluencer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -40,6 +40,7 @@
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.util.clone.Cloner;
+
import java.io.IOException;
/**
@@ -101,13 +102,10 @@ public void read(JmeImporter im) throws IOException {
@Override
public DefaultParticleInfluencer clone() {
- try {
- DefaultParticleInfluencer clone = (DefaultParticleInfluencer) super.clone();
- clone.initialVelocity = initialVelocity.clone();
- return clone;
- } catch (CloneNotSupportedException e) {
- throw new AssertionError();
- }
+ // Set up the cloner for the type of cloning we want to do.
+ Cloner cloner = new Cloner();
+ DefaultParticleInfluencer clone = cloner.clone(this);
+ return clone;
}
/**
diff --git a/jme3-core/src/main/java/com/jme3/effect/influencers/NewtonianParticleInfluencer.java b/jme3-core/src/main/java/com/jme3/effect/influencers/NewtonianParticleInfluencer.java
index a026040b20..614285b38f 100644
--- a/jme3-core/src/main/java/com/jme3/effect/influencers/NewtonianParticleInfluencer.java
+++ b/jme3-core/src/main/java/com/jme3/effect/influencers/NewtonianParticleInfluencer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -54,6 +54,8 @@ public class NewtonianParticleInfluencer extends DefaultParticleInfluencer {
/** Emitters tangent rotation factor. */
protected float surfaceTangentRotation;
+ protected Matrix3f tempMat3 = new Matrix3f();
+
/**
* Constructor. Sets velocity variation to 0.0f.
*/
@@ -71,15 +73,15 @@ public void influenceParticle(Particle particle, EmitterShape emitterShape) {
// calculating surface tangent (velocity contains the 'normal' value)
temp.set(particle.velocity.z * surfaceTangentFactor, particle.velocity.y * surfaceTangentFactor, -particle.velocity.x * surfaceTangentFactor);
if (surfaceTangentRotation != 0.0f) {// rotating the tangent
- Matrix3f m = new Matrix3f();
- m.fromAngleNormalAxis(FastMath.PI * surfaceTangentRotation, particle.velocity);
- temp = m.multLocal(temp);
+ tempMat3.fromAngleNormalAxis(FastMath.PI * surfaceTangentRotation, particle.velocity);
+ temp = tempMat3.multLocal(temp);
}
// applying normal factor (this must be done first)
particle.velocity.multLocal(normalVelocity);
// adding tangent vector
particle.velocity.addLocal(temp);
}
+ particle.velocity.addLocal(initialVelocity);
if (velocityVariation != 0.0f) {
this.applyVelocityVariation(particle);
}
@@ -142,17 +144,6 @@ protected void applyVelocityVariation(Particle particle) {
particle.velocity.addLocal(temp);
}
- @Override
- public NewtonianParticleInfluencer clone() {
- NewtonianParticleInfluencer result = new NewtonianParticleInfluencer();
- result.normalVelocity = normalVelocity;
- result.initialVelocity = initialVelocity;
- result.velocityVariation = velocityVariation;
- result.surfaceTangentFactor = surfaceTangentFactor;
- result.surfaceTangentRotation = surfaceTangentRotation;
- return result;
- }
-
@Override
public void write(JmeExporter ex) throws IOException {
super.write(ex);
diff --git a/jme3-core/src/main/java/com/jme3/effect/influencers/ParticleInfluencer.java b/jme3-core/src/main/java/com/jme3/effect/influencers/ParticleInfluencer.java
index 1350463196..9fc273a121 100644
--- a/jme3-core/src/main/java/com/jme3/effect/influencers/ParticleInfluencer.java
+++ b/jme3-core/src/main/java/com/jme3/effect/influencers/ParticleInfluencer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2018 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -42,7 +42,7 @@
* An interface that defines the methods to affect initial velocity of the particles.
* @author Marcin Roguski (Kaelthas)
*/
-public interface ParticleInfluencer extends Savable, Cloneable, JmeCloneable {
+public interface ParticleInfluencer extends Savable, JmeCloneable {
/**
* This method influences the particle.
@@ -57,7 +57,7 @@ public interface ParticleInfluencer extends Savable, Cloneable, JmeCloneable {
* This method clones the influencer instance.
* @return cloned instance
*/
- public ParticleInfluencer clone();
+ ParticleInfluencer clone();
/**
* @param initialVelocity
diff --git a/jme3-core/src/main/java/com/jme3/effect/shapes/EmitterBoxShape.java b/jme3-core/src/main/java/com/jme3/effect/shapes/EmitterBoxShape.java
index 12f6645809..696ca5a83f 100644
--- a/jme3-core/src/main/java/com/jme3/effect/shapes/EmitterBoxShape.java
+++ b/jme3-core/src/main/java/com/jme3/effect/shapes/EmitterBoxShape.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -40,13 +40,35 @@
import com.jme3.util.clone.Cloner;
import java.io.IOException;
+/**
+ * An {@link EmitterShape} that emits particles randomly within the bounds of an axis-aligned box.
+ * The box is defined by a minimum corner and a length vector.
+ */
public class EmitterBoxShape implements EmitterShape {
- private Vector3f min, len;
+ /**
+ * The minimum corner of the box.
+ */
+ private Vector3f min;
+ /**
+ * The length of the box along each axis. The x, y, and z components of this
+ * vector represent the width, height, and depth of the box, respectively.
+ */
+ private Vector3f len;
+ /**
+ * For serialization only. Do not use.
+ */
public EmitterBoxShape() {
}
+ /**
+ * Constructs an {@code EmitterBoxShape} defined by a minimum and maximum corner.
+ *
+ * @param min The minimum corner of the box.
+ * @param max The maximum corner of the box.
+ * @throws IllegalArgumentException If either {@code min} or {@code max} is null.
+ */
public EmitterBoxShape(Vector3f min, Vector3f max) {
if (min == null || max == null) {
throw new IllegalArgumentException("min or max cannot be null");
@@ -57,6 +79,11 @@ public EmitterBoxShape(Vector3f min, Vector3f max) {
this.len.set(max).subtractLocal(min);
}
+ /**
+ * Generates a random point within the bounds of the box.
+ *
+ * @param store The {@link Vector3f} to store the generated point in.
+ */
@Override
public void getRandomPoint(Vector3f store) {
store.x = min.x + len.x * FastMath.nextRandomFloat();
@@ -65,10 +92,11 @@ public void getRandomPoint(Vector3f store) {
}
/**
- * This method fills the point with data.
- * It does not fill the normal.
- * @param store the variable to store the point data
- * @param normal not used in this class
+ * For a box shape, the normal is not well-defined for points within the volume.
+ * This implementation simply calls {@link #getRandomPoint(Vector3f)} and does not modify the provided normal.
+ *
+ * @param store The {@link Vector3f} to store the generated point in.
+ * @param normal The {@link Vector3f} to store the generated normal in (unused).
*/
@Override
public void getRandomPointAndNormal(Vector3f store, Vector3f normal) {
@@ -108,18 +136,40 @@ public void cloneFields(Cloner cloner, Object original) {
this.len = cloner.clone(len);
}
+ /**
+ * Returns the minimum corner of the emitting box.
+ *
+ * @return The minimum corner.
+ */
public Vector3f getMin() {
return min;
}
+ /**
+ * Sets the minimum corner of the emitting box.
+ *
+ * @param min The new minimum corner.
+ */
public void setMin(Vector3f min) {
this.min = min;
}
+ /**
+ * Returns the length vector of the emitting box. This vector represents the
+ * extent of the box along each axis (length = max - min).
+ *
+ * @return The length vector.
+ */
public Vector3f getLen() {
return len;
}
+ /**
+ * Sets the length vector of the emitting box. This vector should represent
+ * the extent of the box along each axis (length = max - min).
+ *
+ * @param len The new length vector.
+ */
public void setLen(Vector3f len) {
this.len = len;
}
diff --git a/jme3-core/src/main/java/com/jme3/effect/shapes/EmitterMeshFaceShape.java b/jme3-core/src/main/java/com/jme3/effect/shapes/EmitterMeshFaceShape.java
index cda0c911eb..cdaefa40de 100644
--- a/jme3-core/src/main/java/com/jme3/effect/shapes/EmitterMeshFaceShape.java
+++ b/jme3-core/src/main/java/com/jme3/effect/shapes/EmitterMeshFaceShape.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -36,6 +36,7 @@
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.util.BufferUtils;
+
import java.util.ArrayList;
import java.util.List;
@@ -52,79 +53,131 @@ public EmitterMeshFaceShape() {
}
/**
- * Constructor. It stores a copy of vertex list of all meshes.
- * @param meshes
- * a list of meshes that will form the emitter's shape
+ * Constructor. Initializes the emitter shape with a list of meshes.
+ * The vertices and normals for all triangles of these meshes are
+ * extracted and stored internally.
+ *
+ * @param meshes a list of {@link Mesh} objects that will define the
+ * shape from which particles are emitted.
*/
public EmitterMeshFaceShape(List meshes) {
super(meshes);
}
+ /**
+ * Sets the meshes for this emitter shape. This method extracts all
+ * triangle vertices and computes their normals, storing them internally
+ * for subsequent particle emission.
+ *
+ * @param meshes a list of {@link Mesh} objects to set as the emitter's shape.
+ */
@Override
public void setMeshes(List meshes) {
this.vertices = new ArrayList>(meshes.size());
this.normals = new ArrayList>(meshes.size());
+
for (Mesh mesh : meshes) {
Vector3f[] vertexTable = BufferUtils.getVector3Array(mesh.getFloatBuffer(Type.Position));
int[] indices = new int[3];
- List vertices = new ArrayList<>(mesh.getTriangleCount() * 3);
- List normals = new ArrayList<>(mesh.getTriangleCount());
+ List meshVertices = new ArrayList<>(mesh.getTriangleCount() * 3);
+ List meshNormals = new ArrayList<>(mesh.getTriangleCount());
+
for (int i = 0; i < mesh.getTriangleCount(); ++i) {
mesh.getTriangle(i, indices);
- vertices.add(vertexTable[indices[0]]);
- vertices.add(vertexTable[indices[1]]);
- vertices.add(vertexTable[indices[2]]);
- normals.add(FastMath.computeNormal(vertexTable[indices[0]], vertexTable[indices[1]], vertexTable[indices[2]]));
+
+ Vector3f v1 = vertexTable[indices[0]];
+ Vector3f v2 = vertexTable[indices[1]];
+ Vector3f v3 = vertexTable[indices[2]];
+
+ // Add all three vertices of the triangle
+ meshVertices.add(v1);
+ meshVertices.add(v2);
+ meshVertices.add(v3);
+
+ // Compute and add the normal for the current triangle face
+ meshNormals.add(FastMath.computeNormal(v1, v2, v3));
}
- this.vertices.add(vertices);
- this.normals.add(normals);
+ this.vertices.add(meshVertices);
+ this.normals.add(meshNormals);
}
}
/**
- * Randomly selects a point on a random face.
+ * Randomly selects a point on a random face of one of the stored meshes.
+ * The point is generated using barycentric coordinates to ensure uniform
+ * distribution within the selected triangle.
*
- * @param store
- * storage for the coordinates of the selected point
+ * @param store a {@link Vector3f} object where the coordinates of the
+ * selected point will be stored.
*/
@Override
public void getRandomPoint(Vector3f store) {
int meshIndex = FastMath.nextRandomInt(0, vertices.size() - 1);
+ List currVertices = vertices.get(meshIndex);
+ int numVertices = currVertices.size();
+
// the index of the first vertex of a face (must be dividable by 3)
- int vertIndex = FastMath.nextRandomInt(0, vertices.get(meshIndex).size() / 3 - 1) * 3;
- // put the point somewhere between the first and the second vertex of a face
- float moveFactor = FastMath.nextRandomFloat();
- store.set(Vector3f.ZERO);
- store.addLocal(vertices.get(meshIndex).get(vertIndex));
- store.addLocal((vertices.get(meshIndex).get(vertIndex + 1).x - vertices.get(meshIndex).get(vertIndex).x) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 1).y - vertices.get(meshIndex).get(vertIndex).y) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 1).z - vertices.get(meshIndex).get(vertIndex).z) * moveFactor);
- // move the result towards the last face vertex
- moveFactor = FastMath.nextRandomFloat();
- store.addLocal((vertices.get(meshIndex).get(vertIndex + 2).x - store.x) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 2).y - store.y) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 2).z - store.z) * moveFactor);
+ int faceIndex = FastMath.nextRandomInt(0, numVertices / 3 - 1);
+ int vertIndex = faceIndex * 3;
+
+ // Generate the random point on the triangle
+ generateRandomPointOnTriangle(currVertices, vertIndex, store);
}
/**
- * Randomly selects a point on a random face.
- * The {@code normal} argument is set to the normal of the selected face.
+ * Randomly selects a point on a random face of one of the stored meshes,
+ * and also sets the normal of that selected face.
+ * The point is generated using barycentric coordinates for uniform distribution.
*
- * @param store
- * storage for the coordinates of the selected point
- * @param normal
- * storage for the normal of the selected face
+ * @param store a {@link Vector3f} object where the coordinates of the
+ * selected point will be stored.
+ * @param normal a {@link Vector3f} object where the normal of the
+ * selected face will be stored.
*/
@Override
public void getRandomPointAndNormal(Vector3f store, Vector3f normal) {
int meshIndex = FastMath.nextRandomInt(0, vertices.size() - 1);
+ List currVertices = vertices.get(meshIndex);
+ int numVertices = currVertices.size();
+
// the index of the first vertex of a face (must be dividable by 3)
- int faceIndex = FastMath.nextRandomInt(0, vertices.get(meshIndex).size() / 3 - 1);
+ int faceIndex = FastMath.nextRandomInt(0, numVertices / 3 - 1);
int vertIndex = faceIndex * 3;
- // put the point somewhere between the first and the second vertex of a face
- float moveFactor = FastMath.nextRandomFloat();
- store.set(Vector3f.ZERO);
- store.addLocal(vertices.get(meshIndex).get(vertIndex));
- store.addLocal((vertices.get(meshIndex).get(vertIndex + 1).x - vertices.get(meshIndex).get(vertIndex).x) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 1).y - vertices.get(meshIndex).get(vertIndex).y) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 1).z - vertices.get(meshIndex).get(vertIndex).z) * moveFactor);
- // move the result towards the last face vertex
- moveFactor = FastMath.nextRandomFloat();
- store.addLocal((vertices.get(meshIndex).get(vertIndex + 2).x - store.x) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 2).y - store.y) * moveFactor, (vertices.get(meshIndex).get(vertIndex + 2).z - store.z) * moveFactor);
+
+ // Generate the random point on the triangle
+ generateRandomPointOnTriangle(currVertices, vertIndex, store);
+ // Set the normal from the pre-computed normals list for the selected face
normal.set(normals.get(meshIndex).get(faceIndex));
}
+
+ /**
+ * Internal method to generate a random point within a specific triangle
+ * using barycentric coordinates.
+ *
+ * @param currVertices The list of vertices for the current mesh.
+ * @param vertIndex The starting index of the triangle's first vertex
+ * within the {@code currVertices} list.
+ * @param store A {@link Vector3f} object where the calculated point will be stored.
+ */
+ private void generateRandomPointOnTriangle(List currVertices, int vertIndex, Vector3f store) {
+
+ Vector3f v1 = currVertices.get(vertIndex);
+ Vector3f v2 = currVertices.get(vertIndex + 1);
+ Vector3f v3 = currVertices.get(vertIndex + 2);
+
+ // Generate random barycentric coordinates
+ float u = FastMath.nextRandomFloat();
+ float v = FastMath.nextRandomFloat();
+
+ if ((u + v) > 1) {
+ u = 1 - u;
+ v = 1 - v;
+ }
+
+ // P = v1 + u * (v2 - v1) + v * (v3 - v1)
+ store.x = v1.x + u * (v2.x - v1.x) + v * (v3.x - v1.x);
+ store.y = v1.y + u * (v2.y - v1.y) + v * (v3.y - v1.y);
+ store.z = v1.z + u * (v2.z - v1.z) + v * (v3.z - v1.z);
+ }
+
}
diff --git a/jme3-core/src/main/java/com/jme3/effect/shapes/EmitterPointShape.java b/jme3-core/src/main/java/com/jme3/effect/shapes/EmitterPointShape.java
index 0eca81c226..d4b3f44d70 100644
--- a/jme3-core/src/main/java/com/jme3/effect/shapes/EmitterPointShape.java
+++ b/jme3-core/src/main/java/com/jme3/effect/shapes/EmitterPointShape.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -31,6 +31,7 @@
*/
package com.jme3.effect.shapes;
+import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
@@ -38,13 +39,27 @@
import com.jme3.util.clone.Cloner;
import java.io.IOException;
+/**
+ * An {@link EmitterShape} that emits particles from a single point in space.
+ */
public class EmitterPointShape implements EmitterShape {
+ /**
+ * The point in space from which particles are emitted.
+ */
private Vector3f point;
+ /**
+ * For serialization only. Do not use.
+ */
public EmitterPointShape() {
}
+ /**
+ * Constructs an {@code EmitterPointShape} with the given point.
+ *
+ * @param point The point from which particles are emitted.
+ */
public EmitterPointShape(Vector3f point) {
this.point = point;
}
@@ -80,26 +95,43 @@ public void cloneFields(Cloner cloner, Object original) {
this.point = cloner.clone(point);
}
+ /**
+ * For a point shape, the generated point is
+ * always the shape's defined point.
+ *
+ * @param store The {@link Vector3f} to store the generated point in.
+ */
@Override
public void getRandomPoint(Vector3f store) {
store.set(point);
}
/**
- * This method fills the point with data.
- * It does not fill the normal.
- * @param store the variable to store the point data
- * @param normal not used in this class
+ * For a point shape, the generated point is always the shape's defined point.
+ * The normal is not defined for a point shape, so this method does not modify the normal parameter.
+ *
+ * @param store The {@link Vector3f} to store the generated point in.
+ * @param normal The {@link Vector3f} to store the generated normal in (unused).
*/
@Override
public void getRandomPointAndNormal(Vector3f store, Vector3f normal) {
store.set(point);
}
+ /**
+ * Returns the point from which particles are emitted.
+ *
+ * @return The point.
+ */
public Vector3f getPoint() {
return point;
}
+ /**
+ * Sets the point from which particles are emitted.
+ *
+ * @param point The new point.
+ */
public void setPoint(Vector3f point) {
this.point = point;
}
@@ -112,6 +144,7 @@ public void write(JmeExporter ex) throws IOException {
@Override
public void read(JmeImporter im) throws IOException {
- this.point = (Vector3f) im.getCapsule(this).readSavable("point", null);
+ InputCapsule ic = im.getCapsule(this);
+ this.point = (Vector3f) ic.readSavable("point", null);
}
}
diff --git a/jme3-core/src/main/java/com/jme3/effect/shapes/EmitterShape.java b/jme3-core/src/main/java/com/jme3/effect/shapes/EmitterShape.java
index f247412d04..776a4e33ee 100644
--- a/jme3-core/src/main/java/com/jme3/effect/shapes/EmitterShape.java
+++ b/jme3-core/src/main/java/com/jme3/effect/shapes/EmitterShape.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -39,14 +39,14 @@
* This interface declares methods used by all shapes that represent particle emitters.
* @author Kirill
*/
-public interface EmitterShape extends Savable, Cloneable, JmeCloneable {
+public interface EmitterShape extends Savable, JmeCloneable {
/**
* This method fills in the initial position of the particle.
* @param store
* store variable for initial position
*/
- public void getRandomPoint(Vector3f store);
+ void getRandomPoint(Vector3f store);
/**
* This method fills in the initial position of the particle and its normal vector.
@@ -55,11 +55,11 @@ public interface EmitterShape extends Savable, Cloneable, JmeCloneable {
* @param normal
* store variable for initial normal
*/
- public void getRandomPointAndNormal(Vector3f store, Vector3f normal);
+ void getRandomPointAndNormal(Vector3f store, Vector3f normal);
/**
* This method creates a deep clone of the current instance of the emitter shape.
* @return deep clone of the current instance of the emitter shape
*/
- public EmitterShape deepClone();
+ EmitterShape deepClone();
}
diff --git a/jme3-core/src/main/java/com/jme3/effect/shapes/EmitterSphereShape.java b/jme3-core/src/main/java/com/jme3/effect/shapes/EmitterSphereShape.java
index a9d7dabca7..30ec7357d4 100644
--- a/jme3-core/src/main/java/com/jme3/effect/shapes/EmitterSphereShape.java
+++ b/jme3-core/src/main/java/com/jme3/effect/shapes/EmitterSphereShape.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -40,19 +40,38 @@
import com.jme3.util.clone.Cloner;
import java.io.IOException;
+/**
+ * An {@link EmitterShape} that emits particles randomly from within the volume of a sphere.
+ * The sphere is defined by a center point and a radius.
+ */
public class EmitterSphereShape implements EmitterShape {
+ /**
+ * The center point of the sphere.
+ */
private Vector3f center;
+ /**
+ * The radius of the sphere.
+ */
private float radius;
+ /**
+ * For serialization only. Do not use.
+ */
public EmitterSphereShape() {
}
+ /**
+ * Constructs an {@code EmitterSphereShape} with the given center and radius.
+ *
+ * @param center The center point of the sphere.
+ * @param radius The radius of the sphere.
+ * @throws IllegalArgumentException If {@code center} is null, or if {@code radius} is not greater than 0.
+ */
public EmitterSphereShape(Vector3f center, float radius) {
if (center == null) {
throw new IllegalArgumentException("center cannot be null");
}
-
if (radius <= 0) {
throw new IllegalArgumentException("Radius must be greater than 0");
}
@@ -92,6 +111,11 @@ public void cloneFields(Cloner cloner, Object original) {
this.center = cloner.clone(center);
}
+ /**
+ * Generates a random point within the volume of the sphere.
+ *
+ * @param store The {@link Vector3f} to store the generated point in.
+ */
@Override
public void getRandomPoint(Vector3f store) {
do {
@@ -103,23 +127,51 @@ public void getRandomPoint(Vector3f store) {
store.addLocal(center);
}
+ /**
+ * For a sphere shape, the normal is not well-defined for points within the volume.
+ * This implementation simply calls {@link #getRandomPoint(Vector3f)} and does not modify the provided normal.
+ *
+ * @param store The {@link Vector3f} to store the generated point in.
+ * @param normal The {@link Vector3f} to store the generated normal in (unused).
+ */
@Override
public void getRandomPointAndNormal(Vector3f store, Vector3f normal) {
this.getRandomPoint(store);
+ normal.set(store).subtractLocal(center).normalizeLocal();
}
+ /**
+ * Returns the center point of the sphere.
+ *
+ * @return The center point.
+ */
public Vector3f getCenter() {
return center;
}
+ /**
+ * Sets the center point of the sphere.
+ *
+ * @param center The new center point.
+ */
public void setCenter(Vector3f center) {
this.center = center;
}
+ /**
+ * Returns the radius of the sphere.
+ *
+ * @return The radius.
+ */
public float getRadius() {
return radius;
}
+ /**
+ * Sets the radius of the sphere.
+ *
+ * @param radius The new radius.
+ */
public void setRadius(float radius) {
this.radius = radius;
}
diff --git a/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java b/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java
new file mode 100644
index 0000000000..6f07fd1c1c
--- /dev/null
+++ b/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java
@@ -0,0 +1,352 @@
+/*
+ * 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:
+ * 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);
+ * }
+ * });
+ *
+ *
+ * @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 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");
+ }
+
+}
diff --git a/jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java b/jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java
new file mode 100644
index 0000000000..12ba5e99c1
--- /dev/null
+++ b/jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java
@@ -0,0 +1,124 @@
+/*
+ * 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.baker.IBLHybridEnvBakerLight;
+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) {
+ IBLHybridEnvBakerLight 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;
+ }
+
+}
diff --git a/jme3-core/src/main/java/com/jme3/environment/baker/EnvBaker.java b/jme3-core/src/main/java/com/jme3/environment/baker/EnvBaker.java
new file mode 100644
index 0000000000..65ee9805f1
--- /dev/null
+++ b/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 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();
+}
diff --git a/jme3-core/src/main/java/com/jme3/environment/baker/GenericEnvBaker.java b/jme3-core/src/main/java/com/jme3/environment/baker/GenericEnvBaker.java
new file mode 100644
index 0000000000..6831914945
--- /dev/null
+++ b/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 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 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 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;
+ }
+
+}
diff --git a/jme3-core/src/main/java/com/jme3/environment/baker/IBLEnvBaker.java b/jme3-core/src/main/java/com/jme3/environment/baker/IBLEnvBaker.java
new file mode 100644
index 0000000000..982ccc79df
--- /dev/null
+++ b/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();
+}
diff --git a/jme3-core/src/main/java/com/jme3/environment/baker/IBLEnvBakerLight.java b/jme3-core/src/main/java/com/jme3/environment/baker/IBLEnvBakerLight.java
new file mode 100644
index 0000000000..19275d514e
--- /dev/null
+++ b/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();
+}
\ No newline at end of file
diff --git a/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBaker.java b/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBaker.java
new file mode 100644
index 0000000000..5b79a4922f
--- /dev/null
+++ b/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();
+
+ }
+
+}
\ No newline at end of file
diff --git a/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java b/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java
new file mode 100644
index 0000000000..f6284f14ea
--- /dev/null
+++ b/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java
@@ -0,0 +1,178 @@
+/*
+ * 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.environment.util.EnvMapUtils;
+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]);
+ } else {
+ renderOnT = 0;
+ }
+
+ mat.setInt("FaceId", faceId);
+
+ 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);
+ }
+ EnvMapUtils.prepareShCoefs(shCoef);
+ img.dispose();
+
+ }
+}
diff --git a/jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java b/jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java
new file mode 100644
index 0000000000..26b3c1cd65
--- /dev/null
+++ b/jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java
@@ -0,0 +1,210 @@
+/*
+ * 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());
+ EnvMapUtils.prepareShCoefs(shCoef);
+ }
+
+ @Override
+ public Vector3f[] getSphericalHarmonicsCoefficients() {
+ return shCoef;
+ }
+}
\ No newline at end of file
diff --git a/jme3-core/src/main/java/com/jme3/environment/util/BoundingSphereDebug.java b/jme3-core/src/main/java/com/jme3/environment/util/BoundingSphereDebug.java
index c5510dedde..622164e4a1 100644
--- a/jme3-core/src/main/java/com/jme3/environment/util/BoundingSphereDebug.java
+++ b/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 @@ private void setIndexData() {
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;
-
}
+
}
diff --git a/jme3-core/src/main/java/com/jme3/environment/util/Circle.java b/jme3-core/src/main/java/com/jme3/environment/util/Circle.java
new file mode 100644
index 0000000000..34dac36f7c
--- /dev/null
+++ b/jme3-core/src/main/java/com/jme3/environment/util/Circle.java
@@ -0,0 +1,167 @@
+ /*
+ * Copyright (c) 2009-2025 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.environment.util;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.material.Material;
+import com.jme3.material.RenderState;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.renderer.queue.RenderQueue;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.util.BufferUtils;
+
+import java.io.IOException;
+import java.nio.FloatBuffer;
+import java.nio.ShortBuffer;
+
+/**
+ *
A `Circle` is a 2D mesh representing a circular outline (wireframe).
+ * It's defined by a specified number of radial samples, which determine its smoothness.
+ *
+ *
The circle is centered at (0,0,0) in its local coordinate space and has a radius of 1.0.
+ *
+ * @author capdevon
+ */
+public class Circle extends Mesh {
+
+ // The number of segments used to approximate the circle.
+ protected int radialSamples = 256;
+
+ /**
+ * Creates a new `Circle` mesh.
+ */
+ public Circle() {
+ setGeometryData();
+ setIndexData();
+ }
+
+ /**
+ * Initializes the vertex buffers for the circle mesh.
+ */
+ private void setGeometryData() {
+
+ int numVertices = radialSamples + 1;
+
+ FloatBuffer posBuf = BufferUtils.createVector3Buffer(numVertices);
+ FloatBuffer colBuf = BufferUtils.createFloatBuffer(numVertices * 4);
+ FloatBuffer texBuf = BufferUtils.createVector2Buffer(numVertices);
+
+ // --- Generate Geometry Data ---
+ float angleStep = FastMath.TWO_PI / radialSamples;
+
+ // Define the color for the entire circle.
+ ColorRGBA color = ColorRGBA.Orange;
+
+ // Populate the position, color, and texture coordinate buffers.
+ for (int i = 0; i < numVertices; i++) {
+ float angle = angleStep * i;
+ float cos = FastMath.cos(angle);
+ float sin = FastMath.sin(angle);
+
+ posBuf.put(cos).put(sin).put(0);
+ colBuf.put(color.r).put(color.g).put(color.b).put(color.a);
+ texBuf.put(i % 2f).put(i % 2f);
+ }
+
+ setBuffer(Type.Position, 3, posBuf);
+ setBuffer(Type.Color, 4, colBuf);
+ setBuffer(Type.TexCoord, 2, texBuf);
+
+ setMode(Mode.Lines);
+ updateBound();
+ setStatic();
+ }
+
+ /**
+ * Initializes the index buffer for the circle mesh.
+ */
+ private void setIndexData() {
+ // allocate connectivity
+ int numIndices = radialSamples * 2;
+
+ ShortBuffer idxBuf = BufferUtils.createShortBuffer(numIndices);
+ setBuffer(Type.Index, 2, idxBuf);
+
+ // --- Generate Index Data ---
+ for (int i = 0; i < radialSamples; i++) {
+ idxBuf.put((short) i); // Start of segment
+ idxBuf.put((short) (i + 1)); // End of segment
+ }
+ }
+
+ /**
+ * Creates a {@link Geometry} object representing a dashed wireframe circle.
+ *
+ * @param assetManager The application's AssetManager to load materials.
+ * @param name The desired name for the Geometry.
+ * @return A new Geometry instance with a `Circle` mesh.
+ */
+ public static Geometry createShape(AssetManager assetManager, String name) {
+ Circle mesh = new Circle();
+ Geometry geom = new Geometry(name, mesh);
+ geom.setQueueBucket(RenderQueue.Bucket.Transparent);
+
+ Material mat = new Material(assetManager, "Common/MatDefs/Misc/Dashed.j3md");
+ mat.getAdditionalRenderState().setWireframe(true);
+ mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
+ mat.getAdditionalRenderState().setDepthWrite(false);
+ mat.getAdditionalRenderState().setDepthTest(false);
+ mat.getAdditionalRenderState().setLineWidth(2f);
+ mat.setColor("Color", ColorRGBA.Orange);
+ mat.setFloat("DashSize", 0.5f);
+ geom.setMaterial(mat);
+
+ return geom;
+ }
+
+ @Override
+ public void write(JmeExporter ex) throws IOException {
+ super.write(ex);
+ OutputCapsule oc = ex.getCapsule(this);
+ oc.write(radialSamples, "radialSamples", 256);
+ }
+
+ @Override
+ public void read(JmeImporter im) throws IOException {
+ super.read(im);
+ InputCapsule ic = im.getCapsule(this);
+ radialSamples = ic.readInt("radialSamples", 256);
+ }
+
+}
diff --git a/jme3-core/src/main/java/com/jme3/environment/util/LightsDebugState.java b/jme3-core/src/main/java/com/jme3/environment/util/LightsDebugState.java
index e2a159bf04..3653c35ec3 100644
--- a/jme3-core/src/main/java/com/jme3/environment/util/LightsDebugState.java
+++ b/jme3-core/src/main/java/com/jme3/environment/util/LightsDebugState.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -33,154 +33,427 @@
import com.jme3.app.Application;
import com.jme3.app.state.BaseAppState;
-import com.jme3.light.*;
+import com.jme3.asset.AssetManager;
+import com.jme3.light.DirectionalLight;
+import com.jme3.light.Light;
+import com.jme3.light.LightProbe;
+import com.jme3.light.PointLight;
+import com.jme3.light.SpotLight;
import com.jme3.material.Material;
+import com.jme3.material.RenderState;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
+import com.jme3.scene.control.BillboardControl;
+import com.jme3.scene.debug.Arrow;
+import com.jme3.scene.shape.Quad;
import com.jme3.scene.shape.Sphere;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
+import com.jme3.texture.Texture;
+
+import java.util.ArrayDeque;
+import java.util.Iterator;
import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.function.Predicate;
/**
- * A debug state that will display Light gizmos on screen.
- * Still a wip and for now it only displays light probes.
+ * A debug state that visualizes different types of lights in the scene with gizmos.
+ * This state is useful for debugging light positions, ranges, and other properties.
*
* @author nehon
+ * @author capdevon
*/
public class LightsDebugState extends BaseAppState {
- private Node debugNode;
- private final Map probeMapping = new HashMap<>();
- private final List garbage = new ArrayList<>();
- private Geometry debugGeom;
- private Geometry debugBounds;
+ private static final String PROBE_GEOMETRY_NAME = "DebugProbeGeometry";
+ private static final String PROBE_BOUNDS_NAME = "DebugProbeBounds";
+ private static final String SPOT_LIGHT_INNER_RADIUS_NAME = "SpotLightInnerRadius";
+ private static final String SPOT_LIGHT_OUTER_RADIUS_NAME = "SpotLightOuterRadius";
+ private static final String SPOT_LIGHT_RADIUS_NAME = "RadiusNode";
+ private static final String POINT_LIGHT_RADIUS_NAME = "PointLightRadius";
+ private static final String LIGHT_DIR_ARROW_NAME = "LightDirection";
+
+ private final Map lightGizmoMap = new WeakHashMap<>();
+ private final ArrayDeque lightDeque = new ArrayDeque<>();
+ private Predicate lightFilter = x -> true; // Identity Function
+
+ private ViewPort viewPort;
+ private AssetManager assetManager;
private Material debugMaterial;
- private float probeScale = 1.0f;
- private Spatial scene = null;
- private final List probes = new ArrayList<>();
+ private Node debugNode;
+ private Spatial scene; // The scene whose lights will be debugged
+
+ private boolean showOnTop = true;
+ private float lightProbeScale = 1.0f;
+ private final ColorRGBA debugColor = ColorRGBA.DarkGray;
+ private final Quaternion tempRotation = new Quaternion();
@Override
protected void initialize(Application app) {
- debugNode = new Node("Environment debug Node");
- Sphere s = new Sphere(16, 16, 0.15f);
- debugGeom = new Geometry("debugEnvProbe", s);
- debugMaterial = new Material(app.getAssetManager(), "Common/MatDefs/Misc/reflect.j3md");
- debugGeom.setMaterial(debugMaterial);
- debugBounds = BoundingSphereDebug.createDebugSphere(app.getAssetManager());
+
+ viewPort = app.getRenderManager().createMainView("LightsDebugView", app.getCamera());
+ viewPort.setClearFlags(false, showOnTop, true);
+
+ assetManager = app.getAssetManager();
+ debugNode = new Node("LightsDebugNode");
+
+ debugMaterial = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+ debugMaterial.setColor("Color", debugColor);
+ debugMaterial.getAdditionalRenderState().setWireframe(true);
+
if (scene == null) {
scene = app.getViewPort().getScenes().get(0);
}
}
+ private Spatial createBulb() {
+ Quad q = new Quad(0.5f, 0.5f);
+ Geometry lightBulb = new Geometry("LightBulb", q);
+ lightBulb.move(-q.getHeight() / 2f, -q.getWidth() / 2f, 0);
+
+ Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+ Texture tex = assetManager.loadTexture("Common/Textures/lightbulb32.png");
+ mat.setTexture("ColorMap", tex);
+ mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
+ lightBulb.setMaterial(mat);
+ lightBulb.setQueueBucket(RenderQueue.Bucket.Transparent);
+
+ Node billboard = new Node("Billboard");
+ billboard.addControl(new BillboardControl());
+ billboard.attachChild(lightBulb);
+
+ return billboard;
+ }
+
+ private Geometry createRadiusShape(String name, float dashSize) {
+ Geometry radius = Circle.createShape(assetManager, name);
+ Material mat = radius.getMaterial();
+ mat.setColor("Color", debugColor);
+ mat.setFloat("DashSize", dashSize);
+ return radius;
+ }
+
+ private Spatial createPointGizmo() {
+ Node gizmo = new Node("PointLightNode");
+ gizmo.attachChild(createBulb());
+
+ Geometry radius = new Geometry(POINT_LIGHT_RADIUS_NAME, new BoundingSphereDebug());
+ radius.setMaterial(debugMaterial);
+ gizmo.attachChild(radius);
+
+ return gizmo;
+ }
+
+ private Spatial createDirectionalGizmo() {
+ Node gizmo = new Node("DirectionalLightNode");
+ gizmo.move(0, 5, 0);
+ gizmo.attachChild(createBulb());
+
+ Geometry arrow = new Geometry(LIGHT_DIR_ARROW_NAME, new Arrow(Vector3f.UNIT_Z.mult(5f)));
+ arrow.setMaterial(debugMaterial);
+ gizmo.attachChild(arrow);
+
+ return gizmo;
+ }
+
+ private Spatial createSpotGizmo() {
+ Node gizmo = new Node("SpotLightNode");
+ gizmo.attachChild(createBulb());
+
+ Node radiusNode = new Node(SPOT_LIGHT_RADIUS_NAME);
+ gizmo.attachChild(radiusNode);
+
+ Geometry inRadius = createRadiusShape(SPOT_LIGHT_INNER_RADIUS_NAME, 0.725f);
+ radiusNode.attachChild(inRadius);
+
+ Geometry outRadius = createRadiusShape(SPOT_LIGHT_OUTER_RADIUS_NAME, 0.325f);
+ radiusNode.attachChild(outRadius);
+
+ Geometry arrow = new Geometry(LIGHT_DIR_ARROW_NAME, new Arrow(Vector3f.UNIT_Z));
+ arrow.setMaterial(debugMaterial);
+ gizmo.attachChild(arrow);
+
+ return gizmo;
+ }
+
+ private Spatial createLightProbeGizmo() {
+ Node gizmo = new Node("LightProbeNode");
+
+ Sphere sphere = new Sphere(32, 32, lightProbeScale);
+ Geometry probeGeom = new Geometry(PROBE_GEOMETRY_NAME, sphere);
+ Material mat = new Material(assetManager, "Common/MatDefs/Misc/reflect.j3md");
+ probeGeom.setMaterial(mat);
+ gizmo.attachChild(probeGeom);
+
+ Geometry probeBounds = BoundingSphereDebug.createDebugSphere(assetManager);
+ probeBounds.setName(PROBE_BOUNDS_NAME);
+ gizmo.attachChild(probeBounds);
+
+ return gizmo;
+ }
+
+ /**
+ * Updates the light gizmos based on the current state of lights in the scene.
+ * This method is called every frame when the state is enabled.
+ *
+ * @param tpf The time per frame.
+ */
@Override
public void update(float tpf) {
- if (!isEnabled()) {
- return;
- }
- updateLights(scene);
+ updateLightGizmos(scene);
debugNode.updateLogicalState(tpf);
+ cleanUpRemovedLights();
+ }
+
+ /**
+ * Renders the debug gizmos onto the screen.
+ *
+ * @param rm The render manager.
+ */
+ @Override
+ public void render(RenderManager rm) {
debugNode.updateGeometricState();
- cleanProbes();
- }
-
- public void updateLights(Spatial scene) {
- for (Light light : scene.getWorldLightList()) {
- switch (light.getType()) {
-
- case Probe:
- LightProbe probe = (LightProbe) light;
- probes.add(probe);
- Node n = probeMapping.get(probe);
- if (n == null) {
- n = new Node("DebugProbe");
- n.attachChild(debugGeom.clone(true));
- n.attachChild(debugBounds.clone(false));
- debugNode.attachChild(n);
- probeMapping.put(probe, n);
- }
- Geometry probeGeom = ((Geometry) n.getChild(0));
- Material m = probeGeom.getMaterial();
- probeGeom.setLocalScale(probeScale);
- if (probe.isReady()) {
- m.setTexture("CubeMap", probe.getPrefilteredEnvMap());
- }
- n.setLocalTranslation(probe.getPosition());
- n.getChild(1).setLocalScale(probe.getArea().getRadius());
- break;
- default:
- break;
+ }
+
+ /**
+ * Recursively traverses the scene graph to find and update light gizmos.
+ * New gizmos are created for new lights, and existing gizmos are updated.
+ *
+ * @param spatial The current spatial to process for lights.
+ */
+ private void updateLightGizmos(Spatial spatial) {
+ // Add or update gizmos for lights attached to the current spatial
+ for (Light light : spatial.getLocalLightList()) {
+ if (!lightFilter.test(light)) {
+ continue;
+ }
+
+ lightDeque.add(light);
+ Spatial gizmo = lightGizmoMap.get(light);
+
+ if (gizmo == null) {
+ gizmo = createLightGizmo(light);
+ if (gizmo != null) {
+ debugNode.attachChild(gizmo);
+ lightGizmoMap.put(light, gizmo);
+ updateGizmoProperties(light, gizmo); // Set initial properties
+ }
+ } else {
+ updateGizmoProperties(light, gizmo);
}
}
- if (scene instanceof Node) {
- Node n = (Node)scene;
- for (Spatial spatial : n.getChildren()) {
- updateLights(spatial);
+
+ // Recursively call for children if it's a Node
+ if (spatial instanceof Node) {
+ Node node = (Node) spatial;
+ for (Spatial child : node.getChildren()) {
+ updateLightGizmos(child);
}
}
}
/**
- * Set the scenes for which to render light gizmos.
+ * Creates a new gizmo spatial for a given light based on its type.
*
- * @param scene the root of the desired scene (alias created)
+ * @param light The light for which to create a gizmo.
+ * @return A spatial representing the gizmo, or null if the light type is not supported.
*/
- public void setScene(Spatial scene) {
- this.scene = scene;
+ private Spatial createLightGizmo(Light light) {
+ switch (light.getType()) {
+ case Probe:
+ return createLightProbeGizmo();
+ case Point:
+ return createPointGizmo();
+ case Directional:
+ return createDirectionalGizmo();
+ case Spot:
+ return createSpotGizmo();
+ default:
+ // Unsupported light type
+ return null;
+ }
}
- private void cleanProbes() {
- if (probes.size() != probeMapping.size()) {
- for (LightProbe probe : probeMapping.keySet()) {
- if (!probes.contains(probe)) {
- garbage.add(probe);
+ /**
+ * Updates the visual properties and position of a light gizmo based on its corresponding light.
+ *
+ * @param light The light whose properties are used for updating the gizmo.
+ * @param gizmo The spatial representing the light gizmo.
+ */
+ private void updateGizmoProperties(Light light, Spatial gizmo) {
+ Node lightNode = (Node) gizmo;
+
+ switch (light.getType()) {
+ case Probe:
+ LightProbe probe = (LightProbe) light;
+ Geometry probeGeom = (Geometry) lightNode.getChild(PROBE_GEOMETRY_NAME);
+ Geometry probeBounds = (Geometry) lightNode.getChild(PROBE_BOUNDS_NAME);
+
+ // Update texture if probe is ready
+ if (probe.isReady()) {
+ Material mat = probeGeom.getMaterial();
+ if (mat.getTextureParam("CubeMap") == null) {
+ mat.setTexture("CubeMap", probe.getPrefilteredEnvMap());
+ }
}
- }
- for (LightProbe probe : garbage) {
- probeMapping.remove(probe);
- }
- garbage.clear();
- probes.clear();
+ probeGeom.setLocalScale(lightProbeScale);
+ probeBounds.setLocalScale(probe.getArea().getRadius());
+ gizmo.setLocalTranslation(probe.getPosition());
+ break;
+
+ case Point:
+ PointLight pl = (PointLight) light;
+ Geometry radius = (Geometry) lightNode.getChild(POINT_LIGHT_RADIUS_NAME);
+ radius.setLocalScale(pl.getRadius());
+ gizmo.setLocalTranslation(pl.getPosition());
+ break;
+
+ case Spot:
+ SpotLight sl = (SpotLight) light;
+ gizmo.setLocalTranslation(sl.getPosition());
+
+ tempRotation.lookAt(sl.getDirection(), Vector3f.UNIT_Y);
+ gizmo.setLocalRotation(tempRotation);
+
+ float spotRange = sl.getSpotRange();
+ float innerAngle = sl.getSpotInnerAngle();
+ float outerAngle = sl.getSpotOuterAngle();
+ float innerRadius = spotRange * FastMath.tan(innerAngle);
+ float outerRadius = spotRange * FastMath.tan(outerAngle);
+
+ lightNode.getChild(SPOT_LIGHT_INNER_RADIUS_NAME).setLocalScale(innerRadius);
+ lightNode.getChild(SPOT_LIGHT_OUTER_RADIUS_NAME).setLocalScale(outerRadius);
+ lightNode.getChild(SPOT_LIGHT_RADIUS_NAME).setLocalTranslation(0, 0, spotRange);
+ lightNode.getChild(LIGHT_DIR_ARROW_NAME).setLocalScale(spotRange);
+ break;
+
+ case Directional:
+ DirectionalLight dl = (DirectionalLight) light;
+ tempRotation.lookAt(dl.getDirection(), Vector3f.UNIT_Y);
+ gizmo.setLocalRotation(tempRotation);
+ break;
+
+ default:
+ // Unsupported light type
+ break;
}
}
- @Override
- public void render(RenderManager rm) {
- if (!isEnabled()) {
- return;
+ /**
+ * Cleans up gizmos for lights that have been removed from the scene.
+ */
+ private void cleanUpRemovedLights() {
+
+ Iterator> iterator = lightGizmoMap.entrySet().iterator();
+
+ while (iterator.hasNext()) {
+ Map.Entry entry = iterator.next();
+ Light light = entry.getKey();
+
+ if (!lightDeque.contains(light)) {
+ Spatial gizmo = entry.getValue();
+ gizmo.removeFromParent();
+ iterator.remove();
+ }
}
- rm.renderScene(debugNode, getApplication().getViewPort());
+
+ lightDeque.clear();
}
/**
- * returns the scale of the probe's debug sphere
- * @return the scale factor
+ * Sets the scene for which to render light gizmos.
+ * If no scene is set, it defaults to the first scene in the viewport.
+ *
+ * @param scene The root of the desired scene.
+ */
+ public void setScene(Spatial scene) {
+ this.scene = scene;
+ // Clear existing gizmos when the scene changes to avoid displaying gizmos from the old scene
+ debugNode.detachAllChildren();
+ lightGizmoMap.clear();
+ lightDeque.clear();
+ }
+
+ /**
+ * Returns the current scale of the light probe's debug sphere.
+ *
+ * @return The scale factor.
+ */
+ public float getLightProbeScale() {
+ return lightProbeScale;
+ }
+
+ /**
+ * Sets the scale of the light probe's debug sphere.
+ *
+ * @param scale The scale factor (default is 1.0).
+ */
+ public void setLightProbeScale(float scale) {
+ this.lightProbeScale = scale;
+ }
+
+ /**
+ * Checks if the light debug gizmos are set to always
+ * render on top of other scene geometry.
+ *
+ * @return true if gizmos always render on top, false otherwise.
+ */
+ public boolean isShowOnTop() {
+ return showOnTop;
+ }
+
+ /**
+ * Sets whether light debug gizmos should always
+ * render on top of other scene geometry.
+ *
+ * @param showOnTop true to always show gizmos on top, false to respect depth.
*/
- public float getProbeScale() {
- return probeScale;
+ public void setShowOnTop(boolean showOnTop) {
+ this.showOnTop = showOnTop;
+ if (viewPort != null) {
+ viewPort.setClearDepth(showOnTop);
+ }
}
/**
- * sets the scale of the probe's debug sphere
+ * Sets a filter to control which lights are displayed by the debug state.
+ * By default, no filter is applied, meaning all lights are displayed.
*
- * @param probeScale the scale factor (default=1)
+ * @param lightFilter A {@link Predicate} that tests a {@link Light} object.
*/
- public void setProbeScale(float probeScale) {
- this.probeScale = probeScale;
+ public void setLightFilter(Predicate lightFilter) {
+ this.lightFilter = lightFilter;
}
+ /**
+ * Cleans up resources when the app state is detached.
+ *
+ * @param app The application instance.
+ */
@Override
protected void cleanup(Application app) {
+ debugNode.detachAllChildren();
+ lightGizmoMap.clear();
+ lightDeque.clear();
+ debugMaterial = null;
+ app.getRenderManager().removeMainView(viewPort);
}
@Override
protected void onEnable() {
+ viewPort.attachScene(debugNode);
}
@Override
protected void onDisable() {
+ viewPort.detachScene(debugNode);
}
+
}
diff --git a/jme3-core/src/main/java/com/jme3/export/FormatVersion.java b/jme3-core/src/main/java/com/jme3/export/FormatVersion.java
index 7de953abcb..2e34f2365b 100644
--- a/jme3-core/src/main/java/com/jme3/export/FormatVersion.java
+++ b/jme3-core/src/main/java/com/jme3/export/FormatVersion.java
@@ -39,9 +39,16 @@
public final class FormatVersion {
/**
- * Version number of the format
+ * Version number of the format.
+ *
+ * Changes for each version:
+ *
+ *
Undocumented
+ *
Undocumented
+ *
XML prefixes "jme-" to all key names
+ *
*/
- public static final int VERSION = 2;
+ public static final int VERSION = 3;
/**
* Signature of the format: currently, "JME3" as ASCII.
diff --git a/jme3-core/src/main/java/com/jme3/export/JmeExporter.java b/jme3-core/src/main/java/com/jme3/export/JmeExporter.java
index b8c3abc605..f93cb2560d 100644
--- a/jme3-core/src/main/java/com/jme3/export/JmeExporter.java
+++ b/jme3-core/src/main/java/com/jme3/export/JmeExporter.java
@@ -51,14 +51,29 @@ public interface JmeExporter {
public void save(Savable object, OutputStream f) throws IOException;
/**
- * Export the {@link Savable} to a file.
- *
+ * Export the {@link Savable} to a file. If the path to the file doesn't exist, the directories are
+ * made.
+ *
* @param object The savable to export
* @param f The file to export to
* @throws IOException If an io exception occurs during export
*/
- public void save(Savable object, File f) throws IOException;
-
+ default void save(Savable object, File f) throws IOException {
+ save(object, f, true);
+ }
+
+ /**
+ * Export the {@link Savable} to a file. If the path to the file doesn't exist, the parent
+ * directories can be created if the createDirectories flag is true. If the path does
+ * not exist and createDirectories is false, then an exception is thrown.
+ *
+ * @param object The savable to export
+ * @param f The file to export to
+ * @param createDirectories flag to indicate if the directories should be created
+ * @throws IOException If an io exception occurs during export
+ */
+ public void save(Savable object, File f, boolean createDirectories) throws IOException;
+
/**
* Returns the {@link OutputCapsule} for the given savable object.
*
diff --git a/jme3-core/src/main/java/com/jme3/export/SavableClassUtil.java b/jme3-core/src/main/java/com/jme3/export/SavableClassUtil.java
index 81887511cf..79364c96ea 100644
--- a/jme3-core/src/main/java/com/jme3/export/SavableClassUtil.java
+++ b/jme3-core/src/main/java/com/jme3/export/SavableClassUtil.java
@@ -57,7 +57,7 @@
*/
public class SavableClassUtil {
- private final static HashMap CLASS_REMAPPINGS = new HashMap<>();
+ private static final HashMap CLASS_REMAPPINGS = new HashMap<>();
private static void addRemapping(String oldClass, Class extends Savable> newClass) {
CLASS_REMAPPINGS.put(oldClass, newClass.getName());
@@ -201,6 +201,10 @@ public static Savable fromName(String className)
}
}
+ /**
+ * @deprecated use {@link #fromName(java.lang.String)} instead
+ */
+ @Deprecated
public static Savable fromName(String className, List loaders) throws InstantiationException,
InvocationTargetException, NoSuchMethodException,
IllegalAccessException, ClassNotFoundException, IOException {
diff --git a/jme3-core/src/main/java/com/jme3/font/BitmapCharacterSet.java b/jme3-core/src/main/java/com/jme3/font/BitmapCharacterSet.java
index 86e6a5fd8e..13b16785d7 100644
--- a/jme3-core/src/main/java/com/jme3/font/BitmapCharacterSet.java
+++ b/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> characters;
+ private final IntMap> characters;
private int pageSize;
@Override
diff --git a/jme3-core/src/main/java/com/jme3/font/BitmapFont.java b/jme3-core/src/main/java/com/jme3/font/BitmapFont.java
index 0fec21364e..867206c2a7 100644
--- a/jme3-core/src/main/java/com/jme3/font/BitmapFont.java
+++ b/jme3-core/src/main/java/com/jme3/font/BitmapFont.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -31,14 +31,22 @@
*/
package com.jme3.font;
-import com.jme3.export.*;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
import com.jme3.material.Material;
import java.io.IOException;
/**
- * Represents a font within jME that is generated with the AngelCode Bitmap Font Generator
+ * Represents a font loaded from a bitmap font definition
+ * (e.g., generated by AngelCode Bitmap Font Generator).
+ * It manages character sets, font pages (textures), and provides utilities for text measurement and rendering.
+ *
* @author dhdd
+ * @author Yonghoon
*/
public class BitmapFont implements Savable {
@@ -87,33 +95,30 @@ public enum VAlign {
Bottom
}
+ // The character set containing definitions for each character (glyph) in the font.
private BitmapCharacterSet charSet;
+ // An array of materials, where each material corresponds to a font page (texture).
private Material[] pages;
+ // Indicates whether this font is designed for right-to-left (RTL) text rendering.
private boolean rightToLeft = false;
// For cursive bitmap fonts in which letter shape is determined by the adjacent glyphs.
private GlyphParser glyphParser;
/**
- * @return true, if this is a right-to-left font, otherwise it will return false.
+ * Creates a new instance of `BitmapFont`.
+ * This constructor is primarily used for deserialization.
*/
- public boolean isRightToLeft() {
- return rightToLeft;
+ public BitmapFont() {
}
/**
- * Specify if this is a right-to-left font. By default it is set to false.
- * This can be "overwritten" in the BitmapText constructor.
+ * Creates a new {@link BitmapText} instance initialized with this font.
+ * The label's size will be set to the font's rendered size, and its text content
+ * will be set to the provided string.
*
- * @param rightToLeft true → right-to-left, false → left-to-right
- * (default=false)
+ * @param content The initial text content for the label.
+ * @return A new {@link BitmapText} instance.
*/
- public void setRightToLeft(boolean rightToLeft) {
- this.rightToLeft = rightToLeft;
- }
-
- public BitmapFont() {
- }
-
public BitmapText createLabel(String content) {
BitmapText label = new BitmapText(this);
label.setSize(getCharSet().getRenderedSize());
@@ -121,27 +126,81 @@ public BitmapText createLabel(String content) {
return label;
}
+ /**
+ * Checks if this font is configured for right-to-left (RTL) text rendering.
+ *
+ * @return true if this is a right-to-left font, otherwise false (default is left-to-right).
+ */
+ public boolean isRightToLeft() {
+ return rightToLeft;
+ }
+
+ /**
+ * Specifies whether this font should be rendered as right-to-left (RTL).
+ * By default, it is set to false (left-to-right).
+ *
+ * @param rightToLeft true to enable right-to-left rendering; false for left-to-right.
+ */
+ public void setRightToLeft(boolean rightToLeft) {
+ this.rightToLeft = rightToLeft;
+ }
+
+ /**
+ * Returns the preferred size of the font, which is typically its rendered size.
+ *
+ * @return The preferred size of the font in font units.
+ */
public float getPreferredSize() {
return getCharSet().getRenderedSize();
}
+ /**
+ * Sets the character set for this font. The character set contains
+ * information about individual glyphs, their positions, and kerning data.
+ *
+ * @param charSet The {@link BitmapCharacterSet} to associate with this font.
+ */
public void setCharSet(BitmapCharacterSet charSet) {
this.charSet = charSet;
}
+ /**
+ * Sets the array of materials (font pages) for this font. Each material
+ * corresponds to a texture page containing character bitmaps.
+ * The character set's page size is also updated based on the number of pages.
+ *
+ * @param pages An array of {@link Material} objects representing the font pages.
+ */
public void setPages(Material[] pages) {
this.pages = pages;
charSet.setPageSize(pages.length);
}
+ /**
+ * Retrieves a specific font page material by its index.
+ *
+ * @param index The index of the font page to retrieve.
+ * @return The {@link Material} for the specified font page.
+ * @throws IndexOutOfBoundsException if the index is out of bounds.
+ */
public Material getPage(int index) {
return pages[index];
}
+ /**
+ * Returns the total number of font pages (materials) associated with this font.
+ *
+ * @return The number of font pages.
+ */
public int getPageSize() {
return pages.length;
}
+ /**
+ * Retrieves the character set associated with this font.
+ *
+ * @return The {@link BitmapCharacterSet} of this font.
+ */
public BitmapCharacterSet getCharSet() {
return charSet;
}
@@ -192,26 +251,19 @@ private int findKerningAmount(int newLineLastChar, int nextChar) {
return c.getKerning(nextChar);
}
- @Override
- public void write(JmeExporter ex) throws IOException {
- OutputCapsule oc = ex.getCapsule(this);
- oc.write(charSet, "charSet", null);
- oc.write(pages, "pages", null);
- oc.write(rightToLeft, "rightToLeft", false);
- oc.write(glyphParser, "glyphParser", null);
- }
-
- @Override
- public void read(JmeImporter im) throws IOException {
- InputCapsule ic = im.getCapsule(this);
- charSet = (BitmapCharacterSet) ic.readSavable("charSet", null);
- Savable[] pagesSavable = ic.readSavableArray("pages", null);
- pages = new Material[pagesSavable.length];
- System.arraycopy(pagesSavable, 0, pages, 0, pages.length);
- rightToLeft = ic.readBoolean("rightToLeft", false);
- glyphParser = (GlyphParser) ic.readSavable("glyphParser", null);
- }
-
+ /**
+ * Calculates the width of the given text in font units.
+ * This method accounts for character advances, kerning, and line breaks.
+ * It also attempts to skip custom color tags (e.g., "\#RRGGBB#" or "\#RRGGBBAA#")
+ * based on a specific format.
+ *
+ * Note: This method calculates width in "font units" where the font's
+ * {@link BitmapCharacterSet#getRenderedSize() rendered size} is the base.
+ * Actual pixel scaling for display is typically handled by {@link BitmapText}.
+ *
+ * @param text The text to measure.
+ * @return The maximum line width of the text in font units.
+ */
public float getLineWidth(CharSequence text) {
// This method will probably always be a bit of a maintenance
// nightmare since it bases its calculation on a different
@@ -252,29 +304,36 @@ public float getLineWidth(CharSequence text) {
boolean firstCharOfLine = true;
// float sizeScale = (float) block.getSize() / charSet.getRenderedSize();
float sizeScale = 1f;
- CharSequence characters = glyphParser != null ? glyphParser.parse(text) : text;
- for (int i = 0; i < characters.length(); i++) {
- char theChar = characters.charAt(i);
- if (theChar == '\n') {
+ // Use GlyphParser if available for complex script shaping (e.g., cursive fonts).
+ CharSequence processedText = glyphParser != null ? glyphParser.parse(text) : text;
+
+ for (int i = 0; i < processedText.length(); i++) {
+ char currChar = processedText.charAt(i);
+ if (currChar == '\n') {
maxLineWidth = Math.max(maxLineWidth, lineWidth);
lineWidth = 0f;
firstCharOfLine = true;
continue;
}
- BitmapCharacter c = charSet.getCharacter(theChar);
+ BitmapCharacter c = charSet.getCharacter(currChar);
if (c != null) {
- if (theChar == '\\' && i < characters.length() - 1 && characters.charAt(i + 1) == '#') {
- if (i + 5 < characters.length() && characters.charAt(i + 5) == '#') {
+ // Custom color tag skipping logic:
+ // Assumes tags are of the form `\#RRGGBB#` (9 chars total) or `\#RRGGBBAA#` (12 chars total).
+ if (currChar == '\\' && i < processedText.length() - 1 && processedText.charAt(i + 1) == '#') {
+ // Check for `\#XXXXX#` (6 chars after '\', including final '#')
+ if (i + 5 < processedText.length() && processedText.charAt(i + 5) == '#') {
i += 5;
continue;
- } else if (i + 8 < characters.length() && characters.charAt(i + 8) == '#') {
+ }
+ // Check for `\#XXXXXXXX#` (9 chars after '\', including final '#')
+ else if (i + 8 < processedText.length() && processedText.charAt(i + 8) == '#') {
i += 8;
continue;
}
}
if (!firstCharOfLine) {
- lineWidth += findKerningAmount(lastChar, theChar) * sizeScale;
+ lineWidth += findKerningAmount(lastChar, currChar) * sizeScale;
} else {
if (rightToLeft) {
// Ignore offset, so it will be compatible with BitmapText.getLineWidth().
@@ -292,7 +351,7 @@ public float getLineWidth(CharSequence text) {
// If this is the last character of a line, then we really should
// have only added its width. The advance may include extra spacing
// that we don't care about.
- if (i == characters.length() - 1 || characters.charAt(i + 1) == '\n') {
+ if (i == processedText.length() - 1 || processedText.charAt(i + 1) == '\n') {
if (rightToLeft) {
// In RTL text we move the letter x0 by its xAdvance, so
// we should add it to lineWidth.
@@ -315,30 +374,54 @@ public float getLineWidth(CharSequence text) {
return Math.max(maxLineWidth, lineWidth);
}
-
/**
- * Merge two fonts.
- * If two font have the same style, merge will fail.
- * @param newFont Style must be assigned to this.
- * author: Yonghoon
+ * Merges another {@link BitmapFont} into this one.
+ * This operation combines the character sets and font pages.
+ * If both fonts contain the same style, the merge will fail and throw a RuntimeException.
+ *
+ * @param newFont The {@link BitmapFont} to merge into this one. It must have a style assigned.
*/
public void merge(BitmapFont newFont) {
charSet.merge(newFont.charSet);
final int size1 = this.pages.length;
final int size2 = newFont.pages.length;
- Material[] tmp = new Material[size1+size2];
+ Material[] tmp = new Material[size1 + size2];
System.arraycopy(this.pages, 0, tmp, 0, size1);
System.arraycopy(newFont.pages, 0, tmp, size1, size2);
this.pages = tmp;
-
-// this.pages = Arrays.copyOf(this.pages, size1+size2);
-// System.arraycopy(newFont.pages, 0, this.pages, size1, size2);
}
+ /**
+ * Sets the style for the font's character set.
+ * This method is typically used when a font file contains only one style
+ * but needs to be assigned a specific style identifier for merging
+ * with other multi-style fonts.
+ *
+ * @param style The integer style identifier to set.
+ */
public void setStyle(int style) {
charSet.setStyle(style);
}
-}
\ No newline at end of file
+ @Override
+ public void write(JmeExporter ex) throws IOException {
+ OutputCapsule oc = ex.getCapsule(this);
+ oc.write(charSet, "charSet", null);
+ oc.write(pages, "pages", null);
+ oc.write(rightToLeft, "rightToLeft", false);
+ oc.write(glyphParser, "glyphParser", null);
+ }
+
+ @Override
+ public void read(JmeImporter im) throws IOException {
+ InputCapsule ic = im.getCapsule(this);
+ charSet = (BitmapCharacterSet) ic.readSavable("charSet", null);
+ Savable[] pagesSavable = ic.readSavableArray("pages", null);
+ pages = new Material[pagesSavable.length];
+ System.arraycopy(pagesSavable, 0, pages, 0, pages.length);
+ rightToLeft = ic.readBoolean("rightToLeft", false);
+ glyphParser = (GlyphParser) ic.readSavable("glyphParser", null);
+ }
+}
diff --git a/jme3-core/src/main/java/com/jme3/font/BitmapText.java b/jme3-core/src/main/java/com/jme3/font/BitmapText.java
index eee39bf482..6825ad77b0 100644
--- a/jme3-core/src/main/java/com/jme3/font/BitmapText.java
+++ b/jme3-core/src/main/java/com/jme3/font/BitmapText.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -38,20 +38,38 @@
import com.jme3.renderer.RenderManager;
import com.jme3.scene.Node;
import com.jme3.util.clone.Cloner;
+
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
+ * `BitmapText` is a spatial node that displays text using a {@link BitmapFont}.
+ * It handles text layout, alignment, wrapping, coloring, and styling based on
+ * the properties set via its methods. The text is rendered as a series of
+ * quads (rectangles) with character textures from the font's pages.
+ *
* @author YongHoon
*/
public class BitmapText extends Node {
+ // The font used to render this text.
private BitmapFont font;
+ // Stores the text content and its layout properties (size, box, alignment, etc.).
private StringBlock block;
+ // A flag indicating whether the text needs to be re-assembled
private boolean needRefresh = true;
+ // An array of `BitmapTextPage` instances, each corresponding to a font page.
private BitmapTextPage[] textPages;
+ // Manages the individual letter quads, their positions, colors, and styles.
private Letters letters;
+ /**
+ * Creates a new `BitmapText` instance using the specified font.
+ * The text will be rendered left-to-right by default, unless the font itself
+ * is configured for right-to-left rendering.
+ *
+ * @param font The {@link BitmapFont} to use for rendering the text (not null).
+ */
public BitmapText(BitmapFont font) {
this(font, font.isRightToLeft(), false);
}
@@ -69,6 +87,15 @@ public BitmapText(BitmapFont font, boolean rightToLeft) {
this(font, rightToLeft, false);
}
+ /**
+ * Creates a new `BitmapText` instance with the specified font, text direction,
+ * and a flag for array-based rendering.
+ *
+ * @param font The {@link BitmapFont} to use for rendering the text (not null).
+ * @param rightToLeft true for right-to-left text rendering, false for left-to-right.
+ * @param arrayBased If true, the internal text pages will use array-based buffers for rendering.
+ * This might affect performance or compatibility depending on the renderer.
+ */
public BitmapText(BitmapFont font, boolean rightToLeft, boolean arrayBased) {
textPages = new BitmapTextPage[font.getPageSize()];
for (int page = 0; page < textPages.length; page++) {
@@ -84,7 +111,7 @@ public BitmapText(BitmapFont font, boolean rightToLeft, boolean arrayBased) {
@Override
public BitmapText clone() {
- return (BitmapText)super.clone(false);
+ return (BitmapText) super.clone(false);
}
/**
@@ -114,13 +141,19 @@ public void cloneFields(Cloner cloner, Object original) {
// so I guess cloning doesn't come up that often.
}
+ /**
+ * Returns the {@link BitmapFont} currently used by this `BitmapText` instance.
+ *
+ * @return The {@link BitmapFont} object.
+ */
public BitmapFont getFont() {
return font;
}
/**
- * Changes text size
- * @param size text size
+ * Sets the size of the text. This value scales the font's base character sizes.
+ *
+ * @param size The desired text size (e.g., in world units or pixels).
*/
public void setSize(float size) {
block.setSize(size);
@@ -128,13 +161,20 @@ public void setSize(float size) {
letters.invalidate();
}
+ /**
+ * Returns the current size of the text.
+ *
+ * @return The text size.
+ */
public float getSize() {
return block.getSize();
}
/**
+ * Sets the text content to be displayed.
*
- * @param text charsequence to change text to
+ * @param text The `CharSequence` (e.g., `String` or `StringBuilder`) to display.
+ * If null, the text will be set to an empty string.
*/
public void setText(CharSequence text) {
// note: text.toString() is free if text is already a java.lang.String.
@@ -142,72 +182,50 @@ public void setText(CharSequence text) {
}
/**
+ * Sets the text content to be displayed.
+ * If the new text is the same as the current text, no update occurs.
+ * Otherwise, the internal `StringBlock` and `Letters` objects are updated,
+ * and a refresh is flagged to re-layout the text.
*
- * @param text String to change text to
+ * @param text The `String` to display. If null, the text will be set to an empty string.
*/
public void setText(String text) {
text = text == null ? "" : text;
- if (text == block.getText() || block.getText().equals(text)) {
+ if (block.getText().equals(text)) {
return;
}
- /*
- The problem with the below block is that StringBlock carries
- pretty much all of the text-related state of the BitmapText such
- as size, text box, alignment, etc.
-
- I'm not sure why this change was needed and the commit message was
- not entirely helpful because it purports to fix a problem that I've
- never encountered.
-
- If block.setText("") doesn't do the right thing then that's where
- the fix should go because StringBlock carries too much information to
- be blown away every time. -pspeed
-
- Change was made:
- http://code.google.com/p/jmonkeyengine/source/detail?spec=svn9389&r=9389
- Diff:
- http://code.google.com/p/jmonkeyengine/source/diff?path=/trunk/engine/src/core/com/jme3/font/BitmapText.java&format=side&r=9389&old_path=/trunk/engine/src/core/com/jme3/font/BitmapText.java&old=8843
-
- // If the text is empty, reset
- if (text.isEmpty()) {
- detachAllChildren();
-
- for (int page = 0; page < textPages.length; page++) {
- textPages[page] = new BitmapTextPage(font, true, page);
- attachChild(textPages[page]);
- }
-
- block = new StringBlock();
- letters = new Letters(font, block, letters.getQuad().isRightToLeft());
- }
- */
-
// Update the text content
block.setText(text);
letters.setText(text);
-
- // Flag for refresh
needRefresh = true;
}
/**
- * @return returns text
+ * Returns the current text content displayed by this `BitmapText` instance.
+ *
+ * @return The text content as a `String`.
*/
public String getText() {
return block.getText();
}
/**
- * @return color of the text
+ * Returns the base color applied to the entire text.
+ * Note: Substring colors set via `setColor(int, int, ColorRGBA)` or
+ * `setColor(String, ColorRGBA)` will override this base color for their respective ranges.
+ *
+ * @return The base {@link ColorRGBA} of the text.
*/
public ColorRGBA getColor() {
return letters.getBaseColor();
}
/**
- * changes text color. all substring colors are deleted.
- * @param color new color of text
+ * Sets the base color for the entire text.
+ * This operation will clear any previously set substring colors.
+ *
+ * @param color The new base {@link ColorRGBA} for the text.
*/
public void setColor(ColorRGBA color) {
letters.setColor(color);
@@ -216,26 +234,34 @@ public void setColor(ColorRGBA color) {
}
/**
- * Sets an overall alpha that will be applied to all
- * letters. If the alpha passed is -1 then alpha reverts
- * to default... which will be 1 for anything unspecified
- * and color tags will be reset to 1 or their encoded
- * alpha.
+ * Sets an overall alpha (transparency) value that will be applied to all
+ * letters in the text.
+ * If the alpha passed is -1, the alpha reverts to its default behavior:
+ * 1.0 for unspecified parts, and the encoded alpha from any color tags.
*
- * @param alpha the desired alpha, or -1 to revert to the default
+ * @param alpha The desired alpha value (0.0 for fully transparent, 1.0 for fully opaque),
+ * or -1 to revert to default alpha behavior.
*/
public void setAlpha(float alpha) {
letters.setBaseAlpha(alpha);
needRefresh = true;
}
+ /**
+ * Returns the current base alpha value applied to the text.
+ *
+ * @return The base alpha value, or -1 if default alpha behavior is active.
+ */
public float getAlpha() {
return letters.getBaseAlpha();
}
/**
- * Define the area where the BitmapText will be rendered.
- * @param rect position and size box where text is rendered
+ * Defines a rectangular bounding box within which the text will be rendered.
+ * This box is used for text wrapping and alignment.
+ *
+ * @param rect The {@link Rectangle} defining the position (x, y) and size (width, height)
+ * of the text rendering area.
*/
public void setBox(Rectangle rect) {
block.setTextBox(rect);
@@ -244,14 +270,19 @@ public void setBox(Rectangle rect) {
}
/**
- * @return height of the line
+ * Returns the height of a single line of text, scaled by the current text size.
+ *
+ * @return The calculated line height.
*/
public float getLineHeight() {
return font.getLineHeight(block);
}
/**
- * @return height of whole text block
+ * Calculates and returns the total height of the entire text block,
+ * considering all lines and the defined text box (if any).
+ *
+ * @return The total height of the text block.
*/
public float getHeight() {
if (needRefresh) {
@@ -266,7 +297,9 @@ public float getHeight() {
}
/**
- * @return width of line
+ * Calculates and returns the maximum width of any line in the text block.
+ *
+ * @return The maximum line width of the text.
*/
public float getLineWidth() {
if (needRefresh) {
@@ -282,7 +315,9 @@ public float getLineWidth() {
}
/**
- * @return line count
+ * Returns the number of lines the text currently occupies.
+ *
+ * @return The total number of lines.
*/
public int getLineCount() {
if (needRefresh) {
@@ -291,14 +326,21 @@ public int getLineCount() {
return block.getLineCount();
}
+ /**
+ * Returns the current line wrapping mode set for this text.
+ *
+ * @return The {@link LineWrapMode} enum value.
+ */
public LineWrapMode getLineWrapMode() {
return block.getLineWrapMode();
}
/**
- * Set horizontal alignment. Applicable only when text bound is set.
+ * Sets the horizontal alignment for the text within its bounding box.
+ * This is only applicable if a text bounding box has been set using {@link #setBox(Rectangle)}.
*
- * @param align the desired alignment (such as Align.Left)
+ * @param align The desired horizontal alignment (e.g., {@link Align#Left}, {@link Align#Center}, {@link Align#Right}).
+ * @throws RuntimeException If a bounding box is not set and `align` is not `Align.Left`.
*/
public void setAlignment(BitmapFont.Align align) {
if (block.getTextBox() == null && align != Align.Left) {
@@ -310,9 +352,11 @@ public void setAlignment(BitmapFont.Align align) {
}
/**
- * Set vertical alignment. Applicable only when text bound is set.
+ * Sets the vertical alignment for the text within its bounding box.
+ * This is only applicable if a text bounding box has been set using {@link #setBox(Rectangle)}.
*
- * @param align the desired alignment (such as Align.Top)
+ * @param align The desired vertical alignment (e.g., {@link VAlign#Top}, {@link VAlign#Center}, {@link VAlign#Bottom}).
+ * @throws RuntimeException If a bounding box is not set and `align` is not `VAlign.Top`.
*/
public void setVerticalAlignment(BitmapFont.VAlign align) {
if (block.getTextBox() == null && align != VAlign.Top) {
@@ -323,28 +367,42 @@ public void setVerticalAlignment(BitmapFont.VAlign align) {
needRefresh = true;
}
+ /**
+ * Returns the current horizontal alignment set for the text.
+ *
+ * @return The current {@link Align} value.
+ */
public BitmapFont.Align getAlignment() {
return block.getAlignment();
}
+ /**
+ * Returns the current vertical alignment set for the text.
+ *
+ * @return The current {@link VAlign} value.
+ */
public BitmapFont.VAlign getVerticalAlignment() {
return block.getVerticalAlignment();
}
/**
- * Set the font style of substring. If font doesn't contain style, default style is used
- * @param start start index to set style. inclusive.
- * @param end end index to set style. EXCLUSIVE.
- * @param style the style to apply
+ * Sets the font style for a specific substring of the text.
+ * If the font does not contain the specified style, the default style will be used.
+ *
+ * @param start The starting index of the substring (inclusive).
+ * @param end The ending index of the substring (exclusive).
+ * @param style The integer style identifier to apply.
*/
public void setStyle(int start, int end, int style) {
letters.setStyle(start, end, style);
}
/**
- * Set the font style of substring. If font doesn't contain style, default style is applied
- * @param regexp regular expression
- * @param style the style to apply
+ * Sets the font style for all substrings matching a given regular expression.
+ * If the font does not contain the specified style, the default style will be used.
+ *
+ * @param regexp The regular expression string to match against the text.
+ * @param style The integer style identifier to apply.
*/
public void setStyle(String regexp, int style) {
Pattern p = Pattern.compile(regexp);
@@ -355,10 +413,11 @@ public void setStyle(String regexp, int style) {
}
/**
- * Set the color of substring.
- * @param start start index to set style. inclusive.
- * @param end end index to set style. EXCLUSIVE.
- * @param color the desired color
+ * Sets the color for a specific substring of the text.
+ *
+ * @param start The starting index of the substring (inclusive).
+ * @param end The ending index of the substring (exclusive).
+ * @param color The desired {@link ColorRGBA} to apply to the substring.
*/
public void setColor(int start, int end, ColorRGBA color) {
letters.setColor(start, end, color);
@@ -367,9 +426,10 @@ public void setColor(int start, int end, ColorRGBA color) {
}
/**
- * Set the color of substring.
- * @param regexp regular expression
- * @param color the desired color
+ * Sets the color for all substrings matching a given regular expression.
+ *
+ * @param regexp The regular expression string to match against the text.
+ * @param color The desired {@link ColorRGBA} to apply.
*/
public void setColor(String regexp, ColorRGBA color) {
Pattern p = Pattern.compile(regexp);
@@ -382,7 +442,10 @@ public void setColor(String regexp, ColorRGBA color) {
}
/**
- * @param tabs tab positions
+ * Sets custom tab stop positions for the text.
+ * Tab characters (`\t`) will align to these specified positions.
+ *
+ * @param tabs An array of float values representing the horizontal tab stop positions.
*/
public void setTabPosition(float... tabs) {
block.setTabPosition(tabs);
@@ -391,8 +454,10 @@ public void setTabPosition(float... tabs) {
}
/**
- * used for the tabs over the last tab position.
- * @param width tab size
+ * Sets the default width for tabs that extend beyond the last defined tab position.
+ * This value is used if a tab character is encountered after all custom tab stops have been passed.
+ *
+ * @param width The default width for tabs in font units.
*/
public void setTabWidth(float width) {
block.setTabWidth(width);
@@ -401,10 +466,10 @@ public void setTabWidth(float width) {
}
/**
- * for setLineWrapType(LineWrapType.NoWrap),
- * set the last character when the text exceeds the bound.
+ * When {@link LineWrapMode#NoWrap} is used and the text exceeds the bounding box,
+ * this character will be appended to indicate truncation (e.g., '...').
*
- * @param c the character to indicate truncated text
+ * @param c The character to use as the ellipsis.
*/
public void setEllipsisChar(char c) {
block.setEllipsisChar(c);
@@ -413,12 +478,18 @@ public void setEllipsisChar(char c) {
}
/**
- * Available only when bounding is set. setBox() method call is needed in advance.
- * true when
- * @param wrap NoWrap : Letters over the text bound is not shown. the last character is set to '...'(0x2026)
- * Character: Character is split at the end of the line.
- * Word : Word is split at the end of the line.
- * Clip : The text is hard-clipped at the border including showing only a partial letter if it goes beyond the text bound.
+ * Sets the line wrapping mode for the text. This is only applicable when
+ * a text bounding box has been set using {@link #setBox(Rectangle)}.
+ *
+ * @param wrap The desired {@link LineWrapMode}:
+ *
+ *
{@link LineWrapMode#NoWrap}: Letters exceeding the text bound are not shown.
+ * The last visible character might be replaced by an ellipsis character
+ * (set via {@link #setEllipsisChar(char)}).
+ *
{@link LineWrapMode#Character}: Text is split at the end of the line, even in the middle of a word.
+ *
{@link LineWrapMode#Word}: Words are split at the end of the line.
+ *
{@link LineWrapMode#Clip}: The text is hard-clipped at the border, potentially showing only a partial letter.
+ *
*/
public void setLineWrapMode(LineWrapMode wrap) {
if (block.getLineWrapMode() != wrap) {
@@ -436,28 +507,38 @@ public void updateLogicalState(float tpf) {
}
}
+ /**
+ * Assembles the text by generating the quad list (character positions and sizes)
+ * and then populating the vertex buffers of each `BitmapTextPage`.
+ * This method is called internally when `needRefresh` is true.
+ */
private void assemble() {
- // first generate quad list
+ // First, generate or update the list of letter quads
+ // based on current text and layout properties.
letters.update();
- for (int i = 0; i < textPages.length; i++) {
- textPages[i].assemble(letters);
+ // Then, for each font page, assemble its mesh data from the generated quads.
+ for (BitmapTextPage textPage : textPages) {
+ textPage.assemble(letters);
}
needRefresh = false;
}
+ /**
+ * Renders the `BitmapText` spatial. This method iterates through each
+ * `BitmapTextPage`, sets its texture, and renders it using the provided
+ * `RenderManager`.
+ *
+ * @param rm The `RenderManager` responsible for drawing.
+ * @param color The base color to apply during rendering. Note that colors
+ * set per-substring will override this for those parts.
+ */
public void render(RenderManager rm, ColorRGBA color) {
for (BitmapTextPage page : textPages) {
Material mat = page.getMaterial();
mat.setTexture("ColorMap", page.getTexture());
- //ColorRGBA original = getColor(mat, "Color");
- //mat.setColor("Color", color);
+ // mat.setColor("Color", color); // If the material supports a "Color" parameter
mat.render(page, rm);
-
- //if( original == null ) {
- // mat.clearParam("Color");
- //} else {
- // mat.setColor("Color", original);
- //}
}
}
+
}
diff --git a/jme3-core/src/main/java/com/jme3/font/ColorTags.java b/jme3-core/src/main/java/com/jme3/font/ColorTags.java
index a64806a785..304b9832a6 100644
--- a/jme3-core/src/main/java/com/jme3/font/ColorTags.java
+++ b/jme3-core/src/main/java/com/jme3/font/ColorTags.java
@@ -47,7 +47,7 @@
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 colors = new LinkedList<>();
+ private final LinkedList colors = new LinkedList<>();
private String text;
private String original;
private float baseAlpha = -1;
diff --git a/jme3-core/src/main/java/com/jme3/font/LetterQuad.java b/jme3-core/src/main/java/com/jme3/font/LetterQuad.java
index 2611a059f2..ff647ab423 100644
--- a/jme3-core/src/main/java/com/jme3/font/LetterQuad.java
+++ b/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;
diff --git a/jme3-core/src/main/java/com/jme3/font/Letters.java b/jme3-core/src/main/java/com/jme3/font/Letters.java
index 0b9473d03e..b545dee035 100644
--- a/jme3-core/src/main/java/com/jme3/font/Letters.java
+++ b/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;
diff --git a/jme3-core/src/main/java/com/jme3/input/AbstractJoystick.java b/jme3-core/src/main/java/com/jme3/input/AbstractJoystick.java
index 9381f510b7..7eab91ee12 100644
--- a/jme3-core/src/main/java/com/jme3/input/AbstractJoystick.java
+++ b/jme3-core/src/main/java/com/jme3/input/AbstractJoystick.java
@@ -42,13 +42,13 @@
*/
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 axes = new ArrayList<>();
- final private List buttons = new ArrayList<>();
+ private final List axes = new ArrayList<>();
+ private final List buttons = new ArrayList<>();
/**
* Creates a new joystick instance. Only used internally.
diff --git a/jme3-core/src/main/java/com/jme3/input/CameraInput.java b/jme3-core/src/main/java/com/jme3/input/CameraInput.java
index 1d40c716f4..b4e46c8ad2 100644
--- a/jme3-core/src/main/java/com/jme3/input/CameraInput.java
+++ b/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.
diff --git a/jme3-core/src/main/java/com/jme3/input/ChaseCamera.java b/jme3-core/src/main/java/com/jme3/input/ChaseCamera.java
index 057e729062..fe494801b3 100644
--- a/jme3-core/src/main/java/com/jme3/input/ChaseCamera.java
+++ b/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;
diff --git a/jme3-core/src/main/java/com/jme3/input/DefaultJoystickAxis.java b/jme3-core/src/main/java/com/jme3/input/DefaultJoystickAxis.java
index 6e6bff046f..ceb19bc280 100644
--- a/jme3-core/src/main/java/com/jme3/input/DefaultJoystickAxis.java
+++ b/jme3-core/src/main/java/com/jme3/input/DefaultJoystickAxis.java
@@ -40,14 +40,15 @@
*/
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;
+ private float jitterThreshold = 0f;
/**
* Creates a new joystick axis instance. Only used internally.
@@ -166,6 +167,12 @@ public void setDeadZone(float f) {
public String toString() {
return "JoystickAxis[name=" + name + ", parent=" + parent.getName() + ", id=" + axisIndex
+ ", logicalId=" + logicalId + ", isAnalog=" + isAnalog
- + ", isRelative=" + isRelative + ", deadZone=" + deadZone + "]";
+ + ", isRelative=" + isRelative + ", deadZone=" + deadZone +
+ ", jitterThreshold=" + jitterThreshold + "]";
+ }
+
+ @Override
+ public float getJitterThreshold() {
+ return jitterThreshold;
}
}
diff --git a/jme3-core/src/main/java/com/jme3/input/DefaultJoystickButton.java b/jme3-core/src/main/java/com/jme3/input/DefaultJoystickButton.java
index 63cc1e9bfe..2dd45d4191 100644
--- a/jme3-core/src/main/java/com/jme3/input/DefaultJoystickButton.java
+++ b/jme3-core/src/main/java/com/jme3/input/DefaultJoystickButton.java
@@ -40,11 +40,11 @@
*/
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) {
diff --git a/jme3-core/src/main/java/com/jme3/input/FlyByCamera.java b/jme3-core/src/main/java/com/jme3/input/FlyByCamera.java
index 39e40725fd..1a39ee9820 100644
--- a/jme3-core/src/main/java/com/jme3/input/FlyByCamera.java
+++ b/jme3-core/src/main/java/com/jme3/input/FlyByCamera.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -44,10 +44,10 @@
/**
* A first-person camera controller.
- *
+ *
* After creation, you (or FlyCamAppState) must register the controller using
* {@link #registerWithInput(com.jme3.input.InputManager)}.
- *
+ *
* Controls:
* - Move (or, in drag-to-rotate mode, drag) the mouse to rotate the camera
* - Mouse wheel for zooming in or out
@@ -56,25 +56,25 @@
*/
public class FlyByCamera implements AnalogListener, ActionListener {
- final private static String[] mappings = new String[]{
- CameraInput.FLYCAM_LEFT,
- CameraInput.FLYCAM_RIGHT,
- CameraInput.FLYCAM_UP,
- CameraInput.FLYCAM_DOWN,
+ private static final String[] mappings = new String[]{
+ CameraInput.FLYCAM_LEFT,
+ CameraInput.FLYCAM_RIGHT,
+ CameraInput.FLYCAM_UP,
+ CameraInput.FLYCAM_DOWN,
- CameraInput.FLYCAM_STRAFELEFT,
- CameraInput.FLYCAM_STRAFERIGHT,
- CameraInput.FLYCAM_FORWARD,
- CameraInput.FLYCAM_BACKWARD,
+ CameraInput.FLYCAM_STRAFELEFT,
+ CameraInput.FLYCAM_STRAFERIGHT,
+ CameraInput.FLYCAM_FORWARD,
+ CameraInput.FLYCAM_BACKWARD,
- CameraInput.FLYCAM_ZOOMIN,
- CameraInput.FLYCAM_ZOOMOUT,
- CameraInput.FLYCAM_ROTATEDRAG,
+ CameraInput.FLYCAM_ZOOMIN,
+ CameraInput.FLYCAM_ZOOMOUT,
+ CameraInput.FLYCAM_ROTATEDRAG,
- CameraInput.FLYCAM_RISE,
- CameraInput.FLYCAM_LOWER,
+ CameraInput.FLYCAM_RISE,
+ CameraInput.FLYCAM_LOWER,
- CameraInput.FLYCAM_INVERTY
+ CameraInput.FLYCAM_INVERTY
};
/**
* camera controlled by this controller (not null)
@@ -83,7 +83,7 @@ public class FlyByCamera implements AnalogListener, ActionListener {
/**
* normalized "up" direction (a unit vector)
*/
- protected Vector3f initialUpVec;
+ protected Vector3f initialUpVec = new Vector3f();
/**
* rotation-rate multiplier (1=default)
*/
@@ -109,6 +109,15 @@ public class FlyByCamera implements AnalogListener, ActionListener {
protected boolean invertY = false;
protected InputManager inputManager;
+ // Reusable temporary objects to reduce allocations during updates
+ private final Matrix3f tempMat = new Matrix3f();
+ private final Quaternion tempQuat = new Quaternion();
+ private final Vector3f tempUp = new Vector3f();
+ private final Vector3f tempLeft = new Vector3f();
+ private final Vector3f tempDir = new Vector3f();
+ private final Vector3f tempVel = new Vector3f();
+ private final Vector3f tempPos = new Vector3f();
+
/**
* Creates a new FlyByCamera to control the specified camera.
*
@@ -116,7 +125,7 @@ public class FlyByCamera implements AnalogListener, ActionListener {
*/
public FlyByCamera(Camera cam) {
this.cam = cam;
- initialUpVec = cam.getUp().clone();
+ cam.getUp(initialUpVec);
}
/**
@@ -128,63 +137,61 @@ public void setUpVector(Vector3f upVec) {
initialUpVec.set(upVec);
}
- public void setMotionAllowedListener(MotionAllowedListener listener){
+ public void setMotionAllowedListener(MotionAllowedListener listener) {
this.motionAllowed = listener;
}
/**
- * Set the translation speed.
+ * Sets the translation speed of the camera.
*
- * @param moveSpeed new speed (in world units per second)
+ * @param moveSpeed The new translation speed in world units per second. Must be non-negative.
*/
- public void setMoveSpeed(float moveSpeed){
+ public void setMoveSpeed(float moveSpeed) {
this.moveSpeed = moveSpeed;
}
/**
- * Read the translation speed.
+ * Retrieves the current translation speed of the camera.
*
- * @return current speed (in world units per second)
+ * @return The current speed in world units per second.
*/
- public float getMoveSpeed(){
+ public float getMoveSpeed() {
return moveSpeed;
}
/**
- * Set the rotation-rate multiplier. The bigger the multiplier, the more
- * rotation for a given movement of the mouse.
+ * Sets the rotation-rate multiplier for mouse input. A higher value
+ * means the camera rotates more for a given mouse movement.
*
- * @param rotationSpeed new rate multiplier (1=default)
+ * @param rotationSpeed The new rate multiplier (1.0 is default). Must be non-negative.
*/
- public void setRotationSpeed(float rotationSpeed){
+ public void setRotationSpeed(float rotationSpeed) {
this.rotationSpeed = rotationSpeed;
}
/**
- * Read the rotation-rate multiplier. The bigger the multiplier, the more
- * rotation for a given movement of the mouse.
+ * Retrieves the current rotation-rate multiplier.
*
- * @return current rate multiplier (1=default)
+ * @return The current rate multiplier.
*/
- public float getRotationSpeed(){
+ public float getRotationSpeed() {
return rotationSpeed;
}
/**
- * Set the zoom-rate multiplier. The bigger the multiplier, the more zoom
- * for a given movement of the mouse wheel.
+ * Sets the zoom-rate multiplier for mouse wheel input. A higher value
+ * means the camera zooms more for a given mouse wheel scroll.
*
- * @param zoomSpeed new rate multiplier (1=default)
+ * @param zoomSpeed The new rate multiplier (1.0 is default). Must be non-negative.
*/
public void setZoomSpeed(float zoomSpeed) {
this.zoomSpeed = zoomSpeed;
}
/**
- * Read the zoom-rate multiplier. The bigger the multiplier, the more zoom
- * for a given movement of the mouse wheel.
+ * Retrieves the current zoom-rate multiplier.
*
- * @return current rate multiplier (1=default)
+ * @return The current rate multiplier.
*/
public float getZoomSpeed() {
return zoomSpeed;
@@ -196,9 +203,9 @@ public float getZoomSpeed() {
*
* @param enable true to enable, false to disable
*/
- public void setEnabled(boolean enable){
- if (enabled && !enable){
- if (inputManager!= null && (!dragToRotate || (dragToRotate && canRotate))){
+ public void setEnabled(boolean enable) {
+ if (enabled && !enable) {
+ if (inputManager != null && (!dragToRotate || (dragToRotate && canRotate))) {
inputManager.setCursorVisible(true);
}
}
@@ -206,20 +213,19 @@ public void setEnabled(boolean enable){
}
/**
- * Test whether this controller is enabled.
+ * Checks whether this camera controller is currently enabled.
*
- * @return true if enabled, otherwise false
+ * @return {@code true} if enabled, {@code false} otherwise.
* @see #setEnabled(boolean)
*/
- public boolean isEnabled(){
+ public boolean isEnabled() {
return enabled;
}
/**
- * Test whether drag-to-rotate mode is enabled.
- *
- * @return If drag to rotate feature is enabled.
+ * Checks whether drag-to-rotate mode is currently enabled.
*
+ * @return {@code true} if drag-to-rotate is enabled, {@code false} otherwise.
* @see #setDragToRotate(boolean)
*/
public boolean isDragToRotate() {
@@ -245,15 +251,16 @@ public void setDragToRotate(boolean dragToRotate) {
}
/**
- * Register this controller to receive input events from the specified input
- * manager.
+ * Registers this controller to receive input events from the specified
+ * {@link InputManager}. This method sets up all the necessary input mappings
+ * for mouse, keyboard, and joysticks.
*
- * @param inputManager (not null, alias created)
+ * @param inputManager The InputManager instance to register with (must not be null).
*/
- public void registerWithInput(InputManager inputManager){
+ public void registerWithInput(InputManager inputManager) {
this.inputManager = inputManager;
- // both mouse and button - rotation of cam
+ // Mouse and Keyboard Mappings for Rotation
inputManager.addMapping(CameraInput.FLYCAM_LEFT, new MouseAxisTrigger(MouseInput.AXIS_X, true),
new KeyTrigger(KeyInput.KEY_LEFT));
@@ -266,7 +273,7 @@ public void registerWithInput(InputManager inputManager){
inputManager.addMapping(CameraInput.FLYCAM_DOWN, new MouseAxisTrigger(MouseInput.AXIS_Y, true),
new KeyTrigger(KeyInput.KEY_DOWN));
- // mouse only - zoom in/out with wheel, and rotate drag
+ // Mouse Mappings for Zoom and Drag-to-Rotate
inputManager.addMapping(CameraInput.FLYCAM_ZOOMIN, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
inputManager.addMapping(CameraInput.FLYCAM_ZOOMOUT, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
inputManager.addMapping(CameraInput.FLYCAM_ROTATEDRAG, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
@@ -283,13 +290,19 @@ public void registerWithInput(InputManager inputManager){
inputManager.setCursorVisible(dragToRotate || !isEnabled());
Joystick[] joysticks = inputManager.getJoysticks();
- if (joysticks != null && joysticks.length > 0){
+ if (joysticks != null && joysticks.length > 0) {
for (Joystick j : joysticks) {
mapJoystick(j);
}
}
}
+ /**
+ * Configures joystick input mappings for the camera controller. This method
+ * attempts to map joystick axes and buttons to camera actions.
+ *
+ * @param joystick The {@link Joystick} to map (not null).
+ */
protected void mapJoystick(Joystick joystick) {
// Map it differently if there are Z axis
if (joystick.getAxis(JoystickAxis.Z_ROTATION) != null
@@ -308,7 +321,7 @@ protected void mapJoystick(Joystick joystick) {
// And let the dpad be up and down
joystick.getPovYAxis().assignAxis(CameraInput.FLYCAM_RISE, CameraInput.FLYCAM_LOWER);
- if( joystick.getButton("Button 8") != null) {
+ if (joystick.getButton("Button 8") != null) {
// Let the standard select button be the y invert toggle
joystick.getButton("Button 8").assignButton(CameraInput.FLYCAM_INVERTY);
}
@@ -322,7 +335,7 @@ protected void mapJoystick(Joystick joystick) {
}
/**
- * Unregister this controller from its input manager.
+ * Unregisters this controller from its currently associated {@link InputManager}.
*/
public void unregisterInput() {
if (inputManager == null) {
@@ -338,97 +351,112 @@ public void unregisterInput() {
inputManager.removeListener(this);
inputManager.setCursorVisible(!dragToRotate);
- Joystick[] joysticks = inputManager.getJoysticks();
- if (joysticks != null && joysticks.length > 0) {
- // No way to unassign axis
- }
+ // Joysticks cannot be "unassigned" in the same way, but mappings are removed with listener.
+ // Joystick-specific mapping might persist but won't trigger this listener.
+ inputManager = null; // Clear reference
}
/**
- * Rotate the camera by the specified amount around the specified axis.
+ * Rotates the camera by the specified amount around the given axis.
*
- * @param value rotation amount
- * @param axis direction of rotation (a unit vector)
+ * @param value The amount of rotation.
+ * @param axis The axis around which to rotate (a unit vector, unaffected).
*/
protected void rotateCamera(float value, Vector3f axis) {
- if (dragToRotate) {
- if (canRotate) {
-// value = -value;
- } else {
- return;
- }
+ if (dragToRotate && !canRotate) {
+ return; // In drag-to-rotate mode, only rotate if canRotate is true.
}
- Matrix3f mat = new Matrix3f();
- mat.fromAngleNormalAxis(rotationSpeed * value, axis);
+ tempMat.fromAngleNormalAxis(rotationSpeed * value, axis);
- Vector3f up = cam.getUp();
- Vector3f left = cam.getLeft();
- Vector3f dir = cam.getDirection();
+ // Get current camera axes into temporary vectors
+ cam.getUp(tempUp);
+ cam.getLeft(tempLeft);
+ cam.getDirection(tempDir);
- mat.mult(up, up);
- mat.mult(left, left);
- mat.mult(dir, dir);
+ // Apply rotation to the camera's axes
+ tempMat.mult(tempUp, tempUp);
+ tempMat.mult(tempLeft, tempLeft);
+ tempMat.mult(tempDir, tempDir);
- Quaternion q = new Quaternion();
- q.fromAxes(left, up, dir);
- q.normalizeLocal();
+ // Set camera axes using a temporary Quaternion
+ tempQuat.fromAxes(tempLeft, tempUp, tempDir);
+ tempQuat.normalizeLocal(); // Ensure quaternion is normalized
- cam.setAxes(q);
+ cam.setAxes(tempQuat);
}
/**
- * Zoom the camera by the specified amount.
+ * Zooms the camera by the specified amount. This method handles both
+ * perspective and parallel projections.
*
- * @param value zoom amount
+ * @param value The amount to zoom. Positive values typically zoom in, negative out.
*/
protected void zoomCamera(float value) {
- float newFov = cam.getFov() + value * 0.1F * zoomSpeed;
- if (newFov > 0) {
- cam.setFov(newFov);
+ if (cam.isParallelProjection()) {
+ float zoomFactor = 1.0F + value * 0.01F * zoomSpeed;
+ if (zoomFactor > 0F) {
+ float left = zoomFactor * cam.getFrustumLeft();
+ float right = zoomFactor * cam.getFrustumRight();
+ float top = zoomFactor * cam.getFrustumTop();
+ float bottom = zoomFactor * cam.getFrustumBottom();
+ float near = cam.getFrustumNear();
+ float far = cam.getFrustumFar();
+ cam.setFrustum(near, far, left, right, top, bottom);
+ }
+
+ } else { // perspective projection
+ float newFov = cam.getFov() + value * 0.1F * zoomSpeed;
+ // Use a small epsilon to prevent near-zero FoV issues
+ if (newFov > 0.01f) {
+ cam.setFov(newFov);
+ }
}
}
/**
- * Translate the camera upward by the specified amount.
+ * Translates the camera vertically (up or down) by the specified amount,
+ * considering the {@code initialUpVec}.
*
- * @param value translation amount
+ * @param value The translation amount. Positive values move the camera up, negative down.
*/
protected void riseCamera(float value) {
- Vector3f vel = initialUpVec.mult(value * moveSpeed);
- Vector3f pos = cam.getLocation().clone();
+ tempVel.set(initialUpVec).multLocal(value * moveSpeed);
+ tempPos.set(cam.getLocation());
- if (motionAllowed != null)
- motionAllowed.checkMotionAllowed(pos, vel);
- else
- pos.addLocal(vel);
+ if (motionAllowed != null) {
+ motionAllowed.checkMotionAllowed(tempPos.clone(), tempVel.clone());
+ } else {
+ tempPos.addLocal(tempVel);
+ }
- cam.setLocation(pos);
+ cam.setLocation(tempPos);
}
/**
- * Translate the camera left or forward by the specified amount.
+ * Translates the camera left/right or forward/backward by the specified amount.
*
- * @param value translation amount
- * @param sideways true→left, false→forward
+ * @param value The translation amount. Positive values move in the primary
+ * direction (right/forward), negative in the opposite.
+ * @param sideways If {@code true}, the camera moves left/right (strafes).
+ * If {@code false}, the camera moves forward/backward.
*/
protected void moveCamera(float value, boolean sideways) {
- Vector3f vel = new Vector3f();
- Vector3f pos = cam.getLocation().clone();
-
- if (sideways){
- cam.getLeft(vel);
+ if (sideways) {
+ cam.getLeft(tempVel);
} else {
- cam.getDirection(vel);
+ cam.getDirection(tempVel);
}
- vel.multLocal(value * moveSpeed);
+ tempVel.multLocal(value * moveSpeed);
+ tempPos.set(cam.getLocation());
- if (motionAllowed != null)
- motionAllowed.checkMotionAllowed(pos, vel);
- else
- pos.addLocal(vel);
+ if (motionAllowed != null) {
+ motionAllowed.checkMotionAllowed(tempPos.clone(), tempVel.clone());
+ } else {
+ tempPos.addLocal(tempVel);
+ }
- cam.setLocation(pos);
+ cam.setLocation(tempPos);
}
/**
@@ -448,9 +476,9 @@ public void onAnalog(String name, float value, float tpf) {
} else if (name.equals(CameraInput.FLYCAM_RIGHT)) {
rotateCamera(-value, initialUpVec);
} else if (name.equals(CameraInput.FLYCAM_UP)) {
- rotateCamera(-value * (invertY ? -1 : 1), cam.getLeft());
+ rotateCamera(-value * (invertY ? -1 : 1), cam.getLeft(tempLeft));
} else if (name.equals(CameraInput.FLYCAM_DOWN)) {
- rotateCamera(value * (invertY ? -1 : 1), cam.getLeft());
+ rotateCamera(value * (invertY ? -1 : 1), cam.getLeft(tempLeft));
} else if (name.equals(CameraInput.FLYCAM_FORWARD)) {
moveCamera(value, false);
} else if (name.equals(CameraInput.FLYCAM_BACKWARD)) {
@@ -474,20 +502,20 @@ public void onAnalog(String name, float value, float tpf) {
* Callback to notify this controller of an action input event.
*
* @param name name of the input event
- * @param value true if the action is "pressed", false otherwise
+ * @param isPressed true if the action is "pressed", false otherwise
* @param tpf time per frame (in seconds)
*/
@Override
- public void onAction(String name, boolean value, float tpf) {
+ public void onAction(String name, boolean isPressed, float tpf) {
if (!enabled)
return;
if (name.equals(CameraInput.FLYCAM_ROTATEDRAG) && dragToRotate) {
- canRotate = value;
- inputManager.setCursorVisible(!value);
+ canRotate = isPressed;
+ inputManager.setCursorVisible(!isPressed);
} else if (name.equals(CameraInput.FLYCAM_INVERTY)) {
// Invert the "up" direction.
- if (!value) {
+ if (!isPressed) {
invertY = !invertY;
}
}
diff --git a/jme3-core/src/main/java/com/jme3/input/JoystickAxis.java b/jme3-core/src/main/java/com/jme3/input/JoystickAxis.java
index fbbddb9f06..4168e32711 100644
--- a/jme3-core/src/main/java/com/jme3/input/JoystickAxis.java
+++ b/jme3-core/src/main/java/com/jme3/input/JoystickAxis.java
@@ -44,27 +44,14 @@ public interface JoystickAxis {
public static final String Z_ROTATION = "rz";
public static final String LEFT_TRIGGER = "rx";
public static final String RIGHT_TRIGGER = "ry";
-
- // Note: the left/right trigger bit may be a bit controversial in
- // the sense that this is one case where XBox controllers make a lot
- // more sense.
- // I've seen the following mappings for various things:
- //
- // Axis | XBox | Non-Xbox (generally) (includes actual Sony PS4 controllers)
- // --------------+-------+---------------
- // left trigger | z | rx (also button 6)
- // right trigger | rz | ry (also button 7)
- // left stick x | x | x
- // left stick y | y | y
- // right stick x | rx | z
- // right stick y | ry | rz
- //
- // The issue is that in all cases I've seen, the XBox controllers will
- // use the name "xbox" somewhere in their name. The Non-XBox controllers
- // never mention anything uniform... even the PS4 controller only calls
- // itself "Wireless Controller". In that light, it seems easier to make
- // the default the ugly case and the "XBox" way the exception because it
- // can more easily be identified.
+
+ public static final String AXIS_XBOX_LEFT_TRIGGER = LEFT_TRIGGER;
+ public static final String AXIS_XBOX_RIGHT_TRIGGER = RIGHT_TRIGGER;
+ public static final String AXIS_XBOX_LEFT_THUMB_STICK_X = X_AXIS;
+ public static final String AXIS_XBOX_LEFT_THUMB_STICK_Y = Y_AXIS;
+ public static final String AXIS_XBOX_RIGHT_THUMB_STICK_X = Z_AXIS;
+ public static final String AXIS_XBOX_RIGHT_THUMB_STICK_Y = Z_ROTATION;
+
public static final String POV_X = "pov_x";
public static final String POV_Y = "pov_y";
@@ -128,4 +115,15 @@ public interface JoystickAxis {
* @return the radius of the dead zone
*/
public float getDeadZone();
+
+
+ /**
+ * Returns the suggested jitter threshold for this axis. Movements with a delta
+ * smaller than this threshold will be ignored by the backend input system
+ *
+ * @return the jitter threshold
+ */
+ public default float getJitterThreshold(){
+ return 0;
+ }
}
diff --git a/jme3-core/src/main/java/com/jme3/input/JoystickButton.java b/jme3-core/src/main/java/com/jme3/input/JoystickButton.java
index d8d2543ba0..11c0155087 100644
--- a/jme3-core/src/main/java/com/jme3/input/JoystickButton.java
+++ b/jme3-core/src/main/java/com/jme3/input/JoystickButton.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -50,6 +50,30 @@ public interface JoystickButton {
public static final String BUTTON_9 = "9";
public static final String BUTTON_10 = "10";
public static final String BUTTON_11 = "11";
+ public static final String BUTTON_12 = "12";
+ public static final String BUTTON_13 = "13";
+ public static final String BUTTON_14 = "14";
+ public static final String BUTTON_15 = "15";
+
+
+ public static final String BUTTON_XBOX_A = BUTTON_2;
+ public static final String BUTTON_XBOX_B = BUTTON_1;
+ public static final String BUTTON_XBOX_X = BUTTON_3;
+ public static final String BUTTON_XBOX_Y = BUTTON_0;
+ public static final String BUTTON_XBOX_LB = BUTTON_4;
+ public static final String BUTTON_XBOX_RB = BUTTON_5;
+ public static final String BUTTON_XBOX_LT = BUTTON_6;
+ public static final String BUTTON_XBOX_RT = BUTTON_7;
+ public static final String BUTTON_XBOX_BACK = BUTTON_8;
+ public static final String BUTTON_XBOX_START = BUTTON_9;
+ public static final String BUTTON_XBOX_L3 = BUTTON_10;
+ public static final String BUTTON_XBOX_R3 = BUTTON_11;
+
+ public static final String BUTTON_XBOX_DPAD_UP = BUTTON_12;
+ public static final String BUTTON_XBOX_DPAD_DOWN = BUTTON_13;
+ public static final String BUTTON_XBOX_DPAD_LEFT = BUTTON_14;
+ public static final String BUTTON_XBOX_DPAD_RIGHT = BUTTON_15;
+
/**
* Assign the mapping name to receive events from the given button index
diff --git a/jme3-core/src/main/java/com/jme3/input/JoystickCompatibilityMappings.java b/jme3-core/src/main/java/com/jme3/input/JoystickCompatibilityMappings.java
index 105440855d..d2385be4f9 100644
--- a/jme3-core/src/main/java/com/jme3/input/JoystickCompatibilityMappings.java
+++ b/jme3-core/src/main/java/com/jme3/input/JoystickCompatibilityMappings.java
@@ -44,6 +44,8 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import com.jme3.util.res.Resources;
+
/**
* Provides compatibility mapping to different joysticks
@@ -70,8 +72,8 @@ public class JoystickCompatibilityMappings {
private static Map> buttonMappings = new HashMap>();
// Remaps names by regex.
- final private static Map nameRemappings = new HashMap<>();
- final private static Map nameCache = new HashMap<>();
+ private static final Map nameRemappings = new HashMap<>();
+ private static final Map nameCache = new HashMap<>();
static {
loadDefaultMappings();
@@ -554,9 +556,9 @@ public static void loadMappingProperties(URL u) throws IOException {
}
}
- protected static void loadMappings(ClassLoader cl, String path) throws IOException {
+ protected static void loadMappings(String path) throws IOException {
logger.log(Level.FINE, "Searching for mappings for path:{0}", path);
- for (Enumeration en = cl.getResources(path); en.hasMoreElements(); ) {
+ for (Enumeration en = Resources.getResources(path); en.hasMoreElements(); ) {
URL u = en.nextElement();
try {
loadMappingProperties(u);
@@ -574,7 +576,7 @@ protected static void loadMappings(ClassLoader cl, String path) throws IOExcepti
protected static void loadDefaultMappings() {
for (String s : searchPaths) {
try {
- loadMappings(JoystickCompatibilityMappings.class.getClassLoader(), s);
+ loadMappings(s);
} catch (IOException e) {
logger.log(Level.SEVERE, "Error searching resource path:{0}", s);
}
diff --git a/jme3-core/src/main/java/com/jme3/input/RawInputListenerAdapter.java b/jme3-core/src/main/java/com/jme3/input/RawInputListenerAdapter.java
new file mode 100644
index 0000000000..3c59c112eb
--- /dev/null
+++ b/jme3-core/src/main/java/com/jme3/input/RawInputListenerAdapter.java
@@ -0,0 +1,91 @@
+/*
+ * 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.input;
+
+import com.jme3.input.event.JoyAxisEvent;
+import com.jme3.input.event.JoyButtonEvent;
+import com.jme3.input.event.KeyInputEvent;
+import com.jme3.input.event.MouseButtonEvent;
+import com.jme3.input.event.MouseMotionEvent;
+import com.jme3.input.event.TouchEvent;
+
+/**
+ * An abstract adapter class for {@link RawInputListener}.
+ *
+ * This class provides empty implementations for all methods in the
+ * {@link RawInputListener} interface, making it easier to create custom
+ * listeners by extending this class and overriding only the methods of
+ * interest.
+ */
+public abstract class RawInputListenerAdapter implements RawInputListener {
+
+ @Override
+ public void beginInput() {
+ // No-op implementation
+ }
+
+ @Override
+ public void endInput() {
+ // No-op implementation
+ }
+
+ @Override
+ public void onJoyAxisEvent(JoyAxisEvent evt) {
+ // No-op implementation
+ }
+
+ @Override
+ public void onJoyButtonEvent(JoyButtonEvent evt) {
+ // No-op implementation
+ }
+
+ @Override
+ public void onMouseMotionEvent(MouseMotionEvent evt) {
+ // No-op implementation
+ }
+
+ @Override
+ public void onMouseButtonEvent(MouseButtonEvent evt) {
+ // No-op implementation
+ }
+
+ @Override
+ public void onKeyEvent(KeyInputEvent evt) {
+ // No-op implementation
+ }
+
+ @Override
+ public void onTouchEvent(TouchEvent evt) {
+ // No-op implementation
+ }
+
+}
diff --git a/jme3-core/src/main/java/com/jme3/input/event/JoyButtonEvent.java b/jme3-core/src/main/java/com/jme3/input/event/JoyButtonEvent.java
index 5fca5d5703..615bb90212 100644
--- a/jme3-core/src/main/java/com/jme3/input/event/JoyButtonEvent.java
+++ b/jme3-core/src/main/java/com/jme3/input/event/JoyButtonEvent.java
@@ -41,8 +41,8 @@
*/
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;
diff --git a/jme3-core/src/main/java/com/jme3/input/event/KeyInputEvent.java b/jme3-core/src/main/java/com/jme3/input/event/KeyInputEvent.java
index f693d2caad..2fb0294730 100644
--- a/jme3-core/src/main/java/com/jme3/input/event/KeyInputEvent.java
+++ b/jme3-core/src/main/java/com/jme3/input/event/KeyInputEvent.java
@@ -40,10 +40,10 @@
*/
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;
diff --git a/jme3-core/src/main/java/com/jme3/input/event/MouseButtonEvent.java b/jme3-core/src/main/java/com/jme3/input/event/MouseButtonEvent.java
index 83c383cef7..0d5ddc0a52 100644
--- a/jme3-core/src/main/java/com/jme3/input/event/MouseButtonEvent.java
+++ b/jme3-core/src/main/java/com/jme3/input/event/MouseButtonEvent.java
@@ -40,10 +40,10 @@
*/
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;
diff --git a/jme3-core/src/main/java/com/jme3/input/event/MouseMotionEvent.java b/jme3-core/src/main/java/com/jme3/input/event/MouseMotionEvent.java
index e88c09c1b6..0bf269f3cc 100644
--- a/jme3-core/src/main/java/com/jme3/input/event/MouseMotionEvent.java
+++ b/jme3-core/src/main/java/com/jme3/input/event/MouseMotionEvent.java
@@ -40,7 +40,7 @@
*/
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;
diff --git a/jme3-core/src/main/java/com/jme3/light/AmbientLight.java b/jme3-core/src/main/java/com/jme3/light/AmbientLight.java
index 531c7b5d0b..78fc69c41a 100644
--- a/jme3-core/src/main/java/com/jme3/light/AmbientLight.java
+++ b/jme3-core/src/main/java/com/jme3/light/AmbientLight.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2012, 2015 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -83,4 +83,13 @@ public Type getType() {
return Type.Ambient;
}
+ @Override
+ public String toString() {
+ return getClass().getSimpleName()
+ + "[name=" + name
+ + ", color=" + color
+ + ", enabled=" + enabled
+ + "]";
+ }
+
}
diff --git a/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java b/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java
index 53528cf9d0..ba142071f4 100644
--- a/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java
+++ b/jme3-core/src/main/java/com/jme3/light/DirectionalLight.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2012, 2015-2016 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -132,11 +132,6 @@ public Type getType() {
return Type.Directional;
}
- @Override
- public String toString() {
- return getClass().getSimpleName() + "[name=" + name + ", direction=" + direction + ", color=" + color + ", enabled=" + enabled + "]";
- }
-
@Override
public void write(JmeExporter ex) throws IOException {
super.write(ex);
@@ -157,4 +152,14 @@ public DirectionalLight clone() {
l.direction = direction.clone();
return l;
}
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName()
+ + "[name=" + name
+ + ", direction=" + direction
+ + ", color=" + color
+ + ", enabled=" + enabled
+ + "]";
+ }
}
diff --git a/jme3-core/src/main/java/com/jme3/light/Light.java b/jme3-core/src/main/java/com/jme3/light/Light.java
index 4cbea59208..cebb4ae2a0 100644
--- a/jme3-core/src/main/java/com/jme3/light/Light.java
+++ b/jme3-core/src/main/java/com/jme3/light/Light.java
@@ -87,7 +87,7 @@ public enum Type {
Probe(4);
- final private int typeId;
+ private final int typeId;
Type(int type){
this.typeId = type;
diff --git a/jme3-core/src/main/java/com/jme3/light/LightProbe.java b/jme3-core/src/main/java/com/jme3/light/LightProbe.java
index c2c9a584be..7ab654f352 100644
--- a/jme3-core/src/main/java/com/jme3/light/LightProbe.java
+++ b/jme3-core/src/main/java/com/jme3/light/LightProbe.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -48,7 +48,7 @@
/**
* A LightProbe is not exactly a light. It holds environment map information used for Image Based Lighting.
* This is used for indirect lighting in the Physically Based Rendering pipeline.
- *
+ *
* A light probe has a position in world space. This is the position from where the Environment Map are rendered.
* There are two environment data structure held by the LightProbe :
* - The irradiance spherical harmonics factors (used for indirect diffuse lighting in the PBR pipeline).
@@ -57,9 +57,9 @@
* To compute them see
* {@link com.jme3.environment.LightProbeFactory#makeProbe(com.jme3.environment.EnvironmentCamera, com.jme3.scene.Spatial)}
* and {@link EnvironmentCamera}.
- *
+ *
* The light probe has an area of effect centered on its position. It can have a Spherical area or an Oriented Box area
- *
+ *
* A LightProbe will only be taken into account when it's marked as ready and enabled.
* A light probe is ready when it has valid environment map data set.
* Note that you should never call setReady yourself.
@@ -71,7 +71,8 @@
public class LightProbe extends Light implements Savable {
private static final Logger logger = Logger.getLogger(LightProbe.class.getName());
- public static final Matrix4f FALLBACK_MATRIX = new Matrix4f(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1);
+ public static final Matrix4f FALLBACK_MATRIX = new Matrix4f(
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1);
private Vector3f[] shCoefficients;
private TextureCubeMap prefilteredEnvMap;
@@ -149,16 +150,12 @@ public void setPrefilteredMap(TextureCubeMap prefilteredEnvMap) {
* @return the pre-existing matrix
*/
public Matrix4f getUniformMatrix(){
-
Matrix4f mat = area.getUniformMatrix();
-
// setting the (sp) entry of the matrix
mat.m33 = nbMipMaps + 1f / area.getRadius();
-
return mat;
}
-
@Override
public void write(JmeExporter ex) throws IOException {
super.write(ex);
@@ -180,7 +177,7 @@ public void read(JmeImporter im) throws IOException {
position = (Vector3f) ic.readSavable("position", null);
area = (ProbeArea)ic.readSavable("area", null);
if(area == null) {
- // retro compat
+ // retro compatibility
BoundingSphere bounds = (BoundingSphere) ic.readSavable("bounds", new BoundingSphere(1.0f, Vector3f.ZERO));
area = new SphereProbeArea(bounds.getCenter(), bounds.getRadius());
}
@@ -200,7 +197,6 @@ public void read(JmeImporter im) throws IOException {
}
}
-
/**
* returns the bounding volume of this LightProbe
* @return a bounding volume.
@@ -318,8 +314,12 @@ public Type getType() {
@Override
public String toString() {
- return "Light Probe : " + name + " at " + position + " / " + area;
+ return getClass().getSimpleName()
+ + "[name=" + name
+ + ", position=" + position
+ + ", area=" + area
+ + ", enabled=" + enabled
+ + "]";
}
-
}
diff --git a/jme3-core/src/main/java/com/jme3/light/PointLight.java b/jme3-core/src/main/java/com/jme3/light/PointLight.java
index 959147c9a0..37748f48d1 100644
--- a/jme3-core/src/main/java/com/jme3/light/PointLight.java
+++ b/jme3-core/src/main/java/com/jme3/light/PointLight.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2012, 2015-2016, 2018 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -165,6 +165,13 @@ public final void setRadius(float radius) {
if (radius < 0) {
throw new IllegalArgumentException("Light radius cannot be negative");
}
+ if(Float.isNaN(radius)){
+ throw new IllegalArgumentException("Light radius cannot be a NaN (Not a Number) value");
+ }
+
+ float maxSafeRadius = Float.MAX_VALUE / 4.0f;
+ radius = Math.min(radius, maxSafeRadius); // Caps radius to a safe large value; avoids overflow in shaders from values reaching max float value
+
this.radius = radius;
if (radius != 0f) {
this.invRadius = 1f / radius;
@@ -242,4 +249,17 @@ public PointLight clone() {
p.position = position.clone();
return p;
}
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName()
+ + "[name=" + name
+ + ", position=" + position
+ + ", radius=" + radius
+ + ", color=" + color
+ + ", enabled=" + enabled
+ + "]";
+ }
}
+
+
diff --git a/jme3-core/src/main/java/com/jme3/light/SphereProbeArea.java b/jme3-core/src/main/java/com/jme3/light/SphereProbeArea.java
index 89f7c07635..bf5bd1d539 100644
--- a/jme3-core/src/main/java/com/jme3/light/SphereProbeArea.java
+++ b/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() {
}
diff --git a/jme3-core/src/main/java/com/jme3/light/SpotLight.java b/jme3-core/src/main/java/com/jme3/light/SpotLight.java
index 417e845f0b..e122fe2cbc 100644
--- a/jme3-core/src/main/java/com/jme3/light/SpotLight.java
+++ b/jme3-core/src/main/java/com/jme3/light/SpotLight.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -118,8 +118,7 @@ public SpotLight(Vector3f position, Vector3f direction, ColorRGBA color) {
setPosition(position);
setDirection(direction);
}
-
-
+
/**
* Creates a SpotLight at the given position, with the given direction,
* the given range and the given color.
@@ -160,7 +159,6 @@ public SpotLight(Vector3f position, Vector3f direction, float range, ColorRGBA c
setDirection(direction);
setSpotRange(range);
}
-
private void computeAngleParameters() {
float innerCos = FastMath.cos(spotInnerAngle);
@@ -458,5 +456,18 @@ public SpotLight clone() {
s.position = position.clone();
return s;
}
-}
+ @Override
+ public String toString() {
+ return getClass().getSimpleName()
+ + "[name=" + name
+ + ", direction=" + direction
+ + ", position=" + position
+ + ", range=" + spotRange
+ + ", innerAngle=" + spotInnerAngle
+ + ", outerAngle=" + spotOuterAngle
+ + ", color=" + color
+ + ", enabled=" + enabled
+ + "]";
+ }
+}
diff --git a/jme3-core/src/main/java/com/jme3/light/WeightedProbeBlendingStrategy.java b/jme3-core/src/main/java/com/jme3/light/WeightedProbeBlendingStrategy.java
index 0f3013cba6..5c5da45d8e 100644
--- a/jme3-core/src/main/java/com/jme3/light/WeightedProbeBlendingStrategy.java
+++ b/jme3-core/src/main/java/com/jme3/light/WeightedProbeBlendingStrategy.java
@@ -45,7 +45,7 @@
*/
public class WeightedProbeBlendingStrategy implements LightProbeBlendingStrategy {
- private final static int MAX_PROBES = 3;
+ private static final int MAX_PROBES = 3;
List lightProbes = new ArrayList<>();
@Override
diff --git a/jme3-core/src/main/java/com/jme3/material/MatParam.java b/jme3-core/src/main/java/com/jme3/material/MatParam.java
index 35111331be..8e4168133e 100644
--- a/jme3-core/src/main/java/com/jme3/material/MatParam.java
+++ b/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 MatParam(VarType type, String name, Object value) {
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 VarType getVarType() {
/**
* Returns the name of the material parameter.
+ *
* @return the name of the material parameter.
*/
public String getName() {
@@ -158,15 +168,16 @@ public void setValue(Object value) {
}
}
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 @@ public String getValueAsString() {
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 @@ public String getValueAsString() {
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 @@ private String getWrapMode(Texture texVal, Texture.WrapAxis axis) {
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 "";
}
diff --git a/jme3-core/src/main/java/com/jme3/material/MatParamTexture.java b/jme3-core/src/main/java/com/jme3/material/MatParamTexture.java
index 187eebc72d..58bd44418d 100644
--- a/jme3-core/src/main/java/com/jme3/material/MatParamTexture.java
+++ b/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.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 void setColorSpace(ColorSpace colorSpace) {
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);
}
-}
\ No newline at end of file
+
+}
diff --git a/jme3-core/src/main/java/com/jme3/material/Material.java b/jme3-core/src/main/java/com/jme3/material/Material.java
index 926db40236..0c4317a307 100644
--- a/jme3-core/src/main/java/com/jme3/material/Material.java
+++ b/jme3-core/src/main/java/com/jme3/material/Material.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -34,12 +34,20 @@
import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetManager;
import com.jme3.asset.CloneableSmartAsset;
-import com.jme3.export.*;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
import com.jme3.light.LightList;
import com.jme3.material.RenderState.BlendMode;
import com.jme3.material.RenderState.FaceCullMode;
import com.jme3.material.TechniqueDef.LightMode;
-import com.jme3.math.*;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Matrix4f;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.math.Vector4f;
import com.jme3.renderer.Caps;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.Renderer;
@@ -47,14 +55,20 @@
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.TextureImage;
import com.jme3.texture.image.ColorSpace;
import com.jme3.util.ListMap;
import com.jme3.util.SafeArrayList;
import java.io.IOException;
-import java.util.*;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -75,18 +89,37 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
public static final int SAVABLE_VERSION = 2;
private static final Logger logger = Logger.getLogger(Material.class.getName());
- private AssetKey key;
+ private AssetKey> key;
private String name;
private MaterialDef def;
private ListMap paramValues = new ListMap<>();
private Technique technique;
private HashMap 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;
+ /**
+ * Manages and tracks texture and buffer binding units for rendering.
+ * Used internally by the Material class.
+ */
+ public static class BindUnits {
+ /** The current texture unit counter. */
+ public int textureUnit = 0;
+ /** The current buffer unit counter. */
+ public int bufferUnit = 0;
+ }
+ private BindUnits bindUnits = new BindUnits();
+
+ /**
+ * Constructs a new Material instance based on a provided MaterialDef.
+ * The material's parameters will be initialized with default values from the definition.
+ *
+ * @param def The material definition to use (cannot be null).
+ * @throws IllegalArgumentException if def is null.
+ */
public Material(MaterialDef def) {
if (def == null) {
throw new IllegalArgumentException("Material definition cannot be null");
@@ -101,40 +134,48 @@ public Material(MaterialDef def) {
}
}
- public Material(AssetManager contentMan, String defName) {
- this(contentMan.loadAsset(new AssetKey(defName)));
+ /**
+ * Constructs a new Material by loading its MaterialDef from the asset manager.
+ *
+ * @param assetManager The asset manager to load the MaterialDef from.
+ * @param defName The asset path of the .j3md file.
+ */
+ public Material(AssetManager assetManager, String defName) {
+ this(assetManager.loadAsset(new AssetKey(defName)));
}
/**
- * Do not use this constructor. Serialization purposes only.
+ * For serialization only. Do not use.
*/
public Material() {
}
/**
* Returns the asset key name of the asset from which this material was loaded.
+ *
This value will be null unless this material was loaded from a .j3m file.
*
- *
This value will be null unless this material was loaded
- * from a .j3m file.
- *
- * @return Asset key name of the j3m file
+ * @return Asset key name of the .j3m file, or null if not loaded from a file.
*/
public String getAssetName() {
return key != null ? key.getName() : null;
}
/**
- * @return the name of the material (not the same as the asset name), the returned value can be null
+ * Returns the user-defined name of the material.
+ * This name is distinct from the asset name and may be null or not unique.
+ *
+ * @return The name of the material, or null.
*/
public String getName() {
return name;
}
/**
- * This method sets the name of the material.
+ * Sets the user-defined name of the material.
* The name is not the same as the asset name.
- * It can be null and there is no guarantee of its uniqueness.
- * @param name the name of the material
+ * It can be null, and there is no guarantee of its uniqueness.
+ *
+ * @param name The name of the material.
*/
public void setName(String name) {
this.name = name;
@@ -216,7 +257,7 @@ public Material clone() {
}
/**
- * Compares two materials and returns true if they are equal.
+ * Compares two materials for content equality.
* This methods compare definition, parameters, additional render states.
* Since materials are mutable objects, implementing equals() properly is not possible,
* hence the name contentEquals().
@@ -393,7 +434,7 @@ public RenderState getAdditionalRenderState() {
}
/**
- * Get the material definition (j3md file info) that this
+ * Get the material definition (.j3md file info) that this
* material is implementing.
*
* @return the material definition this material implements.
@@ -440,7 +481,7 @@ public MatParamTexture getTextureParam(String name) {
}
return null;
}
-
+
/**
* Returns a collection of all parameters set on this material.
*
@@ -482,7 +523,7 @@ private void checkSetParam(VarType type, String name) {
/**
* Pass a parameter to the material shader.
*
- * @param name the name of the parameter defined in the material definition (j3md)
+ * @param name the name of the parameter defined in the material definition (.j3md)
* @param type the type of the parameter {@link VarType}
* @param value the value of the parameter
*/
@@ -494,7 +535,6 @@ public void setParam(String name, VarType type, Object value) {
} else {
MatParam val = getParam(name);
if (val == null) {
- MatParam paramDef = def.getMaterialParam(name);
paramValues.put(name, new MatParam(type, name, value));
} else {
val.setValue(value);
@@ -503,9 +543,24 @@ public void setParam(String name, VarType type, Object value) {
if (technique != null) {
technique.notifyParamChanged(name, type, value);
}
+ if (type.isImageType()) {
+ // recompute sort id
+ sortingId = -1;
+ }
}
}
+ /**
+ * 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
@@ -541,14 +596,17 @@ public void setTextureParam(String name, VarType type, Texture value) {
}
checkSetParam(type, name);
- MatParamTexture val = getTextureParam(name);
- if (val == null) {
- checkTextureParamColorSpace(name, value);
- paramValues.put(name, new MatParamTexture(type, name, value, value.getImage() != null ? value.getImage().getColorSpace() : null));
+ MatParamTexture param = getTextureParam(name);
+
+ checkTextureParamColorSpace(name, value);
+ ColorSpace colorSpace = value.getImage() != null ? value.getImage().getColorSpace() : null;
+
+ if (param == null) {
+ param = new MatParamTexture(type, name, value, colorSpace);
+ paramValues.put(name, param);
} else {
- checkTextureParamColorSpace(name, value);
- val.setTextureValue(value);
- val.setColorSpace(value.getImage() != null ? value.getImage().getColorSpace() : null);
+ param.setTextureValue(value);
+ param.setColorSpace(colorSpace);
}
if (technique != null) {
@@ -585,8 +643,8 @@ private void checkTextureParamColorSpace(String name, Texture value) {
/**
* Pass a texture to the material shader.
*
- * @param name the name of the texture defined in the material definition
- * (j3md) (for example Texture for Lighting.j3md)
+ * @param name the name of the texture defined in the material definition
+ * (.j3md) (e.g. Texture for Lighting.j3md)
* @param value the Texture object previously loaded by the asset manager
*/
public void setTexture(String name, Texture value) {
@@ -679,14 +737,13 @@ public void setColor(String name, ColorRGBA value) {
}
/**
- * Pass an uniform buffer object to the material shader.
+ * Pass a uniform buffer object to the material shader.
*
* @param name the name of the buffer object defined in the material definition (j3md).
* @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 +753,7 @@ public void setUniformBufferObject(final String name, final BufferObject value)
* @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 +853,7 @@ public void selectTechnique(String name, final RenderManager renderManager) {
sortingId = -1;
}
- private int applyOverrides(Renderer renderer, Shader shader, SafeArrayList overrides, int unit) {
+ private void applyOverrides(Renderer renderer, Shader shader, SafeArrayList overrides, BindUnits bindUnits) {
for (MatParamOverride override : overrides.getArray()) {
VarType type = override.getVarType();
@@ -810,96 +866,110 @@ private int applyOverrides(Renderer renderer, Shader shader, SafeArrayList worldOverrides, SafeArrayList forcedOverrides) {
+ private BindUnits updateShaderMaterialParameters(Renderer renderer, Shader shader,
+ SafeArrayList worldOverrides, SafeArrayList forcedOverrides) {
+
+ bindUnits.textureUnit = 0;
+ bindUnits.bufferUnit = 0;
- int unit = 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++) {
-
MatParam param = paramValues.getValue(i);
VarType type = param.getVarType();
+ updateShaderMaterialParameter(renderer, type, shader, param, bindUnits, false);
+ }
- 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;
- }
+ // TODO: HACKY HACK remove this when texture unit is handled by the uniform.
+ return bindUnits;
+ }
- 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());
- }
- }
+ private void updateRenderState(Geometry geometry, RenderManager renderManager, Renderer renderer, TechniqueDef techniqueDef) {
+ RenderState finalRenderState;
+ if (renderManager.getForcedRenderState() != null) {
+ finalRenderState = mergedRenderState.copyFrom(renderManager.getForcedRenderState());
+ } else if (techniqueDef.getRenderState() != null) {
+ finalRenderState = mergedRenderState.copyFrom(RenderState.DEFAULT);
+ finalRenderState = techniqueDef.getRenderState().copyMergedTo(additionalState, finalRenderState);
+ } else {
+ finalRenderState = mergedRenderState.copyFrom(RenderState.DEFAULT);
+ finalRenderState = RenderState.DEFAULT.copyMergedTo(additionalState, finalRenderState);
}
-
- //TODO HACKY HACK remove this when texture unit is handled by the uniform.
- return unit;
+ // test if the face cull mode should be flipped before render
+ if (finalRenderState.isFaceCullFlippable() && isNormalsBackward(geometry.getWorldScale())) {
+ finalRenderState.flipFaceCull();
+ }
+ renderer.applyRenderState(finalRenderState);
}
/**
- * Returns true if the type is Buffer Object's type.
+ * Returns true if the geometry world scale indicates that normals will be backward.
*
- * @param type the material parameter type.
- * @return true if the type is Buffer Object's type.
+ * @param scalar The geometry's world scale vector.
+ * @return true if the normals are effectively backward; false otherwise.
*/
- private boolean isBO(final VarType type) {
- return type == VarType.BufferObject;
- }
-
- private void updateRenderState(RenderManager renderManager, Renderer renderer, TechniqueDef techniqueDef) {
- if (renderManager.getForcedRenderState() != null) {
- renderer.applyRenderState(renderManager.getForcedRenderState());
- } else {
- if (techniqueDef.getRenderState() != null) {
- renderer.applyRenderState(techniqueDef.getRenderState().copyMergedTo(additionalState, mergedRenderState));
- } else {
- renderer.applyRenderState(RenderState.DEFAULT.copyMergedTo(additionalState, mergedRenderState));
- }
- }
+ private boolean isNormalsBackward(Vector3f scalar) {
+ // count number of negative scalar vector components
+ int n = 0;
+ if (scalar.x < 0) n++;
+ if (scalar.y < 0) n++;
+ if (scalar.z < 0) n++;
+ // An odd number of negative components means the normal vectors
+ // are backward to what they should be.
+ return n == 1 || n == 3;
}
/**
@@ -1028,7 +1098,7 @@ public void render(Geometry geometry, LightList lights, RenderManager renderMana
}
// Apply render state
- updateRenderState(renderManager, renderer, techniqueDef);
+ updateRenderState(geometry, renderManager, renderer, techniqueDef);
// Get world overrides
SafeArrayList overrides = geometry.getWorldMatParamOverrides();
@@ -1043,13 +1113,13 @@ public void render(Geometry geometry, LightList lights, RenderManager renderMana
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);
}
/**
@@ -1066,24 +1136,25 @@ public void render(Geometry geom, RenderManager rm) {
render(geom, geom.getWorldLightList(), rm);
}
+ @Override
+ public String toString() {
+ return "Material[name=" + name +
+ ", def=" + (def != null ? def.getName() : null) +
+ ", tech=" + (technique != null && technique.getDef() != null ? technique.getDef().getName() : null) +
+ "]";
+ }
+
@Override
public void write(JmeExporter ex) throws IOException {
OutputCapsule oc = ex.getCapsule(this);
oc.write(def.getAssetName(), "material_def", null);
oc.write(additionalState, "render_state", null);
oc.write(transparent, "is_transparent", false);
+ oc.write(receivesShadows, "receives_shadows", false);
oc.write(name, "name", null);
oc.writeStringSavableMap(paramValues, "parameters", null);
}
- @Override
- public String toString() {
- return "Material[name=" + name +
- ", def=" + (def != null ? def.getName() : null) +
- ", tech=" + (technique != null && technique.getDef() != null ? technique.getDef().getName() : null) +
- "]";
- }
-
@Override
@SuppressWarnings("unchecked")
public void read(JmeImporter im) throws IOException {
@@ -1092,12 +1163,13 @@ public void read(JmeImporter im) throws IOException {
name = ic.readString("name", null);
additionalState = (RenderState) ic.readSavable("render_state", null);
transparent = ic.readBoolean("is_transparent", false);
+ receivesShadows = ic.readBoolean("receives_shadows", false);
// Load the material def
String defName = ic.readString("material_def", null);
HashMap params = (HashMap) ic.readStringSavableMap("parameters", null);
- boolean enableVcolor = false;
+ boolean enableVertexColor = false;
boolean separateTexCoord = false;
boolean applyDefaultValues = false;
boolean guessRenderStateApply = false;
@@ -1113,7 +1185,7 @@ public void read(JmeImporter im) throws IOException {
// Enable compatibility with old models
if (defName.equalsIgnoreCase("Common/MatDefs/Misc/VertexColor.j3md")) {
// Using VertexColor, switch to Unshaded and set VertexColor=true
- enableVcolor = true;
+ enableVertexColor = true;
defName = "Common/MatDefs/Misc/Unshaded.j3md";
} else if (defName.equalsIgnoreCase("Common/MatDefs/Misc/SimpleTextured.j3md")
|| defName.equalsIgnoreCase("Common/MatDefs/Misc/SolidColor.j3md")) {
@@ -1165,8 +1237,7 @@ public void read(JmeImporter im) throws IOException {
}
if (applyDefaultValues) {
- // compatability with old versions where default vars were
- // not available
+ // compatibility with old versions where default vars were not available
for (MatParam param : def.getMaterialParams()) {
if (param.getValue() != null && paramValues.get(param.getName()) == null) {
setParam(param.getName(), param.getVarType(), param.getValue());
@@ -1185,7 +1256,7 @@ public void read(JmeImporter im) throws IOException {
additionalState.applyStencilTest = additionalState.stencilTest;
additionalState.applyWireFrame = additionalState.wireframe;
}
- if (enableVcolor) {
+ if (enableVertexColor) {
setBoolean("VertexColor", true);
}
if (separateTexCoord) {
diff --git a/jme3-core/src/main/java/com/jme3/material/Materials.java b/jme3-core/src/main/java/com/jme3/material/Materials.java
index 8ddc7b75d3..0b0003323e 100644
--- a/jme3-core/src/main/java/com/jme3/material/Materials.java
+++ b/jme3-core/src/main/java/com/jme3/material/Materials.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -38,13 +38,17 @@
*/
public class Materials {
- public static final String UNSHADED = "Common/MatDefs/Misc/Unshaded.j3md";
- public static final String LIGHTING = "Common/MatDefs/Light/Lighting.j3md";
- public static final String PBR = "Common/MatDefs/Light/PBRLighting.j3md";
+ public static final String SHOW_NORMALS = "Common/MatDefs/Misc/ShowNormals.j3md";
+ public static final String UNSHADED = "Common/MatDefs/Misc/Unshaded.j3md";
+ public static final String LIGHTING = "Common/MatDefs/Light/Lighting.j3md";
+ public static final String PBR = "Common/MatDefs/Light/PBRLighting.j3md";
+ public static final String PARTICLE = "Common/MatDefs/Misc/Particle.j3md";
+ public static final String BILLBOARD = "Common/MatDefs/Misc/Billboard.j3md";
+ public static final String GUI = "Common/MatDefs/Gui/Gui.j3md";
/**
* A private constructor to inhibit instantiation of this class.
*/
private Materials() {
}
-}
\ No newline at end of file
+}
diff --git a/jme3-core/src/main/java/com/jme3/material/RenderState.java b/jme3-core/src/main/java/com/jme3/material/RenderState.java
index e0be1f0563..2e93a4e1f3 100644
--- a/jme3-core/src/main/java/com/jme3/material/RenderState.java
+++ b/jme3-core/src/main/java/com/jme3/material/RenderState.java
@@ -1511,6 +1511,10 @@ public int contentHashCode() {
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 RenderState copyMergedTo(RenderState additionalState, RenderState state)
state.frontStencilFunction = additionalState.frontStencilFunction;
state.backStencilFunction = additionalState.backStencilFunction;
+
+ state.frontStencilMask = additionalState.frontStencilMask;
+ state.frontStencilReference = additionalState.frontStencilReference;
+ state.backStencilMask = additionalState.backStencilMask;
+ state.backStencilReference = additionalState.backStencilReference;
} else {
state.stencilTest = stencilTest;
@@ -1636,6 +1645,11 @@ public RenderState copyMergedTo(RenderState additionalState, RenderState state)
state.frontStencilFunction = frontStencilFunction;
state.backStencilFunction = backStencilFunction;
+
+ state.frontStencilMask = frontStencilMask;
+ state.frontStencilReference = frontStencilReference;
+ state.backStencilMask = backStencilMask;
+ state.backStencilReference = backStencilReference;
}
if (additionalState.applyLineWidth) {
state.lineWidth = additionalState.lineWidth;
@@ -1665,6 +1679,10 @@ public void set(RenderState state) {
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;
@@ -1685,6 +1703,57 @@ public void set(RenderState state) {
sfactorAlpha = state.sfactorAlpha;
dfactorAlpha = state.dfactorAlpha;
}
+
+ /**
+ * Copy all values from the given state to this state.
+ *
{@code FrontAndBack} and {@code Off} are unaffected. This is important
+ * for flipping the cull mode when normal vectors are found to be backward.
+ */
+ public void flipFaceCull() {
+ switch (cullMode) {
+ case Back: cullMode = FaceCullMode.Front; break;
+ case Front: cullMode = FaceCullMode.Back; break;
+ }
+ }
+
+ /**
+ * Checks if the face cull mode is "flippable".
+ *
The cull mode is flippable when it is either {@code Front} or {@code Back}.
+ * @return
+ */
+ public boolean isFaceCullFlippable() {
+ return cullMode == FaceCullMode.Front || cullMode == FaceCullMode.Back;
+ }
+
}
diff --git a/jme3-core/src/main/java/com/jme3/material/Technique.java b/jme3-core/src/main/java/com/jme3/material/Technique.java
index 667594f09c..5091028a85 100644
--- a/jme3-core/src/main/java/com/jme3/material/Technique.java
+++ b/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 @@
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 @@ Shader makeCurrent(RenderManager renderManager, SafeArrayList
* @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);
}
/**
diff --git a/jme3-core/src/main/java/com/jme3/material/TechniqueDef.java b/jme3-core/src/main/java/com/jme3/material/TechniqueDef.java
index daa08d3266..6e40ff366e 100644
--- a/jme3-core/src/main/java/com/jme3/material/TechniqueDef.java
+++ b/jme3-core/src/main/java/com/jme3/material/TechniqueDef.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2023 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -422,11 +422,6 @@ public VarType getDefineIdType(int defineId) {
public void addShaderParamDefine(String paramName, VarType paramType, String defineName) {
int defineId = defineNames.size();
- if (defineId >= DefineList.MAX_DEFINES) {
- throw new IllegalStateException("Cannot have more than " +
- DefineList.MAX_DEFINES + " defines on a technique.");
- }
-
paramToDefineId.put(paramName, defineId);
defineNames.add(defineName);
defineTypes.add(paramType);
@@ -445,11 +440,6 @@ public void addShaderParamDefine(String paramName, VarType paramType, String def
public int addShaderUnmappedDefine(String defineName, VarType defineType) {
int defineId = defineNames.size();
- if (defineId >= DefineList.MAX_DEFINES) {
- throw new IllegalStateException("Cannot have more than " +
- DefineList.MAX_DEFINES + " defines on a technique.");
- }
-
defineNames.add(defineName);
defineTypes.add(defineType);
return defineId;
@@ -834,4 +824,4 @@ public TechniqueDef clone() throws CloneNotSupportedException {
return clone;
}
-}
+}
\ No newline at end of file
diff --git a/jme3-core/src/main/java/com/jme3/material/logic/DefaultTechniqueDefLogic.java b/jme3-core/src/main/java/com/jme3/material/logic/DefaultTechniqueDefLogic.java
index 9bb317219f..ca8f7d1efa 100644
--- a/jme3-core/src/main/java/com/jme3/material/logic/DefaultTechniqueDefLogic.java
+++ b/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 @@
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 @@ protected static ColorRGBA getAmbientColor(LightList lightList, boolean removeLi
@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);
diff --git a/jme3-core/src/main/java/com/jme3/material/logic/MultiPassLightingLogic.java b/jme3-core/src/main/java/com/jme3/material/logic/MultiPassLightingLogic.java
index d5c48b1df9..9340d3560d 100644
--- a/jme3-core/src/main/java/com/jme3/material/logic/MultiPassLightingLogic.java
+++ b/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.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 MultiPassLightingLogic(TechniqueDef techniqueDef) {
}
@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");
diff --git a/jme3-core/src/main/java/com/jme3/material/logic/SinglePassAndImageBasedLightingLogic.java b/jme3-core/src/main/java/com/jme3/material/logic/SinglePassAndImageBasedLightingLogic.java
index 801d5b7837..8e38f2e6ca 100644
--- a/jme3-core/src/main/java/com/jme3/material/logic/SinglePassAndImageBasedLightingLogic.java
+++ b/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 @@
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 lightProbes = new ArrayList<>(3);
+ private final List lightProbes = new ArrayList<>(3);
static {
ADDITIVE_LIGHT.setBlendMode(BlendMode.AlphaAdditive);
@@ -262,22 +263,21 @@ private int setProbeData(RenderManager rm, int lastTexUnit, Uniform lightProbeDa
}
@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) {
diff --git a/jme3-core/src/main/java/com/jme3/material/logic/SinglePassLightingLogic.java b/jme3-core/src/main/java/com/jme3/material/logic/SinglePassLightingLogic.java
index a9b51783c2..58240569ef 100644
--- a/jme3-core/src/main/java/com/jme3/material/logic/SinglePassLightingLogic.java
+++ b/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.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 @@ protected int updateLightListUniforms(Shader shader, Geometry g, LightList light
}
@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();
diff --git a/jme3-core/src/main/java/com/jme3/material/logic/StaticPassLightingLogic.java b/jme3-core/src/main/java/com/jme3/material/logic/StaticPassLightingLogic.java
index 5afee8f4b0..fe35dc4c4d 100644
--- a/jme3-core/src/main/java/com/jme3/material/logic/StaticPassLightingLogic.java
+++ b/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.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 @@ private void updateLightListUniforms(Matrix4f viewMatrix, Shader shader, LightLi
}
@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);
diff --git a/jme3-core/src/main/java/com/jme3/material/logic/TechniqueDefLogic.java b/jme3-core/src/main/java/com/jme3/material/logic/TechniqueDefLogic.java
index d021d28d0f..31f970a176 100644
--- a/jme3-core/src/main/java/com/jme3/material/logic/TechniqueDefLogic.java
+++ b/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 @@
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 Shader makeCurrent(AssetManager assetManager, RenderManager renderManager
* @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);
}
diff --git a/jme3-core/src/main/java/com/jme3/math/AbstractTriangle.java b/jme3-core/src/main/java/com/jme3/math/AbstractTriangle.java
index 87dc01d51f..df1ef1aa01 100644
--- a/jme3-core/src/main/java/com/jme3/math/AbstractTriangle.java
+++ b/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:
+ *
+ *
+ * @return the string representation (not null, not empty)
+ */
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + " [V1: " + get1() + " V2: "
+ + get2() + " V3: " + get3() + "]";
+ }
}
diff --git a/jme3-core/src/main/java/com/jme3/math/ColorRGBA.java b/jme3-core/src/main/java/com/jme3/math/ColorRGBA.java
index 03cde0991d..a70a01bd21 100644
--- a/jme3-core/src/main/java/com/jme3/math/ColorRGBA.java
+++ b/jme3-core/src/main/java/com/jme3/math/ColorRGBA.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -30,18 +30,23 @@
*/
package com.jme3.math;
-import com.jme3.export.*;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+
import java.io.IOException;
/**
* ColorRGBA defines a color made from a collection of red, green
- * and blue values stored in Linear color space. An alpha value determines is
+ * and blue values stored in Linear color space. An alpha value determines its
* transparency.
*
* @author Mark Powell
- * @version $Id: ColorRGBA.java,v 1.29 2007/09/09 18:25:14 irrisor Exp $
*/
public final class ColorRGBA implements Savable, Cloneable, java.io.Serializable {
+
static final float GAMMA = 2.2f;
static final long serialVersionUID = 1;
@@ -153,15 +158,15 @@ public ColorRGBA(float r, float g, float b, float a) {
* Copy constructor creates a new ColorRGBA object, based on
* a provided color.
*
- * @param rgba The ColorRGBA object to copy.
+ * @param color The ColorRGBA object to copy.
*/
- public ColorRGBA(ColorRGBA rgba) {
- this.a = rgba.a;
- this.r = rgba.r;
- this.g = rgba.g;
- this.b = rgba.b;
+ public ColorRGBA(ColorRGBA color) {
+ this.a = color.a;
+ this.r = color.r;
+ this.g = color.g;
+ this.b = color.b;
}
-
+
/**
* Constructor creates a new ColorRGBA object, based on
* a provided Vector4f.
@@ -170,12 +175,9 @@ public ColorRGBA(ColorRGBA rgba) {
* values copied to this color's r, g, b, and a values respectively.
*/
public ColorRGBA(Vector4f vec4) {
- this.a = vec4.w;
- this.r = vec4.x;
- this.g = vec4.y;
- this.b = vec4.z;
- }
-
+ set(vec4);
+ }
+
/**
* Constructor creates a new ColorRGBA object, based on
* a provided Vector3f, at full opacity with a 1.0 alpha value by default
@@ -185,10 +187,8 @@ public ColorRGBA(Vector4f vec4) {
*/
public ColorRGBA(Vector3f vec3) {
this.a = 1.0f;
- this.r = vec3.x;
- this.g = vec3.y;
- this.b = vec3.z;
- }
+ set(vec3);
+ }
/**
* set sets the RGBA values of this ColorRGBA.
@@ -214,24 +214,24 @@ public ColorRGBA set(float r, float g, float b, float a) {
* set sets the values of this ColorRGBA to those
* set by a parameter color.
*
- * @param rgba The color to set this ColorRGBA to.
+ * @param color The color to set this ColorRGBA to.
* @return this
*/
- public ColorRGBA set(ColorRGBA rgba) {
- if (rgba == null) {
+ public ColorRGBA set(ColorRGBA color) {
+ if (color == null) {
r = 0;
g = 0;
b = 0;
a = 0;
} else {
- r = rgba.r;
- g = rgba.g;
- b = rgba.b;
- a = rgba.a;
+ r = color.r;
+ g = color.g;
+ b = color.b;
+ a = color.a;
}
return this;
}
-
+
/**
* set sets the values of this ColorRGBA to those
* set by a parameter Vector4f.
@@ -254,8 +254,8 @@ public ColorRGBA set(Vector4f vec4) {
a = vec4.w;
}
return this;
- }
-
+ }
+
/**
* set sets the values of this ColorRGBA to those
* set by a parameter Vector3f.
@@ -276,7 +276,7 @@ public ColorRGBA set(Vector3f vec3) {
b = vec3.z;
}
return this;
- }
+ }
/**
* Sets the red color to the specified value.
@@ -336,22 +336,18 @@ public void clamp() {
* @return The float array that contains the color components.
*/
public float[] getColorArray() {
- return new float[]{r, g, b, a};
+ return getColorArray(null);
}
/**
* Stores the current r,g,b,a values into the given array. The given array must have a
* length of 4 or greater, or an array index out of bounds exception will be thrown.
*
- * @param store The float array to store the values into.
+ * @param store The float array to store the values into. If null, a new array is created.
* @return The float array after storage.
*/
public float[] getColorArray(float[] store) {
- store[0] = r;
- store[1] = g;
- store[2] = b;
- store[3] = a;
- return store;
+ return toArray(store);
}
/**
@@ -401,11 +397,7 @@ public float getGreen() {
* @return this ColorRGBA
*/
public ColorRGBA interpolateLocal(ColorRGBA finalColor, float changeAmount) {
- this.r = (1 - changeAmount) * this.r + changeAmount * finalColor.r;
- this.g = (1 - changeAmount) * this.g + changeAmount * finalColor.g;
- this.b = (1 - changeAmount) * this.b + changeAmount * finalColor.b;
- this.a = (1 - changeAmount) * this.a + changeAmount * finalColor.a;
- return this;
+ return interpolateLocal(this, finalColor, changeAmount);
}
/**
@@ -416,7 +408,7 @@ public ColorRGBA interpolateLocal(ColorRGBA finalColor, float changeAmount) {
* @param beginColor The beginning color (changeAmount=0).
* @param finalColor The final color to interpolate towards (changeAmount=1).
* @param changeAmount An amount between 0.0 - 1.0 representing a percentage
- * change from beginColor towards finalColor.
+ * change from beginColor towards finalColor.
* @return this ColorRGBA
*/
public ColorRGBA interpolateLocal(ColorRGBA beginColor, ColorRGBA finalColor, float changeAmount) {
@@ -434,11 +426,11 @@ public ColorRGBA interpolateLocal(ColorRGBA beginColor, ColorRGBA finalColor, fl
* @return a random ColorRGBA with an alpha set to 1.
*/
public static ColorRGBA randomColor() {
- ColorRGBA rVal = new ColorRGBA(0, 0, 0, 1);
- rVal.r = FastMath.nextRandomFloat();
- rVal.g = FastMath.nextRandomFloat();
- rVal.b = FastMath.nextRandomFloat();
- return rVal;
+ float r = FastMath.nextRandomFloat();
+ float g = FastMath.nextRandomFloat();
+ float b = FastMath.nextRandomFloat();
+ float a = 1.0f;
+ return new ColorRGBA(r, g, b, a);
}
/**
@@ -535,19 +527,19 @@ public ColorRGBA clone() {
/**
* Saves this ColorRGBA into the given float array.
*
- * @param floats The float array to take this ColorRGBA.
+ * @param store The float array to take this ColorRGBA.
* If null, a new float[4] is created.
* @return The array, with r,g,b,a float values in that order.
*/
- public float[] toArray(float[] floats) {
- if (floats == null) {
- floats = new float[4];
+ public float[] toArray(float[] store) {
+ if (store == null) {
+ store = new float[4];
}
- floats[0] = r;
- floats[1] = g;
- floats[2] = b;
- floats[3] = a;
- return floats;
+ store[0] = r;
+ store[1] = g;
+ store[2] = b;
+ store[3] = a;
+ return store;
}
/**
@@ -605,32 +597,32 @@ public int hashCode() {
* Serialize this color to the specified exporter, for example when
* saving to a J3O file.
*
- * @param e (not null)
+ * @param ex (not null)
* @throws IOException from the exporter
*/
@Override
- public void write(JmeExporter e) throws IOException {
- OutputCapsule capsule = e.getCapsule(this);
- capsule.write(r, "r", 0);
- capsule.write(g, "g", 0);
- capsule.write(b, "b", 0);
- capsule.write(a, "a", 0);
+ public void write(JmeExporter ex) throws IOException {
+ OutputCapsule oc = ex.getCapsule(this);
+ oc.write(r, "r", 0);
+ oc.write(g, "g", 0);
+ oc.write(b, "b", 0);
+ oc.write(a, "a", 0);
}
/**
* De-serialize this color from the specified importer, for example when
* loading from a J3O file.
*
- * @param importer (not null)
+ * @param im (not null)
* @throws IOException from the importer
*/
@Override
- public void read(JmeImporter importer) throws IOException {
- InputCapsule capsule = importer.getCapsule(this);
- r = capsule.readFloat("r", 0);
- g = capsule.readFloat("g", 0);
- b = capsule.readFloat("b", 0);
- a = capsule.readFloat("a", 0);
+ public void read(JmeImporter im) throws IOException {
+ InputCapsule ic = im.getCapsule(this);
+ r = ic.readFloat("r", 0);
+ g = ic.readFloat("g", 0);
+ b = ic.readFloat("b", 0);
+ a = ic.readFloat("a", 0);
}
/**
@@ -641,10 +633,10 @@ public void read(JmeImporter importer) throws IOException {
*/
public byte[] asBytesRGBA() {
byte[] store = new byte[4];
- store[0] = (byte) ((int) (r * 255) & 0xFF);
- store[1] = (byte) ((int) (g * 255) & 0xFF);
- store[2] = (byte) ((int) (b * 255) & 0xFF);
- store[3] = (byte) ((int) (a * 255) & 0xFF);
+ store[0] = toByte(r);
+ store[1] = toByte(g);
+ store[2] = toByte(b);
+ store[3] = toByte(a);
return store;
}
@@ -656,11 +648,7 @@ public byte[] asBytesRGBA() {
* @return The integer representation of this ColorRGBA in a,r,g,b order.
*/
public int asIntARGB() {
- int argb = (((int) (a * 255) & 0xFF) << 24)
- | (((int) (r * 255) & 0xFF) << 16)
- | (((int) (g * 255) & 0xFF) << 8)
- | (((int) (b * 255) & 0xFF));
- return argb;
+ return toInt(a, r, g, b);
}
/**
@@ -671,11 +659,7 @@ public int asIntARGB() {
* @return The integer representation of this ColorRGBA in r,g,b,a order.
*/
public int asIntRGBA() {
- int rgba = (((int) (r * 255) & 0xFF) << 24)
- | (((int) (g * 255) & 0xFF) << 16)
- | (((int) (b * 255) & 0xFF) << 8)
- | (((int) (a * 255) & 0xFF));
- return rgba;
+ return toInt(r, g, b, a);
}
/**
@@ -686,11 +670,7 @@ public int asIntRGBA() {
* @return The integer representation of this ColorRGBA in a,b,g,r order.
*/
public int asIntABGR() {
- int abgr = (((int) (a * 255) & 0xFF) << 24)
- | (((int) (b * 255) & 0xFF) << 16)
- | (((int) (g * 255) & 0xFF) << 8)
- | (((int) (r * 255) & 0xFF));
- return abgr;
+ return toInt(a, b, g, r);
}
/**
@@ -702,10 +682,10 @@ public int asIntABGR() {
* @return this
*/
public ColorRGBA fromIntARGB(int color) {
- a = ((byte) (color >> 24) & 0xFF) / 255f;
- r = ((byte) (color >> 16) & 0xFF) / 255f;
- g = ((byte) (color >> 8) & 0xFF) / 255f;
- b = ((byte) (color) & 0xFF) / 255f;
+ a = fromByte(color >> 24);
+ r = fromByte(color >> 16);
+ g = fromByte(color >> 8);
+ b = fromByte(color);
return this;
}
@@ -717,10 +697,10 @@ public ColorRGBA fromIntARGB(int color) {
* @return this
*/
public ColorRGBA fromIntRGBA(int color) {
- r = ((byte) (color >> 24) & 0xFF) / 255f;
- g = ((byte) (color >> 16) & 0xFF) / 255f;
- b = ((byte) (color >> 8) & 0xFF) / 255f;
- a = ((byte) (color) & 0xFF) / 255f;
+ r = fromByte(color >> 24);
+ g = fromByte(color >> 16);
+ b = fromByte(color >> 8);
+ a = fromByte(color);
return this;
}
@@ -732,10 +712,10 @@ public ColorRGBA fromIntRGBA(int color) {
* @return this
*/
public ColorRGBA fromIntABGR(int color) {
- a = ((byte) (color >> 24) & 0xFF) / 255f;
- b = ((byte) (color >> 16) & 0xFF) / 255f;
- g = ((byte) (color >> 8) & 0xFF) / 255f;
- r = ((byte) (color) & 0xFF) / 255f;
+ a = fromByte(color >> 24);
+ b = fromByte(color >> 16);
+ g = fromByte(color >> 8);
+ r = fromByte(color);
return this;
}
@@ -825,4 +805,29 @@ public ColorRGBA getAsSrgb() {
srgb.a = a;
return srgb;
}
+
+ /**
+ * Helper method to convert a float (0-1) to a byte (0-255).
+ */
+ private byte toByte(float channel) {
+ return (byte) ((int) (channel * 255) & 0xFF);
+ }
+
+ /**
+ * Helper method to convert an int (shifted byte) to a float (0-1).
+ */
+ private float fromByte(int channelByte) {
+ return ((byte) (channelByte) & 0xFF) / 255f;
+ }
+
+ /**
+ * Helper method to combine four float channels into an int.
+ */
+ private int toInt(float c1, float c2, float c3, float c4) {
+ int r = ((int) (c1 * 255) & 0xFF);
+ int g = ((int) (c2 * 255) & 0xFF);
+ int b = ((int) (c3 * 255) & 0xFF);
+ int a = ((int) (c4 * 255) & 0xFF);
+ return (r << 24) | (g << 16) | (b << 8) | a;
+ }
}
diff --git a/jme3-core/src/main/java/com/jme3/math/FastMath.java b/jme3-core/src/main/java/com/jme3/math/FastMath.java
index 712e322e30..d7b533fbd9 100644
--- a/jme3-core/src/main/java/com/jme3/math/FastMath.java
+++ b/jme3-core/src/main/java/com/jme3/math/FastMath.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -40,9 +40,10 @@
* @author Various
* @version $Id: FastMath.java,v 1.45 2007/08/26 08:44:20 irrisor Exp $
*/
-final public class FastMath {
- private FastMath() {
- }
+public final class FastMath {
+
+ private FastMath() {}
+
/**
* A "close to zero" double epsilon value for use
*/
@@ -489,10 +490,8 @@ public static float acos(float fValue) {
if (fValue < 1.0f) {
return (float) Math.acos(fValue);
}
-
return 0.0f;
}
-
return PI;
}
@@ -511,10 +510,8 @@ public static float asin(float fValue) {
if (fValue < 1.0f) {
return (float) Math.asin(fValue);
}
-
return HALF_PI;
}
-
return -HALF_PI;
}
@@ -844,35 +841,111 @@ public static float determinant(double m00, double m01, double m02,
}
/**
- * Returns a random float between 0 and 1.
+ * Generates a pseudorandom {@code float} in the range [0.0, 1.0).
*
- * @return a random float between 0 (inclusive) and 1 (exclusive)
+ * @return A random {@code float} value.
*/
public static float nextRandomFloat() {
return rand.nextFloat();
}
/**
- * Returns a random integer between min and max.
+ * Generates a pseudorandom {@code float} in the range [min, max)
*
- * @param min the desired minimum value
- * @param max the desired maximum value
- * @return a random int between min (inclusive) and max (inclusive)
+ * @param min The lower bound (inclusive).
+ * @param max The upper bound (exclusive).
+ * @return A random {@code float} value within the specified range.
*/
- public static int nextRandomInt(int min, int max) {
- return (int) (nextRandomFloat() * (max - min + 1)) + min;
+ public static float nextRandomFloat(float min, float max) {
+ return min + (max - min) * nextRandomFloat();
}
/**
- * Choose a pseudo-random, uniformly-distributed integer value from
- * the shared generator.
+ * Generates a pseudorandom, uniformly-distributed {@code int} value.
*
- * @return the next integer value
+ * @return The next pseudorandom {@code int} value.
*/
public static int nextRandomInt() {
return rand.nextInt();
}
+ /**
+ * Generates a pseudorandom {@code int} in the range [min, max] (inclusive).
+ *
+ * @param min The lower bound (inclusive).
+ * @param max The upper bound (inclusive).
+ * @return A random {@code int} value within the specified range.
+ */
+ public static int nextRandomInt(int min, int max) {
+ return (int) (nextRandomFloat() * (max - min + 1)) + min;
+ }
+
+ /**
+ * Returns a random point on the surface of a sphere with radius 1.0
+ *
+ * @return A new {@link Vector3f} representing a random point on the surface of the unit sphere.
+ */
+ public static Vector3f onUnitSphere() {
+
+ float u = nextRandomFloat();
+ float v = nextRandomFloat();
+
+ // azimuthal angle: The angle between x-axis in radians [0, 2PI]
+ float theta = FastMath.TWO_PI * u;
+ // polar angle: The angle between z-axis in radians [0, PI]
+ float phi = (float) Math.acos(2f * v - 1f);
+
+ float cosPolar = FastMath.cos(phi);
+ float sinPolar = FastMath.sin(phi);
+ float cosAzim = FastMath.cos(theta);
+ float sinAzim = FastMath.sin(theta);
+
+ return new Vector3f(cosAzim * sinPolar, sinAzim * sinPolar, cosPolar);
+ }
+
+ /**
+ * Returns a random point inside or on a sphere with radius 1.0
+ * This method uses spherical coordinates combined with a cubed-root radius.
+ *
+ * @return A new {@link Vector3f} representing a random point within the unit sphere.
+ */
+ public static Vector3f insideUnitSphere() {
+ float u = nextRandomFloat();
+ // Azimuthal angle [0, 2PI]
+ float theta = FastMath.TWO_PI * nextRandomFloat();
+ // Polar angle [0, PI] for uniform surface distribution
+ float phi = FastMath.acos(2f * nextRandomFloat() - 1f);
+
+ // For uniform distribution within the volume, radius R should be such that R^3 is uniformly distributed.
+ // So, R = cbrt(random_uniform_0_to_1)
+ float radius = (float) Math.cbrt(u);
+
+ float sinPhi = FastMath.sin(phi);
+ float x = radius * sinPhi * FastMath.cos(theta);
+ float y = radius * sinPhi * FastMath.sin(theta);
+ float z = radius * FastMath.cos(phi);
+
+ return new Vector3f(x, y, z);
+ }
+
+ /**
+ * Returns a random point inside or on a circle with radius 1.0.
+ * This method uses polar coordinates combined with a square-root radius.
+ *
+ * @return A new {@link Vector2f} representing a random point within the unit circle.
+ */
+ public static Vector2f insideUnitCircle() {
+ // Angle [0, 2PI]
+ float angle = FastMath.TWO_PI * nextRandomFloat();
+ // For uniform distribution, R^2 is uniform
+ float radius = FastMath.sqrt(nextRandomFloat());
+
+ float x = radius * FastMath.cos(angle);
+ float y = radius * FastMath.sin(angle);
+
+ return new Vector2f(x, y);
+ }
+
/**
* Converts a point from Spherical coordinates to Cartesian (using positive
* Y as up) and stores the results in the store var.
@@ -883,8 +956,7 @@ public static int nextRandomInt() {
* @param store storage for the result (modified if not null)
* @return the Cartesian coordinates (either store or a new vector)
*/
- public static Vector3f sphericalToCartesian(Vector3f sphereCoords,
- Vector3f store) {
+ public static Vector3f sphericalToCartesian(Vector3f sphereCoords, Vector3f store) {
if (store == null) {
store = new Vector3f();
}
@@ -906,8 +978,7 @@ public static Vector3f sphericalToCartesian(Vector3f sphereCoords,
* @return the Cartesian coordinates: x=distance from origin, y=longitude in
* radians, z=latitude in radians (either store or a new vector)
*/
- public static Vector3f cartesianToSpherical(Vector3f cartCoords,
- Vector3f store) {
+ public static Vector3f cartesianToSpherical(Vector3f cartCoords, Vector3f store) {
if (store == null) {
store = new Vector3f();
}
@@ -936,8 +1007,7 @@ public static Vector3f cartesianToSpherical(Vector3f cartCoords,
* @param store storage for the result (modified if not null)
* @return the Cartesian coordinates (either store or a new vector)
*/
- public static Vector3f sphericalToCartesianZ(Vector3f sphereCoords,
- Vector3f store) {
+ public static Vector3f sphericalToCartesianZ(Vector3f sphereCoords, Vector3f store) {
if (store == null) {
store = new Vector3f();
}
@@ -959,8 +1029,7 @@ public static Vector3f sphericalToCartesianZ(Vector3f sphereCoords,
* @return the Cartesian coordinates: x=distance from origin, y=latitude in
* radians, z=longitude in radians (either store or a new vector)
*/
- public static Vector3f cartesianZToSpherical(Vector3f cartCoords,
- Vector3f store) {
+ public static Vector3f cartesianZToSpherical(Vector3f cartCoords, Vector3f store) {
if (store == null) {
store = new Vector3f();
}
@@ -982,12 +1051,9 @@ public static Vector3f cartesianZToSpherical(Vector3f cartCoords,
/**
* Takes a value and expresses it in terms of min to max.
*
- * @param val -
- * the angle to normalize (in radians)
- * @param min
- * the lower limit of the range
- * @param max
- * the upper limit of the range
+ * @param val the angle to normalize (in radians)
+ * @param min the lower limit of the range
+ * @param max the upper limit of the range
* @return the normalized angle (also in radians)
*/
public static float normalize(float val, float min, float max) {
@@ -1135,4 +1201,12 @@ public static short convertFloatToHalf(float flt) {
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;
+ }
+
}
diff --git a/jme3-core/src/main/java/com/jme3/math/Line.java b/jme3-core/src/main/java/com/jme3/math/Line.java
index f215dba4d6..122b05225e 100644
--- a/jme3-core/src/main/java/com/jme3/math/Line.java
+++ b/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 Line clone() {
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:
+ *
+ *
+ * @return the string representation (not null, not empty)
+ */
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + " [Origin: " + origin
+ + " Direction: " + direction + "]";
+ }
}
diff --git a/jme3-core/src/main/java/com/jme3/math/LineSegment.java b/jme3-core/src/main/java/com/jme3/math/LineSegment.java
index a1436cd518..a840f1cd09 100644
--- a/jme3-core/src/main/java/com/jme3/math/LineSegment.java
+++ b/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 LineSegment clone() {
}
}
+ /**
+ * 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:
+ *
Evaluates whether a given point is contained within the axis aligned bounding box
* that contains this LineSegment.
This function is float error aware.
diff --git a/jme3-core/src/main/java/com/jme3/math/Matrix4f.java b/jme3-core/src/main/java/com/jme3/math/Matrix4f.java
index 6c6e7449e2..4f9a6e2f35 100644
--- a/jme3-core/src/main/java/com/jme3/math/Matrix4f.java
+++ b/jme3-core/src/main/java/com/jme3/math/Matrix4f.java
@@ -1802,6 +1802,9 @@ public Vector3f toTranslationVector(Vector3f vector) {
/**
* Determine the rotation component of this 3-D coordinate transform.
*
+ *
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 Quaternion toRotationQuat() {
/**
* Returns the rotation component of the coordinate transform.
*
+ *
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 Quaternion toRotationQuat(Quaternion q) {
/**
* Determine the rotation component of this 3-D coordinate transform.
*
- * @return a new rotation Matrix3f
+ *
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 Matrix3f toRotationMatrix() {
/**
* Determines the rotation component of the coordinate transform.
*
+ *
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 void toRotationMatrix(Matrix3f mat) {
/**
* Determine the scale component of this 3-D coordinate transform.
*
+ *
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 Vector3f toScaleVector() {
/**
* Determines the scale component of the coordinate transform.
*
+ *
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
*/
diff --git a/jme3-core/src/main/java/com/jme3/math/Quaternion.java b/jme3-core/src/main/java/com/jme3/math/Quaternion.java
index 0974a38b7e..3da3feb42f 100644
--- a/jme3-core/src/main/java/com/jme3/math/Quaternion.java
+++ b/jme3-core/src/main/java/com/jme3/math/Quaternion.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2022 jMonkeyEngine
+ * Copyright (c) 2009-2023 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -356,8 +356,10 @@ public float[] toAngles(float[] angles) {
}
/**
- * 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.
+ *
+ *
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 Quaternion fromRotationMatrix(Matrix3f matrix) {
/**
* Sets the quaternion from a rotation matrix with the specified elements.
- * Does not verify that the arguments form a valid rotation matrix.
+ *
+ *
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 Quaternion fromRotationMatrix(Matrix3f matrix) {
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 Matrix4f toTransformMatrix(Matrix4f store) {
* current instance is unaffected.
*
*
Note: preserves the translation and scaling components of
- * {@code result}.
+ * {@code result} unless {@code result} includes reflection.
*
*
Note: the result is created from a normalized version of the current
* instance.
@@ -651,7 +655,7 @@ public Vector3f getRotationColumn(int i, Vector3f store) {
float norm = norm();
if (norm != 1.0f) {
- norm = FastMath.invSqrt(norm);
+ norm = 1.0f / norm;
}
float xx = x * x * norm;
@@ -1013,6 +1017,9 @@ public Quaternion mult(Quaternion q, Quaternion storeResult) {
/**
* Applies the rotation represented by the argument to the current instance.
*
+ *
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) {
@@ -1095,6 +1102,10 @@ public void toAxes(Vector3f axes[]) {
* Rotates the argument vector and returns the result as a new vector. The
* current instance is unaffected.
*
+ *
The quaternion is assumed to be normalized (norm=1). No error checking
+ * is performed; the caller must ensure that the norm is approximately equal
+ * to 1.
+ *
*
Despite the name, the result differs from the mathematical definition
* of vector-quaternion multiplication.
*
@@ -1109,6 +1120,10 @@ public Vector3f mult(Vector3f v) {
* Rotates the argument vector. Despite the name, the current instance is
* unaffected.
*
+ *
The quaternion is assumed to be normalized (norm=1). No error checking
+ * is performed; the caller must ensure that the norm is approximately equal
+ * to 1.
+ *
*
Despite the name, the result differs from the mathematical definition
* of vector-quaternion multiplication.
*
@@ -1177,6 +1192,10 @@ public Quaternion multLocal(float qx, float qy, float qz, float qw) {
* Rotates a specified vector and returns the result in another vector. The
* current instance is unaffected.
*
+ *
The quaternion is assumed to be normalized (norm=1). No error checking
+ * is performed; the caller must ensure that the norm is approximately equal
+ * to 1.
+ *
*
It is safe for {@code v} and {@code store} to be the same object.
*
*
Despite the name, the result differs from the mathematical definition
@@ -1589,4 +1608,27 @@ public Quaternion clone() {
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);
+ }
}
diff --git a/jme3-core/src/main/java/com/jme3/math/Rectangle.java b/jme3-core/src/main/java/com/jme3/math/Rectangle.java
index 570e16f896..67cfcb15d7 100644
--- a/jme3-core/src/main/java/com/jme3/math/Rectangle.java
+++ b/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 Rectangle clone() {
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:
+ *
+ *
+ * @return the string representation (not null, not empty)
+ */
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + " [A: " + a + " B: " + b + " C: " + c + "]";
+ }
}
diff --git a/jme3-core/src/main/java/com/jme3/math/Ring.java b/jme3-core/src/main/java/com/jme3/math/Ring.java
index c6d426d8a7..c1a17f3cf4 100644
--- a/jme3-core/src/main/java/com/jme3/math/Ring.java
+++ b/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 Ring lying on the XZ plane,
diff --git a/jme3-core/src/main/java/com/jme3/math/Spline.java b/jme3-core/src/main/java/com/jme3/math/Spline.java
index d707d10a3f..cdd7f48c77 100644
--- a/jme3-core/src/main/java/com/jme3/math/Spline.java
+++ b/jme3-core/src/main/java/com/jme3/math/Spline.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -32,6 +32,8 @@
package com.jme3.math;
import com.jme3.export.*;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
@@ -41,7 +43,7 @@
*
* @author Nehon
*/
-public class Spline implements Savable {
+public class Spline implements JmeCloneable, Savable {
public enum SplineType {
Linear,
@@ -480,7 +482,18 @@ public void write(JmeExporter ex) throws IOException {
oc.writeSavableArrayList((ArrayList) CRcontrolPoints, "CRControlPoints", null);
oc.write(curveTension, "curveTension", 0.5f);
oc.write(cycle, "cycle", false);
- oc.writeSavableArrayList((ArrayList) knots, "knots", null);
+
+ float[] knotArray;
+ if (knots == null) {
+ knotArray = null;
+ } else {
+ knotArray = new float[knots.size()];
+ for (int i = 0; i < knotArray.length; ++i) {
+ knotArray[i] = knots.get(i);
+ }
+ }
+ oc.write(knotArray, "knots", null);
+
oc.write(weights, "weights", null);
oc.write(basisFunctionDegree, "basisFunctionDegree", 0);
}
@@ -506,13 +519,60 @@ public void read(JmeImporter im) throws IOException {
segmentsLength.add(list[i]);
}
}
- type = in.readEnum("pathSplineType", SplineType.class, SplineType.CatmullRom);
+ type = in.readEnum("type", SplineType.class, SplineType.CatmullRom);
totalLength = in.readFloat("totalLength", 0);
CRcontrolPoints = in.readSavableArrayList("CRControlPoints", null);
curveTension = in.readFloat("curveTension", 0.5f);
cycle = in.readBoolean("cycle", false);
- knots = in.readSavableArrayList("knots", null);
+
+ float[] knotArray = in.readFloatArray("knots", null);
+ if (knotArray == null) {
+ this.knots = null;
+ } else {
+ this.knots = new ArrayList<>(knotArray.length);
+ for (float knot : knotArray) {
+ knots.add(knot);
+ }
+ }
+
weights = in.readFloatArray("weights", null);
basisFunctionDegree = in.readInt("basisFunctionDegree", 0);
}
+
+ /**
+ * Callback from {@link com.jme3.util.clone.Cloner} to convert this
+ * shallow-cloned spline into a deep-cloned one, using the specified cloner
+ * and original to resolve copied fields.
+ *
+ * @param cloner the cloner that's cloning this spline (not null)
+ * @param original the object from which this spline was shallow-cloned (not
+ * null, unaffected)
+ */
+ @Override
+ public void cloneFields(Cloner cloner, Object original) {
+ this.controlPoints = cloner.clone(controlPoints);
+ if (segmentsLength != null) {
+ this.segmentsLength = new ArrayList<>(segmentsLength);
+ }
+ this.CRcontrolPoints = cloner.clone(CRcontrolPoints);
+ if (knots != null) {
+ this.knots = new ArrayList<>(knots);
+ }
+ this.weights = cloner.clone(weights);
+ }
+
+ /**
+ * Creates a shallow clone for the JME cloner.
+ *
+ * @return a new object
+ */
+ @Override
+ public Spline jmeClone() {
+ try {
+ Spline clone = (Spline) clone();
+ return clone;
+ } catch (CloneNotSupportedException exception) {
+ throw new RuntimeException(exception);
+ }
+ }
}
diff --git a/jme3-core/src/main/java/com/jme3/math/Transform.java b/jme3-core/src/main/java/com/jme3/math/Transform.java
index c172d26d4a..de5bfe8ce8 100644
--- a/jme3-core/src/main/java/com/jme3/math/Transform.java
+++ b/jme3-core/src/main/java/com/jme3/math/Transform.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2009-2022 jMonkeyEngine
+ * Copyright (c) 2009-2023 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -121,6 +121,7 @@ public Transform() {
* @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 Transform setRotation(Quaternion rot) {
* @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 Vector3f getTranslation() {
* @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 Transform setScale(Vector3f scale) {
* @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;
}
@@ -257,8 +261,8 @@ public void interpolateTransforms(Transform t1, Transform t2, float delta) {
* Combines with the argument and returns the (modified) current instance.
* This method is used to combine Node and Spatial transforms.
*
- * @param parent the parent transform (not null, unaffected unless it's
- * this)
+ * @param parent the parent transform (not null, {@code parent.rot.norm()}
+ * approximately equal to 1, unaffected unless it's this)
* @return the (modified) current instance (for chaining)
*/
public Transform combineWithParent(Transform parent) {
@@ -286,6 +290,7 @@ public Transform combineWithParent(Transform parent) {
* @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 Transform setTranslation(float x, float y, float z) {
* @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;
}
@@ -308,6 +314,10 @@ public Transform setScale(float x, float y, float z) {
* store. If the store is null, a new Vector3f is
* created to hold the value. Either way, the current instance is
* unaffected, unless store is its translation or scaling.
+ *
+ * The transform's quaternion is assumed to be normalized. No error checking
+ * is performed; the caller should ensure that {@code rot.norm()} is
+ * approximately equal to 1.
*
* @param in the coordinates to transform (not null, unaffected)
* @param store storage for the result (modified if not null)
@@ -330,6 +340,10 @@ public Vector3f transformVector(final Vector3f in, Vector3f store) {
* new Vector3f is created to hold the value. Either way, the current
* instance is unaffected, unless store is its translation or
* scaling.
+ *
+ * The transform's quaternion is assumed to be normalized. No error checking
+ * is performed; the caller should ensure that {@code rot.norm()} is
+ * approximately equal to 1.
*
* @param in the coordinates to transform (not null, unaffected unless it's
* store)
@@ -381,10 +395,13 @@ public Matrix4f toTransformMatrix(Matrix4f store) {
}
/**
- * 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.
*
+ *
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) {
@@ -398,6 +415,10 @@ public void fromTransformMatrix(Matrix4f mat) {
/**
* Returns the inverse. The current instance is unaffected.
*
+ *
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() {
diff --git a/jme3-core/src/main/java/com/jme3/math/Vector2f.java b/jme3-core/src/main/java/com/jme3/math/Vector2f.java
index 1bec9c786b..e32871d75b 100644
--- a/jme3-core/src/main/java/com/jme3/math/Vector2f.java
+++ b/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.
*/
diff --git a/jme3-core/src/main/java/com/jme3/math/Vector3f.java b/jme3-core/src/main/java/com/jme3/math/Vector3f.java
index 89efe8a6fd..fa5a8539cd 100644
--- a/jme3-core/src/main/java/com/jme3/math/Vector3f.java
+++ b/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);
diff --git a/jme3-core/src/main/java/com/jme3/math/Vector4f.java b/jme3-core/src/main/java/com/jme3/math/Vector4f.java
index 826cf29030..cf18c4be50 100644
--- a/jme3-core/src/main/java/com/jme3/math/Vector4f.java
+++ b/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,
diff --git a/jme3-core/src/main/java/com/jme3/opencl/OpenCLObjectManager.java b/jme3-core/src/main/java/com/jme3/opencl/OpenCLObjectManager.java
index 42748855cd..bb5d95b6dd 100644
--- a/jme3-core/src/main/java/com/jme3/opencl/OpenCLObjectManager.java
+++ b/jme3-core/src/main/java/com/jme3/opencl/OpenCLObjectManager.java
@@ -53,8 +53,8 @@ public static OpenCLObjectManager getInstance() {
return INSTANCE;
}
- final private ReferenceQueue