Instancing Discussion

Continuing discussion from 3.2.0 alpha phase - #4 by MoffKalast.

I think this is the thread I was remembering:

Doesn’t seem like it ever got resolved however, so I probably mixed it up with some other as well.

Anyhow he seems to reference a TestInstanceNode in jme3test.scene.instancing

https://hub.jmonkeyengine.org/uploads/default/original/2X/6/6d6d569e9e1cb62e28589476659640c703037e71.png

Which is an hardly a test when you think about it. All of these geometries are not even close to being high in poly count (if you look closely, the spheres are hardly subdivided). The kind of thing that batching would work wonders for…

According to

That test case should have a fps loss as well, no? Unless I’m missing something.

Well, it’s a better test than grass… but yes, still not great.

However, if that test is moving the objects then it’s still not too bad. Moving instanced objects in this case is going to be better than rebatching them all the time.

Compared to batching, sure. But I’m not convinced it would be faster than just not doing either if there was movement involved.

Thousands of draw calls are always going to be worse than one draw call. Always. (Always.)

Note: and if you can produce a good test case where this isn’t true then it points to JME’s management of those children being the issue.

edit: also note: you can use instancing in JME without using the InstancedNode thing. For my own projects, I don’t use InstancedNode but create my meshes directly with instancing enabled. (So my block of instanced trees is one Geometry, for example). And I know for a super-duper fact that this is faster… so that points to even more likely JME management being the cause if a test case really is slower.

Unless that

per object setup inside the driver and that’s not free

outweighs it? For a large amount of objects (or objects that are created and destroyed quickly?) that could very well be the case. I’m just speculating though.

Huh I wasn’t aware of that. Well I mean I guess having all geometries point to the same mesh object is a part of it, but that’s still being drawn separately. Does the UseInstancing material flag fully enable it or is there something else needed to be configured?

To pass instanced data to a shader, all you need to do is call VertexBuffer.setInstanced(true) on a vertex buffer attribute.

Then such attribute will be instanced. If you are writing custom shaders, thats all that has to be done.

The instance flag on the material I think controls whether or not it’s looking for non-instanced vertex attributes for your transforms. You can create these buffers manually and still use the instancing support built into standard JME materials.

The nice thing about creating your own mesh with instanced buffers is that you can choose which ones to instance and which ones to not… you also have even more control than that but that’s somewhat off topic. So you could have a color buffer that is not instanced but a position buffer that is, etc…

This should be also way worse with batching and marginally worse with non-instancing over instancing.

Frankly, it’s hard to talk about hypothetical code. Someone needs to put together a valid example of where instancing is slower than non-instancing and then we can talk about what the specific issues are.

Otherwise, my answer is that it was a magic pixies problem.

Wow, that’s superb. I guess that means we can instance texcoord, color and index buffers on animations as well?

I’m not sure what you mean by creating the mesh yourself @pspeed. Is there any difference from loading a blender exported one and setting the flag on that mesh’s buffers?

I guess I was over-generalizing a bit. This new approach does change everything however, so I’ll have to make some tests before I can say anything more.

Edit: Is it possible for this process to change any material parameters and rendering buckets? Seems like all objects I add this to end up not rendering. I’ll test a bit more however, could be something with my filters.

Okay @pspeed is this how it’s supposed to be setup? Because with instancing enabled everything vanishes…

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.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.shape.Sphere;

public class MCVE extends SimpleApplication{
	
	public static void main(String[] args) {
		MCVE app = new MCVE();
		app.start();
	}

