Computer graphics -- 2007-2008 -- info.uvt.ro/Laboratory 3
Quick links: front; laboratories agenda, 1, 2, 3, 4, 5, 6, 7, 8, evaluation, tools, repository.
Scene manipulation
[edit]Scene units
[edit]An OpenGL scene is composed of scene elements made of OpenGL primitives (vertices, triangles). A question that arises when talking about scenes is what are the units used for object representation. The answer is: anything you want from millimeters to astronomical units. You can even use different units for different objects: km for displaying planets and astronomical units to place these planets into space for example. The ModelView Matrix manages the scaling of all your units to the same unit in the Eye Coordinate Space (see bellow).
Coordinate spaces
[edit]OpenGL uses several coordinate spaces in order to transform the coordinates from the coordinates given by the user to the coordinates used for displaying the scene:
- Object Coordinates -- raw coordinates entered with the glVertex function family;
- Eye Coordinates -- are obtained by the ModelView matrix that contains both modeling and viewing transformations that place the viewer at the origin with the view direction aligned with the negative Z axis, and after transforming the Object Coordinates;
- Clip Coordinates - are obtained by the Projection Matrix after transforming the Eye Coordinates; Clip Coordinate space ranges from -W to W on all three axis (W is the Clip Coordinate W value) and everything outside these boundaries is not displayed (thus it is clipped);
- Normalized Device Coordinates -- Range from -1 to 1 and are obtained after the Perspective division is performed on the Clip Coordinates;
- Window Coordinates -- scaling and translating the Normalized Device Coordinates by the viewport; this gives us the final coordinates that are used to draw the scene objects; you can control this operation by using the glViewport and glDepthRange commands;
You can notice that the typical coordinate transformation workflow goes like this: Object Coordinates -> Eye Coordinates -> Clip Coordinates -> Normalized Device Coordinates -> Window Coordinates.
A special Coordinate System is represented by the World Coordinates that results from transforming Object Coordinates by the modeling transformations (rotations, translations, scalings) stored in the ModelView matrix. Basically the World Coordinates system is different for each application. It is created after the Object Coordinates are rotated, scaled, translated into the world constructed inside the application. (For example planets are translated to their coordinates around the Sun, rotated around their axis to simulate orbit tilt -- inclination -- and also scaled).
In the previous laboratory we set up the projection in the following way:
[...]
gl.glOrtho(0, 1, 0, 1, -1, 1);
[...]
And now if we want to set up the projection so that one world coordinate unit is equal to one screen pixel we could do it the following way inside the init or reshape methods:
[...]
gl.glOrtho(0, windowWidth, 0, windowHeight, -1, 1);
[...]
Important: When setting the Z range from -1 to 1 be sure to use glVertex2* functions to ensure your geometry isn't clipped by the zNear or zFar clipping planes, or if you must use glVertex3* functions be sure to use 0 as the third coordinate.
Links:
Scene transformations
[edit]The scene in OpenGL is manipulated using the ModelView Matrix (Remember from the previous laboratory Computer graphics -- 2007-2008 -- info.uvt.ro/Laboratory 2 that besides this matrix, OpenGL also uses other matrices, such as the Projection Matrix used to store the projection (viewing) information.)
Matrices allow the user to specify all sorts of information. A typical matrix is represented this way:
In JOGL you could define it as:
float[] matrix = new float[16];
Where the elements of the matrix are represented in a one dimensional vector such that the first raw of the matrix are the first four elements in the vector (0 to 3), the second raw are the elements from 4 to 7, and so on.
A very important matrix in OpenGL is the identity matrix which is a matrix that has all the elements equal to 0 except the ones on the primary diagonal:
In OpenGL you can push this matrix into the matrix stack by using the glLoadIdentity function.
Matrices are important because they are handy if we want to perform basic operations on the scene like the following.
Important: we should pay attention to the order in which the operations are executed, as translating and rotating (around a given point) is not the same as first rotating and after that translating (around the same point).
Translation
[edit]The matrix for translation looks like the following:
Where the translation is identified by the vector . For the scene remains unchanged as we obtain the Identity Matrix.
Scaling
[edit]The scaling matrix is identified by the following matrix:
Where the scaling is identified by the vector . For the scene remains unchanged as we obtain the Identity Matrix.
Rotation
[edit]To obtain the rotation one will need three matrices for the 3 spatial coordinates:
The X-axis Rotation:
The Y-axis Rotation:
The Z-axis Rotation:
Where is the angle wish to rotate the scene with.
Important: the rotation is done around the scene origin -- that is the point (0, 0, 0). If we want to rotate around another point we should first translate the origin in that point, and then apply the rotation. (Also all coordinates should take into account the initial translation.
OpenGL functions
[edit]Usually we are not needed to provide the matrices ourselves, as OpenGL provides straightforward mechanisms to manipulate matrices.
The most well known functions are:
- glTranslatef(x, y, z) -- moves the origin of the scene by the given values -- where x, y, z are units of translation and manage the translation on each axis;
- glScalef(x, y, z) -- where x, y, z are scale factors; if you use 1 for each of them no scale will be performed; a value smaller than 1 will mean the object is made smaller, and a number greater of 1 means the object is made bigger;
- glRotatef(angle, x, y, z) -- where angle is the angle in degrees of the rotation and x, y, z the axis on which we want to perform the rotation (0.0 if we do not want to rotate on an particular axis and 1.0 if we want to rotate on it); the rotation will be made around the origin point.
ModelView Matrix stack
[edit]As mentioned before OpenGL uses matrices to represent the scene, but additionally it also uses matrix stacks to ensure, amongst other things, that each object inside the scene will be rendered with its own transformations.
This is done by wrapping the rendering of each object with calls to glPushMatrix and glPopMatrix:
- glPushMatrix -- saves the current state of the ModelView matrix;
- glPopMatrix -- the ModelView matrix is restored to its previous value which was stored by a call to glPushMatrix();
- we should note that when calling push, the current matrix is not modified, so any other further transformations are applied on-top of the current matrix; (if we want to start with a fresh copy we should use glLoadIdentity;)
For example:
gl.glPushMatrix();
gl.glRotatef(90, 1, 0, 0);
drawTriangleGreen();
gl.glPopMatrix();
gl.glPushMatrix();
gl.glTranslatef(0.3f, 0.2f, 0);
drawTriangleRed();
gl.glPopMatrix();
The previous example draws a green triangle rotated with 90 degrees on the X-axis, and a red triangle translated with 0.3 units on the X-axis and 0.2 units on the Y-axis.
Links
[edit]Some useful links that explain what matrices are and how we can perform basic scene operations on them are:
- OpenGL and JOGL:
- Matrices can be your Friends;
- The Matrix and Quaternions FAQ;
- OpenGL -- Transformations;
Java API
[edit]Examples
[edit]In what follows we shall assume that:
- the view volume is -1 to 1 on all the axes;
- the view-port aspect ratio is the same as the view-volume's;
- the only method that differs from the previous laboratory is the display;
We shall present tree different display methods, named display_try_n, that should be called from inside the display method.
Please pay attention to the order and the arguments of the transformation functions.
These examples are also available as a complete, working Eclipse project inside the Subversion repository, under the folder examples/example-02.
display_try_1
[edit]Taking from the previous laboratory what we want to obtain is:
- a triangle defined by the points: (0.5, 0.5); (0, 0) and (1, 0);
- the triangle should rotate around the (0.5, 0.5) point;
As a solution we try only to rotate the triangle around the Z axis with a certain angle:
gl.glRotatef(this.angle, 0, 0, 1);
But this doesn't work, as the triangle will actually revolve arround the (0, 0) point. The problem is that glRotate function rotates only around the specified axes -- that is the revolution point is always (0, 0, 0);
[...]
public void display_try_1(GLAutoDrawable canvas)
{
GL gl = canvas.getGL();
// Erasing the canvas -- filling it with the clear color.
gl.glClear(GL.GL_COLOR_BUFFER_BIT);
// Selecting filling for both the front and back.
gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_FILL);
// Saving the current matrix.
gl.glPushMatrix();
// Rotating the scene around the Z axis.
gl.glRotatef(this.angle, 0, 0, 1);
// Beginning a triangle list.
gl.glBegin(GL.GL_TRIANGLES);
gl.glColor3f(1, 0, 0);
gl.glVertex2f(0.5f, 0.5f);
gl.glColor3f(0, 1, 0);
gl.glVertex2f(0, 0);
gl.glColor3f(0, 0, 1);
gl.glVertex2f(1, 0);
// Ending the triangle list.
gl.glEnd();
// Restoring the previous matrix.
gl.glPopMatrix();
// Forcing the scene to be rendered.
gl.glFlush();
this.angle += 360.0f / 90;
}
[...]
display_try_2
[edit]As a solution to the previous problem before rotating and drawing the triangle, we move the axes origin in the point (0.5, 0.5), rotate, and draw the triangle -- but being careful as the coordinates have changed.
[...]
public void display_try_2(GLAutoDrawable canvas)
{
GL gl = canvas.getGL();
// Erasing the canvas -- filling it with the clear color.
gl.glClear(GL.GL_COLOR_BUFFER_BIT);
// Selecting filling for both the front and back.
gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_FILL);
// Saving the current matrix.
gl.glPushMatrix();
// Translating the scene so that 0, 0
// is the vertex on which we want to rotate.
gl.glTranslatef(0.5f, 0.5f, 0);
// Rotating the scene around the Z axis.
gl.glRotatef(this.angle, 0, 0, 1);
// Beginning a triangle list.
gl.glBegin(GL.GL_TRIANGLES);
gl.glColor3f(1, 0, 0);
gl.glVertex2f(0, 0);
gl.glColor3f(0, 1, 0);
gl.glVertex2f(-0.5f, -0.5f);
gl.glColor3f(0, 0, 1);
gl.glVertex2f(0.5f, -0.5f);
// Ending the triangle list.
gl.glEnd();
// Restoring the previous matrix.
gl.glPopMatrix();
// Forcing the scene to be rendered.
gl.glFlush();
this.angle += 360.0f / 90;
}
[...]
display_try_n
[edit]We would like now to display three revolving triangles with the following properties:
- all of them revolve around the point (0, 0, 0);
- each of them moves with a different speed;
- the more quickly a triangle moves, the smaller it is;
- the slower it moves, the closer is the color to black;
[...]
public void display_try_3(GLAutoDrawable canvas)
{
GL gl = canvas.getGL();
// Erasing the canvas -- filling it with the clear color.
gl.glClear(GL.GL_COLOR_BUFFER_BIT);
// Selecting filling for both the front and back.
gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_FILL);
// Saving the current matrix.
gl.glPushMatrix();
for (int i = 1; i <= 5; i++) {
// Saving the current matrix.
gl.glPushMatrix();
// Setting the color
gl.glColor3f(i / 5.0f, 0, 0);
// Scaling the triangle.
gl.glScalef(1.0f / 5 * (6 - i), 1.0f / 5 * (6 - i), 1.0f / 5 * (6 - i));
// Rotating the scene around the Z axis.
gl.glRotatef(this.angle * i, 0, 0, 1);
// Beginning a triangle list.
gl.glBegin(GL.GL_TRIANGLES);
gl.glVertex2f(0, 0);
gl.glVertex2f(-0.5f, -0.5f);
gl.glVertex2f(0.5f, -0.5f);
// Ending the triangle list.
gl.glEnd();
// Restore the previous matrix.
gl.glPopMatrix();
}
// Restoring the previous matrix.
gl.glPopMatrix();
// Forcing the scene to be rendered.
gl.glFlush();
this.angle += 360.0f / 90;
}
[...]
GLEventListener
[edit]In order to have all the event listeners in one place, we recapitulate and further describe the GLEventListener interface and its purpose.
As seen in the previous laboratory JOGL applications have to implement the GLEventListener interface in order to perform OpenGL operations. When the methods of the GLEventListener interface are called, the underlying OpenGL context associated with the drawable is already made current, and the user fetches the GL object out of the GLAutoDrawable and begins to draw the scene:
- init -- called once, upon context creation;
- display -- called to perform per-frame rendering each time the scene is redrawn;
- reshape -- called when the drawable has been resized; please note that the default implementation automatically resizes the OpenGL viewport so often it is not necessary to do any work in this method; (but usually we should update the view volume to keep the aspect ratio;)
- displayChanged -- allows applications to support on-the-fly screen mode switching; please note that this facility is not yet implemented so the body of this method should be left empty;
OpenGL applications behave in one of two ways:
- repaint only on demand -- when a keyboard or mouse event is triggered for example;
- repaint continually -- done by forcing the call of the display method repeatably, or by using an Animator instance which does this internally;
Important: Always refetch the GL and GLU objects out of the GLDrawable upon each call to the init, display, reshape, and displayChanged methods, and pass the GL object down on the call stack to any drawing routines, as opposed to storing the GL in a field and referencing it from there. The reason is that multi-threading issues inherent to the AWT toolkit make it difficult to reason about which threads certain operations are occurring on, and if the GL object is stored in a field it is unfortunately too easy to accidentally make OpenGL calls from a thread that does not have a current context, and this will usually cause the application to crash.
KeyListener, MouseListener and MouseMotionListener
[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 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 can not occur directly inside the mouse or keyboard handlers; but a mouse or keyboard listener may invoke the GLDrawable instance's repaint method, thus forcing the redisplay.
Java API
[edit]Example
[edit]In what follows we shall present a simple application -- also available inside the Subversion repository under the folder examples/example-03 -- that behaves according to the following rules:
- it assumes that the view volume should be from -1 to 1 on all axes; but if the view port is not a square, it compensates;
- it draws a square in the middle of the scene;
- it draws a cross;
- the cross should follow the mouse -- be exactly under the mouse pointer;
- if we press the up or down keys, the box rotates;
- if we click with the mouse, the box changes it's color;
Please note in the following code, the issues:
- inside the constructor we force the focus of the canvas;
- we save the aspect ratio, as we need it later to draw the cross;
- pay attention to the way we transform the mouse coordinates to the object coordinates;
import java.awt.BorderLayout;
import java.awt.Frame;
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;
import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCanvas;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLEventListener;
import com.sun.opengl.util.Animator;
import com.sun.opengl.util.FPSAnimator;
public class MainFrame
extends Frame
implements
GLEventListener,
KeyListener,
MouseListener,
MouseMotionListener
{
public MainFrame()
{
super("Java OpenGL");
this.setLayout(new BorderLayout());
// Registering a window event listener to handle the closing event.
this.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
this.setSize(800, 600);
this.initializeJogl();
this.setVisible(true);
// Force the canvas to have focus.
this.canvas.requestFocus();
}
private void initializeJogl()
{
// Creating an object to manipulate OpenGL parameters.
GLCapabilities capabilities = new GLCapabilities();
// Setting some OpenGL parameters.
capabilities.setHardwareAccelerated(true);
capabilities.setDoubleBuffered(true);
// Creating an OpenGL display widget -- canvas.
this.canvas = new GLCanvas();
// Adding the canvas in the center of the frame.
this.add(this.canvas, BorderLayout.CENTER);
// Adding an OpenGL event listener to the canvas.
this.canvas.addGLEventListener(this);
// Adding the keyboard and mouse event listeners to the canvas.
this.canvas.addKeyListener(this);
this.canvas.addMouseListener(this);
this.canvas.addMouseMotionListener(this);
// Creating an animator that will redraw the scene 40 times per second.
this.animator = new FPSAnimator(40);
// Registering the canvas to the animator.
this.animator.add(this.canvas);
// Starting the animator.
this.animator.start();
}
public void init(GLAutoDrawable canvas)
{
// Obtaining the GL instance associated with the canvas.
GL gl = canvas.getGL();
// Setting the clear color -- the color which will be used to erase the canvas.
gl.glClearColor(0, 0, 0, 0);
// Selecting the projection matrix.
gl.glMatrixMode(GL.GL_PROJECTION);
// Initializing the projection matrix with the identity matrix.
gl.glLoadIdentity();
// Setting the projection to be orthographic.
// Selecting the view volume to be x from 0 to 1, y from 0 to 1, z from -1 to 1.
gl.glOrtho(-1, 1, -1, 1, -1, 1);
// Initialize animation variables.
this.initializeAnimation();
}
public void reshape(GLAutoDrawable canvas, int left, int top, int width, int height)
{
GL gl = canvas.getGL();
// Selecting the viewport -- the display area -- to be the entire widget.
gl.glViewport(0, 0, width, height);
// Determining the width to height ratio of the widget.
double ratio = (double) width / (double) height;
// Selecting the projection matrix.
gl.glMatrixMode(GL.GL_PROJECTION);
gl.glLoadIdentity();
// Selecting the view volume to be x from 0 to 1, y from 0 to 1, z from -1 to 1.
// But we are careful to keep the aspect ratio and enlarging the width or the height.
if (ratio < 1) {
gl.glOrtho(-1, 1, -1 / ratio, 1 / ratio, -1, 1);
this.aspectRatioX = 1;
this.aspectRatioY = 1 / ratio;
} else {
this.aspectRatioX = 1 * ratio;
this.aspectRatioY = 1;
gl.glOrtho(-1 * ratio, 1 * ratio, -1, 1, -1, 1);
}
// Saving viewport size.
this.viewWidth = width;
this.viewHeight = height;
}
public void displayChanged(GLAutoDrawable canvas, boolean modeChanged, boolean deviceChanged)
{
return;
}
public void display(GLAutoDrawable canvas)
{
GL gl = canvas.getGL();
gl.glClear(GL.GL_COLOR_BUFFER_BIT);
// Draw the rotated box
gl.glPushMatrix();
gl.glRotated(this.boxAngle, 0, 0, 1);
gl.glColor3d(this.boxRed, this.boxGreen, this.boxBlue);
gl.glBegin(GL.GL_QUADS);
gl.glVertex3d(-0.5, -0.5, 0);
gl.glVertex3d(+0.5, -0.5, 0);
gl.glVertex3d(+0.5, +0.5, 0);
gl.glVertex3d(-0.5, +0.5, 0);
gl.glEnd();
gl.glPopMatrix();
// Draw a cross where the mouse is.
gl.glPushMatrix();
gl.glTranslated(this.arrowX, this.arrowY, 0.0);
gl.glColor3d(1, 1, 1);
gl.glBegin(GL.GL_LINES);
gl.glVertex3d(0, -0.1, 0);
gl.glVertex3d(0, 0.1, 0);
gl.glVertex3d(-0.1, 0, 0);
gl.glVertex3d(0.1, 0, 0);
gl.glEnd();
gl.glPopMatrix();
gl.glFlush();
}
private void initializeAnimation()
{
this.arrowX = 0;
this.arrowY = 0;
this.boxAngle = 0.1;
this.boxRed = Math.random();
this.boxGreen = Math.random();
this.boxBlue = Math.random();
}
private double arrowX;
private double arrowY;
private double boxAngle;
private double boxRed;
private double boxGreen;
private double boxBlue;
private int viewWidth;
private int viewHeight;
private double aspectRatioX;
private double aspectRatioY;
public void keyPressed(KeyEvent event)
{
switch (event.getKeyCode()) {
case KeyEvent.VK_UP :
this.boxAngle += 1;
break;
case KeyEvent.VK_DOWN :
this.boxAngle -= 1;
break;
}
}
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)
{
this.boxRed = Math.random();
this.boxGreen = Math.random();
this.boxBlue = Math.random();
}
public void mouseMoved(MouseEvent event)
{
this.arrowX = -1 * (1 - (double) event.getX() / this.viewWidth * 2) * this.aspectRatioX;
this.arrowY = (1 - (double) event.getY() / this.viewHeight * 2) * this.aspectRatioY;
}
public void mouseDragged(MouseEvent event)
{
return;
}
public void mouseEntered(MouseEvent event)
{
return;
}
public void mouseExited(MouseEvent event)
{
return;
}
private GLCanvas canvas;
private Animator animator;
}
Modularization
[edit]In order to make our code more readable and maintainable we should create some separate classes:
- EventMediator that will manage all received events (OpenGL, keyboard, and mouse);
- CommandMediator that will actually handle what happens when a certain event is triggered, it will issue commands to OpenGL and handle all the animation logic;
This is quite different from what you have seen so far where the code for GLEventListener (concerned with initializing, displaying, and reshaping the scene), KeyListener, MouseListener and MouseMotionListener were placed all together inside the same class.
There are only small modifications that are required:
- the MainFrame class does not implement the GLEventListener, KeyListener, MouseListener, and MouseMotionListener anymore;
- we create two classes EventMediator and CommandMediator;
- the EventMediator class implements all those ...Listener interfaces and, where applicable, delegates actions to the CommandMediator; thus we eliminate all the events from the MainFrame class;
- the CommandMediator class is required to perform all the OpenGL commands and to handle all animation logic;
- the initializeJogl method creates a new EventMediator instance that is tied to the canvas (as the listener for all the mentioned events);
Assignment
[edit]This is the third assignment, so please commit it to the folder assignment-03.
Create an application that:
- creates a window and adds an OpenGL component to it;
- it draws a circle in the middle of the window -- it has no filling;
- it draws a rectangle which has the following constraints:
- its center (the intersection of its diagonals) is always situated on the circle;
- its two longer edges are always perpendicular on the line (radius) which connects the rectangle center (as defined above) and the circle center; (we could say that the rectangle is tangent to the circle;)
- it handles the following keyboard events:
- if we press the Up or Down keys the radius of the circle will shrink or increase;
- if we press the Left or Right keys the rectangle moves along the circle to the left or right; (or more exactly the position on the circle changes -- by modifying the angle between the X axis and the radius connecting the rectangle center and the circle center;)
- in all cases the constraints imposed on the rectangle are satisfied;
Observations:
- all translations, rotation, and other geometrical transformations must be done with the help of OpenGL functions (glTranslate, glScale, glRotate function family);
For bonus points you could implement:
- if we move the mouse -- without holding any button -- the line connecting the rectangle and circle centers should pass through the current mouse position; the circle radius should remain the same;
- if we move the mouse -- but holding any of the buttons (left or right buttons):
- the horizontal movements should be interpreted as left or right key presses -- moving the rectangle along the circle;
- the vertical movements should be interpreted as up or down key presses -- increasing or decreasing the circle radius;
- implement mouse trails as described in wikipedia:Pointer trails, but instead of a pointer image use a triangle;
Hints:
- use as a view volume -1 to 1 on all axes;
- in order to draw the circle and the box apply the following operations:
- draw the circle;
- rotate the scene;
- translate the scene with the radius;
- draw the rectangle;
- maybe scaling will be useful;