Search Unity

Average quaternions?

Discussion in 'Scripting' started by jarden, Apr 25, 2011.

  1. jarden

    jarden

    Joined:
    Mar 29, 2011
    Posts:
    74
    I dug this code up from 2006 on this forum:
    Code (csharp):
    1. var Quaternion[] quats;
    2.  
    3. Quaternion avg = Quaternion(0,0,0,0);
    4. for (var q : Quaternion in quats)
    5. {
    6.     if (Quaternion.Dot (q, avg) > 0)
    7.     {
    8.        avg.x += q.x;
    9.        avg.y += q.y ;
    10.        avg.z += q.z;
    11.        avg.w += q.w;
    12.     }
    13.     else
    14.     {
    15.        avg.x += -q.x;
    16.        avg.y += -q.y;
    17.        avg.z += -q.z;
    18.        avg.w += -q.w;
    19.     }
    20. }
    21.  
    22. var mag = Mathf.Sqrt(avg.x* avg.x + avg.y* avg.y + avg.z * avg.z + avg.w * avg.w);
    23.  
    24. if (mag > 0.0001)
    25. {
    26.    avg.x /= mag;
    27.    avg.y /= mag;
    28.    avg.z /= mag;
    29.    avg.w /= mag;
    30. }
    31. else
    32.    avg = quats[0];
    But I think the author said theres an easier way with slerp and multiplication. Anyone know it or some easier code?
     
    Last edited: Apr 25, 2011
  2. Eric5h5

    Eric5h5

    Volunteer Moderator Moderator

    Joined:
    Jul 19, 2006
    Posts:
    32,401
    var average = Quaternion.Lerp(quaternion1, quaternion2, .5); maybe?

    --Eric
     
  3. jarden

    jarden

    Joined:
    Mar 29, 2011
    Posts:
    74
    Whats weird about that is it gives the same value no matter what .5 is. 0, 1, all the same return.
     
  4. Dreamora

    Dreamora

    Joined:
    Apr 5, 2008
    Posts:
    26,601
    then it was the same already at start if the lerp / slerp one is the same value as one of the inputs
     
  5. jarden

    jarden

    Joined:
    Mar 29, 2011
    Posts:
    74
    Thanks all:). For reference I did get a good average with this code:
    Code (csharp):
    1.     var test : Quaternion = Quaternion.identity;
    2.     test = Quaternion.Lerp(first.transform.rotation, second.transform.rotation, 0.5);
     
    Divkaran_NIIT likes this.
  6. Eric5h5

    Eric5h5

    Volunteer Moderator Moderator

    Joined:
    Jul 19, 2006
    Posts:
    32,401
    That's basically what I posted. Replace "quaternion1" with "first.transform.rotation" etc.; you don't need to define variables on separate lines like that.

    --Eric
     
  7. Elecman

    Elecman

    Joined:
    May 5, 2011
    Posts:
    1,374
    Lerp or Slerp is only accurate if you have only two quaternions. If you have more than two, that method will give you a different result if you execute it in a different order.

    Here is the mathematically correct way of averaging more than two quaternions:

    Wiki:
    http://wiki.unity3d.com/index.php/Averaging_Quaternions_and_Vectors

    Code (csharp):
    1.  
    2. //Global variable which holds the amount of rotations which
    3. //need to be averaged.
    4. int addAmount = 0;
    5.  
    6. //Global variable which represents the additive quaternion
    7. Quaternion addedRotation = Quaternion.identity;
    8.  
    9. //The averaged rotational value
    10. Quaternion averageRotation;
    11.  
    12. //multipleRotations is an array which holds all the quaternions
    13. //which need to be averaged.
    14. Quaternion[] multipleRotations new Quaternion[totalAmount];
    15.  
    16. //Loop through all the rotational values.
    17. foreach(Quaternion singleRotation in multipleRotations){
    18.  
    19.     //Temporary values
    20.     float w;
    21.     float x;
    22.     float y;
    23.     float z;
    24.  
    25.     //Amount of separate rotational values so far
    26.     addAmount++;
    27.  
    28.     float addDet = 1.0f / (float)addAmount;
    29.     addedRotation.w += singleRotation.w;
    30.     w = addedRotation.w * addDet;
    31.     addedRotation.x += singleRotation.x;
    32.     x = addedRotation.x * addDet;
    33.     addedRotation.y += singleRotation.y;
    34.     y = addedRotation.y * addDet;
    35.     addedRotation.z += singleRotation.z;
    36.     z = raddedRotation.z * addDet;
    37.  
    38.     //Normalize. Note: experiment to see whether you
    39.     //can skip this step.
    40.     float D = 1.0f / (w*w + x*x + y*y + z*z);
    41.     w *= D;
    42.     x *= D;
    43.     y *= D;
    44.     z *= D;
    45.  
    46.     //The result is valid right away, without
    47.     //first going through the entire array.
    48.     averageRotation = new Quaternion(x, y, z, w);
    49. }
    50.  
    Edit:
    Before you average a quaternion, you have to check whether it has x y z and w reversed compared to the rest. The rotation of q and -q is the same, even though the sign of x y z and w is reversed. However, quaternions q and -q cannot be averaged right away and have to have its sign reversed first.

    The way to solve it is to take the first quaternion in the array and then taking a dot product with each other quaternion. If the result is negative then the quaternion must have x y z and w reversed before it is averaged with the rest of it.

    Like this:
    Code (csharp):
    1.  
    2. //Get an average (mean) from more than two quaternions (with two, slerp would be used).
    3. //Note: this only works if all the quaternions are relatively close together.
    4. //Usage:
    5. //-Cumulative is an external Vector4 which holds all the added x y z and w components.
    6. //-newRotation is the next rotation to be added to the average pool
    7. //-firstRotation is the first quaternion of the array to be averaged
    8. //-addAmount holds the total amount of quaternions which are currently added
    9. //This function returns the current average quaternion
    10. public static Quaternion AverageQuaternion(ref Vector4 cumulative, Quaternion newRotation, Quaternion firstRotation, int addAmount){
    11.  
    12.     float w = 0.0f;
    13.     float x = 0.0f;
    14.     float y = 0.0f;
    15.     float z = 0.0f;
    16.  
    17.     //Before we add the new rotation to the average (mean), we have to check whether the quaternion has to be inverted. Because
    18.     //q and -q are the same rotation, but cannot be averaged, we have to make sure they are all the same.
    19.     if(!Math3d.AreQuaternionsClose(newRotation, firstRotation)){
    20.  
    21.         newRotation = Math3d.InverseSignQuaternion(newRotation);  
    22.     }
    23.  
    24.     //Average the values
    25.     float addDet = 1f/(float)addAmount;
    26.     cumulative.w += newRotation.w;
    27.     w = cumulative.w * addDet;
    28.     cumulative.x += newRotation.x;
    29.     x = cumulative.x * addDet;
    30.     cumulative.y += newRotation.y;
    31.     y = cumulative.y * addDet;
    32.     cumulative.z += newRotation.z;
    33.     z = cumulative.z * addDet;      
    34.  
    35.     //note: if speed is an issue, you can skip the normalization step
    36.     return NormalizeQuaternion(x, y, z, w);
    37. }
    38.  
    39. public static Quaternion NormalizeQuaternion(float x, float y, float z, float w){
    40.  
    41.     float lengthD = 1.0f / (w*w + x*x + y*y + z*z);
    42.     w *= lengthD;
    43.     x *= lengthD;
    44.     y *= lengthD;
    45.     z *= lengthD;
    46.  
    47.     return new Quaternion(x, y, z, w);
    48. }
    49.  
    50. //Changes the sign of the quaternion components. This is not the same as the inverse.
    51. public static Quaternion InverseSignQuaternion(Quaternion q){
    52.  
    53.     return new Quaternion(-q.x, -q.y, -q.z, -q.w);
    54. }
    55.  
    56. //Returns true if the two input quaternions are close to each other. This can
    57. //be used to check whether or not one of two quaternions which are supposed to
    58. //be very similar but has its component signs reversed (q has the same rotation as
    59. //-q)
    60. public static bool AreQuaternionsClose(Quaternion q1, Quaternion q2){
    61.  
    62.     float dot = Quaternion.Dot(q1, q2);
    63.  
    64.     if(dot < 0.0f){
    65.  
    66.         return false;                  
    67.     }
    68.  
    69.     else{
    70.  
    71.         return true;
    72.     }
    73. }
    74.  
     
    Last edited: Apr 19, 2015
  8. konsnos

    konsnos

    Joined:
    Feb 13, 2012
    Posts:
    121
    Sorry to bump this old thread but it's the only one which had the answer I seek.

    Thanks for the solution, but what do you mean by that?
     
  9. Elecman

    Elecman

    Joined:
    May 5, 2011
    Posts:
    1,374
    The rotations from all quaternions should not be too much different.

    Let's say you have a bunch of quaternions, each pointing at a random star in the sky. Averaging those will fail. They are too different.

    Now imagine you have a bunch of quaternions, each pointing at the same star, but made with different measurement equipment in your yard. Those are probably close enough.

    Of course a quaternion doesn't just point at something, but have a rotation as well. Same logic for the rotation part. It shouldn't differ too much.

    I used that function to average a bunch of world space pose estimates from an AR marker. The input estimates surely have some jitter, but the average output is accurate.

    Some more information and links scientific papers here, if you are interested in averaging totally random quaternions:
    http://stackoverflow.com/questions/12374087/average-of-multiple-quaternions
     
  10. hgdebarba

    hgdebarba

    Joined:
    Mar 12, 2015
    Posts:
    5

    Adding to that, mathematically correct is a quite strong term :)
    but according to this it could yield a close approximation as long as the difference in the angles of rotation is small.
    But INSTEAD of
    float D = 1.0f / (w*w + x*x + y*y + z*z);
    you should do
    float D = 1.0f / Mathf.sqrt(w*w + x*x + y*y + z*z);
    (but mind that Unity will normalize it for you, so this can be omitted anyway)
    AND
    you should initialize addedRotation to the first value of the array or set it to 0, 0, 0, 0 (otherwise the identity rotation will be part of your average!)

    The code bellow might work better, and q -q should not be a problem (I am not sure about efficiency though)

    Code (csharp):
    1.  
    2. // assuming qArray.Length > 1
    3. Quaternion AverageQuaternion(Quaternion [] qArray){
    4.     Quaternion qAvg = qArray[0];
    5.     float weight;
    6.     for (int i = 1 ; i < qArray.Length; i++)
    7.     {
    8.         weight = 1.0f / (float)(i+1);
    9.         qAvg = Quaternion.Slerp(qAvg, qArray[i], weight);
    10.     }
    11.     return qAvg;
    12. }
    13.  
     
    BilalAsadi and Deleted User like this.
  11. Elecman

    Elecman

    Joined:
    May 5, 2011
    Posts:
    1,374
    Thanks for the feedback :)
     
  12. tinyant

    tinyant

    Joined:
    Aug 28, 2015
    Posts:
    127
    how to get the right result quaternion when giving different quaternion with weights?

    Q1 0.1
    Q2 0.3
    Q3 0.4
    Q4 0.2

    Get the result Q?
     
  13. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,532
    Slerp(identity, q1, w1) * Slerp(idenity, q2, w2) * Slerp(idenity, q3, w3) * Slerp(identity, q4, w4)

    ...

    That'd be my first attempt
     
    koirat likes this.
  14. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,776
    Calculating average, not weight based.

    This worked for me, based on lordofduct response.

    Code (CSharp):
    1. Quaternion q_average = Quaternion.identity ;
    2.  
    3. List <Quaternion>  qList = new List <Quaternion> ()
    4.  
    5. qList.Add (Q0) ; // add some first quaternion
    6.  
    7. qList.Add (Q1) ; // add some second quaternion
    8.  
    9. qList.Add (Q2) ; // add some next quaternion
    10.  
    11. // ad some more quaternions
    12.  
    13.  
    14. float averageWeight = 1f / siblings.Count ;
    15.  
    16. for ( int i = 0; i < qList.Count; i ++ )
    17. {
    18.     Quaternion q = qList [ i ] ;
    19.  
    20.     // based on [URL='https://forum.unity.com/members/lordofduct.66428/']lordofduct[/URL] response
    21.     q_average *= Quaternion.Slerp ( Quaternion.identity, q, f_averageWeight ) ;
    22. }
    23.    
    24. // output rotation - attach to some object, or whatever
    25. this.transform.rotation = q_average ;
    26.  
     
    Last edited: Apr 15, 2018
  15. Elecman

    Elecman

    Joined:
    May 5, 2011
    Posts:
    1,374
    Thanks!
     
  16. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,998
    In some cases (when you have one look-from-point and lots of lookAt points) it might be simpler to do whatever math on the points, then find one quaternion. You lose local-z spin, but you probably didn't want it anyway.
     
  17. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    Chaining Slerps to together isn't quite the proper way to go about it.

    I would simply convert the quaternions into look directions (forward and up pairs), add up the directions, normalize them, then convert the result pairs back to a quaternion via LookRotation(). and you got your average (which might be a zero length, but hey that actually can be a valid answer).

    Why? Order matters when you add Quaternions together. Unlike normal numbers, Quaternions lack the Communtative property. If you put the same group of Quaternions into that sort of quaternion-only averaging function, with varying orders, you can actually get different averaged quaternion results.
     
  18. DavidSWu

    DavidSWu

    Joined:
    Jun 20, 2016
    Posts:
    183
    It depends on what you are trying to solve for, but I would expect that you will get a reasonable result by first converting the quaternions into a linear space before applying a weight.
    The easiest way to convert to a linear, mostly orthogonal space is to take the derivative.
    Try averaging the weighted logarithms, and the use the exponent of the result.
    This effectively converts every quaternion into an angular velocity and takes resulting average velocity.
    The angular velocities are the velocities that would result in the full rotation after 1s.

    Hope that helps.

    I can provide code if desired.

    David
     
  19. Elecman

    Elecman

    Joined:
    May 5, 2011
    Posts:
    1,374
    Code is always welcome.
     
    DavidSWu likes this.
  20. DavidSWu

    DavidSWu

    Joined:
    Jun 20, 2016
    Posts:
    183
    This has its shortcomings as mentioned, as the problem of averaging rotations is ill posed.
    But it will generally work well for rotations that are not too far apart and for rotations that do not have inherent constraints.
    I.e. where you want to minimize the average rotation between the result and the inputs.
    If you have constraints (like minimize roll) you can do better.

    Code (CSharp):
    1.  
    2. [MethodImpl(MethodImplOptions.AggressiveInlining)]
    3.         internal static Vector3 ToAngularVelocity( this Quaternion q )
    4.         {
    5.             if ( abs(q.w) > 1023.5f / 1024.0f)
    6.                 return new Vector3();
    7.                 var angle = acos( abs(q.w) );
    8.                 var gain = Sign(q.w)*2.0f * angle / Sin(angle);
    9.  
    10.             return new Vector3(q.x * gain, q.y * gain, q.z * gain);
    11.         }
    12.  
    13.  
    14.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    15.         internal static Quaternion FromAngularVelocity( this Vector3 w )
    16.         {
    17.             var mag = w.magnitude;
    18.             if (mag <= 0)
    19.                 return Quaternion.identity;
    20.             var cs = cos(mag * 0.5f);
    21.             var siGain = sin(mag * 0.5f) / mag;
    22.             return new Quaternion(w.x * siGain, w.y * siGain, w.z * siGain, cs);
    23.  
    24.         }
    25.  
    26.         internal static Quaternion Average(Quaternion[] source)
    27.         {
    28.             Assert.IsFalse(source.IsNullOrEmpty());
    29.             Vector3 result = new Vector3();
    30.             foreach (var q in source)
    31.             {
    32.                 result += q.ToAngularVelocity();
    33.             }
    34.  
    35.             return (result / source.Length).FromAngularVelocity();
    36.         }
    37.  
    Hope that helps!

    David
     
    chadfranklin47 likes this.
  21. Elecman

    Elecman

    Joined:
    May 5, 2011
    Posts:
    1,374
    Thanks!
     
  22. DavidSWu

    DavidSWu

    Joined:
    Jun 20, 2016
    Posts:
    183
    Actually you can get a better result if performance is not a concern byt iterating to find the minimum max error:


    Code (CSharp):
    1.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
    2.         internal static Vector3 ToAngularVelocity( this Quaternion q )
    3.         {
    4.             if ( abs(q.w) > 1023.5f / 1024.0f)
    5.                 return new Vector3();
    6.                 var angle = acos( abs(q.w) );
    7.                 var gain = Sign(q.w)*2.0f * angle / Sin(angle);
    8.             return new Vector3(q.x * gain, q.y * gain, q.z * gain);
    9.         }
    10.         [MethodImpl(MethodImplOptions.AggressiveInlining)]
    11.         internal static Quaternion FromAngularVelocity( this Vector3 w )
    12.         {
    13.             var mag = w.magnitude;
    14.             if (mag <= 0)
    15.                 return Quaternion.identity;
    16.             var cs = cos(mag * 0.5f);
    17.             var siGain = sin(mag * 0.5f) / mag;
    18.             return new Quaternion(w.x * siGain, w.y * siGain, w.z * siGain, cs);
    19.         }
    20.         internal static Quaternion Average(this Quaternion refence, Quaternion[] source)
    21.         {
    22.  
    23.             var refernceInverse = refence.Inverse();
    24.             Assert.IsFalse(source.IsNullOrEmpty());
    25.             Vector3 result = new Vector3();
    26.             foreach (var q in source)
    27.             {
    28.                 result += (refernceInverse*q).ToAngularVelocity();
    29.             }
    30.             return refence*((result / source.Length).FromAngularVelocity());
    31.         }
    32.          internal static Quaternion Average(Quaternion[] source)
    33.         {
    34.             Assert.IsFalse(source.IsNullOrEmpty());
    35.             Vector3 result = new Vector3();
    36.             foreach (var q in source)
    37.             {
    38.                 result += q.ToAngularVelocity();
    39.             }
    40.             return (result / source.Length).FromAngularVelocity();
    41.         }
    42.          internal static Quaternion Average(Quaternion[] source, int iterations)
    43.         {
    44.             Assert.IsFalse(source.IsNullOrEmpty());
    45.             var reference = Quaternion.identity;
    46.             for(int i = 0;i < iterations;i++)
    47.             {
    48.                 reference = Average(reference,source);
    49.  
    50.             }
    51.             return reference;
    52.  
    53.         }
     
    domportera and chadfranklin47 like this.
  23. chadfranklin47

    chadfranklin47

    Joined:
    Aug 11, 2015
    Posts:
    229
    Awesome stuff. Is there any way to add weights to each rotation?

    Edit:
    Multiplying the ToAngularVelocity() by the weight (0.0 - 1.0) and returning result.FromAngularVelocity() seems to work fine.
     
    Last edited: Apr 19, 2021
  24. Wiedergaenger

    Wiedergaenger

    Joined:
    Mar 14, 2019
    Posts:
    2
    This is my solution for averaging weighted rotations (quaternions).
    Base is a class for weighted rotations with a weight between 0 and 1.
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5.  
    6. public class WeightedRotation
    7. {
    8.     public Quaternion Rotation;
    9.     public float RotWeight; // between 0 und 1
    10.  
    11.     public WeightedRotation(Quaternion rotation, float weigth)
    12.     {
    13.         if (weigth > 1 || weigth < 0)
    14.         {
    15.             Debug.LogError("weight out of bound");
    16.         }
    17.         else
    18.         {
    19.             Rotation = rotation;
    20.             RotWeight = weigth;
    21.         }
    22.     }
    23. }
    24.     public class AverageRotations : MonoBehaviour
    25.     {
    26.     public List<WeightedRotation> Rotations;
    27.    
    28.         private Quaternion AverageRotation()
    29.         {
    30.  
    31.  
    32.             if (Rotations.Count > 0)
    33.             {
    34.                 Quaternion q_average = Rotations[0].Rotation;
    35.  
    36.                 for (int i = 1; i < Rotations.Count; i++)
    37.                 {
    38.                     float weight = 1f / (i + 1) * Rotations[i].RotWeight;
    39.                     q_average = Quaternion.Slerp(q_average, Rotations[i].Rotation, weight);
    40.  
    41.                 }
    42.  
    43.                 Rotations.Clear();
    44.                 return q_average;
    45.             }
    46.             Rotations.Clear();
    47.             return transform.rotation;
    48.         }
    49.     }
     
  25. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    Like I mentioned before chaining Slerps is not how you average several Quaternions. Slerp is only a valid average between 2 quaternions. Think of it like this.

    Whats the average of [1,2,3,4,5]?
    (1+2+3+4+5)/5 = 15/5 = 3. The average is 3.
    But chaining slerps is basically ((((1+2)/2 + 3)/2 + 4)/2 + 5)/2 = 4.0625. these are not the same! It favors the last elements. The earlier Quaternions become exponentially insignificant the more members you try to calculate this way. It ONLY works if there are two elements! including more will give an incorrectly skewed result.

    Plus, due to the dimension of math Quaternions reside in, they lack the commutative property, meaning order matters... quite a lot. Even just adding the 2 same rotations will give completely different answers based on which one is added to which.

    Take your code for example. Say you take 3 different rotations, give them whatever weights you like and you attempt to calculate that result. Remember that result. now try the same thing again. take the exact same 3 rotations, give them the exact same 3 weights. but you shuffle the order in the list which you used to calculate them. unless you did something like choose the exact same rotation/weights 3 times the 2nd result will be different from the 1st and will play favorites to the later quaternions more.

    To average Quaternions correctly
    By far the simplest way that's actually correct is to convert each quaternion into a forward and up look direction. Doing so pulls the Quaternion out of the higher dimension of math it resides in.
    - Add all forwards together, add all ups together. You can even include your weights if you wanted its trivial for vectors.
    - Then normalize the forwardSum, normalize the upSum.
    - create a new quaternion using that normalizedForwardSum and normalizedUpSum.

    That quaternion is the answer, its the proper averaged quaternion out of all quaternions, weighted or not, and regardless of order, that you've included.
     
    halley and orionsyndrome like this.
  26. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,000
    Well you missed that they used a weight of
    1f / (i + 1)
    . So your example is wrong. The chaining is NOT
    ((((1+2)/2 + 3)/2 + 4)/2 + 5)/2
    but the weight gets progressively smaller and sticks closer to the average value. So the first step would do
    (1*(1/2) + 2*(1/2))
    . The second step would take the result of the first and does this:
    ((1*(1/2) + 2*(1/2))*(2/3) + 3*(1/3)) 
    the next step is
    (((1*(1/2) + 2*(1/2))*(2/3) + 3*(1/3))*(3/4) + 4*(1/4)) 
    and the last one
    (((1*(1/2) + 2*(1/2))*(2/3) + 3*(1/3))*(3/4) + 4*(1/4))*(4/5) + 5*(1/5) 
    which gives you the value 3 as well.

    So the lerp / slerp is not used to simply pick the midpoint but a weighted factor so it adds up correctly.

    Though while this does work mathematically, I'm sure it's not that numerically stable and also not terribly efficient either.
     
  27. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    It may have been a poor example, but my main point actually still stands. To be clear, I didn't have a problem with the pre-weights, I have a problem with the slerps. Even when taking the
    1f / (i + 1)
    pre-weights into account, it doesn't fix the pitfalls Quaternion's Slerp itself is susceptible to.

    Yes, the pre weights correct the typical imbalance for a set of numbers chained this way, and generally it should give an accurate average. Of the formulas posted on this thread that try to solve using a Slerp-chaining technique its among the most accurate implementations.

    but it's not 100% accurate. It doesn't solve the other problems inherent specifically to Quaternions. Using this formula I can still easily think up sets of Quaternions which would calculate to a completely incorrect average, where a correct average is still possible to acquire.

    Lets say you have 3 quaternions A,B,and C. Each with forward vectors Vector3.Left, Vector3.Right, and Vector3.Down. What is the Quaternion result D of Slerp(A,B)? Where should the result point to? Its basically undefined since Slerp can't give a valid answer between rotations that are polar opposites. and now what happens if you try to find Quaternion E from Slerp(D,C)? you end up with an answer that can't be guaranteed to be correct.

    However if you take those same 3 rotations (A,B,C) and change the order in which you slerp them:
    D = Slerp(A,C)
    E = Slerp(D,B)
    you won't get undefined answers. it'll even be the correct average if you include on the pre-weights. But this presents a problem. This means the formula now has to validate each slerp and compensate when it returns undefines. suddenly the formula is no longer so simple if it wants to be consistently correct.

    This is why I push for calculating via look directions so much, it bypasses this issue entirely, its conceptually intuitive to follow the logic, and doesn't require any pre-weighting.
     
  28. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,532
    Looks like I really should have not shot in the dark 5 years ago.
     
    Kurt-Dekker likes this.
  29. Rumbleball

    Rumbleball

    Joined:
    Feb 25, 2022
    Posts:
    6
    Put the code of @Elecman into a out of the box usable class

    Code (CSharp):
    1. public static partial class Math
    2.     {
    3.         public static Quaternion AverageQuaternion(Quaternion[] quats)
    4.         {
    5.             if(quats.Length == 0)
    6.             {
    7.                 return Quaternion.identity;
    8.             }
    9.  
    10.             Vector4 cumulative = new Vector4(0, 0, 0, 0);
    11.  
    12.       foreach(Quaternion quat in quats)
    13.             {
    14.                 AverageQuaternion_Internal(ref cumulative, quat, quats[0]);
    15.             }
    16.  
    17.             float addDet = 1f / (float)quats.Length;
    18.             float x = cumulative.x * addDet;
    19.             float y = cumulative.y * addDet;
    20.             float z = cumulative.z * addDet;
    21.             float w = cumulative.w * addDet;
    22.             //note: if speed is an issue, you can skip the normalization step
    23.             return NormalizeQuaternion(new Quaternion(x,y,z,w));
    24.         }
    25.  
    26.         //Get an average (mean) from more then two quaternions (with two, slerp would be used).
    27.         //Note: this only works if all the quaternions are relatively close together.
    28.         //Usage:
    29.         //-Cumulative is an external Vector4 which holds all the added x y z and w components.
    30.         //-newRotation is the next rotation to be added to the average pool
    31.         //-firstRotation is the first quaternion of the array to be averaged
    32.         //-addAmount holds the total amount of quaternions which are currently added
    33.         static void AverageQuaternion_Internal(ref Vector4 cumulative, Quaternion newRotation, Quaternion firstRotation)
    34.         {
    35.             //Before we add the new rotation to the average (mean), we have to check whether the quaternion has to be inverted. Because
    36.             //q and -q are the same rotation, but cannot be averaged, we have to make sure they are all the same.
    37.             if (!AreQuaternionsClose(newRotation, firstRotation))
    38.             {
    39.                 newRotation = InverseSignQuaternion(newRotation);
    40.             }
    41.  
    42.             //Average the values
    43.             cumulative.w += newRotation.w;
    44.             cumulative.x += newRotation.x;
    45.             cumulative.y += newRotation.y;
    46.             cumulative.z += newRotation.z;
    47.         }
    48.  
    49.         public static Quaternion NormalizeQuaternion(Quaternion quat)
    50.         {
    51.             float lengthD = 1.0f / Mathf.Sqrt(quat.w * quat.w + quat.x * quat.x + quat.y * quat.y + quat.z * quat.z);
    52.             quat.x *= lengthD;
    53.             quat.y *= lengthD;
    54.             quat.z *= lengthD;
    55.             quat.w *= lengthD;
    56.             return quat;
    57.         }
    58.  
    59.         //Changes the sign of the quaternion components. This is not the same as the inverse.
    60.         public static Quaternion InverseSignQuaternion(Quaternion q)
    61.         {
    62.             return new Quaternion(-q.x, -q.y, -q.z, -q.w);
    63.         }
    64.  
    65.         //Returns true if the two input quaternions are close to each other. This can
    66.         //be used to check whether or not one of two quaternions which are supposed to
    67.         //be very similar but has its component signs reversed (q has the same rotation as
    68.         //-q)
    69.         public static bool AreQuaternionsClose(Quaternion q1, Quaternion q2)
    70.         {
    71.             float dot = Quaternion.Dot(q1, q2);
    72.  
    73.             if (dot < 0.0f)
    74.             {
    75.                 return false;
    76.             }
    77.  
    78.             else
    79.             {
    80.                 return true;
    81.             }
    82.         }
    83.     }
     
    Elecman likes this.
  30. CCrowell

    CCrowell

    Joined:
    Mar 31, 2022
    Posts:
    9
    I actually considered this approach before I came here and the problem is if you average two quaternions that are exact opposites of eacth other you get zero for a look vector and zero for an up vector. But in most other cases I think it would work fine? I'm sure you would have to normalized the averaged forward and up vectors. I don't know if you can use a direction vector who's magnitude is not 1 for defining quaternions. In truth, I have never tried.
     
  31. halley

    halley

    Joined:
    Aug 26, 2013
    Posts:
    2,443
    So, I finally had a need for the weighted average of quaternions, and of the three major approaches shown in this thread, I have to say that @JoshuaMcKenzie 's approach was the simplest to implement, and the most predictable.



    The white ball gizmos are denoting the weights that I am applying to each pink monkey target. The weights sum up to 1. (Thanks to @Kybernetik for his implementation of this polar blending tree in Animancer, I am not going to publish my adaptation of that routine.)

    I then orient the golden monkey to match the weighted average of the pink monkey targets' rotations.

    In this snippet, you can see the kind of instability that @CCrowell just mentioned; when the golden monkey moves right behind the center target monkey, there's a little bit of chunkiness, because the farther monkeys are rotated diametrically opposite the center one. In most cases, this won't be a problem, and can be overcome with some careful avoidance of the deadzone.

    Code (CSharp):
    1.  
    2.         Quaternion WeightedAverageQuaternions(Quaternion[] quaternions, float[] weights)
    3.         {
    4.             Assert.IsTrue(quaternions != null && quaternions.Length > 0);
    5.             Assert.IsTrue(weights != null && weights.Length >= quaternions.Length);
    6.  
    7.             int count = quaternions.Length;
    8.             Vector3 forwardSum = Vector3.zero;
    9.             Vector3 upwardSum = Vector3.zero;
    10.             for (int i = 0; i < count; i++)
    11.             {
    12.                 forwardSum += weights[i] * (quaternions[i] * Vector3.forward);
    13.                 upwardSum += weights[i] * (quaternions[i] * Vector3.up);
    14.             }
    15.             forwardSum /= (float)count;
    16.             upwardSum /= (float)count;
    17.  
    18.             return Quaternion.LookRotation(forwardSum, upwardSum);
    19.         }
     
    _geo__ and Chubzdoomer like this.