diff --git a/jme3-android/src/main/java/com/jme3/renderer/android/OGLESShaderRenderer.java b/jme3-android/src/main/java/com/jme3/renderer/android/OGLESShaderRenderer.java index 7c90e0969..919c9dd03 100644 --- a/jme3-android/src/main/java/com/jme3/renderer/android/OGLESShaderRenderer.java +++ b/jme3-android/src/main/java/com/jme3/renderer/android/OGLESShaderRenderer.java @@ -2362,7 +2362,7 @@ public class OGLESShaderRenderer implements Renderer { clearTextureUnits(); } - public void renderMesh(Mesh mesh, int lod, int count) { + public void renderMesh(Mesh mesh, int lod, int count, VertexBuffer[] instanceData) { if (mesh.getVertexCount() == 0) { return; } 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 9faae039b..7bef12e62 100644 --- a/jme3-core/src/main/java/com/jme3/material/Material.java +++ b/jme3-core/src/main/java/com/jme3/material/Material.java @@ -47,6 +47,8 @@ import com.jme3.renderer.RenderManager; import com.jme3.renderer.Renderer; import com.jme3.renderer.queue.RenderQueue.Bucket; import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.instancing.InstancedGeometry; import com.jme3.shader.Shader; import com.jme3.shader.Uniform; import com.jme3.shader.UniformBindingManager; @@ -684,6 +686,17 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable { return ambientLightColor; } + private static void renderMeshFromGeometry(Renderer renderer, Geometry geom) { + Mesh mesh = geom.getMesh(); + int lodLevel = geom.getLodLevel(); + if (geom instanceof InstancedGeometry) { + InstancedGeometry instGeom = (InstancedGeometry) geom; + renderer.renderMesh(mesh, lodLevel, instGeom.getCurrentNumInstances(), instGeom.getAllInstanceData()); + } else { + renderer.renderMesh(mesh, lodLevel, 1, null); + } + } + /** * Uploads the lights in the light list as two uniform arrays.

* *

@@ -858,7 +871,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable { } vars.release(); r.setShader(shader); - r.renderMesh(g.getMesh(), g.getLodLevel(), 1); + renderMeshFromGeometry(r, g); } if (isFirstLight && lightList.size() > 0) { @@ -868,7 +881,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable { lightColor.setValue(VarType.Vector4, ColorRGBA.BlackNoAlpha); lightPos.setValue(VarType.Vector4, nullDirLight); r.setShader(shader); - r.renderMesh(g.getMesh(), g.getLodLevel(), 1); + renderMeshFromGeometry(r, g); } } @@ -1139,7 +1152,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable { r.setShader(shader); } - r.renderMesh(geom.getMesh(), geom.getLodLevel(), 1); + renderMeshFromGeometry(r, geom); } public void write(JmeExporter ex) throws IOException { diff --git a/jme3-core/src/main/java/com/jme3/renderer/Renderer.java b/jme3-core/src/main/java/com/jme3/renderer/Renderer.java index bef528f51..4c1b3605b 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/Renderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/Renderer.java @@ -267,18 +267,23 @@ public interface Renderer { public void deleteBuffer(VertexBuffer vb); /** - * Renders count meshes, with the geometry data supplied. + * Renders count meshes, with the geometry data supplied and + * per-instance data supplied. * The shader which is currently set with setShader is * responsible for transforming the input vertices into clip space * and shading it based on the given vertex attributes. - * The int variable gl_InstanceID can be used to access the current + * The integer variable gl_InstanceID can be used to access the current * instance of the mesh being rendered inside the vertex shader. + * If the instance data is non-null, then it is submitted as a + * per-instance vertex attribute to the shader. * * @param mesh The mesh to render * @param lod The LOD level to use, see {@link Mesh#setLodLevels(com.jme3.scene.VertexBuffer[]) }. * @param count Number of mesh instances to render + * @param instanceData When count is greater than 1, these buffers provide + * the per-instance attributes. */ - public void renderMesh(Mesh mesh, int lod, int count); + public void renderMesh(Mesh mesh, int lod, int count, VertexBuffer[] instanceData); /** * Resets all previously used {@link NativeObject Native Objects} on this Renderer. diff --git a/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java b/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java index 4158be132..9400dc466 100644 --- a/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java +++ b/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java @@ -206,6 +206,14 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable { * either an int or float buffer due to shader attribute types restrictions. */ HWBoneIndex, + + /** + * Information about this instance. + * + * Format should be {@link Format#Float} and number of components + * should be 16. + */ + InstanceData } /** @@ -325,6 +333,7 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable { protected Type bufType; protected Format format; protected boolean normalized = false; + protected transient boolean instanced = false; protected transient boolean dataSizeChanged = false; /** @@ -365,10 +374,14 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable { throw new AssertionError(); } // Are components between 1 and 4? - if (components < 1 || components > 4) { - throw new AssertionError(); - } + // Are components between 1 and 4 and not InstanceData? + if (bufType != Type.InstanceData) { + if (components < 1 || components > 4) { + throw new AssertionError(); + } + } + // Does usage comply with buffer directness? //if (usage == Usage.CpuOnly && data.isDirect()) { // throw new AssertionError(); @@ -531,6 +544,17 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable { return normalized; } + /** + * TODO: + */ + public void setInstanced(boolean instanced) { + this.instanced = instanced; + } + + public boolean isInstanced() { + return instanced; + } + /** * @return The type of information that this buffer has. */ @@ -585,9 +609,12 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable { if (data.isReadOnly()) throw new IllegalArgumentException( "VertexBuffer data cannot be read-only." ); - if (components < 1 || components > 4) - throw new IllegalArgumentException("components must be between 1 and 4"); - + if (bufType != Type.InstanceData) { + if (components < 1 || components > 4) { + throw new IllegalArgumentException("components must be between 1 and 4"); + } + } + this.data = data; this.components = components; this.usage = usage; @@ -982,6 +1009,7 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable { vb.handleRef = new Object(); vb.id = -1; vb.normalized = normalized; + vb.instanced = instanced; vb.offset = offset; vb.stride = stride; vb.updateNeeded = true; @@ -1030,7 +1058,12 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable { return ((long)OBJTYPE_VERTEXBUFFER << 32) | ((long)id); } + @Override public void write(JmeExporter ex) throws IOException { + if (instanced) { + throw new IOException("Serialization of instanced data not allowed"); + } + OutputCapsule oc = ex.getCapsule(this); oc.write(components, "components", 0); oc.write(usage, "usage", Usage.Dynamic); @@ -1064,6 +1097,7 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable { } } + @Override public void read(JmeImporter im) throws IOException { InputCapsule ic = im.getCapsule(this); components = ic.readInt("components", 0); 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 new file mode 100644 index 000000000..1a573ea84 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/scene/instancing/InstancedGeometry.java @@ -0,0 +1,502 @@ +package com.jme3.scene.instancing; + +import com.jme3.export.InputCapsule; +import com.jme3.export.JmeExporter; +import com.jme3.export.JmeImporter; +import com.jme3.export.OutputCapsule; +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 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 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]; + + /** + * Serialization only. Do not use. + */ + public InstancedGeometry() { + super(); + setIgnoreTransform(true); + } + + /** + * 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) { + 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; + } + + /** + * Global user specified per-instance data. + * + * By default set to null, specify an array of VertexBuffers + * via {@link #setGlobalUserInstanceData(com.jme3.scene.VertexBuffer[]) }. + * + * @return global user specified per-instance data. + * @see #setGlobalUserInstanceData(com.jme3.scene.VertexBuffer[]) + */ + public VertexBuffer[] getGlobalUserInstanceData() { + return globalInstanceData; + } + + /** + * Specify global user per-instance data. + * + * By default set to null, specify an array of VertexBuffers + * that contain per-instance vertex attributes. + * + * @param globalInstanceData global user per-instance data. + * + * @throws IllegalArgumentException If one of the VertexBuffers is not + * {@link VertexBuffer#setInstanced(boolean) instanced}. + */ + public void setGlobalUserInstanceData(VertexBuffer[] globalInstanceData) { + this.globalInstanceData = globalInstanceData; + } + + /** + * 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) + */ + 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); + } + + /** + * 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}. + * + * @see Mode + * @see #setCameraUserInstanceData(com.jme3.renderer.Camera, com.jme3.scene.VertexBuffer) + */ + public Map getAllCameraUserInstanceData() { + if (mode == Mode.Auto) { + throw new IllegalStateException("Not allowed in auto mode"); + } + return Collections.unmodifiableMap(instanceDataPerCam); + } + + private void updateInstance(Matrix4f viewMatrix, Matrix4f worldMatrix, float[] store, int offset) { + viewMatrix.mult(worldMatrix, tempMat4); + tempMat4.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 + 3] = tempQuat.getX(); + store[offset + 4] = tempMat4.m01; + store[offset + 5] = tempMat4.m11; + store[offset + 6] = tempMat4.m21; + store[offset + 7] = tempQuat.getY(); + store[offset + 8] = tempMat4.m02; + store[offset + 9] = tempMat4.m12; + store[offset + 10] = tempMat4.m22; + store[offset + 11] = tempQuat.getZ(); + store[offset + 12] = tempMat4.m03; + store[offset + 13] = tempMat4.m13; + store[offset + 14] = tempMat4.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. + * + * This re-allocates internal structures and therefore should be called + * only when necessary. + * + * @param maxNumInstances The maximum number of instances that can be + * rendered. + * + * @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"); + } + if (maxNumInstances < 1) { + throw new IllegalArgumentException("maxNumInstances must be 1 or higher"); + } + + this.worldMatrices = new Matrix4f[maxNumInstances]; + + if (currentNumInstances > maxNumInstances) { + currentNumInstances = maxNumInstances; + } + + // 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); + } + } + } + + public int getMaxNumInstances() { + return worldMatrices.length; + } + + public int getCurrentNumInstances() { + return currentNumInstances; + } + + public void setInstanceTransform(int instanceIndex, Matrix4f worldTransform) { + if (mode == Mode.Manual) { + throw new IllegalStateException("Not allowed in manual mode"); + } + if (worldTransform == null) { + throw new IllegalArgumentException("worldTransform cannot be null"); + } + if (instanceIndex < 0) { + throw new IllegalArgumentException("instanceIndex cannot be smaller than zero"); + } + if (instanceIndex >= currentNumInstances) { + throw new IllegalArgumentException("instanceIndex cannot be larger than currentNumInstances"); + } + // 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"); + } + + // 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); + + setInstanceTransform(instanceIndex, tempMat4.clone()); + } + + public VertexBuffer[] getAllInstanceData() { + VertexBuffer instanceDataForCam = instanceDataPerCam.get(lastCamera); + ArrayList allData = new ArrayList(); + + if (instanceDataForCam != null) { + allData.add(instanceDataForCam); + } + if (globalInstanceData != null) { + allData.addAll(Arrays.asList(globalInstanceData)); + } + + return allData.toArray(new VertexBuffer[allData.size()]); + } + + @Override + 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); + } + } + + @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]; + } + + control = getControl(InstancedGeometryControl.class); + control.geom = this; + } + } +} diff --git a/jme3-core/src/main/java/com/jme3/system/NullRenderer.java b/jme3-core/src/main/java/com/jme3/system/NullRenderer.java index 6a93c8d34..94be6c7c7 100644 --- a/jme3-core/src/main/java/com/jme3/system/NullRenderer.java +++ b/jme3-core/src/main/java/com/jme3/system/NullRenderer.java @@ -137,7 +137,7 @@ public class NullRenderer implements Renderer { public void deleteBuffer(VertexBuffer vb) { } - public void renderMesh(Mesh mesh, int lod, int count) { + public void renderMesh(Mesh mesh, int lod, int count, VertexBuffer[] instanceData) { } public void resetGLObjects() { 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 db480b75f..a2e2172c1 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Misc/ShowNormals.j3md +++ b/jme3-core/src/main/resources/Common/MatDefs/Misc/ShowNormals.j3md @@ -1,10 +1,20 @@ MaterialDef Debug Normals { + MaterialParameters { + // For instancing + Boolean UseInstancing + } + Technique { VertexShader GLSL100: Common/MatDefs/Misc/ShowNormals.vert FragmentShader GLSL100: Common/MatDefs/Misc/ShowNormals.frag WorldParameters { WorldViewProjectionMatrix + ProjectionMatrix } + + Defines { + INSTANCING : UseInstancing + } } } \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/MatDefs/Misc/ShowNormals.vert b/jme3-core/src/main/resources/Common/MatDefs/Misc/ShowNormals.vert index 3813043b0..93cbdc361 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Misc/ShowNormals.vert +++ b/jme3-core/src/main/resources/Common/MatDefs/Misc/ShowNormals.vert @@ -1,4 +1,4 @@ -uniform mat4 g_WorldViewProjectionMatrix; +#import "Common/ShaderLib/Instancing.glsllib" attribute vec3 inPosition; attribute vec3 inNormal; @@ -6,6 +6,6 @@ attribute vec3 inNormal; varying vec3 normal; void main(){ - gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition,1.0); + gl_Position = TransformWorldViewProjection(vec4(inPosition,1.0)); normal = inNormal; } 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 f3711e6bc..5a57426d4 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.j3md +++ b/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.j3md @@ -12,6 +12,9 @@ MaterialDef Unshaded { // The glow color of the object Color GlowColor + // For instancing + Boolean UseInstancing + // For hardware skinning Int NumberOfBones Matrix4Array BoneMatrices @@ -56,9 +59,11 @@ MaterialDef Unshaded { WorldParameters { WorldViewProjectionMatrix + ProjectionMatrix } Defines { + INSTANCING : UseInstancing SEPARATE_TEXCOORD : SeparateTexCoord HAS_COLORMAP : ColorMap HAS_LIGHTMAP : LightMap diff --git a/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.vert b/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.vert index 1d47bb395..63b05ef88 100644 --- a/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.vert +++ b/jme3-core/src/main/resources/Common/MatDefs/Misc/Unshaded.vert @@ -1,6 +1,6 @@ #import "Common/ShaderLib/Skinning.glsllib" +#import "Common/ShaderLib/Instancing.glsllib" -uniform mat4 g_WorldViewProjectionMatrix; attribute vec3 inPosition; #if defined(HAS_COLORMAP) || (defined(HAS_LIGHTMAP) && !defined(SEPARATE_TEXCOORD)) @@ -33,5 +33,6 @@ void main(){ #ifdef NUM_BONES Skinning_Compute(modelSpacePos); #endif - gl_Position = g_WorldViewProjectionMatrix * modelSpacePos; + + gl_Position = TransformWorldViewProjection(modelSpacePos); } \ No newline at end of file diff --git a/jme3-core/src/main/resources/Common/ShaderLib/Instancing.glsllib b/jme3-core/src/main/resources/Common/ShaderLib/Instancing.glsllib new file mode 100644 index 000000000..4bf3fc1e9 --- /dev/null +++ b/jme3-core/src/main/resources/Common/ShaderLib/Instancing.glsllib @@ -0,0 +1,87 @@ +// Instancing GLSL library. +// +// When the INSTANCING define is set in the shader, +// all global matrices are replaced with "instanced" versions. +// One exception is g_NormalMatrix which becomes unusable, +// instead the function ApplyNormalTransform is used to transform +// the normal and tangent vectors into world view space. + +// The world matrix and normal transform quaternion need to be passed +// as vertex attributes "inWorldMatrix" and "inNormalRotationQuaternion" +// respectively. +// The VertexBuffers for those two attributes +// need to be configured into instanced mode (VertexBuffer.setInstanced(true)). +// - inWorldMatrix should have 12 * numInstances floats. +// - inNormalRotationQuaternion should have 4 * numInstances. +// Thus, instancing data occupies 4 vertex attributes (16 / 4 = 4). +// +// The GL_ARB_draw_instanced and GL_ARB_instanced_arrays extensions +// are required (OGL 3.3). + +uniform mat4 g_WorldMatrix; +uniform mat4 g_ViewMatrix; +uniform mat4 g_ProjectionMatrix; +uniform mat4 g_WorldViewMatrix; +uniform mat4 g_WorldViewProjectionMatrix; +uniform mat3 g_NormalMatrix; + +#if defined INSTANCING + +// World Matrix + Normal Rotation Quaternion. +// The World Matrix is the top 3 rows - +// since the bottom row is always 0,0,0,1 for this transform. +// The bottom row is the transpose of the inverse of WorldView Transform +// as a quaternion. i.e. g_NormalMatrix converted to a quaternion. +// +// Using a quaternion instead of a matrix here allows saving approximately +// 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 +// quaternion at the end. +mat4 worldViewMatrix = 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 TransformWorldView(vec4 position) +{ + return worldViewMatrix * position; +} + +vec4 TransformWorldViewProjection(vec4 position) +{ + return g_ProjectionMatrix * TransformWorldView(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); +} + +// Prevent user from using g_** matrices which will have invalid data in this case. +#define g_WorldMatrix Use_the_instancing_functions_for_this +#define g_WorldViewMatrix Use_the_instancing_functions_for_this +#define g_WorldViewProjectionMatrix Use_the_instancing_functions_for_this +#define g_NormalMatrix Use_the_instancing_functions_for_this + +#else + +vec4 TransformWorldView(vec4 position) +{ + return g_WorldViewMatrix * position; +} + +vec4 TransformWorldViewProjection(vec4 position) +{ + return g_WorldViewProjectionMatrix * position; +} + +vec3 TransformNormal(vec3 normal) { + return g_NormalMatrix * normal; +} + +#endif \ No newline at end of file diff --git a/jme3-examples/src/main/java/jme3test/scene/instancing/TestInstancing.java b/jme3-examples/src/main/java/jme3test/scene/instancing/TestInstancing.java new file mode 100644 index 000000000..816f99d61 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/scene/instancing/TestInstancing.java @@ -0,0 +1,111 @@ +/* + * 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.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; + + 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() { + Sphere sphere = new Sphere(10, 10, 0.5f, true, false); + Material 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); + + Node 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); + } + +} diff --git a/jme3-ios/src/main/java/com/jme3/renderer/ios/IGLESShaderRenderer.java b/jme3-ios/src/main/java/com/jme3/renderer/ios/IGLESShaderRenderer.java index 351b92e67..d9bad5e32 100644 --- a/jme3-ios/src/main/java/com/jme3/renderer/ios/IGLESShaderRenderer.java +++ b/jme3-ios/src/main/java/com/jme3/renderer/ios/IGLESShaderRenderer.java @@ -936,7 +936,7 @@ public class IGLESShaderRenderer implements Renderer { * @param lod The LOD level to use, see {@link Mesh#setLodLevels(com.jme3.scene.VertexBuffer[]) }. * @param count Number of mesh instances to render */ - public void renderMesh(Mesh mesh, int lod, int count) { + public void renderMesh(Mesh mesh, int lod, int count, VertexBuffer[] instanceData) { logger.log(Level.FINE, "IGLESShaderRenderer renderMesh"); if (mesh.getVertexCount() == 0) { return; diff --git a/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGL1Renderer.java b/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGL1Renderer.java index e31b1e0d4..491f6df21 100644 --- a/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGL1Renderer.java +++ b/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGL1Renderer.java @@ -1181,7 +1181,7 @@ public class JoglGL1Renderer implements GL1Renderer { resetFixedFuncBindings(); } - public void renderMesh(Mesh mesh, int lod, int count) { + public void renderMesh(Mesh mesh, int lod, int count, VertexBuffer[] instanceData) { if (mesh.getVertexCount() == 0) { return; } diff --git a/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglRenderer.java b/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglRenderer.java index e57c2c0c9..4e48ca409 100644 --- a/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglRenderer.java +++ b/jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglRenderer.java @@ -2554,7 +2554,7 @@ public class JoglRenderer implements Renderer { clearTextureUnits(); } - public void renderMesh(Mesh mesh, int lod, int count) { + public void renderMesh(Mesh mesh, int lod, int count, VertexBuffer[] instanceData) { if (mesh.getVertexCount() == 0) { return; } diff --git a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL1Renderer.java b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL1Renderer.java index 22c1a359c..54ff4fd8d 100644 --- a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL1Renderer.java +++ b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL1Renderer.java @@ -1121,7 +1121,7 @@ public class LwjglGL1Renderer implements GL1Renderer { resetFixedFuncBindings(); } - public void renderMesh(Mesh mesh, int lod, int count) { + public void renderMesh(Mesh mesh, int lod, int count, VertexBuffer[] instanceData) { if (mesh.getVertexCount() == 0) { return; } diff --git a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglRenderer.java b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglRenderer.java index 4db530ead..1403112ce 100644 --- a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglRenderer.java +++ b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglRenderer.java @@ -281,7 +281,7 @@ public class LwjglRenderer implements Renderer { caps.add(Caps.PackedDepthStencilBuffer); } - if (ctxCaps.GL_ARB_draw_instanced) { + if (ctxCaps.GL_ARB_draw_instanced && ctxCaps.GL_ARB_instanced_arrays) { caps.add(Caps.MeshInstancing); } @@ -316,7 +316,8 @@ public class LwjglRenderer implements Renderer { caps.add(Caps.TextureCompressionLATC); } - if (ctxCaps.GL_EXT_packed_float) { + if (ctxCaps.GL_EXT_packed_float || ctxCaps.OpenGL30) { + // This format is part of the OGL3 specification caps.add(Caps.PackedFloatColorBuffer); if (ctxCaps.GL_ARB_half_float_pixel) { // because textures are usually uploaded as RGB16F @@ -329,7 +330,7 @@ public class LwjglRenderer implements Renderer { caps.add(Caps.TextureArray); } - if (ctxCaps.GL_EXT_texture_shared_exponent) { + if (ctxCaps.GL_EXT_texture_shared_exponent || ctxCaps.OpenGL30) { caps.add(Caps.SharedExponentTexture); } @@ -392,8 +393,8 @@ public class LwjglRenderer implements Renderer { caps.add(Caps.Multisample); } - //supports sRGB pipeline - if (ctxCaps.GL_ARB_framebuffer_sRGB && ctxCaps.GL_EXT_texture_sRGB){ + // Supports sRGB pipeline. + if ( (ctxCaps.GL_ARB_framebuffer_sRGB && ctxCaps.GL_EXT_texture_sRGB ) || ctxCaps.OpenGL30 ) { caps.add(Caps.Srgb); } @@ -1529,7 +1530,7 @@ public class LwjglRenderer implements Renderer { } } - if (fb == null) { + if (fb == null) { // unbind any fbos if (context.boundFBO != 0) { glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); @@ -2168,6 +2169,9 @@ public class LwjglRenderer implements Renderer { for (int i = 0; i < attribList.oldLen; i++) { int idx = attribList.oldList[i]; glDisableVertexAttribArray(idx); + if (context.boundAttribs[idx].isInstanced()) { + ARBInstancedArrays.glVertexAttribDivisorARB(idx, 0); + } context.boundAttribs[idx] = null; } context.attribIndexList.copyNewToOld(); @@ -2200,15 +2204,32 @@ public class LwjglRenderer implements Renderer { attrib.setLocation(loc); } } + + int slotsRequired = 1; + if (vb.isInstanced()) { + if (!GLContext.getCapabilities().GL_ARB_instanced_arrays + || !GLContext.getCapabilities().GL_ARB_draw_instanced) { + throw new RendererException("Instancing is required, " + + "but not supported by the " + + "graphics hardware"); + } + if (vb.getNumComponents() > 4 && vb.getNumComponents() % 4 != 0) { + throw new RendererException("Number of components in multi-slot " + + "buffers must be divisible by 4"); + } + slotsRequired = vb.getNumComponents() / 4; + } if (vb.isUpdateNeeded() && idb == null) { updateBufferData(vb); } VertexBuffer[] attribs = context.boundAttribs; - if (!context.attribIndexList.moveToNew(loc)) { - glEnableVertexAttribArray(loc); - //System.out.println("Enabled ATTRIB IDX: "+loc); + for (int i = 0; i < slotsRequired; i++) { + if (!context.attribIndexList.moveToNew(loc + i)) { + glEnableVertexAttribArray(loc + i); + //System.out.println("Enabled ATTRIB IDX: "+loc); + } } if (attribs[loc] != vb) { // NOTE: Use id from interleaved buffer if specified @@ -2222,14 +2243,43 @@ public class LwjglRenderer implements Renderer { //statistics.onVertexBufferUse(vb, false); } - glVertexAttribPointer(loc, - vb.getNumComponents(), - convertFormat(vb.getFormat()), - vb.isNormalized(), - vb.getStride(), - vb.getOffset()); + if (slotsRequired == 1) { + glVertexAttribPointer(loc, + vb.getNumComponents(), + convertFormat(vb.getFormat()), + vb.isNormalized(), + vb.getStride(), + vb.getOffset()); + } else { + for (int i = 0; i < slotsRequired; i++) { + // The pointer maps the next 4 floats in the slot. + // E.g. + // P1: XXXX____________XXXX____________ + // P2: ____XXXX____________XXXX________ + // P3: ________XXXX____________XXXX____ + // P4: ____________XXXX____________XXXX + // stride = 4 bytes in float * 4 floats in slot * num slots + // offset = 4 bytes in float * 4 floats in slot * slot index + glVertexAttribPointer(loc + i, + 4, + convertFormat(vb.getFormat()), + vb.isNormalized(), + 4 * 4 * slotsRequired, + 4 * 4 * i); + } + } - attribs[loc] = vb; + for (int i = 0; i < slotsRequired; i++) { + int slot = loc + i; + if (vb.isInstanced() && (attribs[slot] == null || !attribs[slot].isInstanced())) { + // non-instanced -> instanced + ARBInstancedArrays.glVertexAttribDivisorARB(slot, 1); + } else if (!vb.isInstanced() && attribs[slot] != null && attribs[slot].isInstanced()) { + // instanced -> non-instanced + ARBInstancedArrays.glVertexAttribDivisorARB(slot, 0); + } + attribs[slot] = vb; + } } } else { throw new IllegalStateException("Cannot render mesh without shader bound"); @@ -2241,7 +2291,8 @@ public class LwjglRenderer implements Renderer { } public void drawTriangleArray(Mesh.Mode mode, int count, int vertCount) { - if (count > 1) { + boolean useInstancing = count > 1 && caps.contains(Caps.MeshInstancing); + if (useInstancing) { ARBDrawInstanced.glDrawArraysInstancedARB(convertElementMode(mode), 0, vertCount, count); } else { @@ -2350,7 +2401,7 @@ public class LwjglRenderer implements Renderer { } } - public void updateVertexArray(Mesh mesh) { + public void updateVertexArray(Mesh mesh, VertexBuffer instanceData) { int id = mesh.getId(); if (id == -1) { IntBuffer temp = intBuf1; @@ -2368,6 +2419,10 @@ public class LwjglRenderer implements Renderer { if (interleavedData != null && interleavedData.isUpdateNeeded()) { updateBufferData(interleavedData); } + + if (instanceData != null) { + setVertexAttrib(instanceData, null); + } for (VertexBuffer vb : mesh.getBufferList().getArray()) { if (vb.getBufferType() == Type.InterleavedData @@ -2386,9 +2441,9 @@ public class LwjglRenderer implements Renderer { } } - private void renderMeshVertexArray(Mesh mesh, int lod, int count) { + private void renderMeshVertexArray(Mesh mesh, int lod, int count, VertexBuffer instanceData) { if (mesh.getId() == -1) { - updateVertexArray(mesh); + updateVertexArray(mesh, instanceData); } else { // TODO: Check if it was updated } @@ -2414,25 +2469,25 @@ public class LwjglRenderer implements Renderer { clearTextureUnits(); } - private void renderMeshDefault(Mesh mesh, int lod, int count) { - VertexBuffer indices; - + private void renderMeshDefault(Mesh mesh, int lod, int count, VertexBuffer[] instanceData) { VertexBuffer interleavedData = mesh.getBuffer(Type.InterleavedData); if (interleavedData != null && interleavedData.isUpdateNeeded()) { updateBufferData(interleavedData); } -// IntMap buffers = mesh.getBuffers(); -// SafeArrayList buffersList = mesh.getBufferList(); - + VertexBuffer indices; if (mesh.getNumLodLevels() > 0) { indices = mesh.getLodLevel(lod); } else { indices = mesh.getBuffer(Type.Index); } -// for (Entry entry : buffers) { -// VertexBuffer vb = entry.getValue(); + if (instanceData != null) { + for (VertexBuffer vb : instanceData) { + setVertexAttrib(vb, null); + } + } + for (VertexBuffer vb : mesh.getBufferList().getArray()) { if (vb.getBufferType() == Type.InterleavedData || vb.getUsage() == Usage.CpuOnly // ignore cpu-only buffers @@ -2458,7 +2513,7 @@ public class LwjglRenderer implements Renderer { clearTextureUnits(); } - public void renderMesh(Mesh mesh, int lod, int count) { + public void renderMesh(Mesh mesh, int lod, int count, VertexBuffer[] instanceData) { if (mesh.getVertexCount() == 0) { return; } @@ -2489,7 +2544,7 @@ public class LwjglRenderer implements Renderer { // if (GLContext.getCapabilities().GL_ARB_vertex_array_object){ // renderMeshVertexArray(mesh, lod, count); // }else{ - renderMeshDefault(mesh, lod, count); + renderMeshDefault(mesh, lod, count, instanceData); // } }