diff --git a/.github/workflows/screenshot-test-comment.yml b/.github/workflows/screenshot-test-comment.yml new file mode 100644 index 0000000000..0d9642f07e --- /dev/null +++ b/.github/workflows/screenshot-test-comment.yml @@ -0,0 +1,117 @@ +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 15 + + - 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 + edit-mode: replace + comment-id: ${{ steps.existingCommentId.outputs.comment-id }} diff --git a/gradle.properties b/gradle.properties index b10e8722cb..42b02b8708 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Version number: Major.Minor.SubMinor (e.g. 3.3.0) -jmeVersion = 3.8.0 +jmeVersion = 3.9.0 # Leave empty to autogenerate # (use -PjmeVersionName="myVersion" from commandline to specify a custom version name ) 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/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 d6d39b8a76..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"); @@ -254,27 +310,71 @@ public void setRoomRolloffFactor(float roomRolloffFactor) { } @Override - public boolean equals(Object env2) { - if (env2 == null) + public boolean equals(Object obj) { + + if (!(obj instanceof Environment)) return false; - if (env2 == this) + + if (obj == this) return true; - if (!(env2 instanceof Environment)) - return false; - Environment e2 = (Environment) env2; - return (e2.airAbsorbGainHf == airAbsorbGainHf - && e2.decayHFRatio == decayHFRatio - && e2.decayHfLimit == decayHfLimit - && e2.decayTime == decayTime - && e2.density == density - && e2.diffusion == diffusion - && e2.gain == gain - && e2.gainHf == gainHf - && e2.lateReverbDelay == lateReverbDelay - && e2.lateReverbGain == lateReverbGain - && e2.reflectDelay == reflectDelay - && e2.reflectGain == reflectGain - && e2.roomRolloffFactor == roomRolloffFactor); - } + 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 daa49adb65..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-2023 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,23 +136,33 @@ 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) | (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 aa3c77bbf2..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()); @@ -56,78 +73,96 @@ public class ALAudioRenderer implements AudioRenderer, Runnable { private static final int BUFFER_SIZE = 35280; private static final int STREAMING_BUFFER_COUNT = 5; private static final int MAX_NUM_CHANNELS = 64; - private IntBuffer ib = BufferUtils.createIntBuffer(1); - private final FloatBuffer fb = BufferUtils.createVector3Buffer(2); - private final ByteBuffer nativeBuf = BufferUtils.createByteBuffer(BUFFER_SIZE); - private final byte[] arrayBuf = new byte[BUFFER_SIZE]; - private int[] channels; - private AudioSource[] channelSources; - private int nextChan = 0; - private final ArrayList freeChannels = new ArrayList<>(); + + // 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/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/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..fa30dff6ba 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 @@ -312,6 +312,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); } 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..2c0b4f8555 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 @@ -142,17 +142,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/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..628af9816b 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,50 @@ 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); } + /** + * 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 index d07a039995..6f07fd1c1c 100644 --- a/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java +++ b/jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java @@ -287,8 +287,9 @@ public void setAssetManager(AssetManager assetManager) { } void rebakeNow(RenderManager renderManager) { - IBLHybridEnvBakerLight baker = new IBLGLEnvBakerLight(renderManager, assetManager, Format.RGB16F, Format.Depth, - envMapSize, envMapSize); + IBLHybridEnvBakerLight baker = new IBLGLEnvBakerLight(renderManager, assetManager, Format.RGB16F, + Format.Depth, + envMapSize, envMapSize); baker.setTexturePulling(isRequiredSavableResults()); baker.bakeEnvironment(spatial, getPosition(), frustumNear, frustumFar, filter); diff --git a/jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java b/jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java index d78edc561e..12ba5e99c1 100644 --- a/jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java +++ b/jme3-core/src/main/java/com/jme3/environment/FastLightProbeFactory.java @@ -33,6 +33,7 @@ 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; @@ -74,7 +75,8 @@ public class FastLightProbeFactory { * @return The baked LightProbe */ public static LightProbe makeProbe(RenderManager rm, AssetManager am, int size, Vector3f pos, float frustumNear, float frustumFar, Spatial scene) { - IBLGLEnvBakerLight baker = new IBLGLEnvBakerLight(rm, am, Format.RGB16F, Format.Depth, size, size); + IBLHybridEnvBakerLight baker = new IBLGLEnvBakerLight(rm, am, Format.RGB16F, Format.Depth, size, + size); baker.setTexturePulling(true); baker.bakeEnvironment(scene, pos, frustumNear, frustumFar, null); 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 index 8daa62ef40..f6284f14ea 100644 --- a/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java +++ b/jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java @@ -34,6 +34,7 @@ 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; @@ -130,11 +131,12 @@ public void bakeSphericalHarmonicsCoefficients() { int s = renderOnT; renderOnT = renderOnT == 0 ? 1 : 0; mat.setTexture("ShCoef", shCoefTx[s]); - mat.setInt("FaceId", faceId); } else { renderOnT = 0; } + mat.setInt("FaceId", faceId); + screen.updateLogicalState(0); screen.updateGeometricState(); @@ -169,7 +171,7 @@ else if (weightAccum != c.a) { 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 index d72e0dc30f..26b3c1cd65 100644 --- a/jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java +++ b/jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java @@ -200,6 +200,7 @@ public TextureCubeMap getSpecularIBL() { @Override public void bakeSphericalHarmonicsCoefficients() { shCoef = EnvMapUtils.getSphericalHarmonicsCoefficents(getEnvMap()); + EnvMapUtils.prepareShCoefs(shCoef); } @Override 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 2993c7eed0..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-2024 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) { @@ -1143,5 +1209,4 @@ 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/Spline.java b/jme3-core/src/main/java/com/jme3/math/Spline.java index 5ca85b9081..cdd7f48c77 100644 --- a/jme3-core/src/main/java/com/jme3/math/Spline.java +++ b/jme3-core/src/main/java/com/jme3/math/Spline.java @@ -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, @@ -536,4 +538,41 @@ public void read(JmeImporter im) throws IOException { 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/renderer/Camera.java b/jme3-core/src/main/java/com/jme3/renderer/Camera.java index eb7cca3ae4..b05c3bd86a 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/Camera.java +++ b/jme3-core/src/main/java/com/jme3/renderer/Camera.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 @@ -179,22 +179,22 @@ public enum FrustumIntersect { //view port coordinates /** * Percent value on display where horizontal viewing starts for this camera. - * Default is 0. + * Default is 0. Must be less than {@code viewPortRight}. */ protected float viewPortLeft; /** * Percent value on display where horizontal viewing ends for this camera. - * Default is 1. + * Default is 1. Must be greater than {@code viewPortLeft}. */ protected float viewPortRight; /** * Percent value on display where vertical viewing ends for this camera. - * Default is 1. + * Default is 1. Must be greater than {@code viewPortBottom}. */ protected float viewPortTop; /** * Percent value on display where vertical viewing begins for this camera. - * Default is 0. + * Default is 0. Must be less than {@code viewPortTop}. */ protected float viewPortBottom; /** @@ -1017,7 +1017,8 @@ public float getViewPortLeft() { /** * Sets the left boundary of the viewport. * - * @param left the left boundary of the viewport + * @param left the left boundary of the viewport (<viewPortRight, + * default: 0) */ public void setViewPortLeft(float left) { viewPortLeft = left; @@ -1036,7 +1037,8 @@ public float getViewPortRight() { /** * Sets the right boundary of the viewport. * - * @param right the right boundary of the viewport + * @param right the right boundary of the viewport (>viewPortLeft, + * default: 1) */ public void setViewPortRight(float right) { viewPortRight = right; @@ -1055,7 +1057,8 @@ public float getViewPortTop() { /** * Sets the top boundary of the viewport. * - * @param top the top boundary of the viewport + * @param top the top boundary of the viewport (>viewPortBottom, + * default: 1) */ public void setViewPortTop(float top) { viewPortTop = top; @@ -1074,7 +1077,8 @@ public float getViewPortBottom() { /** * Sets the bottom boundary of the viewport. * - * @param bottom the bottom boundary of the viewport + * @param bottom the bottom boundary of the viewport (<viewPortTop, + * default: 0) */ public void setViewPortBottom(float bottom) { viewPortBottom = bottom; @@ -1084,10 +1088,10 @@ public void setViewPortBottom(float bottom) { /** * Sets the boundaries of the viewport. * - * @param left the left boundary of the viewport (default: 0) - * @param right the right boundary of the viewport (default: 1) - * @param bottom the bottom boundary of the viewport (default: 0) - * @param top the top boundary of the viewport (default: 1) + * @param left the left boundary of the viewport (<right, default: 0) + * @param right the right boundary of the viewport (>left, default: 1) + * @param bottom the bottom boundary of the viewport (<top, default: 0) + * @param top the top boundary of the viewport (>bottom, default: 1) */ public void setViewPort(float left, float right, float bottom, float top) { this.viewPortLeft = left; @@ -1283,6 +1287,15 @@ public void clearViewportChanged() { * Called when the viewport has been changed. */ public void onViewPortChange() { + if (!(viewPortBottom < viewPortTop)) { + throw new IllegalArgumentException( + "Viewport must have bottom < top"); + } + if (!(viewPortLeft < viewPortRight)) { + throw new IllegalArgumentException( + "Viewport must have left < right"); + } + viewportChanged = true; setGuiBounding(); } diff --git a/jme3-core/src/main/java/com/jme3/scene/BatchNode.java b/jme3-core/src/main/java/com/jme3/scene/BatchNode.java index 883090c7aa..aa5d62a02a 100644 --- a/jme3-core/src/main/java/com/jme3/scene/BatchNode.java +++ b/jme3-core/src/main/java/com/jme3/scene/BatchNode.java @@ -31,15 +31,6 @@ */ package com.jme3.scene; -import java.nio.Buffer; -import java.nio.FloatBuffer; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; - import com.jme3.collision.Collidable; import com.jme3.collision.CollisionResults; import com.jme3.material.Material; @@ -50,6 +41,14 @@ import com.jme3.util.TempVars; import com.jme3.util.clone.Cloner; import com.jme3.util.clone.JmeCloneable; +import java.nio.Buffer; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; /** * BatchNode holds geometries that are a batched version of all the geometries that are in its sub scenegraph. @@ -188,6 +187,11 @@ protected void doBatch() { Map> matMap = new HashMap<>(); int nbGeoms = 0; + // Recalculate the maxVertCount during gatherGeometries() so it's always + // accurate. Keep track of what it used to be so we know if temp arrays need + // to be reallocated. + int oldMaxVertCount = maxVertCount; + maxVertCount = 0; gatherGeometries(matMap, this, needsFullRebatch); if (needsFullRebatch) { for (Batch batch : batches.getArray()) { @@ -196,10 +200,6 @@ protected void doBatch() { batches.clear(); batchesByGeom.clear(); } - //only reset maxVertCount if there is something new to batch - if (matMap.size() > 0) { - maxVertCount = 0; - } for (Map.Entry> entry : matMap.entrySet()) { Mesh m = new Mesh(); @@ -244,8 +244,8 @@ protected void doBatch() { logger.log(Level.FINE, "Batched {0} geometries in {1} batches.", new Object[]{nbGeoms, batches.size()}); } - //init the temp arrays if something has been batched only. - if (matMap.size() > 0) { + //init the temp arrays if the size has changed + if (oldMaxVertCount != maxVertCount) { initTempFloatArrays(); } } @@ -285,6 +285,13 @@ private void gatherGeometries(Map> map, Spatial n, bool if (!isBatch(n) && n.getBatchHint() != BatchHint.Never) { Geometry g = (Geometry) n; + + // Need to recalculate the max vert count whether we are rebatching this + // particular geometry or not. + if (maxVertCount < g.getVertexCount()) { + maxVertCount = g.getVertexCount(); + } + if (!g.isGrouped() || rebatch) { if (g.getMaterial() == null) { throw new IllegalStateException("No material is set for Geometry: " + g.getName() + " please set a material before batching"); @@ -385,11 +392,8 @@ private void mergeGeometries(Mesh outMesh, List geometries) { totalVerts += geom.getVertexCount(); totalTris += geom.getTriangleCount(); totalLodLevels = Math.min(totalLodLevels, geom.getMesh().getNumLodLevels()); - if (maxVertCount < geom.getVertexCount()) { - maxVertCount = geom.getVertexCount(); - } + Mesh.Mode listMode; - //float listLineWidth = 1f; int components; switch (geom.getMesh().getMode()) { case Points: @@ -530,7 +534,6 @@ private void doTransforms(FloatBuffer bindBufPos, FloatBuffer bindBufNorm, Float Vector3f norm = vars.vect2; Vector3f tan = vars.vect3; - validateTempFloatArrays(end - start); int length = (end - start) * 3; int tanLength = (end - start) * 4; @@ -611,13 +614,6 @@ private void doTransforms(FloatBuffer bindBufPos, FloatBuffer bindBufNorm, Float } } - private void validateTempFloatArrays(int vertCount) { - if (maxVertCount < vertCount) { - maxVertCount = vertCount; - initTempFloatArrays(); - } - } - private void initTempFloatArrays() { //TODO these arrays should be allocated by chunk instead to avoid recreating them each time the batch is changed. tmpFloat = new float[maxVertCount * 3]; diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/WireSphere.java b/jme3-core/src/main/java/com/jme3/scene/debug/WireSphere.java index 3fa4f48637..72ee624bb0 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/WireSphere.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/WireSphere.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 @@ -33,13 +33,15 @@ import com.jme3.bounding.BoundingSphere; import com.jme3.math.FastMath; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; import com.jme3.scene.Mesh; -import com.jme3.scene.Mesh.Mode; import com.jme3.scene.VertexBuffer; import com.jme3.scene.VertexBuffer.Format; import com.jme3.scene.VertexBuffer.Type; import com.jme3.scene.VertexBuffer.Usage; import com.jme3.util.BufferUtils; + import java.nio.FloatBuffer; import java.nio.ShortBuffer; @@ -151,11 +153,25 @@ public void updatePositions(float radius) { /** * Create a WireSphere from a BoundingSphere * - * @param bsph - * BoundingSphere used to create the WireSphere - * + * @param bsph BoundingSphere used to create the WireSphere */ public void fromBoundingSphere(BoundingSphere bsph) { updatePositions(bsph.getRadius()); } + + /** + * Create a geometry suitable for visualizing the specified bounding sphere. + * + * @param bsph the bounding sphere (not null) + * @return a new Geometry instance in world space + */ + public static Geometry makeGeometry(BoundingSphere bsph) { + WireSphere mesh = new WireSphere(bsph.getRadius()); + Geometry result = new Geometry("bounding sphere", mesh); + + Vector3f center = bsph.getCenter(); + result.setLocalTranslation(center); + + return result; + } } diff --git a/jme3-core/src/main/java/com/jme3/shadow/AbstractShadowFilter.java b/jme3-core/src/main/java/com/jme3/shadow/AbstractShadowFilter.java index f62fc1e5f2..b8e8d4a22b 100644 --- a/jme3-core/src/main/java/com/jme3/shadow/AbstractShadowFilter.java +++ b/jme3-core/src/main/java/com/jme3/shadow/AbstractShadowFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009-2024 jMonkeyEngine + * Copyright (c) 2009-2025 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -51,7 +51,6 @@ import java.io.IOException; /** - * * Generic abstract filter that holds common implementations for the different * shadow filters * @@ -63,9 +62,9 @@ public abstract class AbstractShadowFilter ext protected ViewPort viewPort; /** - * used for serialization + * For serialization only. Do not use. */ - protected AbstractShadowFilter(){ + protected AbstractShadowFilter() { } /** @@ -79,11 +78,8 @@ protected AbstractShadowFilter(){ @SuppressWarnings("all") protected AbstractShadowFilter(AssetManager manager, int shadowMapSize, T shadowRenderer) { super("Post Shadow"); - material = new Material(manager, "Common/MatDefs/Shadow/PostShadowFilter.j3md"); this.shadowRenderer = shadowRenderer; - this.shadowRenderer.setPostShadowMaterial(material); - - //this is legacy setting for shadows with backface shadows + // this is legacy setting for shadows with backface shadows this.shadowRenderer.setRenderBackFacesShadows(true); } @@ -100,6 +96,7 @@ protected boolean isRequiresDepthTexture() { public Material getShadowMaterial() { return material; } + Vector4f tmpv = new Vector4f(); @Override @@ -113,15 +110,15 @@ protected void preFrame(float tpf) { @Override protected void postQueue(RenderQueue queue) { shadowRenderer.postQueue(queue); - if(shadowRenderer.skipPostPass){ - //removing the shadow map so that the post pass is skipped - material.setTexture("ShadowMap0", null); - } + if (shadowRenderer.skipPostPass) { + // removing the shadow map so that the post pass is skipped + material.setTexture("ShadowMap0", null); + } } @Override protected void postFrame(RenderManager renderManager, ViewPort viewPort, FrameBuffer prevFilterBuffer, FrameBuffer sceneBuffer) { - if(!shadowRenderer.skipPostPass){ + if (!shadowRenderer.skipPostPass) { shadowRenderer.setPostShadowParams(); } } @@ -129,15 +126,17 @@ protected void postFrame(RenderManager renderManager, ViewPort viewPort, FrameBu @Override protected void initFilter(AssetManager manager, RenderManager renderManager, ViewPort vp, int w, int h) { shadowRenderer.needsfallBackMaterial = true; + material = new Material(manager, "Common/MatDefs/Shadow/PostShadowFilter.j3md"); + shadowRenderer.setPostShadowMaterial(material); shadowRenderer.initialize(renderManager, vp); this.viewPort = vp; } - - /** + + /** * How far the shadows are rendered in the view * - * @see #setShadowZExtend(float zFar) * @return shadowZExtend + * @see #setShadowZExtend(float zFar) */ public float getShadowZExtend() { return shadowRenderer.getShadowZExtend(); diff --git a/jme3-core/src/main/java/com/jme3/texture/TextureArray.java b/jme3-core/src/main/java/com/jme3/texture/TextureArray.java index 026489cb2a..e839399c33 100644 --- a/jme3-core/src/main/java/com/jme3/texture/TextureArray.java +++ b/jme3-core/src/main/java/com/jme3/texture/TextureArray.java @@ -31,8 +31,13 @@ */ package com.jme3.texture; +import com.jme3.export.InputCapsule; +import com.jme3.export.JmeExporter; +import com.jme3.export.JmeImporter; +import com.jme3.export.OutputCapsule; import com.jme3.texture.Image.Format; import com.jme3.texture.image.ColorSpace; +import java.io.IOException; import java.util.Arrays; import java.util.List; @@ -151,4 +156,44 @@ public void setWrap(WrapMode mode) { this.wrapS = mode; this.wrapT = mode; } -} \ No newline at end of file + + @Override + public void write(JmeExporter e) throws IOException { + super.write(e); + OutputCapsule capsule = e.getCapsule(this); + capsule.write(wrapS, "wrapS", WrapMode.EdgeClamp); + capsule.write(wrapT, "wrapT", WrapMode.EdgeClamp); + } + + @Override + public void read(JmeImporter importer) throws IOException { + super.read(importer); + InputCapsule capsule = importer.getCapsule(this); + wrapS = capsule.readEnum("wrapS", WrapMode.class, WrapMode.EdgeClamp); + wrapT = capsule.readEnum("wrapT", WrapMode.class, WrapMode.EdgeClamp); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof TextureArray)) { + return false; + } + TextureArray that = (TextureArray) other; + if (this.getWrap(WrapAxis.S) != that.getWrap(WrapAxis.S)) + return false; + if (this.getWrap(WrapAxis.T) != that.getWrap(WrapAxis.T)) + return false; + return super.equals(other); + } + + @Override + public int hashCode() { + int hash = super.hashCode(); + hash = 79 * hash + (this.wrapS != null ? this.wrapS.hashCode() : 0); + hash = 79 * hash + (this.wrapT != null ? this.wrapT.hashCode() : 0); + return hash; + } +} diff --git a/jme3-core/src/main/resources/Common/IBL/IBLKernels.frag b/jme3-core/src/main/resources/Common/IBL/IBLKernels.frag index 7f32c0ae01..e9fb4645f5 100644 --- a/jme3-core/src/main/resources/Common/IBL/IBLKernels.frag +++ b/jme3-core/src/main/resources/Common/IBL/IBLKernels.frag @@ -34,7 +34,7 @@ void brdfKernel(){ float NdotH = max(H.z, 0.0); float VdotH = max(dot(V, H), 0.0); if(NdotL > 0.0){ - float G = GeometrySmith(N, V, L, m_Roughness); + float G = GeometrySmith(N, V, L, m_Roughness*m_Roughness); float G_Vis = (G * VdotH) / (NdotH * NdotV); float Fc = pow(1.0 - VdotH, 5.0); A += (1.0 - Fc) * G_Vis; @@ -75,9 +75,7 @@ void prefilteredEnvKernel(){ vec3 R = N; vec3 V = R; - // float a2 = m_Roughness; - float a2 = m_Roughness * m_Roughness; // jme impl, why? - a2 *= a2; + float a2 = m_Roughness * m_Roughness; const uint SAMPLE_COUNT = 1024u; float totalWeight = 0.0; @@ -85,16 +83,25 @@ void prefilteredEnvKernel(){ for(uint i = 0u; i < SAMPLE_COUNT; ++i) { vec4 Xi = Hammersley(i, SAMPLE_COUNT); vec3 H = ImportanceSampleGGX(Xi, a2, N); - float VoH = dot(V,H); + float VoH = max(dot(V, H), 0.0); vec3 L = normalize(2.0 * VoH * H - V); float NdotL = max(dot(N, L), 0.0); if(NdotL > 0.0) { + vec3 sampleColor = texture(m_EnvMap, L).rgb; + + float luminance = dot(sampleColor, vec3(0.2126, 0.7152, 0.0722)); + if (luminance > 64.0) { // TODO use average? + sampleColor *= 64.0/luminance; + } + // TODO: use mipmap - prefilteredColor += texture(m_EnvMap, L).rgb * NdotL; + prefilteredColor += sampleColor * NdotL; totalWeight += NdotL; } + } - prefilteredColor = prefilteredColor / totalWeight; + + if(totalWeight > 0.001) prefilteredColor /= totalWeight; outFragColor = vec4(prefilteredColor, 1.0); } diff --git a/jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.vert b/jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.vert index d4242ee8ad..c1460cc9be 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.vert +++ b/jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.vert @@ -10,8 +10,8 @@ // fog - jayfella #ifdef USE_FOG -varying float fog_distance; -uniform vec3 g_CameraPosition; + varying float fogDistance; + uniform vec3 g_CameraPosition; #endif uniform vec4 m_Ambient; @@ -186,6 +186,6 @@ void main(){ #endif #ifdef USE_FOG - fog_distance = distance(g_CameraPosition, (TransformWorld(modelSpacePos)).xyz); + fogDistance = distance(g_CameraPosition, (TransformWorld(modelSpacePos)).xyz); #endif -} \ No newline at end of file +} diff --git a/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md b/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md index 36fa6c6be9..488c7e565a 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md +++ b/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.j3md @@ -147,14 +147,15 @@ MaterialDef PBR Lighting { // debug the final value of the selected layer as a color output Int DebugValuesMode // Layers: - // 0 - albedo (un-shaded) + // 0 - albedo (unshaded) // 1 - normals // 2 - roughness // 3 - metallic // 4 - ao - // 5 - emissive + // 5 - emissive // 6 - exposure // 7 - alpha + // 8 - geometryNormals } Technique { diff --git a/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.vert b/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.vert index 23427c0795..9e50b0fb75 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.vert +++ b/jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.vert @@ -76,7 +76,7 @@ void main(){ texCoord2 = inTexCoord2; #endif - wPosition = (g_WorldMatrix * vec4(inPosition, 1.0)).xyz; + wPosition = TransformWorld(modelSpacePos).xyz; wNormal = TransformWorldNormal(modelSpaceNorm); wTangent = vec4(TransformWorldNormal(modelSpaceTan),inTangent.w); @@ -88,7 +88,7 @@ void main(){ #endif #ifdef USE_FOG - fogDistance = distance(g_CameraPosition, (g_WorldMatrix * modelSpacePos).xyz); + fogDistance = distance(g_CameraPosition, (TransformWorld(modelSpacePos)).xyz); #endif } diff --git a/jme3-core/src/main/resources/Common/ShaderLib/TangentUtils.glsllib b/jme3-core/src/main/resources/Common/ShaderLib/TangentUtils.glsllib index b07c9c197c..477371155c 100644 --- a/jme3-core/src/main/resources/Common/ShaderLib/TangentUtils.glsllib +++ b/jme3-core/src/main/resources/Common/ShaderLib/TangentUtils.glsllib @@ -4,7 +4,8 @@ //used for calculating tangents in-shader for axis-aligned quads/planes/terrains //primarily used for PBR terrains, since jme terrains do not store pre-calculated tangents by default (thus the tbnMat cannot be used for PBR light calculation like it is in jme's stock PBR shader) vec3 calculateTangentsAndApplyToNormals(in vec3 normalIn, in vec3 worldNorm){ - vec3 baseNorm = worldNorm.rgb + vec3(0, 0, 1); + vec3 baseNorm = worldNorm.rgb + vec3(0.0, 0.0, 1.0); + normalIn *= vec3(-1.0, 1.0, 1.0); normalIn = baseNorm.rgb*dot(baseNorm.rgb, normalIn.rgb)/baseNorm.z - normalIn.rgb; normalIn = normalize(normalIn); diff --git a/jme3-core/src/main/resources/Common/ShaderLib/TriPlanarUtils.glsllib b/jme3-core/src/main/resources/Common/ShaderLib/TriPlanarUtils.glsllib index 7b79a4181b..8134200ae3 100644 --- a/jme3-core/src/main/resources/Common/ShaderLib/TriPlanarUtils.glsllib +++ b/jme3-core/src/main/resources/Common/ShaderLib/TriPlanarUtils.glsllib @@ -1,6 +1,10 @@ #ifndef __TRIPLANAR_UTILS_MODULE__ #define __TRIPLANAR_UTILS_MODULE__ + #ifndef NORMAL_TYPE + #define NORMAL_TYPE -1.0 + #endif + vec3 triBlending; void TriPlanarUtils_calculateBlending(vec3 geometryNormal){ @@ -39,9 +43,9 @@ vec4 col2 = texture2D( map, coords.xz * scale); vec4 col3 = texture2D( map, coords.xy * scale); - col1.xyz = col1.xyz * vec3(2.0) - vec3(1.0); - col2.xyz = col2.xyz * vec3(2.0) - vec3(1.0); - col3.xyz = col3.xyz * vec3(2.0) - vec3(1.0); + col1.xyz = col1.xyz * vec3(2.0, NORMAL_TYPE * 2.0, 2.0) - vec3(1.0, NORMAL_TYPE * 1.0, 1.0); + col2.xyz = col2.xyz * vec3(2.0, NORMAL_TYPE * 2.0, 2.0) - vec3(1.0, NORMAL_TYPE * 1.0, 1.0); + col3.xyz = col3.xyz * vec3(2.0, NORMAL_TYPE * 2.0, 2.0) - vec3(1.0, NORMAL_TYPE * 1.0, 1.0); // blend the results of the 3 planar projections. vec4 tex = normalize(col1 * triBlending.x + col2 * triBlending.y + col3 * triBlending.z); @@ -51,16 +55,16 @@ // triplanar blend for Normal maps in a TextureArray: vec4 getTriPlanarNormalBlendFromTexArray(in vec3 coords, in int idInTexArray, in float scale, in sampler2DArray texArray) { - vec4 col1 = texture2DArray( texArray, vec3((coords.yz * scale), idInTexArray ) ); - vec4 col2 = texture2DArray( texArray, vec3((coords.xz * scale), idInTexArray ) ); - vec4 col3 = texture2DArray( texArray, vec3((coords.xy * scale), idInTexArray ) ); + vec4 col1 = texture2DArray( texArray, vec3((coords.yz * scale), idInTexArray )); + vec4 col2 = texture2DArray( texArray, vec3((coords.xz * scale), idInTexArray )); + vec4 col3 = texture2DArray( texArray, vec3((coords.xy * scale), idInTexArray )); - col1.xyz = col1.xyz * vec3(2.0) - vec3(1.0); - col2.xyz = col2.xyz * vec3(2.0) - vec3(1.0); - col3.xyz = col3.xyz * vec3(2.0) - vec3(1.0); + col1.xyz = col1.xyz * vec3(2.0, NORMAL_TYPE * 2.0, 2.0) - vec3(1.0, NORMAL_TYPE * 1.0, 1.0); + col2.xyz = col2.xyz * vec3(2.0, NORMAL_TYPE * 2.0, 2.0) - vec3(1.0, NORMAL_TYPE * 1.0, 1.0); + col3.xyz = col3.xyz * vec3(2.0, NORMAL_TYPE * 2.0, 2.0) - vec3(1.0, NORMAL_TYPE * 1.0, 1.0); // blend the results of the 3 planar projections. - vec4 tex = normalize(col1 * triBlending.x + col2 * triBlending.y + col3 * triBlending.z); + vec4 tex = normalize(col1 * triBlending.x + col2 * triBlending.y + col3 * triBlending.z); return tex; } diff --git a/jme3-core/src/main/resources/Common/ShaderLib/module/pbrlighting/PBRLightingUtils.glsllib b/jme3-core/src/main/resources/Common/ShaderLib/module/pbrlighting/PBRLightingUtils.glsllib index d30388e62c..41f9e77b9c 100644 --- a/jme3-core/src/main/resources/Common/ShaderLib/module/pbrlighting/PBRLightingUtils.glsllib +++ b/jme3-core/src/main/resources/Common/ShaderLib/module/pbrlighting/PBRLightingUtils.glsllib @@ -384,10 +384,10 @@ //spec gloss tex reads: #ifdef SPECGLOSSPIPELINE + float glossiness = m_Glossiness; #ifdef USE_PACKED_SG vec4 specularColor = texture2D(m_SpecularGlossinessMap, newTexCoord); - float glossiness = specularColor.a * m_Glossiness; - specularColor *= m_Specular; + glossiness *= specularColor.a; #else #ifdef SPECULARMAP vec4 specularColor = texture2D(m_SpecularMap, newTexCoord); @@ -395,17 +395,14 @@ vec4 specularColor = vec4(1.0); #endif #ifdef GLOSSINESSMAP - float glossiness = texture2D(m_GlossinesMap, newTexCoord).r * m_Glossiness; - #else - float glossiness = m_Glossiness; + glossiness *= texture2D(m_GlossinesMap, newTexCoord).r; #endif - specularColor *= m_Specular; - surface.specularColor = specularColor; #endif + specularColor *= m_Specular; + surface.specularColor = specularColor.rgb; + surface.roughness = 1.0 - glossiness; #endif - - vec3 ao = vec3(1.0); #ifdef LIGHTMAP vec3 lightMapColor; @@ -475,7 +472,6 @@ #ifdef SPECGLOSSPIPELINE surface.diffuseColor = surface.albedo;// * (1.0 - max(max(specularColor.r, specularColor.g), specularColor.b)); - surface.roughness = 1.0 - m_Glossiness; surface.fZero = surface.specularColor.xyz; #else surface.specularColor = (0.04 - 0.04 * surface.metallic) + surface.albedo * surface.metallic; // 0.04 is the standard base specular reflectance for non-metallic surfaces in PBR. While values like 0.08 can be used for different implementations, 0.04 aligns with Khronos' PBR specification. @@ -586,54 +582,92 @@ finalLightingScale = max(finalLightingScale, surface.brightestNonGlobalLightStrength); finalLightingScale = max(finalLightingScale, minVertLighting); //essentially just the vertColors.r (aka indoor light exposure) multiplied by the time of day scale. + #if NB_PROBES >= 1 + vec3 color1 = vec3(0.0); + vec3 color2 = vec3(0.0); + vec3 color3 = vec3(0.0); + float weight1 = 1.0; + float weight2 = 0.0; + float weight3 = 0.0; - #if NB_PROBES > 0 - float probeNdfSum = 0.0; - float invProbeNdfSum = 0.0; - - #for i=1..4 ( #if NB_PROBES >= $i $0 #endif ) - vec3 probeColor$i; - float probeNdf$i = renderProbe( + float ndf = renderProbe( surface.viewDir, surface.position, surface.normal, surface.geometryNormal, surface.roughness, - vec4(surface.diffuseColor,1.0), - vec4(surface.specularColor,1.0), + vec4(surface.diffuseColor, 1.0), + vec4(surface.specularColor, 1.0), surface.NdotV, surface.ao, - #if $i == 1 - g_LightProbeData, - #else - g_LightProbeData$i, - #endif - g_ShCoeffs, - g_PrefEnvMap, - probeColor$i - ); - float probeInvNdf$i = max(1.0 - probeNdf$i,0.0); - probeNdfSum += probeNdf$i; - invProbeNdfSum += probeInvNdf$i; - #ifdef USE_AMBIENT_LIGHT - probeColor$i.rgb *= g_AmbientLightColor.rgb; + g_LightProbeData, g_ShCoeffs, g_PrefEnvMap, color1); + #if NB_PROBES >= 2 + float ndf2 = renderProbe( + surface.viewDir, + surface.position, + surface.normal, + surface.geometryNormal, + surface.roughness, + vec4(surface.diffuseColor, 1.0), + vec4(surface.specularColor, 1.0), + surface.NdotV, + surface.ao, + g_LightProbeData2, + g_ShCoeffs2, + g_PrefEnvMap2, + color2); + #endif + #if NB_PROBES == 3 + float ndf3 = renderProbe( + surface.viewDir, + surface.position, + surface.normal, + surface.geometryNormal, + surface.roughness, + vec4(surface.diffuseColor, 1.0), + vec4(surface.specularColor, 1.0), + surface.NdotV, + surface.ao, + g_LightProbeData3, + g_ShCoeffs3, + g_PrefEnvMap3, + color3); + #endif + + #if NB_PROBES >= 2 + float invNdf = max(1.0 - ndf,0.0); + float invNdf2 = max(1.0 - ndf2,0.0); + float sumNdf = ndf + ndf2; + float sumInvNdf = invNdf + invNdf2; + #if NB_PROBES == 3 + float invNdf3 = max(1.0 - ndf3,0.0); + sumNdf += ndf3; + sumInvNdf += invNdf3; + weight3 = ((1.0 - (ndf3 / sumNdf)) / (NB_PROBES - 1)) * (invNdf3 / sumInvNdf); #endif - probeColor$i.rgb *= finalLightingScale; - #endfor - - #if NB_PROBES > 1 - float probeWeightSum = 0.0; - #for i=1..4 ( #if NB_PROBES >= $i $0 #endif ) - float probeWeight$i = ((1.0 - (probeNdf$i / probeNdfSum)) / (NB_PROBES - 1)) * ( probeInvNdf$i / invProbeNdfSum); - probeWeightSum += probeWeight$i; - #endfor - - #for i=1..4 ( #if NB_PROBES >= $i $0 #endif ) - surface.envLightContribution.rgb += probeColor$i * clamp( probeWeight$i / probeWeightSum, 0., 1.); - #endfor - #else - surface.envLightContribution.rgb += probeColor1; + + weight1 = ((1.0 - (ndf / sumNdf)) / (NB_PROBES - 1)) * (invNdf / sumInvNdf); + weight2 = ((1.0 - (ndf2 / sumNdf)) / (NB_PROBES - 1)) * (invNdf2 / sumInvNdf); + + float weightSum = weight1 + weight2 + weight3; + + weight1 /= weightSum; + weight2 /= weightSum; + weight3 /= weightSum; + #endif + + #ifdef USE_AMBIENT_LIGHT + color1.rgb *= g_AmbientLightColor.rgb; + color2.rgb *= g_AmbientLightColor.rgb; + color3.rgb *= g_AmbientLightColor.rgb; #endif + + color1.rgb *= finalLightingScale; + color2.rgb *= finalLightingScale; + color3.rgb *= finalLightingScale; + + surface.envLightContribution.rgb += color1 * clamp(weight1,0.0,1.0) + color2 * clamp(weight2,0.0,1.0) + color3 * clamp(weight3,0.0,1.0); + #endif } #endif diff --git a/jme3-core/src/test/java/com/jme3/audio/AudioFilterTest.java b/jme3-core/src/test/java/com/jme3/audio/AudioFilterTest.java new file mode 100644 index 0000000000..5ccf79d6c0 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/audio/AudioFilterTest.java @@ -0,0 +1,62 @@ +package com.jme3.audio; + +import com.jme3.asset.AssetManager; +import com.jme3.asset.DesktopAssetManager; +import com.jme3.export.binary.BinaryExporter; +import org.junit.Assert; +import org.junit.Test; + +/** + * Automated tests for the Filter class. + * + * @author capdevon + */ +public class AudioFilterTest { + + /** + * Tests serialization and de-serialization of a {@code LowPassFilter}. + */ + @Test + public void testSaveAndLoad_LowPassFilter() { + AssetManager assetManager = new DesktopAssetManager(true); + + LowPassFilter f = new LowPassFilter(.5f, .5f); + LowPassFilter copy = BinaryExporter.saveAndLoad(assetManager, f); + + float delta = 0.001f; + Assert.assertEquals(f.getVolume(), copy.getVolume(), delta); + Assert.assertEquals(f.getHighFreqVolume(), copy.getHighFreqVolume(), delta); + } + + /** + * Tests serialization and de-serialization of a {@code HighPassFilter}. + */ + @Test + public void testSaveAndLoad_HighPassFilter() { + AssetManager assetManager = new DesktopAssetManager(true); + + HighPassFilter f = new HighPassFilter(.5f, .5f); + HighPassFilter copy = BinaryExporter.saveAndLoad(assetManager, f); + + float delta = 0.001f; + Assert.assertEquals(f.getVolume(), copy.getVolume(), delta); + Assert.assertEquals(f.getLowFreqVolume(), copy.getLowFreqVolume(), delta); + } + + /** + * Tests serialization and de-serialization of a {@code BandPassFilter}. + */ + @Test + public void testSaveAndLoad_BandPassFilter() { + AssetManager assetManager = new DesktopAssetManager(true); + + BandPassFilter f = new BandPassFilter(.5f, .5f, .5f); + BandPassFilter copy = BinaryExporter.saveAndLoad(assetManager, f); + + float delta = 0.001f; + Assert.assertEquals(f.getVolume(), copy.getVolume(), delta); + Assert.assertEquals(f.getHighFreqVolume(), copy.getHighFreqVolume(), delta); + Assert.assertEquals(f.getLowFreqVolume(), copy.getLowFreqVolume(), delta); + } + +} diff --git a/jme3-core/src/test/java/com/jme3/audio/AudioNodeTest.java b/jme3-core/src/test/java/com/jme3/audio/AudioNodeTest.java new file mode 100644 index 0000000000..e7e32012f8 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/audio/AudioNodeTest.java @@ -0,0 +1,48 @@ +package com.jme3.audio; + +import com.jme3.asset.AssetManager; +import com.jme3.math.Vector3f; +import com.jme3.system.JmeSystem; +import org.junit.Assert; +import org.junit.Test; + +/** + * Automated tests for the {@code AudioNode} class. + * + * @author capdevon + */ +public class AudioNodeTest { + + @Test + public void testAudioNodeClone() { + AssetManager assetManager = JmeSystem.newAssetManager(AudioNodeTest.class.getResource("/com/jme3/asset/Desktop.cfg")); + + AudioNode audio = new AudioNode(assetManager, + "Sound/Effects/Bang.wav", AudioData.DataType.Buffer); + audio.setDirection(new Vector3f(0, 1, 0)); + audio.setVelocity(new Vector3f(1, 1, 1)); + audio.setDryFilter(new LowPassFilter(1f, .1f)); + audio.setReverbFilter(new LowPassFilter(.5f, .5f)); + + AudioNode clone = audio.clone(); + + Assert.assertNotNull(clone.previousWorldTranslation); + Assert.assertNotSame(audio.previousWorldTranslation, clone.previousWorldTranslation); + Assert.assertEquals(audio.previousWorldTranslation, clone.previousWorldTranslation); + + Assert.assertNotNull(clone.getDirection()); + Assert.assertNotSame(audio.getDirection(), clone.getDirection()); + Assert.assertEquals(audio.getDirection(), clone.getDirection()); + + Assert.assertNotNull(clone.getVelocity()); + Assert.assertNotSame(audio.getVelocity(), clone.getVelocity()); + Assert.assertEquals(audio.getVelocity(), clone.getVelocity()); + + Assert.assertNotNull(clone.getDryFilter()); + Assert.assertNotSame(audio.getDryFilter(), clone.getDryFilter()); + + Assert.assertNotNull(clone.getReverbFilter()); + Assert.assertNotSame(audio.getReverbFilter(), clone.getReverbFilter()); + } + +} diff --git a/jme3-core/src/test/java/com/jme3/cinematic/MotionPathTest.java b/jme3-core/src/test/java/com/jme3/cinematic/MotionPathTest.java new file mode 100644 index 0000000000..7fa2c71f58 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/cinematic/MotionPathTest.java @@ -0,0 +1,75 @@ +/* + * 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.cinematic; + +import com.jme3.math.Vector3f; +import com.jme3.util.clone.Cloner; +import org.junit.Assert; +import org.junit.Test; + +/** + * Verifies that the {@link MotionPath} class works. + * + * @author Stephen Gold + */ +public class MotionPathTest { + + /** + * Verifies that MotionPath cloning works. + */ + @Test + public void cloneMotionPath() { + MotionPath original = new MotionPath(); + original.setCycle(true); + original.addWayPoint(new Vector3f(20, 3, 0)); + original.addWayPoint(new Vector3f(0, 3, 20)); + original.addWayPoint(new Vector3f(-20, 3, 0)); + original.addWayPoint(new Vector3f(0, 3, -20)); + original.setCurveTension(0.83f); + + MotionPath clone = Cloner.deepClone(original); + + // Verify that the clone is non-null and distinct from the original: + Assert.assertNotNull(clone); + Assert.assertTrue(clone != original); + + // Compare the return values of various getters: + Assert.assertEquals( + clone.getCurveTension(), original.getCurveTension(), 0f); + Assert.assertEquals(clone.getLength(), original.getLength(), 0f); + Assert.assertEquals(clone.getNbWayPoints(), original.getNbWayPoints()); + Assert.assertEquals( + clone.getPathSplineType(), original.getPathSplineType()); + Assert.assertEquals(clone.getWayPoint(0), original.getWayPoint(0)); + Assert.assertEquals(clone.isCycle(), original.isCycle()); + } +} diff --git a/jme3-core/src/test/java/com/jme3/effect/influencers/ParticleInfluencerTest.java b/jme3-core/src/test/java/com/jme3/effect/influencers/ParticleInfluencerTest.java new file mode 100644 index 0000000000..1e98da6e57 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/effect/influencers/ParticleInfluencerTest.java @@ -0,0 +1,78 @@ +package com.jme3.effect.influencers; + +import com.jme3.asset.AssetManager; +import com.jme3.asset.DesktopAssetManager; +import com.jme3.export.binary.BinaryExporter; +import com.jme3.math.Vector3f; +import org.junit.Assert; +import org.junit.Test; + +/** + * Automated tests for the {@code ParticleInfluencer} class. + * + * @author capdevon + */ +public class ParticleInfluencerTest { + + /** + * Tests cloning, serialization and de-serialization of a {@code NewtonianParticleInfluencer}. + */ + @Test + public void testNewtonianParticleInfluencer() { + AssetManager assetManager = new DesktopAssetManager(true); + + NewtonianParticleInfluencer inf = new NewtonianParticleInfluencer(); + inf.setNormalVelocity(1); + inf.setSurfaceTangentFactor(0.5f); + inf.setSurfaceTangentRotation(2.5f); + inf.setInitialVelocity(new Vector3f(0, 1, 0)); + inf.setVelocityVariation(2f); + + NewtonianParticleInfluencer clone = (NewtonianParticleInfluencer) inf.clone(); + assertEquals(inf, clone); + Assert.assertNotSame(inf.temp, clone.temp); + + NewtonianParticleInfluencer copy = BinaryExporter.saveAndLoad(assetManager, inf); + assertEquals(inf, copy); + } + + private void assertEquals(NewtonianParticleInfluencer inf, NewtonianParticleInfluencer clone) { + Assert.assertEquals(inf.getNormalVelocity(), clone.getNormalVelocity(), 0.001f); + Assert.assertEquals(inf.getSurfaceTangentFactor(), clone.getSurfaceTangentFactor(), 0.001f); + Assert.assertEquals(inf.getSurfaceTangentRotation(), clone.getSurfaceTangentRotation(), 0.001f); + Assert.assertEquals(inf.getInitialVelocity(), clone.getInitialVelocity()); + Assert.assertEquals(inf.getVelocityVariation(), clone.getVelocityVariation(), 0.001f); + } + + /** + * Tests cloning, serialization and de-serialization of a {@code RadialParticleInfluencer}. + */ + @Test + public void testRadialParticleInfluencer() { + AssetManager assetManager = new DesktopAssetManager(true); + + RadialParticleInfluencer inf = new RadialParticleInfluencer(); + inf.setHorizontal(true); + inf.setOrigin(new Vector3f(0, 1, 0)); + inf.setRadialVelocity(2f); + inf.setInitialVelocity(new Vector3f(0, 1, 0)); + inf.setVelocityVariation(2f); + + RadialParticleInfluencer clone = (RadialParticleInfluencer) inf.clone(); + assertEquals(inf, clone); + Assert.assertNotSame(inf.temp, clone.temp); + Assert.assertNotSame(inf.getOrigin(), clone.getOrigin()); + + RadialParticleInfluencer copy = BinaryExporter.saveAndLoad(assetManager, inf); + assertEquals(inf, copy); + } + + private void assertEquals(RadialParticleInfluencer inf, RadialParticleInfluencer clone) { + Assert.assertEquals(inf.isHorizontal(), clone.isHorizontal()); + Assert.assertEquals(inf.getOrigin(), clone.getOrigin()); + Assert.assertEquals(inf.getRadialVelocity(), clone.getRadialVelocity(), 0.001f); + Assert.assertEquals(inf.getInitialVelocity(), clone.getInitialVelocity()); + Assert.assertEquals(inf.getVelocityVariation(), clone.getVelocityVariation(), 0.001f); + } + +} diff --git a/jme3-core/src/test/java/com/jme3/math/SplineTest.java b/jme3-core/src/test/java/com/jme3/math/SplineTest.java index 3678282069..447bf1d7bb 100644 --- a/jme3-core/src/test/java/com/jme3/math/SplineTest.java +++ b/jme3-core/src/test/java/com/jme3/math/SplineTest.java @@ -34,6 +34,7 @@ import com.jme3.asset.AssetManager; import com.jme3.asset.DesktopAssetManager; import com.jme3.export.binary.BinaryExporter; +import com.jme3.util.clone.Cloner; import java.util.ArrayList; import java.util.List; import org.junit.Assert; @@ -52,6 +53,47 @@ public class SplineTest { // ************************************************************************* // tests + /** + * Verifies that spline cloning works correctly. + */ + @Test + public void cloneSplines() { + // Clone a Bézier spline: + { + Spline test1 = createBezier(); + Spline copy1 = Cloner.deepClone(test1); + assertSplineEquals(test1, copy1); + } + + // Clone a NURB spline: + { + Spline test2 = createNurb(); + Spline copy2 = Cloner.deepClone(test2); + assertSplineEquals(test2, copy2); + } + + // Clone a Catmull-Rom spline: + { + Spline test3 = createCatmullRom(); + Spline copy3 = Cloner.deepClone(test3); + assertSplineEquals(test3, copy3); + } + + // Clone a linear spline: + { + Spline test4 = createLinear(); + Spline copy4 = Cloner.deepClone(test4); + assertSplineEquals(test4, copy4); + } + + // Clone a default spline: + { + Spline test5 = new Spline(); + Spline copy5 = Cloner.deepClone(test5); + assertSplineEquals(test5, copy5); + } + } + /** * Verifies that spline serialization/deserialization works correctly. */ @@ -59,63 +101,28 @@ public class SplineTest { public void saveAndLoadSplines() { // Serialize and deserialize a Bezier spline: { - Vector3f[] controlPoints1 = { - new Vector3f(0f, 1f, 0f), new Vector3f(1f, 2f, 1f), - new Vector3f(1.5f, 1.5f, 1.5f), new Vector3f(2f, 0f, 1f) - }; - - Spline test1 = new Spline( - Spline.SplineType.Bezier, controlPoints1, 0.1f, true); + Spline test1 = createBezier(); Spline copy1 = BinaryExporter.saveAndLoad(assetManager, test1); assertSplineEquals(test1, copy1); } // Serialize and deserialize a NURB spline: { - List controlPoints2 = new ArrayList<>(5); - controlPoints2.add(new Vector4f(0f, 1f, 2f, 3f)); - controlPoints2.add(new Vector4f(3f, 1f, 4f, 0f)); - controlPoints2.add(new Vector4f(2f, 5f, 3f, 0f)); - controlPoints2.add(new Vector4f(3f, 2f, 3f, 1f)); - controlPoints2.add(new Vector4f(0.5f, 1f, 0.6f, 5f)); - List nurbKnots = new ArrayList<>(6); - nurbKnots.add(0.2f); - nurbKnots.add(0.3f); - nurbKnots.add(0.4f); - nurbKnots.add(0.43f); - nurbKnots.add(0.51f); - nurbKnots.add(0.52f); - - Spline test2 = new Spline(controlPoints2, nurbKnots); + Spline test2 = createNurb(); Spline copy2 = BinaryExporter.saveAndLoad(assetManager, test2); assertSplineEquals(test2, copy2); } // Serialize and deserialize a Catmull-Rom spline: { - List controlPoints3 = new ArrayList<>(6); - controlPoints3.add(new Vector3f(0f, 1f, 2f)); - controlPoints3.add(new Vector3f(3f, -1f, 4f)); - controlPoints3.add(new Vector3f(2f, 5f, 3f)); - controlPoints3.add(new Vector3f(3f, -2f, 3f)); - controlPoints3.add(new Vector3f(0.5f, 1f, 0.6f)); - controlPoints3.add(new Vector3f(-0.5f, 4f, 0.2f)); - - Spline test3 = new Spline( - Spline.SplineType.CatmullRom, controlPoints3, 0.01f, false); + Spline test3 = createCatmullRom(); Spline copy3 = BinaryExporter.saveAndLoad(assetManager, test3); assertSplineEquals(test3, copy3); } // Serialize and deserialize a linear spline: { - List controlPoints4 = new ArrayList<>(3); - controlPoints4.add(new Vector3f(3f, -1f, 4f)); - controlPoints4.add(new Vector3f(2f, 0f, 3f)); - controlPoints4.add(new Vector3f(3f, -2f, 3f)); - - Spline test4 = new Spline( - Spline.SplineType.Linear, controlPoints4, 0f, true); + Spline test4 = createLinear(); Spline copy4 = BinaryExporter.saveAndLoad(assetManager, test4); assertSplineEquals(test4, copy4); } @@ -131,14 +138,22 @@ public void saveAndLoadSplines() { // private helper methods /** - * Verify that the specified lists are equivalent. + * Verifies that the specified lists are equivalent but distinct. * * @param s1 the first list to compare (may be null, unaffected) * @param s2 the 2nd list to compare (may be null, unaffected) */ private static void assertListEquals(List a1, List a2) { - if (a1 != a2) { + if (a1 == null || a2 == null) { + // If either list is null, verify that both are null: + Assert.assertNull(a1); + Assert.assertNull(a2); + + } else { + // Verify that the lists are distinct and and of equal length: + Assert.assertTrue(a1 != a2); Assert.assertEquals(a1.size(), a2.size()); + for (int i = 0; i < a1.size(); ++i) { Assert.assertEquals(a1.get(i), a2.get(i)); } @@ -172,4 +187,80 @@ private static void assertSplineEquals(Spline s1, Spline s2) { s1.getTotalLength(), s2.getTotalLength(), 0f); Assert.assertArrayEquals(s1.getWeights(), s2.getWeights(), 0f); } + + /** + * Generates a simple cyclic Bézier spline for testing. + * + * @return a new Spline + */ + private static Spline createBezier() { + Vector3f[] controlPoints1 = { + new Vector3f(0f, 1f, 0f), new Vector3f(1f, 2f, 1f), + new Vector3f(1.5f, 1.5f, 1.5f), new Vector3f(2f, 0f, 1f) + }; + + Spline result = new Spline( + Spline.SplineType.Bezier, controlPoints1, 0.1f, true); + return result; + } + + /** + * Generates a simple acyclic Catmull-Rom spline for testing. + * + * @return a new Spline + */ + private static Spline createCatmullRom() { + List controlPoints3 = new ArrayList<>(6); + controlPoints3.add(new Vector3f(0f, 1f, 2f)); + controlPoints3.add(new Vector3f(3f, -1f, 4f)); + controlPoints3.add(new Vector3f(2f, 5f, 3f)); + controlPoints3.add(new Vector3f(3f, -2f, 3f)); + controlPoints3.add(new Vector3f(0.5f, 1f, 0.6f)); + controlPoints3.add(new Vector3f(-0.5f, 4f, 0.2f)); + + Spline result = new Spline( + Spline.SplineType.CatmullRom, controlPoints3, 0.01f, false); + return result; + } + + /** + * Generates a simple cyclic linear spline for testing. + * + * @return a new Spline + */ + private static Spline createLinear() { + List controlPoints4 = new ArrayList<>(3); + controlPoints4.add(new Vector3f(3f, -1f, 4f)); + controlPoints4.add(new Vector3f(2f, 0f, 3f)); + controlPoints4.add(new Vector3f(3f, -2f, 3f)); + + Spline result = new Spline( + Spline.SplineType.Linear, controlPoints4, 0f, true); + return result; + } + + /** + * Generates a simple NURB spline for testing. + * + * @return a new Spline + */ + private static Spline createNurb() { + List controlPoints2 = new ArrayList<>(5); + controlPoints2.add(new Vector4f(0f, 1f, 2f, 3f)); + controlPoints2.add(new Vector4f(3f, 1f, 4f, 0f)); + controlPoints2.add(new Vector4f(2f, 5f, 3f, 0f)); + controlPoints2.add(new Vector4f(3f, 2f, 3f, 1f)); + controlPoints2.add(new Vector4f(0.5f, 1f, 0.6f, 5f)); + + List nurbKnots = new ArrayList<>(6); + nurbKnots.add(0.2f); + nurbKnots.add(0.3f); + nurbKnots.add(0.4f); + nurbKnots.add(0.43f); + nurbKnots.add(0.51f); + nurbKnots.add(0.52f); + + Spline result = new Spline(controlPoints2, nurbKnots); + return result; + } } diff --git a/jme3-core/src/test/java/com/jme3/renderer/Issue2333Test.java b/jme3-core/src/test/java/com/jme3/renderer/Issue2333Test.java new file mode 100644 index 0000000000..ca553a6137 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/renderer/Issue2333Test.java @@ -0,0 +1,241 @@ +/* + * 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.renderer; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Automated tests for "Camera Viewport Dimensions not Checked" (issue #2333 at + * GitHub). + * + * @author Stephen Gold sgold@sonic.net + */ +public class Issue2333Test { + + /** + * Tests some basic functionality of the viewport settings. + */ + @Test + public void testIssue2333() { + Camera c = new Camera(1, 1); + + // Verify some Camera defaults: + Assert.assertEquals(0f, c.getViewPortBottom(), 0f); + Assert.assertEquals(0f, c.getViewPortLeft(), 0f); + Assert.assertEquals(1f, c.getViewPortRight(), 0f); + Assert.assertEquals(1f, c.getViewPortTop(), 0f); + + // Try some valid settings: + new Camera(1, 1).setViewPort(0.5f, 0.7f, 0.1f, 0.3f); + new Camera(1, 1).setViewPortBottom(0.9f); + new Camera(1, 1).setViewPortLeft(0.99f); + new Camera(1, 1).setViewPortRight(0.01f); + new Camera(1, 1).setViewPortTop(0.1f); + } + + /** + * Verifies that setViewPort() with left = right throws an + * IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void iaeCase01() { + new Camera(1, 1).setViewPort(0.5f, 0.5f, 0f, 1f); + } + + /** + * Verifies that setViewPort() with left > right throws an + * IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void iaeCase02() { + new Camera(1, 1).setViewPort(0.7f, 0.5f, 0f, 1f); + } + + /** + * Verifies that setViewPortLeft() resulting in left = right throws an + * IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void iaeCase03() { + new Camera(1, 1).setViewPortLeft(1f); + } + + /** + * Verifies that setViewPortLeft() resulting in left > right throws an + * IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void iaeCase04() { + new Camera(1, 1).setViewPortLeft(1.1f); + } + + /** + * Verifies that setViewPortRight() resulting in left = right throws an + * IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void iaeCase05() { + new Camera(1, 1).setViewPortRight(0f); + } + + /** + * Verifies that setViewPortRight() resulting in left > right throws an + * IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void iaeCase06() { + new Camera(1, 1).setViewPortRight(-0.1f); + } + + /** + * Verifies that setViewPort() with bottom = top throws an + * IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void iaeCase07() { + new Camera(1, 1).setViewPort(0f, 1f, 0.5f, 0.5f); + } + + /** + * Verifies that setViewPort() with bottom > top throws an + * IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void iaeCase08() { + new Camera(1, 1).setViewPort(0f, 1f, 0.7f, 0.6f); + } + + /** + * Verifies that setViewPortBottom() resulting in bottom = top throws an + * IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void iaeCase09() { + new Camera(1, 1).setViewPortBottom(1f); + } + + /** + * Verifies that setViewPortBottom() resulting in bottom > top throws an + * IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void iaeCase10() { + new Camera(1, 1).setViewPortBottom(2f); + } + + /** + * Verifies that setViewPortTop() resulting in bottom = top throws an + * IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void iaeCase11() { + new Camera(1, 1).setViewPortTop(0f); + } + + /** + * Verifies that setViewPortTop() resulting in bottom > top throws an + * IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void iaeCase12() { + new Camera(1, 1).setViewPortTop(-1f); + } + + /** + * Verifies that setViewPort() with left = NaN throws an + * IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void iaeCase13() { + new Camera(1, 1).setViewPort(Float.NaN, 1f, 0f, 1f); + } + + /** + * Verifies that setViewPort() with right = NaN throws an + * IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void iaeCase14() { + new Camera(1, 1).setViewPort(0f, Float.NaN, 0f, 1f); + } + + /** + * Verifies that setViewPort() with bottom = NaN throws an + * IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void iaeCase15() { + new Camera(1, 1).setViewPort(0f, 1f, Float.NaN, 1f); + } + + /** + * Verifies that setViewPort() with top = NaN throws an + * IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void iaeCase16() { + new Camera(1, 1).setViewPort(0f, 1f, 0f, Float.NaN); + } + + /** + * Verifies that setViewPortBottom(NaN) throws an IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void iaeCase17() { + new Camera(1, 1).setViewPortBottom(Float.NaN); + } + + /** + * Verifies that setViewPortLeft(NaN) throws an IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void iaeCase18() { + new Camera(1, 1).setViewPortLeft(Float.NaN); + } + + /** + * Verifies that setViewPortRight(NaN) throws an IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void iaeCase19() { + new Camera(1, 1).setViewPortRight(Float.NaN); + } + + /** + * Verifies that setViewPortTop(NaN) throws an IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void iaeCase20() { + new Camera(1, 1).setViewPortTop(Float.NaN); + } +} diff --git a/jme3-core/src/test/java/com/jme3/shadow/FilterPostProcessingTest.java b/jme3-core/src/test/java/com/jme3/shadow/FilterPostProcessingTest.java new file mode 100644 index 0000000000..83ff564b0b --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/shadow/FilterPostProcessingTest.java @@ -0,0 +1,68 @@ +package com.jme3.shadow; + +import com.jme3.asset.AssetManager; +import com.jme3.asset.DesktopAssetManager; +import com.jme3.export.binary.BinaryExporter; +import com.jme3.light.DirectionalLight; +import com.jme3.light.PointLight; +import com.jme3.light.SpotLight; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector3f; +import com.jme3.post.Filter; +import com.jme3.post.FilterPostProcessor; +import org.junit.Assert; +import org.junit.Test; + +/** + * Automated tests for the {@code FilterPostProcessing} class. + * + * @author capdevon + */ +public class FilterPostProcessingTest { + + /** + * Tests serialization and de-serialization of a {@code FilterPostProcessing}. + */ + @Test + public void testSaveAndLoad() { + AssetManager assetManager = new DesktopAssetManager(true); + + FilterPostProcessor fpp = new FilterPostProcessor(assetManager); + fpp.addFilter(createDirectionalLightShadowFilter(assetManager)); + fpp.addFilter(createSpotLightShadowFilter(assetManager)); + fpp.addFilter(createPointLightShadowFilter(assetManager)); + + BinaryExporter.saveAndLoad(assetManager, fpp); + } + + private DirectionalLightShadowFilter createDirectionalLightShadowFilter(AssetManager assetManager) { + DirectionalLight light = new DirectionalLight(); + light.setDirection(new Vector3f(-1, -2, -3).normalizeLocal()); + light.setColor(new ColorRGBA(0.8f, 0.8f, 0.8f, 1f)); + + DirectionalLightShadowFilter dlsf = new DirectionalLightShadowFilter(assetManager, 2048, 1); + dlsf.setLight(light); + + return dlsf; + } + + private SpotLightShadowFilter createSpotLightShadowFilter(AssetManager assetManager) { + SpotLight light = new SpotLight(); + light.setColor(new ColorRGBA(0.8f, 0.8f, 0.8f, 1f)); + + SpotLightShadowFilter slsf = new SpotLightShadowFilter(assetManager, 2048); + slsf.setLight(light); + + return slsf; + } + + private PointLightShadowFilter createPointLightShadowFilter(AssetManager assetManager) { + PointLight light = new PointLight(); + light.setColor(new ColorRGBA(0.8f, 0.8f, 0.8f, 1f)); + + PointLightShadowFilter plsf = new PointLightShadowFilter(assetManager, 2048); + plsf.setLight(light); + + return plsf; + } +} diff --git a/jme3-core/src/test/java/com/jme3/texture/TextureArrayTest.java b/jme3-core/src/test/java/com/jme3/texture/TextureArrayTest.java new file mode 100644 index 0000000000..bd861b9879 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/texture/TextureArrayTest.java @@ -0,0 +1,73 @@ +/* + * 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.texture; + +import static org.junit.Assert.*; + +import com.jme3.asset.AssetManager; +import com.jme3.asset.DesktopAssetManager; +import com.jme3.export.binary.BinaryExporter; +import com.jme3.texture.Texture.WrapAxis; +import com.jme3.texture.Texture.WrapMode; +import com.jme3.texture.image.ColorSpace; +import com.jme3.util.BufferUtils; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; + +public class TextureArrayTest { + + private static final AssetManager assetManager = new DesktopAssetManager(); + + @Test + public void testExportWrapMode() { + List images = new ArrayList<>(); + images.add(createImage()); + images.add(createImage()); + TextureArray tex3 = new TextureArray(images); + tex3.setWrap(WrapMode.Repeat); + TextureArray tex4 = BinaryExporter.saveAndLoad(assetManager, tex3); + + assertEquals(tex3.getWrap(WrapAxis.S), tex4.getWrap(WrapAxis.S)); + assertEquals(tex3.getWrap(WrapAxis.T), tex4.getWrap(WrapAxis.T)); + } + + private Image createImage() { + int width = 8; + int height = 8; + int numBytes = 4 * width * height; + ByteBuffer data = BufferUtils.createByteBuffer(numBytes); + return new Image(Image.Format.RGBA8, width, height, data, ColorSpace.Linear); + } + +} diff --git a/jme3-examples/src/main/java/jme3test/audio/TestAmbient.java b/jme3-examples/src/main/java/jme3test/audio/TestAmbient.java index 708fe1c77a..2c5590ce63 100644 --- a/jme3-examples/src/main/java/jme3test/audio/TestAmbient.java +++ b/jme3-examples/src/main/java/jme3test/audio/TestAmbient.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 @@ -39,50 +39,70 @@ import com.jme3.math.ColorRGBA; import com.jme3.math.Vector3f; import com.jme3.scene.Geometry; -import com.jme3.scene.shape.Box; +import com.jme3.scene.Mesh; +import com.jme3.scene.debug.Grid; +import com.jme3.scene.shape.Sphere; public class TestAmbient extends SimpleApplication { - public static void main(String[] args) { - TestAmbient test = new TestAmbient(); - test.start(); - } + public static void main(String[] args) { + TestAmbient test = new TestAmbient(); + test.start(); + } - @Override - public void simpleInitApp() { - float[] eax = new float[]{15, 38.0f, 0.300f, -1000, -3300, 0, - 1.49f, 0.54f, 1.00f, -2560, 0.162f, 0.00f, 0.00f, - 0.00f, -229, 0.088f, 0.00f, 0.00f, 0.00f, 0.125f, 1.000f, - 0.250f, 0.000f, -5.0f, 5000.0f, 250.0f, 0.00f, 0x3f}; - Environment env = new Environment(eax); - audioRenderer.setEnvironment(env); + private final float[] eax = { + 15, 38.0f, 0.300f, -1000, -3300, 0, + 1.49f, 0.54f, 1.00f, -2560, 0.162f, 0.00f, 0.00f, + 0.00f, -229, 0.088f, 0.00f, 0.00f, 0.00f, 0.125f, 1.000f, + 0.250f, 0.000f, -5.0f, 5000.0f, 250.0f, 0.00f, 0x3f + }; - AudioNode waves = new AudioNode(assetManager, - "Sound/Environment/Ocean Waves.ogg", DataType.Buffer); - waves.setPositional(true); - waves.setLocalTranslation(new Vector3f(0, 0,0)); - waves.setMaxDistance(100); - waves.setRefDistance(5); + @Override + public void simpleInitApp() { + configureCamera(); - AudioNode nature = new AudioNode(assetManager, - "Sound/Environment/Nature.ogg", DataType.Stream); - nature.setPositional(false); - nature.setVolume(3); - - waves.playInstance(); - nature.play(); - - // just a blue box to mark the spot - Box box1 = new Box(.5f, .5f, .5f); - Geometry player = new Geometry("Player", box1); - Material mat1 = new Material(assetManager, - "Common/MatDefs/Misc/Unshaded.j3md"); - mat1.setColor("Color", ColorRGBA.Blue); - player.setMaterial(mat1); - rootNode.attachChild(player); - } + Environment env = new Environment(eax); + audioRenderer.setEnvironment(env); - @Override - public void simpleUpdate(float tpf) { - } + AudioNode waves = new AudioNode(assetManager, + "Sound/Environment/Ocean Waves.ogg", DataType.Buffer); + waves.setPositional(true); + waves.setLooping(true); + waves.setReverbEnabled(true); + rootNode.attachChild(waves); + + AudioNode nature = new AudioNode(assetManager, + "Sound/Environment/Nature.ogg", DataType.Stream); + nature.setPositional(false); + nature.setLooping(true); + nature.setVolume(3); + rootNode.attachChild(nature); + + waves.play(); + nature.play(); + + // just a blue sphere to mark the spot + Geometry marker = makeShape("Marker", new Sphere(16, 16, 1f), ColorRGBA.Blue); + waves.attachChild(marker); + + Geometry grid = makeShape("DebugGrid", new Grid(21, 21, 2), ColorRGBA.Gray); + grid.center().move(0, 0, 0); + rootNode.attachChild(grid); + } + + private void configureCamera() { + flyCam.setMoveSpeed(25f); + flyCam.setDragToRotate(true); + + cam.setLocation(Vector3f.UNIT_XYZ.mult(5f)); + cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y); + } + + private Geometry makeShape(String name, Mesh mesh, ColorRGBA color) { + Geometry geo = new Geometry(name, mesh); + Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", color); + geo.setMaterial(mat); + return geo; + } } diff --git a/jme3-examples/src/main/java/jme3test/audio/TestAudioDeviceDisconnect.java b/jme3-examples/src/main/java/jme3test/audio/TestAudioDeviceDisconnect.java new file mode 100644 index 0000000000..5f74f2f0a9 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/audio/TestAudioDeviceDisconnect.java @@ -0,0 +1,63 @@ +package jme3test.audio; + +import com.jme3.app.SimpleApplication; +import com.jme3.audio.AudioData; +import com.jme3.audio.AudioNode; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.input.controls.Trigger; + +/** + * This test demonstrates that destroying and recreating the OpenAL Context + * upon device disconnection is not an optimal solution. + * + * As shown, AudioNode instances playing in a loop cease to play after a device disconnection + * and would require explicit restarting. + * + * This test serves solely to highlight this issue, + * which should be addressed with a more robust solution within + * the ALAudioRenderer class in a dedicated future pull request on Git. + */ +public class TestAudioDeviceDisconnect extends SimpleApplication implements ActionListener { + + public static void main(String[] args) { + TestAudioDeviceDisconnect test = new TestAudioDeviceDisconnect(); + test.start(); + } + + private AudioNode audioSource; + + @Override + public void simpleInitApp() { + audioSource = new AudioNode(assetManager, + "Sound/Environment/Ocean Waves.ogg", AudioData.DataType.Buffer); + audioSource.setName("Waves"); + audioSource.setLooping(true); + rootNode.attachChild(audioSource); + + audioSource.play(); + + registerInputMappings(); + } + + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (!isPressed) return; + + if (name.equals("play")) { + // re-play active sounds + audioSource.play(); + } + } + + private void registerInputMappings() { + addMapping("play", new KeyTrigger(KeyInput.KEY_SPACE)); + } + + private void addMapping(String mappingName, Trigger... triggers) { + inputManager.addMapping(mappingName, triggers); + inputManager.addListener(this, mappingName); + } + +} diff --git a/jme3-examples/src/main/java/jme3test/audio/TestAudioDirectional.java b/jme3-examples/src/main/java/jme3test/audio/TestAudioDirectional.java new file mode 100644 index 0000000000..a996431655 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/audio/TestAudioDirectional.java @@ -0,0 +1,141 @@ +package jme3test.audio; + +import com.jme3.app.SimpleApplication; +import com.jme3.audio.AudioData; +import com.jme3.audio.AudioNode; +import com.jme3.environment.util.BoundingSphereDebug; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.input.controls.Trigger; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.FastMath; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.scene.debug.Arrow; +import com.jme3.scene.debug.Grid; +import com.jme3.scene.shape.Line; + +/** + * @author capdevon + */ +public class TestAudioDirectional extends SimpleApplication implements ActionListener { + + public static void main(String[] args) { + TestAudioDirectional app = new TestAudioDirectional(); + app.start(); + } + + private AudioNode audioSource; + private final Vector3f tempDirection = new Vector3f(); + private boolean rotationEnabled = true; + + @Override + public void simpleInitApp() { + configureCamera(); + + audioSource = new AudioNode(assetManager, + "Sound/Environment/Ocean Waves.ogg", AudioData.DataType.Buffer); + audioSource.setLooping(true); + audioSource.setPositional(true); + audioSource.setMaxDistance(100); + audioSource.setRefDistance(5); + audioSource.setDirectional(true); +// audioSource.setOuterGain(0.2f); // Volume outside the cone is 20% of the inner volume (Not Supported by jME) + audioSource.setInnerAngle(30); // 30-degree cone (15 degrees on each side of the direction) + audioSource.setOuterAngle(90); // 90-degree cone (45 degrees on each side of the direction) + audioSource.play(); + + // just a green sphere to mark the spot + Geometry sphere = BoundingSphereDebug.createDebugSphere(assetManager); + Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", ColorRGBA.Green); + sphere.setMaterial(mat); + sphere.setLocalScale(0.5f); + audioSource.attachChild(sphere); + + float angleIn = audioSource.getInnerAngle() * FastMath.DEG_TO_RAD; + float angleOut = audioSource.getOuterAngle() * FastMath.DEG_TO_RAD; + Vector3f forwardDir = audioSource.getWorldRotation().mult(Vector3f.UNIT_Z); + + audioSource.attachChild(createFOV(angleIn, 20f)); + audioSource.attachChild(createFOV(angleOut, 20f)); + audioSource.attachChild(makeShape("ZAxis", new Arrow(forwardDir.mult(5)), ColorRGBA.Blue)); + rootNode.attachChild(audioSource); + + Geometry grid = makeShape("DebugGrid", new Grid(21, 21, 2), ColorRGBA.Gray); + grid.center().move(0, 0, 0); + rootNode.attachChild(grid); + + registerInputMappings(); + } + + @Override + public void simpleUpdate(float tpf) { + if (rotationEnabled) { + // Example: Rotate the audio node + audioSource.rotate(0, tpf * 0.5f, 0); + audioSource.setDirection(audioSource.getWorldRotation().mult(Vector3f.UNIT_Z, tempDirection)); + } + } + + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (!isPressed) return; + + if (name.equals("toggleDirectional")) { + boolean directional = !audioSource.isDirectional(); + audioSource.setDirectional(directional); + System.out.println("directional: " + directional); + + } else if (name.equals("toggleRotationEnabled")) { + rotationEnabled = !rotationEnabled; + System.out.println("rotationEnabled: " + rotationEnabled); + } + } + + private void registerInputMappings() { + addMapping("toggleDirectional", new KeyTrigger(KeyInput.KEY_SPACE)); + addMapping("toggleRotationEnabled", new KeyTrigger(KeyInput.KEY_P)); + } + + private void addMapping(String mappingName, Trigger... triggers) { + inputManager.addMapping(mappingName, triggers); + inputManager.addListener(this, mappingName); + } + + private void configureCamera() { + flyCam.setMoveSpeed(25f); + flyCam.setDragToRotate(true); + + cam.setLocation(new Vector3f(12, 5, 12)); + cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y); + } + + private Geometry makeShape(String name, Mesh mesh, ColorRGBA color) { + Geometry geo = new Geometry(name, mesh); + Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", color); + geo.setMaterial(mat); + return geo; + } + + private Spatial createFOV(float angleRad, float extent) { + Vector3f origin = new Vector3f(); + Node node = new Node("Cone"); + Vector3f sx = dirFromAngle(angleRad/2).scaleAdd(extent, origin); + Vector3f dx = dirFromAngle(-angleRad/2).scaleAdd(extent, origin); + node.attachChild(makeShape("Line.SX", new Line(origin, sx), ColorRGBA.Red)); + node.attachChild(makeShape("Line.DX", new Line(origin, dx), ColorRGBA.Red)); + + return node; + } + + private Vector3f dirFromAngle(float angleRad) { + return new Vector3f(FastMath.sin(angleRad), 0, FastMath.cos(angleRad)); + } +} diff --git a/jme3-examples/src/main/java/jme3test/audio/TestDoppler.java b/jme3-examples/src/main/java/jme3test/audio/TestDoppler.java index 1065e1cee9..1e1731d2b8 100644 --- a/jme3-examples/src/main/java/jme3test/audio/TestDoppler.java +++ b/jme3-examples/src/main/java/jme3test/audio/TestDoppler.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 @@ -35,11 +35,16 @@ import com.jme3.app.SimpleApplication; import com.jme3.audio.AudioData; import com.jme3.audio.AudioNode; -import com.jme3.math.FastMath; +import com.jme3.font.BitmapText; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; import com.jme3.math.Vector3f; import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.debug.Grid; import com.jme3.scene.shape.Sphere; -import com.jme3.scene.shape.Torus; + +import java.util.Locale; /** * Test Doppler Effect @@ -49,23 +54,17 @@ public class TestDoppler extends SimpleApplication { private float pos = -5; private float vel = 5; private AudioNode ufoNode; + private BitmapText bmp; - public static void main(String[] args){ - TestDoppler test = new TestDoppler(); - test.start(); + public static void main(String[] args) { + TestDoppler app = new TestDoppler(); + app.start(); } @Override public void simpleInitApp() { - flyCam.setMoveSpeed(10); - - Torus torus = new Torus(10, 6, 1, 3); - Geometry g = new Geometry("Torus Geom", torus); - g.rotate(-FastMath.HALF_PI, 0, 0); - g.center(); - - g.setMaterial(assetManager.loadMaterial("Common/Materials/RedColor.j3m")); -// rootNode.attachChild(g); + configureCamera(); + bmp = createLabelText(10, 20, ""); ufoNode = new AudioNode(assetManager, "Sound/Effects/Beep.ogg", AudioData.DataType.Buffer); ufoNode.setLooping(true); @@ -73,22 +72,50 @@ public void simpleInitApp() { ufoNode.setRefDistance(1); ufoNode.setMaxDistance(100000000); ufoNode.setVelocityFromTranslation(true); - ufoNode.play(); + rootNode.attachChild(ufoNode); - Geometry ball = new Geometry("Beeper", new Sphere(10, 10, 0.1f)); - ball.setMaterial(assetManager.loadMaterial("Common/Materials/RedColor.j3m")); + Geometry ball = makeShape("Beeper", new Sphere(10, 10, .5f), ColorRGBA.Red); ufoNode.attachChild(ball); - rootNode.attachChild(ufoNode); + Geometry grid = makeShape("DebugGrid", new Grid(21, 21, 2), ColorRGBA.Gray); + grid.center().move(0, 0, 0); + rootNode.attachChild(grid); + + ufoNode.play(); } + private void configureCamera() { + flyCam.setMoveSpeed(15f); + flyCam.setDragToRotate(true); + + cam.setLocation(Vector3f.UNIT_XYZ.mult(12)); + cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y); + } @Override public void simpleUpdate(float tpf) { pos += tpf * vel; - if (pos < -10 || pos > 10) { + if (pos < -10f || pos > 10f) { vel *= -1; } - ufoNode.setLocalTranslation(new Vector3f(pos, 0, 0)); + ufoNode.setLocalTranslation(pos, 0f, 0f); + bmp.setText(String.format(Locale.ENGLISH, "Audio Position: (%.2f, %.1f, %.1f)", pos, 0f, 0f)); + } + + private BitmapText createLabelText(int x, int y, String text) { + BitmapText bmp = new BitmapText(guiFont); + bmp.setText(text); + bmp.setLocalTranslation(x, settings.getHeight() - y, 0); + bmp.setColor(ColorRGBA.Red); + guiNode.attachChild(bmp); + return bmp; + } + + private Geometry makeShape(String name, Mesh mesh, ColorRGBA color) { + Geometry geo = new Geometry(name, mesh); + Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", color); + geo.setMaterial(mat); + return geo; } } diff --git a/jme3-examples/src/main/java/jme3test/audio/TestOgg.java b/jme3-examples/src/main/java/jme3test/audio/TestOgg.java index 3e4099c660..55cd7bf452 100644 --- a/jme3-examples/src/main/java/jme3test/audio/TestOgg.java +++ b/jme3-examples/src/main/java/jme3test/audio/TestOgg.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 @@ -37,34 +37,170 @@ import com.jme3.audio.AudioNode; import com.jme3.audio.AudioSource; import com.jme3.audio.LowPassFilter; +import com.jme3.font.BitmapText; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.input.controls.Trigger; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.FastMath; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.debug.Grid; +import com.jme3.scene.shape.Sphere; -public class TestOgg extends SimpleApplication { +/** + * + * @author capdevon + */ +public class TestOgg extends SimpleApplication implements ActionListener { + private final StringBuilder sb = new StringBuilder(); + private int frameCount = 0; + private BitmapText bmp; private AudioNode audioSource; + private float volume = 1.0f; + private float pitch = 1.0f; - public static void main(String[] args){ + /** + * ### Filters ### + * Changing a parameter value in the Filter Object after it has been attached to a Source will not + * affect the Source. To update the filter(s) used on a Source, an application must update the + * parameters of a Filter object and then re-attach it to the Source. + */ + private final LowPassFilter dryFilter = new LowPassFilter(1f, .1f); + + public static void main(String[] args) { TestOgg test = new TestOgg(); test.start(); } @Override - public void simpleInitApp(){ - System.out.println("Playing without filter"); + public void simpleInitApp() { + configureCamera(); + + bmp = createLabelText(10, 20, ""); + + // just a blue sphere to mark the spot + Geometry marker = makeShape("Marker", new Sphere(16, 16, 1f), ColorRGBA.Blue); + rootNode.attachChild(marker); + + Geometry grid = makeShape("DebugGrid", new Grid(21, 21, 2), ColorRGBA.Gray); + grid.center().move(0, 0, 0); + rootNode.attachChild(grid); + audioSource = new AudioNode(assetManager, "Sound/Effects/Foot steps.ogg", DataType.Buffer); + audioSource.setName("Foot steps"); + audioSource.setLooping(true); + audioSource.setVolume(volume); + audioSource.setPitch(pitch); + audioSource.setMaxDistance(100); + audioSource.setRefDistance(5); audioSource.play(); + rootNode.attachChild(audioSource); + + registerInputMappings(); + } + + private void configureCamera() { + flyCam.setMoveSpeed(25f); + flyCam.setDragToRotate(true); + + cam.setLocation(Vector3f.UNIT_XYZ.mult(20f)); + cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y); + } + + @Override + public void simpleUpdate(float tpf) { + frameCount++; + if (frameCount % 10 == 0) { + frameCount = 0; + + sb.append("Audio: ").append(audioSource.getName()).append("\n"); + sb.append(audioSource.getAudioData()).append("\n"); + sb.append("Looping: ").append(audioSource.isLooping()).append("\n"); + sb.append("Volume: ").append(String.format("%.2f", audioSource.getVolume())).append("\n"); + sb.append("Pitch: ").append(String.format("%.2f", audioSource.getPitch())).append("\n"); + sb.append("Positional: ").append(audioSource.isPositional()).append("\n"); + sb.append("MaxDistance: ").append(audioSource.getMaxDistance()).append("\n"); + sb.append("RefDistance: ").append(audioSource.getRefDistance()).append("\n"); + sb.append("Status: ").append(audioSource.getStatus()).append("\n"); + sb.append("SourceId: ").append(audioSource.getChannel()).append("\n"); + sb.append("DryFilter: ").append(audioSource.getDryFilter() != null).append("\n"); + sb.append("FilterId: ").append(dryFilter.getId()).append("\n"); + + bmp.setText(sb.toString()); + sb.setLength(0); + } } @Override - public void simpleUpdate(float tpf){ - if (audioSource.getStatus() != AudioSource.Status.Playing){ - audioRenderer.deleteAudioData(audioSource.getAudioData()); - - System.out.println("Playing with low pass filter"); - audioSource = new AudioNode(assetManager, "Sound/Effects/Foot steps.ogg", DataType.Buffer); - audioSource.setDryFilter(new LowPassFilter(1f, .1f)); - audioSource.setVolume(3); - audioSource.play(); + public void onAction(String name, boolean isPressed, float tpf) { + if (!isPressed) return; + + if (name.equals("togglePlayPause")) { + if (audioSource.getStatus() != AudioSource.Status.Playing) { + audioSource.play(); + } else { + audioSource.stop(); + } + } else if (name.equals("togglePositional")) { + boolean positional = audioSource.isPositional(); + audioSource.setPositional(!positional); + + } else if (name.equals("dryFilter")) { + boolean hasFilter = audioSource.getDryFilter() != null; + audioSource.setDryFilter(hasFilter ? null : dryFilter); + + } else if (name.equals("Volume+")) { + volume = FastMath.clamp(volume + 0.1f, 0, 5f); + audioSource.setVolume(volume); + + } else if (name.equals("Volume-")) { + volume = FastMath.clamp(volume - 0.1f, 0, 5f); + audioSource.setVolume(volume); + + } else if (name.equals("Pitch+")) { + pitch = FastMath.clamp(pitch + 0.1f, 0.5f, 2f); + audioSource.setPitch(pitch); + + } else if (name.equals("Pitch-")) { + pitch = FastMath.clamp(pitch - 0.1f, 0.5f, 2f); + audioSource.setPitch(pitch); } } + private void registerInputMappings() { + addMapping("togglePlayPause", new KeyTrigger(KeyInput.KEY_P)); + addMapping("togglePositional", new KeyTrigger(KeyInput.KEY_RETURN)); + addMapping("dryFilter", new KeyTrigger(KeyInput.KEY_SPACE)); + addMapping("Volume+", new KeyTrigger(KeyInput.KEY_I)); + addMapping("Volume-", new KeyTrigger(KeyInput.KEY_K)); + addMapping("Pitch+", new KeyTrigger(KeyInput.KEY_J)); + addMapping("Pitch-", new KeyTrigger(KeyInput.KEY_L)); + } + + private void addMapping(String mappingName, Trigger... triggers) { + inputManager.addMapping(mappingName, triggers); + inputManager.addListener(this, mappingName); + } + + private BitmapText createLabelText(int x, int y, String text) { + BitmapText bmp = new BitmapText(guiFont); + bmp.setText(text); + bmp.setLocalTranslation(x, settings.getHeight() - y, 0); + bmp.setColor(ColorRGBA.Red); + guiNode.attachChild(bmp); + return bmp; + } + + private Geometry makeShape(String name, Mesh mesh, ColorRGBA color) { + Geometry geo = new Geometry(name, mesh); + Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", color); + geo.setMaterial(mat); + return geo; + } } diff --git a/jme3-examples/src/main/java/jme3test/audio/TestReverb.java b/jme3-examples/src/main/java/jme3test/audio/TestReverb.java index aff0a2d464..e21a870993 100644 --- a/jme3-examples/src/main/java/jme3test/audio/TestReverb.java +++ b/jme3-examples/src/main/java/jme3test/audio/TestReverb.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 @@ -35,50 +35,143 @@ import com.jme3.audio.AudioData; import com.jme3.audio.AudioNode; import com.jme3.audio.Environment; +import com.jme3.audio.LowPassFilter; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.input.controls.Trigger; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; import com.jme3.math.FastMath; import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.debug.Grid; +import com.jme3.scene.shape.Sphere; -public class TestReverb extends SimpleApplication { - - private AudioNode audioSource; - private float time = 0; - private float nextTime = 1; - - public static void main(String[] args) { - TestReverb test = new TestReverb(); - test.start(); - } - - @Override - public void simpleInitApp() { - audioSource = new AudioNode(assetManager, "Sound/Effects/Bang.wav", - AudioData.DataType.Buffer); - - float[] eax = new float[]{15, 38.0f, 0.300f, -1000, -3300, 0, - 1.49f, 0.54f, 1.00f, -2560, 0.162f, 0.00f, 0.00f, 0.00f, - -229, 0.088f, 0.00f, 0.00f, 0.00f, 0.125f, 1.000f, 0.250f, - 0.000f, -5.0f, 5000.0f, 250.0f, 0.00f, 0x3f}; - audioRenderer.setEnvironment(new Environment(eax)); - Environment env = Environment.Cavern; - audioRenderer.setEnvironment(env); - } - - @Override - public void simpleUpdate(float tpf) { - time += tpf; - - if (time > nextTime) { - Vector3f v = new Vector3f(); - v.setX(FastMath.nextRandomFloat()); - v.setY(FastMath.nextRandomFloat()); - v.setZ(FastMath.nextRandomFloat()); - v.multLocal(40, 2, 40); - v.subtractLocal(20, 1, 20); - - audioSource.setLocalTranslation(v); - audioSource.playInstance(); - time = 0; - nextTime = FastMath.nextRandomFloat() * 2 + 0.5f; +/** + * @author capdevon + */ +public class TestReverb extends SimpleApplication implements ActionListener { + + public static void main(String[] args) { + TestReverb app = new TestReverb(); + app.start(); + } + + private AudioNode audioSource; + private float time = 0; + private float nextTime = 1; + + /** + * ### Effects ### + * Changing a parameter value in the Effect Object after it has been attached to the Auxiliary Effect + * Slot will not affect the effect in the effect slot. To update the parameters of the effect in the effect + * slot, an application must update the parameters of an Effect object and then re-attach it to the + * Auxiliary Effect Slot. + */ + private int index = 0; + private final Environment[] environments = { + Environment.Cavern, + Environment.AcousticLab, + Environment.Closet, + Environment.Dungeon, + Environment.Garage + }; + + @Override + public void simpleInitApp() { + + configureCamera(); + + // Activate the Environment preset + audioRenderer.setEnvironment(environments[index]); + + // Activate 3D audio + audioSource = new AudioNode(assetManager, "Sound/Effects/Bang.wav", AudioData.DataType.Buffer); + audioSource.setLooping(false); + audioSource.setVolume(1.2f); + audioSource.setPositional(true); + audioSource.setMaxDistance(100); + audioSource.setRefDistance(5); + audioSource.setReverbEnabled(true); + audioSource.setReverbFilter(new LowPassFilter(1f, 1f)); + rootNode.attachChild(audioSource); + + Geometry marker = makeShape("Marker", new Sphere(16, 16, 1f), ColorRGBA.Red); + audioSource.attachChild(marker); + + Geometry grid = makeShape("DebugGrid", new Grid(21, 21, 4), ColorRGBA.Blue); + grid.center().move(0, 0, 0); + rootNode.attachChild(grid); + + registerInputMappings(); + } + + private void configureCamera() { + flyCam.setMoveSpeed(50f); + flyCam.setDragToRotate(true); + + cam.setLocation(Vector3f.UNIT_XYZ.mult(50f)); + cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y); + } + + private Geometry makeShape(String name, Mesh mesh, ColorRGBA color) { + Geometry geo = new Geometry(name, mesh); + Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", color); + geo.setMaterial(mat); + return geo; + } + + @Override + public void simpleUpdate(float tpf) { + time += tpf; + + if (time > nextTime) { + time = 0; + nextTime = FastMath.nextRandomFloat() * 2 + 0.5f; + + Vector3f position = getRandomPosition(); + audioSource.setLocalTranslation(position); + audioSource.playInstance(); + } + } + + private Vector3f getRandomPosition() { + float x = FastMath.nextRandomFloat(); + float y = FastMath.nextRandomFloat(); + float z = FastMath.nextRandomFloat(); + Vector3f vec = new Vector3f(x, y, z); + vec.multLocal(40, 2, 40); + vec.subtractLocal(20, 1, 20); + return vec; + } + + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (!isPressed) return; + + if (name.equals("toggleReverbEnabled")) { + boolean reverbEnabled = !audioSource.isReverbEnabled(); + audioSource.setReverbEnabled(reverbEnabled); + System.out.println("reverbEnabled: " + reverbEnabled); + + } else if (name.equals("nextEnvironment")) { + index = (index + 1) % environments.length; + audioRenderer.setEnvironment(environments[index]); + System.out.println("Next Environment Index: " + index); + } + } + + private void registerInputMappings() { + addMapping("toggleReverbEnabled", new KeyTrigger(KeyInput.KEY_SPACE)); + addMapping("nextEnvironment", new KeyTrigger(KeyInput.KEY_N)); + } + + private void addMapping(String mappingName, Trigger... triggers) { + inputManager.addMapping(mappingName, triggers); + inputManager.addListener(this, mappingName); } - } + } diff --git a/jme3-examples/src/main/java/jme3test/audio/TestWav.java b/jme3-examples/src/main/java/jme3test/audio/TestWav.java index 70ec211f23..a24b2f24d5 100644 --- a/jme3-examples/src/main/java/jme3test/audio/TestWav.java +++ b/jme3-examples/src/main/java/jme3test/audio/TestWav.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 @@ -33,32 +33,107 @@ import com.jme3.app.SimpleApplication; import com.jme3.audio.AudioData; +import com.jme3.audio.AudioKey; import com.jme3.audio.AudioNode; +/** + * @author capdevon + */ public class TestWav extends SimpleApplication { - private float time = 0; - private AudioNode audioSource; + private float time = 0; + private AudioNode audioSource; + + public static void main(String[] args) { + TestWav app = new TestWav(); + app.start(); + } - public static void main(String[] args) { - TestWav test = new TestWav(); - test.start(); - } + @Override + public void simpleInitApp() { + testMaxNumChannels(); + testFakeAudio(); + testPlaySourceInstance(); - @Override - public void simpleUpdate(float tpf) { - time += tpf; - if (time > 1f) { - audioSource.playInstance(); - time = 0; + audioSource = createAudioNode("Sound/Effects/Gun.wav", AudioData.DataType.Buffer); + audioSource.setName("Gun"); + audioSource.setPositional(true); } - } + @Override + public void simpleUpdate(float tpf) { + time += tpf; + if (time > 1f) { + audioSource.playInstance(); + time = 0; + } + } + + /** + * Creates an {@link AudioNode} for the specified audio file. + * This method demonstrates an alternative way to defer the creation + * of an AudioNode by explicitly creating and potentially pre-loading + * the {@link AudioData} and {@link AudioKey} before instantiating + * the AudioNode. This can be useful in scenarios where you want more + * control over the asset loading process or when the AudioData and + * AudioKey are already available. + * + * @param filepath The path to the audio file. + * @param type The desired {@link AudioData.DataType} for the audio. + * @return A new {@code AudioNode} configured with the loaded audio data. + */ + private AudioNode createAudioNode(String filepath, AudioData.DataType type) { + boolean stream = (type == AudioData.DataType.Stream); + boolean streamCache = true; + AudioKey audioKey = new AudioKey(filepath, stream, streamCache); + AudioData data = assetManager.loadAsset(audioKey); + + AudioNode audio = new AudioNode(); + audio.setAudioData(data, audioKey); + return audio; + } + + /** + * WARNING: No channel available to play instance of AudioNode[status=Stopped, vol=0.1] + */ + private void testMaxNumChannels() { + final int MAX_NUM_CHANNELS = 64; + for (int i = 0; i < MAX_NUM_CHANNELS + 1; i++) { + AudioNode audio = createAudioNode("Sound/Effects/Gun.wav", AudioData.DataType.Buffer); + audio.setVolume(0.1f); + audio.playInstance(); + } + } + + /** + * java.lang.UnsupportedOperationException: Cannot play instances of audio streams. Use play() instead. + * at com.jme3.audio.openal.ALAudioRenderer.playSourceInstance() + */ + private void testPlaySourceInstance() { + try { + AudioNode nature = new AudioNode(assetManager, + "Sound/Environment/Nature.ogg", AudioData.DataType.Stream); + audioRenderer.playSourceInstance(nature); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void testFakeAudio() { + /** + * Tests AudioRenderer.playSource() with an + * AudioNode lacking AudioData to observe its handling (typically discard). + */ + AudioNode fakeAudio = new AudioNode() { + @Override + public String toString() { + // includes node name for easier identification in log messages. + return getName() + " (" + AudioNode.class.getSimpleName() + ")"; + } + }; + fakeAudio.setName("FakeAudio"); + audioRenderer.playSource(fakeAudio); + audioRenderer.playSourceInstance(fakeAudio); + } - @Override - public void simpleInitApp() { - audioSource = new AudioNode(assetManager, "Sound/Effects/Gun.wav", - AudioData.DataType.Buffer); - audioSource.setLooping(false); - } } diff --git a/jme3-examples/src/main/java/jme3test/math/TestRandomPoints.java b/jme3-examples/src/main/java/jme3test/math/TestRandomPoints.java new file mode 100644 index 0000000000..ef264b95d1 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/math/TestRandomPoints.java @@ -0,0 +1,84 @@ +package jme3test.math; + +import com.jme3.app.SimpleApplication; +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.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.debug.Arrow; +import com.jme3.scene.debug.Grid; +import com.jme3.scene.debug.WireSphere; +import com.jme3.scene.shape.Sphere; + +/** + * @author capdevon + */ +public class TestRandomPoints extends SimpleApplication { + + public static void main(String[] args) { + TestRandomPoints app = new TestRandomPoints(); + app.start(); + } + + private float radius = 5; + + @Override + public void simpleInitApp() { + configureCamera(); + viewPort.setBackgroundColor(ColorRGBA.DarkGray); + + Geometry grid = makeShape("DebugGrid", new Grid(21, 21, 2), ColorRGBA.LightGray); + grid.center().move(0, 0, 0); + rootNode.attachChild(grid); + + Geometry bsphere = makeShape("BoundingSphere", new WireSphere(radius), ColorRGBA.Red); + rootNode.attachChild(bsphere); + + for (int i = 0; i < 100; i++) { + Vector2f v = FastMath.insideUnitCircle().multLocal(radius); + Arrow arrow = new Arrow(Vector3f.UNIT_Y.negate()); + Geometry geo = makeShape("Arrow." + i, arrow, ColorRGBA.Green); + geo.setLocalTranslation(new Vector3f(v.x, 0, v.y)); + rootNode.attachChild(geo); + } + + for (int i = 0; i < 100; i++) { + Vector3f v = FastMath.insideUnitSphere().multLocal(radius); + Geometry geo = makeShape("Sphere." + i, new Sphere(16, 16, 0.05f), ColorRGBA.Blue); + geo.setLocalTranslation(v); + rootNode.attachChild(geo); + } + + for (int i = 0; i < 100; i++) { + Vector3f v = FastMath.onUnitSphere().multLocal(radius); + Geometry geo = makeShape("Sphere." + i, new Sphere(16, 16, 0.06f), ColorRGBA.Cyan); + geo.setLocalTranslation(v); + rootNode.attachChild(geo); + } + + for (int i = 0; i < 100; i++) { + float value = FastMath.nextRandomFloat(-5, 5); + System.out.println(value); + } + } + + private void configureCamera() { + flyCam.setMoveSpeed(15f); + flyCam.setDragToRotate(true); + + cam.setLocation(Vector3f.UNIT_XYZ.mult(12)); + cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y); + } + + private Geometry makeShape(String name, Mesh mesh, ColorRGBA color) { + Geometry geo = new Geometry(name, mesh); + Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", color); + geo.setMaterial(mat); + return geo; + } + +} diff --git a/jme3-examples/src/main/java/jme3test/scene/instancing/TestInstanceNodeWithPbr.java b/jme3-examples/src/main/java/jme3test/scene/instancing/TestInstanceNodeWithPbr.java new file mode 100644 index 0000000000..0e2c8894f4 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/scene/instancing/TestInstanceNodeWithPbr.java @@ -0,0 +1,92 @@ +package jme3test.scene.instancing; + +import java.util.Locale; + +import com.jme3.app.SimpleApplication; +import com.jme3.font.BitmapText; +import com.jme3.light.DirectionalLight; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.instancing.InstancedNode; +import com.jme3.scene.shape.Box; + +/** + * This test specifically validates the corrected PBR rendering when combined + * with instancing, as addressed in issue #2435. + * + * It creates an InstancedNode + * with a PBR-materialized Box to ensure the fix in PBRLighting.vert correctly + * handles world position calculations for instanced geometry. + */ +public class TestInstanceNodeWithPbr extends SimpleApplication { + + public static void main(String[] args) { + TestInstanceNodeWithPbr app = new TestInstanceNodeWithPbr(); + app.start(); + } + + private BitmapText bmp; + private Geometry box; + private float pos = -5; + private float vel = 5; + + @Override + public void simpleInitApp() { + configureCamera(); + bmp = createLabelText(10, 20, ""); + + InstancedNode instancedNode = new InstancedNode("InstancedNode"); + rootNode.attachChild(instancedNode); + + Box mesh = new Box(0.5f, 0.5f, 0.5f); + box = new Geometry("Box", mesh); + Material pbrMaterial = createPbrMaterial(ColorRGBA.Red); + box.setMaterial(pbrMaterial); + + instancedNode.attachChild(box); + instancedNode.instance(); + + DirectionalLight light = new DirectionalLight(); + light.setDirection(new Vector3f(-1, -2, -3).normalizeLocal()); + rootNode.addLight(light); + } + + private Material createPbrMaterial(ColorRGBA color) { + Material mat = new Material(assetManager, "Common/MatDefs/Light/PBRLighting.j3md"); + mat.setColor("BaseColor", color); + mat.setFloat("Roughness", 0.8f); + mat.setFloat("Metallic", 0.1f); + mat.setBoolean("UseInstancing", true); + return mat; + } + + private void configureCamera() { + flyCam.setMoveSpeed(15f); + flyCam.setDragToRotate(true); + + cam.setLocation(Vector3f.UNIT_XYZ.mult(12)); + cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y); + } + + private BitmapText createLabelText(int x, int y, String text) { + BitmapText bmp = new BitmapText(guiFont); + bmp.setText(text); + bmp.setLocalTranslation(x, settings.getHeight() - y, 0); + bmp.setColor(ColorRGBA.Red); + guiNode.attachChild(bmp); + return bmp; + } + + @Override + public void simpleUpdate(float tpf) { + pos += tpf * vel; + if (pos < -10f || pos > 10f) { + vel *= -1; + } + box.setLocalTranslation(pos, 0f, 0f); + bmp.setText(String.format(Locale.ENGLISH, "BoxPosition: (%.2f, %.1f, %.1f)", pos, 0f, 0f)); + } + +} diff --git a/jme3-examples/src/main/java/jme3test/terrain/PBRTerrainAdvancedTest.java b/jme3-examples/src/main/java/jme3test/terrain/PBRTerrainAdvancedTest.java index 6127c279c0..d4cd04e403 100644 --- a/jme3-examples/src/main/java/jme3test/terrain/PBRTerrainAdvancedTest.java +++ b/jme3-examples/src/main/java/jme3test/terrain/PBRTerrainAdvancedTest.java @@ -189,6 +189,20 @@ public void onAction(String name, boolean pressed, float tpf) { isNight = !isNight; // Ambient and directional light are faded smoothly in update loop below. } + + if(name.length() == 1 && !pressed){ + if(name.equals("-")){ + matTerrain.setInt("DebugValuesMode", -1); + }else{ + try{ + int debugValueMode = Integer.parseInt(name); + matTerrain.setInt("DebugValuesMode", debugValueMode); + } + catch(Exception e){ + + } + } + } } }; @@ -364,16 +378,51 @@ private void setupKeys() { inputManager.addMapping("triPlanar", new KeyTrigger(KeyInput.KEY_P)); inputManager.addMapping("toggleNight", new KeyTrigger(KeyInput.KEY_N)); + inputManager.addMapping("0", new KeyTrigger(KeyInput.KEY_0)); // toggleDebugModeForAlbedo + inputManager.addMapping("1", new KeyTrigger(KeyInput.KEY_1)); // toggleDebugModeForNormalMap + inputManager.addMapping("2", new KeyTrigger(KeyInput.KEY_2)); // toggleDebugModeForRoughness + inputManager.addMapping("3", new KeyTrigger(KeyInput.KEY_3)); // toggleDebugModeForMetallic + inputManager.addMapping("4", new KeyTrigger(KeyInput.KEY_4)); // toggleDebugModeForAo + inputManager.addMapping("5", new KeyTrigger(KeyInput.KEY_5)); // toggleDebugModeForEmissive + inputManager.addMapping("6", new KeyTrigger(KeyInput.KEY_6)); // toggleDebugModeForExposure + inputManager.addMapping("7", new KeyTrigger(KeyInput.KEY_7)); // toggleDebugModeForAlpha + inputManager.addMapping("8", new KeyTrigger(KeyInput.KEY_8)); // toggleDebugModeForGeometryNormals + + inputManager.addMapping("-", new KeyTrigger(KeyInput.KEY_MINUS)); // - key will disable dbug mode + inputManager.addListener(actionListener, "triPlanar"); inputManager.addListener(actionListener, "toggleNight"); + inputManager.addListener(actionListener, "0"); + inputManager.addListener(actionListener, "1"); + inputManager.addListener(actionListener, "2"); + inputManager.addListener(actionListener, "3"); + inputManager.addListener(actionListener, "4"); + inputManager.addListener(actionListener, "5"); + inputManager.addListener(actionListener, "6"); + inputManager.addListener(actionListener, "7"); + inputManager.addListener(actionListener, "8"); + inputManager.addListener(actionListener, "-"); + keybindingsText = new BitmapText(assetManager.loadFont("Interface/Fonts/Default.fnt")); - keybindingsText.setText("Press 'N' to toggle day/night fade (takes a moment) \nPress 'P' to toggle tri-planar mode"); + keybindingsText.setText("Press N to toggle day/night fade (takes a moment) \n" + + "Press P to toggle tri-planar mode\n\n" + + "Press - for Final Render (disable debug view)\n" + + "Press 0 for Albedo debug view\n" + + "Press 1 for Normal Map debug view\n" + + "Press 2 for Roughness debug view\n" + + "Press 3 for Metallic debug view\n" + + "Press 4 for Ambient Occlusion (ao) debug view\n" + + "Press 5 for Emissive debug view\n" + + "Press 6 for Exposure debug view\n" + + "Press 7 for Alpha debug view\n" + + "Press 8 for Geoemtry Normals debug view\n"); + getGuiNode().attachChild(keybindingsText); keybindingsText.move(new Vector3f(200, 120, 0)); + keybindingsText.move(new Vector3f(5, cam.getHeight() * 0.995f, 0)); } - @Override public void simpleUpdate(float tpf) { super.simpleUpdate(tpf); diff --git a/jme3-examples/src/main/java/jme3test/terrain/PBRTerrainTest.java b/jme3-examples/src/main/java/jme3test/terrain/PBRTerrainTest.java index 1006fbd5f4..1500257612 100644 --- a/jme3-examples/src/main/java/jme3test/terrain/PBRTerrainTest.java +++ b/jme3-examples/src/main/java/jme3test/terrain/PBRTerrainTest.java @@ -153,6 +153,22 @@ public void onAction(String name, boolean pressed, float tpf) { isNight = !isNight; // Ambient and directional light are faded smoothly in update loop below. } + + if(name.length() == 1 && !pressed){ + if(name.equals("-")){ + matTerrain.setInt("DebugValuesMode", -1); + }else{ + try{ + int debugValueMode = Integer.parseInt(name); + matTerrain.setInt("DebugValuesMode", debugValueMode); + } + catch(Exception e){ + + } + } + + } + } }; @@ -270,14 +286,49 @@ private void setupKeys() { inputManager.addMapping("triPlanar", new KeyTrigger(KeyInput.KEY_P)); inputManager.addMapping("toggleNight", new KeyTrigger(KeyInput.KEY_N)); + inputManager.addMapping("0", new KeyTrigger(KeyInput.KEY_0)); // toggleDebugModeForAlbedo + inputManager.addMapping("1", new KeyTrigger(KeyInput.KEY_1)); // toggleDebugModeForNormalMap + inputManager.addMapping("2", new KeyTrigger(KeyInput.KEY_2)); // toggleDebugModeForRoughness + inputManager.addMapping("3", new KeyTrigger(KeyInput.KEY_3)); // toggleDebugModeForMetallic + inputManager.addMapping("4", new KeyTrigger(KeyInput.KEY_4)); // toggleDebugModeForAo + inputManager.addMapping("5", new KeyTrigger(KeyInput.KEY_5)); // toggleDebugModeForEmissive + inputManager.addMapping("6", new KeyTrigger(KeyInput.KEY_6)); // toggleDebugModeForExposure + inputManager.addMapping("7", new KeyTrigger(KeyInput.KEY_7)); // toggleDebugModeForAlpha + inputManager.addMapping("8", new KeyTrigger(KeyInput.KEY_8)); // toggleDebugModeForGeometryNormals + + inputManager.addMapping("-", new KeyTrigger(KeyInput.KEY_MINUS)); // - key will disable dbug mode + inputManager.addListener(actionListener, "triPlanar"); inputManager.addListener(actionListener, "toggleNight"); + inputManager.addListener(actionListener, "0"); + inputManager.addListener(actionListener, "1"); + inputManager.addListener(actionListener, "2"); + inputManager.addListener(actionListener, "3"); + inputManager.addListener(actionListener, "4"); + inputManager.addListener(actionListener, "5"); + inputManager.addListener(actionListener, "6"); + inputManager.addListener(actionListener, "7"); + inputManager.addListener(actionListener, "8"); + inputManager.addListener(actionListener, "-"); + keybindingsText = new BitmapText(assetManager.loadFont("Interface/Fonts/Default.fnt")); - keybindingsText.setText("Press 'N' to toggle day/night fade (takes a moment) \nPress 'P' to toggle tri-planar mode"); + keybindingsText.setText("Press N to toggle day/night fade (takes a moment) \n" + + "Press P to toggle tri-planar mode\n\n" + + "Press - for Final Render (disable debug view)\n" + + "Press 0 for Albedo debug view\n" + + "Press 1 for Normal Map debug view\n" + + "Press 2 for Roughness debug view\n" + + "Press 3 for Metallic debug view\n" + + "Press 4 for Ambient Occlusion (ao) debug view\n" + + "Press 5 for Emissive debug view\n" + + "Press 6 for Exposure debug view\n" + + "Press 7 for Alpha debug view\n" + + "Press 8 for Geoemtry Normals debug view\n"); + getGuiNode().attachChild(keybindingsText); - keybindingsText.move(new Vector3f(200, 120, 0)); + keybindingsText.move(new Vector3f(5, cam.getHeight() * 0.995f, 0)); } @Override diff --git a/jme3-examples/src/main/java/jme3test/water/TestPostWater.java b/jme3-examples/src/main/java/jme3test/water/TestPostWater.java index 384d88f7f9..7f6117bc0c 100644 --- a/jme3-examples/src/main/java/jme3test/water/TestPostWater.java +++ b/jme3-examples/src/main/java/jme3test/water/TestPostWater.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 @@ -40,6 +40,7 @@ import com.jme3.input.KeyInput; import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.KeyTrigger; +import com.jme3.input.controls.Trigger; import com.jme3.light.AmbientLight; import com.jme3.light.DirectionalLight; import com.jme3.material.Material; @@ -55,7 +56,9 @@ import com.jme3.renderer.queue.RenderQueue.ShadowMode; import com.jme3.scene.Node; import com.jme3.scene.Spatial; +import com.jme3.terrain.geomipmap.TerrainLodControl; import com.jme3.terrain.geomipmap.TerrainQuad; +import com.jme3.terrain.geomipmap.lodcalc.DistanceLodCalculator; import com.jme3.terrain.heightmap.AbstractHeightMap; import com.jme3.terrain.heightmap.ImageBasedHeightMap; import com.jme3.texture.Texture; @@ -66,18 +69,12 @@ import com.jme3.water.WaterFilter; /** - * test - * * @author normenhansen */ public class TestPostWater extends SimpleApplication { - final private Vector3f lightDir = new Vector3f(-4.9236743f, -1.27054665f, 5.896916f); + private final Vector3f lightDir = new Vector3f(-4.9236743f, -1.27054665f, 5.896916f); private WaterFilter water; - private AudioNode waves; - final private LowPassFilter aboveWaterAudioFilter = new LowPassFilter(1, 1); - final private Filter underWaterAudioFilter = new LowPassFilter(0.5f, 0.1f); - private boolean useDryFilter = true; public static void main(String[] args) { TestPostWater app = new TestPostWater(); @@ -87,173 +84,154 @@ public static void main(String[] args) { @Override public void simpleInitApp() { - setDisplayFps(false); - setDisplayStatView(false); - Node mainScene = new Node("Main Scene"); rootNode.attachChild(mainScene); + configureCamera(); + createSky(mainScene); createTerrain(mainScene); + createLights(mainScene); + createWaterFilter(); + setupPostFilters(); + addAudioClip(); + setupUI(); + registerInputMappings(); + } + + private void configureCamera() { + flyCam.setMoveSpeed(50f); + cam.setLocation(new Vector3f(-370.31592f, 182.04016f, 196.81192f)); + cam.setRotation(new Quaternion(0.015302252f, 0.9304095f, -0.039101653f, 0.3641086f)); + cam.setFrustumFar(2000); + } + + private void createLights(Node mainScene) { DirectionalLight sun = new DirectionalLight(); sun.setDirection(lightDir); - sun.setColor(ColorRGBA.White.clone().multLocal(1f)); mainScene.addLight(sun); - + AmbientLight al = new AmbientLight(); al.setColor(new ColorRGBA(0.1f, 0.1f, 0.1f, 1.0f)); mainScene.addLight(al); - - flyCam.setMoveSpeed(50); + } - //cam.setLocation(new Vector3f(-700, 100, 300)); - //cam.setRotation(new Quaternion().fromAngleAxis(0.5f, Vector3f.UNIT_Z)); -// cam.setLocation(new Vector3f(-327.21957f, 61.6459f, 126.884346f)); -// cam.setRotation(new Quaternion(0.052168474f, 0.9443102f, -0.18395276f, 0.2678024f)); + private void createSky(Node mainScene) { + Spatial sky = SkyFactory.createSky(assetManager, + "Scenes/Beach/FullskiesSunset0068.dds", EnvMapType.CubeMap); + sky.setShadowMode(ShadowMode.Off); + mainScene.attachChild(sky); + } + private void setupUI() { + setText(0, 50, "1 - Set Foam Texture to Foam.jpg"); + setText(0, 80, "2 - Set Foam Texture to Foam2.jpg"); + setText(0, 110, "3 - Set Foam Texture to Foam3.jpg"); + setText(0, 140, "4 - Turn Dry Filter under water On/Off"); + setText(0, 240, "PgUp - Larger Reflection Map"); + setText(0, 270, "PgDn - Smaller Reflection Map"); + } - cam.setLocation(new Vector3f(-370.31592f, 182.04016f, 196.81192f)); - cam.setRotation(new Quaternion(0.015302252f, 0.9304095f, -0.039101653f, 0.3641086f)); + private void setText(int x, int y, String text) { + BitmapText bmp = new BitmapText(guiFont); + bmp.setText(text); + bmp.setLocalTranslation(x, cam.getHeight() - y, 0); + bmp.setColor(ColorRGBA.Red); + guiNode.attachChild(bmp); + } + private void registerInputMappings() { + addMapping("foam1", new KeyTrigger(KeyInput.KEY_1)); + addMapping("foam2", new KeyTrigger(KeyInput.KEY_2)); + addMapping("foam3", new KeyTrigger(KeyInput.KEY_3)); + addMapping("dryFilter", new KeyTrigger(KeyInput.KEY_4)); + addMapping("upRM", new KeyTrigger(KeyInput.KEY_PGUP)); + addMapping("downRM", new KeyTrigger(KeyInput.KEY_PGDN)); + } + private void addMapping(String mappingName, Trigger... triggers) { + inputManager.addMapping(mappingName, triggers); + inputManager.addListener(actionListener, mappingName); + } + private final ActionListener actionListener = new ActionListener() { + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (!isPressed) return; - Spatial sky = SkyFactory.createSky(assetManager, - "Scenes/Beach/FullskiesSunset0068.dds", EnvMapType.CubeMap); - sky.setLocalScale(350); + if (name.equals("foam1")) { + water.setFoamTexture((Texture2D) assetManager.loadTexture("Common/MatDefs/Water/Textures/foam.jpg")); - mainScene.attachChild(sky); - cam.setFrustumFar(4000); + } else if (name.equals("foam2")) { + water.setFoamTexture((Texture2D) assetManager.loadTexture("Common/MatDefs/Water/Textures/foam2.jpg")); - //Water Filter - water = new WaterFilter(rootNode, lightDir); - water.setWaterColor(new ColorRGBA().setAsSrgb(0.0078f, 0.3176f, 0.5f, 1.0f)); - water.setDeepWaterColor(new ColorRGBA().setAsSrgb(0.0039f, 0.00196f, 0.145f, 1.0f)); - water.setUnderWaterFogDistance(80); - water.setWaterTransparency(0.12f); - water.setFoamIntensity(0.4f); - water.setFoamHardness(0.3f); - water.setFoamExistence(new Vector3f(0.8f, 8f, 1f)); - water.setReflectionDisplace(50); - water.setRefractionConstant(0.25f); - water.setColorExtinction(new Vector3f(30, 50, 70)); - water.setCausticsIntensity(0.4f); - water.setWaveScale(0.003f); - water.setMaxAmplitude(2f); - water.setFoamTexture((Texture2D) assetManager.loadTexture("Common/MatDefs/Water/Textures/foam2.jpg")); - water.setRefractionStrength(0.2f); - water.setWaterHeight(initialWaterHeight); - - //Bloom Filter - BloomFilter bloom = new BloomFilter(); + } else if (name.equals("foam3")) { + water.setFoamTexture((Texture2D) assetManager.loadTexture("Common/MatDefs/Water/Textures/foam3.jpg")); + + } else if (name.equals("upRM")) { + water.setReflectionMapSize(Math.min(water.getReflectionMapSize() * 2, 4096)); + System.out.println("Reflection map size : " + water.getReflectionMapSize()); + + } else if (name.equals("downRM")) { + water.setReflectionMapSize(Math.max(water.getReflectionMapSize() / 2, 32)); + System.out.println("Reflection map size : " + water.getReflectionMapSize()); + + } else if (name.equals("dryFilter")) { + useDryFilter = !useDryFilter; + } + } + }; + + private void setupPostFilters() { + BloomFilter bloom = new BloomFilter(); bloom.setExposurePower(55); bloom.setBloomIntensity(1.0f); - - //Light Scattering Filter + LightScatteringFilter lsf = new LightScatteringFilter(lightDir.mult(-300)); - lsf.setLightDensity(0.5f); - - //Depth of field Filter + lsf.setLightDensity(0.5f); + DepthOfFieldFilter dof = new DepthOfFieldFilter(); dof.setFocusDistance(0); dof.setFocusRange(100); - + FilterPostProcessor fpp = new FilterPostProcessor(assetManager); - fpp.addFilter(water); fpp.addFilter(bloom); fpp.addFilter(dof); fpp.addFilter(lsf); fpp.addFilter(new FXAAFilter()); - -// fpp.addFilter(new TranslucentBucketFilter()); + int numSamples = getContext().getSettings().getSamples(); if (numSamples > 0) { fpp.setNumSamples(numSamples); } - - - uw = cam.getLocation().y < waterHeight; - - waves = new AudioNode(assetManager, "Sound/Environment/Ocean Waves.ogg", - DataType.Buffer); - waves.setLooping(true); - updateAudio(); - audioRenderer.playSource(waves); - // viewPort.addProcessor(fpp); + } - setText(0, 50, "1 - Set Foam Texture to Foam.jpg"); - setText(0, 80, "2 - Set Foam Texture to Foam2.jpg"); - setText(0, 110, "3 - Set Foam Texture to Foam3.jpg"); - setText(0, 140, "4 - Turn Dry Filter under water On/Off"); - setText(0, 240, "PgUp - Larger Reflection Map"); - setText(0, 270, "PgDn - Smaller Reflection Map"); - - inputManager.addListener(new ActionListener() { - @Override - public void onAction(String name, boolean isPressed, float tpf) { - if (isPressed) { - if (name.equals("foam1")) { - water.setFoamTexture((Texture2D) assetManager.loadTexture("Common/MatDefs/Water/Textures/foam.jpg")); - } - if (name.equals("foam2")) { - water.setFoamTexture((Texture2D) assetManager.loadTexture("Common/MatDefs/Water/Textures/foam2.jpg")); - } - if (name.equals("foam3")) { - water.setFoamTexture((Texture2D) assetManager.loadTexture("Common/MatDefs/Water/Textures/foam3.jpg")); - } - - if (name.equals("upRM")) { - water.setReflectionMapSize(Math.min(water.getReflectionMapSize() * 2, 4096)); - System.out.println("Reflection map size : " + water.getReflectionMapSize()); - } - if (name.equals("downRM")) { - water.setReflectionMapSize(Math.max(water.getReflectionMapSize() / 2, 32)); - System.out.println("Reflection map size : " + water.getReflectionMapSize()); - } - if (name.equals("dryFilter")) { - useDryFilter = !useDryFilter; - } - } - } - }, "foam1", "foam2", "foam3", "upRM", "downRM", "dryFilter"); - inputManager.addMapping("foam1", new KeyTrigger(KeyInput.KEY_1)); - inputManager.addMapping("foam2", new KeyTrigger(KeyInput.KEY_2)); - inputManager.addMapping("foam3", new KeyTrigger(KeyInput.KEY_3)); - inputManager.addMapping("dryFilter", new KeyTrigger(KeyInput.KEY_4)); - inputManager.addMapping("upRM", new KeyTrigger(KeyInput.KEY_PGUP)); - inputManager.addMapping("downRM", new KeyTrigger(KeyInput.KEY_PGDN)); + private void createWaterFilter() { + //Water Filter + water = new WaterFilter(rootNode, lightDir); + water.setWaterColor(new ColorRGBA().setAsSrgb(0.0078f, 0.3176f, 0.5f, 1.0f)); + water.setDeepWaterColor(new ColorRGBA().setAsSrgb(0.0039f, 0.00196f, 0.145f, 1.0f)); + water.setUnderWaterFogDistance(80); + water.setWaterTransparency(0.12f); + water.setFoamIntensity(0.4f); + water.setFoamHardness(0.3f); + water.setFoamExistence(new Vector3f(0.8f, 8f, 1f)); + water.setReflectionDisplace(50); + water.setRefractionConstant(0.25f); + water.setColorExtinction(new Vector3f(30, 50, 70)); + water.setCausticsIntensity(0.4f); + water.setWaveScale(0.003f); + water.setMaxAmplitude(2f); + water.setFoamTexture((Texture2D) assetManager.loadTexture("Common/MatDefs/Water/Textures/foam2.jpg")); + water.setRefractionStrength(0.2f); + water.setWaterHeight(initialWaterHeight); } - private void createTerrain(Node rootNode) { - Material matRock = new Material(assetManager, - "Common/MatDefs/Terrain/TerrainLighting.j3md"); - matRock.setBoolean("useTriPlanarMapping", false); - matRock.setBoolean("WardIso", true); - matRock.setTexture("AlphaMap", assetManager.loadTexture("Textures/Terrain/splat/alphamap.png")); - Texture heightMapImage = assetManager.loadTexture("Textures/Terrain/splat/mountains512.png"); - Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg"); - grass.setWrap(WrapMode.Repeat); - matRock.setTexture("DiffuseMap", grass); - matRock.setFloat("DiffuseMap_0_scale", 64); - Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg"); - dirt.setWrap(WrapMode.Repeat); - matRock.setTexture("DiffuseMap_1", dirt); - matRock.setFloat("DiffuseMap_1_scale", 16); - Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg"); - rock.setWrap(WrapMode.Repeat); - matRock.setTexture("DiffuseMap_2", rock); - matRock.setFloat("DiffuseMap_2_scale", 128); - Texture normalMap0 = assetManager.loadTexture("Textures/Terrain/splat/grass_normal.jpg"); - normalMap0.setWrap(WrapMode.Repeat); - Texture normalMap1 = assetManager.loadTexture("Textures/Terrain/splat/dirt_normal.png"); - normalMap1.setWrap(WrapMode.Repeat); - Texture normalMap2 = assetManager.loadTexture("Textures/Terrain/splat/road_normal.png"); - normalMap2.setWrap(WrapMode.Repeat); - matRock.setTexture("NormalMap", normalMap0); - matRock.setTexture("NormalMap_1", normalMap1); - matRock.setTexture("NormalMap_2", normalMap2); + private void createTerrain(Node mainScene) { + Material matRock = createTerrainMaterial(); + Texture heightMapImage = assetManager.loadTexture("Textures/Terrain/splat/mountains512.png"); AbstractHeightMap heightmap = null; try { heightmap = new ImageBasedHeightMap(heightMapImage.getImage(), 0.25f); @@ -261,51 +239,86 @@ private void createTerrain(Node rootNode) { } catch (Exception e) { e.printStackTrace(); } - TerrainQuad terrain - = new TerrainQuad("terrain", 65, 513, heightmap.getHeightMap()); + + int patchSize = 64; + int totalSize = 512; + TerrainQuad terrain = new TerrainQuad("terrain", patchSize + 1, totalSize + 1, heightmap.getHeightMap()); + TerrainLodControl control = new TerrainLodControl(terrain, getCamera()); + control.setLodCalculator(new DistanceLodCalculator(patchSize + 1, 2.7f)); // patch size, and a multiplier + terrain.addControl(control); terrain.setMaterial(matRock); - terrain.setLocalScale(new Vector3f(5, 5, 5)); + terrain.setLocalTranslation(new Vector3f(0, -30, 0)); - terrain.setLocked(false); // unlock it so we can edit the height + terrain.setLocalScale(new Vector3f(5, 5, 5)); terrain.setShadowMode(ShadowMode.Receive); - rootNode.attachChild(terrain); + mainScene.attachChild(terrain); + } + + private Material createTerrainMaterial() { + Material matRock = new Material(assetManager, "Common/MatDefs/Terrain/TerrainLighting.j3md"); + matRock.setBoolean("useTriPlanarMapping", false); + matRock.setBoolean("WardIso", true); + matRock.setTexture("AlphaMap", assetManager.loadTexture("Textures/Terrain/splat/alphamap.png")); + + setTexture("Textures/Terrain/splat/grass.jpg", matRock, "DiffuseMap"); + setTexture("Textures/Terrain/splat/dirt.jpg", matRock, "DiffuseMap_1"); + setTexture("Textures/Terrain/splat/road.jpg", matRock, "DiffuseMap_2"); + matRock.setFloat("DiffuseMap_0_scale", 64); + matRock.setFloat("DiffuseMap_1_scale", 16); + matRock.setFloat("DiffuseMap_2_scale", 128); + + setTexture("Textures/Terrain/splat/grass_normal.jpg", matRock, "NormalMap"); + setTexture("Textures/Terrain/splat/dirt_normal.png", matRock, "NormalMap_1"); + setTexture("Textures/Terrain/splat/road_normal.png", matRock, "NormalMap_2"); + return matRock; } - //This part is to emulate tides, slightly varying the height of the water plane + + private void setTexture(String texture, Material mat, String param) { + Texture tex = assetManager.loadTexture(texture); + tex.setWrap(WrapMode.Repeat); + mat.setTexture(param, tex); + } + + // This part is to emulate tides, slightly varying the height of the water plane private float time = 0.0f; private float waterHeight = 0.0f; - final private float initialWaterHeight = 90f;//0.8f; - private boolean uw = false; + private final float initialWaterHeight = 90f; + private boolean underWater = false; + + private AudioNode waves; + private final LowPassFilter aboveWaterAudioFilter = new LowPassFilter(1, 1); + private final LowPassFilter underWaterAudioFilter = new LowPassFilter(0.5f, 0.1f); + private boolean useDryFilter = true; @Override public void simpleUpdate(float tpf) { - super.simpleUpdate(tpf); - // box.updateGeometricState(); time += tpf; waterHeight = (float) Math.cos(((time * 0.6f) % FastMath.TWO_PI)) * 1.5f; water.setWaterHeight(initialWaterHeight + waterHeight); - uw = water.isUnderWater(); + underWater = water.isUnderWater(); updateAudio(); } - - protected void setText(int x, int y, String text) { - BitmapText txt2 = new BitmapText(guiFont); - txt2.setText(text); - txt2.setLocalTranslation(x, cam.getHeight() - y, 0); - txt2.setColor(ColorRGBA.Red); - guiNode.attachChild(txt2); + + private void addAudioClip() { + underWater = cam.getLocation().y < waterHeight; + + waves = new AudioNode(assetManager, "Sound/Environment/Ocean Waves.ogg", DataType.Buffer); + waves.setLooping(true); + updateAudio(); + waves.play(); } /** * Update the audio settings (dry filter and reverb) - * based on boolean fields ({@code uw} and {@code useDryFilter}). + * based on boolean fields ({@code underWater} and {@code useDryFilter}). */ - protected void updateAudio() { + private void updateAudio() { Filter newDryFilter; if (!useDryFilter) { newDryFilter = null; - } else if (uw) { + } else if (underWater) { newDryFilter = underWaterAudioFilter; } else { newDryFilter = aboveWaterAudioFilter; @@ -316,11 +329,11 @@ protected void updateAudio() { waves.setDryFilter(newDryFilter); } - boolean newReverbEnabled = !uw; + boolean newReverbEnabled = !underWater; boolean oldReverbEnabled = waves.isReverbEnabled(); if (oldReverbEnabled != newReverbEnabled) { System.out.println("reverb enabled : " + newReverbEnabled); waves.setReverbEnabled(newReverbEnabled); } } -} \ No newline at end of file +} diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java index cdc754f3a0..6fa525f355 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009-2024 jMonkeyEngine + * Copyright (c) 2009-2025 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -677,7 +677,7 @@ protected String getPrintContextInitInfo(GLData glData) { sb.append('\n') .append(" * Red Size: ").append(glData.redSize); sb.append('\n') - .append(" * Rreen Size: ").append(glData.greenSize); + .append(" * Green Size: ").append(glData.greenSize); sb.append('\n') .append(" * Blue Size: ").append(glData.blueSize); sb.append('\n') diff --git a/jme3-screenshot-tests/README.md b/jme3-screenshot-tests/README.md index 3945ac20cb..9215123bb9 100644 --- a/jme3-screenshot-tests/README.md +++ b/jme3-screenshot-tests/README.md @@ -1,7 +1,8 @@ # jme3-screenshot-tests -This module contains tests that compare screenshots of the JME3 test applications to reference images. The tests are run using -the following command: +This module contains tests that compare screenshots of the JME3 test applications to reference images. Think of these like visual unit tests + +The tests are run using the following command: ``` ./gradlew :jme3-screenshot-test:screenshotTest diff --git a/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/TestDriver.java b/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/TestDriver.java index a0ac646229..6e8bbe20f3 100644 --- a/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/TestDriver.java +++ b/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/TestDriver.java @@ -72,7 +72,7 @@ */ public class TestDriver extends BaseAppState{ - public static final String IMAGES_ARE_DIFFERENT = "Images are different."; + public static final String IMAGES_ARE_DIFFERENT = "Images are different. (If you are running the test locally this is expected, images only reproducible on github CI infrastructure)"; public static final String IMAGES_ARE_DIFFERENT_SIZES = "Images are different sizes."; @@ -197,7 +197,7 @@ private Integer extractNumber(Path path){ }); if(imageFiles.isEmpty()){ - fail("No screenshot found in the temporary directory."); + fail("No screenshot found in the temporary directory. Did the application crash?"); } if(imageFiles.size() != framesToTakeScreenshotsOn.size()){ fail("Not all screenshots were taken, expected " + framesToTakeScreenshotsOn.size() + " but got " + imageFiles.size()); @@ -218,7 +218,7 @@ private Integer extractNumber(Path path){ try{ Path savedImage = saveGeneratedImageToChangedImages(generatedImage, thisFrameBaseImageFileName); attachImage("New image:", thisFrameBaseImageFileName + ".png", savedImage); - String message = "Expected image not found, is this a new test? If so collect the new image from the step artefacts"; + String message = "Expected image not found, is this a new test? If so collect the new image from the step artefacts (on github). If running locally you can see them at build/changed-images but those should not be committed"; if(failureMessage==null){ //only want the first thing to go wrong as the junit test fail reason failureMessage = message; } diff --git a/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/animation/TestIssue2076.java b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/animation/TestIssue2076.java new file mode 100644 index 0000000000..50435b1218 --- /dev/null +++ b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/animation/TestIssue2076.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2024 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.jmonkeyengine.screenshottests.animation; + +import com.jme3.anim.SkinningControl; +import com.jme3.anim.util.AnimMigrationUtils; +import com.jme3.animation.SkeletonControl; +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.asset.AssetManager; +import com.jme3.light.AmbientLight; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.Node; +import com.jme3.scene.VertexBuffer; +import org.jmonkeyengine.screenshottests.testframework.ScreenshotTestBase; +import org.junit.jupiter.api.Test; + +/** + * Screenshot test for JMonkeyEngine issue #2076: software skinning requires vertex + * normals. + * + *

