InstancedNode is incompatible with shadow renderer

Hi!

While working with InstancedNode I faced unpleasant situation. Stack trace:

SEVERE: Uncaught exception thrown in Thread[jME3 Main,5,main]
com.jme3.renderer.RendererException: Attempting to upload empty buffer (limit = 0), that's an error
	at com.jme3.renderer.lwjgl.LwjglGL.checkLimit(LwjglGL.java:22)
	at com.jme3.renderer.lwjgl.LwjglGL.glBufferData(LwjglGL.java:74)
	at com.jme3.renderer.opengl.GLRenderer.updateBufferData(GLRenderer.java:2590)
	at com.jme3.renderer.opengl.GLRenderer.setVertexAttrib(GLRenderer.java:2738)
	at com.jme3.renderer.opengl.GLRenderer.renderMeshDefault(GLRenderer.java:3019)
	at com.jme3.renderer.opengl.GLRenderer.renderMesh(GLRenderer.java:3069)
	at com.jme3.material.logic.DefaultTechniqueDefLogic.renderMeshFromGeometry(DefaultTechniqueDefLogic.java:67)
	at com.jme3.material.logic.DefaultTechniqueDefLogic.render(DefaultTechniqueDefLogic.java:95)
	at com.jme3.material.Technique.render(Technique.java:166)
	at com.jme3.material.Material.render(Material.java:1025)
	at com.jme3.renderer.RenderManager.renderGeometry(RenderManager.java:598)
	at com.jme3.renderer.queue.RenderQueue.renderGeometryList(RenderQueue.java:266)
	at com.jme3.renderer.queue.RenderQueue.renderShadowQueue(RenderQueue.java:275)
	at com.jme3.shadow.AbstractShadowRenderer.renderShadowMap(AbstractShadowRenderer.java:439)
	at com.jme3.shadow.AbstractShadowRenderer.postQueue(AbstractShadowRenderer.java:412)
	at com.jme3.renderer.RenderManager.renderViewPort(RenderManager.java:1103)
	at com.jme3.renderer.RenderManager.render(RenderManager.java:1158)
	at com.jme3.app.SimpleApplication.update(SimpleApplication.java:253)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.runLoop(LwjglAbstractDisplay.java:151)
	at com.jme3.system.lwjgl.LwjglDisplay.runLoop(LwjglDisplay.java:197)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.run(LwjglAbstractDisplay.java:232)
	at java.lang.Thread.run(Thread.java:748)

Below you can find a sample application which will show the issue:


import com.jme3.app.SimpleApplication;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.light.PointLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.instancing.InstancedNode;
import com.jme3.scene.shape.Box;
import com.jme3.shadow.PointLightShadowRenderer;

public class TestRemovedInstanceGeometry extends SimpleApplication implements ActionListener {
    private static final String INPUT_TOGGLE_BOX = "ToggleBox";
    private static final Mesh boxMesh = new Box(0.5f, 0.5f, 0.5f);

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

    private Geometry box;
    private InstancedNode instancedNode;
    private boolean enabled = true;

    @Override
    public void simpleInitApp() {
        instancedNode = new InstancedNode("testInstancedNode");
        instancedNode.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
        rootNode.attachChild(instancedNode);

        box = createBox();
        box.setLocalTranslation(Vector3f.ZERO);
        instancedNode.attachChild(box);
        instancedNode.instance();

        Geometry floor = new Geometry("Floor", new Box(7.0f, 0.2f, 7.0f));
        Material floorMaterial = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        floorMaterial.setColor("Diffuse", ColorRGBA.Green);
        floorMaterial.setBoolean("UseMaterialColors", true);
        floor.setMaterial(floorMaterial);
        floor.setShadowMode(RenderQueue.ShadowMode.Receive);
        floor.setLocalTranslation(new Vector3f(0, -2.5f, 0));
        rootNode.attachChild(floor);

        PointLight pointLight = new PointLight();
        pointLight.setColor(ColorRGBA.White.mult(3f));
        pointLight.setRadius(20f);
        pointLight.setPosition(new Vector3f(-1f, 3f, -4f));
        rootNode.addLight(pointLight);

        PointLightShadowRenderer pointLightShadowRenderer = new PointLightShadowRenderer(assetManager, 256);
        pointLightShadowRenderer.setLight(pointLight);
        viewPort.addProcessor(pointLightShadowRenderer);

        cam.setLocation(new Vector3f(-8f, 3f, 0));
        cam.lookAtDirection(new Vector3f(8f, -3f, 0), Vector3f.UNIT_Y);
        flyCam.setMoveSpeed(10.0f);

        inputManager.addMapping(INPUT_TOGGLE_BOX, new KeyTrigger(KeyInput.KEY_SPACE));
        inputManager.addListener(this, INPUT_TOGGLE_BOX);
    }

    private Geometry createBox() {
        Geometry box = new Geometry("Box", boxMesh);
        Material material = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        material.setBoolean("UseInstancing", true);
        material.setColor("Diffuse", ColorRGBA.Red);
        material.setBoolean("UseMaterialColors", true);
        box.setMaterial(material);

        return box;
    }

    @Override
    public void onAction(String name, boolean isPressed, float tpf) {
        if (isPressed) {
            return;
        }

        if (name.equals(INPUT_TOGGLE_BOX)) {
            enabled = !enabled;

            if (enabled) {
                instancedNode.attachChild(box);
            } else {
                box.removeFromParent();
            }

            instancedNode.instance();
        }
    }
}

There is an instanced box which casts shadow from the point light on the floor. Pressing spacebar removes/adds box from the scene. Exception will happen if to remove the box (with spacebar) when it is in the cam’s frustum, then turn away from the box (which is absent now), and then add the box back (again with spacebar).

Investigation shows next. When we add the geometry to the InstancedNode and call instance(), this geometry’s InstancedGeometry.updateInstances() will not be called.

InstancedGeometry.updateInstances() which updates vertex buffer is called only as a part of InstancedNodeControl.render(). When RenderManager renders the scene, it skips InstancedNodeControl.render() if all the instanced geometries are culled because of this line:

        if (!scene.checkCulling(vp.getCamera())) {
            return;
        }

So, when we add geometry to the InstancedNode, if all InstancedGeometry current geometries are culled, InstanceGeometry vertex buffer will not be updated. It is not a big deal in case of lighting phase (because why waste CPU updating things which will not be seen?). But when we reach shadow phase, situation changes: culled geometries from viewport camera perspective are not culled for shadow cameras (because even if geometry is behind viewport camera, its shadow could be seen). Here comes a problem: shadow renderer receives InstancedNode with InstancedGeometry without updated vertex buffer. If previously there were no such geometries, we will have an exception as above when shadow renderer tries to build its shadow maps. If some the same geometries existed before, we will probably not have an exception, but vertex buffer will not be up to date if all the instanced geometries are culled from the viewport camera.

I see some solutions to the problem:

  1. Force InstancedGeometry.updateInstances() on InstancedNode.instance() - bad because geometry worldMatrix can be changed any time, and we need geometry correct position/rotation/scale even if geometry is behind the camera (culled) in order to cast correct shadow.
  2. Skip vieport camera culling for InstancedGeometry (InstancedNode) - seems the best approach for me.
  3. Call InstancedGeometry.updateInstances() somewhere in the AbstractShadowRenderer, but it will fix the problem only if shadow renderer is present. If I use another scene processor, they will face the same problem. So this solution looks weak.

Thoughts?

Edit: issue can be resolved with instancedNode.setCullHint(Spatial.CullHint.Never); (using approach #2 from above solutions), though I wonder why InstancedNode doesn’t set such cull hint by default, while it sets such cull hint for its InstancedGeometry children: ig.setCullHint(CullHint.Never);. The more instanced geometries there are, the more chance that InstancedNode will not be culled. I think it will mostly never be culled, unless there are very few geometries like in the provided sample application. I propose to set CullHint.Never in the InstancedNode constructor by default. What do you think?

Patch:

--- jme3-core/src/main/java/com/jme3/scene/instancing/InstancedNode.java	(date 1526953408000)
+++ jme3-core/src/main/java/com/jme3/scene/instancing/InstancedNode.java	(date 1527162866802)
@@ -180,12 +180,14 @@
 
     public InstancedNode() {
         super();
+        setCullHint(CullHint.Never);
         // NOTE: since we are deserializing,
         // the control is going to be added automatically here.
     }
 
     public InstancedNode(String name) {
         super(name);
+        setCullHint(CullHint.Never);
         control = new InstancedNodeControl(this);
         addControl(control);
     }
1 Like

Mhhh indeed culling the instance node is not likely to happen a lot…
I will look into it. Thanks for the use case and the analysis.