Detecting Textures Across Multiple Terrains In Unity

Hello all, I hope that as you read this, all is well in your world. The last week has been a truly fitting capstone on the crazy year that has been 2020, especially for U.S. citizens. I want to take your mind off the madness for a moment and talk about something I worked on over the last few days.

As you can read about in my October Update, I’ve been making a skiing game! It’s been awesome so far, and really enjoyable. Something that is crucial to the design of our game is different textures on the terrain beneath the player having differing effects on gameplay. For example, we have a texture that represents “heavy snow”, which will bring down the speed at which the player treks through the snow, as well as an “ice” texture, which would instead speed the player up at a much higher rate, and slow down at a much slower rate. The process itself is not complicated, it just involves getting the terrainData of terrain below the player, and using the player’s relative position to the terrain to calculate what the texture is that is present at that point on of the terrain, and in what strength. While these calculations are somewhat daunting (involving the 3D float array used to represent an alpha map), they’re actually not too complex when broken down.

Where my specific need for this process differs from a lot of what I’ve seen online is that our Unity scene involves upwards of 20 separate terrain objects (and therefore over 20 individual TerrainData data type). The solution here for me was to setup a function to work in tandem with the terrain splat mapping calculations above. This function takes the player’s position, and compares it to the center point of the terrains stored in the array Terrain.activeTerrains. The terrain returned is then interpolated on to determine what textures is beneath the player. Here’s the code return the closest terrain to the player! Just remember I use Scriptable Objects to store live values (such as player posisiton), hence the need for me to specify “.value” to get the Vector3.

Terrain GetCurrentTerrain()
{
    //Array of all terrains
    Terrain[] totalTerrains = Terrain.activeTerrains;

    //Checks on length
    if (totalTerrains.Length == 0) return null;
    else if (totalTerrains.Length == 1) return totalTerrains[0];
    
    //closest terrain, Initialized with totalTerrains[0]
    Terrain closestTerrain = totalTerrains[0];
    
    //Center of terrain at totalTerrains[0]
    Vector3 terrainCenter = new Vector3(closestTerrain.transform.position.x + closestTerrain.terrainData.size.x / 2, playerPos.value.y, closestTerrain.transform.position.z + closestTerrain.terrainData.size.z / 2);
   
    //will be closest distance between player a terrain. Initialized with totalTerrains[0]
    float closestDistance = Vector3.Distance(terrainCenter, playerPos.value);
    

    //Iterate through list of all terrains
    for (int rep = 1; rep < totalTerrains.Length; rep++)
    {
        //currently selected terrain
        Terrain terrain = totalTerrains[rep];
        terrainCenter = new Vector3(terrain.transform.position.x + terrain.terrainData.size.x / 2, playerPos.value.y, terrain.transform.position.z + terrain.terrainData.size.z / 2);

        //Check on distance compared to closest terrain
        float d = Vector3.Distance(terrainCenter, playerPos.value);
        if (d < closestDistance)
        {
            closestDistance = d;
            closestTerrain = totalTerrains[rep];
        }
    }
    //Returns the closest terrain
    return closestTerrain;
}

So now that we have the closest terrain to our player, we need to convert the player’s position in the game to their position on the specific alpha map. This process looks something like this, noting that currentTerrain has just been set to whatever is returned by GetCurrentTerrain():

void GetPlayerTerrainPosition()
{
//Player position relative to terrain
Vector3 playerTerrainPosition = playerPos.value - currentTerrain.transform.position;

//Player position on alphamap of terrain using offset
Vector3 alphamapPosition = new Vector3 (playerTerrainPosition.x / currentTerrain.terrainData.size.x, 0, playerTerrainPosition.z / currentTerrain.terrainData.size.z);

//Properly scales players x and z
float xCoord = alphamapPosition.x * currentTerrain.terrainData.alphamapWidth;
float zCoord = alphamapPosition.z * currentTerrain.terrainData.alphamapHeight;

//Casts as int and sets
xPos = (int)xCoord;
zPos = (int)zCoord;
}

We get out of this call with now our 2 xPos and zPos fields set to the player’s coordinates on the terrain. All that’s left is to take these coordinates and get the alpha map at the player’s position, and determine which terrain texture is applied at that location. One important note is how alpha maps store references to textures. the alpha map is a 3d array where the third value refers to the texture being checked for. For example alphaMap[0,0,0] will return the strength of the texture in slot 0 of the terrain texture layer. alphaMap[0,0,1] will return the strength of texture in slot 1. Hence, splatmapping! We are able to interpolate on various combinations of strength of textures, not just simply player is or isnt on ice. Instead, we can say 30% ice, 70% regular snow, and have our movement variables adjust to that specific combination…. I’m getting off track, but just know this:

textureValues[] is an array of floats representing the strength of each texture at the specified x & z pos. The length of this array is simply set to the number of textures in our terrain layers.

here, rep is used to tie the corresponding spot in textureValues to the value of the texture in that slot of the 3d array

SetPlayerMovementVariables() is currently where we are interpolating on the data gathered here, but essentially the value is clamped from 0 to 1, representing how much of the splatmap at that point is of the texture in the corresponding spot in aMap, and from there we are setting values in our PlayerMovement script. Take a look!

//gets the float (clamped between 0 and 1) of how much of each texture is present at the x & z coord
void CheckTextureBelowPlayer()
{
//Will store the alpha map of the current terrain
float[,,] aMap;

//Uses x position and z position to set aMap to correct alpha map
aMap = currentTerrain.terrainData.GetAlphamaps(xPos, zPos, 1, 1);

//textureValues stores the current stength of the texture stored in the corresponding slot in the alpha Map
for (int rep = 0; rep < textureValues.Length; rep++ )
{
//stores stength of values at that point
textureValues[rep] = aMap[0, 0, rep];
}


//Iterates through to check if any values are greater than 0
for(int rep = 0; rep < textureValues.Length; rep++ )
{
//If terrain is present, sets player movement values
if(textureValues[rep] > 0)
{
SetPlayerMovementVariables(rep, textureValues[rep]);
}

}
}

This flow of operations effectively allows my team’s PlayerMovement script to iterate as usual, but be fed different live values decided by the terrain type. So far this works really well for me, but if I find more to change and tweak, I absolutely will update it here! I hope this helps anyone who is setting out to do this themselves, much like I was a few days ago! Hope you enjoy the code!

One thought on “Detecting Textures Across Multiple Terrains In Unity

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s