OpenGL ES Transformations with Gestures
Learn all about OpenGL ES Transformations by making a 3D, gesture-based model viewer. By Ricardo Rendon Cepeda.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
OpenGL ES Transformations with Gestures
35 mins
- Getting Started
- Gesture Recognizers
- An Overview
- Adding Gesture Recognizers
- Gesture Recognizer Data
- Handling Your Transformations
- The Scale Transformation
- The Translation Transformation
- A Quick Math Lesson: Quaternions
- The Rotation Transformation: Overview
- Z-Axis Rotation With the Rotation Gesture
- X- and Y-Axis Rotation With the Two-Finger Pan Gesture
- Locking Your Gestures/Transformations
- Where to Go From Here?
Handling Your Transformations
With your gesture recognizers all set, you’ll now create a new class to handle your transformations. Click File\New\File… and choose the iOS\Cocoa Touch\Objective-C class template. Enter Transformations for the class and NSObject for the subclass. Make sure both checkboxes are unchecked, click Next and then click Create.
Open Transformations.h and replace the existing file contents with the following:
#import <GLKit/GLKit.h>
@interface Transformations : NSObject
- (id)initWithDepth:(float)z Scale:(float)s Translation:(GLKVector2)t Rotation:(GLKVector3)r;
- (void)start;
- (void)scale:(float)s;
- (void)translate:(GLKVector2)t withMultiplier:(float)m;
- (void)rotate:(GLKVector3)r withMultiplier:(float)m;
- (GLKMatrix4)getModelViewMatrix;
@end
These are the main methods you’ll implement to control your model’s transformations. You’ll examine each in detail within their own sections of the tutorial, but for now they will mostly remain dummy implementations.
Open Transformations.m and replace the existing file contents with the following:
#import "Transformations.h"
@interface Transformations ()
{
// 1
// Depth
float _depth;
}
@end
@implementation Transformations
- (id)initWithDepth:(float)z Scale:(float)s Translation:(GLKVector2)t Rotation:(GLKVector3)r
{
if(self = [super init])
{
// 2
// Depth
_depth = z;
}
return self;
}
- (void)start
{
}
- (void)scale:(float)s
{
}
- (void)translate:(GLKVector2)t withMultiplier:(float)m
{
}
- (void)rotate:(GLKVector3)r withMultiplier:(float)m
{
}
- (GLKMatrix4)getModelViewMatrix
{
// 3
GLKMatrix4 modelViewMatrix = GLKMatrix4Identity;
modelViewMatrix = GLKMatrix4Translate(modelViewMatrix, 0.0f, 0.0f, -_depth);
return modelViewMatrix;
}
@end
There are a few interesting things happening with _depth
, so let’s take a closer look:
-
_depth
is a variable specific toTransformations
which will determine the depth of your object in the scene. - You assign the variable
z
to_depth
in your initializer, and nowhere else. - You position your model-view matrix at the (x,y) center of your view with the values (0.0, 0.0) and with a z-value of
-_depth
. You do this because, in OpenGL ES, the negative z-axis runs into the screen.
That’s all you need to render your model with an appropriate model-view matrix. :]
Open MainViewController.m
and import your new class by adding the following statement to the top of your file:
#import "Transformations.h"
Now add a property to access your new class, right below the @interface
line:
@property (strong, nonatomic) Transformations* transformations;
Next, initialize transformations
by adding the following lines to viewDidLoad
:
// Initialize transformations
self.transformations = [[Transformations alloc] initWithDepth:5.0f Scale:1.0f Translation:GLKVector2Make(0.0f, 0.0f) Rotation:GLKVector3Make(0.0f, 0.0f, 0.0f)];
The only value doing anything here is the depth of 5.0f
. You’re using this value because the projection matrix of your scene has near and far clipping planes of 0.1f
and 10.0f
, respectively (see the function calculateMatrices
), thus placing your model right in the middle of the scene.
Locate the function calculateMatrices
and replace the following lines:
GLKMatrix4 modelViewMatrix = GLKMatrix4Identity;
modelViewMatrix = GLKMatrix4Translate(modelViewMatrix, 0.0f, 0.0f, -2.5f);
With these:
GLKMatrix4 modelViewMatrix = [self.transformations getModelViewMatrix];
Build and run! Your starship is still there, but it appears to have shrunk!
You’re handling your new model-view matrix by transformations
, which set a depth of 5.0 units. Your previous model-view matrix had a depth of 2.5 units, meaning that your starship is now twice as far away. You could easily revert the depth, or you could play around with your starship’s scale…
The Scale Transformation
The first transformation you’ll implement is also the easiest: scale. Open Transformations.m and add the following variables inside the @interface
extension at the top of your file:
// Scale
float _scaleStart;
float _scaleEnd;
All of your transformations will have start and end values. The end value will be the one actually transforming your model-view matrix, while the start value will track the gesture’s event data.
Next, add the following line to initWithDepth:Scale:Translation:Rotation:
, inside the if
statement:
// Scale
_scaleEnd = s;
And add the following line to getModelViewMatrix
, after you translate the model-view matrix—transformation order does matter, as you’ll learn later on:
modelViewMatrix = GLKMatrix4Scale(modelViewMatrix, _scaleEnd, _scaleEnd, _scaleEnd);
With that line, you scale your model-view matrix uniformly in (x,y,z) space.
To test your new code, open MainViewController.m and locate the function viewDidLoad
. Change the Scale:
initialization of self.transformations
from 1.0f
to 2.0f
, like so:
self.transformations = [[Transformations alloc] initWithDepth:5.0f Scale:2.0f Translation:GLKVector2Make(0.0f, 0.0f) Rotation:GLKVector3Make(0.0f, 0.0f, 0.0f)];
Build and run! Your starship will be twice as big as your last run and look a lot more proportional to the size of your scene.
Back in Transformations.m, add the following line to scale:
:
_scaleEnd = s * _scaleStart;
As mentioned before, the starting scale value of a pinch gesture is 1.0, increasing with a zoom-in event and decreasing with a zoom-out event. You haven’t assigned a value to _scaleStart
yet, so here’s a quick question: should it be 1.0? Or maybe s
?
The answer is neither. If you assign either of those values to _scaleStart
, then every time the user performs a new scale gesture, the model-view matrix will scale back to either 1.0 or s
before scaling up or down. This will cause the model to suddenly contract or expand, creating a jittery experience. You want your model to conserve its latest scale so that the transformation is continuously smooth.
To make it so, add the following line to start
:
_scaleStart = _scaleEnd;
You haven’t called start
from anywhere yet, so let’s see where it belongs. Open MainViewController.m and add the following function at the bottom of your file, before the @end
statement:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// Begin transformations
[self.transformations start];
}
touchesBegan:withEvent:
is the first method to respond whenever your iOS device detects a touch on the screen, before the gesture recognizers kick in. Therefore, it’s the perfect place to call start
and conserve your scale values.
Next, locate the function pinch:
and replace the NSLog()
statement with:
[self.transformations scale:scale];
Build and run! Pinch the touchscreen to scale your model up and down. :D
That’s pretty exciting!
The Translation Transformation
Just like a scale transformation, a translation needs two variables to track start and end values. Open Transformations.m and add the following variables inside your @interface
extension:
// Translation
GLKVector2 _translationStart;
GLKVector2 _translationEnd;
Similarly, you only need to initialize _translationEnd
in initWithDepth:Scale:Translation:Rotation:
. Do that now:
// Translation
_translationEnd = t;
Scroll down to the function getModelViewMatrix
and change the following line:
modelViewMatrix = GLKMatrix4Translate(modelViewMatrix, 0.0f, 0.0f, -_depth);
To this:
modelViewMatrix = GLKMatrix4Translate(modelViewMatrix, _translationEnd.x, _translationEnd.y, -_depth);
Next, add the following lines to translate:withMultiplier:
:
// 1
t = GLKVector2MultiplyScalar(t, m);
// 2
float dx = _translationEnd.x + (t.x-_translationStart.x);
float dy = _translationEnd.y - (t.y-_translationStart.y);
// 3
_translationEnd = GLKVector2Make(dx, dy);
_translationStart = GLKVector2Make(t.x, t.y);
Let’s see what’s happening here:
-
m
is a multiplier that helps convert screen coordinates into OpenGL ES coordinates. It is defined when you call the function from MainViewController.m. -
dx
anddy
represent the rate of change of the current translation in x and y, relative to the latest position of_translationEnd
. In screen coordinates, the y-axis is positive in the downwards direction and negative in the upwards direction. In OpenGL ES, the opposite is true. Therefore, you subtract the rate of change in y from_translationEnd.y
. - Finally, you update
_translationEnd
and_translationStart
to reflect the new end and start positions, respectively.
As mentioned before, the starting translation value of a new pan gesture is (0.0, 0.0). That means all new translations will be relative to this origin point, regardless of where the model actually is in the scene. It also means the value assigned to _translationStart
for every new pan gesture will always be the origin.
Add the following line to start
:
_translationStart = GLKVector2Make(0.0f, 0.0f);
Everything is in place, so open MainViewController.m and locate your pan:
function. Replace the NSLog()
statement inside your first if
conditional for a single touch with the following:
[self.transformations translate:GLKVector2Make(x, y) withMultiplier:5.0f];
Build and run! Good job—you can now move your starship around with the touch of a finger! (But not two.)