Przeglądaj źródła

Add new screenshot tests for bill boarding, cartoons, fog and scattering

Richard Tingle 2 miesięcy temu
rodzic
commit
ace67d8392

+ 151 - 0
jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/model/shape/TestBillboard.java

@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2025 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.jmonkeyengine.screenshottests.model.shape;
+
+import com.jme3.app.Application;
+import com.jme3.app.SimpleApplication;
+import com.jme3.app.state.BaseAppState;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.control.BillboardControl;
+import com.jme3.scene.debug.Arrow;
+import com.jme3.scene.debug.Grid;
+import com.jme3.scene.shape.Quad;
+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 test for the Billboard test.
+ * 
+ * <p>This test creates three different billboard alignments (Screen, Camera, AxialY)
+ * with different colored quads. Each billboard is positioned at a different x-coordinate
+ * and has a blue Z-axis arrow attached to it. Screenshots are taken from three different angles:
+ * front, above, and right.
+ * 
+ * @author Richard Tingle (screenshot test adaptation)
+ */
+@SuppressWarnings("OptionalGetWithoutIsPresent")
+public class TestBillboard extends ScreenshotTestBase {
+
+    private static Stream<Arguments> testParameters() {
+        return Stream.of(
+                Arguments.of("fromFront", new Vector3f(0, 1, 15)),
+                Arguments.of("fromAbove", new Vector3f(0, 15, 6)),
+                Arguments.of("fromRight", new Vector3f(-15, 10, 5))
+        );
+    }
+
+    /**
+     * A billboard test with the specified camera parameters.
+     *
+     * @param cameraPosition The position of the camera
+     */
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("testParameters")
+    public void testBillboard(String testName, Vector3f cameraPosition, TestInfo testInfo) {
+        String imageName = testInfo.getTestClass().get().getName() + "." + testInfo.getTestMethod().get().getName() + "_" + testName;
+
+        screenshotTest(new BaseAppState() {
+            @Override
+            protected void initialize(Application app) {
+                SimpleApplication simpleApplication = (SimpleApplication) app;
+                Node rootNode = simpleApplication.getRootNode();
+
+                // Set up the camera
+                simpleApplication.getCamera().setLocation(cameraPosition);
+                simpleApplication.getCamera().lookAt(Vector3f.ZERO, Vector3f.UNIT_Y);
+
+                // Set background color
+                simpleApplication.getViewPort().setBackgroundColor(ColorRGBA.DarkGray);
+
+                // Create grid
+                Geometry grid = makeShape(simpleApplication, "DebugGrid", new Grid(21, 21, 2), ColorRGBA.Gray);
+                grid.center().move(0, 0, 0);
+                rootNode.attachChild(grid);
+
+                // Create billboards with different alignments
+                Node node = createBillboard(simpleApplication, BillboardControl.Alignment.Screen, ColorRGBA.Red);
+                node.setLocalTranslation(-6f, 0, 0);
+                rootNode.attachChild(node);
+
+                node = createBillboard(simpleApplication, BillboardControl.Alignment.Camera, ColorRGBA.Green);
+                node.setLocalTranslation(-2f, 0, 0);
+                rootNode.attachChild(node);
+
+                node = createBillboard(simpleApplication, BillboardControl.Alignment.AxialY, ColorRGBA.Blue);
+                node.setLocalTranslation(2f, 0, 0);
+                rootNode.attachChild(node);
+            }
+
+            @Override
+            protected void cleanup(Application app) {}
+
+            @Override
+            protected void onEnable() {}
+
+            @Override
+            protected void onDisable() {}
+
+            private Node createBillboard(SimpleApplication app, BillboardControl.Alignment alignment, ColorRGBA color) {
+                Node node = new Node("Parent");
+                Quad quad = new Quad(2, 2);
+                Geometry g = makeShape(app, alignment.name(), quad, color);
+                BillboardControl bc = new BillboardControl();
+                bc.setAlignment(alignment);
+                g.addControl(bc);
+                node.attachChild(g);
+                node.attachChild(makeShape(app, "ZAxis", new Arrow(Vector3f.UNIT_Z), ColorRGBA.Blue));
+                return node;
+            }
+
+            private Geometry makeShape(SimpleApplication app, String name, Mesh shape, ColorRGBA color) {
+                Geometry geo = new Geometry(name, shape);
+                Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
+                mat.setColor("Color", color);
+                geo.setMaterial(mat);
+                return geo;
+            }
+        })
+        .setBaseImageFileName(imageName)
+        .setFramesToTakeScreenshotsOn(1)
+        .run();
+    }
+}

+ 156 - 0
jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/post/TestCartoonEdge.java

