Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Ranking vector axes by value?

Discussion in 'Scripting' started by Nanako, Jul 7, 2015.

  1. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    I'm trying to do something which i assumed would be very simple. Given a Vector3, i want to find the biggest axis, and the second biggest.

    Now i'm looking at the complex web of if/else statements i've created, and i'm having to write comments to explain it to myself, this is stupid, there has to be a better way.

    Is it possible to dump the values into a dictionary (axisname, value) and then sort it by value, and extract the first two elements? otherwise, how would you accomplish this task?
     
  2. JamesLeeNZ

    JamesLeeNZ

    Joined:
    Nov 15, 2011
    Posts:
    5,616
    This might work... you're right, it is a giant cluster of if statements. Might be a better way, but putting it into a dictionary/list/etc is overkill.

    Code (csharp):
    1.  
    2.  
    3. Vector2 ReturnLargestAxis(Vector3 axis)
    4. {
    5.   Vector2 Largest;
    6.  
    7.   float xValue = Mathf.abs(axis.x);
    8.   float yValue = Mathf.abs(axis.y);
    9.   float zValue = Mathf.abs(axis.z);
    10.  
    11.  if(xValue > yValue && xValue > zValue)
    12.  {
    13.    Largest.x = axis.x;
    14.    if(yValue > zValue)
    15.      Largest.y = axis.y;
    16.    else Largest.y = axis.z;
    17.  }
    18.  else if(yValue > xValue && yValue > zValue)
    19.  {
    20.    Largest.x = axis.y;
    21.    if(xValue > zValue)  
    22.      Largest.y = axis.x;
    23.    else Largest.y = axis.z;
    24.  }
    25. else if(zValue > xValue && zValue > yValue)
    26. {
    27.    Largest.x = axis.z;
    28.    if(xValue > yValue)  
    29.      Largest.y = axis.x;
    30.    else Largest.y = axis.y;
    31. }
    32. else Debug.LogWarning("Sorry, I couldnt figure it out... " + axis.ToString());
    33. return Largest;
    34.  
    35. }
    36.  
    37.  
     
  3. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    its a little more complex than i had. But i need to track which axes, as well as their values, so that's gonna be a ton more lines in there.

    is the dictionary idea workable? i've never tried sorting or accesssing dictionaries by index, can that even be done?
     
  4. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    You could use a SortedDictionary and call OrderBy with a lambda that sorts on the value. You could also do this with an array of floats and corresponding array of strings. However, in both cases you are left with some allocation/deallocation and an unusual return value: some kind of double/string object array.

    I think it might be better to simply set the smaller of the three axis to 0 and return a Vector3. This way you implicitly know which value belongs to which axis and you could use it normally..

    Regardless how you do it, it would be smart to add this as an extension method to encapsulate the weird logic. I think you could come up with a dozen implementations for this. Here is just one:
    Code (csharp):
    1. public static class Vector3Extensions
    2. {
    3.   public static Vector3 ZeroSmallestAxis(this Vector3 vector)
    4.   {
    5.     var min = Math.Min(vector.x, vector.y, vector.z);
    6.     var zeroed = new Vector3(vector.x > min ? vector.x : 0, vector.y > min ? vector.y : 0, vector.z > min ? vector.z : 0);
    7.     return zeroed;
    8.   }
    9. }
    Ultimately, what would be most useful depends on what your next step with the object is. What do you need the two largest vectors for?
     
    Last edited: Jul 7, 2015
  5. JamesLeeNZ

    JamesLeeNZ

    Joined:
    Nov 15, 2011
    Posts:
    5,616
    nope.
     
  6. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,514
    If that's so, you're going to need to store the axis information in some manner. I personally have a Axis enum already defined for that:

    https://github.com/lordofduct/spacepuppy-unity-framework/blob/master/SpacepuppyBase/Enums.cs

    Code (csharp):
    1.  
    2.     /// <summary>
    3.     /// Describe an axis in cartesian coordinates, Useful for components that need to serialize which axis to use in some fashion.
    4.     /// </summary>
    5.     public enum CartesianAxis
    6.     {
    7.         X = 0,
    8.         Y = 1,
    9.         Z = 2
    10.     }
    11.  
    Now you also need a format for the data to come back in. You need the axis paired with its float value. We could do this as a the dictionary keyvalue pair, but that's a bit fat for what we effectively want. How about we just define a simple struct to store this info:

    Code (csharp):
    1.  
    2.     public struct AxisInfo
    3.     {
    4.         public CartesianAxis Axis;
    5.         public float Position;
    6.    
    7.         public AxisInfo(CartesianAxis axis, float pos)
    8.         {
    9.             this.Axis = axis;
    10.             this.Position = pos;
    11.         }
    12.    
    13.     }
    14.  
    Next all we have to do is construct this data and sort it:

    Code (csharp):
    1.  
    2. Vector3 v = *some vector*;
    3. var arr = new AxisInfo[] {
    4.                 new AxisInfo(CartesianAxis.X, v.x),
    5.                 new AxisInfo(CartesianAxis.Y, v.y),
    6.                 new AxisInfo(CartesianAxis.Z, v.z) };
    7. System.Array.Sort(arr, (a,b) => -a.Position.CompareTo(b.Position) );
    8. //CompareTo would have sorted in ascending, by making the result negative, we're sorting descending
    9.  
     
    Last edited: Jul 7, 2015
    eisenpony likes this.
  7. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    So i'm trying out eisenpony's suggestion, the sortedDictionary

    But how does this work exactly? The reference page doesn't specify if it's sorted in ascending or descending order (i assume it's autosorted as values are added?

    I've set the value of the axis as the key, and an integer (0/1/2 = x/y/z) as the value, since i want to sort on the float

    SortedDictionary<float, int> axes = new SortedDictionary<float, int> { { size.x, ObjectPart.X_AXIS }, { size.y, ObjectPart.Y_AXIS }, { size.z, ObjectPart.Z_AXIS } };

    How do i retrieve the largest entry from that dictionary now?
     
  8. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    Ok this isn't going to work, SortedDictionary (and SortedList) require unique keys. Why? that sucks.

    lets see what other solutions i could use...
     
  9. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    Can you provide more details about your use case? I think lordofduct's solution is very good for generic use and I like my solution if you need to continue operating with vectors.
     
  10. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    Hi guys, i'm back. I eventually found a solution i thought would work (im still fairly sure it should) but doesn't. Can you help?

    I decided to just put them in a list of floats, and then sort that list. Here's a relevant code snippet

    Code (CSharp):
    1.  
    2. Vector3 size = input.collider.bounds.size;
    3. List<float> axes = new List<float>() { size.x, size.y, size.z };
    4.             axes.Sort();
    5.            
    6.            
    7.  
    8.             input.grabAxis = GetAxisVector(axes[axes.Count-1], size);
    9.             input.upAxis = GetAxisVector(axes[axes.Count-2], size);
    10.             input.grabRadius = GetGrabRadius(axes);
    11.            
    12.             Debug.Log("Up: " + TextTools.AxisName(input.upAxis) + " Fwd:" + TextTools.AxisName(input.grabAxis));
    13.  
    What this should do, is make my grabAxis be the longest axis of the object, and upAxis be the secondlongest axis. GrabRadius isn't relevant there.

    Two helper functions are used:
    GetAxisVector takes a float value, and the original vector it came from, and returns a normalised vector with other axes zeroed.

    Code (CSharp):
    1. private Vector3 GetAxisVector(float value, Vector3 size)
    2.             {
    3.                 if (value == size.x)return new Vector3(1, 0, 0);
    4.                 if (value == size.y) return new Vector3(0,1,0);
    5.                 return new Vector3(0,0,1);
    6.             }
    TextTools.AxisName is just for debugging. It takes a vector produced by the above, and returns a single character which is the name of that axis

    Code (CSharp):
    1.     public static string AxisName(Vector3 input)
    2.     {
    3.        
    4.         if (input == xAxis)
    5.         {
    6.             return "X";
    7.         }
    8.         if (input == yAxis)
    9.         {
    10.             return "Y";
    11.         }
    12.         if (input == zAxis)
    13.         {
    14.             return "Z";
    15.         }
    16.  
    17.         return "!";
    18.     }

    Anyways, there are as far as i can see, two problems.
    First is that it's just not working. The Up and grab axes are different every time i rotate the object and re-run this, when they should be consistent values, since they're derived from the size of the object.

    Secondly, is that I'm not sure how, or if it will produce consistent results, in a case where two or all of the axes have the same float value. In that case, i'd ideally like it to consistently rank them by letter order, (eg, Z X Y, if z is smallest but X and Y are the same)
     
  11. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    I'm a little stumped - I think your code should work. Have you verified the input.collider.bounds.size is not changing? Is it possible the code is working correctly but you are using the resulting vectors in the wrong coordinate system i.e., world instead of local or vice versa?

    I was going to suggest adding some more Debug.Log lines but I sincerely think you will be better served learning to use the debugger. Are you running the Unity Editor on a PC? Let us know if you want help setting up the debugger. It will change your life..

    As for your second issue... The documentation on List.Sort() says it is unstable. In other words, there is on guarantee on the order of items with equal values. To work around that, you would need to implement your own IComparer which takes into account the axis name. However, in order to do that, you would need to keep the axis name along side the axis value in some kind of meta object - much like lodofduct did with his AxisInfo class.
     
  12. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    This sounds interesting, count me in. yes please, i've never heard of it
     
  13. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    Ok i think ive found my problem. Collider.Bounds.size returns different values depending on how the object is rotated.

    The object i'm using for testing is a little stick. It's just a cube with the scale set to <0.1, 0.1, 1>. So its long and thin.

    Rotating it arbitrarily and then resetting the script:
    Partsize (0.5, 0.1, 1.0)
    Partsize (1.0, 0.1, 0.1)
    Partsize (0.8, 0.1, 0.7)
    Partsize (1.0, 0.1, 0.2)

    Four random samples
    My guess is it's basing measurements on world axes, and measuring a world-aligned bounding box around the object.
    So this information is not meaningful.

    How can i make it not rotation dependant?
    Just grabbing the object's localscale is not useful. I need to know the actual dimensions of the object in world units.
     
  14. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    The debugger is built into the MonoDevelop IDE. It lets you pause the execution of code so you can watch the computer complete activities one line at a time. You can also inspect the values of variables and parameters while the execution is paused, so you don't have to rely on Debug.Log to see that runtime values of things.

    This is how you use it:

    Attach the Debugger to the Unity Editor. Click the Play button at the to left corner to bring up a list of applications you can attach to. Select the Unity Editor and then press Attach.
    upload_2015-7-14_14-50-45.png

    Within MonoDevelop, create a breakpoint on the line you would like to pause at. Either click the gray bar beside the code or place your caret on this line and press F9. This line will highlight in red and a small red circle will appear in the gray bar.

    upload_2015-7-14_14-51-34.png

    Go back to the Unity Editor and run your game. When the code gets to the breakpoint, the game will freeze and MonoDevelop will blink or otherwise take focus. Your line of code is now highlighted in yellow, indicating it is next to be executed.

    upload_2015-7-14_14-54-35.png

    From here, you can do a couple of things:
    Use the "Locals" inspector at the bottom to view the state of all your variables that are in scope.
    Use the "Watch" inspector to view the state of other variables or results of method calls.
    Use the Immediate console to execute arbitrary commands in code and see what the result is in real time.
    Use the Call Stack inspector to see the entire stack. This is basically the chain of method calls that got you to where you are. You can double click one of the rows to move to that level of the stack and inspect the variables. However, execution will continue from the top of the stack when you resume.

    When you are ready to move on, you can use the execution buttons at the top
    upload_2015-7-14_14-58-33.png

    The Play button is Continue Execution (F5) and will resume normal execution
    Next is Step Over (F10), which allows you to execute the currently highlighted line of code without going into any methods. Basically: finish this line of code, regardless how complex that is and move to the next
    The next button is Step Into (F11). This will move you into the next method to be executed. So if the current line has a method call in it, you will be taken to the first line of that method.
    The last button is Step Out Of (Shift F11). This moves you out of the current method, back to the line that called it in the first place. In other words, this moves one level down the stack.
     
    rhdlamb and chelnok like this.
  15. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    Ok this is a problem. I stopped using monodevelop because of useability issues, and switched to Visual Studio Community 2013. It's a move i don't regret, it's been better all around.

    i'd assume this stuff is built in there too?
     
  16. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    Do you already have the Visual Studio 2013 Tools for Unity? If so, you should see an Attach to Unity Debugger under the Debug menu in Visual Studio.



    Once you are attached, the same commands apply:
    Create breakpoints with F9.
    Continue Execution with F5
    F10 to step over
    F11 to step in
    Shift F11 to step out
     
  17. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    I think you will need to use collider.attachedRigidbody.rotation to convert your collider.bounds.size into local space. I'm afraid that math is a little above me. Hopefully one of the other guys can help.
     
  18. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    I decided to take a stab at this anyways..
    Does this return what you need?
    Code (csharp):
    1. Quaternion.Inverse(collider.attachedRigidbody.rotation) * collider.bounds.size
     
  19. Nanako

    Nanako

    Joined:
    Sep 24, 2014
    Posts:
    1,047
    Nope, it does not. Apparently it's not that simple
    I already tried TransformPoint and InverseTransformPoint, i believe the latter is functionally identical to your attempt.

    Since this seems to be a more complex problem, i've made a seperate thread for it;
    http://forum.unity3d.com/threads/how-do-you-find-the-size-of-a-local-bounding-box.341007/