Questions about the TerrainGridTileLoader of the jme3-terrain module

Hi everyone,
I’m doing some tests with jme3-terrain library. I’m studying the test-cases found in the jme3-examples project.

In particular, I am interested in these three examples:

Suppose all TerrainGrids have a TerrainGridListener.

            terrain.addListener(new TerrainGridListener() {

                @Override
                public void gridMoved(Vector3f newCenter) {
                }

                @Override
                public void tileAttached(Vector3f cell, TerrainQuad quad) {
                    while(quad.getControl(RigidBodyControl.class)!=null){
                        quad.removeControl(RigidBodyControl.class);
                    }
                    quad.addControl(new RigidBodyControl(new HeightfieldCollisionShape(quad.getHeightMap(), terrain.getLocalScale()), 0));
                    bulletAppState.getPhysicsSpace().add(quad);
                }

                @Override
                public void tileDetached(Vector3f cell, TerrainQuad quad) {
                    if (quad.getControl(RigidBodyControl.class) != null) {
                        bulletAppState.getPhysicsSpace().remove(quad);
                        quad.removeControl(RigidBodyControl.class);
                    }
                }

            });

Question 1:

How do I save the texture generated by the FilteredBasis to a png file?

        FractalSum base = new FractalSum();
        base.setRoughness(0.7f);
        base.setFrequency(1.0f);
        base.setAmplitude(1.0f);
        base.setLacunarity(2.12f);
        base.setOctaves(8);
        base.setScale(0.02125f);
        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(base);

        PerturbFilter perturb = new PerturbFilter();
        perturb.setMagnitude(0.119f);

        OptimizedErode therm = new OptimizedErode();
        therm.setRadius(5);
        therm.setTalus(0.011f);

        SmoothFilter smooth = new SmoothFilter();
        smooth.setRadius(1);
        smooth.setEffect(0.7f);

        IterativeFilter iterate = new IterativeFilter();
        iterate.addPreFilter(perturb);
        iterate.addPostFilter(smooth);
        iterate.setFilter(therm);
        iterate.setIterations(1);

        ground.addPreFilter(iterate);

        TerrainGrid terrain = new TerrainGrid("terrain", 33, 129, new FractalTileLoader(ground, 256f));

(eg: see the png files contained in the TerrainGridTestData.zip file).

If I understand correctly, the textures should have dimensions equal to powers of 2: 128x128, 256x256, 512x512 …
I would like to use them with the ImageTileLoader class like this example.

TerrainGrid terrain = new TerrainGrid("terrain", 65, 257, new ImageTileLoader(assetManager, new Namer() {
            @Override
            public String getName(int x, int y) {
                return "Scenes/TerrainMountains/terrain_" + x + "_" + y + ".png";
            }
        }));

Question 2:

how do I save the TerrainQuad generated on j3o file? I would like to use them with the AssetTileLoader class like in this example.

AssetTileLoader tileLoader = new AssetTileLoader(assetManager, "testgrid", "TerrainGrid");
TerrainGrid terrain = new TerrainGrid("terrain", 65, 257, tileLoader);

Question 3:

How can I view the parameters configured for a FilteredBasis in image format?

        FractalSum base = new FractalSum();
        base.setRoughness(0.7f);
        base.setFrequency(1.0f);
        base.setAmplitude(1.0f);
        base.setLacunarity(2.12f);
        base.setOctaves(8);
        base.setScale(0.02125f);
        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(base);

        PerturbFilter perturb = new PerturbFilter();
        perturb.setMagnitude(0.119f);

        OptimizedErode therm = new OptimizedErode();
        therm.setRadius(5);
        therm.setTalus(0.011f);

        SmoothFilter smooth = new SmoothFilter();
        smooth.setRadius(1);
        smooth.setEffect(0.7f);

        IterativeFilter iterate = new IterativeFilter();
        iterate.addPreFilter(perturb);
        iterate.addPostFilter(smooth);
        iterate.setFilter(therm);
        iterate.setIterations(1);

        ground.addPreFilter(iterate);

Here is an example image found in a previous post.

Before moving on to other libraries, I would like to start with the official one (jme3-terrain) kept in the engine core.

Thanks for your help

1 Like

Answer to Question 1:
To save the texture generated by the FilteredBasis to a png file, you can use the following code:
FilteredBasis ground = new FilteredBasis(base);

// add your filters here…

// generate the texture
ImageBasedHeightMap heightmap = new ImageBasedHeightMap(ground.getImage(), 0.25f);
Texture2D texture = new Texture2D(heightmap.getImage());

