- Home /
How can I automatically place grass and other details on my terrain to correspond with the splatmap?
I have a grass texture on certain parts of my terrain, and I want to have a grass detail everywhere that texture is. Is there a quick way to do this?
Answer by duck · Mar 04, 2010 at 11:43 AM
There isn't a quick way, but it is possible, however you'd have to use some of the undocumented terrain commands to read the Alphamap (splat map) data, and then construct a corresponding detail layers to match it.
The commands you'll need to use are:
// read all layers of the alphamap (the splatmap) into a 3D float array: float[,,] alphaMapData = terrainData.GetAlphamaps(x, y, width, height);
// read all detail layers into a 3D int array: int numDetails = terrainData.detailPrototypes.Length; int [,,] detailMapData = new int[terrainData.detailWidth, terrainData.detailHeight, numDetails]; for (int layerNum=0; layerNum < numDetails; layerNum++) { int[,] detailLayer = terrainData.GetDetailLayer(x, y, width, height, layerNum); }
// write all detail data to terrain data: for (int n = 0; n < detailMapData.Length; n++) { terrainData.SetDetailLayer(0, 0, n, detailMapData[n]); }
// write alpha map data to terrain data: terrainData.SetAlphamaps(x, y, alphaMapData);
In your case, you might not want to read the detail layers, you might just want to start with the new int[,,] detailMapData and populate it yourself based on whatever's in the alpha map. You'd then might want to wrap all this up in an editor script, so that you can call it from a menu or something.
Important things to note:
The detail map width and height might be different to the alpha map width and heigth (depending on your terrain settings).
These functions are undocumented. Any future updates of the unity engine might change or remove these functions from the API. This means your project may not work in future versions of the Unity editor, and webplayer builds may not work with future versions of the plugin.
how exactly would these commands be implemented?
Answer by chad · Mar 19, 2010 at 04:04 PM
You can do this with these scripts: http://lemuria.org/projects/wiki/TerrainTools
Answer by RKSandswept · Apr 17, 2014 at 01:06 AM
We do synthetic grass on terrain. I arrived at this info by trial and error and some guesses as to how it is implemented inside Utity3D.
The call SetDetailLayer sets a detail layer. So if your terrain has 4 grasses defined in the detail part, they are indicies 0,1,2,3. So say we have 4 grasses called grass, weed1, weed2, weed3
You make int[z,x] arrays for each layer. Then the value of the array entries is the grass density. We keep track of road paths and various biome regions so we change the mix and keep grass out of roads.
You fill in your arrays and then call SetDetailLayer for each layer.
The array values appear to range from 0 to 14 (why????? Maybe it is stored as a 8 bit byte and 0xFF flags something?) where 0 is no ground cover, and 14 is very dense.
You can set a sub region by passing a smaller array to SetDetailLayer and then the first tow params xBase and yBase to some where in the overall detail grid area.
This code is very particular to our game, but is an example...
WARNING - A later found that x and z are swapped. It should read nearTerrainGrass3[z,x] in lots of places.
Also, our world is X and Z are the ground directions, and Y is altitude.
WorldPosition wp = new WorldPosition(); for(int z = 0; z
debugn = (int)b;
// Terrain splats are:
//
// R G B A
// terrainSplat1 sand mud grass rocky grass
// terrainSplat2 farm woods roadside alpha to terrainSplat1
//
// terrainSplat2 alpha 0.0f mean see through to terrainSplat1
// terrainSplat2 alpha is either 1.0f or 0.0f (no fractional values)
// Also wierdness, terrainSplat1 alpha is inverted as a value for rocky grass.
// Also these draw in layer order R then G then B then Alpha and last wins.
// and also must add up to 1 if factional.
//
// The detail density ranges from 0 to 14.
int grassMax = 14;
switch(b)
{
case eGroundAlphaType.Aspen:
terrainSplat2.SetPixel(x, z, woods2);
nearTerrainGrass0[x,z] = 6;
nearTerrainGrass1[x,z] = 6;
nearTerrainGrass2[x,z] = 6;
nearTerrainGrass3[x,z] = 6;
break;
case eGroundAlphaType.FarmField:
terrainSplat2.SetPixel(x, z, farm2);
nearTerrainGrass0[x,z] = 4;
nearTerrainGrass1[x,z] = 4;
nearTerrainGrass2[x,z] = 4;
nearTerrainGrass3[x,z] = 2;
break;
case eGroundAlphaType.Grass:
terrainSplat1.SetPixel(x, z, grass1);
terrainSplat2.SetPixel(x, z, off2);
nearTerrainGrass0[x,z] = 12;
nearTerrainGrass1[x,z] = 2;
nearTerrainGrass2[x,z] = 2;
nearTerrainGrass3[x,z] = 2;
break;
case eGroundAlphaType.Maple:
terrainSplat2.SetPixel(x, z, woods2);
nearTerrainGrass0[x,z] = 6;
nearTerrainGrass1[x,z] = 6;
nearTerrainGrass2[x,z] = 6;
nearTerrainGrass3[x,z] = 6;
break;
case eGroundAlphaType.Pine:
terrainSplat2.SetPixel(x, z, woods2);
nearTerrainGrass0[x,z] = 6;
nearTerrainGrass1[x,z] = 6;
nearTerrainGrass2[x,z] = 6;
nearTerrainGrass3[x,z] = 6;
break;
case eGroundAlphaType.Road:
terrainSplat2.SetPixel(x, z, roadside2);
nearTerrainGrass0[x,z] = 0;
nearTerrainGrass1[x,z] = 0;
nearTerrainGrass2[x,z] = 0;
nearTerrainGrass3[x,z] = 0;
break;
case eGroundAlphaType.RockyGrass:
terrainSplat1.SetPixel(x, z, rockyGrass1);
terrainSplat2.SetPixel(x, z, off2);
nearTerrainGrass0[x,z] = 9;
nearTerrainGrass1[x,z] = 3;
nearTerrainGrass2[x,z] = 2;
nearTerrainGrass3[x,z] = 2;
break;
default:
terrainSplat1.SetPixel(x, z, grass1);
terrainSplat2.SetPixel(x, z, off2);
nearTerrainGrass0[x,z] = 4;
nearTerrainGrass1[x,z] = 4;
nearTerrainGrass2[x,z] = 4;
nearTerrainGrass3[x,z] = 4;
break;
}
}
}
}
terrainSplat1.Apply();
terrainSplat2.Apply();
// byte[] png = terrainSplat1.EncodeToPNG(); // File.WriteAllBytes("Splat1.png", png); // png = terrainSplat2.EncodeToPNG(); // File.WriteAllBytes("Splat2.png", png);
if(needsSet)
{
TheDeadLinger.tdl.globalTerrain.materialTemplate.SetTexture("_Splat1", terrainSplat1);
TheDeadLinger.tdl.globalTerrain.materialTemplate.SetTexture("_Splat2", terrainSplat2);
TheDeadLinger.tdl.globalTerrain.terrainData.SetDetailLayer(0, 0, 0, nearTerrainGrass0);
TheDeadLinger.tdl.globalTerrain.terrainData.SetDetailLayer(0, 0, 1, nearTerrainGrass1);
TheDeadLinger.tdl.globalTerrain.terrainData.SetDetailLayer(0, 0, 2, nearTerrainGrass2);
TheDeadLinger.tdl.globalTerrain.terrainData.SetDetailLayer(0, 0, 3, nearTerrainGrass3);
}
Answer by theFongz · Jun 12, 2017 at 11:33 PM
Here you go, this one worked for me. It's a bit simple but you should be able to extend it. Thanks @duck and @RKSandswept for putting me on the right path:
public Terrain terrain;
private int detailStrength = 10; //A fair value to make the grass thick and luscious
private int myTextureLayer = 1; //Assumed to be the first non-base texture layer - this is the ground texture from which we wish to sprout grass
private int myDetailLayer = 0; //Assumed to be the first detail layer - this is the grass we wish to auto-populate
// Use this for initialization
void Start () {
TerrainData terrainData = terrain.terrainData;
// get the alhpa maps - i.e. all the ground texture layers
float[,,] alphaMapData = terrainData.GetAlphamaps(0, 0, terrainData.alphamapWidth, terrainData.alphamapHeight);
//get the detail map for the grass layer we're after
int[,] map = terrainData.GetDetailLayer(0, 0, terrainData.detailWidth, terrainData.detailHeight, myDetailLayer);
//now copy-paste the alpha map onto the detail map, pixel by pixel
for(int x = 0 ; x < terrainData.alphamapWidth; x++) {
for(int y = 0 ; y < terrainData.alphamapHeight; y++) {
//Check the Detail Resolution and the Control Texture Resolution in the terrain settings.
//By default the detail resolution is twice the alpha resolution! So every detail co-ordinate is going to have to affect a 2x2 square!
//Would be nice if I could so some anti aliasing but this will have to do for now
int x1 = x * 2;
int x2 = (x * 2)+1;
int y1 = y * 2;
int y2 = (y * 2) + 1;
map [x1, y1] = (int)alphaMapData [x, y, myTextureLayer] * 10;
map [x1, y2] = (int)alphaMapData [x, y, myTextureLayer] * 10;
map [x2, y1] = (int)alphaMapData [x, y, myTextureLayer] * 10;
map [x2, y2] = (int)alphaMapData [x, y, myTextureLayer] * 10;
//if the resolution was the same we could just do the following instead: map [x, y] = (int)alphaMapData [x, y, myTextureLayer] * 10;
}
}
terrainData.SetDetailLayer(0, 0, myDetailLayer, map);
}