A (very) SimpleGrassControl

Hey guys! I’ve been working on a little game in my spare time and i really liked the idea of having grass on my terrain.
So after some snooping around i came across the Forester plugin which does just that but could never get it to work properly. So i used it to make
a very simple grass control which turned out better than i expected. So i thought i’d share it so others can use and abuse it. Hopefully someone will
find this helpful, being a beginner i learnt a lot making it.

I used the grassBase.j3m as the material definition found in the forester plugin for my grass which allows it to sway in the “wind” with the help of some shader black magic.
The texture i made in photoshop, here’s a picture of it in action:

Obviously you will have to edit it to suite your needs and provide a texture:

[java]
import com.jme3.asset.AssetManager;
import com.jme3.material.MatParam;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.material.RenderState.FaceCullMode;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;
import com.jme3.scene.shape.Quad;
import com.jme3.shader.VarType;
import com.jme3.terrain.Terrain;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.texture.Image;
import com.jme3.texture.Texture;
import java.nio.ByteBuffer;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import jme3tools.optimize.GeometryBatchFactory;

/**

  • This is a grass control that can be added to any terrain or any spatial/node that has
  • a terrain object attached to it.
  • This is a simple implementation of a grass control and does not use paging
  • or anything too fancy :stuck_out_tongue:
  • Grass is generated only where there is the first texture layer displayed on the terrain
  • The first layer is the grass layer (only been tested for up to 3 layers)
  • Values and materials should be modified to suite your needs

*/
public class SimpleGrassControl extends AbstractControl{

TerrainQuad terrain;
AssetManager assetManager;
Material faceMat;
Quad faceShape;
Node grassLayer = new Node();
float scale;

/*
 * Should be greater than 1 
 * GrassPatches will be scaled randomly by a factor between 1/patchScaleVariation and patchScaleVariation
 */
float patchScaleVariation = 2f; 
/*
 * The width of the grass Quads
 */
float patchWidth = 20;
/*
 * The height of the Grassquads
 */
float patchHeight = 15;
/*
 * Increment for the uniform grass planting algorithm
 * The lower this value the more dense the grass
 * Making this a very low value may cause memory issues
 */
float inc = 80;

public SimpleGrassControl(AssetManager assetManager, String texturePath)
{
    this.assetManager = assetManager;
    
    faceMat = new Material(assetManager,"Resources/MatDefs/Grass/grassBase.j3md");
    faceMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Off);
    faceMat.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Off);
    faceMat.setTransparent(true);
    faceMat.setTextureParam("ColorMap",VarType.Texture2D,assetManager.loadTexture(texturePath));
    faceMat.setBoolean("VertexLighting",false);
    faceMat.setInt("NumLights", 4);
    faceMat.setBoolean("VertexColors", false);
    faceMat.setBoolean("FadeEnabled", false);
    faceMat.setFloat("FadeEnd", 2000);
    faceMat.setFloat("FadeRange", 0);
    faceMat.setBoolean("FadeEnabled", true);
    faceMat.setBoolean("SelfShadowing", false);
    faceMat.setBoolean("Swaying",true);
    faceMat.setVector3("SwayData",new Vector3f(1.5f,1,5));
    faceMat.setVector2("Wind", new Vector2f(1,1));

}

@Override
public void setSpatial(Spatial spatial) 
{
    super.setSpatial(spatial);
    Node spatNode = (Node)spatial;
    
    if(spatial instanceof Terrain)
    {
        terrain = (TerrainQuad)spatial;
    }
    else
    {
    for(Spatial currentSpatial : spatNode.getChildren())
    {
        if(currentSpatial instanceof Terrain)
        {
            terrain=(TerrainQuad)currentSpatial;
            break;
        }
    }
    }
    
    if(terrain==null||spatNode.getChildren().isEmpty())
        {
            Logger.getLogger(SimpleGrassControl.class.getName()).log(Level.SEVERE, "Could not find terrain object.", new Exception());
            System.exit(0);
        }
    
    scale = ((Spatial)terrain).getWorldScale().x;
  
   //Generate grass uniformly with random offset.
   float terrainWidth = scale*terrain.getTerrainSize(); // get width length of terrain(assuming its a square)
   Vector3f centre = (((Spatial)terrain).getWorldBound().getCenter()); // get the centr location of the terrain
   Vector2f grassPatchRandomOffset = new Vector2f().zero();
   Vector3f candidateGrassPatchLocation = new Vector3f();
   
   for(float x = centre.x - terrainWidth/2 + inc; x < centre.x + terrainWidth/2 - inc; x+=inc)
    {
      for(float z = centre.z - terrainWidth/2 + inc; z < centre.z + terrainWidth/2 - inc; z+=inc)
      {
          grassPatchRandomOffset.set(0, inc);
          grassPatchRandomOffset.multLocal(new Random().nextFloat()); // make the off set length a random distance smaller than the increment size
          grassPatchRandomOffset.rotateAroundOrigin((float)(((int)(Math.random()*359))*(Math.PI/180)), true); // rotate the offset by a random angle
          candidateGrassPatchLocation.set(x+grassPatchRandomOffset.x, terrain.getHeight(new Vector2f(x+grassPatchRandomOffset.x,z+grassPatchRandomOffset.y)), z+grassPatchRandomOffset.y);
          
          if(isGrassLayer(candidateGrassPatchLocation))
          {
          createGrassPatch(candidateGrassPatchLocation);
          }
          
      }
    }
    
    
   grassLayer.scale(1/scale);
   GeometryBatchFactory.optimize(grassLayer);
   terrain.attachChild(grassLayer);
  
 }


