touch3d.zip
is the completed, touch-sensitive version of this project.
The name of the project contained in this zip file is
GLES2Sample
.
Our cube is centered at
glTranslatef
in
ES1Renderer
and
mat4f_LoadTranslation
in
ES2Renderer
.
But it would be simpler to smooth out the edges and corners
and pretend that the cube was a sphere,
centered at the same point.
We will think of the screen as a rectangular plate.
Since our horizontal field of view is 45°
and the plate is (unofficially) 320 units wide,
the plate would have to be at least
Let the screen coördinates run
horizontally from –160 to 160
and vertically from –240 to 240.
If the user touches the screen at coördinates
Consider the ray from the origin
Solve[ {x^2 + y^2 + (z - d)^2 == r^2, x / x0 == y / y0, x / x0 == z / z0}, {x, y, z} ]
In Mathematica,
press shift return to execute the above call to
Solve
.
It finds two solutions,
since the ray could pierce the sphere at up to two points.
We will use the second solution,
because it is closer to the origin,
because its z value is bigger.
After all, when you touch an object,
you’re touching the side of the object that’s closer to you.
Can’t see the PDF picture? Try this link.
startAnimation
in the
applicationDidFinishLaunching:
and
applicationDidBecomeActive:
methods of class
GLES2SampleAppDelegate
.
We will move the cube by dragging on it.ESRenderer.h
immediately before the
@protocol
.
//A CGPoint3 is just like a CGPoint, but with 3 dimensions. typedef struct { CGFloat x, y, z; } CGPoint3;
EAGLView
.
//Was the previous point that was touched on the sphere, //and if so, where was it? BOOL onSpherePrevious; CGPoint3 previous;
EAGLView
.
Declare one of them in
EAGLView.h
.
- (BOOL) convert: (CGPoint) p to3d: (CGPoint3 *) point3d;
Define the three methods in
EAGLView.m
.
//Given a point on the screen, //find the corresponding point on the surface of the sphere. - (BOOL) convert: (CGPoint) p to3d: (CGPoint3 *) surface { CGFloat hfield = 45.0; CGFloat d = -5.0; //distance from origin to center of cube CGSize size = self.bounds.size; CGFloat x0 = p.x - size.width / 2.0 - self.bounds.origin.x; CGFloat y0 = size.height / 2.0 - p.y - self.bounds.origin.y; CGFloat z0 = -(size.width / 2.0) / tan((hfield / 2.0) * M_PI / 180.0); //radius of sphere circumscribed around cube, //i.e., distance from center of cube to one of its vertices CGFloat r = sqrt(3.0); const CGFloat vsquare = x0 * x0 + y0 * y0 + z0 * z0; const CGFloat radicand = 4.0 * d * d * z0 * z0 * z0 * z0 + 4.0 * (r * r - d * d) * z0 * z0 * vsquare; if (radicand < 0.0) { return NO; } const CGFloat root = sqrt(radicand); surface->x = d * x0 * z0 / vsquare + x0 * root / (2.0 * z0 * vsquare); surface->y = (d * y0 * z0) / vsquare + y0 * root / (2.0 * z0 * vsquare); surface->z = (2.0 * d * z0 * z0 + root) / (2.0 * vsquare); return YES; } - (void) touchesBegan: (NSSet *) touches withEvent: (UIEvent *) event { if (touches.count > 0) { CGPoint p = [[touches anyObject] locationInView: self]; onSpherePrevious = [self convert: p to3d: &previous]; } } - (void) touchesMoved: (NSSet *) touches withEvent: (UIEvent *) event { if (onSpherePrevious && touches.count > 0) { CGPoint p = [[touches anyObject] locationInView: self]; CGPoint3 current; if (![self convert: p to3d: ¤t]) { onSpherePrevious = NO; return; } //Cross product of previous and current will be axis of rotation. CGFloat d = -5.0; CGPoint3 axis; axis.x = previous.y * (current.z - d) - (previous.z - d) * current.y; axis.y = (previous.z - d) * current.x - previous.x * (current.z - d); axis.z = previous.x * current.y - previous.y * current.x; //Dot product of previous and current will give angle of rotation in radians. CGFloat theta = acos( (previous.x * current.x + previous.y * current.y + (previous.z - d) * (current.z - d)) / (sqrt(previous.x * previous.x + previous.y * previous.y + (previous.z - d) * (previous.z - d)) * sqrt(current.x * current.x + current.y * current.y + (current.z - d) * (current.z - d)))); [renderer rotate: -theta axis: axis]; previous = current; } }
ESRenderer1
and
ESRenderer2
.
//axis and angle (in radians) of rotation CGPoint3 axis; GLfloat theta;
ESRenderer
protocol to initialize the above instance variables.
Declare it in
ESRenderer.h
.
The first argument is a
float
,
not a
GLfloat
,
because
ESRenderer.h
contains no files that are specific to ES 1.1 or 2.0.
- (void) rotate: (float) t axis: (CGPoint3) p;Define the method in
ES1Renderer.m
and
ES2Renderer.m
.
- (void) rotate: (float) t axis: (CGPoint3) p { theta = t; axis = p; [self render]; }
render
method of class
ES1Renderer
between the calls to
glFrustumf
and
glClearColor
to the following.
glMatrixMode(GL_MODELVIEW); GLfloat orientation[4 * 4]; glGetFloatv(GL_MODELVIEW_MATRIX, orientation); glPushMatrix(); glLoadIdentity(); glRotatef(-theta * 180.0 / M_PI, axis.x, axis.y, axis.z); glMultMatrixf(orientation); glGetFloatv(GL_MODELVIEW_MATRIX, orientation); glLoadIdentity(); glTranslatef(0.0f, 0.0f, -5.0f); glMultMatrixf(orientation);
ES2Renderer
.
Declare it in
ES2Renderer.h
.
GLfloat previousOrientation[4 * 4]; //of cubeInitialize it in the
init
method of class
ES2Renderer
.
mat4f_LoadIdentity(previousOrientation);
render
method of class
ES1Renderer
between the calls to
mat4f_LoadPerspective
and the
mat4f_MultiplyMat4f
that multiplies
proj
times
modelview
to the following.
//Rotate the cube. float temp[4 * 4]; normalize((float *)&axis); mat4f_LoadRotation(theta, (float *)&axis, temp); float currentOrientation[4 * 4]; mat4f_MultiplyMat4f(temp, previousOrientation, currentOrientation); //Move the cube 5 units away from the origin. float translate[3] = {0.0, 0.0, -5.0}; mat4f_LoadTranslation(translate, temp); mat4f_MultiplyMat4f(temp, currentOrientation, modelview); memmove(previousOrientation, currentOrientation, sizeof currentOrientation);
matrix.h
.
void normalize(float *v); void mat4f_LoadRotation(float radians, float *axis, float* mout);Define them in
matrix.c
The rotation matrix came from
Wikipedia.
//Keep the vector pointing in the same direction, but make its length 1. void normalize(float *v) { const float length = sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); if (length > 0) { v[0] /= length; //means v[0] = v[0] / length; v[1] /= length; v[2] /= length; } } //Let mout be the matrix of a rotation around the axis, //where axis is a three-dimensional unit vector. void mat4f_LoadRotation(float radians, float *axis, float* mout) { const float s = sin(radians); const float c = cos(radians); mout[ 0] = axis[0] * axis[0] + (1.0 - axis[0] * axis[0]) * c; mout[ 1] = axis[0] * axis[1] * (1.0 - c) - axis[2] * s; mout[ 2] = axis[0] * axis[2] * (1 - c) + axis[1] * s; mout[ 3] = 0.0; mout[ 4] = axis[0] * axis[1] * (1 - c) + axis[2] * s; mout[ 5] = axis[1] * axis[1] + (1.0 - axis[1] * axis[1]) * c; mout[ 6] = axis[1] * axis[2] * (1 - c) - axis[0] * s; mout[ 7] = 0.0; mout[ 8] = axis[0] * axis[2] * (1 - c) - axis[1] * s; mout[ 9] = axis[1] * axis[2] * (1 - c) + axis[0] * s; mout[10] = axis[2] * axis[2] + (1 - axis[2] * axis[2]) * c; mout[11] = 0.0; mout[12] = 0.0; mout[13] = 0.0; mout[14] = 0.0; mout[15] = 1.0; }
ES1Renderer
and
ES2Renderer
.
For example, move the coördinates and colors of the cube into
a separate class
CODE>Model.
EAGLView
,
ES1Renderer
,
and
ES2Renderer
.
Mention it in only once place.
Ditto for the 45° field of view.