Computer graphics/2013-2014/Laboratory 7

Quick links: front; laboratories agenda, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, JOGL template.


USER CONTROL

edit

Keyboard and mouse control

edit

User input is very important especially in interactive graphic applications such as games or simulators by allowing the user to control the camera or interact with objects in the scene in real time. Usually this is done through the use of a keyboard and a mouse, and occasionally through a joystick, microphone (voice commands), or other input device.

If we want to be informed of keyboard an mouse events we have to register specific listeners besides the GLEventListener:

  • KeyListener -- used to inform us of keyboard events
  • MouseListener -- used to inform us of mouse clicks
  • MouseMotionListener -- used to inform us of mouse movement

These issues are AWT specific, thus they are assumed to be known. You could find more information at the following links:

Please pay attention as the mouse coordinates are given in the view-port coordinates -- that is how we obtain the number of pixels from left and top -- and we must transform them in the object coordinates.

Important: OpenGL applications require the user to handle keyboard and mouse events in a different thread that the one on which it is performing the OpenGL operations as these operations cannot occur directly inside the mouse or keyboard handlers. A mouse or keyboard listener may invoke the GLDrawable instance's repaint method thus forcing the redisplay.

Links:

In order to enable your JOGL application to handle mouse or keyboard events you need to implement several interfaces:

  • KeyListener
    • keyPressed method
    • keyReleased method
    • keyTyped method
  • MouseListener
    • mousePressed method
    • mouseReleased method
    • mouseClicked method
    • mouseEntered method
    • mouseExited method
  • MouseMotionListener
    • mouseMoved method
    • mouseDragged method

The following fragment of code exemplifies the previous:

import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

public class MainFrame
		extends JFrame
		implements
				GLEventListener,
				// These are the new interfaces one needs to implement
				KeyListener,
				MouseListener,
				MouseMotionListener
{
	
	private void initializeJogl()
	{
		[...]

		// Adding the keyboard and mouse event listeners to the canvas.
		this.canvas.addKeyListener(this);
		this.canvas.addMouseListener(this);
		this.canvas.addMouseMotionListener(this);

		[...]
	}
						
	public void keyPressed(KeyEvent event)
	{
		return;
	}
	
	public void keyReleased(KeyEvent event)
	{
		return;
	}
	
	public void keyTyped(KeyEvent event)
	{
		return;
	}
	
	public void mousePressed(MouseEvent event)
	{
		return;
	}
	
	public void mouseReleased(MouseEvent event)
	{
		return;
	}
	
	public void mouseClicked(MouseEvent event)
	{
		return;
	}
	
	public void mouseMoved(MouseEvent event)
	{
		return;
	}
	
	public void mouseDragged(MouseEvent event)
	{
		return;
	}
	
	public void mouseEntered(MouseEvent event)
	{
		return;
	}
	
	public void mouseExited(MouseEvent event)
	{
		return;
	}
}

MORE ON MODELVIEW MANAGEMENT

edit

The scene in (J)OGL is manipulated by using the ModelView Matrix Besides this matrix the Projection Matrix is used for defining the viewing volume.

Revisit Computer graphics -- 2013-2014 -- info.uvt.ro/Laboratory 2 for more details.

Next we will learn about how we can transform our scene and more precisely how we can move, scale or rotate objects. To do this first we need to remember that (J)OGL uses 4x4 matrices to store scene information. We can always choose to save the current matrix by using the glPushMatrix() method (function) which pushes the current matrix on the stack. In the same way calling glPopMatrix() method (function) retrieves the last saved matrix. This is useful for example when we want to draw planet Earth in orbit and then draw the Moon around it.

Model

edit

Transformations: Rotation, Translation, Scaling

edit

Model transformations involve rotating, scaling or translating objects in your scene.

IMPORTANT: One has to pay attention to the order these transformations are applied as a translation after a rotation does not produce the same effect as a rotation followed by a translation.

(J))OGL puts at the programmer's disposal a couple of very intuitive methods (functions) for handling object transformations: glRotate*(angle, x, y, z), glScale*( factorX, factorY, factorZ) (for values inside the (0-1) interval the object gets scaled down) and glTranslate*(x, y, z).

