Touch a 3D object

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 (0, 0, d), where d = –5 as a result of our calls to 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 160 / tan 22½° units away from the viewer to fit on the screen.

z0 = –160 / tan 22½° ≅ 386.2741706073

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 (x0, y0), we will treat it as if he or she touched the point (x0, y0, z0) in three-dimensional space.

Consider the ray from the origin (0, 0, 0) through (x0, y0, z0). Where would it intersect the surface of the sphere of radius r centered at (0, 0, d)? I asked Mathematica to find the solutions.

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.

Create the project

  1. Start with the previous project. Comment out the calls to startAnimation in the applicationDidFinishLaunching: and applicationDidBecomeActive: methods of class GLES2SampleAppDelegate. We will move the cube by dragging on it.
  2. Add the following structure to ESRenderer.h immediately before the @protocol.
    //A CGPoint3 is just like a CGPoint, but with 3 dimensions.
    
    typedef struct {
    	CGFloat x, y, z;
    } CGPoint3;
    
  3. Add two new instance variables to class EAGLView.
    	//Was the previous point that was touched on the sphere,
    	//and if so, where was it?
    	BOOL onSpherePrevious;
    	CGPoint3 previous;
    
    
  4. Add three new methods to class 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: &current]) {
    			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;
    	}
    }
    
  5. Add the following instance variables to classes ESRenderer1 and ESRenderer2.
    	//axis and angle (in radians) of rotation
    	CGPoint3 axis;
    	GLfloat theta;
    
  6. Add the following method to the 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];
    }
    
  7. Change all the code in the 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);
    
  8. Add the following instance variable to class ES2Renderer. Declare it in ES2Renderer.h.
    	GLfloat previousOrientation[4 * 4]; //of cube
    
    Initialize it in the init method of class ES2Renderer.
    		mat4f_LoadIdentity(previousOrientation);
    
  9. Change all the code in the 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);
    
  10. Declare two new utility functions 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;
    }
    

Things to try

  1. Remove the duplication in classes ES1Renderer and ES2Renderer. For example, move the coördinates and colors of the cube into a separate class CODE>Model.
  2. The distance from the origin to the center of the cube (5 units) is mentioned in classes EAGLView, ES1Renderer, and ES2Renderer. Mention it in only once place. Ditto for the 45° field of view.