JME3.2 MacOs: Offscreen rendering leads to "Invalid refresh flags for spatial Root Node, flags: RF_BOUND" warning

Hello all togehter,

I have a question regarding a warning while doing offscreen renderings (Camera view extender).

I have created an appState which loads a PreView ViewPort in the Init code.

protected void initialize(Application app) {
 //...
 offView = sApp.getRenderManager().createPreView("farView", newCam);
 //...
}

From a controls context I update the content of the viewport dynamically with components from the rootNode scenegraph.

new AbstractControl() {
        @Override
        protected void controlRender(RenderManager rm, ViewPort vp) {
        }

        @Override
        protected void controlUpdate(float tpf) {
            // ...
            offView.clearScenes();
            sApp.getRootNode().getChildren()
                    .stream()
                    .filter(child -> !child.equals(farView))
                    .forEach(offView::attachScene);
            // ...
        }
    }

When I run this code (whenever something from rootNode gets visible on the farView screen I get the following warning (Important: I have controls which perform rotations or translations).

WARNUNG: Invalid refresh flags for spatial Root Node, flags: RF_BOUND
Sep 14, 2017 6:59:05 PM com.jme3.scene.Spatial checkCulling

When using JME3.1 I even get an SceneGraph has changed after calling updateGeometricState() error. Is there anything wrong with the code? I can’t see at the moment.

Regards,
Harry

Well, when do you call your offView updates in relation to everything else?

You have an ordering problem somewhere in the code we can’t see. You can either try to put together a simple test case that illustrates the issue or do a bunch more debugging of your code.

I suspect it’s going to be tough for us to debug it from here. Suffice it to say that these checks are really simple and are never “wrong”.

Note: if I read your controlUpdate() properly you are getting the children from the main root node and then attaching them to your other scenes… it’s going to be super super super important that this runs at the exact right time related to both updates. I also question why this is a control as it’s doing global stuff… that’s usually for an app state to do.

Thank you very much for the quick response. I have added the control for another purpose earlier here and didn’t remove it from here because it seemed to work. After shifting the update to the appstate update (see below) no change in behavior occurred.
Currently I am investigating the code once more and preparing a test case to reconstruct the warnings because obviously I didn’t understand the update sequence properly.

I would like to share the code of the appstate:

 package sk.util.scene.infinity;

 import com.jme3.app.Application;
 import com.jme3.app.SimpleApplication;
 import com.jme3.app.state.BaseAppState;
 import com.jme3.material.Material;
 import com.jme3.math.ColorRGBA;
 import com.jme3.math.FastMath;
 import com.jme3.math.Vector3f;
 import com.jme3.post.FilterPostProcessor;
 import com.jme3.post.filters.BloomFilter;
 import com.jme3.renderer.Camera;
 import com.jme3.renderer.ViewPort;
 import com.jme3.renderer.queue.RenderQueue;
 import com.jme3.scene.Geometry;
 import com.jme3.scene.Node;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.shape.Box;
 import com.jme3.texture.FrameBuffer;
 import com.jme3.texture.Image;
 import com.jme3.texture.Texture;
 import com.jme3.texture.Texture2D;
 import java.util.Optional;
 import java.util.logging.Logger;

 /**
   * the InfinityViewAppState works as an view extender to visualize objects
   * beyond the far plane of the cameras viewport by adding another viewport with
   * a camera of a larger frustum for scene offscreen rendering.
   *
   * @author harryschwenk
   */
 public class InfinityViewAppState extends BaseAppState {

private static final Logger LOG = Logger
        .getLogger(InfinityViewAppState.class.getName());

// init --> cleanup
private ViewPort offView = null;
private Node farView;

// Finals
private final float farPlaneDistance;
private final int resolutionScaleShift;
private SimpleApplication sApp;
private Camera newCam;

/**
 * Constructor of the Infinity app state. The first argument represents the
 * distance of the screen which shall be just inside the camera frustum to
 * be the farthest object visible.
 *
 *
 * @param farPlaneDistance Distance of the background screen for the far
 * view beyond the frustum of current view.
 * @param resolutionScaleShift Resolution factor (*2^-n) for the next view.
 * 1 halfs the resolutionn, 2 quarters it.
 */
public InfinityViewAppState(float farPlaneDistance, int resolutionScaleShift) {
    this.farPlaneDistance = farPlaneDistance;
    this.resolutionScaleShift = resolutionScaleShift;
}

/**
 *
 * @param tpf
 */
@Override
public void update(float tpf) {
    updateGraphic();
}

/**
 * The node which contains the background screen.
 *
 * @return
 */
public Optional<Spatial> getFarView() {
    return Optional.ofNullable(farView);
}

/**
 * private method where the background element and the offscreen viewport is being updated.
 */
private void updateGraphic() {
    newCam.setFrustum(
            sApp.getCamera().getFrustumNear() * farPlaneDistance,
            sApp.getCamera().getFrustumFar() * farPlaneDistance,
            sApp.getCamera().getFrustumLeft() * farPlaneDistance,
            sApp.getCamera().getFrustumRight() * farPlaneDistance,
            sApp.getCamera().getFrustumTop() * farPlaneDistance,
            sApp.getCamera().getFrustumBottom() * farPlaneDistance);

    farView.setLocalTranslation(sApp.getCamera().getLocation()
            .add(sApp.getCamera().getRotation()
                    .mult(new Vector3f(0, 0, farPlaneDistance))));

    farView.setLocalRotation(sApp.getCamera().getRotation());
    newCam.setLocation(sApp.getCamera().getLocation());
    newCam.setRotation(sApp.getCamera().getRotation());

    // Reattach root node: Yes I know it is slow. I'll care about performance later...
    offView.clearScenes();
    sApp.getRootNode().getChildren()
            .stream()
            // Exclude the plane at the outer border of the frustum. Otherwise pictures will be recursive
            //                        .filter(child -> planeForExclusion.get().map(other -> !child.equals(other)).orElse(true))
            //                        // Exclude the plane at the inner border of the frustum. Otherwise picutres will be recursive
            .filter(child -> !child.equals(farView))
            .forEach(offView::attachScene);
}

/**
 * Initialize viewport to perform offscreen rendering of the whole scene
 * with a camera with a bigger frustum: the background and the viewport are
 * generated here.
 *
 * @param app
 */
@Override
protected void initialize(Application app) {
    sApp = ((SimpleApplication) app);

    newCam = sApp.getCamera().clone();
    int width = newCam.getWidth() >> resolutionScaleShift;
    int height = newCam.getHeight() >> resolutionScaleShift;

    Texture2D offTex = new Texture2D(width, height, Image.Format.RGBA8);
    offTex.setMinFilter(Texture.MinFilter.Trilinear);
    offTex.setMagFilter(Texture.MagFilter.Bilinear);

    FrameBuffer offBuffer = new FrameBuffer(width, height, 1);
    offBuffer.setDepthBuffer(Image.Format.Depth);
    offBuffer.setColorTexture(offTex);

    offView = sApp.getRenderManager().createPreView("farView", newCam);
    offView.setClearFlags(true, true, true);
    offView.setOutputFrameBuffer(offBuffer);
    FilterPostProcessor fpp = new FilterPostProcessor(sApp.getAssetManager());

    BloomFilter bf = new BloomFilter(BloomFilter.GlowMode.Objects);
    fpp.addFilter(bf);
    bf.setBlurScale(0.0f);
    offView.addProcessor(fpp);

    Geometry screen = new Geometry();
    screen
            .setMesh(new Box(sApp.getCamera().getFrustumRight() * farPlaneDistance, sApp
                    .getCamera().getFrustumTop() * farPlaneDistance, 0F));
    screen
            .setMaterial(new Material(sApp.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"));
    screen.getMaterial().setColor("Color", ColorRGBA.White);
    screen.getMaterial().setTexture("ColorMap", offTex);
    screen.rotate(0, FastMath.PI, 0);
    screen.getMaterial().getAdditionalRenderState().setDepthWrite(true);
    screen.getMaterial().getAdditionalRenderState().setDepthTest(true);

    farView = new Node("~farplane" + this);
    farView.setQueueBucket(RenderQueue.Bucket.Opaque);
    farView.attachChild(screen);

    sApp.getRootNode().attachChild(farView);
}

/**
 * Cleanup.
 *
 * @param app
 */
@Override
protected void cleanup(Application app) {
    offView.clearScenes();
    offView.clearProcessors();

    ((SimpleApplication) app).getRootNode().detachChild(farView);
    farView = null;

    app.getRenderManager().removePreView(offView);
    offView = null;
}

@Override
protected void onEnable() {

}

@Override
protected void onDisable() {

}
}

Also the test case (part of simple application):

   @Override
    public void simpleInitApp() {
       for (long cntr = 0; cntr < 64; cntr++) {
        // Some colored boxes: the next one is twice as big than the precursor and also moved 
        // twice as far away
        // box sizes: 1: 2m * 2m * 2m
        // 2: 4m * 4m * 4m
        // 3: 8m * 8m * 8m
        // ...
        // 64: 1948 light years * 1948 light years * 1948 light years
        Box boxMesh = new Box(1 << cntr, 1 << cntr, 1 << cntr);
        Geometry boxGeo = new Geometry("Colored Box", boxMesh);
        Material boxMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        boxMat.setColor("Color", new ColorRGBA(1 - (cntr / 32F), cntr / 32F, (0.25F * cntr) % 1, 0.1F));
        boxGeo.setMaterial(boxMat);
        System.out.println("box " + cntr + " moved " + 4 * FastMath.pow(1.98F, cntr) + " away");
        boxGeo.move(100, 100, -150 - 4 * FastMath.pow(1.98F, cntr));

        final long cntrA = 32 - cntr;
        boxGeo.addControl(new AbstractControl() {
            @Override
            protected void controlUpdate(float tpf) {
                getSpatial().rotate(0.1F * tpf * cntrA / 32F, 0.1F * tpf * cntrA / 32F, 0.1F * tpf * cntrA / 32F);

            }

            @Override
            protected void controlRender(RenderManager rm, ViewPort vp) {
            }
        });
        rootNode.attachChild(boxGeo);
    }

    // Common use case: Stack the Infinity app states like this and view should increase 
    // to 1000000 km without artefacts at close distance.
    getStateManager().attach(new InfinityViewAppState(999.9F * 999.9F * 999.9F, 0)); // 1e6 km - 1e9 km
    getStateManager().attach(new InfinityViewAppState(999.9F * 999.9F, 0)); // 1000 km - 1e6 km
    getStateManager().attach(new InfinityViewAppState(999.9F, 0)); // 1 km - 1000 km
}

unfortunately with this test case I couldn’t find the problem. I continue investigating. Maybe some of you can say anything whats wrong with the sequence of the appstate. Thanks in advance.

Regards, Harry