IMPORTANT: In (J)OGL the operations need to be placed in the code in the opposite order than in reality. For example a translation followed by a rotation in real life, is represented in (J)OGL as a rotation followed by a translation. See this for details.

For example the next fragment of code places a sphere at the center of the screen and makes another one rotate around it. It also explains how glPushMatrix and glPopMatrix methods (functions) work:

	float angle = 0.0f;

	public void display(GLAutoDrawable canvas)
	{
		GL gl = canvas.getGL();
 
		gl.glClear(GL.GL_COLOR_BUFFER_BIT);
		gl.glClear(GL.GL_DEPTH_BUFFER_BIT);

		// Start with a fresh transformation i.e. the 4x4 identity matrix.
		gl.glLoadIdentity();
		
		// Save (push) the current matrix on the stack.
		gl.glPushMatrix();

		// Translate the first sphere to coordinates (0,0,0).
		gl.glTranslatef (0.0f, 0.0f, 0.0f);
		// Then draw it.
		this.drawSphere(gl, glu, 0.5f, false);

		// Save (push) on the stack the matrix holding the transformations produced by translating the first sphere. 
		gl.glPushMatrix();

		// NOTE THE FOLLOWING ORDER OF OPERATIONS. THEY ACHIEVE A TRANSLATION FOLLOWED BY A ROTATION IN REALITY.

		// Rotate it with angle degrees around the X axis.
		gl.glRotatef (angle, 1, 0, 0);
		// Translate the second sphere to coordinates (10,0,0).
		gl.glTranslatef (10.0f, 0.0f, 0.0f);
		// Scale it to be half the size of the first one.
		gl.glScalef (0.5f, 0.5f, 0.5f);
		// Draw the second sphere.
		this.drawSphere(gl, glu, 0.5f, false);

		// Restore (pop) from the stack the matrix holding the transformations produced by translating the first sphere.
		gl.glPopMatrix();

		// Restore (pop) from the stack the matrix holding the transformations prior to our translation of the first sphere. 
		gl.glPopMatrix();

		gl.glFlush();

		// Increase the angle of rotation by 5 degrees.
		angle += 5;
	}

	private void drawSphere(GL gl, GLU glu, float radius, boolean texturing)
	{

		if (texturing)
		{
			this.texture.bind();
			this.texture.enable();
		}

		GLUquadric sphere = glu.gluNewQuadric();
		if (texturing)
		{
	        	// Enabling texturing on the quadric.
			glu.gluQuadricTexture(sphere, true);
		}
		glu.gluSphere(sphere, radius, 64, 64);
		glu.gluDeleteQuadric(sphere);
	}

Viewing

edit

Camera control

edit

An important feature in 3D graphics is the ability to control the camera. This ability allows the user to navigate through the scene, zooming in objects, avoiding others, orienting himself in space, etc.

There are basically two ways of controlling your camera:

  • keeping the camera fixed and moving the world (rotations, translations) to keep the illusion of movement. For details see [1]. The basic ideas for this approach are:
    • rotate the world around the origin in the opposite direction of the camera rotation
    • and translate the world in the opposite manner that the camera has been translated
  • move the camera around and draw the 3D environment relative to coordinate system origin

Solution 1: Translate and Rotate the scene relative to the camera and keep the camera position fixed

edit
 
// The x position
private float xpos;

// The rotation value on the y axis
private float yrot;

// The z position
private float zpos;

private float heading;

// Walkbias for head bobbing effect
private float walkbias = 0.0f;

// The angle used in calculating walkbias */
private float walkbiasangle = 0.0f;

// The value used for looking up or down pgup or pgdown
private float lookupdown = 0.0f;

// Define an array to keep track of the key that was pressed
private boolean[] keys = new boolean[250];

public void keyPressed(KeyEvent ke)
{
	if(ke.getKeyCode() < 250)
		keys[ke.getKeyCode()] = true;
}

public void keyReleased(KeyEvent ke)
{
	if (ke.getKeyCode() < 250)
		keys[ke.getKeyCode()] = false;
}

public void display(GLAutoDrawable drawable)
{
	// Compute rotation and translation angles
	float xTrans = -xpos;
	float yTrans = -walkbias - 0.43f;
	float zTrans = -zpos;
	float sceneroty = 360.0f - yrot;
	
	// Perform operations on the scene
	gl.glRotatef(lookupdown, 1.0f, 0.0f, 0.0f);
	gl.glRotatef(sceneroty, 0.0f, 1.0f, 0.0f);
	
	gl.glTranslatef(xTrans, yTrans, zTrans);	

	// Draw the scene
	[...]
	
	// Check which key was pressed
	if (keys[KeyEvent.VK_RIGHT]) {
		heading -= 3.0f;
		yrot = heading;
	}
	
	if (keys[KeyEvent.VK_LEFT]) {
		heading += 3.0f;
		yrot = heading;
	}
	
	if (keys[KeyEvent.VK_UP]) {
		
		xpos -= (float)Math.sin(Math.toRadians(heading)) * 0.1f; // Move On The X-Plane Based On Player Direction
		zpos -= (float)Math.cos(Math.toRadians(heading)) * 0.1f; // Move On The Z-Plane Based On Player Direction
		
		if (walkbiasangle >= 359.0f)
			walkbiasangle = 0.0f;
		else
			walkbiasangle += 10.0f;
		
		walkbias = (float)Math.sin(Math.toRadians(walkbiasangle))/20.0f; // Causes The Player To Bounce
	}
	
	if (keys[KeyEvent.VK_DOWN]) {
		
		xpos += (float)Math.sin(Math.toRadians(heading)) * 0.1f; // Move On The X-Plane Based On Player Direction
		zpos += (float)Math.cos(Math.toRadians(heading)) * 0.1f; // Move On The Z-Plane Based On Player Direction
		
		if (walkbiasangle <= 1.0f)
			walkbiasangle = 359.0f;
		else
			walkbiasangle -= 10.0f;
		
		walkbias = (float)Math.sin(Math.toRadians(walkbiasangle))/20.0f; // Causes The Player To Bounce
	}
	
	if (keys[KeyEvent.VK_PAGE_UP]) {
		lookupdown += 2.0f;
	}
	
	if (keys[KeyEvent.VK_PAGE_DOWN]) {
		lookupdown -= 2.0f;
	}
}

Solution 2: Use the gluLookAt method to re-orientate and reposition the camera

edit

For those interested we also show the solution for the second variant in which we use the gluLookAt method to re-orientate and reposition the camera. The following code fragment shows how we can define methods for handling the moving and aiming of the camera. An additional the polarToCartesian method is used to convert polar (spherical) coordinates to Cartesian coordinates. The aimCamera and moveCamera methods are being called inside the display method at the beginning just after the glClear method call:

IMPORTANT: By default if azimuth and elevation are set to 0 then the camera is oriented parallel with the positive Z axis towards infinity.

// Define camera variables
float cameraAzimuth = 0.0f, cameraSpeed = 0.0f, cameraElevation = 0.0f;

// Set camera at (0, 0, -20)
float cameraCoordsPosx = 0.0f, cameraCoordsPosy = 0.0f, cameraCoordsPosz = -20.0f;

// Set camera orientation
float cameraUpx = 0.0f, cameraUpy = 1.0f, cameraUpz = 0.0f;

public void moveCamera()
{
	float[] tmp = polarToCartesian(cameraAzimuth, cameraSpeed, cameraElevation);
	
	// Replace old x, y, z coords for camera
	cameraCoordsPosx += tmp[0];
	cameraCoordsPosy += tmp[1];
	cameraCoordsPosz += tmp[2];
}

