By Adam Nagy

This is a continuation of the previous post Revit model viewer for iOS - part 2

**The iOS App part - view manipulation**

We'll now make the application more interactive by responding to gestures to modify the view orientation. I've seen samples where the rotation, translation, etc were stored separately and then used to modify the view or called OpenGL functions directly to modify the view orientation. Since the GLK effect object (in our case GLKBaseEffect) already keeps track of the transformation matrix, I tought I would modify that directly.

I was a bit confused about the transformation matrix at first as I haven't used such things for a while, and since everyone understands things faster in different ways, I created my own drawing to help me with this.

Note: the area behind the UIToolbar that is placed at the bottom of the screen is actually part of the GLKView no matter if the toolbar is transparent or not. That's why the house does not seem to be vertically in the middle if you ignore the toolbar area.

The modelviewMatrix describes the model axes (X, Y, Z) and translation to the model origin (T) using the OpenGL coordinate system (x, y, z):

x | y | z | t | |

X | m00 | m01 | m02 | m03 |

Y | m10 | m11 | m12 | m13 |

Z | m20 | m21 | m22 | m23 |

T | m30 | m31 | m32 | m33 |

The above house model's origin is at the bottom left corner of the front of the house, the X is pointing from the left to the right, the Y is pointing towards the back of the house and Z is pointing upwards. If the house's width is 12, depth is 8 and height is 10, then the center point will be at (6, 4, 5). When you are using the GLKMatrix4MakeLookAt() function to set the model view matrix in a way that you'll be looking at the center of the house from e.g. a distance of 20 ...

// All below coordinates are defined in

// Model Coordinate System (X, Y, Z)

GLKVector3 center = GLKVector3Make(6, 4, 5);

GLKVector3 centerToEye = GLKVector3Make(0, -20, 0);

GLKVector3 eye = GLKVector3Add(center, centerToEye);

baseEffect.transform.modelviewMatrix =

GLKMatrix4MakeLookAt(

eye.x, eye.y, eye.z,

center.x, center.y, center.z,

0, 0, 1 /* upVector = OpenGL y axis = Model Z axis */);

... then you'll get a matrix like this:

x | y | z | t | |

X | 1 | 0 | 0 | 0 |

Y | 0 | 0 | -1 | 0 |

Z | 0 | 1 | 0 | 0 |

T | -6 | -5 | -16 | 1 |

You get -16 for the z part of T because the translation vector only points at the model origin and does not go all the way back to the model center (-16 = -20 + (depth / 2)).

When you want to rotate around the center of the model, then you have to move/translate the model view matrix from the center position to the eye position, do the rotation, and then move the matrix back. In our case the distance between the eye and model center position is held by the 'distance' global variable:

- (void)rotateMatrix:(float *)rotX

rotY:(float *)rotY

rotZ:(float *)rotZ

{

GLKMatrix4 mx = _baseEffect.transform.modelviewMatrix;

// In this function we are providing the coordinates in

// OpenGL coordinate system (x, y, z)

// So the below toOrigin translation is acting along the z axis,

// which is perpendicular to the screen

// Here 'toOrigin' means translation to the origin of the

// OpenGL coordinate system (screen/view center)

GLKMatrix4 toOrigin = GLKMatrix4MakeTranslation(0, 0, distance);

mx = GLKMatrix4Multiply(toOrigin, mx);

if (rotX != nil)

mx = GLKMatrix4Multiply(GLKMatrix4MakeXRotation(*rotX), mx);

if (rotY != nil)

mx = GLKMatrix4Multiply(GLKMatrix4MakeYRotation(*rotY), mx);

if (rotZ != nil)

mx = GLKMatrix4Multiply(GLKMatrix4MakeZRotation(*rotZ), mx);

GLKMatrix4 fromOrigin = GLKMatrix4MakeTranslation(0, 0, -distance);

mx = GLKMatrix4Multiply(fromOrigin, mx);

_baseEffect.transform.modelviewMatrix = mx;

}

Here are the other transformation functions:

- (void)panMatrix:(float)x y:(float)y

{

GLKMatrix4 mx = _baseEffect.transform.modelviewMatrix;

GLKMatrix4 pan =

GLKMatrix4MakeTranslation(

x * distance * .005, y * distance * .005, 0);

mx = GLKMatrix4Multiply(pan, mx);

_baseEffect.transform.modelviewMatrix = mx;

}

- (void)scaleMatrix:(float)scale

{

GLKMatrix4 mx = _baseEffect.transform.modelviewMatrix;

if (scale < 1)

scale = -1 / scale;

// We just move along the view Z to show scale like effect

GLKMatrix4 scaler =

GLKMatrix4MakeTranslation(0, 0, scale * distance * .03);

mx = GLKMatrix4Multiply(scaler, mx);

_baseEffect.transform.modelviewMatrix = mx;

}

Note: the .005 and .03 values used above are just experimental and not based on any calculation.

We need to catch the gestures in order to respond to them. You can just go into the storyboard editor and drag&drop gestures onto the GLKView, then set up the sent actions for the control that can be handled by our functions. Inside these functions we can use the previously created view matrix manipulation functions:

- (IBAction)onPinch:(UIPinchGestureRecognizer *)sender

{

static double _prevScale;

if ([sender state] == UIGestureRecognizerStateBegan)

{

_prevScale = 1;

}

else if ([sender state] == UIGestureRecognizerStateChanged)

{

[self scaleMatrix:(sender.scale / _prevScale)];

_prevScale = sender.scale;

NSLog(@"onPinch with scale %f", sender.scale);

[self updateGLView];

}

}

- (IBAction)onPan:(UIPanGestureRecognizer *)sender

{

static CGPoint _prevPoint;

static NSUInteger _numOfTouches = 0;

if ([sender state] == UIGestureRecognizerStateBegan)

{

_prevPoint = [sender locationInView:self.view];

// Number of touches can change, so better stick to the one that

// is started

_numOfTouches = sender.numberOfTouches;

}

else if ([sender state] == UIGestureRecognizerStateChanged &&

_numOfTouches == sender.numberOfTouches)

{

CGPoint pt = [sender locationInView:self.view];

// Two touches is panning

if (_numOfTouches == 2)

{

[self panMatrix:(pt.x - _prevPoint.x) y:-(pt.y - _prevPoint.y)];

}

// One touch is rotation

else

{

float rotX = (pt.y - _prevPoint.y) / 180;

float rotY = (pt.x - _prevPoint.x) / 180;

[self rotateMatrix:&rotX rotY:&rotY rotZ:nil];

}

_prevPoint = pt;

[self updateGLView];

}

NSLog(@"onPan with state %d and number %d", [sender state],

[sender numberOfTouches]);

}

- (IBAction)onRotate:(UIRotationGestureRecognizer *)sender

{

static double _prevRotation;

if ([sender state] == UIGestureRecognizerStateBegan)

{

_prevRotation = 0;

}

else if ([sender state] == UIGestureRecognizerStateChanged)

{

float rotZ = -([sender rotation] - _prevRotation);

[self rotateMatrix:nil rotY:nil rotZ:&rotZ];

_prevRotation = [sender rotation];

NSLog(@"onRotate with angle %f", rotZ);

[self updateGLView];

}

}

I hope you found these posts useful. Here is the finished project: Download RevitModelViewer - iOS App - 2012-06-20

Note: this is just a sample and not a fine tuned project ready to be used in production ;)