Ver código fonte

Merge pull request #2417 from richardTingle/#2416-pbr-screenshot-tests

#2416 pbr screenshot tests
Ryan McDonough 4 meses atrás
pai
commit
59b3349b7c
18 arquivos alterados com 989 adições e 0 exclusões
  1. 192 0
      jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/light/pbr/TestPBRLighting.java
  2. 138 0
      jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/light/pbr/TestPBRSimple.java
  3. 291 0
      jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/terrain/TestPBRTerrain.java
  4. 368 0
      jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/terrain/TestPBRTerrainAdvanced.java
  5. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_DefaultDirectionalLight_f12.png
  6. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_HighRoughness_f12.png
  7. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_LowRoughness_f12.png
  8. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_UpdatedDirectionalLight_f12.png
  9. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithRealtimeBaking_f10.png
  10. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithoutRealtimeBaking_f10.png
  11. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_FinalRender_f5.png
  12. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_GeometryNormals_f5.png
  13. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_MetallicMap_f5.png
  14. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_NormalMap_f5.png
  15. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_RoughnessMap_f5.png
  16. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrainAdvanced.testPBRTerrainAdvanced_AmbientOcclusion_f5.png
  17. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrainAdvanced.testPBRTerrainAdvanced_Emissive_f5.png
  18. BIN
      jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrainAdvanced.testPBRTerrainAdvanced_FinalRender_f5.png

+ 192 - 0
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<Arguments> 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();
+    }
+}

+ 138 - 0
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<Arguments> 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();
+    }
+}

+ 291 - 0
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<Arguments> 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();
+    }
+}

+ 368 - 0
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:
+ * <ul>
+ * <li> AmbientOcclusion in the Red channel </li>
+ * <li> Roughness in the Green channel </li>
+ * <li> Metallic in the Blue channel </li>
+ * <li> EmissiveIntensity in the Alpha channel </li>
+ * </ul>
+ *
+ * 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<Arguments> 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<Image> albedoImages = new ArrayList<>();
+                List<Image> normalMapImages = new ArrayList<>();
+                List<Image> 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();
+    }
+}

BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_DefaultDirectionalLight_f12.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_HighRoughness_f12.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_LowRoughness_f12.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRLighting.testPBRLighting_UpdatedDirectionalLight_f12.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithRealtimeBaking_f10.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.light.pbr.TestPBRSimple.testPBRSimple_WithoutRealtimeBaking_f10.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_FinalRender_f5.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_GeometryNormals_f5.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_MetallicMap_f5.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_NormalMap_f5.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrain.testPBRTerrain_RoughnessMap_f5.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrainAdvanced.testPBRTerrainAdvanced_AmbientOcclusion_f5.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrainAdvanced.testPBRTerrainAdvanced_Emissive_f5.png


BIN
jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.terrain.TestPBRTerrainAdvanced.testPBRTerrainAdvanced_FinalRender_f5.png