[Solved] Texture Splat Maps & ImageRasters

I’m trying to use a Terrain’s alphaMap image for 2d texture splatting to generate 3d grass and foilage on the ground using an ImageRaster. I got parts of this to work, but I’m running into 2 problems so far and would appreciate if anyone could help point me on the right track :slight_smile::

  1. I don’t know how to properly find the splat maps for terrain textures. Although some 3d grass shows up with my code, I can’t get it to appear in the correct x/z location relative to the 2d grass texture. I made my terrain in the editor, so I’ve never seen its texture splat map, and I don’t even really know which splat-color represents the texture I’m checking for

  2. When I do get grass to show up, it’s not in the correct Y location. Sometimes I get a NaN value for my variable “yValue” using the code below to reference the heightmap.

Here’s my code so far:

   Image image = terrain.getMaterial().getTextureParam("AlphaMap_1").getTextureValue().getImage(); 

   MipMapGenerator.generateMipMaps(image);
   MipMapImageRaster raster = new MipMapImageRaster(image, 0);
    for(int x = 0; x < tileWidth; x+= 6){
             for(int z = 0; z < tileWidth; z+=6){
                      ColorRGBA pixelColor = raster.getPixel(x, z);
                      float red = pixelColor.getRed(); 
                      if(red > .3f){
                            Spatial grass = app.getAssetManager().loadModel("Models/foilage/grassPatch0/grassPatchDry.j3o");
                            float yValue = t.getHeight(new Vector2f(x,z));  // <-- returns NaN sometimes
                            grass.setLocalTranslation(x-(tileWidth/2), 5 , z-(tileWidth/2));
                            item.getParent().attachChild(grass);
                                    
                                }
                      }
                }

edit: if I make a simple image and use that rather than referencing the alpha map, then the grass appears with the correct x/z coordinates relative to the image. If possible I’d sill rather reference the Terrain splat maps that I already painted 2d textures on, that way I can change the color and size of 3d grass based on the opacity of the painted grass texture. I’m still having trouble with the Y position though.

I don’t understand what you are doing, why you use ImageRaster? The terrain is renderered by shader.

Would plz tell more details about what you want to do? One or more screenshots maybe useful for people who would like help you.

And these two articles may help, too.

http://jmonkeyengine.github.io/wiki/jme3/beginner/hello_terrain.html
http://jmonkeyengine.github.io/wiki/jme3/advanced/terrain.html

I’ll try and explain what I’m trying to do better and add some pictures, sorry if my original post is confusing.

For example, right now I’m using this 512x512 image on my terrain tiles which are 512 size as well:

So that I can quickly generate geometries like grass or flowers based on the color or opacity of the image, in order to do something like this:

This generates a big patch of the same grass placed correctly based on the first image, but I can’t get them to the correct y value. I’m trying to do that by referencing the terrain’s heightmap array, but it returns NaN and I can’t adjust the y position as a result, so I think I’m doing something wrong there.

I’m also trying to find a way to reference the Terrain’s alpha map with the flat textures tiled onto it. Since I made my scene and sculpted / painted the terrain in the editor, I don’t know where to get its alpha map so that I can use those images. Then I could use that image instead of the first image I posted with the red blob that just makes one big ugly patch of grass.

I’m pretty much trying to pair a geometry to the texture on the terrain beneath it, and then I can base the density or height of the geometries based on the texture’s opacity. I’m also only using the ImageRaster because I’ve been searching for a way to do this, and using an ImageRaster is the first thing I’ve tried so far, so I’m open to any other suggestions.

Im not sure why you are getting NaN floats when you check a height, but dont you have some kind of heightmap array to work from instead? Casting a ray down from the max height would work also but its cheating really.

1 Like

1) It depends on how you set the textures of the Terrain Material.

As your code showing that you getTextureParam(“AlphaMap_1”), I assume you use Common/MatDefs/Terrain/TerrainLighting.j3md. You can find some information here: http://jmonkeyengine.github.io/wiki/jme3/advanced/terrain.html

  • 4 channels of “AlphaMap”:
    R : DiffuseMap, DiffuseMap_0_scale, NormalMap
    G : DiffuseMap_1, DiffuseMap_1_scale, NormalMap_1
    B : DiffuseMap_2, DiffuseMap_2_scale, NormalMap_2
    A : DiffuseMap_3, DiffuseMap_3_scale, NormalMap_3

  • 4 channels of “AlphaMap_1”:
    R : DiffuseMap_4, DiffuseMap_4_scale, NormalMap_4
    G : DiffuseMap_5, DiffuseMap_5_scale, NormalMap_5
    B : DiffuseMap_6, DiffuseMap_6_scale, NormalMap_6
    A : DiffuseMap_7, DiffuseMap_7_scale, NormalMap_7

  • 4 channels of “AlphaMap_2”:
    R : DiffuseMap_8, DiffuseMap_8_scale, NormalMap_8
    G : DiffuseMap_9, DiffuseMap_9_scale, NormalMap_9
    B : DiffuseMap_10, DiffuseMap_10_scale, NormalMap_10
    A : DiffuseMap_11, DiffuseMap_11_scale, NormalMap_11

In that material define file, the R, G, B, A channel of “AlphaMap_1” stand for DiffuseMap_4, DiffuseMap_5, DiffuseMap_6, DiffuseMap_7. If your grass texture is set to “DiffuseMap_6”, which means you need to use the blue channel of “AlphaMap_1”.

If your grass texture is set to “DiffuseMap” or “DiffuseMap_1”, then you should use red or green channel of “AlphaMap”

  1. I don’t know why neither.
2 Likes

Take a look at this example

This is what javadoc says about returning NAN value:

2 Likes

I tried with rays and still had an issue getting the proper Y location, so that tells me it must be a logic error on my part with the Y value

It looks like I must be putting invalid coordinates, I’ll have to check the coordinates I’m using to get the Y value from the height map array

@yan
That helps a lot, thanks, I was blindly using the first alpha map and trying to guess which color stood for which texture by trial and error, which probably wasn’t a good decision :sweat_smile:.

I’ll give this a try again and see if i can get things working how I had hoped. Thanks for the help guys :slight_smile:

I solved the issue I was having using terrain.getHeight(Vector2f). I was inputting vector2fs between (0,0) and (512,512) within my 2 for loops. However it looks like I should have been using world coordinates, so I just added the world translation of the terrain to my coordinates and it works.
new Vector2f((x + xWorldLoc), (z + zWorldLoc)));

3 Likes

Even though this topic is solved, I would like to show how I did the grass generation for the terrain as I posted a screenshot in the last WIP screenshot thread.

I have been away the last two days, otherwise I would have replied to this topic earlier…

However, here is the app state. (Note that in my case another system (not shown here) will batch the grass afterwards!)

package de.gamedevbaden.crucified.appstates.view;

import com.jme3.app.Application;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.texture.Texture;
import com.jme3.texture.image.ImageRaster;

import java.util.ArrayList;
import java.util.List;

/**
 * This app state will provide the functionality to generate grass for areas which are painted with a grass texture.
 */
public class TerrainGrassGeneratorAppState extends AbstractAppState {

