Orienting objects in OpenGL

What we are trying to do

We are trying to draw objects (let's call them models) in 3D space with correct position and orientation, using OpenGL. These example do this in Java, using JOGL, but the ideas would be the same in any OpenGL implementation.

We want to do this by drawing the object at 0,0,0, and 'pointing' in a fixed direction ( we use 0,0,1 here). Then we want to use the OpenGL transformation functions (that is, glRotated ) to make the model appear pointing in the correct direction, which means both 'around' and 'up and down'.

Why do we want to do this?

This is a question not usually asked. The obvious way to draw a model is using the drawing primitives like drawing QUADS, with the vertex data calculated by the program to orient the model correctly.

First, a minor reason this is no good - speed. If your program calculates the vertex values, that will be done by the machine CPU, and it is not especially good at it. Whereas if you use the OpenGL transform functions, these will usually be done by the processor in the graphics card, which is what it was designed to do, leaving your CPU to run your programs.

But more signficantly, any model which is at all realistic will have a lot of vertex data, and you simply can't design it using Java code. Instead you use a 3d modelling program like Blender, save the model, then your Java program reads in the vertex data (and normals and materials and textures) and draws those vertices. But in Blender you drew it at 0,0,0 pointing at 0,0,1 - so your Java program has to transform it from there.

What is the problem?

Several problems really

One, how do we represent the orientation? For example, we could use 2 angles, which would be like latitude and longitude, to fix which way it is pointing. Plus a third angle to specify a roll around that axis. That's not a good way, because..

Two, we need a good way to control the orientation by the user. For example if it is a car we want to be able to turn left or right, or for a plane, we want to change the pitch nose down or up. These are relative to the model, not the world - which is why latitude and longitude are not good.

Three, we've got to draw this just using the OpenGL glRotated function. This takes an angle argument, plus 3 vector components fixing the axis about which to rotate. So the last 2 points must fit in with an (angle, axis) set of transforms.

A solution

I'm not arguing this is an optimal soultion, but at least it is a solution. I'm thinking of an aircraft, and I want to control roll and pitch. This uses 4 classes

ExploreOrientation. This contains main(), and keyboard input. Keys QA control pitch, and OP roll

Stick.javaThis is the object model, a simple stick plane

WMVector.java. This is a 3d vector class

Quaternion.java. Quaternions are needed by the vector class to rotate one vector around another

Representing the orientation

1

I'm using 2 vectors. forward is which way the model is pointing, and up is which is up for the model. This is similar to the camera parameters. In the picture, the blue line is pointing 'into' the screen.

In addition I need to maintain a 'roll' variable which is the angle through which it has rolled. This is redundant, but simple and convenient. Why we need it is described later. Consequently the start of my model class is:

public class Stick {
    /** spatial coords of the center */
    private double x,y,z;
    /** orientation is parameterised as
     * a vector called forward - which way we are pointing, and
     * up - which way is 'up' from the object point of view
     */
    WMVector forward;
    WMVector up;
Java2html

How to draw it

This seems deceptively simple. We do a first rotation, which turns 0,0,1 to point the correct way (vector forward). This goes through an angle called, (oh dear) angle, and the axis is called turnAxis. Both of these are calulated in a method called reCalc(), called just when the orientation changes.

Then there is a second rotation to produce a correct roll orientation. This is always about 0,0,1, with an angle called angle2, also calculated in reCalc() when needed. angle and angle2 are in radians, so we have to convert to degrees here. fixedDraw() draws the model with forward pointing at 0,0,1. Here is the code:.

public void draw(GL gl)
    {        
        gl.glRotated(angle*180/Math.PI, turnAxis.getX(), turnAxis.getY(), turnAxis.getZ());
        gl.glRotated(angle2*180/Math.PI, 0,0,1);
        fixedDraw(gl);        
    }
Java2html

and this shows turnAxis, angle and forward:

2

Doing reCalc

OK this is the tricky part. We maintain the following class fields:

    private final WMVector zaxis=new WMVector(0,0,1);
    private final WMVector yaxis = new WMVector(0,1,0);
    /** angle is the angle needed to rotate through to make it point the correct way */
    private double angle;
    /** angle2 is the angle to roll to get this correct */
    private double angle2;
    /** turnAxis is the vector to rotate around by 'angle' to point correct way */
    private WMVector turnAxis;
    private double rollAngle=0.0;
Java2html

and here (big breath) is reCalc()

  /** calculations need for altered orientation */
    private void reCalc()
    {
        // how much to rotate to point in correct direction
        angle = forward.angleWith(zaxis);
        // axis to turn on
        turnAxis = zaxis.cross(forward);
        if (!turnAxis.isZero()) // unless forward on z axis
       {
        turnAxis=turnAxis.normalise();
        // where would this rotation have tranformed the up?
        WMVector newUp = yaxis.rotate(angle, turnAxis);
        // what is the angle between that and the actual up?
        angle2 = newUp.angleWith(up);
        if (rollAngle<=0angle2=-angle2;
        // this is the correct angle to roll

       }
        else angle2=rollAngle;
    }
Java2html

This starts by finding the angle between the z axis and forward, by using the angleWith method of my WMVector class. It then forms turnAxis, as the cross-product of the z axis and forward. No problem.

But suppose forward and z axis are parallel - which in fact they are, initially. Then the cross product is undefined - or here, comes as a zero vector. In this case (that is, is zero ) we just do something simple, rotate about the z axis by the roll angle, which we maintain when we do a roll.

Now the other, general case, when forward and the z axis are not aligned. The issue here is that the first rotation will also rotate the initial up vector by a certain amount - so that the we can't simply rotate by roll angle. I suggest you sit and think about that for a while - it took me about 3 weeks

Ok. So first we just normalise the turnAxis, then we work out where the rotation would have turned the initial up direction, which was the y axis. This is called newUp. Then we work out the angle between newUp and up, giving us angle2, the actual angle we need to do the roll rotation. What could be simpler?

There is a final twist. The angleWith() method that returns the angle between 2 vectors alwyas yields a positive value, in the rangle 0 to π. This is correct - but sometimes we need to rotate the other way. So we check rollAngle, and reverse the sign of angle2 if we need to.

Doing a roll

To do a roll, we just need to change rollAngle, rotate the up vector about the forward vector, and reCalc() the orientation parameters:

public void roll()
    {
        // rotate the up vector around the forward axis
        rollAngle+=0.01;
        up=up.rotate(0.01, forward);
        reCalc();        
    }
Java2html

This turns around 0.01 of a radian, or about half a degree. The unRoll() method simply uses -0.01.

Altering pitch

This is just a little vector manipulation. We first work out sideways, which is perpendicular to both forward and up. Then we add a small amount of the up vector to the forward vector, to tilt it up. This would make it bigger, so we re-normalise it. And having pitched up, the up vector should tilt back, so we make it the cross product of sideways and forward, and normalise it.

public void pitchUp()
    {
       // Add to the forward vector a small amount of the up vector
       WMVector sideways = forward.cross(up);
       WMVector newForward = forward.add(up.times(0.01));
       forward=newForward.normalise();
       // adjust up so is still perpendicular to forward
       up= sideways.cross(forward);
       up = up.normalise();
       reCalc()// adjust orientation parameters
    }
Java2html

Pitch down is the same except we add a negative amount of up

Rotate one vector about another

Apart from the reCalc() method, this is fairly simple. However it does require rotating one vector about another. Hearn and Baker 'Computer Graphics' second edition, page 419, was my source for this. There are 2 methods - doing it by 2 rotations onto an axis, the 'actual' rotation about that axis, then the inverse rotations to put it back. That is not straightforward, so I used Quaternions - which are also not straightforward - but hey, it works.