Detecting terrain texture at position

I have a terrain with two textures painted on it. One is a grass texture and another is sand.
I want to detect which one of these textures my player is walking on and play a corresponding looping audio clip (footstep sound).
Does anybody know if there a way to get the terrain texture name at a specific position on the terrain?

So I found the way to get all terrain textures and based on their opacity I can determine which footstep sound to play.
Here is how I did it.

void Start()
{
    mTerrainData = Terrain.activeTerrain.terrainData;
    alphamapWidth = mTerrainData.alphamapWidth;
    alphamapHeight = mTerrainData.alphamapHeight;

    mSplatmapData = mTerrainData.GetAlphamaps(0, 0, alphamapWidth, alphamapHeight);
    mNumTextures = mSplatmapData.Length / (alphamapWidth * alphamapHeight);
}

private Vector3 ConvertToSplatMapCoordinate(Vector3 playerPos)
{
    Vector3 vecRet = new Vector3();
    Terrain ter = Terrain.activeTerrain;
    Vector3 terPosition = ter.transform.position;
    vecRet.x = ((playerPos.x - terPosition.x) / ter.terrainData.size.x) * ter.terrainData.alphamapWidth;
    vecRet.z = ((playerPos.z - terPosition.z) / ter.terrainData.size.z) * ter.terrainData.alphamapHeight;
    return vecRet;
}

void Update()
{
    int terrainIdx = GetActiveTerrainTextureIdx();
    PlayFootStepSound(terrainIdx);
}

int GetActiveTerrainTextureIdx()
{
    Vector3 playerPos = PlayerController.Instance.position;
    Vector3 TerrainCord = ConvertToSplatMapCoordinate(playerPos);
    int ret = 0;
    float comp = 0f;
    for (int i = 0; i < mNumTextures; i++)
    {
        if (comp < mSplatmapData[(int)TerrainCord.z, (int)TerrainCord.x, i])
            ret = i;
    }
    return ret;
}
9 Likes

can you put in the start of the code(like variables and that)

Umm… So what about the variables and other stuff like that to initialize the code?(Weak programmer, need help)

I cleaned up the code a little bit. Here is the full script:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TerrainManager : MonoBehaviour
{
    public TerrainData mTerrainData;
    public int alphamapWidth;
    public int alphamapHeight;
 
    public float[,,] mSplatmapData;
    public int mNumTextures;

    void Start()
    {
        GetTerrainProps();
    }

    private void GetTerrainProps() {
        mTerrainData = Terrain.activeTerrain.terrainData;
        alphamapWidth = mTerrainData.alphamapWidth;
        alphamapHeight = mTerrainData.alphamapHeight;
 
        mSplatmapData = mTerrainData.GetAlphamaps(0, 0, alphamapWidth, alphamapHeight);
        mNumTextures = mSplatmapData.Length / (alphamapWidth * alphamapHeight);
    }

    private Vector3 ConvertToSplatMapCoordinate(Vector3 playerPos)
    {
        Vector3 vecRet = new Vector3();
        Terrain ter = Terrain.activeTerrain;
        Vector3 terPosition = ter.transform.position;
        vecRet.x = ((playerPos.x - terPosition.x) / ter.terrainData.size.x) * ter.terrainData.alphamapWidth;
        vecRet.z = ((playerPos.z - terPosition.z) / ter.terrainData.size.z) * ter.terrainData.alphamapHeight;
        return vecRet;
    }

    private int GetActiveTerrainTextureIdx(Vector3 pos)
    {
        Vector3 TerrainCord = ConvertToSplatMapCoordinate(pos);
        int ret = 0;
        float comp = 0f;
        for (int i = 0; i < mNumTextures; i++)
        {
            if (comp < mSplatmapData[(int)TerrainCord.z, (int)TerrainCord.x, i])
                ret = i;
        }
        return ret;
    }

    public int GetTerrainAtPosition(Vector3 pos)
    {
        int terrainIdx = GetActiveTerrainTextureIdx(pos);
        return terrainIdx;
    }
}
5 Likes

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.

https://github.com/JimmyCushnie/JimmysUnityUtilities/blob/master/Scripts/TerrainTextureDetector.cs

4 Likes

Hey, is there a fix for these two lines or is 2019.3 just not compatible with “ContainsIndex”? Do we need to find these indices the long way?

 if (!CachedTerrainAlphamapData.ContainsIndex(alphamapCoordinates.x, dimension: 1))
            return -1;

        if (!CachedTerrainAlphamapData.ContainsIndex(alphamapCoordinates.z, dimension: 0))
            return -1;

ContainsIndex is a custom extension method from JUU. The code looks like this:

public static bool ContainsIndex(this Array array, int index, int dimension)
{
    if (index < 0)
        return false;

    return index < array.GetLength(dimension);
}

If you stick that in a static class, you'll be able to use the ContainsIndex function and TerrainTextureDetector should compile. You can also install the full JUU library, but that's probably overkill if you only want the one thing from it*.*

1 Like

Hey there! Thanks for the work here, I am getting a few errors after putting that ContainsIndex class:

TerrainTextureDetector.cs(76,24): error CS0116: A namespace cannot directly contain members such as fields or methods

TerrainTextureDetector.cs(76,43): error CS0246: The type or namespace name 'Array' could not be found (are you missing a using directive or an assembly reference?)

Assets\1_Scripts\TerrainTextureDetector.cs(76,24): error CS1106: Extension method must be defined in a non-generic static class

Any idea what I did wrong?

ContainsIndex is not a class, it is a method. It must be put inside a class. Note my instruction -- "If you stick that in a static class". Please read the documentation on extension methods. Also consider reading the error messages, which tell you exactly what's wrong and how to fix it.

As for the CS0246, the Array class being referenced is this one, in the System namespace. You'll need a using System; to fix it.

1 Like

hello so i dont get any errors but where do i set the sounds for each texture?

Thanks!

@cxode Very very useful script, thank you very much!

1 Like

Cheers, I'm glad I could help :)

not working for terrain placed not in zero coord.
old:

        Vector3Int ConvertToAlphamapCoordinates(Vector3 _worldPosition)
        {
            Vector3 relativePosition = _worldPosition - transform.position;
            // Important note: terrains cannot be rotated, so we don't have to worry about rotation

            return new Vector3Int
            (
                x: Mathf.RoundToInt((relativePosition.x / ter.terrainData.size.x) * ter.terrainData.alphamapWidth),
                y: 0,
                z: Mathf.RoundToInt((relativePosition.z / ter.terrainData.size.z) * ter.terrainData.alphamapHeight)
            );
        }

need to add into this method substract oper for x and z. rel pos - your ter pos
new:

        Vector3Int ConvertToAlphamapCoordinates(Vector3 _worldPosition)
        {
            Vector3 relativePosition = _worldPosition - transform.position;
            // Important note: terrains cannot be rotated, so we don't have to worry about rotation

            return new Vector3Int
            (
                x: Mathf.RoundToInt(((relativePosition.x - terTr.position.x) / ter.terrainData.size.x) * ter.terrainData.alphamapWidth),
                y: 0,
                z: Mathf.RoundToInt(((relativePosition.z - terTr.position.z) / ter.terrainData.size.z) * ter.terrainData.alphamapHeight)
            );
        }