Hello!
! UPDATE !
For those interested in a complete grass/tree, easy to setup system based on this tree and the grass system check out ‘** Critias Foliage System **’.
! UPDATE !
A full project with all the code included can be found here.
One of Unity’s ‘big’ problems is the lack of a decent vegetation system. Developing an open world quest I had to fix that issues. For me none of the current asset store or unity sollutions (sorry guys) proved to be effective and to both look good and work well.
My goal was the performance and looks of certain AAA games out there, that sucesfully used SpeedTree for everything from grass to trees.
The current system uses SpeedTree grass with it’s wind animation that are directly exported from the Unity’s SpeedTree editor.
Implementation:
Grass mesh data counts per patch: 570 verts, 234 tris with uv/uv2/uv3/uv4/colors, just like good ol’ SpeedTree exports them.
Grass blade count: 10 milion - Well the count doesn’t really matter, only the density per patch matters.
Terrain size: 2250 meters x 2250 meters
Big cell size: 90 meters, 25 cells in length, 625 total cells
Small cell count: 5 subdivisions for a large cell, 25 small cells per large cell, 18M each, at 15625 total count
Grass density: 16000 blades per large patch, 640 blades per small patch
Take one terrain and divide it into large cells, creating a grid.
(Overview)
(Grid large cell)
(Grid small cells)
Don’t worry, we don’t need to keep all those references and ‘BoxCollider’ objects, we can safely delete them. All we need is their world bounds that is stored with the grass.
Having that determining in which cell the player is, is trivial based on terrain’s local space player position using interpolation.
Vector3 pos = ; // - get your player's position -
Vector3 size = terrain.terrainData.size;
int row = Mathf.Clamp(Mathf.FloorToInt(pos.x / size.x * m_Cells), 0, m_Cells - 1);
int col = Mathf.Clamp(Mathf.FloorToInt(pos.z / size.z * m_Cells), 0, m_Cells - 1);
After we determine in which large cell we are, take all the sourounding cells of that cell and test them for visibility using ‘GeometryUtility’ class.
if (GeometryUtility.TestPlanesAABB(planes, bigGrassBox.m_Bounds))
{
// If it is visible display grass
totalUpdated += Render(bigGrassBox, ref cameraPosition, planes);
}
We only need to test for 9 visibility bounds. Ussualy there are something like a 3 big grids mostly visibile, and out of the rest only a few small cells are visibile.
If a big cell is visible we do the following:
- Iterate through it’s small grids
- If the small grid within the grass distance AND it is visible proceed
for(int cellIndex = 0; cellIndex < data.m_Cells.Length; cellIndex++)
{
...
if (distanceToSmallerGrid < m_VisibilityRange + box.m_Bounds.size.x && GeometryUtility.TestPlanesAABB(clipPlanes, box.m_Bounds))
{
...
}
}
We test the distance first since since it the ‘TestPlanesAABB’ is more ‘computationally expensive’ :p.
After we determine that a small grid is within range AND visible we iterate through it’s grass patches and start collecting them if we have the ‘DrawMeshInstanced’ API or simply draw them if we don’t. We only collect a grass patch if it is withing viewing range. We don’t test it for visibility, that is why we have that ton of grids, to mitigate the need of testing the visibility.
And how much grass we draw? Well of course not all of it for distance cells. We chose it using a dynamic lod function:
private int DynamicLOD(float cameraDistance, float cameraNearZ, float cameraFarZ, int maxBladeCount)
{
return (int)((1 - 7 * (cameraDistance - cameraNearZ) / (8 * cameraFarZ)) * maxBladeCount);
}
// 625 is the maximum grass blade count per small patch, can be variable based on your density
int grassCount = DynamicLOD(distanceToSmallerGrid, 0.3, m_VisibilityRange, 625);
for (int i = index; i < grassCount; i++)
{
...
bool castShadow = m_CastReceiveShadows ? (distance < m_ShadowDistance ? true : false) : false;
if (distance < m_VisibilityRange)
{
...
if(hasInstanced)
{
if(castShadow)
grassBatchMatrixShadow.Add(grass.m_WorldMatrix);
else
grassBatchMatrixNoShadow.Add(grass.m_WorldMatrix);
}
else
{
if (distance < m_AnimationRange)
{
Grahpics.DrawMesh(grass, .... whatever animated, castShadow);
}
else
{
Grahpics.DrawMesh(grass, .... whatever animated not animated, castShadow);
}
}
}
}
if(hasInstanced)
{
Grahpics.DrawMeshInstanced(grassBatchMatrixShadow, ... whatever, true);
Grahpics.DrawMeshInstanced(grassBatchMatrixNoShadow, ... whatever, false);
}
This can be further optimized so that we don’t test with ‘if’ each blade count but have before we enter a small cell a function chosen based on the hardware capabilities. But these are details.
Performance:
- 10 milion blades of grass, 2250M terrain, 90M cell size, 5 cell subdivisions *
Unity 5.4 - GTX 970:
60 FPS at 50M of grass distance, 30M animation, 10M shadows - DrawMesh API (we don’t really have an alternative…)
Unity 5.5 - GTX 970:
300 FPS at 50M of grass distance, 50m animation, no shadows - DrawMeshInstanced API
150 FPS at 50M of grass distance, 50m animation, full shadows - DrawMeshInstanced API
37 FPS at 50M of grass distance, 30M animation, 10M shadows - DrawMesh API
Limitations:
- No player interaction. But with some smart shaders, that also becomes possible, by activating only the grass that is very close around the player and bend
it accordingly when it collides. We can also optimize the creation of colliders in a ton of ways that I will not cover now. - Large memory footprint since it uses grass with positions. We can mitigate that with:
- Procedural grass based on ground texture, generated at runtime
- Holding in the list only the grass type and position, modifying the scale at runtime procedurally
- Loading grass cell data in memory only when required, unloading data that we don’t require, based on active grid
- No fading. But that can be fixed with some SpeedTree shader modifications. It’s still work in progress
- Grass popping when changing distance. Can be fixed so that DynamicLod is only applied to grass that is less visible. For example let us not apply it to flowers…
It seems that ‘DrawMesh’ is slower on 5.5 than in 5.4 at least in the beta. I’m sure that’s going to ruin somebody’s game :)).
Of course as a optimization we can draw only partly the shadows, something like 20-30 meters and the rest with no shadows in order to keep the FPS at 200-250.
Tested on a “GTX 750Ti Golden Sample” too, with good results.
A GTX970 is pretty high end so, of course, I hope that as soon as possible I can provide a demo in order to let you all test the system on your system’s configuration and provide with feedback and suggestions. I also hope that for our game (The Unwritten Critias, A full open-world game) we can have some in-game footage soon, in order to see the system applied in a real game! The game is not only grass, but a ton of other objects and effects, afterall.
And it seems that we’re forced to wait for 5.5 due to it’s DrawMeshInstanced API. We can’t let the grass to eat up everything else.
Some screenshots:
(90 meters of pure powergrass… With flowers.)
(50M all animated)
References:
- Dynamic LOD function from:
http://amd-dev.wpengine.netdna-cdn.com/wordpress/media/2012/10/i3dGrassFINAL.pdf
! UPDATE !
You can now find the demo here.
! Update !
Is someone is interesting on the tree system also, feel free to check it out here.
Update 1:
- Camera distance based scaling so that distant grass does not pop any more
- Possibility to set batch size sent to GPU
Update2:
- Removed DX9 support, only DX11/DX12