public void aimCamera(GL2 gl, GLU glu)
{
	gl.glLoadIdentity();
	
	// Calculate new eye vector
	float[] tmp = polarToCartesian(cameraAzimuth, 100.0f, cameraElevation);
	
	// Calculate new up vector
	float[] camUp = polarToCartesian(cameraAzimuth, 100.0f, cameraElevation + 90);
	
	cameraUpx = camUp[0];
	cameraUpy = camUp[1];
	cameraUpz = camUp[2];
	
	glu.gluLookAt(cameraCoordsPosx, cameraCoordsPosy, cameraCoordsPosz,
			cameraCoordsPosx + tmp[0], cameraCoordsPosy + tmp[1],
			cameraCoordsPosz + tmp[2], cameraUpx, cameraUpy, cameraUpz);
}

private float[] polarToCartesian (float azimuth, float length, float altitude)
{
	float[] result = new float[3];
	float x, y, z;
	
	// Do x-z calculation
	float theta = (float)Math.toRadians(90 - azimuth);
	float tantheta = (float) Math.tan(theta);
	float radian_alt = (float)Math.toRadians(altitude);
	float cospsi = (float) Math.cos(radian_alt);
	
	x = (float) Math.sqrt((length * length) / (tantheta * tantheta + 1));
	z = tantheta * x;
	
	x = -x;
	
	if ((azimuth >= 180.0 && azimuth <= 360.0) || azimuth == 0.0f) {
		x = -x;
		z = -z;
	}
	
	// Calculate y, and adjust x and z
	y = (float) (Math.sqrt(z * z + x * x) * Math.sin(radian_alt));
	
	if (length < 0) {
		x = -x;
		z = -z;
		y = -y;
	}
	
	x = x * cospsi;
	z = z * cospsi;
	
	// In contrast we could use the simplest form for computing Cartesian from Spherical as follows:
	// x = (float)(length * Math.sin(Math.toRadians(altitude))*Math.cos(Math.toRadians(azimuth)));
	// y = (float)(length * Math.sin(Math.toRadians(altitude))*Math.sin(Math.toRadians(azimuth)));
	// z = (float)(length * Math.cos(Math.toRadians(altitude)));


	result[0] = x;
	result[1] = y;
	result[2] = z;
	
	return result;
}

public void keyPressed(KeyEvent event)
{ 
	if (event.getKeyCode()== KeyEvent.VK_UP) {
		cameraElevation -= 2;
	}
		
	if (event.getKeyCode()== KeyEvent.VK_DOWN) {
		cameraElevation += 2;
	}
				
	if (event.getKeyCode()== KeyEvent.VK_RIGHT) {
		cameraAzimuth -= 2;
	}

	if (event.getKeyCode()== KeyEvent.VK_LEFT) {
		cameraAzimuth += 2;
	}
		
	if (event.getKeyCode()== KeyEvent.VK_I) {
		cameraSpeed += 0.05;
	}

	if (event.getKeyCode()== KeyEvent.VK_O) {
		cameraSpeed -= 0.05;
	}

	if (event.getKeyCode()== KeyEvent.VK_S) {
		cameraSpeed = 0;
	}

	if (cameraAzimuth > 359)
		cameraAzimuth = 1;
		
	if (cameraAzimuth < 1)
		cameraAzimuth = 359;	 	
}	

public void display(GLAutoDrawable drawable)
{
	gl.glClear(...);

	gl.glLoadIdentity();

	// Always be sure to place these to method calls after glClear and glLoadIdentity are called.
	aimCamera(gl, glu);
	moveCamera();
	
	// Draw the scene.
	[...]
}

Exercises

edit
  • Add three spheres to the scene: the first one should be placed at the center of the scene, the second should rotate around the first one and the third one around the second one (Sun-Earth-Moon system).
    • Each sphere should have a texture applied on it.
    • The second sphere should have two textures on it as follows: the first one represents the earth surface, while the second shows the clouds. The clouds should also move on the sphere surface.
    • Also lighting and materials should be enabled. One ambiental plus a diffuse light near the center of the scene to simulate light from the Sun should exist. Make the Sun glow (emission light).
    • HINT: use two concentric spheres and apply blending to make the clouds mix with the surface.
  • Add camera control to the previous scene so that the user could explore the surroundings. Try both methods described in this laboratory. Which one is more suited?