Skip to content

147. 3D – Understanding the camera settings

January 30, 2014

In this post, I want to discuss something about the camera command – the “up vector –  that took me a long time to understand. It’s one of those things that makes me think “Why couldn’t someone just explain that simply, right at the beginning?”.

I’ll also explain how to get the camera to follow behind an object, even if it is rotating all over the place.

You need to know something about 3D to understand this.

The camera command has 9 items, made of 3 x,y,z vectors, as follows

  • the position of the camera (x,y,z)
  • the point at which the camera is looking (x,y,z)
  • the “up vector” of the camera (x,y,z)

So if I write camera(0,10,5,0,0,-1000)

  • the camera is at (0,10,5)
  • the camera is looking at (0,0,-1000)
  • the “up vector” is (0,1,0) because that is the default if you don’t provide it

It is quite logical to provide the camera position, and what it is looking at. But what is the up vector, and why does it default to (0,1,0)? What do the numbers mean?

The up vector

You probably haven’t thought about it, but providing the camera position and what it is looking at, is not enough. Sometimes we may want to tilt the camera left or right, or turn it upside down. So we need to provide this information to OpenGL, and that is the job of the “up vector”.

Imagine that the camera has an arrow sticking out of the top (or if you prefer, that you – the viewer – have an arrow sticking out of the top of your head!). The up vector tells Codea which way to point the arrow (and I guess that’s where its name came from).

So the default up vector of (0,1,0) points upwards.

Why does (0,1,0) point upwards? Because this is a directional vector, not a point. What it means is that if you draw a line from (0,0,0) through (0,1,0) and then keep drawing, that is the direction of the vector.

So because (0,1,0) is directly above (0,0,0), the vector (0,1,0) points directly up in the air, in line with the y axis. And if the arrow sticking out of the top of the camera is pointing upwards, then the camera is the normal way up, which is what we would expect for the default.

If, instead, you wanted to lay the camera on its left side, the arrow sticking out of its top would point towards -x, so the up vector would be (-1,0,0).

If you wanted it upside down, the arrow would point downwards, so the up vector would be (0,-1,0).

What will you see if you change the up value?

What will you see if you draw something with a vector of (-1,0,0), laying the camera on its left side?

You can imagine this easily by looking at an object and tilting your head to the left. The object will tilt the other way, to the right.

An example may show this best. Explanation underneath.

function setup()
    fontSize(128)
    fill(255)
    up={}
    up[1]=vec3(0, 1,0)   print("1 = right way up")
    up[2]=vec3(-1,0,0)  print("2 = rotated left")
    up[3]=vec3( 1,0,0)   print("3 = rotated right")
    up[4]=vec3(0,-1,0)  print("4 = upside down")
    up[5]=vec3(-.5,.5,0):normalize() print("5 = 45 degrees left")
    parameter.integer("CameraPosition",1,5,1)
end

function draw()
    background(0)
    perspective()
    local c=up[CameraPosition] --use selected angle
    camera(0,0,10,0,0,0,c.x,c.y,c.z)
    pushMatrix()
    translate(0,0,-600)
    sprite("Planet Cute:Character Princess Girl",0,0)
    popMatrix()
end

This code shows an image with five different camera positions. You’ll see the image always turns the opposite way from the camera.

Note that for the last angle, tilting 45 degrees left, I set the direction vector as (-0.5, 0.5, 0), ie pointing halfway left. But direction vectors need to be “normalised” (which means that if you add the squares of their x, y and z values, you get 1). I didn’t need to normalise the other vectors because the sum of their squares clearly add to 1, but this one needs it done.

Side note(skip if you don’t care)

If you’re wondering why direction vectors need to be normalised, let me give you an example. The vectors (1,1,0), (2,2,0) and (1000,1000,0) are all in the same direction (45 degrees to the right, halfway between x and y), so if you draw a line from (0,0,0), it would pass through all three points.

But if we used these vectors in our calculations, they would all give different answers. That is because they are actually a combination of two things multiplied together

  • size
  • direction

The size (or length) of a vector = sqrt(xx + yy + z*z), ie the square root of the sum of the squares of the x, y and z values.

The direction of a vector = vector / size (which will always have a size of 1)

So if we have a directional line, it doesn’t matter which point we choose from that line, to be our vector, because we will divide by the size, leaving only direction, which will be exactly the same for any point on the line.

And that’s why direction vectors need to be normalised. Tricky, huh?

Tracking an object with the camera

Suppose you want the camera to follow an aeroplane on the screen, and if the plane rolls to one side, then you want the camera to roll as well, ie to follow it as though the camera was attached to the plane.

This means that even though the plane is turning in all directions, it will stay the right way up in the centre of your screen, and the rest of the scene will be turning instead (because the camera is”attached to” the plane).

This requires several steps. Just follow carefully.

1. Translate and rotate your plane before drawing it, as normal

But don’t draw the plane yet, until we set the camera position!

2. Make a copy of modelMatrix()

local m = modelMatrix()

This is a matrix that gives OpenGL the final rotated position of the plane, and also its position in space.

Multiplying anything by this matrix will rotate it in the same direction as the plane.

