Instancing Discussion

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.

Mhh ok… well maybe we can work on that though…

Eh I don’t think there’s a point.

Even as gpus get better and nearly every pc in existence supports 3.1 and onwards we’ll always have macOS stuck at 2.0 because reasons.

So the options will be either have a compatibility mode for macs or not support them at all. Or just not use instancing.

Its not a good idea to allocate FloatBuffers every frame.
To update a vertex buffer:

VertexBuffer vb = mesh.getBuffer(VertexBuffer.Type.InstanceData);
FloatBuffer fb = (FloatBuffer)vb.getData();
//do any changes to fb, but remember what buffers are
//buffers have position, mark, limit, capacity
//   -capacity is maximum length, this does not change
//   if you need bigger capacity, either change the buffer or create new instanced geom
//   -limit is the current length, make sure the limit 
//	  corresponds with your (instance count)x(data per instance)
			
//eg lets clear all and add new
//clear
fb.clear();
//just make sure object.size can fit into the buffer
for (int i = 0; i < objects.size(); i++) {
    Matrix4f mat4 = objects.get(i).transform.toTransformMatrix();
    mat4.fillFloatBuffer(data, true);
}
//flip will set the proper limit
fb.flip();
			
//finally update the buffer
vb.updateData(data);
//update number of instances(instanceCount) in the mesh
mesh.updateCounts();

You start by creating a buffer that can hold enough data for you. Eg. you create one that can hold 1k shots.
It’s the same principle how for example array list manages its array. Once the capacity is not enough, you either enlarge the buffer or create a separate one with new geom.

1 Like

I should mark this thread and read it later.

I played around with the code above. I tried to add light and shadow to this code.

First I need to change the last row of transformMatrix, so the light works correctlly.

    for (int i = 0; i < objects.size(); i++) {
        Shot shot = objects.get(i);
        Matrix4f mat4 = shot.transform.toTransformMatrix();
        
        Quaternion rot = shot.transform.getRotation();
        mat4.m30 = rot.getX();
        mat4.m31 = rot.getY();
        mat4.m32 = rot.getZ();
        mat4.m33 = rot.getW();
        
        mat4.fillFloatBuffer(fb, true);
    }

But I duno how to make shadow work.

This is the code.

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.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Matrix4f;
import com.jme3.math.Quaternion;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.post.FilterPostProcessor;
import com.jme3.post.filters.FXAAFilter;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
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.scene.shape.Quad;
import com.jme3.shadow.DirectionalLightShadowFilter;
import com.jme3.shadow.EdgeFilteringMode;
import com.jme3.util.BufferUtils;

public class MCVE extends SimpleApplication {

    public final static float COOL_DOWN = 0.02f;
    public final static float LIFE = 10f;
    public final static int MAX_BUFFER_LENGTH = (int) (LIFE / COOL_DOWN * 16 * 2);
    
    private float cd = 0;
    private boolean instance = true;
    
    private boolean shoot = false;

    private Mesh mesh;
    private Geometry model;
    private ArrayList<Shot> objects = new ArrayList<Shot>();

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

    @Override
    public void simpleInitApp() {
        setupCamera();
        setupLights();
        setupScene();
        setupInput();
    }
    
    /**
     * setup camera
     */
    private void setupCamera() {
        flyCam.setMoveSpeed(10);
        cam.setFrustumPerspective(70f, (float) cam.getWidth() / cam.getHeight(), 1f, 1000f);
        cam.setLocation(Vector3f.UNIT_Z.mult(10f));
    }
    
    /**
     * setup lights
     */
    private void setupLights() {
        // add lights
        ColorRGBA lightColor = new ColorRGBA(1, 1, 1, 1);
        
        DirectionalLight sun = new DirectionalLight();
        sun.setDirection(new Vector3f(-3, -4, -5).normalizeLocal());
        sun.setColor(lightColor.mult(0.7f));
        
        AmbientLight ambient = new AmbientLight();
        ambient.setColor(lightColor.mult(0.3f));
        
        rootNode.addLight(sun);
        rootNode.addLight(ambient);
        
        FilterPostProcessor fpp = new FilterPostProcessor(assetManager);
        viewPort.addProcessor(fpp);
        
        // add shadow filter
        DirectionalLightShadowFilter dlsf = new DirectionalLightShadowFilter(assetManager, 1024, 4);
        dlsf.setLight(sun);
        dlsf.setEdgeFilteringMode(EdgeFilteringMode.PCFPOISSON);
        dlsf.setRenderBackFacesShadows(false);
        fpp.addFilter(dlsf);
        
        // add fxaa filter
        FXAAFilter fxaa = new FXAAFilter();
        fpp.addFilter(fxaa);
    }
    
