Sky visibility in a simple scene

A peculiar bug in my game has been annoying me for over a month. The best workaround I found was to make sure there was always an unshaded mesh in the camera’s view. Today I finally succeeded in boiling 25K lines of code down to a simple test app – see below – but the phenomenon still puzzles me.

The essence of the bug is that the visibility of the sky depends on the presence or visibility of other geometries. In the test app below, the red “cousin” geometry is centered in the camera’s initial view. Turning the camera (with the mouse) so that the cousin is out of view causes the sky to disappear – the viewport reverts to background color. Adjust the camera so that the cousin is in view, and the sky reappears.

Pressing the ‘N’ key causes a pink “niece” geometry (located at the origin, outside the camera’s initial view) to toggle out of and into the scene graph. (I include a simple scene graph dump to you help visualize what’s going on.)

Removing the niece causes the sky to be visible regardless of whether the brother is in view or not. Adding her back in causes the on-off behavior of the sky to resume.

I’m curious whether this phenomenon can be reproduced by others. Any idea what’s going on here?

[java]
package mygame;

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.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
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.Texture;
import com.jme3.util.SkyFactory;

/**

  • A simple JME3 app to demonstrate rendering “blackouts” which occur with a

  • cube-mapped sky.
    */
    public class Main
    extends SimpleApplication
    implements ActionListener {

    Node uncleNode = new Node(“uncle”);
    Geometry cousinGeometry;
    Node brotherNode = new Node(“brother”);
    Geometry nieceGeometry;

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

    @Override
    public void simpleInitApp() {
    rootNode.attachChild(uncleNode);

     Node motherNode = new Node("mother");
     rootNode.attachChild(motherNode);
    
     motherNode.attachChild(brotherNode);
    
     Material cousinMaterial =
             new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
     cousinMaterial.setColor("Color", ColorRGBA.Red);
     Box smallCube = new Box(0.1f, 0.1f, 0.1f);
     cousinGeometry = new Geometry("cousin", smallCube);
     cousinGeometry.setMaterial(cousinMaterial);
     cousinGeometry.setLocalTranslation(0f, 1f, -3f);
     uncleNode.attachChild(cousinGeometry);
    
     Material nieceMaterial =
             new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
     nieceMaterial.setColor("Color", ColorRGBA.Pink);
     nieceGeometry = new Geometry("niece", smallCube);
     nieceGeometry.setMaterial(nieceMaterial);
     brotherNode.attachChild(nieceGeometry);
    
     Texture side = assetManager.loadTexture("Interface/Logo/Monkey.png");
     Spatial sky = SkyFactory.createSky(assetManager, side, side, side,
             side, side, side);
     motherNode.attachChild(sky);
    
     Vector3f cameraLocation = new Vector3f(0f, 1f, 0f);
     cam.setLocation(cameraLocation);
    
     printSubtree(rootNode, "");
    
     inputManager.addMapping("toggle", new KeyTrigger(KeyInput.KEY_N));
     inputManager.addListener(this, "toggle");
    

    }

    @Override
    public void onAction(String actionString, boolean ongoing, float unused) {
    if (!ongoing) {
    return;
    }
    if (“toggle”.equals(actionString)) {

         if (nieceGeometry.getParent() == null) {
             System.out.println("\n ATTACHING niece\n");
             brotherNode.attachChild(nieceGeometry);
             
         } else {
             System.out.println("\n DETACHING niece\n");
             brotherNode.detachChild(nieceGeometry);
         }
         
         printSubtree(rootNode, "");
     }
    

    }

    static void printSubtree(Spatial spatial, String indent) {
    assert indent != null;

     if (spatial == null) {
         return;
     }
     System.out.printf("%s%c ", indent, describeType(spatial));
     System.out.printf("\"%s\"", spatial.getName());
     System.out.println();
     if (spatial instanceof Node) {
         Node node = (Node) spatial;
         for (Spatial child : node.getChildren()) {
             printSubtree(child, indent + "  ");
         }
     }
    

    }

    static char describeType(Spatial spatial) {
    if (spatial instanceof Geometry) {
    return ‘g’;
    } else if (spatial instanceof Node) {
    return ‘n’;
    }
    return ‘?’;
    }
    }

[/java]

motherNode.attachChild(sky);

Try attaching the sky to the root instead of a child node. My guess is that mother node is getting culled because if only the sky is in view then it has no bounds. (Which would be a bug but there’s also no particularly good reason to put the sky in a non-root node.)

Attaching the sky as a child of rootNode does indeed prevent it from disappearing.

My game uses SimpleWater, which wants all geometries visible in reflections to live in a subtree. I’d assumed that if the sky were attached to rootNode, it wouldn’t be visible reflected in the water. However, it turns out that this is not the case, so attaching the sky there should provide a workaround.

Thanks.

Oops! There was a bug in my SimpleWater test.

It seems my old assumption about sky attachment with SimpleWater was correct after all: if the sky is attached to rootNode, it isn’t visible in reflections. So that won’t serve a workaround for my game.

However, I suspect you (@pspeed) are right about culling being the culprit. I’ll experiment with Spatial.setCullHint() and see if that helps.

Adding sky.setCullHint(Spatial.CullHint.Never); fixes the bug in the test app, but not in my game.

sigh

I need to step away from the computer for a spell.

@sgold said: Adding sky.setCullHint(Spatial.CullHint.Never); fixes the bug in the test app, but not in my game.

sigh

I need to step away from the computer for a spell.

Then it seems like there might be a bug or that you are maybe running an old JME. I checked the code before posting before and the factory seems to already set the cull hint to Never.

Status update:

