diff --git a/jme3-core/src/main/java/com/jme3/material/logic/DefaultTechniqueDefLogic.java b/jme3-core/src/main/java/com/jme3/material/logic/DefaultTechniqueDefLogic.java index 86ce66391f..9bb317219f 100644 --- a/jme3-core/src/main/java/com/jme3/material/logic/DefaultTechniqueDefLogic.java +++ b/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 static void renderMeshFromGeometry(Renderer renderer, Geometry geom) { 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); } diff --git a/jme3-core/src/main/java/com/jme3/scene/instancing/InstancedGeometry.java b/jme3-core/src/main/java/com/jme3/scene/instancing/InstancedGeometry.java index 30eb3b6fcf..83301ef0cc 100644 --- a/jme3-core/src/main/java/com/jme3/scene/instancing/InstancedGeometry.java +++ b/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 int getMaxNumInstances() { 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 void updateInstances() { 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 void updateInstances() { 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 @@ protected void updateWorldBound() { } } + if (resultBound == null) { + resultBound = new BoundingBox(getWorldTranslation(), 0f, 0f, 0f); + } this.worldBound = resultBound; } @@ -432,4 +457,13 @@ public void read(JmeImporter importer) throws IOException { geometries[i] = (Geometry) geometrySavables[i]; } } + + /** + * Destroy internal buffers. + */ + protected void cleanup() { + BufferUtils.destroyDirectBuffer(transformInstanceData.getData()); + transformInstanceData = null; + geometries = null; + } } diff --git a/jme3-core/src/main/java/com/jme3/scene/instancing/InstancedNode.java b/jme3-core/src/main/java/com/jme3/scene/instancing/InstancedNode.java index 0f53e1bddc..7557df67f8 100644 --- a/jme3-core/src/main/java/com/jme3/scene/instancing/InstancedNode.java +++ b/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 @@ private InstancedGeometry lookUpByGeometry(Geometry geom) { + "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 @@ private void removeFromInstancedGeometry(Geometry geom) { InstancedGeometry ig = igByGeom.remove(geom); if (ig != null) { ig.deleteInstance(geom); + if (ig.isEmpty()) { + detachChild(ig); + } } } @@ -258,6 +262,9 @@ private void relocateInInstancedGeometry(Geometry geom) { throw new AssertionError(); } oldIG.deleteInstance(geom); + if (oldIG.isEmpty()) { + detachChild(oldIG); + } newIG.addInstance(geom); igByGeom.put(geom, newIG); } @@ -286,6 +293,14 @@ public Spatial detachChildAt(int index) { 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; } diff --git a/jme3-examples/src/main/java/jme3test/scene/instancing/TestInstancedNodeAttachDetachWithPicking.java b/jme3-examples/src/main/java/jme3test/scene/instancing/TestInstancedNodeAttachDetachWithPicking.java new file mode 100644 index 0000000000..599aa133bc --- /dev/null +++ b/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(); + } +} \ No newline at end of file diff --git a/jme3-examples/src/main/java/jme3test/scene/instancing/TestInstancedNodeAttachDetachWithShadowFilter.java b/jme3-examples/src/main/java/jme3test/scene/instancing/TestInstancedNodeAttachDetachWithShadowFilter.java new file mode 100644 index 0000000000..4a5d10219a --- /dev/null +++ b/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); + } +} \ No newline at end of file