TerrainGrid pages out TerrainQuad currently on

I have been using terrainGrid for a while now. I have been prototyping other systems in my game so I have kind of been ignoring something that happens with terraingrid. The problem is that every so often terrainGrid pages out a terrainQuad from underneath you. To start with I thought that it might be something todo with different systems that I attached to the terraingrid on loading such as physics system and navigation mesh. So I went back to the TerrainFractalGridTest.Java and set this up with the size terrain grid that I am using in my game. I have put in a button press that highlights the problem.



Steps to replicate.

  1. Run TerrainFractalGridTest.Java.
  2. Wait until all terrain has paged in.
  3. Press E
  4. The terrainGrid will page out the current terrain quad.
  5. It pages in the two new terrain quad you can see in the view.
  6. TerrainGrid then pages current terrain quad back in.
  7. If you hand characters on the current terrain they have now dropped through the ground.



    The problem with this is that it makes terrain grid unusable in a game. Your characters drop through the ground while moving about in there world.



    Here is the code so highlight this Bug.

    [java]



    package jme3test.terrain;



    import com.jme3.app.SimpleApplication;

    import com.jme3.app.state.ScreenshotAppState;

    import com.jme3.bullet.control.CharacterControl;

    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.terrain.geomipmap.TerrainGrid;

    import com.jme3.terrain.geomipmap.TerrainGridLodControl;

    import com.jme3.terrain.geomipmap.TerrainLodControl;

    import com.jme3.terrain.geomipmap.grid.FractalTileLoader;

    import com.jme3.terrain.geomipmap.lodcalc.DistanceLodCalculator;

    import com.jme3.terrain.noise.ShaderUtils;

    import com.jme3.terrain.noise.basis.FilteredBasis;

    import com.jme3.terrain.noise.filter.IterativeFilter;

    import com.jme3.terrain.noise.filter.OptimizedErode;

    import com.jme3.terrain.noise.filter.PerturbFilter;

    import com.jme3.terrain.noise.filter.SmoothFilter;

    import com.jme3.terrain.noise.fractal.FractalSum;

    import com.jme3.terrain.noise.modulator.NoiseModulator;

    import com.jme3.texture.Texture;

    import com.jme3.texture.Texture.WrapMode;



    public class TerrainFractalGridTest extends SimpleApplication {



    private Material mat_terrain;

    private TerrainGrid terrain;

    private float grassScale = 64;

    private float dirtScale = 16;

    private float rockScale = 128;



    public static void main(final String[] args) {

    TerrainFractalGridTest app = new TerrainFractalGridTest();

    app.start();

    }

    private CharacterControl player3;

    private FractalSum base;

    private PerturbFilter perturb;

    private OptimizedErode therm;

    private SmoothFilter smooth;

    private IterativeFilter iterate;



    @Override

    public void simpleInitApp() {

    this.flyCam.setMoveSpeed(100f);

    ScreenshotAppState state = new ScreenshotAppState();

    this.stateManager.attach(state);



    // TERRAIN TEXTURE material

    this.mat_terrain = new Material(this.assetManager, "Common/MatDefs/Terrain/HeightBasedTerrain.j3md");



    // Parameters to material:

    // regionXColorMap: X = 1…4 the texture that should be appliad to state X

    // regionX: a Vector3f containing the following information:

    // regionX.x: the start height of the region

    // regionX.y: the end height of the region

    // regionX.z: the texture scale for the region

    // it might not be the most elegant way for storing these 3 values, but it packs the data nicely :slight_smile:

    // slopeColorMap: the texture to be used for cliffs, and steep mountain sites

    // slopeTileFactor: the texture scale for slopes

    // terrainSize: the total size of the terrain (used for scaling the texture)

    // GRASS texture

    Texture grass = this.assetManager.loadTexture("Textures/Terrain/splat/grass.jpg");

    grass.setWrap(WrapMode.Repeat);

    this.mat_terrain.setTexture("region1ColorMap", grass);

    this.mat_terrain.setVector3("region1", new Vector3f(15, 200, this.grassScale));



    // DIRT texture

    Texture dirt = this.assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg");

    dirt.setWrap(WrapMode.Repeat);

    this.mat_terrain.setTexture("region2ColorMap", dirt);

    this.mat_terrain.setVector3("region2", new Vector3f(0, 20, this.dirtScale));



    // ROCK texture

    Texture rock = this.assetManager.loadTexture("Textures/Terrain/Rock2/rock.jpg");

    rock.setWrap(WrapMode.Repeat);

    this.mat_terrain.setTexture("region3ColorMap", rock);

    this.mat_terrain.setVector3("region3", new Vector3f(198, 260, this.rockScale));



    this.mat_terrain.setTexture("region4ColorMap", rock);

    this.mat_terrain.setVector3("region4", new Vector3f(198, 260, this.rockScale));



    this.mat_terrain.setTexture("slopeColorMap", rock);

    this.mat_terrain.setFloat("slopeTileFactor", 32);



    this.mat_terrain.setFloat("terrainSize", 513);



    Material GreenWireFrame = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");

    GreenWireFrame.setColor("Color", ColorRGBA.Green);

    GreenWireFrame.getAdditionalRenderState().setWireframe(true);



    this.base = new FractalSum();

    this.base.setRoughness(0.7f);

    this.base.setFrequency(1.0f);

    this.base.setAmplitude(1.0f);

    this.base.setLacunarity(2.12f);

    this.base.setOctaves(8);

    this.base.setScale(0.02125f);

    this.base.addModulator(new NoiseModulator() {



    @Override

    public float value(float… in) {

    return ShaderUtils.clamp(in[0] * 0.5f + 0.5f, 0, 1);

    }

    });



    FilteredBasis ground = new FilteredBasis(this.base);



    this.perturb = new PerturbFilter();

    this.perturb.setMagnitude(0.119f);



    this.therm = new OptimizedErode();

    this.therm.setRadius(5);

    this.therm.setTalus(0.011f);



    this.smooth = new SmoothFilter();

    this.smooth.setRadius(1);

    this.smooth.setEffect(0.7f);



    this.iterate = new IterativeFilter();

    this.iterate.addPreFilter(this.perturb);

    this.iterate.addPostFilter(this.smooth);

    this.iterate.setFilter(this.therm);

    this.iterate.setIterations(1);



    ground.addPreFilter(this.iterate);



    this.terrain = new TerrainGrid("terrain", 65, 513, new FractalTileLoader(ground, 256f));



    this.terrain.setMaterial(this.mat_terrain);

    this.terrain.setLocalTranslation(0, 0, 0);

    this.terrain.setLocalScale(2f, 1f, 2f);

    this.rootNode.attachChild(this.terrain);



    TerrainLodControl control = new TerrainGridLodControl(this.terrain, this.getCamera());

    control.setLodCalculator(new DistanceLodCalculator(65, 1.5f)); // patch size, and a multiplier

    this.terrain.addControl(control);







    this.getCamera().setLocation(new Vector3f(0, 300, 0));



    this.viewPort.setBackgroundColor(new ColorRGBA(0.7f, 0.8f, 1f, 1f));





    // You can map one or several inputs to one named action

    this.inputManager.addMapping("MoveCamera", new KeyTrigger(KeyInput.KEY_E));

    this.inputManager.addListener(this.actionListener, "MoveCamera");





    }



    boolean bFlipFlop=false;

    private final ActionListener actionListener = new ActionListener()

    {

    @Override

    public void onAction(final String name, final boolean keyPressed, final float tpf)

    {

    if (name.equals("MoveCamera")&& keyPressed)

    {

    if(bFlipFlop)

    {

    TerrainFractalGridTest.this.getCamera().lookAtDirection(new Vector3f(-0.6315f, -0.3342f, -0.6995f), TerrainFractalGridTest.this.getCamera().getUp());



    TerrainFractalGridTest.this.getCamera().setLocation(new Vector3f(0, 200, 0));

    bFlipFlop = false;

    }

    else{

    TerrainFractalGridTest.this.getCamera().lookAtDirection(new Vector3f(-0.6315f, -0.3342f, -0.6995f), TerrainFractalGridTest.this.getCamera().getUp());



    TerrainFractalGridTest.this.getCamera().setLocation(new Vector3f(-265, 200, -265));

    bFlipFlop = true;

    }



    }

    }

    };



    @Override

    public void simpleUpdate(final float tpf) {

    }

    }



    [/java]