    /**
     * setup scene
     */
    private void setupScene() {
        // setup material
        Material material = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        material.setColor("Diffuse", new ColorRGBA(0.7f, 1f, 1f, 1f));
        material.setColor("Ambient", new ColorRGBA(0.7f, 1f, 1f, 1f));
        material.setColor("Specular", ColorRGBA.White);
        material.setBoolean("UseMaterialColors", true);
        material.setBoolean("UseInstancing", instance);

        // create a cube mesh
        mesh = new Box(1f, 1f, 1f);
        
        // setup instance data
        VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.InstanceData);
        vb.setInstanced(true);
        FloatBuffer data = BufferUtils.createFloatBuffer(MAX_BUFFER_LENGTH);
        vb.setupData(Usage.Static, 16, VertexBuffer.Format.Float, data);
        
        mesh.setBuffer(vb);
        
        // setup model
        model = new Geometry("cube", mesh);
        model.setCullHint(CullHint.Never);
        model.setLocalTranslation(0, 0, 0);
        model.setMaterial(material);
        rootNode.attachChild(model);

        rootNode.setCullHint(CullHint.Never);
        
        // make a floor to receive shadow
        Material mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        mat.setColor("Diffuse", new ColorRGBA(1f, 0.8f, 0.6f, 1f));
        mat.setColor("Ambient", new ColorRGBA(1f, 0.8f, 0.6f, 1f));
        mat.setColor("Specular", new ColorRGBA(1, 1, 1, 1));
        mat.setBoolean("UseMaterialColors", true);
        
        Geometry floor = new Geometry("floor", new Quad(2000, 2000));
        floor.setMaterial(mat);
        floor.rotate(-0.5f * FastMath.PI, 0, 0);
        floor.center();
        
        rootNode.attachChild(floor);
        
        // setup shadow mode
        floor.setShadowMode(ShadowMode.Receive);
        model.setShadowMode(ShadowMode.CastAndReceive);
    }
    
    /**
     * setup input
     */
    private void setupInput() {
        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) {
        cd += tpf;
        if (shoot) {
            if (cd >= COOL_DOWN) {
                cd = 0f;
                
                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 vb = mesh.getBuffer(VertexBuffer.Type.InstanceData);
        FloatBuffer fb = (FloatBuffer)vb.getData();
        
        fb.clear();
        for (int i = 0; i < objects.size(); i++) {
            Shot shot = objects.get(i);
            Matrix4f mat4 = shot.transform.toTransformMatrix();
            
            Quaternion rot = shot.transform.getRotation();
            mat4.m30 = rot.getX();
            mat4.m31 = rot.getY();
            mat4.m32 = rot.getZ();
            mat4.m33 = rot.getW();
            
            mat4.fillFloatBuffer(fb, true);
        }
        fb.flip();

        vb.updateData(fb);
        mesh.updateCounts();
        mesh.updateBound();
    }
}

class Shot {

    float ttl = MCVE.LIFE;
    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));
    }

}

For what it’s worth, the instancing I use in the IsoSurfaceDemo is compatible with JME’s shaders, supports lighting, shadows, etc… I create my own meshes but they work fine with JME materials.

As you can see from your screenshots, there are shadows, but they are “cut off”.
Furthermore, don’t forget that I have used CullHint.Never just for testing.

In other words, mesh.updateBounds() will update the bounding volume as if it were normal non instanced mesh.

Thus, it is up to you to calculate the correct bounding volume.
Eg. if you use

mesh.setBound(new BoundingBox(Vector3f.ZERO, 1000,1000,1000));

the shadows with work within 1000 units around origin.

2 Likes

Cool