1
votes

So I am using a quaternion to create a segment of two points in 3D space, and trying to recompute a similar quaternion later on (one representing the same vector through space; I am aware that the segment's rotation around itself is undefined). I am creating the segment as such:

sf::Vector3<float> Start(0, 0, 0);
sf::Vector3<float> End = Start;

//Create a vector from the start to the end
sf::Vector3<float> Translation = Orientation.MultVect(sf::Vector3<float>(0, 1, 0));

//Add that vector onto the start position
End.x += Translation.x * Length;
End.y += Translation.y * Length;
End.z += Translation.z * Length;

Where Orientation::MultVect() looks like this:

sf::Vector3<float> Quaternion::MultVect(sf::Vector3<float> Vector)
{
    //From http://www.idevgames.com/articles/quaternions
    Quaternion VectorQuat = Quaternion();
    VectorQuat.x = Vector.x;
    VectorQuat.y = Vector.y;
    VectorQuat.z = Vector.z;
    VectorQuat.w = 0.0;

    Quaternion Inverse = (*this);
    Inverse.Invert();

    Quaternion Result = Inverse * VectorQuat * (*this);

    sf::Vector3<float> ResultVector;
    ResultVector.x = Result.x;
    ResultVector.y = Result.y;
    ResultVector.z = Result.z;

    return ResultVector;
}

Now this function seems to work rather well in other contexts, so I don't think the problem is here, but you never know. I should also mention that the point ends up where I expect it to, given the Quaternion I feed if (which I construct from Euler angles, sometimes with multiplication with other quaternions).

The problem appears, to me, to lie in recomputing the quaternion from Start and End. To do so, I use this function, which works well when orienting objects in the scene towards other objects (unless the objects in question are along the exact same Y axis, in which case I get quaternions with NaN values). Here is how I do that:

Quaternion Quaternion::FromLookVector(sf::Vector3<float> FromPoint, sf::Vector3<float> ToPoint)
{
    ///Based on this post:
    ///http://stackoverflow.com/questions/13014973/quaternion-rotate-to
    //Get the normalized vector from origin position to ToPoint
    sf::Vector3<double> VectorTo(ToPoint.x - FromPoint.x,
                                 ToPoint.y - FromPoint.y,
                                 ToPoint.z - FromPoint.z);
    //Get the length of VectorTo
    double VectorLength = sqrt(VectorTo.x*VectorTo.x +
                               VectorTo.y*VectorTo.y +
                               VectorTo.z*VectorTo.z);
    //Normalize VectorTo
    VectorTo.x /= -VectorLength;
    VectorTo.y /= -VectorLength;
    VectorTo.z /= -VectorLength;

    //Define a unit up vector
    sf::Vector3<double> VectorUp(0, -1, 0);

    //The X axis is the cross product of both
    //Get the cross product as the axis of rotation
    sf::Vector3<double> AxisX(VectorTo.y*VectorUp.z - VectorTo.z*VectorUp.y,
                              VectorTo.z*VectorUp.x - VectorTo.x*VectorUp.z,
                              VectorTo.x*VectorUp.y - VectorTo.y*VectorUp.x);
    //Normalize the axis
    //Get the length of VectorTo
    double AxisXLength = sqrt(AxisX.x*AxisX.x +
                              AxisX.y*AxisX.y +
                              AxisX.z*AxisX.z);

    //Normalize VectorTo
    AxisX.x /= AxisXLength;
    AxisX.y /= AxisXLength;
    AxisX.z /= AxisXLength;

    //Get the adjusted Y vector
    //Get the cross product of the other two axes
    sf::Vector3<double> AxisY(VectorTo.y*AxisX.z - VectorTo.z*AxisX.y,
                              VectorTo.z*AxisX.x - VectorTo.x*AxisX.z,
                              VectorTo.x*AxisX.y - VectorTo.y*AxisX.x);
    //Normalize the axis
    //Get the length of VectorTo
    double AxisYLength = sqrt(AxisY.x*AxisY.x +
                              AxisY.y*AxisY.y +
                              AxisY.z*AxisY.z);
    //Normalize VectorTo
    AxisY.x /= AxisYLength;
    AxisY.y /= AxisYLength;
    AxisY.z /= AxisYLength;

    //A matrix representing the Thing's orientation
    GLfloat RotationMatrix[16] = {(float)AxisX.x,
                                  (float)AxisX.y,
                                  (float)AxisX.z,
                                  0,
                                  (float)AxisY.x,
                                  (float)AxisY.y,
                                  (float)AxisY.z,
                                  0,
                                  (float)VectorTo.x,
                                  (float)VectorTo.y,
                                  (float)VectorTo.z,
                                  0,
                                  0,
                                  0,
                                  0,
                                  1};

    Quaternion LookQuat = Quaternion::FromMatrix(RotationMatrix);

    //Reset the quaternion orientation
    return LookQuat;
}

So when I compute the segments, I also check what their reconstructed values would be, like this:

sf::Vector3<float> Start(0, 0, 0);
sf::Vector3<float> End = Start;

//Create a vector from the start to the end
sf::Vector3<float> Translation = Orientation.MultVect(sf::Vector3<float>(0, 1, 0));
//Add that vector onto the start position
End.x += Translation.x * Length;
End.y += Translation.y * Length;
End.z += Translation.z * Length;

std::cout << "STATIC END     (";
std::cout << End.x << ",";
std::cout << End.y << ",";
std::cout << End.z << ")\n";

///TEST
Quaternion Reconstructed = Quaternion::FromLookVector(Start, End);
Translation = Reconstructed.MultVect(sf::Vector3<float>(0, 1, 0));
sf::Vector3<float> TestEnd = Start;
TestEnd.x += Translation.x * Length;
TestEnd.y += Translation.y * Length;
TestEnd.z += Translation.z * Length;

std::cout << "RECONSTRUCTED END (";
std::cout << TestEnd.x << ",";
std::cout << TestEnd.y << ",";
std::cout << TestEnd.z << ")\n";

And the two don't match up. For example, if the static end point is (0,14.3998,0.0558498), then the recomputed point is (0,8.05585,-6.39976). The two should be identical, though. The undefined part of the rotation shouldn't change the position of the end point, only the roll (or Z-rotation, or whatever you want to call it), which, since this is a segment, doesn't matter.

Note that when I end up using this for things other than simple segments, the roll will matter, which is why I use an up vector to make sure the objects I place along these segments will always have their tops facing upwards as much as possible (objects looking straight up or down can have a special arbitrary roll determined separately, if need be). Another goal is creating multiple segments strung together, each rotating relative to the orientation of the one that came before it rather than rotating relative to global space.

So what am I doing wrong here? Why can't I recompute a second quaternion that performs the same translation as the first one?

1
Quaternion multiplication does not commute. Quaternions are relative transforms (not absolutes). Finally, you need to narrow your code down more: just because the code may be self-consistent, it doesn't mean it's what you want.Brett Hale

1 Answers

2
votes

I'm not entirely sure how you're calculating the 'rotating' quaternion between the two vectors, but I'm pretty sure it's very cumbersome. At least, if I understand you correctly, you have 'look' vectors which point in some direction, and the object 'looks' along that direction from the origin (0,0,0), correct?.

If the above is the case, it should not be too difficult. One thing I find quite peculiar though is that your quaternion - vector multiplication seems to be in reverse order. I have quaternion * vector defined as:

quat qt = *this * quat(0, vec.x, vec.y, vec.z) * inverse();
return vec3(qt.x, qt.y, qt.z);

In which the quat constructor is defined as quat(w, x, y, z) and the inverse() method returns a copy. Inverse is equal to conjugate, and it is defined as (w, -x, -y, -z). BUT, for this to be true your quaternions have to be normalized, only then will they actually represent an orientation (and only then will the inverse equal the conjugate). Then I have quaternion multiplication defined as follows:

// This describes A * B (not communative!)
w = A.w * B.w - A.x * B.x - A.y * B.y - A.z * B.z;
x = A.w * B.x + A.x * B.w + A.y * B.z - A.z * B.y;
y = A.w * B.y + A.y * B.w + A.z * B.x - A.x * B.z;
z = A.w * B.z + A.z * B.w + A.x * B.y - A.y * B.x;

With that out of the way, you want to be able to construct a quaternion from 'angle-axis'. Meaning it should take an axis of rotation, and an angle to rotate around that axis (in radians). I shall just give you that algorithm, as it doesn't make much sense intuitively:

// if axis is already unit length, remove the division
double halfAngle = angle * 0.5f; // In radians
double scale = sin(halfAngle) / axis.magnitude();

w = cos(halfAngle);
x = axis.x * scale;
y = axis.y * scale;
z = axis.z * scale;

So now we just have to calculate an axis to rotate around, and how much we want to rotate around it, in radians. At first sight this might seem complex, but it is just a case of understanding what is going on. You have two vectors, A and B. You want to calculate a quaternion which describes a rotation FROM A, TO B. To get the axis to rotate around, we just want a perpendicular axis to both, obviously this would be done by taking the cross product. If you're using a right handed coordinate system, it would be:

axis = A × B

If you're using a left handed coordinate system, I think you should just inverse the order, but don't take my word on that. Now to get the angle between the two vectors. This can very simply be done by taking the dot product. The only catch is that you have to normalize both vectors, so they have length 1, and won't alter the outcome of the dot product. This way the dot product will return the cosine of the angle, so to get the actual angle we can do:

angle = acos(normalize(A) * normalize(B))

The multiplication sign stands for the dot product of course. Now we just plug the axis and angle in the algorithm I gave you above, and we have a quaternion describing a 'rotation' from look vector A to look vector B. Now if the vectors point in the exact same direction, it would be unwise to apply the algorithm, as the axis will be (0,0,0). If you look at the algorithm I hope you see that will either try to divide by zero or simply output all zeros. So whenever I apply that algorithm I first check if the axis is not all zeros.

The formula you're currently using seems very strange and inefficient to me. I don't really understand why you're first computing a matrix either, computing a quaternion from a matrix is quite an expensive computation. In fact I believe computing the opposite, a matrix from a quaternion, is even faster.

Anyway, good luck getting it to work!