@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2025 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.jmonkeyengine.screenshottests.post;
+
+import com.jme3.app.Application;
+import com.jme3.app.SimpleApplication;
+import com.jme3.app.state.BaseAppState;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+import com.jme3.post.FilterPostProcessor;
+import com.jme3.post.filters.CartoonEdgeFilter;
+import com.jme3.renderer.Caps;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.Spatial.CullHint;
+import com.jme3.texture.Texture;
+import org.jmonkeyengine.screenshottests.testframework.ScreenshotTestBase;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Screenshot test for the CartoonEdge filter.
+ * 
+ * <p>This test creates a scene with a monkey head model that has a cartoon/cel-shaded effect
+ * applied to it. The CartoonEdgeFilter is used to create yellow outlines around the edges
+ * of the model, and a toon shader is applied to the model's material to create the cel-shaded look.
+ * 
+ * @author Richard Tingle (screenshot test adaptation)
+ */
+public class TestCartoonEdge extends ScreenshotTestBase {
+
+    /**
+     * This test creates a scene with a cartoon-shaded monkey head model.
+     */
+    @Test
+    public void testCartoonEdge() {
+        screenshotTest(new BaseAppState() {
+            @Override
+            protected void initialize(Application app) {
+                SimpleApplication simpleApplication = (SimpleApplication) app;
+                Node rootNode = simpleApplication.getRootNode();
+
+                // Set background color
+                simpleApplication.getViewPort().setBackgroundColor(ColorRGBA.Gray);
+
+                // Set up camera
+                simpleApplication.getCamera().setLocation(new Vector3f(-1, 2, -5));
+                simpleApplication.getCamera().lookAt(Vector3f.ZERO, Vector3f.UNIT_Y);
+                simpleApplication.getCamera().setFrustumFar(300);
+
+                // Set up root node
+                rootNode.setCullHint(CullHint.Never);
+
+                // Set up lighting
+                setupLighting(rootNode);
+                
+                // Set up model
+                setupModel(simpleApplication, rootNode);
+                
+                // Set up filters
+                setupFilters(simpleApplication);
+            }
+
+            private void setupFilters(SimpleApplication app) {
+                if (app.getRenderer().getCaps().contains(Caps.GLSL100)) {
+                    FilterPostProcessor fpp = new FilterPostProcessor(app.getAssetManager());
+
+                    CartoonEdgeFilter toon = new CartoonEdgeFilter();
+                    toon.setEdgeColor(ColorRGBA.Yellow);
+                    fpp.addFilter(toon);
+                    app.getViewPort().addProcessor(fpp);
+                }
+            }
+
+            private void setupLighting(Node rootNode) {
+                DirectionalLight dl = new DirectionalLight();
+                dl.setDirection(new Vector3f(-1, -1, 1).normalizeLocal());
+                dl.setColor(new ColorRGBA(2, 2, 2, 1));
+                rootNode.addLight(dl);
+            }
+
+            private void setupModel(SimpleApplication app, Node rootNode) {
+                Spatial model = app.getAssetManager().loadModel("Models/MonkeyHead/MonkeyHead.mesh.xml");
+                makeToonish(app, model);
+                model.rotate(0, FastMath.PI, 0);
+                rootNode.attachChild(model);
+            }
+
+            private void makeToonish(SimpleApplication app, Spatial spatial) {
+                if (spatial instanceof Node) {
+                    Node n = (Node) spatial;
+                    for (Spatial child : n.getChildren()) {
+                        makeToonish(app, child);
+                    }
+                } else if (spatial instanceof Geometry) {
+                    Geometry g = (Geometry) spatial;
+                    Material m = g.getMaterial();
+                    if (m.getMaterialDef().getMaterialParam("UseMaterialColors") != null) {
+                        Texture t = app.getAssetManager().loadTexture("Textures/ColorRamp/toon.png");
+                        m.setTexture("ColorRamp", t);
+                        m.setBoolean("UseMaterialColors", true);
+                        m.setColor("Specular", ColorRGBA.Black);
+                        m.setColor("Diffuse", ColorRGBA.White);
+                        m.setBoolean("VertexLighting", true);
+                    }
+                }
+            }
+
+            @Override
+            protected void cleanup(Application app) {
+            }
+
+            @Override
+            protected void onEnable() {
+            }
+
+            @Override
+            protected void onDisable() {
+            }
+        })
+        .setFramesToTakeScreenshotsOn(1)
+        .run();
+    }
+}

+ 172 - 0
jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/post/TestFog.java