If the issue is resolved, 2 copies of the Jaime model will be rendered in the screenshot. + * + *

If the issue is present, then the application will immediately crash, + * typically with a {@code NullPointerException}. + * + * @author Stephen Gold (original test) + * @author Richard Tingle (screenshot test adaptation) + */ +public class TestIssue2076 extends ScreenshotTestBase { + + /** + * This test creates a scene with two Jaime models, one using the old animation system + * and one using the new animation system, both with software skinning and no vertex normals. + */ + @Test + public void testIssue2076() { + screenshotTest(new BaseAppState() { + @Override + protected void initialize(Application app) { + SimpleApplication simpleApplication = (SimpleApplication) app; + Node rootNode = simpleApplication.getRootNode(); + AssetManager assetManager = simpleApplication.getAssetManager(); + + // Add ambient light + AmbientLight ambientLight = new AmbientLight(); + ambientLight.setColor(new ColorRGBA(1f, 1f, 1f, 1f)); + rootNode.addLight(ambientLight); + + /* + * The original Jaime model was chosen for testing because it includes + * tangent buffers (needed to trigger issue #2076) and uses the old + * animation system (so it can be easily used to test both systems). + */ + String assetPath = "Models/Jaime/Jaime.j3o"; + + // Test old animation system + Node oldJaime = (Node) assetManager.loadModel(assetPath); + rootNode.attachChild(oldJaime); + oldJaime.setLocalTranslation(-1f, 0f, 0f); + + // Enable software skinning + SkeletonControl skeletonControl = oldJaime.getControl(SkeletonControl.class); + skeletonControl.setHardwareSkinningPreferred(false); + + // Remove its vertex normals + Geometry oldGeometry = (Geometry) oldJaime.getChild(0); + Mesh oldMesh = oldGeometry.getMesh(); + oldMesh.clearBuffer(VertexBuffer.Type.Normal); + oldMesh.clearBuffer(VertexBuffer.Type.BindPoseNormal); + + // Test new animation system + Node newJaime = (Node) assetManager.loadModel(assetPath); + AnimMigrationUtils.migrate(newJaime); + rootNode.attachChild(newJaime); + newJaime.setLocalTranslation(1f, 0f, 0f); + + // Enable software skinning + SkinningControl skinningControl = newJaime.getControl(SkinningControl.class); + skinningControl.setHardwareSkinningPreferred(false); + + // Remove its vertex normals + Geometry newGeometry = (Geometry) newJaime.getChild(0); + Mesh newMesh = newGeometry.getMesh(); + newMesh.clearBuffer(VertexBuffer.Type.Normal); + newMesh.clearBuffer(VertexBuffer.Type.BindPoseNormal); + + // Position the camera to see both models + simpleApplication.getCamera().setLocation(new Vector3f(0f, 0f, 5f)); + } + + @Override + protected void cleanup(Application app) { + } + + @Override + protected void onEnable() { + } + + @Override + protected void onDisable() { + } + + @Override + public void update(float tpf) { + super.update(tpf); + } + }).run(); + } +} \ No newline at end of file diff --git a/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/animation/TestMotionPath.java b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/animation/TestMotionPath.java new file mode 100644 index 0000000000..28a5e042d2 --- /dev/null +++ b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/animation/TestMotionPath.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2024 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.jmonkeyengine.screenshottests.animation; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.asset.AssetManager; +import com.jme3.cinematic.MotionPath; +import com.jme3.cinematic.MotionPathListener; +import com.jme3.cinematic.events.MotionEvent; +import com.jme3.font.BitmapText; +import com.jme3.input.ChaseCamera; +import com.jme3.light.DirectionalLight; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.FastMath; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.scene.shape.Box; +import org.jmonkeyengine.screenshottests.testframework.ScreenshotTestBase; +import org.junit.jupiter.api.Test; + +/** + * Screenshot test for the MotionPath functionality. + * + *

This test creates a teapot model that follows a predefined path with several waypoints. + * The animation is automatically started and screenshots are taken at frames 10 and 60 + * to capture the teapot at different positions along the path. + * + * @author Richard Tingle (screenshot test adaptation) + */ +public class TestMotionPath extends ScreenshotTestBase { + + /** + * This test creates a scene with a teapot following a motion path. + */ + @Test + public void testMotionPath() { + screenshotTest(new BaseAppState() { + private Spatial teapot; + private MotionPath path; + private MotionEvent motionControl; + private BitmapText wayPointsText; + + @Override + protected void initialize(Application app) { + SimpleApplication simpleApplication = (SimpleApplication) app; + Node rootNode = simpleApplication.getRootNode(); + Node guiNode = simpleApplication.getGuiNode(); + AssetManager assetManager = simpleApplication.getAssetManager(); + + // Set camera position + app.getCamera().setLocation(new Vector3f(8.4399185f, 11.189463f, 14.267577f)); + + // Create the scene + createScene(rootNode, assetManager); + + // Create the motion path + path = new MotionPath(); + path.addWayPoint(new Vector3f(10, 3, 0)); + path.addWayPoint(new Vector3f(10, 3, 10)); + path.addWayPoint(new Vector3f(-40, 3, 10)); + path.addWayPoint(new Vector3f(-40, 3, 0)); + path.addWayPoint(new Vector3f(-40, 8, 0)); + path.addWayPoint(new Vector3f(10, 8, 0)); + path.addWayPoint(new Vector3f(10, 8, 10)); + path.addWayPoint(new Vector3f(15, 8, 10)); + path.enableDebugShape(assetManager, rootNode); + + // Create the motion event + motionControl = new MotionEvent(teapot, path); + motionControl.setDirectionType(MotionEvent.Direction.PathAndRotation); + motionControl.setRotation(new Quaternion().fromAngleNormalAxis(-FastMath.HALF_PI, Vector3f.UNIT_Y)); + motionControl.setInitialDuration(10f); + motionControl.setSpeed(2f); + + // Create text for waypoint notifications + wayPointsText = new BitmapText(assetManager.loadFont("Interface/Fonts/Default.fnt")); + wayPointsText.setSize(wayPointsText.getFont().getCharSet().getRenderedSize()); + guiNode.attachChild(wayPointsText); + + // Add listener for waypoint events + path.addListener(new MotionPathListener() { + @Override + public void onWayPointReach(MotionEvent control, int wayPointIndex) { + if (path.getNbWayPoints() == wayPointIndex + 1) { + wayPointsText.setText(control.getSpatial().getName() + " Finished!!! "); + } else { + wayPointsText.setText(control.getSpatial().getName() + " Reached way point " + wayPointIndex); + } + wayPointsText.setLocalTranslation( + (app.getCamera().getWidth() - wayPointsText.getLineWidth()) / 2, + app.getCamera().getHeight(), + 0); + } + }); + + // note that the ChaseCamera is self-initialising, so just creating this object attaches it + new ChaseCamera(getApplication().getCamera(), teapot); + + // Start the animation automatically + motionControl.play(); + } + + private void createScene(Node rootNode, AssetManager assetManager) { + // Create materials + Material mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); + mat.setFloat("Shininess", 1f); + mat.setBoolean("UseMaterialColors", true); + mat.setColor("Ambient", ColorRGBA.Black); + mat.setColor("Diffuse", ColorRGBA.DarkGray); + mat.setColor("Specular", ColorRGBA.White.mult(0.6f)); + + Material matSoil = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); + matSoil.setBoolean("UseMaterialColors", true); + matSoil.setColor("Ambient", ColorRGBA.Black); + matSoil.setColor("Diffuse", ColorRGBA.Black); + matSoil.setColor("Specular", ColorRGBA.Black); + + // Create teapot + teapot = assetManager.loadModel("Models/Teapot/Teapot.obj"); + teapot.setName("Teapot"); + teapot.setLocalScale(3); + teapot.setMaterial(mat); + rootNode.attachChild(teapot); + + // Create ground + Geometry soil = new Geometry("soil", new Box(50, 1, 50)); + soil.setLocalTranslation(0, -1, 0); + soil.setMaterial(matSoil); + rootNode.attachChild(soil); + + // Add light + DirectionalLight light = new DirectionalLight(); + light.setDirection(new Vector3f(0, -1, 0).normalizeLocal()); + light.setColor(ColorRGBA.White.mult(1.5f)); + rootNode.addLight(light); + } + + @Override + protected void cleanup(Application app) { + } + + @Override + protected void onEnable() { + } + + @Override + protected void onDisable() { + } + }) + .setFramesToTakeScreenshotsOn(10, 60) + .run(); + } +} \ No newline at end of file diff --git a/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/light/pbr/TestPBRLighting.java b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/light/pbr/TestPBRLighting.java new file mode 100644 index 0000000000..0cbd19da24 --- /dev/null +++ b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/light/pbr/TestPBRLighting.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2024 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.jmonkeyengine.screenshottests.light.pbr; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.asset.AssetManager; +import com.jme3.environment.EnvironmentCamera; +import com.jme3.environment.FastLightProbeFactory; +import com.jme3.light.DirectionalLight; +import com.jme3.light.LightProbe; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector3f; +import com.jme3.post.FilterPostProcessor; +import com.jme3.post.filters.ToneMapFilter; +import com.jme3.renderer.Camera; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.texture.plugins.ktx.KTXLoader; +import com.jme3.util.SkyFactory; +import com.jme3.util.mikktspace.MikktspaceTangentGenerator; +import org.jmonkeyengine.screenshottests.testframework.ScreenshotTestBase; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +/** + * Screenshot tests for PBR lighting. + * + * @author nehon - original test + * @author Richard Tingle (aka richtea) - screenshot test adaptation + * + */ +public class TestPBRLighting extends ScreenshotTestBase { + + private static Stream testParameters() { + return Stream.of( + Arguments.of("LowRoughness", 0.1f, false), + Arguments.of("HighRoughness", 1.0f, false), + Arguments.of("DefaultDirectionalLight", 0.5f, false), + Arguments.of("UpdatedDirectionalLight", 0.5f, true) + ); + } + + /** + * Test PBR lighting with different parameters + * + * @param testName The name of the test (used for screenshot filename) + * @param roughness The roughness value to use + * @param updateLight Whether to update the directional light to match camera direction + */ + @ParameterizedTest(name = "{0}") + @MethodSource("testParameters") + public void testPBRLighting(String testName, float roughness, boolean updateLight, TestInfo testInfo) { + + if(!testInfo.getTestClass().isPresent() || !testInfo.getTestMethod().isPresent()) { + throw new RuntimeException("Test preconditions not met"); + } + + String imageName = testInfo.getTestClass().get().getName() + "." + testInfo.getTestMethod().get().getName() + "_" + testName; + + screenshotTest(new BaseAppState() { + private static final int RESOLUTION = 256; + + private Node modelNode; + private int frame = 0; + + @Override + protected void initialize(Application app) { + Camera cam = app.getCamera(); + cam.setLocation(new Vector3f(18, 10, 0)); + cam.lookAt(new Vector3f(0, 0, 0), Vector3f.UNIT_Y); + + AssetManager assetManager = app.getAssetManager(); + assetManager.registerLoader(KTXLoader.class, "ktx"); + + app.getViewPort().setBackgroundColor(ColorRGBA.White); + + modelNode = new Node("modelNode"); + Geometry model = (Geometry) assetManager.loadModel("Models/Tank/tank.j3o"); + MikktspaceTangentGenerator.generate(model); + modelNode.attachChild(model); + + DirectionalLight dl = new DirectionalLight(); + dl.setDirection(new Vector3f(-1, -1, -1).normalizeLocal()); + SimpleApplication simpleApp = (SimpleApplication) app; + simpleApp.getRootNode().addLight(dl); + dl.setColor(ColorRGBA.White); + + // If we need to update the light direction to match camera + if (updateLight) { + dl.setDirection(app.getCamera().getDirection().normalize()); + } + + simpleApp.getRootNode().attachChild(modelNode); + + FilterPostProcessor fpp = new FilterPostProcessor(assetManager); + int numSamples = app.getContext().getSettings().getSamples(); + if (numSamples > 0) { + fpp.setNumSamples(numSamples); + } + + fpp.addFilter(new ToneMapFilter(Vector3f.UNIT_XYZ.mult(4.0f))); + app.getViewPort().addProcessor(fpp); + + Spatial sky = SkyFactory.createSky(assetManager, "Textures/Sky/Path.hdr", SkyFactory.EnvMapType.EquirectMap); + simpleApp.getRootNode().attachChild(sky); + + Material pbrMat = assetManager.loadMaterial("Models/Tank/tank.j3m"); + pbrMat.setFloat("Roughness", roughness); + model.setMaterial(pbrMat); + + // Set up environment camera + EnvironmentCamera envCam = new EnvironmentCamera(RESOLUTION, new Vector3f(0, 3f, 0)); + app.getStateManager().attach(envCam); + } + + @Override + protected void cleanup(Application app) {} + + @Override + protected void onEnable() {} + + @Override + protected void onDisable() {} + + @Override + public void update(float tpf) { + frame++; + + if (frame == 2) { + modelNode.removeFromParent(); + LightProbe probe; + + SimpleApplication simpleApp = (SimpleApplication) getApplication(); + probe = FastLightProbeFactory.makeProbe(simpleApp.getRenderManager(), + simpleApp.getAssetManager(), + RESOLUTION, + Vector3f.ZERO, + 1f, + 1000f, + simpleApp.getRootNode()); + + probe.getArea().setRadius(100); + simpleApp.getRootNode().addLight(probe); + } + + if (frame > 10 && modelNode.getParent() == null) { + SimpleApplication simpleApp = (SimpleApplication) getApplication(); + simpleApp.getRootNode().attachChild(modelNode); + } + } + }).setBaseImageFileName(imageName) + .setFramesToTakeScreenshotsOn(12) + .run(); + } +} diff --git a/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/light/pbr/TestPBRSimple.java b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/light/pbr/TestPBRSimple.java new file mode 100644 index 0000000000..70220f7eef --- /dev/null +++ b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/light/pbr/TestPBRSimple.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2024 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.jmonkeyengine.screenshottests.light.pbr; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.asset.AssetManager; +import com.jme3.environment.EnvironmentProbeControl; +import com.jme3.material.Material; +import com.jme3.math.Vector3f; +import com.jme3.renderer.Camera; +import com.jme3.scene.Geometry; +import com.jme3.scene.Spatial; +import com.jme3.util.SkyFactory; +import com.jme3.util.mikktspace.MikktspaceTangentGenerator; +import org.jmonkeyengine.screenshottests.testframework.ScreenshotTestBase; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +/** + * A simpler PBR example that uses EnvironmentProbeControl to bake the environment + * + * @author Richard Tingle (aka richtea) - screenshot test adaptation + */ +public class TestPBRSimple extends ScreenshotTestBase { + + private static Stream testParameters() { + return Stream.of( + Arguments.of("WithRealtimeBaking", true), + Arguments.of("WithoutRealtimeBaking", false) + ); + } + + /** + * Test PBR simple with different parameters + * + * @param testName The name of the test (used for screenshot filename) + * @param realtimeBaking Whether to use realtime baking + */ + @ParameterizedTest(name = "{0}") + @MethodSource("testParameters") + public void testPBRSimple(String testName, boolean realtimeBaking, TestInfo testInfo) { + if(!testInfo.getTestClass().isPresent() || !testInfo.getTestMethod().isPresent()) { + throw new RuntimeException("Test preconditions not met"); + } + + String imageName = testInfo.getTestClass().get().getName() + "." + testInfo.getTestMethod().get().getName() + "_" + testName; + + screenshotTest(new BaseAppState() { + private int frame = 0; + + @Override + protected void initialize(Application app) { + Camera cam = app.getCamera(); + cam.setLocation(new Vector3f(18, 10, 0)); + cam.lookAt(new Vector3f(0, 0, 0), Vector3f.UNIT_Y); + + AssetManager assetManager = app.getAssetManager(); + SimpleApplication simpleApp = (SimpleApplication) app; + + // Create the tank model + Geometry model = (Geometry) assetManager.loadModel("Models/Tank/tank.j3o"); + MikktspaceTangentGenerator.generate(model); + + Material pbrMat = assetManager.loadMaterial("Models/Tank/tank.j3m"); + model.setMaterial(pbrMat); + simpleApp.getRootNode().attachChild(model); + + // Create sky + Spatial sky = SkyFactory.createSky(assetManager, "Textures/Sky/Path.hdr", SkyFactory.EnvMapType.EquirectMap); + simpleApp.getRootNode().attachChild(sky); + + // Create baker control + EnvironmentProbeControl envProbe = new EnvironmentProbeControl(assetManager, 256); + simpleApp.getRootNode().addControl(envProbe); + + // Tag the sky, only the tagged spatials will be rendered in the env map + envProbe.tag(sky); + } + + @Override + protected void cleanup(Application app) {} + + @Override + protected void onEnable() {} + + @Override + protected void onDisable() {} + + @Override + public void update(float tpf) { + if (realtimeBaking) { + frame++; + if (frame == 2) { + SimpleApplication simpleApp = (SimpleApplication) getApplication(); + simpleApp.getRootNode().getControl(EnvironmentProbeControl.class).rebake(); + } + } + } + }).setBaseImageFileName(imageName) + .setFramesToTakeScreenshotsOn(10) + .run(); + } +} \ No newline at end of file diff --git a/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/material/TestSimpleBumps.java b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/material/TestSimpleBumps.java new file mode 100644 index 0000000000..0e4e53df54 --- /dev/null +++ b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/material/TestSimpleBumps.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2024 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.jmonkeyengine.screenshottests.material; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.asset.AssetManager; +import com.jme3.light.PointLight; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.FastMath; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.scene.shape.Quad; +import com.jme3.scene.shape.Sphere; +import com.jme3.util.mikktspace.MikktspaceTangentGenerator; +import org.jmonkeyengine.screenshottests.testframework.ScreenshotTestBase; +import org.junit.jupiter.api.Test; + +/** + * Screenshot test for the SimpleBumps material test. + * + *

This test creates a quad with a bump map material and a point light that orbits around it. + * The light's position is represented by a small red sphere. Screenshots are taken at frames 10 and 60 + * to capture the light at different positions in its orbit. + * + * @author Richard Tingle (screenshot test adaptation) + */ +public class TestSimpleBumps extends ScreenshotTestBase { + + /** + * This test creates a scene with a bump-mapped quad and an orbiting light. + */ + @Test + public void testSimpleBumps() { + screenshotTest(new BaseAppState() { + private float angle; + private PointLight pl; + private Spatial lightMdl; + + @Override + protected void initialize(Application app) { + SimpleApplication simpleApplication = (SimpleApplication) app; + Node rootNode = simpleApplication.getRootNode(); + AssetManager assetManager = simpleApplication.getAssetManager(); + + // Create quad with bump map material + Quad quadMesh = new Quad(1, 1); + Geometry sphere = new Geometry("Rock Ball", quadMesh); + Material mat = assetManager.loadMaterial("Textures/BumpMapTest/SimpleBump.j3m"); + sphere.setMaterial(mat); + MikktspaceTangentGenerator.generate(sphere); + rootNode.attachChild(sphere); + + // Create light representation + lightMdl = new Geometry("Light", new Sphere(10, 10, 0.1f)); + lightMdl.setMaterial(assetManager.loadMaterial("Common/Materials/RedColor.j3m")); + rootNode.attachChild(lightMdl); + + // Create point light + pl = new PointLight(); + pl.setColor(ColorRGBA.White); + pl.setPosition(new Vector3f(0f, 0f, 4f)); + rootNode.addLight(pl); + } + + @Override + protected void cleanup(Application app) { + } + + @Override + protected void onEnable() { + } + + @Override + protected void onDisable() { + } + + @Override + public void update(float tpf) { + super.update(tpf); + + angle += tpf * 2f; + angle %= FastMath.TWO_PI; + + pl.setPosition(new Vector3f(FastMath.cos(angle) * 4f, 0.5f, FastMath.sin(angle) * 4f)); + lightMdl.setLocalTranslation(pl.getPosition()); + } + }) + .setFramesToTakeScreenshotsOn(10, 60) + .run(); + } +} \ No newline at end of file diff --git a/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/scene/instancing/TestInstanceNodeWithPbr.java b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/scene/instancing/TestInstanceNodeWithPbr.java new file mode 100644 index 0000000000..9069fb4464 --- /dev/null +++ b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/scene/instancing/TestInstanceNodeWithPbr.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2024 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.jmonkeyengine.screenshottests.scene.instancing; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.font.BitmapText; +import com.jme3.light.DirectionalLight; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.instancing.InstancedNode; +import com.jme3.scene.shape.Box; +import org.jmonkeyengine.screenshottests.testframework.ScreenshotTestBase; +import org.junit.jupiter.api.Test; + +import java.util.Locale; + +/** + * This test specifically validates the corrected PBR rendering when combined + * with instancing, as addressed in issue #2435. + * + *

+ * It creates an InstancedNode with a PBR-materialized Box to ensure the fix in + * PBRLighting.vert correctly handles world position calculations for instanced geometry. + *

+ * + * @author Ryan McDonough - original test + * @author Richard Tingle (aka richtea) - screenshot test adaptation + */ +public class TestInstanceNodeWithPbr extends ScreenshotTestBase { + + @Test + public void testInstanceNodeWithPbr() { + screenshotTest( + new BaseAppState() { + private Geometry box; + private float pos = -5; + private float vel = 50; + private BitmapText bmp; + + @Override + protected void initialize(Application app) { + SimpleApplication simpleApp = (SimpleApplication) app; + + app.getCamera().setLocation(Vector3f.UNIT_XYZ.mult(12)); + app.getCamera().lookAt(Vector3f.ZERO, Vector3f.UNIT_Y); + + bmp = new BitmapText(app.getAssetManager().loadFont("Interface/Fonts/Default.fnt")); + bmp.setText(""); + bmp.setLocalTranslation(10, app.getContext().getSettings().getHeight() - 20, 0); + bmp.setColor(ColorRGBA.Red); + simpleApp.getGuiNode().attachChild(bmp); + + InstancedNode instancedNode = new InstancedNode("InstancedNode"); + simpleApp.getRootNode().attachChild(instancedNode); + + Box mesh = new Box(0.5f, 0.5f, 0.5f); + box = new Geometry("Box", mesh); + Material pbrMaterial = createPbrMaterial(app, ColorRGBA.Red); + box.setMaterial(pbrMaterial); + + instancedNode.attachChild(box); + instancedNode.instance(); + + DirectionalLight light = new DirectionalLight(); + light.setDirection(new Vector3f(-1, -2, -3).normalizeLocal()); + simpleApp.getRootNode().addLight(light); + } + + private Material createPbrMaterial(Application app, ColorRGBA color) { + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Light/PBRLighting.j3md"); + mat.setColor("BaseColor", color); + mat.setFloat("Roughness", 0.8f); + mat.setFloat("Metallic", 0.1f); + mat.setBoolean("UseInstancing", true); + return mat; + } + + @Override + public void update(float tpf) { + pos += tpf * vel; + box.setLocalTranslation(pos, 0f, 0f); + + bmp.setText(String.format(Locale.ENGLISH, "BoxPosition: (%.2f, %.1f, %.1f)", pos, 0f, 0f)); + } + + @Override + protected void cleanup(Application app) {} + + @Override + protected void onEnable() {} + + @Override + protected void onDisable() { } + } + ) + .setFramesToTakeScreenshotsOn(1, 10) + .run(); + } +} \ No newline at end of file diff --git a/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/terrain/TestPBRTerrain.java b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/terrain/TestPBRTerrain.java new file mode 100644 index 0000000000..37f8063300 --- /dev/null +++ b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/terrain/TestPBRTerrain.java @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2024 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.jmonkeyengine.screenshottests.terrain; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.asset.AssetManager; +import com.jme3.asset.TextureKey; +import com.jme3.light.AmbientLight; +import com.jme3.light.DirectionalLight; +import com.jme3.light.LightProbe; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector3f; +import com.jme3.terrain.geomipmap.TerrainLodControl; +import com.jme3.terrain.geomipmap.TerrainQuad; +import com.jme3.terrain.geomipmap.lodcalc.DistanceLodCalculator; +import com.jme3.terrain.heightmap.AbstractHeightMap; +import com.jme3.terrain.heightmap.ImageBasedHeightMap; +import com.jme3.texture.Texture; +import com.jme3.texture.Texture.WrapMode; +import org.jmonkeyengine.screenshottests.testframework.ScreenshotTestBase; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +/** + * This test uses 'PBRTerrain.j3md' to create a terrain Material for PBR. + * + * Upon running the app, the user should see a mountainous, terrain-based + * landscape with some grassy areas, some snowy areas, and some tiled roads and + * gravel paths weaving between the valleys. Snow should be slightly + * shiny/reflective, and marble texture should be even shinier. If you would + * like to know what each texture is supposed to look like, you can find the + * textures used for this test case located in jme3-testdata. + * + * Uses assets from CC0Textures.com, licensed under CC0 1.0 Universal. For more + * information on the textures this test case uses, view the license.txt file + * located in the jme3-testdata directory where these textures are located: + * jme3-testdata/src/main/resources/Textures/Terrain/PBR + * + * @author yaRnMcDonuts (Original manual test) + * @author Richard Tingle (aka richtea) - screenshot test adaptation + */ +@SuppressWarnings("FieldCanBeLocal") +public class TestPBRTerrain extends ScreenshotTestBase { + + private static Stream testParameters() { + return Stream.of( + Arguments.of("FinalRender", 0), + Arguments.of("NormalMap", 1), + Arguments.of("RoughnessMap", 2), + Arguments.of("MetallicMap", 3), + Arguments.of("GeometryNormals", 8) + ); + } + + /** + * Test PBR terrain with different debug modes + * + * @param testName The name of the test (used for screenshot filename) + * @param debugMode The debug mode to use + */ + @ParameterizedTest(name = "{0}") + @MethodSource("testParameters") + public void testPBRTerrain(String testName, int debugMode, TestInfo testInfo) { + + if(!testInfo.getTestClass().isPresent() || !testInfo.getTestMethod().isPresent()) { + throw new RuntimeException("Test preconditions not met"); + } + + String imageName = testInfo.getTestClass().get().getName() + "." + testInfo.getTestMethod().get().getName() + "_" + testName; + + screenshotTest(new BaseAppState() { + private TerrainQuad terrain; + private Material matTerrain; + + private final int terrainSize = 512; + private final int patchSize = 256; + private final float dirtScale = 24; + private final float darkRockScale = 24; + private final float snowScale = 64; + private final float tileRoadScale = 64; + private final float grassScale = 24; + private final float marbleScale = 64; + private final float gravelScale = 64; + + @Override + protected void initialize(Application app) { + SimpleApplication simpleApp = (SimpleApplication) app; + AssetManager assetManager = app.getAssetManager(); + + setUpTerrain(simpleApp, assetManager); + setUpTerrainMaterial(assetManager); + setUpLights(simpleApp, assetManager); + setUpCamera(app); + + // Set debug mode + matTerrain.setInt("DebugValuesMode", debugMode); + } + + private void setUpTerrainMaterial(AssetManager assetManager) { + // PBR terrain matdef + matTerrain = new Material(assetManager, "Common/MatDefs/Terrain/PBRTerrain.j3md"); + + matTerrain.setBoolean("useTriPlanarMapping", false); + + // ALPHA map (for splat textures) + matTerrain.setTexture("AlphaMap", assetManager.loadTexture("Textures/Terrain/splat/alpha1.png")); + matTerrain.setTexture("AlphaMap_1", assetManager.loadTexture("Textures/Terrain/splat/alpha2.png")); + + // DIRT texture + Texture dirt = assetManager.loadTexture("Textures/Terrain/PBR/Ground037_1K_Color.png"); + dirt.setWrap(WrapMode.Repeat); + matTerrain.setTexture("AlbedoMap_0", dirt); + matTerrain.setFloat("AlbedoMap_0_scale", dirtScale); + matTerrain.setFloat("Roughness_0", 1); + matTerrain.setFloat("Metallic_0", 0); + + // DARK ROCK texture + Texture darkRock = assetManager.loadTexture("Textures/Terrain/PBR/Rock035_1K_Color.png"); + darkRock.setWrap(WrapMode.Repeat); + matTerrain.setTexture("AlbedoMap_1", darkRock); + matTerrain.setFloat("AlbedoMap_1_scale", darkRockScale); + matTerrain.setFloat("Roughness_1", 0.92f); + matTerrain.setFloat("Metallic_1", 0.02f); + + // SNOW texture + Texture snow = assetManager.loadTexture("Textures/Terrain/PBR/Snow006_1K_Color.png"); + snow.setWrap(WrapMode.Repeat); + matTerrain.setTexture("AlbedoMap_2", snow); + matTerrain.setFloat("AlbedoMap_2_scale", snowScale); + matTerrain.setFloat("Roughness_2", 0.55f); + matTerrain.setFloat("Metallic_2", 0.12f); + + // TILES texture + Texture tiles = assetManager.loadTexture("Textures/Terrain/PBR/Tiles083_1K_Color.png"); + tiles.setWrap(WrapMode.Repeat); + matTerrain.setTexture("AlbedoMap_3", tiles); + matTerrain.setFloat("AlbedoMap_3_scale", tileRoadScale); + matTerrain.setFloat("Roughness_3", 0.87f); + matTerrain.setFloat("Metallic_3", 0.08f); + + // GRASS texture + Texture grass = assetManager.loadTexture("Textures/Terrain/PBR/Ground037_1K_Color.png"); + grass.setWrap(WrapMode.Repeat); + matTerrain.setTexture("AlbedoMap_4", grass); + matTerrain.setFloat("AlbedoMap_4_scale", grassScale); + matTerrain.setFloat("Roughness_4", 1); + matTerrain.setFloat("Metallic_4", 0); + + // MARBLE texture + Texture marble = assetManager.loadTexture("Textures/Terrain/PBR/Marble013_1K_Color.png"); + marble.setWrap(WrapMode.Repeat); + matTerrain.setTexture("AlbedoMap_5", marble); + matTerrain.setFloat("AlbedoMap_5_scale", marbleScale); + matTerrain.setFloat("Roughness_5", 0.06f); + matTerrain.setFloat("Metallic_5", 0.8f); + + // Gravel texture + Texture gravel = assetManager.loadTexture("Textures/Terrain/PBR/Gravel015_1K_Color.png"); + gravel.setWrap(WrapMode.Repeat); + matTerrain.setTexture("AlbedoMap_6", gravel); + matTerrain.setFloat("AlbedoMap_6_scale", gravelScale); + matTerrain.setFloat("Roughness_6", 0.9f); + matTerrain.setFloat("Metallic_6", 0.07f); + + // NORMAL MAPS + Texture normalMapDirt = assetManager.loadTexture("Textures/Terrain/PBR/Ground036_1K_Normal.png"); + normalMapDirt.setWrap(WrapMode.Repeat); + + Texture normalMapDarkRock = assetManager.loadTexture("Textures/Terrain/PBR/Rock035_1K_Normal.png"); + normalMapDarkRock.setWrap(WrapMode.Repeat); + + Texture normalMapSnow = assetManager.loadTexture("Textures/Terrain/PBR/Snow006_1K_Normal.png"); + normalMapSnow.setWrap(WrapMode.Repeat); + + Texture normalMapGravel = assetManager.loadTexture("Textures/Terrain/PBR/Gravel015_1K_Normal.png"); + normalMapGravel.setWrap(WrapMode.Repeat); + + Texture normalMapGrass = assetManager.loadTexture("Textures/Terrain/PBR/Ground037_1K_Normal.png"); + normalMapGrass.setWrap(WrapMode.Repeat); + + Texture normalMapTiles = assetManager.loadTexture("Textures/Terrain/PBR/Tiles083_1K_Normal.png"); + normalMapTiles.setWrap(WrapMode.Repeat); + + matTerrain.setTexture("NormalMap_0", normalMapDirt); + matTerrain.setTexture("NormalMap_1", normalMapDarkRock); + matTerrain.setTexture("NormalMap_2", normalMapSnow); + matTerrain.setTexture("NormalMap_3", normalMapTiles); + matTerrain.setTexture("NormalMap_4", normalMapGrass); + matTerrain.setTexture("NormalMap_6", normalMapGravel); + + terrain.setMaterial(matTerrain); + } + + private void setUpTerrain(SimpleApplication simpleApp, AssetManager assetManager) { + // HEIGHTMAP image (for the terrain heightmap) + TextureKey hmKey = new TextureKey("Textures/Terrain/splat/mountains512.png", false); + Texture heightMapImage = assetManager.loadTexture(hmKey); + + // CREATE HEIGHTMAP + AbstractHeightMap heightmap; + try { + heightmap = new ImageBasedHeightMap(heightMapImage.getImage(), 0.3f); + heightmap.load(); + heightmap.smooth(0.9f, 1); + } catch (Exception e) { + throw new RuntimeException(e); + } + + terrain = new TerrainQuad("terrain", patchSize + 1, terrainSize + 1, heightmap.getHeightMap()); + TerrainLodControl control = new TerrainLodControl(terrain, getApplication().getCamera()); + control.setLodCalculator(new DistanceLodCalculator(patchSize + 1, 2.7f)); // patch size, and a multiplier + terrain.addControl(control); + terrain.setMaterial(matTerrain); + terrain.setLocalTranslation(0, -100, 0); + terrain.setLocalScale(1f, 1f, 1f); + simpleApp.getRootNode().attachChild(terrain); + } + + private void setUpLights(SimpleApplication simpleApp, AssetManager assetManager) { + LightProbe probe = (LightProbe) assetManager.loadAsset("Scenes/LightProbes/quarry_Probe.j3o"); + + probe.setAreaType(LightProbe.AreaType.Spherical); + probe.getArea().setRadius(2000); + probe.getArea().setCenter(new Vector3f(0, 0, 0)); + simpleApp.getRootNode().addLight(probe); + + DirectionalLight directionalLight = new DirectionalLight(); + directionalLight.setDirection((new Vector3f(-0.3f, -0.5f, -0.3f)).normalize()); + directionalLight.setColor(ColorRGBA.White); + simpleApp.getRootNode().addLight(directionalLight); + + AmbientLight ambientLight = new AmbientLight(); + ambientLight.setColor(ColorRGBA.White); + simpleApp.getRootNode().addLight(ambientLight); + } + + private void setUpCamera(Application app) { + app.getCamera().setLocation(new Vector3f(0, 10, -10)); + app.getCamera().lookAtDirection(new Vector3f(0, -1.5f, -1).normalizeLocal(), Vector3f.UNIT_Y); + } + + @Override + protected void cleanup(Application app) {} + + @Override + protected void onEnable() {} + + @Override + protected void onDisable() {} + + }).setBaseImageFileName(imageName) + .setFramesToTakeScreenshotsOn(5) + .run(); + } +} diff --git a/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/terrain/TestPBRTerrainAdvanced.java b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/terrain/TestPBRTerrainAdvanced.java new file mode 100644 index 0000000000..a1f5830896 --- /dev/null +++ b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/terrain/TestPBRTerrainAdvanced.java @@ -0,0 +1,368 @@ +/* + * Copyright (c) 2024 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.jmonkeyengine.screenshottests.terrain; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.asset.AssetManager; +import com.jme3.asset.TextureKey; +import com.jme3.light.AmbientLight; +import com.jme3.light.DirectionalLight; +import com.jme3.light.LightProbe; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector3f; +import com.jme3.shader.VarType; +import com.jme3.terrain.geomipmap.TerrainLodControl; +import com.jme3.terrain.geomipmap.TerrainQuad; +import com.jme3.terrain.geomipmap.lodcalc.DistanceLodCalculator; +import com.jme3.terrain.heightmap.AbstractHeightMap; +import com.jme3.terrain.heightmap.ImageBasedHeightMap; +import com.jme3.texture.Image; +import com.jme3.texture.Texture; +import com.jme3.texture.Texture.WrapMode; +import com.jme3.texture.Texture.MagFilter; +import com.jme3.texture.Texture.MinFilter; +import com.jme3.texture.TextureArray; +import org.jmonkeyengine.screenshottests.testframework.ScreenshotTestBase; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + + +/** + * This test uses 'AdvancedPBRTerrain.j3md' to create a terrain Material with + * more textures than 'PBRTerrain.j3md' can handle. + * + * Upon running the app, the user should see a mountainous, terrain-based + * landscape with some grassy areas, some snowy areas, and some tiled roads and + * gravel paths weaving between the valleys. Snow should be slightly + * shiny/reflective, and marble texture should be even shinier. If you would + * like to know what each texture is supposed to look like, you can find the + * textures used for this test case located in jme3-testdata. + + * The MetallicRoughness map stores: + *
    + *
  • AmbientOcclusion in the Red channel
  • + *
  • Roughness in the Green channel
  • + *
  • Metallic in the Blue channel
  • + *
  • EmissiveIntensity in the Alpha channel
  • + *
+ * + * The shaders are still subject to the GLSL max limit of 16 textures, however + * each TextureArray counts as a single texture, and each TextureArray can store + * multiple images. For more information on texture arrays see: + * https://www.khronos.org/opengl/wiki/Array_Texture + * + * Uses assets from CC0Textures.com, licensed under CC0 1.0 Universal. For more + * information on the textures this test case uses, view the license.txt file + * located in the jme3-testdata directory where these textures are located: + * jme3-testdata/src/main/resources/Textures/Terrain/PBR + * + * @author yaRnMcDonuts - original test + * @author Richard Tingle (aka richtea) - screenshot test adaptation + */ +@SuppressWarnings("FieldCanBeLocal") +public class TestPBRTerrainAdvanced extends ScreenshotTestBase { + + private static Stream testParameters() { + return Stream.of( + Arguments.of("FinalRender", 0), + Arguments.of("AmbientOcclusion", 4), + Arguments.of("Emissive", 5) + ); + } + + /** + * Test advanced PBR terrain with different debug modes + * + * @param testName The name of the test (used for screenshot filename) + * @param debugMode The debug mode to use + */ + @ParameterizedTest(name = "{0}") + @MethodSource("testParameters") + public void testPBRTerrainAdvanced(String testName, int debugMode, TestInfo testInfo) { + if(!testInfo.getTestClass().isPresent() || !testInfo.getTestMethod().isPresent()) { + throw new RuntimeException("Test preconditions not met"); + } + + String imageName = testInfo.getTestClass().get().getName() + "." + testInfo.getTestMethod().get().getName() + "_" + testName; + + screenshotTest(new BaseAppState() { + private TerrainQuad terrain; + private Material matTerrain; + + private final int terrainSize = 512; + private final int patchSize = 256; + private final float dirtScale = 24; + private final float darkRockScale = 24; + private final float snowScale = 64; + private final float tileRoadScale = 64; + private final float grassScale = 24; + private final float marbleScale = 64; + private final float gravelScale = 64; + + private final ColorRGBA tilesEmissiveColor = new ColorRGBA(0.12f, 0.02f, 0.23f, 0.85f); //dim magenta emission + private final ColorRGBA marbleEmissiveColor = new ColorRGBA(0.0f, 0.0f, 1.0f, 1.0f); //fully saturated blue emission + + @Override + protected void initialize(Application app) { + SimpleApplication simpleApp = (SimpleApplication) app; + AssetManager assetManager = app.getAssetManager(); + + setUpTerrain(simpleApp, assetManager); + setUpTerrainMaterial(assetManager); + setUpLights(simpleApp, assetManager); + setUpCamera(app); + + // Set debug mode + matTerrain.setInt("DebugValuesMode", debugMode); + } + + private void setUpTerrainMaterial(AssetManager assetManager) { + // advanced PBR terrain matdef + matTerrain = new Material(assetManager, "Common/MatDefs/Terrain/AdvancedPBRTerrain.j3md"); + + matTerrain.setBoolean("useTriPlanarMapping", false); + + // ALPHA map (for splat textures) + matTerrain.setTexture("AlphaMap", assetManager.loadTexture("Textures/Terrain/splat/alpha1.png")); + matTerrain.setTexture("AlphaMap_1", assetManager.loadTexture("Textures/Terrain/splat/alpha2.png")); + + // load textures for texture arrays + // These MUST all have the same dimensions and format in order to be put into a texture array. + //ALBEDO MAPS + Texture dirt = assetManager.loadTexture("Textures/Terrain/PBR/Ground037_1K_Color.png"); + Texture darkRock = assetManager.loadTexture("Textures/Terrain/PBR/Rock035_1K_Color.png"); + Texture snow = assetManager.loadTexture("Textures/Terrain/PBR/Snow006_1K_Color.png"); + Texture tileRoad = assetManager.loadTexture("Textures/Terrain/PBR/Tiles083_1K_Color.png"); + Texture grass = assetManager.loadTexture("Textures/Terrain/PBR/Ground037_1K_Color.png"); + Texture marble = assetManager.loadTexture("Textures/Terrain/PBR/Marble013_1K_Color.png"); + Texture gravel = assetManager.loadTexture("Textures/Terrain/PBR/Gravel015_1K_Color.png"); + + // NORMAL MAPS + Texture normalMapDirt = assetManager.loadTexture("Textures/Terrain/PBR/Ground036_1K_Normal.png"); + Texture normalMapDarkRock = assetManager.loadTexture("Textures/Terrain/PBR/Rock035_1K_Normal.png"); + Texture normalMapSnow = assetManager.loadTexture("Textures/Terrain/PBR/Snow006_1K_Normal.png"); + Texture normalMapGravel = assetManager.loadTexture("Textures/Terrain/PBR/Gravel015_1K_Normal.png"); + Texture normalMapGrass = assetManager.loadTexture("Textures/Terrain/PBR/Ground037_1K_Normal.png"); + Texture normalMapMarble = assetManager.loadTexture("Textures/Terrain/PBR/Marble013_1K_Normal.png"); + Texture normalMapRoad = assetManager.loadTexture("Textures/Terrain/PBR/Tiles083_1K_Normal.png"); + + //PACKED METALLIC/ROUGHNESS / AMBIENT OCCLUSION / EMISSIVE INTENSITY MAPS + Texture metallicRoughnessAoEiMapDirt = assetManager.loadTexture("Textures/Terrain/PBR/Ground036_PackedMetallicRoughnessMap.png"); + Texture metallicRoughnessAoEiMapDarkRock = assetManager.loadTexture("Textures/Terrain/PBR/Rock035_PackedMetallicRoughnessMap.png"); + Texture metallicRoughnessAoEiMapSnow = assetManager.loadTexture("Textures/Terrain/PBR/Snow006_PackedMetallicRoughnessMap.png"); + Texture metallicRoughnessAoEiMapGravel = assetManager.loadTexture("Textures/Terrain/PBR/Gravel_015_PackedMetallicRoughnessMap.png"); + Texture metallicRoughnessAoEiMapGrass = assetManager.loadTexture("Textures/Terrain/PBR/Ground037_PackedMetallicRoughnessMap.png"); + Texture metallicRoughnessAoEiMapMarble = assetManager.loadTexture("Textures/Terrain/PBR/Marble013_PackedMetallicRoughnessMap.png"); + Texture metallicRoughnessAoEiMapRoad = assetManager.loadTexture("Textures/Terrain/PBR/Tiles083_PackedMetallicRoughnessMap.png"); + + // put all images into lists to create texture arrays. + List albedoImages = new ArrayList<>(); + List normalMapImages = new ArrayList<>(); + List metallicRoughnessAoEiMapImages = new ArrayList<>(); + + albedoImages.add(dirt.getImage()); //0 + albedoImages.add(darkRock.getImage()); //1 + albedoImages.add(snow.getImage()); //2 + albedoImages.add(tileRoad.getImage()); //3 + albedoImages.add(grass.getImage()); //4 + albedoImages.add(marble.getImage()); //5 + albedoImages.add(gravel.getImage()); //6 + + normalMapImages.add(normalMapDirt.getImage()); //0 + normalMapImages.add(normalMapDarkRock.getImage()); //1 + normalMapImages.add(normalMapSnow.getImage()); //2 + normalMapImages.add(normalMapRoad.getImage()); //3 + normalMapImages.add(normalMapGrass.getImage()); //4 + normalMapImages.add(normalMapMarble.getImage()); //5 + normalMapImages.add(normalMapGravel.getImage()); //6 + + metallicRoughnessAoEiMapImages.add(metallicRoughnessAoEiMapDirt.getImage()); //0 + metallicRoughnessAoEiMapImages.add(metallicRoughnessAoEiMapDarkRock.getImage()); //1 + metallicRoughnessAoEiMapImages.add(metallicRoughnessAoEiMapSnow.getImage()); //2 + metallicRoughnessAoEiMapImages.add(metallicRoughnessAoEiMapRoad.getImage()); //3 + metallicRoughnessAoEiMapImages.add(metallicRoughnessAoEiMapGrass.getImage()); //4 + metallicRoughnessAoEiMapImages.add(metallicRoughnessAoEiMapMarble.getImage()); //5 + metallicRoughnessAoEiMapImages.add(metallicRoughnessAoEiMapGravel.getImage()); //6 + + //initiate texture arrays + TextureArray albedoTextureArray = new TextureArray(albedoImages); + TextureArray normalParallaxTextureArray = new TextureArray(normalMapImages); // parallax is not used currently + TextureArray metallicRoughnessAoEiTextureArray = new TextureArray(metallicRoughnessAoEiMapImages); + + //apply wrapMode to the whole texture array, rather than each individual texture in the array + setWrapAndMipMaps(albedoTextureArray); + setWrapAndMipMaps(normalParallaxTextureArray); + setWrapAndMipMaps(metallicRoughnessAoEiTextureArray); + + //assign texture array to materials + matTerrain.setParam("AlbedoTextureArray", VarType.TextureArray, albedoTextureArray); + matTerrain.setParam("NormalParallaxTextureArray", VarType.TextureArray, normalParallaxTextureArray); + matTerrain.setParam("MetallicRoughnessAoEiTextureArray", VarType.TextureArray, metallicRoughnessAoEiTextureArray); + + //set up texture slots: + matTerrain.setInt("AlbedoMap_0", 0); // dirt is index 0 in the albedo image list + matTerrain.setFloat("AlbedoMap_0_scale", dirtScale); + matTerrain.setFloat("Roughness_0", 1); + matTerrain.setFloat("Metallic_0", 0.02f); + + matTerrain.setInt("AlbedoMap_1", 1); // darkRock is index 1 in the albedo image list + matTerrain.setFloat("AlbedoMap_1_scale", darkRockScale); + matTerrain.setFloat("Roughness_1", 1); + matTerrain.setFloat("Metallic_1", 0.04f); + + matTerrain.setInt("AlbedoMap_2", 2); + matTerrain.setFloat("AlbedoMap_2_scale", snowScale); + matTerrain.setFloat("Roughness_2", 0.72f); + matTerrain.setFloat("Metallic_2", 0.12f); + + matTerrain.setInt("AlbedoMap_3", 3); + matTerrain.setFloat("AlbedoMap_3_scale", tileRoadScale); + matTerrain.setFloat("Roughness_3", 1); + matTerrain.setFloat("Metallic_3", 0.04f); + + matTerrain.setInt("AlbedoMap_4", 4); + matTerrain.setFloat("AlbedoMap_4_scale", grassScale); + matTerrain.setFloat("Roughness_4", 1); + matTerrain.setFloat("Metallic_4", 0); + + matTerrain.setInt("AlbedoMap_5", 5); + matTerrain.setFloat("AlbedoMap_5_scale", marbleScale); + matTerrain.setFloat("Roughness_5", 1); + matTerrain.setFloat("Metallic_5", 0.2f); + + matTerrain.setInt("AlbedoMap_6", 6); + matTerrain.setFloat("AlbedoMap_6_scale", gravelScale); + matTerrain.setFloat("Roughness_6", 1); + matTerrain.setFloat("Metallic_6", 0.01f); + + // NORMAL MAPS + matTerrain.setInt("NormalMap_0", 0); + matTerrain.setInt("NormalMap_1", 1); + matTerrain.setInt("NormalMap_2", 2); + matTerrain.setInt("NormalMap_3", 3); + matTerrain.setInt("NormalMap_4", 4); + matTerrain.setInt("NormalMap_5", 5); + matTerrain.setInt("NormalMap_6", 6); + + //METALLIC/ROUGHNESS/AO/EI MAPS + matTerrain.setInt("MetallicRoughnessMap_0", 0); + matTerrain.setInt("MetallicRoughnessMap_1", 1); + matTerrain.setInt("MetallicRoughnessMap_2", 2); + matTerrain.setInt("MetallicRoughnessMap_3", 3); + matTerrain.setInt("MetallicRoughnessMap_4", 4); + matTerrain.setInt("MetallicRoughnessMap_5", 5); + matTerrain.setInt("MetallicRoughnessMap_6", 6); + + //EMISSIVE + matTerrain.setColor("EmissiveColor_5", marbleEmissiveColor); + matTerrain.setColor("EmissiveColor_3", tilesEmissiveColor); + + terrain.setMaterial(matTerrain); + } + + private void setWrapAndMipMaps(Texture texture) { + texture.setWrap(WrapMode.Repeat); + texture.setMinFilter(MinFilter.Trilinear); + texture.setMagFilter(MagFilter.Bilinear); + } + + private void setUpTerrain(SimpleApplication simpleApp, AssetManager assetManager) { + // HEIGHTMAP image (for the terrain heightmap) + TextureKey hmKey = new TextureKey("Textures/Terrain/splat/mountains512.png", false); + Texture heightMapImage = assetManager.loadTexture(hmKey); + + // CREATE HEIGHTMAP + AbstractHeightMap heightmap; + try { + heightmap = new ImageBasedHeightMap(heightMapImage.getImage(), 0.3f); + heightmap.load(); + heightmap.smooth(0.9f, 1); + } catch (Exception e) { + throw new RuntimeException(e); + } + + terrain = new TerrainQuad("terrain", patchSize + 1, terrainSize + 1, heightmap.getHeightMap()); + TerrainLodControl control = new TerrainLodControl(terrain, getApplication().getCamera()); + control.setLodCalculator(new DistanceLodCalculator(patchSize + 1, 2.7f)); // patch size, and a multiplier + terrain.addControl(control); + terrain.setMaterial(matTerrain); + terrain.setLocalTranslation(0, -100, 0); + terrain.setLocalScale(1f, 1f, 1f); + simpleApp.getRootNode().attachChild(terrain); + } + + private void setUpLights(SimpleApplication simpleApp, AssetManager assetManager) { + LightProbe probe = (LightProbe) assetManager.loadAsset("Scenes/LightProbes/quarry_Probe.j3o"); + + probe.setAreaType(LightProbe.AreaType.Spherical); + probe.getArea().setRadius(2000); + probe.getArea().setCenter(new Vector3f(0, 0, 0)); + simpleApp.getRootNode().addLight(probe); + + DirectionalLight directionalLight = new DirectionalLight(); + directionalLight.setDirection((new Vector3f(-0.3f, -0.5f, -0.3f)).normalize()); + directionalLight.setColor(ColorRGBA.White); + simpleApp.getRootNode().addLight(directionalLight); + + AmbientLight ambientLight = new AmbientLight(); + ambientLight.setColor(ColorRGBA.White); + simpleApp.getRootNode().addLight(ambientLight); + } + + private void setUpCamera(Application app) { + app.getCamera().setLocation(new Vector3f(0, 10, -10)); + app.getCamera().lookAtDirection(new Vector3f(0, -1.5f, -1).normalizeLocal(), Vector3f.UNIT_Y); + } + + @Override + protected void cleanup(Application app) {} + + @Override + protected void onEnable() {} + + @Override + protected void onDisable() {} + + }).setBaseImageFileName(imageName) + .setFramesToTakeScreenshotsOn(5) + .run(); + } +} \ No newline at end of file diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.animation.TestIssue2076.testIssue2076_f1.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.animation.TestIssue2076.testIssue2076_f1.png new file mode 100644 index 0000000000..f4cbc42002 Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.animation.TestIssue2076.testIssue2076_f1.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.animation.TestMotionPath.testMotionPath_f10.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.animation.TestMotionPath.testMotionPath_f10.png new file mode 100644 index 0000000000..028530e9c8 Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.animation.TestMotionPath.testMotionPath_f10.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.animation.TestMotionPath.testMotionPath_f60.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.animation.TestMotionPath.testMotionPath_f60.png new file mode 100644 index 0000000000..cca8ae963f Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.animation.TestMotionPath.testMotionPath_f60.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_DefaultDirectionalLight_f12.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_DefaultDirectionalLight_f12.png new file mode 100644 index 0000000000..79b1dada0e Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_DefaultDirectionalLight_f12.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_HighRoughness_f12.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_HighRoughness_f12.png new file mode 100644 index 0000000000..bd1789c7e8 Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_HighRoughness_f12.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_LowRoughness_f12.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_LowRoughness_f12.png new file mode 100644 index 0000000000..1f734de5cf Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_LowRoughness_f12.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_UpdatedDirectionalLight_f12.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_UpdatedDirectionalLight_f12.png new file mode 100644 index 0000000000..15f219dd73 Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_UpdatedDirectionalLight_f12.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithRealtimeBaking_f10.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithRealtimeBaking_f10.png new file mode 100644 index 0000000000..f9bc99fe37 Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithRealtimeBaking_f10.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithoutRealtimeBaking_f10.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithoutRealtimeBaking_f10.png new file mode 100644 index 0000000000..f9bc99fe37 Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithoutRealtimeBaking_f10.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.material.TestSimpleBumps.testSimpleBumps_f10.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.material.TestSimpleBumps.testSimpleBumps_f10.png new file mode 100644 index 0000000000..4608681de3 Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.material.TestSimpleBumps.testSimpleBumps_f10.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.material.TestSimpleBumps.testSimpleBumps_f60.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.material.TestSimpleBumps.testSimpleBumps_f60.png new file mode 100644 index 0000000000..11ac17390f Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.material.TestSimpleBumps.testSimpleBumps_f60.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.scene.instancing.TestInstanceNodeWithPbr.testInstanceNodeWithPbr_f1.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.scene.instancing.TestInstanceNodeWithPbr.testInstanceNodeWithPbr_f1.png new file mode 100644 index 0000000000..c761934d2d Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.scene.instancing.TestInstanceNodeWithPbr.testInstanceNodeWithPbr_f1.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.scene.instancing.TestInstanceNodeWithPbr.testInstanceNodeWithPbr_f10.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.scene.instancing.TestInstanceNodeWithPbr.testInstanceNodeWithPbr_f10.png new file mode 100644 index 0000000000..2ddf12bb33 Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.scene.instancing.TestInstanceNodeWithPbr.testInstanceNodeWithPbr_f10.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_FinalRender_f5.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_FinalRender_f5.png new file mode 100644 index 0000000000..aa8c62dcd4 Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_FinalRender_f5.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_GeometryNormals_f5.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_GeometryNormals_f5.png new file mode 100644 index 0000000000..215274f23d Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_GeometryNormals_f5.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_MetallicMap_f5.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_MetallicMap_f5.png new file mode 100644 index 0000000000..dbbe6cf3a5 Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_MetallicMap_f5.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_NormalMap_f5.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_NormalMap_f5.png new file mode 100644 index 0000000000..791339d4c3 Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_NormalMap_f5.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_RoughnessMap_f5.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_RoughnessMap_f5.png new file mode 100644 index 0000000000..167ec2eeb7 Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_RoughnessMap_f5.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrainAdvanced.testPBRTerrainAdvanced_AmbientOcclusion_f5.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrainAdvanced.testPBRTerrainAdvanced_AmbientOcclusion_f5.png new file mode 100644 index 0000000000..5c515e1e26 Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrainAdvanced.testPBRTerrainAdvanced_AmbientOcclusion_f5.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrainAdvanced.testPBRTerrainAdvanced_Emissive_f5.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrainAdvanced.testPBRTerrainAdvanced_Emissive_f5.png new file mode 100644 index 0000000000..f1304956c3 Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrainAdvanced.testPBRTerrainAdvanced_Emissive_f5.png differ diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrainAdvanced.testPBRTerrainAdvanced_FinalRender_f5.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrainAdvanced.testPBRTerrainAdvanced_FinalRender_f5.png new file mode 100644 index 0000000000..aa8c62dcd4 Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrainAdvanced.testPBRTerrainAdvanced_FinalRender_f5.png differ diff --git a/jme3-terrain/src/main/resources/Common/MatDefs/Terrain/AdvancedPBRTerrain.frag b/jme3-terrain/src/main/resources/Common/MatDefs/Terrain/AdvancedPBRTerrain.frag index d889ec82b7..9cad93f886 100644 --- a/jme3-terrain/src/main/resources/Common/MatDefs/Terrain/AdvancedPBRTerrain.frag +++ b/jme3-terrain/src/main/resources/Common/MatDefs/Terrain/AdvancedPBRTerrain.frag @@ -49,7 +49,7 @@ void main(){ // read and blend up to 12 texture layers #for i=0..12 (#ifdef ALBEDOMAP_$i $0 #endif) - PBRTerrainTextureLayer terrainTextureLayer_$i = PBRTerrainUtils_createAdvancedPBRTerrainLayer($i); + PBRTerrainTextureLayer terrainTextureLayer_$i = PBRTerrainUtils_createAdvancedPBRTerrainLayer($i, surface.geometryNormal); #ifdef USE_FIRST_LAYER_AS_TRANSPARENCY if($i == 0){ @@ -59,29 +59,51 @@ void main(){ } #endif + terrainTextureLayer_$i.roughness = m_Roughness_$i; + terrainTextureLayer_$i.metallic = m_Metallic_$i; terrainTextureLayer_$i.emission = m_EmissiveColor_$i; - - #if defined(TRI_PLANAR_MAPPING) || defined(TRI_PLANAR_MAPPING_$i) - //triplanar: - - PBRTerrainUtils_readTriPlanarAlbedoTexArray(ALBEDOMAP_$i, m_AlbedoMap_$i_scale, m_AlbedoTextureArray, terrainTextureLayer_$i); - #ifdef NORMALMAP_$i - PBRTerrainUtils_readTriPlanarNormalTexArray(NORMALMAP_$i, m_AlbedoMap_$i_scale, m_NormalParallaxTextureArray, terrainTextureLayer_$i); - #endif - #ifdef METALLICROUGHNESSMAP_$i - PBRTerrainUtils_readTriPlanarMetallicRoughnessAoEiTexArray(METALLICROUGHNESSMAP_$i, m_AlbedoMap_$i_scale, m_MetallicRoughnessAoEiTextureArray, terrainTextureLayer_$i); - #endif - #else - //non tri-planar: - - PBRTerrainUtils_readAlbedoTexArray(ALBEDOMAP_$i, m_AlbedoMap_$i_scale, m_AlbedoTextureArray, terrainTextureLayer_$i); - #ifdef NORMALMAP_$i - PBRTerrainUtils_readNormalTexArray(NORMALMAP_$i, m_AlbedoMap_$i_scale, m_NormalParallaxTextureArray, terrainTextureLayer_$i); - #endif - #ifdef METALLICROUGHNESSMAP_$i - PBRTerrainUtils_readMetallicRoughnessAoEiTexArray(METALLICROUGHNESSMAP_$i, m_AlbedoMap_$i_scale, m_MetallicRoughnessAoEiTextureArray, terrainTextureLayer_$i); - #endif - #endif + + #ifdef USE_TEXTURE_ARRAYS + #if defined(TRI_PLANAR_MAPPING) || defined(TRI_PLANAR_MAPPING_$i) + //triplanar for texture arrays: + PBRTerrainUtils_readTriPlanarAlbedoTexArray(m_AlbedoMap_$i, m_AlbedoMap_$i_scale, m_AlbedoTextureArray, terrainTextureLayer_$i); + #ifdef NORMALMAP_$i + PBRTerrainUtils_readTriPlanarNormalTexArray(m_NormalMap_$i, m_AlbedoMap_$i_scale, m_NormalParallaxTextureArray, terrainTextureLayer_$i); + #endif + #ifdef METALLICROUGHNESSMAP_$i + PBRTerrainUtils_readTriPlanarMetallicRoughnessAoEiTexArray(m_MetallicRoughnessMap_$i, m_AlbedoMap_$i_scale, m_MetallicRoughnessAoEiTextureArray, terrainTextureLayer_$i); + #endif + #else + //non tri-planar for texture arrays: + PBRTerrainUtils_readAlbedoTexArray(m_AlbedoMap_$i, m_AlbedoMap_$i_scale, m_AlbedoTextureArray, terrainTextureLayer_$i); + #ifdef NORMALMAP_$i + PBRTerrainUtils_readNormalTexArray(m_NormalMap_$i, m_AlbedoMap_$i_scale, m_NormalParallaxTextureArray, terrainTextureLayer_$i); + #endif + #ifdef METALLICROUGHNESSMAP_$i + PBRTerrainUtils_readMetallicRoughnessAoEiTexArray(m_MetallicRoughnessMap_$i, m_AlbedoMap_$i_scale, m_MetallicRoughnessAoEiTextureArray, terrainTextureLayer_$i); + #endif + #endif + #else + #if defined(TRI_PLANAR_MAPPING) || defined(TRI_PLANAR_MAPPING_$i) + //triplanar texture reads: + PBRTerrainUtils_readTriPlanarAlbedoTexture(m_AlbedoMap_$i, m_AlbedoMap_$i_scale, terrainTextureLayer_$i); + #ifdef NORMALMAP_$i + PBRTerrainUtils_readTriPlanarNormalTexture(m_NormalMap_$i, m_AlbedoMap_$i_scale, terrainTextureLayer_$i); + #endif + #ifdef METALLICROUGHNESSMAP_$i + PBRTerrainUtils_readTriPlanarMetallicRoughnessAoEiTexture(m_MetallicRoughnessMap_$i, m_AlbedoMap_$i_scale, terrainTextureLayer_$i); + #endif + #else + //non tri-planar texture reads: + PBRTerrainUtils_readAlbedoTexture(m_AlbedoMap_$i, m_AlbedoMap_$i_scale, terrainTextureLayer_$i); + #ifdef NORMALMAP_$i + PBRTerrainUtils_readNormalTexture(m_NormalMap_$i, m_AlbedoMap_$i_scale, terrainTextureLayer_$i); + #endif + #ifdef METALLICROUGHNESSMAP_$i + PBRTerrainUtils_readMetallicRoughnessAoEiTexture(m_MetallicRoughnessMap_$i, m_AlbedoMap_$i_scale, terrainTextureLayer_$i); + #endif + #endif + #endif //CUSTOM LIB EXAMPLE: uses a custom alpha map to desaturate albedo color for a color-removal effect #ifdef AFFLICTIONTEXTURE @@ -129,8 +151,8 @@ void main(){ gl_FragColor.rgb += surface.directLightContribution; gl_FragColor.rgb += surface.envLightContribution; gl_FragColor.rgb += surface.emission; - gl_FragColor.a = surface.alpha; - + gl_FragColor.a = surface.alpha; + #ifdef USE_FOG gl_FragColor = MaterialFog_calculateFogColor(vec4(gl_FragColor)); #endif @@ -138,5 +160,5 @@ void main(){ //outputs the final value of the selected layer as a color for debug purposes. #ifdef DEBUG_VALUES_MODE gl_FragColor = PBRLightingUtils_getColorOutputForDebugMode(m_DebugValuesMode, vec4(gl_FragColor.rgba), surface); - #endif + #endif } diff --git a/jme3-terrain/src/main/resources/Common/MatDefs/Terrain/AdvancedPBRTerrain.j3md b/jme3-terrain/src/main/resources/Common/MatDefs/Terrain/AdvancedPBRTerrain.j3md index 105a38b8b0..2098bd77f3 100644 --- a/jme3-terrain/src/main/resources/Common/MatDefs/Terrain/AdvancedPBRTerrain.j3md +++ b/jme3-terrain/src/main/resources/Common/MatDefs/Terrain/AdvancedPBRTerrain.j3md @@ -4,8 +4,8 @@ MaterialDef AdvancedPBRTerrain { Int BoundDrawBuffer Texture2D SunLightExposureMap - Boolean UseVertexColorsAsSunIntensity //set true to make the vertex color's R channel how exposed a vertex is to the sun - Float StaticSunIntensity //used for setting the sun exposure value for a whole material + Boolean UseVertexColorsAsSunExposure //set true to make the vertex color's R channel how exposed a vertex is to the sun + Float StaticSunExposure //used for setting the sun exposure value for a whole material //these are usually generated at run time or setup in a level editor per-geometry, so that models indoors can have the DirectionalLight dimmed accordingly. Boolean BrightenIndoorShadows //set true if shadows are enabled and indoor areas without full sun exposure are too dark compared to when shadows are turned off in settings @@ -16,6 +16,9 @@ MaterialDef AdvancedPBRTerrain { TextureArray NormalParallaxTextureArray -LINEAR TextureArray MetallicRoughnessAoEiTextureArray -LINEAR + //The type of normal map: -1.0 (DirectX), 1.0 (OpenGl) + Float NormalType : -1.0 + // Specular-AA Boolean UseSpecularAA : true // screen space variance,Use the slider to set the strength of the geometric specular anti-aliasing effect between 0 and 1. Higher values produce a blurrier result with less aliasing. @@ -28,7 +31,6 @@ MaterialDef AdvancedPBRTerrain { Float AfflictionMetallicValue : 0.0 Float AfflictionEmissiveValue : 0.0 //note that this is simplified into one value, rather than 2 with power and intensity like the regular pbr values. - // affliction texture splatting & desaturation functionality Boolean UseTriplanarAfflictionMapping @@ -43,7 +45,6 @@ MaterialDef AdvancedPBRTerrain { Float SplatNoiseVar - Int AfflictionMode_0 : 1 Int AfflictionMode_1 : 1 Int AfflictionMode_2 : 1 @@ -109,7 +110,6 @@ MaterialDef AdvancedPBRTerrain { Int AlbedoMap_10 Int AlbedoMap_11 - Float AlbedoMap_0_scale : 1 Float AlbedoMap_1_scale : 1 Float AlbedoMap_2_scale : 1 @@ -136,7 +136,6 @@ MaterialDef AdvancedPBRTerrain { Boolean UseTriPlanarMapping_10 Boolean UseTriPlanarMapping_11 - Int NormalMap_0 Int NormalMap_1 Int NormalMap_2 @@ -150,7 +149,6 @@ MaterialDef AdvancedPBRTerrain { Int NormalMap_10 Int NormalMap_11 - Int MetallicRoughnessMap_0 Int MetallicRoughnessMap_1 Int MetallicRoughnessMap_2 @@ -164,7 +162,6 @@ MaterialDef AdvancedPBRTerrain { Int MetallicRoughnessMap_10 Int MetallicRoughnessMap_11 - Float ParallaxHeight_0 Float ParallaxHeight_1 Float ParallaxHeight_2 @@ -178,13 +175,11 @@ MaterialDef AdvancedPBRTerrain { Float ParallaxHeight_10 Float ParallaxHeight_11 - - //used in order to convert world coords to tex coords so afflictionTexture accurately represents the world in cases where terrain is not scaled at a 1,1,1 value Float TileWidth : 0 Vector3 TileLocation - // debug the final value of the selected layer as a color output + // debug the final value of the selected layer as a color output Int DebugValuesMode // Layers: // 0 - albedo (unshaded) @@ -195,6 +190,7 @@ MaterialDef AdvancedPBRTerrain { // 5 - emissive // 6 - exposure // 7 - alpha + // 8 - geometryNormals // use tri-planar mapping Boolean useTriPlanarMapping @@ -204,13 +200,6 @@ MaterialDef AdvancedPBRTerrain { Texture2D AlphaMap_1 -LINEAR Texture2D AlphaMap_2 -LINEAR - Boolean UseSpecGloss - Texture2D SpecularMap - Texture2D GlossinessMap - Texture2D SpecularGlossinessMap - Color Specular : 1.0 1.0 1.0 1.0 - Float Glossiness : 1.0 - Vector4 ProbeData // Prefiltered Env Map for indirect specular lighting @@ -222,7 +211,6 @@ MaterialDef AdvancedPBRTerrain { //integrate BRDF map for indirect Lighting Texture2D IntegrateBRDF -LINEAR - //shadows Int FilterMode Boolean HardwareShadows @@ -289,7 +277,6 @@ MaterialDef AdvancedPBRTerrain { ViewProjectionMatrix ViewMatrix Time - } Defines { @@ -301,10 +288,12 @@ MaterialDef AdvancedPBRTerrain { FOG_EXPSQ : ExpSqFog EXPOSUREMAP : SunLightExposureMap - USE_VERTEX_COLORS_AS_SUN_EXPOSURE : UseVertexColorsAsSunIntensity - STATIC_SUN_EXPOSURE : StaticSunIntensity + USE_VERTEX_COLORS_AS_SUN_EXPOSURE : UseVertexColorsAsSunExposure + STATIC_SUN_EXPOSURE : StaticSunExposure BRIGHTEN_INDOOR_SHADOWS : BrightenIndoorShadows + NORMAL_TYPE: NormalType + USE_FIRST_LAYER_AS_TRANSPARENCY : UseFirstLayerAsTransparency SPECULAR_AA : UseSpecularAA @@ -323,6 +312,8 @@ MaterialDef AdvancedPBRTerrain { AFFLICTIONEMISSIVEMAP : SplatEmissiveMap USE_SPLAT_NOISE : SplatNoiseVar + USE_TRIPLANAR_AFFLICTION_MAPPING : UseTriplanarAfflictionMapping + TRI_PLANAR_MAPPING : useTriPlanarMapping ALPHAMAP : AlphaMap @@ -383,10 +374,10 @@ MaterialDef AdvancedPBRTerrain { DEBUG_VALUES_MODE : DebugValuesMode + USE_TEXTURE_ARRAYS : AlbedoTextureArray } } - Technique PreShadow { VertexShader GLSL300 GLSL150 GLSL100 : Common/MatDefs/Shadow/PreShadow.vert diff --git a/jme3-terrain/src/main/resources/Common/MatDefs/Terrain/Modular/PBRTerrainUtils.glsllib b/jme3-terrain/src/main/resources/Common/MatDefs/Terrain/Modular/PBRTerrainUtils.glsllib index 9dc9390c45..e731ad2e27 100644 --- a/jme3-terrain/src/main/resources/Common/MatDefs/Terrain/Modular/PBRTerrainUtils.glsllib +++ b/jme3-terrain/src/main/resources/Common/MatDefs/Terrain/Modular/PBRTerrainUtils.glsllib @@ -7,11 +7,17 @@ #import "Common/ShaderLib/TriPlanarUtils.glsllib" #ifdef ENABLE_PBRTerrainUtils_readPBRTerrainLayers + + #ifndef NORMAL_TYPE + #define NORMAL_TYPE -1.0 + #endif - //texture arrays: - uniform sampler2DArray m_AlbedoTextureArray; - uniform sampler2DArray m_NormalParallaxTextureArray; - uniform sampler2DArray m_MetallicRoughnessAoEiTextureArray; + #ifdef USE_TEXTURE_ARRAYS + //texture arrays: + uniform sampler2DArray m_AlbedoTextureArray; + uniform sampler2DArray m_NormalParallaxTextureArray; + uniform sampler2DArray m_MetallicRoughnessAoEiTextureArray; + #endif //texture-slot params for 12 unique texture slots (0-11) where the integer value points to the desired texture's index in the corresponding texture array: #for i=0..12 (#ifdef ALBEDOMAP_$i $0 #endif) @@ -21,15 +27,25 @@ uniform float m_AlbedoMap_$i_scale; uniform vec4 m_EmissiveColor_$i; - uniform int m_AlbedoMap_$i; + #ifdef USE_TEXTURE_ARRAYS + uniform int m_AlbedoMap_$i; + #ifdef NORMALMAP_$i + uniform int m_NormalMap_$i; + #endif + #ifdef METALLICROUGHNESSMAP_$i + uniform int m_MetallicRoughnessMap_$i; + #endif + #else + uniform sampler2D m_AlbedoMap_$i; + #ifdef NORMALMAP_$i + uniform sampler2D m_NormalMap_$i; + #endif + #ifdef METALLICROUGHNESSMAP_$i + uniform sampler2D m_MetallicRoughnessMap_$i; + #endif + #endif #endfor - #for n=0..12 (#ifdef METALLICROUGHNESSMAP_$n $0 #endif) - uniform int m_MetallicRoughnessMap_$n; - #endfor - #for x=0..12 (#ifdef NORMALMAP_$x $0 #endif) - uniform int m_NormalMap_$x; - #endfor //3 alpha maps : #ifdef ALPHAMAP @@ -41,9 +57,9 @@ #ifdef ALPHAMAP_2 uniform sampler2D m_AlphaMap_2; #endif + vec4 alphaBlend_0, alphaBlend_1, alphaBlend_2; - void PBRTerrainUtils_readAlphaMaps(){ #ifdef ALPHAMAP @@ -54,9 +70,7 @@ #endif #ifdef ALPHAMAP_2 alphaBlend_2 = texture2D( m_AlphaMap_2, texCoord.xy ); - #endif - - + #endif } float PBRTerrainUtils_getAlphaBlendFromChannel(int layer){ @@ -85,80 +99,124 @@ break; } + finalAlphaBlendForLayer = clamp(finalAlphaBlendForLayer, 0.0, 1.0); + return finalAlphaBlendForLayer; } - - PBRTerrainTextureLayer PBRTerrainUtils_createAdvancedPBRTerrainLayer(int layerNum){ + PBRTerrainTextureLayer PBRTerrainUtils_createAdvancedPBRTerrainLayer(int layerNum, vec3 geometryNormal){ PBRTerrainTextureLayer terrainTextureLayer; terrainTextureLayer.blendValue = PBRTerrainUtils_getAlphaBlendFromChannel(layerNum); + terrainTextureLayer.albedo = vec4(1.0); + terrainTextureLayer.emission = vec4(0.0); + terrainTextureLayer.normal = geometryNormal; + terrainTextureLayer.alpha = 1.0; + terrainTextureLayer.ao = 1.0; + terrainTextureLayer.roughness = 1.0; + terrainTextureLayer.metallic = 0.0; + terrainTextureLayer.height = 0.0; return terrainTextureLayer; - } + } - //________ - + //3 functions to update layers from respective packed data vecs: void updateLayerFromPackedAlbedoMap(inout vec4 packedAlbedoVec, inout PBRTerrainTextureLayer layer){ layer.albedo = packedAlbedoVec; layer.alpha = packedAlbedoVec.a; } void updateLayerFromPackedNormalParallaxVec(inout vec4 packedNormalParallaxVec, inout PBRTerrainTextureLayer layer){ - layer.normal = calculateTangentsAndApplyToNormals(packedNormalParallaxVec.rgb, PBRLightingUtils_getWorldNormal()); + layer.normal = normalize(calculateTangentsAndApplyToNormals(packedNormalParallaxVec.rgb, PBRLightingUtils_getWorldNormal())); layer.height = packedNormalParallaxVec.a; } void updateLayerFromPackedMRAoEiVec(inout vec4 packedMRAoEiVec, inout PBRTerrainTextureLayer layer){ - layer.ao = packedMRAoEiVec.r; - layer.roughness = packedMRAoEiVec.g; - layer.metallic = packedMRAoEiVec.b; + layer.ao = packedMRAoEiVec.r; //ao only comes from texture (no float scalars) so no *= is done here + layer.roughness *= packedMRAoEiVec.g; + layer.metallic *= packedMRAoEiVec.b; layer.emission *= packedMRAoEiVec.a * layer.emission.a; + } + //________________________________ + // Basic Texture Reads: + + // Albedo: + void PBRTerrainUtils_readAlbedoTexture(in sampler2D tex, in float scale, inout PBRTerrainTextureLayer layer){ + vec4 packedAlbedoVec = texture2D(tex, texCoord * scale); + updateLayerFromPackedAlbedoMap(packedAlbedoVec, layer); } - //________ - - // read Triplanar Albedo from TextureArray: - void PBRTerrainUtils_readTriPlanarAlbedoTexArray(in int indexInTexArray, in float scale, in sampler2DArray texArray, inout PBRTerrainTextureLayer layer){ - vec4 packedAlbedoVec = getTriPlanarBlendFromTexArray(lPosition, indexInTexArray, scale, texArray); + // normal: + void PBRTerrainUtils_readNormalTexture(in sampler2D tex, in float scale, inout PBRTerrainTextureLayer layer){ + vec4 packedNormalParallaxVec = texture2D(tex, texCoord * scale); + packedNormalParallaxVec.xyz = normalize(packedNormalParallaxVec.xyz * vec3(2.0, NORMAL_TYPE * 2.0, 2.0) - vec3(1.0, NORMAL_TYPE * 1.0, 1.0)); + updateLayerFromPackedNormalParallaxVec(packedNormalParallaxVec, layer); + } + // metallicRoughnessAoEi: + void PBRTerrainUtils_readMetallicRoughnessAoEiTexture(in sampler2D tex, in float scale, inout PBRTerrainTextureLayer layer){ + vec4 packedMRAoEi = texture2D(tex, texCoord * scale); + updateLayerFromPackedMRAoEiVec(packedMRAoEi, layer); + } + //________________________________ + // Basic Triplanar Reads: + + // Triplanar Albedo: + void PBRTerrainUtils_readTriPlanarAlbedoTexture(in sampler2D tex, in float scale, inout PBRTerrainTextureLayer layer){ + vec4 packedAlbedoVec = getTriPlanarBlend(lPosition, tex, scale); updateLayerFromPackedAlbedoMap(packedAlbedoVec, layer); } - // read Triplanar normal from TextureArray: - void PBRTerrainUtils_readTriPlanarNormalTexArray(in int indexInTexArray, in float scale, in sampler2DArray texArray, inout PBRTerrainTextureLayer layer){ - vec4 packedNormalParallaxVec = getTriPlanarBlendFromTexArray(lPosition, indexInTexArray, scale, texArray); + // Triplanar normal: + void PBRTerrainUtils_readTriPlanarNormalTexture(in sampler2D tex, in float scale, inout PBRTerrainTextureLayer layer){ + vec4 packedNormalParallaxVec = getTriPlanarNormalBlend(lPosition, tex, scale); updateLayerFromPackedNormalParallaxVec(packedNormalParallaxVec, layer); } - // read TriPlanar metallicRoughnessAoEi from TextureArray: - void PBRTerrainUtils_readTriPlanarMetallicRoughnessAoEiTexArray(in int indexInTexArray, in float scale, in sampler2DArray texArray, inout PBRTerrainTextureLayer layer){ - vec4 packedMRAoEi = getTriPlanarBlendFromTexArray(lPosition, indexInTexArray, scale, texArray); + // TriPlanar metallicRoughnessAoEi: + void PBRTerrainUtils_readTriPlanarMetallicRoughnessAoEiTexture(in sampler2D tex, in float scale, inout PBRTerrainTextureLayer layer){ + vec4 packedMRAoEi = getTriPlanarBlend(lPosition, tex, scale); updateLayerFromPackedMRAoEiVec(packedMRAoEi, layer); - } - //________ - - // read Albedo from TextureArray: + } + //________________________________ + // Basic TexArray reads: + + // Albedo TextureArray: void PBRTerrainUtils_readAlbedoTexArray(in int indexInTexArray, in float scale, in sampler2DArray texArray, inout PBRTerrainTextureLayer layer){ vec4 packedAlbedoVec = texture2DArray(texArray, vec3(texCoord * scale, indexInTexArray)); - updateLayerFromPackedAlbedoMap(packedAlbedoVec, layer); - + updateLayerFromPackedAlbedoMap(packedAlbedoVec, layer); } - // read Normal from TextureArray: + // Normal TextureArray: void PBRTerrainUtils_readNormalTexArray(in int indexInTexArray, in float scale, in sampler2DArray texArray, inout PBRTerrainTextureLayer layer){ vec4 packedNormalParallaxVec = texture2DArray(texArray, vec3(texCoord * scale, indexInTexArray)); + packedNormalParallaxVec.xyz = normalize(packedNormalParallaxVec.xyz * vec3(2.0, NORMAL_TYPE * 2.0, 2.0) - vec3(1.0, NORMAL_TYPE * 1.0, 1.0)); updateLayerFromPackedNormalParallaxVec(packedNormalParallaxVec, layer); } - // read metallicRoughnessAoEi from TextureArray: + // metallicRoughnessAoEi TextureArray: void PBRTerrainUtils_readMetallicRoughnessAoEiTexArray(in int indexInTexArray, float scale, in sampler2DArray texArray, inout PBRTerrainTextureLayer layer){ vec4 packedMRAoEi = texture2DArray(texArray, vec3(texCoord * scale, indexInTexArray)); - updateLayerFromPackedMRAoEiVec(packedMRAoEi, layer); - + updateLayerFromPackedMRAoEiVec(packedMRAoEi, layer); } - //________ - - - - void PBRTerrainUtils_blendPBRTerrainLayer(inout PBRSurface surface, inout PBRTerrainTextureLayer layer){ - - - //mix values from this index layer to final output values based on finalAlphaBlendForLayer + //________________________________ + // Triplanar TexArray reads: + + // Triplana Albedo TextureArray: + void PBRTerrainUtils_readTriPlanarAlbedoTexArray(in int indexInTexArray, in float scale, in sampler2DArray texArray, inout PBRTerrainTextureLayer layer){ + vec4 packedAlbedoVec = getTriPlanarBlendFromTexArray(lPosition, indexInTexArray, scale, texArray); + updateLayerFromPackedAlbedoMap(packedAlbedoVec, layer); + } + // Triplanar normal TextureArray: + void PBRTerrainUtils_readTriPlanarNormalTexArray(in int indexInTexArray, in float scale, in sampler2DArray texArray, inout PBRTerrainTextureLayer layer){ + vec4 packedNormalParallaxVec = getTriPlanarNormalBlendFromTexArray(lPosition, indexInTexArray, scale, texArray); + updateLayerFromPackedNormalParallaxVec(packedNormalParallaxVec, layer); + } + // TriPlanar metallicRoughnessAoEi TextureArray: + void PBRTerrainUtils_readTriPlanarMetallicRoughnessAoEiTexArray(in int indexInTexArray, in float scale, in sampler2DArray texArray, inout PBRTerrainTextureLayer layer){ + vec4 packedMRAoEi = getTriPlanarBlendFromTexArray(lPosition, indexInTexArray, scale, texArray); + updateLayerFromPackedMRAoEiVec(packedMRAoEi, layer); + } + //_______________________________ + + //blend layer function. This mixes each layer's pbr vars over top of the current surface values based on the layer's blendValue + void PBRTerrainUtils_blendPBRTerrainLayer(inout PBRSurface surface, inout PBRTerrainTextureLayer layer){ + layer.ao = clamp(layer.ao, 0.0, 1.0); + surface.albedo = mix(surface.albedo, layer.albedo.rgb, layer.blendValue); - surface.normal = mix(surface.normal.rgb, layer.normal, layer.blendValue); + surface.normal = normalize(mix(surface.normal.rgb, layer.normal, layer.blendValue)); surface.metallic = mix(surface.metallic, layer.metallic, layer.blendValue); surface.roughness = mix(surface.roughness, layer.roughness, layer.blendValue); surface.ao = mix(surface.ao, vec3(layer.ao), layer.blendValue); @@ -167,4 +225,3 @@ #endif #endif - diff --git a/jme3-terrain/src/main/resources/Common/MatDefs/Terrain/PBRTerrain.frag b/jme3-terrain/src/main/resources/Common/MatDefs/Terrain/PBRTerrain.frag deleted file mode 100644 index b3705383c3..0000000000 --- a/jme3-terrain/src/main/resources/Common/MatDefs/Terrain/PBRTerrain.frag +++ /dev/null @@ -1,601 +0,0 @@ -#import "Common/ShaderLib/GLSLCompat.glsllib" -#import "Common/ShaderLib/PBR.glsllib" -#import "Common/ShaderLib/Parallax.glsllib" -#import "Common/ShaderLib/Lighting.glsllib" -#import "Common/MatDefs/Terrain/AfflictionLib.glsllib" - -varying vec3 wPosition; -varying vec3 vNormal; -varying vec2 texCoord; -uniform vec3 g_CameraPosition; -varying vec3 vPosition; -varying vec3 vnPosition; -varying vec3 vViewDir; -varying vec4 vLightDir; -varying vec4 vnLightDir; -varying vec3 lightVec; -varying vec3 inNormal; -varying vec3 wNormal; - -// Specular-AA -#ifdef SPECULAR_AA_SCREEN_SPACE_VARIANCE - uniform float m_SpecularAASigma; -#endif -#ifdef SPECULAR_AA_THRESHOLD - uniform float m_SpecularAAKappa; -#endif - -#ifdef DEBUG_VALUES_MODE - uniform int m_DebugValuesMode; -#endif - -uniform vec4 g_LightData[NB_LIGHTS]; -uniform vec4 g_AmbientLightColor; - -#if NB_PROBES >= 1 - uniform samplerCube g_PrefEnvMap; - uniform vec3 g_ShCoeffs[9]; - uniform mat4 g_LightProbeData; -#endif -#if NB_PROBES >= 2 - uniform samplerCube g_PrefEnvMap2; - uniform vec3 g_ShCoeffs2[9]; - uniform mat4 g_LightProbeData2; -#endif -#if NB_PROBES == 3 - uniform samplerCube g_PrefEnvMap3; - uniform vec3 g_ShCoeffs3[9]; - uniform mat4 g_LightProbeData3; -#endif - -#ifdef TRI_PLANAR_MAPPING - varying vec4 wVertex; -#endif - -//texture-slot params for 12 unique texture slots (0-11) : -#for i=0..12 ( $0 ) - uniform int m_AfflictionMode_$i; - uniform float m_Roughness_$i; - uniform float m_Metallic_$i; - - #ifdef ALBEDOMAP_$i - uniform sampler2D m_AlbedoMap_$i; - #endif - #ifdef ALBEDOMAP_$i_SCALE - uniform float m_AlbedoMap_$i_scale; - #endif - #ifdef NORMALMAP_$i - uniform sampler2D m_NormalMap_$i; - #endif -#endfor - -//3 alpha maps : -#ifdef ALPHAMAP - uniform sampler2D m_AlphaMap; -#endif -#ifdef ALPHAMAP_1 - uniform sampler2D m_AlphaMap_1; -#endif -#ifdef ALPHAMAP_2 - uniform sampler2D m_AlphaMap_2; -#endif - -#ifdef DISCARD_ALPHA - uniform float m_AlphaDiscardThreshold; -#endif - -//fog vars for basic fog : -#ifdef USE_FOG -#import "Common/ShaderLib/MaterialFog.glsllib" - uniform vec4 m_FogColor; - float fogDistance; - - uniform vec2 m_LinearFog; -#endif -#ifdef FOG_EXP - uniform float m_ExpFog; -#endif -#ifdef FOG_EXPSQ - uniform float m_ExpSqFog; -#endif - -//sun intensity is a secondary AO value that can be painted per-vertex in the red channel of the -// vertex colors, or it can be set as a static value for an entire material with the StaticSunIntensity float param -#if defined(USE_VERTEX_COLORS_AS_SUN_INTENSITY) - varying vec4 vertColors; -#endif - -#ifdef STATIC_SUN_INTENSITY - uniform float m_StaticSunIntensity; -#endif -//sun intensity AO value is only applied to the directional light, not to point lights, so it is important to track if the -//sun is more/less bright than the brightest point light for each fragment to determine how the light probe's ambient light should be scaled later on in light calculation code -float brightestPointLight = 0.0; - -//optional affliction paramaters that use the AfflictionAlphaMap's green channel for splatting m_SplatAlbedoMap and the red channel for splatting desaturation : -#ifdef AFFLICTIONTEXTURE - uniform sampler2D m_AfflictionAlphaMap; -#endif -#ifdef USE_SPLAT_NOISE - uniform float m_SplatNoiseVar; -#endif -//only defined for non-terrain geoemtries and terrains that are not positioned nor sized in correlation to the 2d array of AfflictionAlphaMaps used for splatting accross large tile based scenes in a grid -#ifdef TILELOCATION - uniform float m_TileWidth; - uniform vec3 m_TileLocation; -#endif -#ifdef AFFLICTIONALBEDOMAP - uniform sampler2D m_SplatAlbedoMap; -#endif -#ifdef AFFLICTIONNORMALMAP - uniform sampler2D m_SplatNormalMap; -#endif -#ifdef AFFLICTIONROUGHNESSMETALLICMAP - uniform sampler2D m_SplatRoughnessMetallicMap; -#endif -#ifdef AFFLICTIONEMISSIVEMAP - uniform sampler2D m_SplatEmissiveMap; -#endif - -uniform int m_AfflictionSplatScale; -uniform float m_AfflictionRoughnessValue; -uniform float m_AfflictionMetallicValue; -uniform float m_AfflictionEmissiveValue; -uniform vec4 m_AfflictionEmissiveColor; - -vec4 afflictionVector; -float noiseHash; -float livelinessValue; -float afflictionValue; -int afflictionMode = 1; - -//general temp vars : -vec4 tempAlbedo, tempNormal, tempEmissiveColor; -float tempParallax, tempMetallic, tempRoughness, tempAo, tempEmissiveIntensity; - -vec3 viewDir; -vec2 coord; -vec4 albedo = vec4(1.0); -vec3 normal = vec3(0.5,0.5,1); -vec3 norm; -float Metallic; -float Roughness; -float packedAoValue = 1.0; -vec4 emissive; -float emissiveIntensity = 1.0; -float indoorSunLightExposure = 1.0; - -vec4 packedMetallicRoughnessAoEiVec; -vec4 packedNormalParallaxVec; - -void main(){ - - #ifdef USE_FOG - fogDistance = distance(g_CameraPosition, wPosition.xyz); - #endif - - indoorSunLightExposure = 1.0; - - viewDir = normalize(g_CameraPosition - wPosition); - - norm = normalize(wNormal); - normal = norm; - - afflictionVector = vec4(1.0, 0.0, 1.0, 0.0); //r channel is sturation, g channel is affliction splat texture intensity, b and a unused (might use b channel for wetness eventually) - - #ifdef AFFLICTIONTEXTURE - - #ifdef TILELOCATION - //subterrains that are not centred in tile or equal to tile width in total size need to have m_TileWidth pre-set. (tileWidth is the x,z dimesnions that the AfflictionAlphaMap represents) - vec2 tileCoords; - float xPos, zPos; - - vec3 locInTile = (wPosition - m_TileLocation); - - locInTile += vec3(m_TileWidth/2, 0, m_TileWidth/2); - - xPos = (locInTile.x / m_TileWidth); - zPos = 1 - (locInTile.z / m_TileWidth); - - tileCoords = vec2(xPos, zPos); - - afflictionVector = texture2D(m_AfflictionAlphaMap, tileCoords).rgba; - #else - // ..othrewise when terrain size matches tileWidth and location matches tileLocation, the terrain's texCoords can be used for simple texel fetching of the AfflictionAlphaMap - afflictionVector = texture2D(m_AfflictionAlphaMap, texCoord.xy).rgba; - #endif - #endif - - livelinessValue = afflictionVector.r; - afflictionValue = afflictionVector.g; - - #ifdef ALBEDOMAP_0 - #ifdef ALPHAMAP - - vec4 alphaBlend; - vec4 alphaBlend_0, alphaBlend_1, alphaBlend_2; - int texChannelForAlphaBlending; - - alphaBlend_0 = texture2D( m_AlphaMap, texCoord.xy ); - - #ifdef ALPHAMAP_1 - alphaBlend_1 = texture2D( m_AlphaMap_1, texCoord.xy ); - #endif - #ifdef ALPHAMAP_2 - alphaBlend_2 = texture2D( m_AlphaMap_2, texCoord.xy ); - #endif - - vec2 texSlotCoords; - - float finalAlphaBlendForLayer = 1.0; - - vec3 blending = abs( norm ); - blending = (blending -0.2) * 0.7; - blending = normalize(max(blending, 0.00001)); // Force weights to sum to 1.0 (very important!) - float b = (blending.x + blending.y + blending.z); - blending /= vec3(b, b, b); - - #for i=0..12 (#ifdef ALBEDOMAP_$i $0 #endif) - - //assign texture slot's blending from index's correct alpha map - if($i <= 3){ - alphaBlend = alphaBlend_0; - }else if($i <= 7){ - alphaBlend = alphaBlend_1; - }else if($i <= 11){ - alphaBlend = alphaBlend_2; - } - - texChannelForAlphaBlending = int(mod(float($i), 4.0)); //pick the correct channel (r g b or a) based on the layer's index - switch(texChannelForAlphaBlending) { - case 0: - finalAlphaBlendForLayer = alphaBlend.r; - break; - case 1: - finalAlphaBlendForLayer = alphaBlend.g; - break; - case 2: - finalAlphaBlendForLayer = alphaBlend.b; - break; - case 3: - finalAlphaBlendForLayer = alphaBlend.a; - break; - } - - afflictionMode = m_AfflictionMode_$i; - - #ifdef TRI_PLANAR_MAPPING - //tri planar - tempAlbedo = getTriPlanarBlend(wVertex, blending, m_AlbedoMap_$i, m_AlbedoMap_$i_scale); - - #ifdef NORMALMAP_$i - tempNormal.rgb = getTriPlanarBlend(wVertex, blending, m_NormalMap_$i, m_AlbedoMap_$i_scale).rgb; - tempNormal.rgb = calculateTangentsAndApplyToNormals(tempNormal.rgb, wNormal);// this gets rid of the need for pre-generating tangents for TerrainPatches, since doing so doesn't seem to work (tbnMat is always blank for terrains even with tangents pre-generated, not sure why...) - #else - tempNormal.rgb = wNormal.rgb; - #endif - #else - - // non triplanar - texSlotCoords = texCoord * m_AlbedoMap_$i_scale; - - tempAlbedo.rgb = texture2D(m_AlbedoMap_$i, texSlotCoords).rgb; - #ifdef NORMALMAP_$i - tempNormal.xyz = texture2D(m_NormalMap_$i, texSlotCoords).xyz; - tempNormal.rgb = calculateTangentsAndApplyToNormals(tempNormal.rgb, wNormal); - #else - tempNormal.rgb = wNormal.rgb; - #endif - #endif - - //note: most of these functions can be found in AfflictionLib.glslib - tempAlbedo.rgb = alterLiveliness(tempAlbedo.rgb, livelinessValue, afflictionMode); //changes saturation of albedo for this layer; does nothing if not using AfflictionAlphaMap for affliction splatting - - //mix values from this index layer to final output values based on finalAlphaBlendForLayer - albedo.rgb = mix(albedo.rgb, tempAlbedo.rgb , finalAlphaBlendForLayer); - normal.rgb = mix(normal.rgb, tempNormal.rgb, finalAlphaBlendForLayer); - Metallic = mix(Metallic, m_Metallic_$i, finalAlphaBlendForLayer); - Roughness = mix(Roughness, m_Roughness_$i, finalAlphaBlendForLayer); - - #endfor - #endif - #endif - - - float alpha = albedo.a; - #ifdef DISCARD_ALPHA - if(alpha < m_AlphaDiscardThreshold){ - discard; - } - #endif - - - //APPLY AFFLICTIONN TO THE PIXEL - #ifdef AFFLICTIONTEXTURE - vec4 afflictionAlbedo; - - float newAfflictionScale = m_AfflictionSplatScale; - vec2 newScaledCoords; - - #ifdef AFFLICTIONALBEDOMAP - #ifdef TRI_PLANAR_MAPPING - newAfflictionScale = newAfflictionScale / 256; - afflictionAlbedo = getTriPlanarBlend(wVertex, blending, m_SplatAlbedoMap , newAfflictionScale); - #else - newScaledCoords = mod(wPosition.xz / m_AfflictionSplatScale, 0.985); - afflictionAlbedo = texture2D(m_SplatAlbedoMap , newScaledCoords); - #endif - - #else - afflictionAlbedo = vec4(1.0, 1.0, 1.0, 1.0); - #endif - - vec3 afflictionNormal; - #ifdef AFFLICTIONNORMALMAP - #ifdef TRI_PLANAR_MAPPING - - afflictionNormal = getTriPlanarBlend(wVertex, blending, m_SplatNormalMap , newAfflictionScale).rgb; - - #else - afflictionNormal = texture2D(m_SplatNormalMap , newScaledCoords).rgb; - #endif - - #else - afflictionNormal = norm; - - #endif - float afflictionMetallic = m_AfflictionMetallicValue; - float afflictionRoughness = m_AfflictionRoughnessValue; - float afflictionAo = 1.0; - - - vec4 afflictionEmissive = m_AfflictionEmissiveColor; - float afflictionEmissiveIntensity = m_AfflictionEmissiveValue; - - - #ifdef AFFLICTIONROUGHNESSMETALLICMAP - vec4 metallicRoughnessAoEiVec = texture2D(m_SplatRoughnessMetallicMap, newScaledCoords); - afflictionRoughness *= metallicRoughnessAoEiVec.g; - afflictionMetallic *= metallicRoughnessAoEiVec.b; - afflictionAo = metallicRoughnessAoEiVec.r; - afflictionEmissiveIntensity *= metallicRoughnessAoEiVec.a; //important not to leave this channel all black by accident when creating the mraoei map if using affliction emissiveness - - #endif - - #ifdef AFFLICTIONEMISSIVEMAP - vec4 emissiveMapColor = texture2D(m_SplatEmissiveMap, newScaledCoords); - afflictionEmissive *= emissiveMapColor; - #endif - - float adjustedAfflictionValue = afflictionValue; - #ifdef USE_SPLAT_NOISE - noiseHash = getStaticNoiseVar0(wPosition, afflictionValue * m_SplatNoiseVar); - - adjustedAfflictionValue = getAdjustedAfflictionVar(afflictionValue); - if(afflictionValue >= 0.99){ - adjustedAfflictionValue = afflictionValue; - } - #else - noiseHash = 1.0; - #endif - - Roughness = alterAfflictionRoughness(adjustedAfflictionValue, Roughness, afflictionRoughness, noiseHash); - Metallic = alterAfflictionMetallic(adjustedAfflictionValue, Metallic, afflictionMetallic, noiseHash); - albedo = alterAfflictionColor(adjustedAfflictionValue, albedo, afflictionAlbedo, noiseHash ); - normal = alterAfflictionNormalsForTerrain(adjustedAfflictionValue, normal, afflictionNormal, noiseHash , wNormal); - emissive = alterAfflictionGlow(adjustedAfflictionValue, emissive, afflictionEmissive, noiseHash); - emissiveIntensity = alterAfflictionEmissiveIntensity(adjustedAfflictionValue, emissiveIntensity, afflictionEmissiveIntensity, noiseHash); - emissiveIntensity *= afflictionEmissive.a; - //affliction ao value blended below after specular calculation - #endif - -// spec gloss pipeline code would go here if supported, but likely will not be for terrain shaders as defines are limited and heavily used - -float specular = 0.5; -float nonMetalSpec = 0.08 * specular; -vec4 specularColor = (nonMetalSpec - nonMetalSpec * Metallic) + albedo * Metallic; -vec4 diffuseColor = albedo - albedo * Metallic; -vec3 fZero = vec3(specular); - -gl_FragColor.rgb = vec3(0.0); - -//simple ao calculation, no support for lightmaps like stock pbr shader.. (probably could add lightmap support with another texture array, but -// that would add another texture read per slot and require removing 12 other defines to make room...) - vec3 ao = vec3(packedAoValue); - - #ifdef AFFLICTIONTEXTURE - ao = alterAfflictionAo(afflictionValue, ao, vec3(afflictionAo), noiseHash); // alter the AO map for affliction values - #endif - ao.rgb = ao.rrr; - specularColor.rgb *= ao; - - #ifdef STATIC_SUN_INTENSITY - indoorSunLightExposure = m_StaticSunIntensity; //single float value to indicate percentage of - //sunlight hitting the model (only works for small models or models with 100% consistent sunlighting accross every pixel) - #endif - #ifdef USE_VERTEX_COLORS_AS_SUN_INTENSITY - indoorSunLightExposure = vertColors.r * indoorSunLightExposure; //use R channel of vertexColors for.. - #endif - // similar purpose as above... - //but uses r channel vert colors like an AO map specifically - //for sunlight (solution for scaling lighting for indoor - // and shadey/dimly lit models, especially big ones that - // span accross varying directionalLight exposure) - brightestPointLight = 0.0; - - - float ndotv = max( dot( normal, viewDir ),0.0); - #ifdef SPECULAR_AA - float sigma = 1.0; - float kappa = 0.18; - #ifdef SPECULAR_AA_SCREEN_SPACE_VARIANCE - sigma = m_SpecularAASigma; - #endif - #ifdef SPECULAR_AA_THRESHOLD - kappa = m_SpecularAAKappa; - #endif - #endif - for( int i = 0;i < NB_LIGHTS; i+=3){ - vec4 lightColor = g_LightData[i]; - vec4 lightData1 = g_LightData[i+1]; - vec4 lightDir; - vec3 lightVec; - lightComputeDir(wPosition, lightColor.w, lightData1, lightDir, lightVec); - - float fallOff = 1.0; - #if __VERSION__ >= 110 - // allow use of control flow - if(lightColor.w > 1.0){ - #endif - fallOff = computeSpotFalloff(g_LightData[i+2], lightVec); - #if __VERSION__ >= 110 - } - #endif - //point light attenuation - fallOff *= lightDir.w; - - lightDir.xyz = normalize(lightDir.xyz); - vec3 directDiffuse; - vec3 directSpecular; - - #ifdef SPECULAR_AA - float hdotv = PBR_ComputeDirectLightWithSpecularAA( - normal, lightDir.xyz, viewDir, - lightColor.rgb, fZero, Roughness, sigma, kappa, ndotv, - directDiffuse, directSpecular); - #else - float hdotv = PBR_ComputeDirectLight( - normal, lightDir.xyz, viewDir, - lightColor.rgb, fZero, Roughness, ndotv, - directDiffuse, directSpecular); - #endif - - vec3 directLighting = diffuseColor.rgb *directDiffuse + directSpecular; - - #if defined(USE_VERTEX_COLORS_AS_SUN_INTENSITY) || defined(STATIC_SUN_INTENSITY) - if(fallOff == 1.0){ - directLighting.rgb *= indoorSunLightExposure;// ... *^. to scale down how intense just the sun is (ambient and direct light are 1.0 fallOff) - - } - else{ - brightestPointLight = max(fallOff, brightestPointLight); - - } - #endif - - gl_FragColor.rgb += directLighting * fallOff; - - } - - float minVertLighting; - #ifdef BRIGHTEN_INDOOR_SHADOWS - minVertLighting = 0.0833; //brighten shadows so that caves/indoors which are naturally covered from the DL shadows are not way too dark compared to when shadows are off (mostly only necessary for naturally dark scenes, or dark areas when using the sun intensity code above) - #else - minVertLighting = 0.0533; - - #endif - - indoorSunLightExposure = max(indoorSunLightExposure, brightestPointLight); - indoorSunLightExposure = max(indoorSunLightExposure, minVertLighting); //scale the indoorSunLightExposure back up to account for the brightest point light nearby before scaling light probes by this value below - - #if NB_PROBES >= 1 - vec3 color1 = vec3(0.0); - vec3 color2 = vec3(0.0); - vec3 color3 = vec3(0.0); - float weight1 = 1.0; - float weight2 = 0.0; - float weight3 = 0.0; - - float ndf = renderProbe(viewDir, wPosition, normal, norm, Roughness, diffuseColor, specularColor, ndotv, ao, g_LightProbeData, g_ShCoeffs, g_PrefEnvMap, color1); - #if NB_PROBES >= 2 - float ndf2 = renderProbe(viewDir, wPosition, normal, norm, Roughness, diffuseColor, specularColor, ndotv, ao, g_LightProbeData2, g_ShCoeffs2, g_PrefEnvMap2, color2); - #endif - #if NB_PROBES == 3 - float ndf3 = renderProbe(viewDir, wPosition, normal, norm, Roughness, diffuseColor, specularColor, ndotv, ao, g_LightProbeData3, g_ShCoeffs3, g_PrefEnvMap3, color3); - #endif - - #if NB_PROBES >= 2 - float invNdf = max(1.0 - ndf,0.0); - float invNdf2 = max(1.0 - ndf2,0.0); - float sumNdf = ndf + ndf2; - float sumInvNdf = invNdf + invNdf2; - #if NB_PROBES == 3 - float invNdf3 = max(1.0 - ndf3,0.0); - sumNdf += ndf3; - sumInvNdf += invNdf3; - weight3 = ((1.0 - (ndf3 / sumNdf)) / (NB_PROBES - 1)) * (invNdf3 / sumInvNdf); - #endif - - weight1 = ((1.0 - (ndf / sumNdf)) / (NB_PROBES - 1)) * (invNdf / sumInvNdf); - weight2 = ((1.0 - (ndf2 / sumNdf)) / (NB_PROBES - 1)) * (invNdf2 / sumInvNdf); - - float weightSum = weight1 + weight2 + weight3; - - weight1 /= weightSum; - weight2 /= weightSum; - weight3 /= weightSum; - #endif - - #ifdef USE_AMBIENT_LIGHT - color1.rgb *= g_AmbientLightColor.rgb; - color2.rgb *= g_AmbientLightColor.rgb; - color3.rgb *= g_AmbientLightColor.rgb; - #endif - - -// multiply probes by the indoorSunLightExposure, as determined by pixel's sunlightExposure and adjusted for -// nearby point/spot lights ( will be multiplied by 1.0 and left unchanged if you are not defining any of the sunlight exposure variables for dimming indoors areas) - color1.rgb *= indoorSunLightExposure; - color2.rgb *= indoorSunLightExposure; - color3.rgb *= indoorSunLightExposure; - - gl_FragColor.rgb += color1 * clamp(weight1,0.0,1.0) + color2 * clamp(weight2,0.0,1.0) + color3 * clamp(weight3,0.0,1.0); - - #endif - - if(emissive.a > 0){ - emissive = emissive * pow(emissive.a * 5, emissiveIntensity) * emissiveIntensity * 20 * emissive.a; - } - - // emissive = emissive * pow(emissiveIntensity * 2.3, emissive.a); - - gl_FragColor += emissive; - - // add fog after the lighting because shadows will cause the fog to darken - // which just results in the geometry looking like it's changed color - #ifdef USE_FOG - #ifdef FOG_LINEAR - gl_FragColor = getFogLinear(gl_FragColor, m_FogColor, m_LinearFog.x, m_LinearFog.y, fogDistance); - #endif - #ifdef FOG_EXP - gl_FragColor = getFogExp(gl_FragColor, m_FogColor, m_ExpFog, fogDistance); - #endif - #ifdef FOG_EXPSQ - gl_FragColor = getFogExpSquare(gl_FragColor, m_FogColor, m_ExpSqFog, fogDistance); - #endif - #endif - - //outputs the final value of the selected layer as a color for debug purposes. - #ifdef DEBUG_VALUES_MODE - if(m_DebugValuesMode == 0){ - gl_FragColor.rgb = vec3(albedo); - } - else if(m_DebugValuesMode == 1){ - gl_FragColor.rgb = vec3(normal); - } - else if(m_DebugValuesMode == 2){ - gl_FragColor.rgb = vec3(Roughness); - } - else if(m_DebugValuesMode == 3){ - gl_FragColor.rgb = vec3(Metallic); - } - else if(m_DebugValuesMode == 4){ - gl_FragColor.rgb = ao.rgb; - } - else if(m_DebugValuesMode == 5){ - gl_FragColor.rgb = vec3(emissive.rgb); - } - #endif - gl_FragColor.a = albedo.a; - -} diff --git a/jme3-terrain/src/main/resources/Common/MatDefs/Terrain/PBRTerrain.j3md b/jme3-terrain/src/main/resources/Common/MatDefs/Terrain/PBRTerrain.j3md index 707442fafd..92e601c1c1 100644 --- a/jme3-terrain/src/main/resources/Common/MatDefs/Terrain/PBRTerrain.j3md +++ b/jme3-terrain/src/main/resources/Common/MatDefs/Terrain/PBRTerrain.j3md @@ -4,14 +4,16 @@ MaterialDef PBR Terrain { MaterialParameters { Int BoundDrawBuffer - - Boolean UseVertexColorsAsSunIntensity //set true to make the vertex color's R channel how exposed a vertex is to the sun - Float StaticSunIntensity //used for setting the sun exposure value for a whole material + Texture2D SunLightExposureMap + Boolean UseVertexColorsAsSunExposure //set true to make the vertex color's R channel how exposed a vertex is to the sun + Float StaticSunExposure //used for setting the sun exposure value for a whole material //these are usually generated at run time or setup in a level editor per-geometry, so that models indoors can have the DirectionalLight dimmed accordingly. Boolean BrightenIndoorShadows //set true if shadows are enabled and indoor areas without full sun exposure are too dark compared to when shadows are turned off in settings + Boolean UseFirstLayerAsTransparency + Boolean UseTriplanarAfflictionMapping Int AfflictionSplatScale : 8 Float AfflictionRoughnessValue : 1.0 @@ -24,6 +26,9 @@ MaterialDef PBR Terrain { Texture2D SplatRoughnessMetallicMap -LINEAR Texture2D SplatEmissiveMap -LINEAR + //The type of normal map: -1.0 (DirectX), 1.0 (OpenGl) + Float NormalType : -1.0 + // Specular-AA Boolean UseSpecularAA : true // screen space variance,Use the slider to set the strength of the geometric specular anti-aliasing effect between 0 and 1. Higher values produce a blurrier result with less aliasing. @@ -78,18 +83,44 @@ MaterialDef PBR Terrain { Float Metallic_10 : 0.0 Float Metallic_11 : 0.0 - - // debug the final value of the selected layer as a color output + Color EmissiveColor_0 : 0.0 0.0 0.0 0.0 + Color EmissiveColor_1 : 0.0 0.0 0.0 0.0 + Color EmissiveColor_2 : 0.0 0.0 0.0 0.0 + Color EmissiveColor_3 : 0.0 0.0 0.0 0.0 + Color EmissiveColor_4 : 0.0 0.0 0.0 0.0 + Color EmissiveColor_5 : 0.0 0.0 0.0 0.0 + Color EmissiveColor_6 : 0.0 0.0 0.0 0.0 + Color EmissiveColor_7 : 0.0 0.0 0.0 0.0 + Color EmissiveColor_8 : 0.0 0.0 0.0 0.0 + Color EmissiveColor_9 : 0.0 0.0 0.0 0.0 + Color EmissiveColor_10 : 0.0 0.0 0.0 0.0 + Color EmissiveColor_11 : 0.0 0.0 0.0 0.0 + + Boolean UseTriPlanarMapping_0 + Boolean UseTriPlanarMapping_1 + Boolean UseTriPlanarMapping_2 + Boolean UseTriPlanarMapping_3 + Boolean UseTriPlanarMapping_4 + Boolean UseTriPlanarMapping_5 + Boolean UseTriPlanarMapping_6 + Boolean UseTriPlanarMapping_7 + Boolean UseTriPlanarMapping_8 + Boolean UseTriPlanarMapping_9 + Boolean UseTriPlanarMapping_10 + Boolean UseTriPlanarMapping_11 + + // debug the final value of the selected layer as a color output Int DebugValuesMode - // Layers: - // 0 - albedo (un-shaded) + // 0 - albedo (unshaded) // 1 - normals // 2 - roughness // 3 - metallic // 4 - ao - // 5 - emissive - + // 5 - emissive + // 6 - exposure + // 7 - alpha + // 8 - geometryNormals // use tri-planar mapping Boolean useTriPlanarMapping @@ -154,21 +185,11 @@ MaterialDef PBR Terrain { Float AlbedoMap_11_scale Texture2D NormalMap_11 -LINEAR - - // Texture that specifies alpha values Texture2D AlphaMap -LINEAR Texture2D AlphaMap_1 -LINEAR Texture2D AlphaMap_2 -LINEAR - // For Spec gloss pipeline - Boolean UseSpecGloss - Texture2D SpecularMap - Texture2D GlossinessMap - Texture2D SpecularGlossinessMap - Color Specular : 1.0 1.0 1.0 1.0 - Float Glossiness : 1.0 - Vector4 ProbeData // Prefiltered Env Map for indirect specular lighting @@ -221,7 +242,6 @@ MaterialDef PBR Terrain { Boolean BackfaceShadows : false - Boolean UseFog Color FogColor Vector2 LinearFog @@ -237,7 +257,7 @@ MaterialDef PBR Terrain { LightMode SinglePassAndImageBased VertexShader GLSL300 GLSL150 GLSL130 GLSL100: Common/MatDefs/Terrain/PBRTerrain.vert - FragmentShader GLSL300 GLSL150 GLSL130 GLSL100: Common/MatDefs/Terrain/PBRTerrain.frag + FragmentShader GLSL300 GLSL150 GLSL130 GLSL100: Common/MatDefs/Terrain/AdvancedPBRTerrain.frag WorldParameters { WorldViewProjectionMatrix @@ -247,7 +267,6 @@ MaterialDef PBR Terrain { ViewProjectionMatrix ViewMatrix Time - } Defines { @@ -256,6 +275,7 @@ MaterialDef PBR Terrain { TILELOCATION : TileLocation AFFLICTIONTEXTURE : AfflictionAlphaMap + USE_TRIPLANAR_AFFLICTION_MAPPING : UseTriplanarAfflictionMapping AFFLICTIONALBEDOMAP: SplatAlbedoMap AFFLICTIONNORMALMAP : SplatNormalMap AFFLICTIONROUGHNESSMETALLICMAP : SplatRoughnessMetallicMap @@ -267,11 +287,15 @@ MaterialDef PBR Terrain { SPECULAR_AA_SCREEN_SPACE_VARIANCE : SpecularAASigma SPECULAR_AA_THRESHOLD : SpecularAAKappa - - USE_VERTEX_COLORS_AS_SUN_INTENSITY : UseVertexColorsAsSunIntensity - STATIC_SUN_INTENSITY : StaticSunIntensity + EXPOSUREMAP : SunLightExposureMap + USE_VERTEX_COLORS_AS_SUN_EXPOSURE : UseVertexColorsAsSunExposure + STATIC_SUN_EXPOSURE : StaticSunExposure BRIGHTEN_INDOOR_SHADOWS : BrightenIndoorShadows + NORMAL_TYPE: NormalType + + USE_FIRST_LAYER_AS_TRANSPARENCY : UseFirstLayerAsTransparency + DISCARD_ALPHA : AlphaDiscardThreshold USE_FOG : UseFog @@ -323,6 +347,19 @@ MaterialDef PBR Terrain { ALBEDOMAP_10_SCALE : AlbedoMap_10_scale ALBEDOMAP_11_SCALE : AlbedoMap_11_scale + TRI_PLANAR_MAPPING_0 : UseTriPlanarMapping_0 + TRI_PLANAR_MAPPING_1 : UseTriPlanarMapping_1 + TRI_PLANAR_MAPPING_2 : UseTriPlanarMapping_2 + TRI_PLANAR_MAPPING_3 : UseTriPlanarMapping_3 + TRI_PLANAR_MAPPING_4 : UseTriPlanarMapping_4 + TRI_PLANAR_MAPPING_5 : UseTriPlanarMapping_5 + TRI_PLANAR_MAPPING_6 : UseTriPlanarMapping_6 + TRI_PLANAR_MAPPING_7 : UseTriPlanarMapping_7 + TRI_PLANAR_MAPPING_8 : UseTriPlanarMapping_8 + TRI_PLANAR_MAPPING_9 : UseTriPlanarMapping_9 + TRI_PLANAR_MAPPING_10 : UseTriPlanarMapping_10 + TRI_PLANAR_MAPPING_11 : UseTriPlanarMapping_11 + DEBUG_VALUES_MODE : DebugValuesMode }