Skip to content

198. Looking at objects in 3D

February 12, 2015

If you have an object at one point, how do you rotate it to face another point?

OpenGL has a special “look at” command, but we don’t have that in Codea.

However, we can write one using cross products. The problem is understanding it, even though it’s only 5 lines long.

This post explains how it works (and it’s a neat use of cross products, too).

First, you won’t understand this post unless you’ve read the previous posts on vectors and cross products.

This is the function

function LookAtMatrix(source,target,up)
    local Z=(source-target):normalize()
    up=up or vec3(0,1,0)
    local X=(up:cross(Z)):normalize()
    local Y=(Z:cross(X)):normalize()
    return matrix(X.x,X.y,X.z,0,Y.x,Y.y,Y.z,0,Z.x,Z.y,Z.z,0,
                  source.x,source.y,source.z,1)
end

Let’s pick it apart.

You give it

  • a source vec3 – where your object is – and
  • a target vec3 – what you want to look at
  • (optionally), an up vec3 (but ignore it for now)

The function gives you back a matrix, and if you use it for drawing your object, it will rotate it to face the target, as in the example below.

local m=LookAtMatrix(objectPos, targetPos)
modelMatrix(m) --tell Codea to use m for drawing 
object:draw() --draw the object, rotated to face the target

Let’s look at what is happening in the function.  Three variables X, Y and Z are calculated, and put into a matrix that is returned. It’s the calculation of those variables that we need to understand.

I always find it easier to understand if I can visualise it, so imagine you are standing facing forward with your arms outstretched to the sides.

  • the y axis runs vertically up your body
  • the x axis runs sideways through your arms from left to right
  • you are standing on a line, the z axis, that runs forward into -z
  • the target is a red star, up and to the right.

We start with the z axis, by pointing it directly at the target object. It is a direction, so it needs to be normalised (as discussed in a couple of recent posts).

local Z=(source-target):normalize()

Next, we turn the x axis. so it is at a right angle to z, and we are facing the target.

You would expect this to be the answer.

But how about this? The x axis is still at right angles to z, so it is a valid alternative.

In fact, the x axis could be at any angle around the z axis, even leaving us upside down. How do we decide between them?

The answer is that our scene has an “up” direction defined by the camera settings, which defaults to (0,1,0), or vertical. Our x axis should be at right angles to this direction. You can see this if you think of the up direction as being a line that runs up from your feet to your head. It is at right angles to the line through your arms.

So x needs to be at right angles not just to the new z, but also to the up direction. And guess what – the cross product does just that, giving you the direction that is at right angles to two other direction vectors.

And that gives us these lines, which should explain why we have an up parameter.

up=up or vec3(0,1,0)
local X=(up:cross(Z)):normalize()

There is just one axis left, y, because our body, having been turned, needs to be tilted back so we can look up to the target.

This axis needs to be at right angles to x and z, so the cross product does the job again.

local Y=(Z:cross(X)):normalize()

Note the order – you would not get the right answer with X:cross(Z). Always go in the sequence X, Y, Z, then X again.

Now all we need to do is put X, Y and Z in the first three rows of the matrix, and the source position in the bottom row, and pass the matrix back. I’ve explained this matrix here.

There is just one puzzle I spent a while figuring out. If we use the up vector as a substitute for the y axis, to calculate the x axis, why do we need to then calculate y from x and z immediately afterwards? Isn’t the y axis the up vector?

The answer is that when we use the up vector and z to calculate x, we’re not finished, because the up vector (looking vertically straight up) isn’t at right angles to z, which is pointing upward at an angle in our example. We need to tilt it backwards, keeping it at right angles to x while we do that. And that’s what we are doing when we calculate y.

 Trying out the code

The code at the bottom shows three figures on a line in the distance. The camera runs left and right in front of them, and as it gets to the sides, you can see the figures are just 2D pictures. Except that the middle picture turns to face the camera, using our LookAt function.

You’ll also notice that the middle picture is back to front. This is because when we draw an image in 3D, the default direction is facing forwards into -z. So when we rotate the picture to face the camera, ie backwards, it reverses the image too. If you want to prevent this, you can flip the x axis of the image so it reverses again, by making it negative.

--change this (which draws the image with width of 20 pixels)
sprite(img,0,0,20)
--to this
sprite(img,0,0,-20,33)
--(you need to put the y value in too, otherwise Codea 
--will assume it is the same as x, and flip upside down!)

Here is the code for the video above.

function setup()
    img=readImage("Planet Cute:Character Horn Girl")
    camPos=vec3(-200,5,125)
    --animate camera
    tween(10,camPos,{x =200},{easing = tween.easing.linear, 
                              loop = tween.loop.pingpong } )
end

function draw()
    background(0)
    perspective()
    camera(camPos.x,camPos.y,camPos.z,0,0,0)
    --draw line behind figures
    pushMatrix()
    strokeWidth(1)
    stroke(255)
    translate(0,-10,-5)
    line(-50,0,50,0)
    popMatrix()
    --draw figures that don't rotate
    sprite(img,-25,0,20)
    sprite(img,25,0,20)
    --rotate and draw central figure
    local m=LookAtMatrix(vec3(0,0,0),camPos)
    modelMatrix(m)
    sprite(img,0,0,20)
end
    
function LookAtMatrix(source,target,up)
    local Z=(source-target):normalize()
    up=up or vec3(0,1,0)
    local X=(up:cross(Z)):normalize()
    local Y=(Z:cross(X)):normalize()
    return matrix(X.x,X.y,X.z,0,Y.x,Y.y,Y.z,0,Z.x,Z.y,Z.z,0,
                  source.x,source.y,source.z,1)
end

 

 

 

From → 3D, Graphics, Programming

4 Comments
  1. Anonymous permalink

    super usefull for oldscool 3d dungeons. thanks)

Trackbacks & Pingbacks

  1. 204. Castle 3D – Refining the exterior | coolcodea
  2. 219. World of Warships #2 – shooting | coolcodea
  3. Index of posts | coolcodea

Leave a comment