Search Unity

How to indentify a tree type or prefab with raycasting?

Discussion in 'Scripting' started by Revontuli, May 12, 2011.

  1. Revontuli

    Revontuli

    Joined:
    May 12, 2011
    Posts:
    29
    Short version of the question:

    How can I identify a tree (determining if it's, say, a pine vs. an oak) that's part of the terrain if I can get a collision from a ray cast from a camera?

    Long version of the question, with details:

    The project I'm working on seeks to embed information about species of plants and animals in the virtual world - by looking at a tree or animal, data should appear about that type of organism. Since we're in the middle of a forest, we're trying have this apply to the flora in the terrain system as well...

    I've seen a few posts with similar questions, but nothing that's quite what I'm looking for. I only need to read information, so the common problems of dynamically changing the terrain and updating it should not apply.

    The world I'm working on is large, fairly static, but contains thousands of trees, so searching through the entire list of treeInstances is not feasible. Nor would placing colliders by hand, for that matter. That said, the capsule colliders for the trees _are_ working, and while a ray cast from the camera only detects the collider associated with the "terrain", it seems to detect tree-capsule collisions along with the ground itself just fine. It just returns "terrain" as the object hit, and I need the actual tree.

    Some scripts I've seen use the location of the raycast collision and search through _all_ treeinstances - I've tried this, but there are far too many trees to search for this way, and the the program grinds to a halt and freezes. Plus, I doubt this should be necessary if I have a collision point...and I personally don't actually need a specific, individual tree, just its type.

    My current plan is to convert the ray cast collision point into an x,y coordinate and try to determine what kind of tree is at that point on the splatTexture in the terrain info. Does this approach make sense? This is sort of a roundabout way of doing things, and right now I can't directly access the splatTextures from the script (I only want to read them, honest!) and I'm not really sure how Unity embeds tree prefabs types into its splatTextures...

    Is there a more straightforward way to find a treeInstance prefab type given a collision point on a terrain?

    This is sort of the dynamic terrain API problem in reverse, and while I may eventually do some asset swapping (say, a summer oak for a winter oak) the terrain heights and trees themselves should stay where they originally were, I just need to be able to mine that information during gameplay. Any help on this would be appreciated!
     
    Wylaryzel likes this.
  2. JFFM

    JFFM

    Joined:
    Nov 13, 2010
    Posts:
    336
    At runtime something to the tune of:

    Code (csharp):
    1.  
    2.  
    3. //Assumption: You have a class attached to your tree objects which contains the metadata that you wish to display
    4. //OR: You have a name-to-metadata mapping in your class, and a function to look through this array of tree-name //mappings and return the information you want
    5.  
    6. [Serializable]
    7. public class TreeMapping{
    8.  
    9. [indent]
    10. public string treeObjectName;
    11. public TreeInformation treeInformation;
    12. [/indent]
    13.  
    14. }
    15.  
    16. [Serializable]
    17. public class TreeInformation{
    18. [indent]
    19. public string treeInformation;
    20. [/indent]
    21. }
    22.  
    23. public TreeMapping[] treeInfoMappings;
    24.  
    25. if(Physics.Raycast(your ray properties)){
    26.  
    27. [indent]
    28. Debug.Log("Your ray struck: " + collider.gameObject.name);
    29. TreeInformation t = collider.gameObject.GetComponentOfType(typeof(TreeInformation)) as TreeInformation;
    30.  
    31. if(t != null){
    32. [indent]
    33. Debug.Log("This object was a tree - with metadata: " + t.ToString());
    34. Debug.Log("As such, I advise you to do whatever you feel like with this information.");
    35. [/indent]
    36. }
    37. else{
    38. [indent]
    39. Debug.Log("Ok. I am assuming you are mapping gameobject names to metadata.");
    40. TreeInformation t = GetInformationForTreeName(collider.gameObject.name);
    41. Debug.Log("In this case, your tree is defined by information: " + t.ToString());
    42. [/indent]
    43. }
    44. [/indent]
    45.  
    46. }
    47.  
    Is that roughly what you're looking for?

    It shooouuuuld be. Although admittedly I skimmed the bulky part of your post ;)

    -JFFM
     
  3. KyleStaves

    KyleStaves

    Joined:
    Nov 4, 2009
    Posts:
    821
    As far as I know, the tree colliders get merged into the terrain collider - so it's all one big collider.

    You could manually generate colliders for each tree at runtime - but the performance impact of having that many colliders in the scene would likely be huge.

    Curious, why can't you access the splatTextures? I can and do in several areas at runtime (and in the editor for custom scripts). The thing is it doesn't really make the terrain splats in the way you may think.

    Basically the terrain texture is built using the following data:
    TerrainData.splatPrototypes ---> This stores the actual prototype texture data
    TerrainData.alphamapResolution ---> How big the alphamap is
    TerrainData.GetAlphamaps ----> Returns the actual alphamap data

    Unity doesn't store the finished texture anywhere that you can access it. Instead, you need to piece it together yourself based on a combination of the splat prototypes and the alphamaps returned from GetAlphamaps.

    I'm not sure why you would need that though - the tree data and the splat data are stored in two different places. The tree data is only stored in:

    TerrainData.treeInstances[]

    ----------------------------------------------------------------------------------------

    Here is something you could try, though it's a *little* convoluted...

    Code (csharp):
    1.  
    2. public class TreeFinder {
    3.     Dictionary<Vector2, List<TreeInstance>> _treeDictionary;
    4.     const int STORAGE_RESOLUTION = 256;
    5.    
    6.     void Awake () {
    7.         _treeDictionary = new whatever();
    8.        
    9.         for (int i = 0; i < STORAGE_RESOLUTION; i++){
    10.             for (int j = 0; j < STORAGE_RESOLUTION; j++){
    11.                 _treeDictionary.Add(new Vector2(i,j), new List<TreeInstance>());
    12.             }
    13.         }
    14.        
    15.         foreach(TreeInstance ti in Terrain.activeTerrain.terrainData.TreeInstances){
    16.             // Find which cell in your 2D storage map this tree falls into
    17.             int thisTreeX;
    18.             int thisTreeZ;
    19.            
    20.             // Step 2: Add this tree to that list
    21.             _treeDictionary[new Vector2(thisTreeX, thisTreeZ)].Add(ti);
    22.         }
    23.     }
    24.    
    25.     TreeInstance FindTreeAt(Vector3 hitPoint){
    26.         TreeInstance returnedValue;
    27.         // Convert hitPoint to a point in your storage cell
    28.        
    29.         for (int i = 0; i < _treeDictionary[converted hit point].Count; i++){
    30.             if (returnedValue == null || this tree is closer to the original hitPoint than returnedValue){
    31.                 returnedValue = _treeDictionary[converted hit point][i];
    32.             }
    33.         }
    34.        
    35.         return returnedValue;
    36.     }
    37. }
    38.  

    Obviously that's napkin programming and totally not complete - but it should give you the general idea. I believe of the top of my head that treeInstances are stored between 0 and 1 (so if it's at 0.5,0.5 it is in the middle of your terrain piece) - should be very simple to convert their position to a world position though.

    But basically the idea is to use memory to save runtime performance; make a ton of lists sorted by cells in the 3D world and pick the appropriate cell based on the hit and only cycle through the trees that fall within that cell. It'll probably add a couple of seconds to your startup time for the level, but it should be borderline negligible in terms of runtime performance.

    The key is that you won't iterate through cells to find the right one - you'll just convert the world position to a cell coordinate and you'll automatically have a list of trees there. If you turn up an empty cell - something incorrect happened (or your raycast simply wasn't as close to the tree as you thought).

    To be honest, I would probably make my own struct instead of using Vector2 - I'm not sure what the internals of a Vector3 are so I'm not sure how they would compare to one another. You could make your own IntPair struct that is basically just an x/z coordinate pair to have control over that yourself.

    Also, if for some reason a tree is on a border, it's totally possible that you could raycast very close to the tree but still hit the wrong cell. In that case, you could always check the adjacent cells (so if you hit say, Vector2(25,25) - check Vector2(25,25) (24,25) (24,24) (26,25)...) - depending on the size of your cells it should still be negligible at runtime.

    Just a thought, interesting question though.
     
    Last edited: May 13, 2011
  4. Revontuli

    Revontuli

    Joined:
    May 12, 2011
    Posts:
    29
    Thank you for the quick responses!

    I'll give the cell/list system a try - I was thinking of trying something along those lines, although the version listed above makes a little more sense than my desperate approach, and it wouldn't be the first time I did a convoluted cell-based collision system :) As for not reading the splat texture, I think it's an import setting thing, and I now know that doesn't contain the info I need...

    I realize the terrain system API is still pretty closed, and that the terrain lumps everything into one collider, but its seems like there should at least be the possibility of finding out which tree collider was hit - the capsules are a distinctly different shape than the ground mesh, and at some level the ray that was cast has to know that it hit a capsule instead of the ground mesh, even though both results give "terrain" as the object hit. It would save a few headaches if I could get that info directly and track it back to the tree's prefab. Maybe Unity is storing everything in a different way, but it seems like that sort of direct "digital paper trail" would be there...
     
  5. Antitheory

    Antitheory

    Joined:
    Nov 14, 2010
    Posts:
    549
    Ok so I should put my 2 cents in here because I've already implemented essentially what you need...

    In my game you are a bird and can land in the Terrain trees. When your Raycast hits one it compares this hit.point to a KDTree filled with 100,000 or so possible perch locations (which are generating by first scanning all the TreePrototypes for spots where the bird could land, and then calculating each world-space location for all the trees). Each of these perch locations references the index of the TreeInstance in the treeInstance array which is part of the TerrainData object. Then I swap out that Tree with a GameObject version with a MeshCollider so the bird can actually land in the tree and walk along the branches etc.

    Now this works great for me, but could be overkill depending on what you want.... But I may have a different solution for you. In order to get my AI pathfinding working on the Terrain I needed the tree-colliders to be on a separate layer from the Terrain. To do this I simply didn't generate the tree-colliders for the terrain, and then made a custom script to generate the colliders as distinct objects. If you went this route, it would be pretty easy to just attach the a "tree type" variable to each of these colliders. (it could even just be the name of the gameObject containing the collider).


    Code (csharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using System.Linq;
    4. using System.Text;
    5. using UnityEngine;
    6.  
    7. public class TreeColliderMaker
    8. {
    9.     public static void MakeTreeColliders(Terrain terrain)
    10.     {
    11.         GameObject treeColliders = new GameObject("Tree Colliders");
    12.         treeColliders.transform.parent = terrain.transform;
    13.         TerrainData td = terrain.terrainData;
    14.         TreeInstance[] tis = td.treeInstances;
    15.         TreePrototype[] tps = td.treePrototypes;
    16.         Bounds[] bounds = new Bounds[tps.Length];
    17.         float[] radii = new float[tps.Length];
    18.         //IEnumerable<World.Tree> worldTrees = Globals.instance.worldProperties.terrains.First(wt => wt.name == terrain.name).GetTrees();
    19.         for (int i = 0; i < tps.Length; i++)
    20.         {
    21.             bounds[i] = tps[i].prefab.gameObject.GetComponent<MeshFilter>().sharedMesh.bounds;
    22.             //radii[i] = worldTrees.First(wt => wt.name == tps[i].prefab.name).trunkSize;
    23.         }
    24.         int index = 0;
    25.         foreach (TreeInstance ti in tis)
    26.         {
    27.             GameObject tc = new GameObject("TC" + String.Format("{0:00000}", index));
    28.             CapsuleCollider cc = tc.AddComponent<CapsuleCollider>();
    29.             cc.direction = 1;
    30.             //cc.radius = radii[ti.prototypeIndex] * ti.widthScale;
    31.             cc.height = bounds[ti.prototypeIndex].size.y * ti.heightScale;
    32.             cc.center = Vector3.up * cc.height / 2f;
    33.             tc.transform.parent = treeColliders.transform;
    34.             tc.transform.position = TerrainExtras.WorldCoordinates(terrain, ti.position);
    35.             index++;
    36.         }
    37.     }
    38.  
    39. }
    Note that I have commented out the lines that define the radius of the collider. Because they relate to other classes in my project. You could have your own way of accessing the appropriate radius.

    You'll need this function too, or you could roll your own.

    Code (csharp):
    1.  
    2. public class TerrainExtras
    3. {
    4.  
    5.     //...
    6.  
    7.     public static Vector3 WorldCoordinates(Terrain terrain, Vector3 point)
    8.     {
    9.         Vector3 tdSize = terrain.terrainData.size;
    10.         point.y = terrain.terrainData.GetHeight((int)(point.x * terrain.terrainData.heightmapWidth), (int)(point.z * terrain.terrainData.heightmapHeight));
    11.         point.x *= tdSize.x;
    12.         point.z *= tdSize.z;
    13.         point += terrain.transform.position;
    14.         return point;
    15.     }
    16. }
    Oh and here's the editor script for making the terrain colliders so you don't have to do it at run-time.

    Code (csharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3.  
    4. public class TreeCollidersEditor
    5. {
    6.  
    7.  
    8.  
    9.     [MenuItem("GameObject/Make Tree Colliders")]
    10.     static void MakeTreeCollidersMenuItem()
    11.     {
    12.         foreach (Transform selection in Selection.transforms)
    13.         {
    14.  
    15.             Terrain terrain = selection.GetComponent<Terrain>();
    16.             if (terrain)
    17.                 TreeColliderMaker.MakeTreeColliders(terrain);
    18.  
    19.         }
    20.     }
    21.  
    22.     [MenuItem("GameObject/Make Tree Colliders", true)]
    23.     static bool ValidateMakeTreeCollidersMenuItem()
    24.     {
    25.         if (Selection.activeTransform)
    26.         {
    27.             foreach (Transform selection in Selection.transforms)
    28.             {
    29.                 Terrain terrain = selection.GetComponent<Terrain>();
    30.                 if (terrain)
    31.                     return true;
    32.             }
    33.             return false;
    34.         }
    35.         else
    36.             return false;
    37.     }
    38.  
    39. }
     
    Last edited: May 14, 2011
  6. Revontuli

    Revontuli

    Joined:
    May 12, 2011
    Posts:
    29
    Got the system to work - thank you everyone!

    Antitheory - your code worked great, and while I might try to optimize more to suit my needs, it did the trick pretty much out of the box. I had stumbled upon some posts related to your project earlier and wondered how you ended up doing things. KyleStaves, I may do a more sophisticated subdivision approach along the lines you described if performance starts to lag more. Speed seems to take a little hit but is certainly acceptable given the scope of the dynamic I need, and impressive considering I have added several thousand capsules to the scene!

    I still wonder if you can "short circuit" the terrain collision system to get the info more directly, since this seems to basically be duplicating info Unity presumably has somewhere, but this approach seems to work well enough.

    Again, many thanks for everyone's help!
     
  7. Antitheory

    Antitheory

    Joined:
    Nov 14, 2010
    Posts:
    549
    I was worried about performance when I did it this way too. I wonder if telling Unity that the GameObjects which hold the colliders were static would increase performance at all... I will check this out.

    It doesn't seem to have too much of a negative effect on performance for me (with thousands of trees).
     
  8. Antitheory

    Antitheory

    Joined:
    Nov 14, 2010
    Posts:
    549
    Just thought I would note that this is indeed another feasible method of doing what you are doing. As I mentioned in my first post I implemented this before I started using the discrete collider method (which was actually needed only for a pathfinding feature).

    The key to implementing this search is a KD Tree.

    I use this to find perch-locations from a Tree (no pun intended) of over 100,000 points, with no noticeable performance hit.