Skip to content

212. Shooting at a 3D target by touching the screen

April 23, 2015

I don’t think I’ve covered this one before.

Suppose you are standing somewhere in your 3D scene, looking in a certain direction. You can see an enemy player, and you touch the screen to shoot at him. How do we figure out if you hit him or not? I’m assuming Codea knows where the enemy player is, but the problem is that it is a 3D position, and you just touched a 2D screen.

This post will show you how to do it. If you are interested in learning about 3D, you should find it helps you understand the difference between 2D and 3D more clearly, and also perspective. You may not understand all of it, but every little helps.

Background

Skip this section if you just want the answer to the shooting question.

The picture below shows how OpenGL draws your scene.

The camera is like an eye, with a field of view (FOV) – the four grey lines leading from the camera into the distance. The camera can see only things that are inside the rectangle formed by these lines, and that rectangle gets bigger as you get further from the camera (just as in real life, you can see more of [say] a mountain as it gets further away).

I’ve added two rectangles, one close to the camera, and one further away. These show what the camera can “see” at these distances.

Now I’d like you to imagine that the closer rectangle is the iPad screen, and it’s like a window, looking onto everything further away. So if the camera sees an object in the distance (marked “z=-1200” above), it draws a point on the iPad screen which is on the straight line between the camera and the object (at x=233 in this case).

And this is exactly how OpenGL works.

How is the rectangle size decided? You decide it when you use the perspective command, ie perspective(40), where the number is in degrees. If you don’t provide a number and just write perspective(), Codea uses a default of 45 degrees.

What does this mean? Simply that the angle between the left and right sides of the rectangle is 45 degrees, and the same for the top and bottom.

This means that if you want to know what can be seen on the screen at any distance (eg if you want to add objects all over the screen 1000 pixels away, but only where they can be seen), you can use simple trigonometry to figure this out, as I showed here.

How far away is the iPad screen, then? OpenGL puts it at the distance where, if the centre of the screen is (0,0), then the sides of the screen are -1 and +1, ie the width and height is 2. (OK, I know the screen is not square, but there is an adjustment factor that OpenGL applies to the width to allow for this. Let’s just talk about the height, it’s a little simpler).

So the iPad screen is at the distance from the camera where the height of the rectangle is exactly 2,  which will depend on the FOV angle you specified (and you can again use trigonometry to figure out the distance). OpenGL draws everything on this screen in the range from -1 to +1 for both x and y, and Codea then converts it into the range 0 to WIDTH for x, and 0 to HEIGHT for y, to make life easier for us.

Most of these details don’t matter for the solution below, but I thought it worth trying to explain how it all works.

Converting a 2D touch to 3D

Suppose I touch the iPad screen at the point (233,400), which I’ve marked x=233 in the picture above, so I can shoot at the enemy player far away at z=-1200 in the distance.

To figure out if I hit the enemy, I want to use a trick. I can

  • calculate the distance d from the camera to the enemy,
  • rotate the camera until I’m pointing straight along my aiming direction (the red line above), then
  • travel exactly d pixels forwards in that direction

If my aim was perfect, I will be exactly at the position of the enemy. If I didn’t aim perfectly, I will be to the left, right, above or below the enemy. So I can simply calculate the distance between the enemy and my end point. If it’s within some limit, eg 50 pixels, then I score a hit, otherwise I miss.

Calculating angles

The touch point is to the left of the middle. We need to figure out the angle, relative to the middle, ie how much do we need to turn to look along the red line.

We can do this because we know that the screen covers a total angle of the FOV angle we specified in the perspective command. If this is the default of 45 degrees, it means the left hand side of our iPad screen is at -22.5 degrees, and the right hand side is at +22.5 degrees.

So we can calculate the angle we need with

angle = (x / WIDTH – 0.5) * FOV = (233/1024 – 0.5) * 45 = -12.2 (degrees)

This is the sideways angle, but we have a vertical angle too, ie up or down of the middle. This is similar

vertical angle = (y / HEIGHT – 0.5) * 45 = (400/768  -0.5) * 45 = +0.9 (degrees)

If we rotate the camera by these two angles (from wherever the camera is looking at the moment), we will be looking straight at the point we touched, and the object behind it that we are trying to shoot.

Putting it together

I need to rotate left/right and up/down, using the angles above. I’m going to avoid complicated rotation calculations and let Codea do the heavy lifting, by using a rotation matrix. All that means is that I create this matrix

m = matrix (1,0,0,0, 0,1,0,0, 0,0,0,1, 0,0,0,0)

and  apply rotations to it.

But this is 3D. If we want to rotate to the left, that is a rotation on the y axis, not the x axis. And rotating left on y is positive, not negative (I suspect they make it this confusing just to make it impossible for people like us to understand it).

So this is the left/right rotation code (note the minus sign)

y_rot = -(x / WIDTH - 0.5) * FOV
m = m:rotate(y_rot,0,1,0)

This is the up/down rotation code, which is a rotation on the x axis

x_rot = (y / HEIGHT - 0.5) * FOV
m = m:rotate(x_rot,1,0,0)

Now we can rotate the camera. But we have a problem. It is not always looking at (0,0,0), ie it may be rotated to point somewhere else. So we need to combine that rotation with our aiming rotations above. Fortunately, this is not difficult, we simply multiply the rotation matrix, by the vector we have put into the camera command. So if we have

camera( camPos.x, camPos.y camPos.z, camLook.x, camLook.y, camLook.z)

ie the camera is at position camPos, and is looking at position camLook, then our camera is rotated in the direction camLook – camPos,  and m * (camLook – camPos) will combine our rotations, and now the camera is pointing along our touch direction (the red line in the picture). Note we need to normalize the resulting direction so it has a length of 1.

To move along that direction, we need the distance from the camera to the enemy, ie

d = camPos:dist(enemyPos)

and we multiply it by the rotated camera direction, and add the starting camera position, camPos. So

hitPoint = camPos + (m * (camLook-camPos)):normalize() * d

We’re there now. The last step is to calculate the distance from this point to the enemy

error = hitPoint:dist(enemyPos)

and now it’s up to you to decide if that is close enough to be a hit.

This may seem to have been a long and complicated journey, but the code is pretty compact, just 8 lines.


--returns how close (in pixels) the touch was to the enemy
--FOV is number, touchPoint is vec2, all others are vec3
function ShootError(FOV, camPos, camLook, enemyPos, touchPoint)
    local x = (touchPoint.y / HEIGHT - 0.5) * FOV
    local y = -(touchPoint.x / WIDTH - 0.5) * FOV
    local m = matrix (1,0,0,0, 0,1,0,0, 0,0,0,1, 0,0,0,0)
    m = m:rotate(x,1,0,0)
    m = m:rotate(y,0,1,0)
    local d = camPos:dist(enemyPos)
    local v = camPos + (m*(camLook-camPos)):normalize() * d
    return v:dist(enemyPos) 
end

Finally, some test code for you to try

function setup()
    --choose these any way you like
    --as long as you can still see the princess
    camPos=vec3(1000,-500,34)
    enemyPos=camPos+vec3(655,-40,-500)
    camLook=camPos+vec3(1,0,-1)
    FOV=45
end

function draw()
    background(0)
    perspective(FOV) 
    camera(camPos.x,camPos.y,camPos.z,camLook.x,camLook.y,camLook.z)
    translate(enemyPos:unpack())
    sprite("Planet Cute:Character Princess Girl",0,0,200)
end

function touched(t)
    if t.state==ENDED then
        local e=ShootError(FOV,camPos,camLook,enemyPos,t)
        print("Error=",e)
    end
end

function ShootError(FOV,camPos,camLook,enemyPos,touchPoint)
    local x=(touchPoint.y/HEIGHT-0.5)*FOV
    local y=-(touchPoint.x/WIDTH-0.5)*FOV
    local m=matrix(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,0)
    m=m:rotate(y,0,1,0)
    m=m:rotate(x,1,0,0)  
    local d=camPos:dist(enemyPos) 
    local v=camPos+(m*(camLook-camPos)):normalize()*d 
    return v:dist(enemyPos)
end

 

From → 3D, Graphics, Programming

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: