Unity, get the "actual" current Terrain?

5.9k views Asked by At

Unity has a function Terrain.sampleHeight(point) which is great, it instantly gives you the height of the Terrain underfoot rather than having to cast.

However, any non-trivial project has more than one Terrain. (Indeed any physically large scene inevitably features terrain stitching, one way or another.)

Unity has a function Terrain.activeTerrain which - I'm not making this up - gives you: the "first one loaded"

Obviously that is completely useless.

Is fact, is there a fast way to get the Terrain "under you"? You can then use the fast function .sampleHeight ?

{Please note, of course, you could ... cast to find a Terrain under you! But you would then have your altitude so there's no need to worry about .sampleHeight !}

In short is there a matching function to use with sampleHeight which lets that function know which Terrain to use for a given xyz?

(Or indeed, is sampleHeight just a fairly useless demo function, usable only in demos with one Terrain?)

6

There are 6 answers

0
Fattie On BEST ANSWER

It turns out the answer is simply NO, Unity does not provide such a function.

1
Ingo On

You can use this function to get the Closest Terrain to your current Position:

int GetClosestTerrain(Vector3 CheckPos)
{
    int terrainIndex = 0;
    float lowDist = float.MaxValue;

    for (int i = 0; i < _terrains.Length; i++)
    {
      var center = new Vector3(_terrains[i].transform.position.x + _terrains[i].terrainData.size.x / 2, CheckPos.y, _terrains[i].transform.position.z + _terrains[i].terrainData.size.z / 2);

      float dist = Vector3.Distance(center, CheckPos);

      if (dist < lowDist)
      {
        lowDist = dist;
        terrainIndex = i;
      }
    }
    return terrainIndex;
}

and then you can use the function like this:

private Terrain[] _terrains;

void Start()
  {
    _terrains = Terrain.activeTerrains;

    Vector3 start_pos = Vector3.zero;

    start_pos.y = _terrains[GetClosestTerrain(start_pos)].SampleHeight(start_pos);

  }
7
Programmer On

Is there in fact a fast way to get the Terrain "under you" - so as to then use the fast function .sampleHeight ?

Yes, it can be done.

(Or indeed, is sampleHeight just a fairly useless demo function, usable only in demos with one Terrain?)

No


There is Terrain.activeTerrain which returns the main terrain in the scene. There is also Terrain.activeTerrains (notice the "s" at the end) which returns active terrains in the scene.

Obtain the terrains with Terrain.activeTerrains which returns Terrain array then use Terrain.GetPosition function to obtain its position. Get the current terrain by finding the closest terrain from the player's position. You can do this by sorting the terrain position, using Vector3.Distance or Vector3.sqrMagnitude (faster).

Terrain GetClosestCurrentTerrain(Vector3 playerPos)
{
    //Get all terrain
    Terrain[] terrains = Terrain.activeTerrains;

    //Make sure that terrains length is ok
    if (terrains.Length == 0)
        return null;

    //If just one, return that one terrain
    if (terrains.Length == 1)
        return terrains[0];

    //Get the closest one to the player
    float lowDist = (terrains[0].GetPosition() - playerPos).sqrMagnitude;
    var terrainIndex = 0;

    for (int i = 1; i < terrains.Length; i++)
    {
        Terrain terrain = terrains[i];
        Vector3 terrainPos = terrain.GetPosition();

        //Find the distance and check if it is lower than the last one then store it
        var dist = (terrainPos - playerPos).sqrMagnitude;
        if (dist < lowDist)
        {
            lowDist = dist;
            terrainIndex = i;
        }
    }
    return terrains[terrainIndex];
}

USAGE:

Assuming that the player's position is transform.position:

//Get the current terrain
Terrain terrain = GetClosestCurrentTerrain(transform.position);
Vector3 point = new Vector3(0, 0, 0);
//Can now use SampleHeight
float yHeight = terrain.SampleHeight(point);

While it's possible to do it with Terrain.SampleHeight, this can be simplified with a simple raycast from the player's position down to the Terrain.

Vector3 SampleHeightWithRaycast(Vector3 playerPos)
{
    float groundDistOffset = 2f;
    RaycastHit hit;
    //Raycast down to terrain
    if (Physics.Raycast(playerPos, -Vector3.up, out hit))
    {
        //Get y position
        playerPos.y = (hit.point + Vector3.up * groundDistOffset).y;
    }
    return playerPos;
}
0
Michael Sander On
public static Terrain GetClosestTerrain(Vector3 position)
{
    return Terrain.activeTerrains.OrderBy(x =>
    {
        var terrainPosition = x.transform.position;
        var terrainSize = x.terrainData.size * 0.5f;
        var terrainCenter = new Vector3(terrainPosition.x + terrainSize.x, position.y, terrainPosition.z + terrainSize.z);
        return Vector3.Distance(terrainCenter, position);
    }).First();
}
4
Haxel0rd On

Raycast solution: (this was not asked, but for those looking for Solution using Raycast)

Raycast down from Player, ignore everything that has not Layer of "Terrain" (Layer can be easily set in inspector).

Code:

    void Update() {
    // Put this on Player! Raycast's down (raylength=10f), if we hit something, check if the Layers name is "Terrain", if yes, return its instanceID
    RaycastHit hit;
    if (Physics.Raycast (transform.localPosition, transform.TransformDirection (Vector3.down), out hit, 10f, 1 << LayerMask.NameToLayer("Terrain"))) {
        Debug.Log(hit.transform.gameObject.GetInstanceID());
    }
}

At this point already, you have a reference to the Terrain by "hit.transform.gameObject".

For my case, i wanted to reference this terrain by its instanceID:

    // any other script
    public static UnityEngine.Object FindObjectFromInstanceID(int goID) {
    return (UnityEngine.Object)typeof(UnityEngine.Object)
            .GetMethod("FindObjectFromInstanceID", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)
            .Invoke(null, new object[] { goID });
}

But as written above, if you want the Terrain itself (as Terrain object) and not the instanceID, then "hit.transform.gameObject" will give you the reference already.

Input and code snippets taken from these links:

0
miralong On

Terrain.GetPosition() = Terrain.transform.position = position in world
working method:

Terrain[] _terrains = Terrain.activeTerrains;

int GetClosestCurrentTerrain(Vector3 playerPos)
{
    //Get the closest one to the player
    var center = new Vector3(_terrains[0].transform.position.x + _terrains[0].terrainData.size.x / 2, playerPos.y, _terrains[0].transform.position.z + _terrains[0].terrainData.size.z / 2);
    float lowDist = (center - playerPos).sqrMagnitude;
    var terrainIndex = 0;

    for (int i = 0; i < _terrains.Length; i++)
    {
        center = new Vector3(_terrains[i].transform.position.x + _terrains[i].terrainData.size.x / 2, playerPos.y, _terrains[i].transform.position.z + _terrains[i].terrainData.size.z / 2);

        //Find the distance and check if it is lower than the last one then store it
        var dist = (center - playerPos).sqrMagnitude;
        if (dist < lowDist)
        {
            lowDist = dist;
            terrainIndex = i;
        }
    }
    return terrainIndex;
}