Hi everyone ! I’m here to discuss about a relatively new matter and not over hoped subject ! A grass system for JMonkey. I’m warning you this’ll be a really long subject so if you want to leave it’s now
1°) Introduction
A grass system might seem easy to implement or easy to see what you want to achieve, “I just want grass in my game that’s it”. But it isn’t that simple, sadly… There is so many approach that you can’t even choose what you really want to do, and in the mess of this, each on of this technique as an impact on the grass looking and the performance. But there is a time when you have to choose and I’ll talk about what I personally choose. But you are really invited to express yourself on this. How to improve ? What do you think of this choice ? What would you do if you where me ? Or anything that could help .
First a screenshot
As you can see there is many triangles in this scene (maybe a bit too much) but the overhaul looking isn’t that bad. Right now the performance is still miserable and the number of test variable and comment or even misconception is huge but I’m planning to rewrite it all from scratch with what I learn on the first test and make something that could be used by most of you guys.
2°) The planting
Like a gardener you need to know how to plant your plants, if you want them to looks good together, and that’s exactly what we are going to talk. The first thing to notice is that if you are a good gardener you’ll plants all your seed aligned horizontally and vertically like that.
But since we try to reproduce the randomness of a grassy area we need it to be randomly translated. We need to do something like that
float tempX = x + (float) FastMath.rand.nextGaussian() / 4;
tempX = FastMath.clamp(tempX, locX, locX + patchSize * 2);
float tempZ = z + (float) FastMath.rand.nextGaussian() / 4;
tempZ = FastMath.clamp(tempZ, locZ, locZ + patchSize * 2);
But we need it to not get translated too much or else it’ll be out of the terrain space that’s why the value is clamped. And every grass quad need to be on the terrain so we’ll look for the terrain height at the given point
heightAt = terrain.getHeight(new Vector2f(locX+tempX, locZ+tempZ));
//This method doesn't give me a really accurate results but with a perpendicular ray
//cast above the terrain some value a totally wrong and messing up everything
So we have everything planted really well, but there is one thing you might want to change during the planting process. The fact that you need a density for this grass of yours. The density is simply done by augmenting or decreasing the float value of the planting loop. It allow the grass to be planted with a given density on the terrain.
for (float x = locX; x < (locX + patchSize * 2); x += grassDensity) {
for (float z = locZ; z < (locZ + patchSize * 2); z += grassDensity) {
//Future work
//grassDensity = 1-alphaMap[x][y];
plant();
}
}
##2.1°) Which seeds to use ?
Once you have determined where you want to plant your grass, you need to know what kind of seeds you’ll use for this particular position. by that, I mean that you need to create something to hold this information. You give the position information to a GrassFactory and it’ll give you a GrassBlade object with all the informations you need to create the grass mesh.
public class GrassBlade{
//The position of the grass
public Vector3f position;
//The size (we'll need it to translate the quad vertex to face the camera position)
public float size;
//The index of the texture atla to use (not implemented yet)
public int index;
//A contrast value which is given by a perlin noise array holded by the grass factory
public float color;
public GrassBlade(Vector3f pos, float color, float s, int index){
this.position = pos;
this.color = color;
this.size = s;
this.index = index;
}
}
The GrassFactory is a Singleton with an init function called at the beginning of the grass process.
##2.2°) Always look at the sun
In my mind a grass system must be a static shadowed object. I mean the performance issue with a real time grass object with shadow casting can be a really big problem. So the next step to this system will be static shadowing, by casting a ray in the given physic space to see if each blade of grass is hidden by something directly from the sun, and if it is, just make it darker.
Like so :
This is just a first approach on this problem it isn’t implemented yet.
##2.3°) Density on hills
One of the other problem I encounter during the planting process is that on hills we need a higher grass density to hide the terrain. Let me show you what I mean :
As you can see the terrain is visible on hills there is a solution during the shader process to hide this, but it cost really a lot in terms of performances.
The method is explained here : http://www2.disney.co.uk/cms_res/blackrockstudio/pdf/Foliage_Rendering_in_Pure.pdf
So maybe casting a ray to see if there is any non-hidden part of the terrain, or maybe just increase the density on hills might be able to solve the problem. I still don’t know what to do to fix this issue
##3°) Organize your garden
So now we have our seeds planted at a defined location, and we have everything we need to make our grassy terrain dream come true. But at this point we have over a millions of polygons to render, and your might have something like 1 fps or even less. We need to do something about this ! One of the possible solutions is to split your grass object into several smaller part, and each one of them will contain the planting informations we need. What’s cool about this idea is that you can edit your terrain or grass in real time because you only need to recreate the modified part, and do not recreate the whole creation process. The impact in terms of overall performance is great because all the faraway chunk of grass will not be rendered or even checked.
##3.1°) How to ?
We want to put it on the JMonkey terrain system, so we’ll create a grid that fit the size of the terrain. Since TerrainQuad use a Grid system too. We just have to use the same number used to create the Terrain (or maybe less for the patchSize) but anyway it’s really simple. We create a GrassArea a Node that will hold the whole grass system. And split it in 4 GrassPatch at each corner. In the GrassPatch if the size of the patch is equal to the size of the final patch size then we stop the splitting process and create a GrassGeom to contain all the GrassBlade mentionned before.
##3.2°) How to control the generating process
The key for this system is that we need to control the way it show and generate grass blade. All this system work with the Camera. And is implemented so that each frame we’ll check in which area the camera is and look the for GrassPatch with the correct distance. It’s a recursive loop with a distance check every time we want to go further in the GrassArea tree system.
But since we only want the grass in front of us, we need to do compute the drawing distance with the camera direction like so
With this really simple technique we can improve the grass distance view by 50% without any cost in terms of performances ! In terms of recursive loop and distance the process is shown below
We just explore the grass area tree, and check for distance until we found the grass geom object with the correct distance in the correct patch to draw them. If a grass geom is at the right distance he return his GrassBlade list to draw to the GrassControl.
##3.3°) Finally ! Drawing the Grass !
The grass area control now have all the GrassBlade to draw but still, we don’t have anything relevant. And we have one step until we can do this ! Sorting the grass . Since all the grass blade you’ll draw are transparent and the z-buffer is defined when the mesh is created, we have to sort all the grass blade depending on the distance between the camera and the quad. To do so we have a comparator that’ll sort all of them.
grassComp = new Comparator<GrassBlade>() {
public int compare(GrassBlade o1, GrassBlade o2) {
float dist1 = compareRealDistance(cam.getLocation(), o1.position);
float dist2 = compareRealDistance(cam.getLocation(), o2.position);
if (dist1 > dist2) {
return -1;
}
if (dist1 == dist2) {
return 0;
}
return 1;
}
};
Collections.sort(grassList, grassComp);
The final step ! At least for the JMonkey part . We need to draw this mesh. This work is done by the GrassArea itself.
public void createMesh(List<GrassBlade> grassBlades)
We need to create 4 essential buffer and one more for the Billboarding and even for every fancy stuff we can imagine.
FloatBuffer pb = BufferUtils.createVector3Buffer(triCount * 4);
FloatBuffer nb = BufferUtils.createVector3Buffer(triCount * 4);
FloatBuffer tb1 = BufferUtils.createVector2Buffer(triCount * 4);
FloatBuffer tb2 = BufferUtils.createVector2Buffer(triCount * 4);
IntBuffer ib = BufferUtils.createIntBuffer(triCount * 6);
int i = 0;
for (GrassBlade grassB : grassBlades) {
Vector3f p1 = grassB.position;
float size = grassB.size;
float color = grassB.color;
//The Grass quad vertex position
/*p1.x-size/2,p1.y+size,0------------p1.x+size/2,p1.y+size,0
| |
| |
| |
p1.x-size/2,p1.y,0------------p1.x+size/2,p1.y,0 */
pb.put(p1.x - size / 2).put(p1.y + 0).put(p1.z + 0);
pb.put(p1.x + size / 2).put(p1.y + 0).put(p1.z + 0);
pb.put(p1.x - size / 2).put(p1.y + size).put(p1.z + 0);
pb.put(p1.x + size / 2).put(p1.y + size).put(p1.z + 0);
tb1.put(0).put(0);
tb1.put(1).put(0);
tb1.put(0).put(1);
tb1.put(1).put(1);
//color is the perlin noise contrast information and size the size of the grass blade as well as the vertically aligned vertices
tb2.put(color).put(size);
tb2.put(color).put(-size);
tb2.put(color).put(size);
tb2.put(color).put(-size);
//The normal of the grass blade
nb.put(0).put(0).put(1);
nb.put(0).put(0).put(1);
nb.put(0).put(0).put(1);
nb.put(0).put(0).put(1);
//The index buffer (in which we need to link those vertex)
ib.put(2 + i * 4).put(0 + i * 4).put(1 + i * 4).put(1 + i * 4).put(3 + i * 4).put(2 + i * 4);
i++;
}
mesh.setBuffer(Type.Position, 3, pb);
mesh.setBuffer(Type.Normal, 3, nb);
mesh.setBuffer(Type.TexCoord, 2, tb1);
mesh.setBuffer(Type.TexCoord2, 2, tb2);
mesh.setBuffer(Type.Index, 1, ib);
grassGeom.updateModelBound();
And after that you have your GrassMesh !!!
##4°) Be a japanese GARDENER
And now the final step ! SHADER . First,all of what I was talking about earlier in this subject can be done with a single Geometry shader, but since JMe doesn’t fully support GeometryShader we’ll not talk about this further more and get right into the subject of vertex shader and fragment shader !
In the vertex shader we need to billboard all the quad mesh. What it means is that we need them to face the camera to have something realistic and pretty . There is 2 method for this subject. The first one is to face the camera direction ! And the second one is to face the camera position. I choose the second one because in the first one you can see the quad moving along has you move your eyes in the scenes which can be pretty disturbing.
##4.1°) Look at me GRASS ! Look at me !
So what’s going to happen in the vertex shader ? First we’ll illustrate the scene, remember the mesh creation process ? We created all the quad blade aligned along the X axis ! And so at the beginning of our shader process we have something like that
What we need to do is translate both vertically aligned vertices along the camera position and grass center vector. Well since an image is more explicit that anything else
For each vertices you have something like that
vec3 toAdd = vec3(0,0,0);
cameraOffset = normalize(cameraOffset);
float angle = atan(cameraOffset.z, cameraOffset.x);
toAdd.z += -cos(angle) * abs(texCoord2.y);
toAdd.x += sin(angle) * abs(texCoord2.y);
toAdd.x += abs(texCoord2.y);
modelSpacePos.xyz += toAdd/2;
//And then we project
projPosition = projectionMatrix * viewMatrix * modelSpacePos;
We also need to calculate the diffuse light… Since I’m really not a shader guru I’ll not explain my lightning technique since it’s just a bunch of things I find on the Internet… But I’ll do it at some point.
And also we need to give our fragment shader the perlin noise information
colorToMix = texCoord2.x;
##4.2°) Green is the grass ! Grass is the Green
This the last part… (Finally)
We need to give our grass some good looking and the fragment shader is designed to do this ! We’ll manipulate the color of the grass by first giving it a texture and put on the table a bunch of other things.
In this, I’ll use two texture, the first one is a standard ColorMap with all the texture color information and the second an alpha mask based on a perlin noise texture that we’ll use to create a dissolving effect for the grass. First let’s load those two textures
vec4 base_pixel = texture2D(texture,texCoord);
vec4 mask_pixel = texture2D(dissolveTexture,texCoord);
And then we need to mix this texture with a diffuseLighting, and interpolate those value depending on the given points vertical position. Don’t really know what it does but that give me a great effect
finalDiffuse += vec3(3,3,3);
finalDiffuse = vec3(mix(minGrass, finalDiffuse.rgb, texCoord.y));
finalDiffuse = vec3(mix(finalDiffuse.rgb, maxGrass, texCoord.y));
base_pixel.rgb = base_pixel.rgb * finalDiffuse;
Then we’ll use the perlin noise information to give some variety to the grass contrast
base_pixel.rgb = base_pixel.rgb * colorToMix;
Finally we’ll make the texture dissolve in the faraway, to make the transition between the grass and the terrain really smooth
//camDist is the camera distance between the current grass and grass dist is the desired grass distance value
float mMult = 1.0 - pow(min(camDist / grassDist, 1.0), 10.0);
base_pixel.a = base_pixel.a*mMult;
base_pixel.a = base_pixel.a-min(((mask_pixel.r*((camDist/(grassDist))))),(camDist/grassDist));
if(base_pixel.a < 0.01){
discard;
}
##5°) Do you grass now ?
Are you still here ? If you read all of that really good job, because English isn’t really my cup of tea. I really hope this can help people like me to understand the purpose of a grass system. Don’t forget to criticise and express yourself on this subject.
People that helped me : @pspeed, @nehon
Sources :
https://en.wikibooks.org/wiki/GLSL_Programming/Unity/Billboards
http://hub.jmonkeyengine.org/t/how-to-go-with-billboarding-geometries-inside-a-vertex-shader
http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter01.html
http://www2.disney.co.uk/cms_res/blackrockstudio/pdf/Foliage_Rendering_in_Pure.pdf
Thanks, Stomrage