From 99ae154931d54823cc5676d3cb67a21b6832e7d3 Mon Sep 17 00:00:00 2001 From: zzuegg Date: Mon, 26 Jan 2026 19:54:07 +0100 Subject: [PATCH 1/8] *Add FullscreenTriangle.java --- .../jme3/scene/shape/FullscreenTriangle.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 jme3-core/src/main/java/com/jme3/scene/shape/FullscreenTriangle.java diff --git a/jme3-core/src/main/java/com/jme3/scene/shape/FullscreenTriangle.java b/jme3-core/src/main/java/com/jme3/scene/shape/FullscreenTriangle.java new file mode 100644 index 0000000000..cad38d4433 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/scene/shape/FullscreenTriangle.java @@ -0,0 +1,34 @@ +package com.jme3.scene.shape; + +import com.jme3.scene.Mesh; +import com.jme3.scene.VertexBuffer; + + +/** + * The FullscreenTriangle class defines a mesh representing a single + * triangle that spans the entire screen. It is typically used in rendering + * techniques where a fullscreen quad or triangle is needed, such as in + * post-processing effects or screen-space operations. + */ +public class FullscreenTriangle extends Mesh { + private static final float[] POSITIONS = { + -1, -1, 0, + 3, -1, 0, + -1, 3, 0 + }; + + private static final float[] TEXCOORDS = { + 0,0, + 2,0, + 0,2 + }; + + + public FullscreenTriangle() { + super(); + setBuffer(VertexBuffer.Type.Position, 3, POSITIONS); + setBuffer(VertexBuffer.Type.TexCoord, 2, TEXCOORDS); + setBuffer(VertexBuffer.Type.Index, 3, new short[]{0, 1, 2}); + updateBound(); + } +} From 26295f96c5cd3cf1686511f5f7d80be730c3d3f7 Mon Sep 17 00:00:00 2001 From: zzuegg Date: Mon, 26 Jan 2026 19:54:31 +0100 Subject: [PATCH 2/8] *Update Postprocessor to use the new FullscreenTriangle geometry --- .../src/main/java/com/jme3/post/FilterPostProcessor.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java b/jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java index fa0e14ab77..98f5b2672c 100644 --- a/jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java +++ b/jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java @@ -46,6 +46,8 @@ import com.jme3.renderer.Renderer; import com.jme3.renderer.ViewPort; import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.shape.FullscreenTriangle; import com.jme3.texture.FrameBuffer; import com.jme3.texture.FrameBuffer.FrameBufferTarget; import com.jme3.texture.Image.Format; @@ -88,7 +90,7 @@ public class FilterPostProcessor implements SceneProcessor, Savable { private Texture2D depthTexture; private SafeArrayList filters = new SafeArrayList<>(Filter.class); private AssetManager assetManager; - private Picture fsQuad; + private Geometry fsQuad; private boolean computeDepth = false; private FrameBuffer outputBuffer; private int width; @@ -181,9 +183,7 @@ public void initialize(RenderManager rm, ViewPort vp) { renderManager = rm; renderer = rm.getRenderer(); viewPort = vp; - fsQuad = new Picture("filter full screen quad"); - fsQuad.setWidth(1); - fsQuad.setHeight(1); + fsQuad = new Geometry("FsQuad", new FullscreenTriangle()); // Determine optimal framebuffer format based on renderer capabilities if (!renderer.getCaps().contains(Caps.PackedFloatTexture)) { From bc6b3bf23d86c71d0ef9d13004eb6590fd25cc91 Mon Sep 17 00:00:00 2001 From: zzuegg Date: Mon, 26 Jan 2026 19:55:37 +0100 Subject: [PATCH 3/8] Changed post vertex shaders to work with the already correct position coordinates --- .../src/main/resources/Common/MatDefs/Post/Post.vert | 5 ++--- .../src/main/resources/Common/MatDefs/Post/Post15.vert | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/jme3-effects/src/main/resources/Common/MatDefs/Post/Post.vert b/jme3-effects/src/main/resources/Common/MatDefs/Post/Post.vert index 96219f9f03..68c2361c48 100644 --- a/jme3-effects/src/main/resources/Common/MatDefs/Post/Post.vert +++ b/jme3-effects/src/main/resources/Common/MatDefs/Post/Post.vert @@ -4,8 +4,7 @@ attribute vec2 inTexCoord; varying vec2 texCoord; -void main() { - vec2 pos = inPosition.xy * 2.0 - 1.0; - gl_Position = vec4(pos, 0.0, 1.0); +void main() { + gl_Position = vec4(inPosition.xy, 0.0, 1.0); texCoord = inTexCoord; } \ No newline at end of file diff --git a/jme3-effects/src/main/resources/Common/MatDefs/Post/Post15.vert b/jme3-effects/src/main/resources/Common/MatDefs/Post/Post15.vert index 975780ea29..bb6810727f 100644 --- a/jme3-effects/src/main/resources/Common/MatDefs/Post/Post15.vert +++ b/jme3-effects/src/main/resources/Common/MatDefs/Post/Post15.vert @@ -6,7 +6,6 @@ in vec2 inTexCoord; out vec2 texCoord; void main() { - vec2 pos = inPosition.xy * 2.0 - 1.0; - gl_Position = vec4(pos, 0.0, 1.0); + gl_Position = vec4(inPosition.xy, 0.0, 1.0); texCoord = inTexCoord; } \ No newline at end of file From 0e78b63091ae6336e4649d03f42f969b4223fe0a Mon Sep 17 00:00:00 2001 From: zzuegg Date: Mon, 26 Jan 2026 20:49:08 +0100 Subject: [PATCH 4/8] Revert to the old vertex shader code --- jme3-effects/src/main/resources/Common/MatDefs/Post/Post.vert | 3 ++- .../src/main/resources/Common/MatDefs/Post/Post15.vert | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/jme3-effects/src/main/resources/Common/MatDefs/Post/Post.vert b/jme3-effects/src/main/resources/Common/MatDefs/Post/Post.vert index 68c2361c48..7f7d3c19be 100644 --- a/jme3-effects/src/main/resources/Common/MatDefs/Post/Post.vert +++ b/jme3-effects/src/main/resources/Common/MatDefs/Post/Post.vert @@ -5,6 +5,7 @@ attribute vec2 inTexCoord; varying vec2 texCoord; void main() { - gl_Position = vec4(inPosition.xy, 0.0, 1.0); + vec2 pos = inPosition.xy * 2.0 - 1.0; + gl_Position = vec4(pos, 0.0, 1.0); texCoord = inTexCoord; } \ No newline at end of file diff --git a/jme3-effects/src/main/resources/Common/MatDefs/Post/Post15.vert b/jme3-effects/src/main/resources/Common/MatDefs/Post/Post15.vert index bb6810727f..cb3d0757b4 100644 --- a/jme3-effects/src/main/resources/Common/MatDefs/Post/Post15.vert +++ b/jme3-effects/src/main/resources/Common/MatDefs/Post/Post15.vert @@ -6,6 +6,7 @@ in vec2 inTexCoord; out vec2 texCoord; void main() { - gl_Position = vec4(inPosition.xy, 0.0, 1.0); + vec2 pos = inPosition.xy * 2.0 - 1.0; + gl_Position = vec4(pos, 0.0, 1.0); texCoord = inTexCoord; } \ No newline at end of file From ea76ce70d4b2e864e1ddc15bde5c58f21c9fd46d Mon Sep 17 00:00:00 2001 From: zzuegg Date: Mon, 26 Jan 2026 20:52:47 +0100 Subject: [PATCH 5/8] Reworked the positions so that the vertex positions are correct with the post.vert transformations --- .../java/com/jme3/scene/shape/FullscreenTriangle.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/scene/shape/FullscreenTriangle.java b/jme3-core/src/main/java/com/jme3/scene/shape/FullscreenTriangle.java index cad38d4433..a46f7e5a9e 100644 --- a/jme3-core/src/main/java/com/jme3/scene/shape/FullscreenTriangle.java +++ b/jme3-core/src/main/java/com/jme3/scene/shape/FullscreenTriangle.java @@ -11,10 +11,15 @@ * post-processing effects or screen-space operations. */ public class FullscreenTriangle extends Mesh { + + /** + * Encapsulates the vertex positions for a fullscreen triangle. + * The positions are transformed by the vertex shader to cover the entire screen. + */ private static final float[] POSITIONS = { - -1, -1, 0, - 3, -1, 0, - -1, 3, 0 + 0, 0, 0, // -1 -1 0 after vertex shader transform + 2, 0, 0, // 3 -1 0 after vertex shader transform + 0, 2, 0 // -1 3 0 after vertex shader transform }; private static final float[] TEXCOORDS = { From 8c1bb36570597f2b09eb79ca1f2c5ef967aa99fd Mon Sep 17 00:00:00 2001 From: zzuegg Date: Mon, 26 Jan 2026 21:51:46 +0100 Subject: [PATCH 6/8] Added support for using fullscreen triangle or quad in `FilterPostProcessor` rendering. --- .../com/jme3/post/FilterPostProcessor.java | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java b/jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java index 98f5b2672c..bbc4a72679 100644 --- a/jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java +++ b/jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java @@ -90,6 +90,7 @@ public class FilterPostProcessor implements SceneProcessor, Savable { private Texture2D depthTexture; private SafeArrayList filters = new SafeArrayList<>(Filter.class); private AssetManager assetManager; + private boolean useFullscreenTriangle = false; private Geometry fsQuad; private boolean computeDepth = false; private FrameBuffer outputBuffer; @@ -118,6 +119,18 @@ public FilterPostProcessor(AssetManager assetManager) { this.assetManager = assetManager; } + /** + * Constructs a new instance of FilterPostProcessor. + * + * @param assetManager The asset manager used to load resources needed by the processor. + * @param useFullscreenTriangle If true, a fullscreen triangle will be used for rendering; + * otherwise, a quad will be used. + */ + public FilterPostProcessor(AssetManager assetManager, boolean useFullscreenTriangle) { + this(assetManager); + this.useFullscreenTriangle = useFullscreenTriangle; + } + /** * Serialization-only constructor. Do not use this constructor directly; * use {@link #FilterPostProcessor(AssetManager)}. @@ -183,7 +196,14 @@ public void initialize(RenderManager rm, ViewPort vp) { renderManager = rm; renderer = rm.getRenderer(); viewPort = vp; - fsQuad = new Geometry("FsQuad", new FullscreenTriangle()); + if(useFullscreenTriangle) { + fsQuad = new Geometry("FsQuad", new FullscreenTriangle()); + }else{ + Picture fullscreenQuad = new Picture("filter full screen quad"); + fullscreenQuad.setWidth(1); + fullscreenQuad.setHeight(1); + fsQuad = fullscreenQuad; + } // Determine optimal framebuffer format based on renderer capabilities if (!renderer.getCaps().contains(Caps.PackedFloatTexture)) { @@ -715,6 +735,7 @@ public Format getFrameBufferDepthFormat() { public void write(JmeExporter ex) throws IOException { OutputCapsule oc = ex.getCapsule(this); oc.write(numSamples, "numSamples", 0); + oc.write(useFullscreenTriangle, "useFullscreenTriangle", false); oc.writeSavableArrayList(new ArrayList(filters), "filters", null); } @@ -723,6 +744,7 @@ public void write(JmeExporter ex) throws IOException { public void read(JmeImporter im) throws IOException { InputCapsule ic = im.getCapsule(this); numSamples = ic.readInt("numSamples", 0); + useFullscreenTriangle = ic.readBoolean("useFullscreenTriangle", false); filters = new SafeArrayList<>(Filter.class, ic.readSavableArrayList("filters", null)); for (Filter filter : filters.getArray()) { filter.setProcessor(this); From e1f0a784a0ba5754489c6607841a8b4f677d9171 Mon Sep 17 00:00:00 2001 From: Richard Tingle <6330028+richardTingle@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:20:32 +0000 Subject: [PATCH 7/8] Introduce support for multi-scenario screenshot tests to validate identical results across approaches. --- .../testframework/Scenario.java | 13 ++ .../testframework/ScreenshotTest.java | 14 +- .../testframework/ScreenshotTestBase.java | 14 +- .../testframework/TestDriver.java | 166 +++++++++++------- 4 files changed, 144 insertions(+), 63 deletions(-) create mode 100644 jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/Scenario.java diff --git a/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/Scenario.java b/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/Scenario.java new file mode 100644 index 0000000000..64b0141ee4 --- /dev/null +++ b/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/Scenario.java @@ -0,0 +1,13 @@ +package org.jmonkeyengine.screenshottests.testframework; + +import com.jme3.app.state.AppState; + +public class Scenario { + String scenarioName; + AppState[] states; + + public Scenario(String scenarioName, AppState... states) { + this.scenarioName = scenarioName; + this.states = states; + } +} diff --git a/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ScreenshotTest.java b/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ScreenshotTest.java index 5bff5d9960..f207e73d27 100644 --- a/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ScreenshotTest.java +++ b/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ScreenshotTest.java @@ -47,7 +47,11 @@ public class ScreenshotTest{ TestType testType = TestType.MUST_PASS; - AppState[] states; + /** + * Usually there will be a single scenario but sometimes it will be desirable to test that two ways + * of doing something produce the same result. In that case there will be multiple scenarios. + */ + List scenarios = new ArrayList<>(); List framesToTakeScreenshotsOn = new ArrayList<>(); @@ -56,7 +60,11 @@ public class ScreenshotTest{ String baseImageFileName = null; public ScreenshotTest(AppState... initialStates){ - states = initialStates; + scenarios.add(new Scenario("SimpleSingleScenario", initialStates)); + framesToTakeScreenshotsOn.add(1); //default behaviour is to take a screenshot on the first frame + } + public ScreenshotTest(Scenario... scenarios){ + this.scenarios.addAll(Arrays.asList(scenarios)); framesToTakeScreenshotsOn.add(1); //default behaviour is to take a screenshot on the first frame } @@ -100,7 +108,7 @@ public void run(){ String imageFilePrefix = baseImageFileName == null ? calculateImageFilePrefix() : baseImageFileName; - TestDriver.bootAppForTest(testType,settings,imageFilePrefix, framesToTakeScreenshotsOn, states); + TestDriver.bootAppForTest(testType,settings,imageFilePrefix, framesToTakeScreenshotsOn, scenarios); } diff --git a/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ScreenshotTestBase.java b/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ScreenshotTestBase.java index 81553e614e..147946e161 100644 --- a/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ScreenshotTestBase.java +++ b/jme3-screenshot-tests/src/main/java/org/jmonkeyengine/screenshottests/testframework/ScreenshotTestBase.java @@ -47,10 +47,22 @@ public abstract class ScreenshotTestBase{ /** * Initialises a screenshot test. The resulting object should be configured (if neccessary) and then started * by calling {@link ScreenshotTest#run()}. - * @param initialStates + * @param initialStates the states that will create the JME environment * @return */ public ScreenshotTest screenshotTest(AppState... initialStates){ return new ScreenshotTest(initialStates); } + + /** + * Permits multiple scenarios to be tested in a single test. Each scenario should give identical results and + * will have a screenshot taken on the same frame. + * + *

+ * This is intended for testing migrations where the old and new approach should both give identical results. + *

+ */ + public ScreenshotTest screenshotMultiScenarioTest(Scenario... scenarios){ + return new ScreenshotTest(scenarios); + } } 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 ca0cfa98a9..2d7220de14 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 @@ -56,7 +56,9 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -78,7 +80,9 @@ public class TestDriver extends BaseAppState{ private static final Logger logger = Logger.getLogger(TestDriver.class.getName()); - 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 = "Generated images is different from committed image. (If you are running the test locally this is expected, images only reproducible on github CI infrastructure)"; + + public static final String IMAGES_ARE_DIFFERENT_BETWEEN_SCENARIOS = "Images are different between scenarios."; public static final String IMAGES_ARE_DIFFERENT_SIZES = "Images are different sizes."; @@ -145,85 +149,127 @@ public void update(float tpf){ * - After all the frames have been taken it stops the application * - Compares the screenshot to the expected screenshot (if any). Fails the test if they are different */ - public static void bootAppForTest(TestType testType, AppSettings appSettings, String baseImageFileName, List framesToTakeScreenshotsOn, AppState... initialStates){ - FastMath.rand.setSeed(0); //try to make things deterministic by setting the random seed + public static void bootAppForTest(TestType testType, AppSettings appSettings, String baseImageFileName, List framesToTakeScreenshotsOn, List scenarios){ + Collections.sort(framesToTakeScreenshotsOn); - Path imageTempDir; + List tempFolders = new ArrayList<>(); + Map> imageFilesPerScenario = new HashMap<>(); + + // usually there is a single scenario, but the framework can be set up to expect multiple scenarios that give identical results + for(Scenario scenario : scenarios) { + FastMath.rand.setSeed(0); //try to make things deterministic by setting the random seed + Path imageTempDir; + try { + imageTempDir = Files.createTempDirectory("jmeSnapshotTest"); + } catch (IOException e) { + throw new RuntimeException(e); + } + tempFolders.add(imageTempDir); - try{ - imageTempDir = Files.createTempDirectory("jmeSnapshotTest"); - } catch(IOException e){ - throw new RuntimeException(e); - } + ScreenshotNoInputAppState screenshotAppState = new ScreenshotNoInputAppState(imageTempDir.toString() + "/"); + String screenshotAppFileNamePrefix = "Screenshot-"; + screenshotAppState.setFileName(screenshotAppFileNamePrefix); - ScreenshotNoInputAppState screenshotAppState = new ScreenshotNoInputAppState(imageTempDir.toString() + "/"); - String screenshotAppFileNamePrefix = "Screenshot-"; - screenshotAppState.setFileName(screenshotAppFileNamePrefix); + List states = new ArrayList<>(Arrays.asList(scenario.states)); + TestDriver testDriver = new TestDriver(screenshotAppState, framesToTakeScreenshotsOn); + states.add(screenshotAppState); + states.add(testDriver); - List states = new ArrayList<>(Arrays.asList(initialStates)); - TestDriver testDriver = new TestDriver(screenshotAppState, framesToTakeScreenshotsOn); - states.add(screenshotAppState); - states.add(testDriver); + SimpleApplication app = new App(states.toArray(new AppState[0])); + app.setSettings(appSettings); + app.setShowSettings(false); - SimpleApplication app = new App(states.toArray(new AppState[0])); - app.setSettings(appSettings); - app.setShowSettings(false); + testDriver.waitLatch = new CountDownLatch(1); + executor.execute(() -> app.start(JmeContext.Type.Display)); - testDriver.waitLatch = new CountDownLatch(1); - executor.execute(() -> app.start(JmeContext.Type.Display)); + int maxWaitTimeMilliseconds = 45000; - int maxWaitTimeMilliseconds = 45000; + try { + boolean exitedProperly = testDriver.waitLatch.await(maxWaitTimeMilliseconds, TimeUnit.MILLISECONDS); - try { - boolean exitedProperly = testDriver.waitLatch.await(maxWaitTimeMilliseconds, TimeUnit.MILLISECONDS); + if (!exitedProperly) { + logger.warning("Test driver did not exit in " + maxWaitTimeMilliseconds + "ms. Timed out"); + app.stop(true); + } - if(!exitedProperly){ - logger.warning("Test driver did not exit in " + maxWaitTimeMilliseconds + "ms. Timed out"); - app.stop(true); + Thread.sleep(1000); //give time for openGL is fully released before starting a new test (get random JVM crashes without this) + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); } - Thread.sleep(1000); //give time for openGL is fully released before starting a new test (get random JVM crashes without this) - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } + //search the imageTempDir + List imageFiles = new ArrayList<>(); + try (Stream paths = Files.list(imageTempDir)) { + paths.forEach(imageFiles::add); + } catch (IOException e) { + throw new RuntimeException(e); + } - //search the imageTempDir - List imageFiles = new ArrayList<>(); - try(Stream paths = Files.list(imageTempDir)){ - paths.forEach(imageFiles::add); - } catch(IOException e){ - throw new RuntimeException(e); - } + //this resorts with natural numeric ordering (so App10.png comes after App9.png) + imageFiles.sort(new Comparator() { + @Override + public int compare(Path p1, Path p2) { + return extractNumber(p1).compareTo(extractNumber(p2)); + } - //this resorts with natural numeric ordering (so App10.png comes after App9.png) - imageFiles.sort(new Comparator(){ - @Override - public int compare(Path p1, Path p2){ - return extractNumber(p1).compareTo(extractNumber(p2)); + private Integer extractNumber(Path path) { + String name = path.getFileName().toString(); + int numStart = screenshotAppFileNamePrefix.length(); + int numEnd = name.lastIndexOf(".png"); + return Integer.parseInt(name.substring(numStart, numEnd)); + } + }); + if (imageFiles.isEmpty()) { + fail("No screenshot found in the temporary directory. Did the application crash?"); } - - private Integer extractNumber(Path path){ - String name = path.getFileName().toString(); - int numStart = screenshotAppFileNamePrefix.length(); - int numEnd = name.lastIndexOf(".png"); - return Integer.parseInt(name.substring(numStart, numEnd)); + if (imageFiles.size() != framesToTakeScreenshotsOn.size()) { + fail("Not all screenshots were taken, expected " + framesToTakeScreenshotsOn.size() + " but got " + imageFiles.size()); } - }); - if(imageFiles.isEmpty()){ - fail("No screenshot found in the temporary directory. Did the application crash?"); + imageFilesPerScenario.put(scenario, imageFiles); } - if(imageFiles.size() != framesToTakeScreenshotsOn.size()){ - fail("Not all screenshots were taken, expected " + framesToTakeScreenshotsOn.size() + " but got " + imageFiles.size()); - } - String failureMessage = null; try { + List primeScenarioScreenshots = imageFilesPerScenario.get(scenarios.get(0)); + + if(imageFilesPerScenario.size()>1){ + String primeScenarioName = scenarios.get(0).scenarioName; + + // check each scenario gave the same results (before checking a single scenario against the reference images + for(int i=1;i otherScenarioScreenshots = imageFilesPerScenario.get(scenarios.get(i)); + for(int screenshotIndex=0;screenshotIndex Date: Wed, 28 Jan 2026 20:19:38 +0100 Subject: [PATCH 8/8] Updated `TestFog` to support multi-scenario screenshot testing with `FullscreenQuad` and `FullscreenTriangle` setups. --- .../screenshottests/post/TestFog.java | 103 +++++++++++++++++- 1 file changed, 101 insertions(+), 2 deletions(-) diff --git a/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/post/TestFog.java b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/post/TestFog.java index 796feb0edb..d92b9af020 100644 --- a/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/post/TestFog.java +++ b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/post/TestFog.java @@ -49,6 +49,7 @@ import com.jme3.terrain.heightmap.ImageBasedHeightMap; import com.jme3.texture.Texture; import com.jme3.util.SkyFactory; +import org.jmonkeyengine.screenshottests.testframework.Scenario; import org.jmonkeyengine.screenshottests.testframework.ScreenshotTestBase; import org.junit.jupiter.api.Test; @@ -67,7 +68,7 @@ public class TestFog extends ScreenshotTestBase { */ @Test public void testFog() { - screenshotTest(new BaseAppState() { + screenshotMultiScenarioTest(new Scenario("FullscreenQuad",new BaseAppState() { @Override protected void initialize(Application app) { SimpleApplication simpleApplication = (SimpleApplication) app; @@ -165,7 +166,105 @@ public void update(float tpf) { System.out.println(getApplication().getCamera().getLocation()); } - }) + }),new Scenario("FullscreenTriangle",new BaseAppState() { + @Override + protected void initialize(Application app) { + SimpleApplication simpleApplication = (SimpleApplication) app; + Node rootNode = simpleApplication.getRootNode(); + + simpleApplication.getCamera().setLocation(new Vector3f(-34.74095f, 95.21318f, -287.4945f)); + simpleApplication.getCamera().setRotation(new Quaternion(0.023536969f, 0.9361278f, -0.016098259f, -0.35050195f)); + + Node mainScene = new Node(); + + mainScene.attachChild(SkyFactory.createSky(simpleApplication.getAssetManager(), + "Textures/Sky/Bright/BrightSky.dds", + SkyFactory.EnvMapType.CubeMap)); + + createTerrain(mainScene, app.getAssetManager()); + + DirectionalLight sun = new DirectionalLight(); + Vector3f lightDir = new Vector3f(-0.37352666f, -0.50444174f, -0.7784704f); + sun.setDirection(lightDir); + sun.setColor(ColorRGBA.White.clone().multLocal(2)); + mainScene.addLight(sun); + + rootNode.attachChild(mainScene); + + FilterPostProcessor fpp = new FilterPostProcessor(simpleApplication.getAssetManager(),true); + + FogFilter fog = new FogFilter(); + fog.setFogColor(new ColorRGBA(0.9f, 0.9f, 0.9f, 1.0f)); + fog.setFogDistance(155); + fog.setFogDensity(1.0f); + fpp.addFilter(fog); + simpleApplication.getViewPort().addProcessor(fpp); + } + + + private void createTerrain(Node rootNode, AssetManager assetManager) { + 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(Texture.WrapMode.Repeat); + matRock.setTexture("DiffuseMap", grass); + matRock.setFloat("DiffuseMap_0_scale", 64); + Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg"); + dirt.setWrap(Texture.WrapMode.Repeat); + matRock.setTexture("DiffuseMap_1", dirt); + matRock.setFloat("DiffuseMap_1_scale", 16); + Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg"); + rock.setWrap(Texture.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(Texture.WrapMode.Repeat); + Texture normalMap1 = assetManager.loadTexture("Textures/Terrain/splat/dirt_normal.png"); + normalMap1.setWrap(Texture.WrapMode.Repeat); + Texture normalMap2 = assetManager.loadTexture("Textures/Terrain/splat/road_normal.png"); + normalMap2.setWrap(Texture.WrapMode.Repeat); + matRock.setTexture("NormalMap", normalMap0); + matRock.setTexture("NormalMap_1", normalMap1); + matRock.setTexture("NormalMap_2", normalMap2); + + AbstractHeightMap heightmap = new ImageBasedHeightMap(heightMapImage.getImage(), 0.25f); + heightmap.load(); + + TerrainQuad terrain = new TerrainQuad("terrain", 65, 513, heightmap.getHeightMap()); + + 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.setShadowMode(RenderQueue.ShadowMode.Receive); + rootNode.attachChild(terrain); + + } + + + @Override + protected void cleanup(Application app) { + } + + @Override + protected void onEnable() { + } + + @Override + protected void onDisable() { + } + + @Override + public void update(float tpf) { + super.update(tpf); + System.out.println(getApplication().getCamera().getLocation()); + } + + })) .setFramesToTakeScreenshotsOn(1) .run(); }