Quaternions and camera angle

Started by kramer, April 02, 2006, 06:20:36

Previous topic - Next topic

kramer

Hi All,

I just finished building a quaternion based view/movement system for my seemingly never-to-be-finished terrain demo based on NeHe's example.

The relevant code is:
private static void setPerspective()
	{
		Quaternion pitchQuaternion = new Quaternion();
		pitchQuaternion.setFromAxisAngle(new Vector4f(1f, 0f, 0f, pitch * PI_OVER_180));
		Quaternion headingQuaternion = new Quaternion();
		headingQuaternion.setFromAxisAngle(new Vector4f(0f, 1f, 0f, heading * PI_OVER_180));
		Quaternion rotation = Quaternion.mul(pitchQuaternion, headingQuaternion, null);
		
		// orient the camera...
		Matrix4f matrix = convertQuaternionToMatrix4f(rotation);
		FloatBuffer matrixBuffer = BufferUtils.createFloatBuffer(4*4);
		matrix.store(matrixBuffer);
		matrixBuffer.rewind();
		GL11.glMultMatrix(matrixBuffer);
		
		// calculate direction vector...
		Matrix4f pitchMatrix = convertQuaternionToMatrix4f(pitchQuaternion);
		float j = pitchMatrix.m21;		
		Quaternion temp = Quaternion.mul(headingQuaternion, pitchQuaternion, null);
		matrix = convertQuaternionToMatrix4f(temp);
		matrix.store(matrixBuffer);
		matrixBuffer.rewind();
		direction = new Vector3f(matrix.m20, j, matrix.m22);
	}
	
	private static Matrix4f convertQuaternionToMatrix4f(Quaternion q)
	{
		Matrix4f matrix = new Matrix4f();
		matrix.m00 = 1.0f - 2.0f * ( q.getY() * q.getY() + q.getZ() * q.getZ() );
		matrix.m01 = 2.0f * (q.getX() * q.getY() + q.getZ() * q.getW());
		matrix.m02 = 2.0f * (q.getX() * q.getZ() - q.getY() * q.getW());
		matrix.m03 = 0.0f;
		
		// Second row
		matrix.m10 = 2.0f * ( q.getX() * q.getY() - q.getZ() * q.getW() );
		matrix.m11 = 1.0f - 2.0f * ( q.getX() * q.getX() + q.getZ() * q.getZ() );
		matrix.m12 = 2.0f * (q.getZ() * q.getY() + q.getX() * q.getW() );
		matrix.m13 = 0.0f;

		// Third row
		matrix.m20 = 2.0f * ( q.getX() * q.getZ() + q.getY() * q.getW() );
		matrix.m21 = 2.0f * ( q.getY() * q.getZ() - q.getX() * q.getW() );
		matrix.m22 = 1.0f - 2.0f * ( q.getX() * q.getX() + q.getY() * q.getY() );
		matrix.m23 = 0.0f;

		// Fourth row
		matrix.m30 = 0;
		matrix.m31 = 0;
		matrix.m32 = 0;
		matrix.m33 = 1.0f;

		return matrix;
	}


Then in my render() loop, I translate the view based on the vector 'direction'.

Now this all works fine, but it seems like a really inefficient way of doing things.  

So my question is, is there a more efficient way of doing this?  I suppose I could reuse the Quaternion objects, rather than creating them again each time, but I only really halfway understand the maths, so I can't think what else to do to clean it up.

kramer

Ok, I spent a lot of time reading up on quaternions and cameras, etc... and cleaned the code up a lot and made a proper Camera class which does pitch, bearing (yaw), movement, strafing (side-to side movement).  I will paste the whole thing here, and hopefully it will help someone out at some point... if anyone improves on it, please post your improvement (particularly if you can figure out roll or SLERP...)
package org.kramer.graphics;

import java.nio.FloatBuffer;

import org.lwjgl.BufferUtils;
import org.lwjgl.util.vector.Matrix4f;
import org.lwjgl.util.vector.Quaternion;
import org.lwjgl.util.vector.Vector3f;
import org.lwjgl.util.vector.Vector4f;

/**
 * Need to add roll(), lookAt(), and maybe SLERP
 * 
 * @author Craig Henderson
 *
 */
public class Camera
{
    private final static float PI_OVER_180 = 0.0174532925f;            // ...convert between degrees and radians    
	
    private Vector3f position = new Vector3f();
    private Vector3f direction = new Vector3f();
	private FloatBuffer viewMatrixBuffer = BufferUtils.createFloatBuffer(4*4);
    
    private float pitchAngle;
    private float bearingAngle;
    private Quaternion pitch;
    private Quaternion bearing;
    private Quaternion rotation;
    
    public Camera()
    {
		pitch = new Quaternion();
		bearing = new Quaternion();
		rotation = new Quaternion();
		bearingAngle = 0;
		pitchAngle = 0;
    }
    