@Override
protected void controlUpdate(float tpf) {
 
}

@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
    
}

private void createGrassPatch(Vector3f location)
{
    
    Node grassPatch = new Node();
    float selectedSizeVariation = (float)(new Random().nextFloat()*(patchScaleVariation-(1/patchScaleVariation)))+(1/patchScaleVariation);
    faceShape = new Quad((patchWidth*selectedSizeVariation),patchHeight*selectedSizeVariation,false);
    Geometry face1 = new Geometry("face1",faceShape);
    face1.move(-(patchWidth*selectedSizeVariation)/2, 0, 0);
    grassPatch.attachChild(face1);
    
    Geometry face2 = new Geometry("face2",faceShape);
    face2.rotate(new Quaternion().fromAngleAxis(-FastMath.PI/2,   new Vector3f(0,1,0)));
    face2.move(0, 0, -(patchWidth*selectedSizeVariation)/2);
    grassPatch.attachChild(face2);
 
    grassPatch.setCullHint(Spatial.CullHint.Dynamic);
    grassPatch.setQueueBucket(RenderQueue.Bucket.Transparent);
    
    face1.setMaterial(faceMat);
    face2.setMaterial(faceMat);
 
    grassPatch.rotate(new Quaternion().fromAngleAxis( (((int)(Math.random()*359))+1) *(FastMath.PI/190),   new Vector3f(0,1,0)));
    grassPatch.setLocalTranslation(location);

    grassLayer.attachChild(grassPatch);
}

private boolean isGrassLayer(Vector3f pos)
{
    MatParam matParam = terrain.getMaterial(null).getParam("AlphaMap");
    Texture tex = (Texture) matParam.getValue();
    Image image = tex.getImage();
    Vector2f uv = getPointPercentagePosition(terrain, pos);
 
    ByteBuffer buf = image.getData(0);
    int width = image.getWidth();
    int height = image.getHeight();

    int x = (int)(uv.x*width);
    int y = (int)(uv.y*height);
  
    int position = (y*width + x) * 4;
    ColorRGBA color = new ColorRGBA().set(ColorRGBA.Black);
    
    buf.position( position );
    color.set(byte2float(buf.get()), byte2float(buf.get()), byte2float(buf.get()), byte2float(buf.get()));
    
    if(color.r==1 && color.b==0 && color.g==0)
    {
        return true;
    }
    else
    {
        return false;
    }
}

private Vector2f getPointPercentagePosition(Terrain terrain, Vector3f worldLoc) {
    Vector2f uv = new Vector2f(worldLoc.x,-worldLoc.z);
    uv.subtractLocal(((Node)terrain).getWorldTranslation().x*scale, ((Node)terrain).getWorldTranslation().z*scale); // center it on 0,0
    float scaledSize = terrain.getTerrainSize()*scale;
    uv.addLocal(scaledSize/2, scaledSize/2); // shift the bottom left corner up to 0,0
    uv.divideLocal(scaledSize); // get the location as a percentage
    
    return uv;
}

private float byte2float(byte b){
    return ((float)(b & 0xFF)) / 255f;
}

}
[/java]

Example of use:
[java]
Spatial map = assetManager.loadModel("Scenes/testMap.j3o");
map.addControl(new SimpleGrassControl(assetManager,"textures/grassSprite2.png")); //That's it
[/java]

19 Likes

Wow nice! Thanks for sharing this. I like how simple and “jme-modular” it is :slight_smile: I’d make the generic reference to app just a reference to an AssetManager though, then you can also implement the read() and write() methods so the class automatically can be saved along with the terrain in a j3o :slight_smile:

