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..bbc4a72679 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,8 @@ public class FilterPostProcessor implements SceneProcessor, Savable { private Texture2D depthTexture; private SafeArrayList filters = new SafeArrayList<>(Filter.class); private AssetManager assetManager; - private Picture fsQuad; + private boolean useFullscreenTriangle = false; + private Geometry fsQuad; private boolean computeDepth = false; private FrameBuffer outputBuffer; private int width; @@ -116,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)}. @@ -181,9 +196,14 @@ 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); + 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); 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..a46f7e5a9e --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/scene/shape/FullscreenTriangle.java @@ -0,0 +1,39 @@ +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 { + + /** + * 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 = { + 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 = { + 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(); + } +} 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..7f7d3c19be 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,8 @@ attribute vec2 inTexCoord; varying vec2 texCoord; -void main() { +void main() { vec2 pos = inPosition.xy * 2.0 - 1.0; - gl_Position = vec4(pos, 0.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 975780ea29..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,7 +6,7 @@ in vec2 inTexCoord; out vec2 texCoord; void main() { - vec2 pos = inPosition.xy * 2.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-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