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.
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 Try to modify a previous program in which something is drawn, changing the position of the camera via The 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:
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 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 To translate to the position you want, just call function Here, 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 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, Now that we are placed in the desired position, we can draw our square with the Z-coordinate of the vertices as being Because we are working with a 3D world, let's use the same Here you have the code. Notice that function This program draws the folling square:
If you change the Z-value, in function 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 new type of callback procedure you'll learn in this lesson is the Suppose a function called Its signature must be:
Where an All of this stuff is defined in module GLUT_CBGlobal, which is automatically imported if your program already imports module GLUT.
The Now we are going to develop an example using both rotation and translation. Let me first introduce rotation to you. The function Once again, 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 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?
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 Once again, Try to add the following line to our first example of this lesson (the red square), just before drawing the square:
You'll get the following result (a streched red square in the X-axis):
lookAt
. Check out what happens.
The Modelview Matrix
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...
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)]
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
translate
:
translate :: Vector3 a -> IO ()
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))
translate (Vector3 0.0 0.0 ((-7.0) :: 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)]
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
.
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
translate
, from -7.0 to -1.0, you'll get the following result:
The Idle Callback
idleFunc
. It defines a procedure that will be executed every time your HOpenGL program is idle, as its name suggests.
idle
will be your idleFunc
callback procedure. Declare it in the main
:
idleFunc (Just idle)
idle :: IdleAction
IdleAction
is defined as follows:
type IdleAction = IO ()
idleFunc
callback procedure will be important for us to create our first animated example!
A More Elaborated Example
rotate
is defined as follows:
rotate :: a -> Vector3 a -> IO ()
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)
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
Scaling
scale
:
scale :: a -> a -> a -> IO ()
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:
scale 1.0 1.0 (0.5 :: GLfloat)
scale 1.0 2.0 (1.0 :: GLfloat)
scale (-1.0) 2.0 (1.0 :: GLfloat)
scale 2.0 1.0 (1.0 :: GLfloat)
Hints and Tips
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:
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