TerrainQuad loop issue

Looks like TerrainQuad can’t be added to rootNode in loop, is it right? I’m making a worldGenerator and trying to generate new Sections in loop, but in result only the first Section I was standing on added to scene. (I’ve made a try to draw tracers using JME3debug, so tracers are added (Also as spheres) but TerrainQuad still doesn’t get attached to rootNode) What’s wrong?

I know that TerrainQuad is a part of a much more complicated structure than just a simple mesh, so how should I work that out to get it added?

This should be possible to do. Adding a terrain should be as simple as adding any other spatial with rootNode.attachChild(terrain) and then positioning it with something like terrain.setLocalTranslation(terrainSize * xCell, 0, terainSize * zCell).

If you share the code you’re using then I should be able to help find where the error is.

Of course, that’s how I’m trying to add it, but that still doesn’t work :face_with_raised_eyebrow:

    public TerrainQuad generateChunkTerrain(Vector3f chunkPosition, long chunkNum, int chunkSize, Material material, boolean showBorder) {
        HeightMap heightMap = generateHeightMap(chunkPosition, chunkSize);
        TerrainQuad terrain = new TerrainQuad("terrain-"+chunkNum, 65, chunkSize + 1, heightMap.getHeightMap());
        terrain.setMaterial(material);

        float[] heightArray = heightMap.getHeightMap();
        HeightfieldCollisionShape collisionShape = new HeightfieldCollisionShape(heightArray, terrain.getLocalScale());
        RigidBodyControl rigidBodyControl = new RigidBodyControl(collisionShape, 0);
        terrain.addControl(rigidBodyControl);
        bulletAppState.getPhysicsSpace().add(rigidBodyControl);
        terrain.setLocalTranslation(chunkPosition);
        rootNode.attachChild(terrain);

        if(showBorder) {
            WireBox wireBox = new WireBox(chunkSize * 0.5f, terrain.getLocalScale().y, chunkSize * 0.5f);
            Geometry wireBoxGeom = new Geometry("wireBox-"+chunkNum, wireBox);
            wireBoxGeom.setLocalTranslation(chunkPosition);
            wireBoxGeom.setMaterial(material);
            rootNode.attachChild(wireBoxGeom);
        }

        addTestMesh(chunkNum, material, chunkPosition);

        return terrain;
    }

I know, I know that I souldn’t return TerrainQuad, but I’m storing it in a map, to remember what chunk/section we’ve already processed, will improve that later, when it will finally work xD

That chunk of code looks correct at a first look-through. You should post all of your code related to your terrain generation method so we can see the whole picture.

I suspect the issue is likely related to the part of your code where you are calling generateChunkTerrain() in a loop.

Maybe you are passing in the same Vector3f to all of the methods, or are only running the loop once.

Typically when I have an issue like this that I can’t find, I will add something like a System.out.println(chunkPosition) call to your generateChunkTerrain() method and then when the app runs you might find that it only prints out one location for one terrain, or maybe it prints out the same location for multiple terrains. And that can help point you to the source of the issue. Either way I suspect it is related to some other code so I’d need to see all of the related code to know for certain.

Not once, I’ve already found that out with System.Out.Priintln();

    public void generateAndLoadChunks(Vector3f playerPosition, boolean showBorder) {
        Vector3f newChunkPosition = calculateChunkPosition(playerPosition);
        TerrainQuad terrainQuad;
        for (int i = -1; i <= 1; i++) {
            for (int j = -1; j <= 1; j++) {
                Vector3f chunkPosition = newChunkPosition.add(i * chunkSize, 0, j * chunkSize);
                if (!chunkMap.containsKey(chunkPosition) && playerPosition.distance(chunkPosition) <= chunkLoadDistance) {
                    terrainQuad = terrainGenerator.generateChunkTerrain(chunkPosition, chunkNum, chunkSize, material, showBorder);
                    chunkMap.put(chunkPosition, terrainQuad);
                    System.out.println("Adding chunk " + chunkNum+ ' ' + chunkPosition);
                    chunkNum+=1;
                }
            }
        }
        unloadOldChunks(newChunkPosition, playerPosition);
    }

The most interesting, that Sout works correctly 0_0

Player position is also correct (I’ve made a display of a player position on screen and it equals with a console message I get when printing playerLocation from code)

As you can see, other stuff can be added, but TerrainQuad - no
That’s console output
image

We’re adding chunks around the player and also, if I’m moving forward new chunks will be generated (I can see new spheres and wire boxes appearing while approaching)

1 Like

I still don’t see anything wrong that stands out to me, but maybe at a second read-through when I have some more time I might see something I’m missing…

But for now, the only other 2 things I would suggest trying to narrow down the issue are to 1.) remove the physics and 2.) try removing or commenting out your unloadOldChunks() method since there’s a chance it could be doing something wrong to detach chunks that shouldn’t be detached. I doubt removing physics will fix it, and I think your physics init code is correct, but its always worth a try removing physics for a run, then if a bug goes away that tells you your physics init order might or something like that could be wrong. But I’d have to assume its more likely to be related to unloadOldChunks just because that’s code I still haven’t seen.

I’ve already tried that, I wouldn’t come here with a question until I tried to resolve it myself
That’s unloadOldChunks, I tried removing it, that doesn’t help (If to be true, I also tried to System.out.println(); in unloadOldChunks() it even doesn’t work out that if() section, so that’s not about that method)

    public void unloadOldChunks(Vector3f newChunkPosition, Vector3f playerPosition) {
        Set<Vector3f> chunksToRemove = chunkMap.keySet().stream()
                .filter(chunkPosition -> chunkPosition.distance(newChunkPosition) > chunkLoadDistance)
                .collect(Collectors.toSet());

        for (Vector3f chunkPosition : chunksToRemove) {
            if (chunkPosition.distance(playerPosition) > chunkLoadDistance) {
                TerrainQuad terrainQuad = chunkMap.remove(chunkPosition);
                terrainQuad.removeFromParent();
            }
        }
    }

Well, Look, here are all my classes for terrainGen

package org.foxesworld.newgame.engine.world.terrain;

import com.jme3.material.Material;
import com.jme3.math.Vector3f;
import com.jme3.terrain.geomipmap.TerrainQuad;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

public class ChunkManager {

    private TerrainGenerator terrainGenerator;
    private Map<Vector3f, TerrainQuad> chunkMap = new HashMap<>();
    private long chunkNum = 0;
    private final int chunkSize = 32;
    private final float chunkLoadDistance;
    private Material material;

    public ChunkManager(TerrainGenerator terrainGenerator, float chunkLoadDistance, Material material) {
        this.terrainGenerator = terrainGenerator;
        this.chunkLoadDistance = chunkLoadDistance;
        this.material = material;
    }

    public void generateAndLoadChunks(Vector3f playerPosition, boolean showBorder) {
        Vector3f newChunkPosition = calculateChunkPosition(playerPosition);
        TerrainQuad terrainQuad;
        for (int i = -1; i <= 1; i++) {
            for (int j = -1; j <= 1; j++) {
                Vector3f chunkPosition = newChunkPosition.add(i * chunkSize, 0, j * chunkSize);
                if (!chunkMap.containsKey(chunkPosition) && playerPosition.distance(chunkPosition) <= chunkLoadDistance) {
                    terrainQuad = terrainGenerator.generateChunkTerrain(chunkPosition, chunkNum, chunkSize, material, showBorder);
                    chunkMap.put(chunkPosition, terrainQuad);
                    System.out.println("Adding chunk " + chunkNum+ ' ' + chunkPosition);
                    chunkNum+=1;
                }
            }
        }
        unloadOldChunks(newChunkPosition, playerPosition);
    }

    public Vector3f calculateChunkPosition(Vector3f playerPosition) {
        int x = Math.round(playerPosition.x / chunkSize) * chunkSize;
        int z = Math.round(playerPosition.z / chunkSize) * chunkSize;
        return new Vector3f(x, 0, z);
    }

    public void unloadOldChunks(Vector3f newChunkPosition, Vector3f playerPosition) {
        Set<Vector3f> chunksToRemove = chunkMap.keySet().stream()
                .filter(chunkPosition -> chunkPosition.distance(newChunkPosition) > chunkLoadDistance)
                .collect(Collectors.toSet());

        for (Vector3f chunkPosition : chunksToRemove) {
            if (chunkPosition.distance(playerPosition) > chunkLoadDistance) {
                TerrainQuad terrainQuad = chunkMap.remove(chunkPosition);
                terrainQuad.removeFromParent();
            }
        }
    }

}

package org.foxesworld.newgame.engine.world.terrain;

import com.jme3.asset.AssetManager;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.shapes.HeightfieldCollisionShape;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.material.Material;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.debug.WireBox;
import com.jme3.scene.shape.Sphere;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.terrain.heightmap.AbstractHeightMap;
import com.jme3.terrain.heightmap.HeightMap;
import com.jme3.terrain.noise.basis.ImprovedNoise;

public class TerrainGenerator {

    private BulletAppState bulletAppState;
    private Node rootNode;

    public TerrainGenerator(BulletAppState bulletAppState, Node rootNode) {
        this.bulletAppState = bulletAppState;
        this.rootNode = rootNode;
    }

