6

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?)

Fattie
  • 27,874
  • 70
  • 431
  • 719

6 Answers6

4

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;
}
Programmer
  • 121,791
  • 22
  • 236
  • 328
  • I am not sure how `Terrain.GetPosition` work as the document didn't add more info on it and I haven't setup a scene to verify but I believe it's the middle point of the terrain. *"guess it is true that the one under you will be the closest one?"* Nope. Since I am comparing with `Vector3`, both x,y,z components are used to determine this. It simply checks for the closest terrain. – Programmer Sep 17 '18 at 03:38
  • If you want it to only do this check with the y-axis then simply change the `(terrains[0].GetPosition() - playerPos).sqrMagnitude;` and `var dist = (terrainPos - playerPos).sqrMagnitude;` section to use the y component. The rest of the code should remain unchanged. – Programmer Sep 17 '18 at 03:38
  • I said sort in my answer or `Vector3.Distance` or `Vector3.sqrMagnitude` but decided to use `Vector3.sqrMagnitude` because it's faster. Also, you mentioned something about *fast way* so using linq functions would be doing the opposite. – Programmer Sep 17 '18 at 03:44
  • I haven't tested that to see of how it works but it sounds plausible the way you described it. – Programmer Sep 17 '18 at 03:47
  • (.activeTerrain is utterly unrelated to this question) – Fattie Dec 15 '18 at 19:36
  • Terrain.GetPosition() = Terrain.transform.position = position in world NOT center. For terrain center you need for example: 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); – miralong Sep 07 '19 at 21:56
4

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;
}

miralong
  • 789
  • 8
  • 11
2

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

Fattie
  • 27,874
  • 70
  • 431
  • 719
0

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);

  }
Ingo
  • 5,239
  • 1
  • 30
  • 24
  • By the way the real easy way to sort by distance is just `OrderBy( .. distance .. ).ToList();` Example http://answers.unity.com/questions/341065/sort-a-list-of-gameobjects-by-distance.html – Fattie Sep 03 '20 at 18:22
0
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();
}
Michael Sander
  • 2,677
  • 23
  • 29
0

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:

Haxel0rd
  • 1
  • 2
  • Haxel, while your example code is perfect for what it does, please note that the whole point of the question was: **Please note, of course, you could ... cast to find a Terrain under you!** based on the fact that `sampleHeight` exists, they are already continuously using processor power to find out "sampleHeight", so they must - logically - have already found "terrain under you" and/or "nearest terrain"; hence, it's nuts they don't expose that information. The answer to the question on the page does indeed seem to be "no - bizarrely, Unity do not expose it, you have to duplicate their work" – Fattie Nov 22 '22 at 13:06
  • WTF right? Just BTW in some case "nearest" terrain (see the other answers) is more suitable/reliable and/or cheaper to calculate that casting for it – Fattie Nov 22 '22 at 13:07
  • 1
    Hi, sorry man i initially have not seen the part about Raycast in OG Post, i have fitted the title of my answer accordingly. Cheers – Haxel0rd Dec 13 '22 at 03:55
  • no issue! the code will be great for googlers ! – Fattie Dec 13 '22 at 13:23