Viewing 2: Rotating, Translating, Scaling...
(updated in 05/09/2001)

Previous | Index | Next

Ok, now you already know how to define the viewing rules of your HOpenGL world. In this lesson, I'm going to show you how to explore viewing in a large way. Let's start learning a very important concept in our HOpenGL applications.


Using the "camera"

The first thing you should keep in mind in this lesson is that you see things in HOpenGL as if you were using a camera. So let's see how to set the main properties of this camera. To do this, just call function lookAt:

	lookAt :: (GLdouble,GLdouble,GLdouble) -> (GLdouble,GLdouble,GLdouble)
		  -> (GLdouble,GLdouble,GLdouble) -> IO ()

The first tuple refers to where the camera (or eye position) will be placed, the second refers to where the camera is aimed (or "looking to") and the third says which way is up. For example, the following code:

	lookAt (0.0, 1.0, (-3.0)) (0.0,0.0,0.0) (1.0,0.0,0.0)

will place the camera at (0.0,1.0,-3.0), aim the camera lens towards (0.0,0.0,0.0), and specify the up-vector as (1.0,0.0,0.0). The up-vector defines a unique orientation for the camera. Its default value is (0.0,1.0,0.0). The default position of the camera, (as you may have already guessed) is (0.0,0.0,0.0), and its default aiming is (0.0,0.0,-100.0). This says that the camera is looking to inside the screen. The value -100.0 could be replaced by any negative value (can you understand why?).

Try to modify a previous program in which something is drawn, changing the position of the camera via lookAt. Check out what happens.


The Modelview Matrix

The Modelview Matrix is responsible for doing translation, rotation and scaling transformations to our final scene that will be displayed in the screen. "But why do we need to do rotate or translate?", you may ask. Ok, here we go...

Suppose you'd like to draw a simple square which is situated inside the screen (its Z coordinate is negative). The following code would fit our purposes:

	beginEnd Quads $ mapM_ vertex [
		Vertex3 0.20 0.20  (-1.0),
		Vertex3 0.80 0.20  (-1.0),
		Vertex3 0.80 0.80  (-1.0),
		Vertex3 0.20 0.80 ((-1.0) :: GLfloat)]

Suppose now you'd like to draw a square rotated 45 degrees. You can calculate the position of the new vertices and use something like the above code to draw it. Although the way to draw the square, in these two examples, are not wrong, I would recommend you not to do this if you are working with animations and a 3D world. Why? Because it's much better to let HOpenGL do the translation/rotation calculations for you. The only thing you need to do is to tell to where you'd like to translate/rotate, and them draw the primitive as if you where located in position (0,0,0) and with no rotation.

Making an analogy, suppose you are trying to draw a rotated square in a sheet of paper. Suppose also that you know very well how to draw a square with no rotation, but find difficult to draw it rotated. So you can be smart and, instead of drawing a rotated square, you can rotate your sheet of paper and draw a non-rotated square. When you look back your sheet of paper in its original position, the cube will be rotated, wouldn't it?

Another very important argument for doing this is that things get much more simpler if you work this way when using animations. Notice that when we talk about translating and rotating, we are not referring to the position and angle of the camera, but to the position and angle of "the pen you are using to draw". If you are feeling lost, just check out the following examples.

Let's draw a simple square which is situated inside the screen (its Z coordinate is negative) using this different process. You need first to translante into the screen. In order to translate into the screen (and rotate or scale, also), you need first to set you current matrix as the Modelview Matrix. You should also initialize it, otherwise you may work with a Modelview Matrix that was previously changed and weird results can appear. These two things are done by:

	matrixmode Modelview
	loadIdentity

To translate to the position you want, just call function translate:

	translate :: Vector3 a -> IO ()

Here, a can be a GLfloat or a GLdouble value. One thing you should keep in mind is that the following code, for example:

	translate (Vector3 15.0 3.0 (2.0 :: GLfloat))