3. Calculate the position of the camera behind the plane

Suppose you want the camera to be 300 pixels behind the plane. So if the plane was not rotated, the camera would be at the plane’s position, plus the vector (0,0,300).

Note it is +300 pixels (not -300) because the z axis is reversed, in 3D, so it becomes more negative as you go forward, and more positive as you go back (which is what we want here).

To calculate the new camera position after the plane is translated and rotated, we simply need to translate and rotate our vector (0,0,300) in exactly the same way as the plane.

How do we do this? OpenGL keeps a 4×4 matrix called modelMatrix, which stores the final values for all rotations and translations. We multiply this matrix by our vector (0,0,300).

Now Codea doesn’t have a function to do this, so we have to do it the long way. See the notes underneath this code.

offset=vec3(0,0,300) --[1]
m=modelMatrix()  --[2]
--camera location  [3]
cam=vec3(m[13]+offset.x*m[1]+offset.y*m[5]+offset.z*m[9],
         m[14]+offset.x*m[2]+offset.y*m[6]+offset.z*m[10],
         m[15]+offset.x*m[3]+offset.y*m[7]+offset.z*m[11])
--assume pos is a vector containing the plane position [4]
camera(cam.x,cam.y,cam.z,pos.x,pos.y,pos.z)

[1] Our camera position, relative to the plane

[2] First, we make a copy of the modelMatrix

[3] We calculate camera location by multiplying offset by modelMatrix, and adding the current plane location. modelMatrix is a 4×4 table, but it is not 2D, but a 1D, running from 1 to 16.

Looking at our copy of modelMatrix called m, the first row across is m[1], m[2], m[3], m[4], and the next row starts with m[5], and so on, all the way down to m[16] in the bottom right hand corner.

The first 3 rows and columns have the rotation information, and this is what we multiply by our offset vector. Then we add the current x,y,z position of the plane, which is stored in m[13], m[14] and m[15].

[4] We point the camera at the plane

So now we have positioned the camera behind the plane, looking at it. No matter where the plane goes, or how it turns, we will be behind it. But our camera is upright, so if the plane rolls to the left, the camera will not roll with it.

So we have one final step.

4. Rotate the camera up vector as well

--this code goes after we calculate camera position
up=vec3(m[5],m[6],m[7]) --rotate up vector
--assume pos is a vector with the plane position
camera(cam.x,cam.y,cam.z,pos.x,pos.y,pos.z,up.x,up.y,up.z)
--now draw the plane

We rotate the up vector, multiplying modelMatrix by the up vector (0,1,0), to get a new up vector, which you put in the camera command. In plain English, we are rotating the up vector in exactly the same way as the aeroplane.

The calculation is a lot simpler for the up vector, because the x and z values are zero, and we can leave out most of the multiplications.

Below is a complete code example, with a spaceship sprite, which is rotated at random angles. There are two parameters to show you what happens when the camera follows the ship, and a button that randomly changes the rotation of the spaceship.

(View the video below full screen to see the notes written on it).

With “Follow” turned off, the camera doesn’t move, and you are just looking at the rotated image. If you turn “Follow” on, then the camera moves behind the spaceship, and no matter how many times you press the button to change the rotations, you will always be looking straight at it.

However, although the spaceship is always facing you, it may be rotated left or right, because we aren’t tilting the camera yet. We’ve simply changed its position so it follows the ship. The up value is still (0,1,0), or straight up.

When you turn on “FollowAndTilt”, then the camera rotates too, and the ship should always be the right way up in the middle of your screen. Now the camera is “attached to” the ship.

function setup()
    SetupTest()
    parameter.boolean("Follow",false)
    parameter.boolean("FollowAndTilt",false)
    parameter.action("New_Test",SetupTest)
end

function SetupTest()
    a=90
    rotation=vec3(math.random(-a,a),math.random(-a,a),math.random(-a,a))
    offset=vec3(0,0,400)
    ship=readImage("Space Art:Red Ship")
    pos=vec3(0,0,-600)
end

function draw()
    background(0)
    perspective()
    pushMatrix()
    translate(pos.x,pos.y,pos.z)
    rotate(rotation.x,1,0,0)
    rotate(rotation.y,0,1,0)
    rotate(rotation.z,0,0,1)
    if Follow then
        --calculate location of camera using offset
        m=modelMatrix()
        cam=vec3(m[13]+offset.x*m[1]+offset.y*m[5]+offset.z*m[9],
        m[14]+offset.x*m[2]+offset.y*m[6]+offset.z*m[10],
        m[15]+offset.x*m[3]+offset.y*m[7]+offset.z*m[11])
        if FollowAndTilt then
            up=vec3(m[5],m[6],m[7])
        else
            up=vec3(0,1,0)
        end
        camera(cam.x,cam.y,cam.z,pos.x,pos.y,pos.z,up.x,up.y,up.z)
    else
        camera(0,0,10,0,0,0)
    end
    sprite(ship,0,0)
    popMatrix()
end

I hope some of this makes sense to you. 3D is not easy!

Advertisements

From → 3D, Graphics

One Comment

Trackbacks & Pingbacks

  1. Index of posts | coolcodea

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: