Computer graphics -- 2007-2008 -- info.uvt.ro/Laboratory 4

From Wikiversity
Important! These pages are somehow outdated and it is recommended to consult the newer version at Computer graphics -- 2008-2009 -- info.uvt.ro (by Marc Frâncu).

Quick links: front; laboratories agenda, 1, 2, 3, 4, 5, 6, 7, 8, evaluation, tools, repository.


Using the GLU library[edit]

As mentioned before OGL only provides methods for basic operations and deals mostly with primitives when regarding scene objects. This has lead to the development of several libraries and toolkits that are built on top of OpenGL and provide helper methods for drawing complex objects, dealing with projections, etc. One of them is the GLU.

In C we can use this library by just calling the libraries functions (they all have the prefix glu), but in Java we must create a GLU class instance and use it's methods. Further more we could cache the instance in one of our member variables, in order to minimize the object proliferation.

So we should just update the initializeJogl method accordingly, and we are ready to use the methods of the GLU inside our application.

Java API[edit]

Examples[edit]

[...]
	private void initializeJogl()
	{
		[...]
		
		this.glu = new GLU();
	}
	
	[...]
	
	private GLU glu;
}

View-volume and projection[edit]

In previous laboratories we have assumed and used a 2D view through the orthographic projection (by using the glOrtho method). However, the power of OpenGL comes out when it is used in 3D graphics, and creating and setting up a 3D scene is simply a matter of defining the view volume and setting a perspective projection with the aid of the following methods:

  • glFrustum(left, right, bottom, top, near, far) -- from GL instances;
  • gluPerspective(fovy, aspect, near, far) -- from GLU instances;

Links[edit]

Java API[edit]

Example[edit]

After we understand how these functions work, all we have to do in order to use them is to modify our init and reshape methods accordingly:

[...]
	public void init(GLAutoDrawable canvas)
	{
		GL gl = canvas.getGL();
		
		[...]
		
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glLoadIdentity();
		glu.gluPerspective (38, 1, 3, 5);
		
		[...]
	}
	
	public void reshape(GLAutoDrawable canvas, int left, int top, int width, int height)
	{
		GL gl = canvas.getGL();
		
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glLoadIdentity();
		
		double ratio = (double) width / (double) height;
		glu.gluPerspective (38, ratio, 3, 5);
		
		gl.glViewport(0, 0, width, height);
	}
[...]

Shading and depth test[edit]

In general, in order to obtain better visual effects, we could set the shading model to GL_SMOOTH. Also in order for our objects to obey the projection laws and cover (hide) one another -- based on the position relative to the viewer -- we should enable the GL_DEPTH_TEST option and set the depth test function.

In order to accomplish the previously tasks we update the init method accordingly.

Links[edit]

Java API[edit]

Example[edit]

[...]
	public void init(GLAutoDrawable canvas)
	{
		GL gl = canvas.getGL();
		
		[...]
		
		gl.glShadeModel(GL.GL_SMOOTH);
		
		gl.glEnable(GL.GL_DEPTH_TEST);
		gl.glDepthFunc(GL.GL_LESS);
		
		[...]
	}
[...]

Additionally when the depth test is enabled we must also clear the GL_DEPTH_BUFFER each time the scene is redrawn:

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

Lights[edit]

Before proceeding with the drawing of 3D objects we must be familiar with the concept of lighting and materials.

Light is what makes our scene (the objects) visible and has an important role in creating the illusion of 3D through the way in which vertices are lit. For example in the case of a sphere we might have a lighter area, corresponding to the point where the intensity of the light is greatest, and a diminishing effect towards the outside of that area. Of course this is also influenced by the materials used when drawing the objects (more on materials will come bellow). Without lights our objects will look like plain 2D objects!

The light that reaches your eye from the polygon surface arrives by four different mechanisms -- thus different light types:

  • ambiental light -- the light that comes from all directions equally and is scattered in all directions equally by the polygons in our scene;
  • diffuse light -- the light that comes from a particular (distant) point source (a star or candle) and hits surfaces with an intensity that depends on whether they face towards the light or away from it; this light is responsible for defining the 3D shapes of the objects;
  • specular light -- the light that comes from a point source, but it is reflected in the manner of a mirror where most of the light bounces off in a particular direction defined by the surface's shape; it is best used to describe whether a surface is shinny or dull;
  • emission light -- the light actually emitted by the surface equally in all directions;

Also in order to use lights first we have to be enable the feature with glEnable(GL_LIGHTING); however, this is not enough as we must still define and enable each particular light used inside a scene (at least 8 are supported on all video cards). This can be done using glLightfv which allows us to specify:

  • the light involved;
  • the light type -- there are 10 different possible values, and you can find them at: [1];
  • the values for the light RGB channels.

Links[edit]

Java API[edit]

Example[edit]

The following code shows how we can put everything together:

  • updating the init method to enable lighting;
  • updating the display method to define the lights with certain characteristics;

Important: When lighting is enabled, OpenGL suddenly needs to know the orientation of the surface at each polygon vertex. You need to call glNormal3f for each vertex to define the normals of the vertices.

[...]
	
	public void init(GLAutoDrawable canvas)
	{
		GL gl = canvas.getGL();
		
		[...]
		
		gl.glEnable(GL.GL_LIGHTING);
		gl.glEnable(GL.GL_LIGHT0);
		gl.glEnable(GL.GL_LIGHT1);
		gl.glEnable(GL.GL_LIGHT2);
		
		[...]
	}
	
[...]
	
	public void display(GLAutoDrawable canvas)
	{
		GL gl = canvas.getGL();
		
		[...]
		
		gl.glLightfv(GL.GL_LIGHT0, GL.GL_AMBIENT, new float [] {0.2f, 0.0f, 0.0f, 1f}, 0);
		
		[...]
		
		gl.glLightfv(GL.GL_LIGHT1, GL.GL_SPECULAR, new float [] {0.9f, 0.9f, 0.9f, 1f}, 0);
		gl.glLightfv(GL.GL_LIGHT1, GL.GL_POSITION, new float [] {0, 0, 0, 1f}, 0);
		
		gl.glLightfv(GL.GL_LIGHT2, GL.GL_SPECULAR, new float [] {0.9f, 0.9f, 0.9f, 1f}, 0);
		gl.glLightfv(GL.GL_LIGHT2, GL.GL_POSITION, new float [] {0f, 0f, 0f, 1f}, 0);
		
		[...]
	}
	
[...]

Materials[edit]

As said before materials and lights are essential to each other, and there are some important things we need to know before proceeding:

  • the glColor commands are not useful (or usable) when lighting is enabled if certain measures are not taken;
  • if however we want to use this command it should be done after we have enabled GL_COLOR_MATERIAL by using glEnable;
  • the properties of COLOR_MATERIAL can be set by using glColorMaterial(GLenum face, GLenum mode) which causes a material color to track the current color;
  • now we can safely use the glColor commands;

On the other hand we can use the other alternative by calling glMaterial commands before actually drawing the object, and thus being able to setup color and light handling.

Links[edit]

Java API[edit]

Examples[edit]

Enabeling the GL_COLOR_MATERIAL):

[...]
	public void init(GLAutoDrawable canvas)
	{
		GL gl = canvas.getGL();
		
		[...]
		
		gl.glEnable(GL.GL_COLOR_MATERIAL);
		gl.glColorMaterial(GL.GL_FRONT_AND_BACK, GL.GL_AMBIENT_AND_DIFFUSE);
		
		[...]
	}
[...]

Using glMaterial to draw a yellow sphere:

[...]
	public void display(GLAutoDrawable canvas)
	{
		GL gl = canvas.getGL();
		
		[...]
			
		gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT, new float [] {0.8f, 0.8f, 0.0f, 1f}, 0);
		gl.glMaterialfv(GL.GL_FRONT, GL.GL_DIFFUSE, new float [] {0.8f, 0.8f, 0.0f, 1f}, 0);
		gl.glMaterialfv(GL.GL_FRONT, GL.GL_SPECULAR, new float [] {0.8f, 0.8f, 0.0f, 1f}, 0);
		gl.glMaterialfv(GL.GL_FRONT, GL.GL_EMISSION, new float [] {0.5f, 0.5f, 0f, 1f}, 0);
		
		GLUquadric sun = glu.gluNewQuadric ();
		glu.gluSphere (sun, 0.3, 32, 32);
		glu.gluDeleteQuadric (sun);
		
		[...]	
	}
[...]

Drawing simple 3D objects[edit]

By using GLU library[edit]

Now that we know about setting up the view-volume, lights and materials for a 3D scenes we can begin drawing some basic shapes such as spheres, toruses, pyramids, etc. In order to achieve this we will make use of the GLU library.

You can also notice the use of two new methods called glPushAttrib which takes one parameter: a mask that indicates which groups of state variables to save on the attribute stack, and glPopAttrib. More information here. In our example we store the GL_CURRENT_BIT which contains data about:

  • current RGBA color;
  • current color index;
  • current normal vector;
  • current texture coordinates;
  • current raster position;
  • etc.;

For now we can omit doing this, but it will be helpful when we shall put a texture on our glowing sphere.

The following code draws a sphere using the GLU method gluSphere:

[...]
	public void display(GLAutoDrawable canvas)
	{
		
		[...]
		
		GLUquadric sun = glu.gluNewQuadric ();
		glu.gluSphere (sun, 0.3, 32, 32);
		glu.gluDeleteQuadric (sun);
		
		[...]
	}
[...]

The next example shows how we can simulate glowing using the GL_EMISSION constant applied to the glMaterial method:

[...]
	public void display(GLAutoDrawable drawable) 
	{
		[...]
		
		float sun_glow[] = {1.7f, 1.5f, 1.0f, 1.0f};
		gl.glPushMatrix();
		gl.glPushAttrib(GL_CURRENT_BIT);
			gl.glMaterialfv(GL_FRONT, GL_EMISSION, sun_glow, 0);    
			gl.glRotated(90.0, 1.0, 0.0, 0.0);
			gl.glColor3d(1, 1, 1);
			GLUquadric q = glu.gluNewQuadric();
			glu.gluQuadricDrawStyle(q, GLU_FILL);
			glu.gluQuadricNormals(q, GLU_SMOOTH);
			glu.gluSphere(q, 34, 40, 40);
			glu.gluDeleteQuadric(q);
		gl.glPopAttrib();
		gl.glPopMatrix();
		
		[...]
	}
[...]

By using just the OpenGL primitives[edit]

Ok, so we've painted a sphere using GLU. But this is not the only solution! We can always use OpenGL lists to paint complex objects.

Java API[edit]

Example[edit]

The following example will draw a torus made of vertices. We could observe that we need to set the normals for each of them explicitly when lighting is enabled! (Code is taken from here).

[...]
	private void drawTorus(GL gl, double R, double r, int N, int n)
	{
		double rr = 1.5 * r;
		
		double dv = 2 * Math.PI / n;
		double dw = 2 * Math.PI / N;
		
		double v = 0.0;
		double w = 0.0;
		
		while(w < (2 * Math.PI + dw))
		{
			v = 0.0f;
			gl.glBegin(GL.GL_TRIANGLE_STRIP);
			
			while (v < (2*Math.PI + dv))
			{
				// Set normal for vertex
				gl.glNormal3d((R + rr * Math.cos(v)) * Math.cos(w) - (R + r * Math.cos(v)) * Math.cos(w),
					(R + rr * Math.cos(v)) * Math.sin(w) - (R + r * Math.cos(v)) * Math.sin(w),
					(rr * Math.sin(v) - r * Math.sin(v)));
				
				// Set vertex
				gl.glVertex3d((R + r * Math.cos(v)) * Math.cos(w),
					(R + r * Math.cos(v)) * Math.sin(w),
					r * Math.sin(v));
				
				// Set normal for vertex 
				gl.glNormal3d((R + rr * Math.cos(v + dv)) * Math.cos(w + dw) - (R + r * Math.cos(v + dv)) * Math.cos(w + dw),
					(R + rr * Math.cos(v + dv)) * Math.sin(w + dw) - (R + r * Math.cos(v + dv)) * Math.sin(w + dw),
					rr * Math.sin(v + dv) - r * Math.sin(v + dv));
				
				// Set vertex
				gl.glVertex3d((R + r * Math.cos(v + dv)) * Math.cos(w + dw),
					(R + r * Math.cos(v + dv)) * Math.sin(w + dw),
					r * Math.sin(v + dv));
				
				v += dv;
			}
			
			gl.glEnd();
			
			w += dw;
		}
	}
[...]
[...]

	public void init(GLAutoDrawable canvas)
	{
		GL gl = canvas.getGL();
		
		[...]
		
		gl.glNewList(1, GL.GL_COMPILE);
		this.drawTorus(gl, 0.5, 0.1, 8, 8);
		gl.glEndList();
		
		[...]
	}