will translate the current position for 15.0 units in the X-axis, 3.0 in the Y-axis and 2.0 in the Z-axis, which is not the same as setting the current position to (15.0,3.0,2.0).

Ok, know let's apply this to our example. We only want to move to inside the screen, so we would like to translate the current position for a negative unit, say, -7.0. This is done by:

	translate (Vector3 0.0 0.0 ((-7.0) :: GLfloat))

Now that we are placed in the desired position, we can draw our square with the Z-coordinate of the vertices as being 0.0. After moving, always think as if you were in the "middle point" of the primitive you are going to draw in a 3D world (in this case, think as if you were in the middle of the square). So here you have the code for drawing the square:

	beginEnd Quads $ mapM_ vertex [
		Vertex3 (-0.5) (-0.5) 0.0,
		Vertex3   0.5  (-0.5) 0.0,
		Vertex3   0.5    0.5  0.0,
		Vertex3 (-0.5)   0.5 (0.0 :: GLfloat)]

Because we are working with a 3D world, let's use the same reshape function I introduced to you in the Hints and Tips section of the last lesson. Notice that, because this function already sets the current matrix as being the Modelview Matrix when it ends, the only thing we need to do before calling translate is loadIdentity.

Here you have the code. Notice that function translate is defined in module GL_CoordTrans, which is automatically imported if your program already imports module GL (this is the same for functions rotate and scale we will see in this lesson).

	import GLUT
	import GL

	myInit :: IO () 
	myInit = do
		clearColor (Color4 0.0 0.0 0.0 0.0)
	
	reshape :: ReshapeAction
	reshape screenSize@(WindowSize w h) = do
		viewport (WindowPosition 0 0) screenSize
		matrixMode Projection
		loadIdentity
		let	near   = 1
			far    = 80
			fov    = 90
			ang    = (fov*pi)/(360 :: Double)
			top    = near / ( cos(ang) / sin(ang) )
			aspect = fromIntegral(w)/fromIntegral(h)
			right = top*aspect
	   	frustum (-right) right (-top) top near far
	   	matrixMode Modelview
	   		
	drawSquare :: IO ()
	drawSquare = do
		color (Color3 1.0 0.0 0.0 :: Color3 GLfloat)
		beginEnd Quads $ mapM_ vertex [
			Vertex3 (-0.5) (-0.5) 0.0,
			Vertex3   0.5  (-0.5) 0.0,
			Vertex3   0.5    0.5  0.0,
			Vertex3 (-0.5)   0.5 (0.0 :: GLfloat)]
		
			
	display :: DisplayAction
	display = do 
		clear [ColorBufferBit]
		loadIdentity
		translate (Vector3 0.0 0.0 ((-7.0) :: GLfloat))
		drawSquare
		flush
	
	main :: IO ()
	main = do
		GLUT.init Nothing
		createWindow "Translating Example" (return ()) [ Single, GLUT.Rgb ]
				(Just (WindowPosition 100 100))
				(Just (WindowSize     250 250))
		myInit
		reshapeFunc (Just reshape)
		displayFunc (display)
		mainLoop

This program draws the folling square:


If you change the Z-value, in function translate, from -7.0 to -1.0, you'll get the following result:


That's because now we are tanslating less units into the screen. In other words, the square is closer to the "camera", as you may have guessed.


The Idle Callback

The new type of callback procedure you'll learn in this lesson is the idleFunc. It defines a procedure that will be executed every time your HOpenGL program is idle, as its name suggests.

Suppose a function called idle will be your idleFunc callback procedure. Declare it in the main:

	idleFunc (Just idle)

Its signature must be:

	idle :: IdleAction

Where an IdleAction is defined as follows:

	type IdleAction = IO ()

All of this stuff is defined in module GLUT_CBGlobal, which is automatically imported if your program already imports module GLUT.

The idleFunc callback procedure will be important for us to create our first animated example!


A More Elaborated Example