    public TerrainQuad generateChunkTerrain(Vector3f chunkPosition, long chunkNum, int chunkSize, Material material, boolean showBorder) {
        HeightMap heightMap = generateHeightMap(chunkPosition, chunkSize);
        TerrainQuad terrain = new TerrainQuad("terrain-"+chunkNum, 65, chunkSize + 1, heightMap.getHeightMap());
        terrain.setMaterial(material);

        float[] heightArray = heightMap.getHeightMap();
        HeightfieldCollisionShape collisionShape = new HeightfieldCollisionShape(heightArray, terrain.getLocalScale());
        RigidBodyControl rigidBodyControl = new RigidBodyControl(collisionShape, 0);
        terrain.addControl(rigidBodyControl);
        bulletAppState.getPhysicsSpace().add(rigidBodyControl);
        terrain.setLocalTranslation(chunkPosition);
        rootNode.attachChild(terrain);

        if(showBorder) {
            WireBox wireBox = new WireBox(chunkSize * 0.5f, terrain.getLocalScale().y, chunkSize * 0.5f);
            Geometry wireBoxGeom = new Geometry("wireBox-"+chunkNum, wireBox);
            wireBoxGeom.setLocalTranslation(chunkPosition);
            wireBoxGeom.setMaterial(material);
            rootNode.attachChild(wireBoxGeom);
        }

        addTestMesh(chunkNum, material, chunkPosition);

        return terrain;
    }

    private  void addTestMesh(long chunkNum, Material material, Vector3f chunkPosition){
        Sphere sunSphere = new Sphere(32, 32, 2f);
        Geometry geo = new Geometry("tst"+chunkNum, sunSphere);
        geo.setMaterial(material);
        geo.setLocalTranslation(chunkPosition);
        rootNode.attachChild(geo);
    }


    public HeightMap generateHeightMap(Vector3f chunkPosition, int chunkSize) {
        AbstractHeightMap heightMap = new AbstractHeightMap() {
            @Override
            public boolean load() {
                int width = chunkSize + 1;
                int height = chunkSize + 1;
                this.heightData = new float[width * height];

                // Calculate noise parameters
                float scale = 0.01f; // Controls the frequency of the noise
                int seed = 566767896;    // Seed for the random number generator

                // Generate height data using Perlin noise
                for (int x = 0; x < width; x++) {
                    for (int z = 0; z < height; z++) {
                        float normalizedX = (chunkPosition.x + x) * scale;
                        float normalizedZ = (chunkPosition.z + z) * scale;

                        float noiseValue = ImprovedNoise.noise(normalizedX, normalizedZ, seed);
                        float heightValue = noiseValue * 50f; // Scale the noise to control height

                        setHeightAtPoint((float) x, z, (int) heightValue);
                    }
                }

                normalizeTerrain(0.5f); // Normalize the terrain heights
                return true;
            }
        };

        heightMap.load();
        return heightMap;
    }
}


Noise and unloading old chunks are not as important at the moment, at first I just need to get new chunks generated as far as I go further

I have no way of knowing what you’ve tried, nor how experienced you are with a particular part of JME. It’s also very common for even a skilled game dev to overlook little things, so if I haven’t seen all of the code or ran it first-hand then I will always mention these simpler things first. Its just like how an IT support person will often ask “is your computer plugged in” as a first question to solving a hardware issue, its not meant as an insult, its just the fact that simple mistakes are often the cause of issues and should not be overlooked.

1 Like

And in that case I would have at least one chunk added to my scene, but it ads only one chunk I’m standing on :roll_eyes: And loop is flooding with System.out.println(newChunkPos); each frame, so we can see that method is fired not once

That’s console output

P.S It will be flooding Checking chunk at position: (x,x,x) Distance to player: xx,xxxxx until I terminate an application

Alright, that’s not a problem :smile:

Maybe it doesn’t run properly on Java 17?

If a single terrain at 0,0,0 is rendering properly and no others are rendering (despite being attached at their proper location), then that is sometimes indicative that you could be using local coordinates for something that expects world coordinates.

One area this can occur with terrains is when you edit the height of a specific vertex using the wrong method with world/local coordinates when it expects the other. But it’s been a while since I’ve wrote terrain editing code so I’m not sure off the top of my head which is right…

But I can only take a guess without being able to see this method, since that’s where one of the setHeight() method call is likely at:


I also don’t think the java version could be causing it. I am also using java 17 and don’t have any similar issues with terrains.

I would just keep trying to simplify things until you can find the thing that fixes the problem when you remove it. Like removing physics, using flat terrains without heightmap, removing LODs, etc etc. (or you could do it in the opposite order and make a test case where you start simple and slowly add things until it matches your more complex implementation, and usually you’ll find the cause of the issue along the way. But if i have a complex system like yours I think its first easier to strip its features down before moving to a test-case)

Otherwise it could also just be some minor logic error that isn’t immediately apparent and could require a few look-throughs or more debugging to become apparent.

1 Like

Hello, I have some improvements! You were correct! After disabling physics it started working.
I’ve removed this state terrain.addControl(rigidBodyControl);

1 Like