This shows several features
To start with, the vector class ( the source code is here ). This is a simple thing, offering some simple vector operations:
package org.waltermilner;
public class WMVector {
private float x, y, z;
// for debugging..
public void show(String string) {
System.out.println(string + ": x=" + x + " y=" + y + " z=" + z);
}
public float getX() {
return x;
}
public float getY() {
return y;
}
public float getZ() {
return z;
}
public WMVector(float x, float y, float z) {
this.x = x;
this.y = y;
this.z = z;
}
// scalar multiplication
public WMVector times(float scalar) {
WMVector temp = new WMVector(x * scalar, y * scalar, z * scalar);
return temp;
}
// cross product
public WMVector cross(WMVector b) {
WMVector temp = new WMVector(y * b.z - z * b.y, z * b.x - x * b.z, x * b.y - y * b.x);
return temp;
}
public WMVector add(WMVector other) {
WMVector temp = new WMVector(x + other.x, y + other.y, z + other.z);
return temp;
}
// subtraction
public WMVector sub(WMVector other) {
WMVector temp = new WMVector(x - other.x, y - other.y, z - other.z);
return temp;
}
// return magnitude of this vector
public float getMag() {
float mag = x * x + y * y + z * z;
mag = (float) Math.sqrt(mag);
return mag;
}
// return a vector with same direction as this, but with the given magnitude
public WMVector setMag(float m) {
float mag = getMag();
WMVector temp = new WMVector(x, y, z);
temp = temp.normalise();
temp = temp.times(mag);
return temp;
}
// return a vector of unit magnitude, parallel to this
public WMVector normalise() {
float mag = x * x + y * y + z * z;
mag = (float) Math.sqrt(mag);
WMVector temp = new WMVector(x / mag, y / mag, z / mag);
return temp;
}
}
Then to use this for moving around ( the complete source code is here ). We declare some vector attributes:
private WMVector camera = new WMVector(1.0f, 3.0f, 5.0f);
private WMVector lookAt = new WMVector(0.0f, 0.0f, 0.0f);
private WMVector up = new WMVector(0.0f, 1.0f, 0.0f);
and use these in display as
.. final GL gl = gLDrawable.getGL(); gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT); gl.glLoadIdentity();// Reset The View glu.gluLookAt(camera.getX(), camera.getY(), camera.getZ(), lookAt.getX(), lookAt.getY(), lookAt.getZ(), up.getX(), up.getY(), up.getZ()); glut.glutSolidSphere(1.0, 20, 20); ..
We are using vectors in two different ways here. The camera vector defines the position of the camera, as a vector with one end at (0,0,0) and the other at the camera. Similarly lookAt defines the location of the point the camera is looking at. But the vector called up is not a position, but a direction. It defines which way is up as far as the camera is concerned.
For movement, we have 3 floats determining the forward speed of the camera, and the rate it is rotating 'upwards' and 'sideways', in relation to the up vector:
private float forwardSpeed = 0.0f;
private float rotSpeed = 0.0f;
private float pitchSpeed = 0.0f;
Now we need methods to move 'forward' and rotate the camera. Moving forwards is easiest. We have to work out which way is forward. Then add a bit of that vector to camera - which will move the camera forward. The size of the bit is determined by the forward speed. We also move the lookAt point forward the same amount:
// move the camera forward, in the direction it is looking at
private void moveForward() {
WMVector lookDir = lookAt.sub(camera).normalise();
WMVector movement = lookDir.times(forwardSpeed);
camera = camera.add(movement);
lookAt = lookAt.add(movement);
}
The line WMVector lookDir = lookAt.sub(camera).normalise(); is like in vector notation lookDir = lookAt - camera so it makes a vector from the camera to the point we are looking at. But it also normalises it. Otherwise if we were further away we'd move more which would seem faster.
We work out how far to move with a scalar multiplication:
movement = forwardspeed * lookDir
Then move the camera and the lookAt point that amount. We do not need a moveBack, since if forwardSpeed is negative, we will move back.
To turn upwards is a bit trickier. We could do a proper vector rotation, but this code does it by finding the lookDir vector, then adding a bit of the up vector to it, putting it back to its original length, and moving the lookAt point to the end of it. We also need to tip the up vector back. We do this by forming the cross product of the initial lookDir and up (which is called sideways); then the cross product of sideways and the new lookDir, so it will be a right angles to the new forward direction:
// change the lookAt point to look up
private void lookUp() {
// make vector from where we are to point we are looking at
WMVector lookDir = lookAt.sub(camera);
// find magnitude of this = how far away we are
float lookMag = lookDir.getMag();
// vector at right angle to lookdir and up - needed to reset up
WMVector sideWays = up.cross(lookDir);
// form vector temp = lookDir + 0.05up
WMVector temp = lookDir.add(up.times(pitchSpeed));
// temp is new lookDir - restore magnitude
temp = temp.setMag(lookMag);
// set lookAt =camera + temp
lookAt = camera.add(temp);
lookDir = lookAt.sub(camera);
// make sure up is at right angles to new look at
up = lookDir.cross(sideWays);
up = up.normalise();
}
Again if pitchSpeed is negative, we go nose down.
To turn sideways is similar:
// change the lookAt point to look right
private void turnRight() {
// make vector from where we are to point we are looking at
WMVector lookDir = lookAt.sub(camera);
// find magnitude of this = how far away we are
float lookMag = lookDir.getMag();
// form vector at right angles to camera up and direction we are looking
WMVector c = up.cross(lookDir);
// form vector temp = lookDir - rotSpeed * c
WMVector temp = lookDir.sub(c.times(rotSpeed));
// alter magnitude to what it was
temp = temp.setMag(lookMag);
// set lookAt =camera + temp
lookAt = camera.add(temp);
}
In the display method, we might modify the forward speed. If we are moving forward, we slow down a bit, and if backwards, we make it less negative. If we are going very slowly, we just stop. And so long as we nave not stopped, we moveForward():
boolean stopped;
if (forwardSpeed > 0.0f) {
forwardSpeed -= 1e-6f;
}
if (forwardSpeed < 0.0f) {
forwardSpeed += 1e-5f;
}
if (forwardSpeed < 1e-8 && forwardSpeed > -1e-8) {
stopped = true;
} else {
stopped = false;
}
if (!stopped) {
moveForward();
}
There is very similar code for the rotSpeed and the pitchSpeed.
For keyboard control, we maintain flags for the keys we are interested in:
// key states
boolean keyA = false; // means not currently down
boolean keyD = false;
boolean keyW = false;
boolean keyS = false;
boolean keyE = false;
boolean keyF = false;
And in the keyListener interface, modify this when the keys go down and up..
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_W:
keyW = true;
break;
case KeyEvent.VK_S:
keyS = true;
break;
..
public void keyReleased(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_W:
keyW = false;
break;
case KeyEvent.VK_S:
keyS = false;
break;
Then in the start of the display method, we process the key states..
private void processKeys() {
if (keyA) {
rotSpeed -= 1e-4f;
}
if (keyD) {
rotSpeed += 1e-4f;
}
if (keyW) {
forwardSpeed += 1e-4f;
}
For mouse control, we register this as listeners..
public void init(GLAutoDrawable gLDrawable) {
final GL gl = gLDrawable.getGL();
..
gLDrawable.addKeyListener(this);
gLDrawable.addMouseMotionListener(this);
gLDrawable.addMouseWheelListener(this);
}
Then we implement mouseDragged and mouse wheel moved (mouseX and mouseY are attributes..
public void mouseDragged(MouseEvent e) {
int x = e.getX();
int y = e.getY();
if (x > mouseX) {
rotSpeed += 1e-4f;
}
if (x < mouseX) {
rotSpeed -= 1e-4f;
}
if (y < mouseY) {
pitchSpeed += 1e-4f;
}
if (y > mouseY) {
pitchSpeed -= 1e-4f;
}
mouseX = x;
mouseY = y;
}
..
public void mouseWheelMoved(MouseWheelEvent e) {
int notches = e.getWheelRotation();
forwardSpeed += notches * 1e-3f;
}