IllegalStateException when I try spatial.checkCulling()

So, I’m getting a weird bug. I’m currently trying to implement billboarding spatials. I know I should do this in the shaders, but I have no experience in GLSL and have no idea how to write shaders.

However, if I billboard every object in the world, the game runs pretty slow. So, I’m currently checking wheter the spatial is being culled or not. This way, I only rotate the objects that are being rendered and, as such, improve performance.

The problem is, I’m getting an IllegalStateException:

java.lang.IllegalStateException: Scene graph is not properly updated for rendering.
State was changed after rootNode.updateGeometricState() call. 
Make sure you do not modify the scene from another thread!
Problem spatial name: props.TallGrass

And I’m not modifying the scene from another thread. The code is running in the update() method of my appState, so I think that should be thread-safe.

My VisualAppState:

public class VisualAppState extends AbstractAppState {
    
    private SimpleApplication app;
    private EntityData ed;
    private EntitySet entities;
    public final HashMap<EntityId, Spatial> models;
    public final HashMap<String, BatchNode> batchedNodes;
    private ModelLoader modelLoader;
    
    public VisualAppState() {
        this.models = new HashMap<EntityId, Spatial>();
        this.batchedNodes = new HashMap<String, BatchNode>();
    }
    
    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        super.initialize(stateManager, app);
        
        this.app = (SimpleApplication)app;
        this.ed = this.app.getStateManager().getState(EntityDataState.class).getEntityData();
        this.entities = this.ed.getEntities(Transform.class, Model.class);
        
        this.modelLoader = new ModelLoader(this.app.getAssetManager());
    }
    
    @Override
    public void update(float tpf) {
        if(entities.applyChanges()) {
            removeModels(entities.getRemovedEntities());
            addModels(entities.getAddedEntities());
            updateModels(entities.getChangedEntities());
        }
        for(BatchNode batchNode : batchedNodes.values()) {
            if(batchNode.getUserData("batched").equals(false)) {
                batchNode.setUserData("batched", true);
                batchNode.batch();
            }
        }
        for(Spatial s : batchedNodes.get(ModelLoader.Models.P_TallGrass.name).getChildren()) {
            if(!s.checkCulling(app.getCamera())) {
                s.lookAt(app.getCamera().getLocation(), Vector3f.UNIT_Y);
            }
        }
    }
    
    private void removeModels(Set<Entity> entities) {
        for(Entity e : entities) {
            Spatial s = models.remove(e.getId());
            BatchNode batchNode = batchedNodes.get(s.getName());
            s.removeFromParent();
            if(batchNode.getChildren().isEmpty()) {
                batchNode.removeFromParent();
                batchedNodes.remove(s.getName());
            }
        }
    }

    private void addModels(Set<Entity> entities) {
        for(Entity e : entities) {
            Spatial s = createVisual(e);
            
            models.put(e.getId(), s);
            updateModelSpatial(e, s);
            //this.app.getRootNode().attachChild(s);
            BatchNode batchNode = batchedNodes.get(s.getName());
            if(batchNode == null) {
                batchNode = new BatchNode();
                batchedNodes.put(s.getName(), batchNode);
                this.app.getRootNode().attachChild(batchNode);
            }
            batchNode.setUserData("batched", false);
            batchNode.attachChild(s);
        }
    }
    
    private void updateModels(Set<Entity> entities) {
        for(Entity e : entities) {
            Spatial s = models.get(e.getId());
            updateModelSpatial(e, s);
        }
    }
    
    private void updateModelSpatial(Entity e, Spatial s) {
        Transform t = e.get(Transform.class);
        s.setLocalTranslation(t.getPosition());
        s.setLocalRotation(t.getRotation());
        s.setLocalScale(t.getScale());
    }
    
    private Spatial createVisual(Entity e) {
        Model m = e.get(Model.class);
        return modelLoader.load(m.getModel());
    }
    
    @Override
    public void cleanup() {
        super.cleanup();
    }
}

The entity “props.TallGrass” is added trough GameplayAppState:

public class GameplayAppState extends AbstractAppState{
    
    private SimpleApplication app;
    private AssetManager assetManager;
    private Node rootNode;
    private Camera cam;
    private FlyByCamera flyCam;
    
@Override
    public void initialize(AppStateManager stateManager, Application app) {
        // Sets local variables
        this.app = (SimpleApplication) app;
        this.assetManager = this.app.getAssetManager();
        this.rootNode = this.app.getRootNode();
        this.cam = this.app.getCamera();
        this.flyCam = this.app.getFlyByCamera();
        
        // Loads the terrain
        Spatial terrain = assetManager.loadModel("Scenes/Terrain/Terrain.j3o");
        rootNode.attachChild(terrain);
        // Adds normals
        //TangentBinormalGenerator.generate(terrain);
        
        // Sets the camera's position and velocity
        cam.setLocation(new Vector3f(0, 3, 0));
        flyCam.setMoveSpeed(16);
        
        // Adds lights
        DirectionalLight sun = new DirectionalLight();
        sun.setDirection(new Vector3f(-1, -10, -5));
        sun.setColor(ColorRGBA.White);
        
        AmbientLight ambient = new AmbientLight();
        ambient.setColor(ColorRGBA.Gray);
        
        rootNode.addLight(sun);
        rootNode.addLight(ambient);
        
        // Gets the Terrain object
        Node temp = (Node)terrain;
        Terrain ground = (Terrain)temp.getChild("Terrain");
        temp.getChild("Terrain").getControl(TerrainLodControl.class).setCamera(cam);
        
        // Gets the entityData
        EntityData ed = stateManager.getState(EntityDataState.class).getEntityData();
        
        for(int i = 0; i < 1000; i++) {
            int x = FastMath.nextRandomInt(-256, 256);
            int z = FastMath.nextRandomInt(-256, 256);
            float y = ground.getHeight(new Vector2f(x, z));
            
            EntityId id = ed.createEntity();
            ed.setComponents(id,
                        new Transform(new Vector3f(x, y, z)),
                        new Model(Models.P_Tree1.name));
        }
        
        for(int i = 0; i < 1000; i++) {
            int x = FastMath.nextRandomInt(-256, 256);
            int z = FastMath.nextRandomInt(-256, 256);
            float y = ground.getHeight(new Vector2f(x, z));
            
            EntityId id = ed.createEntity();
            ed.setComponents(id,
                        new Transform(new Vector3f(x, y, z)),
                        new Model(Models.P_TallGrass.name));
        }
        
        // Continues to initialize
        super.initialize(stateManager, app);
    }

    @Override
    public void update(float tpf) {
        
    }

    @Override
    public void cleanup() {
        
        
        // Continues to cleanup
        super.cleanup();
    }
}

On a similar note, I am also getting randomly an exception saying “Compare function result changed!”. I wasn’t able to replicate that exception now, but as soon as it comes out again, I’ll add to the thread.

Thanks,
Ev1lbl0w

Billboarding this way is never going to be fast. Do note that even the BillboardControl would be more efficient because it only changes the orientation if not culled… It also avoids all of the iterator construction.

Ugh… especially if you are changing the children of a batch. That means you have to rebatch that every frame and you lose all (ALL) of the benefits of having a batch in the first place.

If you want batched grass there is no way around doing billboarding in the shader. Culling or not, doesn’t matter. If you want to manually billboard then you should not batch because you lose all of the benefits of batching and add in an expensive batch operation to make things worse.

Edit: really until you want to adopt someone else’s shaders or learn to do billboarding in shaders then you should stay away from batching your grass. This is not the only problem you will encounter with that.

1 Like

Thanks @pspeed. Then I’m gonna need someone to please make a shader for me. One of the main reasons for using an engine is not to have to learn GLSL and shaders.

And also, thanks. I didn’t know I had to batch all objects again if I moved one of them. Had no idea. Thank you, I would be doing something really stupid if it wasn’t you xD

You could try to adapt my code from the IsoSurfaceDemo and library. It has a grass batcher thing and already has a shader and stuff. Even has wind.

1 Like

Thanks @pspeed. I would like to ask permission to copy-paste your Grass vertex and fragment shaders and the Grass.j3md files.

Of course, I’ll add you as my project contributor.

Yeah, you have my permission. The license gives you the permission anyway but thanks for asking. :slight_smile:

1 Like