I want to achieve momentum after touches ended. I am using GLKit on iOS with Objective-C and the OpenGL ES 2.0 API. I am using Quaternions to rotate an object centered about the origin of my world using Arcball Rotation. when I rotate with touches, I want to continue the rotation with diminishing speed for some time until there is no rotation left to do.
My thoughts: 1.) Get current angle from Quaternion 2.) Convert Quaternion to 4x4 Matrix 3.) Use GLKit to rotate Matrix with decreasing angles from the current quaternion 4.) At some point invalidate the timer
I've tried to rotate the matrix after touches ended with X,Y, and Z axis independently, as well as all together, but the resulting rotation is never about the axis I would expect from my Quaternion. The arcball rotation works just fine when I move the object using my finger - it is only when the finger is released that the rotation continues about some seemingly arbitrary axis. How do I get the axis to be consistent with the last state of quaternion rotation when I lifted my finger? Also, I noticed from the docs that the GetAngle function for the Quaternion should return negative values of radians when the object is rotated counter-clockwise. I always get positive values from this function even when rotating counter-clockwise.
Thank you very much for any insight you could provide!
// Called when touches are ended
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
// The momentum var is initialized to a value
self.momentumVar = 0.05;
// Now since we stopped touching, decay the rotation to simulate momentum
self.momentumTimer = [NSTimer scheduledTimerWithTimeInterval:0.025
target:self
selector:@selector(decayGLKQuaternion)
userInfo:nil
repeats:YES];
}
// Rotate about the current axis after touches ended but for a greater angle ( radians )
- (void)decayGLKQuaternion {
//return;
// What is the current angle for the quaternion?
float currentQuatAngle = GLKQuaternionAngle(self.quat);
// Decay the value each time
self.momentumVar = currentQuatAngle * 0.0055;
NSLog(@"QUAT Angle %f %f",GLKQuaternionAngle(self.quat),GLKMathRadiansToDegrees(GLKQuaternionAngle(self.quat)));
GLKMatrix4 newMat = GLKMatrix4MakeWithQuaternion(self.quat);
float rotate = self.momentumVar * 0.75;
//newMat = GLKMatrix4RotateX(newMat, rotate);
//newMat = GLKMatrix4RotateY(newMat, rotate);
//newMat = GLKMatrix4RotateZ(newMat, rotate);
newMat = GLKMatrix4Rotate(newMat, rotate, 1, 0, 0);
newMat = GLKMatrix4Rotate(newMat, rotate, 0, 1, 0);
newMat = GLKMatrix4Rotate(newMat, rotate, 0, 1, 1);
self.quat = GLKQuaternionMakeWithMatrix4(newMat);
}
Here's a simpler approach. Don't try to decay a quaternion, or a rotational velocity. Instead, store the movement vector of the touch (in 2D view coordinates) and decay that movement vector.
I assume that self.quat
is the current rotation quaternion, and that in touchesMoved:withEvent:
, you call some method that updates self.quat
based on the touch movement vector. Let's say the method is called rotateQuaternionWithVector:
. So your touchesMoved:withEvent:
probably looks something like this:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self.view];
// get touch delta
CGPoint delta = CGPointMake(location.x - iniLocation.x, -(location.y - iniLocation.y));
iniLocation = location;
// rotate
[self rotateQuaternionWithVector:delta];
}
(That method is from The Strange Agency's arcball code, which I used to test this answer.)
I assume you also have a method that gets called 60 times per second to draw each frame of your simulation. I assume that method is named update
.
Give yourself some new instance variables:
@implementation ViewController {
... existing instance variables ...
CGPoint pendingMomentumVector; // movement vector as of most recent touchesMoved:withEvent:
NSTimeInterval priorMomentumVectorTime; // time of most recent touchesMoved:withEvent:
CGPoint momentumVector; // momentum vector to apply at each simulation step
}
When the touch starts, initialize the variables:
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
priorMomentumVectorTime = touch.timestamp;
momentumVector = CGPointZero;
pendingMomentumVector = CGPointZero;
... existing touchesBegan:withEvent: code ...
}
When the touch moves, update the variables:
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self.view];
CGPoint priorLocation = [touch previousLocationInView:self.view];
pendingMomentumVector = CGPointMake(location.x - priorLocation.x, -(location.y - priorLocation.y));
priorMomentumVectorTime = touch.timestamp;
... existing touchesMoved:withEvent: code ...
}
When the touch ends, set momentumVector
. This requires some care, because the touchesEnded:withEvent:
message sometimes includes additional movement, and sometimes comes almost instantly after a separate movement event. So you need to check the timestamp:
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self.view];
if (touch.timestamp - priorMomentumVectorTime >= 1/60.0) {
CGPoint priorLocation = [touch previousLocationInView:self.view];
momentumVector = CGPointMake(location.x - priorLocation.x, -(location.y - priorLocation.y));
} else {
momentumVector = pendingMomentumVector;
}
}
Finally, modify your update
method (which I assume gets called 60 times per second) to update self.quat
based on momentumVector
and to decay momentumVector
:
- (void)update {
[self applyMomentum];
... existing update code ...
}
- (void)applyMomentum {
[self rotateQuaternionWithVector:momentumVector];
momentumVector.x *= 0.9f;
momentumVector.y *= 0.9f;
}