Using terrains in Unity brings you several advantages, one of which is the possibility to scatter trees and grass or other foliage over the terrain with a paint brush, rather than positioning foliage models by hand. You can, in fact, add thousands of grass billboards by painting the terrain, and these will be drawn using […]
This article was posted by Independent Software, a website and database application development company based in Maputo, Mozambique. Our website offers regular write-ups on technical and design issues, ranging from details at code level to 3D Studio Max rendering. Read more about Independent Software's philosophy, or get in touch with Independent Software.
Using terrains in Unity brings you several advantages, one of which is the possibility to scatter trees and grass or other foliage over the terrain with a paint brush, rather than positioning foliage models by hand. You can, in fact, add thousands of grass billboards by painting the terrain, and these will be drawn using only very few draw calls.
The grass billboards will event sway gently in the wind. In the same way, you can paint huge numbers of trees onto your terrain, all of which vary slightly so that no two trees look exactly the same. Similarly, you can liberally scatter rocks around as well.
Sometimes, you will want to interact with the grass that’s on your terrain through a script. In my case, I wanted to be able to remove grass from specific terrain quads, which would happen when the player builds a new building or road on my terrain. I didn’t want bits of grass poking through my models!
As it turns out, creating thousands of grass billboards on a terrain using a paint brush comes at a price. You might think that Unity tracks each bit of grass so that you can remove it individually, but this is actually not the case. Rather, in order to speed things up, Unity has a very particular way of storing your terrain information.
When you create (or modify) a terrain, Unity will have you set a number of parameters. First off, you’ll set your terrain’s physical dimensions, in Unity units (that is, meters). In the above example, I created a terrain of 192×192 meters, with a maximum height of 64. Lower heights, incidentally, will make any hills that you have look smoother as your heightmap’s color range is spread over fewer units.
You’ll also set the heightmap resolution, which determines how may quads there will be spread over the full terrain. In the example, the heightmap resolution is 513, which means that there will be 512 quads both horizontally and vertically (heightmaps are always square). So, that’s a whopping 262,144 quads for the whole terrain.
Now we’re getting to the grass. In the example, the parameter Detail Resolution is set to 64. This means that in any direction, there will be 64 “patches” of grass. We had 512 quads in any direction on the terrain, but we’ll have only 64 grass patches. That means that each grass path will cover 64 quads. How does this work? Unity will actually store a 64×64 pixel bitmap whose pixels values indicate the quantity of grass in each spot. This means:
Higher detail resolution = higher patch of grass vs. terrain quad density = more draw calls.
Detail Resolution per Patch
Now there’s yet another parameter that we can set: Detail Resolution per Patch. In previous versions of Unity this was fixed at 8, but you can now increase the value. This parameter controls how blocks in the detail bitmap are grouped to be sent to the graphics card as a single draw call (or actually a couple). If this value were set to 1, then each pixel in the detail map would result in one or more draw calls. For our 64×64 map, this will mean at least 4096 draw calls (and probably double or triple that). The lowest value is 8, and that means (64/8)*(64/8) = (at least) 64 draw calls. So:
Lower detail resolution per path = more draw calls per patch
If you’re not too bothered with the exact placement of your grass, then you can increase the parameter’s value. On the other hand, if you must have fine control on where your plants live, say, on the banks of a river, then you’ll need a higher detail resolution and a lower detail resolution per patch.
It is important to note that terrain details are actually store in multiple layers. You might add a grass billboard to your terrain, and then another one, and then a model for rocks. When you paint these different types of details on your map, you are actually creating three layers (i.e. three detail bitmaps). When you need to remove details, you’ll have to remove them from each layer.
Removing Terrain Details through Code
The purpose of this article is to show how you can remove terrain details through code. Possible reasons for this might be:
- A building or road is placed on the terrain and you must remove the grass under it;
- A tractor rolls over the terrain and leaves a trail of cut grass behind it (I actually saw this on the Unity boards somewhere).
Let’s see how details are removed. In the following example, terrain details are removed from the lowest layer (layer 0):
// Set all pixels in a detail map to zero.
function RemoveGrass(t: Terrain)
// Get all of layer zero.
var map = t.terrainData.GetDetailLayer(0, 0,
t.terrainData.detailWidth, t.terrainData.detailHeight, 0);
// For each pixel in the detail map...
for (var y = 0; y < t.terrainData.detailHeight; y++)
for (var x = 0; x < t.terrainData.detailWidth; x++)
map[x,y] = 0.0;
// Assign the modified map back.
t.terrainData.SetDetailLayer(0, 0, 0, map);
What happens here?
- The GetDetailLayer(x,y,width,height,layer) method gets us an array (the detail bitmap) for a given layer (it will fail if the layer does not exist).
- We can edit the values in this array. Here, we set them all to zero.
- The SetDetailLayer(x,y,layer,array) assigns the map back to the detail bitmap.
Similarly, you can use the same methods to actually add details to a layer.
Removing Grass in a Specific Location
So far, things look promising. We can add and remove details to a terrain through code. But here, my friends, is where things go awry. Usually, you will want to remove terrain details in a very specific area. The tractor has just rolled by and leaves cut grass in its wake, or a building is built on a specific set of terrain quads and you need to remove the grass from just those quads.
Editing the array returned by GetDetailLayer allows you to edit the value of each pixel in the detail map. So far so good, but our detail map is only 64×64 while our terrain heightmap has a resolution of 512×512. This means we’ll not be able to affect the terrain details on just one terrain quad. Sure, we could boost the resolution of the detail map, but this would go at the cost of an exponentially increasing number of draw calls.
Suppose for a moment that we actually have a detail map of sufficiently high resolution to allow us to cut the grass on a single terrain quad (or a group of quads if you’re not too fussy about the grass cutting location). Then we’ll still have to deal with the Detail Resolution per Patch. You cannot in fact edit a single cell of the details array that GetDetailLayer returns. Instead, you’ll need to edit groups of cells that conform to the detail resolution per patch. If the patch resolution is 8, then you’ll need to give the same value to 8×8 groups of array cells, for each interval of 8 cells.
Therefore, the situation worsens. To be able to cut grass on a single terrain quad, we would need a detail map of equal resolution to the heightmap, and a detail resolution per patch of 1. The latter is not allowed, and the minimum value is 8 – something that can only be gotten around by further increasing the detail resolution by a factor of 8!
The GetDetailLayer and SetDetailLayer methods of the Terrain component allow you to add or remove details from a terrain. However, the resolution that you can cut grass in depends on the Detail Resolution and the Detail Resolution per Patch of your terrain. If the Detail Resolution is equal to the heightmap resolution, then the Detail Resolution per Patch still have a minimum value of 8 so you can cut grass only in 8×8 terrain quad sections.
The only way out is the dramatically reduce your terrain heightmap resolution, so that you can have a Detail Resolution which is 8 times larger.