(1) After exercise, food, and a nap, I feel like a brand new monkey :slight_smile:
(2) I’m working in 3.0Stable at this point, with occasional forays to the bleeding edge.
(3) @pspeed is correct that SkyFactory sets the cull hint.
(4) My conclusion that sky.setCullHint(Spatial.CullHint.Never); fixed the bug was erroneous, probably due a testing error on my part.
(5) However, rootNode.setCullHint(Spatial.CullHint.Never); DOES fix the bug, both in the test app and in my game.

For efficiency sake, I should set the cull hint to dynamic wherever possible, but at least I see a way forward.

I’m not sure whether this leaves any action items for JME3 code and/or documentation. Neither TestSimpleWater nor TestSceneWater seems to suffer from this pitfall, so perhaps it’s a corner case. If not, perhaps it should be mentioned in the wiki.

It would be interesting to know what the world bounds is of the root node (or mother node or whatever) in this case. The sky is supposed to have an infinite sized world bound… maybe that is failing or something.

rootNode.getWorldBound().toString() returns

BoundingBox [Center: (0.0, 1.0, -3.0) xExtent: 0.1 yExtent: 0.100000024 zExtent: 0.099999905]

sky.getWorldBound.toString() returns:

BoundingSphere [Radius: Infinity Center: (0.0, 0.0, 0.0)]

Since rootNode is sky’s grandparent, shouldn’t rootNode’s bounds have infinite extent also?

@pspeed said: It would be interesting to know what the world bounds is of the root node (or mother node or whatever) in this case. The sky is supposed to have an infinite sized world bound... maybe that is failing or something.

The sky is simply not culled, so it doesn’t depend on the bounds really.

@normen said: The sky is simply not culled, so it doesn't depend on the bounds really.

Except the parent node of the sky IS being culled… which is the entire point of this thread, really.

So I’m trying to figure out why and we are narrowing in on some things. Now I’m starting to think that the javadoc is right and it depends on the order the child is attached… which means the sky factory’s setting of CullHint.Never is misleading since that can only ever work in the root node.

@sgold said: rootNode.getWorldBound().toString() returns

BoundingBox [Center: (0.0, 1.0, -3.0) xExtent: 0.1 yExtent: 0.100000024 zExtent: 0.099999905]

sky.getWorldBound.toString() returns:

BoundingSphere [Radius: Infinity Center: (0.0, 0.0, 0.0)]

Since rootNode is sky’s grandparent, shouldn’t rootNode’s bounds have infinite extent also?

Ok, so combining this with something Normen said… try setting the cull hint of the sky to Inherit. In theory that should fix the bounding shape of the root node. If not then there’s a bug there. (Note: I do not think this is the proper solution it would just confirm some things.)

@pspeed: Setting the cull hint of the sky to Inherit in the test app does not alter the app’s behavior as far as I can tell.

@sgold said: @pspeed: Setting the cull hint of the sky to Inherit in the test app does not alter the app's behavior as far as I can tell.

But was is the world bound of the root node or mother node after doing that?

Oh this is interesting:

root: BoundingBox [Center: (0.0, 1.0, -3.0) xExtent: 0.1 yExtent: 0.100000024 zExtent: 0.099999905]
mother: BoundingBox [Center: (NaN, NaN, NaN) xExtent: NaN yExtent: NaN zExtent: NaN]

Yeah, I didn’t expect the math to work right.

Basically, as written, a Sky cannot work in a node other than root unless you do some extra scene graph setup. And attachment order indeed has nothing to with it (I looked at the code) and that bit of documentation now seems erroneous.

If you want your sky to work in a non-root node then you need to set CullHint.Never on that node… basically, any parent of the sky up to the root needs cull hint set to Never. (root should be unnecessary since I don’t think it is ever culled but I could be wrong… but normally sky works in the root node so I’m going to guess it’s ok to not set root to cull never.)

There is no magic going on in this case… and that’s kind of the “problem” but I think having the scene graph automatically mark the ancestral tree as cull Never is bad for other reasons since it won’t remember why it did it to reverse it if the sky is removed. You, the user, does remember.

The workaround you suggest is exactly what I’ve implemented in my game. So that’s taken care of.

I haven’t looked, but I suspect the bounding volume calculations could be modified to avoid those NaNs, and that might eliminate the need for a workaround. I’ll leave that to more experienced hands.

Now that I understand how Spatial.setCullHint() works, perhaps I can compose a better JavaDoc for it.

@sgold said: The workaround you suggest is exactly what I've implemented in my game. So that's taken care of.

I haven’t looked, but I suspect the bounding volume calculations could be modified to avoid those NaNs, and that might eliminate the need for a workaround. I’ll leave that to more experienced hands.

Now that I understand how Spatial.setCullHint() works, perhaps I can compose a better JavaDoc for it.

Well, “infinite bounds” is sort of a hack, in my opinion to get “don’t ever cull this” which is something that CullHint.Never already takes care of.

I suspect that the code that tries to expand the bounding box around an infinite sphere just gets really confused.

I believe I’ve tracked the bug down to com.jme3.bounding.BoundingBox.merge(Vector3f, float, float, float, BoundingBox).

When Node.updateWorldBound() is called on the “mother” node, the calculation of the new center in line 535 produces NaN while attempting to average +Infinity and -Infinity. To handle the “infinite bounds” hack, a test for non-numbers is needed here. I will code a fix, test, and submit it.

1 Like

It would be good to have that hole fixed.

Do note that the proper solution to your original problem is to mark the appropriate hierarchy as CullHint.Never, thus avoiding even doing the frustum check in the first place. (Pretty sure because of the infinite bounds it would have to check all six planes.)