Now we are going to develop an example using both rotation and translation. Let me first introduce rotation to you. The function rotate is defined as follows:

	rotate :: a -> Vector3 a -> IO ()

Once again, a can be a GLfloat or a GLdouble value. The first argument of this function is the angle of rotation (in degress), and the second one refers to how this rotation will happen in the X, Y and Z-axis. For example, the code:

	rotate 90.0 (Vector3 1.0 0.5 (0.0 :: GLfloat)

will produce a rotation of 90 degrees in the X-axis, 45 degrees in the Y-axis and 0 degrees in the Z-axis. As you may have guessed, just multiply the angle by the vector value related to the axis to know how much degrees will be rotated in this axis.

As I said, this will be an animated example. Let's make three triangles rotate in the three different axes (each), and let's make a fourth triangle rotate in all axes. We will store the current value of the angle in an IORef, which will be incremented by our idleFunc callback procedure. This procedure will also effectively draw the triangles. I'll directly show the code to you, with some importants comments to make it easier for you to understand it.

	import GLUT
	import GL
	-- to use the IORef stuff:
	import IOExts
	
	myInit :: IO () 
	myInit = do
		clearColor (Color4 0.0 0.0 0.0 0.0)
	
	
	idle :: IORef Float -> IdleAction
	idle angle = do
	
		clear [ColorBufferBit]
		-- read the value of the current angle
		a <- readIORef angle
		
		-- initialize the Modelview Matrix
		loadIdentity
		-- translate to the upper-left part of the screen,
		-- and also into the screen
		translate (Vector3 (-2.0) 2.0 ( (-5.0) :: GLfloat))
		-- rotate "a" degrees in the X-axis
		rotate a (Vector3 1.0 0.0 (0.0 :: GLfloat))
		-- change the current color to red
		color (Color3 1.0 0.0 (0.0 :: GLfloat))
		-- draw the first triangle
		drawTriangle
		
		-- initialize the Modelview Matrix
		loadIdentity
		-- translate to the upper-right part of the screen,
		-- and also into the screen
		translate (Vector3 2.0 2.0 ( (-5.0) :: GLfloat))
		-- rotate "a" degrees in the Y-axis
		rotate a (Vector3 0.0 1.0 (0.0 :: GLfloat))
		-- change the current color to green
		color (Color3 0.0 1.0 (0.0 :: GLfloat))
		-- draw the second triangle
		drawTriangle
		
		-- initialize the Modelview Matrix
		loadIdentity
		-- translate to the lower-right part of the screen,
		-- and also into the screen
		translate (Vector3 2.0 (-2.0) ( (-5.0) :: GLfloat))
		-- rotate "a" degrees in the Z-axis
		rotate a (Vector3 0.0 0.0 (1.0 :: GLfloat))
		-- change the current color to blue
		color (Color3 0.0 0.0 (1.0 :: GLfloat))
		-- draw the third triangle
		drawTriangle
		
		-- initialize the Modelview Matrix
		loadIdentity
		-- translate to the lower-left part of the screen,
		-- and also into the screen
		translate (Vector3 (-2.0) (-2.0) ( (-5.0) :: GLfloat))
		-- rotate "a" degrees in all axes
		rotate a (Vector3 1.0 1.0 (1.0 :: GLfloat))
		-- change the current color to yellow
		color (Color3 1.0 1.0 (0.0 :: GLfloat))
		-- draw the fourth triangle
		drawTriangle
		
		-- update the rotation angle by 2 degrees
		writeIORef angle (a + 2.0)
		
		-- swap the front and back buffers
		swapBuffers
		
		-- force flushing
		flush
	
	
	reshape :: ReshapeAction
	reshape screenSize@(WindowSize w h) = do
		viewport (WindowPosition 0 0) screenSize
		matrixMode Projection
		loadIdentity
		let	near   = 1
			far    = 80
			fov    = 90
			ang    = (fov*pi)/(360 :: Double)
			top    = near / ( cos(ang) / sin(ang) )
			aspect = fromIntegral(w)/fromIntegral(h)
			right = top*aspect
	   	frustum (-right) right (-top) top near far
	   	matrixMode Modelview
	   	
	-- Effectively draw the triangle
	drawTriangle :: IO ()
	drawTriangle = do
		beginEnd Triangles $ mapM_ vertex [
			Vertex3 (-0.5) (-0.5) 0.0,
			Vertex3   0.5  (-0.5) 0.0,
			Vertex3   0.0    0.5 (0.0 :: GLfloat)]
		
	main :: IO ()
	main = do
		GLUT.init Nothing
		createWindow "Elaborated Example" (return ()) [ GLUT.Double, GLUT.Rgb ]
				(Just (WindowPosition 100 100))
				(Just (WindowSize     500 600))
		-- create the IORef that will be used as the current angle
		angle <- newIORef 0.0
		myInit
		reshapeFunc (Just reshape)
		-- declare our idleFunc procedure, with an IORef Float as an parameter
		idleFunc (Just (idle angle))
		mainLoop

Notice that now we are using double buffering mode, to make the animation look better. Can you think about an easy way to produce this animation without using rotation and translation?


Scaling

The last thing left to talk about is scaling. There is nothing special about it. It is used, as it name suggests, to set the scale of our HOpenGL world. Check out the function scale:

	scale :: a -> a -> a -> IO ()

Once again, a can be a GLfloat or a GLdouble value. This function stretches, shrinks, or reflects an object along the axes. Each x, y, and z coordinate of every point in the object is multiplied by the corresponding argument x, y, or z. Notice that:

  • To shrink, use values between 0 and 1. For example, the following code will shrink an object along the Z-axis:
    		scale 1.0 1.0 (0.5 :: GLfloat)
    	
  • To stretch, use values larger than 1. For example, the following code will strech an object along the Y-axis:
    		scale 1.0 2.0 (1.0 :: GLfloat)
    	
  • To reflect, use negatives values. For example, the following code will reflect an object along the X-axis:
    		scale (-1.0) 2.0 (1.0 :: GLfloat)
    	
  • The default scale value for all axes is 1.0.
  • Using 0 (zero) as a parameter of this function will cause strange results.

Try to add the following line to our first example of this lesson (the red square), just before drawing the square:

	scale 2.0 1.0 (1.0 :: GLfloat)

You'll get the following result (a streched red square in the X-axis):





Hints and Tips
  • The speed of your animation, when using idleFunc callback procedures, really depends on how fast is the computer you are running your HOpenGL application. If you would like to use a time-based callback procedure, than you are looking for timerFunc. For example, suppose timer will be the timerFunc callback procedure. Its declaration in the main should be as follows:
    		timerFunc milliseconds (timer)
    	
    The function timer must have the following signature:
    		timer :: TimerAction
    	
    And TimerAction is defined as follows:
    		type TimerAction = IO ()
    	
    The value of milliseconds tells the amound of time (in milliseconds) remaining to the execution of function timer. Please notice that you'll need to declare timer again (inside itself) if you wish it to be executed until your program ends. Check out this code:
    		timer :: TimerAction
    		timer = do
    			a lot of stuff
    			timerFunc 1000 (timer)
    	
    This will make timer to be executed every second (1000 milliseconds). Without the last line of the code above, the function would be executed only once (because it was declared only once, in the main. All of this stuff is defined in module GLUT_CBGlobal, which is automatically imported if your program already imports module GLUT.



Downloads:
  • Source code of the first example (translating) you've learned in this lesson.
  • Source code of the second example (translating + rotating & animation) you've learned in this lesson.
  • Source code of the third example (translating + scaling) you've learned in this lesson.


HOpenGL Tutorial - Andre W B Furtado
Viewing 2: Rotating, Translating, Scaling...
www.cin.ufpe.br/~haskell/hopengl/viewing2.html
Last updated in 05/09/2001
Informatics Center (CIn) - UFPE
Recife - PE - Brazil