@@ -0,0 +1,172 @@
+/*
+ * Copyright (c) 2025 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.jmonkeyengine.screenshottests.post;
+
+import com.jme3.app.Application;
+import com.jme3.app.SimpleApplication;
+import com.jme3.app.state.BaseAppState;
+import com.jme3.asset.AssetManager;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.post.FilterPostProcessor;
+import com.jme3.post.filters.FogFilter;
+import com.jme3.renderer.queue.RenderQueue;
+import com.jme3.scene.Node;
+import com.jme3.terrain.geomipmap.TerrainQuad;
+import com.jme3.terrain.heightmap.AbstractHeightMap;
+import com.jme3.terrain.heightmap.ImageBasedHeightMap;
+import com.jme3.texture.Texture;
+import com.jme3.util.SkyFactory;
+import org.jmonkeyengine.screenshottests.testframework.ScreenshotTestBase;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Screenshot test for the Fog filter.
+ * 
+ * <p>This test creates a scene with a terrain and sky, with a fog effect applied.
+ * The fog is a light gray color and has a specific density and distance setting.
+ * 
+ * @author Richard Tingle (screenshot test adaptation)
+ */
+public class TestFog extends ScreenshotTestBase {
+
+    /**
+     * This test creates a scene with a fog effect.
+     */
+    @Test
+    public void testFog() {
+        screenshotTest(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());
+
+                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();
+    }
+}

+ 126 - 0
jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/post/TestLightScattering.java

@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2025 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.jmonkeyengine.screenshottests.post;
+
+import com.jme3.app.Application;
+import com.jme3.app.SimpleApplication;
+import com.jme3.app.state.BaseAppState;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.post.FilterPostProcessor;
+import com.jme3.post.filters.LightScatteringFilter;
+import com.jme3.renderer.queue.RenderQueue.ShadowMode;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+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.Test;
+
+/**
+ * Screenshot test for the LightScattering filter.
+ * 
+ * <p>This test creates a scene with a terrain model and a sky, with a light scattering
+ * (god rays) effect applied. The effect simulates light rays scattering through the atmosphere
+ * from a bright light source (like the sun).
+ * 
+ * @author Richard Tingle (screenshot test adaptation)
+ */
+public class TestLightScattering extends ScreenshotTestBase {
+
+    /**
+     * This test creates a scene with a light scattering effect.
+     */
+    @Test
+    public void testLightScattering() {
+        screenshotTest(new BaseAppState() {
+            @Override
+            protected void initialize(Application app) {
+                SimpleApplication simpleApplication = (SimpleApplication) app;
+                Node rootNode = simpleApplication.getRootNode();
+
+                simpleApplication.getCamera().setLocation(new Vector3f(55.35316f, -0.27061665f, 27.092093f));
+                simpleApplication.getCamera().setRotation(new Quaternion(0.010414706f, 0.9874893f, 0.13880467f, -0.07409228f));
+
+                Material mat = simpleApplication.getAssetManager().loadMaterial("Textures/Terrain/Rocky/Rocky.j3m");
+                Spatial scene = simpleApplication.getAssetManager().loadModel("Models/Terrain/Terrain.mesh.xml");
+                MikktspaceTangentGenerator.generate(((Geometry) ((Node) scene).getChild(0)).getMesh());
+                scene.setMaterial(mat);
+                scene.setShadowMode(ShadowMode.CastAndReceive);
+                scene.setLocalScale(400);
+                scene.setLocalTranslation(0, -10, -120);
+                rootNode.attachChild(scene);
+
+                rootNode.attachChild(SkyFactory.createSky(simpleApplication.getAssetManager(),
+                        "Textures/Sky/Bright/FullskiesBlueClear03.dds", 
+                        SkyFactory.EnvMapType.CubeMap));
+
+                DirectionalLight sun = new DirectionalLight();
+                Vector3f lightDir = new Vector3f(-0.12f, -0.3729129f, 0.74847335f);
+                sun.setDirection(lightDir);
+                sun.setColor(ColorRGBA.White.clone().multLocal(2));
+                scene.addLight(sun);
+
+                FilterPostProcessor fpp = new FilterPostProcessor(simpleApplication.getAssetManager());
+
+                Vector3f lightPos = lightDir.normalize().negate().multLocal(3000);
+                LightScatteringFilter filter = new LightScatteringFilter(lightPos);
+
+                filter.setLightDensity(1.0f);
+                filter.setBlurStart(0.02f);
+                filter.setBlurWidth(0.9f);
+                filter.setLightPosition(lightPos);
+                
+                fpp.addFilter(filter);
+                simpleApplication.getViewPort().addProcessor(fpp);
+            }
+
+            @Override
+            protected void cleanup(Application app) {
+            }
+
+            @Override
+            protected void onEnable() {
+            }
+
+            @Override
+            protected void onDisable() {
+            }
+        })
+        .setFramesToTakeScreenshotsOn(1)
+        .run();
+    }
+}