The forester plugin is not maintained by its creator anymore and this kind of wrapping is exactly the last bit that is missing for it. If you want you can try and modify and/or extend it further instead of “wrapping” it like this.

1 Like

Thanks Normen, i edited the code so that it just uses an AssetManager and a texture path now. I think ill probably start looking into reading and writing too.

2 Likes

have u discovered what shader params “SwayData” and “Wind” do, kinda do not see difference between them :-?

@vvishmaster this is really great! I’ll try it soon!
Just a question: why did you make it as a control?
Possibly it’s easier to make it as AppState or even give a user make a choice?

Anyway thank you a lot!

@mifth said: @vvishmaster this is really great! I'll try it soon! Just a question: why did you make it as a control? Possibly it's easier to make it as AppState or even give a user make a choice?

Anyway thank you a lot!

I think a Control is the best and most modular way to use this. This way you can make it work in any place of the scenegraph. In an AppState you’d have to specify where to put the grass anyway. You can easily make an AppState to control multiple Controls yourself, too.

3 Likes
@normen said: I think a Control is the best and most modular way to use this. This way you can make it work in any place of the scenegraph. In an AppState you'd have to specify where to put the grass anyway. You can easily make an AppState to control multiple Controls yourself, too.

thanks.

[Deleted: see post below]

@vvishmaster

If you could provide me with the materials and shaders needed to make this sway with the wind, and everything work. I would appreciate that.

The material definition as well as all the needed shaders are in the TheForester plugin available from the update centre. Atleast they were at the time of posting.
I included the library here just in case http://rapidshare.com/share/7E778B0E835B4AB0C24250C58CBB99B1

Make sure you include that library in your project.

As for the texture unfortunately i don’t have access to the texture i made in the picture at the moment but any grass texture with an alpha channel will do.

Google “grass sprite png” or make your own in GIMP or photoshop.

Hope that helps :slight_smile: .

1 Like

@vvishmaster when I create a terrain and add the control to it, nothing happens.

I know that the problem is that the isgrasslayer() method, it doesn’t know which is the color red on the alphamap. But I don’t know how to provide the alphamap. It is supposed to be included in the terrain when the terrain is created.

When I set isgrasslayer() to always true, the grass shows up, but as you would expect it is in the wrong layer which makes the grass look funky.

How am I supposed to provide your class with the rbg alpha map? It is not reading any rbg alphamap.

Edit:

I added this one line to force the standard terrain alpha-mapping. This line makes the grass show up, which makes things fine. But the grass if as tall as my character, but I can fix that, though.

[java]
candidateGrassPatchLocation.set(x+grassPatchRandomOffset.x, terrain.getHeight(new Vector2f(x+grassPatchRandomOffset.x,z+grassPatchRandomOffset.y)), z+grassPatchRandomOffset.y);

terrain.getMaterial().setTextureParam(“AlphaMap”, VarType.Texture2D, assetManager.loadTexture(“Textures/red.png”)); //(at)pixelapp: I added this single line.

if(isGrassLayer(candidateGrassPatchLocation))
[/java]

I guess I can work it out from here. But I would be great if you tell me what am I supposed to be doing as per the first paragraph of this post.

Edit:

I found the answer.

The way the JMonkey Terrain Editor paints the red on the alpha-map texture is a different format/layer from what GIMP or other image processing apps do/read. So for your grass class to read the image you should make sure the red on the alpha-map is readable by conventional image processing apps, otherwise all the SimpleGrassControl class sees is a transparent texture.

1 Like

Look at those flowers!

As @vvishmaster said, I didn’t have to modify the code posted above.

[These are not the final game assets for the game, please disregard blurry textures and pointy 3D models].

2 Likes

Its looking good! I’m glad you managed to get it working :slight_smile:

Hi @vvishmaster, here is your shader at work:

I did not use your control because I already have height maps I wanted to reuse for seedability with my terrain in a whole, but I found your work very good!

1 Like

Can anyone help me? I don’t see the grass when I run. thanks…
Provide sample code, it would be great help…
gluckos at alice .it

Hi, could you show us your code please? It will be much simpler to help you if you did.

