Thread: 3D Rotation (with Quaternions!)

  1. #1
    Registered /usr
    Join Date
    Aug 2001
    Location
    Newport, South Wales, UK
    Posts
    1,273

    Red face 3D Rotation (with Quaternions!)

    Hello,

    I'm trying to make a simple 3D wireframe model viewer. This is being done using the good ol' Windows GD of I as opposed to Direct3D or OpenGL. Performance is not really my objective here, accuracy is a lot more important.

    Reading around, quaternions seem all the rage. To that end, I have attempted to project the vertices of the model into 2D based on a global quaternion (the user can rotate the entire object and zoom in/out, nothing else. The object stays in the centre of the window).

    The properties of the current scene are stored in pScene, as thus:-
    Code:
    typedef struct {
    	unsigned short usWidth;
    	unsigned short usHeight;
    	t_angle3d aRotation; // roll, pitch, yaw (radians)
    	double dZoom; // z translation
    	double dQuaternion[4];
    } t_scene;
    The quaternion is initialized at the beginning of the render operation using the members of pScene->aRotation as input:-
    Code:
    void EulerToQuat(double dRoll, double dPitch, double dYaw, double *pdQuaternion)
    {
    	double cr, cp, cy, sr, sp, sy, cpcy, spsy;
    
    	cr = cos(dRoll / 2);
    	cp = cos(dPitch / 2);
    	cy = cos(dYaw / 2);
    	sr = sin(dRoll / 2);
    	sp = sin(dPitch / 2);
    	sy = sin(dYaw / 2);
    	cpcy = cp * cy;
    	spsy = sp * sy;
    	pdQuaternion[0] = (cr * cpcy) + (sr * spsy);
    	pdQuaternion[1] = (sr * cpcy) - (cr * spsy);
    	pdQuaternion[2] = (cr * sp * cy) + (sr * cp * sy);
    	pdQuaternion[3] = (cr * cp * sy) - (sr * sp * cy);
    }
    Each vertex in the model is projected using the below function. If they are found to be projected in or behind the camera (z <= 0), they are discarded:-
    Code:
    int ProjectVertex(t_scene *pScene, t_vertex3d *pV, t_vertex2d *pVout)
    {
        double dWsquared, dXsquared, dYsquared, dZsquared;
        t_vertex3d vTemp;
    
        // I should probably store these in pScene if I'm rotating everything the same way
        dWsquared = pScene->dQuaternion[0] * pScene->dQuaternion[0];
        dXsquared = pScene->dQuaternion[1] * pScene->dQuaternion[1];
        dYsquared = pScene->dQuaternion[2] * pScene->dQuaternion[2];
        dZsquared = pScene->dQuaternion[3] * pScene->dQuaternion[3];
        vTemp.x = pV->x * (dWsquared + dXsquared - dYsquared - dZsquared) + (2 * ((pScene->dQuaternion[0] * pScene->dQuaternion[2] * pV->z) + (pScene->dQuaternion[1] * pScene->dQuaternion[3] * pV->z) + (pScene->dQuaternion[1] * pScene->dQuaternion[2] * pV->y) - (pScene->dQuaternion[0] * pScene->dQuaternion[3] * pV->y)));
        vTemp.y = pV->y * (dWsquared - dXsquared + dYsquared - dZsquared) + (2 * ((pScene->dQuaternion[1] * pScene->dQuaternion[2] * pV->x) + (pScene->dQuaternion[0] * pScene->dQuaternion[3] * pV->x) + (pScene->dQuaternion[2] * pScene->dQuaternion[3] * pV->z) - (pScene->dQuaternion[1] * pScene->dQuaternion[0] * pV->z)));
        vTemp.z = pV->z * (dWsquared - dXsquared - dYsquared + dZsquared) + (2 * ((pScene->dQuaternion[1] * pScene->dQuaternion[3] * pV->x) - (pScene->dQuaternion[0] * pScene->dQuaternion[2] * pV->x) + (pScene->dQuaternion[0] * pScene->dQuaternion[1] * pV->y) + (pScene->dQuaternion[3] * pScene->dQuaternion[2] * pV->y)));
        vTemp.z -= pScene->dZoom;
        if (vTemp.z <= 0)
            return -1;
    
        if (pVout)
        {
            // I'm not sure where "32" comes from, it just seems a reasonable scaling factor
            // Examples I have seen use (40 / 10) but where does this come from?!?
            pVout->x = ((vTemp.x / vTemp.z) * 32) + (pScene->usWidth / 2);
            pVout->y = ((vTemp.y / vTemp.z) * -32) + (pScene->usHeight / 2);
        }
    
        return 0;
    }
    This, er, almost works but doesn't.
    There's something up with the projection. It seems to vary between horizontally squashed to stretched when rotated through 360 degrees pitch.

    Can anyone help?

  2. #2
    Registered User
    Join Date
    Mar 2002
    Posts
    125
    I'm going to guess the problem is somewhere in your EulerToQuat function, but it's hard to tell since the maths part of your code is kind of hard to read.
    If speed isn't a concern, I wouldn't be calculating a quaternion from Euler angles directly since it's a complicated function that's easy to make mistakes in, and hard for others to read. Instead, implement a quaternion multiplication function and use that to chain each individual rotation for the yaw, pitch and roll parts. This should result in an EulerToQuat function that is much easier to read and check for errors. (on that note, there's really no reason to use a quaternion for this purpose at all, but I'm guessing you're writing this as a training exercise and want to use quaternions to get familiar with them)

    In the meantime, it's worth checking whether your quaternion is properly normalized. If it isn't (which could be the case if you made a mistake in the EulerToQuat function) it would cause weird effects since a non-unit quaternion no longer corresponds to a rotation in 3d space. Maybe you could add a key to log the length of your quaternion (sqrt(W^2+X^2+Y^2+Z^2)) or try normalizing it before use in the program to see whether that's the problem.
    Typing stuff in Code::Blocks 8.02, compiling stuff with MinGW 3.4.5.

  3. #3
    Registered User VirtualAce's Avatar
    Join Date
    Aug 2001
    Posts
    9,607
    How about a class that handles quaternions?
    How about a class that handles the matrix transformations?
    How about a class that handles the drawing / rendering?

    All of these abstractions will make your life much easier. Even if you are not using D3D or OGL you will still have to create a transformation pipeline. The math used stays the same regardless of which rendering API you choose or choose not to use.

    I would not worry about quaternions and rotation until you have a proper transformation pipeline written. Once you have that you should be able to plug quaternions in quite easily.
    Also you should not need to use sin() and cos() while working with any of this if you take the correct approach.

  4. #4
    Registered /usr
    Join Date
    Aug 2001
    Location
    Newport, South Wales, UK
    Posts
    1,273
    Quote Originally Posted by Boksha View Post
    I'm going to guess the problem is somewhere in your EulerToQuat function, but it's hard to tell since the maths part of your code is kind of hard to read.
    Fortunately that function was a straight copy-paste, right at the beginning of doing this.

    I think the readability issue may be something to do with the fact that I like my code to look close to the way the computer executes it, e.g. a sequence of intrinsic ops. Blame it on my grounding in assembler.
    Quote Originally Posted by Boksha View Post
    If speed isn't a concern, I wouldn't be calculating a quaternion from Euler angles directly since it's a complicated function that's easy to make mistakes in, and hard for others to read. Instead, implement a quaternion multiplication function and use that to chain each individual rotation for the yaw, pitch and roll parts. This should result in an EulerToQuat function that is much easier to read and check for errors. (on that note, there's really no reason to use a quaternion for this purpose at all, but I'm guessing you're writing this as a training exercise and want to use quaternions to get familiar with them)
    Yes, but that makes it harder for me to read (see above!)

    To explain the application a bit more, I want to project a 3D object onto a plane, aside from viewing it to export the projection of the vertices into an SVG (Scalable Vector Graphics) file. The SVG I will then use for wickedy super-scalable 2D in-game graphics(TM).

    The 3D bit is but a small part of my evil plan. I don't want to spend months on it if I can avoid that, but I do want it to be as accurate a rendering of the object as is conceivably possible. No speed optimizations that may affect vertex projection. As the input is more or less a triangle mesh a wireframe representation doesn't take long to software render on a modern CPU anyway.

    I am also holding out on the possibility that quaternions may have a further application in 2D transformations so doing this in this way will help me down the road.
    Quote Originally Posted by Boksha View Post
    In the meantime, it's worth checking whether your quaternion is properly normalized.
    Checked this, I think because the cos()s and sin()s are related that the calculation in this function is inherently normalized. You would have to be able to read it to know that though, or debug.
    Quote Originally Posted by VirtualAce View Post
    How about a cheese and pickle sandwich?
    Urgh, no thanks Bub. Pickle's horrible.

    To rephrase and answer your questions, I want to understand what is going on before I stuff things I have little experience with into larger and larger containers.
    Yes, I am going to produce spaghetti code.
    Yes, it's going to perform like a dog.
    No, I am not after an easy life in this instance.

    Once I get the hang of it and can follow what is happening in my own code, then I can implement your suggestions.
    Quote Originally Posted by VirtualAce View Post
    Also you should not need to use sin() and cos() while working with any of this if you take the correct approach.
    How so? Tricks?

    P.S. I did figure out the fault in the projection!
    "32" is slightly too arbitrary a constant, the factor is supposed to be calculated from the FOV angle. In my case, it will always be 90 degrees, so a simplified aspect-corrected version is thus:-
    Code:
    //ProjectVertex
        if (pVout)
        {
            // this must be the same for both X and Y in order for aspect ratio to be preserved
            // use half of the shorter of the two dimensions
            dFactor = (pScene->usWidth > pScene->usHeight) ? (pScene->usHeight / 2) : (pScene->usWidth / 2);
            pVout->x = ((vTemp.x / vTemp.z) * dFactor) + (pScene->usWidth / 2);
            pVout->y = ((vTemp.y / vTemp.z) * -dFactor) + (pScene->usHeight / 2);
        }
    Last edited by SMurf; 07-24-2011 at 07:55 PM.

  5. #5
    Registered User VirtualAce's Avatar
    Join Date
    Aug 2001
    Posts
    9,607
    In order to create the final rotation matrix from the quaternion you will have to use sin() and cos() and converting to/from quat to matrix representations will require their use as well. You are correct in this and my post was unclear.

    To that end, I have attempted to project the vertices of the model into 2D based on a global quaternion (the user can rotate the entire object and zoom in/out, nothing else. The object stays in the centre of the window).
    Isn't this what every rendering pipeline does? It transforms 3D coordinates into 2D screen coordinates.

    Transform the local vertices to world coordinates via SRT or ISROT. The R will be your final rotation matrix built from your quaternion math. The view matrix will come from your camera which can also be quaternion based but does not have to be. Finally the projection matrix can be built based on the desired near/far plane, FOV, etc.

    So:

    World (Identity / (Parent or Root) * Scale * Rotation * Translation) * View * Projection = WVP matrix
    Rotation is achieved by creating a rotational quaternion, doing operations on it to rotate, etc., and then converting the final quaternion back into a matrix to be used in the world transform. You can extract the final up, right, and look vectors from your world transformation matrix.

    The only functions I have to do this are for left handed matrices and I'm assuming you are using right-handed matrices. I have a quaternion camera which could be converted easily into a class that handles the world transform and uses quaternions instead of Euler angles but it is pure Direct3D-based code which probably will not help you.

    Performance is not really my objective here, accuracy is a lot more important.
    That is not the only reason you would use Direct3D or OGL. They have constructs that make operations like the one you are attempting to do much simpler. You can use the D3DX math library without actually using Direct3D or DirectX. Again it uses left-handed matrices so you would need to transpose the results if you are using right-handed.
    Last edited by VirtualAce; 07-26-2011 at 09:53 PM.

  6. #6
    Registered /usr
    Join Date
    Aug 2001
    Location
    Newport, South Wales, UK
    Posts
    1,273
    Actually I am using a left-handed system. I can visualize it more naturally, for some reason.

    I would prefer to avoid a dependency on D3D, that would tie me to Windows. I am using GDI for graphics but in such a limited way as to make reimplementation easy (a function to draw lines, a function to fill).

    I now have another question though, which I will open another thread for.

  7. #7
    Registered User VirtualAce's Avatar
    Join Date
    Aug 2001
    Posts
    9,607
    The only dependency would be on things like
    • D3DXVECTOR3 - A 3D vector class with overloaded operators
    • D3DXMATRIX - A 4x4 matrix class with overloaded operators
    • D3DXQUATERNION - A quaternion class with overloaded operators
    • Several D3DX math functions that convert from a quaternion to a rotation matrix and vice versa


    You could certainly code these yourself. I rely on the D3DX versions b/c I do not mind that dependency. But nothing says you cannot code the functions yourself b/c they are simply math functions that you can find in any 3D math book or perhaps online.

    I was very surprised when I moved to quaternions because I thought the code would be more complicated. Quite the opposite is true. Quaternions allowed me to remove about 30% to 40% of my code for axis-angle rotations and the code is much more clear and concise.

Popular pages Recent additions subscribe to a feed

Similar Threads

  1. Euler Angles to Quaternions
    By sugarfree in forum Game Programming
    Replies: 9
    Last Post: 05-24-2010, 10:17 AM
  2. Derivation of Matrix to Quaternions conversion
    By sarah22 in forum Game Programming
    Replies: 1
    Last Post: 12-05-2009, 02:35 PM
  3. Bit rotation...
    By Junior89 in forum C++ Programming
    Replies: 4
    Last Post: 06-10-2007, 03:47 PM
  4. Rotation
    By peckitt99 in forum Windows Programming
    Replies: 4
    Last Post: 03-25-2007, 06:46 AM
  5. Quaternions
    By Lurker in forum Game Programming
    Replies: 9
    Last Post: 09-06-2003, 04:24 PM