Thats not a bug, you think too simple. Ofc anything on the terrain would also have to be paged.

I don’t think you have run the sample code that i have posted.



The current tile should never page out under any circumstances.

Yeah, paging the stuff on the terrain… You are thinking spatial = real world object which is too simple. The game data is abstract and the scenegraph is a visualization. If your characters fall through the ground because the visual display is off then you have no need for paging at all or you are doing it wrong as in you should abstract your game data and just diplay the results using the scenegraph.

Have you tried running the sample code?

No but reading it I can see that you move the camera away from the center, hence the terrain is unloaded. You don’t move the character, hence it falls.

You are correct the camera is moved to the point on the current tile that makes the 3 old tiles to page out. The 3 new tiles should then page into view. The problem is that once the camera is moved to the paging point the current tile pages out this destroys the players sense of infinite terrain.

The player is supposed to be where the cam is, your size relations are off if you see two terrain tiles in each direction.

I tried it quickly the tile/quad visually flickered once like it was unloaded and reloaded, did not have time to see if that was actually the case or just a visual artifact.

But I guess what @mage is trying to say is this:

Start location at 0,200,0 with tile 512 in size.

Jump to -256, 200, -256. We are still over the starting quad.

The jump causes new tiles to be loaded (the ones we are heading towards) but it also unloads and reloads the starting quad.



Even though other entities should be coded so that they unload when the tile unloads it seems unecessary that the tile we are over reloads.



Is that a good summary?

That is a very good summary.



It’s the reload of the starting quad that I think should not happen.

There is an issue with the TerrainGrid API at the moment where all tiles do get detached but then immediately reattached. They aren’t re-loaded, but the physics ends up detaching/reattaching from it. I’ve never had a character fall through the terrain from this, but other things you might be doing in the TerrainGridListener methods could cause problems if it is assuming ‘detach’ means it is gone from the scene, when it might have just moved.

I just committed a fix for it, so please give it a try and see if it helps with your issue. So now tileAttached() is only called when a tile is attached to the tree and on the scene graph, and tileDetached() is only called when a tile is removed from the tree and scene graph. There was also a problem with the LRU cache being too small and throwing out a quarter of the tiles before it needed to access them.



On a side note it appears the HttpZipLocator is not finding the proper test data file…

1 Like

Thanks for looking into this. I will check out your fixs as soon as I am able



Cheers