Part I A Simple Game of Air Hockey 2. Defining Vertices and Shaders
9.3 Moving Around an Object by Dragging 177
Now that we’re able to test if the mallet has been touched, we’ll work on solving the next part of the puzzle: Where does the mallet go when we drag it around? We can think of things in this way: the mallet lies flat on the table, so when we move our finger around, the mallet should move with our finger and continue to lie flat on the table. We can figure out the right position by doing a ray-plane intersection test.
Let’s complete the definition for handleTouchDrag():
AirHockeyTouch/src/com/airhockey/android/AirHockeyRenderer.java
public void handleTouchDrag(float normalizedX, float normalizedY) { if (malletPressed) {
Ray ray = convertNormalized2DPointToRay(normalizedX, normalizedY);
// Define a plane representing our air hockey table.
Plane plane = new Plane(new Point(0, 0, 0), new Vector(0, 1, 0));
// Find out where the touched point intersects the plane
// representing our table. We'll move the mallet along this plane.
Point touchedPoint = Geometry.intersectionPoint(ray, plane);
blueMalletPosition =
new Point(touchedPoint.x, mallet.height / 2f, touchedPoint.z);
} }
Moving Around an Object by Dragging • 177
We only want to drag the mallet around if we had initially pressed on it with a finger, so first we check to see that malletPressed is true. If it is, then we do the same ray conversion that we were doing in handleTouchPress(). Once we have the ray representing the touched point, we find out where that ray intersects with the plane represented by our air hockey table, and then we move the mallet to that point.
Let’s add the code for Plane to Geometry:
AirHockeyTouch/src/com/airhockey/android/util/Geometry.java public static class Plane {
public final Point point;
public final Vector normal;
public Plane(Point point, Vector normal) { this.point = point;
this.normal = normal;
} }
This definition of a plane is very simple: it consists of a normal vector and a point on that plane; the normal vector of a plane is simply a vector that is perpendicular to that plane. There are other possible definitions of a plane, but this is the one that we’ll work with.
In the following image, we can see an example of a plane located at (0, 0, 0) with a normal of (0, 1, 0):
There’s also a ray at (-2, 1, 0) with a vector of (1, -1, 0). We’ll use this plane and ray to explain the intersection test. Let’s add the following code to calcu- late the intersection point:
Chapter 9. Adding Touch Feedback: Interacting with Our Air Hockey Game • 178
AirHockeyTouch/src/com/airhockey/android/util/Geometry.java
public static Point intersectionPoint(Ray ray, Plane plane) { Vector rayToPlaneVector = vectorBetween(ray.point, plane.point);
float scaleFactor = rayToPlaneVector.dotProduct(plane.normal) / ray.vector.dotProduct(plane.normal);
Point intersectionPoint = ray.point.translate(ray.vector.scale(scaleFactor));
return intersectionPoint;
}
To calculate the intersection point, we need to figure out how much we need to scale the ray’s vector until it touches the plane exactly; this is the scaling factor. We can then translate the ray’s point by this scaled vector to find the intersection point.
To calculate the scaling factor, we first create a vector between the ray’s starting point and a point on the plane. We then calculate the dot product between that vector and the plane’s normal.5
The dot product of two vectors is directly related to (though usually not equivalent to) the cosine between those two vectors. As an example, if we had two parallel vectors of (1, 0, 0) and (1, 0, 0), then the angle between them would be 0 degrees and the cosine of this angle would be 1. If we had two perpendicular vectors of (1, 0, 0) and (0, 0, 1), then the angle between them would be 90 degrees and the cosine of this angle would be 0.
To figure out the scaling amount, we can take the dot product between the ray-to-plane vector and the plane normal and divide that by the dot product between the ray vector and the plane normal. This will give us the scaling factor that we need.
A special case happens when the ray is parallel to the plane: in this case, there is no possible intersection point between the ray and the plane. The ray will be perpendicular to the plane normal, the dot product will be 0, and we’ll get a division by 0 when we try to calculate the scaling factor. We’ll end up with an intersection point that is full of floating-point NaNs, which is short- hand for “not a number.”
Don’t worry if you don’t understand this in complete detail. The important part is that it works; for the mathematically curious, there’s a good explanation on Wikipedia that you can read to learn more.6
5. http://en.wikipedia.org/wiki/Dot_product
6. http://en.wikipedia.org/wiki/Line-plane_intersection
Moving Around an Object by Dragging • 179
We’ll need to fill in the missing blanks by adding the following code to Vector:
AirHockeyTouch/src/com/airhockey/android/util/Geometry.java public float dotProduct(Vector other) {
return x * other.x + y * other.y + z * other.z;
}
public Vector scale(float f) { return new Vector(
x * f, y * f, z * f);
}
The first method, dotProduct(), calculates the dot product between two vectors.7 The second, scale(), scales each component of the vector evenly by the scale amount.
An Example of a Ray-Plane Intersection Test
As before, we’ll also walk through an example just to see how the numbers pan out. Let’s use the following example of a plane with a ray:
We have a plane at (0, 0, 0) with a normal of (0, 1, 0), and we have a ray at (-2, 1, 0) with a vector of (1, -1, 0). If we extend the vector far enough, where would this ray hit the plane? Let’s go through the math and find out.
First we need to assign rayToPlaneVector to the vector between the plane and the ray. This should get set to (0, 0, 0) - (-2, 1, 0) = (2, -1, 0).
Then the next step is to calculate scaleFactor. Once we calculate the dot prod- ucts, the equation reduces to -1/-1, which gives us a scaling factor of 1.
7. http://en.wikipedia.org/wiki/Dot_product
Chapter 9. Adding Touch Feedback: Interacting with Our Air Hockey Game • 180
To get the intersection point, we just need to translate the ray point by the scaled ray vector. The ray vector is scaled by 1, so we can just add the vector to the point to get (-2, 1, 0) + (1, -1, 0) = (-1, 0, 0). This is where the ray intersects with the plane.
We’ve now added everything we needed to get handleTouchDrag() to work. There’s only one part left: we need to go back to AirHockeyRenderer and actually use the new point when drawing the blue mallet. Let’s update onDrawFrame() and update the second call to positionObjectInScene() as follows:
AirHockeyTouch/src/com/airhockey/android/AirHockeyRenderer.java
positionObjectInScene(blueMalletPosition.x, blueMalletPosition.y, blueMalletPosition.z);
Go ahead and give this a run; you should now be able to drag the mallet around on the screen and watch it follow your fingertip!