Răsfoiți Sursa

Improve & bug fix InstancedGeometry and add new tests. (#1467)

* Improve & bug fix InstancedGeometry and add new tests.

* Update the copyright dates
Ali-RS 4 ani în urmă
părinte
comite
58f9ee911b

+ 5 - 3
jme3-core/src/main/java/com/jme3/material/logic/DefaultTechniqueDefLogic.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2015 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -64,8 +64,10 @@ public class DefaultTechniqueDefLogic implements TechniqueDefLogic {
         int lodLevel = geom.getLodLevel();
         if (geom instanceof InstancedGeometry) {
             InstancedGeometry instGeom = (InstancedGeometry) geom;
-            renderer.renderMesh(mesh, lodLevel, instGeom.getActualNumInstances(),
-                    instGeom.getAllInstanceData());
+            int numVisibleInstances = instGeom.getNumVisibleInstances();
+            if (numVisibleInstances > 0) {
+                renderer.renderMesh(mesh, lodLevel, numVisibleInstances, instGeom.getAllInstanceData());
+            }
         } else {
             renderer.renderMesh(mesh, lodLevel, 1, null);
         }

+ 40 - 6
jme3-core/src/main/java/com/jme3/scene/instancing/InstancedGeometry.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2019 jMonkeyEngine
+ * Copyright (c) 2009-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -31,6 +31,7 @@
  */
 package com.jme3.scene.instancing;
 
+import com.jme3.bounding.BoundingBox;
 import com.jme3.bounding.BoundingVolume;
 import com.jme3.collision.Collidable;
 import com.jme3.collision.CollisionResults;
@@ -67,7 +68,7 @@ public class InstancedGeometry extends Geometry {
     private Geometry[] geometries = new Geometry[1];
 
     private int firstUnusedIndex = 0;
-    private int numCulledGeometries = 0;
+    private int numVisibleInstances = 0;
     private Camera cam;
 
     public InstancedGeometry() {
@@ -213,8 +214,28 @@ public class InstancedGeometry extends Geometry {
         return geometries.length;
     }
 
-    public int getActualNumInstances() {
-        return firstUnusedIndex - numCulledGeometries;
+    /**
+     * @return The number of instances are visible by camera.
+     */
+    public int getNumVisibleInstances() {
+        return numVisibleInstances;
+    }
+
+    /**
+     * @return The number of instances are in this {@link InstancedGeometry}
+     */
+    public int getNumInstances() {
+        int count = 0;
+        for (int i = 0; i < geometries.length; i++) {
+            if (geometries[i] != null) {
+                count++;
+            }
+        }
+        return count;
+    }
+
+    public boolean isEmpty() {
+        return getNumInstances() == 0;
     }
 
     private void swap(int idx1, int idx2) {
@@ -256,7 +277,7 @@ public class InstancedGeometry extends Geometry {
         fb.limit(fb.capacity());
         fb.position(0);
 
-        numCulledGeometries = 0;
+        int numCulledGeometries = 0;
         TempVars vars = TempVars.get();
         {
             float[] temp = vars.matrixWrite;
@@ -300,7 +321,8 @@ public class InstancedGeometry extends Geometry {
 
         fb.flip();
 
-        if (fb.limit() / INSTANCE_SIZE != (firstUnusedIndex - numCulledGeometries)) {
+        numVisibleInstances = firstUnusedIndex - numCulledGeometries;
+        if (fb.limit() / INSTANCE_SIZE != numVisibleInstances) {
             throw new AssertionError();
         }
 
@@ -370,6 +392,9 @@ public class InstancedGeometry extends Geometry {
             }
         }
 
+        if (resultBound == null) {
+            resultBound = new BoundingBox(getWorldTranslation(), 0f, 0f, 0f);
+        }
         this.worldBound = resultBound;
     }
 
@@ -432,4 +457,13 @@ public class InstancedGeometry extends Geometry {
             geometries[i] = (Geometry) geometrySavables[i];
         }
     }
+
+    /**
+     *  Destroy internal buffers.
+     */
+    protected void cleanup() {
+        BufferUtils.destroyDirectBuffer(transformInstanceData.getData());
+        transformInstanceData = null;
+        geometries = null;
+    }
 }

+ 16 - 1
jme3-core/src/main/java/com/jme3/scene/instancing/InstancedNode.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014-2020 jMonkeyEngine
+ * Copyright (c) 2014-2021 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -218,6 +218,7 @@ public class InstancedNode extends GeometryGroupNode {
                     + "lod-" + lookUp.lodLevel);
             ig.setMaterial(lookUp.material);
             ig.setMesh(lookUp.mesh);
+            if (lookUp.lodLevel > 0) ig.setLodLevel(lookUp.lodLevel);
             ig.setUserData(UserData.JME_PHYSICSIGNORE, true);
             ig.setCullHint(CullHint.Never);
             ig.setShadowMode(RenderQueue.ShadowMode.Inherit);
@@ -247,6 +248,9 @@ public class InstancedNode extends GeometryGroupNode {
         InstancedGeometry ig = igByGeom.remove(geom);
         if (ig != null) {
             ig.deleteInstance(geom);
+            if (ig.isEmpty()) {
+                detachChild(ig);
+            }
         }
     }
 
@@ -258,6 +262,9 @@ public class InstancedNode extends GeometryGroupNode {
                 throw new AssertionError();
             }
             oldIG.deleteInstance(geom);
+            if (oldIG.isEmpty()) {
+                detachChild(oldIG);
+            }
             newIG.addInstance(geom);
             igByGeom.put(geom, newIG);
         }
@@ -286,6 +293,14 @@ public class InstancedNode extends GeometryGroupNode {
         Spatial s = super.detachChildAt(index);
         if (s instanceof Node) {
             ungroupSceneGraph(s);
+        } else if (s instanceof InstancedGeometry) {
+            InstancedGeometry ig = (InstancedGeometry) s;
+            lookUp.mesh = ig.getMesh();
+            lookUp.material = ig.getMaterial();
+            lookUp.lodLevel = ig.getLodLevel();
+
+            instancesMap.remove(lookUp, ig);
+            ig.cleanup();
         }
         return s;
     }

+ 195 - 0
jme3-examples/src/main/java/jme3test/scene/instancing/TestInstancedNodeAttachDetachWithPicking.java

@@ -0,0 +1,195 @@
+/*
+ * Copyright (c) 2009-2021 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 jme3test.scene.instancing;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.collision.CollisionResult;
+import com.jme3.collision.CollisionResults;
+import com.jme3.font.BitmapText;
+import com.jme3.input.MouseInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.MouseButtonTrigger;
+import com.jme3.light.AmbientLight;
+import com.jme3.light.PointLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Ray;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.instancing.InstancedNode;
+import com.jme3.scene.shape.Box;
+import com.jme3.scene.shape.Sphere;
+import com.jme3.system.AppSettings;
+
+
+/**
+ * A test case for using instancing with ray casting.
+ *
+ * Based on distance from camera, swap in/out more/less detailed geometry to/from an InstancedNode.
+ *
+ * @author duncanj
+ */
+public class TestInstancedNodeAttachDetachWithPicking extends SimpleApplication {
+    public static void main(String[] args) {
+        TestInstancedNodeAttachDetachWithPicking app = new TestInstancedNodeAttachDetachWithPicking();
+        AppSettings settings = new AppSettings(true);
+        settings.setVSync(false);
+        app.setSettings(settings);
+        app.start();
+    }
+
+    private InstancedNode instancedNode;
+
+    private Vector3f[] locations = new Vector3f[10];
+    private Geometry[] spheres = new Geometry[10];
+    private Geometry[] boxes = new Geometry[10];
+
+    @Override
+    public void simpleInitApp() {
+        addPointLight();
+        addAmbientLight();
+
+        Material material = createInstancedLightingMaterial();
+
+        instancedNode = new InstancedNode("theParentInstancedNode");
+        rootNode.attachChild(instancedNode);
+        Sphere sphereMesh = new Sphere(16, 16, 1f);
+        Box boxMesh = new Box(0.7f, 0.7f, 0.7f);
+        // create 10 spheres & boxes, positioned along Z-axis successively further from the camera
+        for (int i = 0; i < 10; i++) {
+            Vector3f location = new Vector3f(0, -3, -(i*5));
+            locations[i] = location;
+
+            Geometry sphere = new Geometry("sphere", sphereMesh);
+            sphere.setMaterial(material);
+            sphere.setLocalTranslation(location);
+            instancedNode.attachChild(sphere);       // initially just add the spheres to the InstancedNode
+            spheres[i] = sphere;
+
+            Geometry box = new Geometry("box", boxMesh);
+            box.setMaterial(material);
+            box.setLocalTranslation(location);
+            boxes[i] = box;
+        }
+        instancedNode.instance();
+
+        flyCam.setMoveSpeed(30);
+
+
+        addCrossHairs();
+
+        // when you left-click, print the distance to the object to system.out
+        inputManager.addMapping("leftClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
+        inputManager.addListener(new ActionListener() {
+            @Override
+            public void onAction(String name, boolean isPressed, float tpf) {
+                if( isPressed ) {
+                    CollisionResult result = pickFromCamera();
+                    if( result != null ) {
+                        System.out.println("Picked = " + result.getGeometry() + ", Distance = "+result.getDistance());
+                    }
+                }
+            }
+        }, "leftClick");
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        // Each frame, determine the distance to each sphere/box from the camera.
+        // If the object is > 25 units away, switch in the Box.  If it's nearer, switch in the Sphere.
+        // Normally we wouldn't do this every frame, only when player has moved a sufficient distance, etc.
+
+
+        boolean modified = false;
+        for (int i = 0; i < 10; i++) {
+            Vector3f location = locations[i];
+            float distance = location.distance(cam.getLocation());
+
+            if(distance > 25.0f && boxes[i].getParent() == null) {
+                modified = true;
+                instancedNode.attachChild(boxes[i]);
+                instancedNode.detachChild(spheres[i]);
+            } else if(distance <= 25.0f && spheres[i].getParent() == null) {
+                modified = true;
+                instancedNode.attachChild(spheres[i]);
+                instancedNode.detachChild(boxes[i]);
+            }
+        }
+
+        if(modified) {
+            instancedNode.instance();
+        }
+    }
+
+    private Material createInstancedLightingMaterial() {
+        Material material = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        material.setBoolean("UseMaterialColors", true);
+        material.setBoolean("UseInstancing", true);
+        material.setColor("Ambient", ColorRGBA.Red);
+        material.setColor("Diffuse", ColorRGBA.Red);
+        material.setColor("Specular", ColorRGBA.Red);
+        material.setFloat("Shininess", 1.0f);
+        return material;
+    }
+
+    private void addAmbientLight() {
+        AmbientLight ambientLight = new AmbientLight(new ColorRGBA(0.2f, 0.2f, 0.2f, 1.0f));
+        rootNode.addLight(ambientLight);
+    }
+
+    private void addPointLight() {
+        PointLight pointLight = new PointLight();
+        pointLight.setColor(ColorRGBA.White);
+        pointLight.setRadius(100f);
+        pointLight.setPosition(new Vector3f(10f, 10f, 0));
+        rootNode.addLight(pointLight);
+    }
+
+    private void addCrossHairs() {
+        BitmapText ch = new BitmapText(guiFont, false);
+        ch.setSize(guiFont.getCharSet().getRenderedSize()+4);
+        ch.setText("+"); // crosshairs
+        ch.setColor(ColorRGBA.White);
+        ch.setLocalTranslation( // center
+                settings.getWidth() / 2 - ch.getLineWidth() / 2,
+                settings.getHeight() / 2 + ch.getLineHeight() / 2, 0);
+        guiNode.attachChild(ch);
+    }
+
+    private CollisionResult pickFromCamera() {
+        CollisionResults results = new CollisionResults();
+        Ray ray = new Ray(cam.getLocation(), cam.getDirection());
+        instancedNode.collideWith(ray, results);
+        return results.getClosestCollision();
+    }
+}

+ 176 - 0
jme3-examples/src/main/java/jme3test/scene/instancing/TestInstancedNodeAttachDetachWithShadowFilter.java

@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2009-2021 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 jme3test.scene.instancing;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.light.AmbientLight;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
+import com.jme3.post.FilterPostProcessor;
+import com.jme3.renderer.queue.RenderQueue;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.instancing.InstancedNode;
+import com.jme3.scene.shape.Box;
+import com.jme3.scene.shape.Sphere;
+import com.jme3.shadow.DirectionalLightShadowFilter;
+import com.jme3.shadow.EdgeFilteringMode;
+import com.jme3.system.AppSettings;
+
+/**
+ * A test case for using instancing with shadow filter.
+ *
+ * Based on distance from camera, swap in/out more/less detailed geometry to/from an InstancedNode.
+ *
+ * @author duncanj
+ */
+public class TestInstancedNodeAttachDetachWithShadowFilter extends SimpleApplication {
+    public static void main(String[] args) {
+        TestInstancedNodeAttachDetachWithShadowFilter app = new TestInstancedNodeAttachDetachWithShadowFilter();
+        AppSettings settings = new AppSettings(true);
+        settings.setVSync(false);
+        app.setSettings(settings);
+        app.start();
+    }
+
+    private FilterPostProcessor filterPostProcessor;
+    private InstancedNode instancedNode;
+
+    private Vector3f[] locations = new Vector3f[10];
+    private Geometry[] spheres = new Geometry[10];
+    private Geometry[] boxes = new Geometry[10];
+
+    @Override
+    public void simpleInitApp() {
+        filterPostProcessor = new FilterPostProcessor(assetManager);
+        getViewPort().addProcessor(filterPostProcessor);
+
+        addDirectionalLight();
+        addAmbientLight();
+
+        Material instancingMaterial = createLightingMaterial(true, ColorRGBA.LightGray);
+
+        instancedNode = new InstancedNode("theParentInstancedNode");
+        instancedNode.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
+        rootNode.attachChild(instancedNode);
+
+        // create 10 spheres & boxes, along the z-axis, successively further from the camera
+        Mesh sphereMesh = new Sphere(32, 32, 1f);
+        Mesh boxMesh = new Box(0.7f, 0.7f, 0.7f);
+        for (int z = 0; z < 10; z++) {
+            Vector3f location = new Vector3f(0, -3, -(z * 4));
+            locations[z] = location;
+
+            Geometry sphere = new Geometry("sphere", sphereMesh);
+            sphere.setMaterial(instancingMaterial);
+            sphere.setLocalTranslation(location);
+            instancedNode.attachChild(sphere);       // initially just add the spheres to the InstancedNode
+            spheres[z] = sphere;
+
+            Geometry box = new Geometry("box", boxMesh);
+            box.setMaterial(instancingMaterial);
+            box.setLocalTranslation(location);
+            boxes[z] = box;
+        }
+
+        instancedNode.instance();
+
+
+        Geometry floor = new Geometry("floor", new Box(20, 0.1f, 40));
+        floor.setMaterial(createLightingMaterial(false, ColorRGBA.Yellow));
+        floor.setLocalTranslation(5, -5, 0);
+        floor.setShadowMode(RenderQueue.ShadowMode.Receive);
+        rootNode.attachChild(floor);
+
+        flyCam.setMoveSpeed(30);
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        // Each frame, determine the distance to each sphere/box from the camera.
+        // If the object is > 25 units away, switch in the Box.  If it's nearer, switch in the Sphere.
+        // Normally we wouldn't do this every frame, only when player has moved a sufficient distance, etc.
+
+        boolean modified = false;
+        for (int i = 0; i < 10; i++) {
+            Vector3f location = locations[i];
+            float distance = location.distance(cam.getLocation());
+
+            if(distance > 25.0f && boxes[i].getParent() == null) {
+                modified = true;
+                instancedNode.attachChild(boxes[i]);
+                instancedNode.detachChild(spheres[i]);
+            } else if(distance <= 25.0f && spheres[i].getParent() == null) {
+                modified = true;
+                instancedNode.attachChild(spheres[i]);
+                instancedNode.detachChild(boxes[i]);
+            }
+        }
+
+        if(modified) {
+            instancedNode.instance();
+        }
+    }
+
+    private Material createLightingMaterial(boolean useInstancing, ColorRGBA color) {
+        Material material = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+        material.setBoolean("UseMaterialColors", true);
+        material.setBoolean("UseInstancing", useInstancing);
+        material.setColor("Ambient", color);
+        material.setColor("Diffuse", color);
+        material.setColor("Specular", color);
+        material.setFloat("Shininess", 1.0f);
+        return material;
+    }
+
+    private void addAmbientLight() {
+        AmbientLight ambientLight = new AmbientLight(new ColorRGBA(0.1f, 0.1f, 0.1f, 1.0f));
+        rootNode.addLight(ambientLight);
+    }
+
+    private void addDirectionalLight() {
+        DirectionalLight light = new DirectionalLight();
+
+        light.setColor(ColorRGBA.White);
+        light.setDirection(new Vector3f(-1, -1, -1));
+
+        DirectionalLightShadowFilter dlsf = new DirectionalLightShadowFilter(assetManager, 1024, 1);
+        dlsf.setLight(light);
+        dlsf.setEdgeFilteringMode(EdgeFilteringMode.PCFPOISSON);
+        filterPostProcessor.addFilter(dlsf);
+
+        rootNode.addLight(light);
+    }
+}