3
\$\begingroup\$

Simply calculate the average for 2 Quaternions should work like follows right? :

Quaternion.Lerp(rotationlist[0].transform.rotation, rotationlist[1].transform.rotation, 0.5f);

And now I tried to put this into a recursive function, like this:

private Quaternion calcAvg(int pos, List<GameObject> rotationlist)
{
    if (pos < rotationlist.Count)
    {
        return Quaternion.Lerp(rotationlist[pos].transform.rotation, calcAvg(++pos, rotationlist), 0.5f);
    }
    return ???;
}

But I'm not absolutely sure, what to put into the second return.

Edit, I added the last position into the second return but now I get an out of bounds exception:

private Quaternion calcAvg(int pos, List<GameObject> markerList)
{
        if (pos < markerList.Count)
        {
            return Quaternion.Lerp(markerList[pos].transform.rotation, calcAvg(++pos, markerList), 0.5f);
        }
        return markerList[pos].transform.rotation;
}

Edit, fixed the issue, I will test this now but it looks alright :

private Quaternion calcAvg(int pos, List<GameObject> markerList)
{
        if (pos < markerList.Count)
        {
            return Quaternion.Lerp(markerList[pos].transform.rotation, calcAvg(++pos, markerList), 0.5f);
        }
        return markerList[pos].transform.rotation;
}
\$\endgroup\$
1
  • \$\begingroup\$ trivia -- I think you need if(pos < markerList.Count - 1)... \$\endgroup\$ Commented Apr 11, 2016 at 17:20

4 Answers 4

0
\$\begingroup\$

In your approach, the first quat in the list gets weighted at 50%, and all the rest together will be weighted for the other 50%.

If you had 4 quats, with your approach, their relative weightings will be 0.5, 0.25, 0.125, 0.125.

You could fix this my changing the 0.5f to something computed based on list and position.

You could also make the recursion more balanced with something like:

calcAvg(List<quat> quats):
  if(quats.size == 0): ERROR
  if(quats.size == 1):
    return quats[0]
  List<quat> firstHalf = first half of quats list
  List<quat> secondHalf = second half of quats list
  q1 = calcAvg(firstHalf)
  q2 = calcAvg(secondHalf)
  return Lerp(q1, q2, firstHalf.size / quats.size)

I'm not completely certain of the geometric characteristics of this kind of Angle Averaging. Probably fine for game use...

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Rereading my answer, realized I made a completely false claim that the half-by-half recursion reduced the total number of Lerps... edited. Half-by-half is slightly more arithmetically stable is all. \$\endgroup\$ Commented Apr 14, 2016 at 14:37
  • 1
    \$\begingroup\$ Note that Quaternion.Lerp(a, b, 0.5) will return a quaternion halfway between a & b, but Quaternion.Lerp(a, b, 0.25) will not in general return a quaternion one-quarter of the way. This is because rotation quaternions are points on the surface of a hypersphere. Lerp blends them in Cartesian coordinates, then normalizes back to the surface of the sphere. This only exactly agrees with the fraction along the arc at three points: start, end, and halfway (although for most game purposes it's often "close enough"). Quaternion.Slerp will give you more precision if you need, but is more expensive. \$\endgroup\$
    – DMGregory
    Commented Apr 14, 2016 at 15:34
5
\$\begingroup\$

Under the hood, Lerp will simply lerp each component of the quaternion, then normalise the result. A square root is hidden in there for each call to Lerp. It is therefore very inefficient. Also, any floating point rounding errors will accumulate.

This will be a lot faster, and also a lot more precise:

private Quaternion calcAvg(List<GameObject> rotationlist)
{
    if (rotationlist.Count == 0)
        throw new ArgumentException();

    float x, y, z, w;
    foreach (var go in rotationlist)
    {
        var q = go.transform.rotation;
        x += q.x; y += q.y; z += q.z; w += q.w;
    }
    float k = 1.0f / Mathf.Sqrt(x * x + y * y + z * z + w * w);
    return new Quaternion(x * k, y * k, z * k, w * k);
}
\$\endgroup\$
2
  • \$\begingroup\$ Looks pretty promising thanks, the reason why I used Lerp was simply, that I have no idea about quaternion calculation, but I guess I should get into that. \$\endgroup\$
    – TobiasW
    Commented Apr 15, 2016 at 6:18
  • \$\begingroup\$ I was watching a video today which recommends exactly this method for this problem (around 40 minutes in), since it's both fast and order-independent. It comes with the caveat that it works well over narrow ranges, so exercise caution when extending it to quaternions that might vary wildly. \$\endgroup\$
    – DMGregory
    Commented Nov 6, 2016 at 21:33
0
\$\begingroup\$

Sam's logic above is sound, but the code had some errors.

private static Quaternion CalcAvg(List<Transform> rotationlist)
{
    if (rotationlist.Count == 0)
        throw new ArgumentException();

    float x = 0, y = 0, z = 0, w = 0;
    foreach (Transform t in rotationlist)
    {
        x += t.rotation.x;
        y += t.rotation.y;
        z += t.rotation.z;
        w += t.rotation.w;
    }
    float k = 1.0f / Mathf.Sqrt(x * x + y * y + z * z + w * w);
    return new Quaternion(x * k, y * k, z * k, w * k);
}
\$\endgroup\$
1
  • 1
    \$\begingroup\$ This looks like it should have been proposed as an edit to the existing answer, not a new answer. \$\endgroup\$
    – DMGregory
    Commented Feb 22, 2023 at 3:56
-1
\$\begingroup\$
  1. Convert the quaternions into angle-vectors by using Quaternion.eulerAngles
  2. Calculate the average of each of the three axis' of these vectors.
  3. Convert the resulting axis-averages back to a Quaternion using Quaternion.Euler.
\$\endgroup\$
4
  • \$\begingroup\$ I think both of these methods fail to capture the idea of averaging rotations. For method A, let's average the rotations r*(1, y+) and r*(-1, y+). For most values of q the "average" will be r, which makes sense, but if r = identity then the average is 180 degrees opposed. This inconsistency doesn't match the behaviour of "averaging" operations in other contexts. For method B, consider the average of (90, z+) (180, z+) - intuitively we would expect the average to be a twist about the forward axis by something more than 90 and less than 180 - but method B will give us the identity quaternion. \$\endgroup\$
    – DMGregory
    Commented Apr 14, 2016 at 15:10
  • \$\begingroup\$ @DMGregory I realized that the second method will simply add the rotations, not average them, so I removed it. But I can not really follow your critique at the first one, because I consider identity-rotations a corner-case which is quite undefined in this context. So I will keep it for now. \$\endgroup\$
    – Philipp
    Commented Apr 14, 2016 at 15:14
  • \$\begingroup\$ My comment is in comparison with averaging numbers or vectors. The average of (x+a) and (x-a) is x, no matter what x we start with. The operation Avg_a(x) behaves uniformly over the whole domain. The method described here doesn't share this property - the fact that it has corner cases (and not limited to just the identity - anywhere the Euler angle function has a discontinuity will also exhibit this problem) makes it dissimilar from what we tend to think of as an average. \$\endgroup\$
    – DMGregory
    Commented Apr 14, 2016 at 15:20
  • \$\begingroup\$ Intuitively, I don't trust this "averaging euler angles" idea, since the axis order matters. I suspect that r * methodA(a1, a2...aN) != methodA(r * a1, r * a2...r * aN) if you see what I mean. Need to test. //// But come to think of it, since LERP(q1,q2) is linear, could the OPs implementation be as simple as AVG(q1,q2...qN) and normalizing? (Still not SLERP-accurate, and undefined for in some cases, but...) \$\endgroup\$ Commented Apr 14, 2016 at 17:34

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .