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 :
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
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’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.
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”
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 .
I’ll give this a try again and see if i can get things working how I had hoped. Thanks for the help guys
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)));
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);
}
}
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!
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.
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.