Terrain with multiple splat textures - how can I detect which kind is under my character's location?

How can you detect if a character walk on for example grass, gravel or sand? Currently it seems that you can only assign one physics material or tag for one big terrain?

This is slightly trickier than it might initially seem - partly because any given location on the terrain can be a mixture of two or more types of terrain, and partly because the data used to store this information is relatively complex.

Briefly, you need to read Alpha Map data from the terrain class using the GetAlphaMaps function. The alphamap data is a grid of values spread over the area of the terrain, and each entry in the grid contains the values used to "mix" each of the different terrain textures, so more specifically you need to calculate which grid cell your player is in, then read the mix values for that grid cell.

I've written about this in more detail here, however I'm tempted to wrap this up quickly into a helper function which can be used without getting knee deep in the terrain API :)

-- EDIT --

Here you go. Add the large script below in as a new C# script in your project. Name the file "TerrainSurface".

You'll then be able to use these helper functions to read either the mix values of textures, or just get the most dominant texture index at a given world position. Eg:

function Update() {
    var surfaceIndex = TerrainSurface.GetMainTexture(transform.position);
}

Your "surfaceIndex" will now contain an integer denoting the zero-based-index of the texture which is the most dominant texture at that location. If you have 4 textures added to your terrain, the value will be one of 0,1,2 or 3.

Alternatively you can read the actual mix of textures, like this:

function Update() {
    var surfaceMix = TerrainSurface.GetTextureMix(transform.position);
}

Your "surfaceMix" will contain an array of floating point values, denoting the relative mix of each texture at that location. For example, if you have 4 textures, and the 4th texture is "Mud", you could use this to determine how muddy the current location is, like this: (remember, the indices are zero-based, so the 4th texture has an index of 3)

function Update() {
    var surfaceMix = TerrainSurface.GetTextureMix(transform.position);
    var muddiness = surfaceMix[3];
}


And below is the main helper script. Add this in as a new C# script in your project. Name the file "TerrainSurface".

You can use the functions from C# scripts straight off. If you want to use these function from other Javascript scripts (as in the examples above), you need to put the script in a folder that gives it an earlier compilation pass. To do this, make a folder in your Project called "Plugins" and put the C# TerrainSurface script in there.

Enjoy!

// -- TerrainSurface.cs --

using UnityEngine;
using System.Collections;

public class TerrainSurface {

    public static float[] GetTextureMix(Vector3 worldPos) {

        // returns an array containing the relative mix of textures
        // on the main terrain at this world position.

        // The number of values in the array will equal the number
        // of textures added to the terrain.

        Terrain terrain = Terrain.activeTerrain;
        TerrainData terrainData = terrain.terrainData;
        Vector3 terrainPos = terrain.transform.position;

        // calculate which splat map cell the worldPos falls within (ignoring y)
        int mapX = (int)(((worldPos.x - terrainPos.x) / terrainData.size.x) * terrainData.alphamapWidth);
        int mapZ = (int)(((worldPos.z - terrainPos.z) / terrainData.size.z) * terrainData.alphamapHeight);

        // get the splat data for this cell as a 1x1xN 3d array (where N = number of textures)
        float[,,] splatmapData = terrainData.GetAlphamaps(mapX,mapZ,1,1);

        // extract the 3D array data to a 1D array:
        float[] cellMix = new float[splatmapData.GetUpperBound(2)+1];
        for (int n=0; n<cellMix.Length; ++n)
        {
            cellMix[n] = splatmapData[0,0,n];    
        }

        return cellMix;        

    }

    public static int GetMainTexture(Vector3 worldPos) {

        // returns the zero-based index of the most dominant texture
        // on the main terrain at this world position.

        float[] mix = GetTextureMix(worldPos);

        float maxMix = 0;
        int maxIndex = 0;

        // loop through each mix value and find the maximum
        for (int n=0; n<mix.Length; ++n)
        {
            if (mix[n] > maxMix)
            {
                maxIndex = n;
                maxMix = mix[n];
            }
        }

        return maxIndex;

    }

}

Here is my JavaScript translation of the same cs code:
Thanks Ben! (works splendidly) !

// – TerrainSurface.js – //
// Ben Pitt //
// JS translation by MartianGames :slight_smile: //

public class TerrainSurface {

public static function GetTextureMix(worldPos:Vector3) {
    // returns an array containing the relative mix of textures
    // on the main terrain at this world position.

    // The number of values in the array will equal the number
    // of textures added to the terrain.

    var terrain:Terrain = Terrain.activeTerrain;
    var terrainData:TerrainData = terrain.terrainData;
    var terrainPos:Vector3 = terrain.transform.position;

    // calculate which splat map cell the worldPos falls within (ignoring y)
    var mapX:int = (((worldPos.x - terrainPos.x) / terrainData.size.x) * terrainData.alphamapWidth);
    var mapZ:int = (((worldPos.z - terrainPos.z) / terrainData.size.z) * terrainData.alphamapHeight);

    // get the splat data for this cell as a 1x1xN 3d array (where N = number of textures)
    var splatmapData = terrainData.GetAlphamaps(mapX,mapZ,1,1);

    // extract the 3D array data to a 1D array:
    var cellMix = new float[splatmapData.GetUpperBound(2)+1];
    for (var n=0; n<cellMix.Length; ++n)
    {
        cellMix[n] = splatmapData[0,0,n];    
    }

    return cellMix;        

}

public static function GetMainTexture(worldPos:Vector3) {

    // returns the zero-based index of the most dominant texture
    // on the main terrain at this world position.

    var mix = GetTextureMix(worldPos);

    var maxMix = 0f;
    var maxIndex = 0;

    // loop through each mix value and find the maximum
    for (var n=0; n<mix.Length; ++n)
    {
        if (mix[n] > maxMix)
        {
            maxIndex = n;
            maxMix = mix[n];
        }
    }

    return maxIndex;

}

}

I would suggest that you place a collider or something like that over, for example grass. Then give that collider a tag of Grass, or a name of Grass.

Then in your script you have something like:

    function OnCollisionEnter(collisionInfo : Collision) {
        print("Now Touching: " + collisionInfo.transform.name);
        PlayGrass = true;
    }

And place that within your Player

EDIT---

With that in place you can do something like this:

var PlayGrass = false;

function Update()
{

if(PlayGrass == true)
{
audio.Play(Grass Audio File);
}
}

Rough Coding there, but you get the picture

   // calculate which splat map cell the worldPos falls within (ignoring y)
    int mapX = (int)(((worldPos.x - terrainPos.x) / terrainData.size.x) * terrainData.alphamapWidth);
    int mapZ = (int)(((worldPos.z - terrainPos.z) / terrainData.size.z) * terrainData.alphamapHeight);

I can not fully understand it, could you explain it for me? Thanks a lot.

You cant use if statements to return an int. Cannot convert int to bool…

I made a script for getting this value. Compared to the other scripts here, it has cleaner code, and it is more extensible (proper support for multiple terrains in a scene). It’s free & open source on my github, and I’m using it in production, so it will receive maintenance indefinitely.