@.Ben. said: Hi, could you show us your code please? It will be much simpler to help you if you did.
Thanks for the reply!! https://www.dropbox.com/s/wbcjxtsr564783u/Screenshot%202014-10-22%2021.58.38.png?dl=0
MaterialDef My MaterialDef {

    MaterialParameters {

        //Fading parameters (don't set these manually).
        Float FadeEnd
        Float FadeRange
        Boolean FadeEnabled
    

        //Is the grass mesh a distant impostor?
        Boolean IsImpostor
        
        //Is the grass swaying or not?
        Boolean Swaying
        //The wind vector (determines direction and amplitude of the swaying function).
        Vector2 Wind
        //The swaying frequency
        Float SwayFrequency

        //The texture
        Texture2D ColorMap
        //The perlin noise for stipple fading.
        Texture2D AlphaNoiseMap
    }

    Technique {
        VertexShader GLSL120:   Resources/Grass.vert
        FragmentShader GLSL120: Resources/Grass.frag

        WorldParameters {
            WorldViewProjectionMatrix
            WorldMatrix
            CameraPosition
            Time
        }

        Defines {
            SWAYING : Swaying
            FADE_ENABLED : FadeEnabled
            IS_IMPOSTOR : IsImpostor
        }
    }

}
@GluckOs said: Thanks for the reply!! https://www.dropbox.com/s/wbcjxtsr564783u/Screenshot%202014-10-22%2021.58.38.png?dl=0

The error on the screenshot talks about the VertexLighting parameter which I do have in my J3MD file, why isn’t yours showing it? Did you strip variables down? Try and leave the shader file exactly like it was because in Grass.vert it is making calculations based on if the VertexLighting variable is true or false. Now it’s UNDEFINED which throws the exception.

Use this J3MD file instead, which contains all the appropriate definitions:


MaterialDef Grass {

    MaterialParameters {
        
        //Fading parameters (don't set these manually).
        Float FadeEnd
        Float FadeRange
        Boolean FadeEnabled
        
        //Is the grass swaying or not?
        Boolean Swaying
        //The wind vector (determines direction and amplitude of the swaying function).
        Vector2 Wind
        //Combined vector for various fading data.
        //x = The swaying frequency
        //y = The swaying variation (how the offset varies between patches)
        //z = Maximum swaying distance (grass beyond this distance does not move).
        Vector3 SwayData

        //Use lighting
        Boolean VertexLighting

        //Use vertex colors (requires a colormap provided to the grassloader).
        Boolean VertexColors

        //Use grass self-shadowing.
        Boolean SelfShadowing

        //The texture
        Texture2D ColorMap
        //The perlin noise for stipple fading.
        Texture2D AlphaNoiseMap

        //When texture alpha is below this value, the pixel is discarded
        Float AlphaThreshold

        Float opacity
        
        //Used internally.
        Int NumLights
    }

    Technique {

        LightMode SinglePass

        VertexShader GLSL100:   Shaders/Grass/Grass.vert
        FragmentShader GLSL100: Shaders/Grass/Grass.frag

        WorldParameters {
            WorldViewProjectionMatrix
            WorldMatrix
            ModelViewMatrix
            ProjectionMatrix
            ViewMatrix
            ModelViewProjectionMatrix
            WorldViewMatrix
            CameraPosition
            Time
        }

        Defines {
            SWAYING : Swaying
            FADE_ENABLED : FadeEnabled
            VERTEX_LIGHTING : VertexLighting
            VERTEX_COLORS : VertexColors
            SELF_SHADOWING : SelfShadowing
            NUM_LIGHTS : NumLights
        }
    }

    Technique PreShadow {

        VertexShader GLSL100 :   MatDefs/BillboardPreShadow/BillboardPreShadow.vert
        FragmentShader GLSL100 : MatDefs/BillboardPreShadow/BillboardPreShadow.frag

        WorldParameters {
            WorldViewProjectionMatrix
            WorldViewMatrix
            ProjectionMatrix
        }

        Defines {
            DIFFUSEMAP_ALPHA : DiffuseMap
        }

        RenderState {
            FaceCull Off
            DepthTest On
            DepthWrite On
            PolyOffset 5 0
            ColorWrite Off
        }
    }

    Technique PreNormalPass {

        VertexShader GLSL100 :   Common/MatDefs/SSAO/normal.vert
        FragmentShader GLSL100 : Common/MatDefs/SSAO/normal.frag

        WorldParameters {
            WorldViewProjectionMatrix
            WorldViewMatrix
            NormalMatrix
        }

        Defines {
            SWAYING : Swaying
            FADE_ENABLED : FadeEnabled
        }

        RenderState {

        }

    }

}
1 Like

thanks…
I do not have the following files:

MatDefs/BillboardPreShadow/BillboardPreShadow.vert
MatDefs/BillboardPreShadow/BillboardPreShadow.frag
Common/MatDefs/SSAO/normal.vert
Common/MatDefs/SSAO/normal.frag

where can I find them?

Thanks again, I hope I can run the code…

You have them, because the /MatDefs/ [EDIT: MatDefs does NOT come with the libraries, sorry my mistake] and /Common/ directories come with the JME3 librairies. It’s already in the compiler path and it should work as is without you creating any of those files, at least here it works out of the box, as is.