diff --git a/jme3-core/src/main/java/com/jme3/material/Material.java b/jme3-core/src/main/java/com/jme3/material/Material.java index 956fc7744..242f41773 100644 --- a/jme3-core/src/main/java/com/jme3/material/Material.java +++ b/jme3-core/src/main/java/com/jme3/material/Material.java @@ -702,7 +702,11 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable { int lodLevel = geom.getLodLevel(); if (geom instanceof InstancedGeometry) { InstancedGeometry instGeom = (InstancedGeometry) geom; - renderer.renderMesh(mesh, lodLevel, instGeom.getCurrentNumInstances(), instGeom.getAllInstanceData()); + int numInstances = instGeom.getActualNumInstances(); + if (numInstances == 0) { + return; + } + renderer.renderMesh(mesh, lodLevel, numInstances, 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 86f922c2a..d9259ab99 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 @@ -39,172 +39,49 @@ import com.jme3.export.Savable; import com.jme3.math.Matrix3f; import com.jme3.math.Matrix4f; import com.jme3.math.Quaternion; -import com.jme3.math.Transform; -import com.jme3.renderer.Camera; -import com.jme3.renderer.RenderManager; -import com.jme3.renderer.ViewPort; import com.jme3.scene.Geometry; -import com.jme3.scene.Mesh; import com.jme3.scene.Spatial; import com.jme3.scene.VertexBuffer; import com.jme3.scene.VertexBuffer.Format; import com.jme3.scene.VertexBuffer.Type; import com.jme3.scene.VertexBuffer.Usage; -import com.jme3.scene.control.AbstractControl; import com.jme3.util.BufferUtils; +import com.jme3.util.TempVars; import java.io.IOException; import java.nio.FloatBuffer; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -/** - * InstancedGeometry allows rendering many similar - * geometries efficiently through a feature called geometry - * instancing. - * - *

- * All rendered geometries share material, mesh, and lod level - * but have different world transforms or possibly other parameters. - * The settings for all instances are inherited from this geometry's - * {@link #setMesh(com.jme3.scene.Mesh) mesh}, - * {@link #setMaterial(com.jme3.material.Material) material} and - * {@link #setLodLevel(int) lod level} and cannot be changed per-instance. - *

- * - *

- * In order to receive any per-instance parameters, the material's shader - * must be changed to retrieve per-instance data via - * {@link VertexBuffer#setInstanced(boolean) instanced vertex attributes} - * or uniform arrays indexed with the GLSL built-in uniform - * gl_InstanceID. At the very least, they should use the - * functions specified in Instancing.glsllib shader library - * to transform vertex positions and normals instead of multiplying by the - * built-in matrix uniforms. - *

- * - *

- * This class can operate in two modes, {@link InstancedGeometry.Mode#Auto} - * and {@link InstancedGeometry.Mode#Manual}. See the respective enums - * for more information

- * - *

- * Prior to usage, the maximum number of instances must be set via - * {@link #setMaxNumInstances(int) } and the current number of instances set - * via {@link #setCurrentNumInstances(int) }. The user is then - * expected to provide transforms for all instances up to the number - * of current instances. - *

- * - * @author Kirill Vainer - */ public class InstancedGeometry extends Geometry { - /** - * Indicates how the per-instance data is to be specified. - */ - public static enum Mode { - - /** - * The user must specify all per-instance transforms and - * parameters manually via - * {@link InstancedGeometry#setGlobalUserInstanceData(com.jme3.scene.VertexBuffer[]) } - * or - * {@link InstancedGeometry#setCameraUserInstanceData(com.jme3.renderer.Camera, com.jme3.scene.VertexBuffer) }. - */ - Manual, - - /** - * The user - * {@link InstancedGeometry#setInstanceTransform(int, com.jme3.math.Transform) provides world transforms} - * and then uses the Instancing.glsllib transform functions in the - * shader to transform vertex attributes to the respective spaces. - * Additional per-instance data can be specified via - * {@link InstancedGeometry#setManualGlobalInstanceData(com.jme3.scene.VertexBuffer[]) }. - * {@link #setManualCameraInstanceData(com.jme3.renderer.Camera, com.jme3.scene.VertexBuffer) } - * cannot be used at this mode since it is computed automatically. - */ - Auto - } - - private static class InstancedGeometryControl extends AbstractControl { - - private InstancedGeometry geom; - - public InstancedGeometryControl() { - } - - public InstancedGeometryControl(InstancedGeometry geom) { - this.geom = geom; - } - - @Override - protected void controlUpdate(float tpf) { - } - - @Override - protected void controlRender(RenderManager rm, ViewPort vp) { - geom.renderFromControl(vp.getCamera()); - } - } - private static final int INSTANCE_SIZE = 16; - private InstancedGeometry.Mode mode; - private InstancedGeometryControl control; - private int currentNumInstances = 1; - private Camera lastCamera = null; - private Matrix4f[] worldMatrices = new Matrix4f[1]; private VertexBuffer[] globalInstanceData; + private VertexBuffer transformInstanceData; + private Geometry[] geometries = new Geometry[1]; - private final HashMap instanceDataPerCam - = new HashMap(); - - // TODO: determine if perhaps its better to use TempVars here. - - private final Matrix4f tempMat4 = new Matrix4f(); - private final Matrix4f tempMat4_2 = new Matrix4f(); - private final Matrix3f tempMat3 = new Matrix3f(); - private final Quaternion tempQuat = new Quaternion(); - private final float[] tempFloatArray = new float[16]; - + private int firstUnusedIndex = 0; + /** * Serialization only. Do not use. */ public InstancedGeometry() { super(); setIgnoreTransform(true); + setMaxNumInstances(1); } /** * Creates instanced geometry with the specified mode and name. * - * @param mode The {@link Mode} at which the instanced geometry operates at. * @param name The name of the spatial. * - * @see Mode * @see Spatial#Spatial(java.lang.String) */ - public InstancedGeometry(InstancedGeometry.Mode mode, String name) { + public InstancedGeometry(String name) { super(name); - this.mode = mode; setIgnoreTransform(true); - if (mode == InstancedGeometry.Mode.Auto) { - control = new InstancedGeometryControl(this); - addControl(control); - } - } - - /** - * The mode with which this instanced geometry was initialized - * with. Cannot be changed after initialization. - * - * @return instanced geometry mode. - */ - public InstancedGeometry.Mode getMode() { - return mode; + setMaxNumInstances(1); } /** @@ -238,170 +115,54 @@ public class InstancedGeometry extends Geometry { /** * Specify camera specific user per-instance data. * - * Only applies when operating in {@link Mode#Manual}. - * When operating in {@link Mode#Auto}, this data is computed automatically, - * and using this method is not allowed. - * - * @param camera The camera for which per-instance data is to be set. - * @param cameraInstanceData The camera's per-instance data. - * - * @throws IllegalArgumentException If camera is null. - * @throws IllegalStateException If {@link #getMode() mode} is set to - * {@link Mode#Auto}. - * - * @see Mode - * @see #getCameraUserInstanceData(com.jme3.renderer.Camera) - */ - public void setCameraUserInstanceData(Camera camera, VertexBuffer cameraInstanceData) { - if (mode == Mode.Auto) { - throw new IllegalStateException("Not allowed in auto mode"); - } - if (camera == null) { - throw new IllegalArgumentException("camera cannot be null"); - } - instanceDataPerCam.put(camera, cameraInstanceData); - } - - /** - * Return camera specific user per-instance data. - * - * Only applies when operating in {@link Mode#Manual}. - * When operating in {@link Mode#Auto}, this data is computed automatically, - * and using this method is not allowed. - * - * @param camera The camera to look up the per-instance data for. - * @return The per-instance data, or null if none was specified - * for the given camera. - * - * @throws IllegalArgumentException If camera is null. - * @throws IllegalStateException If {@link #getMode() mode} is set to - * {@link Mode#Auto}. - * - * @see Mode - * @see #setCameraUserInstanceData(com.jme3.renderer.Camera, com.jme3.scene.VertexBuffer) + * @param transformInstanceData The transforms for each instance. */ - public VertexBuffer getCameraUserInstanceData(Camera camera) { - if (mode == Mode.Auto) { - throw new IllegalStateException("Not allowed in auto mode"); - } - if (camera == null) { - throw new IllegalArgumentException("camera cannot be null"); - } - return instanceDataPerCam.get(camera); + public void setTransformUserInstanceData(VertexBuffer transformInstanceData) { + this.transformInstanceData = transformInstanceData; } /** - * Return a read only map with the mappings between cameras and camera - * specific per-instance data. - * - * Only applies when operating in {@link Mode#Manual}. - * When operating in {@link Mode#Auto}, this data is computed automatically, - * and using this method is not allowed. - * - * @return read only map with the mappings between cameras and camera - * specific per-instance data. - * - * @throws IllegalStateException If {@link #getMode() mode} is set to - * {@link Mode#Auto}. + * Return user per-instance transform data. * - * @see Mode - * @see #setCameraUserInstanceData(com.jme3.renderer.Camera, com.jme3.scene.VertexBuffer) + * @return The per-instance transform data. + * + * @see #setTransformUserInstanceData(com.jme3.scene.VertexBuffer) */ - public Map getAllCameraUserInstanceData() { - if (mode == Mode.Auto) { - throw new IllegalStateException("Not allowed in auto mode"); - } - return Collections.unmodifiableMap(instanceDataPerCam); + public VertexBuffer getTransformUserInstanceData() { + return transformInstanceData; } - private void updateInstance(Matrix4f viewMatrix, Matrix4f worldMatrix, float[] store, int offset) { - viewMatrix.mult(worldMatrix, tempMat4); - tempMat4.toRotationMatrix(tempMat3); + private void updateInstance(Matrix4f worldMatrix, float[] store, + int offset, Matrix3f tempMat3, + Quaternion tempQuat) { + worldMatrix.toRotationMatrix(tempMat3); tempMat3.invertLocal(); - + // NOTE: No need to take the transpose in order to encode // into quaternion, the multiplication in the shader is vec * quat // apparently... tempQuat.fromRotationMatrix(tempMat3); - + // Column-major encoding. The "W" field in each of the encoded // vectors represents the quaternion. - store[offset + 0] = tempMat4.m00; - store[offset + 1] = tempMat4.m10; - store[offset + 2] = tempMat4.m20; + store[offset + 0] = worldMatrix.m00; + store[offset + 1] = worldMatrix.m10; + store[offset + 2] = worldMatrix.m20; store[offset + 3] = tempQuat.getX(); - store[offset + 4] = tempMat4.m01; - store[offset + 5] = tempMat4.m11; - store[offset + 6] = tempMat4.m21; + store[offset + 4] = worldMatrix.m01; + store[offset + 5] = worldMatrix.m11; + store[offset + 6] = worldMatrix.m21; store[offset + 7] = tempQuat.getY(); - store[offset + 8] = tempMat4.m02; - store[offset + 9] = tempMat4.m12; - store[offset + 10] = tempMat4.m22; + store[offset + 8] = worldMatrix.m02; + store[offset + 9] = worldMatrix.m12; + store[offset + 10] = worldMatrix.m22; store[offset + 11] = tempQuat.getZ(); - store[offset + 12] = tempMat4.m03; - store[offset + 13] = tempMat4.m13; - store[offset + 14] = tempMat4.m23; + store[offset + 12] = worldMatrix.m03; + store[offset + 13] = worldMatrix.m13; + store[offset + 14] = worldMatrix.m23; store[offset + 15] = tempQuat.getW(); } - private void renderFromControl(Camera cam) { - if (mode != Mode.Auto) { - return; - } - - // Get the instance data VBO for this camera. - VertexBuffer instanceDataVB = instanceDataPerCam.get(cam); - FloatBuffer instanceData; - - if (instanceDataVB == null) { - // This is a new camera, create instance data VBO for it. - instanceData = BufferUtils.createFloatBuffer(worldMatrices.length * INSTANCE_SIZE); - instanceDataVB = new VertexBuffer(Type.InstanceData); - instanceDataVB.setInstanced(true); - instanceDataVB.setupData(Usage.Stream, INSTANCE_SIZE, Format.Float, instanceData); - instanceDataPerCam.put(cam, instanceDataVB); - } else { - // Retrieve the current instance data buffer. - instanceData = (FloatBuffer) instanceDataVB.getData(); - } - - Matrix4f viewMatrix = cam.getViewMatrix(); - - instanceData.limit(instanceData.capacity()); - instanceData.position(0); - - assert currentNumInstances <= worldMatrices.length; - - for (int i = 0; i < currentNumInstances; i++) { - Matrix4f worldMatrix = worldMatrices[i]; - if (worldMatrix == null) { - worldMatrix = Matrix4f.IDENTITY; - } - updateInstance(viewMatrix, worldMatrix, tempFloatArray, 0); - instanceData.put(tempFloatArray); - } - - instanceData.flip(); - - this.lastCamera = cam; - instanceDataVB.updateData(instanceDataVB.getData()); - } - - /** - * Set the current number of instances to be rendered. - * - * @param currentNumInstances the current number of instances to be rendered. - * - * @throws IllegalArgumentException If current number of instances is - * greater than the maximum number of instances. - */ - public void setCurrentNumInstances(int currentNumInstances) { - if (currentNumInstances > worldMatrices.length) { - throw new IllegalArgumentException("currentNumInstances cannot be larger than maxNumInstances"); - } - this.currentNumInstances = currentNumInstances; - } - /** * Set the maximum amount of instances that can be rendered by this * instanced geometry when mode is set to auto. @@ -415,88 +176,164 @@ public class InstancedGeometry extends Geometry { * @throws IllegalStateException If mode is set to manual. * @throws IllegalArgumentException If maxNumInstances is zero or negative */ - public void setMaxNumInstances(int maxNumInstances) { - if (mode == Mode.Manual) { - throw new IllegalStateException("Not allowed in manual mode"); - } + public final void setMaxNumInstances(int maxNumInstances) { if (maxNumInstances < 1) { throw new IllegalArgumentException("maxNumInstances must be 1 or higher"); } - this.worldMatrices = new Matrix4f[maxNumInstances]; + Geometry[] originalGeometries = geometries; + this.geometries = new Geometry[maxNumInstances]; - if (currentNumInstances > maxNumInstances) { - currentNumInstances = maxNumInstances; + if (originalGeometries != null) { + System.arraycopy(originalGeometries, 0, geometries, 0, originalGeometries.length); } - // Resize instance data for each of the cameras. - for (VertexBuffer instanceDataVB : instanceDataPerCam.values()) { - FloatBuffer instanceData = (FloatBuffer) instanceDataVB.getData(); - if (instanceData.capacity() / INSTANCE_SIZE != worldMatrices.length) { - // Delete old data. - BufferUtils.destroyDirectBuffer(instanceData); - - // Resize instance data for this camera. - // Create new data with new length. - instanceData = BufferUtils.createFloatBuffer(worldMatrices.length * INSTANCE_SIZE); - instanceDataVB.updateData(instanceData); - } + // Resize instance data. + if (transformInstanceData != null) { + BufferUtils.destroyDirectBuffer(transformInstanceData.getData()); + transformInstanceData.updateData(BufferUtils.createFloatBuffer(geometries.length * INSTANCE_SIZE)); + } else if (transformInstanceData == null) { + transformInstanceData = new VertexBuffer(Type.InstanceData); + transformInstanceData.setInstanced(true); + transformInstanceData.setupData(Usage.Stream, + INSTANCE_SIZE, + Format.Float, + BufferUtils.createFloatBuffer(geometries.length * INSTANCE_SIZE)); } } public int getMaxNumInstances() { - return worldMatrices.length; + return geometries.length; } - public int getCurrentNumInstances() { - return currentNumInstances; + public int getActualNumInstances() { + return firstUnusedIndex; } - public void setInstanceTransform(int instanceIndex, Matrix4f worldTransform) { - if (mode == Mode.Manual) { - throw new IllegalStateException("Not allowed in manual mode"); + private void swap(int idx1, int idx2) { + Geometry g = geometries[idx1]; + geometries[idx1] = geometries[idx2]; + geometries[idx2] = g; + + if (geometries[idx1] != null) { + InstancedNode.setGeometryStartIndex2(geometries[idx1], idx1); } - if (worldTransform == null) { - throw new IllegalArgumentException("worldTransform cannot be null"); + if (geometries[idx2] != null) { + InstancedNode.setGeometryStartIndex2(geometries[idx2], idx2); } - if (instanceIndex < 0) { - throw new IllegalArgumentException("instanceIndex cannot be smaller than zero"); + } + + private void sanitize(boolean insideEntriesNonNull) { + if (firstUnusedIndex >= geometries.length) { + throw new AssertionError(); } - if (instanceIndex >= currentNumInstances) { - throw new IllegalArgumentException("instanceIndex cannot be larger than currentNumInstances"); + for (int i = 0; i < geometries.length; i++) { + if (i < firstUnusedIndex) { + if (geometries[i] == null) { + if (insideEntriesNonNull) { + throw new AssertionError(); + } + } else if (InstancedNode.getGeometryStartIndex2(geometries[i]) != i) { + throw new AssertionError(); + } + } else { + if (geometries[i] != null) { + throw new AssertionError(); + } + } } - // TODO: Determine if need to make a copy of matrix or just doing this - // is fine. - worldMatrices[instanceIndex] = worldTransform; } - public void setInstanceTransform(int instanceIndex, Transform worldTransform) { - if (worldTransform == null) { - throw new IllegalArgumentException("worldTransform cannot be null"); + public void updateInstances() { + FloatBuffer fb = (FloatBuffer) transformInstanceData.getData(); + fb.limit(fb.capacity()); + fb.position(0); + + TempVars vars = TempVars.get(); + { + float[] temp = vars.matrixWrite; + + for (int i = 0; i < firstUnusedIndex; i++) { + Geometry geom = geometries[i]; + + if (geom == null) { + geom = geometries[firstUnusedIndex - 1]; + + if (geom == null) { + throw new AssertionError(); + } + + swap(i, firstUnusedIndex - 1); + + while (geometries[firstUnusedIndex -1] == null) { + firstUnusedIndex--; + } + } + + Matrix4f worldMatrix = geom.getWorldMatrix(); + updateInstance(worldMatrix, temp, 0, vars.tempMat3, vars.quat1); + fb.put(temp); + } } + vars.release(); - // Compute the world transform matrix. - tempMat4.loadIdentity(); - tempMat4.setRotationQuaternion(worldTransform.getRotation()); - tempMat4.setTranslation(worldTransform.getTranslation()); - tempMat4_2.loadIdentity(); - tempMat4_2.scale(worldTransform.getScale()); - tempMat4.multLocal(tempMat4_2); + fb.flip(); - setInstanceTransform(instanceIndex, tempMat4.clone()); + if (fb.limit() / INSTANCE_SIZE != firstUnusedIndex) { + throw new AssertionError(); + } + + transformInstanceData.updateData(fb); + } + + public void deleteInstance(Geometry geom) { + int idx = InstancedNode.getGeometryStartIndex2(geom); + InstancedNode.setGeometryStartIndex2(geom, -1); + + geometries[idx] = null; + + if (idx == firstUnusedIndex - 1) { + // Deleting the last element. + // Move index back. + firstUnusedIndex--; + while (geometries[firstUnusedIndex] == null) { + firstUnusedIndex--; + if (firstUnusedIndex < 0) { + break; + } + } + firstUnusedIndex++; + } else { + // Deleting element in the middle + } + } + + public void addInstance(Geometry geometry) { + if (geometry == null) { + throw new IllegalArgumentException("geometry cannot be null"); + } + + // Take an index from the end. + if (firstUnusedIndex + 1 >= geometries.length) { + // No more room. + setMaxNumInstances(getMaxNumInstances() * 2); + } + + int freeIndex = firstUnusedIndex; + firstUnusedIndex++; + + geometries[freeIndex] = geometry; + InstancedNode.setGeometryStartIndex2(geometry, freeIndex); } public VertexBuffer[] getAllInstanceData() { - VertexBuffer instanceDataForCam = instanceDataPerCam.get(lastCamera); ArrayList allData = new ArrayList(); - - if (instanceDataForCam != null) { - allData.add(instanceDataForCam); + if (transformInstanceData != null) { + allData.add(transformInstanceData); } if (globalInstanceData != null) { allData.addAll(Arrays.asList(globalInstanceData)); } - return allData.toArray(new VertexBuffer[allData.size()]); } @@ -504,30 +341,20 @@ public class InstancedGeometry extends Geometry { public void write(JmeExporter exporter) throws IOException { super.write(exporter); OutputCapsule capsule = exporter.getCapsule(this); - capsule.write(currentNumInstances, "cur_num_instances", 1); - capsule.write(mode, "instancing_mode", InstancedGeometry.Mode.Auto); - if (mode == Mode.Auto) { - capsule.write(worldMatrices, "world_matrices", null); - } + //capsule.write(currentNumInstances, "cur_num_instances", 1); + capsule.write(geometries, "geometries", null); } @Override public void read(JmeImporter importer) throws IOException { super.read(importer); InputCapsule capsule = importer.getCapsule(this); - currentNumInstances = capsule.readInt("cur_num_instances", 1); - mode = capsule.readEnum("instancing_mode", InstancedGeometry.Mode.class, - InstancedGeometry.Mode.Auto); - - if (mode == Mode.Auto) { - Savable[] matrixSavables = capsule.readSavableArray("world_matrices", null); - worldMatrices = new Matrix4f[matrixSavables.length]; - for (int i = 0; i < worldMatrices.length; i++) { - worldMatrices[i] = (Matrix4f) matrixSavables[i]; - } + //currentNumInstances = capsule.readInt("cur_num_instances", 1); - control = getControl(InstancedGeometryControl.class); - control.geom = this; + Savable[] geometrySavables = capsule.readSavableArray("geometries", null); + geometries = new Geometry[geometrySavables.length]; + for (int i = 0; i < geometrySavables.length; i++) { + geometries[i] = (Geometry) geometrySavables[i]; } } } 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 new file mode 100644 index 000000000..fa636c87e --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/scene/instancing/InstancedNode.java @@ -0,0 +1,259 @@ +package com.jme3.scene.instancing; + +import com.jme3.material.Material; +import com.jme3.renderer.RenderManager; +import com.jme3.renderer.ViewPort; +import com.jme3.scene.BatchNode; +import com.jme3.scene.Geometry; +import com.jme3.scene.GeometryGroupNode; +import com.jme3.scene.Mesh; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.scene.UserData; +import com.jme3.scene.control.AbstractControl; +import com.jme3.scene.control.Control; +import java.util.HashMap; + +public class InstancedNode extends GeometryGroupNode { + + static int getGeometryStartIndex2(Geometry geom) { + return getGeometryStartIndex(geom); + } + + static void setGeometryStartIndex2(Geometry geom, int startIndex) { + setGeometryStartIndex(geom, startIndex); + } + + private static class InstanceTypeKey implements Cloneable { + + Mesh mesh; + Material material; + int lodLevel; + + public InstanceTypeKey(Mesh mesh, Material material, int lodLevel) { + this.mesh = mesh; + this.material = material; + this.lodLevel = lodLevel; + } + + public InstanceTypeKey(){ + } + + @Override + public int hashCode() { + int hash = 3; + hash = 41 * hash + this.mesh.hashCode(); + hash = 41 * hash + this.material.hashCode(); + hash = 41 * hash + this.lodLevel; + return hash; + } + + @Override + public boolean equals(Object obj) { + final InstanceTypeKey other = (InstanceTypeKey) obj; + if (this.mesh != other.mesh) { + return false; + } + if (this.material != other.material) { + return false; + } + if (this.lodLevel != other.lodLevel) { + return false; + } + return true; + } + + @Override + public InstanceTypeKey clone() { + try { + return (InstanceTypeKey) super.clone(); + } catch (CloneNotSupportedException ex) { + throw new AssertionError(); + } + } + } + + private static class InstancedNodeControl extends AbstractControl { + + private InstancedNode node; + + public InstancedNodeControl() { + } + + public InstancedNodeControl(InstancedNode node) { + this.node = node; + } + + @Override + public Control cloneForSpatial(Spatial spatial) { + return this; + // WARNING: Sets wrong control on spatial. Will be + // fixed automatically by InstancedNode.clone() method. + } + + @Override + protected void controlUpdate(float tpf) { + } + + @Override + protected void controlRender(RenderManager rm, ViewPort vp) { + node.renderFromControl(); + } + } + + protected final HashMap igByGeom + = new HashMap(); + + private final InstanceTypeKey lookUp = new InstanceTypeKey(); + + private final HashMap instancesMap = + new HashMap(); + + public InstancedNode() { + super(); + // NOTE: since we are deserializing, + // the control is going to be added automatically here. + } + + public InstancedNode(String name) { + super(name); + addControl(new InstancedNodeControl(this)); + } + + private void renderFromControl() { + for (InstancedGeometry ig : instancesMap.values()) { + ig.updateInstances(); + } + } + + private static boolean isInstancedGeometry(Geometry geom) { + return geom instanceof InstancedGeometry; + } + + private InstancedGeometry lookUpByGeometry(Geometry geom) { + lookUp.mesh = geom.getMesh(); + lookUp.material = geom.getMaterial(); + lookUp.lodLevel = geom.getLodLevel(); + + InstancedGeometry ig = instancesMap.get(lookUp); + + if (ig == null) { + ig = new InstancedGeometry( + "material-" + lookUp.material.getMaterialDef().getName() + "," + + "lod-" + lookUp.lodLevel); + ig.setMaterial(lookUp.material); + ig.setMesh(lookUp.mesh); + ig.setUserData(UserData.JME_PHYSICSIGNORE, true); + ig.setCullHint(CullHint.Never); + instancesMap.put(lookUp.clone(), ig); + attachChild(ig); + } + + return ig; + } + + private void removeFromInstancedGeometry(Geometry geom) { + InstancedGeometry ig = igByGeom.remove(geom); + if (ig != null) { + ig.deleteInstance(geom); + } + } + + private void ungroupSceneGraph(Spatial s) { + if (s instanceof Node) { + for (Spatial sp : ((Node) s).getChildren()) { + ungroupSceneGraph(sp); + } + } else if (s instanceof Geometry) { + Geometry g = (Geometry) s; + if (g.isGrouped()) { + // Will invoke onGeometryUnassociated automatically. + g.unassociateFromGroupNode(); + if (InstancedNode.getGeometryStartIndex(g) != -1) { + throw new AssertionError(); + } + } + } + } + + @Override + public Spatial detachChildAt(int index) { + Spatial s = super.detachChildAt(index); + if (s instanceof Node) { + ungroupSceneGraph(s); + } + return s; + } + + private void instance(Spatial n) { + if (n instanceof Geometry) { + Geometry g = (Geometry) n; + if (!g.isGrouped() && g.getBatchHint() != BatchHint.Never) { + InstancedGeometry ig = lookUpByGeometry(g); + igByGeom.put(g, ig); + g.associateWithGroupNode(this, 0); + ig.addInstance(g); + } + } else if (n instanceof Node) { + for (Spatial child : ((Node) n).getChildren()) { + if (child instanceof GeometryGroupNode) { + continue; + } + instance(child); + } + } + } + + public void instance() { + instance(this); + } + + @Override + public Node clone(boolean cloneMaterials) { + InstancedNode clone = (InstancedNode)super.clone(cloneMaterials); + if (instancesMap.size() > 0) { + // Remove all instanced geometries from the clone + for (int i = 0; i < clone.children.size(); i++) { + if (clone.children.get(i) instanceof InstancedGeometry) { + clone.children.remove(i); + } + } + + // Clear state (which is incorrect) + clone.igByGeom.clear(); + clone.instancesMap.clear(); + clone.instance(); + } + return clone; + } + + private void majorChange(Geometry geom) { + InstancedGeometry oldIG = igByGeom.get(geom); + InstancedGeometry newIG = lookUpByGeometry(geom); + if (oldIG != newIG) { + oldIG.deleteInstance(geom); + newIG.addInstance(geom); + igByGeom.put(geom, newIG); + } + } + + @Override + public void onTransformChange(Geometry geom) { + // Handled automatically + } + + @Override + public void onMaterialChange(Geometry geom) { + majorChange(geom); + } + + @Override + public void onMeshChange(Geometry geom) { + majorChange(geom); + } + + @Override + public void onGeoemtryUnassociated(Geometry geom) { + removeFromInstancedGeometry(geom); + } +} diff --git a/jme3-core/src/main/resources/Common/MatDefs/Misc/ShowNormals.j3md b/jme3-core/src/main/resources/Common/MatDefs/Misc/ShowNormals.j3md index a2e2172c1..63b7470b2 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Misc/ShowNormals.j3md +++ b/jme3-core/src/main/resources/Common/MatDefs/Misc/ShowNormals.j3md @@ -10,6 +10,8 @@ MaterialDef Debug Normals { WorldParameters { WorldViewProjectionMatrix + ViewProjectionMatrix + ViewMatrix ProjectionMatrix } diff --git a/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.j3md b/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.j3md index 5a57426d4..40d64cb7e 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.j3md +++ b/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.j3md @@ -59,7 +59,8 @@ MaterialDef Unshaded { WorldParameters { WorldViewProjectionMatrix - ProjectionMatrix + ViewProjectionMatrix + ViewMatrix } Defines { diff --git a/jme3-core/src/main/resources/Common/ShaderLib/Instancing.glsllib b/jme3-core/src/main/resources/Common/ShaderLib/Instancing.glsllib index 4bf3fc1e9..ae34935c5 100644 --- a/jme3-core/src/main/resources/Common/ShaderLib/Instancing.glsllib +++ b/jme3-core/src/main/resources/Common/ShaderLib/Instancing.glsllib @@ -23,6 +23,7 @@ uniform mat4 g_ViewMatrix; uniform mat4 g_ProjectionMatrix; uniform mat4 g_WorldViewMatrix; uniform mat4 g_WorldViewProjectionMatrix; +uniform mat4 g_ViewProjectionMatrix; uniform mat3 g_NormalMatrix; #if defined INSTANCING @@ -37,29 +38,36 @@ uniform mat3 g_NormalMatrix; // 2 vertex attributes which now can be used for additional per-vertex data. attribute mat4 inInstanceData; -// Extract the world view matrix out of the instance data, leaving out the +// Extract the world matrix out of the instance data, leaving out the // quaternion at the end. -mat4 worldViewMatrix = mat4(vec4(inInstanceData[0].xyz, 0.0), +mat4 worldMatrix = mat4(vec4(inInstanceData[0].xyz, 0.0), vec4(inInstanceData[1].xyz, 0.0), vec4(inInstanceData[2].xyz, 0.0), vec4(inInstanceData[3].xyz, 1.0)); +vec4 TransformWorld(vec4 position) +{ + return (worldMatrix * position); +} vec4 TransformWorldView(vec4 position) { - return worldViewMatrix * position; + return g_ViewMatrix * TransformWorld(position); } vec4 TransformWorldViewProjection(vec4 position) { - return g_ProjectionMatrix * TransformWorldView(position); + return g_ViewProjectionMatrix * TransformWorld(position); } vec3 TransformNormal(vec3 vec) { vec4 quat = vec4(inInstanceData[0].w, inInstanceData[1].w, inInstanceData[2].w, inInstanceData[3].w); - return vec + vec3(2.0) * cross(cross(vec, quat.xyz) + vec3(quat.w) * vec, quat.xyz); + + vec3 worldNormal = vec + vec3(2.0) * cross(cross(vec, quat.xyz) + vec3(quat.w) * vec, quat.xyz); + + return (g_ViewMatrix * vec4(worldNormal, 0.0)).xyz; } // Prevent user from using g_** matrices which will have invalid data in this case. diff --git a/jme3-examples/src/main/java/jme3test/scene/instancing/TestInstanceNode.java b/jme3-examples/src/main/java/jme3test/scene/instancing/TestInstanceNode.java new file mode 100644 index 000000000..c99406122 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/scene/instancing/TestInstanceNode.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2009-2012 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.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.FastMath; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.Spatial; +import com.jme3.scene.Spatial.CullHint; +import com.jme3.scene.instancing.InstancedGeometry; +import com.jme3.scene.instancing.InstancedNode; +import com.jme3.scene.shape.Box; +import com.jme3.scene.shape.Sphere; +import com.jme3.system.AppSettings; + +public class TestInstanceNode extends SimpleApplication { + + private Mesh mesh1; + private Mesh mesh2; + private final Material[] materials = new Material[6]; + private InstancedNode instancedNode; + private float time = 0; + + public static void main(String[] args){ + TestInstanceNode app = new TestInstanceNode(); + AppSettings settings = new AppSettings(true); + settings.setVSync(false); + app.setSettings(settings); + app.start(); + } + + private Geometry createInstance(float x, float z) { + Mesh mesh; + if (FastMath.nextRandomInt(0, 1) == 1) mesh = mesh2; + else mesh = mesh1; + Geometry geometry = new Geometry("randomGeom", mesh); + geometry.setMaterial(materials[FastMath.nextRandomInt(0, materials.length - 1)]); + geometry.setLocalTranslation(x, 0, z); + return geometry; + } + + @Override + public void simpleInitApp() { + mesh1 = new Sphere(13, 13, 0.4f, true, false); + mesh2 = new Box(0.4f, 0.4f, 0.4f); + + materials[0] = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + materials[0].setBoolean("UseInstancing", true); + materials[0].setColor("Color", ColorRGBA.Red); + + materials[1] = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + materials[1].setBoolean("UseInstancing", true); + materials[1].setColor("Color", ColorRGBA.Green); + + materials[2] = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + materials[2].setBoolean("UseInstancing", true); + materials[2].setColor("Color", ColorRGBA.Blue); + + materials[3] = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + materials[3].setBoolean("UseInstancing", true); + materials[3].setColor("Color", ColorRGBA.Cyan); + + materials[4] = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + materials[4].setBoolean("UseInstancing", true); + materials[4].setColor("Color", ColorRGBA.Magenta); + + materials[5] = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + materials[5].setBoolean("UseInstancing", true); + materials[5].setColor("Color", ColorRGBA.Yellow); + + instancedNode = new InstancedNode("instanced_node"); + instancedNode.setCullHint(CullHint.Never); + + rootNode.attachChild(instancedNode); + + int extent = 30; + + for (int y = -extent; y < extent; y++) { + for (int x = -extent; x < extent; x++) { + Geometry instance = createInstance(x, y); + + float height = (smoothstep(0, 1, FastMath.nextRandomFloat()) * 2.5f) - 1.25f; + instance.setUserData("height", height); + instance.setUserData("dir", 1f); + + instancedNode.attachChild(instance); + } + } + + instancedNode.instance(); + + cam.setLocation(new Vector3f(38.373516f, 6.689055f, 38.482082f)); + cam.setRotation(new Quaternion(-0.04004206f, 0.918326f, -0.096310444f, -0.38183528f)); + flyCam.setMoveSpeed(15); + //flyCam.setEnabled(false); + } + + private float smoothstep(float edge0, float edge1, float x) { + // Scale, bias and saturate x to 0..1 range + x = FastMath.clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + // Evaluate polynomial + return x * x * (3 - 2 * x); + } + + + @Override + public void simpleUpdate(float tpf) { + time += tpf; + + if (time > 1f) { + time = 0f; + + for (Spatial instance : instancedNode.getChildren()) { + if (!(instance instanceof InstancedGeometry)) { + Geometry geom = (Geometry) instance; + geom.setMaterial(materials[FastMath.nextRandomInt(0, materials.length - 1)]); + + Mesh mesh; + if (FastMath.nextRandomInt(0, 1) == 1) mesh = mesh2; + else mesh = mesh1; + geom.setMesh(mesh); + } + } + } + + for (Spatial child : instancedNode.getChildren()) { + if (!(child instanceof InstancedGeometry)) { + float val = child.getUserData("height"); + float dir = child.getUserData("dir"); + + val += (dir + ((FastMath.nextRandomFloat() * 0.5f) - 0.25f)) * tpf; + + if (val > 1f) { + val = 1f; + dir = -dir; + } else if (val < 0f) { + val = 0f; + dir = -dir; + } + + Vector3f translation = child.getLocalTranslation(); + translation.y = (smoothstep(0, 1, val) * 2.5f) - 1.25f; + + child.setUserData("height", val); + child.setUserData("dir", dir); + + child.setLocalTranslation(translation); + } + } + } +} diff --git a/jme3-examples/src/main/java/jme3test/scene/instancing/TestInstancing.java b/jme3-examples/src/main/java/jme3test/scene/instancing/TestInstancing.java deleted file mode 100644 index 2496b4cb2..000000000 --- a/jme3-examples/src/main/java/jme3test/scene/instancing/TestInstancing.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright (c) 2009-2012 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.input.KeyInput; -import com.jme3.input.controls.ActionListener; -import com.jme3.input.controls.AnalogListener; -import com.jme3.input.controls.KeyTrigger; -import com.jme3.material.Material; -import com.jme3.math.Quaternion; -import com.jme3.math.Vector3f; -import com.jme3.scene.Geometry; -import com.jme3.scene.Node; -import com.jme3.scene.Spatial; -import com.jme3.scene.Spatial.CullHint; -import com.jme3.scene.instancing.InstancedGeometry; -import com.jme3.scene.shape.Sphere; - -public class TestInstancing extends SimpleApplication { - - private InstancedGeometry instancedGeometry; - private Node instancedGeoms; - private Material material; - private boolean enabled = true; - - public static void main(String[] args){ - TestInstancing app = new TestInstancing(); - //app.setShowSettings(false); - //app.setDisplayFps(false); - //app.setDisplayStatView(false); - app.start(); - } - - private Geometry createInstance(float x, float z) { - // Note: it doesn't matter what mesh or material we set here. - Geometry geometry = new Geometry("randomGeom", instancedGeometry.getMesh()); - geometry.setMaterial(instancedGeometry.getMaterial()); - geometry.setLocalTranslation(x, 0, z); - return geometry; - } - - @Override - public void simpleInitApp() { - initInputs(); - - Sphere sphere = new Sphere(10, 10, 0.5f, true, false); - material = new Material(assetManager, "Common/MatDefs/Misc/ShowNormals.j3md"); - material.setBoolean("UseInstancing", true); - - instancedGeometry = new InstancedGeometry(InstancedGeometry.Mode.Auto, "instanced_geom"); - instancedGeometry.setMaxNumInstances(60 * 60); - instancedGeometry.setCurrentNumInstances(60 * 60); - instancedGeometry.setCullHint(CullHint.Never); - instancedGeometry.setMesh(sphere); - instancedGeometry.setMaterial(material); - rootNode.attachChild(instancedGeometry); - - instancedGeoms = new Node("instances_node"); - - // Important: Do not render these geometries, only - // use their world transforms to instance them via - // InstancedGeometry. - instancedGeoms.setCullHint(CullHint.Always); - - for (int y = -30; y < 30; y++) { - for (int x = -30; x < 30; x++) { - Geometry instance = createInstance(x, y); - instancedGeoms.attachChild(instance); - } - } - - rootNode.attachChild(instancedGeoms); - rootNode.setCullHint(CullHint.Never); - - int instanceIndex = 0; - for (Spatial child : instancedGeoms.getChildren()) { - if (instanceIndex < instancedGeometry.getMaxNumInstances()) { - instancedGeometry.setInstanceTransform(instanceIndex++, child.getWorldTransform()); - } - } - - instancedGeometry.setCurrentNumInstances(instanceIndex); - - cam.setLocation(new Vector3f(38.373516f, 6.689055f, 38.482082f)); - cam.setRotation(new Quaternion(-0.04004206f, 0.918326f, -0.096310444f, -0.38183528f)); - flyCam.setMoveSpeed(15); - } - - private void initInputs() { - inputManager.addMapping("toggle", new KeyTrigger(KeyInput.KEY_SPACE)); - - ActionListener acl = new ActionListener() { - - public void onAction(String name, boolean keyPressed, float tpf) { - if (name.equals("toggle") && keyPressed) { - if (enabled) { - enabled = false; - instancedGeoms.setCullHint(CullHint.Dynamic); - instancedGeometry.setCullHint(CullHint.Always); - material.setBoolean("UseInstancing", false); - System.out.println("Instancing OFF"); - } else { - enabled = true; - instancedGeoms.setCullHint(CullHint.Always); - instancedGeometry.setCullHint(CullHint.Never); - material.setBoolean("UseInstancing", true); - System.out.println("Instancing ON"); - } - } - } - }; - - inputManager.addListener(acl, "toggle"); - } -}