[...]
[...]
public void display(GLAutoDrawable drawable) 
{
	[...]
	
	// Draw the torus by calling the list we created in the init method
	gl.glCallList(1);
}
[...]

Camera movement[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 [2];) the basic ideas 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(heading * PI_OVER_180) * 0.1f; // Move On The X-Plane Based On Player Direction
		zpos -= (float)Math.cos(heading * PI_OVER_180) * 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(walkbiasangle * PI_OVER_180)/20.0f; // Causes The Player To Bounce
	}
	
	if (keys[KeyEvent.VK_DOWN]) {
		
		xpos += (float)Math.sin(heading * PI_OVER_180) * 0.1f; // Move On The X-Plane Based On Player Direction
		zpos += (float)Math.cos(heading * PI_OVER_180) * 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(walkbiasangle * PI_OVER_180)/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 reorientate 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 reorientate and reposition the camera each time. The arguments for the gluLookAt method are:

  • eyeX, eyeY, eyeZ -- specifies the position of the eye point (camera position);
  • centerX, centerY, centerZ -- specifies the position of the reference point (center of the scene);
  • upX, upY, upZ -- specifies the direction of the up vector (camera orientation);

The following code snippet shows how we can define methods for handling the moving and aiming of the camera. An additional polarToCartesian method is used to convert polar 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.

// Define camera variables
float cameraAzimuth = 1.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(GL 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 radian_correction = 3.14159265358979323846f / 180.0f;
	float[] result = new float[3];
	float x, y, z;
	
	// Do x-z calculation
	float theta = (90 - azimuth) * radian_correction;
	float tantheta = (float) Math.tan(theta);
	float radian_alt = altitude * radian_correction;
	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;
	
	result[0] = x;
	result[1] = y;
	result[2] = z;
	
	return result;
}

public void mouseDragged(MouseEvent event)
{
	float MOUSE_SENSITIVITY = 0.05f;
	
	double deltaX = (double) event.getX() - screenCenterX;
	double deltaY = (double) event.getY() - screenCenterY;
	
	// Set the mouse position to be the center of the screen
	// Using GLUT in C we could do glutWarpPointer(screenCenterX, screenCenterY);
	event.translatePoint(event.getX() + deltaX, event.getY() + deltaY);
	
	deltaX = (double)event.getX() - screenCenterX;
	deltaY = (double)event.getY() - screenCenterY;
	
	if (event.getButton() == event.BUTTON1) {
		cameraElevation += deltaY * MOUSE_SENSITIVITY;
		cameraAzimuth += deltaX * MOUSE_SENSITIVITY;
	}
	
	if (event.getButton() == event.BUTTON2)
		cameraSpeed += - deltaY * MOUSE_SENSITIVITY;
	
	if (cameraAzimuth > 359)
		cameraAzimuth = 1;
	
	if (cameraAzimuth < 1)
		cameraAzimuth = 359;
}

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


	
	aimCamera(gl, glu);
	moveCamera();
	
	[...]
}

Assignment[edit]

This is the forth assignment, so please commit it to the folder assignment-04.

For this laboratory you will have to create a 3D scene which simulates our inner solar system. It will consist of the following:

  • a (dim) ambient light set to white;
  • a sphere (with the radius 20) in the origin of the coordinate system that will be covered with a yellow glowing material (this is the Sun);
  • a specular light set at coordinates (0, 0, 0) -- thus the Sun illuminates the other planets;
  • four smaller spheres (with the radius: 3, 8, 8, and 4) that will rotate around the main yellow sphere in the XoZ plane; the four spheres will be colored with brown, orange, blue and red; (these are the Mercury, Venus, Earth, and Mars planets);
  • additionally, another sphere (with the radius 2) will rotate around the Earth;
  • the user must have the ability to navigate inside the scene using the arrow keys:
    • pressing the left and right keys will make the camera look at left or right;
    • pressing the up and down keys will make the camera zoom in or zoom out;
    • the camera will be originally placed at the coordinates (0, 0, 20) -- if you choose to implement the 2nd camera placing solution;

For bonus points you must:

  • make the entire scene rotate around the X axis when dragging the mouse;
  • make the spheres also rotate around themselves (like Earth is doing around its own axis);
  • add collision detection for the camera; when you approach to much of a certain sphere (you touch its surface) the camera needs to stop, and you must not be allowed to continue moving inside the sphere;