Search code examples
c++mathopenglrotationquaternions

3D object is rotating along all three axes when only X-Y rotation is desired


So I am implementing rotations for my 3D objects in an OpenGL scene, and so far the basic rotations themselves (using quaternions) work fine. I have also mostly solved the "look-at" problem, such that I can now make any 3D object, including the camera, face any point in 3D space exactly and without difficulty.

However, when commanding an object to face a given point, it will frequently roll along its local Z axis; see this image, where the small ship is looking at the point where the red, green and blue lines converge. You can see how it does not look like one might expect from an object that, say, rotated along its Y axis to face the green line, then tilted down along its X axis to face the meeting point of the three lines.

enter image description here

I don't necessarily want this. I would rather it look directly at that point but be tilted somewhat further clockwise (as viewed from the front), such that the tips of its wings are at the same Y level. Since the whole point of facing a point is that the local Z axis passes through that point, it shouldn't matter how the object is rotated around its local Z axis, yet the resulting rotation is consistently skewed, though the skew seems to depend on the relative position of the object and its point of focus.

In any case, here is my LookAt() code, which I would like to modify such that I have more control over the final Z rotation.

void Thing::LookAt(sf::Vector3<float> Target)
{
    ///Derived from pseudocode found here:
    ///http://stackoverflow.com/questions/13014973/quaternion-rotate-to
    //Reset the rotation to default
    m_Orientation = Quaternion();

    //Get the normalized vector from the camera position to Target
    sf::Vector3<double> VectorTo(Target.x - m_Position.x,
                                 Target.y - m_Position.y,
                                 Target.z - m_Position.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;

    //Straight-ahead vector
    sf::Vector3<double> LocalVector = m_Orientation.MultVect(sf::Vector3<double>(0, 0, -1));

    //Get the cross product as the axis of rotation
    sf::Vector3<double> Axis(VectorTo.y*LocalVector.z - VectorTo.z*LocalVector.y,
                             VectorTo.z*LocalVector.x - VectorTo.x*LocalVector.z,
                             VectorTo.x*LocalVector.y - VectorTo.y*LocalVector.x);
    //Normalize the axis
    //Get the length of VectorTo
    double AxisLength = sqrt(Axis.x*Axis.x +
                             Axis.y*Axis.y +
                             Axis.z*Axis.z);
    //Normalize VectorTo
    Axis.x /= AxisLength;
    Axis.y /= AxisLength;
    Axis.z /= AxisLength;

    //Get the dot product to find the angle
    double DotProduct = (VectorTo.x*LocalVector.x +
                        VectorTo.y*LocalVector.y +
                        VectorTo.z*LocalVector.z);
    double Angle = acos(DotProduct);

    //Determine whether or not the angle is positive
    //Get the cross product of the axis and the local vector
    sf::Vector3<double> ThirdVect(Axis.y*LocalVector.z - Axis.z*LocalVector.y,
                                  Axis.z*LocalVector.x - Axis.x*LocalVector.z,
                                  Axis.x*LocalVector.y - Axis.y*LocalVector.x);
    //If the dot product of that and the local vector is negative, so is the angle
    if (ThirdVect.x*VectorTo.x + ThirdVect.y*VectorTo.y + ThirdVect.z*VectorTo.z < 0)
    {
        Angle = -Angle;
    }

    //Finally, create a quaternion
    //Quaternion AxisAngle;
    m_Orientation.FromAxisAngle(Angle, Axis.x, Axis.y, Axis.z);
    m_Orientation.RotationMatrix(m_RotationMatrix);
}

My problem is that I don't know how I might obtain control over the final rotation around the Z axis. Note that when I extract a Z angle from the m_Orientation quaternion, it tells me that the ship pictured above is rotated 0 radians around its Z axis. I tried manually setting the Z component to 0 and renormalizing the quaternion, but (obviously I suppose) that didn't work.

For the time being, it would be fine to calculate the particular Z component required for the ship to be rotated "topside up," i.e. such that its two wingtips are on the same Y level and its Y axis is as steep as possible, but I'd like a clearer idea of how to manipulate the final Z rotation. Any ideas or resources?


Solution

  • If you perform a single rotation about an arbitrary axis, it is natural that you get this result. Have you imagined the transformation?

    However, there is an easier way to construct the rotation matrix. You just have to find the vectors the axes are mapped to.

    So e.g. the z-axis (forward direction) is mapped to z = VectorTo or z = -VectorTo if the model points towards -z. To preserve the up-direction, you need to define it. Let's say up = (0, 1, 0). Then the x-axis is mapped to the cross product of x = z x up. And finally you need to recalculate the up-vector to maintain a pure rotation with y = x x z. (The x in the middle specifies the cross product operator).

    Having calculated these vectors, you are now able to construct the transformation matrix as follows (with the normalized vectors):

        /  x.x  y.x  z.x  0  \
        |  x.y  y.y  z.y  0  |
    T = |  x.z  y.z  z.z  0  |
        \  0    0    0    1  /
    

    Depending on your API, you might need to transpose this matrix.

    If you need the quaternion, you can use a method like this one.