	@Override
	public void simpleInitApp(){		
		flyCam.setMoveSpeed(10);
		cam.setFrustumPerspective(70f, (float) cam.getWidth() / cam.getHeight(), 1f, 800000f);
		cam.setLocation(Vector3f.UNIT_Z.mult(10f));
		
        DirectionalLight sun = new DirectionalLight();
        sun.setDirection(new Vector3f(0.5f,0.4f,0.1f));
        sun.setColor(ColorRGBA.White.clone().multLocal(1f));
        rootNode.addLight(sun);
        
        AmbientLight al = new AmbientLight();
        al.setColor(new ColorRGBA(0.05f, 0.05f, 0.05f, 1.0f));
        rootNode.addLight(al);
		
		boolean instance = true;		
		
		Mesh sphere = new Sphere(256,256,5, true, false);
		sphere.getBuffer(VertexBuffer.Type.Index).setInstanced(instance);
		sphere.getBuffer(VertexBuffer.Type.Position).setInstanced(instance);
		sphere.getBuffer(VertexBuffer.Type.TexCoord).setInstanced(instance);
		sphere.getBuffer(VertexBuffer.Type.Normal).setInstanced(instance);
		
		for (int i = -5; i < 5; i++) {
			for (int j = -5; j < 5; j++) {
				for (int k = -5; k < 5; k++) {
					
			        Material stone_mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
			        stone_mat.setTexture("DiffuseMap", assetManager.loadTexture("Textures/Terrain/Rock/Rock.PNG"));
			        stone_mat.setBoolean("UseInstancing", instance);
					
					Geometry model = new Geometry("sph", sphere);
					model.setLocalTranslation(i*15, j*15, k*15);
					model.setMaterial(stone_mat);
					rootNode.attachChild(model);
					
				}
			}
		}

	}
}

No.

  • Use only one geometry.
  • Check which attributes are instanced in your material.
    → you are using “Common/MatDefs/Light/Lighting.j3md”
    …-> check Common/MatDefs/Light/Lighting.vert
    … …->it imports from Common/ShaderLib/Instancing.glsllib
    check this file as it has helpful comments regarding the instancing for materials using this

Excerpt from “Common/ShaderLib/Instancing.glsllib”

// 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;

This is your instanced data
Thus in jme, you set only VertexBuffer.Type.InstancedData as instanced when using the mentioned material.

What does the InstancedData buffer contain?

  • the excerpt from “Common/ShaderLib/Instancing.glsllib” describes this:
    it wants the world matrix: only top 3 rows and rotation
  • you supply as many of those as you have your instances: thus if you want to make 100 instances the InstancedData will contain 100x(World matrix top 3 rows + rot).

Some caveats:

//To create a buffer to hold numberOfInstances
data = BufferUtils.createFloatBuffer(16*numberOfInstances);

dataVb = new VertexBuffer(VertexBuffer.Type.InstanceData);
dataVb.setInstanced(true); //setting Instanced to true
dataVb.setupData(<usage...>, 16, VertexBuffer.Format.Float, data);
//Set the vertex buffer on the mesh
mesh.setBuffer(texSlotVb);
//Calculates the number of instances, don't forget to call
mesh.updateCounts();

//Not sure wheter bounds are calculated properly with instances
//mesh.updateBound();
//If not, its up to you to calculate the bound
//OR for testing, set CullHint.Never on your Geometry
geom = new Geometry("g", mesh);
geom.setCullHint(Spatial.CullHint.Never);
2 Likes

Oh. Now I see. Thanks. So one needs to actually feed the transform data about objects into the material of a single geometry…

sigh I thought this was going to be useful. With so much overhead I might as well just use a custom mesh and adjust the buffers, giving me what is basically already implemented as the particle emitter. No wonder nobody uses this damn thing.

I mean how are you supposed to handle this with objects that aren’t final and constant? One needs to be a “main” one and others are kind of shader copies of it. Really awkward if at one point there isn’t any of them on the scene at all and the main one needs to be removed.

Just one question, you’re saying that the instancedData is a buffer, why is it defined as a single mat4?

attribute mat4 inInstanceData;

Shouldn’t it be something more like:

attribute mat4 inInstanceData[];

Edit: @The_Leo Hold on, would it be possible to cull the main geometry and only render the instances? Wait, is there even a “main” geometry? Or am I looking at a bunch of geoms with no instances set, rendering nothing?

When you issue a draw command for an instanced object.

  • The shader is executed for each instance.
  • The instanced attribute, eg inInstancedData has the data for the currently being proccessed instance only. Thus that is why it is defined as “attribute mat4 inInstanceData;”

The advantage of instancing is that you issue one draw command to render many objects.

^ As explained above. The shader is executed x times the number of instances. Thus, eg if you would create a geom and leave the instanced data empty - eg. 0 instances. Nothing would be rendered. (If zero instances is allowed.) Thus, there is no “main” geometry.

Instancing is useful for rendering the same geometry multiple times:

  • eg dense foilage, trees, grass, flowers, etc
  • billboards, partices, …

Furthermore, you are not limited to using the existing shaders. You can write your own instanced shaders.
For example, you could instance 8 floats: 3 for position, 4 for color, etc. It is up to you to decide.

1 Like

Right, makes sense. And then the shader uses that specific data to set transforms, got it.

I should be able to get it working now.

1 Like

Alright so I’ve applied that to my prior example and it seems to just freeze up on starting.

import java.nio.FloatBuffer;

import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.math.Matrix4f;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.VertexBuffer.Usage;
import com.jme3.scene.shape.Sphere;
import com.jme3.util.BufferUtils;

public class MCVE extends SimpleApplication{
	
	public static void main(String[] args) {
		MCVE app = new MCVE();
		app.start();
	}

	@Override
	public void simpleInitApp(){		
		flyCam.setMoveSpeed(10);
		cam.setFrustumPerspective(70f, (float) cam.getWidth() / cam.getHeight(), 1f, 800000f);
		cam.setLocation(Vector3f.UNIT_Z.mult(10f));
		
		boolean instance = true;		
		
		Mesh mesh = new Sphere(256,256,5, true, false);
		mesh.getBuffer(VertexBuffer.Type.Index).setInstanced(instance);
		mesh.getBuffer(VertexBuffer.Type.Position).setInstanced(instance);
		mesh.getBuffer(VertexBuffer.Type.TexCoord).setInstanced(instance);
		mesh.getBuffer(VertexBuffer.Type.Normal).setInstanced(instance);
		
        Material stone_mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        stone_mat.setTexture("ColorMap", assetManager.loadTexture("Textures/Terrain/Rock/Rock.PNG"));
        stone_mat.setBoolean("UseInstancing", instance);
		
		Geometry model = new Geometry("sph", mesh);
		model.setLocalTranslation(0, 0, 0);
		model.setMaterial(stone_mat);
		rootNode.attachChild(model);
		
		VertexBuffer dataVb = new VertexBuffer(VertexBuffer.Type.InstanceData);
		dataVb.setInstanced(true);
		
		FloatBuffer data = BufferUtils.createFloatBuffer(16*(14*14*14));
		
		for (int i = -7; i < 7; i++) {
			for (int j = -7; j < 7; j++) {
				for (int k = -7; k < 7; k++) {
					
					Transform t = new Transform();
					t.setTranslation(i*15, j*15, k*15);
					
					Matrix4f mat4 = t.toTransformMatrix();
					
					data.put(mat4.toFloatBuffer());
				}
			}
		}
		
		dataVb.setupData(Usage.Static, 16, VertexBuffer.Format.Float, data);
		
		mesh.setBuffer(dataVb);
		mesh.updateCounts();

	}
}

Also I’m not sure what to pick in the Usage enum. The mesh itself isn’t going to change so I figured Static would be good, but does position/rotation changing matter?

The usage on InstancedData buffers relates to the usage of the content inside the InstancedData buffer. Yes static is fine if you do not intend to change it often.

Here is the your test case working.

import java.nio.FloatBuffer;

import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Matrix4f;
import com.jme3.math.Quaternion;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
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.Usage;
import com.jme3.scene.shape.Sphere;
import com.jme3.util.BufferUtils;

public class MCVE extends SimpleApplication {
	
	public static void main(String[] args) {
		MCVE app = new MCVE();
		app.start();
	}

	@Override
	public void simpleInitApp(){		
		flyCam.setMoveSpeed(10);
		cam.setFrustumPerspective(70f, (float) cam.getWidth() / cam.getHeight(), 1f, 1000f);
		cam.setLocation(Vector3f.UNIT_Z.mult(10f));
		
		boolean instance = true;		
		
		//65280 Vertex per sphere is pretty much, right
		//Mesh mesh = new Sphere(256,256,5, true, false);
		Mesh mesh = new Sphere(25,25,5, true, false);

         //These Are not instanced but shared among instances
         // mesh.getBuffer(VertexBuffer.Type.Index).setInstanced(instance);
	//mesh.getBuffer(VertexBuffer.Type.Position).setInstanced(instance);
        //mesh.getBuffer(VertexBuffer.Type.TexCoord).setInstanced(instance);
	//mesh.getBuffer(VertexBuffer.Type.Normal).setInstanced(instance);
		
        Material stone_mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        stone_mat.setTexture("ColorMap", assetManager.loadTexture("Textures/Terrain/Rock/Rock.PNG"));
        stone_mat.setBoolean("UseInstancing", instance);
		
		
		VertexBuffer dataVb = new VertexBuffer(VertexBuffer.Type.InstanceData);
		dataVb.setInstanced(true);
		
		
		FloatBuffer data = BufferUtils.createFloatBuffer(16*(14*14*14));
		
		
		for (int i = -7; i < 7; i++) {
			for (int j = -7; j < 7; j++) {
				for (int k = -7; k < 7; k++) {
					Transform t = new Transform();
					t.setTranslation(i*15, j*15, k*15);
					
					Matrix4f mat4 = t.toTransformMatrix();
					
					//The last 4 are rotation
					/*mat4.m30 = 0; 
					mat4.m31 = 0;
					mat4.m32 = 0;
					mat4.m33 = 1;*/
										
					
					//data.put(mat4.toFloatBuffer());  //WRONG ORDER
					//USE
					//data.put(mat4.toFloatBuffer(true));
					
					//OR
					mat4.fillFloatBuffer(data, true);
				}
			}
		}
		
		data.flip();
		
		dataVb.setupData(Usage.Static, 16, VertexBuffer.Format.Float, data);
		
		mesh.setBuffer(dataVb);
		mesh.updateCounts();
		mesh.updateBound();
		
		Geometry model = new Geometry("sph", mesh);
		model.setCullHint(Spatial.CullHint.Never);
		
		model.setLocalTranslation(0, 0, 0);
		model.setMaterial(stone_mat);
		rootNode.attachChild(model);
		
		rootNode.setCullHint(Spatial.CullHint.Never);

	}
}
2 Likes

Awesome thanks!

It is somewhat setup to not be changed however, which isn’t really what I need (projectiles). The good thing is that my idea of having it dynamically rebuilt every frame works as well:

import java.nio.FloatBuffer;
import java.util.ArrayList;

import com.jme3.app.SimpleApplication;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.material.Material;
import com.jme3.math.Matrix4f;
import com.jme3.math.Quaternion;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Spatial.CullHint;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.VertexBuffer.Usage;
import com.jme3.scene.shape.Box;
import com.jme3.util.BufferUtils;

public class MCVE extends SimpleApplication {
	
	private static boolean shoot = false;
	
	private static Mesh boxes;
	private static Geometry model;
	private static ArrayList<Shot> objects = new ArrayList<Shot>();
	
	public static void main(String[] args) {
		MCVE app = new MCVE();
		app.start();
	}

	@Override
	public void simpleInitApp(){		
		flyCam.setMoveSpeed(10);
		cam.setFrustumPerspective(70f, (float) cam.getWidth() / cam.getHeight(), 1f, 1000f);
		cam.setLocation(Vector3f.UNIT_Z.mult(10f));
		
		boolean instance = true;		
		
		boxes = new Box(1f,1f,1f);
		
        Material stone_mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        stone_mat.setTexture("ColorMap", assetManager.loadTexture("Textures/Terrain/Rock/Rock.PNG"));
        stone_mat.setBoolean("UseInstancing", instance);
        
		model = new Geometry("sph", boxes);
		model.setCullHint(CullHint.Never);
		model.setLocalTranslation(0, 0, 0);
		model.setMaterial(stone_mat);
		rootNode.attachChild(model);
		
		rootNode.setCullHint(CullHint.Never);
		
        inputManager.addListener(new ActionListener() {
            public void onAction(String name, boolean isPressed, float tpf) {
            	if(name.equals("shoot"))
            		shoot = isPressed;
            }
        }, "shoot");
        inputManager.addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
	}
	
	@Override
    public void simpleUpdate(float tpf){
		if(shoot){
			Vector3f side = cam.getRotation().getRotationColumn(0).mult(3);
			
			objects.add(new Shot(cam.getLocation().add(side),cam.getRotation()));
			objects.add(new Shot(cam.getLocation().add(side.negate()),cam.getRotation()));
		}
		
		for (int i = 0; i < objects.size(); i++) {
			Shot b = objects.get(i);
			b.ttl-=tpf;
			
			if(b.ttl > 0)
				b.update(tpf);
			else
			{
				objects.remove(b);
				i--;
			}
		}
		
		recalculate();
	}

	private void recalculate() {
		if(objects.size() == 0){
			return;
		}
		
		VertexBuffer dataVb = new VertexBuffer(VertexBuffer.Type.InstanceData);
		dataVb.setInstanced(true);

		FloatBuffer data = BufferUtils.createFloatBuffer(16*objects.size());
		
		for (int i = 0; i < objects.size(); i++) {
			Matrix4f mat4 = objects.get(i).transform.toTransformMatrix();
			mat4.fillFloatBuffer(data, true);
		}
		
		data.flip();
		dataVb.setupData(Usage.Static, 16, VertexBuffer.Format.Float, data);
		
		boxes.clearBuffer(VertexBuffer.Type.InstanceData);
		boxes.setBuffer(dataVb);
		boxes.updateCounts();
		boxes.updateBound();
	}
}

class Shot{
	
	float ttl = 10;
	Transform transform;
	
	public Shot(Vector3f loc, Quaternion dir){
		transform = new Transform();
		transform.setTranslation(loc);
		transform.setRotation(dir);
	}
	
	public void update(float tpf){
		Vector3f dir = transform.getRotation().getRotationColumn(2).mult(60*tpf);
		transform.setTranslation(transform.getTranslation().add(dir));
	}
	
}

Funny thing, the previous test I posted was made with static (very highly subdivided) spheres ran at like 30 fps when not instanced and around 5 fps otherwise.

So I figured I should test with something low poly like boxes. Yet here it seems to perform in a splendid fashion.

I think the idea that instancing is for heavy meshes is somewhat off, with the main difference being the possibility of easier geometry moving here. (and not having to murder yourself with buffer editing when batching)

It’s still a bit annoying that the InstanceData buffer needs to be cleared every frame, but I don’t see an alternative from all the errors I get trying to edit the existing one.

Having to set everything to not cull itself is a bit of a hard sell as well.

The main problem is probably that this requires one hell of an infrastructure - one that has to be fully disabled and replaced with something else when not running on opengl 3.1+

What errors?

Not sure why all this is easier than using the InstanceNode though. It does pretty much all what you ended up to do.
Anyway… at least now you’ve seen it for yourself, Instancing seems very nice on the paper, but in practice… not that much, for all the reasons you mentioned.

Something something data has already been sent to the gpu error if I recall right. (on dataVb.setupData since I need to replace the floatbuffer with one that’s possibly a different length)

Well it’s not easier by a long shot, it just actually seems to work now in terms of performance gain. All the extra management work the node has to do always seems to negate it and slams framerate into the ground.