    /**
     * Constructor with initial direction values
     * @param initialBearing
     * @param initialPitch
     */
    public Camera(float initialBearing, float initialPitch)
    {
		pitch = new Quaternion();
		bearing = new Quaternion();
		rotation = new Quaternion();
		bearingAngle = initialBearing;
		pitchAngle = initialPitch;
    }

	/**
	 * This should be called whenever pitch, bearing, or roll is changed
	 * to recalculate the matrix
	 */
	public void reorient()
	{
		// quaternion multiply is non-commutative (very important)...
		Quaternion.mul(pitch, bearing, rotation);
		
		// orient the camera...
		Matrix4f matrix = convertQuaternionToMatrix4f(rotation);
		matrix.store(viewMatrixBuffer);
		viewMatrixBuffer.rewind();
		
		// calculate direction vector...
		Matrix4f pitchMatrix = convertQuaternionToMatrix4f(pitch);
		Quaternion temp = Quaternion.mul(bearing, pitch, null);
		matrix = convertQuaternionToMatrix4f(temp);
		direction.x = matrix.m20;
		direction.y = pitchMatrix.m21;
		direction.z = matrix.m22;
	}
    
    /**
     * Change the bearing (yaw)
     * @param bearing delta in degrees
     */
    public void bearing(float bearingDelta)
    {
    	bearingAngle += bearingDelta;
		bearing.setFromAxisAngle(new Vector4f(0f, 1f, 0f, bearingAngle * PI_OVER_180));
    }
    
    /**
     * Change the pitch
     * @param pitch delta in degrees
     */
    public void pitch(float pitchDelta)
    {
    	pitchAngle += pitchDelta;
		pitch.setFromAxisAngle(new Vector4f(1f, 0f, 0f, pitchAngle * PI_OVER_180));
    }
    
//    /**
//     * Change direction to focus on a certain point in the world
//     * @param position
//     */
//    public void lookAt(Vector3f position)
//    {
//    	GLU.glLookAt
//    }
    
    /**
     * Move in the direction of view by a certain amount
     * @param units
     */
    public void move(float units)
    {
    	position.x += direction.x * units;
    	position.y += direction.y * units;
    	position.z += direction.z * units;
    }
    
    /**
     * Move side to side
     * @param units
     */
    public void strafe(float units)
    {
    	Vector3f up = new Vector3f(0,1,0);		// ...I would have made this a constant, except that if ever i do roll(), it won't be 
    	Vector3f cross = Vector3f.cross(direction, up, null);
    	
    	position.x += cross.x * units;
    	position.y += cross.y * units;
    	position.z += cross.z * units;
    }

	/**
	 * @return the camera's position in the world
	 */
	public Vector3f getPosition()
	{
		return position;
	}

    /**
     * Place camera at a certain position
     * @param position
     */
	public void setPosition(Vector3f position)
	{
		this.position = position;
	}

	public Vector3f getDirection()
	{
		return direction;
	}

	/**
	 * Call GL11.glMultMatrix() on this matrix in your render loop to
	 * set the camera's view.
	 * @return buffer of a 4x4 matrix for the view transformation
	 */
	public FloatBuffer getViewMatrixBuffer()
	{
		return viewMatrixBuffer;
	}
    
	private static Matrix4f convertQuaternionToMatrix4f(Quaternion q)
	{
		Matrix4f matrix = new Matrix4f();
		matrix.m00 = 1.0f - 2.0f * ( q.getY() * q.getY() + q.getZ() * q.getZ() );
		matrix.m01 = 2.0f * (q.getX() * q.getY() + q.getZ() * q.getW());
		matrix.m02 = 2.0f * (q.getX() * q.getZ() - q.getY() * q.getW());
		matrix.m03 = 0.0f;
		
		// Second row
		matrix.m10 = 2.0f * ( q.getX() * q.getY() - q.getZ() * q.getW() );
		matrix.m11 = 1.0f - 2.0f * ( q.getX() * q.getX() + q.getZ() * q.getZ() );
		matrix.m12 = 2.0f * (q.getZ() * q.getY() + q.getX() * q.getW() );
		matrix.m13 = 0.0f;

		// Third row
		matrix.m20 = 2.0f * ( q.getX() * q.getZ() + q.getY() * q.getW() );
		matrix.m21 = 2.0f * ( q.getY() * q.getZ() - q.getX() * q.getW() );
		matrix.m22 = 1.0f - 2.0f * ( q.getX() * q.getX() + q.getY() * q.getY() );
		matrix.m23 = 0.0f;

		// Fourth row
		matrix.m30 = 0;
		matrix.m31 = 0;
		matrix.m32 = 0;
		matrix.m33 = 1.0f;

		return matrix;
	}

}


...after calling pitch() or bearing() in your input loop (eg: mouse move), call reorient() to recalculate the quaternions and view matrix. then in your render loop do somethign like this:
    	GL11.glLoadIdentity();                                      // Reset The Current Modelview Matrix
		GL11.glMultMatrix(camera.getViewMatrixBuffer());
     	GL11.glTranslatef(-camera.getPosition().x, -camera.getPosition().y, camera.getPosition().z);  


Hope someone finds this useful  :)