    private Spatial grassModel;

    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        this.grassModel = app.getAssetManager().loadModel("Models/Grass/SimpleGrass.j3o");
        super.initialize(stateManager, app);
    }

    /**
     * Will create one node which contains all the grass entities.
     * Note: You should batch the grass afterwards, otherwise there might be a huge FPS drop for larger scenes.
     * @param terrain the terrain you want generate grass for
     * @param grassTexturePos the position of the texture (0 = first texture, 1 = second texture, ...)
     * @return a node containing all grass batches.
     */
    public Node createGrassForTerrain(TerrainQuad terrain, int grassTexturePos) {
        if (terrain == null) {
            return null;
        }

        // the following node will contain all grass batches
        Node grassNode = new Node("GrassNode");
        grassNode.setQueueBucket(RenderQueue.Bucket.Transparent);

        // first we get the alpha map
        Material terrainMaterial = terrain.getMaterial();
        Texture alphaMap = terrainMaterial.getTextureParam("AlphaMap").getTextureValue();
        Texture alphaMap_1 = terrainMaterial.getTextureParam("AlphaMap_1").getTextureValue();
        Texture alphaMap_2 = terrainMaterial.getTextureParam("AlphaMap_2").getTextureValue();

        // we get all alpha maps (there are maximum 3 alpha maps)
        List<ImageRaster> rasterList = new ArrayList<>();
        if (alphaMap != null) {
            rasterList.add(ImageRaster.create(alphaMap.getImage()));
            if (alphaMap_1 != null) {
                rasterList.add(ImageRaster.create(alphaMap_1.getImage()));
                if (alphaMap_2 != null) {
                    rasterList.add(ImageRaster.create(alphaMap_2.getImage()));
                }
            }
        }

        ImageRaster[] rasters = rasterList.toArray(new ImageRaster[rasterList.size()]);
        Vector2f v = new Vector2f();

        for (int z = 0; z < rasters[0].getHeight(); z++) {
            for (int x = 0; x < rasters[0].getWidth(); x++) {
                if (isThereGrass(x, z, rasters, grassTexturePos)) {
                    // place grass
                    Spatial grass = grassModel.clone();
                    v.set(x, z);
                    grass.setLocalTranslation(turnIntoTranslation(v, terrain));
                    grass.setLocalRotation(grass.getLocalRotation().fromAngles(new float[]{0, (float) (Math.random() * 180 * FastMath.DEG_TO_RAD), 0}));
                    grassNode.attachChild(grass);
                }
            }
        }

        return grassNode;
    }

    private boolean isThereGrass(int x, int z, ImageRaster[] raster, int posGrassTexture) {
        float threshold = 0.7f; // the intensity of the grass texture which is needed to create grass
        float otherThreshold = 0.1f; // the maximum allowed intensity of other textures at a certain position
        // each alpha map can store information about 4 diffuse maps maximum
        // we now want to get the texture with the grass
        int mapNr = posGrassTexture / 4;
        int colorPos = posGrassTexture % 4; // so we know if it's red, blue, green, or alpha channel

        ImageRaster r = raster[mapNr];
        ColorRGBA c = r.getPixel(x, z);

        if (colorPos == 0) {
            // red
            if (c.getRed() >= threshold) {
                // we now check if there are other textures "overprinting" this texture
                if (c.getGreen() < otherThreshold && c.getBlue() < otherThreshold && c.getAlpha() < otherThreshold) {
                    // this texture is fine, we need to check the others as well
                   return isPixelVisible(mapNr, raster, otherThreshold, x, z);
                }
            }
        } else if (colorPos == 1) {
            // green
            if (c.getGreen() >= threshold) {
                if (c.getBlue() < otherThreshold && c.getAlpha() < otherThreshold) {
                    return isPixelVisible(mapNr, raster, otherThreshold, x, z);
                }
            }
        } else if (colorPos == 2) {
            // blue
            if (c.getBlue() >= threshold) {
                if (c.getAlpha() < threshold) {
                    return isPixelVisible(mapNr, raster, otherThreshold, x, z);
                }
            }
        } else if (colorPos == 3) {
            // alpha
            if (c.getAlpha() >= threshold) {
                return isPixelVisible(mapNr, raster, otherThreshold, x, z);
            }
        }
        return false;
    }

    private boolean isPixelVisible(int mapNr, ImageRaster[] raster, float otherThreshold, int x, int z) {
        // this method checks if the next textures overdraw the grass texture
        // if they don't, the grass texture is the last one and because of that visible
        if (mapNr+1 < raster.length) {
            for (int i = mapNr+1; i < raster.length; i++) {
                ImageRaster r2 = raster[i];
                ColorRGBA c2 = r2.getPixel(x, z);
                if (! (c2.getRed() < otherThreshold && c2.getGreen() < otherThreshold && c2.getBlue() < otherThreshold && c2.getAlpha() < otherThreshold)) {
                    // there is another texture overdrawing the desired (grass) texture, so the grass is not visible at this position
                    return false;
                }
            }
            // we went through all other maps and did not return
            // so there is no other texture overdrawing the grass texture, we can return true
            return true;
        } else {
            // there aren't any more textures, so we can return true
            return true;
        }
    }

    private Vector3f turnIntoTranslation(Vector2f pixelPos, TerrainQuad terrain) {
        float offset = terrain.getTerrainSize() / 2f;
        float x = pixelPos.x - offset;
        float z = (pixelPos.y - offset) * (-1);
        float y = terrain.getHeight(new Vector2f(x,z));
        return new Vector3f(x,y,z);
    }

}

Maybe someone can make use of that :slight_smile:

Best regards
Domenic

4 Likes

Thanks! I’ve still been having trouble figuring out which threshold values for each color to check for, so that will help a whole lot. It was actually your post in the WIP thread that inspired me to try something like this, so I appreciate the help! :slight_smile:

1 Like

Bearing in mind that right now im deep into procedural generation of all things, i went a step further using noise. You could use your splat color as a qualifier. The possibility that grass can grow. Then posterize some kind of organic noise function (perlin/simplex/etc) so that clumps of grass grows with “paths” through it. Add some slight variation to the positions, too. The “paths” save on grass blades but the variation makes it appear fuller in view due to the “layering” effect across the z-plane (the eye-view of the player). And of course it doesnt just look like a carpet of uniform grass now.

1 Like

Thanks, I haven’t worked on making the grass appear more natural yet but I’ll have to keep that in mind when I do, especially if that will help save on grass blades. Right now I just have a big carpet of grass, and despite using a billboard impostor for grass patches at a moderate distance, I’m still getting a high vert/triangle count with a lower FPS.