// save the texture to a file
File file = new File(“texture.png”);
try {
ImageIO.write(texture.getImage(), “png”, file);
} catch (IOException e) {
e.printStackTrace();
}
This code creates a FilteredBasis object and adds your filters to it. Then it generates an ImageBasedHeightMap from the filtered image and creates a Texture2D object from the heightmap image. Finally, it saves the texture to a png file using the ImageIO.write() method.
Answer to Question 2:
To save the TerrainQuad generated on a j3o file, you can use the following code:
BinaryExporter exporter = BinaryExporter.getInstance();
File file = new File(“terrain.j3o”);
try {
exporter.save(terrain, file);
} catch (IOException ex) {
ex.printStackTrace();
}
This code creates a BinaryExporter object and saves the TerrainQuad to a j3o file using the exporter.save() method.

Answer to Question 3:
To view the parameters configured for a FilteredBasis in image format, you can use the following code:

FilteredBasis ground = new FilteredBasis(base);

// add your filters here…

// get the filtered image
BufferedImage filteredImage = ground.getImage();

// create a JFrame to display the image
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.getContentPane().setLayout(new BorderLayout());
JLabel label = new JLabel(new ImageIcon(filteredImage));
frame.getContentPane().add(label, BorderLayout.CENTER);
frame.pack();
frame.setVisible(true);
This code creates a FilteredBasis object and adds your filters to it. Then it gets the filtered image using the getImage() method and displays it in a JFrame using a JLabel and an ImageIcon.

1 Like

Hi @KeithLaster and welcome to the community!
Thank you very much for the clear and detailed answer.
I tried your snippet, but there is a problem:

  • The ground.getImage() method does not exist.
  • The heightmap.getImage() method does not exist.
  • The texture.getImage() method returns an object of type Image while the ImageIO.write() method expects an object of type RenderedImage.
	public static void main(String[] args) {
		FractalSum base = new FractalSum();
		base.setRoughness(0.7f);
		base.setFrequency(1.0f);
		base.setAmplitude(1.0f);
		base.setLacunarity(2.12f);
		base.setOctaves(8);
		base.setScale(0.02125f);
		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(base);

		PerturbFilter perturb = new PerturbFilter();
		perturb.setMagnitude(0.119f);

		OptimizedErode therm = new OptimizedErode();
		therm.setRadius(5);
		therm.setTalus(0.011f);

		SmoothFilter smooth = new SmoothFilter();
		smooth.setRadius(1);
		smooth.setEffect(0.7f);

		IterativeFilter iterate = new IterativeFilter();
		iterate.addPreFilter(perturb);
		iterate.addPostFilter(smooth);
		iterate.setFilter(therm);
		iterate.setIterations(1);

		ground.addPreFilter(iterate);
		
		// generate the texture
		// The ground.getImage() method does not exist.
		ImageBasedHeightMap heightmap = new ImageBasedHeightMap(ground.getImage(), 0.25f);
		// The heightmap.getImage() method does not exist
		Texture2D texture = new Texture2D(heightmap.getImage());

		// save the texture to a file
		File file = new File("texture.png");
		try {
			//The `texture.getImage()` method returns an object of type `Image`
			//while the `ImageIO.write()` method expects an object of type `RenderedImage`
			ImageIO.write(texture.getImage(), "png", file);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

Could you provide me with a working test case please?

Thank you for your time.

ChatGPT?

indeed, looks like very much

At the moment it is not possible for me. Could you kindly help me? Have you tried? Did you find something interesting?
Thank you

I meant that the answer looks like it’s generated by ChatGPT.

2 Likes

Now that you’ve pointed it out to me, I think you’re right. :sweat_smile:

I found this conversation in a previous post.

@rickard Could you explain me better how to do it with a code snippet?

I’ve no recollection of writing that, 9 years ago :smiley:
Since I don’t have much experience with terrain, and with the date in mind, I figured I had probably done something for the cookbook, and indeed:

Get the height values:

terrainValues = new float[size][size];
for(int y = 0; y < size; y++) {
     for(int x = 0; x < size; x++) {
          terrainValues[x][y] = FastMath.clamp(fractalSum.value(x, 0, y) + 0.5f, 0f, 1f);
     }
}

Create a BufferedImage with the same size and populate it with the terrainValues.

Use ImageIO.write to save the file.

Then you can load them like in your Question 1, I guess…

(Couldn’t find the example code, just the paper book at this time)

1 Like

You can save a texture like this:

        Texture texture = ...;
        BufferedImage bi = ImageToAwt.convert(texture.getImage(), false, true, 0);
        File imageFile = new File(assetFolder, texture.getKey().getName());
        try(FileImageOutputStream ios = new FileImageOutputStream(imageFile)) {
            ImageIO.write(bi, "png", ios);
        } catch (IOException ex) {
            log.error("Failed saving image:" + imageFile, ex);
        }

It can be saved like a regular spatial

JmeExporter exporter = BinaryExporter.getInstance();
exporter.save(terrain, file);
1 Like

Thanks so much for the tips guys. I really appreciate it.

Unfortunately there are still some steps missing.

My goal is to understand how the images found in the TerrainGridTestData package were generated.

Class TerrainGridTest shows how to use them via object ImageTileLoader, but there’s no example explaining how they were obtained. Class ImageTileLoader is unusable if you don’t know this fundamental step.

        this.terrain = new TerrainGrid("terrain", 65, 257, new ImageTileLoader(assetManager, new Namer() {
            @Override
            public String getName(int x, int y) {
                return "Scenes/TerrainMountains/terrain_" + x + "_" + y + ".png";
            }
        }));
        
        this.terrain.setMaterial(mat_terrain);
        this.terrain.setLocalTranslation(0, 0, 0);
        this.terrain.setLocalScale(1f, 1f, 1f);
        this.rootNode.attachChild(this.terrain);

Would you help me to find out how the black and white maps were generated and saved please?

I am not sure how those heightmap tile textures in the TerrainGridTestData were generated as I have not used the terrain grid before, but I think you can do something like this for exporting the terrain heightmap as a png image file.

Do it for each terrain tile in the grid.

        BoundingBox bb = (BoundingBox) terrain.getWorldBound();

        Vector3f min = new Vector3f();
        Vector3f max = new Vector3f();
        bb.getMin(min);
        bb.getMax(max);

        BufferedImage bufferedImage = new BufferedImage(MAP_SIZE, MAP_SIZE, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = (Graphics2D) bufferedImage.getGraphics();

        for(int z = 0; z < MAP_SIZE; z++) {
            
            for(int x = 0; x < MAP_SIZE; x++) {
               
                float height = terrain.getHeight(new Vector2f(x - (MAP_SIZE / 2), z - (MAP_SIZE / 2)));

                int gray = (int) (255 * FastMath.unInterpolateLinear(height, min.y, max.y));
                g.setColor(new Color(gray, gray, gray));
                g.drawOval(x, z, 1, 1);
               
            }
            
        }
   
        ImageIO.write(bufferedImage, "png", outputFile);

Note that I have not tested this! Hope it helps

1 Like

Thanks a lot @Ali_RS . You are a black belt in computer graphics!
It seems to work but there is still a small flaw on the edges.

I added a couple of System.outs and found that some values ​​returned by the terrain.getHeight(...) function are Float.NaN. Could this be the problem?

Here are some generated images:
heightmap_0_0 heightmap_0_1 heightmap_0_-1

	private void generateImageMap(TerrainQuad quad) {
		int MAP_SIZE = quad.getTerrainSize();
		BoundingBox bb = (BoundingBox) quad.getWorldBound();

		Vector3f min = new Vector3f();
		Vector3f max = new Vector3f();
		bb.getMin(min);
		bb.getMax(max);

		BufferedImage img = new BufferedImage(MAP_SIZE, MAP_SIZE, BufferedImage.TYPE_INT_ARGB);
		Graphics2D g = (Graphics2D) img.getGraphics();

		for (int z = 0; z < MAP_SIZE; z++) {
			for (int x = 0; x < MAP_SIZE; x++) {
				float height = quad.getHeight(new Vector2f(x - (MAP_SIZE / 2), z - (MAP_SIZE / 2)));
				float value = FastMath.unInterpolateLinear(height, min.y, max.y);
				System.out.println("BufferedImage MAP_SIZE " + MAP_SIZE + " height " + height + " " + value);
				System.out.println("Min " + min + " Max " + max);

				int gray = (int) (255 * value);
				g.setColor(new Color(gray, gray, gray));
				g.drawOval(x, z, 1, 1);
			}

		}
		ImageIO.write(img, "png", texture);
	}
1 Like

What is the MAP_SIZE value?

129

image

    private int patchSize = 65;
    private int maxVisibleSize = 257;

        terrain = new TerrainGrid("MyTerrain", patchSize, maxVisibleSize, tileLoader);
        terrain.setMaterial(matTerrain);
        terrain.setLocalTranslation(0, 0, 0);
        terrain.setLocalScale(2f, 1f, 2f);
        terrain.setShadowMode(RenderQueue.ShadowMode.Receive);

        TerrainLodControl lod = new TerrainGridLodControl(terrain, camera);
        lod.setLodCalculator(new DistanceLodCalculator(patchSize, 2.7f)); // patch size, and a multiplier
        terrain.addControl(lod);

Not sure what goes wrong! Maybe set MAP_SIZE = 128; and see if it makes any difference :thinking:

Tried with 128 (MAP_SIZE -1), same result:

	private void generateImageMap(TerrainQuad quad) {
		int MAP_SIZE = quad.getTerrainSize() - 1;
		BoundingBox bb = (BoundingBox) quad.getWorldBound();
		...
	}